diff options
author | Alex Brainman <alex.brainman@gmail.com> | 2017-08-09 11:33:40 +1000 |
---|---|---|
committer | Alex Brainman <alex.brainman@gmail.com> | 2017-10-05 04:16:00 +0000 |
commit | 66c03d39f3aa65ec522c41e56c569391786539a7 (patch) | |
tree | a9951c90ce45fecb2cd9dbd2cc50c8f0419bf159 /src/path | |
parent | 56462d0f10f4d88f30e0b9a6763835c85c3cd632 (diff) | |
download | go-66c03d39f3aa65ec522c41e56c569391786539a7.tar.gz go-66c03d39f3aa65ec522c41e56c569391786539a7.zip |
path/filepath: re-implement windows EvalSymlinks
CL 41834 used approach suggested by Raymond Chen in
https://blogs.msdn.microsoft.com/oldnewthing/20100212-00/?p=14963/
to implement os.Stat by getting Windows I/O manager
follow symbolic links.
Do the same for filepath.EvalSymlinks, when existing
strategy fails.
Updates #19922
Fixes #20506
Change-Id: I15f3d3a80256bae86ac4fb321fd8877e84d8834f
Reviewed-on: https://go-review.googlesource.com/55612
Reviewed-by: Ian Lance Taylor <iant@golang.org>
Diffstat (limited to 'src/path')
-rw-r--r-- | src/path/filepath/path_windows_test.go | 34 | ||||
-rw-r--r-- | src/path/filepath/symlink_windows.go | 97 |
2 files changed, 129 insertions, 2 deletions
diff --git a/src/path/filepath/path_windows_test.go b/src/path/filepath/path_windows_test.go index d1b89bbc71..2ec5f5ef44 100644 --- a/src/path/filepath/path_windows_test.go +++ b/src/path/filepath/path_windows_test.go @@ -516,3 +516,37 @@ func TestWalkDirectorySymlink(t *testing.T) { testenv.MustHaveSymlink(t) testWalkMklink(t, "D") } + +func TestNTNamespaceSymlink(t *testing.T) { + output, _ := exec.Command("cmd", "/c", "mklink", "/?").Output() + if !strings.Contains(string(output), " /J ") { + t.Skip("skipping test because mklink command does not support junctions") + } + + tmpdir, err := ioutil.TempDir("", "TestNTNamespaceSymlink") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(tmpdir) + + vol := filepath.VolumeName(tmpdir) + output, err = exec.Command("cmd", "/c", "mountvol", vol, "/L").CombinedOutput() + if err != nil { + t.Fatalf("failed to run mountvol %v /L: %v %q", vol, err, output) + } + target := strings.Trim(string(output), " \n\r") + + link := filepath.Join(tmpdir, "link") + output, err = exec.Command("cmd", "/c", "mklink", "/J", link, target).CombinedOutput() + if err != nil { + t.Fatalf("failed to run mklink %v %v: %v %q", link, target, err, output) + } + + got, err := filepath.EvalSymlinks(link) + if err != nil { + t.Fatal(err) + } + if want := vol + `\`; got != want { + t.Errorf(`EvalSymlinks(%q): got %q, want %q`, link, got, want) + } +} diff --git a/src/path/filepath/symlink_windows.go b/src/path/filepath/symlink_windows.go index f771fe3a8a..78cde4aa09 100644 --- a/src/path/filepath/symlink_windows.go +++ b/src/path/filepath/symlink_windows.go @@ -5,6 +5,9 @@ package filepath import ( + "errors" + "internal/syscall/windows" + "os" "strings" "syscall" ) @@ -106,10 +109,100 @@ func toNorm(path string, normBase func(string) (string, error)) (string, error) return volume + normPath, nil } +// evalSymlinksUsingGetFinalPathNameByHandle uses Windows +// GetFinalPathNameByHandle API to retrieve the final +// path for the specified file. +func evalSymlinksUsingGetFinalPathNameByHandle(path string) (string, error) { + err := windows.LoadGetFinalPathNameByHandle() + if err != nil { + // we must be using old version of Windows + return "", err + } + + if path == "" { + return path, nil + } + + // Use Windows I/O manager to dereference the symbolic link, as per + // https://blogs.msdn.microsoft.com/oldnewthing/20100212-00/?p=14963/ + p, err := syscall.UTF16PtrFromString(path) + if err != nil { + return "", err + } + h, err := syscall.CreateFile(p, 0, 0, nil, + syscall.OPEN_EXISTING, syscall.FILE_FLAG_BACKUP_SEMANTICS, 0) + if err != nil { + return "", err + } + defer syscall.CloseHandle(h) + + buf := make([]uint16, 100) + for { + n, err := windows.GetFinalPathNameByHandle(h, &buf[0], uint32(len(buf)), windows.VOLUME_NAME_DOS) + if err != nil { + return "", err + } + if n < uint32(len(buf)) { + break + } + buf = make([]uint16, n) + } + s := syscall.UTF16ToString(buf) + if len(s) > 4 && s[:4] == `\\?\` { + s = s[4:] + if len(s) > 3 && s[:3] == `UNC` { + // return path like \\server\share\... + return `\` + s[3:], nil + } + return s, nil + } + return "", errors.New("GetFinalPathNameByHandle returned unexpected path=" + s) +} + +func samefile(path1, path2 string) bool { + fi1, err := os.Lstat(path1) + if err != nil { + return false + } + fi2, err := os.Lstat(path2) + if err != nil { + return false + } + return os.SameFile(fi1, fi2) +} + func evalSymlinks(path string) (string, error) { - path, err := walkSymlinks(path) + newpath, err := walkSymlinks(path) + if err != nil { + newpath2, err2 := evalSymlinksUsingGetFinalPathNameByHandle(path) + if err2 == nil { + return toNorm(newpath2, normBase) + } + return "", err + } + newpath, err = toNorm(newpath, normBase) if err != nil { + newpath2, err2 := evalSymlinksUsingGetFinalPathNameByHandle(path) + if err2 == nil { + return toNorm(newpath2, normBase) + } return "", err } - return toNorm(path, normBase) + if strings.ToUpper(newpath) == strings.ToUpper(path) { + // walkSymlinks did not actually walk any symlinks, + // so we don't need to try GetFinalPathNameByHandle. + return newpath, nil + } + newpath2, err2 := evalSymlinksUsingGetFinalPathNameByHandle(path) + if err2 != nil { + return newpath, nil + } + newpath2, err2 = toNorm(newpath2, normBase) + if err2 != nil { + return newpath, nil + } + if samefile(newpath, newpath2) { + return newpath, nil + } + return newpath2, nil } |