Infer Windows UNC paths with forward and mixed slashes (#49424)

## Why

`LegacyAppPathString` treated `//server/share/project` as POSIX, losing its UNC interpretation when inferring a `PathUri`.

## What changed

Infer Windows syntax whenever an API path starts with two separators, including forward and mixed slashes, independently of the host. For example, `//server/share/project` now converts to `file://server/share/project`. Double-slash POSIX paths require an explicit `PathConvention::Posix`; inferred paths still undergo validation.

## Testing

Add regression tests for UNC and namespace paths, mixed separators, URI serialization round trips, explicit POSIX conversion, convention mismatches, and malformed UNC paths.

GitOrigin-RevId: 720893de517addb7a938e6784d1ed0f05df3ec0d
This commit is contained in:
Dmitriy Kovalenko
2026-09-30 01:28:02 +00:00
committed by copyberry
parent ab84d71f57
commit d1d0e89558
2 changed files with 71 additions and 5 deletions
+11 -4
View File
@@ -162,9 +162,11 @@ impl LegacyAppPathString {
/// Infers the path convention of an absolute API path from its spelling.
///
/// Relative paths and ambiguous spellings return `None`. In particular,
/// slash-prefixed paths are treated as POSIX even when they could also be
/// interpreted as slash-delimited Windows UNC paths.
/// Two leading separators select Windows UNC or namespace syntax, including
/// forward and mixed slashes, independently of the current host. This favors
/// UNC paths over ambiguous double-slash POSIX paths. Call [`Self::to_path_uri`]
/// with an explicit POSIX convention to preserve that interpretation.
/// Relative paths return `None`; inferred prefixes still require validation.
pub fn infer_absolute_path_convention(&self) -> Option<PathConvention> {
let bytes = self.0.as_bytes();
let has_windows_drive_root = matches!(
@@ -172,7 +174,12 @@ impl LegacyAppPathString {
[drive, b':', separator, ..]
if drive.is_ascii_alphabetic() && is_windows_separator_byte(*separator)
);
if has_windows_drive_root || self.0.starts_with(r"\\") {
let has_windows_unc_root = matches!(
bytes,
[first, second, ..]
if is_windows_separator_byte(*first) && is_windows_separator_byte(*second)
);
if has_windows_drive_root || has_windows_unc_root {
Some(PathConvention::Windows)
} else if self.0.starts_with('/') {
Some(PathConvention::Posix)
@@ -418,7 +418,7 @@ fn infers_absolute_path_conventions_from_api_text() {
(r"\\.\COM1", Some(PathConvention::Windows)),
("/workspace/file.rs", Some(PathConvention::Posix)),
("/C:/workspace/file.rs", Some(PathConvention::Posix)),
("//server/share/file.rs", Some(PathConvention::Posix)),
("//server/share/file.rs", Some(PathConvention::Windows)),
("", None),
(".", None),
("subdir/file.rs", None),
@@ -657,3 +657,62 @@ fn serializes_and_deserializes_as_a_string() {
rendered
);
}
#[test]
fn inferred_unc_paths_accept_forward_and_mixed_slashes_on_every_host() {
for raw in [
"//server/share/project",
r"//server/share\project",
r"/\server/share/project",
r"\/server/share/project",
r"\\server\share\project",
"//wsl.localhost/Ubuntu/home/user/project",
"//127.0.0.1/c$/project",
"//LOCALHOST/c$/project",
"//?/C:/project",
] {
let path = LegacyAppPathString::from_string(raw);
let expected = path.to_path_uri(PathConvention::Windows).expect(raw);
let inferred = path.to_inferred_path_uri().expect(raw);
assert_eq!(
(inferred.clone(), inferred.infer_path_convention()),
(expected, Some(PathConvention::Windows)),
"{raw}"
);
let serialized = serde_json::to_string(&inferred).expect("serialize URI");
assert_eq!(
serde_json::from_str::<PathUri>(&serialized).expect("deserialize URI"),
inferred,
"{raw}"
);
}
let slash = LegacyAppPathString::from_string("//server/share/project")
.to_inferred_path_uri()
.unwrap();
assert_eq!(
slash,
PathUri::parse("file://server/share/project").unwrap()
);
}
#[test]
fn double_slash_posix_paths_require_an_explicit_convention() {
let path = LegacyAppPathString::from_string("//server/share/project");
assert_eq!(
path.to_path_uri(PathConvention::Posix).unwrap(),
PathUri::parse("file:///server/share/project").unwrap()
);
let cwd = PathUri::parse("file:///workspace").unwrap();
assert!(matches!(
path.resolve_against(&cwd, /*user_home_dir*/ None),
Err(LegacyAppPathStringError::MismatchedConvention { .. })
));
for raw in ["//", "//server", "//server/", "///server/share"] {
assert!(
LegacyAppPathString::from_string(raw)
.to_inferred_path_uri()
.is_none(),
"{raw}"
);
}
}