diff --git a/lib/fs/basicfs_windows.go b/lib/fs/basicfs_windows.go index d33b41a1a..cedf917d4 100644 --- a/lib/fs/basicfs_windows.go +++ b/lib/fs/basicfs_windows.go @@ -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 } diff --git a/lib/fs/basicfs_windows_test.go b/lib/fs/basicfs_windows_test.go index 343a5d75f..915b2c28e 100644 --- a/lib/fs/basicfs_windows_test.go +++ b/lib/fs/basicfs_windows_test.go @@ -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) + } + } + } +}