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 bc63990679..2a8f4564d2 100644 --- a/codex-rs/utils/path-uri/src/api_path_string.rs +++ b/codex-rs/utils/path-uri/src/api_path_string.rs @@ -30,7 +30,7 @@ use ts_rs::TS; /// `String` and is instead encouraged to convert through [`PathUri`] or /// [`AbsolutePathBuf`]. Relative path text remains valid until an operation /// such as [`Self::to_path_uri`] requires an absolute path. -#[derive(Clone, Debug, PartialEq, Eq, Hash, Deserialize, TS)] +#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Deserialize, TS)] #[serde(transparent)] #[ts(type = "string")] pub struct LegacyAppPathString(String); @@ -81,6 +81,28 @@ impl LegacyAppPathString { }) } + /// Parses this API string as an absolute path native to the current host. + /// + /// Host-specific path namespaces are normalized before the path is converted + /// to a URI. Relative and foreign-platform paths are rejected. + pub fn to_host_path_uri(&self) -> Result { + self.to_path_uri(PathConvention::native()) + } + + /// Parses this API string as an absolute path native to the current host. + /// + /// Host-specific path namespaces are normalized. Relative and + /// foreign-platform paths are rejected. + pub fn to_host_abs_path(&self) -> Result { + let convention = PathConvention::native(); + self.to_path_uri(convention)?.to_abs_path().map_err(|_| { + LegacyAppPathStringError::InvalidNativePath { + path: self.0.clone(), + convention: Some(convention), + } + }) + } + /// Parses this API string as an absolute path using the convention inferred from its spelling. pub fn to_inferred_path_uri(&self) -> Option { PathUri::try_from(self.clone()).ok() diff --git a/codex-rs/utils/path-uri/src/lib.rs b/codex-rs/utils/path-uri/src/lib.rs index ccc9cfd79b..5686ddea24 100644 --- a/codex-rs/utils/path-uri/src/lib.rs +++ b/codex-rs/utils/path-uri/src/lib.rs @@ -263,24 +263,50 @@ impl PathUri { if self.0.host_str() != base.0.host_str() { return false; } - - let Some(path_segments) = containment_path_segments( - &self.0, - self.infer_path_convention() - .unwrap_or(PathConvention::Posix), - ) else { + let Some(convention) = self.infer_path_convention() else { return false; }; - let Some(base_segments) = containment_path_segments( - &base.0, - base.infer_path_convention() - .unwrap_or(PathConvention::Posix), - ) else { + if base.infer_path_convention() != Some(convention) { + return false; + } + + let Some(path_segments) = containment_path_segments(&self.0, convention) else { + return false; + }; + let Some(base_segments) = containment_path_segments(&base.0, convention) else { return false; }; path_segments.starts_with(&base_segments) } + /// Returns this path relative to `base` using `/` as the separator. + /// + /// Both paths are compared lexically in URI space, without consulting the + /// host filesystem. `None` is returned when the paths use different native + /// conventions or authorities, when this path is outside `base`, or when an + /// encoded native separator makes containment ambiguous. + pub fn relative_path_from(&self, base: &Self) -> Option { + if self == base { + return Some(String::new()); + } + + let convention = self.infer_path_convention()?; + if base.infer_path_convention()? != convention || self.0.host_str() != base.0.host_str() { + return None; + } + + let path_segments = containment_path_segments(&self.0, convention)?; + let base_segments = containment_path_segments(&base.0, convention)?; + let relative_segments = path_segments.strip_prefix(base_segments.as_slice())?; + Some( + relative_segments + .iter() + .map(|segment| decode_uri_path(segment)) + .collect::>() + .join("/"), + ) + } + /// Lexically resolves native absolute or relative path text against this URI. /// /// Path text is interpreted using the POSIX or Windows convention inferred