Skip to main content

delta_kit/
delta.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2//! Delta codec implementation. See the crate root for the wire format.
3
4use std::collections::HashMap;
5
6use thiserror::Error;
7
8/// Block size used by the Rabin rolling-hash strategy (4 KiB).
9pub 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/// Which declared value or checksum failed to verify while applying a delta.
25#[derive(Debug, Clone, Copy, PartialEq, Eq)]
26pub enum MismatchSide {
27    /// The declared target length did not match the reconstruction.
28    Target,
29    /// The base checksum did not match.
30    Base,
31    /// The reconstructed target checksum did not match.
32    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/// Errors produced while applying a delta.
46///
47/// `compute_delta` is total and cannot fail; only `apply_delta` validates.
48#[derive(Error, Debug, Clone, PartialEq, Eq)]
49pub enum DeltaError {
50    /// The delta was empty (no opcode byte).
51    #[error("delta is empty")]
52    Empty,
53
54    /// The top-level opcode is not one of `0x00..=0x03`.
55    #[error("invalid delta opcode: {0:#04x}")]
56    InvalidOpcode(u8),
57
58    /// A header or record ended before the format requires.
59    #[error("truncated delta (opcode {opcode:#04x}): needed {needed} bytes, got {got}")]
60    Truncated {
61        /// The encoding opcode being decoded.
62        opcode: u8,
63        /// Minimum bytes the format requires at this point.
64        needed: usize,
65        /// Bytes actually available.
66        got: usize,
67    },
68
69    /// An instruction byte inside a `0x02` stream is not `0x01`/`0x02`.
70    #[error("invalid instruction opcode: {0:#04x}")]
71    InvalidInstruction(u8),
72
73    /// A `Copy` instruction referenced bytes outside the base.
74    #[error("copy out of range: base_offset {base_offset} + length {length} exceeds base length {base_len}")]
75    CopyOutOfRange {
76        /// Declared start offset into the base.
77        base_offset: u64,
78        /// Declared copy length.
79        length: u32,
80        /// Actual base length.
81        base_len: usize,
82    },
83
84    /// An `Insert` instruction declared more data than the delta holds.
85    #[error("insert out of range: declared {declared} bytes, {remaining} available")]
86    InsertOutOfRange {
87        /// Declared insert length.
88        declared: u32,
89        /// Bytes remaining in the delta.
90        remaining: usize,
91    },
92
93    /// The reconstructed output did not match the declared target length.
94    #[error("target length mismatch ({side}): declared {declared}, reconstructed {reconstructed}")]
95    TargetLengthMismatch {
96        /// Which declared length was contradicted.
97        side: MismatchSide,
98        /// The length declared in the delta.
99        declared: usize,
100        /// The length actually reconstructed.
101        reconstructed: usize,
102    },
103
104    /// A `blake3` prefix checksum of a `0x03` binary delta did not match.
105    #[error("checksum mismatch: {side} checksum does not match")]
106    ChecksumMismatch {
107        /// Which half failed (base or reconstructed target).
108        side: MismatchSide,
109    },
110
111    /// The embedded Zstd frame failed to decompress.
112    #[error("decompression error: {0}")]
113    Decompression(String),
114
115    /// A `0x03` binary delta was received by a build compiled without the
116    /// `zstd` feature; it cannot be decoded.
117    #[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/// XOR the target against the base (base-length prefix), appending the
181/// target tail beyond the base length.
182#[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/// Compute a delta transforming `base` into `target`.
337///
338/// Returns `(base_copy, delta)`. The first element is simply a copy of
339/// `base`, kept for API compatibility with the origin `suture-protocol`
340/// signature. The delta always reconstructs `target` via [`apply_delta`]
341/// and uses the smallest applicable encoding (see the crate docs).
342#[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
396/// Read a little-endian `u64` at `at`, requiring `at + 8 <= delta.len()`.
397fn 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    // INVARIANT (documented-infallible): `end - at == 8` by construction.
407    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
412/// Read a little-endian `u32` at `at`, requiring `at + 4 <= delta.len()`.
413fn 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    // INVARIANT (documented-infallible): `end - at == 4` by construction.
423    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
436/// Apply `delta` to `base`, reconstructing the target.
437///
438/// This validates the delta strictly: truncated headers, unknown
439/// opcodes/instructions, out-of-range copies, inserts past the end of the
440/// delta, and length/checksum contradictions all return
441/// [`DeltaError`]. Deltas produced by [`compute_delta`] always
442/// round-trip.
443pub 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    /// All fallible steps in tests use `?`; the crate keeps a strict
654    /// zero-`unwrap()` policy (verified by grep).
655    type TestResult = Result<(), Box<dyn std::error::Error>>;
656
657    // === Ported from suture-protocol ===
658
659    #[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    // === Strategy coverage ===
688
689    #[cfg(feature = "zstd")]
690    #[test]
691    fn test_binary_delta_roundtrip() -> TestResult {
692        // Zero bytes trip is_likely_binary -> XOR+Zstd (0x03) path.
693        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        // Text-like (zero-free) inputs, both >= BLOCK_SIZE -> 0x02 path.
709        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        // Completely different, tiny, binary -> 0x00 full content.
734        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    // === Hardened decode behavior ===
744
745    #[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        // Origin silently identity-decoded this; we reject.
753        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        // 0x01 needs 25 bytes.
763        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        // 0x02 needs 13 bytes.
773        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        // 0x03 needs 41 bytes.
787        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()); // target_len
810        d.extend_from_slice(&1u32.to_le_bytes()); // one instruction
811        d.push(INSTR_COPY);
812        d.extend_from_slice(&1_000u64.to_le_bytes()); // base_offset beyond base
813        d.extend_from_slice(&2u32.to_le_bytes()); // length
814        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()); // target_len
824        d.extend_from_slice(&1u32.to_le_bytes()); // one instruction
825        d.push(INSTR_INSERT);
826        d.extend_from_slice(&100u32.to_le_bytes()); // claims 100 bytes, provides 0
827        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); // not Copy/Insert
839        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()); // claims 9...
849        d.extend_from_slice(&0u32.to_le_bytes()); // ...but zero instructions
850        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        // Craft a valid-looking 0x03 whose base checksum won't match.
860        let mut d = vec![OP_BINARY_XOR];
861        d.extend_from_slice(&4u64.to_le_bytes()); // target_len
862        d.extend_from_slice(&[0u8; 16]); // wrong base checksum
863        d.extend_from_slice(&[0u8; 16]); // target checksum (unreached)
864        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        // Flip a byte in the compressed payload.
883        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        // Empty target: changed (0) < target (0) is false -> full.
901        assert_eq!(delta, vec![OP_FULL]);
902    }
903}