Skip to content

Commit

Permalink
tarfs: resolve symlinks when adding new files
Browse files Browse the repository at this point in the history
Previously, the package didn't consider ordering between directories,
their children, and symlinks. For example, given the following
archive:

- z/
- z/b
- a → z

If the archive producer walks and then sorts the input files then
deduplicates by inode, the resulting order is:

- a → z
- a/b
- z/

These edge cases are now handled, along with other edge cases that
arise from resolving symlinks.

Signed-off-by: Hank Donnay <[email protected]>
  • Loading branch information
hdonnay committed Jun 29, 2022
1 parent 8b93324 commit 4d1ba0d
Show file tree
Hide file tree
Showing 2 changed files with 113 additions and 5 deletions.
37 changes: 32 additions & 5 deletions pkg/tarfs/tarfs.go
Original file line number Diff line number Diff line change
Expand Up @@ -125,18 +125,45 @@ func (f *FS) add(name string, ino inode) error {
n := name
for n != "." {
n = filepath.Dir(n)
ti, ok := f.lookup[n]
if !ok {
ti = len(f.inode)
f.inode = append(f.inode, newDir(n))
f.lookup[n] = ti
ti := f.lookupOrMkdir(n)
// Now we need to resolve "ti" to an index of TypeDir.
// This handles the cases encoded in the "Symlinks" test.
cycle := make(map[int]struct{})
Resolve:
for {
if _, ok := cycle[ti]; ok {
return fmt.Errorf("tarfs: found cycle when resolving member %q", n)
}
cycle[ti] = struct{}{}
i := &f.inode[ti]
switch i.h.Typeflag {
case tar.TypeDir:
break Resolve
case tar.TypeSymlink:
ti = f.lookupOrMkdir(i.h.Linkname)
case tar.TypeReg:
return fmt.Errorf("tarfs: found symlink to regular file at %q while connecting child %q", n, name)
}
}
f.inode[ti].children[i] = struct{}{}
i = ti
}
return nil
}

// LookupOrMkdir looks up or creates a dir with the provided name.
//
// The inode index of the dir is reported.
func (f *FS) lookupOrMkdir(n string) int {
i, ok := f.lookup[n]
if !ok {
i = len(f.inode)
f.inode = append(f.inode, newDir(n))
f.lookup[n] = i
}
return i
}

// GetInode returns the inode backing "name".
//
// The "op" parameter is used in error reporting.
Expand Down
81 changes: 81 additions & 0 deletions pkg/tarfs/tarfs_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import (
"io"
"io/fs"
"os"
"path"
"path/filepath"
"sync"
"testing"
"testing/fstest"
Expand Down Expand Up @@ -227,3 +229,82 @@ func mktar(t *testing.T, in fs.FS, tw *tar.Writer) fs.WalkDirFunc {
return nil
}
}

func TestSymlinks(t *testing.T) {
tmp := t.TempDir()
run := func(wantErr bool, hs []tar.Header) func(*testing.T) {
return func(t *testing.T) {
t.Helper()
f, err := os.Create(filepath.Join(tmp, path.Base(t.Name())))
if err != nil {
t.Fatal(err)
}
defer f.Close()

tw := tar.NewWriter(f)
for i := range hs {
if err := tw.WriteHeader(&hs[i]); err != nil {
t.Error(err)
}
}
if err := tw.Close(); err != nil {
t.Error(err)
}

_, err = New(f)
t.Log(err)
if (err != nil) != wantErr {
t.Fail()
}
}
}
t.Run("Ordered", run(false, []tar.Header{
{Name: `a/`},
{
Typeflag: tar.TypeSymlink,
Name: `b`,
Linkname: `a`,
},
{Name: `b/c`},
}))
t.Run("Unordered", run(false, []tar.Header{
{
Typeflag: tar.TypeSymlink,
Name: `b`,
Linkname: `a`,
},
{Name: `b/c`},
{Name: `a/`},
}))
t.Run("LinkToReg", run(true, []tar.Header{
{Name: `a`},
{
Typeflag: tar.TypeSymlink,
Name: `b`,
Linkname: `a`,
},
{Name: `b/c`},
}))
t.Run("UnorderedLinkToReg", run(true, []tar.Header{
{
Typeflag: tar.TypeSymlink,
Name: `b`,
Linkname: `a`,
},
{Name: `b/c`},
{Name: `a`},
}))
t.Run("Cycle", run(true, []tar.Header{
{
Typeflag: tar.TypeSymlink,
Name: `b`,
Linkname: `a`,
},
{
Typeflag: tar.TypeSymlink,
Name: `a`,
Linkname: `b`,
},
{Name: `b/c`},
}))
}

0 comments on commit 4d1ba0d

Please sign in to comment.