Skip to content

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

Open

Description

@berial

Summary

package:path's WindowsStyle.absolutePathToUri throws FormatException when given a Windows Extended-Length UNC path of the form \\?\UNC\server\share\.... These paths are returned by Directory.resolveSymbolicLinksSync() (which calls the Windows API GetFinalPathNameByHandle) for any UNC path, and are also the documented way to bypass MAX_PATH limits on Windows.

Note: this is not the root cause of Analysis Server failing on WSL projects (that's analyzer's _PhysicalResource.exists missing try-catch, filed separately). But it is a real bug that affects any code calling path.toUri() on a resolveSymbolicLinksSync() result.

Reproduction

pubspec.yaml:

name: test_path_unc
environment:
  sdk: ^3.0.0
dependencies:
  path: ^1.9.0

test.dart:

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

void main() {
  final resolved = Directory.current.resolveSymbolicLinksSync();
  print('resolved: $resolved');

  final uri = p.toUri(resolved);   // <- throws
  print('uri: $uri');
}

Run from a UNC/mapped-drive directory:

cd W:\tmp\test_path_unc   (W: -> \\wsl.localhost\Ubuntu-24.04)
dart run test.dart

Actual output

resolved: \\?\UNC\wsl.localhost\Ubuntu-24.04\tmp\test_path_unc

FormatException: Invalid character (at character 1)
?
^

#0  _Uri._fail (dart:core/uri.dart:2050:5)
#1  _Uri._normalizeRegName (dart:core/uri.dart:2598:9)
#2  _Uri._makeHost (dart:core/uri.dart:2453:12)
#3  new _Uri (dart:core/uri.dart:1955:12)
#4  WindowsStyle.absolutePathToUri (package:path/src/style/windows.dart:121:14)
#5  Context.toUri (package:path/src/context.dart:1062:20)
#6  toUri (package:path/path.dart:459:35)

Boundary test

Input Result
\\?\UNC\wsl.localhost\Ubuntu-24.04\home\berial FAIL
\\?\UNC\localhost\c$\Users FAIL
\\?\UNC\127.0.0.1\c$\Users FAIL
\\?\UNC\someserver\share\path FAIL (even non-existent server)
\\wsl.localhost\Ubuntu-24.04\home\berial OK
\\localhost\c$\Users OK
\\someserver\share\path OK
W:\home\berial OK

So the bug is not WSL-specific — it affects all Windows Extended UNC paths (\\?\UNC\*).

Note: Uri.file() (from dart:core) handles \\?\UNC\... correctly (strips the prefix), so this is specifically a package:path issue, not a dart:core issue.

Root cause

pkgs/path/lib/src/style/windows.dart:29:

final rootPattern = RegExp(r'^(\\\\[^\\]+\\[^\\/]+|[a-zA-Z]:[/\\])');

The first alternative \\\\[^\\]+\\[^\\/]+ is meant to match \\server\share, but it also matches \\?\UNC because [^\\]+ happily matches ?.

Then in absolutePathToUri (line 121):

final rootParts = parsed.root!.split('\\').where((part) => part != '');
parsed.parts.insert(0, rootParts.last);
// ...
return Uri(scheme: 'file', host: rootParts.first, pathSegments: parsed.parts);
  • parsed.root = \\?\UNC
  • split('\\') → ['', '', '?', 'UNC']
  • where((part) => part != '') → ['?', 'UNC']
  • rootParts.first = '?'
  • Uri(host: '?') → FormatException (? is not a valid URI host)

Suggested fix

Either strip the \\?\ / \\?\UNC\ prefix before parsing, or refine the rootPattern to not match \\?\:

