Skip to content

add patch all and fuzzy patch #14

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

Closed
wants to merge 5 commits into from
Closed
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
176 changes: 162 additions & 14 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,75 @@ 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 (pos, fuzzy) = find_position(image, hunk, options.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,16 +223,19 @@ 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));
}
}
}

Expand Down Expand Up @@ -180,10 +264,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 Down Expand Up @@ -241,3 +328,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(),
&[]
);
}
}
21 changes: 21 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
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};