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