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 alloc::string::String;
5use alloc::vec;
6use alloc::vec::Vec;
7
8use hashbrown::HashMap;
9use thiserror::Error;
10
11/// Block size used by the Rabin rolling-hash strategy (4 KiB).
12pub 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/// Which declared value or checksum failed to verify while applying a delta.
28#[derive(Debug, Clone, Copy, PartialEq, Eq)]
29pub enum MismatchSide {
30    /// The declared target length did not match the reconstruction.
31    Target,
32    /// The base checksum did not match.
33    Base,
34    /// The reconstructed target checksum did not match.
35    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/// Errors produced while applying a delta.
49///
50/// `compute_delta` is total and cannot fail; only `apply_delta` validates.
51#[derive(Error, Debug, Clone, PartialEq, Eq)]
52pub enum DeltaError {
53    /// The delta was empty (no opcode byte).
54    #[error("delta is empty")]
55    Empty,
56
57    /// The top-level opcode is not one of `0x00..=0x03`.
58    #[error("invalid delta opcode: {0:#04x}")]
59    InvalidOpcode(u8),
60
61    /// A header or record ended before the format requires.
62    #[error("truncated delta (opcode {opcode:#04x}): needed {needed} bytes, got {got}")]
63    Truncated {
64        /// The encoding opcode being decoded.
65        opcode: u8,
66        /// Minimum bytes the format requires at this point.
67        needed: usize,
68        /// Bytes actually available.
69        got: usize,
70    },
71
72    /// An instruction byte inside a `0x02` stream is not `0x01`/`0x02`.
73    #[error("invalid instruction opcode: {0:#04x}")]
74    InvalidInstruction(u8),
75
76    /// A `Copy` instruction referenced bytes outside the base.
77    #[error("copy out of range: base_offset {base_offset} + length {length} exceeds base length {base_len}")]
78    CopyOutOfRange {
79        /// Declared start offset into the base.
80        base_offset: u64,
81        /// Declared copy length.
82        length: u32,
83        /// Actual base length.
84        base_len: usize,
85    },
86
87    /// An `Insert` instruction declared more data than the delta holds.
88    #[error("insert out of range: declared {declared} bytes, {remaining} available")]
89    InsertOutOfRange {
90        /// Declared insert length.
91        declared: u32,
92        /// Bytes remaining in the delta.
93        remaining: usize,
94    },
95
96    /// The reconstructed output did not match the declared target length.
97    #[error("target length mismatch ({side}): declared {declared}, reconstructed {reconstructed}")]
98    TargetLengthMismatch {
99        /// Which declared length was contradicted.
100        side: MismatchSide,
101        /// The length declared in the delta.
102        declared: usize,
103        /// The length actually reconstructed.
104        reconstructed: usize,
105    },
106
107    /// A `blake3` prefix checksum of a `0x03` binary delta did not match.
108    #[error("checksum mismatch: {side} checksum does not match")]
109    ChecksumMismatch {
110        /// Which half failed (base or reconstructed target).
111        side: MismatchSide,
112    },
113
114    /// The embedded Zstd frame failed to decompress.
115    #[error("decompression error: {0}")]
116    Decompression(String),
117
118    /// A `0x03` binary delta was received by a build compiled without the
119    /// `zstd` feature; it cannot be decoded.
120    #[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/// XOR the target against the base (base-length prefix), appending the
184/// target tail beyond the base length.
185#[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/// Compute a `0x03` binary XOR+Zstd delta directly, bypassing the
199/// binary-sniffing gate of [`compute_delta`].
200///
201/// Returns `None` when the compressed XOR stream is not smaller than the
202/// target itself. Requires the `zstd` feature.
203#[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/// Compute a delta transforming `base` into `target`.
346///
347/// Returns `(base_copy, delta)`. The first element is simply a copy of
348/// `base`, kept for API compatibility with the origin `suture-protocol`
349/// signature. The delta always reconstructs `target` via [`apply_delta`]
350/// and uses the smallest applicable encoding (see the crate docs).
351#[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
405/// Read a little-endian `u64` at `at`, requiring `at + 8 <= delta.len()`.
406fn 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    // INVARIANT (documented-infallible): `end - at == 8` by construction.
416    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
421/// Read a little-endian `u32` at `at`, requiring `at + 4 <= delta.len()`.
422fn 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    // INVARIANT (documented-infallible): `end - at == 4` by construction.
432    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
445/// Apply `delta` to `base`, reconstructing the target.
446///
447/// This validates the delta strictly: truncated headers, unknown
448/// opcodes/instructions, out-of-range copies, inserts past the end of the
449/// delta, and length/checksum contradictions all return
450/// [`DeltaError`]. Deltas produced by [`compute_delta`] always
451/// round-trip.
452pub 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
658/// Read a little-endian `u64` at `at`, yielding `0` when out of bounds.
659/// Mirrors the origin decoder's `try_into().unwrap_or([0; 8])` fallback.
660fn 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
670/// Read a little-endian `u32` at `at`, yielding `0` when out of bounds.
671fn 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/// Apply `delta` to `base` with the origin `suture-protocol` semantics.
682///
683/// This is a behavior-preserving port of the pre-extraction decoder:
684/// malformed input is silently repaired instead of rejected. For
685/// consumers whose public contract is the origin's infallible
686/// `apply_delta(base, delta) -> Vec<u8>`, this is the drop-in delegate.
687///
688/// Origin-observed behavior, reproduced exactly:
689///
690/// - an empty delta decodes to an empty vector;
691/// - an unknown top-level opcode, a truncated header, or a failed Zstd
692///   frame decode passes the delta bytes through unchanged;
693/// - a `0x02` stream stops at the first truncated or unknown
694///   instruction and returns the partial reconstruction; out-of-range
695///   `Copy` records and past-the-end `Insert` records are skipped;
696/// - a `0x03` base or target checksum mismatch returns an empty vector;
697/// - declared lengths are never validated (a length is only used as an
698///   allocation hint, capped against attacker-controlled values).
699///
700/// On well-formed deltas — everything [`compute_delta`] produces — this
701/// agrees byte-for-byte with [`apply_delta`]. New code should prefer
702/// [`apply_delta`].
703#[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    /// All fallible steps in tests use `?`; the crate keeps a strict
826    /// zero-`unwrap()` policy (verified by grep).
827    type TestResult = Result<(), Box<dyn std::error::Error>>;
828
829    // === Ported from suture-protocol ===
830
831    #[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    // === Strategy coverage ===
860
861    #[cfg(feature = "zstd")]
862    #[test]
863    fn test_binary_delta_roundtrip() -> TestResult {
864        // Zero bytes trip is_likely_binary -> XOR+Zstd (0x03) path.
865        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        // Text-like (zero-free) inputs, both >= BLOCK_SIZE -> 0x02 path.
881        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        // Completely different, tiny, binary -> 0x00 full content.
906        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    // === Hardened decode behavior ===
916
917    #[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        // Origin silently identity-decoded this; we reject.
925        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        // 0x01 needs 25 bytes.
935        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        // 0x02 needs 13 bytes.
945        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        // 0x03 needs 41 bytes.
959        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()); // target_len
982        d.extend_from_slice(&1u32.to_le_bytes()); // one instruction
983        d.push(INSTR_COPY);
984        d.extend_from_slice(&1_000u64.to_le_bytes()); // base_offset beyond base
985        d.extend_from_slice(&2u32.to_le_bytes()); // length
986        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()); // target_len
996        d.extend_from_slice(&1u32.to_le_bytes()); // one instruction
997        d.push(INSTR_INSERT);
998        d.extend_from_slice(&100u32.to_le_bytes()); // claims 100 bytes, provides 0
999        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); // not Copy/Insert
1011        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()); // claims 9...
1021        d.extend_from_slice(&0u32.to_le_bytes()); // ...but zero instructions
1022        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        // Craft a valid-looking 0x03 whose base checksum won't match.
1032        let mut d = vec![OP_BINARY_XOR];
1033        d.extend_from_slice(&4u64.to_le_bytes()); // target_len
1034        d.extend_from_slice(&[0u8; 16]); // wrong base checksum
1035        d.extend_from_slice(&[0u8; 16]); // target checksum (unreached)
1036        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        // Flip a byte in the compressed payload.
1055        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        // Empty target: changed (0) < target (0) is false -> full.
1073        assert_eq!(delta, vec![OP_FULL]);
1074    }
1075
1076    // === Lenient (origin-compatible) decode behavior ===
1077
1078    #[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()); // target_len
1101        d.extend_from_slice(&1u32.to_le_bytes()); // one instruction
1102        d.push(INSTR_COPY);
1103        d.extend_from_slice(&1_000u64.to_le_bytes()); // beyond base
1104        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()); // claims 100, provides 0
1115        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()); // target_len
1122        d.extend_from_slice(&2u32.to_le_bytes()); // two instructions
1123        d.push(INSTR_COPY);
1124        d.extend_from_slice(&0u64.to_le_bytes());
1125        d.extend_from_slice(&4u32.to_le_bytes()); // copies "abcd"
1126        d.push(0x42); // unknown: stop, keep partial result
1127        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; // corrupt the base checksum
1139        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}