Last active
April 11, 2018 03:58
-
-
Save cofyc/860b134c593c1d81e19ca34a5c26bb6f to your computer and use it in GitHub Desktop.
EvalSymlinksAtRoot
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package filepath | |
import ( | |
"errors" | |
"os" | |
"path/filepath" | |
"runtime" | |
) | |
// See https://github.com/kubernetes/kubernetes/pull/61489#discussion_r179732719 | |
// Evaluate symlinks in filesystem at given root. | |
func EvalSymlinksAtRoot(path string, root string) (string, error) { | |
return walkSymlinks(path, root) | |
} | |
func walkSymlinks(path string, root string) (string, error) { | |
if path == "" { | |
return path, nil | |
} | |
var linksWalked int // to protect against cycles | |
for { | |
i := linksWalked | |
newpath, err := walkLinks(path, root, &linksWalked) | |
if err != nil { | |
return "", err | |
} | |
if runtime.GOOS == "windows" { | |
// walkLinks(".", ...) always returns "." on unix. | |
// But on windows it returns symlink target, if current | |
// directory is a symlink. Stop the walk, if symlink | |
// target is not absolute path, and return "." | |
// to the caller (just like unix does). | |
// Same for "C:.". | |
if path[volumeNameLen(path):] == "." && !filepath.IsAbs(newpath) { | |
return path, nil | |
} | |
} | |
if i == linksWalked { | |
return filepath.Clean(newpath), nil | |
} | |
path = newpath | |
} | |
} | |
func walkLinks(path string, root string, linksWalked *int) (string, error) { | |
switch dir, file := filepath.Split(path); { | |
case dir == "": | |
newpath, _, err := walkLink(file, root, linksWalked) | |
return newpath, err | |
case file == "": | |
if isDriveLetter(dir) { | |
return dir, nil | |
} | |
if os.IsPathSeparator(dir[len(dir)-1]) { | |
if isRoot(dir) { | |
return dir, nil | |
} | |
return walkLinks(dir[:len(dir)-1], root, linksWalked) | |
} | |
newpath, _, err := walkLink(dir, root, linksWalked) | |
return newpath, err | |
default: | |
newdir, err := walkLinks(dir, root, linksWalked) | |
if err != nil { | |
return "", err | |
} | |
newpath, islink, err := walkLink(filepath.Join(newdir, file), root, linksWalked) | |
if err != nil { | |
return "", err | |
} | |
if !islink { | |
return newpath, nil | |
} | |
if filepath.IsAbs(newpath) || os.IsPathSeparator(newpath[0]) { | |
return newpath, nil | |
} | |
return filepath.Join(newdir, newpath), nil | |
} | |
} | |
func walkLink(path string, root string, linksWalked *int) (newpath string, islink bool, err error) { | |
if *linksWalked > 255 { | |
return "", false, errors.New("EvalSymlinks: too many links") | |
} | |
fi, err := os.Lstat(filepath.Join(root, path)) | |
if err != nil { | |
return "", false, err | |
} | |
if fi.Mode()&os.ModeSymlink == 0 { | |
return path, false, nil | |
} | |
newpath, err = os.Readlink(filepath.Join(root, path)) | |
if err != nil { | |
return "", false, err | |
} | |
*linksWalked++ | |
return newpath, true, nil | |
} | |
// volumeNameLen returns length of the leading volume name on Windows. | |
// It returns 0 elsewhere. | |
func volumeNameLen(path string) int { | |
return 0 | |
} | |
// isDriveLetter returns true if path is Windows drive letter (like "c:"). | |
func isDriveLetter(path string) bool { | |
if runtime.GOOS != "windows" { | |
return false | |
} | |
return len(path) == 2 && path[1] == ':' | |
} | |
// isRoot returns true if path is root of file system | |
// (`/` on unix and `/`, `\`, `c:\` or `c:/` on windows). | |
func isRoot(path string) bool { | |
if runtime.GOOS != "windows" { | |
return path == "/" | |
} | |
switch len(path) { | |
case 1: | |
return os.IsPathSeparator(path[0]) | |
case 3: | |
return path[1] == ':' && os.IsPathSeparator(path[2]) | |
} | |
return false | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package filepath | |
import ( | |
"os" | |
"reflect" | |
"syscall" | |
"testing" | |
) | |
// run setup.sh on the host before testing | |
// TODO prepare test environmnet automatically | |
func TestEvalSymlinksAtRoot(t *testing.T) { | |
testcases := []struct { | |
name string | |
path string | |
expectedPath string | |
expectedErr error | |
}{ | |
{ | |
"one-level-link", | |
"/tmp/foo/bar", | |
"/tmp/foo/baz", | |
nil, | |
}, | |
{ | |
"two-level-link", | |
"/tmp/foo/twolevel", | |
"/tmp/foo/baz", | |
nil, | |
}, | |
{ | |
"link-to-invalid-path", | |
"/tmp/foo/invalid", | |
"", | |
&os.PathError{Op: "lstat", Path: "/rootfs/does-not-exist", Err: syscall.ENOENT}, | |
}, | |
{ | |
"link-to-path-which-exists-in-caller-fs", | |
"/tmp/foo/rootfs", | |
"", | |
&os.PathError{Op: "lstat", Path: "/rootfs/tmp/foo/rootfs", Err: syscall.ENOENT}, | |
}, | |
} | |
for _, v := range testcases { | |
path, err := EvalSymlinksAtRoot(v.path, "/rootfs") | |
if path != v.expectedPath { | |
t.Errorf("test %s: expected path %v, got %v", v.name, v.expectedPath, path) | |
} | |
if !reflect.DeepEqual(err, v.expectedErr) { | |
t.Errorf("test %s: expected error %#v, got %#v", v.name, v.expectedErr, err) | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/bin/bash | |
# | |
# Setup paths in host before testing: | |
rm -r /tmp/foo | |
mkdir -p /tmp/foo | |
touch /tmp/foo/baz | |
# one level link | |
ln -fs /tmp/foo/baz /tmp/foo/bar | |
# two level link | |
ln -fs /tmp/foo/bar /tmp/foo/twolevel | |
# symbolic link to invalid path | |
ln -fs /does-not-exist /tmp/foo/invalid | |
# symbolic link to invalid path but exist in caller filesystem | |
ln -fs /rootfs /tmp/foo/rootfs |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment