apply_patch: seek_sequence ignores its documented EOF fallback, failing valid "*** End of File" hunks

Open 💬 2 comments Opened Jul 18, 2026 by SEPURI-SAI-KRISHNA

What issue are you seeing?

codex-rs/apply-patch/src/seek_sequence.rs documents a two-stage search when
eof is true, but only implements the first stage — the code contradicts its own
doc comment.

The doc comment (lines 1–6) states:

> When eof is true, we first try starting at the end-of-file (so that patterns
> intended to match file endings are applied at the end), and fall back to
> searching from start if needed.

The implementation replaces the search origin instead of falling back to it:

``rust
let search_start = if eof && lines.len() >= pattern.len() {
lines.len() - pattern.len() // only ever the tail offset
} else {
start
};
// every matching pass loops
for i in search_start..=lines.len()-pattern.len()
`
When
eof is true, all four matching passes (exact, rstrip, trim, unicode-normalize)
only test the single end-of-file offset. There is no fallback to
start, so if the
pattern is not literally the final lines of the file,
seek_sequence returns None`
even though the pattern exists earlier.

Impact: compute_replacements (lib.rs:767, 779) calls this with
eof = chunk.is_end_of_file, i.e. for every update hunk ending in *** End of File.
If such a hunk's old_lines are not exactly the file's last lines (trailing lines
follow the edited region, or a slight miscount), the whole patch fails with
Failed to find expected lines — instead of matching the region that does exist. The
documented leniency that would rescue it is simply absent, and no existing test
covers the eof-fallback path.

Observed on current main (commit b8b61bc69).

What steps can reproduce the bug?

seek_sequence is pub(crate), so the precise reproduction is a unit test added to
its tests module (in codex-rs/apply-patch/src/seek_sequence.rs). The pattern
exists at the start of the file but not at the end:

``rust
#[test]
fn test_eof_falls_back_to_start_when_not_anchored_to_end() {
let lines = to_vec(&["alpha", "beta", "gamma", "delta"]);
let pattern = to_vec(&["alpha", "beta"]);
assert_eq!(
seek_sequence(&lines, &pattern, /*start*/ 0, /*eof*/ true),
Some(0)
);
}
``
Run:

``
cargo nextest run -p codex-apply-patch -E 'test(test_eof_falls_back_to_start)'
``

Actual result on unmodified main:

``
thread '...test_eof_falls_back_to_start_when_not_anchored_to_end' panicked at
apply-patch/src/seek_sequence.rs:
assertion
left == right failed
left: None
right: Some(0)
test result: FAILED. 0 passed; 1 failed
``

For comparison, the same lookup with eof = false returns Some(0) as expected, so
the failure is specific to the eof path.

What is the expected behavior?

Per the function's own documentation, an eof search should try the end-of-file
position first and then fall back to a full search from start. So the assertion
above should return Some(0), while a repeated pattern should still prefer its
end-of-file occurrence.

A minimal fix — fall back to a full search only after the eof-anchored passes fail,
which preserves EOF-preference:

  // immediately before the final `None`:
  if eof && search_start > start {
      return seek_sequence(lines, pattern, start, /*eof*/ false);
  }
  ```

  With this change the full crate suite passes, including two added regression tests
  (the fallback case above, plus an EOF-preference case asserting a repeated pattern
  still matches the last occurrence under `eof = true`):

  ```
  cargo nextest run -p codex-apply-patch
  Summary  88 tests run: 88 passed, 0 skipped
  ```

### Additional information

 Additional information

  Root cause is a documentation/implementation mismatch introduced by the `eof`
  anchoring optimization: it narrows the search to the tail offset without preserving
  the documented fallback, and no test exercised the fallback path so the regression
  went unnoticed.

  I have a branch-ready fix plus the two regression tests prepared and verified locally
  (fails before, 88/88 pass after). Happy to open a PR if the team would like — noting
  docs/contributing.md makes external code contributions invitation-only, so I'm filing
  this as a bug report first.

View original on GitHub ↗

This issue has 2 comments on GitHub. Read the full discussion on GitHub ↗