From 985e727960293e6b361407efc0034262f6a5abd5 Mon Sep 17 00:00:00 2001 From: Adam Perry Date: Wed, 17 Jun 2026 18:28:12 +0000 Subject: [PATCH] path-uri: decouple native path parsing --- .../utils/path-uri/src/api_path_string.rs | 135 +----------------- codex-rs/utils/path-uri/src/lib.rs | 130 ++++++++++++++++- 2 files changed, 135 insertions(+), 130 deletions(-) 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 0881c64d7c01..5fc204b178c9 100644 --- a/codex-rs/utils/path-uri/src/api_path_string.rs +++ b/codex-rs/utils/path-uri/src/api_path_string.rs @@ -1,4 +1,6 @@ +use crate::PathConvention; use crate::PathUri; +use crate::is_windows_separator_byte; use codex_utils_absolute_path::AbsolutePathBuf; use schemars::JsonSchema; use serde::Deserialize; @@ -33,10 +35,6 @@ use ts_rs::TS; pub struct LegacyAppPathString(String); impl LegacyAppPathString { - pub(super) fn from_str(path: &str) -> Self { - Self(path.to_string()) - } - /// Renders an absolute path using the current host's path convention. pub fn from_abs_path(path: &AbsolutePathBuf) -> Self { Self(path.to_string_lossy().into_owned()) @@ -69,13 +67,11 @@ impl LegacyAppPathString { &self, convention: PathConvention, ) -> Result { - let path = match convention { - PathConvention::Posix => parse_posix_path(&self.0), - PathConvention::Windows => parse_windows_path(&self.0), - }; - path.ok_or_else(|| LegacyAppPathStringError::InvalidNativePath { - path: self.0.clone(), - convention, + PathUri::from_absolute_native_path(&self.0, convention).ok_or_else(|| { + LegacyAppPathStringError::InvalidNativePath { + path: self.0.clone(), + convention, + } }) } @@ -115,88 +111,6 @@ impl From for LegacyAppPathString { } } -fn parse_posix_path(path: &str) -> Option { - let path = path.strip_prefix('/')?; - if path.contains('\0') { - return Some(PathUri::from_opaque_path_bytes( - format!("/{path}").as_bytes(), - )); - } - path_uri_from_segments(/*host*/ None, path.split('/')) -} - -fn parse_windows_path(path: &str) -> Option { - let bytes = path.as_bytes(); - let uses_namespace = matches!( - bytes, - [first, second, namespace @ (b'.' | b'?'), separator, ..] - if is_windows_separator_byte(*first) - && is_windows_separator_byte(*second) - && is_windows_separator_byte(*separator) - && matches!(*namespace, b'.' | b'?') - ); - if uses_namespace || path.contains('\0') { - return Some(windows_opaque_path_uri(path)); - } - - if matches!( - bytes, - [drive, b':', separator, ..] - if drive.is_ascii_alphabetic() && is_windows_separator_byte(*separator) - ) { - return path_uri_from_segments( - /*host*/ None, - std::iter::once(&path[..2]).chain(path[3..].split(is_windows_separator_char)), - ); - } - - if matches!(bytes, [first, second, ..] - if is_windows_separator_byte(*first) && is_windows_separator_byte(*second)) - { - let mut components = path[2..].split(is_windows_separator_char); - let host = components.next().filter(|host| !host.is_empty())?; - let share = components.next().filter(|share| !share.is_empty())?; - return path_uri_from_segments(Some(host), std::iter::once(share).chain(components)) - .or_else(|| Some(windows_opaque_path_uri(path))); - } - - None -} - -fn path_uri_from_segments<'a>( - host: Option<&str>, - segments: impl Iterator, -) -> Option { - let mut url = url::Url::parse("file:///").ok()?; - if let Some(host) = host { - url.set_host(Some(host)).ok()?; - } - { - let mut url_segments = url.path_segments_mut().ok()?; - url_segments.clear(); - for segment in segments { - url_segments.push(segment); - } - } - PathUri::try_from(url).ok() -} - -fn windows_opaque_path_uri(path: &str) -> PathUri { - let path_bytes = path - .encode_utf16() - .flat_map(u16::to_le_bytes) - .collect::>(); - PathUri::from_opaque_path_bytes(&path_bytes) -} - -fn is_windows_separator_char(character: char) -> bool { - matches!(character, '\\' | '/') -} - -fn is_windows_separator_byte(character: u8) -> bool { - matches!(character, b'\\' | b'/') -} - fn render_opaque_fallback( path: &PathUri, path_bytes: &[u8], @@ -372,41 +286,6 @@ pub enum LegacyAppPathStringError { }, } -/// Path syntax used to render a [`PathUri`] as an operating-system path. -/// -/// This describes path grammar rather than a specific operating system because -/// Linux and macOS share the POSIX representation relevant here. -#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema, TS)] -#[serde(rename_all = "snake_case")] -#[ts(rename_all = "snake_case")] -pub enum PathConvention { - Posix, - Windows, -} - -impl PathConvention { - /// Returns the path convention used by the current process. - #[cfg(windows)] - pub const fn native() -> Self { - Self::Windows - } - - /// Returns the path convention used by the current process. - #[cfg(unix)] - pub const fn native() -> Self { - Self::Posix - } -} - -impl fmt::Display for PathConvention { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Self::Posix => f.write_str("POSIX"), - Self::Windows => f.write_str("Windows"), - } - } -} - #[cfg(test)] #[path = "api_path_string_tests.rs"] mod tests; diff --git a/codex-rs/utils/path-uri/src/lib.rs b/codex-rs/utils/path-uri/src/lib.rs index a7f3c83369f1..5e8b5b2cbc28 100644 --- a/codex-rs/utils/path-uri/src/lib.rs +++ b/codex-rs/utils/path-uri/src/lib.rs @@ -21,7 +21,6 @@ mod api_path_string; pub use api_path_string::LegacyAppPathString; pub use api_path_string::LegacyAppPathStringError; -pub use api_path_string::PathConvention; pub const FILE_SCHEME: &str = "file"; const BAD_PATH_URI_PREFIX: &str = "file:///%00/bad/path/"; @@ -97,6 +96,16 @@ impl PathUri { Self::from_opaque_path_bytes(&path_bytes) } + pub(crate) fn from_absolute_native_path( + path: &str, + convention: PathConvention, + ) -> Option { + match convention { + PathConvention::Posix => parse_posix_path(path), + PathConvention::Windows => parse_windows_path(path), + } + } + fn from_opaque_path_bytes(path_bytes: &[u8]) -> Self { let encoded_path = base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(path_bytes); let Ok(uri) = Self::parse(&format!("{BAD_PATH_URI_PREFIX}{encoded_path}")) else { @@ -240,7 +249,7 @@ impl PathUri { })?; // An absolute native path is already fully resolved, so replace the base URI's main path // instead of appending it. - if let Ok(absolute) = LegacyAppPathString::from_str(path).to_path_uri(convention) { + if let Some(absolute) = Self::from_absolute_native_path(path, convention) { return Ok(absolute); } let path_bytes = path.as_bytes(); @@ -521,6 +530,88 @@ fn infer_opaque_path_convention(path_bytes: &[u8]) -> Option { (has_drive || has_unc_prefix).then_some(PathConvention::Windows) } +fn parse_posix_path(path: &str) -> Option { + let path = path.strip_prefix('/')?; + if path.contains('\0') { + return Some(PathUri::from_opaque_path_bytes( + format!("/{path}").as_bytes(), + )); + } + path_uri_from_segments(/*host*/ None, path.split('/')) +} + +fn parse_windows_path(path: &str) -> Option { + let bytes = path.as_bytes(); + let uses_namespace = matches!( + bytes, + [first, second, namespace @ (b'.' | b'?'), separator, ..] + if is_windows_separator_byte(*first) + && is_windows_separator_byte(*second) + && is_windows_separator_byte(*separator) + && matches!(*namespace, b'.' | b'?') + ); + if uses_namespace || path.contains('\0') { + return Some(windows_opaque_path_uri(path)); + } + + if matches!( + bytes, + [drive, b':', separator, ..] + if drive.is_ascii_alphabetic() && is_windows_separator_byte(*separator) + ) { + return path_uri_from_segments( + /*host*/ None, + std::iter::once(&path[..2]).chain(path[3..].split(is_windows_separator_char)), + ); + } + + if matches!(bytes, [first, second, ..] + if is_windows_separator_byte(*first) && is_windows_separator_byte(*second)) + { + let mut components = path[2..].split(is_windows_separator_char); + let host = components.next().filter(|host| !host.is_empty())?; + let share = components.next().filter(|share| !share.is_empty())?; + return path_uri_from_segments(Some(host), std::iter::once(share).chain(components)) + .or_else(|| Some(windows_opaque_path_uri(path))); + } + + None +} + +fn path_uri_from_segments<'a>( + host: Option<&str>, + segments: impl Iterator, +) -> Option { + let mut url = Url::parse("file:///").ok()?; + if let Some(host) = host { + url.set_host(Some(host)).ok()?; + } + { + let mut url_segments = url.path_segments_mut().ok()?; + url_segments.clear(); + for segment in segments { + url_segments.push(segment); + } + } + PathUri::try_from(url).ok() +} + +fn windows_opaque_path_uri(path: &str) -> PathUri { + let path_bytes = path + .encode_utf16() + .flat_map(u16::to_le_bytes) + .collect::>(); + PathUri::from_opaque_path_bytes(&path_bytes) +} + +fn is_windows_separator_char(character: char) -> bool { + matches!(character, '\\' | '/') +} + +pub(crate) fn is_windows_separator_byte(character: u8) -> bool { + matches!(character, b'\\' | b'/') +} + /// Rejects URI metadata that has no defined meaning for `file:` URIs. fn validate_common_known_uri(url: &Url) -> Result<(), PathUriParseError> { if !url.username().is_empty() || url.password().is_some() { @@ -573,6 +664,41 @@ pub enum PathUriParseError { JoinPathMustBeRelative(String), } +/// Path syntax used to render a [`PathUri`] as an operating-system path. +/// +/// This describes path grammar rather than a specific operating system because +/// Linux and macOS share the POSIX representation relevant here. +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema, TS)] +#[serde(rename_all = "snake_case")] +#[ts(rename_all = "snake_case")] +pub enum PathConvention { + Posix, + Windows, +} + +impl PathConvention { + /// Returns the path convention used by the current process. + #[cfg(windows)] + pub const fn native() -> Self { + Self::Windows + } + + /// Returns the path convention used by the current process. + #[cfg(unix)] + pub const fn native() -> Self { + Self::Posix + } +} + +impl fmt::Display for PathConvention { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Posix => f.write_str("POSIX"), + Self::Windows => f.write_str("Windows"), + } + } +} + #[cfg(test)] #[path = "tests.rs"] mod tests;