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 ®ularFile{ 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 = ®ularFile{} 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 ®ularFile{ 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