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