diff --git a/codex-rs/utils/path-uri/src/api_path_string.rs b/codex-rs/utils/path-uri/src/api_path_string.rs index 3b9def4ba4..b2ecdf0aed 100644 --- a/codex-rs/utils/path-uri/src/api_path_string.rs +++ b/codex-rs/utils/path-uri/src/api_path_string.rs @@ -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 { 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) diff --git a/codex-rs/utils/path-uri/src/api_path_string_tests.rs b/codex-rs/utils/path-uri/src/api_path_string_tests.rs index d5a621d290..b115a9006b 100644 --- a/codex-rs/utils/path-uri/src/api_path_string_tests.rs +++ b/codex-rs/utils/path-uri/src/api_path_string_tests.rs @@ -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::(&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}" + ); + } +}