mirror of
https://github.com/openai/codex.git
synced 2026-10-23 18:08:45 +00:00
## Why Updating a CRLF file with `apply_patch` previously normalized its line endings to LF unless preservation was explicitly enabled. Preserve the file's existing line endings without requiring an opt-in. ## What changed - Use line-ending-preserving updates for both tool calls and standalone `apply_patch` invocations, removing the legacy normalization mode. - Mark `apply_patch_preserve_line_endings` as removed. - Always set `CODEX_APPLY_PATCH_PRESERVE_LINE_ENDINGS=1` in child environments for compatibility with older patch executables, overriding stale values. ## Testing Update CLI, tool-call, shell-heredoc, and app-server tests to check CRLF preservation by default. Keep coverage for overriding stale compatibility environment values. GitOrigin-RevId: 96d0b0e5350faa3fc7a45d63627a132657b44523
151 lines
5.4 KiB
Rust
151 lines
5.4 KiB
Rust
use codex_apply_patch::ApplyPatchOptions;
|
|
use codex_apply_patch::MaybeApplyPatchVerified;
|
|
use codex_apply_patch::apply_patch_with_options;
|
|
use codex_apply_patch::parse_patch;
|
|
use codex_apply_patch::verify_apply_patch_args;
|
|
use codex_exec_server::LOCAL_FS;
|
|
use codex_utils_path_uri::PathUri;
|
|
use pretty_assertions::assert_eq;
|
|
use std::fs;
|
|
use std::os::unix::fs::symlink;
|
|
|
|
#[tokio::test]
|
|
async fn no_follow_rejects_leaf_and_ancestor_links_for_every_patch_operation() -> anyhow::Result<()>
|
|
{
|
|
for path in ["link.txt", "linked/victim.txt"] {
|
|
for body in [
|
|
format!("*** Add File: {path}\n+changed"),
|
|
format!("*** Update File: {path}\n@@\n-original\n+changed"),
|
|
format!("*** Delete File: {path}"),
|
|
format!("*** Update File: {path}\n*** Move to: moved.txt\n@@\n-original\n+changed"),
|
|
format!("*** Update File: source.txt\n*** Move to: {path}\n@@\n-original\n+changed"),
|
|
"*** Add File: linked/new/nested.txt\n+changed".to_string(),
|
|
"*** Update File: source.txt\n*** Move to: linked/new/nested.txt\n@@\n-original\n+changed".to_string(),
|
|
] {
|
|
let temp = tempfile::tempdir()?;
|
|
// macOS's temporary directory can itself contain a /var symlink.
|
|
let root = temp.path().canonicalize()?;
|
|
let work = root.join("work");
|
|
let outside = root.join("outside");
|
|
fs::create_dir(&work)?;
|
|
fs::create_dir(&outside)?;
|
|
fs::write(outside.join("victim.txt"), "original\n")?;
|
|
fs::write(work.join("source.txt"), "original\n")?;
|
|
symlink(outside.join("victim.txt"), work.join("link.txt"))?;
|
|
symlink(&outside, work.join("linked"))?;
|
|
|
|
let patch = format!("*** Begin Patch\n{body}\n*** End Patch");
|
|
let failure = apply_patch_with_options(
|
|
&patch,
|
|
ApplyPatchOptions {
|
|
follow_symlinks: false,
|
|
},
|
|
&PathUri::from_host_native_path(&work)?,
|
|
&mut Vec::new(),
|
|
&mut Vec::new(),
|
|
LOCAL_FS.as_ref(),
|
|
/*sandbox*/ None,
|
|
)
|
|
.await
|
|
.expect_err(&body);
|
|
|
|
assert!(failure.delta().is_empty(), "{body}");
|
|
assert_eq!(fs::read_to_string(outside.join("victim.txt"))?, "original\n");
|
|
assert_eq!(fs::read_to_string(work.join("source.txt"))?, "original\n");
|
|
assert!(!outside.join("new").exists(), "{body}");
|
|
assert!(!work.join("moved.txt").exists(), "{body}");
|
|
assert!(fs::symlink_metadata(work.join("link.txt"))?.is_symlink());
|
|
}
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn no_follow_rechecks_paths_after_verification() -> anyhow::Result<()> {
|
|
let temp = tempfile::tempdir()?;
|
|
let root = temp.path().canonicalize()?;
|
|
fs::create_dir(root.join("approved"))?;
|
|
fs::create_dir(root.join("outside"))?;
|
|
fs::write(root.join("approved/file.txt"), "original\n")?;
|
|
fs::write(root.join("outside/file.txt"), "original\n")?;
|
|
let cwd = PathUri::from_host_native_path(&root)?;
|
|
let patch = "*** Begin Patch\n*** Update File: approved/file.txt\n@@\n-original\n+changed\n*** End Patch";
|
|
let MaybeApplyPatchVerified::Body(action) = verify_apply_patch_args(
|
|
parse_patch(patch)?,
|
|
&cwd,
|
|
LOCAL_FS.as_ref(),
|
|
/*sandbox*/ None,
|
|
)
|
|
.await
|
|
else {
|
|
panic!("regular-file patch should verify");
|
|
};
|
|
fs::rename(root.join("approved"), root.join("original"))?;
|
|
symlink(root.join("outside"), root.join("approved"))?;
|
|
|
|
apply_patch_with_options(
|
|
&action.patch,
|
|
ApplyPatchOptions {
|
|
follow_symlinks: false,
|
|
},
|
|
&cwd,
|
|
&mut Vec::new(),
|
|
&mut Vec::new(),
|
|
LOCAL_FS.as_ref(),
|
|
/*sandbox*/ None,
|
|
)
|
|
.await
|
|
.expect_err("swapped ancestor must not be followed");
|
|
assert_eq!(
|
|
fs::read_to_string(root.join("outside/file.txt"))?,
|
|
"original\n"
|
|
);
|
|
assert_eq!(
|
|
fs::read_to_string(root.join("original/file.txt"))?,
|
|
"original\n"
|
|
);
|
|
Ok(())
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn no_follow_applies_regular_files_and_default_still_follows_links() -> anyhow::Result<()> {
|
|
let temp = tempfile::tempdir()?;
|
|
let root = temp.path().canonicalize()?;
|
|
let cwd = PathUri::from_host_native_path(&root)?;
|
|
let patch = "*** Begin Patch\n*** Add File: new/file.txt\n+original\n*** Update File: new/file.txt\n*** Move to: moved/file.txt\n@@\n-original\n+changed\n*** Add File: remove.txt\n+remove\n*** Delete File: remove.txt\n*** End Patch";
|
|
apply_patch_with_options(
|
|
patch,
|
|
ApplyPatchOptions {
|
|
follow_symlinks: false,
|
|
},
|
|
&cwd,
|
|
&mut Vec::new(),
|
|
&mut Vec::new(),
|
|
LOCAL_FS.as_ref(),
|
|
/*sandbox*/ None,
|
|
)
|
|
.await?;
|
|
assert_eq!(
|
|
fs::read_to_string(root.join("moved/file.txt"))?,
|
|
"changed\n"
|
|
);
|
|
assert!(!root.join("new/file.txt").exists());
|
|
assert!(!root.join("remove.txt").exists());
|
|
|
|
symlink(root.join("moved/file.txt"), root.join("link.txt"))?;
|
|
codex_apply_patch::apply_patch(
|
|
"*** Begin Patch\n*** Update File: link.txt\n@@\n-changed\n+followed\n*** End Patch",
|
|
&cwd,
|
|
&mut Vec::new(),
|
|
&mut Vec::new(),
|
|
LOCAL_FS.as_ref(),
|
|
/*sandbox*/ None,
|
|
)
|
|
.await?;
|
|
assert_eq!(
|
|
fs::read_to_string(root.join("moved/file.txt"))?,
|
|
"followed\n"
|
|
);
|
|
Ok(())
|
|
}
|