Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 6 additions & 36 deletions src/patch/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,29 +24,17 @@ const NO_NEWLINE_AT_EOF: &str = "\\ No newline at end of file";

/// Representation of all the differences between two files
///
/// # Parsing modes
/// # Parsing behavior
///
/// `Patch` provides two parsing modes with different strictness levels,
/// modeled after the behavior of GNU patch and `git apply`:
/// [`from_str`] and [`from_bytes`] follow `git apply` behavior:
/// trailing non-patch content after a complete hunk is ignored,
/// but orphaned hunk headers hidden behind trailing content are rejected.
///
/// | Scenario | GNU patch | git apply | [`from_str`] | [`from_str_strict`] |
/// |-----------------------------------|-------------|-----------|--------------|---------------------|
/// | Junk after all hunks are complete | Ignores | Ignores | Ignores | Ignores |
/// | Junk between hunks | Ignores[^1] | Errors | Ignores[^1] | Errors |
///
/// [^1]: "Ignores" here means silently stopping at the junk.
/// Only hunks before it are parsed; later hunks are dropped.
///
/// [`from_str`] and [`from_bytes`] follow GNU patch behavior,
/// silently ignoring non-patch content after a hunk's line counts are satisfied.
///
/// [`from_str_strict`] and [`from_bytes_strict`] follow `git apply` behavior,
/// additionally rejecting orphaned hunk headers hidden behind trailing content.
/// For parsing multi-file patches, use [`PatchSet`] instead.
///
/// [`from_str`]: Patch::from_str
/// [`from_bytes`]: Patch::from_bytes
/// [`from_str_strict`]: Patch::from_str_strict
/// [`from_bytes_strict`]: Patch::from_bytes_strict
/// [`PatchSet`]: crate::patch_set::PatchSet
#[derive(PartialEq, Eq)]
pub struct Patch<'a, T: ToOwned + ?Sized> {
// TODO GNU patch is able to parse patches without filename headers.
Expand Down Expand Up @@ -150,31 +138,13 @@ impl<'a> Patch<'a, str> {
pub fn from_str(s: &'a str) -> Result<Patch<'a, str>, ParsePatchError> {
parse::parse(s)
}

/// Parse a `Patch` from a string in strict mode
///
/// Unlike [`Patch::from_str`],
/// this rejects orphaned hunk headers hidden after trailing content,
/// matching `git apply` behavior.
pub fn from_str_strict(s: &'a str) -> Result<Patch<'a, str>, ParsePatchError> {
parse::parse_strict(s)
}
}

impl<'a> Patch<'a, [u8]> {
/// Parse a `Patch` from bytes
pub fn from_bytes(s: &'a [u8]) -> Result<Patch<'a, [u8]>, ParsePatchError> {
parse::parse_bytes(s)
}

/// Parse a `Patch` from bytes in strict mode
///
/// Unlike [`Patch::from_bytes`],
/// this rejects orphaned hunk headers hidden after trailing content,
/// matching `git apply` behavior.
pub fn from_bytes_strict(s: &'a [u8]) -> Result<Patch<'a, [u8]>, ParsePatchError> {
parse::parse_bytes_strict(s)
}
}

impl<T: ToOwned + ?Sized> Clone for Patch<'_, T> {
Expand Down
15 changes: 0 additions & 15 deletions src/patch/parse.rs
Original file line number Diff line number Diff line change
Expand Up @@ -92,27 +92,12 @@ impl<'a, T: Text + ?Sized> Parser<'a, T> {
}
}

// TODO: make a better API for lib consumers
//
// Too many different variants of `parse*` functions here.
// And that also propogate to `Patch::from_{str,bytes}{,_strict}`.

pub fn parse(input: &str) -> Result<Patch<'_, str>> {
let (result, _consumed) = parse_one(input, ParseOpts::default());
result
}

pub fn parse_strict(input: &str) -> Result<Patch<'_, str>> {
let (result, _consumed) = parse_one(input, ParseOpts::default().reject_orphaned_hunks());
result
}

pub fn parse_bytes(input: &[u8]) -> Result<Patch<'_, [u8]>> {
let (result, _consumed) = parse_one(input, ParseOpts::default());
result
}

