From d4fc6e6b540a88a09f72e622735deb156e269fb3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jelmer=20Vernoo=C4=B3?= Date: Wed, 15 Apr 2026 21:39:44 +0100 Subject: [PATCH] unified: error on truncated hunk bodies instead of silent termination MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit iter_hunks used `iter_lines.next()?` inside the per-hunk body loop. When a hunk header declared more orig/mod lines than the input provided, the `?` propagated the inner None as a None from the from_fn closure — which terminated iteration without recording the partial hunk and without returning an error. Callers couldn't tell "valid empty input" from "truncated patch", and miscounted ranges upstream were silently swallowed. Replace the `?` with an explicit None match that returns PatchSyntax("Truncated hunk body"). Two regression tests cover both forms: a partial body and an EOF immediately after the header. --- src/unified.rs | 46 +++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 45 insertions(+), 1 deletion(-) diff --git a/src/unified.rs b/src/unified.rs index 81870a8..861d71b 100644 --- a/src/unified.rs +++ b/src/unified.rs @@ -296,7 +296,22 @@ where let mut orig_size = 0; let mut mod_size = 0; while orig_size < new_hunk.orig_range || mod_size < new_hunk.mod_range { - let line = iter_lines.next()?; + let line = match iter_lines.next() { + Some(line) => line, + None => { + // Truncated hunk body: the header + // promised more lines than the input + // provides. Surface this as an + // error — silently dropping the + // partial hunk masks both malformed + // patches and miscounted ranges + // upstream. + return Some(Err(Error::PatchSyntax( + "Truncated hunk body", + Vec::new().into_boxed_slice(), + ))); + } + }; match HunkLine::parse_line(line) { Err(_) => { return Some(Err(Error::PatchSyntax( @@ -375,6 +390,35 @@ mod iter_hunks_tests { assert_eq!(&expected_hunk, hunks.first().unwrap()); } + + /// Regression: a hunk header that promises more body lines than + /// the input contains used to silently terminate iteration with + /// no error and no recorded hunk. That made it impossible for + /// callers to distinguish "valid empty input" from "truncated + /// patch". Now it must surface as `PatchSyntax`. + #[test] + fn test_iter_hunks_truncated_body_is_error() { + // Header promises 2 orig + 2 mod lines, but only 2 body + // lines (-foo, +bar) are present — orig_size and mod_size + // each reach 1, never 2. + let mut lines = super::splitlines(b"@@ -1,2 +1,2 @@\n-foo\n+bar\n"); + let result: Result, Error> = super::iter_hunks(&mut lines).collect(); + match result { + Err(Error::PatchSyntax(msg, _)) => { + assert_eq!(msg, "Truncated hunk body"); + } + other => panic!("expected Err(PatchSyntax), got {:?}", other), + } + } + + /// Companion: an EOF immediately after the hunk header (no body + /// at all) is also a truncation. + #[test] + fn test_iter_hunks_no_body_is_error() { + let mut lines = super::splitlines(b"@@ -1,1 +1,1 @@\n"); + let result: Result, Error> = super::iter_hunks(&mut lines).collect(); + assert!(matches!(result, Err(Error::PatchSyntax(_, _)))); + } } /// Parse a patch file