use sley_core::{GitError, ObjectFormat, ObjectId, RepoPath, Result};
use std::path::{Path, PathBuf};
use crate::{name, ws};
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum HunkLine {
Context(Vec<u8>),
Insert(Vec<u8>),
Delete(Vec<u8>),
}
impl HunkLine {
pub fn content(&self) -> &[u8] {
match self {
Self::Context(bytes) | Self::Insert(bytes) | Self::Delete(bytes) => bytes,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Hunk {
pub old_start: usize,
pub old_len: usize,
pub new_start: usize,
pub new_len: usize,
pub lines: Vec<HunkLine>,
pub old_no_newline: bool,
pub new_no_newline: bool,
pub line_input_lines: Vec<usize>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct FilePatch {
pub old_path: Option<Vec<u8>>,
pub new_path: Option<Vec<u8>>,
pub old_mode: Option<u32>,
pub new_mode: Option<u32>,
pub hunks: Vec<Hunk>,
pub is_new: bool,
pub is_delete: bool,
pub is_rename: bool,
pub is_copy: bool,
pub similarity: Option<u8>,
pub dissimilarity: Option<u8>,
pub old_oid_hex: Option<Vec<u8>>,
pub new_oid_hex: Option<Vec<u8>>,
pub is_binary: bool,
pub binary: Option<BinaryPatch>,
pub is_toplevel_relative: bool,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct BinaryPatch {
pub forward: BinaryHunk,
pub reverse: Option<BinaryHunk>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct BinaryHunk {
pub method: BinaryMethod,
pub origlen: usize,
pub deflated: Vec<u8>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BinaryMethod {
Literal,
Delta,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ApplyOutcome {
Applied(Vec<u8>),
Rejected,
}
const MIN_FUZZ_CONTEXT: usize = usize::MAX;
pub fn parse_unified_patch(input: &[u8]) -> Result<Vec<FilePatch>> {
parse_unified_patch_with_recount(input, false)
}
pub fn parse_unified_patch_with_recount(input: &[u8], recount: bool) -> Result<Vec<FilePatch>> {
parse_unified_patch_with_options(input, recount, &PatchPathOptions::default())
}
#[derive(Clone)]
pub struct PatchPathOptions {
pub p_value: usize,
pub p_value_known: bool,
pub root: Vec<u8>,
pub prefix: Vec<u8>,
}
impl Default for PatchPathOptions {
fn default() -> Self {
PatchPathOptions {
p_value: 1,
p_value_known: false,
root: Vec::new(),
prefix: Vec::new(),
}
}
}
pub fn parse_unified_patch_with_options(
input: &[u8],
recount: bool,
options: &PatchPathOptions,
) -> Result<Vec<FilePatch>> {
let lines = split_patch_lines(input);
let mut parser = PatchParser {
lines: &lines,
index: 0,
recount,
p_value: options.p_value,
p_value_known: options.p_value_known,
root: options.root.clone(),
prefix: options.prefix.clone(),
};
parser.parse()
}
pub fn apply_file_patch(base: &[u8], patch: &FilePatch) -> ApplyOutcome {
apply_file_patch_with_options(base, patch, &ApplyFileOptions::default())
}
#[derive(Clone, Default)]
pub struct ApplyFileOptions {
pub unidiff_zero: bool,
}
pub fn reverse_file_patch(patch: &FilePatch) -> FilePatch {
let hunks = patch
.hunks
.iter()
.map(|hunk| {
let lines = hunk
.lines
.iter()
.map(|line| match line {
HunkLine::Context(b) => HunkLine::Context(b.clone()),
HunkLine::Insert(b) => HunkLine::Delete(b.clone()),
HunkLine::Delete(b) => HunkLine::Insert(b.clone()),
})
.collect();
Hunk {
old_start: hunk.new_start,
old_len: hunk.new_len,
new_start: hunk.old_start,
new_len: hunk.old_len,
lines,
old_no_newline: hunk.new_no_newline,
new_no_newline: hunk.old_no_newline,
line_input_lines: hunk.line_input_lines.clone(),
}
})
.collect();
let (old_mode, new_mode) = if patch.new_mode.is_some() || patch.is_delete {
(patch.new_mode, patch.old_mode)
} else {
(patch.old_mode, patch.new_mode)
};
FilePatch {
old_path: patch.new_path.clone(),
new_path: patch.old_path.clone(),
old_mode,
new_mode,
hunks,
is_new: patch.is_delete,
is_delete: patch.is_new,
is_rename: patch.is_rename,
is_copy: patch.is_copy,
similarity: patch.similarity,
dissimilarity: patch.dissimilarity,
old_oid_hex: patch.new_oid_hex.clone(),
new_oid_hex: patch.old_oid_hex.clone(),
is_binary: patch.is_binary,
binary: patch.binary.as_ref().map(|binary| BinaryPatch {
forward: binary
.reverse
.clone()
.unwrap_or_else(|| binary.forward.clone()),
reverse: Some(binary.forward.clone()),
}),
is_toplevel_relative: patch.is_toplevel_relative,
}
}
pub fn apply_file_patch_with_options(
base: &[u8],
patch: &FilePatch,
options: &ApplyFileOptions,
) -> ApplyOutcome {
if patch.is_delete && patch.hunks.is_empty() {
return ApplyOutcome::Applied(Vec::new());
}
let base_for_match: &[u8] = if patch.is_new { b"" } else { base };
let mut image = split_blob_lines(base_for_match);
let mut running_offset: isize = 0;
for hunk in &patch.hunks {
match apply_one_hunk(&mut image, hunk, running_offset, options.unidiff_zero) {
Some(drift) => running_offset += drift,
None => return ApplyOutcome::Rejected,
}
}
ApplyOutcome::Applied(join_lines(&image))
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RejectApply {
pub content: Vec<u8>,
pub rejected: Vec<usize>,
}
pub fn apply_file_patch_rejecting(
base: &[u8],
patch: &FilePatch,
options: &ApplyFileOptions,
) -> RejectApply {
if patch.is_delete && patch.hunks.is_empty() {
return RejectApply {
content: Vec::new(),
rejected: Vec::new(),
};
}
let base_for_match: &[u8] = if patch.is_new { b"" } else { base };
let mut image = split_blob_lines(base_for_match);
let mut running_offset: isize = 0;
let mut rejected = Vec::new();
for (index, hunk) in patch.hunks.iter().enumerate() {
match apply_one_hunk(&mut image, hunk, running_offset, options.unidiff_zero) {
Some(drift) => running_offset += drift,
None => rejected.push(index),
}
}
RejectApply {
content: join_lines(&image),
rejected,
}
}
pub fn render_reject_hunk(hunk: &Hunk) -> Vec<u8> {
fn range(start: usize, count: usize) -> String {
if count == 1 {
start.to_string()
} else {
format!("{start},{count}")
}
}
let mut out = Vec::new();
out.extend_from_slice(b"@@ -");
out.extend_from_slice(range(hunk.old_start, hunk.old_len).as_bytes());
out.extend_from_slice(b" +");
out.extend_from_slice(range(hunk.new_start, hunk.new_len).as_bytes());
out.extend_from_slice(b" @@\n");
let last_old = hunk
.lines
.iter()
.rposition(|line| matches!(line, HunkLine::Context(_) | HunkLine::Delete(_)));
let last_new = hunk
.lines
.iter()
.rposition(|line| matches!(line, HunkLine::Context(_) | HunkLine::Insert(_)));
for (index, line) in hunk.lines.iter().enumerate() {
let (prefix, content) = match line {
HunkLine::Context(bytes) => (b' ', bytes),
HunkLine::Insert(bytes) => (b'+', bytes),
HunkLine::Delete(bytes) => (b'-', bytes),
};
out.push(prefix);
out.extend_from_slice(content);
out.push(b'\n');
let old_incomplete = hunk.old_no_newline && Some(index) == last_old;
let new_incomplete = hunk.new_no_newline && Some(index) == last_new;
if old_incomplete || new_incomplete {
out.extend_from_slice(b"\\ No newline at end of file\n");
}
}
out
}
#[derive(Clone, Copy)]
pub struct WsApplyOptions {
pub unidiff_zero: bool,
pub ws_rule: ws::WsRule,
pub ws_fix: bool,
pub ws_ignore_change: bool,
}
pub enum WsApplyOutcome {
Applied {
content: Vec<u8>,
blank_at_eof_removed: usize,
},
Rejected,
}
#[derive(Clone)]
struct WsImageLine {
content: Vec<u8>,
no_newline: bool,
common: bool,
}
impl WsImageLine {
fn bytes(&self) -> Vec<u8> {
let mut out = self.content.clone();
if !self.no_newline {
out.push(b'\n');
}
out
}
}
fn line_bytes(line: &Line) -> Vec<u8> {
let mut out = line.content.clone();
if !line.no_newline {
out.push(b'\n');
}
out
}
fn ws_line_from_bytes(bytes: Vec<u8>, common: bool) -> WsImageLine {
if bytes.last() == Some(&b'\n') {
WsImageLine {
content: bytes[..bytes.len() - 1].to_vec(),
no_newline: false,
common,
}
} else {
WsImageLine {
content: bytes,
no_newline: true,
common,
}
}
}
pub fn apply_file_patch_ws(
base: &[u8],
patch: &FilePatch,
opts: &WsApplyOptions,
) -> WsApplyOutcome {
if patch.is_delete && patch.hunks.is_empty() {
return WsApplyOutcome::Applied {
content: Vec::new(),
blank_at_eof_removed: 0,
};
}
let base_for_match: &[u8] = if patch.is_new { b"" } else { base };
let mut image = split_blob_lines(base_for_match);
let mut running_offset: isize = 0;
let mut blank_removed = 0usize;
for hunk in &patch.hunks {
match apply_one_fragment_ws(&mut image, hunk, running_offset, opts, &mut blank_removed) {
Some(drift) => running_offset += drift,
None => return WsApplyOutcome::Rejected,
}
}
WsApplyOutcome::Applied {
content: join_lines(&image),
blank_at_eof_removed: blank_removed,
}
}
fn apply_one_fragment_ws(
image: &mut Vec<Line>,
hunk: &Hunk,
running_offset: isize,
opts: &WsApplyOptions,
blank_removed: &mut usize,
) -> Option<isize> {
let blank_eof = opts.ws_rule & ws::WS_BLANK_AT_EOF != 0;
let mut preimage: Vec<WsImageLine> = Vec::new();
let mut postimage: Vec<WsImageLine> = Vec::new();
let mut leading = 0usize;
let mut trailing = 0usize;
let mut seen_change = false;
let mut new_blank_lines_at_end = 0usize;
for hl in &hunk.lines {
let mut added_blank_line = false;
let mut is_blank_context = false;
match hl {
HunkLine::Context(bytes) => {
if blank_eof && ws::ws_blank_line(bytes) {
is_blank_context = true;
}
preimage.push(WsImageLine {
content: bytes.clone(),
no_newline: false,
common: true,
});
postimage.push(WsImageLine {
content: bytes.clone(),
no_newline: false,
common: true,
});
if !seen_change {
leading += 1;
}
trailing += 1;
}
HunkLine::Delete(bytes) => {
preimage.push(WsImageLine {
content: bytes.clone(),
no_newline: false,
common: false,
});
seen_change = true;
trailing = 0;
}
HunkLine::Insert(bytes) => {
postimage.push(WsImageLine {
content: bytes.clone(),
no_newline: false,
common: false,
});
if blank_eof && ws::ws_blank_line(bytes) {
added_blank_line = true;
}
seen_change = true;
trailing = 0;
}
}
if added_blank_line {
new_blank_lines_at_end += 1;
} else if is_blank_context {
} else {
new_blank_lines_at_end = 0;
}
}
if hunk.old_no_newline
&& let Some(last) = preimage.last_mut()
{
last.no_newline = true;
}
if hunk.new_no_newline
&& let Some(last) = postimage.last_mut()
{
last.no_newline = true;
}
let mut match_beginning = hunk.old_start == 0 || (hunk.old_start == 1 && !opts.unidiff_zero);
let mut match_end = !opts.unidiff_zero && trailing == 0;
let mut expected = if preimage.is_empty() {
new_side_position(hunk, running_offset)
} else {
expected_position(hunk, running_offset)
};
let hunk_expected = expected;
let mut leading_v = leading;
let mut trailing_v = trailing;
let applied_pos = loop {
if let Some(pos) = find_pos_ws(
image,
&mut preimage,
&mut postimage,
expected,
opts,
match_beginning,
match_end,
) {
break pos;
}
#[allow(clippy::absurd_extreme_comparisons)]
if leading_v <= MIN_FUZZ_CONTEXT && trailing_v <= MIN_FUZZ_CONTEXT {
return None;
}
if match_beginning || match_end {
match_beginning = false;
match_end = false;
continue;
}
if leading_v >= trailing_v {
preimage.remove(0);
postimage.remove(0);
expected -= 1;
leading_v -= 1;
}
if trailing_v > leading_v {
preimage.pop();
postimage.pop();
trailing_v -= 1;
}
};
if new_blank_lines_at_end > 0
&& preimage.len() + applied_pos >= image.len()
&& blank_eof
&& opts.ws_fix
{
for _ in 0..new_blank_lines_at_end {
postimage.pop();
}
*blank_removed += new_blank_lines_at_end;
}
let preimage_limit = preimage.len().min(image.len() - applied_pos);
let replacement: Vec<Line> = postimage
.iter()
.map(|line| Line {
content: line.content.clone(),
no_newline: line.no_newline,
})
.collect();
image.splice(applied_pos..applied_pos + preimage_limit, replacement);
Some(applied_pos as isize - hunk_expected)
}
fn find_pos_ws(
image: &[Line],
preimage: &mut Vec<WsImageLine>,
postimage: &mut Vec<WsImageLine>,
expected: isize,
opts: &WsApplyOptions,
match_beginning: bool,
match_end: bool,
) -> Option<usize> {
let line_nr = image.len();
let pre_nr = preimage.len();
let mut line: isize = if match_beginning {
0
} else if match_end {
line_nr as isize - pre_nr as isize
} else {
expected
};
if line < 0 {
line = 0;
}
if line as usize > line_nr {
line = line_nr as isize;
}
let start = line as usize;
let mut backwards = start;
let mut forwards = start;
let mut current = start;
let mut i: u64 = 0;
loop {
if match_fragment_ws(
image,
preimage,
postimage,
current,
opts,
match_beginning,
match_end,
) {
return Some(current);
}
loop {
if backwards == 0 && forwards == line_nr {
return None;
}
if i & 1 == 1 {
if backwards == 0 {
i += 1;
continue;
}
backwards -= 1;
current = backwards;
} else {
if forwards == line_nr {
i += 1;
continue;
}
forwards += 1;
current = forwards;
}
break;
}
i += 1;
}
}
fn match_fragment_ws(
image: &[Line],
preimage: &mut Vec<WsImageLine>,
postimage: &mut Vec<WsImageLine>,
current_lno: usize,
opts: &WsApplyOptions,
match_beginning: bool,
match_end: bool,
) -> bool {
let blank_eof = opts.ws_rule & ws::WS_BLANK_AT_EOF != 0;
let preimage_limit: usize;
if preimage.len() + current_lno <= image.len() {
preimage_limit = preimage.len();
if match_end && (preimage.len() + current_lno != image.len()) {
return false;
}
} else if opts.ws_fix && blank_eof {
preimage_limit = image.len() - current_lno;
} else {
return false;
}
if match_beginning && current_lno != 0 {
return false;
}
if preimage_limit == preimage.len() {
let mut exact = true;
if match_end && current_lno + preimage_limit != image.len() {
exact = false;
}
if exact {
for i in 0..preimage_limit {
let img = &image[current_lno + i];
let pre = &preimage[i];
if img.content != pre.content || img.no_newline != pre.no_newline {
exact = false;
break;
}
}
}
if exact {
return true;
}
} else {
let mut all_blank = true;
for line in preimage.iter().take(preimage_limit) {
if !line.content.iter().all(|&b| ws::is_space(b)) {
all_blank = false;
break;
}
}
if all_blank {
return false;
}
}
if opts.ws_ignore_change {
return fuzzy_match_ws(image, preimage, postimage, current_lno, preimage_limit);
}
if !opts.ws_fix {
return false;
}
let mut fixed: Vec<WsImageLine> = Vec::with_capacity(preimage.len());
for i in 0..preimage_limit {
let fixed_pre = ws::ws_fix_bytes(&preimage[i].bytes(), opts.ws_rule);
let fixed_tgt = ws::ws_fix_bytes(&line_bytes(&image[current_lno + i]), opts.ws_rule);
if fixed_pre != fixed_tgt {
return false;
}
fixed.push(ws_line_from_bytes(fixed_pre, preimage[i].common));
}
for line in preimage.iter().skip(preimage_limit) {
let fixed_pre = ws::ws_fix_bytes(&line.bytes(), opts.ws_rule);
if !fixed_pre.iter().all(|&b| ws::is_space(b)) {
return false;
}
fixed.push(ws_line_from_bytes(fixed_pre, line.common));
}
update_pre_post_images_ws(preimage, postimage, fixed);
true
}
fn fuzzy_match_ws(
image: &[Line],
preimage: &mut Vec<WsImageLine>,
postimage: &mut Vec<WsImageLine>,
current_lno: usize,
preimage_limit: usize,
) -> bool {
for i in 0..preimage_limit {
if !fuzzy_matchlines(&line_bytes(&image[current_lno + i]), &preimage[i].bytes()) {
return false;
}
}
for line in preimage.iter().skip(preimage_limit) {
if !line.bytes().iter().all(|&b| ws::is_space(b)) {
return false;
}
}
let mut fixed: Vec<WsImageLine> = Vec::with_capacity(preimage.len());
for i in 0..preimage_limit {
let img = &image[current_lno + i];
fixed.push(WsImageLine {
content: img.content.clone(),
no_newline: img.no_newline,
common: preimage[i].common,
});
}
for line in preimage.iter().skip(preimage_limit) {
fixed.push(line.clone());
}
update_pre_post_images_ws(preimage, postimage, fixed);
true
}
fn fuzzy_matchlines(s1: &[u8], s2: &[u8]) -> bool {
let trim = |s: &[u8]| {
let mut end = s.len();
while end > 0 && (s[end - 1] == b'\r' || s[end - 1] == b'\n') {
end -= 1;
}
end
};
let end1 = trim(s1);
let end2 = trim(s2);
let (mut i, mut j) = (0usize, 0usize);
while i < end1 && j < end2 {
if ws::is_space(s1[i]) {
if !ws::is_space(s2[j]) {
return false;
}
while i < end1 && ws::is_space(s1[i]) {
i += 1;
}
while j < end2 && ws::is_space(s2[j]) {
j += 1;
}
} else if s1[i] != s2[j] {
return false;
} else {
i += 1;
j += 1;
}
}
i == end1 && j == end2
}
fn update_pre_post_images_ws(
preimage: &mut Vec<WsImageLine>,
postimage: &mut Vec<WsImageLine>,
fixed: Vec<WsImageLine>,
) {
*preimage = fixed;
let mut new_post: Vec<WsImageLine> = Vec::with_capacity(postimage.len());
let mut ctx = 0usize;
for line in postimage.iter() {
if !line.common {
new_post.push(line.clone());
continue;
}
while ctx < preimage.len() && !preimage[ctx].common {
ctx += 1;
}
if ctx >= preimage.len() {
continue;
}
new_post.push(WsImageLine {
content: preimage[ctx].content.clone(),
no_newline: preimage[ctx].no_newline,
common: true,
});
ctx += 1;
}
*postimage = new_post;
}
fn apply_one_hunk(
image: &mut Vec<Line>,
hunk: &Hunk,
running_offset: isize,
unidiff_zero: bool,
) -> Option<isize> {
let mut preimage: Vec<Line> = Vec::new();
let mut postimage: Vec<Line> = Vec::new();
let mut leading = 0usize; let mut trailing = 0usize; let mut seen_change = false;
for hl in &hunk.lines {
match hl {
HunkLine::Context(bytes) => {
preimage.push(Line {
content: bytes.clone(),
no_newline: false,
});
postimage.push(Line {
content: bytes.clone(),
no_newline: false,
});
if !seen_change {
leading += 1;
}
trailing += 1;
}
HunkLine::Delete(bytes) => {
preimage.push(Line {
content: bytes.clone(),
no_newline: false,
});
seen_change = true;
trailing = 0;
}
HunkLine::Insert(bytes) => {
postimage.push(Line {
content: bytes.clone(),
no_newline: false,
});
seen_change = true;
trailing = 0;
}
}
}
if hunk.old_no_newline
&& let Some(last) = preimage.last_mut()
{
last.no_newline = true;
}
if hunk.new_no_newline
&& let Some(last) = postimage.last_mut()
{
last.no_newline = true;
}
let mut match_beginning = hunk.old_start == 0 || (hunk.old_start == 1 && !unidiff_zero);
let mut match_end = !unidiff_zero && trailing == 0;
let mut expected = if preimage.is_empty() {
new_side_position(hunk, running_offset)
} else {
expected_position(hunk, running_offset)
};
let hunk_expected = expected;
loop {
if let Some(pos) = find_hunk_pos(image, &preimage, expected, match_beginning, match_end) {
let take = preimage.len();
let replacement: Vec<Line> = postimage.clone();
image.splice(pos..pos + take, replacement);
return Some(pos as isize - hunk_expected);
}
#[allow(clippy::absurd_extreme_comparisons)]
if leading <= MIN_FUZZ_CONTEXT && trailing <= MIN_FUZZ_CONTEXT {
return None;
}
if match_beginning || match_end {
match_beginning = false;
match_end = false;
continue;
}
if leading >= trailing {
preimage.remove(0);
postimage.remove(0);
expected -= 1;
leading -= 1;
}
if trailing > leading {
preimage.pop();
postimage.pop();
trailing -= 1;
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct Line {
pub(crate) content: Vec<u8>,
pub(crate) no_newline: bool,
}
pub(crate) fn split_blob_lines(data: &[u8]) -> Vec<Line> {
let mut lines = Vec::new();
let mut start = 0usize;
while start < data.len() {
match data[start..].iter().position(|&b| b == b'\n') {
Some(rel) => {
let end = start + rel;
lines.push(Line {
content: data[start..end].to_vec(),
no_newline: false,
});
start = end + 1;
}
None => {
lines.push(Line {
content: data[start..].to_vec(),
no_newline: true,
});
start = data.len();
}
}
}
lines
}
fn join_lines(lines: &[Line]) -> Vec<u8> {
let mut out = Vec::new();
for line in lines {
out.extend_from_slice(&line.content);
if !line.no_newline {
out.push(b'\n');
}
}
out
}
fn expected_position(hunk: &Hunk, running_offset: isize) -> isize {
let base = if hunk.old_start == 0 {
0
} else {
hunk.old_start as isize - 1
};
base + running_offset
}
fn new_side_position(hunk: &Hunk, running_offset: isize) -> isize {
let base = if hunk.new_start == 0 {
0
} else {
hunk.new_start as isize - 1
};
base + running_offset
}
fn find_hunk_pos(
image: &[Line],
preimage: &[Line],
expected: isize,
match_beginning: bool,
match_end: bool,
) -> Option<usize> {
let line_nr = image.len();
let pre_nr = preimage.len();
let mut line: isize = if match_beginning {
0
} else if match_end {
line_nr as isize - pre_nr as isize
} else {
expected
};
if line < 0 {
line = 0;
}
if line as usize > line_nr {
line = line_nr as isize;
}
let start = line as usize;
let mut backwards = start;
let mut forwards = start;
let mut current = start;
let mut i: u64 = 0;
loop {
if preimage_matches_at(image, preimage, current, match_beginning, match_end) {
return Some(current);
}
loop {
if backwards == 0 && forwards == line_nr {
return None;
}
if i & 1 == 1 {
if backwards == 0 {
i += 1;
continue;
}
backwards -= 1;
current = backwards;
} else {
if forwards == line_nr {
i += 1;
continue;
}
forwards += 1;
current = forwards;
}
break;
}
i += 1;
}
}
fn preimage_matches_at(
image: &[Line],
preimage: &[Line],
pos: usize,
match_beginning: bool,
match_end: bool,
) -> bool {
if match_beginning && pos != 0 {
return false;
}
if pos + preimage.len() > image.len() {
return false;
}
if match_end && pos + preimage.len() != image.len() {
return false;
}
for (i, pre) in preimage.iter().enumerate() {
let img = &image[pos + i];
if img.content != pre.content {
return false;
}
if pre.no_newline != img.no_newline {
return false;
}
}
true
}
fn split_patch_lines(input: &[u8]) -> Vec<&[u8]> {
let mut lines = Vec::new();
let mut start = 0usize;
while start < input.len() {
match input[start..].iter().position(|&b| b == b'\n') {
Some(rel) => {
let end = start + rel;
lines.push(&input[start..end]);
start = end + 1;
}
None => {
lines.push(&input[start..]);
start = input.len();
}
}
}
lines
}
struct PatchParser<'a> {
lines: &'a [&'a [u8]],
index: usize,
recount: bool,
p_value: usize,
p_value_known: bool,
root: Vec<u8>,
prefix: Vec<u8>,
}
impl<'a> PatchParser<'a> {
fn parse(&mut self) -> Result<Vec<FilePatch>> {
let mut patches = Vec::new();
while self.index < self.lines.len() {
let line = self.lines[self.index];
if line.starts_with(b"diff --git ") {
patches.push(self.parse_file(Some(line))?);
} else if line.starts_with(b"--- ") {
patches.push(self.parse_file(None)?);
} else if line.starts_with(b"@@ ") {
return Err(GitError::InvalidFormat(
"hunk header encountered before any file header".to_string(),
));
} else {
self.index += 1;
}
}
Ok(patches)
}
fn parse_file(&mut self, diff_line: Option<&[u8]>) -> Result<FilePatch> {
match diff_line {
Some(diff_line) => self.parse_git_file(diff_line),
None => self.parse_traditional_file(),
}
}
fn p_minus_one(&self) -> usize {
self.p_value.saturating_sub(1)
}
fn parse_git_file(&mut self, diff_line: &[u8]) -> Result<FilePatch> {
let mut patch = empty_file_patch();
let rest = &diff_line[b"diff --git ".len()..];
let mut def_name = name::git_header_name(self.p_value, rest);
if let (Some(d), false) = (def_name.as_mut(), self.root.is_empty()) {
let mut s = self.root.clone();
s.extend_from_slice(d);
*d = s;
}
self.index += 1;
patch.is_toplevel_relative = true;
let mut binary_seen = false;
while self.index < self.lines.len() {
let line = self.lines[self.index];
if line.starts_with(b"--- ") {
self.parse_git_old_header(&line[b"--- ".len()..], &mut patch);
self.index += 1;
break;
} else if line.starts_with(b"@@ ") {
break;
} else if line.starts_with(b"diff --git ") {
break;
} else if let Some(rest) = strip_prefix(line, b"old mode ") {
patch.old_mode = Some(self.parse_mode_line(rest)?);
} else if let Some(rest) = strip_prefix(line, b"new mode ") {
patch.new_mode = Some(self.parse_mode_line(rest)?);
} else if let Some(rest) = strip_prefix(line, b"new file mode ") {
patch.is_new = true;
patch.new_mode = Some(self.parse_mode_line(rest)?);
patch.new_path = def_name.clone();
} else if let Some(rest) = strip_prefix(line, b"deleted file mode ") {
patch.is_delete = true;
patch.old_mode = Some(self.parse_mode_line(rest)?);
patch.old_path = def_name.clone();
} else if let Some(rest) = strip_prefix(line, b"index ") {
self.parse_index_line(rest, &mut patch)?;
} else if let Some(rest) = strip_prefix(line, b"rename from ") {
patch.is_rename = true;
patch.old_path = name::find_name(rest, None, self.p_minus_one(), 0, &self.root);
} else if let Some(rest) = strip_prefix(line, b"rename to ") {
patch.is_rename = true;
patch.new_path = name::find_name(rest, None, self.p_minus_one(), 0, &self.root);
} else if let Some(rest) = strip_prefix(line, b"copy from ") {
patch.is_copy = true;
patch.old_path = name::find_name(rest, None, self.p_minus_one(), 0, &self.root);
} else if let Some(rest) = strip_prefix(line, b"copy to ") {
patch.is_copy = true;
patch.new_path = name::find_name(rest, None, self.p_minus_one(), 0, &self.root);
} else if let Some(rest) = strip_prefix(line, b"similarity index ") {
patch.similarity = parse_percent(rest);
} else if let Some(rest) = strip_prefix(line, b"dissimilarity index ") {
patch.dissimilarity = parse_percent(rest);
} else if line == b"GIT binary patch" {
let gitbin_line = self.index + 1;
patch.is_binary = true;
patch.binary = Some(self.parse_binary_block(gitbin_line)?);
binary_seen = true;
break;
} else if apply_is_binary_files_differ(line) {
patch.is_binary = true;
binary_seen = true;
self.index += 1;
break;
} else {
self.index += 1;
continue;
}
self.index += 1;
}
if !binary_seen
&& self.index < self.lines.len()
&& self.lines[self.index].starts_with(b"+++ ")
{
let line = self.lines[self.index];
self.parse_git_new_header(&line[b"+++ ".len()..], &mut patch);
self.index += 1;
}
if patch.old_path.is_none() && patch.new_path.is_none() {
match &def_name {
Some(d) => {
patch.old_path = Some(d.clone());
patch.new_path = Some(d.clone());
}
None => {
return Err(GitError::InvalidFormat(format!(
"git diff header lacks filename information when removing {} \
leading pathname components",
self.p_value
)));
}
}
}
if !binary_seen {
self.parse_hunks(&mut patch)?;
}
Ok(patch)
}
fn parse_index_line(&self, rest: &[u8], patch: &mut FilePatch) -> Result<()> {
let Some(dotdot) = find_subslice(rest, b"..") else {
return Ok(());
};
let old = &rest[..dotdot];
let after = &rest[dotdot + 2..];
let (new, mode_part) = match after.iter().position(|&b| b == b' ') {
Some(space) => (&after[..space], Some(&after[space + 1..])),
None => (after, None),
};
if !old.is_empty() {
patch.old_oid_hex = Some(old.to_vec());
}
if !new.is_empty() {
patch.new_oid_hex = Some(new.to_vec());
}
if let Some(mode) = mode_part
&& !mode.is_empty()
{
patch.old_mode = Some(self.parse_mode_line(mode)?);
}
Ok(())
}
fn parse_mode_line(&self, rest: &[u8]) -> Result<u32> {
let mut value: u32 = 0;
let mut i = 0;
while i < rest.len() && (b'0'..=b'7').contains(&rest[i]) {
value = value
.checked_mul(8)
.and_then(|value| value.checked_add((rest[i] - b'0') as u32))
.ok_or_else(|| self.invalid_mode_error(rest))?;
i += 1;
}
if i == 0 || (i < rest.len() && !rest[i].is_ascii_whitespace()) {
return Err(self.invalid_mode_error(rest));
}
Ok(value)
}
fn invalid_mode_error(&self, rest: &[u8]) -> GitError {
GitError::InvalidFormat(format!(
"invalid mode on line {}: {}",
self.index + 1,
lossy(rest)
))
}
fn parse_binary_block(&mut self, gitbin_line: usize) -> Result<BinaryPatch> {
self.index += 1;
let forward = match self.parse_binary_hunk()? {
Some(hunk) => hunk,
None => {
return Err(GitError::InvalidFormat(format!(
"binary-unrecognized:{gitbin_line}"
)));
}
};
let reverse = self.parse_binary_hunk()?;
Ok(BinaryPatch { forward, reverse })
}
fn parse_binary_hunk(&mut self) -> Result<Option<BinaryHunk>> {
if self.index >= self.lines.len() {
return Ok(None);
}
let line = self.lines[self.index];
let (method, num) = if let Some(rest) = strip_prefix(line, b"delta ") {
(BinaryMethod::Delta, rest)
} else if let Some(rest) = strip_prefix(line, b"literal ") {
(BinaryMethod::Literal, rest)
} else {
return Ok(None);
};
let origlen = parse_leading_usize(num).map_err(|()| self.corrupt_binary_error())?;
self.index += 1;
let mut deflated = Vec::new();
loop {
if self.index >= self.lines.len() {
return Err(self.corrupt_binary_error());
}
let data = self.lines[self.index];
if data.is_empty() {
self.index += 1;
break;
}
let len = data.len();
if len < 6 || !(len - 1).is_multiple_of(5) {
return Err(self.corrupt_binary_error());
}
let max_byte_length = (len - 1) / 5 * 4;
let byte_length = match data[0] {
b'A'..=b'Z' => (data[0] - b'A') as usize + 1,
b'a'..=b'z' => (data[0] - b'a') as usize + 27,
_ => return Err(self.corrupt_binary_error()),
};
if max_byte_length < byte_length || byte_length <= max_byte_length.saturating_sub(4) {
return Err(self.corrupt_binary_error());
}
let decoded = decode_base85(&data[1..], byte_length)
.ok_or_else(|| self.corrupt_binary_error())?;
deflated.extend_from_slice(&decoded);
self.index += 1;
}
Ok(Some(BinaryHunk {
method,
origlen,
deflated,
}))
}
fn corrupt_binary_error(&self) -> GitError {
GitError::InvalidFormat(format!("binary-corrupt:{}", self.index + 1))
}
fn parse_git_old_header(&self, rest: &[u8], patch: &mut FilePatch) {
if name::is_dev_null(rest) {
patch.is_new = true;
patch.old_path = None;
} else if patch.old_path.is_none() {
patch.old_path = name::find_name(rest, None, self.p_value, name::TERM_TAB, &self.root);
}
}
fn parse_git_new_header(&self, rest: &[u8], patch: &mut FilePatch) {
if name::is_dev_null(rest) {
patch.is_delete = true;
patch.new_path = None;
} else if patch.new_path.is_none() {
patch.new_path = name::find_name(rest, None, self.p_value, name::TERM_TAB, &self.root);
}
}
fn parse_traditional_file(&mut self) -> Result<FilePatch> {
let mut patch = empty_file_patch();
let first_line = self.lines[self.index];
let first = first_line[b"--- ".len()..].to_vec();
self.index += 1;
let second = if self.index < self.lines.len() && self.lines[self.index].starts_with(b"+++ ")
{
let s = self.lines[self.index][b"+++ ".len()..].to_vec();
self.index += 1;
Some(s)
} else {
None
};
if let Some(second) = &second {
if !self.p_value_known {
let p0 = name::guess_p_value(&first, &self.root, &self.prefix);
let q0 = name::guess_p_value(second, &self.root, &self.prefix);
let p = if p0.is_none() { q0 } else { p0 };
if let Some(pv) = p
&& Some(pv) == q0
{
self.p_value = pv;
self.p_value_known = true;
}
}
let name = if name::is_dev_null(&first) {
patch.is_new = true;
let name = name::find_name_traditional(second, None, self.p_value, &self.root);
patch.new_path = name.clone();
name
} else if name::is_dev_null(second) {
patch.is_delete = true;
let name = name::find_name_traditional(&first, None, self.p_value, &self.root);
patch.old_path = name.clone();
name
} else {
let first_name =
name::find_name_traditional(&first, None, self.p_value, &self.root);
let name = name::find_name_traditional(
second,
first_name.as_deref(),
self.p_value,
&self.root,
);
if name::has_epoch_timestamp(&first) {
patch.is_new = true;
patch.new_path = name.clone();
} else if name::has_epoch_timestamp(second) {
patch.is_delete = true;
patch.old_path = name.clone();
} else {
patch.old_path = name.clone();
patch.new_path = name.clone();
}
name
};
if name.is_none() {
return Err(GitError::InvalidFormat(format!(
"unable to find filename in patch at line {}",
self.index
)));
}
}
self.parse_hunks(&mut patch)?;
Ok(patch)
}
fn parse_hunks(&mut self, patch: &mut FilePatch) -> Result<()> {
while self.index < self.lines.len() {
let line = self.lines[self.index];
if line.starts_with(b"@@ -") {
let hunk = self.parse_hunk()?;
patch.hunks.push(hunk);
} else if line.starts_with(b"diff --git ") {
break;
} else if line.starts_with(b"--- ") {
break;
} else {
self.index += 1;
}
}
Ok(())
}
fn parse_hunk(&mut self) -> Result<Hunk> {
let header = self.lines[self.index];
let (old_start, old_len, new_start, new_len) = parse_hunk_header(header)?;
self.index += 1;
let mut hunk = Hunk {
old_start,
old_len,
new_start,
new_len,
lines: Vec::new(),
old_no_newline: false,
new_no_newline: false,
line_input_lines: Vec::new(),
};
let mut old_seen = 0usize;
let mut new_seen = 0usize;
while self.index < self.lines.len() {
if !self.recount && old_seen >= old_len && new_seen >= new_len {
break;
}
let line = self.lines[self.index];
if self.recount
&& (line.starts_with(b"@@ ")
|| line.starts_with(b"diff --git ")
|| line.starts_with(b"diff a/")
|| line.starts_with(b"--- "))
{
break;
}
if line.is_empty() {
hunk.lines.push(HunkLine::Context(Vec::new()));
hunk.line_input_lines.push(self.index + 1);
old_seen += 1;
new_seen += 1;
self.index += 1;
continue;
}
match line[0] {
b' ' => {
hunk.lines.push(HunkLine::Context(line[1..].to_vec()));
hunk.line_input_lines.push(self.index + 1);
old_seen += 1;
new_seen += 1;
}
b'+' => {
hunk.lines.push(HunkLine::Insert(line[1..].to_vec()));
hunk.line_input_lines.push(self.index + 1);
new_seen += 1;
}
b'-' => {
hunk.lines.push(HunkLine::Delete(line[1..].to_vec()));
hunk.line_input_lines.push(self.index + 1);
old_seen += 1;
}
b'\\' => {
self.mark_no_newline(&mut hunk);
self.index += 1;
continue;
}
_ => {
return Err(GitError::InvalidFormat(format!(
"corrupt-hunk-body:{}",
self.index + 1
)));
}
}
self.index += 1;
}
if self.index < self.lines.len() && self.lines[self.index].starts_with(b"\\") {
self.mark_no_newline(&mut hunk);
self.index += 1;
}
if self.recount {
hunk.old_len = old_seen;
hunk.new_len = new_seen;
} else if old_seen != old_len || new_seen != new_len {
return Err(GitError::InvalidFormat(format!(
"hunk body line counts mismatch: header declared -{old_len},+{new_len} \
but body had -{old_seen},+{new_seen}"
)));
}
Ok(hunk)
}
fn mark_no_newline(&self, hunk: &mut Hunk) {
match hunk.lines.last() {
Some(HunkLine::Context(_)) => {
hunk.old_no_newline = true;
hunk.new_no_newline = true;
}
Some(HunkLine::Insert(_)) => hunk.new_no_newline = true,
Some(HunkLine::Delete(_)) => hunk.old_no_newline = true,
None => {}
}
}
}
fn empty_file_patch() -> FilePatch {
FilePatch {
old_path: None,
new_path: None,
old_mode: None,
new_mode: None,
hunks: Vec::new(),
is_new: false,
is_delete: false,
is_rename: false,
is_copy: false,
similarity: None,
dissimilarity: None,
old_oid_hex: None,
new_oid_hex: None,
is_binary: false,
binary: None,
is_toplevel_relative: false,
}
}
fn parse_hunk_header(line: &[u8]) -> Result<(usize, usize, usize, usize)> {
let err = || GitError::InvalidFormat(format!("malformed hunk header: {}", lossy(line)));
let rest = strip_prefix(line, b"@@ ").ok_or_else(err)?;
let close = find_subslice(rest, b" @@").ok_or_else(err)?;
let ranges = &rest[..close];
let mut parts = ranges.split(|&b| b == b' ').filter(|p| !p.is_empty());
let old = parts.next().ok_or_else(err)?;
let new = parts.next().ok_or_else(err)?;
let old = strip_prefix(old, b"-").ok_or_else(err)?;
let new = strip_prefix(new, b"+").ok_or_else(err)?;
let (old_start, old_len) = parse_range(old).ok_or_else(err)?;
let (new_start, new_len) = parse_range(new).ok_or_else(err)?;
Ok((old_start, old_len, new_start, new_len))
}
fn parse_range(range: &[u8]) -> Option<(usize, usize)> {
match range.iter().position(|&b| b == b',') {
Some(comma) => {
let start = parse_usize(&range[..comma])?;
let len = parse_usize(&range[comma + 1..])?;
Some((start, len))
}
None => Some((parse_usize(range)?, 1)),
}
}
fn parse_usize(bytes: &[u8]) -> Option<usize> {
if bytes.is_empty() {
return None;
}
let mut value: usize = 0;
for &b in bytes {
if !b.is_ascii_digit() {
return None;
}
value = value.checked_mul(10)?.checked_add((b - b'0') as usize)?;
}
Some(value)
}
fn parse_percent(bytes: &[u8]) -> Option<u8> {
let trimmed = trim_ascii_end(bytes)
.strip_suffix(b"%")
.unwrap_or(trim_ascii_end(bytes));
let value = parse_usize(trimmed)?;
u8::try_from(value).ok().filter(|value| *value <= 100)
}
fn strip_prefix<'b>(line: &'b [u8], prefix: &[u8]) -> Option<&'b [u8]> {
if line.starts_with(prefix) {
Some(&line[prefix.len()..])
} else {
None
}
}
fn apply_is_binary_files_differ(line: &[u8]) -> bool {
line.ends_with(b" differ")
&& (line.starts_with(b"Binary files ") || line.starts_with(b"Files "))
}
pub(crate) fn parse_leading_usize(bytes: &[u8]) -> std::result::Result<usize, ()> {
let mut value = 0usize;
for &b in bytes {
if !b.is_ascii_digit() {
break;
}
value = value.checked_mul(10).ok_or(())?;
value = value.checked_add((b - b'0') as usize).ok_or(())?;
}
Ok(value)
}
const BASE85_ALPHABET: &[u8; 85] =
b"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz!#$%&()*+-;<=>?@^_`{|}~";
fn base85_value(ch: u8) -> Option<u32> {
BASE85_ALPHABET
.iter()
.position(|&c| c == ch)
.map(|index| index as u32)
}
fn decode_base85(buffer: &[u8], len: usize) -> Option<Vec<u8>> {
let mut out = Vec::with_capacity(len);
let mut pos = 0usize;
let mut remaining = len;
while remaining > 0 {
let mut acc: u32 = 0;
for _ in 0..4 {
let de = base85_value(*buffer.get(pos)?)?;
pos += 1;
acc = acc * 85 + de;
}
let de = base85_value(*buffer.get(pos)?)?;
pos += 1;
if 0xffff_ffffu32 / 85 < acc {
return None;
}
acc *= 85;
if 0xffff_ffffu32 - de < acc {
return None;
}
acc += de;
let cnt = remaining.min(4);
remaining -= cnt;
let bytes = acc.to_be_bytes();
out.extend_from_slice(&bytes[..cnt]);
}
Some(out)
}
pub fn git_patch_delta(base: &[u8], delta: &[u8]) -> Option<Vec<u8>> {
let mut data = 0usize;
let read_hdr_size = |data: &mut usize| -> Option<usize> {
let mut size = 0usize;
let mut shift = 0u32;
loop {
let cmd = *delta.get(*data)?;
*data += 1;
size |= ((cmd & 0x7f) as usize).checked_shl(shift)?;
shift += 7;
if cmd & 0x80 == 0 {
break;
}
}
Some(size)
};
let base_size = read_hdr_size(&mut data)?;
if base_size != base.len() {
return None;
}
let result_size = read_hdr_size(&mut data)?;
let mut out = Vec::with_capacity(sley_pack::inflate::bounded_inflate_reserve(
result_size,
delta.len(),
));
while data < delta.len() {
let cmd = delta[data];
data += 1;
if cmd & 0x80 != 0 {
let mut cp_off = 0usize;
let mut cp_size = 0usize;
if cmd & 0x01 != 0 {
cp_off = *delta.get(data)? as usize;
data += 1;
}
if cmd & 0x02 != 0 {
cp_off |= (*delta.get(data)? as usize) << 8;
data += 1;
}
if cmd & 0x04 != 0 {
cp_off |= (*delta.get(data)? as usize) << 16;
data += 1;
}
if cmd & 0x08 != 0 {
cp_off |= (*delta.get(data)? as usize) << 24;
data += 1;
}
if cmd & 0x10 != 0 {
cp_size = *delta.get(data)? as usize;
data += 1;
}
if cmd & 0x20 != 0 {
cp_size |= (*delta.get(data)? as usize) << 8;
data += 1;
}
if cmd & 0x40 != 0 {
cp_size |= (*delta.get(data)? as usize) << 16;
data += 1;
}
if cp_size == 0 {
cp_size = 0x10000;
}
let end = cp_off.checked_add(cp_size)?;
if end > base.len() || cp_size > result_size {
return None;
}
out.extend_from_slice(&base[cp_off..end]);
} else if cmd != 0 {
let len = cmd as usize;
let end = data.checked_add(len)?;
if end > delta.len() {
return None;
}
out.extend_from_slice(&delta[data..end]);
data = end;
} else {
return None;
}
}
if data != delta.len() || out.len() != result_size {
return None;
}
Some(out)
}
fn find_subslice(haystack: &[u8], needle: &[u8]) -> Option<usize> {
if needle.is_empty() || needle.len() > haystack.len() {
return None;
}
haystack
.windows(needle.len())
.position(|window| window == needle)
}
fn trim_ascii_end(bytes: &[u8]) -> &[u8] {
let mut end = bytes.len();
while end > 0 && (bytes[end - 1] == b' ' || bytes[end - 1] == b'\r') {
end -= 1;
}
&bytes[..end]
}
fn lossy(bytes: &[u8]) -> String {
String::from_utf8_lossy(bytes).into_owned()
}