summaryrefslogtreecommitdiff
path: root/pkg/sif/descriptor_input.go
blob: c55cf51f9de3bb56126f543e37587acd7935b78b (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
// Copyright (c) 2021, Sylabs Inc. All rights reserved.
// This software is licensed under a 3-clause BSD license. Please consult the
// LICENSE file distributed with the sources of this project regarding your
// rights to use or distribute this software.

package sif

import (
	"crypto"
	"errors"
	"fmt"
	"io"
	"os"
	"time"
)

// descriptorOpts accumulates data object options.
type descriptorOpts struct {
	groupID   uint32
	linkID    uint32
	alignment int
	name      string
	extra     interface{}
	t         time.Time
}

// DescriptorInputOpt are used to specify data object options.
type DescriptorInputOpt func(DataType, *descriptorOpts) error

// OptNoGroup specifies the data object is not contained within a data object group.
func OptNoGroup() DescriptorInputOpt {
	return func(_ DataType, opts *descriptorOpts) error {
		opts.groupID = 0
		return nil
	}
}

// OptGroupID specifies groupID as data object group ID.
func OptGroupID(groupID uint32) DescriptorInputOpt {
	return func(_ DataType, opts *descriptorOpts) error {
		if groupID == 0 {
			return ErrInvalidGroupID
		}
		opts.groupID = groupID
		return nil
	}
}

// OptLinkedID specifies that the data object is linked to the data object with the specified ID.
func OptLinkedID(id uint32) DescriptorInputOpt {
	return func(_ DataType, opts *descriptorOpts) error {
		if id == 0 {
			return ErrInvalidObjectID
		}
		opts.linkID = id
		return nil
	}
}

// OptLinkedGroupID specifies that the data object is linked to the data object group with the
// specified groupID.
func OptLinkedGroupID(groupID uint32) DescriptorInputOpt {
	return func(_ DataType, opts *descriptorOpts) error {
		if groupID == 0 {
			return ErrInvalidGroupID
		}
		opts.linkID = groupID | descrGroupMask
		return nil
	}
}

// OptObjectAlignment specifies n as the data alignment requirement.
func OptObjectAlignment(n int) DescriptorInputOpt {
	return func(_ DataType, opts *descriptorOpts) error {
		opts.alignment = n
		return nil
	}
}

// OptObjectName specifies name as the data object name.
func OptObjectName(name string) DescriptorInputOpt {
	return func(_ DataType, opts *descriptorOpts) error {
		opts.name = name
		return nil
	}
}

// OptObjectTime specifies t as the data object creation time.
func OptObjectTime(t time.Time) DescriptorInputOpt {
	return func(_ DataType, opts *descriptorOpts) error {
		opts.t = t
		return nil
	}
}

type unexpectedDataTypeError struct {
	got  DataType
	want []DataType
}

func (e *unexpectedDataTypeError) Error() string {
	return fmt.Sprintf("unexpected data type %v, expected one of: %v", e.got, e.want)
}

func (e *unexpectedDataTypeError) Is(target error) bool {
	//nolint:errorlint // don't compare wrapped errors in Is()
	t, ok := target.(*unexpectedDataTypeError)
	if !ok {
		return false
	}

	if len(t.want) > 0 {
		// Use a map to check that the "want" errors in e and t contain the same values, ignoring
		// any ordering differences.
		acc := make(map[DataType]int, len(t.want))

		// Increment counter for each data type in e.
		for _, dt := range e.want {
			if _, ok := acc[dt]; !ok {
				acc[dt] = 0
			}
			acc[dt]++
		}

		// Decrement counter for each data type in e.
		for _, dt := range t.want {
			if _, ok := acc[dt]; !ok {
				return false
			}
			acc[dt]--
		}

		// If the "want" errors in e and t are equivalent, all counters should be zero.
		for _, n := range acc {
			if n != 0 {
				return false
			}
		}
	}

	return (e.got == t.got || t.got == 0)
}

// OptCryptoMessageMetadata sets metadata for a crypto message data object. The format type is set
// to ft, and the message type is set to mt.
//
// If this option is applied to a data object with an incompatible type, an error is returned.
func OptCryptoMessageMetadata(ft FormatType, mt MessageType) DescriptorInputOpt {
	return func(t DataType, opts *descriptorOpts) error {
		if got, want := t, DataCryptoMessage; got != want {
			return &unexpectedDataTypeError{got, []DataType{want}}
		}

		m := cryptoMessage{
			Formattype:  ft,
			Messagetype: mt,
		}

		opts.extra = m
		return nil
	}
}

var errUnknownArchitcture = errors.New("unknown architecture")

// OptPartitionMetadata sets metadata for a partition data object. The filesystem type is set to
// fs, the partition type is set to pt, and the CPU architecture is set to arch. The value of arch
// should be the architecture as represented by the Go runtime.
//
// If this option is applied to a data object with an incompatible type, an error is returned.
func OptPartitionMetadata(fs FSType, pt PartType, arch string) DescriptorInputOpt {
	return func(t DataType, opts *descriptorOpts) error {
		if got, want := t, DataPartition; got != want {
			return &unexpectedDataTypeError{got, []DataType{want}}
		}

		sifarch := getSIFArch(arch)
		if sifarch == hdrArchUnknown {
			return fmt.Errorf("%w: %v", errUnknownArchitcture, arch)
		}

		p := partition{
			Fstype:   fs,
			Parttype: pt,
			Arch:     sifarch,
		}

		opts.extra = p
		return nil
	}
}

// sifHashType converts h into a HashType.
func sifHashType(h crypto.Hash) hashType {
	switch h {
	case crypto.SHA256:
		return hashSHA256
	case crypto.SHA384:
		return hashSHA384
	case crypto.SHA512:
		return hashSHA512
	case crypto.BLAKE2s_256:
		return hashBLAKE2S
	case crypto.BLAKE2b_256:
		return hashBLAKE2B
	}
	return 0
}

// OptSignatureMetadata sets metadata for a signature data object. The hash type is set to ht, and
// the signing entity fingerprint is set to fp.
//
// If this option is applied to a data object with an incompatible type, an error is returned.
func OptSignatureMetadata(ht crypto.Hash, fp []byte) DescriptorInputOpt {
	return func(t DataType, opts *descriptorOpts) error {
		if got, want := t, DataSignature; got != want {
			return &unexpectedDataTypeError{got, []DataType{want}}
		}

		s := signature{
			Hashtype: sifHashType(ht),
		}
		copy(s.Entity[:], fp)

		opts.extra = s
		return nil
	}
}

// DescriptorInput describes a new data object.
type DescriptorInput struct {
	dt   DataType
	r    io.Reader
	opts descriptorOpts
}

// DefaultObjectGroup is the default group that data objects are placed in.
const DefaultObjectGroup = 1

// NewDescriptorInput returns a DescriptorInput representing a data object of type t, with contents
// read from r, configured according to opts.
//
// It is possible (and often necessary) to store additional metadata related to certain types of
// data objects. Consider supplying options such as OptCryptoMessageMetadata, OptPartitionMetadata,
// and OptSignatureMetadata for this purpose.
//
// By default, the data object will be placed in the default data object group (1). To override
// this behavior, use OptNoGroup or OptGroupID. To link this data object, use OptLinkedID or
// OptLinkedGroupID.
//
// By default, the data object will be aligned according to the system's memory page size. To
// override this behavior, consider using OptObjectAlignment.
//
// By default, no name is set for data object. To set a name, use OptObjectName.
//
// When creating a new image, data object creation/modification times are set to the image creation
// time. When modifying an existing image, the data object creation/modification time is set to the
// image modification time. To override this behavior, consider using OptObjectTime.
func NewDescriptorInput(t DataType, r io.Reader, opts ...DescriptorInputOpt) (DescriptorInput, error) {
	dopts := descriptorOpts{
		groupID:   DefaultObjectGroup,
		alignment: os.Getpagesize(),
	}

	for _, opt := range opts {
		if err := opt(t, &dopts); err != nil {
			return DescriptorInput{}, fmt.Errorf("%w", err)
		}
	}

	di := DescriptorInput{
		dt:   t,
		r:    r,
		opts: dopts,
	}

	return di, nil
}

// fillDescriptor fills d according to di. If di does not explicitly specify a time value, use t.
func (di DescriptorInput) fillDescriptor(t time.Time, d *rawDescriptor) error {
	d.DataType = di.dt
	d.GroupID = di.opts.groupID | descrGroupMask
	d.LinkedID = di.opts.linkID

	if !di.opts.t.IsZero() {
		t = di.opts.t
	}
	d.CreatedAt = t.Unix()
	d.ModifiedAt = t.Unix()

	d.UID = 0
	d.GID = 0

	if err := d.setName(di.opts.name); err != nil {
		return err
	}

	return d.setExtra(di.opts.extra)
}