From 8707a35113501a9988f06162ca5c2b27d4f90a58 Mon Sep 17 00:00:00 2001 From: iceweasel-oai Date: Wed, 29 Jul 2026 00:01:19 +0000 Subject: [PATCH] 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 --- codex-rs/utils/absolute-path/src/lib.rs | 3 +- codex-rs/utils/path-uri/src/lib.rs | 25 ++++++++++ codex-rs/utils/path-uri/src/tests.rs | 61 +++++++++++++++++++++++++ 3 files changed, 88 insertions(+), 1 deletion(-) diff --git a/codex-rs/utils/absolute-path/src/lib.rs b/codex-rs/utils/absolute-path/src/lib.rs index 6c37e0866e..b4be15a244 100644 --- a/codex-rs/utils/absolute-path/src/lib.rs +++ b/codex-rs/utils/absolute-path/src/lib.rs @@ -150,7 +150,8 @@ fn normalize_path_for_platform(path: &Path) -> Cow<'_, Path> { Cow::Borrowed(path) } -fn normalize_windows_device_path(path: &str) -> Option { +/// Normalizes Windows drive and UNC namespace aliases on any host. +pub fn normalize_windows_device_path(path: &str) -> Option { if let Some(unc) = path.strip_prefix(r"\\?\UNC\") { return Some(format!(r"\\{unc}")); } diff --git a/codex-rs/utils/path-uri/src/lib.rs b/codex-rs/utils/path-uri/src/lib.rs index 80d1798a42..5e372cc2d1 100644 --- a/codex-rs/utils/path-uri/src/lib.rs +++ b/codex-rs/utils/path-uri/src/lib.rs @@ -4,6 +4,7 @@ use base64::Engine; use codex_utils_absolute_path::AbsolutePathBuf; +use codex_utils_absolute_path::normalize_windows_device_path; use schemars::JsonSchema; use serde::Deserialize; use serde::Deserializer; @@ -661,6 +662,30 @@ fn parse_posix_path(path: &str) -> Option { } fn parse_windows_path(path: &str) -> Option { + 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 { let bytes = path.as_bytes(); let uses_namespace = matches!( bytes, diff --git a/codex-rs/utils/path-uri/src/tests.rs b/codex-rs/utils/path-uri/src/tests.rs index 548d740037..333bd5dc09 100644 --- a/codex-rs/utils/path-uri/src/tests.rs +++ b/codex-rs/utils/path-uri/src/tests.rs @@ -738,6 +738,26 @@ fn join_normalizes_absolute_parent_segments() { r"\\server\share\a\..\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"), @@ -765,6 +785,47 @@ fn join_normalizes_absolute_parent_segments() { ] { let base = PathUri::parse(base).expect("valid base 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}"); } }