girt 0.17.0

An incremental, idiomatic Rust implementation of Git
Documentation
use super::{Applied, Error, FilePatch, HunkResult, Limits};

pub(super) const OUTPUT_BYTES: &str = "output bytes";

pub(super) fn apply(file: &FilePatch, preimage: &[u8], limits: Limits) -> Result<Applied, Error> {
    if preimage.len() > limits.max_input_bytes {
        return Err(Error::Limit("input bytes"));
    }
    if file.hunks.len() > limits.max_hunks {
        return Err(Error::Limit("hunks"));
    }
    if file.old_path.is_none() && !preimage.is_empty() {
        return Err(Error::Mismatch { line: file.line });
    }
    let mut result = Applied {
        content: Vec::new(),
        hunks: Vec::new(),
    };
    let mut remaining = preimage;
    let mut old_line = 0;
    let mut new_line = 0;
    let mut postimage_ended = false;
    let mut remaining_lines = limits.max_lines;
    for hunk in &file.hunks {
        let mismatch = || Error::Mismatch { line: hunk.line };
        if postimage_ended
            || hunk.old_start < old_line
            || (file.old_path.is_none() && !preimage.is_empty())
        {
            return Err(mismatch());
        }
        while old_line < hunk.old_start {
            let line = take_line(&mut remaining).ok_or_else(mismatch)?;
            append(&mut result.content, line, limits)?;
            old_line += 1;
            new_line += 1;
        }
        if hunk.new_start != new_line {
            return Err(mismatch());
        }
        result.hunks.push(HunkResult {
            patch_line: hunk.line,
            old_start: old_line,
            new_start: new_line,
        });
        for line in &hunk.lines {
            remaining_lines = remaining_lines
                .checked_sub(1)
                .ok_or(Error::Limit("content lines"))?;
            if line.kind != b'+' {
                let actual = take_line(&mut remaining).ok_or_else(mismatch)?;
                if actual != line.bytes {
                    return Err(mismatch());
                }
                old_line += 1;
            }
            if line.kind != b'-' {
                if postimage_ended {
                    return Err(mismatch());
                }
                postimage_ended = !line.bytes.ends_with(b"\n");
                append(&mut result.content, &line.bytes, limits)?;
                new_line += 1;
            }
        }
    }
    if postimage_ended && !remaining.is_empty() {
        return Err(Error::Mismatch {
            line: file.hunks.last().map_or(file.line, |hunk| hunk.line),
        });
    }
    append(&mut result.content, remaining, limits)?;
    if file.new_path.is_none() && !result.content.is_empty() {
        return Err(Error::Mismatch {
            line: file.hunks.last().map_or(file.line, |hunk| hunk.line),
        });
    }
    Ok(result)
}

fn take_line<'a>(remaining: &mut &'a [u8]) -> Option<&'a [u8]> {
    if remaining.is_empty() {
        return None;
    }
    let end = remaining
        .iter()
        .position(|b| *b == b'\n')
        .map_or(remaining.len(), |i| i + 1);
    let (line, rest) = remaining.split_at(end);
    *remaining = rest;
    Some(line)
}

fn append(output: &mut Vec<u8>, bytes: &[u8], limits: Limits) -> Result<(), Error> {
    if bytes.len() > limits.max_output_bytes.saturating_sub(output.len()) {
        return Err(Error::Limit(OUTPUT_BYTES));
    }
    output.extend_from_slice(bytes);
    Ok(())
}