pub fn parse_bytes_strict(input: &[u8]) -> Result<Patch<'_, [u8]>> {
let (result, _consumed) = parse_one(input, ParseOpts::default().reject_orphaned_hunks());
result
}
Expand Down
37 changes: 17 additions & 20 deletions src/patch/tests.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
use super::error::ParsePatchErrorKind;
use super::parse::parse;
use super::parse::parse_bytes;
use super::parse::parse_bytes_strict;
use super::parse::parse_strict;
use alloc::format;
use alloc::string::ToString;

Expand Down Expand Up @@ -103,12 +101,10 @@ some trailing garbage
}

#[test]
fn garbage_between_hunks_stops_parsing() {
// GNU patch would try to parse the second @@ as a new patch
// and fail because there's no `---` header.
//
// diffy `Patch` is a single patch parser, so should just ignore everything
// after the first complete hunk when garbage is encountered.
fn garbage_between_hunks_rejects_orphaned_header() {
// Junk between hunks hides the second @@ header.
// This is rejected because the orphaned hunk header
// indicates a malformed patch.
let s = "\
--- a/file.txt
+++ b/file.txt
Expand All @@ -120,9 +116,10 @@ not a hunk line
-b
+B
";
let patch = parse(s).unwrap();
// Only first hunk is parsed; second @@ is ignored as garbage
assert_eq!(patch.hunks().len(), 1);
assert_eq!(
parse(s).unwrap_err().kind,
ParsePatchErrorKind::OrphanedHunkHeader,
);
}

#[test]
Expand All @@ -146,12 +143,12 @@ trailing garbage

// Strict mode (git-apply behavior): rejects orphaned hunk headers
// hidden behind trailing content, but allows plain trailing junk.
mod strict_mode {
mod trailing_content {
use super::*;

#[test]
fn trailing_junk_allowed() {
// git apply accepts trailing junk after all hunks
// Trailing junk after all hunks is accepted
let s = "\
--- a/file.txt
+++ b/file.txt
Expand All @@ -160,7 +157,7 @@ mod strict_mode {
+new
this is trailing garbage
";
let patch = parse_strict(s).unwrap();
let patch = parse(s).unwrap();
assert_eq!(patch.hunks().len(), 1);
}

Expand All @@ -174,13 +171,13 @@ this is trailing garbage
+new
this is trailing garbage
";
let patch = parse_bytes_strict(&s[..]).unwrap();
let patch = parse_bytes(&s[..]).unwrap();
assert_eq!(patch.hunks().len(), 1);
}

#[test]
fn orphaned_hunk_header_after_junk() {
// Junk between hunks hides the second @@ — strict rejects this
// Junk between hunks hides the second @@ — rejected
// since git apply errors with "patch fragment without header".
let s = "\
--- a/file.txt
Expand All @@ -194,7 +191,7 @@ not a hunk line
+B
";
assert_eq!(
parse_strict(s).unwrap_err().kind,
parse(s).unwrap_err().kind,
ParsePatchErrorKind::OrphanedHunkHeader,
);
}
Expand All @@ -208,7 +205,7 @@ not a hunk line
-old
+new
";
let patch = parse_strict(s).unwrap();
let patch = parse(s).unwrap();
assert_eq!(patch.hunks().len(), 1);
}

Expand All @@ -224,7 +221,7 @@ not a hunk line
-b
+B
";
let patch = parse_strict(s).unwrap();
let patch = parse(s).unwrap();
assert_eq!(patch.hunks().len(), 2);
}

Expand All @@ -240,7 +237,7 @@ garbage before hunk complete
line 3
";
assert_eq!(
parse_strict(s).unwrap_err().kind,
parse(s).unwrap_err().kind,
ParsePatchErrorKind::UnexpectedHunkLine,
);
}
Expand Down