Skip to main content

java_diff_utils_rs/patch/
patch.rs

1use serde::{Deserialize, Serialize};
2use std::fmt;
3
4use crate::algorithm::change::Change;
5
6use super::chunk::Chunk;
7use super::conflict_output::ConflictOutput;
8use super::delta::Delta;
9use super::delta_type::DeltaType;
10use super::error::PatchError;
11use super::verify_chunk::VerifyChunk;
12
13struct PatchApplyingContext<'a, T> {
14    result: &'a mut Vec<T>,
15    max_fuzz: usize,
16    last_patch_end: isize,
17    current_fuzz: usize,
18    default_position: usize,
19    before_out_range: bool,
20    after_out_range: bool,
21}
22
23impl<'a, T> PatchApplyingContext<'a, T> {
24    fn new(result: &'a mut Vec<T>, max_fuzz: usize) -> Self {
25        Self {
26            result,
27            max_fuzz,
28            last_patch_end: -1,
29            current_fuzz: 0,
30            default_position: 0,
31            before_out_range: false,
32            after_out_range: false,
33        }
34    }
35}
36
37/// Represents a collection of deltas to transform a source sequence into a target sequence.
38#[derive(Serialize, Deserialize)]
39#[serde(bound(serialize = "T: Serialize", deserialize = "T: Deserialize<'de>"))]
40pub struct Patch<T> {
41    deltas: Vec<Delta<T>>,
42    #[serde(skip, default)]
43    fuzzy_source: Option<Vec<T>>,
44    #[serde(skip, default)]
45    conflict_output: Option<Box<dyn ConflictOutput<T>>>,
46}
47
48impl<T: fmt::Debug> fmt::Debug for Patch<T> {
49    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
50        f.debug_struct("Patch")
51            .field("deltas", &self.deltas)
52            .field("has_conflict_output", &self.conflict_output.is_some())
53            .finish()
54    }
55}
56
57impl<T: Clone> Clone for Patch<T>
58where
59    Delta<T>: Clone,
60{
61    fn clone(&self) -> Self {
62        Self {
63            deltas: self.deltas.clone(),
64            fuzzy_source: self.fuzzy_source.clone(),
65            conflict_output: None,
66        }
67    }
68}
69
70impl<T: PartialEq> PartialEq for Patch<T> {
71    fn eq(&self, other: &Self) -> bool {
72        self.deltas == other.deltas
73    }
74}
75
76impl<T: Eq> Eq for Patch<T> {}
77
78impl<T> Default for Patch<T> {
79    fn default() -> Self {
80        Self::new()
81    }
82}
83
84impl<T> Patch<T> {
85    /// Creates a new empty `Patch`.
86    pub fn new() -> Self {
87        Self::with_capacity(10)
88    }
89
90    /// Creates a new empty `Patch` with a pre-allocated delta capacity.
91    pub fn with_capacity(capacity: usize) -> Self {
92        Self {
93            deltas: Vec::with_capacity(capacity),
94            fuzzy_source: None,
95            conflict_output: None,
96        }
97    }
98
99    /// Configures custom conflict resolution output behavior.
100    #[must_use]
101    pub fn with_conflict_output<C>(mut self, conflict_output: C) -> Self
102    where
103        C: ConflictOutput<T> + 'static,
104    {
105        self.conflict_output = Some(Box::new(conflict_output));
106        self
107    }
108
109    /// Appends a new delta modification record to this patch.
110    pub fn add_delta(&mut self, delta: impl Into<Delta<T>>) {
111        self.deltas.push(delta.into());
112    }
113
114    /// Returns an immutable slice reference to the deltas.
115    pub fn get_deltas(&self) -> &[Delta<T>] {
116        &self.deltas
117    }
118
119    /// Returns a slice reference of deltas contained in this patch.
120    pub fn deltas(&self) -> &[Delta<T>] {
121        &self.deltas
122    }
123
124    /// Returns a mutable slice reference to the deltas.
125    pub fn deltas_mut(&mut self) -> &mut [Delta<T>] {
126        &mut self.deltas
127    }
128
129    /// Sorts internal deltas in-place by source chunk position.
130    pub fn sort_deltas(&mut self) {
131        self.deltas.sort_by_key(|d| d.source().position());
132    }
133
134    /// Applies this patch to a slice, returning a new patched vector.
135    pub fn apply_to(&self, target: &[T]) -> Result<Vec<T>, PatchError>
136    where
137        T: Clone + PartialEq,
138    {
139        let mut result = target.to_vec();
140        self.apply_to_existing(&mut result)?;
141        Ok(result)
142    }
143
144    /// Applies this patch in-place to an existing vector using shared `&self`.
145    pub fn apply_to_existing(&self, target: &mut Vec<T>) -> Result<(), PatchError>
146    where
147        T: Clone + PartialEq,
148    {
149        let mut sorted_deltas: Vec<&Delta<T>> = self.deltas.iter().collect();
150        sorted_deltas.sort_by_key(|d| d.source().position());
151
152        for delta in sorted_deltas.into_iter().rev() {
153            let valid = delta.verify_and_apply_to(target)?;
154
155            if valid != VerifyChunk::Ok {
156                if let Some(ref handler) = self.conflict_output {
157                    handler.process_conflict(valid, delta, target)?;
158                } else {
159                    return Err(PatchError::PatchFailed(format!(
160                        "Could not apply patch due to {:?}",
161                        valid
162                    )));
163                }
164            }
165        }
166
167        Ok(())
168    }
169
170    /// Restores (un-applies) this patch on a target slice, returning a new restored vector.
171    pub fn restore(&self, target: &[T]) -> Result<Vec<T>, PatchError>
172    where
173        T: Clone + PartialEq,
174    {
175        let mut result = target.to_vec();
176        self.restore_to_existing(&mut result)?;
177        Ok(result)
178    }
179
180    /// Restores changes in-place on an existing vector using shared `&self`.
181    pub fn restore_to_existing(&self, target: &mut Vec<T>) -> Result<(), PatchError>
182    where
183        T: Clone + PartialEq,
184    {
185        let mut sorted_deltas: Vec<&Delta<T>> = self.deltas.iter().collect();
186        sorted_deltas.sort_by_key(|d| d.source().position());
187
188        for delta in sorted_deltas.into_iter().rev() {
189            delta.restore(target)?;
190        }
191
192        Ok(())
193    }
194
195    /// Applies this patch using fuzzy context matching.
196    pub fn apply_fuzzy(&self, target: &[T], max_fuzz: usize) -> Result<Vec<T>, PatchError>
197    where
198        T: Clone + PartialEq,
199    {
200        let mut result = target.to_vec();
201        let mut ctx = PatchApplyingContext::new(&mut result, max_fuzz);
202
203        let mut sorted_deltas: Vec<&Delta<T>> = self.deltas.iter().collect();
204        sorted_deltas.sort_by_key(|d| (d.source().position(), d.source().is_empty()));
205
206        let alignment_offset = match self.fuzzy_source.as_deref() {
207            Some(source) => find_sequence_offset(source, target).ok_or_else(|| {
208                PatchError::PatchFailed(
209                    "Cannot apply fuzzy patch without unchanged source context".into(),
210                )
211            })?,
212            None => 0,
213        };
214        let mut cumulative_offset = alignment_offset;
215
216        for delta in sorted_deltas {
217            if let Some(source) = self.fuzzy_source.as_deref() {
218                let source_position = delta.source().position();
219                let aligned_position = source_position as isize + alignment_offset;
220                if aligned_position >= 0 && !delta.source().is_empty() {
221                    let aligned_position = aligned_position as usize;
222                    let source_fuzz = (0..=delta.source().len())
223                        .find(|fuzz| {
224                            delta
225                                .source()
226                                .verify_chunk_at(target, *fuzz, aligned_position)
227                                .is_ok_and(|status| status == VerifyChunk::Ok)
228                        })
229                        .unwrap_or(delta.source().len());
230                    let mut required_fuzz = source_fuzz;
231
232                    if source_fuzz > 0 {
233                        for context_index in [
234                            source_position.checked_sub(1),
235                            source_position.checked_add(delta.source().len()),
236                        ]
237                        .into_iter()
238                        .flatten()
239                        {
240                            let target_index = context_index as isize + alignment_offset;
241                            if context_index < source.len()
242                                && target_index >= 0
243                                && (target_index as usize) < target.len()
244                                && source[context_index] != target[target_index as usize]
245                            {
246                                required_fuzz += 1;
247                            }
248                        }
249                        required_fuzz = required_fuzz.min(2);
250                    }
251
252                    if max_fuzz < required_fuzz {
253                        return Err(PatchError::PatchFailed(format!(
254                            "Fuzzy match requires fuzz {}, but maximum is {}",
255                            required_fuzz, max_fuzz
256                        )));
257                    }
258                }
259            }
260
261            let src_pos = delta.source().position() as isize;
262            let default_pos = src_pos + cumulative_offset;
263
264            if default_pos < 0 {
265                if let Some(ref handler) = self.conflict_output {
266                    handler.process_conflict(
267                        VerifyChunk::ContentDoesNotMatchTarget,
268                        delta,
269                        ctx.result,
270                    )?;
271                } else {
272                    return Err(PatchError::PatchFailed(
273                        "Negative fuzzy offset invalid for target sequence".into(),
274                    ));
275                }
276                continue;
277            }
278
279            ctx.default_position = default_pos as usize;
280
281            if let Some(patch_position) = find_position_fuzzy(&mut ctx, delta)? {
282                let old_len = ctx.result.len();
283                let fuzz = if delta.delta_type() == DeltaType::Insert {
284                    0
285                } else {
286                    ctx.current_fuzz
287                };
288                delta.apply_fuzzy_to_at(ctx.result, fuzz, patch_position)?;
289                let new_len = ctx.result.len();
290
291                let found_slop = patch_position as isize - default_pos;
292                let length_delta = (new_len as isize) - (old_len as isize);
293                cumulative_offset += found_slop + length_delta;
294
295                // Detect a pure Deletion without needing a DeltaType enum:
296                // If the applied change shrunk the array by exactly the size of the source chunk,
297                // it is a Delete delta, meaning its footprint in the resulting array is 0.
298                let src_len = delta.source().len();
299                let is_delete = src_len > 0 && length_delta == -(src_len as isize);
300
301                let effective_source_len = if is_delete { 0 } else { src_len };
302
303                ctx.last_patch_end = patch_position as isize + effective_source_len as isize;
304            } else if let Some(ref handler) = self.conflict_output {
305                handler.process_conflict(
306                    VerifyChunk::ContentDoesNotMatchTarget,
307                    delta,
308                    ctx.result,
309                )?;
310            } else {
311                return Err(PatchError::PatchFailed(format!(
312                    "Could not find fuzzy match position for delta at position {}",
313                    delta.source().position()
314                )));
315            }
316        }
317
318        Ok(result)
319    }
320
321    /// Constructs a `Patch` from sequences and raw algorithm `Change` records.
322    pub fn generate(original: &[T], revised: &[T], changes: &[Change], include_equals: bool) -> Self
323    where
324        T: Clone,
325    {
326        let mut patch = Self::with_capacity(changes.len());
327        patch.fuzzy_source = Some(original.to_vec());
328        let mut start_original = 0;
329        let mut start_revised = 0;
330
331        let mut sorted_changes = changes.to_vec();
332        sorted_changes.sort_by_key(|c| c.start_original);
333
334        for change in &sorted_changes {
335            if include_equals && start_original < change.start_original {
336                patch.add_delta(Delta::new(
337                    DeltaType::Equal,
338                    build_chunk(start_original, change.start_original, original),
339                    build_chunk(start_revised, change.start_revised, revised),
340                ));
341            }
342
343            let org_chunk = build_chunk(change.start_original, change.end_original, original);
344            let rev_chunk = build_chunk(change.start_revised, change.end_revised, revised);
345
346            patch.add_delta(Delta::new(change.delta_type, org_chunk, rev_chunk));
347
348            start_original = change.end_original;
349            start_revised = change.end_revised;
350        }
351
352        if include_equals && start_original < original.len() {
353            patch.add_delta(Delta::new(
354                DeltaType::Equal,
355                build_chunk(start_original, original.len(), original),
356                build_chunk(start_revised, revised.len(), revised),
357            ));
358        }
359
360        patch
361    }
362}
363
364fn build_chunk<T: Clone>(start: usize, end: usize, data: &[T]) -> Chunk<T> {
365    let lines = if start < end && start < data.len() {
366        let actual_end = end.min(data.len());
367        data[start..actual_end].to_vec()
368    } else {
369        Vec::new()
370    };
371    Chunk::with_lines(start, lines)
372}
373
374fn find_sequence_offset<T: PartialEq>(source: &[T], target: &[T]) -> Option<isize> {
375    let min_offset = -(source.len() as isize);
376    let max_offset = target.len() as isize;
377    let mut best_offset: isize = 0;
378    let mut best_score = 0;
379
380    for offset in min_offset..=max_offset {
381        let score = source
382            .iter()
383            .enumerate()
384            .filter(|(index, line)| {
385                let target_index = *index as isize + offset;
386                target_index >= 0
387                    && (target_index as usize) < target.len()
388                    && target[target_index as usize] == **line
389            })
390            .count();
391
392        if score > best_score || (score == best_score && offset.abs() < best_offset.abs()) {
393            best_score = score;
394            best_offset = offset;
395        }
396    }
397
398    (best_score > 0).then_some(best_offset)
399}
400
401fn find_position_fuzzy<T: PartialEq>(
402    ctx: &mut PatchApplyingContext<'_, T>,
403    delta: &Delta<T>,
404) -> Result<Option<usize>, PatchError> {
405    if delta.source().is_empty() && ctx.default_position < ctx.last_patch_end as usize {
406        return Ok(Some(ctx.last_patch_end as usize));
407    }
408
409    for fuzz in 0..=ctx.max_fuzz {
410        ctx.current_fuzz = fuzz;
411        if let Some(pos) = find_position_with_fuzz(ctx, delta, fuzz)? {
412            return Ok(Some(pos));
413        }
414    }
415    Ok(None)
416}
417
418fn find_position_with_fuzz<T: PartialEq>(
419    ctx: &mut PatchApplyingContext<'_, T>,
420    delta: &Delta<T>,
421    fuzz: usize,
422) -> Result<Option<usize>, PatchError> {
423    ctx.before_out_range = false;
424    ctx.after_out_range = false;
425
426    let mut more_delta = 0_usize;
427    loop {
428        if let Some(pos) = find_position_with_fuzz_and_more_delta(ctx, delta, fuzz, more_delta)? {
429            return Ok(Some(pos));
430        }
431
432        if ctx.before_out_range && ctx.after_out_range {
433            break;
434        }
435
436        match more_delta.checked_add(1) {
437            Some(next) => more_delta = next,
438            None => break,
439        }
440    }
441
442    Ok(None)
443}
444
445fn find_position_with_fuzz_and_more_delta<T: PartialEq>(
446    ctx: &mut PatchApplyingContext<'_, T>,
447    delta: &Delta<T>,
448    fuzz: usize,
449    more_delta: usize,
450) -> Result<Option<usize>, PatchError> {
451    if !ctx.before_out_range {
452        if ctx.default_position < more_delta {
453            ctx.before_out_range = true;
454        } else {
455            let begin_at = ctx.default_position - more_delta;
456            let begin_at_isize = begin_at as isize;
457
458            if begin_at_isize < ctx.last_patch_end {
459                ctx.before_out_range = true;
460            }
461        }
462    }
463
464    if !ctx.after_out_range {
465        let src_len = delta.source().len();
466        let effective_len = src_len.saturating_sub(2 * fuzz);
467
468        // FIX: Prevent usize wrap-around from bypassing the loop termination guard
469        let begin_at = ctx
470            .default_position
471            .saturating_add(more_delta)
472            .saturating_add(effective_len);
473
474        if begin_at > ctx.result.len() {
475            ctx.after_out_range = true;
476        }
477    }
478
479    if !ctx.before_out_range {
480        let test_pos = ctx.default_position - more_delta;
481        let before = delta.source().verify_chunk_at(ctx.result, fuzz, test_pos)?;
482        if before == VerifyChunk::Ok {
483            return Ok(Some(test_pos));
484        }
485    }
486
487    if !ctx.after_out_range && more_delta > 0 {
488        // FIX: Prevent overflow on the forward probe position
489        let test_pos = ctx.default_position.saturating_add(more_delta);
490        let after = delta.source().verify_chunk_at(ctx.result, fuzz, test_pos)?;
491        if after == VerifyChunk::Ok {
492            return Ok(Some(test_pos));
493        }
494    }
495
496    Ok(None)
497}
498
499impl<T: fmt::Display> fmt::Display for Patch<T> {
500    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
501        write!(f, "Patch{{deltas=[")?;
502        for (i, d) in self.deltas.iter().enumerate() {
503            if i > 0 {
504                write!(f, ", ")?;
505            }
506            write!(f, "{}", d)?;
507        }
508        write!(f, "]}}")?;
509        Ok(())
510    }
511}