Normalize Windows namespace paths in path URIs (#35851)

## What changed

- Convert Windows device-namespace drive paths such as `\\?\D:\reports` and
  `\\.\D:\reports` to canonical drive-based `file:` URIs.
- Convert device-namespace UNC paths to canonical hosted `file:` URIs.
- Preserve unsupported or ambiguous namespace paths as opaque path URIs instead
  of interpreting them as filesystem paths.

## Testing

Add coverage for namespace path conversion and joining, including malformed UNC
paths, reserved device names, and volume identifiers.

GitOrigin-RevId: 234f69bc2787e5d318bc0320b94e7392abfb5602
This commit is contained in:
iceweasel-oai authored and copyberry committed 2026-07-29 00:13:06 +00:00
1 parent 101d6b8cb2
commit 8707a35113
3 files changed
+88 -1

No files matched your search

+2 -1
View File
@@ -150,7 +150,8 @@ fn normalize_path_for_platform(path: &Path) -> Cow<'_, Path> {
Cow::Borrowed(path) Cow::Borrowed(path)
} }
fn normalize_windows_device_path(path: &str) -> Option<String> { /// Normalizes Windows drive and UNC namespace aliases on any host.
pub fn normalize_windows_device_path(path: &str) -> Option<String> {
if let Some(unc) = path.strip_prefix(r"\\?\UNC\") { if let Some(unc) = path.strip_prefix(r"\\?\UNC\") {
return Some(format!(r"\\{unc}")); return Some(format!(r"\\{unc}"));
} }
+25
View File
@@ -4,6 +4,7 @@
use base64::Engine; use base64::Engine;
use codex_utils_absolute_path::AbsolutePathBuf; use codex_utils_absolute_path::AbsolutePathBuf;
use codex_utils_absolute_path::normalize_windows_device_path;
use schemars::JsonSchema; use schemars::JsonSchema;
use serde::Deserialize; use serde::Deserialize;
use serde::Deserializer; use serde::Deserializer;
@@ -661,6 +662,30 @@ fn parse_posix_path(path: &str) -> Option<PathUri> {
} }
fn parse_windows_path(path: &str) -> Option<PathUri> { fn parse_windows_path(path: &str) -> Option<PathUri> {
if let Some(normalized_path) = normalize_windows_device_path(path) {
if let Some(unc_path) = normalized_path.strip_prefix(r"\\") {
let mut components = unc_path.split(is_windows_separator_char);
if matches!(components.next(), None | Some("" | "." | ".."))
|| matches!(components.next(), None | Some("" | "." | ".."))
{
return Some(windows_opaque_path_uri(path));
}
}
return Some(
parse_unnormalized_windows_path(&normalized_path)
.filter(|uri| {
uri.infer_path_convention() == Some(PathConvention::Windows)
&& uri.opaque_fallback_bytes().is_none()
&& (!normalized_path.starts_with(r"\\")
|| uri.0.host_str().is_some_and(|host| !host.is_empty()))
})
.unwrap_or_else(|| windows_opaque_path_uri(path)),
);
}
parse_unnormalized_windows_path(path)
}
fn parse_unnormalized_windows_path(path: &str) -> Option<PathUri> {
let bytes = path.as_bytes(); let bytes = path.as_bytes();
let uses_namespace = matches!( let uses_namespace = matches!(
bytes, bytes,
+61
View File
@@ -738,6 +738,26 @@ fn join_normalizes_absolute_parent_segments() {
r"\\server\share\a\..\b", r"\\server\share\a\..\b",
"file://server/share/b", "file://server/share/b",
), ),
(
"file:///C:/workspace",
r"\\?\D:\reports\report.pdf",
"file:///D:/reports/report.pdf",
),
(
"file:///C:/workspace",
r"\\.\D:\reports\report.pdf",
"file:///D:/reports/report.pdf",
),
(
"file:///C:/workspace",
r"\\?\UNC\server\share\reports\report.pdf",
"file://server/share/reports/report.pdf",
),
(
"file:///C:/workspace",
r"\\.\UNC\server\share\reports\report.pdf",
"file://server/share/reports/report.pdf",
),
("file:///workspace", "/tmp//a/../b", "file:///tmp/b"), ("file:///workspace", "/tmp//a/../b", "file:///tmp/b"),
("file:///workspace", "/tmp/a/..//b", "file:///tmp/b"), ("file:///workspace", "/tmp/a/..//b", "file:///tmp/b"),
("file:///workspace", "/tmp/a///../b", "file:///tmp/b"), ("file:///workspace", "/tmp/a///../b", "file:///tmp/b"),
@@ -765,6 +785,47 @@ fn join_normalizes_absolute_parent_segments() {
] { ] {
let base = PathUri::parse(base).expect("valid base URI"); let base = PathUri::parse(base).expect("valid base URI");
let expected = PathUri::parse(expected).expect("valid expected URI"); let expected = PathUri::parse(expected).expect("valid expected URI");
if normalize_windows_device_path(path).is_some() {
assert_eq!(
LegacyAppPathString::from_string(path).to_path_uri(PathConvention::Windows),
Ok(expected.clone()),
"converting {path}"
);
}
assert_eq!(base.join(path), Ok(expected), "joining {path}");
}
}
#[test]
fn windows_namespace_normalization_preserves_opaque_paths() {
let base = PathUri::parse("file:///C:/workspace").expect("valid Windows base URI");
for path in [
r"\\?\UNC\server",
r"\\.\UNC\server",
r"\\?\UNC\localhost\share\report.pdf",
r"\\.\UNC\LOCALHOST\share\report.pdf",
r"\\?\UNC\.\share\report.pdf",
r"\\.\UNC\..\share\report.pdf",
r"\\?\UNC\server\.\report.pdf",
r"\\.\UNC\server\..\report.pdf",
r"\\?\UNC\?\UNC\?\C:\report.pdf",
r"\\.\UNC\?\UNC\?\C:\report.pdf",
r"\\.\COM1",
r"\\?\Volume{00000000-0000-0000-0000-000000000000}\report.pdf",
] {
let expected = windows_opaque_path_uri(path);
assert_eq!(
PathUri::from_absolute_native_path(path, PathConvention::Windows),
Some(expected.clone()),
"parsing {path}"
);
assert_eq!(
LegacyAppPathString::from_string(path).to_path_uri(PathConvention::Windows),
Ok(expected.clone()),
"converting {path}"
);
assert_eq!(base.join(path), Ok(expected), "joining {path}"); assert_eq!(base.join(path), Ok(expected), "joining {path}");
} }
} }