Skip to content

[path] Strip Windows Extended-Length Path prefixes in absolutePathToUri - #981

Open
berial wants to merge 4 commits into
dart-lang:mainfrom
berial:fix/path-windows-extended-unc
Open

[path] Strip Windows Extended-Length Path prefixes in absolutePathToUri#981
berial wants to merge 4 commits into
dart-lang:mainfrom
berial:fix/path-windows-extended-unc

Conversation

@berial

@berial berial commented Jul 23, 2026

Copy link
Copy Markdown

Problem

WindowsStyle.absolutePathToUri (and therefore toUri()) throws FormatException when given Windows Extended-Length Path prefixes:

  • \\?\C:\long\path
  • \\?\UNC\server\share\path

These prefixes are documented in Win32 File Namespaces and are increasingly common because:

  1. GetFinalPathNameByHandle always returns the \\?\UNC\ form for any UNC path
  2. WSL paths (\\wsl.localhost\<distro>\...) hit this format whenever Windows APIs normalize them
  3. Paths exceeding MAX_PATH (260 chars) require the \\?\ prefix

Repro

import 'package:path/path.dart' as p;

void main() {
  // Works fine — Uri.file() strips the prefix
  print(Uri.file(r'\\?\UNC\wsl.localhost\Ubuntu-24.04\tmp\foo'));
  // → file://wsl.localhost/Ubuntu-24.04/tmp/foo

  // Throws FormatException — path.toUri() does not strip the prefix
  print(p.toUri(r'\\?\UNC\wsl.localhost\Ubuntu-24.04\tmp\foo'));
  // → FormatException: Illegal character in path
}

Root cause

absolutePathToUri does not strip the \\?\ prefix before parsing. ParsedPath.parse then misinterprets \\?\UNC as a UNC root with ? as the server name, which is not a valid URI host — causing FormatException when passed to the Uri constructor.

This makes path.toUri() inconsistent with Uri.file() / Uri.directory() from dart: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() with Uri.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.fromFile does, and copy that."

Tests

Added 6 test cases to pkgs/path/test/windows_test.dart covering:

Input Expected URI
\\?\UNC\server\share\path\to\foo file://server/share/path/to/foo
\\?\UNC\wsl.localhost\Ubuntu-24.04\tmp\foo file://wsl.localhost/Ubuntu-24.04/tmp/foo
\\?\UNC\server\share file://server/share
\\?\UNC\server\share\ file://server/share/
\\?\C:\path\to\foo file:///C:/path/to/foo
\\?\C:\ file:///C:/

All existing tests + 6 new tests pass (68 total in windows_test.dart). dart analyze reports no issues.

Scope

This PR intentionally only fixes absolutePathToUri (the function reported in #980). Other methods like rootLength / split may also benefit from prefix stripping, but those are out of scope for this focused fix.

Fixes #980

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
@berial
berial requested a review from a team as a code owner July 23, 2026 06:24
@google-cla

google-cla Bot commented Jul 23, 2026

Copy link
Copy Markdown

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.

Comment thread pkgs/path/lib/src/style/windows.dart Outdated
// 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\')) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can be (path.startsWith(r'UNC\', r'\\?\'.length)) to not check the same chars again.

Comment thread pkgs/path/lib/src/style/windows.dart Outdated
// 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);

@lrhn lrhn Jul 24, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can be path = r'\' + path.substring(7);.
I'd probably do path = path.replaceRange(0, r'\\?\UNC'.length, r'\');.
(Creates fewer intermediate strings.)

Comment thread pkgs/path/lib/src/style/windows.dart Outdated
if (path.startsWith(r'\\?\UNC\')) {
path = r'\\' + path.substring(8);
} else {
path = path.substring(4);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

path = path.substring(r'\\?\'.length);

Comment thread pkgs/path/lib/src/style/windows.dart Outdated
if (path.startsWith(r'\\?\UNC\')) {
path = r'\\' + path.substring(8);
} else {
path = path.substring(4);

@lrhn lrhn Jul 24, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.)

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@natebosch
natebosch requested a review from lrhn July 27, 2026 18:21
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[path] \WindowsStyle.absolutePathToUri\ throws FormatException on Windows Extended-Length UNC paths

2 participants