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