aboutsummaryrefslogtreecommitdiff
path: root/src/path
diff options
context:
space:
mode:
authorAlex Brainman <alex.brainman@gmail.com>2017-08-09 11:33:40 +1000
committerAlex Brainman <alex.brainman@gmail.com>2017-10-05 04:16:00 +0000
commit66c03d39f3aa65ec522c41e56c569391786539a7 (patch)
treea9951c90ce45fecb2cd9dbd2cc50c8f0419bf159 /src/path
parent56462d0f10f4d88f30e0b9a6763835c85c3cd632 (diff)
downloadgo-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.go34
-rw-r--r--src/path/filepath/symlink_windows.go97
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
}