diff --git a/src/apply.rs b/src/apply.rs index 884e7cc..0431823 100644 --- a/src/apply.rs +++ b/src/apply.rs @@ -2,6 +2,7 @@ use crate::{ patch::{Hunk, Line, Patch}, utils::LineIter, }; +use std::collections::VecDeque; use std::{fmt, iter}; /// An error returned when [`apply`]ing a `Patch` fails @@ -51,6 +52,28 @@ impl Clone for ImageLine<'_, T> { } } +#[derive(Debug)] +pub struct ApplyOptions { + max_fuzzy: usize, +} + +impl Default for ApplyOptions { + fn default() -> Self { + ApplyOptions::new() + } +} + +impl ApplyOptions { + pub fn new() -> Self { + ApplyOptions { max_fuzzy: 0 } + } + + pub fn with_max_fuzzy(mut self, max_fuzzy: usize) -> Self { + self.max_fuzzy = max_fuzzy; + self + } +} + /// Apply a `Patch` to a base image /// /// ``` @@ -94,7 +117,7 @@ pub fn apply(base_image: &str, patch: &Patch<'_, str>) -> Result) -> Result .collect(); for (i, hunk) in patch.hunks().iter().enumerate() { - apply_hunk(&mut image, hunk).map_err(|_| ApplyError(i + 1))?; + apply_hunk(&mut image, hunk, &ApplyOptions::new()).map_err(|_| ApplyError(i + 1))?; } Ok(image @@ -117,17 +140,79 @@ pub fn apply_bytes(base_image: &[u8], patch: &Patch<'_, [u8]>) -> Result .collect()) } +/// Try applying all hunks a `Patch` to a base image +pub fn apply_all_bytes( + base_image: &[u8], + patch: &Patch<'_, [u8]>, + options: ApplyOptions, +) -> (Vec, Vec) { + let mut image: Vec<_> = LineIter::new(base_image) + .map(ImageLine::Unpatched) + .collect(); + + let mut failed_indices = Vec::new(); + + for (i, hunk) in patch.hunks().iter().enumerate() { + if let Some(_) = apply_hunk(&mut image, hunk, &options).err() { + failed_indices.push(i); + } + } + + ( + image + .into_iter() + .flat_map(ImageLine::into_inner) + .copied() + .collect(), + failed_indices, + ) +} + +/// Try applying all hunks a `Patch` to a base image +pub fn apply_all( + base_image: &str, + patch: &Patch<'_, str>, + options: ApplyOptions, +) -> (String, Vec) { + let mut image: Vec<_> = LineIter::new(base_image) + .map(ImageLine::Unpatched) + .collect(); + + let mut failed_indices = Vec::new(); + + for (i, hunk) in patch.hunks().iter().enumerate() { + if let Some(_) = apply_hunk(&mut image, hunk, &options).err() { + failed_indices.push(i); + } + } + + ( + image.into_iter().map(ImageLine::into_inner).collect(), + failed_indices, + ) +} + fn apply_hunk<'a, T: PartialEq + ?Sized>( image: &mut Vec>, hunk: &Hunk<'a, T>, + options: &ApplyOptions, ) -> Result<(), ()> { // Find position - let pos = find_position(image, hunk).ok_or(())?; + + let max_fuzzy = pre_context_line_count(hunk.lines()) + .min(post_context_line_count(hunk.lines())) + .min(options.max_fuzzy); + let (pos, fuzzy) = find_position(image, hunk, max_fuzzy).ok_or(())?; + let begin = pos + fuzzy; + let end = pos + + pre_image_line_count(hunk.lines()) + .checked_sub(fuzzy) + .unwrap_or(0); // update image image.splice( - pos..pos + pre_image_line_count(hunk.lines()), - post_image(hunk.lines()).map(ImageLine::Patched), + begin..end, + skip_last(post_image(hunk.lines()).skip(fuzzy), fuzzy).map(ImageLine::Patched), ); Ok(()) @@ -142,22 +227,40 @@ fn apply_hunk<'a, T: PartialEq + ?Sized>( fn find_position( image: &[ImageLine], hunk: &Hunk<'_, T>, -) -> Option { + max_fuzzy: usize, +) -> Option<(usize, usize)> { let pos = hunk.new_range().start().saturating_sub(1); - // Create an iterator that starts with 'pos' and then interleaves - // moving pos backward/foward by one. - let backward = (0..pos).rev(); - let forward = pos + 1..image.len(); - for pos in iter::once(pos).chain(interleave(backward, forward)) { - if match_fragment(image, hunk.lines(), pos) { - return Some(pos); + for fuzzy in 0..=max_fuzzy { + // Create an iterator that starts with 'pos' and then interleaves + // moving pos backward/foward by one. + let backward = (0..pos).rev(); + let forward = pos + 1..image.len(); + for pos in iter::once(pos).chain(interleave(backward, forward)) { + if match_fragment(image, hunk.lines(), pos, fuzzy) { + return Some((pos, fuzzy)); + } } } None } +fn pre_context_line_count(lines: &[Line<'_, T>]) -> usize { + lines + .iter() + .take_while(|x| matches!(x, Line::Context(_))) + .count() +} + +fn post_context_line_count(lines: &[Line<'_, T>]) -> usize { + lines + .iter() + .rev() + .take_while(|x| matches!(x, Line::Context(_))) + .count() +} + fn pre_image_line_count(lines: &[Line<'_, T>]) -> usize { pre_image(lines).count() } @@ -180,10 +283,13 @@ fn match_fragment( image: &[ImageLine], lines: &[Line<'_, T>], pos: usize, + fuzzy: usize, ) -> bool { let len = pre_image_line_count(lines); + let begin = pos + fuzzy; + let end = pos + len.checked_sub(fuzzy).unwrap_or(0); - let image = if let Some(image) = image.get(pos..pos + len) { + let image = if let Some(image) = image.get(begin..end) { image } else { return false; @@ -194,7 +300,7 @@ fn match_fragment( return false; } - pre_image(lines).eq(image.iter().map(ImageLine::inner)) + pre_image(&lines[fuzzy..len - fuzzy]).eq(image.iter().map(ImageLine::inner)) } #[derive(Debug)] @@ -241,3 +347,64 @@ where } } } + +fn skip_last(iter: I, count: usize) -> SkipLast { + SkipLast { + iter: iter.fuse(), + buffer: VecDeque::with_capacity(count), + count, + } +} + +#[derive(Debug)] +struct SkipLast, Item> { + iter: iter::Fuse, + buffer: VecDeque, + count: usize, +} + +impl, Item> Iterator for SkipLast { + type Item = Item; + + fn next(&mut self) -> Option { + if self.count == 0 { + return self.iter.next(); + } + while self.buffer.len() != self.count { + self.buffer.push_front(self.iter.next()?); + } + let next = self.iter.next()?; + let res = self.buffer.pop_back()?; + self.buffer.push_front(next); + Some(res) + } +} + +#[cfg(test)] +mod skip_last_test { + use crate::apply::skip_last; + + #[test] + fn skip_last_test() { + let a = [1, 2, 3, 4, 5, 6, 7]; + + assert_eq!( + skip_last(a.iter().copied(), 0) + .collect::>() + .as_slice(), + &[1, 2, 3, 4, 5, 6, 7] + ); + assert_eq!( + skip_last(a.iter().copied(), 5) + .collect::>() + .as_slice(), + &[1, 2] + ); + assert_eq!( + skip_last(a.iter().copied(), 7) + .collect::>() + .as_slice(), + &[] + ); + } +} diff --git a/src/diff/tests.rs b/src/diff/tests.rs index aeb1558..daee0e0 100644 --- a/src/diff/tests.rs +++ b/src/diff/tests.rs @@ -341,6 +341,27 @@ macro_rules! assert_patch { crate::apply_bytes($old.as_bytes(), &bpatch).unwrap(), $new.as_bytes() ); + assert_eq!( + crate::apply_all_bytes($old.as_bytes(), &bpatch, crate::ApplyOptions::new()).0, + $new.as_bytes() + ); + assert_eq!( + crate::apply_all_bytes( + $old.as_bytes(), + &bpatch, + crate::ApplyOptions::new().with_max_fuzzy(1) + ) + .0, + $new.as_bytes() + ); + assert_eq!( + crate::apply_all($old, &patch, crate::ApplyOptions::new()).0, + $new + ); + assert_eq!( + crate::apply_all($old, &patch, crate::ApplyOptions::new().with_max_fuzzy(1)).0, + $new + ); }; ($old:ident, $new:ident, $expected:ident $(,)?) => { assert_patch!(DiffOptions::default(), $old, $new, $expected); @@ -611,3 +632,30 @@ void Chunk_copy(Chunk *src, size_t src_start, Chunk *dst, size_t dst_start, size "; assert_patch!(original, a, expected_diffy); } + +#[test] +fn fuzzy_patch() { + let diff = Patch::from_str( + "\ +--- original ++++ modified +@@ -1,6 +1,6 @@ + A + B +-C +-D ++E ++F + G + H +", + ) + .unwrap(); + let newer = "0\nB\nC\nD\nG\nH\n"; + let expected = "0\nB\nE\nF\nG\nH\n"; + println!("{}", diff); + assert_eq!( + crate::apply_all(newer, &diff, crate::ApplyOptions::new().with_max_fuzzy(2)), + (expected.to_owned(), vec![]), + ) +} diff --git a/src/lib.rs b/src/lib.rs index 2d4b0dc..2c8e70f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -219,7 +219,7 @@ mod patch; mod range; mod utils; -pub use apply::{apply, apply_bytes, ApplyError}; +pub use apply::{apply, apply_all, apply_all_bytes, apply_bytes, ApplyError, ApplyOptions}; pub use diff::{create_patch, create_patch_bytes, DiffOptions}; pub use merge::{merge, merge_bytes, ConflictStyle, MergeOptions}; pub use patch::{Hunk, HunkRange, Line, ParsePatchError, Patch, PatchFormatter};