1use std::collections::HashMap;
5
6use thiserror::Error;
7
8pub const BLOCK_SIZE: usize = 4096;
10
11const RABIN_BASE: u64 = 257;
12const MERSENNE61: u64 = (1u64 << 61) - 1;
13
14const BINARY_CHECK_WINDOW: usize = 8192;
15
16const OP_FULL: u8 = 0x00;
17const OP_PREFIX_SUFFIX: u8 = 0x01;
18const OP_INSTRUCTIONS: u8 = 0x02;
19const OP_BINARY_XOR: u8 = 0x03;
20
21const INSTR_COPY: u8 = 0x01;
22const INSTR_INSERT: u8 = 0x02;
23
24#[derive(Debug, Clone, Copy, PartialEq, Eq)]
26pub enum MismatchSide {
27 Target,
29 Base,
31 Result,
33}
34
35impl std::fmt::Display for MismatchSide {
36 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
37 f.write_str(match self {
38 MismatchSide::Target => "target",
39 MismatchSide::Base => "base",
40 MismatchSide::Result => "result",
41 })
42 }
43}
44
45#[derive(Error, Debug, Clone, PartialEq, Eq)]
49pub enum DeltaError {
50 #[error("delta is empty")]
52 Empty,
53
54 #[error("invalid delta opcode: {0:#04x}")]
56 InvalidOpcode(u8),
57
58 #[error("truncated delta (opcode {opcode:#04x}): needed {needed} bytes, got {got}")]
60 Truncated {
61 opcode: u8,
63 needed: usize,
65 got: usize,
67 },
68
69 #[error("invalid instruction opcode: {0:#04x}")]
71 InvalidInstruction(u8),
72
73 #[error("copy out of range: base_offset {base_offset} + length {length} exceeds base length {base_len}")]
75 CopyOutOfRange {
76 base_offset: u64,
78 length: u32,
80 base_len: usize,
82 },
83
84 #[error("insert out of range: declared {declared} bytes, {remaining} available")]
86 InsertOutOfRange {
87 declared: u32,
89 remaining: usize,
91 },
92
93 #[error("target length mismatch ({side}): declared {declared}, reconstructed {reconstructed}")]
95 TargetLengthMismatch {
96 side: MismatchSide,
98 declared: usize,
100 reconstructed: usize,
102 },
103
104 #[error("checksum mismatch: {side} checksum does not match")]
106 ChecksumMismatch {
107 side: MismatchSide,
109 },
110
111 #[error("decompression error: {0}")]
113 Decompression(String),
114
115 #[error("binary (0x03) delta requires the \"zstd\" feature")]
118 ZstdDisabled,
119}
120
121#[inline]
122fn mersenne_reduce(x: u128) -> u64 {
123 let mut r = (x & u128::from(MERSENNE61)) + (x >> 61);
124 if r >= u128::from(MERSENNE61) {
125 r -= u128::from(MERSENNE61);
126 }
127 r as u64
128}
129
130#[inline]
131fn mod_sub(a: u64, b: u64) -> u64 {
132 mersenne_reduce(u128::from(a) + u128::from(MERSENNE61) - u128::from(b))
133}
134
135fn mod_pow(mut base: u64, mut exp: usize) -> u64 {
136 let mut result: u64 = 1;
137 while exp > 0 {
138 if exp & 1 == 1 {
139 result = mersenne_reduce(u128::from(result) * u128::from(base));
140 }
141 base = mersenne_reduce(u128::from(base) * u128::from(base));
142 exp >>= 1;
143 }
144 result
145}
146
147fn rabin_hash(data: &[u8]) -> u64 {
148 let mut h: u64 = 0;
149 for &b in data {
150 h = mersenne_reduce(u128::from(h) * u128::from(RABIN_BASE) + u128::from(b));
151 }
152 h
153}
154
155fn rabin_roll(h: u64, old_byte: u8, new_byte: u8, base_power: u64) -> u64 {
156 let old_contrib = mersenne_reduce(u128::from(old_byte) * u128::from(base_power));
157 let h2 = mod_sub(h, old_contrib);
158 mersenne_reduce(u128::from(h2) * u128::from(RABIN_BASE) + u128::from(new_byte))
159}
160
161fn strong_hash(data: &[u8]) -> u64 {
162 let mut h: u64 = 0xcbf2_3ce4_8422_2325;
163 for &b in data {
164 h ^= u64::from(b);
165 h = h.wrapping_mul(0x0100_0000_01b3);
166 }
167 h ^= h >> 33;
168 h = h.wrapping_mul(0xff51_afd7_ed55_8ccd);
169 h ^= h >> 33;
170 h = h.wrapping_mul(0xc4ce_b9fe_1a85_ec53);
171 h ^= h >> 33;
172 h
173}
174
175fn is_likely_binary(data: &[u8]) -> bool {
176 let window = std::cmp::min(data.len(), BINARY_CHECK_WINDOW);
177 data[..window].contains(&0u8)
178}
179
180#[cfg(feature = "zstd")]
183fn xor_streams(base: &[u8], target: &[u8]) -> Vec<u8> {
184 let min_len = base.len().min(target.len());
185 let mut xor_data = Vec::with_capacity(target.len());
186 for i in 0..min_len {
187 xor_data.push(base[i] ^ target[i]);
188 }
189 if target.len() > base.len() {
190 xor_data.extend_from_slice(&target[base.len()..]);
191 }
192 xor_data
193}
194
195#[cfg(feature = "zstd")]
196fn compute_binary_delta(base: &[u8], target: &[u8]) -> Option<Vec<u8>> {
197 let base_hash = blake3::hash(base);
198 let target_hash = blake3::hash(target);
199
200 let xor_data = xor_streams(base, target);
201 let compressed = zstd::encode_all(xor_data.as_slice(), 3).ok()?;
202
203 if compressed.len() >= target.len() {
204 return None;
205 }
206
207 let mut delta = Vec::with_capacity(41 + compressed.len());
208 delta.push(OP_BINARY_XOR);
209 delta.extend_from_slice(&(target.len() as u64).to_le_bytes());
210 delta.extend_from_slice(&base_hash.as_bytes()[..16]);
211 delta.extend_from_slice(&target_hash.as_bytes()[..16]);
212 delta.extend_from_slice(&compressed);
213
214 Some(delta)
215}
216
217enum DeltaInstr {
218 Copy { base_offset: u64, length: u32 },
219 Insert { data: Vec<u8> },
220}
221
222fn compute_rolling_delta(base: &[u8], target: &[u8]) -> Option<Vec<u8>> {
223 if base.len() < BLOCK_SIZE || target.len() < BLOCK_SIZE {
224 return None;
225 }
226
227 let num_blocks = base.len() / BLOCK_SIZE;
228 if num_blocks == 0 {
229 return None;
230 }
231
232 let mut hash_table: HashMap<u64, Vec<(usize, u64)>> = HashMap::new();
233 for i in 0..num_blocks {
234 let block = &base[i * BLOCK_SIZE..(i + 1) * BLOCK_SIZE];
235 let rh = rabin_hash(block);
236 let sh = strong_hash(block);
237 hash_table.entry(rh).or_default().push((i, sh));
238 }
239
240 let base_power = mod_pow(RABIN_BASE, BLOCK_SIZE - 1);
241 let mut instructions: Vec<DeltaInstr> = Vec::new();
242 let mut pending_insert_start: usize = 0;
243 let mut pos: usize = 0;
244 let mut prev_rabin: Option<u64> = None;
245
246 while pos + BLOCK_SIZE <= target.len() {
247 let rh = match prev_rabin {
248 Some(pr) if pos > 0 => rabin_roll(
249 pr,
250 target[pos - 1],
251 target[pos + BLOCK_SIZE - 1],
252 base_power,
253 ),
254 _ => rabin_hash(&target[pos..pos + BLOCK_SIZE]),
255 };
256 prev_rabin = Some(rh);
257
258 let mut matched = false;
259 if let Some(candidates) = hash_table.get(&rh) {
260 let sh = strong_hash(&target[pos..pos + BLOCK_SIZE]);
261 for &(block_idx, ref_sh) in candidates {
262 if sh == ref_sh {
263 let base_offset = block_idx * BLOCK_SIZE;
264 let mut match_len = BLOCK_SIZE;
265
266 while pos + match_len < target.len()
267 && base_offset + match_len < base.len()
268 && target[pos + match_len] == base[base_offset + match_len]
269 {
270 match_len += 1;
271 }
272
273 let match_len = match_len.min(u32::MAX as usize);
274
275 if pending_insert_start < pos {
276 instructions.push(DeltaInstr::Insert {
277 data: target[pending_insert_start..pos].to_vec(),
278 });
279 }
280
281 instructions.push(DeltaInstr::Copy {
282 base_offset: base_offset as u64,
283 length: match_len as u32,
284 });
285
286 pos += match_len;
287 pending_insert_start = pos;
288 prev_rabin = None;
289 matched = true;
290 break;
291 }
292 }
293 }
294
295 if !matched {
296 pos += 1;
297 }
298 }
299
300 if pending_insert_start < target.len() {
301 instructions.push(DeltaInstr::Insert {
302 data: target[pending_insert_start..].to_vec(),
303 });
304 }
305
306 let mut delta = Vec::new();
307 delta.push(OP_INSTRUCTIONS);
308 delta.extend_from_slice(&(target.len() as u64).to_le_bytes());
309 delta.extend_from_slice(&(instructions.len() as u32).to_le_bytes());
310
311 for instr in &instructions {
312 match instr {
313 DeltaInstr::Copy {
314 base_offset,
315 length,
316 } => {
317 delta.push(INSTR_COPY);
318 delta.extend_from_slice(&base_offset.to_le_bytes());
319 delta.extend_from_slice(&length.to_le_bytes());
320 }
321 DeltaInstr::Insert { data } => {
322 delta.push(INSTR_INSERT);
323 delta.extend_from_slice(&(data.len() as u32).to_le_bytes());
324 delta.extend_from_slice(data);
325 }
326 }
327 }
328
329 if delta.len() < target.len() {
330 Some(delta)
331 } else {
332 None
333 }
334}
335
336#[must_use]
343pub fn compute_delta(base: &[u8], target: &[u8]) -> (Vec<u8>, Vec<u8>) {
344 #[cfg(feature = "zstd")]
345 if is_likely_binary(base) || is_likely_binary(target) {
346 if let Some(delta) = compute_binary_delta(base, target) {
347 return (base.to_vec(), delta);
348 }
349 }
350
351 if base.len() >= BLOCK_SIZE && target.len() >= BLOCK_SIZE {
352 if let Some(delta) = compute_rolling_delta(base, target) {
353 return (base.to_vec(), delta);
354 }
355 let mut full = vec![OP_FULL];
356 full.extend_from_slice(target);
357 return (base.to_vec(), full);
358 }
359
360 let prefix_len = base
361 .iter()
362 .zip(target.iter())
363 .take_while(|(a, b)| a == b)
364 .count();
365
366 let max_suffix_base = base.len().saturating_sub(prefix_len);
367 let max_suffix_target = target.len().saturating_sub(prefix_len);
368 let suffix_len = base[prefix_len..]
369 .iter()
370 .rev()
371 .zip(target[prefix_len..].iter().rev())
372 .take_while(|(a, b)| a == b)
373 .count()
374 .min(max_suffix_base)
375 .min(max_suffix_target);
376
377 let changed_start = prefix_len;
378 let changed_end_target = target.len().saturating_sub(suffix_len);
379 let changed = &target[changed_start..changed_end_target];
380
381 if changed.len() < target.len() {
382 let mut delta = Vec::new();
383 delta.push(OP_PREFIX_SUFFIX);
384 delta.extend_from_slice(&(prefix_len as u64).to_le_bytes());
385 delta.extend_from_slice(&(suffix_len as u64).to_le_bytes());
386 delta.extend_from_slice(&(target.len() as u64).to_le_bytes());
387 delta.extend_from_slice(changed);
388 (base.to_vec(), delta)
389 } else {
390 let mut full = vec![OP_FULL];
391 full.extend_from_slice(target);
392 (base.to_vec(), full)
393 }
394}
395
396fn read_u64_le(delta: &[u8], at: usize, opcode: u8) -> Result<u64, DeltaError> {
398 let end = at
399 .checked_add(8)
400 .filter(|&e| e <= delta.len())
401 .ok_or(DeltaError::Truncated {
402 opcode,
403 needed: at + 8,
404 got: delta.len(),
405 })?;
406 let bytes =
408 <[u8; 8]>::try_from(&delta[at..end]).expect("slice length is exactly 8 by construction");
409 Ok(u64::from_le_bytes(bytes))
410}
411
412fn read_u32_le(delta: &[u8], at: usize, opcode: u8) -> Result<u32, DeltaError> {
414 let end = at
415 .checked_add(4)
416 .filter(|&e| e <= delta.len())
417 .ok_or(DeltaError::Truncated {
418 opcode,
419 needed: at + 4,
420 got: delta.len(),
421 })?;
422 let bytes =
424 <[u8; 4]>::try_from(&delta[at..end]).expect("slice length is exactly 4 by construction");
425 Ok(u32::from_le_bytes(bytes))
426}
427
428fn length_mismatch(side: MismatchSide, declared: u64, reconstructed: usize) -> DeltaError {
429 DeltaError::TargetLengthMismatch {
430 side,
431 declared: usize::try_from(declared).unwrap_or(usize::MAX),
432 reconstructed,
433 }
434}
435
436pub fn apply_delta(base: &[u8], delta: &[u8]) -> Result<Vec<u8>, DeltaError> {
444 let Some((&opcode, rest)) = delta.split_first() else {
445 return Err(DeltaError::Empty);
446 };
447
448 match opcode {
449 OP_FULL => Ok(rest.to_vec()),
450
451 OP_PREFIX_SUFFIX => {
452 if delta.len() < 25 {
453 return Err(DeltaError::Truncated {
454 opcode,
455 needed: 25,
456 got: delta.len(),
457 });
458 }
459 let prefix_len = read_u64_le(delta, 1, opcode)? as usize;
460 let suffix_len = read_u64_le(delta, 9, opcode)? as usize;
461 let total_len = read_u64_le(delta, 17, opcode)?;
462 let changed = &delta[25..];
463
464 let prefix_take = prefix_len.min(base.len());
465 let mut result = Vec::with_capacity(total_len.min(usize::MAX as u64) as usize);
466 result.extend_from_slice(&base[..prefix_take]);
467 result.extend_from_slice(changed);
468 result.extend_from_slice(&base[base.len().saturating_sub(suffix_len)..]);
469
470 if result.len() as u64 != total_len {
471 return Err(length_mismatch(
472 MismatchSide::Target,
473 total_len,
474 result.len(),
475 ));
476 }
477 Ok(result)
478 }
479
480 OP_INSTRUCTIONS => {
481 if delta.len() < 13 {
482 return Err(DeltaError::Truncated {
483 opcode,
484 needed: 13,
485 got: delta.len(),
486 });
487 }
488 let target_len = read_u64_le(delta, 1, opcode)?;
489 let num_instr = read_u32_le(delta, 9, opcode)?;
490 let mut result: Vec<u8> =
491 Vec::with_capacity(target_len.min(usize::MAX as u64) as usize);
492 let mut offset = 13usize;
493
494 for _ in 0..num_instr {
495 let Some(&instr) = delta.get(offset) else {
496 return Err(DeltaError::Truncated {
497 opcode,
498 needed: offset + 1,
499 got: delta.len(),
500 });
501 };
502 match instr {
503 INSTR_COPY => {
504 if offset + 13 > delta.len() {
505 return Err(DeltaError::Truncated {
506 opcode,
507 needed: offset + 13,
508 got: delta.len(),
509 });
510 }
511 let base_offset = read_u64_le(delta, offset + 1, opcode)?;
512 let length = read_u32_le(delta, offset + 9, opcode)?;
513 let bo = usize::try_from(base_offset).map_err(|_| {
514 DeltaError::CopyOutOfRange {
515 base_offset,
516 length,
517 base_len: base.len(),
518 }
519 })?;
520 let ln =
521 usize::try_from(length).map_err(|_| DeltaError::CopyOutOfRange {
522 base_offset,
523 length,
524 base_len: base.len(),
525 })?;
526 let end = bo.saturating_add(ln);
527 if end > base.len() {
528 return Err(DeltaError::CopyOutOfRange {
529 base_offset,
530 length,
531 base_len: base.len(),
532 });
533 }
534 result.extend_from_slice(&base[bo..end]);
535 offset += 13;
536 }
537 INSTR_INSERT => {
538 if offset + 5 > delta.len() {
539 return Err(DeltaError::Truncated {
540 opcode,
541 needed: offset + 5,
542 got: delta.len(),
543 });
544 }
545 let declared_len = read_u32_le(delta, offset + 1, opcode)?;
546 let length = usize::try_from(declared_len).map_err(|_| {
547 DeltaError::InsertOutOfRange {
548 declared: declared_len,
549 remaining: delta.len() - offset - 5,
550 }
551 })?;
552 let data_end = offset.checked_add(5).and_then(|v| v.checked_add(length));
553 match data_end {
554 None => {
555 return Err(DeltaError::InsertOutOfRange {
556 declared: declared_len,
557 remaining: delta.len() - offset - 5,
558 })
559 }
560 Some(data_end) if data_end > delta.len() => {
561 return Err(DeltaError::InsertOutOfRange {
562 declared: declared_len,
563 remaining: delta.len() - offset - 5,
564 })
565 }
566 Some(data_end) => {
567 result.extend_from_slice(&delta[offset + 5..data_end]);
568 offset = data_end;
569 }
570 }
571 }
572 other => return Err(DeltaError::InvalidInstruction(other)),
573 }
574 }
575
576 if result.len() as u64 != target_len {
577 return Err(length_mismatch(
578 MismatchSide::Target,
579 target_len,
580 result.len(),
581 ));
582 }
583 Ok(result)
584 }
585
586 OP_BINARY_XOR => {
587 #[cfg(feature = "zstd")]
588 {
589 if delta.len() < 41 {
590 return Err(DeltaError::Truncated {
591 opcode,
592 needed: 41,
593 got: delta.len(),
594 });
595 }
596 let target_len = read_u64_le(delta, 1, opcode)?;
597 let base_checksum = &delta[9..25];
598 let target_checksum = &delta[25..41];
599 let compressed = &delta[41..];
600
601 let base_hash = blake3::hash(base);
602 if base_hash.as_bytes()[..16] != *base_checksum {
603 return Err(DeltaError::ChecksumMismatch {
604 side: MismatchSide::Base,
605 });
606 }
607
608 let xor_data = zstd::decode_all(compressed)
609 .map_err(|e| DeltaError::Decompression(e.to_string()))?;
610
611 let mut result = Vec::with_capacity(target_len.min(usize::MAX as u64) as usize);
612 let min_len = base.len().min(xor_data.len());
613 for i in 0..min_len {
614 result.push(base[i] ^ xor_data[i]);
615 }
616 if xor_data.len() > base.len() {
617 result.extend_from_slice(&xor_data[base.len()..]);
618 }
619
620 if result.len() as u64 != target_len {
621 return Err(length_mismatch(
622 MismatchSide::Target,
623 target_len,
624 result.len(),
625 ));
626 }
627
628 let result_hash = blake3::hash(&result);
629 if result_hash.as_bytes()[..16] != *target_checksum {
630 return Err(DeltaError::ChecksumMismatch {
631 side: MismatchSide::Result,
632 });
633 }
634
635 Ok(result)
636 }
637
638 #[cfg(not(feature = "zstd"))]
639 {
640 let _ = rest;
641 Err(DeltaError::ZstdDisabled)
642 }
643 }
644
645 other => Err(DeltaError::InvalidOpcode(other)),
646 }
647}
648
649#[cfg(test)]
650mod tests {
651 use super::*;
652
653 type TestResult = Result<(), Box<dyn std::error::Error>>;
656
657 #[test]
660 fn test_delta_roundtrip() {
661 let base = b"Hello, World!";
662 let target = b"Hello, Rust!";
663 let (_base_copy, delta) = compute_delta(base, target);
664 let result = apply_delta(base, &delta).expect("well-formed delta must apply");
665 assert_eq!(result, target);
666 }
667
668 #[test]
669 fn test_delta_no_change() {
670 let base = b"identical data here";
671 let target = b"identical data here";
672 let (_base_copy, delta) = compute_delta(base, target);
673 assert!(delta.len() < target.len() + 25);
674 let result = apply_delta(base, &delta).expect("well-formed delta must apply");
675 assert_eq!(result, target);
676 }
677
678 #[test]
679 fn test_delta_completely_different() {
680 let base = b"AAAA";
681 let target = b"BBBB";
682 let (_base_copy, delta) = compute_delta(base, target);
683 let result = apply_delta(base, &delta).expect("well-formed delta must apply");
684 assert_eq!(result, target);
685 }
686
687 #[cfg(feature = "zstd")]
690 #[test]
691 fn test_binary_delta_roundtrip() -> TestResult {
692 let base = vec![0u8; 8192];
694 let mut target = vec![0u8; 8192];
695 target[100] = 0xAB;
696 let last = target.len() - 1;
697 target[last] = 0xCD;
698
699 let (_c, delta) = compute_delta(&base, &target);
700 assert_eq!(delta[0], OP_BINARY_XOR, "binary inputs must use 0x03");
701 let result = apply_delta(&base, &delta)?;
702 assert_eq!(result, target);
703 Ok(())
704 }
705
706 #[test]
707 fn test_rolling_delta_roundtrip_and_opcode() -> TestResult {
708 let base: Vec<u8> = (0..3 * BLOCK_SIZE).map(|i| b'A' + (i % 26) as u8).collect();
710 let mut target = base.clone();
711 target.splice(100..110, b"XX".to_vec());
712
713 let (_c, delta) = compute_delta(&base, &target);
714 assert_eq!(delta[0], OP_INSTRUCTIONS, "large text inputs must use 0x02");
715 assert!(delta.len() < target.len(), "rolling delta must shrink here");
716 let result = apply_delta(&base, &delta)?;
717 assert_eq!(result, target);
718 Ok(())
719 }
720
721 #[test]
722 fn test_prefix_suffix_opcode() -> TestResult {
723 let base = b"Hello, World!".to_vec();
724 let target = b"Hello, Rust!".to_vec();
725 let (_c, delta) = compute_delta(&base, &target);
726 assert_eq!(delta[0], OP_PREFIX_SUFFIX);
727 assert_eq!(apply_delta(&base, &delta)?, target);
728 Ok(())
729 }
730
731 #[test]
732 fn test_full_opcode_fallback() -> TestResult {
733 let base = vec![0u8; 4];
735 let target = vec![1u8, 2, 3];
736 let (_c, delta) = compute_delta(&base, &target);
737 assert_eq!(delta[0], OP_FULL);
738 assert_eq!(&delta[1..], &target[..]);
739 assert_eq!(apply_delta(&base, &delta)?, target);
740 Ok(())
741 }
742
743 #[test]
746 fn test_apply_empty_delta_is_error() {
747 assert_eq!(apply_delta(b"abc", &[]), Err(DeltaError::Empty));
748 }
749
750 #[test]
751 fn test_apply_unknown_opcode_is_error() {
752 assert_eq!(
754 apply_delta(b"abc", &[0x7F, 1, 2, 3]),
755 Err(DeltaError::InvalidOpcode(0x7F))
756 );
757 }
758
759 #[cfg(feature = "zstd")]
760 #[test]
761 fn test_apply_truncated_headers_are_errors() {
762 let short01 = vec![OP_PREFIX_SUFFIX; 10];
764 assert!(matches!(
765 apply_delta(b"abc", &short01),
766 Err(DeltaError::Truncated {
767 opcode: OP_PREFIX_SUFFIX,
768 ..
769 })
770 ));
771
772 let short02 = vec![OP_INSTRUCTIONS; 8];
774 assert!(matches!(
775 apply_delta(b"abc", &short02),
776 Err(DeltaError::Truncated {
777 opcode: OP_INSTRUCTIONS,
778 ..
779 })
780 ));
781 }
782
783 #[cfg(feature = "zstd")]
784 #[test]
785 fn test_apply_truncated_binary_header_is_error() {
786 let short03 = vec![OP_BINARY_XOR; 20];
788 assert!(matches!(
789 apply_delta(b"abc", &short03),
790 Err(DeltaError::Truncated {
791 opcode: OP_BINARY_XOR,
792 ..
793 })
794 ));
795 }
796
797 #[cfg(not(feature = "zstd"))]
798 #[test]
799 fn test_binary_opcode_disabled_without_feature() {
800 assert_eq!(
801 apply_delta(b"abc", &[OP_BINARY_XOR]),
802 Err(DeltaError::ZstdDisabled)
803 );
804 }
805
806 #[test]
807 fn test_apply_copy_out_of_range_is_error() {
808 let mut d = vec![OP_INSTRUCTIONS];
809 d.extend_from_slice(&5u64.to_le_bytes()); d.extend_from_slice(&1u32.to_le_bytes()); d.push(INSTR_COPY);
812 d.extend_from_slice(&1_000u64.to_le_bytes()); d.extend_from_slice(&2u32.to_le_bytes()); assert!(matches!(
815 apply_delta(b"abc", &d),
816 Err(DeltaError::CopyOutOfRange { .. })
817 ));
818 }
819
820 #[test]
821 fn test_apply_insert_past_end_is_error() {
822 let mut d = vec![OP_INSTRUCTIONS];
823 d.extend_from_slice(&8u64.to_le_bytes()); d.extend_from_slice(&1u32.to_le_bytes()); d.push(INSTR_INSERT);
826 d.extend_from_slice(&100u32.to_le_bytes()); assert!(matches!(
828 apply_delta(b"abc", &d),
829 Err(DeltaError::InsertOutOfRange { .. })
830 ));
831 }
832
833 #[test]
834 fn test_apply_unknown_instruction_is_error() {
835 let mut d = vec![OP_INSTRUCTIONS];
836 d.extend_from_slice(&1u64.to_le_bytes());
837 d.extend_from_slice(&1u32.to_le_bytes());
838 d.push(0x42); assert_eq!(
840 apply_delta(b"abc", &d),
841 Err(DeltaError::InvalidInstruction(0x42))
842 );
843 }
844
845 #[test]
846 fn test_apply_target_length_mismatch_is_error() {
847 let mut d = vec![OP_INSTRUCTIONS];
848 d.extend_from_slice(&9u64.to_le_bytes()); d.extend_from_slice(&0u32.to_le_bytes()); assert!(matches!(
851 apply_delta(b"abc", &d),
852 Err(DeltaError::TargetLengthMismatch { .. })
853 ));
854 }
855
856 #[cfg(feature = "zstd")]
857 #[test]
858 fn test_apply_binary_checksum_mismatch_is_error() -> TestResult {
859 let mut d = vec![OP_BINARY_XOR];
861 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)?;
865 d.extend_from_slice(&frame);
866 assert!(matches!(
867 apply_delta(b"zzzz", &d),
868 Err(DeltaError::ChecksumMismatch {
869 side: MismatchSide::Base
870 })
871 ));
872 Ok(())
873 }
874
875 #[cfg(feature = "zstd")]
876 #[test]
877 fn test_binary_delta_tamper_is_error() -> TestResult {
878 let base = vec![0u8; 4096];
879 let target = vec![7u8; 4096];
880 let (_c, mut delta) = compute_delta(&base, &target);
881 assert_eq!(delta[0], OP_BINARY_XOR);
882 let last = delta.len() - 1;
884 delta[last] ^= 0xFF;
885 assert!(apply_delta(&base, &delta).is_err());
886 Ok(())
887 }
888
889 #[test]
890 fn test_empty_target() -> TestResult {
891 let (_c, delta) = compute_delta(b"base", b"");
892 assert_eq!(delta, vec![OP_FULL]);
893 assert_eq!(apply_delta(b"base", &delta)?, Vec::<u8>::new());
894 Ok(())
895 }
896
897 #[test]
898 fn test_identical_empty() {
899 let (_c, delta) = compute_delta(b"", b"");
900 assert_eq!(delta, vec![OP_FULL]);
902 }
903}