Fix Windows path inference for opaque URIs with slash prefixes (#49388)

## Why

Opaque Windows paths encoded as UTF-16LE could be mistaken for POSIX paths when they began with `/`, or fail Windows convention inference when their leading separators mixed `/` and `\`. This prevented them from retaining the Windows path convention across URI serialization.

## What changed

Distinguish a UTF-16LE slash from a POSIX root and recognize either Windows separator in both positions of an opaque UNC prefix.

## Testing

Add round-trip coverage for Windows drive, UNC, device, and namespace paths, including forward and mixed separators. Cover JSON serialization, absolute-path joins, relative-path separator handling, and Windows-only host-native conversions.

GitOrigin-RevId: d4e8a40a314cfc030c30fa8c124bfdd1e565076d
This commit is contained in:
Dmitriy Kovalenko
2026-09-29 22:49:42 +00:00
committed by copyberry
parent a44afa527c
commit d6571322d2
2 changed files with 141 additions and 2 deletions
+4 -2
View File
@@ -810,7 +810,8 @@ fn native_path_segments_start_with(
}
fn infer_opaque_path_convention(path_bytes: &[u8]) -> Option<PathConvention> {
if path_bytes.starts_with(b"/") {
// A UTF-16LE Windows slash starts with `/\0`, unlike a POSIX root.
if path_bytes.starts_with(b"/") && !path_bytes.starts_with(b"/\0") {
return Some(PathConvention::Posix);
}
if !path_bytes.len().is_multiple_of(2) {
@@ -824,7 +825,8 @@ fn infer_opaque_path_convention(path_bytes: &[u8]) -> Option<PathConvention> {
let second = path_wide.next()?;
let has_drive = u8::try_from(first).is_ok_and(|drive| drive.is_ascii_alphabetic())
&& second == u16::from(b':');
let has_unc_prefix = first == u16::from(b'\\') && second == u16::from(b'\\');
let is_separator = |character| character == u16::from(b'\\') || character == u16::from(b'/');
let has_unc_prefix = is_separator(first) && is_separator(second);
(has_drive || has_unc_prefix).then_some(PathConvention::Windows)
}
+137
View File
@@ -532,6 +532,38 @@ fn file_uri_round_trips_literal_percent_characters() {
assert_eq!(uri.basename(), Some("file".to_string()));
}
#[cfg(windows)]
#[test]
fn host_windows_paths_round_trip_through_file_uris() {
for native in [
r"C:\temp\test-file.txt",
r"C:/temp/test-file.txt",
r"C:\temp/test-file.txt",
r"\\127.0.0.1\c$\temp\test-file.txt",
r"//127.0.0.1/c$/temp/test-file.txt",
r"\\LOCALHOST\c$\temp\test-file.txt",
r"//LOCALHOST/c$/temp/test-file.txt",
r"\\.\C:\temp\test-file.txt",
r"\\?\C:\temp\test-file.txt",
r"\\.\UNC\LOCALHOST\c$\temp\test-file.txt",
r"\\?\UNC\server\share\temp\test-file.txt",
r"\\.\COM1",
r"\\?\Volume{00000000-0000-0000-0000-000000000000}\file.txt",
] {
let expected =
codex_utils_absolute_path::AbsolutePathBuf::from_absolute_path_checked(native)
.expect(native);
let uri = PathUri::from_host_native_path(native).expect(native);
let restored = PathUri::parse(&uri.to_string()).expect("parse URI");
assert_eq!(
restored.infer_path_convention(),
Some(PathConvention::Windows),
"{native}"
);
assert_eq!(restored.to_abs_path().expect(native), expected, "{native}");
}
}
#[test]
#[cfg(windows)]
fn file_uri_round_trips_windows_unc_paths() {
@@ -957,6 +989,93 @@ fn join_normalizes_absolute_parent_segments() {
}
}
#[test]
fn absolute_windows_paths_round_trip() {
for (native, expected) in [
(r"c:\temp\test-file.txt", r"C:\temp\test-file.txt"),
(r"C:/temp/test-file.txt", r"C:\temp\test-file.txt"),
(r"C:\temp/test-file.txt", r"C:\temp\test-file.txt"),
(
r"\\127.0.0.1\c$\temp\test-file.txt",
r"\\127.0.0.1\c$\temp\test-file.txt",
),
(
r"//127.0.0.1/c$/temp/test-file.txt",
r"\\127.0.0.1\c$\temp\test-file.txt",
),
(
r"\\server/share\temp/test-file.txt",
r"\\server\share\temp\test-file.txt",
),
(
r"\\LOCALHOST\c$\temp\test-file.txt",
r"\\LOCALHOST\c$\temp\test-file.txt",
),
(
r"//LOCALHOST/c$/temp/test-file.txt",
r"//LOCALHOST/c$/temp/test-file.txt",
),
(
r"/\LOCALHOST/c$/temp/test-file.txt",
r"/\LOCALHOST/c$/temp/test-file.txt",
),
(
r"\/LOCALHOST/c$/temp/test-file.txt",
r"\/LOCALHOST/c$/temp/test-file.txt",
),
(r"\\.\c:\temp\test-file.txt", r"C:\temp\test-file.txt"),
(r"\\?\c:\temp\test-file.txt", r"C:\temp\test-file.txt"),
(
r"\\.\UNC\server\c$\temp\test-file.txt",
r"\\server\c$\temp\test-file.txt",
),
(
r"\\?\UNC\server\c$\temp\test-file.txt",
r"\\server\c$\temp\test-file.txt",
),
(
r"\\.\UNC\LOCALHOST\c$\temp\test-file.txt",
r"\\.\UNC\LOCALHOST\c$\temp\test-file.txt",
),
(r"\\.\COM1", r"\\.\COM1"),
(
r"\\?\Volume{00000000-0000-0000-0000-000000000000}\file.txt",
r"\\?\Volume{00000000-0000-0000-0000-000000000000}\file.txt",
),
(r"//./COM1", r"//./COM1"),
(r"//./C:/temp/test-file.txt", r"//./C:/temp/test-file.txt"),
(r"//?/C:/temp/test-file.txt", r"//?/C:/temp/test-file.txt"),
(
r"//./UNC/server/share/file.txt",
r"//./UNC/server/share/file.txt",
),
(
r"//?/UNC/server/share/file.txt",
r"//?/UNC/server/share/file.txt",
),
(
r"C:/a b/日本語/100%#file.txt:stream",
r"C:\a b\日本語\100%#file.txt:stream",
),
] {
let uri = LegacyAppPathString::from_string(native)
.to_path_uri(PathConvention::Windows)
.expect(native);
let serialized = serde_json::to_string(&uri).expect("serialize URI");
let restored: PathUri = serde_json::from_str(&serialized).expect("deserialize URI");
assert_eq!(
(
restored.infer_path_convention(),
restored.inferred_native_path_string()
),
(Some(PathConvention::Windows), expected.to_string()),
"{native}"
);
let base = PathUri::parse("file:///D:/workspace").expect("base URI");
assert_eq!(base.join(native).expect(native), restored, "{native}");
}
}
#[test]
fn windows_namespace_normalization_preserves_opaque_paths() {
let base = PathUri::parse("file:///C:/workspace").expect("valid Windows base URI");
@@ -1059,6 +1178,24 @@ fn join_replaces_windows_absolute_path() {
);
}
#[test]
fn windows_relative_paths_accept_both_separators() {
let base = PathUri::parse("file:///C:/workspace/src").expect("base URI");
for (native, expected) in [
(r"..\tests/file.rs", "file:///C:/workspace/tests/file.rs"),
(r"../tests\file.rs", "file:///C:/workspace/tests/file.rs"),
(r"/temp/file.rs", "file:///C:/temp/file.rs"),
(r"\temp/file.rs", "file:///C:/temp/file.rs"),
(r"c:tests/file.rs", "file:///C:/workspace/src/tests/file.rs"),
] {
assert_eq!(
base.join(native).expect(native),
PathUri::parse(expected).unwrap()
);
}
assert!(base.join("D:tests/file.rs").is_err());
}
#[test]
fn join_windows_root_relative_path_preserves_drive_or_share() {
for (base, path, expected) in [