diff --git a/drivers/aufs/aufs.go b/drivers/aufs/aufs.go index 0b17662109..bd7f4f670d 100644 --- a/drivers/aufs/aufs.go +++ b/drivers/aufs/aufs.go @@ -38,6 +38,7 @@ import ( "time" graphdriver "github.com/containers/storage/drivers" + "github.com/containers/storage/drivers/unionbackfill" "github.com/containers/storage/pkg/archive" "github.com/containers/storage/pkg/chrootarchive" "github.com/containers/storage/pkg/directory" @@ -46,6 +47,7 @@ import ( mountpk "github.com/containers/storage/pkg/mount" "github.com/containers/storage/pkg/parsers" "github.com/containers/storage/pkg/system" + "github.com/containers/storage/pkg/tarbackfill" "github.com/containers/storage/pkg/unshare" "github.com/opencontainers/selinux/go-selinux/label" "github.com/sirupsen/logrus" @@ -564,6 +566,16 @@ func (a *Driver) applyDiff(id string, idMappings *idtools.IDMappings, diff io.Re if idMappings == nil { idMappings = &idtools.IDMappings{} } + parentDiffDirs, err := a.getParentLayerPaths(id) + if err != nil { + return err + } + if len(parentDiffDirs) > 0 { + backfiller := unionbackfill.NewBackfiller(idMappings, parentDiffDirs) + rc := tarbackfill.NewIOReaderWithBackfiller(diff, backfiller) + defer rc.Close() + diff = rc + } return chrootarchive.UntarUncompressed(diff, path.Join(a.rootPath(), "diff", id), &archive.TarOptions{ UIDMaps: idMappings.UIDs(), GIDMaps: idMappings.GIDs(), diff --git a/drivers/driver.go b/drivers/driver.go index 1fb04dc3ed..b984e9b289 100644 --- a/drivers/driver.go +++ b/drivers/driver.go @@ -188,6 +188,7 @@ type DriverWithDifferOutput struct { Metadata string BigData map[string][]byte TarSplit []byte + ImplicitDirs []string TOCDigest digest.Digest } diff --git a/drivers/overlay/overlay.go b/drivers/overlay/overlay.go index 3170f09645..f4259fa3ae 100644 --- a/drivers/overlay/overlay.go +++ b/drivers/overlay/overlay.go @@ -21,6 +21,7 @@ import ( graphdriver "github.com/containers/storage/drivers" "github.com/containers/storage/drivers/overlayutils" "github.com/containers/storage/drivers/quota" + "github.com/containers/storage/drivers/unionbackfill" "github.com/containers/storage/pkg/archive" "github.com/containers/storage/pkg/chrootarchive" "github.com/containers/storage/pkg/directory" @@ -30,6 +31,7 @@ import ( "github.com/containers/storage/pkg/mount" "github.com/containers/storage/pkg/parsers" "github.com/containers/storage/pkg/system" + "github.com/containers/storage/pkg/tarbackfill" "github.com/containers/storage/pkg/unshare" units "github.com/docker/go-units" "github.com/hashicorp/go-multierror" @@ -2082,6 +2084,49 @@ func (d *Driver) ApplyDiffFromStagingDirectory(id, parent, stagingDirectory stri return err } + lowerDiffDirs, err := d.getLowerDiffPaths(id) + if err != nil { + return err + } + if len(lowerDiffDirs) > 0 { + backfiller := unionbackfill.NewBackfiller(options.Mappings, lowerDiffDirs) + for _, implicitDir := range diffOutput.ImplicitDirs { + hdr, err := backfiller.Backfill(implicitDir) + if err != nil { + return err + } + if hdr == nil { + continue + } + path := filepath.Join(stagingDirectory, implicitDir) + idPair := idtools.IDPair{UID: hdr.Uid, GID: hdr.Gid} + if options.Mappings != nil { + if mapped, err := options.Mappings.ToHost(idPair); err == nil { + idPair = mapped + } + } + if err := os.Chown(path, idPair.UID, idPair.GID); err != nil { + return err + } + for xattr, xval := range hdr.Xattrs { + if err := system.Lsetxattr(path, xattr, []byte(xval), 0); err != nil { + return err + } + } + if err := os.Chmod(path, os.FileMode(hdr.Mode)&os.ModePerm); err != nil { + return err + } + atime := hdr.AccessTime + mtime := hdr.ModTime + if atime.IsZero() { + atime = mtime + } + if err := os.Chtimes(path, atime, mtime); err != nil { + return err + } + } + } + diffOutput.UncompressedDigest = diffOutput.TOCDigest return os.Rename(stagingDirectory, diffPath) @@ -2116,7 +2161,18 @@ func (d *Driver) ApplyDiff(id, parent string, options graphdriver.ApplyDiffOpts) logrus.Debugf("Applying tar in %s", applyDir) // Overlay doesn't need the parent id to apply the diff - if err := untar(options.Diff, applyDir, &archive.TarOptions{ + diff := options.Diff + lowerDiffDirs, err := d.getLowerDiffPaths(id) + if err != nil { + return 0, err + } + if len(lowerDiffDirs) > 0 { + backfiller := unionbackfill.NewBackfiller(idMappings, lowerDiffDirs) + rc := tarbackfill.NewIOReaderWithBackfiller(diff, backfiller) + defer rc.Close() + diff = rc + } + if err := untar(diff, applyDir, &archive.TarOptions{ UIDMaps: idMappings.UIDs(), GIDMaps: idMappings.GIDs(), IgnoreChownErrors: d.options.ignoreChownErrors, diff --git a/drivers/unionbackfill/backfill.go b/drivers/unionbackfill/backfill.go new file mode 100644 index 0000000000..0530c19a48 --- /dev/null +++ b/drivers/unionbackfill/backfill.go @@ -0,0 +1,106 @@ +package unionbackfill + +import ( + "archive/tar" + "io/fs" + "os" + "path" + "path/filepath" + "strings" + + "github.com/containers/storage/pkg/archive" + "github.com/containers/storage/pkg/idtools" + "github.com/containers/storage/pkg/system" +) + +// NewBackfiller supplies a backfiller whose Backfill method provides the +// ownership/permissions/attributes of a directory from a lower layer so that +// we don't have to create it in an upper layer using default values that will +// be mistaken for a reason that the directory was pulled up to that layer. +func NewBackfiller(idmap *idtools.IDMappings, lowerDiffDirs []string) *backfiller { + if idmap != nil { + uidMaps, gidMaps := idmap.UIDs(), idmap.GIDs() + if len(uidMaps) > 0 || len(gidMaps) > 0 { + idmap = idtools.NewIDMappingsFromMaps(append([]idtools.IDMap{}, uidMaps...), append([]idtools.IDMap{}, gidMaps...)) + } + } + return &backfiller{idmap: idmap, lowerDiffDirs: append([]string{}, lowerDiffDirs...)} +} + +type backfiller struct { + idmap *idtools.IDMappings + lowerDiffDirs []string +} + +// Backfill supplies the ownership/permissions/attributes of a directory from a +// lower layer so that we don't have to create it in an upper layer using +// default values that will be mistaken for a reason that the directory was +// pulled up to that layer. +func (b *backfiller) Backfill(pathname string) (*tar.Header, error) { + for _, lowerDiffDir := range b.lowerDiffDirs { + candidate := filepath.Join(lowerDiffDir, pathname) + // if the asked-for path is in this lower, return a tar header for it + if st, err := os.Lstat(candidate); err == nil { + var linkTarget string + if st.Mode()&fs.ModeType == fs.ModeSymlink { + target, err := os.Readlink(candidate) + if err != nil { + return nil, err + } + linkTarget = target + } + hdr, err := tar.FileInfoHeader(st, linkTarget) + if err != nil { + return nil, err + } + // this is where we'd delete "opaque" from the header, if FileInfoHeader read xattrs + hdr.Name = strings.Trim(filepath.ToSlash(pathname), "/") + if st.Mode()&fs.ModeType == fs.ModeDir { + hdr.Name += "/" + } + if b.idmap != nil && !b.idmap.Empty() { + if uid, gid, err := b.idmap.ToContainer(idtools.IDPair{UID: hdr.Uid, GID: hdr.Gid}); err == nil { + hdr.Uid, hdr.Gid = uid, gid + } + } + return hdr, nil + } + // if the directory or any of its parents is marked opaque, we're done looking at lowers + p := strings.Trim(pathname, "/") + subpathname := "" + for { + dir, subdir := filepath.Split(p) + dir = strings.Trim(dir, "/") + if dir == p { + break + } + // kernel overlay style + xval, err := system.Lgetxattr(filepath.Join(lowerDiffDir, dir), archive.GetOverlayXattrName("opaque")) + if err == nil && len(xval) == 1 && xval[0] == 'y' { + return nil, nil + } + // aufs or fuse-overlayfs using aufs-like whiteouts + if _, err := os.Stat(filepath.Join(lowerDiffDir, dir, archive.WhiteoutOpaqueDir)); err == nil { + return nil, nil + } + // kernel overlay "redirect" - starting with the next lower layer, we'll need to look elsewhere + subpathname = strings.Trim(path.Join(subdir, subpathname), "/") + xval, err = system.Lgetxattr(filepath.Join(lowerDiffDir, dir), archive.GetOverlayXattrName("redirect")) + if err == nil && len(xval) > 0 { + subdir := string(xval) + if path.IsAbs(subdir) { + // path is relative to the root of the mount point + pathname = path.Join(subdir, subpathname) + } else { + // path is relative to the current directory + parent, _ := filepath.Split(dir) + parent = strings.Trim(parent, "/") + pathname = path.Join(parent, subdir, subpathname) + } + break + } + p = dir + } + } + return nil, nil +} diff --git a/drivers/unionbackfill/backfill_test.go b/drivers/unionbackfill/backfill_test.go new file mode 100644 index 0000000000..dd0d61d6d9 --- /dev/null +++ b/drivers/unionbackfill/backfill_test.go @@ -0,0 +1,270 @@ +package unionbackfill + +import ( + "archive/tar" + "errors" + "fmt" + "io/fs" + "io/ioutil" + "os" + "path" + "path/filepath" + "strings" + "syscall" + "testing" + "time" + + "github.com/containers/storage/pkg/archive" + "github.com/containers/storage/pkg/system" + "github.com/containers/storage/pkg/tarbackfill" + "github.com/stretchr/testify/require" +) + +func TestBackfiller(t *testing.T) { + tmp := t.TempDir() + subdirs := make([]string, 0, 10) + lower := filepath.Join(tmp, "lower") + require.NoError(t, os.Mkdir(lower, 0o755)) + for i := 0; i < cap(subdirs); i++ { + subdir := filepath.Join(lower, fmt.Sprintf("%d", i)) + require.NoError(t, os.Mkdir(subdir, 0o755)) + subdirs = append(subdirs, subdir) + } + epoch := time.Time{}.UTC() + early := time.Unix(1000000000, 234567).UTC() + // mark some parts of lowers as opaque (i.e., stop here when looking for content) + for _, opaqueDir := range []string{ + "4/a/b/c/d", + "5/a/b/c", + "6/a/b", + "7/a", + "8", + ".", + } { + // create the opaque marker in the specified directory + parent := filepath.Join(lower, opaqueDir) + err := os.MkdirAll(parent, 0o755) + require.NoError(t, err) + f, err := os.Create(filepath.Join(parent, archive.WhiteoutOpaqueDir)) + require.NoError(t, err) + f.Close() + // create a piece of content that we should see in the opaque directory + f, err = os.Create(filepath.Join(parent, "in-opaque")) + require.NoError(t, err) + f.Close() + os.Chtimes(filepath.Join(parent, "in-opaque"), epoch, epoch) + } + // some content that should be hidden because it's below an opaque, higher directory + for _, hiddenItemDir := range []string{ + "5/a/b/c/d", + "6/a/b/c", + "7/a/b", + "8/a", + "9", + } { + parent := filepath.Join(lower, hiddenItemDir) + err := os.MkdirAll(parent, 0o755) + require.NoError(t, err) + f, err := os.Create(filepath.Join(parent, "hidden")) + require.NoError(t, err) + f.Close() + } + // some content that we expect to be able to find + for _, visibleItemDir := range []string{ + "2/a/b/c/d/e", + "3/a/b/c/d", + "4/a/b/c", + "5/a/b", + "6/a", + } { + parent := filepath.Join(lower, visibleItemDir) + err := os.MkdirAll(parent, 0o755) + require.NoError(t, err) + f, err := os.Create(filepath.Join(parent, "visible")) + require.NoError(t, err) + require.NoError(t, f.Chmod(0o640)) + f.Close() + require.NoError(t, os.Chtimes(filepath.Join(parent, "visible"), early, early)) + } + var backfiller tarbackfill.Backfiller = NewBackfiller(nil, subdirs) + testCases := []struct { + requested, actual string + }{ + {"a/b/c/d/hidden", ""}, + {"a/b/c/hidden", ""}, + {"a/b/hidden", ""}, + {"a/hidden", ""}, + {"hidden", ""}, + {"a/b/c/d/in-opaque", "4/a/b/c/d/in-opaque"}, + {"a/b/c/in-opaque", "5/a/b/c/in-opaque"}, + {"a/b/in-opaque", "6/a/b/in-opaque"}, + {"a/in-opaque", "7/a/in-opaque"}, + {"in-opaque", "8/in-opaque"}, + {"a/b/c/d/e/visible", "2/a/b/c/d/e/visible"}, + {"a/b/c/d/visible", "3/a/b/c/d/visible"}, + {"a/b/c/visible", "4/a/b/c/visible"}, + {"a/b/visible", "5/a/b/visible"}, + {"a/visible", "6/a/visible"}, + } + for testCase := range testCases { + t.Run(testCases[testCase].requested, func(t *testing.T) { + hdr, err := backfiller.Backfill(testCases[testCase].requested) + require.NoError(t, err) + if testCases[testCase].actual == "" { + require.Nilf(t, hdr, "expected to not find content for path %q", testCases[testCase].requested) + } else { + require.NotNilf(t, hdr, "expected to find content for path %q", testCases[testCase].requested) + info, err := os.Lstat(filepath.Join(lower, testCases[testCase].actual)) + require.NoErrorf(t, err, "internal error looking for %q", testCases[testCase].actual) + expectedHdr, err := tar.FileInfoHeader(info, "") + require.NoErrorf(t, err, "internal error converting info about %q to a header", testCases[testCase].actual) + require.NotNilf(t, expectedHdr, "internal error converting info about %q to a header", testCases[testCase].actual) + expectedHdr.Name = testCases[testCase].requested + require.Equalf(t, *expectedHdr, *hdr, "unexpected header values for %q", testCases[testCase].actual) + } + }) + } +} + +func TestRedirectBackfiller(t *testing.T) { + tmp := t.TempDir() + mergedDir := filepath.Join(tmp, "merged") + require.NoError(t, os.Mkdir(mergedDir, 0o755)) + workDir := filepath.Join(tmp, "work") + require.NoError(t, os.Mkdir(workDir, 0o755)) + + directoryMode := 0o710 + directoryUid := 7 + directoryGid := 8 + now := time.Unix(time.Now().Unix(), 0) + defaultMode := 0o755 + + // create a directory we'll move around and put a directory under it and content in _that_ + layerDir := filepath.Join(tmp, "layer0") + layerDirs := []string{layerDir} + targetDir := filepath.Join(layerDirs[0], "a", "b", "c", "d", "e", "f", "g", "h", "template") + require.NoError(t, os.MkdirAll(targetDir, fs.FileMode(defaultMode))) + require.NoError(t, ioutil.WriteFile(filepath.Join(targetDir, "file"), []byte("some content"), 0o644)) + require.NoError(t, os.Chown(targetDir, directoryUid, directoryGid)) + require.NoError(t, os.Chmod(targetDir, fs.FileMode(directoryMode))) + require.NoError(t, os.Chtimes(targetDir, now, now)) + + // construct the location of the parent directory that we'll move once the overlay fs is mounted + targetDir = strings.ReplaceAll(filepath.Dir(targetDir), layerDir, mergedDir) + + mount := func() error { + redirectArg := "redirect_dir=on" + workdirArg := fmt.Sprintf("workdir=%s", workDir) + upperArg := fmt.Sprintf("upperdir=%s", layerDirs[0]) + var lowers []string + for i := 1; i < len(layerDirs); i++ { + lowers = append(lowers, layerDirs[i]) + } + lowersArg := fmt.Sprintf("lowerdir=%s", strings.Join(lowers, ":")) + mountOptArgs := []string{redirectArg, workdirArg, lowersArg, upperArg} + mountOpts := strings.Join(mountOptArgs, ",") + return syscall.Mount("none", mergedDir, "overlay", 0, mountOpts) + } + unmount := func() error { + return syscall.Unmount(mergedDir, 0) + } + defer unmount() + + // mount, then rename the mobile directory through the overlay mount + layerDir = filepath.Join(tmp, "layer1") + layerDirs = append([]string{layerDir}, layerDirs...) + require.NoError(t, os.Mkdir(layerDir, fs.FileMode(defaultMode))) + require.NoError(t, mount()) + newTargetDir := filepath.Join(mergedDir, "a", "b", "c", "d", "e", "f", "g", "h-new") + err := os.Rename(targetDir, newTargetDir) + if err != nil && (errors.Is(err, syscall.EXDEV) || errors.Is(err, syscall.EINVAL)) { + t.Skipf("unexpected error %v during rename - unable to test with redirect_dir=on", err) + } + require.NoError(t, err) + targetDir = newTargetDir + require.NoError(t, unmount()) + + // check that the kernel attached a "redirect" overlay attribute to the + // directory in the upper layer + xval, err := system.Lgetxattr(strings.ReplaceAll(targetDir, mergedDir, layerDir), archive.GetOverlayXattrName("redirect")) + if err != nil || len(xval) == 0 { + t.Skipf("kernel did not set redirect attribute in upper directory, can't test this") + } + + // add another layer in which we move it again + layerDir = filepath.Join(tmp, "layer2") + layerDirs = append([]string{layerDir}, layerDirs...) + require.NoError(t, os.Mkdir(layerDir, fs.FileMode(defaultMode))) + require.NoError(t, mount()) + newTargetDir = filepath.Join(mergedDir, "look-in-a-subdirectory") + require.NoError(t, os.Rename(targetDir, newTargetDir)) + targetDir = newTargetDir + require.NoError(t, unmount()) + + // add another layer in which we move it again + layerDir = filepath.Join(tmp, "layer3") + layerDirs = append([]string{layerDir}, layerDirs...) + require.NoError(t, os.Mkdir(layerDir, fs.FileMode(defaultMode))) + require.NoError(t, mount()) + newTargetDir = filepath.Join(mergedDir, "a", "b", "c", "d", "look-in-a-parent-sibling-directory") + require.NoError(t, os.Rename(targetDir, newTargetDir)) + targetDir = newTargetDir + require.NoError(t, unmount()) + + // add another layer in which we move it again + layerDir = filepath.Join(tmp, "layer4") + layerDirs = append([]string{layerDir}, layerDirs...) + require.NoError(t, os.Mkdir(layerDir, fs.FileMode(defaultMode))) + require.NoError(t, mount()) + newTargetDir = filepath.Join(mergedDir, "a", "b", "c", "d", "look-in-a-sibling-directory") + require.NoError(t, os.Rename(targetDir, newTargetDir)) + targetDir = newTargetDir + require.NoError(t, unmount()) + + // add another layer in which we move it again + layerDir = filepath.Join(tmp, "layer5") + layerDirs = append([]string{layerDir}, layerDirs...) + require.NoError(t, os.Mkdir(layerDir, fs.FileMode(defaultMode))) + require.NoError(t, mount()) + require.NoError(t, os.Mkdir(filepath.Join(mergedDir, "a", "b", "c", "d", "e", "f", "g", "h"), 0o755)) + newTargetDir = filepath.Join(mergedDir, "a", "b", "c", "d", "e", "f", "g", "h", "template") + require.NoError(t, os.Rename(targetDir, newTargetDir)) + // targetDir = newTargetDir + require.NoError(t, unmount()) + + // add another layer in which nothing happens + layerDir = filepath.Join(tmp, "layer6") + layerDirs = append([]string{layerDir}, layerDirs...) + + // start looking around + backfiller := NewBackfiller(nil, layerDirs) + hdr, err := backfiller.Backfill(path.Join("/", "a", "b", "c")) + require.NoError(t, err) + require.NotNil(t, hdr) + require.Equal(t, path.Join("a", "b", "c")+"/", hdr.Name) + hdr, err = backfiller.Backfill(path.Join("a", "b", "c")) + require.NoError(t, err) + require.NotNil(t, hdr) + require.Equal(t, path.Join("a", "b", "c")+"/", hdr.Name) + hdr, err = backfiller.Backfill(path.Join("a", "b", "d")) + require.NoError(t, err) + require.Nil(t, hdr) + hdr, err = backfiller.Backfill(path.Join("a", "b", "c", "d", "e", "f", "g", "h", "template")) + require.NoError(t, err) + require.NotNil(t, hdr) + require.Equal(t, int64(defaultMode), int64(hdr.Mode)) + require.Equal(t, 0, int(hdr.Uid)) + require.Equal(t, 0, int(hdr.Gid)) + hdr, err = backfiller.Backfill(path.Join("a", "b", "c", "d", "e", "f", "g", "h", "template", "template")) + require.NoError(t, err) + require.NotNil(t, hdr) + require.Equal(t, int64(directoryMode), int64(hdr.Mode)) + require.Equal(t, directoryUid, int(hdr.Uid)) + require.Equal(t, directoryGid, int(hdr.Gid)) + hdr, err = backfiller.Backfill(path.Join("a", "b", "c", "d", "e", "f", "g", "h", "template", "template", "file")) + require.NoError(t, err) + require.NotNil(t, hdr) + require.Equal(t, int64(0o644), int64(hdr.Mode)) + require.Equal(t, os.Getuid(), int(hdr.Uid)) + require.Equal(t, os.Getgid(), int(hdr.Gid)) +} diff --git a/pkg/chunked/internal/compression.go b/pkg/chunked/internal/compression.go index 49074eadfc..a6708ec122 100644 --- a/pkg/chunked/internal/compression.go +++ b/pkg/chunked/internal/compression.go @@ -48,7 +48,7 @@ type FileMetadata struct { ChunkDigest string `json:"chunkDigest,omitempty"` ChunkType string `json:"chunkType,omitempty"` - // internal: computed by mergeTOCEntries. + // internal: computed by mergeTocEntries. Chunks []*FileMetadata `json:"-"` } diff --git a/pkg/chunked/storage_linux.go b/pkg/chunked/storage_linux.go index f130560829..e196d59444 100644 --- a/pkg/chunked/storage_linux.go +++ b/pkg/chunked/storage_linux.go @@ -109,11 +109,17 @@ func doHardLink(srcFd int, destDirFd int, destBase string) error { return err } -func copyFileContent(srcFd int, destFile string, dirfd int, mode os.FileMode, useHardLinks bool) (*os.File, int64, error) { +// copyFileContent copies the contents of the file with descriptor `srcFd` into +// `destFile`, which is named relative to the directory opened as `dirfd`. The +// newly-created file will have its mode set to `mode`. +// Returns an `*os.File` for the newly-created-or-just-overwritten file, its +// size, and the names of any intermediate directories which needed to be +// created before the file could be created. +func copyFileContent(srcFd int, destFile string, dirfd int, mode os.FileMode, useHardLinks bool) (*os.File, int64, []string, error) { src := fmt.Sprintf("/proc/self/fd/%d", srcFd) st, err := os.Stat(src) if err != nil { - return nil, -1, fmt.Errorf("copy file content for %q: %w", destFile, err) + return nil, -1, nil, fmt.Errorf("copy file content for %q: %w", destFile, err) } copyWithFileRange, copyWithFileClone := true, true @@ -121,29 +127,29 @@ func copyFileContent(srcFd int, destFile string, dirfd int, mode os.FileMode, us if useHardLinks { destDirPath := filepath.Dir(destFile) destBase := filepath.Base(destFile) - destDir, err := openFileUnderRoot(destDirPath, dirfd, 0, mode) + destDir, implicitDirs, err := openFileUnderRoot(destDirPath, dirfd, 0, mode) if err == nil { defer destDir.Close() err := doHardLink(srcFd, int(destDir.Fd()), destBase) if err == nil { - return nil, st.Size(), nil + return nil, st.Size(), implicitDirs, nil } } } // If the destination file already exists, we shouldn't blow it away - dstFile, err := openFileUnderRoot(destFile, dirfd, newFileFlags, mode) + dstFile, implicitDirs, err := openFileUnderRoot(destFile, dirfd, newFileFlags, mode) if err != nil { - return nil, -1, fmt.Errorf("open file %q under rootfs for copy: %w", destFile, err) + return nil, -1, nil, fmt.Errorf("open file %q under rootfs for copy: %w", destFile, err) } err = driversCopy.CopyRegularToFile(src, dstFile, st, ©WithFileRange, ©WithFileClone) if err != nil { dstFile.Close() - return nil, -1, fmt.Errorf("copy to file %q under rootfs: %w", destFile, err) + return nil, -1, nil, fmt.Errorf("copy to file %q under rootfs: %w", destFile, err) } - return dstFile, st.Size(), nil + return dstFile, st.Size(), implicitDirs, nil } // GetTOCDigest returns the digest of the TOC as recorded in the annotations. @@ -240,24 +246,28 @@ func makeCopyBuffer() []byte { // name is the path to the file to copy in source. // dirfd is an open file descriptor to the destination root directory. // useHardLinks defines whether the deduplication can be performed using hard links. -func copyFileFromOtherLayer(file *internal.FileMetadata, source string, name string, dirfd int, useHardLinks bool) (bool, *os.File, int64, error) { +// Returns a boolean indicating whether it succeeded or not, and if it succeeded, a +// handle for the new file, the size of the file, and the names of any +// subdirectories which needed to be created before the new file could be +// created. +func copyFileFromOtherLayer(file *internal.FileMetadata, source string, name string, dirfd int, useHardLinks bool) (bool, *os.File, int64, []string, error) { srcDirfd, err := unix.Open(source, unix.O_RDONLY, 0) if err != nil { - return false, nil, 0, fmt.Errorf("open source file: %w", err) + return false, nil, 0, nil, fmt.Errorf("open source file: %w", err) } defer unix.Close(srcDirfd) - srcFile, err := openFileUnderRoot(name, srcDirfd, unix.O_RDONLY, 0) + srcFile, implicitDirs, err := openFileUnderRoot(name, srcDirfd, unix.O_RDONLY, 0) if err != nil { - return false, nil, 0, fmt.Errorf("open source file under target rootfs (%s): %w", name, err) + return false, nil, 0, nil, fmt.Errorf("open source file under target rootfs: %w", err) } defer srcFile.Close() - dstFile, written, err := copyFileContent(int(srcFile.Fd()), file.Name, dirfd, 0, useHardLinks) + dstFile, written, alsoImplicitDirs, err := copyFileContent(int(srcFile.Fd()), file.Name, dirfd, 0, useHardLinks) if err != nil { - return false, nil, 0, fmt.Errorf("copy content to %q: %w", file.Name, err) + return false, nil, 0, nil, fmt.Errorf("copy content to %q: %w", file.Name, err) } - return true, dstFile, written, nil + return true, dstFile, written, append(implicitDirs, alsoImplicitDirs...), nil } // canDedupMetadataWithHardLink says whether it is possible to deduplicate file with otherFile. @@ -320,15 +330,18 @@ func canDedupFileWithHardLink(file *internal.FileMetadata, fd int, s os.FileInfo // ostreeRepos is a list of OSTree repos. // dirfd is an open fd to the destination checkout. // useHardLinks defines whether the deduplication can be performed using hard links. -func findFileInOSTreeRepos(file *internal.FileMetadata, ostreeRepos []string, dirfd int, useHardLinks bool) (bool, *os.File, int64, error) { +// Returns a boolean indicating success, and on success an `*os.File` for the +// newly-created file, its size, and the names of any intermediate directories +// which needed to be created before the file could be created. +func findFileInOSTreeRepos(file *internal.FileMetadata, ostreeRepos []string, dirfd int, useHardLinks bool) (bool, *os.File, int64, []string, error) { digest, err := digest.Parse(file.Digest) if err != nil { logrus.Debugf("could not parse digest: %v", err) - return false, nil, 0, nil + return false, nil, 0, nil, nil } payloadLink := digest.Encoded() + ".payload-link" if len(payloadLink) < 2 { - return false, nil, 0, nil + return false, nil, 0, nil, nil } for _, repo := range ostreeRepos { @@ -343,7 +356,7 @@ func findFileInOSTreeRepos(file *internal.FileMetadata, ostreeRepos []string, di fd, err := unix.Open(sourceFile, unix.O_RDONLY|unix.O_NONBLOCK, 0) if err != nil { logrus.Debugf("could not open sourceFile %s: %v", sourceFile, err) - return false, nil, 0, nil + return false, nil, 0, nil, nil } f := os.NewFile(uintptr(fd), "fd") defer f.Close() @@ -353,19 +366,19 @@ func findFileInOSTreeRepos(file *internal.FileMetadata, ostreeRepos []string, di continue } - dstFile, written, err := copyFileContent(fd, file.Name, dirfd, 0, useHardLinks) + dstFile, written, implicitDirs, err := copyFileContent(fd, file.Name, dirfd, 0, useHardLinks) if err != nil { logrus.Debugf("could not copyFileContent: %v", err) - return false, nil, 0, nil + return false, nil, 0, nil, nil } - return true, dstFile, written, nil + return true, dstFile, written, implicitDirs, nil } // If hard links deduplication was used and it has failed, try again without hard links. if useHardLinks { return findFileInOSTreeRepos(file, ostreeRepos, dirfd, false) } - return false, nil, 0, nil + return false, nil, 0, nil, nil } // findFileInOtherLayers finds the specified file in other layers. @@ -373,10 +386,14 @@ func findFileInOSTreeRepos(file *internal.FileMetadata, ostreeRepos []string, di // file is the file to look for. // dirfd is an open file descriptor to the checkout root directory. // useHardLinks defines whether the deduplication can be performed using hard links. -func findFileInOtherLayers(cache *layersCache, file *internal.FileMetadata, dirfd int, useHardLinks bool) (bool, *os.File, int64, error) { +// Returns a boolean indicating whether it succeeded or not, and if it succeeded, a +// handle for the new file, the size of the file, and the names of any +// subdirectories which needed to be created before the new file could be +// created. +func findFileInOtherLayers(cache *layersCache, file *internal.FileMetadata, dirfd int, useHardLinks bool) (bool, *os.File, int64, []string, error) { target, name, err := cache.findFileInOtherLayers(file, useHardLinks) if err != nil || name == "" { - return false, nil, 0, err + return false, nil, 0, nil, err } return copyFileFromOtherLayer(file, target, name, dirfd, useHardLinks) } @@ -455,10 +472,13 @@ func (o *originFile) OpenFile() (io.ReadCloser, error) { } defer unix.Close(srcDirfd) - srcFile, err := openFileUnderRoot(o.Path, srcDirfd, unix.O_RDONLY, 0) + srcFile, implicitDirs, err := openFileUnderRoot(o.Path, srcDirfd, unix.O_RDONLY, 0) if err != nil { return nil, fmt.Errorf("open source file under target rootfs: %w", err) } + if len(implicitDirs) != 0 { + return nil, fmt.Errorf("did not expect to have to create %q in source directory", implicitDirs) + } if _, err := srcFile.Seek(o.Offset, 0); err != nil { srcFile.Close() @@ -488,12 +508,14 @@ func setFileAttrs(dirfd int, file *os.File, mode os.FileMode, metadata *internal if usePath { dirName := filepath.Dir(metadata.Name) if dirName != "" { - parentFd, err := openFileUnderRoot(dirName, dirfd, unix.O_PATH|unix.O_DIRECTORY, 0) + parentFd, implicitDirs, err := openFileUnderRoot(dirName, dirfd, unix.O_PATH|unix.O_DIRECTORY, 0) if err != nil { return err } defer parentFd.Close() - + if len(implicitDirs) != 0 { + return fmt.Errorf("did not expect to create any more directories while setting attributes on %q", metadata.Name) + } dirfd = int(parentFd.Fd()) } baseName = filepath.Base(metadata.Name) @@ -654,61 +676,66 @@ func openFileUnderRootRaw(dirfd int, name string, flags uint64, mode os.FileMode // dirfd is an open file descriptor to the target checkout directory. // flags are the flags to pass to the open syscall. // mode specifies the mode to use for newly created files. -func openFileUnderRoot(name string, dirfd int, flags uint64, mode os.FileMode) (*os.File, error) { +// Returns an `*os.File` for the possibly-newly-created file, and the names of +// any intermediate directories which needed to be created before the file +// could be created or opened. +func openFileUnderRoot(name string, dirfd int, flags uint64, mode os.FileMode) (*os.File, []string, error) { fd, err := openFileUnderRootRaw(dirfd, name, flags, mode) if err == nil { - return os.NewFile(uintptr(fd), name), nil + return os.NewFile(uintptr(fd), name), nil, nil } hasCreate := (flags & unix.O_CREAT) != 0 if errors.Is(err, unix.ENOENT) && hasCreate { parent := filepath.Dir(name) if parent != "" { - newDirfd, err2 := openOrCreateDirUnderRoot(parent, dirfd, 0) + newDirfd, implicitDirs, err2 := openOrCreateDirUnderRoot(parent, dirfd, 0) if err2 == nil { defer newDirfd.Close() fd, err := openFileUnderRootRaw(int(newDirfd.Fd()), filepath.Base(name), flags, mode) if err == nil { - return os.NewFile(uintptr(fd), name), nil + return os.NewFile(uintptr(fd), name), implicitDirs, nil } } } } - return nil, fmt.Errorf("open %q under the rootfs: %w", name, err) + return nil, nil, fmt.Errorf("open %q under the rootfs: %w", name, err) } // openOrCreateDirUnderRoot safely opens a directory or create it if it is missing. // name is the path to open relative to dirfd. // dirfd is an open file descriptor to the target checkout directory. // mode specifies the mode to use for newly created files. -func openOrCreateDirUnderRoot(name string, dirfd int, mode os.FileMode) (*os.File, error) { +// Returns an `*os.File` for the directory, and the names of any directories +// which needed to be created, including possibly the specified directory itself. +func openOrCreateDirUnderRoot(name string, dirfd int, mode os.FileMode) (*os.File, []string, error) { fd, err := openFileUnderRootRaw(dirfd, name, unix.O_DIRECTORY|unix.O_RDONLY, mode) if err == nil { - return os.NewFile(uintptr(fd), name), nil + return os.NewFile(uintptr(fd), name), nil, nil } if errors.Is(err, unix.ENOENT) { parent := filepath.Dir(name) if parent != "" { - pDir, err2 := openOrCreateDirUnderRoot(parent, dirfd, mode) + pDir, implicitDirs, err2 := openOrCreateDirUnderRoot(parent, dirfd, mode) if err2 != nil { - return nil, err + return nil, nil, err } defer pDir.Close() baseName := filepath.Base(name) if err2 := unix.Mkdirat(int(pDir.Fd()), baseName, 0o755); err2 != nil { - return nil, err + return nil, nil, err } fd, err = openFileUnderRootRaw(int(pDir.Fd()), baseName, unix.O_DIRECTORY|unix.O_RDONLY, mode) if err == nil { - return os.NewFile(uintptr(fd), name), nil + return os.NewFile(uintptr(fd), name), append(implicitDirs, name), nil } } } - return nil, err + return nil, nil, err } func (c *chunkedDiffer) prepareCompressedStreamToFile(partCompression compressedFileType, from io.Reader, mf *missingFileChunk) (compressedFileType, error) { @@ -831,10 +858,14 @@ type destinationFile struct { options *archive.TarOptions } -func openDestinationFile(dirfd int, metadata *internal.FileMetadata, options *archive.TarOptions) (*destinationFile, error) { - file, err := openFileUnderRoot(metadata.Name, dirfd, newFileFlags, 0) +// openDestinationFile opens the item, relative to `dirfd`, with the name +// specified in `metadata`. +// Returns a file handle and the names of any intermediate directories which +// needed to be created in order to create or open the destination. +func openDestinationFile(dirfd int, metadata *internal.FileMetadata, options *archive.TarOptions) (*destinationFile, []string, error) { + file, implicitDirs, err := openFileUnderRoot(metadata.Name, dirfd, newFileFlags, 0) if err != nil { - return nil, err + return nil, nil, err } digester := digest.Canonical.Digester() @@ -849,7 +880,7 @@ func openDestinationFile(dirfd int, metadata *internal.FileMetadata, options *ar metadata: metadata, options: options, dirfd: dirfd, - }, nil + }, implicitDirs, nil } func (d *destinationFile) Close() (Err error) { @@ -878,7 +909,7 @@ func closeDestinationFiles(files chan *destinationFile, errors chan error) { close(errors) } -func (c *chunkedDiffer) storeMissingFiles(streams chan io.ReadCloser, errs chan error, dest string, dirfd int, missingParts []missingPart, options *archive.TarOptions) (Err error) { +func (c *chunkedDiffer) storeMissingFiles(streams chan io.ReadCloser, errs chan error, dest string, dirfd int, missingParts []missingPart, options *archive.TarOptions) (implicitDirs []string, Err error) { var destFile *destinationFile filesToClose := make(chan *destinationFile, 3) @@ -904,7 +935,7 @@ func (c *chunkedDiffer) storeMissingFiles(streams chan io.ReadCloser, errs chan var err error part, err = missingPart.OriginFile.OpenFile() if err != nil { - return err + return nil, err } partCompression = fileTypeNoCompression case missingPart.SourceChunk != nil: @@ -913,15 +944,15 @@ func (c *chunkedDiffer) storeMissingFiles(streams chan io.ReadCloser, errs chan part = p case err := <-errs: if err == nil { - return errors.New("not enough data returned from the server") + return nil, errors.New("not enough data returned from the server") } - return err + return nil, err } if part == nil { - return errors.New("invalid stream returned") + return nil, errors.New("invalid stream returned") } default: - return errors.New("internal error: missing part misses both local and remote data stream") + return nil, errors.New("internal error: missing part misses both local and remote data stream") } for _, mf := range missingPart.Chunks { @@ -965,7 +996,7 @@ func (c *chunkedDiffer) storeMissingFiles(streams chan io.ReadCloser, errs chan } filesToClose <- destFile } - destFile, err = openDestinationFile(dirfd, mf.File, options) + destFile, implicitDirs, err = openDestinationFile(dirfd, mf.File, options) if err != nil { Err = err goto exit @@ -993,10 +1024,10 @@ func (c *chunkedDiffer) storeMissingFiles(streams chan io.ReadCloser, errs chan } if destFile != nil { - return destFile.Close() + return implicitDirs, destFile.Close() } - return nil + return implicitDirs, nil } func mergeMissingChunks(missingParts []missingPart, target int) []missingPart { @@ -1076,7 +1107,7 @@ func mergeMissingChunks(missingParts []missingPart, target int) []missingPart { return newMissingParts } -func (c *chunkedDiffer) retrieveMissingFiles(dest string, dirfd int, missingParts []missingPart, options *archive.TarOptions) error { +func (c *chunkedDiffer) retrieveMissingFiles(dest string, dirfd int, missingParts []missingPart, options *archive.TarOptions) ([]string, error) { var chunksToRequest []ImageSourceChunk calculateChunksToRequest := func() { @@ -1104,7 +1135,7 @@ func (c *chunkedDiffer) retrieveMissingFiles(dest string, dirfd int, missingPart requested := len(missingParts) // If the server cannot handle at least 64 chunks in a single request, just give up. if requested < 64 { - return err + return nil, err } // Merge more chunks to request @@ -1112,111 +1143,119 @@ func (c *chunkedDiffer) retrieveMissingFiles(dest string, dirfd int, missingPart calculateChunksToRequest() continue } - return err + return nil, err } - if err := c.storeMissingFiles(streams, errs, dest, dirfd, missingParts, options); err != nil { - return err + implicitDirs, err := c.storeMissingFiles(streams, errs, dest, dirfd, missingParts, options) + if err != nil { + return nil, err } - return nil + return implicitDirs, nil } -func safeMkdir(dirfd int, mode os.FileMode, name string, metadata *internal.FileMetadata, options *archive.TarOptions) error { +func safeMkdir(dirfd int, mode os.FileMode, name string, metadata *internal.FileMetadata, options *archive.TarOptions) ([]string, []string, error) { parent := filepath.Dir(name) base := filepath.Base(name) + var implicitDirs, explicitDirs []string parentFd := dirfd if parent != "." { - parentFile, err := openOrCreateDirUnderRoot(parent, dirfd, 0) + parentFile, theseImplicitDirs, err := openOrCreateDirUnderRoot(parent, dirfd, 0) if err != nil { - return err + return nil, nil, err } + implicitDirs = theseImplicitDirs defer parentFile.Close() parentFd = int(parentFile.Fd()) } if err := unix.Mkdirat(parentFd, base, uint32(mode)); err != nil { if !os.IsExist(err) { - return fmt.Errorf("mkdir %q: %w", name, err) + return nil, nil, fmt.Errorf("mkdir %q: %w", name, err) } } + explicitDirs = append(explicitDirs, name) - file, err := openFileUnderRoot(base, parentFd, unix.O_DIRECTORY|unix.O_RDONLY, 0) + file, alsoImplicitDirs, err := openFileUnderRoot(base, parentFd, unix.O_DIRECTORY|unix.O_RDONLY, 0) if err != nil { - return err + return nil, nil, err } defer file.Close() - return setFileAttrs(dirfd, file, mode, metadata, options, false) + return append(implicitDirs, alsoImplicitDirs...), explicitDirs, setFileAttrs(dirfd, file, mode, metadata, options, false) } -func safeLink(dirfd int, mode os.FileMode, metadata *internal.FileMetadata, options *archive.TarOptions) error { - sourceFile, err := openFileUnderRoot(metadata.Linkname, dirfd, unix.O_PATH|unix.O_RDONLY|unix.O_NOFOLLOW, 0) +func safeLink(dirfd int, mode os.FileMode, metadata *internal.FileMetadata, options *archive.TarOptions) ([]string, error) { + sourceFile, implicitDirs, err := openFileUnderRoot(metadata.Linkname, dirfd, unix.O_PATH|unix.O_RDONLY|unix.O_NOFOLLOW, 0) if err != nil { - return err + return nil, err } defer sourceFile.Close() destDir, destBase := filepath.Dir(metadata.Name), filepath.Base(metadata.Name) destDirFd := dirfd if destDir != "." { - f, err := openOrCreateDirUnderRoot(destDir, dirfd, 0) + f, theseImplicitDirs, err := openOrCreateDirUnderRoot(destDir, dirfd, 0) if err != nil { - return err + return nil, err } defer f.Close() + implicitDirs = append(implicitDirs, theseImplicitDirs...) destDirFd = int(f.Fd()) } err = doHardLink(int(sourceFile.Fd()), destDirFd, destBase) if err != nil { - return fmt.Errorf("create hardlink %q pointing to %q: %w", metadata.Name, metadata.Linkname, err) + return nil, fmt.Errorf("create hardlink %q pointing to %q: %w", metadata.Name, metadata.Linkname, err) } - newFile, err := openFileUnderRoot(metadata.Name, dirfd, unix.O_WRONLY|unix.O_NOFOLLOW, 0) + newFile, theseImplicitDirs, err := openFileUnderRoot(metadata.Name, dirfd, unix.O_WRONLY|unix.O_NOFOLLOW, 0) if err != nil { // If the target is a symlink, open the file with O_PATH. if errors.Is(err, unix.ELOOP) { - newFile, err := openFileUnderRoot(metadata.Name, dirfd, unix.O_PATH|unix.O_NOFOLLOW, 0) + newFile, thoseImplicitDirs, err := openFileUnderRoot(metadata.Name, dirfd, unix.O_PATH|unix.O_NOFOLLOW, 0) if err != nil { - return err + return nil, err } defer newFile.Close() - return setFileAttrs(dirfd, newFile, mode, metadata, options, true) + return append(implicitDirs, thoseImplicitDirs...), setFileAttrs(dirfd, newFile, mode, metadata, options, true) } - return err + return nil, err } defer newFile.Close() - return setFileAttrs(dirfd, newFile, mode, metadata, options, false) + return append(implicitDirs, theseImplicitDirs...), setFileAttrs(dirfd, newFile, mode, metadata, options, false) } -func safeSymlink(dirfd int, mode os.FileMode, metadata *internal.FileMetadata, options *archive.TarOptions) error { +func safeSymlink(dirfd int, mode os.FileMode, metadata *internal.FileMetadata, options *archive.TarOptions) ([]string, error) { + var implicitDirs []string destDir, destBase := filepath.Dir(metadata.Name), filepath.Base(metadata.Name) destDirFd := dirfd if destDir != "." { - f, err := openOrCreateDirUnderRoot(destDir, dirfd, 0) + f, theseImplicitDirs, err := openOrCreateDirUnderRoot(destDir, dirfd, 0) if err != nil { - return err + return nil, err } defer f.Close() + implicitDirs = theseImplicitDirs destDirFd = int(f.Fd()) } if err := unix.Symlinkat(metadata.Linkname, destDirFd, destBase); err != nil { - return fmt.Errorf("create symlink %q pointing to %q: %w", metadata.Name, metadata.Linkname, err) + return nil, fmt.Errorf("create symlink %q pointing to %q: %w", metadata.Name, metadata.Linkname, err) } - return nil + return implicitDirs, nil } type whiteoutHandler struct { - Dirfd int - Root string + Dirfd int + Root string + implicitDirs []string } func (d whiteoutHandler) Setxattr(path, name string, value []byte) error { - file, err := openOrCreateDirUnderRoot(path, d.Dirfd, 0) + file, implicitDirs, err := openOrCreateDirUnderRoot(path, d.Dirfd, 0) if err != nil { return err } @@ -1225,24 +1264,29 @@ func (d whiteoutHandler) Setxattr(path, name string, value []byte) error { if err := unix.Fsetxattr(int(file.Fd()), name, value, 0); err != nil { return fmt.Errorf("set xattr %s=%q for %q: %w", name, value, path, err) } + d.implicitDirs = append(d.implicitDirs, implicitDirs...) return nil } func (d whiteoutHandler) Mknod(path string, mode uint32, dev int) error { + var implicitDirs []string dir := filepath.Dir(path) base := filepath.Base(path) dirfd := d.Dirfd if dir != "" { - dir, err := openOrCreateDirUnderRoot(dir, d.Dirfd, 0) + dir, theseImplicitDirs, err := openOrCreateDirUnderRoot(dir, d.Dirfd, 0) if err != nil { return err } defer dir.Close() + implicitDirs = theseImplicitDirs dirfd = int(dir.Fd()) } + d.implicitDirs = append(d.implicitDirs, implicitDirs...) + if err := unix.Mknodat(dirfd, base, mode, dev); err != nil { return fmt.Errorf("mknod %q: %w", path, err) } @@ -1258,12 +1302,14 @@ func checkChownErr(err error, name string, uid, gid int) error { } func (d whiteoutHandler) Chown(path string, uid, gid int) error { - file, err := openFileUnderRoot(path, d.Dirfd, unix.O_PATH, 0) + file, implicitDirs, err := openFileUnderRoot(path, d.Dirfd, unix.O_PATH, 0) if err != nil { return err } defer file.Close() + d.implicitDirs = append(d.implicitDirs, implicitDirs...) + if err := unix.Fchownat(int(file.Fd()), "", uid, gid, unix.AT_EMPTY_PATH); err != nil { var stat unix.Stat_t if unix.Fstat(int(file.Fd()), &stat) == nil { @@ -1296,7 +1342,7 @@ type findAndCopyFileOptions struct { options *archive.TarOptions } -func (c *chunkedDiffer) findAndCopyFile(dirfd int, r *internal.FileMetadata, copyOptions *findAndCopyFileOptions, mode os.FileMode) (bool, error) { +func (c *chunkedDiffer) findAndCopyFile(dirfd int, r *internal.FileMetadata, copyOptions *findAndCopyFileOptions, mode os.FileMode) (bool, []string, error) { finalizeFile := func(dstFile *os.File) error { if dstFile != nil { defer dstFile.Close() @@ -1307,29 +1353,30 @@ func (c *chunkedDiffer) findAndCopyFile(dirfd int, r *internal.FileMetadata, cop return nil } - found, dstFile, _, err := findFileInOtherLayers(c.layersCache, r, dirfd, copyOptions.useHardLinks) + found, dstFile, _, implicitDirs, err := findFileInOtherLayers(c.layersCache, r, dirfd, copyOptions.useHardLinks) if err != nil { - return false, err + return false, nil, err } if found { if err := finalizeFile(dstFile); err != nil { - return false, err + return false, nil, err } - return true, nil + return true, implicitDirs, nil } - found, dstFile, _, err = findFileInOSTreeRepos(r, copyOptions.ostreeRepos, dirfd, copyOptions.useHardLinks) + found, dstFile, _, moreImplicitDirs, err := findFileInOSTreeRepos(r, copyOptions.ostreeRepos, dirfd, copyOptions.useHardLinks) if err != nil { - return false, err + return false, nil, err } + implicitDirs = append(implicitDirs, moreImplicitDirs...) if found { if err := finalizeFile(dstFile); err != nil { - return false, err + return false, nil, err } - return true, nil + return true, implicitDirs, nil } - return false, nil + return false, implicitDirs, nil } func makeEntriesFlat(mergedEntries []internal.FileMetadata) ([]internal.FileMetadata, error) { @@ -1479,8 +1526,9 @@ func (c *chunkedDiffer) ApplyDiff(dest string, options *archive.TarOptions, diff mode os.FileMode metadata *internal.FileMetadata - found bool - err error + found bool + implicitDirs []string + err error } var wg sync.WaitGroup @@ -1502,14 +1550,17 @@ func (c *chunkedDiffer) ApplyDiff(dest string, options *archive.TarOptions, diff go func() { defer wg.Done() for job := range jobs { - found, err := c.findAndCopyFile(dirfd, job.metadata, ©Options, job.mode) + found, implicitDirs, err := c.findAndCopyFile(dirfd, job.metadata, ©Options, job.mode) job.err = err + job.implicitDirs = implicitDirs job.found = found copyResults[job.njob] = job } }() } + var implicitDirs, explicitDirs []string + filesToWaitFor := 0 for i, r := range mergedEntries { if options.ForceMask != nil { @@ -1545,6 +1596,7 @@ func (c *chunkedDiffer) ApplyDiff(dest string, options *archive.TarOptions, diff if err != nil { return output, err } + implicitDirs = append(implicitDirs, handler.implicitDirs...) if !writeFile { continue } @@ -1554,27 +1606,32 @@ func (c *chunkedDiffer) ApplyDiff(dest string, options *archive.TarOptions, diff // Create directly empty files. if r.Size == 0 { // Used to have a scope for cleanup. - createEmptyFile := func() error { - file, err := openFileUnderRoot(r.Name, dirfd, newFileFlags, 0) + createEmptyFile := func() ([]string, error) { + file, implicitDirs, err := openFileUnderRoot(r.Name, dirfd, newFileFlags, 0) if err != nil { - return err + return nil, err } defer file.Close() if err := setFileAttrs(dirfd, file, mode, &r, options, false); err != nil { - return err + return nil, err } - return nil + return implicitDirs, nil } - if err := createEmptyFile(); err != nil { + theseImplicitDirs, err := createEmptyFile() + if err != nil { return output, err } + implicitDirs = append(implicitDirs, theseImplicitDirs...) continue } case tar.TypeDir: - if err := safeMkdir(dirfd, mode, r.Name, &r, options); err != nil { + theseImplicitDirs, theseExplicitDirs, err := safeMkdir(dirfd, mode, r.Name, &r, options) + if err != nil { return output, err } + implicitDirs = append(implicitDirs, theseImplicitDirs...) + explicitDirs = append(explicitDirs, theseExplicitDirs...) continue case tar.TypeLink: @@ -1591,9 +1648,11 @@ func (c *chunkedDiffer) ApplyDiff(dest string, options *archive.TarOptions, diff continue case tar.TypeSymlink: - if err := safeSymlink(dirfd, mode, &r, options); err != nil { + theseImplicitDirs, err := safeSymlink(dirfd, mode, &r, options) + if err != nil { return output, err } + implicitDirs = append(implicitDirs, theseImplicitDirs...) continue case tar.TypeChar: @@ -1641,6 +1700,8 @@ func (c *chunkedDiffer) ApplyDiff(dest string, options *archive.TarOptions, diff remainingSize := r.Size + implicitDirs = append(implicitDirs, res.implicitDirs...) + // the file is missing, attempt to find individual chunks. for _, chunk := range r.Chunks { compressedSize := int64(chunk.EndOffset - chunk.Offset) @@ -1672,13 +1733,17 @@ func (c *chunkedDiffer) ApplyDiff(dest string, options *archive.TarOptions, diff if err != nil { return output, err } - if offset >= 0 && validateChunkChecksum(chunk, root, path, offset, c.copyBuffer) { - missingPartsSize -= size - mp.OriginFile = &originFile{ - Root: root, - Path: path, - Offset: offset, + if offset >= 0 { + theseImplicitDirs, digestsMatch := validateChunkChecksum(chunk, root, path, offset, c.copyBuffer) + if digestsMatch { + missingPartsSize -= size + mp.OriginFile = &originFile{ + Root: root, + Path: path, + Offset: offset, + } } + implicitDirs = append(implicitDirs, theseImplicitDirs...) } case internal.ChunkTypeZeros: missingPartsSize -= size @@ -1694,17 +1759,37 @@ func (c *chunkedDiffer) ApplyDiff(dest string, options *archive.TarOptions, diff // There are some missing files. Prepare a multirange request for the missing chunks. if len(missingParts) > 0 { missingParts = mergeMissingChunks(missingParts, maxNumberMissingChunks) - if err := c.retrieveMissingFiles(dest, dirfd, missingParts, options); err != nil { + theseImplicitDirs, err := c.retrieveMissingFiles(dest, dirfd, missingParts, options) + if err != nil { return output, err } + implicitDirs = append(implicitDirs, theseImplicitDirs...) } for _, m := range hardLinks { - if err := safeLink(m.dirfd, m.mode, m.metadata, options); err != nil { + theseImplicitDirs, err := safeLink(m.dirfd, m.mode, m.metadata, options) + if err != nil { return output, err } + implicitDirs = append(implicitDirs, theseImplicitDirs...) } + output.ImplicitDirs = func() []string { + dirs := make(map[string]struct{}) + for _, dir := range implicitDirs { + dirs[strings.Trim(dir, "/")] = struct{}{} + } + for _, dir := range explicitDirs { + delete(dirs, strings.Trim(dir, "/")) + } + wereNeverExplicit := make([]string, 0, len(dirs)) + for dir := range dirs { + wereNeverExplicit = append(wereNeverExplicit, dir) + } + sort.Strings(wereNeverExplicit) + return wereNeverExplicit + }() + if totalChunksSize > 0 { logrus.Debugf("Missing %d bytes out of %d (%.2f %%)", missingPartsSize, totalChunksSize, float32(missingPartsSize*100.0)/float32(totalChunksSize)) } @@ -1798,34 +1883,34 @@ func (c *chunkedDiffer) mergeTocEntries(fileType compressedFileType, entries []i // validateChunkChecksum checks if the file at $root/$path[offset:chunk.ChunkSize] has the // same digest as chunk.ChunkDigest -func validateChunkChecksum(chunk *internal.FileMetadata, root, path string, offset int64, copyBuffer []byte) bool { +func validateChunkChecksum(chunk *internal.FileMetadata, root, path string, offset int64, copyBuffer []byte) ([]string, bool) { parentDirfd, err := unix.Open(root, unix.O_PATH, 0) if err != nil { - return false + return nil, false } defer unix.Close(parentDirfd) - fd, err := openFileUnderRoot(path, parentDirfd, unix.O_RDONLY, 0) + fd, implicitDirs, err := openFileUnderRoot(path, parentDirfd, unix.O_RDONLY, 0) if err != nil { - return false + return nil, false } defer fd.Close() if _, err := unix.Seek(int(fd.Fd()), offset, 0); err != nil { - return false + return nil, false } r := io.LimitReader(fd, chunk.ChunkSize) digester := digest.Canonical.Digester() if _, err := io.CopyBuffer(digester.Hash(), r, copyBuffer); err != nil { - return false + return nil, false } digest, err := digest.Parse(chunk.ChunkDigest) if err != nil { - return false + return nil, false } - return digester.Digest() == digest + return implicitDirs, digester.Digest() == digest } diff --git a/pkg/tarbackfill/tarbackfill.go b/pkg/tarbackfill/tarbackfill.go new file mode 100644 index 0000000000..8efc2b071c --- /dev/null +++ b/pkg/tarbackfill/tarbackfill.go @@ -0,0 +1,159 @@ +package tarbackfill + +import ( + "archive/tar" + "io" + "path" + "strings" +) + +// Reader wraps a tar.Reader so that if an item which would be read from it is +// in a directory which is not included in the archive, a specified Backfiller +// interface's Backfill() method will be called to supply a tar.Header which +// will be inserted into the stream just ahead of that item. +type Reader struct { + *tar.Reader + backfiller Backfiller + seen map[string]struct{} + queue []*tar.Header + currentIsQueued bool + err error +} + +// Backfiller is a wrapper for Backfill, which can supply headers to insert +// into an archive which is on its way to being extracted. +type Backfiller interface { + // Backfill either returns an entry for the passed-in path, nil if + // no entry should be added to the stream, or an error if something + // unexpected happened. + Backfill(string) (*tar.Header, error) +} + +// NewReaderWithBackfiller creates a new Reader reading from r, asking the +// passed-in Backfiller for information about parent directories which it +// hasn't seen yet. +func NewReaderWithBackfiller(r *tar.Reader, backfiller Backfiller) *Reader { + reader := &Reader{ + Reader: r, + backfiller: backfiller, + seen: make(map[string]struct{}), + } + return reader +} + +// Next returns either the next item from the archive we're filtering, or a +// synthesized entry for a directory that arguably should have been in that +// archive. +func (r *Reader) Next() (*tar.Header, error) { + // Drain the queue first. + if len(r.queue) > 0 { + next, queue := r.queue[0], r.queue[1:] + r.queue = queue + r.currentIsQueued = len(r.queue) > 0 + return next, nil + } + // If we've hit the end of the archive, we've hit the end of the archive. + r.currentIsQueued = false + if r.err != nil { + return nil, r.err + } + // Check what's next in the archive. + hdr, err := r.Reader.Next() + if err != nil { + r.err = err + } + if hdr == nil { + return hdr, err + } + for { + // Trim off an initial or final path separator. + name := strings.Trim(hdr.Name, "/") + if hdr.Typeflag == tar.TypeDir { + // Trim off an initial or final path separator, and + // note that we won't need to supply it later. + r.seen[name] = struct{}{} + } + // Figure out which directory this item is directly in. + p := name + dir, _ := path.Split(name) + var newHdr *tar.Header + for dir != p { + var bfErr error + dir = strings.Trim(dir, "/") + // If we already saw that directory, no need to interfere (further). + if _, ok := r.seen[dir]; dir == "" || ok || dir == name { + return hdr, err + } + // Ask the backfiller what to do here. + newHdr, bfErr = r.backfiller.Backfill(dir) + if bfErr != nil { + r.err = bfErr + return nil, bfErr + } + if newHdr == nil { + dir, _ = path.Split(dir) + continue + } + // Make sure the Name looks right, then queue up the current entry. + newHdr.Format = tar.FormatPAX + newHdr.Name = strings.Trim(newHdr.Name, "/") + if newHdr.Typeflag == tar.TypeDir { + // We won't need to supply it later. + r.seen[newHdr.Name] = struct{}{} + newHdr.Name += "/" + } + r.queue = append([]*tar.Header{hdr}, r.queue...) + hdr = newHdr + r.currentIsQueued = true + dir, _ = path.Split(dir) + } + } +} + +// Read will either read from a real entry in the archive, or pretend very hard +// that an entry we inserted had no content. +func (r *Reader) Read(b []byte) (int, error) { + if r.currentIsQueued { + return 0, nil + } + return r.Reader.Read(b) +} + +// NewIOReaderWithBackfiller creates a new ReadCloser for reading from a +// Reader, asking the passed-in Backfiller for parent directories of items in +// the archive that aren't in the archive. +func NewIOReaderWithBackfiller(reader io.Reader, backfiller Backfiller) io.ReadCloser { + rc, wc := io.Pipe() + go func() { + r := tar.NewReader(reader) + tr := NewReaderWithBackfiller(r, backfiller) + tw := tar.NewWriter(wc) + hdr, err := tr.Next() + defer func() { + closeErr := tw.Close() + io.Copy(wc, reader) + if err != nil { + wc.CloseWithError(err) + } else if closeErr != nil { + wc.CloseWithError(closeErr) + } else { + wc.Close() + } + }() + for hdr != nil { + if writeError := tw.WriteHeader(hdr); writeError != nil { + return + } + if err != nil { + break + } + if hdr.Size != 0 { + if _, err = io.Copy(tw, tr); err != nil { + return + } + } + hdr, err = tr.Next() + } + }() + return rc +} diff --git a/pkg/tarbackfill/tarbackfill_test.go b/pkg/tarbackfill/tarbackfill_test.go new file mode 100644 index 0000000000..d3e917be04 --- /dev/null +++ b/pkg/tarbackfill/tarbackfill_test.go @@ -0,0 +1,475 @@ +package tarbackfill + +import ( + "archive/tar" + "bytes" + "fmt" + "io" + "io/ioutil" + "sort" + "testing" + "time" + + "github.com/containers/storage/pkg/stringutils" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func makeTarByteSlice(headers []*tar.Header, trailerLength int) []byte { + var buf bytes.Buffer + block := make([]byte, 256) + for i := 0; i < 256; i++ { + block[i] = byte(i % 256) + } + tw := tar.NewWriter(&buf) + for i := range headers { + hdr := *headers[i] + hdr.Format = tar.FormatPAX + tw.WriteHeader(&hdr) + if hdr.Size > 0 { + written := int64(0) + for written < hdr.Size { + left := hdr.Size - written + if left > int64(len(block)) { + left = int64(len(block)) + } + n, err := tw.Write(block[:int(left)]) + if err != nil { + break + } + written += int64(n) + } + } + tw.Flush() + } + tw.Close() + padding := make([]byte, trailerLength) // some layer diffs have more trailing zeros than necessary, we need to preserve them + buf.Write(padding) + return buf.Bytes() +} + +func consumeTar(t *testing.T, reader io.Reader, fn func(*tar.Header)) { + t.Helper() + t.Run("parse", func(t *testing.T) { + tr := tar.NewReader(reader) + hdr, err := tr.Next() + for hdr != nil { + if fn != nil { + fn(hdr) + } + if hdr.Size != 0 { + n, err := io.Copy(ioutil.Discard, tr) + require.NoErrorf(t, err, "unexpected error copying entry payload for %q", hdr.Name) + require.Equalf(t, hdr.Size, n, "payload for %q had unexpected length", hdr.Name) + } + if err != nil { + break + } + hdr, err = tr.Next() + } + require.ErrorIs(t, err, io.EOF, "hit an error that wasn't EOF") + _, err = io.Copy(io.Discard, reader) + require.NoError(t, err, "while draining possible trailer") + }) +} + +type backfillerLogger struct { + t *testing.T + log *[]string + backfill bool + mode int64 + uid, gid int + date time.Time +} + +func (b *backfillerLogger) Backfill(path string) (*tar.Header, error) { + if !stringutils.InSlice(*(b.log), path) { + *(b.log) = append(*(b.log), path) + sort.Strings(*(b.log)) + } + if b.backfill { + return &tar.Header{Name: path, Typeflag: tar.TypeDir, Mode: b.mode, Uid: b.uid, Gid: b.gid, ModTime: b.date}, nil + } + return nil, nil +} + +func newBackfillerLogger(t *testing.T, log *[]string, backfill bool, mode int64, uid, gid int, date time.Time) *backfillerLogger { + return &backfillerLogger{t: t, log: log, backfill: backfill, mode: mode, uid: uid, gid: gid, date: date} +} + +func TestNewIOReaderWithBackfiller(t *testing.T) { + directoryMode := int64(0o750) + directoryUid := 5 + directoryGid := 6 + now := time.Now().UTC() + testCases := []struct { + description string + inputs []*tar.Header + backfills []string + outputs []*tar.Header + }{ + { + description: "empty", + }, + { + description: "base", + inputs: []*tar.Header{ + { + Name: "a", + Typeflag: tar.TypeReg, + Mode: 0o644, + Uid: 1, + Gid: 1, + Size: 2, + ModTime: now, + }, + }, + outputs: []*tar.Header{ + { + Name: "a", + Typeflag: tar.TypeReg, + Mode: 0o644, + Uid: 1, + Gid: 1, + Size: 2, + ModTime: now, + }, + }, + }, + { + description: "topdir", + inputs: []*tar.Header{ + { + Name: "a", + Typeflag: tar.TypeDir, + Mode: 0o750, + Uid: 1, + Gid: 1, + Size: 0, + ModTime: now, + }, + }, + outputs: []*tar.Header{ + { + Name: "a", + Typeflag: tar.TypeDir, + Mode: 0o750, + Uid: 1, + Gid: 1, + Size: 0, + ModTime: now, + }, + }, + }, + { + description: "shallow", + inputs: []*tar.Header{ + { + Name: "a/b", + Typeflag: tar.TypeReg, + Mode: 0o644, + Uid: 1, + Gid: 2, + Size: 1234, + ModTime: now, + }, + { + Name: "a/c", + Typeflag: tar.TypeReg, + Mode: 0o644, + Uid: 3, + Gid: 4, + Size: 1234, + ModTime: now, + }, + { + Name: "a/d", + Typeflag: tar.TypeDir, + Mode: 0o700, + Uid: 5, + Gid: 6, + Size: 0, + ModTime: now, + }, + }, + backfills: []string{ + "a", + }, + outputs: []*tar.Header{ + { + Name: "a/", + Typeflag: tar.TypeDir, + Mode: directoryMode, + Uid: directoryUid, + Gid: directoryGid, + Size: 0, + ModTime: now, + }, + { + Name: "a/b", + Typeflag: tar.TypeReg, + Mode: 0o644, + Uid: 1, + Gid: 2, + Size: 1234, + ModTime: now, + }, + { + Name: "a/c", + Typeflag: tar.TypeReg, + Mode: 0o644, + Uid: 3, + Gid: 4, + Size: 1234, + ModTime: now, + }, + { + Name: "a/d", + Typeflag: tar.TypeDir, + Mode: 0o700, + Uid: 5, + Gid: 6, + Size: 0, + ModTime: now, + }, + }, + }, + { + description: "deep", + inputs: []*tar.Header{ + { + Name: "a/c", + Typeflag: tar.TypeReg, + Mode: 0o644, + Uid: 3, + Gid: 4, + Size: 1234, + ModTime: now, + }, + { + Name: "a/b/c/d/", + Typeflag: tar.TypeDir, + Mode: 0o700, + Uid: 1, + Gid: 2, + Size: 0, + ModTime: now, + }, + { + Name: "a/b/c/d/e/f/g", + Typeflag: tar.TypeReg, + Mode: 0o644, + Uid: 3, + Gid: 4, + Size: 12346, + ModTime: now, + }, + { + Name: "b/c/d/e/f/g/", + Typeflag: tar.TypeDir, + Mode: 0o711, + Uid: 5, + Gid: 6, + Size: 0, + ModTime: now, + }, + }, + backfills: []string{ + "a", + "a/b", + "a/b/c", + "a/b/c/d/e", + "a/b/c/d/e/f", + "b", + "b/c", + "b/c/d", + "b/c/d/e", + "b/c/d/e/f", + }, + outputs: []*tar.Header{ + { + Name: "a/", + Typeflag: tar.TypeDir, + Mode: directoryMode, + Uid: directoryUid, + Gid: directoryGid, + Size: 0, + ModTime: now, + }, + { + Name: "a/c", + Typeflag: tar.TypeReg, + Mode: 0o644, + Uid: 1, + Gid: 2, + Size: 1234, + ModTime: now, + }, + { + Name: "a/b/", + Typeflag: tar.TypeDir, + Mode: directoryMode, + Uid: directoryUid, + Gid: directoryGid, + Size: 0, + ModTime: now, + }, + { + Name: "a/b/c/", + Typeflag: tar.TypeDir, + Mode: directoryMode, + Uid: directoryUid, + Gid: directoryGid, + Size: 0, + ModTime: now, + }, + { + Name: "a/b/c/d/", + Typeflag: tar.TypeDir, + Mode: 0o700, + Uid: 1, + Gid: 2, + Size: 0, + ModTime: now, + }, + { + Name: "a/b/c/d/e/", + Typeflag: tar.TypeDir, + Mode: directoryMode, + Uid: directoryUid, + Gid: directoryGid, + Size: 0, + ModTime: now, + }, + { + Name: "a/b/c/d/e/f/", + Typeflag: tar.TypeDir, + Mode: directoryMode, + Uid: directoryUid, + Gid: directoryGid, + Size: 0, + ModTime: now, + }, + { + Name: "a/b/c/d/e/f/g", + Typeflag: tar.TypeReg, + Mode: 0o644, + Uid: 3, + Gid: 4, + Size: 12346, + ModTime: now, + }, + { + Name: "b/", + Typeflag: tar.TypeDir, + Mode: directoryMode, + Uid: directoryUid, + Gid: directoryGid, + Size: 0, + ModTime: now, + }, + { + Name: "b/c/", + Typeflag: tar.TypeDir, + Mode: directoryMode, + Uid: directoryUid, + Gid: directoryGid, + Size: 0, + ModTime: now, + }, + { + Name: "b/c/d/", + Typeflag: tar.TypeDir, + Mode: directoryMode, + Uid: directoryUid, + Gid: directoryGid, + Size: 0, + ModTime: now, + }, + { + Name: "b/c/d/e/", + Typeflag: tar.TypeDir, + Mode: directoryMode, + Uid: directoryUid, + Gid: directoryGid, + Size: 0, + ModTime: now, + }, + { + Name: "b/c/d/e/f/", + Typeflag: tar.TypeDir, + Mode: directoryMode, + Uid: directoryUid, + Gid: directoryGid, + Size: 0, + ModTime: now, + }, + { + Name: "b/c/d/e/f/g/", + Typeflag: tar.TypeDir, + Mode: 0o711, + Uid: 5, + Gid: 6, + Size: 0, + ModTime: now, + }, + }, + }, + } + for testCase := range testCases { + t.Run(testCases[testCase].description, func(t *testing.T) { + for _, paddingSize := range []int{0, 512, 1024, 2048, 4096, 8192} { + t.Run(fmt.Sprintf("paddingSize=%d", paddingSize), func(t *testing.T) { + tarBytes := makeTarByteSlice(testCases[testCase].inputs, paddingSize) + + t.Run("basic", func(t *testing.T) { + tarBytesReader := bytes.NewReader(tarBytes) + consumeTar(t, tarBytesReader, nil) + assert.Zero(t, tarBytesReader.Len()) + }) + + t.Run("logged", func(t *testing.T) { + var backfillLog []string + tarBytesReader := bytes.NewReader(tarBytes) + rc := NewIOReaderWithBackfiller(tarBytesReader, newBackfillerLogger(t, &backfillLog, false, 0o700, 1, 2, time.Time{})) + defer rc.Close() + consumeTar(t, rc, nil) + require.Equal(t, testCases[testCase].backfills, backfillLog, "backfill not called exactly the right number of times") + assert.Zero(t, tarBytesReader.Len()) + }) + + t.Run("broken", func(t *testing.T) { + var backfillLog []string + tarBytesReader := bytes.NewReader(tarBytes) + rc := NewIOReaderWithBackfiller(tarBytesReader, newBackfillerLogger(t, &backfillLog, false, directoryMode, directoryUid, directoryGid, now)) + defer rc.Close() + consumeTar(t, rc, nil) + assert.Zero(t, tarBytesReader.Len()) + }) + + t.Run("filled", func(t *testing.T) { + var backfillLog []string + tarBytesReader := bytes.NewReader(tarBytes) + rc := NewIOReaderWithBackfiller(tarBytesReader, newBackfillerLogger(t, &backfillLog, true, directoryMode, directoryUid, directoryGid, now)) + defer rc.Close() + outputs := make([]*tar.Header, 0, len(testCases[testCase].inputs)+len(testCases[testCase].backfills)) + consumeTar(t, rc, func(hdr *tar.Header) { tmp := *hdr; hdr = &tmp; outputs = append(outputs, hdr) }) + require.Equal(t, len(testCases[testCase].outputs), len(outputs), "wrong number of output entries") + assert.Zero(t, tarBytesReader.Len()) + if len(outputs) != 0 { + for i := range outputs { + expected := testCases[testCase].outputs[i] + actual := outputs[i] + require.EqualValuesf(t, expected.Name, actual.Name, "output %d name", i) + require.EqualValuesf(t, expected.Mode, actual.Mode, "output %d mode", i) + require.EqualValuesf(t, expected.Typeflag, actual.Typeflag, "output %d type", i) + require.Truef(t, actual.ModTime.UTC().Equal(expected.ModTime.UTC()), "output %d (%q) date differs (%v != %v)", i, actual.Name, actual.ModTime.UTC(), expected.ModTime.UTC()) + } + } + require.Equal(t, testCases[testCase].backfills, backfillLog, "backfill not called exactly the right number of times") + }) + }) + } + }) + } +} diff --git a/tests/apply-diff.bats b/tests/apply-diff.bats index 0f4351dd1d..a8d24faca7 100644 --- a/tests/apply-diff.bats +++ b/tests/apply-diff.bats @@ -44,3 +44,71 @@ load helpers checkchanges checkdiffs } + +@test "apply-implicitdir-diff" { + # We need "tar" to build layer diffs. + if test -z "$(which tar 2> /dev/null)" ; then + skip "need tar" + fi + + # Create one layer diff, then another that includes added/modified + # items but _not_ the directories that contain them. + pushd $TESTDIR + mkdir subdirectory1 + chmod 0700 subdirectory1 + mkdir subdirectory2 + chmod 0750 subdirectory2 + tar cvf lower.tar subdirectory1 subdirectory2 + touch subdirectory1/testfile1 subdirectory2/testfile2 + tar cvf middle.tar subdirectory1/testfile1 subdirectory2/testfile2 + popd + + # Create layers and populate them using the diffs. + run storage --debug=false create-layer + [ "$status" -eq 0 ] + [ "$output" != "" ] + lowerlayer="$output" + storage applydiff -f "$TESTDIR"/lower.tar "$lowerlayer" + + run storage --debug=false create-layer "$lowerlayer" + [ "$status" -eq 0 ] + [ "$output" != "" ] + middlelayer="$output" + storage applydiff -f "$TESTDIR"/middle.tar "$middlelayer" + + run storage --debug=false create-layer "$middlelayer" + [ "$status" -eq 0 ] + [ "$output" != "" ] + upperlayer="$output" + + run storage --debug=false mount "$upperlayer" + [ "$status" -eq 0 ] + [ "$output" != "" ] + mountpoint="$output" + + run stat -c %a "$TESTDIR"/subdirectory1 + [ "$status" -eq 0 ] + [ "$output" != "" ] + expected="$output" + echo subdirectory1 should have mode $expected + + run stat -c %a "$mountpoint"/subdirectory1 + [ "$status" -eq 0 ] + [ "$output" != "" ] + actual="$output" + echo subdirectory1 has mode $actual + [ "$actual" = "$expected" ] + + run stat -c %a "$TESTDIR"/subdirectory2 + [ "$status" -eq 0 ] + [ "$output" != "" ] + expected="$output" + echo subdirectory2 should have mode $expected + + run stat -c %a "$mountpoint"/subdirectory2 + [ "$status" -eq 0 ] + [ "$output" != "" ] + actual="$output" + echo subdirectory2 has mode $actual + [ "$actual" = "$expected" ] +}