Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ If you don’t have the tool:

- Tests should use pretty_assertions::assert_eq for clearer diffs. Import this at the top of the test module if it isn't already.
- Prefer deep equals comparisons whenever possible. Perform `assert_eq!()` on entire objects, rather than individual fields.
- Avoid mutating process environment in tests; prefer passing environment-derived flags or dependencies from above.

### Integration tests (core)

Expand Down
8 changes: 5 additions & 3 deletions codex-rs/core/src/config/service.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ use crate::config_loader::ConfigLayerStack;
use crate::config_loader::LoaderOverrides;
use crate::config_loader::load_config_layers_state;
use crate::config_loader::merge_toml_values;
use crate::path_utils;
use codex_app_server_protocol::Config as ApiConfig;
use codex_app_server_protocol::ConfigBatchWriteParams;
use codex_app_server_protocol::ConfigLayerMetadata;
Expand Down Expand Up @@ -470,9 +471,10 @@ fn validate_config(value: &TomlValue) -> Result<(), toml::de::Error> {
}

fn paths_match(expected: &Path, provided: &Path) -> bool {
if let (Ok(expanded_expected), Ok(expanded_provided)) =
(expected.canonicalize(), provided.canonicalize())
{
if let (Ok(expanded_expected), Ok(expanded_provided)) = (
path_utils::normalize_for_path_comparison(expected),
path_utils::normalize_for_path_comparison(provided),
) {
return expanded_expected == expanded_provided;
}

Expand Down
1 change: 1 addition & 0 deletions codex-rs/core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ mod mcp_tool_call;
mod message_history;
mod model_provider_info;
pub mod parse_command;
pub mod path_utils;
pub mod powershell;
pub mod sandboxing;
mod stream_events_utils;
Expand Down
116 changes: 116 additions & 0 deletions codex-rs/core/src/path_utils.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
use std::path::Path;
use std::path::PathBuf;

use crate::env;

pub fn normalize_for_path_comparison(path: &Path) -> std::io::Result<PathBuf> {
let canonical = path.canonicalize()?;
Ok(normalize_for_wsl(canonical))
}

fn normalize_for_wsl(path: PathBuf) -> PathBuf {
normalize_for_wsl_with_flag(path, env::is_wsl())
}

fn normalize_for_wsl_with_flag(path: PathBuf, is_wsl: bool) -> PathBuf {
if !is_wsl {
return path;
}

if !is_wsl_case_insensitive_path(&path) {
return path;
}

lower_ascii_path(path)
}

fn is_wsl_case_insensitive_path(path: &Path) -> bool {
#[cfg(target_os = "linux")]
{
use std::os::unix::ffi::OsStrExt;
use std::path::Component;

let mut components = path.components();
let Some(Component::RootDir) = components.next() else {
return false;
};
let Some(Component::Normal(mnt)) = components.next() else {
return false;
};
if !ascii_eq_ignore_case(mnt.as_bytes(), b"mnt") {
return false;
}
let Some(Component::Normal(drive)) = components.next() else {
return false;
};
let drive_bytes = drive.as_bytes();
drive_bytes.len() == 1 && drive_bytes[0].is_ascii_alphabetic()
}
#[cfg(not(target_os = "linux"))]
{
let _ = path;
false
}
}

#[cfg(target_os = "linux")]
fn ascii_eq_ignore_case(left: &[u8], right: &[u8]) -> bool {
left.len() == right.len()
&& left
.iter()
.zip(right)
.all(|(lhs, rhs)| lhs.to_ascii_lowercase() == *rhs)
}

#[cfg(target_os = "linux")]
fn lower_ascii_path(path: PathBuf) -> PathBuf {
use std::ffi::OsString;
use std::os::unix::ffi::OsStrExt;
use std::os::unix::ffi::OsStringExt;

// WSL mounts Windows drives under /mnt/<drive>, which are case-insensitive.
let bytes = path.as_os_str().as_bytes();
let mut lowered = Vec::with_capacity(bytes.len());
for byte in bytes {
lowered.push(byte.to_ascii_lowercase());
}
PathBuf::from(OsString::from_vec(lowered))
}

#[cfg(not(target_os = "linux"))]
fn lower_ascii_path(path: PathBuf) -> PathBuf {
path
}

#[cfg(test)]
mod tests {
#[cfg(target_os = "linux")]
mod wsl {
use super::super::normalize_for_wsl_with_flag;
use pretty_assertions::assert_eq;
use std::path::PathBuf;

#[test]
fn wsl_mnt_drive_paths_lowercase() {
let normalized = normalize_for_wsl_with_flag(PathBuf::from("/mnt/C/Users/Dev"), true);

assert_eq!(normalized, PathBuf::from("/mnt/c/users/dev"));
}

#[test]
fn wsl_non_drive_paths_unchanged() {
let path = PathBuf::from("/mnt/cc/Users/Dev");
let normalized = normalize_for_wsl_with_flag(path.clone(), true);

assert_eq!(normalized, path);
}

#[test]
fn wsl_non_mnt_paths_unchanged() {
let path = PathBuf::from("/home/Dev");
let normalized = normalize_for_wsl_with_flag(path.clone(), true);

assert_eq!(normalized, path);
}
}
}
6 changes: 5 additions & 1 deletion codex-rs/tui/src/resume_picker.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ use codex_core::ConversationsPage;
use codex_core::Cursor;
use codex_core::INTERACTIVE_SESSION_SOURCES;
use codex_core::RolloutRecorder;
use codex_core::path_utils;
use codex_protocol::items::TurnItem;
use color_eyre::eyre::Result;
use crossterm::event::KeyCode;
Expand Down Expand Up @@ -670,7 +671,10 @@ fn extract_session_meta_from_head(head: &[serde_json::Value]) -> (Option<PathBuf
}

fn paths_match(a: &Path, b: &Path) -> bool {
if let (Ok(ca), Ok(cb)) = (a.canonicalize(), b.canonicalize()) {
if let (Ok(ca), Ok(cb)) = (
path_utils::normalize_for_path_comparison(a),
path_utils::normalize_for_path_comparison(b),
) {
return ca == cb;
}
a == b
Expand Down
6 changes: 5 additions & 1 deletion codex-rs/tui2/src/resume_picker.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ use codex_core::ConversationsPage;
use codex_core::Cursor;
use codex_core::INTERACTIVE_SESSION_SOURCES;
use codex_core::RolloutRecorder;
use codex_core::path_utils;
use codex_protocol::items::TurnItem;
use color_eyre::eyre::Result;
use crossterm::event::KeyCode;
Expand Down Expand Up @@ -670,7 +671,10 @@ fn extract_session_meta_from_head(head: &[serde_json::Value]) -> (Option<PathBuf
}

fn paths_match(a: &Path, b: &Path) -> bool {
if let (Ok(ca), Ok(cb)) = (a.canonicalize(), b.canonicalize()) {
if let (Ok(ca), Ok(cb)) = (
path_utils::normalize_for_path_comparison(a),
path_utils::normalize_for_path_comparison(b),
) {
return ca == cb;
}
a == b
Expand Down
Loading