use crate::replay_patch::{PatchHunk, PatchLine, split_keeping_newlines};
#[derive(Clone, Debug, Eq, PartialEq)]
struct ImageLine {
text: Vec<u8>,
patched: bool,
}
fn is_not_hash_whitespace(byte: &u8) -> bool {
return !matches!(*byte, b' ' | b'\t' | b'\n' | 0x0b | 0x0c | b'\r');
}
fn is_change_line(line: &PatchLine) -> bool {
return line.op != b' ';
}
fn position_that_matches(image: &[ImageLine], preimage: &[Vec<u8>], at: i64, match_beginning: bool, match_end: bool) -> Option<usize> {
let index: usize = usize::try_from(at).ok()?;
if matches_at(image, preimage, index, match_beginning, match_end) {
return Some(index);
}
return None;
}
fn hash_key(text: &[u8]) -> Vec<u8> {
return text
.iter()
.copied()
.filter(is_not_hash_whitespace)
.collect();
}
fn line_matches(line: &ImageLine, wanted: &[u8], incomplete_last: bool, match_end: bool) -> bool {
if line.patched {
return false;
}
if !incomplete_last || match_end {
return line.text == wanted;
}
return line.text.starts_with(wanted) && hash_key(&line.text) == hash_key(wanted);
}
fn matches_at(
image: &[ImageLine],
preimage: &[Vec<u8>],
at: usize,
match_beginning: bool,
match_end: bool,
) -> bool {
let end: usize = preimage.len() + at;
if end > image.len() || (match_end && end != image.len()) || (match_beginning && at != 0) {
return false;
}
for (index, wanted) in preimage.iter().enumerate() {
let incomplete_last: bool = index + 1 == preimage.len() && !wanted.ends_with(b"\n");
if !line_matches(&image[at + index], wanted, incomplete_last, match_end) {
return false;
}
}
return true;
}
fn find_position(
image: &[ImageLine],
preimage: &[Vec<u8>],
line: i64,
match_beginning: bool,
match_end: bool,
) -> Option<usize> {
let length: i64 = i64::try_from(image.len()).unwrap_or(i64::MAX);
let preimage_length: i64 = i64::try_from(preimage.len()).unwrap_or(i64::MAX);
let anchored: i64 = if match_beginning {
0
} else if match_end {
length - preimage_length
} else {
line
};
let first: i64 = anchored.max(0).min(length);
if let Some(found) = position_that_matches(image, preimage, first, match_beginning, match_end) {
return Some(found);
}
let mut distance: i64 = 1;
while first + distance <= length || first - distance >= 0 {
if let Some(found) = position_that_matches(image, preimage, first + distance, match_beginning, match_end) {
return Some(found);
}
if let Some(found) = position_that_matches(image, preimage, first - distance, match_beginning, match_end) {
return Some(found);
}
distance += 1;
}
return None;
}
fn reverse_hunk(image: &[ImageLine], hunk: &PatchHunk) -> Option<Vec<ImageLine>> {
let mut preimage: Vec<Vec<u8>> = Vec::new();
let mut postimage: Vec<ImageLine> = Vec::new();
for line in &hunk.lines {
if line.op != b'-' {
preimage.push(line.text.clone());
}
if line.op != b'+' {
postimage.push(ImageLine {
text: line.text.clone(),
patched: true,
});
}
}
let trailing: usize = match hunk.lines.iter().rposition(is_change_line) {
Some(last_change) => hunk.lines.len() - 1 - last_change,
None => hunk.lines.len(),
};
let line: i64 = if hunk.old_start == 0 { 0 } else { hunk.old_start - 1 };
let at: usize = find_position(
image,
&preimage,
line,
hunk.new_start <= 1,
trailing == 0,
)?;
let mut rebuilt: Vec<ImageLine> = image[..at].to_vec();
rebuilt.extend(postimage);
rebuilt.extend_from_slice(&image[at + preimage.len()..]);
return Some(rebuilt);
}
pub fn reverse_applies(hunks: &[PatchHunk], prepared: &[u8]) -> bool {
let mut image: Vec<ImageLine> = Vec::new();
for text in split_keeping_newlines(prepared) {
image.push(ImageLine {
text,
patched: false,
});
}
for hunk in hunks {
match reverse_hunk(&image, hunk) {
Some(next) => image = next,
None => return false,
}
}
return true;
}
#[cfg(test)]
#[path = "replay_reverse_apply_test_support.rs"]
mod test_support_batch;
#[cfg(test)]
#[path = "replay_reverse_apply_tests.rs"]
mod tests;