diff --git a/pkgs/path/CHANGELOG.md b/pkgs/path/CHANGELOG.md index b309d17a..38006305 100644 --- a/pkgs/path/CHANGELOG.md +++ b/pkgs/path/CHANGELOG.md @@ -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. diff --git a/pkgs/path/lib/src/style/windows.dart b/pkgs/path/lib/src/style/windows.dart index 14c2d8bd..246809ec 100644 --- a/pkgs/path/lib/src/style/windows.dart +++ b/pkgs/path/lib/src/style/windows.dart @@ -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". @@ -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". @@ -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); } diff --git a/pkgs/path/test/windows_test.dart b/pkgs/path/test/windows_test.dart index 88ceb3a0..c73098d2 100644 --- a/pkgs/path/test/windows_test.dart +++ b/pkgs/path/test/windows_test.dart @@ -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', () {