From 217ab29b6c8d1ed9f05e2e99a2ca3b939ef82525 Mon Sep 17 00:00:00 2001 From: Adam Perry Date: Sat, 13 Jun 2026 21:07:24 +0000 Subject: [PATCH] path-uri: infer native path conventions --- codex-rs/utils/path-uri/src/lib.rs | 76 +++++++++++++++++++++++++--- codex-rs/utils/path-uri/src/tests.rs | 34 +++++++++++++ 2 files changed, 103 insertions(+), 7 deletions(-) diff --git a/codex-rs/utils/path-uri/src/lib.rs b/codex-rs/utils/path-uri/src/lib.rs index 468d577c1a..ee7311a4c3 100644 --- a/codex-rs/utils/path-uri/src/lib.rs +++ b/codex-rs/utils/path-uri/src/lib.rs @@ -35,15 +35,18 @@ const BAD_PATH_URI_PREFIX: &str = "file:///%00/bad/path/"; /// created by [`Self::from_abs_path`] are opaque to these lexical operations. /// /// `file:` paths retain their URI spelling so they can be parsed independently -/// of the current host. In particular, `/C:/src` remains ambiguous between a -/// Windows drive path and a valid POSIX path until [`Self::to_abs_path`] -/// applies the current host's rules. A local POSIX `file:` URI can also retain -/// percent-encoded non-UTF-8 bytes for lossless native round trips. +/// of the current host. In particular, `/C:/src` is structurally ambiguous +/// between a Windows drive path and a valid POSIX path; +/// [`Self::infer_path_convention`] applies a documented heuristic, while +/// [`Self::to_abs_path`] applies the current host's rules. A local POSIX +/// `file:` URI can also retain percent-encoded non-UTF-8 bytes for lossless +/// native round trips. /// /// Like [VS Code resources], path operations use `/` URI separators on every -/// host. They preserve a URL authority but do not infer Windows drive or UNC -/// roots from path text. Native path normalization, filesystem aliases, -/// symlinks, case sensitivity, and Unicode normalization are not resolved. +/// host. Lexical path operations preserve a URL authority without interpreting +/// Windows drive or UNC roots from path text. Native path normalization, +/// filesystem aliases, symlinks, case sensitivity, and Unicode normalization +/// are not resolved. /// /// Serde represents a `PathUri` as its canonical URI string. Deserialization /// also accepts an absolute native path for compatibility with fields that @@ -129,6 +132,42 @@ impl PathUri { decode_bad_path_uri(&self.0) } + /// Infers the native path convention represented by this URI. + /// + /// A URI authority is treated as a Windows UNC host, and a leading + /// drive-letter segment such as `C:` is treated as a Windows drive. All + /// other ordinary file URIs are treated as POSIX paths. This deliberately + /// classifies `file:///C:/src` as Windows even though `/C:/src` is also a + /// valid POSIX path. In practice, POSIX paths with a drive-shaped first + /// component are rare enough that recognizing foreign Windows paths is the + /// more useful default. + /// + /// Opaque fallback URIs are inspected for an absolute POSIX byte prefix or + /// an absolute Windows UTF-16LE prefix. `None` is returned when their + /// payload does not identify either convention. + /// + /// TODO(anp): Once `PathUri` carries an environment identifier, prefer the + /// environment's declared convention over this spelling-based heuristic. + pub fn infer_path_convention(&self) -> Option { + if let Some(path_bytes) = self.opaque_fallback_bytes() { + return infer_opaque_path_convention(&path_bytes); + } + if self.0.host_str().is_some() { + return Some(PathConvention::Windows); + } + + let has_windows_drive = self + .0 + .path_segments() + .and_then(|mut segments| segments.find(|segment| !segment.is_empty())) + .is_some_and(is_windows_drive_uri_segment); + if has_windows_drive { + Some(PathConvention::Windows) + } else { + Some(PathConvention::Posix) + } + } + /// Returns the decoded final URI path segment, or `None` for the URI root /// or an opaque fallback URI created by [`Self::from_abs_path`]. /// @@ -389,6 +428,29 @@ fn decode_bad_path_uri(url: &Url) -> Option> { .then_some(path_bytes) } +fn is_windows_drive_uri_segment(segment: &str) -> bool { + matches!(segment.as_bytes(), [drive, b':'] if drive.is_ascii_alphabetic()) +} + +fn infer_opaque_path_convention(path_bytes: &[u8]) -> Option { + if path_bytes.starts_with(b"/") { + return Some(PathConvention::Posix); + } + if !path_bytes.len().is_multiple_of(2) { + return None; + } + + let mut path_wide = path_bytes + .chunks_exact(2) + .map(|bytes| u16::from_le_bytes([bytes[0], bytes[1]])); + let first = path_wide.next()?; + let second = path_wide.next()?; + let has_drive = u8::try_from(first).is_ok_and(|drive| drive.is_ascii_alphabetic()) + && second == u16::from(b':'); + let has_unc_prefix = first == u16::from(b'\\') && second == u16::from(b'\\'); + (has_drive || has_unc_prefix).then_some(PathConvention::Windows) +} + /// 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() { diff --git a/codex-rs/utils/path-uri/src/tests.rs b/codex-rs/utils/path-uri/src/tests.rs index f4ad44ae9a..eae388f822 100644 --- a/codex-rs/utils/path-uri/src/tests.rs +++ b/codex-rs/utils/path-uri/src/tests.rs @@ -58,6 +58,40 @@ fn file_uri_parses_a_windows_path_on_any_host() { ); } +#[test] +fn infers_path_conventions_from_uri_shape() { + for (uri, expected) in [ + ("file:///", Some(PathConvention::Posix)), + ("file:///home/alice/src", Some(PathConvention::Posix)), + ("file:///C:/Users/Alice/src", Some(PathConvention::Windows)), + ("file:///d:", Some(PathConvention::Windows)), + ("file://server/share/src", Some(PathConvention::Windows)), + ( + "file:///%00/bad/path/L3RtcC9udWxsLQAt_y1ieXRl", + Some(PathConvention::Posix), + ), + ( + "file:///%00/bad/path/XABcAC4AXABDAE8ATQAxAFwA", + Some(PathConvention::Windows), + ), + ("file:///%00/bad/path/YQ", None), + ] { + let path = PathUri::parse(uri).expect("valid path URI"); + + assert_eq!(path.infer_path_convention(), expected, "inferring {uri}"); + } +} + +#[test] +fn drive_shaped_posix_uri_is_intentionally_inferred_as_windows() { + let path = PathUri::parse("file:///C:/actually/a/posix/path").expect("valid path URI"); + + // `/C:/...` is valid on POSIX, but treating this uncommon spelling as a + // Windows drive lets callers render the overwhelmingly more common foreign + // Windows URI without separately carrying its source convention. + assert_eq!(path.infer_path_convention(), Some(PathConvention::Windows)); +} + #[cfg(windows)] #[test] fn file_uri_falls_back_for_windows_prefixes_without_a_uri_representation() {