use super::Hunk;
use super::HunkRange;
use super::Line;
use super::NO_NEWLINE_AT_EOF;
use super::error::ParsePatchError;
use super::error::ParsePatchErrorKind;
use crate::patch::Patch;
use crate::utils::LineIter;
use crate::utils::Text;
use crate::utils::escaped_filename;
use alloc::borrow::Cow;
use alloc::vec::Vec;
type Result<T, E = ParsePatchError> = core::result::Result<T, E>;
#[derive(Clone, Copy)]
pub(crate) struct ParseOpts {
skip_preamble: bool,
reject_orphaned_hunks: bool,
}
impl Default for ParseOpts {
fn default() -> Self {
Self {
skip_preamble: true,
reject_orphaned_hunks: false,
}
}
}
impl ParseOpts {
pub(crate) fn no_skip_preamble(mut self) -> Self {
self.skip_preamble = false;
self
}
pub(crate) fn reject_orphaned_hunks(mut self) -> Self {
self.reject_orphaned_hunks = true;
self
}
}
struct Parser<'a, T: Text + ?Sized> {
lines: core::iter::Peekable<LineIter<'a, T>>,
offset: usize,
}
impl<'a, T: Text + ?Sized> Parser<'a, T> {
fn new(input: &'a T) -> Self {
Self {
lines: LineIter::new(input).peekable(),
offset: 0,
}
}
fn peek(&mut self) -> Option<&&'a T> {
self.lines.peek()
}
fn offset(&self) -> usize {
self.offset
}
fn next(&mut self) -> Result<&'a T> {
let line = self
.lines
.next()
.ok_or_else(|| self.error(ParsePatchErrorKind::UnexpectedEof))?;
self.offset += line.len();
Ok(line)
}
fn error(&self, kind: ParsePatchErrorKind) -> ParsePatchError {
ParsePatchError::new(kind, self.offset..self.offset)
}
fn error_at(&self, kind: ParsePatchErrorKind, offset: usize) -> ParsePatchError {
ParsePatchError::new(kind, offset..offset)
}
}
pub fn parse(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().reject_orphaned_hunks());
result
}
pub(crate) fn parse_one<T: Text + ?Sized>(
input: &T,
opts: ParseOpts,
) -> (Result<Patch<'_, T>>, usize) {
let mut parser = Parser::new(input);
let header = match patch_header(&mut parser, &opts) {
Ok(h) => h,
Err(e) => return (Err(e), parser.offset()),
};
let hunks = match hunks(&mut parser) {
Ok(h) => h,
Err(e) => return (Err(e), parser.offset()),
};
if opts.reject_orphaned_hunks {
if let Err(e) = reject_orphaned_hunk_headers(&mut parser) {
return (Err(e), parser.offset());
}
}
(Ok(Patch::new(header.0, header.1, hunks)), parser.offset())
}
#[expect(clippy::type_complexity)]
fn patch_header<'a, T: Text + ?Sized>(
parser: &mut Parser<'a, T>,
opts: &ParseOpts,
) -> Result<(Option<Cow<'a, T>>, Option<Cow<'a, T>>)> {
if opts.skip_preamble {
skip_header_preamble(parser)?;
}
let mut filename1 = None;
let mut filename2 = None;
while let Some(line) = parser.peek() {
if line.starts_with("--- ") {
if filename1.is_some() {
return Err(parser.error(ParsePatchErrorKind::MultipleOriginalHeaders));
}
filename1 = Some(parse_filename("--- ", parser.next()?)?);
} else if line.starts_with("+++ ") {
if filename2.is_some() {
return Err(parser.error(ParsePatchErrorKind::MultipleModifiedHeaders));
}
filename2 = Some(parse_filename("+++ ", parser.next()?)?);
} else {
break;
}
}
Ok((filename1, filename2))
}
fn skip_header_preamble<T: Text + ?Sized>(parser: &mut Parser<'_, T>) -> Result<()> {
while let Some(line) = parser.peek() {
if line.starts_with("--- ") | line.starts_with("+++ ") | line.starts_with("@@ ") {
break;
}
parser.next()?;
}
Ok(())
}
fn parse_filename<'a, T: Text + ?Sized>(prefix: &str, line: &'a T) -> Result<Cow<'a, T>> {
let line = line
.strip_prefix(prefix)
.ok_or(ParsePatchErrorKind::InvalidFilename)?;
let filename = if let Some((filename, _)) = line.split_at_exclusive("\t") {
filename
} else if let Some((filename, _)) = line.split_at_exclusive("\n") {
filename
} else {
return Err(ParsePatchErrorKind::FilenameUnterminated.into());
};
let filename = escaped_filename(filename)?;
Ok(filename)
}
fn verify_hunks_in_order<T: ?Sized>(hunks: &[Hunk<'_, T>]) -> bool {
for hunk in hunks.windows(2) {
if hunk[0].old_range.end() > hunk[1].old_range.start()
|| hunk[0].new_range.end() > hunk[1].new_range.start()
{
return false;
}
}
true
}
fn reject_orphaned_hunk_headers<T: Text + ?Sized>(parser: &mut Parser<'_, T>) -> Result<()> {
while let Some(line) = parser.peek() {
if line.starts_with("@@ ") {
return Err(parser.error(ParsePatchErrorKind::OrphanedHunkHeader));
}
parser.next()?;
}
Ok(())
}
fn hunks<'a, T: Text + ?Sized>(parser: &mut Parser<'a, T>) -> Result<Vec<Hunk<'a, T>>> {
let mut hunks = Vec::new();
while parser.peek().is_some_and(|line| line.starts_with("@@ ")) {
hunks.push(hunk(parser)?);
}
if !verify_hunks_in_order(&hunks) {
return Err(parser.error(ParsePatchErrorKind::HunksOutOfOrder));
}
Ok(hunks)
}
fn hunk<'a, T: Text + ?Sized>(parser: &mut Parser<'a, T>) -> Result<Hunk<'a, T>> {
let hunk_start = parser.offset();
let header_line = parser.next()?;
let (range1, range2, function_context) =
hunk_header(header_line).map_err(|e| parser.error_at(e.kind, hunk_start))?;
let lines = hunk_lines(parser, range1.len, range2.len, hunk_start)?;
Ok(Hunk::new(range1, range2, function_context, lines))
}
fn hunk_header<T: Text + ?Sized>(input: &T) -> Result<(HunkRange, HunkRange, Option<&T>)> {
let input = input
.strip_prefix("@@ ")
.ok_or(ParsePatchErrorKind::InvalidHunkHeader)?;
let (ranges, function_context) = input
.split_at_exclusive(" @@")
.ok_or(ParsePatchErrorKind::HunkHeaderUnterminated)?;
let function_context = function_context.strip_prefix(" ");
let (range1, range2) = ranges
.split_at_exclusive(" ")
.ok_or(ParsePatchErrorKind::InvalidHunkHeader)?;
let range1 = range(
range1
.strip_prefix("-")
.ok_or(ParsePatchErrorKind::InvalidHunkHeader)?,
)?;
let range2 = range(
range2
.strip_prefix("+")
.ok_or(ParsePatchErrorKind::InvalidHunkHeader)?,
)?;
Ok((range1, range2, function_context))
}
fn range<T: Text + ?Sized>(s: &T) -> Result<HunkRange> {
let (start, len): (usize, usize) = if let Some((start, len)) = s.split_at_exclusive(",") {
(
start.parse().ok_or(ParsePatchErrorKind::InvalidRange)?,
len.parse().ok_or(ParsePatchErrorKind::InvalidRange)?,
)
} else {
(s.parse().ok_or(ParsePatchErrorKind::InvalidRange)?, 1)
};
start
.checked_add(len)
.ok_or(ParsePatchErrorKind::InvalidRange)?;
Ok(HunkRange::new(start, len))
}
fn hunk_lines<'a, T: Text + ?Sized>(
parser: &mut Parser<'a, T>,
expected_old: usize,
expected_new: usize,
hunk_start: usize,
) -> Result<Vec<Line<'a, T>>> {
let mut lines: Vec<Line<'a, T>> = Vec::new();
let mut no_newline_context = false;
let mut no_newline_delete = false;
let mut no_newline_insert = false;
let mut old_count = 0;
let mut new_count = 0;
while let Some(line) = parser.peek() {
let hunk_complete = old_count >= expected_old && new_count >= expected_new;
let line = if line.starts_with("@") {
break;
} else if no_newline_context {
if hunk_complete {
break;
}
return Err(parser.error(ParsePatchErrorKind::ExpectedEndOfHunk));
} else if let Some(line) = line.strip_prefix(" ") {
if hunk_complete {
break;
}
Line::Context(line)
} else if line.starts_with("\n") {
if hunk_complete {
break;
}
Line::Context(*line)
} else if let Some(line) = line.strip_prefix("-") {
if no_newline_delete {
return Err(parser.error(ParsePatchErrorKind::TooManyDeletedLines));
}
if hunk_complete {
break;
}
Line::Delete(line)
} else if let Some(line) = line.strip_prefix("+") {
if no_newline_insert {
return Err(parser.error(ParsePatchErrorKind::TooManyInsertedLines));
}
if hunk_complete {
break;
}
Line::Insert(line)
} else if line.starts_with(NO_NEWLINE_AT_EOF) {
let last_line = lines
.pop()
.ok_or_else(|| parser.error(ParsePatchErrorKind::UnexpectedNoNewlineMarker))?;
let modified = match last_line {
Line::Context(line) => {
no_newline_context = true;
Line::Context(strip_newline(line)?)
}
Line::Delete(line) => {
no_newline_delete = true;
Line::Delete(strip_newline(line)?)
}
Line::Insert(line) => {
no_newline_insert = true;
Line::Insert(strip_newline(line)?)
}
};
lines.push(modified);
parser.next()?;
continue;
} else {
if hunk_complete {
break;
}
return Err(parser.error(ParsePatchErrorKind::UnexpectedHunkLine));
};
match &line {
Line::Context(_) => {
old_count += 1;
new_count += 1;
}
Line::Delete(_) => {
old_count += 1;
}
Line::Insert(_) => {
new_count += 1;
}
}
lines.push(line);
parser.next()?;
}
if old_count != expected_old || new_count != expected_new {
return Err(parser.error_at(ParsePatchErrorKind::HunkMismatch, hunk_start));
}
Ok(lines)
}
fn strip_newline<T: Text + ?Sized>(s: &T) -> Result<&T> {
if let Some(stripped) = s.strip_suffix("\n") {
Ok(stripped)
} else {
Err(ParsePatchErrorKind::MissingNewline.into())
}
}