[path] Strip Windows Extended-Length Path prefixes in absolutePathToUri - #981
[path] Strip Windows Extended-Length Path prefixes in absolutePathToUri#981berial wants to merge 4 commits into
Conversation
Aligns `WindowsStyle.absolutePathToUri` (and therefore `toUri()`) with `Uri.file()` / `Uri.directory()` from `dart:io` by stripping Win32 Extended-Length Path prefixes before conversion: - `\\?\C:\path` -> `C:\path` -> `file:///C:/path` - `\\?\UNC\server\share` -> `\\server\share` -> `file://server/share` Previously, `toUri(r'\\?\UNC\wsl.localhost\Ubuntu-24.04\tmp\foo')` threw `FormatException` because the `\\?\UNC` prefix was misinterpreted as a UNC root with `?` as the host. This made `path.toUri()` inconsistent with `Uri.file()`, which already handles these prefixes correctly. Fixes dart-lang#980
|
Thanks for your pull request! It looks like this may be your first contribution to a Google open source project. Before we can look at your pull request, you'll need to sign a Contributor License Agreement (CLA). View this failed invocation of the CLA check for more information. For the most up to date status, view the checks section at the bottom of the pull request. |
| // should not leak into the resulting file URI. See: | ||
| // https://learn.microsoft.com/en-us/windows/win32/fileio/naming-a-file#win32-file-namespaces | ||
| if (path.startsWith(r'\\?\')) { | ||
| if (path.startsWith(r'\\?\UNC\')) { |
There was a problem hiding this comment.
Can be (path.startsWith(r'UNC\', r'\\?\'.length)) to not check the same chars again.
| // https://learn.microsoft.com/en-us/windows/win32/fileio/naming-a-file#win32-file-namespaces | ||
| if (path.startsWith(r'\\?\')) { | ||
| if (path.startsWith(r'\\?\UNC\')) { | ||
| path = r'\\' + path.substring(8); |
There was a problem hiding this comment.
Can be path = r'\' + path.substring(7);.
I'd probably do path = path.replaceRange(0, r'\\?\UNC'.length, r'\');.
(Creates fewer intermediate strings.)
| if (path.startsWith(r'\\?\UNC\')) { | ||
| path = r'\\' + path.substring(8); | ||
| } else { | ||
| path = path.substring(4); |
There was a problem hiding this comment.
path = path.substring(r'\\?\'.length);
| if (path.startsWith(r'\\?\UNC\')) { | ||
| path = r'\\' + path.substring(8); | ||
| } else { | ||
| path = path.substring(4); |
There was a problem hiding this comment.
This doesn't handle \\?\PIPE\.... We can probably assume that won't happen for any reasonable use, but I think it is a valid Win32 path. (Not entirely sure for what, probably named pipes. With more than 260 chars in the name!)
That is one argument for not necessarily considering \\?\UNC\ as unintended by the user. Maybe they know what they're doing. (But the common user don't, so maybe we just need a backdoor to allow preserving the path.)
There was a problem hiding this comment.
Agreed — \\?\PIPE\... is a Win32 named pipe, which lives in the device namespace, not the filesystem. Converting it to a file: URI is semantically meaningless since file: URIs only address filesystem resources. (Dart accesses named pipes via Socket/IPC, not File.)
With the current fix, \\?\PIPE\foo becomes \PIPE\foo after stripping the \\?\ prefix. Since a single leading backslash is not a UNC root, ParsedPath.parse treats it as a relative path — no throw, but also not a "real" pipe path. That's a degenerate case I think we can accept, since there's no correct file: URI representation to produce anyway.
If a user genuinely needs to preserve the \\?\ prefix (e.g., to opt back into the >260-char path semantics that dart:io handles at the Win32 boundary, as you noted), an opt-in preserveExtendedPrefix flag on toUri/absolutePathToUri would be the cleaner backdoor. But I'd leave that out of this PR unless you want it now — the current change just stops the spurious throw for the common filesystem cases (\\?\C:\... and \\?\UNC\...), which is what's biting real-world tools.
Problem
WindowsStyle.absolutePathToUri(and thereforetoUri()) throwsFormatExceptionwhen given Windows Extended-Length Path prefixes:\\?\C:\long\path\\?\UNC\server\share\pathThese prefixes are documented in Win32 File Namespaces and are increasingly common because:
GetFinalPathNameByHandlealways returns the\\?\UNC\form for any UNC path\\wsl.localhost\<distro>\...) hit this format whenever Windows APIs normalize themMAX_PATH(260 chars) require the\\?\prefixRepro
Root cause
absolutePathToUridoes not strip the\\?\prefix before parsing.ParsedPath.parsethen misinterprets\\?\UNCas a UNC root with?as the server name, which is not a valid URI host — causingFormatExceptionwhen passed to theUriconstructor.This makes
path.toUri()inconsistent withUri.file()/Uri.directory()fromdart:io, which already handle these prefixes correctly.Fix
Strip the Win32 Extended-Length Path prefix at the entry of
absolutePathToUri, before any parsing:\\?\UNC\server\share\...→\\server\share\...(strip\\?\UNC\, prepend\\)\\?\C:\path\...→C:\path\...(strip\\?\)This aligns
path.toUri()withUri.file()— the\\?\prefix is a Win32 API artifact (enables paths > 260 chars and disables DOS device-name interpretation), not user intent, and should not leak into the resulting file URI.Discussed in #980 — @lrhn confirmed the direction: "Maybe we just need to see what
Uri.fromFiledoes, and copy that."Tests
Added 6 test cases to
pkgs/path/test/windows_test.dartcovering:\\?\UNC\server\share\path\to\foofile://server/share/path/to/foo\\?\UNC\wsl.localhost\Ubuntu-24.04\tmp\foofile://wsl.localhost/Ubuntu-24.04/tmp/foo\\?\UNC\server\sharefile://server/share\\?\UNC\server\share\file://server/share/\\?\C:\path\to\foofile:///C:/path/to/foo\\?\C:\file:///C:/All existing tests + 6 new tests pass (68 total in
windows_test.dart).dart analyzereports no issues.Scope
This PR intentionally only fixes
absolutePathToUri(the function reported in #980). Other methods likerootLength/splitmay also benefit from prefix stripping, but those are out of scope for this focused fix.Fixes #980