Skip to main content

lz4/block/
decompress_core.rs

1//! LZ4 block decompression core engine.
2//!
3//! Implements the algorithms in lz4.c v1.10.0 (lines 1969–2447):
4//!   - `read_variable_length` — bounded variable-length integer decoder
5//!   - `decompress_generic`   — the main, security-critical safe decompression loop
6//!
7//! # Security boundary
8//!
9//! This module is the **security-critical decompression path**.  Every bounds
10//! check present in the C source has an exact Rust equivalent here.  No check
11//! may be elided.  Malformed or truncated input must return
12//! `Err(DecompressError::MalformedInput)` — it must **never** panic or cause
13//! undefined behaviour.
14//!
15//! All `unsafe` blocks carry an explicit `// SAFETY:` comment.
16
17use core::ptr;
18
19use super::types::{
20    read_le16, wild_copy8, write32, DictDirective, DEC64TABLE, INC32TABLE, LASTLITERALS,
21    MATCH_SAFEGUARD_DISTANCE, MFLIMIT, MINMATCH, ML_BITS, ML_MASK, RUN_MASK, WILDCOPYLENGTH,
22};
23
24// ─────────────────────────────────────────────────────────────────────────────
25// Error type
26// ─────────────────────────────────────────────────────────────────────────────
27
28/// Errors returned by LZ4 block decompression.
29#[derive(Debug, Clone, Copy, PartialEq, Eq)]
30pub enum DecompressError {
31    /// The compressed data is malformed, truncated, or the dimensions supplied
32    /// by the caller are inconsistent.  Equivalent to a negative return value
33    /// from the C `LZ4_decompress_safe` family.
34    MalformedInput,
35}
36
37// ─────────────────────────────────────────────────────────────────────────────
38// Returns the decompression error for all invalid-input, out-of-bounds, and
39// truncation conditions.
40// ─────────────────────────────────────────────────────────────────────────────
41
42#[inline(always)]
43fn output_error<T>() -> Result<T, DecompressError> {
44    Err(DecompressError::MalformedInput)
45}
46
47// ─────────────────────────────────────────────────────────────────────────────
48// read_variable_length — lz4.c:1978-2014
49// ─────────────────────────────────────────────────────────────────────────────
50
51/// Sentinel value returned by `read_variable_length` on error.
52/// Corresponds to C `rvl_error = (Rvl_t)(-1)`.
53const RVL_ERROR: usize = usize::MAX;
54
55/// Read a variable-length integer from the input stream.
56///
57/// Accumulates `u8` bytes into a `usize` sum until a byte < 255 is read, or
58/// until `ilimit` is reached (which is an error).
59///
60/// `initial_check`: if `true`, fail immediately when `ip >= ilimit` before
61/// reading the first byte (mirrors the C `initial_check` parameter).
62///
63/// Returns `RVL_ERROR` on any parsing failure; otherwise the accumulated value
64/// (which must be added to the caller's running length counter).
65///
66/// # Safety
67/// `ip` must point into the same allocation as `ilimit`.
68/// All bytes in `[ip, ilimit]` must be readable.
69#[inline(always)]
70unsafe fn read_variable_length(
71    ip: &mut *const u8,
72    ilimit: *const u8,
73    initial_check: bool,
74) -> usize {
75    let mut s: usize;
76    let mut length: usize = 0;
77
78    if initial_check && *ip >= ilimit {
79        // No bytes remain before the limit before the first byte is read.
80        return RVL_ERROR;
81    }
82
83    // Read first byte.
84    // SAFETY: ensured by caller that *ip is a valid readable address.
85    s = **ip as usize;
86    *ip = (*ip).add(1);
87    length += s;
88
89    if *ip > ilimit {
90        // The pointer advanced past the limit after consuming the first byte.
91        return RVL_ERROR;
92    }
93
94    // 32-bit overflow guard: if usize is 32 bits and the accumulated value
95    // already exceeds half of usize::MAX, further additions could wrap.
96    if core::mem::size_of::<usize>() < 8 && length > usize::MAX / 2 {
97        return RVL_ERROR;
98    }
99
100    if s != 255 {
101        return length;
102    }
103
104    // Continue reading 0xFF bytes.
105    loop {
106        // SAFETY: *ip is within the buffer (checked at top of each iteration).
107        s = **ip as usize;
108        *ip = (*ip).add(1);
109        length += s;
110
111        if *ip > ilimit {
112            return RVL_ERROR;
113        }
114
115        if core::mem::size_of::<usize>() < 8 && length > usize::MAX / 2 {
116            return RVL_ERROR;
117        }
118
119        if s != 255 {
120            break;
121        }
122    }
123
124    length
125}
126
127// ─────────────────────────────────────────────────────────────────────────────
128// decompress_generic — lz4.c:2022-2445
129// ─────────────────────────────────────────────────────────────────────────────
130
131/// Core LZ4 block decompression loop.
132///
133/// This is the Rust equivalent of `LZ4_decompress_generic` from lz4.c.
134/// It covers all use-cases through its parameters:
135///
136/// | Parameter        | Meaning                                              |
137/// |-----------------|------------------------------------------------------|
138/// | `src`            | Compressed input slice                               |
139/// | `dst`            | Output buffer                                        |
140/// | `output_size`    | `dst.len()` (capacity) in full-block mode; capacity  |
141/// |                  | in partial-decode mode                               |
142/// | `partial_decoding` | `false` = decode full block; `true` = may stop early |
143/// | `dict`           | Dictionary mode (noDict / withPrefix64k / extDict)   |
144/// | `low_prefix`     | Always ≤ `dst.as_ptr()`; equals `dst.as_ptr()` when  |
145/// |                  | no prefix                                            |
146/// | `dict_start`     | Start of external dictionary (only for `UsingExtDict`)|
147/// | `dict_size`      | External dictionary size in bytes                    |
148///
149/// Returns the number of bytes written to `dst` on success, or
150/// `Err(DecompressError::MalformedInput)` for any invalid input.
151///
152/// # Safety
153/// - `low_prefix` must be ≤ `dst` (pointer ≤ start of output buffer).
154/// - When `dict == DictDirective::UsingExtDict`, `dict_start` must be a valid
155///   pointer to `dict_size` readable bytes, and `dict_start..dict_start+dict_size`
156///   must not alias the output buffer in an unsafe way.
157/// - `dst` must have at least `output_size + WILDCOPYLENGTH + MINMATCH` bytes
158///   of backing storage to absorb wildcard-copy overruns (callers outside this
159///   crate are responsible for reserving sufficient buffer space).
160#[allow(clippy::too_many_arguments)]
161pub unsafe fn decompress_generic(
162    src: *const u8,
163    dst: *mut u8,
164    src_size: usize,
165    output_size: usize,
166    partial_decoding: bool,
167    dict: DictDirective,
168    low_prefix: *const u8,
169    dict_start: *const u8, // only meaningful when dict == UsingExtDict
170    dict_size: usize,
171) -> Result<usize, DecompressError> {
172    // ── Validate top-level arguments ─────────────────────────────────────────
173    if src.is_null() || (output_size as isize) < 0 {
174        return output_error();
175    }
176
177    // ── Set up working pointers ───────────────────────────────────────────────
178    // SAFETY: src_size bytes of readable memory begin at src (caller contract).
179    let mut ip: *const u8 = src;
180    let iend: *const u8 = src.add(src_size);
181
182    // SAFETY: output_size bytes of writable memory begin at dst (caller contract).
183    let mut op: *mut u8 = dst;
184    let oend: *mut u8 = dst.add(output_size);
185
186    // Pointer to end of external dictionary.
187    let dict_end: *const u8 = if dict_start.is_null() {
188        ptr::null()
189    } else {
190        dict_start.add(dict_size)
191    };
192
193    // Whether we need to validate that match back-references fall within the
194    // combined (dict + current output) window.  When dict_size >= 64 KiB the
195    // full LZ4 64-KiB window is always valid, so we skip the check.
196    let check_offset: bool = dict_size < 64 * 1024;
197
198    // Shortcut pointers: define the "safe zone" where both input and output
199    // have enough remaining space for the two-stage fast-path copy.
200    // shortiend = iend - 14 (maxLL) - 2 (offset field)
201    // shortoend = oend - 14 (maxLL) - 18 (maxML)
202    let short_iend: *const u8 = if src_size >= 16 {
203        iend.sub(14).sub(2)
204    } else {
205        src
206    };
207    let short_oend: *mut u8 = if output_size >= 32 {
208        oend.sub(14).sub(18)
209    } else {
210        dst
211    };
212
213    // ── Special cases ─────────────────────────────────────────────────────────
214    // Equivalent to C assert(lowPrefix <= op).
215    debug_assert!(low_prefix <= op as *const u8);
216
217    if output_size == 0 {
218        if partial_decoding {
219            return Ok(0);
220        }
221        // Empty dst is only valid when the compressed block is a single 0-token.
222        return if src_size == 1 && *src == 0 {
223            Ok(0)
224        } else {
225            output_error()
226        };
227    }
228    if src_size == 0 {
229        return output_error();
230    }
231
232    // ── Main decode loop ──────────────────────────────────────────────────────
233    //
234    // Control-flow mapping from C goto labels:
235    //   goto _output_error  →  return output_error()
236    //   goto _copy_match    →  handled by shared match-decode block at end of
237    //                          each loop iteration (both the shortcut-failure
238    //                          path and the normal path set up `ml`/`offset`/
239    //                          `match_ptr` and fall through to the same code)
240    //   break (EOF)         →  break out of 'decode loop
241    'decode: loop {
242        // Every iteration starts with a new token byte.
243        // C: assert(ip < iend);
244        debug_assert!(ip < iend);
245
246        // SAFETY: ip < iend guarantees one readable byte.
247        let token: u8 = *ip;
248        ip = ip.add(1);
249
250        let mut lit_length: usize = (token >> ML_BITS as u8) as usize;
251
252        // Variables shared between shortcut and normal path, set before the
253        // match-decode section at the bottom of the loop.
254        let offset: usize;
255        let match_ptr: *const u8;
256        let ml: usize; // match length, token nibble only (not yet extended)
257
258        // ── Two-stage shortcut (C: lines 2230-2261) ───────────────────────────
259        //
260        // When literal length is short (< 15) and there is ample space in both
261        // input and output, skip the full bounds-checked paths and copy 16/18
262        // bytes at once.
263        if lit_length != RUN_MASK as usize
264            && (ip < short_iend)
265            && (op as *const u8 <= short_oend as *const u8)
266        {
267            // Stage 1: copy exactly `lit_length` bytes (via a 16-byte write).
268            // SAFETY: The shortcut conditions guarantee:
269            //   - ip + 16 <= iend  (short_iend = iend - 16)
270            //   - op + 16 <= oend  (short_oend = oend - 32)
271            // so reading 16 from ip and writing 16 to op are both in bounds.
272            ptr::copy_nonoverlapping(ip, op, 16);
273            op = op.add(lit_length);
274            ip = ip.add(lit_length);
275
276            // Stage 2: decode match info.
277            ml = (token & ML_MASK as u8) as usize;
278
279            // SAFETY: short_iend = iend - 16, lit_length <= 14, so ip has
280            // advanced by at most 14 bytes; ip + 2 <= iend is guaranteed.
281            let off16 = read_le16(ip) as usize;
282            ip = ip.add(2);
283
284            // SAFETY: op >= dst; off16 may be 0 (checked later).
285            let mp = (op as *const u8).wrapping_sub(off16);
286
287            if ml != ML_MASK as usize
288                && off16 >= 8
289                && (dict == DictDirective::WithPrefix64k || mp >= low_prefix)
290            {
291                // Fast 18-byte match copy — no overlap possible (offset >= 8).
292                // SAFETY: The shortcut conditions guarantee op + 18 <= oend.
293                // mp >= low_prefix guarantees the source is within the valid window.
294                ptr::copy_nonoverlapping(mp, op, 8);
295                ptr::copy_nonoverlapping(mp.add(8), op.add(8), 8);
296                ptr::copy_nonoverlapping(mp.add(16), op.add(16), 2);
297                op = op.add(ml + MINMATCH);
298                continue 'decode;
299            }
300
301            // Stage 2 did not qualify for the fast copy; the literal copy
302            // already happened, the offset is already consumed.  Fall through
303            // to the shared match-decode section below.
304            offset = off16;
305            match_ptr = mp;
306        } else {
307            // ── Full literal decode path (C: lines 2263-2334) ─────────────────
308
309            if lit_length == RUN_MASK as usize {
310                // SAFETY: iend - RUN_MASK is the ilimit for the variable-length
311                // literal reader (C: `iend - RUN_MASK`).
312                let ilimit = if src_size >= RUN_MASK as usize {
313                    iend.sub(RUN_MASK as usize)
314                } else {
315                    src
316                };
317                let addl = read_variable_length(&mut ip, ilimit, true);
318                if addl == RVL_ERROR {
319                    return output_error();
320                }
321                lit_length += addl;
322
323                // Pointer wrap-around detection (matches C uptrval overflow check).
324                if (op as usize).wrapping_add(lit_length) < op as usize {
325                    return output_error();
326                }
327                if (ip as usize).wrapping_add(lit_length) < ip as usize {
328                    return output_error();
329                }
330            }
331
332            // Copy literals.
333            let cpy: *mut u8 = op.add(lit_length);
334
335            // Check whether we are at the last sequence or near the buffer ends.
336            // C: (cpy > oend-MFLIMIT) || (ip+length > iend-(2+1+LASTLITERALS))
337            let near_out_end = cpy > oend.sub(MFLIMIT);
338            let near_in_end = ip.add(lit_length) > iend.sub(2 + 1 + LASTLITERALS);
339
340            if near_out_end || near_in_end {
341                // Slow / last-sequence path.
342                if partial_decoding {
343                    // Clamp literal length to whatever fits in input.
344                    let (lit_length, cpy) = if ip.add(lit_length) > iend {
345                        let ll = iend as usize - ip as usize;
346                        (ll, op.add(ll))
347                    } else {
348                        (lit_length, cpy)
349                    };
350                    // Clamp to output capacity.
351                    let (lit_length, cpy) = if cpy > oend {
352                        let ll = oend as usize - op as usize;
353                        (ll, oend)
354                    } else {
355                        (lit_length, cpy)
356                    };
357
358                    // SAFETY: src and dst may overlap in in-place decompression;
359                    // ptr::copy (memmove) handles overlapping regions correctly.
360                    ptr::copy(ip, op, lit_length);
361                    ip = ip.add(lit_length);
362                    op = cpy;
363
364                    // Break when output is full or input is exhausted (need at least
365                    // 2 bytes for a match offset).  The `!partial_decoding` guard is
366                    // always false in this branch; it mirrors the C source's unified
367                    // condition and is left for structural clarity.
368                    if !partial_decoding || cpy == oend || ip >= iend.sub(2) {
369                        break 'decode;
370                    }
371                } else {
372                    // Full-block mode: this must be the last sequence.
373                    // C: (ip+length != iend) || (cpy > oend) → _output_error
374                    if ip.add(lit_length) != iend || cpy > oend {
375                        return output_error();
376                    }
377                    // SAFETY: same as above — memmove for in-place safety.
378                    ptr::copy(ip, op, lit_length);
379                    op = cpy;
380                    break 'decode;
381                }
382            } else {
383                // Normal path: wildcard-copy.
384                // SAFETY: wild_copy8 may write up to 8 bytes past `cpy`.
385                // The condition `!near_out_end` guarantees cpy <= oend - MFLIMIT,
386                // and MFLIMIT (12) > WILDCOPYLENGTH (8), so the overrun is safe.
387                wild_copy8(op, ip, cpy);
388                ip = ip.add(lit_length);
389                op = cpy;
390            }
391
392            // Read match offset (2 bytes).
393            // SAFETY: !near_in_end guarantees ip + 2 <= iend.
394            offset = read_le16(ip) as usize;
395            ip = ip.add(2);
396
397            // SAFETY: op >= dst; arithmetic may produce a pointer before the
398            // output buffer if offset is bogus — validated below.
399            match_ptr = (op as *const u8).wrapping_sub(offset);
400
401            ml = (token & ML_MASK as u8) as usize;
402        }
403
404        // ── _copy_match: (C: line 2344) ───────────────────────────────────────
405        //
406        // Reached from BOTH the shortcut-failure path and the normal path.
407        // At this point:
408        //   - `ml`        = `token & ML_MASK` (may need extension)
409        //   - `offset`    = 16-bit back-reference distance (already consumed)
410        //   - `match_ptr` = `op - offset` (may point before dst / into dict)
411        //   - `ip`        = positioned after the offset field
412
413        let mut ml_ext = ml;
414
415        if ml == ML_MASK as usize {
416            // Extended match length.
417            // ilimit = iend - LASTLITERALS + 1  (C: line 2346)
418            let ilimit = if src_size >= LASTLITERALS {
419                iend.sub(LASTLITERALS).add(1)
420            } else {
421                src
422            };
423            let addl = read_variable_length(&mut ip, ilimit, false);
424            if addl == RVL_ERROR {
425                return output_error();
426            }
427            ml_ext += addl;
428
429            // Overflow detection: C `(uptrval)(op)+length < (uptrval)op`
430            if (op as usize).wrapping_add(ml_ext) < op as usize {
431                return output_error();
432            }
433        }
434        let match_length: usize = ml_ext + MINMATCH;
435
436        // ── Bounds check: offset validity ──────────────────────────────────────
437        // C: if (checkOffset) && (match + dictSize < lowPrefix) → _output_error
438        //
439        // SAFETY: this is a pointer-arithmetic comparison; wrapping is intentional
440        // (a bogus match_ptr far before the buffer will wrap and be < low_prefix).
441        if check_offset && (match_ptr as usize).wrapping_add(dict_size) < low_prefix as usize {
442            return output_error();
443        }
444
445        // ── External-dictionary match (C: lines 2358-2384) ────────────────────
446        if dict == DictDirective::UsingExtDict && (match_ptr as *const u8) < low_prefix {
447            // The reference is before the current output prefix → it lives in the
448            // external dictionary.
449            debug_assert!(!dict_end.is_null());
450
451            // Partial-decode or full-block end-of-block constraint.
452            let match_length = if op.add(match_length) > oend.sub(LASTLITERALS) {
453                if partial_decoding {
454                    // Clamp to available output.
455                    // SAFETY: oend >= op (loop invariant).
456                    (oend as usize - op as usize).min(match_length)
457                } else {
458                    return output_error();
459                }
460            } else {
461                match_length
462            };
463
464            // Distance from match_ptr to the start of the current output prefix.
465            let copy_size = low_prefix as usize - match_ptr as usize;
466
467            if match_length <= copy_size {
468                // Match fits entirely within the external dictionary.
469                // SAFETY: dict_end - copy_size is a valid address inside the
470                // dictionary allocation; we copy `match_length` bytes from it.
471                let dict_src = dict_end.sub(copy_size);
472                // ptr::copy handles overlapping in-place scenarios.
473                ptr::copy(dict_src, op, match_length);
474                op = op.add(match_length);
475            } else {
476                // Match spans both dictionary and current output prefix.
477                let rest_size = match_length - copy_size;
478
479                // First: copy `copy_size` bytes from tail of external dict.
480                // SAFETY: dict_end - copy_size .. dict_end is valid dict memory.
481                ptr::copy_nonoverlapping(dict_end.sub(copy_size), op, copy_size);
482                op = op.add(copy_size);
483
484                // Then: copy `rest_size` bytes from the start of the prefix.
485                // This may overlap the current output — handle carefully.
486                if rest_size > (op as usize - low_prefix as usize) {
487                    // Overlapping: must copy byte-by-byte.
488                    let end_of_match: *mut u8 = op.add(rest_size);
489                    let mut copy_from: *const u8 = low_prefix;
490                    // SAFETY: copy_from stays within the current output block
491                    // (low_prefix..op is already written), advancing in lock-step.
492                    while op < end_of_match {
493                        *op = *copy_from;
494                        op = op.add(1);
495                        copy_from = copy_from.add(1);
496                    }
497                } else {
498                    // No overlap: plain memcpy from prefix start.
499                    // SAFETY: low_prefix .. low_prefix + rest_size is within
500                    // the already-written output window.
501                    ptr::copy_nonoverlapping(low_prefix, op, rest_size);
502                    op = op.add(rest_size);
503                }
504            }
505            continue 'decode;
506        }
507
508        // ── Within-block match copy ────────────────────────────────────────────
509        // C: assert(match >= lowPrefix);
510        debug_assert!(match_ptr >= low_prefix);
511
512        let cpy: *mut u8 = op.add(match_length);
513
514        // Partial-decode: near the end of the output buffer we cannot use the
515        // fast wildcopy routines.
516        if partial_decoding && cpy > oend.sub(MATCH_SAFEGUARD_DISTANCE) {
517            let mlen = (oend as usize - op as usize).min(match_length);
518            let match_end: *const u8 = match_ptr.add(mlen);
519            let copy_end: *mut u8 = op.add(mlen);
520
521            if match_end > op as *const u8 {
522                // Overlap: copy byte-by-byte.
523                // SAFETY: Both src and dst are within valid memory; byte-by-byte
524                // copy handles the overlap correctly.
525                let mut mp = match_ptr;
526                while op < copy_end {
527                    *op = *mp;
528                    op = op.add(1);
529                    mp = mp.add(1);
530                }
531            } else {
532                // No overlap.
533                // SAFETY: mlen bytes are available at match_ptr (validated by
534                // the offset check above).
535                ptr::copy_nonoverlapping(match_ptr, op, mlen);
536            }
537            op = copy_end;
538            if op == oend {
539                break 'decode;
540            }
541            continue 'decode;
542        }
543
544        // Standard match copy.
545        // First handle the tricky small-offset (< 8) overlapping case using the
546        // offset tables, then handle offsets >= 8 with a plain 8-byte copy.
547        let mut mp: *const u8 = match_ptr; // local mutable alias for adjustment
548
549        if offset < 8 {
550            // Small offset: may be a repeating-byte or repeating-pair pattern.
551            // Write 0 first so that memory-sanitizers see an initialised value
552            // if offset == 0 (which is an error, caught by the offset check).
553            // SAFETY: op has at least 4 bytes of space (cpy > op + MINMATCH).
554            write32(op, 0);
555            // SAFETY: mp is within the valid window (checked by offset guard above).
556            *op = *mp;
557            *op.add(1) = *mp.add(1);
558            *op.add(2) = *mp.add(2);
559            *op.add(3) = *mp.add(3);
560            mp = mp.add(INC32TABLE[offset] as usize);
561            // SAFETY: copy 4 bytes from adjusted match position.
562            ptr::copy_nonoverlapping(mp, op.add(4), 4);
563            mp = mp.offset(-(DEC64TABLE[offset] as isize));
564        } else {
565            // SAFETY: offset >= 8 means source and destination 8-byte chunks
566            // cannot overlap; copy_nonoverlapping is safe.
567            ptr::copy_nonoverlapping(mp, op, 8);
568            mp = mp.add(8);
569        }
570        op = op.add(8);
571
572        // Finish the match copy, handling the near-end-of-buffer case.
573        if cpy > oend.sub(MATCH_SAFEGUARD_DISTANCE) {
574            // Close to the output end: cannot use wildCopy8 freely.
575            let o_copy_limit: *mut u8 = oend.sub(WILDCOPYLENGTH - 1);
576
577            // C: if (cpy > oend-LASTLITERALS) → _output_error
578            // The last LASTLITERALS bytes of the block must be literals.
579            if cpy > oend.sub(LASTLITERALS) {
580                return output_error();
581            }
582
583            if op < o_copy_limit {
584                // SAFETY: wild_copy8 may write 8 bytes past o_copy_limit; the
585                // margin `oend - o_copy_limit = WILDCOPYLENGTH - 1 = 7` bytes is
586                // within the buffer's WILDCOPYLENGTH reserved tail.
587                wild_copy8(op, mp, o_copy_limit);
588                // SAFETY: arithmetic matches the bytes written by wild_copy8.
589                mp = mp.add(o_copy_limit as usize - op as usize);
590                op = o_copy_limit;
591            }
592
593            // Final byte-by-byte copy up to cpy.
594            // SAFETY: cpy <= oend - LASTLITERALS <= oend; op < cpy.
595            while op < cpy {
596                *op = *mp;
597                op = op.add(1);
598                mp = mp.add(1);
599            }
600        } else {
601            // Normal case: plenty of room.
602            // SAFETY: copy_nonoverlapping safe (8 bytes, offset >= 8 on this path).
603            ptr::copy_nonoverlapping(mp, op, 8);
604            if match_length > 16 {
605                // SAFETY: wild_copy8 may write 8 bytes past cpy; the caller
606                // must have reserved at least WILDCOPYLENGTH bytes past oend.
607                wild_copy8(op.add(8), mp.add(8), cpy);
608            }
609        }
610
611        // Wildcopy correction: advance op to the exact end of the match.
612        op = cpy;
613    } // end 'decode
614
615    // ── End of decoding ───────────────────────────────────────────────────────
616    // Return the number of bytes written to the output buffer.
617    // SAFETY: op started at dst and only advanced forward; op - dst is the count.
618    Ok(op as usize - dst as usize)
619}
620
621// ─────────────────────────────────────────────────────────────────────────────
622// Public safe wrappers
623// ─────────────────────────────────────────────────────────────────────────────
624
625/// Decompress a full LZ4 block (no dictionary).
626///
627/// Equivalent to `LZ4_decompress_safe`.
628///
629/// Returns the number of bytes written into `dst` on success, or
630/// `Err(DecompressError::MalformedInput)` if the input is invalid.
631pub fn decompress_safe(src: &[u8], dst: &mut [u8]) -> Result<usize, DecompressError> {
632    if dst.is_empty() {
633        // Special case: zero-capacity output.
634        if src.len() == 1 && src[0] == 0 {
635            return Ok(0);
636        }
637        return output_error();
638    }
639
640    // SAFETY:
641    //   - `src.as_ptr()` and `dst.as_mut_ptr()` are valid, non-null, and
642    //     correctly sized by the slice invariants.
643    //   - `low_prefix == dst.as_ptr()` (no prefix).
644    //   - `dict_start` is null and `dict_size` is 0.
645    //   - The caller is responsible for providing a `dst` buffer that is large
646    //     enough; we pass `dst.len()` as the output capacity.
647    unsafe {
648        decompress_generic(
649            src.as_ptr(),
650            dst.as_mut_ptr(),
651            src.len(),
652            dst.len(),
653            false, // full block
654            DictDirective::NoDict,
655            dst.as_ptr(), // low_prefix == dst start
656            ptr::null(),  // no external dictionary
657            0,
658        )
659    }
660}
661
662/// Decompress up to `target_output_size` bytes from an LZ4 block (no dict).
663///
664/// Equivalent to `LZ4_decompress_safe_partial`.
665///
666/// `dst.len()` is the capacity of the output buffer; `target_output_size` is
667/// the number of decompressed bytes the caller wants.  If the compressed block
668/// contains more data it will be decoded up to the limit.
669///
670/// Returns the number of bytes written into `dst`, or
671/// `Err(DecompressError::MalformedInput)` on error.
672pub fn decompress_safe_partial(
673    src: &[u8],
674    dst: &mut [u8],
675    target_output_size: usize,
676) -> Result<usize, DecompressError> {
677    let output_size = target_output_size.min(dst.len());
678
679    // SAFETY: same contracts as `decompress_safe`; partial_decoding = true.
680    unsafe {
681        decompress_generic(
682            src.as_ptr(),
683            dst.as_mut_ptr(),
684            src.len(),
685            output_size,
686            true, // partial decode
687            DictDirective::NoDict,
688            dst.as_ptr(),
689            ptr::null(),
690            0,
691        )
692    }
693}
694
695/// Decompress an LZ4 block using an external dictionary.
696///
697/// Equivalent to `LZ4_decompress_safe_usingDict` (for the non-split,
698/// non-streaming case).
699///
700/// `dict` must be the same dictionary that was used during compression.
701/// Returns the number of bytes written into `dst`, or
702/// `Err(DecompressError::MalformedInput)` on error.
703pub fn decompress_safe_using_dict(
704    src: &[u8],
705    dst: &mut [u8],
706    dict: &[u8],
707) -> Result<usize, DecompressError> {
708    if dict.is_empty() {
709        return decompress_safe(src, dst);
710    }
711
712    // SAFETY:
713    //   - All slices are valid by Rust slice invariants.
714    //   - low_prefix == dst.as_ptr() (no prior output prefix).
715    //   - dict_start / dict_size describe the external dictionary.
716    unsafe {
717        decompress_generic(
718            src.as_ptr(),
719            dst.as_mut_ptr(),
720            src.len(),
721            dst.len(),
722            false,
723            DictDirective::UsingExtDict,
724            dst.as_ptr(), // low_prefix: nothing before dst
725            dict.as_ptr(),
726            dict.len(),
727        )
728    }
729}