|
| 1 | +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 |
| 2 | + |
| 3 | +Date: Tue, 12 Mar 2024 12:26:53 +0100 |
| 4 | +Subject: [PATCH] os: support UNC paths and .. segments in fixLongPath |
| 5 | +MIME-Version: 1.0 |
| 6 | +Content-Type: text/plain; charset=UTF-8 |
| 7 | +Content-Transfer-Encoding: 8bit |
| 8 | + |
| 9 | +This CL reimplements fixLongPath using syscall.GetFullPathName instead |
| 10 | +of a custom implementation that was not handling UNC paths and .. |
| 11 | +segments correctly. It also fixes a bug here multiple trailing \ |
| 12 | +were removed instead of replaced by a single one. |
| 13 | + |
| 14 | +The new implementation is slower than the previous one, as it does a |
| 15 | +syscall and needs to convert UTF-8 to UTF-16 (and back), but it is |
| 16 | +correct and should be fast enough for most use cases. |
| 17 | + |
| 18 | +goos: windows |
| 19 | +goarch: amd64 |
| 20 | +pkg: os |
| 21 | +cpu: Intel(R) Core(TM) i7-10850H CPU @ 2.70GHz |
| 22 | + │ old.txt │ new.txt │ |
| 23 | + │ sec/op │ sec/op vs base │ |
| 24 | +LongPath-12 1.007µ ± 53% 4.093µ ± 109% +306.41% (p=0.000 n=10) |
| 25 | + |
| 26 | + │ old.txt │ new.txt │ |
| 27 | + │ B/op │ B/op vs base │ |
| 28 | +LongPath-12 576.0 ± 0% 1376.0 ± 0% +138.89% (p=0.000 n=10) |
| 29 | + |
| 30 | + │ old.txt │ new.txt │ |
| 31 | + │ allocs/op │ allocs/op vs base │ |
| 32 | +LongPath-12 2.000 ± 0% 3.000 ± 0% +50.00% (p=0.000 n=10) |
| 33 | + |
| 34 | +Fixes #41734. |
| 35 | + |
| 36 | +Change-Id: Iced5cf47f56f6ab0ca74a6e2374c31a75100902d |
| 37 | +Reviewed-on: https://go-review.googlesource.com/c/go/+/570995 |
| 38 | +Reviewed-by: Damien Neil < [email protected]> |
| 39 | +TryBot-Result: Gopher Robot < [email protected]> |
| 40 | +Reviewed-by: David Chase < [email protected]> |
| 41 | +LUCI-TryBot-Result: Go LUCI < [email protected]> |
| 42 | +(cherry picked from commit 2e8d84f148c69404b8eec86d9149785a3f4e3e92) |
| 43 | +--- |
| 44 | + src/os/path_windows.go | 80 +++++++++++++++++-------------------- |
| 45 | + src/os/path_windows_test.go | 29 ++++++++++---- |
| 46 | + 2 files changed, 57 insertions(+), 52 deletions(-) |
| 47 | + |
| 48 | +diff --git a/src/os/path_windows.go b/src/os/path_windows.go |
| 49 | +index 052202514825ec..de654f1df9e4be 100644 |
| 50 | +--- a/src/os/path_windows.go |
| 51 | ++++ b/src/os/path_windows.go |
| 52 | +@@ -4,6 +4,10 @@ |
| 53 | + |
| 54 | + package os |
| 55 | + |
| 56 | ++import ( |
| 57 | ++ "syscall" |
| 58 | ++) |
| 59 | ++ |
| 60 | + const ( |
| 61 | + PathSeparator = '\\' // OS-specific path separator |
| 62 | + PathListSeparator = ';' // OS-specific path list separator |
| 63 | +@@ -134,10 +138,8 @@ var canUseLongPaths bool |
| 64 | + |
| 65 | + // fixLongPath returns the extended-length (\\?\-prefixed) form of |
| 66 | + // path when needed, in order to avoid the default 260 character file |
| 67 | +-// path limit imposed by Windows. If path is not easily converted to |
| 68 | +-// the extended-length form (for example, if path is a relative path |
| 69 | +-// or contains .. elements), or is short enough, fixLongPath returns |
| 70 | +-// path unmodified. |
| 71 | ++// path limit imposed by Windows. If the path is short enough or is relative, |
| 72 | ++// fixLongPath returns path unmodified. |
| 73 | + // |
| 74 | + // See https://learn.microsoft.com/en-us/windows/win32/fileio/naming-a-file#maximum-path-length-limitation |
| 75 | + func fixLongPath(path string) string { |
| 76 | +@@ -161,19 +163,8 @@ func fixLongPath(path string) string { |
| 77 | + return path |
| 78 | + } |
| 79 | + |
| 80 | +- // The extended form begins with \\?\, as in |
| 81 | +- // \\?\c:\windows\foo.txt or \\?\UNC\server\share\foo.txt. |
| 82 | +- // The extended form disables evaluation of . and .. path |
| 83 | +- // elements and disables the interpretation of / as equivalent |
| 84 | +- // to \. The conversion here rewrites / to \ and elides |
| 85 | +- // . elements as well as trailing or duplicate separators. For |
| 86 | +- // simplicity it avoids the conversion entirely for relative |
| 87 | +- // paths or paths containing .. elements. For now, |
| 88 | +- // \\server\share paths are not converted to |
| 89 | +- // \\?\UNC\server\share paths because the rules for doing so |
| 90 | +- // are less well-specified. |
| 91 | +- if len(path) >= 2 && path[:2] == `\\` { |
| 92 | +- // Don't canonicalize UNC paths. |
| 93 | ++ if prefix := path[:4]; prefix == `\\.\` || prefix == `\\?\` || prefix == `\??\` { |
| 94 | ++ // Don't fix. Device path or extended path form. |
| 95 | + return path |
| 96 | + } |
| 97 | + if !isAbs(path) { |
| 98 | +@@ -181,36 +172,37 @@ func fixLongPath(path string) string { |
| 99 | + return path |
| 100 | + } |
| 101 | + |
| 102 | +- const prefix = `\\?` |
| 103 | ++ var prefix []uint16 |
| 104 | ++ var isUNC bool |
| 105 | ++ if path[:2] == `\\` { |
| 106 | ++ // UNC path, prepend the \\?\UNC\ prefix. |
| 107 | ++ prefix = []uint16{'\\', '\\', '?', '\\', 'U', 'N', 'C', '\\'} |
| 108 | ++ isUNC = true |
| 109 | ++ } else { |
| 110 | ++ prefix = []uint16{'\\', '\\', '?', '\\'} |
| 111 | ++ } |
| 112 | + |
| 113 | +- pathbuf := make([]byte, len(prefix)+len(path)+len(`\`)) |
| 114 | +- copy(pathbuf, prefix) |
| 115 | +- n := len(path) |
| 116 | +- r, w := 0, len(prefix) |
| 117 | +- for r < n { |
| 118 | +- switch { |
| 119 | +- case IsPathSeparator(path[r]): |
| 120 | +- // empty block |
| 121 | +- r++ |
| 122 | +- case path[r] == '.' && (r+1 == n || IsPathSeparator(path[r+1])): |
| 123 | +- // /./ |
| 124 | +- r++ |
| 125 | +- case r+1 < n && path[r] == '.' && path[r+1] == '.' && (r+2 == n || IsPathSeparator(path[r+2])): |
| 126 | +- // /../ is currently unhandled |
| 127 | ++ p, err := syscall.UTF16FromString(path) |
| 128 | ++ if err != nil { |
| 129 | ++ return path |
| 130 | ++ } |
| 131 | ++ n := uint32(len(p)) |
| 132 | ++ var buf []uint16 |
| 133 | ++ for { |
| 134 | ++ buf = make([]uint16, n+uint32(len(prefix))) |
| 135 | ++ n, err = syscall.GetFullPathName(&p[0], n, &buf[len(prefix)], nil) |
| 136 | ++ if err != nil { |
| 137 | + return path |
| 138 | +- default: |
| 139 | +- pathbuf[w] = '\\' |
| 140 | +- w++ |
| 141 | +- for ; r < n && !IsPathSeparator(path[r]); r++ { |
| 142 | +- pathbuf[w] = path[r] |
| 143 | +- w++ |
| 144 | +- } |
| 145 | ++ } |
| 146 | ++ if n <= uint32(len(buf)-len(prefix)) { |
| 147 | ++ buf = buf[:n+uint32(len(prefix))] |
| 148 | ++ break |
| 149 | + } |
| 150 | + } |
| 151 | +- // A drive's root directory needs a trailing \ |
| 152 | +- if w == len(`\\?\c:`) { |
| 153 | +- pathbuf[w] = '\\' |
| 154 | +- w++ |
| 155 | ++ if isUNC { |
| 156 | ++ // Remove leading \\. |
| 157 | ++ buf = buf[2:] |
| 158 | + } |
| 159 | +- return string(pathbuf[:w]) |
| 160 | ++ copy(buf, prefix) |
| 161 | ++ return syscall.UTF16ToString(buf) |
| 162 | + } |
| 163 | +diff --git a/src/os/path_windows_test.go b/src/os/path_windows_test.go |
| 164 | +index 4e5e501d1f124c..1a18a53cae0212 100644 |
| 165 | +--- a/src/os/path_windows_test.go |
| 166 | ++++ b/src/os/path_windows_test.go |
| 167 | +@@ -16,10 +16,14 @@ import ( |
| 168 | + ) |
| 169 | + |
| 170 | + func TestFixLongPath(t *testing.T) { |
| 171 | +- if os.CanUseLongPaths { |
| 172 | +- return |
| 173 | +- } |
| 174 | +- t.Parallel() |
| 175 | ++ // Test fixLongPath even if long path are supported by the system, |
| 176 | ++ // else the function might not be tested at all when the test builders |
| 177 | ++ // support long paths. |
| 178 | ++ old := os.CanUseLongPaths |
| 179 | ++ os.CanUseLongPaths = false |
| 180 | ++ t.Cleanup(func() { |
| 181 | ++ os.CanUseLongPaths = old |
| 182 | ++ }) |
| 183 | + |
| 184 | + // 248 is long enough to trigger the longer-than-248 checks in |
| 185 | + // fixLongPath, but short enough not to make a path component |
| 186 | +@@ -38,19 +42,20 @@ func TestFixLongPath(t *testing.T) { |
| 187 | + // cases below where it doesn't. |
| 188 | + {`C:\long\foo.txt`, `\\?\C:\long\foo.txt`}, |
| 189 | + {`C:/long/foo.txt`, `\\?\C:\long\foo.txt`}, |
| 190 | +- {`C:\long\foo\\bar\.\baz\\`, `\\?\C:\long\foo\bar\baz`}, |
| 191 | +- {`\\unc\path`, `\\unc\path`}, |
| 192 | ++ {`C:\long\foo\\bar\.\baz\\`, `\\?\C:\long\foo\bar\baz\`}, |
| 193 | ++ {`\\server\path\long`, `\\?\UNC\server\path\long`}, |
| 194 | + {`long.txt`, `long.txt`}, |
| 195 | + {`C:long.txt`, `C:long.txt`}, |
| 196 | +- {`c:\long\..\bar\baz`, `c:\long\..\bar\baz`}, |
| 197 | ++ {`c:\long\..\bar\baz`, `\\?\c:\bar\baz`}, |
| 198 | + {`\\?\c:\long\foo.txt`, `\\?\c:\long\foo.txt`}, |
| 199 | + {`\\?\c:\long/foo.txt`, `\\?\c:\long/foo.txt`}, |
| 200 | ++ {`\??\c:\long/foo.txt`, `\??\c:\long/foo.txt`}, |
| 201 | + } { |
| 202 | + in := strings.ReplaceAll(test.in, "long", veryLong) |
| 203 | + want := strings.ReplaceAll(test.want, "long", veryLong) |
| 204 | + if got := os.FixLongPath(in); got != want { |
| 205 | + got = strings.ReplaceAll(got, veryLong, "long") |
| 206 | +- t.Errorf("fixLongPath(%q) = %q; want %q", test.in, got, test.want) |
| 207 | ++ t.Errorf("fixLongPath(%#q) = %#q; want %#q", test.in, got, test.want) |
| 208 | + } |
| 209 | + } |
| 210 | + } |
| 211 | +@@ -155,3 +160,11 @@ func TestMkdirAllVolumeNameAtRoot(t *testing.T) { |
| 212 | + volName := syscall.UTF16ToString(buf[:]) |
| 213 | + testMkdirAllAtRoot(t, volName) |
| 214 | + } |
| 215 | ++ |
| 216 | ++func BenchmarkLongPath(b *testing.B) { |
| 217 | ++ veryLong := `C:\l` + strings.Repeat("o", 248) + "ng" |
| 218 | ++ b.ReportAllocs() |
| 219 | ++ for i := 0; i < b.N; i++ { |
| 220 | ++ os.FixLongPath(veryLong) |
| 221 | ++ } |
| 222 | ++} |
0 commit comments