diff --git a/archive/tar.go b/archive/tar.go index fa9601c69..99cfc35fc 100644 --- a/archive/tar.go +++ b/archive/tar.go @@ -395,6 +395,7 @@ type changeWriter struct { whiteoutT time.Time inodeSrc map[uint64]string inodeRefs map[uint64][]string + addedDirs map[string]struct{} } func newChangeWriter(w io.Writer, source string) *changeWriter { @@ -404,6 +405,7 @@ func newChangeWriter(w io.Writer, source string) *changeWriter { whiteoutT: time.Now(), inodeSrc: map[uint64]string{}, inodeRefs: map[uint64][]string{}, + addedDirs: map[string]struct{}{}, } } @@ -416,6 +418,7 @@ func (cw *changeWriter) HandleChange(k fs.ChangeKind, p string, f os.FileInfo, e whiteOutBase := filepath.Base(p) whiteOut := filepath.Join(whiteOutDir, whiteoutPrefix+whiteOutBase) hdr := &tar.Header{ + Typeflag: tar.TypeReg, Name: whiteOut[1:], Size: 0, ModTime: cw.whiteoutT, @@ -485,10 +488,8 @@ func (cw *changeWriter) HandleChange(k fs.ChangeKind, p string, f os.FileInfo, e additionalLinks = cw.inodeRefs[inode] delete(cw.inodeRefs, inode) } - } else if k == fs.ChangeKindUnmodified && !f.IsDir() { + } else if k == fs.ChangeKindUnmodified { // Nothing to write to diff - // Unmodified directories should still be written to keep - // directory permissions correct on direct unpack return nil } @@ -501,6 +502,9 @@ func (cw *changeWriter) HandleChange(k fs.ChangeKind, p string, f os.FileInfo, e hdr.PAXRecords[paxSchilyXattr+"security.capability"] = string(capability) } + if err := cw.includeParents(hdr); err != nil { + return err + } if err := cw.tw.WriteHeader(hdr); err != nil { return errors.Wrap(err, "failed to write file header") } @@ -530,6 +534,10 @@ func (cw *changeWriter) HandleChange(k fs.ChangeKind, p string, f os.FileInfo, e hdr.Typeflag = tar.TypeLink hdr.Linkname = source hdr.Size = 0 + + if err := cw.includeParents(hdr); err != nil { + return err + } if err := cw.tw.WriteHeader(hdr); err != nil { return errors.Wrap(err, "failed to write file header") } @@ -546,6 +554,29 @@ func (cw *changeWriter) Close() error { return nil } +func (cw *changeWriter) includeParents(hdr *tar.Header) error { + name := strings.TrimRight(hdr.Name, "/") + fname := filepath.Join(cw.source, name) + parent := filepath.Dir(name) + pname := filepath.Join(cw.source, parent) + + // Do not include root directory as parent + if fname != cw.source && pname != cw.source { + _, ok := cw.addedDirs[parent] + if !ok { + cw.addedDirs[parent] = struct{}{} + fi, err := os.Stat(pname) + if err != nil { + return err + } + if err := cw.HandleChange(fs.ChangeKindModify, parent, fi, nil); err != nil { + return err + } + } + } + return nil +} + func copyBuffered(ctx context.Context, dst io.Writer, src io.Reader) (written int64, err error) { buf := bufferPool.Get().(*[]byte) defer bufferPool.Put(buf) diff --git a/archive/tar_test.go b/archive/tar_test.go index 276a47d00..d5e10a6c6 100644 --- a/archive/tar_test.go +++ b/archive/tar_test.go @@ -698,6 +698,44 @@ func TestDiffTar(t *testing.T) { fstest.CreateFile("/d2/f", []byte("ok"), 0644), ), }, + { + name: "HardlinkParentInclusion", + validators: []tarEntryValidator{ + dirEntry("d2/", 0755), + fileEntry("d2/l1", []byte("link me"), 0644), + // d1/f1 and its parent is included after the new link, + // before the new link was included, these files would + // not habe needed + dirEntry("d1/", 0755), + linkEntry("d1/f1", "d2/l1"), + dirEntry("d3/", 0755), + fileEntry("d3/l1", []byte("link me"), 0644), + dirEntry("d4/", 0755), + linkEntry("d4/f1", "d3/l1"), + whiteoutEntry("d6/l1"), + whiteoutEntry("d6/l2"), + }, + a: fstest.Apply( + fstest.CreateDir("/d1/", 0755), + fstest.CreateFile("/d1/f1", []byte("link me"), 0644), + fstest.CreateDir("/d2/", 0755), + fstest.CreateFile("/d2/f1", []byte("link me"), 0644), + fstest.CreateDir("/d3/", 0755), + fstest.CreateDir("/d4/", 0755), + fstest.CreateFile("/d4/f1", []byte("link me"), 0644), + fstest.CreateDir("/d5/", 0755), + fstest.CreateFile("/d5/f1", []byte("link me"), 0644), + fstest.CreateDir("/d6/", 0755), + fstest.Link("/d1/f1", "/d6/l1"), + fstest.Link("/d5/f1", "/d6/l2"), + ), + b: fstest.Apply( + fstest.Link("/d1/f1", "/d2/l1"), + fstest.Link("/d4/f1", "/d3/l1"), + fstest.Remove("/d6/l1"), + fstest.Remove("/d6/l2"), + ), + }, } for _, at := range tests { @@ -740,6 +778,37 @@ func fileEntry(name string, expected []byte, mode int) tarEntryValidator { } } +func linkEntry(name, link string) tarEntryValidator { + return func(hdr *tar.Header, b []byte) error { + if hdr.Typeflag != tar.TypeLink { + return errors.New("not link type") + } + if hdr.Name != name { + return errors.Errorf("wrong name %q, expected %q", hdr.Name, name) + } + if hdr.Linkname != link { + return errors.Errorf("wrong link %q, expected %q", hdr.Linkname, link) + } + return nil + } +} + +func whiteoutEntry(name string) tarEntryValidator { + whiteOutDir := filepath.Dir(name) + whiteOutBase := filepath.Base(name) + whiteOut := filepath.Join(whiteOutDir, whiteoutPrefix+whiteOutBase) + + return func(hdr *tar.Header, b []byte) error { + if hdr.Typeflag != tar.TypeReg { + return errors.Errorf("not file type: %q", hdr.Typeflag) + } + if hdr.Name != whiteOut { + return errors.Errorf("wrong name %q, expected whiteout %q", hdr.Name, name) + } + return nil + } +} + func makeDiffTarTest(validators []tarEntryValidator, a, b fstest.Applier) func(*testing.T) { return func(t *testing.T) { ad, err := ioutil.TempDir("", "test-make-diff-tar-")