1use alloc::string::String;
5use alloc::vec;
6use alloc::vec::Vec;
7
8use hashbrown::HashMap;
9use thiserror::Error;
10
11pub const BLOCK_SIZE: usize = 4096;
13
14const RABIN_BASE: u64 = 257;
15const MERSENNE61: u64 = (1u64 << 61) - 1;
16
17const BINARY_CHECK_WINDOW: usize = 8192;
18
19const OP_FULL: u8 = 0x00;
20const OP_PREFIX_SUFFIX: u8 = 0x01;
21const OP_INSTRUCTIONS: u8 = 0x02;
22const OP_BINARY_XOR: u8 = 0x03;
23
24const INSTR_COPY: u8 = 0x01;
25const INSTR_INSERT: u8 = 0x02;
26
27#[derive(Debug, Clone, Copy, PartialEq, Eq)]
29pub enum MismatchSide {
30 Target,
32 Base,
34 Result,
36}
37
38impl core::fmt::Display for MismatchSide {
39 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
40 f.write_str(match self {
41 MismatchSide::Target => "target",
42 MismatchSide::Base => "base",
43 MismatchSide::Result => "result",
44 })
45 }
46}
47
48#[derive(Error, Debug, Clone, PartialEq, Eq)]
52pub enum DeltaError {
53 #[error("delta is empty")]
55 Empty,
56
57 #[error("invalid delta opcode: {0:#04x}")]
59 InvalidOpcode(u8),
60
61 #[error("truncated delta (opcode {opcode:#04x}): needed {needed} bytes, got {got}")]
63 Truncated {
64 opcode: u8,
66 needed: usize,
68 got: usize,
70 },
71
72 #[error("invalid instruction opcode: {0:#04x}")]
74 InvalidInstruction(u8),
75
76 #[error("copy out of range: base_offset {base_offset} + length {length} exceeds base length {base_len}")]
78 CopyOutOfRange {
79 base_offset: u64,
81 length: u32,
83 base_len: usize,
85 },
86
87 #[error("insert out of range: declared {declared} bytes, {remaining} available")]
89 InsertOutOfRange {
90 declared: u32,
92 remaining: usize,
94 },
95
96 #[error("target length mismatch ({side}): declared {declared}, reconstructed {reconstructed}")]
98 TargetLengthMismatch {
99 side: MismatchSide,
101 declared: usize,
103 reconstructed: usize,
105 },
106
107 #[error("checksum mismatch: {side} checksum does not match")]
109 ChecksumMismatch {
110 side: MismatchSide,
112 },
113
114 #[error("decompression error: {0}")]
116 Decompression(String),
117
118 #[error("binary (0x03) delta requires the \"zstd\" feature")]
121 ZstdDisabled,
122}
123
124#[inline]
125fn mersenne_reduce(x: u128) -> u64 {
126 let mut r = (x & u128::from(MERSENNE61)) + (x >> 61);
127 if r >= u128::from(MERSENNE61) {
128 r -= u128::from(MERSENNE61);
129 }
130 r as u64
131}
132
133#[inline]
134fn mod_sub(a: u64, b: u64) -> u64 {
135 mersenne_reduce(u128::from(a) + u128::from(MERSENNE61) - u128::from(b))
136}
137
138fn mod_pow(mut base: u64, mut exp: usize) -> u64 {
139 let mut result: u64 = 1;
140 while exp > 0 {
141 if exp & 1 == 1 {
142 result = mersenne_reduce(u128::from(result) * u128::from(base));
143 }
144 base = mersenne_reduce(u128::from(base) * u128::from(base));
145 exp >>= 1;
146 }
147 result
148}
149
150fn rabin_hash(data: &[u8]) -> u64 {
151 let mut h: u64 = 0;
152 for &b in data {
153 h = mersenne_reduce(u128::from(h) * u128::from(RABIN_BASE) + u128::from(b));
154 }
155 h
156}
157
158fn rabin_roll(h: u64, old_byte: u8, new_byte: u8, base_power: u64) -> u64 {
159 let old_contrib = mersenne_reduce(u128::from(old_byte) * u128::from(base_power));
160 let h2 = mod_sub(h, old_contrib);
161 mersenne_reduce(u128::from(h2) * u128::from(RABIN_BASE) + u128::from(new_byte))
162}
163
164fn strong_hash(data: &[u8]) -> u64 {
165 let mut h: u64 = 0xcbf2_3ce4_8422_2325;
166 for &b in data {
167 h ^= u64::from(b);
168 h = h.wrapping_mul(0x0100_0000_01b3);
169 }
170 h ^= h >> 33;
171 h = h.wrapping_mul(0xff51_afd7_ed55_8ccd);
172 h ^= h >> 33;
173 h = h.wrapping_mul(0xc4ce_b9fe_1a85_ec53);
174 h ^= h >> 33;
175 h
176}
177
178fn is_likely_binary(data: &[u8]) -> bool {
179 let window = core::cmp::min(data.len(), BINARY_CHECK_WINDOW);
180 data[..window].contains(&0u8)
181}
182
183#[cfg(feature = "zstd")]
186fn xor_streams(base: &[u8], target: &[u8]) -> Vec<u8> {
187 let min_len = base.len().min(target.len());
188 let mut xor_data = Vec::with_capacity(target.len());
189 for i in 0..min_len {
190 xor_data.push(base[i] ^ target[i]);
191 }
192 if target.len() > base.len() {
193 xor_data.extend_from_slice(&target[base.len()..]);
194 }
195 xor_data
196}
197
198#[cfg(feature = "zstd")]
204#[must_use]
205pub fn compute_binary_delta(base: &[u8], target: &[u8]) -> Option<Vec<u8>> {
206 let base_hash = blake3::hash(base);
207 let target_hash = blake3::hash(target);
208
209 let xor_data = xor_streams(base, target);
210 let compressed = zstd::encode_all(xor_data.as_slice(), 3).ok()?;
211
212 if compressed.len() >= target.len() {
213 return None;
214 }
215
216 let mut delta = Vec::with_capacity(41 + compressed.len());
217 delta.push(OP_BINARY_XOR);
218 delta.extend_from_slice(&(target.len() as u64).to_le_bytes());
219 delta.extend_from_slice(&base_hash.as_bytes()[..16]);
220 delta.extend_from_slice(&target_hash.as_bytes()[..16]);
221 delta.extend_from_slice(&compressed);
222
223 Some(delta)
224}
225
226enum DeltaInstr {
227 Copy { base_offset: u64, length: u32 },
228 Insert { data: Vec<u8> },
229}
230
231fn compute_rolling_delta(base: &[u8], target: &[u8]) -> Option<Vec<u8>> {
232 if base.len() < BLOCK_SIZE || target.len() < BLOCK_SIZE {
233 return None;
234 }
235
236 let num_blocks = base.len() / BLOCK_SIZE;
237 if num_blocks == 0 {
238 return None;
239 }
240
241 let mut hash_table: HashMap<u64, Vec<(usize, u64)>> = HashMap::new();
242 for i in 0..num_blocks {
243 let block = &base[i * BLOCK_SIZE..(i + 1) * BLOCK_SIZE];
244 let rh = rabin_hash(block);
245 let sh = strong_hash(block);
246 hash_table.entry(rh).or_default().push((i, sh));
247 }
248
249 let base_power = mod_pow(RABIN_BASE, BLOCK_SIZE - 1);
250 let mut instructions: Vec<DeltaInstr> = Vec::new();
251 let mut pending_insert_start: usize = 0;
252 let mut pos: usize = 0;
253 let mut prev_rabin: Option<u64> = None;
254
255 while pos + BLOCK_SIZE <= target.len() {
256 let rh = match prev_rabin {
257 Some(pr) if pos > 0 => rabin_roll(
258 pr,
259 target[pos - 1],
260 target[pos + BLOCK_SIZE - 1],
261 base_power,
262 ),
263 _ => rabin_hash(&target[pos..pos + BLOCK_SIZE]),
264 };
265 prev_rabin = Some(rh);
266
267 let mut matched = false;
268 if let Some(candidates) = hash_table.get(&rh) {
269 let sh = strong_hash(&target[pos..pos + BLOCK_SIZE]);
270 for &(block_idx, ref_sh) in candidates {
271 if sh == ref_sh {
272 let base_offset = block_idx * BLOCK_SIZE;
273 let mut match_len = BLOCK_SIZE;
274
275 while pos + match_len < target.len()
276 && base_offset + match_len < base.len()
277 && target[pos + match_len] == base[base_offset + match_len]
278 {
279 match_len += 1;
280 }
281
282 let match_len = match_len.min(u32::MAX as usize);
283
284 if pending_insert_start < pos {
285 instructions.push(DeltaInstr::Insert {
286 data: target[pending_insert_start..pos].to_vec(),
287 });
288 }
289
290 instructions.push(DeltaInstr::Copy {
291 base_offset: base_offset as u64,
292 length: match_len as u32,
293 });
294
295 pos += match_len;
296 pending_insert_start = pos;
297 prev_rabin = None;
298 matched = true;
299 break;
300 }
301 }
302 }
303
304 if !matched {
305 pos += 1;
306 }
307 }
308
309 if pending_insert_start < target.len() {
310 instructions.push(DeltaInstr::Insert {
311 data: target[pending_insert_start..].to_vec(),
312 });
313 }
314
315 let mut delta = Vec::new();
316 delta.push(OP_INSTRUCTIONS);
317 delta.extend_from_slice(&(target.len() as u64).to_le_bytes());
318 delta.extend_from_slice(&(instructions.len() as u32).to_le_bytes());
319
320 for instr in &instructions {
321 match instr {
322 DeltaInstr::Copy {
323 base_offset,
324 length,
325 } => {
326 delta.push(INSTR_COPY);
327 delta.extend_from_slice(&base_offset.to_le_bytes());
328 delta.extend_from_slice(&length.to_le_bytes());
329 }
330 DeltaInstr::Insert { data } => {
331 delta.push(INSTR_INSERT);
332 delta.extend_from_slice(&(data.len() as u32).to_le_bytes());
333 delta.extend_from_slice(data);
334 }
335 }
336 }
337
338 if delta.len() < target.len() {
339 Some(delta)
340 } else {
341 None
342 }
343}
344
345#[must_use]
352pub fn compute_delta(base: &[u8], target: &[u8]) -> (Vec<u8>, Vec<u8>) {
353 #[cfg(feature = "zstd")]
354 if is_likely_binary(base) || is_likely_binary(target) {
355 if let Some(delta) = compute_binary_delta(base, target) {
356 return (base.to_vec(), delta);
357 }
358 }
359
360 if base.len() >= BLOCK_SIZE && target.len() >= BLOCK_SIZE {
361 if let Some(delta) = compute_rolling_delta(base, target) {
362 return (base.to_vec(), delta);
363 }
364 let mut full = vec![OP_FULL];
365 full.extend_from_slice(target);
366 return (base.to_vec(), full);
367 }
368
369 let prefix_len = base
370 .iter()
371 .zip(target.iter())
372 .take_while(|(a, b)| a == b)
373 .count();
374
375 let max_suffix_base = base.len().saturating_sub(prefix_len);
376 let max_suffix_target = target.len().saturating_sub(prefix_len);
377 let suffix_len = base[prefix_len..]
378 .iter()
379 .rev()
380 .zip(target[prefix_len..].iter().rev())
381 .take_while(|(a, b)| a == b)
382 .count()
383 .min(max_suffix_base)
384 .min(max_suffix_target);
385
386 let changed_start = prefix_len;
387 let changed_end_target = target.len().saturating_sub(suffix_len);
388 let changed = &target[changed_start..changed_end_target];
389
390 if changed.len() < target.len() {
391 let mut delta = Vec::new();
392 delta.push(OP_PREFIX_SUFFIX);
393 delta.extend_from_slice(&(prefix_len as u64).to_le_bytes());
394 delta.extend_from_slice(&(suffix_len as u64).to_le_bytes());
395 delta.extend_from_slice(&(target.len() as u64).to_le_bytes());
396 delta.extend_from_slice(changed);
397 (base.to_vec(), delta)
398 } else {
399 let mut full = vec![OP_FULL];
400 full.extend_from_slice(target);
401 (base.to_vec(), full)
402 }
403}
404
405fn read_u64_le(delta: &[u8], at: usize, opcode: u8) -> Result<u64, DeltaError> {
407 let end = at
408 .checked_add(8)
409 .filter(|&e| e <= delta.len())
410 .ok_or(DeltaError::Truncated {
411 opcode,
412 needed: at + 8,
413 got: delta.len(),
414 })?;
415 let bytes =
417 <[u8; 8]>::try_from(&delta[at..end]).expect("slice length is exactly 8 by construction");
418 Ok(u64::from_le_bytes(bytes))
419}
420
421fn read_u32_le(delta: &[u8], at: usize, opcode: u8) -> Result<u32, DeltaError> {
423 let end = at
424 .checked_add(4)
425 .filter(|&e| e <= delta.len())
426 .ok_or(DeltaError::Truncated {
427 opcode,
428 needed: at + 4,
429 got: delta.len(),
430 })?;
431 let bytes =
433 <[u8; 4]>::try_from(&delta[at..end]).expect("slice length is exactly 4 by construction");
434 Ok(u32::from_le_bytes(bytes))
435}
436
437fn length_mismatch(side: MismatchSide, declared: u64, reconstructed: usize) -> DeltaError {
438 DeltaError::TargetLengthMismatch {
439 side,
440 declared: usize::try_from(declared).unwrap_or(usize::MAX),
441 reconstructed,
442 }
443}
444
445pub fn apply_delta(base: &[u8], delta: &[u8]) -> Result<Vec<u8>, DeltaError> {
453 let Some((&opcode, rest)) = delta.split_first() else {
454 return Err(DeltaError::Empty);
455 };
456
457 match opcode {
458 OP_FULL => Ok(rest.to_vec()),
459
460 OP_PREFIX_SUFFIX => {
461 if delta.len() < 25 {
462 return Err(DeltaError::Truncated {
463 opcode,
464 needed: 25,
465 got: delta.len(),
466 });
467 }
468 let prefix_len = read_u64_le(delta, 1, opcode)? as usize;
469 let suffix_len = read_u64_le(delta, 9, opcode)? as usize;
470 let total_len = read_u64_le(delta, 17, opcode)?;
471 let changed = &delta[25..];
472
473 let prefix_take = prefix_len.min(base.len());
474 let mut result = Vec::with_capacity(total_len.min(usize::MAX as u64) as usize);
475 result.extend_from_slice(&base[..prefix_take]);
476 result.extend_from_slice(changed);
477 result.extend_from_slice(&base[base.len().saturating_sub(suffix_len)..]);
478
479 if result.len() as u64 != total_len {
480 return Err(length_mismatch(
481 MismatchSide::Target,
482 total_len,
483 result.len(),
484 ));
485 }
486 Ok(result)
487 }
488
489 OP_INSTRUCTIONS => {
490 if delta.len() < 13 {
491 return Err(DeltaError::Truncated {
492 opcode,
493 needed: 13,
494 got: delta.len(),
495 });
496 }
497 let target_len = read_u64_le(delta, 1, opcode)?;
498 let num_instr = read_u32_le(delta, 9, opcode)?;
499 let mut result: Vec<u8> =
500 Vec::with_capacity(target_len.min(usize::MAX as u64) as usize);
501 let mut offset = 13usize;
502
503 for _ in 0..num_instr {
504 let Some(&instr) = delta.get(offset) else {
505 return Err(DeltaError::Truncated {
506 opcode,
507 needed: offset + 1,
508 got: delta.len(),
509 });
510 };
511 match instr {
512 INSTR_COPY => {
513 if offset + 13 > delta.len() {
514 return Err(DeltaError::Truncated {
515 opcode,
516 needed: offset + 13,
517 got: delta.len(),
518 });
519 }
520 let base_offset = read_u64_le(delta, offset + 1, opcode)?;
521 let length = read_u32_le(delta, offset + 9, opcode)?;
522 let bo = usize::try_from(base_offset).map_err(|_| {
523 DeltaError::CopyOutOfRange {
524 base_offset,
525 length,
526 base_len: base.len(),
527 }
528 })?;
529 let ln =
530 usize::try_from(length).map_err(|_| DeltaError::CopyOutOfRange {
531 base_offset,
532 length,
533 base_len: base.len(),
534 })?;
535 let end = bo.saturating_add(ln);
536 if end > base.len() {
537 return Err(DeltaError::CopyOutOfRange {
538 base_offset,
539 length,
540 base_len: base.len(),
541 });
542 }
543 result.extend_from_slice(&base[bo..end]);
544 offset += 13;
545 }
546 INSTR_INSERT => {
547 if offset + 5 > delta.len() {
548 return Err(DeltaError::Truncated {
549 opcode,
550 needed: offset + 5,
551 got: delta.len(),
552 });
553 }
554 let declared_len = read_u32_le(delta, offset + 1, opcode)?;
555 let length = usize::try_from(declared_len).map_err(|_| {
556 DeltaError::InsertOutOfRange {
557 declared: declared_len,
558 remaining: delta.len() - offset - 5,
559 }
560 })?;
561 let data_end = offset.checked_add(5).and_then(|v| v.checked_add(length));
562 match data_end {
563 None => {
564 return Err(DeltaError::InsertOutOfRange {
565 declared: declared_len,
566 remaining: delta.len() - offset - 5,
567 })
568 }
569 Some(data_end) if data_end > delta.len() => {
570 return Err(DeltaError::InsertOutOfRange {
571 declared: declared_len,
572 remaining: delta.len() - offset - 5,
573 })
574 }
575 Some(data_end) => {
576 result.extend_from_slice(&delta[offset + 5..data_end]);
577 offset = data_end;
578 }
579 }
580 }
581 other => return Err(DeltaError::InvalidInstruction(other)),
582 }
583 }
584
585 if result.len() as u64 != target_len {
586 return Err(length_mismatch(
587 MismatchSide::Target,
588 target_len,
589 result.len(),
590 ));
591 }
592 Ok(result)
593 }
594
595 OP_BINARY_XOR => {
596 #[cfg(feature = "zstd")]
597 {
598 if delta.len() < 41 {
599 return Err(DeltaError::Truncated {
600 opcode,
601 needed: 41,
602 got: delta.len(),
603 });
604 }
605 let target_len = read_u64_le(delta, 1, opcode)?;
606 let base_checksum = &delta[9..25];
607 let target_checksum = &delta[25..41];
608 let compressed = &delta[41..];
609
610 let base_hash = blake3::hash(base);
611 if base_hash.as_bytes()[..16] != *base_checksum {
612 return Err(DeltaError::ChecksumMismatch {
613 side: MismatchSide::Base,
614 });
615 }
616
617 let xor_data = zstd::decode_all(compressed)
618 .map_err(|e| DeltaError::Decompression(e.to_string()))?;
619
620 let mut result = Vec::with_capacity(target_len.min(usize::MAX as u64) as usize);
621 let min_len = base.len().min(xor_data.len());
622 for i in 0..min_len {
623 result.push(base[i] ^ xor_data[i]);
624 }
625 if xor_data.len() > base.len() {
626 result.extend_from_slice(&xor_data[base.len()..]);
627 }
628
629 if result.len() as u64 != target_len {
630 return Err(length_mismatch(
631 MismatchSide::Target,
632 target_len,
633 result.len(),
634 ));
635 }
636
637 let result_hash = blake3::hash(&result);
638 if result_hash.as_bytes()[..16] != *target_checksum {
639 return Err(DeltaError::ChecksumMismatch {
640 side: MismatchSide::Result,
641 });
642 }
643
644 Ok(result)
645 }
646
647 #[cfg(not(feature = "zstd"))]
648 {
649 let _ = rest;
650 Err(DeltaError::ZstdDisabled)
651 }
652 }
653
654 other => Err(DeltaError::InvalidOpcode(other)),
655 }
656}
657
658fn read_u64_or_zero(delta: &[u8], at: usize) -> u64 {
661 let mut bytes = [0u8; 8];
662 if let Some(slice) = delta.get(at..at.saturating_add(8)) {
663 if slice.len() == 8 {
664 bytes.copy_from_slice(slice);
665 }
666 }
667 u64::from_le_bytes(bytes)
668}
669
670fn read_u32_or_zero(delta: &[u8], at: usize) -> u32 {
672 let mut bytes = [0u8; 4];
673 if let Some(slice) = delta.get(at..at.saturating_add(4)) {
674 if slice.len() == 4 {
675 bytes.copy_from_slice(slice);
676 }
677 }
678 u32::from_le_bytes(bytes)
679}
680
681#[must_use]
704pub fn apply_delta_lenient(base: &[u8], delta: &[u8]) -> Vec<u8> {
705 let Some((&opcode, rest)) = delta.split_first() else {
706 return Vec::new();
707 };
708
709 match opcode {
710 OP_FULL => rest.to_vec(),
711
712 OP_PREFIX_SUFFIX => {
713 if delta.len() < 25 {
714 return delta.to_vec();
715 }
716 let prefix_len = read_u64_or_zero(delta, 1) as usize;
717 let suffix_len = read_u64_or_zero(delta, 9) as usize;
718 let total_len = read_u64_or_zero(delta, 17) as usize;
719 let changed = &delta[25..];
720
721 let prefix_take = prefix_len.min(base.len());
722 let mut result = Vec::with_capacity(
723 total_len.min(prefix_take + changed.len() + base.len().min(suffix_len)),
724 );
725 result.extend_from_slice(&base[..prefix_take]);
726 result.extend_from_slice(changed);
727 result.extend_from_slice(&base[base.len().saturating_sub(suffix_len)..]);
728 result
729 }
730
731 OP_INSTRUCTIONS => {
732 if delta.len() < 13 {
733 return delta.to_vec();
734 }
735 let target_len = read_u64_or_zero(delta, 1) as usize;
736 let num_instr = read_u32_or_zero(delta, 9) as usize;
737 let mut result = Vec::with_capacity(target_len.min(delta.len() + base.len()));
738 let mut offset = 13usize;
739
740 for _ in 0..num_instr {
741 if offset >= delta.len() {
742 break;
743 }
744 match delta[offset] {
745 INSTR_COPY => {
746 if offset + 13 > delta.len() {
747 break;
748 }
749 let base_offset = read_u64_or_zero(delta, offset + 1) as usize;
750 let length = read_u32_or_zero(delta, offset + 9) as usize;
751 let end = base_offset.saturating_add(length);
752 if end <= base.len() {
753 result.extend_from_slice(&base[base_offset..end]);
754 }
755 offset += 13;
756 }
757 INSTR_INSERT => {
758 if offset + 5 > delta.len() {
759 break;
760 }
761 let length = read_u32_or_zero(delta, offset + 1) as usize;
762 let data_end = offset.saturating_add(5).saturating_add(length);
763 if data_end <= delta.len() {
764 result.extend_from_slice(&delta[offset + 5..data_end]);
765 offset = data_end;
766 }
767 }
768 _ => break,
769 }
770 }
771
772 result
773 }
774
775 OP_BINARY_XOR => {
776 #[cfg(feature = "zstd")]
777 {
778 if delta.len() < 41 {
779 return delta.to_vec();
780 }
781 let base_checksum = &delta[9..25];
782 let target_checksum = &delta[25..41];
783 let compressed = &delta[41..];
784
785 let base_hash = blake3::hash(base);
786 if base_hash.as_bytes()[..16] != *base_checksum {
787 return Vec::new();
788 }
789
790 let Ok(xor_data) = zstd::decode_all(compressed) else {
791 return delta.to_vec();
792 };
793
794 let mut result = Vec::with_capacity(base.len().max(xor_data.len()));
795 let min_len = base.len().min(xor_data.len());
796 for i in 0..min_len {
797 result.push(base[i] ^ xor_data[i]);
798 }
799 if xor_data.len() > base.len() {
800 result.extend_from_slice(&xor_data[base.len()..]);
801 }
802
803 let result_hash = blake3::hash(&result);
804 if result_hash.as_bytes()[..16] != *target_checksum {
805 return Vec::new();
806 }
807
808 result
809 }
810
811 #[cfg(not(feature = "zstd"))]
812 {
813 delta.to_vec()
814 }
815 }
816
817 _ => delta.to_vec(),
818 }
819}
820
821#[cfg(test)]
822mod tests {
823 use super::*;
824
825 type TestResult = Result<(), Box<dyn std::error::Error>>;
828
829 #[test]
832 fn test_delta_roundtrip() {
833 let base = b"Hello, World!";
834 let target = b"Hello, Rust!";
835 let (_base_copy, delta) = compute_delta(base, target);
836 let result = apply_delta(base, &delta).expect("well-formed delta must apply");
837 assert_eq!(result, target);
838 }
839
840 #[test]
841 fn test_delta_no_change() {
842 let base = b"identical data here";
843 let target = b"identical data here";
844 let (_base_copy, delta) = compute_delta(base, target);
845 assert!(delta.len() < target.len() + 25);
846 let result = apply_delta(base, &delta).expect("well-formed delta must apply");
847 assert_eq!(result, target);
848 }
849
850 #[test]
851 fn test_delta_completely_different() {
852 let base = b"AAAA";
853 let target = b"BBBB";
854 let (_base_copy, delta) = compute_delta(base, target);
855 let result = apply_delta(base, &delta).expect("well-formed delta must apply");
856 assert_eq!(result, target);
857 }
858
859 #[cfg(feature = "zstd")]
862 #[test]
863 fn test_binary_delta_roundtrip() -> TestResult {
864 let base = vec![0u8; 8192];
866 let mut target = vec![0u8; 8192];
867 target[100] = 0xAB;
868 let last = target.len() - 1;
869 target[last] = 0xCD;
870
871 let (_c, delta) = compute_delta(&base, &target);
872 assert_eq!(delta[0], OP_BINARY_XOR, "binary inputs must use 0x03");
873 let result = apply_delta(&base, &delta)?;
874 assert_eq!(result, target);
875 Ok(())
876 }
877
878 #[test]
879 fn test_rolling_delta_roundtrip_and_opcode() -> TestResult {
880 let base: Vec<u8> = (0..3 * BLOCK_SIZE).map(|i| b'A' + (i % 26) as u8).collect();
882 let mut target = base.clone();
883 target.splice(100..110, b"XX".to_vec());
884
885 let (_c, delta) = compute_delta(&base, &target);
886 assert_eq!(delta[0], OP_INSTRUCTIONS, "large text inputs must use 0x02");
887 assert!(delta.len() < target.len(), "rolling delta must shrink here");
888 let result = apply_delta(&base, &delta)?;
889 assert_eq!(result, target);
890 Ok(())
891 }
892
893 #[test]
894 fn test_prefix_suffix_opcode() -> TestResult {
895 let base = b"Hello, World!".to_vec();
896 let target = b"Hello, Rust!".to_vec();
897 let (_c, delta) = compute_delta(&base, &target);
898 assert_eq!(delta[0], OP_PREFIX_SUFFIX);
899 assert_eq!(apply_delta(&base, &delta)?, target);
900 Ok(())
901 }
902
903 #[test]
904 fn test_full_opcode_fallback() -> TestResult {
905 let base = vec![0u8; 4];
907 let target = vec![1u8, 2, 3];
908 let (_c, delta) = compute_delta(&base, &target);
909 assert_eq!(delta[0], OP_FULL);
910 assert_eq!(&delta[1..], &target[..]);
911 assert_eq!(apply_delta(&base, &delta)?, target);
912 Ok(())
913 }
914
915 #[test]
918 fn test_apply_empty_delta_is_error() {
919 assert_eq!(apply_delta(b"abc", &[]), Err(DeltaError::Empty));
920 }
921
922 #[test]
923 fn test_apply_unknown_opcode_is_error() {
924 assert_eq!(
926 apply_delta(b"abc", &[0x7F, 1, 2, 3]),
927 Err(DeltaError::InvalidOpcode(0x7F))
928 );
929 }
930
931 #[cfg(feature = "zstd")]
932 #[test]
933 fn test_apply_truncated_headers_are_errors() {
934 let short01 = vec![OP_PREFIX_SUFFIX; 10];
936 assert!(matches!(
937 apply_delta(b"abc", &short01),
938 Err(DeltaError::Truncated {
939 opcode: OP_PREFIX_SUFFIX,
940 ..
941 })
942 ));
943
944 let short02 = vec![OP_INSTRUCTIONS; 8];
946 assert!(matches!(
947 apply_delta(b"abc", &short02),
948 Err(DeltaError::Truncated {
949 opcode: OP_INSTRUCTIONS,
950 ..
951 })
952 ));
953 }
954
955 #[cfg(feature = "zstd")]
956 #[test]
957 fn test_apply_truncated_binary_header_is_error() {
958 let short03 = vec![OP_BINARY_XOR; 20];
960 assert!(matches!(
961 apply_delta(b"abc", &short03),
962 Err(DeltaError::Truncated {
963 opcode: OP_BINARY_XOR,
964 ..
965 })
966 ));
967 }
968
969 #[cfg(not(feature = "zstd"))]
970 #[test]
971 fn test_binary_opcode_disabled_without_feature() {
972 assert_eq!(
973 apply_delta(b"abc", &[OP_BINARY_XOR]),
974 Err(DeltaError::ZstdDisabled)
975 );
976 }
977
978 #[test]
979 fn test_apply_copy_out_of_range_is_error() {
980 let mut d = vec![OP_INSTRUCTIONS];
981 d.extend_from_slice(&5u64.to_le_bytes()); d.extend_from_slice(&1u32.to_le_bytes()); d.push(INSTR_COPY);
984 d.extend_from_slice(&1_000u64.to_le_bytes()); d.extend_from_slice(&2u32.to_le_bytes()); assert!(matches!(
987 apply_delta(b"abc", &d),
988 Err(DeltaError::CopyOutOfRange { .. })
989 ));
990 }
991
992 #[test]
993 fn test_apply_insert_past_end_is_error() {
994 let mut d = vec![OP_INSTRUCTIONS];
995 d.extend_from_slice(&8u64.to_le_bytes()); d.extend_from_slice(&1u32.to_le_bytes()); d.push(INSTR_INSERT);
998 d.extend_from_slice(&100u32.to_le_bytes()); assert!(matches!(
1000 apply_delta(b"abc", &d),
1001 Err(DeltaError::InsertOutOfRange { .. })
1002 ));
1003 }
1004
1005 #[test]
1006 fn test_apply_unknown_instruction_is_error() {
1007 let mut d = vec![OP_INSTRUCTIONS];
1008 d.extend_from_slice(&1u64.to_le_bytes());
1009 d.extend_from_slice(&1u32.to_le_bytes());
1010 d.push(0x42); assert_eq!(
1012 apply_delta(b"abc", &d),
1013 Err(DeltaError::InvalidInstruction(0x42))
1014 );
1015 }
1016
1017 #[test]
1018 fn test_apply_target_length_mismatch_is_error() {
1019 let mut d = vec![OP_INSTRUCTIONS];
1020 d.extend_from_slice(&9u64.to_le_bytes()); d.extend_from_slice(&0u32.to_le_bytes()); assert!(matches!(
1023 apply_delta(b"abc", &d),
1024 Err(DeltaError::TargetLengthMismatch { .. })
1025 ));
1026 }
1027
1028 #[cfg(feature = "zstd")]
1029 #[test]
1030 fn test_apply_binary_checksum_mismatch_is_error() -> TestResult {
1031 let mut d = vec![OP_BINARY_XOR];
1033 d.extend_from_slice(&4u64.to_le_bytes()); d.extend_from_slice(&[0u8; 16]); d.extend_from_slice(&[0u8; 16]); let frame = zstd::encode_all(b"abcd".as_slice(), 3)?;
1037 d.extend_from_slice(&frame);
1038 assert!(matches!(
1039 apply_delta(b"zzzz", &d),
1040 Err(DeltaError::ChecksumMismatch {
1041 side: MismatchSide::Base
1042 })
1043 ));
1044 Ok(())
1045 }
1046
1047 #[cfg(feature = "zstd")]
1048 #[test]
1049 fn test_binary_delta_tamper_is_error() -> TestResult {
1050 let base = vec![0u8; 4096];
1051 let target = vec![7u8; 4096];
1052 let (_c, mut delta) = compute_delta(&base, &target);
1053 assert_eq!(delta[0], OP_BINARY_XOR);
1054 let last = delta.len() - 1;
1056 delta[last] ^= 0xFF;
1057 assert!(apply_delta(&base, &delta).is_err());
1058 Ok(())
1059 }
1060
1061 #[test]
1062 fn test_empty_target() -> TestResult {
1063 let (_c, delta) = compute_delta(b"base", b"");
1064 assert_eq!(delta, vec![OP_FULL]);
1065 assert_eq!(apply_delta(b"base", &delta)?, Vec::<u8>::new());
1066 Ok(())
1067 }
1068
1069 #[test]
1070 fn test_identical_empty() {
1071 let (_c, delta) = compute_delta(b"", b"");
1072 assert_eq!(delta, vec![OP_FULL]);
1074 }
1075
1076 #[test]
1079 fn test_lenient_empty_delta_returns_empty() {
1080 assert_eq!(apply_delta_lenient(b"abc", &[]), Vec::<u8>::new());
1081 }
1082
1083 #[test]
1084 fn test_lenient_unknown_opcode_echoes_delta() {
1085 assert_eq!(
1086 apply_delta_lenient(b"abc", &[0x7F, 1, 2, 3]),
1087 vec![0x7F, 1, 2, 3]
1088 );
1089 }
1090
1091 #[test]
1092 fn test_lenient_truncated_prefix_suffix_echoes_delta() {
1093 let short01 = vec![OP_PREFIX_SUFFIX; 10];
1094 assert_eq!(apply_delta_lenient(b"abc", &short01), short01);
1095 }
1096
1097 #[test]
1098 fn test_lenient_skips_out_of_range_copy() {
1099 let mut d = vec![OP_INSTRUCTIONS];
1100 d.extend_from_slice(&5u64.to_le_bytes()); d.extend_from_slice(&1u32.to_le_bytes()); d.push(INSTR_COPY);
1103 d.extend_from_slice(&1_000u64.to_le_bytes()); d.extend_from_slice(&2u32.to_le_bytes());
1105 assert_eq!(apply_delta_lenient(b"abc", &d), Vec::<u8>::new());
1106 }
1107
1108 #[test]
1109 fn test_lenient_skips_insert_past_end() {
1110 let mut d = vec![OP_INSTRUCTIONS];
1111 d.extend_from_slice(&8u64.to_le_bytes());
1112 d.extend_from_slice(&1u32.to_le_bytes());
1113 d.push(INSTR_INSERT);
1114 d.extend_from_slice(&100u32.to_le_bytes()); assert_eq!(apply_delta_lenient(b"abc", &d), Vec::<u8>::new());
1116 }
1117
1118 #[test]
1119 fn test_lenient_returns_partial_on_unknown_instruction() {
1120 let mut d = vec![OP_INSTRUCTIONS];
1121 d.extend_from_slice(&4u64.to_le_bytes()); d.extend_from_slice(&2u32.to_le_bytes()); d.push(INSTR_COPY);
1124 d.extend_from_slice(&0u64.to_le_bytes());
1125 d.extend_from_slice(&4u32.to_le_bytes()); d.push(0x42); assert_eq!(apply_delta_lenient(b"abcd", &d), b"abcd".to_vec());
1128 }
1129
1130 #[cfg(feature = "zstd")]
1131 #[test]
1132 fn test_lenient_checksum_mismatch_returns_empty() {
1133 let base = vec![0u8; 4096];
1134 let mut target = vec![0u8; 4096];
1135 target[100] = 0xAB;
1136 let (_c, mut delta) = compute_delta(&base, &target);
1137 assert_eq!(delta[0], OP_BINARY_XOR);
1138 delta[9] ^= 0xFF; assert_eq!(apply_delta_lenient(&base, &delta), Vec::<u8>::new());
1140 }
1141
1142 #[test]
1143 fn test_lenient_agrees_with_strict_on_computed_deltas() -> TestResult {
1144 let cases: Vec<(Vec<u8>, Vec<u8>)> = vec![
1145 (b"Hello, World!".to_vec(), b"Hello, Rust!".to_vec()),
1146 (b"keep me".to_vec(), b"keep me too".to_vec()),
1147 (vec![0u8; 4], vec![1, 2, 3]),
1148 (
1149 (0..3 * BLOCK_SIZE).map(|i| b'A' + (i % 26) as u8).collect(),
1150 {
1151 let mut t = (0..3 * BLOCK_SIZE)
1152 .map(|i| b'A' + (i % 26) as u8)
1153 .collect::<Vec<_>>();
1154 t.splice(100..110, b"XX".to_vec());
1155 t
1156 },
1157 ),
1158 ];
1159 for (base, target) in &cases {
1160 let (_c, delta) = compute_delta(base, target);
1161 assert_eq!(
1162 apply_delta_lenient(base, &delta),
1163 apply_delta(base, &delta)?,
1164 "lenient and strict must agree on compute_delta output"
1165 );
1166 }
1167 Ok(())
1168 }
1169
1170 #[cfg(feature = "zstd")]
1171 #[test]
1172 fn test_lenient_binary_roundtrip() -> TestResult {
1173 let base = vec![0u8; 8192];
1174 let mut target = vec![0u8; 8192];
1175 target[100] = 0xAB;
1176 let last = target.len() - 1;
1177 target[last] = 0xCD;
1178 let (_c, delta) = compute_delta(&base, &target);
1179 assert_eq!(delta[0], OP_BINARY_XOR);
1180 assert_eq!(apply_delta_lenient(&base, &delta), target);
1181 assert_eq!(apply_delta(&base, &delta)?, target);
1182 Ok(())
1183 }
1184
1185 #[cfg(feature = "zstd")]
1186 #[test]
1187 fn test_compute_binary_delta_public_surface() {
1188 let data: Vec<u8> = (0..8192).map(|i| (i % 251) as u8).collect();
1189 let delta = compute_binary_delta(&data, &data).expect("identical inputs compress");
1190 assert_eq!(delta[0], OP_BINARY_XOR);
1191 assert!(delta.len() < 100, "identical inputs must compress tiny");
1192 assert_eq!(apply_delta_lenient(&data, &delta), data);
1193 }
1194}