apply_patch: seek_sequence ignores its documented EOF fallback, failing valid "*** End of File" hunks
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:
``rustfor i in search_start..=lines.len()-pattern.len()
let search_start = if eof && lines.len() >= pattern.len() {
lines.len() - pattern.len() // only ever the tail offset
} else {
start
};
// every matching pass loops `
eof
When is true, all four matching passes (exact, rstrip, trim, unicode-normalize)start
only test the single end-of-file offset. There is no fallback to , so if theseek_sequence
pattern is not literally the final lines of the file, 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:
``left == right
thread '...test_eof_falls_back_to_start_when_not_anchored_to_end' panicked at
apply-patch/src/seek_sequence.rs:
assertion 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.This issue has 2 comments on GitHub. Read the full discussion on GitHub ↗