From fab3e245ff7c0b43d0b3ec0e61f9ce0270ced72f Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Thu, 2 Jul 2026 15:48:25 -0400 Subject: [PATCH] Support CR line endings in apply_patch --- codex-rs/apply-patch/src/text_file.rs | 25 ++++++++++++++++-------- codex-rs/apply-patch/tests/suite/tool.rs | 16 +++++++++++++-- 2 files changed, 31 insertions(+), 10 deletions(-) diff --git a/codex-rs/apply-patch/src/text_file.rs b/codex-rs/apply-patch/src/text_file.rs index da5fa237f3..b7d598ca97 100644 --- a/codex-rs/apply-patch/src/text_file.rs +++ b/codex-rs/apply-patch/src/text_file.rs @@ -4,6 +4,7 @@ pub(super) type Replacement = (usize, usize, Vec); enum LineEnding { Lf, CrLf, + Cr, } impl LineEnding { @@ -11,6 +12,7 @@ impl LineEnding { match self { Self::Lf => "\n", Self::CrLf => "\r\n", + Self::Cr => "\r", } } } @@ -34,20 +36,27 @@ impl SourceFile { let mut lines = Vec::new(); let mut preferred_ending = None; let mut line_start = 0; + let mut cursor = 0; - for (newline, _) in contents.match_indices('\n') { - let line = &contents[line_start..newline]; - let (text, ending) = if let Some(text) = line.strip_suffix('\r') { - (text, LineEnding::CrLf) - } else { - (line, LineEnding::Lf) + while cursor < contents.len() { + let (ending, ending_len) = match contents.as_bytes()[cursor] { + b'\r' if contents.as_bytes().get(cursor + 1) == Some(&b'\n') => { + (LineEnding::CrLf, 2) + } + b'\r' => (LineEnding::Cr, 1), + b'\n' => (LineEnding::Lf, 1), + _ => { + cursor += 1; + continue; + } }; preferred_ending.get_or_insert(ending); lines.push(SourceLine { - text: text.to_string(), + text: contents[line_start..cursor].to_string(), ending: Some(ending), }); - line_start = newline + 1; + cursor += ending_len; + line_start = cursor; } if line_start < contents.len() { diff --git a/codex-rs/apply-patch/tests/suite/tool.rs b/codex-rs/apply-patch/tests/suite/tool.rs index 3a50bdefad..9ec880931d 100644 --- a/codex-rs/apply-patch/tests/suite/tool.rs +++ b/codex-rs/apply-patch/tests/suite/tool.rs @@ -96,6 +96,18 @@ fn test_apply_patch_cli_preserves_crlf_from_target_file() -> anyhow::Result<()> ) } +#[test] +fn test_apply_patch_cli_preserves_cr_from_target_file() -> anyhow::Result<()> { + let patch = "*** Begin Patch\n*** Update File: cr.txt\n@@\n-one\n+uno\n@@\n two\n+\n+between\n three\n*** End Patch"; + + assert_apply_patch_updates_file( + "cr.txt", + b"one\rtwo\rthree\r", + patch, + b"uno\rtwo\r\rbetween\rthree\r", + ) +} + #[test] fn test_apply_patch_cli_preserves_change_order_with_repeated_lines() -> anyhow::Result<()> { let patch = @@ -118,9 +130,9 @@ fn test_apply_patch_cli_preserves_untouched_mixed_line_endings() -> anyhow::Resu assert_apply_patch_updates_file( "mixed.txt", - b"one\r\ntwo\nthree\r\nfour\n", + b"one\r\ntwo\rthree\nfour\r\n", patch, - b"one\r\ntwo\nTHREE\r\nfour\n", + b"one\r\ntwo\rTHREE\r\nfour\r\n", ) }