// Option A: strip extended-length prefix in absolutePathToUri
Uri absolutePathToUri(String path) {
  if (path.startsWith(r'\\?\UNC\\')) {
    path = r'\\' + path.substring(8);
  } else if (path.startsWith(r'\\?\')) {
    path = path.substring(4);
  }
  // ... existing logic
}
// Option B: refine rootPattern
final rootPattern = RegExp(r'^(\\\\[^\\?][^\\]*\\[^\\/]+|[a-zA-Z]:[/\\])');

Environment

  • Dart SDK: 3.11.5 (stable) on windows_x64
  • package:path: 1.9.1 (also confirmed main branch is affected)
  • OS: Windows 11 + WSL2 (Ubuntu-24.04)

Activity

  1. lrhn commented on Jul 20, 2026

    @lrhn
    Member

    Windows paths are ... complicated.
    First thing to decide is always what the intended behavior should be. When there are multiple paths that mean the same thing, the code recognize that they mean the same thing and give the same answer to all queries (that don't actually look for the syntactic difference).

    For a Windows path of \\?\UNC\wsl.localhost\Ubuntu-24.04\tmp\test_path_unc, what should the corresponding URI be.

    One option is to preserve the entire path: file:////?/UNC/wsl.localhost/Ubuntu-24.04/tmp/test_path_unc.
    Another is to recognize wsl.localhost as a host name, and ?\UNC\ as an ignorable prefix: file://wsl.localhost/Ubutut-24.04/tmp/test_path_unc.

    Either can work.
    I agree that it shouldn't throw a spurious error. It it's an error, it should be a deliberate one.

    (Usual reference for Windows paths: https://learn.microsoft.com/en-us/dotnet/standard/io/file-path-formats)

  2. berial commented on Jul 21, 2026

    @berial
    Author

    Hi @lrhn, thanks for the response.

    I'd recommend option B (recognize wsl.localhost as host, strip ?\UNC\ prefix):

    \\?\UNC\wsl.localhost\Ubuntu-24.04\tmp\foo
      → file://wsl.localhost/Ubuntu-24.04/tmp/foo
    

    Rationale:

    1. Consistency with Uri.file() — Dart SDK's Uri.file() already does this. Uri.file(r'\\?\UNC\wsl.localhost\Ubuntu-24.04\tmp\foo') correctly returns file://wsl.localhost/Ubuntu-24.04/tmp/foo without throwing. Having path.toUri() throw on the same input creates an inconsistency that bites real-world tools (e.g., analyzer internals).

    2. WSL paths are a real Windows use case — \\wsl.localhost\<distro>\... is how Windows natively exposes WSL filesystems. Users hit this path format whenever they cross the Windows/WSL boundary, which is increasingly common.

    3. Option A (file:////?/UNC/...) leaks implementation detail — the \\?\ prefix is a Windows API artifact (GetFinalPathNameByHandle always returns it for UNC paths), not user intent. Exposing it in URIs just pushes the problem downstream.

    Repro:

    import 'package:path/path.dart' as p;
    
    void main() {
      // Works fine
      print(Uri.file(r'\\?\UNC\wsl.localhost\Ubuntu-24.04\tmp\foo'));
      // → file://wsl.localhost/Ubuntu-24.04/tmp/foo
    
      // Throws FormatException
      print(p.toUri(r'\\?\UNC\wsl.localhost\Ubuntu-24.04\tmp\foo'));
      // → FormatException: Illegal character in path
    }

    Happy to submit a PR if you confirm the direction.

  3. lrhn commented on Jul 22, 2026

    @lrhn
    Member

    Makes sense. Windows is complicated, but that doesn't need to leak to the user.

    There are the path length limits, where \\?\UNC\C:\longpath doesn't have the same limit on length as C:\longpath, but that should be handled in the dart:io operations, not by the user. (If we pass in a potentially-too-long path on Windows, which isn't already UNC, prefix it with \\?\UNC in the call to CreateFile, not before.)
    Same if the Dart string path contains a reserved device name, then prefixing \\?\UNC should turn off that. Again something that can be done just before passing the path to Windows, the Dart path doesn't need to know that. (Or probably, always prefix with \\?\UNC unless the path already starts with \\?\.

    That would mean that a Dart File.path on Windows is an abstraction over the Windows file path system.
    It removes information, which may not always be what a user wants.

    We should be very clear, up-front, about which parts are removed, which are retained, and when that happens.
    Maybe we just need to see what Uri.fromFile does, and copy that.

    And then document it, because if we don't have a written specification of what's going on, we'll certainly be inconsistent.

  4. berial commented on Jul 23, 2026

    @berial
    Author

    I've opened #981 with a fix. All tests pass, and the CLA has been signed.

  5. lrhn commented on Jul 24, 2026

    @lrhn
    Member

    Change seems reasonable to me. (Not an expert, all I know I read on the interwebs.)

  6. added
    type-bugIncorrect behavior (everything from a crash to more subtle misbehavior)
    on Aug 13, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    type-bugIncorrect behavior (everything from a crash to more subtle misbehavior)

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions