lib/fs: Fallback EvalSymlinks method on windows (fixes #5609) (#5611)

This commit is contained in:
Mingxuan Lin 2019-06-10 14:33:53 +02:00 committed by Simon Frei
parent 1054ce9354
commit eb4fe808c5
2 changed files with 109 additions and 1 deletions

View File

@ -211,6 +211,63 @@ func isMaybeWin83(absPath string) bool {
return strings.Contains(strings.TrimPrefix(filepath.Base(absPath), WindowsTempPrefix), "~")
}
func getFinalPathName(in string) (string, error) {
// Return the normalized path
// Wrap the call to GetFinalPathNameByHandleW
// The string returned by this function uses the \?\ syntax
// Implies GetFullPathName + GetLongPathName
kernel32, err := syscall.LoadDLL("kernel32.dll")
if err != nil {
return "", err
}
GetFinalPathNameByHandleW, err := kernel32.FindProc("GetFinalPathNameByHandleW")
// https://github.com/golang/go/blob/ff048033e4304898245d843e79ed1a0897006c6d/src/internal/syscall/windows/syscall_windows.go#L303
if err != nil {
return "", err
}
inPath, err := syscall.UTF16PtrFromString(in)
if err != nil {
return "", err
}
// Get a file handler
h, err := syscall.CreateFile(inPath,
syscall.GENERIC_READ,
syscall.FILE_SHARE_READ,
nil,
syscall.OPEN_EXISTING,
uint32(syscall.FILE_FLAG_BACKUP_SEMANTICS),
0)
if err != nil {
return "", err
}
defer syscall.CloseHandle(h)
// Call GetFinalPathNameByHandleW
var VOLUME_NAME_DOS uint32 = 0x0 // not yet defined in syscall
var bufSize uint32 = syscall.MAX_PATH // 260
for i := 0; i < 2; i++ {
buf := make([]uint16, bufSize)
var ret uintptr
ret, _, err = GetFinalPathNameByHandleW.Call(
uintptr(h), // HANDLE hFile
uintptr(unsafe.Pointer(&buf[0])), // LPWSTR lpszFilePath
uintptr(bufSize), // DWORD cchFilePath
uintptr(VOLUME_NAME_DOS), // DWORD dwFlags
)
// The returned value is the actual length of the norm path
// After Win 10 build 1607, MAX_PATH limitations have been removed
// so it is necessary to check newBufSize
newBufSize := uint32(ret) + 1
if ret == 0 || newBufSize > bufSize*100 {
break
}
if newBufSize <= bufSize {
return syscall.UTF16ToString(buf), nil
}
bufSize = newBufSize
}
return "", err
}
func evalSymlinks(in string) (string, error) {
out, err := filepath.EvalSymlinks(in)
if err != nil && strings.HasPrefix(in, `\\?\`) {
@ -218,7 +275,19 @@ func evalSymlinks(in string) (string, error) {
out, err = filepath.EvalSymlinks(in[4:])
}
if err != nil {
return "", err
// Try to get a normalized path from Win-API
var err1 error
out, err1 = getFinalPathName(in)
if err1 != nil {
return "", err // return the prior error
}
// Trim UNC prefix, equivalent to
// https://github.com/golang/go/blob/2396101e0590cb7d77556924249c26af0ccd9eff/src/os/file_windows.go#L470
if strings.HasPrefix(out, `\\?\UNC\`) {
out = `\` + out[7:] // path like \\server\share\...
} else {
out = strings.TrimPrefix(out, `\\?\`)
}
}
return longFilenameSupport(out), nil
}

View File

@ -139,3 +139,42 @@ func TestRelUnrootedCheckedWindows(t *testing.T) {
}
}
}
func TestGetFinalPath(t *testing.T) {
testCases := []struct {
input string
expectedPath string
eqToEvalSyml bool
ignoreMissing bool
}{
{`c:\`, `C:\`, true, false},
{`\\?\c:\`, `C:\`, false, false},
{`c:\wInDows\sYstEm32`, `C:\Windows\System32`, true, false},
{`c:\parent\child`, `C:\parent\child`, false, true},
}
for _, testCase := range testCases {
out, err := getFinalPathName(testCase.input)
if err != nil {
if testCase.ignoreMissing && os.IsNotExist(err) {
continue
}
t.Errorf("getFinalPathName failed at %q with error %s", testCase.input, err)
}
// Trim UNC prefix
if strings.HasPrefix(out, `\\?\UNC\`) {
out = `\` + out[7:]
} else {
out = strings.TrimPrefix(out, `\\?\`)
}
if out != testCase.expectedPath {
t.Errorf("getFinalPathName got wrong path: %q (expected %q)", out, testCase.expectedPath)
}
if testCase.eqToEvalSyml {
evlPath, err1 := filepath.EvalSymlinks(testCase.input)
if err1 != nil || out != evlPath {
t.Errorf("EvalSymlinks got different results %q %s", evlPath, err1)
}
}
}
}