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}