Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add apply all and fuzzy patch #15

Open
wants to merge 9 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
197 changes: 182 additions & 15 deletions src/apply.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -51,6 +52,28 @@ impl<T: ?Sized> 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
///
/// ```
Expand Down Expand Up @@ -94,7 +117,7 @@ pub fn apply(base_image: &str, patch: &Patch<'_, str>) -> Result<String, ApplyEr
.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.into_iter().map(ImageLine::into_inner).collect())
Expand All @@ -107,7 +130,7 @@ pub fn apply_bytes(base_image: &[u8], patch: &Patch<'_, [u8]>) -> Result<Vec<u8>
.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
Expand All @@ -117,17 +140,79 @@ pub fn apply_bytes(base_image: &[u8], patch: &Patch<'_, [u8]>) -> Result<Vec<u8>
.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<u8>, Vec<usize>) {
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<usize>) {
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<ImageLine<'a, T>>,
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(())
Expand All @@ -142,22 +227,40 @@ fn apply_hunk<'a, T: PartialEq + ?Sized>(
fn find_position<T: PartialEq + ?Sized>(
image: &[ImageLine<T>],
hunk: &Hunk<'_, T>,
) -> Option<usize> {
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<T: ?Sized>(lines: &[Line<'_, T>]) -> usize {
lines
.iter()
.take_while(|x| matches!(x, Line::Context(_)))
.count()
}

fn post_context_line_count<T: ?Sized>(lines: &[Line<'_, T>]) -> usize {
lines
.iter()
.rev()
.take_while(|x| matches!(x, Line::Context(_)))
.count()
}

fn pre_image_line_count<T: ?Sized>(lines: &[Line<'_, T>]) -> usize {
pre_image(lines).count()
}
Expand All @@ -180,10 +283,13 @@ fn match_fragment<T: PartialEq + ?Sized>(
image: &[ImageLine<T>],
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;
Expand All @@ -194,7 +300,7 @@ fn match_fragment<T: PartialEq + ?Sized>(
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)]
Expand Down Expand Up @@ -241,3 +347,64 @@ where
}
}
}

fn skip_last<I: Iterator>(iter: I, count: usize) -> SkipLast<I, I::Item> {
SkipLast {
iter: iter.fuse(),
buffer: VecDeque::with_capacity(count),
count,
}
}

#[derive(Debug)]
struct SkipLast<Iter: Iterator<Item = Item>, Item> {
iter: iter::Fuse<Iter>,
buffer: VecDeque<Item>,
count: usize,
}

impl<Iter: Iterator<Item = Item>, Item> Iterator for SkipLast<Iter, Item> {
type Item = Item;

fn next(&mut self) -> Option<Self::Item> {
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::<Vec<_>>()
.as_slice(),
&[1, 2, 3, 4, 5, 6, 7]
);
assert_eq!(
skip_last(a.iter().copied(), 5)
.collect::<Vec<_>>()
.as_slice(),
&[1, 2]
);
assert_eq!(
skip_last(a.iter().copied(), 7)
.collect::<Vec<_>>()
.as_slice(),
&[]
);
}
}
48 changes: 48 additions & 0 deletions src/diff/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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![]),
)
}
2 changes: 1 addition & 1 deletion src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};