From 350b24d1773b805bc011534a9e52aa2f1fce523e Mon Sep 17 00:00:00 2001 From: starr-openai Date: Fri, 3 Apr 2026 23:13:55 -0700 Subject: [PATCH] codex: normalize apply_patch fs paths against symlinked ancestors --- codex-rs/core/src/apply_patch.rs | 23 +++++++++++++++++++++-- codex-rs/core/src/apply_patch_tests.rs | 19 +++++++++++++++++++ 2 files changed, 40 insertions(+), 2 deletions(-) diff --git a/codex-rs/core/src/apply_patch.rs b/codex-rs/core/src/apply_patch.rs index 2e928b8812..4472090c6d 100644 --- a/codex-rs/core/src/apply_patch.rs +++ b/codex-rs/core/src/apply_patch.rs @@ -163,12 +163,31 @@ impl ApplyPatchFileSystem for EnvironmentApplyPatchFileSystem { } fn absolute_path(path: &std::path::Path) -> std::result::Result { - AbsolutePathBuf::from_absolute_path(path).map_err(|error| { + let path = AbsolutePathBuf::from_absolute_path(path).map_err(|error| { ApplyPatchError::io_error( format!("Expected absolute path for apply_patch: {}", path.display()), io::Error::new(io::ErrorKind::InvalidInput, error.to_string()), ) - }) + })?; + Ok(normalize_existing_ancestor_path(path)) +} + +fn normalize_existing_ancestor_path(path: AbsolutePathBuf) -> AbsolutePathBuf { + let raw_path = path.to_path_buf(); + for ancestor in raw_path.ancestors() { + let Ok(canonical_ancestor) = ancestor.canonicalize() else { + continue; + }; + let Ok(suffix) = raw_path.strip_prefix(ancestor) else { + continue; + }; + if let Ok(normalized_path) = + AbsolutePathBuf::from_absolute_path(canonical_ancestor.join(suffix)) + { + return normalized_path; + } + } + path } pub(crate) async fn apply_patch( diff --git a/codex-rs/core/src/apply_patch_tests.rs b/codex-rs/core/src/apply_patch_tests.rs index 8441ff7439..1dd0c8a180 100644 --- a/codex-rs/core/src/apply_patch_tests.rs +++ b/codex-rs/core/src/apply_patch_tests.rs @@ -31,6 +31,25 @@ fn convert_apply_patch_maps_add_variant() { ); } +#[cfg(unix)] +#[test] +fn absolute_path_normalizes_existing_symlink_ancestor() { + use std::os::unix::fs::symlink; + + let tmp = tempdir().expect("tempdir"); + let real_root = tmp.path().join("real"); + let link_root = tmp.path().join("link"); + std::fs::create_dir_all(&real_root).expect("create real root"); + symlink(&real_root, &link_root).expect("create symlink"); + + let path = link_root.join("nested").join("file.txt"); + let got = absolute_path(path.as_path()).expect("normalize absolute path"); + let expected = AbsolutePathBuf::from_absolute_path(real_root.join("nested/file.txt")) + .expect("expected normalized path"); + + assert_eq!(got, expected); +} + #[derive(Default)] struct RecordingExecutorFileSystem { raw_reads: Mutex>,