| /* |
| Copyright The containerd Authors. |
| |
| Licensed under the Apache License, Version 2.0 (the "License"); |
| you may not use this file except in compliance with the License. |
| You may obtain a copy of the License at |
| |
| http://www.apache.org/licenses/LICENSE-2.0 |
| |
| Unless required by applicable law or agreed to in writing, software |
| distributed under the License is distributed on an "AS IS" BASIS, |
| WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| See the License for the specific language governing permissions and |
| limitations under the License. |
| */ |
| |
| package continuity |
| |
| import ( |
| "errors" |
| "fmt" |
| "os" |
| "reflect" |
| "sort" |
| |
| pb "github.com/containerd/continuity/proto" |
| "github.com/opencontainers/go-digest" |
| ) |
| |
| // TODO(stevvooe): A record based model, somewhat sketched out at the bottom |
| // of this file, will be more flexible. Another possibly is to tie the package |
| // interface directly to the protobuf type. This will have efficiency |
| // advantages at the cost coupling the nasty codegen types to the exported |
| // interface. |
| |
| type Resource interface { |
| // Path provides the primary resource path relative to the bundle root. In |
| // cases where resources have more than one path, such as with hard links, |
| // this will return the primary path, which is often just the first entry. |
| Path() string |
| |
| // Mode returns the |
| Mode() os.FileMode |
| |
| UID() int64 |
| GID() int64 |
| } |
| |
| // ByPath provides the canonical sort order for a set of resources. Use with |
| // sort.Stable for deterministic sorting. |
| type ByPath []Resource |
| |
| func (bp ByPath) Len() int { return len(bp) } |
| func (bp ByPath) Swap(i, j int) { bp[i], bp[j] = bp[j], bp[i] } |
| func (bp ByPath) Less(i, j int) bool { return bp[i].Path() < bp[j].Path() } |
| |
| type XAttrer interface { |
| XAttrs() map[string][]byte |
| } |
| |
| // Hardlinkable is an interface that a resource type satisfies if it can be a |
| // hardlink target. |
| type Hardlinkable interface { |
| // Paths returns all paths of the resource, including the primary path |
| // returned by Resource.Path. If len(Paths()) > 1, the resource is a hard |
| // link. |
| Paths() []string |
| } |
| |
| type RegularFile interface { |
| Resource |
| XAttrer |
| Hardlinkable |
| |
| Size() int64 |
| Digests() []digest.Digest |
| } |
| |
| // Merge two or more Resources into new file. Typically, this should be |
| // used to merge regular files as hardlinks. If the files are not identical, |
| // other than Paths and Digests, the merge will fail and an error will be |
| // returned. |
| func Merge(fs ...Resource) (Resource, error) { |
| if len(fs) < 1 { |
| return nil, fmt.Errorf("please provide a resource to merge") |
| } |
| |
| if len(fs) == 1 { |
| return fs[0], nil |
| } |
| |
| var paths []string |
| var digests []digest.Digest |
| bypath := map[string][]Resource{} |
| |
| // The attributes are all compared against the first to make sure they |
| // agree before adding to the above collections. If any of these don't |
| // correctly validate, the merge fails. |
| prototype := fs[0] |
| xattrs := make(map[string][]byte) |
| |
| // initialize xattrs for use below. All files must have same xattrs. |
| if prototypeXAttrer, ok := prototype.(XAttrer); ok { |
| for attr, value := range prototypeXAttrer.XAttrs() { |
| xattrs[attr] = value |
| } |
| } |
| |
| for _, f := range fs { |
| h, isHardlinkable := f.(Hardlinkable) |
| if !isHardlinkable { |
| return nil, errNotAHardLink |
| } |
| |
| if f.Mode() != prototype.Mode() { |
| return nil, fmt.Errorf("modes do not match: %v != %v", f.Mode(), prototype.Mode()) |
| } |
| |
| if f.UID() != prototype.UID() { |
| return nil, fmt.Errorf("uid does not match: %v != %v", f.UID(), prototype.UID()) |
| } |
| |
| if f.GID() != prototype.GID() { |
| return nil, fmt.Errorf("gid does not match: %v != %v", f.GID(), prototype.GID()) |
| } |
| |
| if xattrer, ok := f.(XAttrer); ok { |
| fxattrs := xattrer.XAttrs() |
| if !reflect.DeepEqual(fxattrs, xattrs) { |
| return nil, fmt.Errorf("resource %q xattrs do not match: %v != %v", f, fxattrs, xattrs) |
| } |
| } |
| |
| for _, p := range h.Paths() { |
| pfs, ok := bypath[p] |
| if !ok { |
| // ensure paths are unique by only appending on a new path. |
| paths = append(paths, p) |
| } |
| |
| bypath[p] = append(pfs, f) |
| } |
| |
| if regFile, isRegFile := f.(RegularFile); isRegFile { |
| prototypeRegFile, prototypeIsRegFile := prototype.(RegularFile) |
| if !prototypeIsRegFile { |
| return nil, errors.New("prototype is not a regular file") |
| } |
| |
| if regFile.Size() != prototypeRegFile.Size() { |
| return nil, fmt.Errorf("size does not match: %v != %v", regFile.Size(), prototypeRegFile.Size()) |
| } |
| |
| digests = append(digests, regFile.Digests()...) |
| } else if device, isDevice := f.(Device); isDevice { |
| prototypeDevice, prototypeIsDevice := prototype.(Device) |
| if !prototypeIsDevice { |
| return nil, errors.New("prototype is not a device") |
| } |
| |
| if device.Major() != prototypeDevice.Major() { |
| return nil, fmt.Errorf("major number does not match: %v != %v", device.Major(), prototypeDevice.Major()) |
| } |
| if device.Minor() != prototypeDevice.Minor() { |
| return nil, fmt.Errorf("minor number does not match: %v != %v", device.Minor(), prototypeDevice.Minor()) |
| } |
| } else if _, isNamedPipe := f.(NamedPipe); isNamedPipe { |
| _, prototypeIsNamedPipe := prototype.(NamedPipe) |
| if !prototypeIsNamedPipe { |
| return nil, errors.New("prototype is not a named pipe") |
| } |
| } else { |
| return nil, errNotAHardLink |
| } |
| } |
| |
| sort.Stable(sort.StringSlice(paths)) |
| |
| // Choose a "canonical" file. Really, it is just the first file to sort |
| // against. We also effectively select the very first digest as the |
| // "canonical" one for this file. |
| first := bypath[paths[0]][0] |
| |
| resource := resource{ |
| paths: paths, |
| mode: first.Mode(), |
| uid: first.UID(), |
| gid: first.GID(), |
| xattrs: xattrs, |
| } |
| |
| switch typedF := first.(type) { |
| case RegularFile: |
| var err error |
| digests, err = uniqifyDigests(digests...) |
| if err != nil { |
| return nil, err |
| } |
| |
| return ®ularFile{ |
| resource: resource, |
| size: typedF.Size(), |
| digests: digests, |
| }, nil |
| case Device: |
| return &device{ |
| resource: resource, |
| major: typedF.Major(), |
| minor: typedF.Minor(), |
| }, nil |
| |
| case NamedPipe: |
| return &namedPipe{ |
| resource: resource, |
| }, nil |
| |
| default: |
| return nil, errNotAHardLink |
| } |
| } |
| |
| type Directory interface { |
| Resource |
| XAttrer |
| |
| // Directory is a no-op method to identify directory objects by interface. |
| Directory() |
| } |
| |
| type SymLink interface { |
| Resource |
| |
| // Target returns the target of the symlink contained in the . |
| Target() string |
| } |
| |
| type NamedPipe interface { |
| Resource |
| Hardlinkable |
| XAttrer |
| |
| // Pipe is a no-op method to allow consistent resolution of NamedPipe |
| // interface. |
| Pipe() |
| } |
| |
| type Device interface { |
| Resource |
| Hardlinkable |
| XAttrer |
| |
| Major() uint64 |
| Minor() uint64 |
| } |
| |
| type resource struct { |
| paths []string |
| mode os.FileMode |
| uid, gid int64 |
| xattrs map[string][]byte |
| } |
| |
| var _ Resource = &resource{} |
| |
| func (r *resource) Path() string { |
| if len(r.paths) < 1 { |
| return "" |
| } |
| |
| return r.paths[0] |
| } |
| |
| func (r *resource) Mode() os.FileMode { |
| return r.mode |
| } |
| |
| func (r *resource) UID() int64 { |
| return r.uid |
| } |
| |
| func (r *resource) GID() int64 { |
| return r.gid |
| } |
| |
| type regularFile struct { |
| resource |
| size int64 |
| digests []digest.Digest |
| } |
| |
| var _ RegularFile = ®ularFile{} |
| |
| // newRegularFile returns the RegularFile, using the populated base resource |
| // and one or more digests of the content. |
| func newRegularFile(base resource, paths []string, size int64, dgsts ...digest.Digest) (RegularFile, error) { |
| if !base.Mode().IsRegular() { |
| return nil, fmt.Errorf("not a regular file") |
| } |
| |
| base.paths = make([]string, len(paths)) |
| copy(base.paths, paths) |
| |
| // make our own copy of digests |
| ds := make([]digest.Digest, len(dgsts)) |
| copy(ds, dgsts) |
| |
| return ®ularFile{ |
| resource: base, |
| size: size, |
| digests: ds, |
| }, nil |
| } |
| |
| func (rf *regularFile) Paths() []string { |
| paths := make([]string, len(rf.paths)) |
| copy(paths, rf.paths) |
| return paths |
| } |
| |
| func (rf *regularFile) Size() int64 { |
| return rf.size |
| } |
| |
| func (rf *regularFile) Digests() []digest.Digest { |
| digests := make([]digest.Digest, len(rf.digests)) |
| copy(digests, rf.digests) |
| return digests |
| } |
| |
| func (rf *regularFile) XAttrs() map[string][]byte { |
| xattrs := make(map[string][]byte, len(rf.xattrs)) |
| |
| for attr, value := range rf.xattrs { |
| xattrs[attr] = append(xattrs[attr], value...) |
| } |
| |
| return xattrs |
| } |
| |
| type directory struct { |
| resource |
| } |
| |
| var _ Directory = &directory{} |
| |
| func newDirectory(base resource) (Directory, error) { |
| if !base.Mode().IsDir() { |
| return nil, fmt.Errorf("not a directory") |
| } |
| |
| return &directory{ |
| resource: base, |
| }, nil |
| } |
| |
| func (d *directory) Directory() {} |
| |
| func (d *directory) XAttrs() map[string][]byte { |
| xattrs := make(map[string][]byte, len(d.xattrs)) |
| |
| for attr, value := range d.xattrs { |
| xattrs[attr] = append(xattrs[attr], value...) |
| } |
| |
| return xattrs |
| } |
| |
| type symLink struct { |
| resource |
| target string |
| } |
| |
| var _ SymLink = &symLink{} |
| |
| func newSymLink(base resource, target string) (SymLink, error) { |
| if base.Mode()&os.ModeSymlink == 0 { |
| return nil, fmt.Errorf("not a symlink") |
| } |
| |
| return &symLink{ |
| resource: base, |
| target: target, |
| }, nil |
| } |
| |
| func (l *symLink) Target() string { |
| return l.target |
| } |
| |
| type namedPipe struct { |
| resource |
| } |
| |
| var _ NamedPipe = &namedPipe{} |
| |
| func newNamedPipe(base resource, paths []string) (NamedPipe, error) { |
| if base.Mode()&os.ModeNamedPipe == 0 { |
| return nil, fmt.Errorf("not a namedpipe") |
| } |
| |
| base.paths = make([]string, len(paths)) |
| copy(base.paths, paths) |
| |
| return &namedPipe{ |
| resource: base, |
| }, nil |
| } |
| |
| func (np *namedPipe) Pipe() {} |
| |
| func (np *namedPipe) Paths() []string { |
| paths := make([]string, len(np.paths)) |
| copy(paths, np.paths) |
| return paths |
| } |
| |
| func (np *namedPipe) XAttrs() map[string][]byte { |
| xattrs := make(map[string][]byte, len(np.xattrs)) |
| |
| for attr, value := range np.xattrs { |
| xattrs[attr] = append(xattrs[attr], value...) |
| } |
| |
| return xattrs |
| } |
| |
| type device struct { |
| resource |
| major, minor uint64 |
| } |
| |
| var _ Device = &device{} |
| |
| func newDevice(base resource, paths []string, major, minor uint64) (Device, error) { |
| if base.Mode()&os.ModeDevice == 0 { |
| return nil, fmt.Errorf("not a device") |
| } |
| |
| base.paths = make([]string, len(paths)) |
| copy(base.paths, paths) |
| |
| return &device{ |
| resource: base, |
| major: major, |
| minor: minor, |
| }, nil |
| } |
| |
| func (d *device) Paths() []string { |
| paths := make([]string, len(d.paths)) |
| copy(paths, d.paths) |
| return paths |
| } |
| |
| func (d *device) XAttrs() map[string][]byte { |
| xattrs := make(map[string][]byte, len(d.xattrs)) |
| |
| for attr, value := range d.xattrs { |
| xattrs[attr] = append(xattrs[attr], value...) |
| } |
| |
| return xattrs |
| } |
| |
| func (d device) Major() uint64 { |
| return d.major |
| } |
| |
| func (d device) Minor() uint64 { |
| return d.minor |
| } |
| |
| // toProto converts a resource to a protobuf record. We'd like to push this |
| // the individual types but we want to keep this all together during |
| // prototyping. |
| func toProto(resource Resource) *pb.Resource { |
| b := &pb.Resource{ |
| Path: []string{resource.Path()}, |
| Mode: uint32(resource.Mode()), |
| Uid: resource.UID(), |
| Gid: resource.GID(), |
| } |
| |
| if xattrer, ok := resource.(XAttrer); ok { |
| // Sorts the XAttrs by name for consistent ordering. |
| keys := []string{} |
| xattrs := xattrer.XAttrs() |
| for k := range xattrs { |
| keys = append(keys, k) |
| } |
| sort.Strings(keys) |
| |
| for _, k := range keys { |
| b.Xattr = append(b.Xattr, &pb.XAttr{Name: k, Data: xattrs[k]}) |
| } |
| } |
| |
| switch r := resource.(type) { |
| case RegularFile: |
| b.Path = r.Paths() |
| b.Size = uint64(r.Size()) |
| |
| for _, dgst := range r.Digests() { |
| b.Digest = append(b.Digest, dgst.String()) |
| } |
| case SymLink: |
| b.Target = r.Target() |
| case Device: |
| b.Major, b.Minor = r.Major(), r.Minor() |
| b.Path = r.Paths() |
| case NamedPipe: |
| b.Path = r.Paths() |
| } |
| |
| // enforce a few stability guarantees that may not be provided by the |
| // resource implementation. |
| sort.Strings(b.Path) |
| |
| return b |
| } |
| |
| // fromProto converts from a protobuf Resource to a Resource interface. |
| func fromProto(b *pb.Resource) (Resource, error) { |
| base := &resource{ |
| paths: b.Path, |
| mode: os.FileMode(b.Mode), |
| uid: b.Uid, |
| gid: b.Gid, |
| } |
| |
| base.xattrs = make(map[string][]byte, len(b.Xattr)) |
| |
| for _, attr := range b.Xattr { |
| base.xattrs[attr.Name] = attr.Data |
| } |
| |
| switch { |
| case base.Mode().IsRegular(): |
| dgsts := make([]digest.Digest, len(b.Digest)) |
| for i, dgst := range b.Digest { |
| // TODO(stevvooe): Should we be validating at this point? |
| dgsts[i] = digest.Digest(dgst) |
| } |
| |
| return newRegularFile(*base, b.Path, int64(b.Size), dgsts...) |
| case base.Mode().IsDir(): |
| return newDirectory(*base) |
| case base.Mode()&os.ModeSymlink != 0: |
| return newSymLink(*base, b.Target) |
| case base.Mode()&os.ModeNamedPipe != 0: |
| return newNamedPipe(*base, b.Path) |
| case base.Mode()&os.ModeDevice != 0: |
| return newDevice(*base, b.Path, b.Major, b.Minor) |
| } |
| |
| return nil, fmt.Errorf("unknown resource record (%#v): %s", b, base.Mode()) |
| } |
| |
| // NOTE(stevvooe): An alternative model that supports inline declaration. |
| // Convenient for unit testing where inline declarations may be desirable but |
| // creates an awkward API for the standard use case. |
| |
| // type ResourceKind int |
| |
| // const ( |
| // ResourceRegularFile = iota + 1 |
| // ResourceDirectory |
| // ResourceSymLink |
| // Resource |
| // ) |
| |
| // type Resource struct { |
| // Kind ResourceKind |
| // Paths []string |
| // Mode os.FileMode |
| // UID string |
| // GID string |
| // Size int64 |
| // Digests []digest.Digest |
| // Target string |
| // Major, Minor int |
| // XAttrs map[string][]byte |
| // } |
| |
| // type RegularFile struct { |
| // Paths []string |
| // Size int64 |
| // Digests []digest.Digest |
| // Perm os.FileMode // os.ModePerm + sticky, setuid, setgid |
| // } |