Move RootPath to fs package
This moves the RootPath function out of the archive package and into the fs package for external use. Signed-off-by: Michael Crosby <crosbymichael@gmail.com>
This commit is contained in:
99
fs/path.go
99
fs/path.go
@@ -7,6 +7,12 @@ import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
var (
|
||||
errTooManyLinks = errors.New("too many links")
|
||||
)
|
||||
|
||||
type currentPath struct {
|
||||
@@ -160,3 +166,96 @@ func nextPath(ctx context.Context, pathC <-chan *currentPath) (*currentPath, err
|
||||
return p, nil
|
||||
}
|
||||
}
|
||||
|
||||
// RootPath joins a path with a root, evaluating and bounding any
|
||||
// symlink to the root directory.
|
||||
func RootPath(root, path string) (string, error) {
|
||||
if path == "" {
|
||||
return root, nil
|
||||
}
|
||||
var linksWalked int // to protect against cycles
|
||||
for {
|
||||
i := linksWalked
|
||||
newpath, err := walkLinks(root, path, &linksWalked)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
path = newpath
|
||||
if i == linksWalked {
|
||||
newpath = filepath.Join("/", newpath)
|
||||
if path == newpath {
|
||||
return filepath.Join(root, newpath), nil
|
||||
}
|
||||
path = newpath
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func walkLink(root, path string, linksWalked *int) (newpath string, islink bool, err error) {
|
||||
if *linksWalked > 255 {
|
||||
return "", false, errTooManyLinks
|
||||
}
|
||||
|
||||
path = filepath.Join("/", path)
|
||||
if path == "/" {
|
||||
return path, false, nil
|
||||
}
|
||||
realPath := filepath.Join(root, path)
|
||||
|
||||
fi, err := os.Lstat(realPath)
|
||||
if err != nil {
|
||||
// If path does not yet exist, treat as non-symlink
|
||||
if os.IsNotExist(err) {
|
||||
return path, false, nil
|
||||
}
|
||||
return "", false, err
|
||||
}
|
||||
if fi.Mode()&os.ModeSymlink == 0 {
|
||||
return path, false, nil
|
||||
}
|
||||
newpath, err = os.Readlink(realPath)
|
||||
if err != nil {
|
||||
return "", false, err
|
||||
}
|
||||
if filepath.IsAbs(newpath) && strings.HasPrefix(newpath, root) {
|
||||
newpath = newpath[:len(root)]
|
||||
if !strings.HasPrefix(newpath, "/") {
|
||||
newpath = "/" + newpath
|
||||
}
|
||||
}
|
||||
*linksWalked++
|
||||
return newpath, true, nil
|
||||
}
|
||||
|
||||
func walkLinks(root, path string, linksWalked *int) (string, error) {
|
||||
switch dir, file := filepath.Split(path); {
|
||||
case dir == "":
|
||||
newpath, _, err := walkLink(root, file, linksWalked)
|
||||
return newpath, err
|
||||
case file == "":
|
||||
if os.IsPathSeparator(dir[len(dir)-1]) {
|
||||
if dir == "/" {
|
||||
return dir, nil
|
||||
}
|
||||
return walkLinks(root, dir[:len(dir)-1], linksWalked)
|
||||
}
|
||||
newpath, _, err := walkLink(root, dir, linksWalked)
|
||||
return newpath, err
|
||||
default:
|
||||
newdir, err := walkLinks(root, dir, linksWalked)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
newpath, islink, err := walkLink(root, filepath.Join(newdir, file), linksWalked)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if !islink {
|
||||
return newpath, nil
|
||||
}
|
||||
if filepath.IsAbs(newpath) {
|
||||
return newpath, nil
|
||||
}
|
||||
return filepath.Join(newdir, newpath), nil
|
||||
}
|
||||
}
|
||||
|
||||
295
fs/path_test.go
Normal file
295
fs/path_test.go
Normal file
@@ -0,0 +1,295 @@
|
||||
// +build !windows
|
||||
|
||||
package fs
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/containerd/containerd/fs/fstest"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
type rootCheck struct {
|
||||
unresolved string
|
||||
expected string
|
||||
scope func(string) string
|
||||
cause error
|
||||
}
|
||||
|
||||
func TestRootPath(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
apply fstest.Applier
|
||||
checks []rootCheck
|
||||
scope func(string) (string, error)
|
||||
}{
|
||||
{
|
||||
name: "SymlinkAbsolute",
|
||||
apply: Symlink("/b", "fs/a/d"),
|
||||
checks: Check("fs/a/d/c/data", "b/c/data"),
|
||||
},
|
||||
{
|
||||
name: "SymlinkRelativePath",
|
||||
apply: Symlink("a", "fs/i"),
|
||||
checks: Check("fs/i", "fs/a"),
|
||||
},
|
||||
{
|
||||
name: "SymlinkSkipSymlinksOutsideScope",
|
||||
apply: Symlink("realdir", "linkdir"),
|
||||
checks: CheckWithScope("foo/bar", "foo/bar", "linkdir"),
|
||||
},
|
||||
{
|
||||
name: "SymlinkLastLink",
|
||||
apply: Symlink("/b", "fs/a/d"),
|
||||
checks: Check("fs/a/d", "b"),
|
||||
},
|
||||
{
|
||||
name: "SymlinkRelativeLinkChangeScope",
|
||||
apply: Symlink("../b", "fs/a/e"),
|
||||
checks: CheckAll(
|
||||
Check("fs/a/e/c/data", "fs/b/c/data"),
|
||||
CheckWithScope("e", "b", "fs/a"), // Original return
|
||||
),
|
||||
},
|
||||
{
|
||||
name: "SymlinkDeepRelativeLinkChangeScope",
|
||||
apply: Symlink("../../../../test", "fs/a/f"),
|
||||
checks: CheckAll(
|
||||
Check("fs/a/f", "test"), // Original return
|
||||
CheckWithScope("a/f", "test", "fs"), // Original return
|
||||
),
|
||||
},
|
||||
{
|
||||
name: "SymlinkRelativeLinkChain",
|
||||
apply: fstest.Apply(
|
||||
Symlink("../g", "fs/b/h"),
|
||||
fstest.Symlink("../../../../../../../../../../../../root", "fs/g"),
|
||||
),
|
||||
checks: Check("fs/b/h", "root"),
|
||||
},
|
||||
{
|
||||
name: "SymlinkBreakoutPath",
|
||||
apply: Symlink("../i/a", "fs/j/k"),
|
||||
checks: CheckWithScope("k", "i/a", "fs/j"),
|
||||
},
|
||||
{
|
||||
name: "SymlinkToRoot",
|
||||
apply: Symlink("/", "foo"),
|
||||
checks: Check("foo", ""),
|
||||
},
|
||||
{
|
||||
name: "SymlinkSlashDotdot",
|
||||
apply: Symlink("/../../", "foo"),
|
||||
checks: Check("foo", ""),
|
||||
},
|
||||
{
|
||||
name: "SymlinkDotdot",
|
||||
apply: Symlink("../../", "foo"),
|
||||
checks: Check("foo", ""),
|
||||
},
|
||||
{
|
||||
name: "SymlinkRelativePath2",
|
||||
apply: Symlink("baz/target", "bar/foo"),
|
||||
checks: Check("bar/foo", "bar/baz/target"),
|
||||
},
|
||||
{
|
||||
name: "SymlinkScopeLink",
|
||||
apply: fstest.Apply(
|
||||
Symlink("root2", "root"),
|
||||
Symlink("../bar", "root2/foo"),
|
||||
),
|
||||
checks: CheckWithScope("foo", "bar", "root"),
|
||||
},
|
||||
{
|
||||
name: "SymlinkSelf",
|
||||
apply: fstest.Apply(
|
||||
Symlink("foo", "root/foo"),
|
||||
),
|
||||
checks: ErrorWithScope("foo", "root", errTooManyLinks),
|
||||
},
|
||||
{
|
||||
name: "SymlinkCircular",
|
||||
apply: fstest.Apply(
|
||||
Symlink("foo", "bar"),
|
||||
Symlink("bar", "foo"),
|
||||
),
|
||||
checks: ErrorWithScope("foo", "", errTooManyLinks), //TODO: Test for circular error
|
||||
},
|
||||
{
|
||||
name: "SymlinkCircularUnderRoot",
|
||||
apply: fstest.Apply(
|
||||
Symlink("baz", "root/bar"),
|
||||
Symlink("../bak", "root/baz"),
|
||||
Symlink("/bar", "root/bak"),
|
||||
),
|
||||
checks: ErrorWithScope("bar", "root", errTooManyLinks), // TODO: Test for circular error
|
||||
},
|
||||
{
|
||||
name: "SymlinkComplexChain",
|
||||
apply: fstest.Apply(
|
||||
fstest.CreateDir("root2", 0777),
|
||||
Symlink("root2", "root"),
|
||||
Symlink("r/s", "root/a"),
|
||||
Symlink("../root/t", "root/r"),
|
||||
Symlink("/../u", "root/root/t/s/b"),
|
||||
Symlink(".", "root/u/c"),
|
||||
Symlink("../v", "root/u/x/y"),
|
||||
Symlink("/../w", "root/u/v"),
|
||||
),
|
||||
checks: CheckWithScope("a/b/c/x/y/z", "w/z", "root"), // Original return
|
||||
},
|
||||
{
|
||||
name: "SymlinkBreakoutNonExistent",
|
||||
apply: fstest.Apply(
|
||||
Symlink("/", "root/slash"),
|
||||
Symlink("/idontexist/../slash", "root/sym"),
|
||||
),
|
||||
checks: CheckWithScope("sym/file", "file", "root"),
|
||||
},
|
||||
{
|
||||
name: "SymlinkNoLexicalCleaning",
|
||||
apply: fstest.Apply(
|
||||
Symlink("/foo/bar", "root/sym"),
|
||||
Symlink("/sym/../baz", "root/hello"),
|
||||
),
|
||||
checks: CheckWithScope("hello", "foo/baz", "root"),
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, makeRootPathTest(t, test.apply, test.checks))
|
||||
}
|
||||
|
||||
// Add related tests which are unable to follow same pattern
|
||||
t.Run("SymlinkRootScope", testRootPathSymlinkRootScope)
|
||||
t.Run("SymlinkEmpty", testRootPathSymlinkEmpty)
|
||||
}
|
||||
|
||||
func testRootPathSymlinkRootScope(t *testing.T) {
|
||||
tmpdir, err := ioutil.TempDir("", "TestFollowSymlinkRootScope")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer os.RemoveAll(tmpdir)
|
||||
|
||||
expected, err := filepath.EvalSymlinks(tmpdir)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
rewrite, err := RootPath("/", tmpdir)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if rewrite != expected {
|
||||
t.Fatalf("expected %q got %q", expected, rewrite)
|
||||
}
|
||||
}
|
||||
func testRootPathSymlinkEmpty(t *testing.T) {
|
||||
wd, err := os.Getwd()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
res, err := RootPath(wd, "")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if res != wd {
|
||||
t.Fatalf("expected %q got %q", wd, res)
|
||||
}
|
||||
}
|
||||
|
||||
func makeRootPathTest(t *testing.T, apply fstest.Applier, checks []rootCheck) func(t *testing.T) {
|
||||
return func(t *testing.T) {
|
||||
applyDir, err := ioutil.TempDir("", "test-root-path-")
|
||||
if err != nil {
|
||||
t.Fatalf("Unable to make temp directory: %+v", err)
|
||||
}
|
||||
defer os.RemoveAll(applyDir)
|
||||
|
||||
if apply != nil {
|
||||
if err := apply.Apply(applyDir); err != nil {
|
||||
t.Fatalf("Apply failed: %+v", err)
|
||||
}
|
||||
}
|
||||
|
||||
for i, check := range checks {
|
||||
root := applyDir
|
||||
if check.scope != nil {
|
||||
root = check.scope(root)
|
||||
}
|
||||
|
||||
actual, err := RootPath(root, check.unresolved)
|
||||
if check.cause != nil {
|
||||
if err == nil {
|
||||
t.Errorf("(Check %d) Expected error %q, %q evaluated as %q", i+1, check.cause.Error(), check.unresolved, actual)
|
||||
}
|
||||
if errors.Cause(err) != check.cause {
|
||||
t.Fatalf("(Check %d) Failed to evaluate root path: %+v", i+1, err)
|
||||
}
|
||||
} else {
|
||||
expected := filepath.Join(root, check.expected)
|
||||
if err != nil {
|
||||
t.Fatalf("(Check %d) Failed to evaluate root path: %+v", i+1, err)
|
||||
}
|
||||
if actual != expected {
|
||||
t.Errorf("(Check %d) Unexpected evaluated path %q, expected %q", i+1, actual, expected)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func Check(unresolved, expected string) []rootCheck {
|
||||
return []rootCheck{
|
||||
{
|
||||
unresolved: unresolved,
|
||||
expected: expected,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func CheckWithScope(unresolved, expected, scope string) []rootCheck {
|
||||
return []rootCheck{
|
||||
{
|
||||
unresolved: unresolved,
|
||||
expected: expected,
|
||||
scope: func(root string) string {
|
||||
return filepath.Join(root, scope)
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func ErrorWithScope(unresolved, scope string, cause error) []rootCheck {
|
||||
return []rootCheck{
|
||||
{
|
||||
unresolved: unresolved,
|
||||
cause: cause,
|
||||
scope: func(root string) string {
|
||||
return filepath.Join(root, scope)
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func CheckAll(checks ...[]rootCheck) []rootCheck {
|
||||
all := make([]rootCheck, 0, len(checks))
|
||||
for _, c := range checks {
|
||||
all = append(all, c...)
|
||||
}
|
||||
return all
|
||||
}
|
||||
|
||||
func Symlink(oldname, newname string) fstest.Applier {
|
||||
dir := filepath.Dir(newname)
|
||||
if dir != "" {
|
||||
return fstest.Apply(
|
||||
fstest.CreateDir(dir, 0755),
|
||||
fstest.Symlink(oldname, newname),
|
||||
)
|
||||
}
|
||||
return fstest.Symlink(oldname, newname)
|
||||
}
|
||||
Reference in New Issue
Block a user