diff --git a/symlink/fs.go b/symlink/fs.go new file mode 100644 index 0000000..e91d33d --- /dev/null +++ b/symlink/fs.go @@ -0,0 +1,72 @@ +package symlink + +import ( + "fmt" + "os" + "path/filepath" + "strings" +) + +// FollowSymlink will follow an existing link and scope it to the root +// path provided. +func FollowSymlinkInScope(link, root string) (string, error) { + prev := "/" + + root, err := filepath.Abs(root) + if err != nil { + return "", err + } + + link, err = filepath.Abs(link) + if err != nil { + return "", err + } + + if !strings.HasPrefix(filepath.Dir(link), root) { + return "", fmt.Errorf("%s is not within %s", link, root) + } + + for _, p := range strings.Split(link, "/") { + prev = filepath.Join(prev, p) + prev = filepath.Clean(prev) + + for { + if !strings.HasPrefix(prev, root) { + // Don't resolve symlinks outside of root. For example, + // we don't have to check /home in the below. + // + // /home -> usr/home + // FollowSymlinkInScope("/home/bob/foo/bar", "/home/bob/foo") + break + } + + stat, err := os.Lstat(prev) + if err != nil { + if os.IsNotExist(err) { + break + } + return "", err + } + if stat.Mode()&os.ModeSymlink == os.ModeSymlink { + dest, err := os.Readlink(prev) + if err != nil { + return "", err + } + + switch dest[0] { + case '/': + prev = filepath.Join(root, dest) + case '.': + prev, _ = filepath.Abs(prev) + + if prev = filepath.Clean(filepath.Join(filepath.Dir(prev), dest)); len(prev) < len(root) { + prev = filepath.Join(root, filepath.Base(dest)) + } + } + } else { + break + } + } + } + return prev, nil +} diff --git a/symlink/fs_test.go b/symlink/fs_test.go new file mode 100644 index 0000000..1f12aa3 --- /dev/null +++ b/symlink/fs_test.go @@ -0,0 +1,108 @@ +package symlink + +import ( + "io/ioutil" + "os" + "path/filepath" + "testing" +) + +func abs(t *testing.T, p string) string { + o, err := filepath.Abs(p) + if err != nil { + t.Fatal(err) + } + return o +} + +func TestFollowSymLinkNormal(t *testing.T) { + link := "testdata/fs/a/d/c/data" + + rewrite, err := FollowSymlinkInScope(link, "testdata") + if err != nil { + t.Fatal(err) + } + + if expected := abs(t, "testdata/b/c/data"); expected != rewrite { + t.Fatalf("Expected %s got %s", expected, rewrite) + } +} + +func TestFollowSymLinkUnderLinkedDir(t *testing.T) { + dir, err := ioutil.TempDir("", "docker-fs-test") + if err != nil { + t.Fatal(err) + } + + os.Mkdir(filepath.Join(dir, "realdir"), 0700) + os.Symlink("realdir", filepath.Join(dir, "linkdir")) + + linkDir := filepath.Join(dir, "linkdir", "foo") + dirUnderLinkDir := filepath.Join(dir, "linkdir", "foo", "bar") + os.MkdirAll(dirUnderLinkDir, 0700) + + rewrite, err := FollowSymlinkInScope(dirUnderLinkDir, linkDir) + if err != nil { + t.Fatal(err) + } + + if rewrite != dirUnderLinkDir { + t.Fatalf("Expected %s got %s", dirUnderLinkDir, rewrite) + } +} + +func TestFollowSymLinkRandomString(t *testing.T) { + if _, err := FollowSymlinkInScope("toto", "testdata"); err == nil { + t.Fatal("Random string should fail but didn't") + } +} + +func TestFollowSymLinkLastLink(t *testing.T) { + link := "testdata/fs/a/d" + + rewrite, err := FollowSymlinkInScope(link, "testdata") + if err != nil { + t.Fatal(err) + } + + if expected := abs(t, "testdata/b"); expected != rewrite { + t.Fatalf("Expected %s got %s", expected, rewrite) + } +} + +func TestFollowSymLinkRelativeLink(t *testing.T) { + link := "testdata/fs/a/e/c/data" + + rewrite, err := FollowSymlinkInScope(link, "testdata") + if err != nil { + t.Fatal(err) + } + + if expected := abs(t, "testdata/fs/b/c/data"); expected != rewrite { + t.Fatalf("Expected %s got %s", expected, rewrite) + } +} + +func TestFollowSymLinkRelativeLinkScope(t *testing.T) { + link := "testdata/fs/a/f" + + rewrite, err := FollowSymlinkInScope(link, "testdata") + if err != nil { + t.Fatal(err) + } + + if expected := abs(t, "testdata/test"); expected != rewrite { + t.Fatalf("Expected %s got %s", expected, rewrite) + } + + link = "testdata/fs/b/h" + + rewrite, err = FollowSymlinkInScope(link, "testdata") + if err != nil { + t.Fatal(err) + } + + if expected := abs(t, "testdata/root"); expected != rewrite { + t.Fatalf("Expected %s got %s", expected, rewrite) + } +} diff --git a/symlink/testdata/fs/a/d b/symlink/testdata/fs/a/d new file mode 120000 index 0000000..28abc96 --- /dev/null +++ b/symlink/testdata/fs/a/d @@ -0,0 +1 @@ +/b \ No newline at end of file diff --git a/symlink/testdata/fs/a/e b/symlink/testdata/fs/a/e new file mode 120000 index 0000000..42532fe --- /dev/null +++ b/symlink/testdata/fs/a/e @@ -0,0 +1 @@ +../b \ No newline at end of file diff --git a/symlink/testdata/fs/a/f b/symlink/testdata/fs/a/f new file mode 120000 index 0000000..21de7ed --- /dev/null +++ b/symlink/testdata/fs/a/f @@ -0,0 +1 @@ +../../../../test \ No newline at end of file diff --git a/symlink/testdata/fs/b/h b/symlink/testdata/fs/b/h new file mode 120000 index 0000000..24387a6 --- /dev/null +++ b/symlink/testdata/fs/b/h @@ -0,0 +1 @@ +../g \ No newline at end of file diff --git a/symlink/testdata/fs/g b/symlink/testdata/fs/g new file mode 120000 index 0000000..0ce5de0 --- /dev/null +++ b/symlink/testdata/fs/g @@ -0,0 +1 @@ +../../../../../../../../../../../../root \ No newline at end of file