java_diff_utils_rs/patch/
patch.rs1use 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#[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 pub fn new() -> Self {
87 Self::with_capacity(10)
88 }
89
90 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 #[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 pub fn add_delta(&mut self, delta: impl Into<Delta<T>>) {
111 self.deltas.push(delta.into());
112 }
113
114 pub fn get_deltas(&self) -> &[Delta<T>] {
116 &self.deltas
117 }
118
119 pub fn deltas(&self) -> &[Delta<T>] {
121 &self.deltas
122 }
123
124 pub fn deltas_mut(&mut self) -> &mut [Delta<T>] {
126 &mut self.deltas
127 }
128
129 pub fn sort_deltas(&mut self) {
131 self.deltas.sort_by_key(|d| d.source().position());
132 }
133
134 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 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 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 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 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 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 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 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 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}