Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions pkgs/path/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
## 1.9.2-wip

- `WindowsStyle.absolutePathToUri` (and therefore `toUri()`) now strips Windows
Extended-Length Path prefixes (`\\?\` and `\\?\UNC\`) instead of
throwing a `FormatException`. This aligns `path.toUri()` with
`Uri.file()` / `Uri.directory()` from `dart:io`.
- Make `Url` style better at recognizing schemes and authorities.
Only consider a path as being a schemed URL if it starts with a
valid scheme.
Expand Down
29 changes: 21 additions & 8 deletions pkgs/path/lib/src/style/windows.dart
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,24 @@ class WindowsStyle extends InternalStyle {

@override
Uri absolutePathToUri(String path) {
// Strip Windows Extended-Length Path prefixes so that `toUri()` aligns
// with `Uri.file()` / `Uri.directory()` from `dart:io`.
//
// `\\?\C:\path` -> `C:\path`
// `\\?\UNC\server\share` -> `\\server\share`
//
// The `\\?\` prefix is a Win32 API artifact (enables paths > 260 chars
// and disables DOS device-name interpretation). It is not user intent and
// 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\', r'\\?\'.length)) {
path = path.replaceRange(0, r'\\?\UNC'.length, r'\');
} else {
path = path.substring(r'\\?\'.length);
}
}

final parsed = ParsedPath.parse(path, this);
if (parsed.root!.startsWith(r'\\')) {
// Network paths become "file://server/share/path/to/file".
Expand All @@ -119,10 +137,7 @@ class WindowsStyle extends InternalStyle {
}

return Uri(
scheme: 'file',
host: rootParts.first,
pathSegments: parsed.parts,
);
scheme: 'file', host: rootParts.first, pathSegments: parsed.parts);
} else {
// Drive-letter paths become "file:///C:/path/to/file".

Expand All @@ -136,10 +151,8 @@ class WindowsStyle extends InternalStyle {

// Get rid of the trailing "\" in "C:\" because the URI constructor will
// add a separator on its own.
parsed.parts.insert(
0,
parsed.root!.replaceAll('/', '').replaceAll('\\', ''),
);
parsed.parts
.insert(0, parsed.root!.replaceAll('/', '').replaceAll('\\', ''));

return Uri(scheme: 'file', pathSegments: parsed.parts);
}
Expand Down
25 changes: 25 additions & 0 deletions pkgs/path/test/windows_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -1058,6 +1058,31 @@ void main() {
Uri.parse('_%7B_%7D_%60_%5E_%20_%22_%25_'),
);
expect(context.toUri(''), Uri.parse(''));

// Windows Extended-Length Path prefixes are stripped to align with
// `Uri.file()` from `dart:io`. See:
// https://learn.microsoft.com/en-us/windows/win32/fileio/naming-a-file#win32-file-namespaces
expect(
context.toUri(r'\\?\UNC\server\share\path\to\foo'),
Uri.parse('file://server/share/path/to/foo'),
);
expect(
context.toUri(r'\\?\UNC\wsl.localhost\Ubuntu-24.04\tmp\foo'),
Uri.parse('file://wsl.localhost/Ubuntu-24.04/tmp/foo'),
);
expect(
context.toUri(r'\\?\UNC\server\share'),
Uri.parse('file://server/share'),
);
expect(
context.toUri(r'\\?\UNC\server\share\'),
Uri.parse('file://server/share/'),
);
expect(
context.toUri(r'\\?\C:\path\to\foo'),
Uri.parse('file:///C:/path/to/foo'),
);
expect(context.toUri(r'\\?\C:\'), Uri.parse('file:///C:/'));
});

group('prettyUri', () {
Expand Down
Loading