Skip to content

Do not panic on a repeated or leading no-newline marker - #75

Merged
jelmer merged 1 commit into
breezy-team:masterfrom
youdie006:no-newline-marker-panic
Sep 1, 2026
Merged

jelmer merged 1 commit into
breezy-team:masterfrom
youdie006:no-newline-marker-panic

Conversation

@youdie006

Copy link
Copy Markdown
Contributor

parse_patch panics on two shapes of \ No newline at end of file, even though it returns
Result<_, Error>.

src/unified.rs:128 in iter_lines_handle_nl:

if line == NO_NL {
    if let Some(last) = last_line.as_mut() {
        assert!(last.ends_with(b"\n"));
        // Drop the last newline from `last`
        *last = &last[..last.len() - 1];
    } else {
        panic!("No newline indicator without previous line");
    }

The first marker strips the trailing newline from the held line. A second consecutive marker
then re-asserts on the line that no longer has one:

input (81 bytes):
--- \n+++ \n@@ -1 +1 @@\n a\n\ No newline at end of file\n\ No newline at end of file\n

thread 'main' panicked at src/unified.rs:130:21:
assertion failed: last.ends_with(b"\n")
parse_patch -> PANIC

A marker as the first line takes the other branch:

input: \ No newline at end of file\n

thread 'main' panicked at src/unified.rs:134:21:
No newline indicator without previous line
parse_patch -> PANIC

Both fire in debug and release — assert! is not debug-only. parse_patch (src/unified.rs:476)
and parse_patches (src/unified.rs:1165) both return Result, so a caller handling malformed
input correctly still aborts.

Change

Neither case has anything to strip, and neither is fatal: a repeated marker is redundant, and a
leading one refers to nothing. Using strip_suffix makes both a no-op without changing the public
signature, which is impl Iterator<Item = &[u8]> and cannot carry an error:

if line == NO_NL {
    // A repeated or leading marker has nothing to strip; it is not fatal.
    if let Some(last) = last_line.as_mut() {
        if let Some(stripped) = last.strip_suffix(b"\n") {
            *last = stripped;
        }
    }
}

That preserves the documented behaviour — a line that had no terminating newline is produced
without one — and the existing test_iter_lines_handle_nl still passes unchanged.

Verification

Two regression tests added next to it. Red with only the iter_lines_handle_nl body reverted (both
panic at src/unified.rs:130), green with the change: 254 passed, 0 failed plus the 2 doc
tests. cargo fmt --check clean.

Found by driving the five targets in fuzz/fuzz_targets/ directly — grep -rniE "fuzz" .github/workflows/ returns nothing, so the harness added in #65 has never run. 200,000 mutated
inputs produced 86 panics, and a run with a location-counting hook attributed every one to this
single site.

Adjacent, not included here

src/unified.rs:1925 and :1936 compute self.orig_pos - 1 unguarded. @@ -0,0 +1,3 @@ — what
GNU diff emits for an added file, and which this repo parses in its own test at src/apply.rs:729
— makes that underflow: a debug panic, and in release it wraps so shift_to_mod returns
Some(0) for every position. src/apply.rs:170 and :274 already write the same expression as
orig_pos.saturating_sub(1). Happy to send that separately if you'd like it.


Disclosure: prepared with AI assistance; I verified both reproductions, the red/green and the suite
runs myself.

iter_lines_handle_nl asserted that the held line ends in a newline before
stripping it, so a second consecutive marker aborted, and a marker with no
line before it hit an unconditional panic. Both are reachable from
parse_patch, which returns Result.
@youdie006
youdie006 requested a review from jelmer as a code owner September 1, 2026 03:22
@jelmer
jelmer merged commit 341ce8c into breezy-team:master Sep 1, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants