diff --git a/pkg/filesystem/resolve.go b/pkg/filesystem/resolve.go index 94b62de942..2fc8695552 100644 --- a/pkg/filesystem/resolve.go +++ b/pkg/filesystem/resolve.go @@ -125,8 +125,10 @@ func resolveSymlinkAncestor(path string) (string, error) { if !filepath.IsAbs(path) { return "", errors.New("dest path must be abs") } + last := "" newPath := path + loop: for newPath != "/" { fi, err := os.Lstat(newPath) @@ -134,11 +136,15 @@ loop: return "", errors.Wrap(err, "failed to lstat") } - switch mode := fi.Mode(); { - case mode&os.ModeSymlink != 0: + if util.IsSymlink(fi) { last = filepath.Base(newPath) newPath = filepath.Dir(newPath) - default: + } else { + // Even if the filenode pointed to by newPath is a regular file, + // one of its ancestors could be a symlink. We call filepath.EvalSymlinks + // to test whether there are any links in the path. If the output of + // EvalSymlinks is different than the input we know one of the nodes in the + // the path is a link. target, err := filepath.EvalSymlinks(newPath) if err != nil { return "", err diff --git a/pkg/filesystem/resolve_test.go b/pkg/filesystem/resolve_test.go index a2a7be3a39..bf9982dc9b 100644 --- a/pkg/filesystem/resolve_test.go +++ b/pkg/filesystem/resolve_test.go @@ -183,3 +183,202 @@ func Test_ResolvePaths(t *testing.T) { validateResults(t, files, expectedFiles, err) }) } + +func Test_resolveSymlinkAncestor(t *testing.T) { + setupDirs := func(t *testing.T) (string, string) { + testDir, err := ioutil.TempDir("", "") + if err != nil { + t.Fatal(err) + } + + targetDir := filepath.Join(testDir, "bar", "baz") + + if err := os.MkdirAll(targetDir, 0777); err != nil { + t.Fatal(err) + } + + targetPath := filepath.Join(targetDir, "bam.txt") + + if err := ioutil.WriteFile(targetPath, []byte("meow"), 0777); err != nil { + t.Fatal(err) + } + + return testDir, targetPath + } + + t.Run("path is a symlink", func(t *testing.T) { + testDir, targetPath := setupDirs(t) + + linkDir := filepath.Join(testDir, "foo", "buzz") + + if err := os.MkdirAll(linkDir, 0777); err != nil { + t.Fatal(err) + } + + linkPath := filepath.Join(linkDir, "zoom.txt") + + if err := os.Symlink(targetPath, linkPath); err != nil { + t.Fatal(err) + } + + expected := linkPath + + actual, err := resolveSymlinkAncestor(linkPath) + if err != nil { + t.Errorf("expected err to be nil but was %s", err) + } + + if actual != expected { + t.Errorf("expected result to be %s not %s", expected, actual) + } + }) + + t.Run("path is a dead symlink", func(t *testing.T) { + testDir, targetPath := setupDirs(t) + defer os.RemoveAll(testDir) + + linkDir := filepath.Join(testDir, "foo", "buzz") + + if err := os.MkdirAll(linkDir, 0777); err != nil { + t.Fatal(err) + } + + linkPath := filepath.Join(linkDir, "zoom.txt") + + if err := os.Symlink(targetPath, linkPath); err != nil { + t.Fatal(err) + } + + if err := os.Remove(targetPath); err != nil { + t.Fatal(err) + } + + expected := linkPath + + actual, err := resolveSymlinkAncestor(linkPath) + if err != nil { + t.Errorf("expected err to be nil but was %s", err) + } + + if actual != expected { + t.Errorf("expected result to be %s not %s", expected, actual) + } + }) + + t.Run("path is not a symlink", func(t *testing.T) { + _, targetPath := setupDirs(t) + + expected := targetPath + + actual, err := resolveSymlinkAncestor(targetPath) + if err != nil { + t.Errorf("expected err to be nil but was %s", err) + } + + if actual != expected { + t.Errorf("expected result to be %s not %s", expected, actual) + } + }) + + t.Run("parent of path is a symlink", func(t *testing.T) { + testDir, targetPath := setupDirs(t) + targetDir := filepath.Dir(targetPath) + + linkDir := filepath.Join(testDir, "foo") + + if err := os.MkdirAll(linkDir, 0777); err != nil { + t.Fatal(err) + } + + linkDir = filepath.Join(linkDir, "gaz") + + if err := os.Symlink(targetDir, linkDir); err != nil { + t.Fatal(err) + } + + linkPath := filepath.Join(linkDir, filepath.Base(targetPath)) + + expected := linkDir + + actual, err := resolveSymlinkAncestor(linkPath) + if err != nil { + t.Errorf("expected err to be nil but was %s", err) + } + + if actual != expected { + t.Errorf("expected result to be %s not %s", expected, actual) + } + }) + + t.Run("parent of path is a dead symlink", func(t *testing.T) { + testDir, targetPath := setupDirs(t) + targetDir := filepath.Dir(targetPath) + + linkDir := filepath.Join(testDir, "foo") + + if err := os.MkdirAll(linkDir, 0777); err != nil { + t.Fatal(err) + } + + linkDir = filepath.Join(linkDir, "gaz") + + if err := os.Symlink(targetDir, linkDir); err != nil { + t.Fatal(err) + } + + if err := os.RemoveAll(targetDir); err != nil { + t.Fatal(err) + } + + linkPath := filepath.Join(linkDir, filepath.Base(targetPath)) + + _, err := resolveSymlinkAncestor(linkPath) + if err == nil { + t.Error("expected err to not be nil") + } + }) + + t.Run("great grandparent of path is a symlink", func(t *testing.T) { + testDir, err := ioutil.TempDir("", "") + if err != nil { + t.Fatal(err) + } + + targetDir := filepath.Join(testDir, "bar", "baz") + + if err := os.MkdirAll(targetDir, 0777); err != nil { + t.Fatal(err) + } + + targetPath := filepath.Join(targetDir, "bam.txt") + + if err := ioutil.WriteFile(targetPath, []byte("meow"), 0777); err != nil { + t.Fatal(err) + } + + linkDir := filepath.Join(testDir, "foo") + + if err := os.Symlink(filepath.Dir(targetDir), linkDir); err != nil { + t.Fatal(err) + } + + linkPath := filepath.Join( + linkDir, + filepath.Join( + filepath.Base(targetDir), + filepath.Base(targetPath), + ), + ) + + expected := linkDir + + actual, err := resolveSymlinkAncestor(linkPath) + if err != nil { + t.Errorf("expected err to be nil but was %s", err) + } + + if actual != expected { + t.Errorf("expected result to be %s not %s", expected, actual) + } + }) +}