1package continuity
2
3import (
4	"errors"
5	"fmt"
6	"os"
7	"reflect"
8	"sort"
9
10	pb "github.com/containerd/continuity/proto"
11	"github.com/opencontainers/go-digest"
12)
13
14// TODO(stevvooe): A record based model, somewhat sketched out at the bottom
15// of this file, will be more flexible. Another possibly is to tie the package
16// interface directly to the protobuf type. This will have efficiency
17// advantages at the cost coupling the nasty codegen types to the exported
18// interface.
19
20type Resource interface {
21	// Path provides the primary resource path relative to the bundle root. In
22	// cases where resources have more than one path, such as with hard links,
23	// this will return the primary path, which is often just the first entry.
24	Path() string
25
26	// Mode returns the
27	Mode() os.FileMode
28
29	UID() int64
30	GID() int64
31}
32
33// ByPath provides the canonical sort order for a set of resources. Use with
34// sort.Stable for deterministic sorting.
35type ByPath []Resource
36
37func (bp ByPath) Len() int           { return len(bp) }
38func (bp ByPath) Swap(i, j int)      { bp[i], bp[j] = bp[j], bp[i] }
39func (bp ByPath) Less(i, j int) bool { return bp[i].Path() < bp[j].Path() }
40
41type XAttrer interface {
42	XAttrs() map[string][]byte
43}
44
45// Hardlinkable is an interface that a resource type satisfies if it can be a
46// hardlink target.
47type Hardlinkable interface {
48	// Paths returns all paths of the resource, including the primary path
49	// returned by Resource.Path. If len(Paths()) > 1, the resource is a hard
50	// link.
51	Paths() []string
52}
53
54type RegularFile interface {
55	Resource
56	XAttrer
57	Hardlinkable
58
59	Size() int64
60	Digests() []digest.Digest
61}
62
63// Merge two or more Resources into new file. Typically, this should be
64// used to merge regular files as hardlinks. If the files are not identical,
65// other than Paths and Digests, the merge will fail and an error will be
66// returned.
67func Merge(fs ...Resource) (Resource, error) {
68	if len(fs) < 1 {
69		return nil, fmt.Errorf("please provide a resource to merge")
70	}
71
72	if len(fs) == 1 {
73		return fs[0], nil
74	}
75
76	var paths []string
77	var digests []digest.Digest
78	bypath := map[string][]Resource{}
79
80	// The attributes are all compared against the first to make sure they
81	// agree before adding to the above collections. If any of these don't
82	// correctly validate, the merge fails.
83	prototype := fs[0]
84	xattrs := make(map[string][]byte)
85
86	// initialize xattrs for use below. All files must have same xattrs.
87	if prototypeXAttrer, ok := prototype.(XAttrer); ok {
88		for attr, value := range prototypeXAttrer.XAttrs() {
89			xattrs[attr] = value
90		}
91	}
92
93	for _, f := range fs {
94		h, isHardlinkable := f.(Hardlinkable)
95		if !isHardlinkable {
96			return nil, errNotAHardLink
97		}
98
99		if f.Mode() != prototype.Mode() {
100			return nil, fmt.Errorf("modes do not match: %v != %v", f.Mode(), prototype.Mode())
101		}
102
103		if f.UID() != prototype.UID() {
104			return nil, fmt.Errorf("uid does not match: %v != %v", f.UID(), prototype.UID())
105		}
106
107		if f.GID() != prototype.GID() {
108			return nil, fmt.Errorf("gid does not match: %v != %v", f.GID(), prototype.GID())
109		}
110
111		if xattrer, ok := f.(XAttrer); ok {
112			fxattrs := xattrer.XAttrs()
113			if !reflect.DeepEqual(fxattrs, xattrs) {
114				return nil, fmt.Errorf("resource %q xattrs do not match: %v != %v", f, fxattrs, xattrs)
115			}
116		}
117
118		for _, p := range h.Paths() {
119			pfs, ok := bypath[p]
120			if !ok {
121				// ensure paths are unique by only appending on a new path.
122				paths = append(paths, p)
123			}
124
125			bypath[p] = append(pfs, f)
126		}
127
128		if regFile, isRegFile := f.(RegularFile); isRegFile {
129			prototypeRegFile, prototypeIsRegFile := prototype.(RegularFile)
130			if !prototypeIsRegFile {
131				return nil, errors.New("prototype is not a regular file")
132			}
133
134			if regFile.Size() != prototypeRegFile.Size() {
135				return nil, fmt.Errorf("size does not match: %v != %v", regFile.Size(), prototypeRegFile.Size())
136			}
137
138			digests = append(digests, regFile.Digests()...)
139		} else if device, isDevice := f.(Device); isDevice {
140			prototypeDevice, prototypeIsDevice := prototype.(Device)
141			if !prototypeIsDevice {
142				return nil, errors.New("prototype is not a device")
143			}
144
145			if device.Major() != prototypeDevice.Major() {
146				return nil, fmt.Errorf("major number does not match: %v != %v", device.Major(), prototypeDevice.Major())
147			}
148			if device.Minor() != prototypeDevice.Minor() {
149				return nil, fmt.Errorf("minor number does not match: %v != %v", device.Minor(), prototypeDevice.Minor())
150			}
151		} else if _, isNamedPipe := f.(NamedPipe); isNamedPipe {
152			_, prototypeIsNamedPipe := prototype.(NamedPipe)
153			if !prototypeIsNamedPipe {
154				return nil, errors.New("prototype is not a named pipe")
155			}
156		} else {
157			return nil, errNotAHardLink
158		}
159	}
160
161	sort.Stable(sort.StringSlice(paths))
162
163	// Choose a "canonical" file. Really, it is just the first file to sort
164	// against. We also effectively select the very first digest as the
165	// "canonical" one for this file.
166	first := bypath[paths[0]][0]
167
168	resource := resource{
169		paths:  paths,
170		mode:   first.Mode(),
171		uid:    first.UID(),
172		gid:    first.GID(),
173		xattrs: xattrs,
174	}
175
176	switch typedF := first.(type) {
177	case RegularFile:
178		var err error
179		digests, err = uniqifyDigests(digests...)
180		if err != nil {
181			return nil, err
182		}
183
184		return &regularFile{
185			resource: resource,
186			size:     typedF.Size(),
187			digests:  digests,
188		}, nil
189	case Device:
190		return &device{
191			resource: resource,
192			major:    typedF.Major(),
193			minor:    typedF.Minor(),
194		}, nil
195
196	case NamedPipe:
197		return &namedPipe{
198			resource: resource,
199		}, nil
200
201	default:
202		return nil, errNotAHardLink
203	}
204}
205
206type Directory interface {
207	Resource
208	XAttrer
209
210	// Directory is a no-op method to identify directory objects by interface.
211	Directory()
212}
213
214type SymLink interface {
215	Resource
216
217	// Target returns the target of the symlink contained in the .
218	Target() string
219}
220
221type NamedPipe interface {
222	Resource
223	Hardlinkable
224	XAttrer
225
226	// Pipe is a no-op method to allow consistent resolution of NamedPipe
227	// interface.
228	Pipe()
229}
230
231type Device interface {
232	Resource
233	Hardlinkable
234	XAttrer
235
236	Major() uint64
237	Minor() uint64
238}
239
240type resource struct {
241	paths    []string
242	mode     os.FileMode
243	uid, gid int64
244	xattrs   map[string][]byte
245}
246
247var _ Resource = &resource{}
248
249func (r *resource) Path() string {
250	if len(r.paths) < 1 {
251		return ""
252	}
253
254	return r.paths[0]
255}
256
257func (r *resource) Mode() os.FileMode {
258	return r.mode
259}
260
261func (r *resource) UID() int64 {
262	return r.uid
263}
264
265func (r *resource) GID() int64 {
266	return r.gid
267}
268
269type regularFile struct {
270	resource
271	size    int64
272	digests []digest.Digest
273}
274
275var _ RegularFile = &regularFile{}
276
277// newRegularFile returns the RegularFile, using the populated base resource
278// and one or more digests of the content.
279func newRegularFile(base resource, paths []string, size int64, dgsts ...digest.Digest) (RegularFile, error) {
280	if !base.Mode().IsRegular() {
281		return nil, fmt.Errorf("not a regular file")
282	}
283
284	base.paths = make([]string, len(paths))
285	copy(base.paths, paths)
286
287	// make our own copy of digests
288	ds := make([]digest.Digest, len(dgsts))
289	copy(ds, dgsts)
290
291	return &regularFile{
292		resource: base,
293		size:     size,
294		digests:  ds,
295	}, nil
296}
297
298func (rf *regularFile) Paths() []string {
299	paths := make([]string, len(rf.paths))
300	copy(paths, rf.paths)
301	return paths
302}
303
304func (rf *regularFile) Size() int64 {
305	return rf.size
306}
307
308func (rf *regularFile) Digests() []digest.Digest {
309	digests := make([]digest.Digest, len(rf.digests))
310	copy(digests, rf.digests)
311	return digests
312}
313
314func (rf *regularFile) XAttrs() map[string][]byte {
315	xattrs := make(map[string][]byte, len(rf.xattrs))
316
317	for attr, value := range rf.xattrs {
318		xattrs[attr] = append(xattrs[attr], value...)
319	}
320
321	return xattrs
322}
323
324type directory struct {
325	resource
326}
327
328var _ Directory = &directory{}
329
330func newDirectory(base resource) (Directory, error) {
331	if !base.Mode().IsDir() {
332		return nil, fmt.Errorf("not a directory")
333	}
334
335	return &directory{
336		resource: base,
337	}, nil
338}
339
340func (d *directory) Directory() {}
341
342func (d *directory) XAttrs() map[string][]byte {
343	xattrs := make(map[string][]byte, len(d.xattrs))
344
345	for attr, value := range d.xattrs {
346		xattrs[attr] = append(xattrs[attr], value...)
347	}
348
349	return xattrs
350}
351
352type symLink struct {
353	resource
354	target string
355}
356
357var _ SymLink = &symLink{}
358
359func newSymLink(base resource, target string) (SymLink, error) {
360	if base.Mode()&os.ModeSymlink == 0 {
361		return nil, fmt.Errorf("not a symlink")
362	}
363
364	return &symLink{
365		resource: base,
366		target:   target,
367	}, nil
368}
369
370func (l *symLink) Target() string {
371	return l.target
372}
373
374type namedPipe struct {
375	resource
376}
377
378var _ NamedPipe = &namedPipe{}
379
380func newNamedPipe(base resource, paths []string) (NamedPipe, error) {
381	if base.Mode()&os.ModeNamedPipe == 0 {
382		return nil, fmt.Errorf("not a namedpipe")
383	}
384
385	base.paths = make([]string, len(paths))
386	copy(base.paths, paths)
387
388	return &namedPipe{
389		resource: base,
390	}, nil
391}
392
393func (np *namedPipe) Pipe() {}
394
395func (np *namedPipe) Paths() []string {
396	paths := make([]string, len(np.paths))
397	copy(paths, np.paths)
398	return paths
399}
400
401func (np *namedPipe) XAttrs() map[string][]byte {
402	xattrs := make(map[string][]byte, len(np.xattrs))
403
404	for attr, value := range np.xattrs {
405		xattrs[attr] = append(xattrs[attr], value...)
406	}
407
408	return xattrs
409}
410
411type device struct {
412	resource
413	major, minor uint64
414}
415
416var _ Device = &device{}
417
418func newDevice(base resource, paths []string, major, minor uint64) (Device, error) {
419	if base.Mode()&os.ModeDevice == 0 {
420		return nil, fmt.Errorf("not a device")
421	}
422
423	base.paths = make([]string, len(paths))
424	copy(base.paths, paths)
425
426	return &device{
427		resource: base,
428		major:    major,
429		minor:    minor,
430	}, nil
431}
432
433func (d *device) Paths() []string {
434	paths := make([]string, len(d.paths))
435	copy(paths, d.paths)
436	return paths
437}
438
439func (d *device) XAttrs() map[string][]byte {
440	xattrs := make(map[string][]byte, len(d.xattrs))
441
442	for attr, value := range d.xattrs {
443		xattrs[attr] = append(xattrs[attr], value...)
444	}
445
446	return xattrs
447}
448
449func (d device) Major() uint64 {
450	return d.major
451}
452
453func (d device) Minor() uint64 {
454	return d.minor
455}
456
457// toProto converts a resource to a protobuf record. We'd like to push this
458// the individual types but we want to keep this all together during
459// prototyping.
460func toProto(resource Resource) *pb.Resource {
461	b := &pb.Resource{
462		Path: []string{resource.Path()},
463		Mode: uint32(resource.Mode()),
464		Uid:  resource.UID(),
465		Gid:  resource.GID(),
466	}
467
468	if xattrer, ok := resource.(XAttrer); ok {
469		// Sorts the XAttrs by name for consistent ordering.
470		keys := []string{}
471		xattrs := xattrer.XAttrs()
472		for k := range xattrs {
473			keys = append(keys, k)
474		}
475		sort.Strings(keys)
476
477		for _, k := range keys {
478			b.Xattr = append(b.Xattr, &pb.XAttr{Name: k, Data: xattrs[k]})
479		}
480	}
481
482	switch r := resource.(type) {
483	case RegularFile:
484		b.Path = r.Paths()
485		b.Size = uint64(r.Size())
486
487		for _, dgst := range r.Digests() {
488			b.Digest = append(b.Digest, dgst.String())
489		}
490	case SymLink:
491		b.Target = r.Target()
492	case Device:
493		b.Major, b.Minor = r.Major(), r.Minor()
494		b.Path = r.Paths()
495	case NamedPipe:
496		b.Path = r.Paths()
497	}
498
499	// enforce a few stability guarantees that may not be provided by the
500	// resource implementation.
501	sort.Strings(b.Path)
502
503	return b
504}
505
506// fromProto converts from a protobuf Resource to a Resource interface.
507func fromProto(b *pb.Resource) (Resource, error) {
508	base := &resource{
509		paths: b.Path,
510		mode:  os.FileMode(b.Mode),
511		uid:   b.Uid,
512		gid:   b.Gid,
513	}
514
515	base.xattrs = make(map[string][]byte, len(b.Xattr))
516
517	for _, attr := range b.Xattr {
518		base.xattrs[attr.Name] = attr.Data
519	}
520
521	switch {
522	case base.Mode().IsRegular():
523		dgsts := make([]digest.Digest, len(b.Digest))
524		for i, dgst := range b.Digest {
525			// TODO(stevvooe): Should we be validating at this point?
526			dgsts[i] = digest.Digest(dgst)
527		}
528
529		return newRegularFile(*base, b.Path, int64(b.Size), dgsts...)
530	case base.Mode().IsDir():
531		return newDirectory(*base)
532	case base.Mode()&os.ModeSymlink != 0:
533		return newSymLink(*base, b.Target)
534	case base.Mode()&os.ModeNamedPipe != 0:
535		return newNamedPipe(*base, b.Path)
536	case base.Mode()&os.ModeDevice != 0:
537		return newDevice(*base, b.Path, b.Major, b.Minor)
538	}
539
540	return nil, fmt.Errorf("unknown resource record (%#v): %s", b, base.Mode())
541}
542
543// NOTE(stevvooe): An alternative model that supports inline declaration.
544// Convenient for unit testing where inline declarations may be desirable but
545// creates an awkward API for the standard use case.
546
547// type ResourceKind int
548
549// const (
550// 	ResourceRegularFile = iota + 1
551// 	ResourceDirectory
552// 	ResourceSymLink
553// 	Resource
554// )
555
556// type Resource struct {
557// 	Kind         ResourceKind
558// 	Paths        []string
559// 	Mode         os.FileMode
560// 	UID          string
561// 	GID          string
562// 	Size         int64
563// 	Digests      []digest.Digest
564// 	Target       string
565// 	Major, Minor int
566// 	XAttrs       map[string][]byte
567// }
568
569// type RegularFile struct {
570// 	Paths   []string
571//  Size 	int64
572// 	Digests []digest.Digest
573// 	Perm    os.FileMode // os.ModePerm + sticky, setuid, setgid
574// }
575