From ee6516aa31958fcf3991615c0dec48d8103a61cd Mon Sep 17 00:00:00 2001 From: Simon Frei Date: Mon, 4 Jun 2018 13:41:03 +0200 Subject: [PATCH] lib/fs: Resolve 8.3 filenames from watcher (ref #3800) (#4975) --- lib/fs/basicfs_test.go | 36 +------------ lib/fs/basicfs_unix.go | 4 ++ lib/fs/basicfs_watch.go | 2 +- lib/fs/basicfs_windows.go | 37 ++++++++++++++ lib/fs/basicfs_windows_test.go | 92 ++++++++++++++++++++++++++++++++++ 5 files changed, 136 insertions(+), 35 deletions(-) create mode 100644 lib/fs/basicfs_windows_test.go diff --git a/lib/fs/basicfs_test.go b/lib/fs/basicfs_test.go index 0fe754c36..0d68d5890 100644 --- a/lib/fs/basicfs_test.go +++ b/lib/fs/basicfs_test.go @@ -12,12 +12,12 @@ import ( "path/filepath" "runtime" "sort" - "strings" "testing" "time" ) -func setup(t *testing.T) (Filesystem, string) { +func setup(t *testing.T) (*BasicFilesystem, string) { + t.Helper() dir, err := ioutil.TempDir("", "") if err != nil { t.Fatal(err) @@ -304,38 +304,6 @@ func TestUsage(t *testing.T) { } } -func TestWindowsPaths(t *testing.T) { - if runtime.GOOS != "windows" { - t.Skip("Not useful on non-Windows") - return - } - - testCases := []struct { - input string - expectedRoot string - expectedURI string - }{ - {`e:\`, `\\?\e:\`, `e:\`}, - {`\\?\e:\`, `\\?\e:\`, `e:\`}, - {`\\192.0.2.22\network\share`, `\\192.0.2.22\network\share`, `\\192.0.2.22\network\share`}, - } - - for _, testCase := range testCases { - fs := newBasicFilesystem(testCase.input) - if fs.root != testCase.expectedRoot { - t.Errorf("root %q != %q", fs.root, testCase.expectedRoot) - } - if fs.URI() != testCase.expectedURI { - t.Errorf("uri %q != %q", fs.URI(), testCase.expectedURI) - } - } - - fs := newBasicFilesystem(`relative\path`) - if fs.root == `relative\path` || !strings.HasPrefix(fs.root, "\\\\?\\") { - t.Errorf("%q == %q, expected absolutification", fs.root, `relative\path`) - } -} - func TestRooted(t *testing.T) { type testcase struct { root string diff --git a/lib/fs/basicfs_unix.go b/lib/fs/basicfs_unix.go index 5456cf4c5..322916f64 100644 --- a/lib/fs/basicfs_unix.go +++ b/lib/fs/basicfs_unix.go @@ -51,3 +51,7 @@ func (f *BasicFilesystem) Hide(name string) error { func (f *BasicFilesystem) Roots() ([]string, error) { return []string{"/"}, nil } + +func (f *BasicFilesystem) resolveWin83(absPath string) string { + return absPath +} diff --git a/lib/fs/basicfs_watch.go b/lib/fs/basicfs_watch.go index 31a76c537..19802b951 100644 --- a/lib/fs/basicfs_watch.go +++ b/lib/fs/basicfs_watch.go @@ -117,5 +117,5 @@ func (f *BasicFilesystem) unrootedChecked(absPath string) string { if !strings.HasPrefix(absPath, f.rootSymlinkEvaluated) { panic(fmt.Sprintf("bug: Notify backend is processing a change outside of the filesystem root: root==%v, rootSymEval==%v, path==%v", f.root, f.rootSymlinkEvaluated, absPath)) } - return f.unrootedSymlinkEvaluated(absPath) + return f.unrootedSymlinkEvaluated(f.resolveWin83(absPath)) } diff --git a/lib/fs/basicfs_windows.go b/lib/fs/basicfs_windows.go index c6eadaf67..44950ec0c 100644 --- a/lib/fs/basicfs_windows.go +++ b/lib/fs/basicfs_windows.go @@ -14,6 +14,7 @@ import ( "fmt" "os" "path/filepath" + "strings" "syscall" "unsafe" ) @@ -150,3 +151,39 @@ func (f *BasicFilesystem) Roots() ([]string, error) { return drives, nil } + +func (f *BasicFilesystem) resolveWin83(absPath string) string { + if !isMaybeWin83(absPath) { + return absPath + } + if in, err := syscall.UTF16FromString(absPath); err == nil { + out := make([]uint16, 4*len(absPath)) // *2 for UTF16 and *2 to double path length + if n, err := syscall.GetLongPathName(&in[0], &out[0], uint32(len(out))); err == nil { + if n <= uint32(len(out)) { + return syscall.UTF16ToString(out[:n]) + } + out = make([]uint16, n) + if _, err = syscall.GetLongPathName(&in[0], &out[0], n); err == nil { + return syscall.UTF16ToString(out) + } + } + } + // Failed getting the long path. Return the part of the path which is + // already a long path. + for absPath = filepath.Dir(absPath); strings.HasPrefix(absPath, f.rootSymlinkEvaluated); absPath = filepath.Dir(absPath) { + if !isMaybeWin83(absPath) { + return absPath + } + } + return f.rootSymlinkEvaluated +} + +func isMaybeWin83(absPath string) bool { + if !strings.Contains(absPath, "~") { + return false + } + if strings.Contains(filepath.Dir(absPath), "~") { + return true + } + return strings.Contains(strings.TrimPrefix(filepath.Base(absPath), WindowsTempPrefix), "~") +} diff --git a/lib/fs/basicfs_windows_test.go b/lib/fs/basicfs_windows_test.go new file mode 100644 index 000000000..9625c591f --- /dev/null +++ b/lib/fs/basicfs_windows_test.go @@ -0,0 +1,92 @@ +// Copyright (C) 2018 The Syncthing Authors. +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this file, +// You can obtain one at https://mozilla.org/MPL/2.0/. + +// +build windows + +package fs + +import ( + "os" + "path/filepath" + "strings" + "testing" +) + +func TestWindowsPaths(t *testing.T) { + testCases := []struct { + input string + expectedRoot string + expectedURI string + }{ + {`e:\`, `\\?\e:\`, `e:\`}, + {`\\?\e:\`, `\\?\e:\`, `e:\`}, + {`\\192.0.2.22\network\share`, `\\192.0.2.22\network\share`, `\\192.0.2.22\network\share`}, + } + + for _, testCase := range testCases { + fs := newBasicFilesystem(testCase.input) + if fs.root != testCase.expectedRoot { + t.Errorf("root %q != %q", fs.root, testCase.expectedRoot) + } + if fs.URI() != testCase.expectedURI { + t.Errorf("uri %q != %q", fs.URI(), testCase.expectedURI) + } + } + + fs := newBasicFilesystem(`relative\path`) + if fs.root == `relative\path` || !strings.HasPrefix(fs.root, "\\\\?\\") { + t.Errorf("%q == %q, expected absolutification", fs.root, `relative\path`) + } +} + +func TestResolveWindows83(t *testing.T) { + fs, dir := setup(t) + defer os.RemoveAll(dir) + + shortAbs, _ := fs.rooted("LFDATA~1") + long := "LFDataTool" + longAbs, _ := fs.rooted(long) + deleted, _ := fs.rooted(filepath.Join("foo", "LFDATA~1")) + notShort, _ := fs.rooted(filepath.Join("foo", "bar", "baz")) + + fd, err := fs.Create(long) + if err != nil { + t.Fatal(err) + } + fd.Close() + + if res := fs.resolveWin83(shortAbs); res != longAbs { + t.Errorf(`Resolving for 8.3 names of "%v" resulted in "%v", expected "%v"`, shortAbs, res, longAbs) + } + if res := fs.resolveWin83(deleted); res != filepath.Dir(deleted) { + t.Errorf(`Resolving for 8.3 names of "%v" resulted in "%v", expected "%v"`, deleted, res, filepath.Dir(deleted)) + } + if res := fs.resolveWin83(notShort); res != notShort { + t.Errorf(`Resolving for 8.3 names of "%v" resulted in "%v", expected "%v"`, notShort, res, notShort) + } +} + +func TestIsWindows83(t *testing.T) { + fs, dir := setup(t) + defer os.RemoveAll(dir) + + tempTop, _ := fs.rooted(TempName("baz")) + tempBelow, _ := fs.rooted(filepath.Join("foo", "bar", TempName("baz"))) + short, _ := fs.rooted(filepath.Join("LFDATA~1", TempName("baz"))) + tempAndShort, _ := fs.rooted(filepath.Join("LFDATA~1", TempName("baz"))) + + for _, f := range []string{tempTop, tempBelow} { + if isMaybeWin83(f) { + t.Errorf(`"%v" is not a windows 8.3 path"`, f) + } + } + + for _, f := range []string{short, tempAndShort} { + if !isMaybeWin83(f) { + t.Errorf(`"%v" is not a windows 8.3 path"`, f) + } + } +}