Skip to content

Commit eb4fe80

Browse files
gdlmximsodin
authored andcommitted
lib/fs: Fallback EvalSymlinks method on windows (fixes syncthing#5609) (syncthing#5611)
1 parent 1054ce9 commit eb4fe80

File tree

2 files changed

+109
-1
lines changed

2 files changed

+109
-1
lines changed

lib/fs/basicfs_windows.go

Lines changed: 70 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -211,14 +211,83 @@ func isMaybeWin83(absPath string) bool {
211211
return strings.Contains(strings.TrimPrefix(filepath.Base(absPath), WindowsTempPrefix), "~")
212212
}
213213

214+
func getFinalPathName(in string) (string, error) {
215+
// Return the normalized path
216+
// Wrap the call to GetFinalPathNameByHandleW
217+
// The string returned by this function uses the \?\ syntax
218+
// Implies GetFullPathName + GetLongPathName
219+
kernel32, err := syscall.LoadDLL("kernel32.dll")
220+
if err != nil {
221+
return "", err
222+
}
223+
GetFinalPathNameByHandleW, err := kernel32.FindProc("GetFinalPathNameByHandleW")
224+
// https://github.com/golang/go/blob/ff048033e4304898245d843e79ed1a0897006c6d/src/internal/syscall/windows/syscall_windows.go#L303
225+
if err != nil {
226+
return "", err
227+
}
228+
inPath, err := syscall.UTF16PtrFromString(in)
229+
if err != nil {
230+
return "", err
231+
}
232+
// Get a file handler
233+
h, err := syscall.CreateFile(inPath,
234+
syscall.GENERIC_READ,
235+
syscall.FILE_SHARE_READ,
236+
nil,
237+
syscall.OPEN_EXISTING,
238+
uint32(syscall.FILE_FLAG_BACKUP_SEMANTICS),
239+
0)
240+
if err != nil {
241+
return "", err
242+
}
243+
defer syscall.CloseHandle(h)
244+
// Call GetFinalPathNameByHandleW
245+
var VOLUME_NAME_DOS uint32 = 0x0 // not yet defined in syscall
246+
var bufSize uint32 = syscall.MAX_PATH // 260
247+
for i := 0; i < 2; i++ {
248+
buf := make([]uint16, bufSize)
249+
var ret uintptr
250+
ret, _, err = GetFinalPathNameByHandleW.Call(
251+
uintptr(h), // HANDLE hFile
252+
uintptr(unsafe.Pointer(&buf[0])), // LPWSTR lpszFilePath
253+
uintptr(bufSize), // DWORD cchFilePath
254+
uintptr(VOLUME_NAME_DOS), // DWORD dwFlags
255+
)
256+
// The returned value is the actual length of the norm path
257+
// After Win 10 build 1607, MAX_PATH limitations have been removed
258+
// so it is necessary to check newBufSize
259+
newBufSize := uint32(ret) + 1
260+
if ret == 0 || newBufSize > bufSize*100 {
261+
break
262+
}
263+
if newBufSize <= bufSize {
264+
return syscall.UTF16ToString(buf), nil
265+
}
266+
bufSize = newBufSize
267+
}
268+
return "", err
269+
}
270+
214271
func evalSymlinks(in string) (string, error) {
215272
out, err := filepath.EvalSymlinks(in)
216273
if err != nil && strings.HasPrefix(in, `\\?\`) {
217274
// Try again without the `\\?\` prefix
218275
out, err = filepath.EvalSymlinks(in[4:])
219276
}
220277
if err != nil {
221-
return "", err
278+
// Try to get a normalized path from Win-API
279+
var err1 error
280+
out, err1 = getFinalPathName(in)
281+
if err1 != nil {
282+
return "", err // return the prior error
283+
}
284+
// Trim UNC prefix, equivalent to
285+
// https://github.com/golang/go/blob/2396101e0590cb7d77556924249c26af0ccd9eff/src/os/file_windows.go#L470
286+
if strings.HasPrefix(out, `\\?\UNC\`) {
287+
out = `\` + out[7:] // path like \\server\share\...
288+
} else {
289+
out = strings.TrimPrefix(out, `\\?\`)
290+
}
222291
}
223292
return longFilenameSupport(out), nil
224293
}

lib/fs/basicfs_windows_test.go

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,3 +139,42 @@ func TestRelUnrootedCheckedWindows(t *testing.T) {
139139
}
140140
}
141141
}
142+
143+
func TestGetFinalPath(t *testing.T) {
144+
testCases := []struct {
145+
input string
146+
expectedPath string
147+
eqToEvalSyml bool
148+
ignoreMissing bool
149+
}{
150+
{`c:\`, `C:\`, true, false},
151+
{`\\?\c:\`, `C:\`, false, false},
152+
{`c:\wInDows\sYstEm32`, `C:\Windows\System32`, true, false},
153+
{`c:\parent\child`, `C:\parent\child`, false, true},
154+
}
155+
156+
for _, testCase := range testCases {
157+
out, err := getFinalPathName(testCase.input)
158+
if err != nil {
159+
if testCase.ignoreMissing && os.IsNotExist(err) {
160+
continue
161+
}
162+
t.Errorf("getFinalPathName failed at %q with error %s", testCase.input, err)
163+
}
164+
// Trim UNC prefix
165+
if strings.HasPrefix(out, `\\?\UNC\`) {
166+
out = `\` + out[7:]
167+
} else {
168+
out = strings.TrimPrefix(out, `\\?\`)
169+
}
170+
if out != testCase.expectedPath {
171+
t.Errorf("getFinalPathName got wrong path: %q (expected %q)", out, testCase.expectedPath)
172+
}
173+
if testCase.eqToEvalSyml {
174+
evlPath, err1 := filepath.EvalSymlinks(testCase.input)
175+
if err1 != nil || out != evlPath {
176+
t.Errorf("EvalSymlinks got different results %q %s", evlPath, err1)
177+
}
178+
}
179+
}
180+
}

0 commit comments

Comments
 (0)