structured_zstd/decoding/frame_decoder.rs
1//! Framedecoder is the main low-level struct users interact with to decode zstd frames
2//!
3//! Zstandard compressed data is made of one or more frames. Each frame is independent and can be
4//! decompressed independently of other frames. This module contains structures
5//! and utilities that can be used to decode a frame.
6
7use super::frame;
8use crate::decoding;
9use crate::decoding::block_decoder::BlockDecoder;
10use crate::decoding::buffer_backend::BufferBackend;
11use crate::decoding::dictionary::{Dictionary, DictionaryHandle};
12use crate::decoding::errors::{DecodeBlockContentError, FrameDecoderError};
13use crate::decoding::flat_buf::FlatBuf;
14use crate::decoding::ringbuffer::RingBuffer;
15use crate::decoding::scratch::DecoderScratch;
16use crate::io::{Error, Read, Write};
17use alloc::collections::BTreeMap;
18use alloc::vec::Vec;
19use core::convert::TryInto;
20
21use crate::common::MAXIMUM_ALLOWED_WINDOW_SIZE;
22
23/// Build the block-header decode error. With the `lsm` feature it captures
24/// the failing block's index and frame offset (block-precise recovery);
25/// without it, the legacy positionless variant — so the default build's
26/// error surface stays byte-identical to the upstream zstd.
27#[cfg(feature = "lsm")]
28fn block_header_decode_error(
29 source: crate::decoding::errors::BlockHeaderReadError,
30 block_index: u32,
31 frame_offset: u32,
32) -> FrameDecoderError {
33 FrameDecoderError::FailedToReadBlockHeaderAt {
34 source,
35 block_index,
36 frame_offset,
37 }
38}
39#[cfg(not(feature = "lsm"))]
40fn block_header_decode_error(
41 source: crate::decoding::errors::BlockHeaderReadError,
42 _block_index: u32,
43 _frame_offset: u32,
44) -> FrameDecoderError {
45 FrameDecoderError::FailedToReadBlockHeader(source)
46}
47
48/// Build the block-body decode error. With `lsm` it captures the block
49/// index, frame offset, and the failing block's structural metadata
50/// (reconstructed from its header); without it, the legacy variant.
51#[cfg(feature = "lsm")]
52fn block_body_decode_error(
53 source: DecodeBlockContentError,
54 block_index: u32,
55 frame_offset: u32,
56 header: &crate::blocks::block::BlockHeader,
57 header_size: u8,
58) -> FrameDecoderError {
59 use crate::blocks::block::BlockType;
60 // Physical wire body vs the raw `Block_Size` field: RLE writes a single
61 // body byte while `Block_Size` carries the repeat count; Raw/Compressed
62 // bodies match the field.
63 let (body_size, block_size_field) = match header.block_type {
64 BlockType::RLE => (1u32, header.decompressed_size),
65 _ => (header.content_size, header.content_size),
66 };
67 FrameDecoderError::FailedToReadBlockBodyAt {
68 source,
69 block_index,
70 frame_offset,
71 block: crate::encoding::frame_emit_info::FrameBlock {
72 offset_in_frame: frame_offset,
73 header_size,
74 body_size,
75 block_size_field,
76 block_type: header.block_type,
77 last_block: header.last_block,
78 // Raw/RLE carry their regenerated size in the header;
79 // a Compressed block's is unknown until decoded, so
80 // `read_block_header` leaves `decompressed_size` 0 here.
81 decompressed_size: header.decompressed_size,
82 },
83 }
84}
85#[cfg(not(feature = "lsm"))]
86fn block_body_decode_error(
87 source: DecodeBlockContentError,
88 _block_index: u32,
89 _frame_offset: u32,
90 _header: &crate::blocks::block::BlockHeader,
91 _header_size: u8,
92) -> FrameDecoderError {
93 FrameDecoderError::FailedToReadBlockBody(source)
94}
95
96/// Low level Zstandard decoder that can be used to decompress frames with fine control over when and how many bytes are decoded.
97///
98/// This decoder is able to decode frames only partially and gives control
99/// over how many bytes/blocks will be decoded at a time (so you don't have to decode a 10GB file into memory all at once).
100/// It reads bytes as needed from a provided source and can be read from to collect partial results.
101///
102/// If you want to just read the whole frame with an `io::Read` without having to deal with manually calling [FrameDecoder::decode_blocks]
103/// you can use the provided [crate::decoding::StreamingDecoder] wich wraps this FrameDecoder.
104///
105/// Workflow is as follows:
106/// ```
107/// use structured_zstd::decoding::BlockDecodingStrategy;
108///
109/// # #[cfg(feature = "std")]
110/// use std::io::{Read, Write};
111///
112/// // no_std environments can use the crate's own Read traits
113/// # #[cfg(not(feature = "std"))]
114/// use structured_zstd::io::{Read, Write};
115///
116/// fn decode_this(mut file: impl Read) {
117/// //Create a new decoder
118/// let mut frame_dec = structured_zstd::decoding::FrameDecoder::new();
119/// let mut result = Vec::new();
120///
121/// // Use reset or init to make the decoder ready to decode the frame from the io::Read
122/// frame_dec.reset(&mut file).unwrap();
123///
124/// // Loop until the frame has been decoded completely
125/// while !frame_dec.is_finished() {
126/// // decode (roughly) batch_size many bytes
127/// frame_dec.decode_blocks(&mut file, BlockDecodingStrategy::UptoBytes(1024)).unwrap();
128///
129/// // read from the decoder to collect bytes from the internal buffer
130/// let bytes_read = frame_dec.read(result.as_mut_slice()).unwrap();
131///
132/// // then do something with it
133/// do_something(&result[0..bytes_read]);
134/// }
135///
136/// // handle the last chunk of data
137/// while frame_dec.can_collect() > 0 {
138/// let x = frame_dec.read(result.as_mut_slice()).unwrap();
139///
140/// do_something(&result[0..x]);
141/// }
142/// }
143///
144/// fn do_something(data: &[u8]) {
145/// # #[cfg(feature = "std")]
146/// std::io::stdout().write_all(data).unwrap();
147/// }
148/// ```
149pub struct FrameDecoder {
150 state: Option<FrameDecoderState>,
151 /// Test-only observability: frames decoded via `run_direct_decode`.
152 /// The direct and buffered paths are byte-identical, so dispatch
153 /// regressions (e.g. re-excluding dictionary frames from the direct
154 /// gate) are invisible to output assertions; tests pin the path here.
155 #[cfg(test)]
156 direct_frames: u64,
157 // Registered dictionaries are stored by shared handle (Arc/Rc) so a
158 // single content copy is referenced by every frame the decoder decodes
159 // (upstream zstd `ZSTD_refDDict`), rather than re-copied into the decode buffer
160 // per frame. `add_dict` wraps an owned `Dictionary` into a handle.
161 owned_dicts: BTreeMap<u32, DictionaryHandle>,
162 #[cfg(target_has_atomic = "ptr")]
163 shared_dicts: BTreeMap<u32, DictionaryHandle>,
164 #[cfg(not(target_has_atomic = "ptr"))]
165 shared_dicts: (),
166 /// `ZSTD_f_zstd1_magicless` — when true, [`Self::init`] / [`Self::reset`]
167 /// expect frames without the 4-byte magic number prefix.
168 /// Default false (standard zstd format).
169 magicless: bool,
170 /// How the optional content checksum is handled. Default
171 /// [`ContentChecksum::EmitOnly`] (compute + expose, no error on
172 /// mismatch). Set via [`Self::set_content_checksum`].
173 content_checksum: ContentChecksum,
174 /// Pinned `Dictionary_ID` expectation set via
175 /// [`Self::expect_dict_id`]. `None` (default) disables the
176 /// check; `Some(0)` matches frames whose header omits the
177 /// optional dict_id (treated as "no dictionary"). Validated in
178 /// [`Self::reset`] AFTER the frame header parses successfully
179 /// and BEFORE any block decode work.
180 #[cfg(feature = "lsm")]
181 expect_dict_id: Option<u32>,
182 /// Pinned `Window_Descriptor` byte expectation set via
183 /// [`Self::expect_window_descriptor`]. `None` (default)
184 /// disables the check. Validated in [`Self::reset`] AFTER the
185 /// frame header parses successfully and BEFORE any block
186 /// decode work. Single-segment frames (which omit the
187 /// `Window_Descriptor` byte from the wire) surface as
188 /// [`crate::decoding::errors::FrameDecoderError::UnexpectedWindowDescriptor`]
189 /// with `found: None`.
190 #[cfg(feature = "lsm")]
191 expect_window_descriptor: Option<u8>,
192 /// When `true`, the per-block decode loop XXH64-hashes each
193 /// block's decompressed bytes and stores the low-32-bit digest in
194 /// [`Self::computed_block_checksums`]. Default `false` (zero
195 /// cost). Set via [`Self::enable_per_block_checksums`]. Gated on
196 /// `all(lsm, hash)` because XXH64 lives behind the `hash`
197 /// feature.
198 #[cfg(all(feature = "lsm", feature = "hash"))]
199 per_block_checksums_enabled: bool,
200 /// Per-block XXH64 (low 32 bits) digests captured during the
201 /// current frame's decode when `per_block_checksums_enabled` is
202 /// set. Reset at the start of every new frame. Gated on
203 /// `all(lsm, hash)` (see `per_block_checksums_enabled`).
204 #[cfg(all(feature = "lsm", feature = "hash"))]
205 computed_block_checksums: alloc::vec::Vec<u32>,
206 /// Best kernel this CPU offers, resolved once per decoder rather than per
207 /// block decoder built. A chunked decode enters `decode_from_to` once per
208 /// caller-sized target, so detecting there put an atomic read and a branch
209 /// on every call; feature detection belongs before the work, not inside it.
210 kernel: crate::cpu_kernel::CpuKernelTag,
211}
212
213/// How the decoder treats a frame's optional XXH64 content checksum
214/// (RFC 8878 Content_Checksum_flag). The XXH64 pass over the decompressed
215/// output is a measurable share of decode time, so it is made skippable.
216///
217/// ```
218/// use structured_zstd::decoding::{ContentChecksum, FrameDecoder};
219/// let mut decoder = FrameDecoder::new();
220/// decoder.set_content_checksum(ContentChecksum::Verify);
221/// ```
222#[derive(Copy, Clone, PartialEq, Eq, Debug, Default)]
223pub enum ContentChecksum {
224 /// Skip the XXH64 pass entirely: no compute, no verify.
225 /// `get_calculated_checksum()` returns `None`.
226 None,
227 /// Compute the checksum and expose it via the accessors, but do not
228 /// error on a mismatch. This is the default and matches the historical
229 /// behaviour (callers verify manually if they wish).
230 #[default]
231 EmitOnly,
232 /// Compute the checksum and compare it against the frame's stored value;
233 /// a disagreement fails the decode with
234 /// [`FrameDecoderError::ChecksumMismatch`].
235 /// Without the `hash` feature there is no way to compute a digest, so
236 /// `Verify` cannot detect a mismatch and behaves like `None`.
237 Verify,
238}
239
240/// Decode-relevant identity of a frame, used to reject a [`ResumeState`]
241/// captured from one frame being applied to a frame of a different shape. Covers
242/// every header field that changes how blocks decode (buffer sizing, backend
243/// kind, entropy/dictionary context, trailing-checksum handling, declared
244/// content size, magicless framing).
245///
246/// This is a SHAPE guard, not a content-unique fingerprint: two distinct frames
247/// that happen to share all these header fields produce the same key (no cheap
248/// header field uniquely identifies frame content). It catches the realistic
249/// accidental misuse — applying a snapshot to a frame with a different
250/// window/dictionary/size — with a typed error instead of byte-wrong output.
251/// Pairing a `ResumeState` with the correct frame's compressed source and
252/// `window_prime` remains the caller's contract.
253#[cfg(feature = "lsm")]
254#[derive(Clone, Copy, PartialEq, Eq, Debug)]
255struct FrameKey {
256 window_size: u64,
257 frame_content_size: u64,
258 /// `Dictionary_ID` declared in the frame header (`None` when omitted).
259 dictionary_id: Option<u32>,
260 /// Dictionary actually applied to the decoder (`state.using_dict`). This is
261 /// distinct from `dictionary_id`: a frame with a dictless header can still
262 /// be decoded with an explicit dictionary via `reset_with_dict_handle` /
263 /// `force_dict`, and two such decodes with different dictionaries must NOT
264 /// compare equal — keying only on the header field would miss that.
265 active_dictionary_id: Option<u32>,
266 single_segment: bool,
267 content_checksum: bool,
268 magicless: bool,
269}
270
271#[cfg(feature = "lsm")]
272impl FrameKey {
273 fn from_state(state: &FrameDecoderState, magicless: bool) -> FrameKey {
274 let header = &state.frame_header;
275 FrameKey {
276 window_size: header.window_size().unwrap_or(0),
277 frame_content_size: header.frame_content_size(),
278 dictionary_id: header.dictionary_id(),
279 active_dictionary_id: state.using_dict,
280 single_segment: header.descriptor.single_segment_flag(),
281 content_checksum: header.descriptor.content_checksum_flag(),
282 magicless,
283 }
284 }
285}
286
287/// XXH64 of a contiguous byte slice — the resume-side counterpart to
288/// [`DecoderScratchKind::window_tail_hash`]. Streaming XXH64 is chunk-boundary
289/// independent, so this single-slice hash equals the emit-side two-slice hash
290/// over the same bytes.
291#[cfg(all(feature = "lsm", feature = "hash"))]
292fn xxh64_of(bytes: &[u8]) -> u64 {
293 use core::hash::Hasher;
294 let mut h = twox_hash::XxHash64::with_seed(0);
295 h.write(bytes);
296 h.finish()
297}
298
299/// Cross-block decode state needed to resume a cold partial decode at an inner
300/// block boundary, emitted by [`FrameDecoder::decode_blocks_partial`] when its
301/// `emit_resume` argument is `true` (returned in
302/// [`PartialDecode::resume_state`]) and fed back via that same method's
303/// [`resume`](FrameDecoder::decode_blocks_partial) argument
304/// ([`ResumeInput`]).
305///
306/// A zstd block does not carry all the state required to decode it in
307/// isolation: besides the shared match window (the decompressed output history),
308/// a Compressed block may reuse the previous block's entropy tables via
309/// `Repeat_Mode` (literals Huffman + the LL/OF/ML FSE distributions) and always
310/// continues the running repeat-offset history. This snapshot carries exactly
311/// that carry-over state plus the resume coordinates, so resuming is
312/// byte-identical to a contiguous decode even across a dropped decoder. The
313/// window itself is NOT stored here — the caller supplies it back through
314/// [`ResumeInput::window_prime`] from the decompressed output it already
315/// persists. Neither is the dictionary: for a dictionary frame the caller
316/// re-attaches it to the resuming decoder via [`FrameDecoder::reset`] /
317/// [`FrameDecoder::reset_with_dict_handle`] (it already holds the dictionary
318/// from encode time), and the snapshot records only the dictionary's identity
319/// so a resume under a different dictionary is rejected.
320///
321/// Behind the `lsm` Cargo feature.
322#[cfg(feature = "lsm")]
323#[cfg_attr(docsrs, doc(cfg(feature = "lsm")))]
324pub struct ResumeState {
325 /// Identity of the frame this state was captured from. Compared against the
326 /// frame currently reset into the decoder before any state is restored, so a
327 /// snapshot from a different frame shape is rejected with
328 /// [`FrameDecoderError::ResumeFrameMismatch`] instead of silently producing
329 /// byte-wrong output.
330 frame_key: FrameKey,
331 /// Index of the block to resume AT (the first block NOT yet decoded).
332 block_index: u32,
333 /// Cumulative decompressed byte count produced before `block_index`.
334 output_offset: u64,
335 /// FSE tables (LL/OF/ML) as of the last decoded block — the source for a
336 /// `Repeat_Mode` resume block.
337 fse: crate::decoding::scratch::FSEScratch,
338 /// Huffman literals table as of the last decoded block — the source for a
339 /// treeless (repeat) literals resume block.
340 huf: crate::decoding::scratch::HuffmanScratch,
341 /// Running repeat-offset history (`offset_hist`) as of the last decoded
342 /// block.
343 offset_hist: [u32; 3],
344 /// XXH64 of the exact window-prime bytes (the last `min(window_size,
345 /// output_offset)` decompressed bytes) captured at emit. Verified at resume
346 /// against the caller-supplied [`ResumeInput::window_prime`]: a content
347 /// mismatch (wrong frame, wrong or corrupted prime) is a near-unique
348 /// (≈2⁻⁶⁴) signal and is rejected with
349 /// [`FrameDecoderError::ResumeFrameMismatch`]. This is the content-exact
350 /// guard; [`FrameKey`] is the cheap shape pre-check that works without the
351 /// `hash` feature. Behind `all(lsm, hash)`.
352 #[cfg(feature = "hash")]
353 window_hash: u64,
354}
355
356#[cfg(feature = "lsm")]
357impl ResumeState {
358 /// Inner block index this state resumes at (the first block not yet
359 /// decoded). Pass it as the `end_block` lower bound (and as `start_block`)
360 /// of the resuming
361 /// [`decode_blocks_partial`](FrameDecoder::decode_blocks_partial) call.
362 pub fn block_index(&self) -> u32 {
363 self.block_index
364 }
365
366 /// Cumulative decompressed byte count produced before
367 /// [`block_index`](Self::block_index) — i.e. the decompressed offset at
368 /// which the resumed output begins. Equals
369 /// `FrameEmitInfo::decompressed_byte_range(block_index).start`. Use it to
370 /// slice the `window_prime` tail the resumed call needs.
371 pub fn output_offset(&self) -> u64 {
372 self.output_offset
373 }
374}
375
376// Manual Debug: the entropy tables are large internal scratch with no useful
377// Debug surface; only the resume coordinates are worth printing (and this lets
378// `PartialDecode` keep its derived Debug).
379#[cfg(feature = "lsm")]
380impl core::fmt::Debug for ResumeState {
381 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
382 f.debug_struct("ResumeState")
383 .field("block_index", &self.block_index)
384 .field("output_offset", &self.output_offset)
385 .finish_non_exhaustive()
386 }
387}
388
389/// Resume input fed to [`FrameDecoder::decode_blocks_partial`]'s `resume`
390/// argument to continue a cold partial decode without re-decompressing the
391/// preceding blocks.
392///
393/// Behind the `lsm` Cargo feature.
394#[cfg(feature = "lsm")]
395#[cfg_attr(docsrs, doc(cfg(feature = "lsm")))]
396pub struct ResumeInput<'a> {
397 /// The caller's already-decompressed output ending just before
398 /// [`ResumeState::block_index`]. Must contain at least the last
399 /// `min(window_size, output_offset)` bytes (a full match window, or the
400 /// whole prefix when it is shorter than one window); anything beyond the
401 /// last `window_size` bytes is ignored, so passing the entire prefix is
402 /// also valid (capped internally, bounding resume memory to one window).
403 pub window_prime: &'a [u8],
404 /// Cross-block entropy/repcode state emitted by the prior
405 /// [`decode_blocks_partial`](FrameDecoder::decode_blocks_partial) call.
406 pub state: &'a ResumeState,
407}
408
409/// Backend-tagged decode scratch — chosen at frame-reset time based
410/// on the parsed `FrameHeader.descriptor.single_segment_flag()` and
411/// kept stable through the lifetime of the frame. The match in each
412/// helper below dispatches **once per call** (e.g. once per block in
413/// `decode_block_content`, once per drain in `drain_to_writer`) —
414/// never inside the hot push/repeat loop, which is fully
415/// monomorphised through the `DecoderScratch<B>` generic.
416enum DecoderScratchKind {
417 Ring(DecoderScratch<RingBuffer>),
418 Flat(DecoderScratch<FlatBuf>),
419}
420
421impl DecoderScratchKind {
422 fn new_ring(window_size: usize) -> Self {
423 // Lazy ring-buffer allocation: do NOT `reserve(window_size)` here.
424 // The direct-decode path (`run_direct_decode`) writes through
425 // `UserSliceBackend` and never touches the ring; allocating it
426 // eagerly wastes one full window of peak memory on the common
427 // direct-eligible frame. On the non-direct path the window is
428 // pre-reserved once at frame entry (`decode_all_impl` and
429 // `decode_blocks` both call `DecoderScratchKind::reserve_buffer`
430 // before any block writes), so multi-block frames pay one
431 // amortised grow instead of repeated `reserve_amortized` steps
432 // per block. Issue #279 round 2.
433 let s = DecoderScratch::<RingBuffer>::new(window_size);
434 Self::Ring(s)
435 }
436
437 /// Construct a flat-backed scratch for a single-segment frame.
438 /// `frame_content_size` is the upcoming output size in bytes
439 /// (== `window_size` when the flag is set).
440 ///
441 /// Lazy buffer allocation (mirrors [`Self::new_ring`]): do NOT
442 /// pre-size the `FlatBuf`. The direct-decode path
443 /// (`run_direct_decode`) writes through `UserSliceBackend` and never
444 /// touches this buffer, so eagerly allocating a full FCS wastes one
445 /// whole content-size of peak memory on the common direct-eligible
446 /// single-segment frame. The non-direct fallback reserves it once via
447 /// `reserve_buffer(window_size)` at frame entry before any block
448 /// write (`FlatBuf::reserve` adds the `WILDCOPY_OVERLENGTH` slack),
449 /// and every inline-exec site (trait method and per-kernel macros)
450 /// now carries a tight-tail bounded copy, so a tight buffer can never
451 /// overshoot regardless of construction-time slack.
452 fn new_flat(frame_content_size: usize) -> Self {
453 let s = DecoderScratch::<FlatBuf>::new(frame_content_size);
454 Self::Flat(s)
455 }
456
457 /// Reset (or transition between) backends for a new frame.
458 /// Reuses the existing `DecoderScratch` allocations (FSE / HUF
459 /// tables, sequence vec, etc.) when the backend kind is unchanged
460 /// — only the underlying buffer is re-sized for the new frame.
461 /// Building a fresh `DecoderScratch` on every frame would
462 /// re-allocate everything and was measured at +255 % vs ring on
463 /// small frames; reusing it keeps the small-frame cost flat.
464 fn reset(&mut self, frame: &frame::FrameHeader, window_size: usize) {
465 if frame.descriptor.single_segment_flag() {
466 match self {
467 Self::Flat(s) => {
468 s.reset(window_size);
469 // `DecoderScratch::reset` clears the backing buffer and
470 // updates `window_size` WITHOUT reserving it (it may still
471 // resize the per-block scratch Vecs up to
472 // `min(window_size, MAX_BLOCK_SIZE)`). Backing-buffer
473 // capacity is decided one layer up: direct-eligible frames
474 // never touch it, and the non-direct path pre-reserves once
475 // via `reserve_buffer(window_size)` at frame entry.
476 }
477 Self::Ring(_) => *self = Self::new_flat(window_size),
478 }
479 } else {
480 match self {
481 Self::Ring(s) => s.reset(window_size),
482 Self::Flat(_) => *self = Self::new_ring(window_size),
483 }
484 }
485 }
486
487 fn init_from_dict(&mut self, dict: &DictionaryHandle) {
488 match self {
489 Self::Ring(s) => s.init_from_dict(dict),
490 Self::Flat(s) => s.init_from_dict(dict),
491 }
492 }
493
494 #[inline]
495 fn buffer_len(&self) -> usize {
496 match self {
497 Self::Ring(s) => s.buffer.len(),
498 Self::Flat(s) => s.buffer.len(),
499 }
500 }
501
502 fn workspace_bytes(&self) -> usize {
503 match self {
504 Self::Ring(s) => s.workspace_bytes(),
505 Self::Flat(s) => s.workspace_bytes(),
506 }
507 }
508
509 /// Pre-reserve the backing buffer to `window_size` in a single
510 /// allocation. Called once on the non-direct (`decode_blocks`) path
511 /// after direct-eligibility is ruled out, so multi-segment fallback
512 /// decodes don't pay repeated `reserve_amortized` grow steps
513 /// (128 KiB → 256 KiB → ... → window) as blocks accumulate.
514 ///
515 /// Direct-eligible frames never call this and pay zero backing-buffer
516 /// allocation for the window, on BOTH backends: `new_ring` and
517 /// `new_flat` are each lazy (no pre-reserve), so a direct-eligible
518 /// frame writes only through `UserSliceBackend` and leaves this
519 /// buffer empty.
520 ///
521 /// `target` is the TARGET buffer capacity (`decoding_buffer_size`) and
522 /// `growth_limit` the most later growth may reach
523 /// (`decoding_buffer_limit`); the method itself computes the shortfall
524 /// past the bytes already buffered before calling the backend's
525 /// ADDITIONAL-semantics `reserve_exact`. That keeps re-entries (the
526 /// decode_all fallback loop runs `decode_blocks` once per strategy
527 /// chunk, and streaming callers invoke it per call) from growing a
528 /// window-full buffer toward 2x window, while per-block growth keeps
529 /// the amortized `reserve`.
530 /// Hand the buffer what the frame declared it would produce, so the
531 /// per-block reservation can stop at the frame's remainder.
532 #[inline]
533 fn set_declared_content(&mut self, content_size: Option<u64>) {
534 match self {
535 Self::Ring(s) => s.buffer.set_declared_content(content_size),
536 Self::Flat(s) => s.buffer.set_declared_content(content_size),
537 }
538 }
539
540 #[inline]
541 fn reserve_buffer(&mut self, target: usize, growth_limit: usize) {
542 let window_size = target;
543 // Exact growth: this is the one-shot pre-reservation, and a request
544 // landing one slack past the retained capacity (e.g. a dictionary
545 // prefix already loaded into the buffer) must not DOUBLE a
546 // window-sized allocation through the amortized policy. Per-block
547 // growth keeps the amortized `reserve`.
548 //
549 // `reserve_exact` takes ADDITIONAL capacity, so request only the
550 // shortfall past the bytes already buffered: the decode_all
551 // fallback loop re-enters `decode_blocks` once per strategy chunk,
552 // and re-requesting the full window each iteration would grow a
553 // window-sized buffer toward 2x window.
554 match self {
555 Self::Ring(s) => {
556 // The ring's growth rounds to the next power of two, capped at
557 // its limit; with the limit at the target for this one-shot
558 // reservation it lands exactly there, and later growth (a
559 // compressed block's own reservation) may then reach the
560 // frame's real limit, never double past it.
561 s.buffer.set_growth_limit(window_size);
562 let additional = window_size.saturating_sub(s.buffer.len());
563 s.buffer.reserve_exact(additional);
564 s.buffer.set_growth_limit(growth_limit);
565 }
566 Self::Flat(s) => {
567 let additional = window_size.saturating_sub(s.buffer.len());
568 s.buffer.reserve_exact(additional);
569 }
570 }
571 }
572
573 /// Last `n` bytes of the visible buffer as `(s1, s2)` (wrap-aware).
574 /// Routes through whichever backend the current scratch holds.
575 #[cfg(all(feature = "lsm", feature = "hash"))]
576 fn last_n_as_slices(&self, n: usize) -> (&[u8], &[u8]) {
577 match self {
578 Self::Ring(s) => s.buffer.last_n_as_slices(n),
579 Self::Flat(s) => s.buffer.last_n_as_slices(n),
580 }
581 }
582
583 fn buffer_drain(&mut self) -> Vec<u8> {
584 match self {
585 Self::Ring(s) => s.buffer.drain(),
586 Self::Flat(s) => s.buffer.drain(),
587 }
588 }
589
590 fn buffer_drain_to_window_size(&mut self) -> Option<Vec<u8>> {
591 match self {
592 Self::Ring(s) => s.buffer.drain_to_window_size(),
593 Self::Flat(s) => s.buffer.drain_to_window_size(),
594 }
595 }
596
597 fn buffer_drain_to_writer(&mut self, sink: impl Write) -> Result<usize, Error> {
598 match self {
599 Self::Ring(s) => s.buffer.drain_to_writer(sink),
600 Self::Flat(s) => s.buffer.drain_to_writer(sink),
601 }
602 }
603
604 fn buffer_drain_to_window_size_writer(&mut self, sink: impl Write) -> Result<usize, Error> {
605 match self {
606 Self::Ring(s) => s.buffer.drain_to_window_size_writer(sink),
607 Self::Flat(s) => s.buffer.drain_to_window_size_writer(sink),
608 }
609 }
610
611 fn buffer_can_drain(&self) -> usize {
612 match self {
613 Self::Ring(s) => s.buffer.can_drain(),
614 Self::Flat(s) => s.buffer.can_drain(),
615 }
616 }
617
618 fn buffer_can_drain_to_window_size(&self) -> Option<usize> {
619 match self {
620 Self::Ring(s) => s.buffer.can_drain_to_window_size(),
621 Self::Flat(s) => s.buffer.can_drain_to_window_size(),
622 }
623 }
624
625 fn buffer_read(&mut self, target: &mut [u8]) -> Result<usize, Error> {
626 match self {
627 Self::Ring(s) => s.buffer.read(target),
628 Self::Flat(s) => s.buffer.read(target),
629 }
630 }
631
632 /// [`Self::buffer_read`], also reporting the drainable bytes `target` had
633 /// no room for.
634 fn buffer_read_reporting_pending(
635 &mut self,
636 target: &mut [u8],
637 ) -> Result<(usize, usize), Error> {
638 match self {
639 Self::Ring(s) => s.buffer.read_reporting_pending(target),
640 Self::Flat(s) => s.buffer.read_reporting_pending(target),
641 }
642 }
643
644 fn buffer_read_all(&mut self, target: &mut [u8]) -> Result<usize, Error> {
645 match self {
646 Self::Ring(s) => s.buffer.read_all(target),
647 Self::Flat(s) => s.buffer.read_all(target),
648 }
649 }
650
651 /// Drop visible output beyond `window_size` without producing it,
652 /// keeping the most recent `window_size` bytes available to back
653 /// future match copies. Used by `decode_blocks_partial` to bound
654 /// memory while decoding the leading (skipped) blocks into the window.
655 #[cfg(feature = "lsm")]
656 fn buffer_drop_to_window_size(&mut self) -> usize {
657 match self {
658 Self::Ring(s) => s.buffer.drop_to_window_size(),
659 Self::Flat(s) => s.buffer.drop_to_window_size(),
660 }
661 }
662
663 /// Drop exactly `n` bytes from the front of the visible output without
664 /// producing them. Used by `decode_blocks_partial` to discard the
665 /// leading blocks' window-context bytes once the in-range blocks are
666 /// decoded (match resolution complete), leaving only the in-range output.
667 #[cfg(feature = "lsm")]
668 fn buffer_discard_front(&mut self, n: usize) {
669 match self {
670 Self::Ring(s) => s.buffer.discard_front(n),
671 Self::Flat(s) => s.buffer.discard_front(n),
672 }
673 }
674
675 /// Prime the match window with the caller's already-decompressed tail for
676 /// a resumed partial decode. Routes through whichever backend the current
677 /// scratch holds. See
678 /// [`DecodeBuffer::prime_window`](crate::decoding::decode_buffer::DecodeBuffer::prime_window).
679 #[cfg(feature = "lsm")]
680 fn prime_window(&mut self, prefix: &[u8], total_output: u64) {
681 match self {
682 Self::Ring(s) => s.buffer.prime_window(prefix, total_output),
683 Self::Flat(s) => s.buffer.prime_window(prefix, total_output),
684 }
685 }
686
687 /// Total decompressed bytes produced so far (the buffer's running output
688 /// counter, unaffected by window drops / drains). Used to stamp a captured
689 /// [`ResumeState`]'s `output_offset`.
690 #[cfg(feature = "lsm")]
691 fn total_output(&self) -> u64 {
692 match self {
693 Self::Ring(s) => s.buffer.total_output(),
694 Self::Flat(s) => s.buffer.total_output(),
695 }
696 }
697
698 /// Clone the cross-block entropy/repcode state (FSE + Huffman tables +
699 /// `offset_hist`) out of the live scratch for a [`ResumeState`] snapshot.
700 #[cfg(feature = "lsm")]
701 fn export_entropy(
702 &self,
703 dict: Option<&crate::decoding::dictionary::Dictionary>,
704 ) -> (
705 crate::decoding::scratch::FSEScratch,
706 crate::decoding::scratch::HuffmanScratch,
707 [u32; 3],
708 ) {
709 let (fse_src, huf_src, offset_hist) = match self {
710 Self::Ring(s) => (&s.fse, &s.huf, s.offset_hist),
711 Self::Flat(s) => (&s.fse, &s.huf, s.offset_hist),
712 };
713 // The live scratch may still be `Dict`-sourced; `reinit_from` /
714 // `reinit_resolved_from` resolve those axes through the borrow into a
715 // self-contained `Local` snapshot, so the dictionary is required here
716 // whenever the captured frame is dict-backed.
717 let mut fse = crate::decoding::scratch::FSEScratch::new();
718 fse.reinit_from(fse_src, dict);
719 let mut huf = crate::decoding::scratch::HuffmanScratch::new();
720 huf.reinit_resolved_from(huf_src, dict);
721 (fse, huf, offset_hist)
722 }
723
724 /// Install entropy/repcode state from a [`ResumeState`] into the live
725 /// scratch so a `Repeat_Mode` / treeless resume block resolves against the
726 /// same tables a contiguous decode would have carried over.
727 #[cfg(feature = "lsm")]
728 fn restore_entropy(&mut self, state: &ResumeState) {
729 // The `ResumeState` snapshot is a self-contained `Local` materialization
730 // (export resolved every Dict axis into local bytes and detached), so no
731 // dictionary borrow is needed to install it back.
732 match self {
733 Self::Ring(s) => {
734 s.fse.reinit_from(&state.fse, None);
735 s.huf.reinit_resolved_from(&state.huf, None);
736 s.offset_hist = state.offset_hist;
737 }
738 Self::Flat(s) => {
739 s.fse.reinit_from(&state.fse, None);
740 s.huf.reinit_resolved_from(&state.huf, None);
741 s.offset_hist = state.offset_hist;
742 }
743 }
744 }
745
746 /// XXH64 of the window-prime bytes for a [`ResumeState`]: the last
747 /// `min(window_size, buffer_len)` bytes of the current buffer, which at emit
748 /// time are exactly the match-window context the resume block will see.
749 /// Wrap-aware via `last_n_as_slices` — streaming XXH64 over the two slices
750 /// equals a single hash over the contiguous `window_prime` at resume.
751 #[cfg(all(feature = "lsm", feature = "hash"))]
752 fn window_tail_hash(&self, window_size: usize) -> u64 {
753 use core::hash::Hasher;
754 let n = core::cmp::min(window_size, self.buffer_len());
755 let (s1, s2) = self.last_n_as_slices(n);
756 let mut h = twox_hash::XxHash64::with_seed(0);
757 h.write(s1);
758 h.write(s2);
759 h.finish()
760 }
761
762 fn decode_block_content<R: Read>(
763 &mut self,
764 decoder: &mut BlockDecoder,
765 header: &crate::blocks::block::BlockHeader,
766 source: R,
767 dict: Option<&crate::decoding::dictionary::Dictionary>,
768 ) -> Result<u64, DecodeBlockContentError> {
769 match self {
770 Self::Ring(s) => decoder.decode_block_content(header, s, dict, source),
771 Self::Flat(s) => decoder.decode_block_content(header, s, dict, source),
772 }
773 }
774
775 #[cfg(feature = "hash")]
776 fn hash_finish(&self) -> u64 {
777 use core::hash::Hasher;
778 match self {
779 Self::Ring(s) => s.buffer.hash.finish(),
780 Self::Flat(s) => s.buffer.hash.finish(),
781 }
782 }
783
784 /// Forward the drain-time hash toggle to the inner `DecodeBuffer`
785 /// (streaming path). Called by the frame layer from the decoder's
786 /// `ContentChecksum` mode before each decode.
787 #[cfg(feature = "hash")]
788 fn set_compute_hash(&mut self, compute: bool) {
789 match self {
790 Self::Ring(s) => s.buffer.set_compute_hash(compute),
791 Self::Flat(s) => s.buffer.set_compute_hash(compute),
792 }
793 }
794}
795
796struct FrameDecoderState {
797 pub frame_header: frame::FrameHeader,
798 decoder_scratch: DecoderScratchKind,
799 frame_finished: bool,
800 block_counter: usize,
801 bytes_read_counter: u64,
802 check_sum: Option<u32>,
803 using_dict: Option<u32>,
804 /// The dictionary handle applied to this frame, owned for the frame's whole
805 /// decode so the block loop can hand the scratch a `&Dictionary` borrow at
806 /// every `Dict`-sourced table read. ONE refcount clone per dict-apply (not
807 /// per block, not per frame on the reuse path — the same handle stays held
808 /// across `init_with_dict_handle` -> `decode_blocks`); the decode loop
809 /// borrows from this field (disjoint from `decoder_scratch`) with zero
810 /// further clones. `None` on the no-dict path. Cleared by `reset`.
811 active_dict: Option<DictionaryHandle>,
812}
813
814pub enum BlockDecodingStrategy {
815 All,
816 UptoBlocks(usize),
817 UptoBytes(usize),
818}
819
820/// Outcome of [`FrameDecoder::decode_blocks_partial`]: the decompressed
821/// bytes of the requested inner-block range plus where (if anywhere)
822/// decoding stopped early.
823///
824/// Behind the `lsm` Cargo feature.
825#[cfg(feature = "lsm")]
826#[derive(Debug)]
827pub struct PartialDecode {
828 /// Decompressed bytes of the in-range blocks actually decoded, in
829 /// frame order, as one contiguous buffer. `data.len()` equals the sum
830 /// of the decompressed sizes of blocks `start_block .. start_block +
831 /// blocks_decoded`.
832 pub data: alloc::vec::Vec<u8>,
833 /// First block whose output is in [`data`](Self::data): the requested
834 /// `start_block` on a fresh decode, or [`ResumeState::block_index`] when
835 /// resuming (the caller-supplied `start_block` is ignored in resume mode).
836 pub start_block: u32,
837 /// Number of in-range blocks successfully decoded into
838 /// [`data`](Self::data).
839 pub blocks_decoded: u32,
840 /// `Some((block_index, error))` if decoding stopped on a failing block
841 /// before reaching `end_block` (a corrupt block inside the range, or a
842 /// leading block needed for window context). `None` if the requested
843 /// range decoded cleanly or the frame's last block was reached first.
844 ///
845 /// When the failing block is a leading context block
846 /// (`block_index < start_block`), the in-range window could not be
847 /// built so [`data`](Self::data) is empty and `blocks_decoded` is 0.
848 pub stopped_at: Option<(u32, FrameDecoderError)>,
849 /// `true` if the frame's last block was reached during this decode.
850 pub frame_finished: bool,
851 /// Cross-block carry-over state for resuming the next extent. Feed it back
852 /// (with the matching `window_prime`) via the `resume` argument of a
853 /// later [`FrameDecoder::decode_blocks_partial`] to continue from
854 /// [`ResumeState::block_index`] without re-decompressing the prefix.
855 ///
856 /// `None` in two cases: emission was not requested (`emit_resume = false`),
857 /// OR this decode reached the frame's last block ([`frame_finished`] is
858 /// `true`) — there is no following block to resume from, so no snapshot is
859 /// emitted even with `emit_resume = true`. Callers walking a frame
860 /// incrementally should therefore stop when `frame_finished` is set rather
861 /// than treat a `None` here as "emission disabled".
862 ///
863 /// [`frame_finished`]: Self::frame_finished
864 pub resume_state: Option<ResumeState>,
865}
866
867impl FrameDecoderState {
868 /// Window size to actually reserve for this frame's decode buffer.
869 /// A declared content size caps the useful window: matches can never
870 /// reference further back than the bytes that will ever exist, so an
871 /// encoder-declared window above the FCS (e.g. a level-preset window
872 /// on a smaller input) must not inflate the reservation. Every
873 /// `reserve_buffer` site routes through this so the cap is uniform
874 /// across `decode_all_impl`, `decode_blocks`, and the partial path.
875 fn useful_window_size(&self) -> usize {
876 let window_size = self.frame_header.window_size().unwrap_or(0);
877 if self.frame_header.fcs_declared() {
878 window_size.min(self.frame_header.frame_content_size()) as usize
879 } else {
880 window_size as usize
881 }
882 }
883
884 /// The most a frame decoding through the buffer holds, which its growth
885 /// stops at. A single-segment frame's buffer holds its whole content,
886 /// which is its window. A multi-segment frame's ring holds the
887 /// content-capped window plus room for the next block, since each
888 /// compressed block reserves a whole block before it decodes: without it
889 /// the first block past a full window grew the ring and copied the window
890 /// across, and a limit at the content left no block of room once the
891 /// window filled. Upstream sizes its stream buffer as window + block too
892 /// (`ZSTD_decodingBufferSize_min`); it can cap at the content because its
893 /// buffer is not a ring.
894 ///
895 /// A declared content size does NOT cap this. A frame can declare less than
896 /// its blocks go on to produce, and that is caught by the check against the
897 /// declaration once the bytes exist; a limit that stopped the ring short of
898 /// them would instead have the write run out of buffer, which the ring
899 /// asserts on rather than reports. Only the up-front reservation takes the
900 /// declaration ([`Self::decoding_buffer_size`]).
901 fn decoding_buffer_limit(&self) -> usize {
902 let useful_window = self.useful_window_size();
903 if self.frame_header.descriptor.single_segment_flag() {
904 return useful_window;
905 }
906 let window_size = self.frame_header.window_size().unwrap_or(0) as usize;
907 // No overflow: the window was checked against
908 // `MAXIMUM_ALLOWED_WINDOW_SIZE` when the header was taken.
909 useful_window + window_size.min(crate::common::MAX_BLOCK_SIZE as usize)
910 }
911
912 /// What to reserve up front: the limit, except for a multi-segment frame
913 /// whose declared content fits its window, which reserves just its
914 /// content. Only compressed blocks need the block of room past it, and
915 /// they reserve it themselves; the limit then caps that growth at one
916 /// block. Such frames are rare (encoders mark a frame that fits its window
917 /// single-segment), and a small Raw or RLE one should not pay a block.
918 ///
919 /// A frame that declares its size never reserves past the declaration: the
920 /// block of room is there for what a block still has to produce, and an
921 /// honest frame produces exactly what it promised. A 1 MiB window declaring
922 /// one byte more reserved a whole block of room for that byte. A frame that
923 /// goes on to exceed its declaration grows into the limit and is then caught
924 /// by the check against it.
925 fn decoding_buffer_size(&self) -> usize {
926 let window_size = self.frame_header.window_size().unwrap_or(0);
927 if !self.frame_header.fcs_declared() {
928 return self.decoding_buffer_limit();
929 }
930 let declared = self.frame_header.frame_content_size();
931 if declared <= window_size {
932 return self.useful_window_size();
933 }
934 self.decoding_buffer_limit()
935 .min(usize::try_from(declared).unwrap_or(usize::MAX))
936 }
937
938 /// Reserve this frame's decode buffer ([`Self::decoding_buffer_size`])
939 /// and cap its later growth ([`Self::decoding_buffer_limit`]).
940 fn reserve_decoding_buffer(&mut self) {
941 let target = self.decoding_buffer_size();
942 let growth_limit = self.decoding_buffer_limit();
943 // What the frame promised to produce, so the per-block reservation can
944 // ask for the smaller of a block and what is left of that promise.
945 let declared = self
946 .frame_header
947 .fcs_declared()
948 .then(|| self.frame_header.frame_content_size());
949 self.decoder_scratch.set_declared_content(declared);
950 self.decoder_scratch.reserve_buffer(target, growth_limit);
951 }
952
953 /// Construct a new frame decoder state, reading the frame header
954 /// from `source`. When `magicless` is `true`, the 4-byte magic
955 /// number prefix is NOT consumed (upstream zstd `ZSTD_f_zstd1_magicless`).
956 /// Crate-internal — reached only via `FrameDecoder::init` /
957 /// `FrameDecoder::init_with_dict_handle`. The decode buffer is
958 /// allocated lazily on BOTH backends (`new_ring` and `new_flat`):
959 /// direct-eligible frames pay zero buffer allocation, and the
960 /// non-direct fallback reserves `window_size` once in
961 /// `decode_all_impl` / `decode_blocks` via `reserve_buffer` before
962 /// any block write.
963 #[inline]
964 pub(crate) fn new_with_format(
965 source: impl Read,
966 magicless: bool,
967 ) -> Result<FrameDecoderState, FrameDecoderError> {
968 let (frame, header_size) = frame::read_frame_header_with_format(source, magicless)?;
969 Self::new_with_parsed_header(frame, header_size)
970 }
971
972 /// Build a fresh state from an already-parsed frame header (the non-parsing
973 /// tail of [`Self::new_with_format`]). Shared by the `Read` path and the
974 /// slice-direct path ([`FrameDecoder::reset_from_slice`]).
975 pub(crate) fn new_with_parsed_header(
976 frame: frame::FrameHeader,
977 header_size: u8,
978 ) -> Result<FrameDecoderState, FrameDecoderError> {
979 let window_size = frame.window_size()?;
980
981 if window_size > MAXIMUM_ALLOWED_WINDOW_SIZE {
982 return Err(FrameDecoderError::WindowSizeTooBig {
983 requested: window_size,
984 });
985 }
986
987 let decoder_scratch = if frame.descriptor.single_segment_flag() {
988 DecoderScratchKind::new_flat(window_size as usize)
989 } else {
990 DecoderScratchKind::new_ring(window_size as usize)
991 };
992 Ok(FrameDecoderState {
993 frame_header: frame,
994 frame_finished: false,
995 block_counter: 0,
996 decoder_scratch,
997 bytes_read_counter: u64::from(header_size),
998 check_sum: None,
999 using_dict: None,
1000 active_dict: None,
1001 })
1002 }
1003
1004 /// Reset this state for a new frame read from `source`, reusing
1005 /// existing allocations. When `magicless` is `true`, the frame
1006 /// header is read WITHOUT expecting a magic-number prefix
1007 /// (upstream zstd `ZSTD_f_zstd1_magicless`). Crate-internal — reached
1008 /// only via `FrameDecoder::reset`.
1009 ///
1010 /// `DecodeBuffer::reset` no longer reserves window_size for either
1011 /// backend — capacity decisions live one layer up. Both backends are
1012 /// lazy: direct-eligible frames pay zero backing-buffer allocation
1013 /// here (they write through `UserSliceBackend`), and the non-direct
1014 /// path is pre-reserved by `decode_all_impl` / `decode_blocks` via
1015 /// `DecoderScratchKind::reserve_buffer(window_size)` before any block
1016 /// write. A reused scratch whose new frame fits within prior capacity
1017 /// reuses it; a larger one grows on that same `reserve_buffer` call.
1018 #[inline]
1019 pub(crate) fn reset_with_format(
1020 &mut self,
1021 source: impl Read,
1022 magicless: bool,
1023 ) -> Result<(), FrameDecoderError> {
1024 let (frame_header, header_size) = frame::read_frame_header_with_format(source, magicless)?;
1025 self.reset_with_parsed_header(frame_header, header_size)
1026 }
1027
1028 /// Apply an already-parsed frame header to this state (the non-parsing tail
1029 /// of [`Self::reset_with_format`]). Shared by the `Read` path and the slice-direct
1030 /// path ([`FrameDecoder::reset_from_slice`]).
1031 #[inline]
1032 pub(crate) fn reset_with_parsed_header(
1033 &mut self,
1034 frame_header: frame::FrameHeader,
1035 header_size: u8,
1036 ) -> Result<(), FrameDecoderError> {
1037 let window_size = frame_header.window_size()?;
1038
1039 if window_size > MAXIMUM_ALLOWED_WINDOW_SIZE {
1040 return Err(FrameDecoderError::WindowSizeTooBig {
1041 requested: window_size,
1042 });
1043 }
1044
1045 self.decoder_scratch
1046 .reset(&frame_header, window_size as usize);
1047 self.frame_header = frame_header;
1048 self.frame_finished = false;
1049 self.block_counter = 0;
1050 self.bytes_read_counter = u64::from(header_size);
1051 self.check_sum = None;
1052 self.using_dict = None;
1053 // `active_dict` is intentionally NOT cleared here: it is only ever READ
1054 // while a scratch table source is `Dict`, which `init_from_dict` arms
1055 // on a dict frame and which a no-dict frame leaves `Local` (so a stale
1056 // held handle is never read). Keeping it lets the per-apply `ptr::eq`
1057 // reuse-check below skip the clone when the SAME dictionary is
1058 // re-applied frame-over-frame (the CoordiNode per-label-dict hot path)
1059 // — zero refcount churn on reuse, one clone only on a genuine swap.
1060 Ok(())
1061 }
1062
1063 /// Hold the dictionary handle for this frame's whole decode so the block
1064 /// loop can borrow `&Dictionary` at every `Dict`-sourced read. Clones the
1065 /// handle ONLY when it is a different dictionary than the one already held
1066 /// (`ptr::eq` on the `Arc`'s pointee) — so re-applying the SAME dictionary
1067 /// frame-over-frame (the reuse hot path) costs zero refcount churn.
1068 fn set_active_dict(&mut self, dict: &DictionaryHandle) {
1069 if self
1070 .active_dict
1071 .as_ref()
1072 .is_none_or(|held| !core::ptr::eq(held.as_dict(), dict.as_dict()))
1073 {
1074 self.active_dict = Some(dict.clone());
1075 }
1076 }
1077}
1078
1079impl Default for FrameDecoder {
1080 fn default() -> Self {
1081 Self::new()
1082 }
1083}
1084
1085impl FrameDecoder {
1086 /// This will create a new decoder without allocating anything yet.
1087 /// init()/reset() will allocate all needed buffers if it is the first time this decoder is used
1088 /// else they just reset these buffers with not further allocations
1089 pub fn new() -> FrameDecoder {
1090 FrameDecoder {
1091 state: None,
1092 #[cfg(test)]
1093 direct_frames: 0,
1094 owned_dicts: BTreeMap::new(),
1095 #[cfg(target_has_atomic = "ptr")]
1096 shared_dicts: BTreeMap::new(),
1097 #[cfg(not(target_has_atomic = "ptr"))]
1098 shared_dicts: (),
1099 magicless: false,
1100 content_checksum: ContentChecksum::EmitOnly,
1101 #[cfg(feature = "lsm")]
1102 expect_dict_id: None,
1103 #[cfg(feature = "lsm")]
1104 expect_window_descriptor: None,
1105 #[cfg(all(feature = "lsm", feature = "hash"))]
1106 per_block_checksums_enabled: false,
1107 #[cfg(all(feature = "lsm", feature = "hash"))]
1108 computed_block_checksums: alloc::vec::Vec::new(),
1109 kernel: crate::cpu_kernel::detect_cpu_kernel(),
1110 }
1111 }
1112
1113 /// Decode with a named CPU tier instead of the one this machine selected.
1114 ///
1115 /// Which tier runs is otherwise a property of the host, so a test on any
1116 /// one machine covers exactly one of them. The tiers are separate
1117 /// monomorphs with their own gates and their own argument plumbing, and
1118 /// a divergence between them is precisely what no single-host run can see.
1119 ///
1120 /// The caller is responsible for naming a tier the CPU can actually
1121 /// execute: the monoliths carry `target_feature`, and asking for one the
1122 /// machine lacks is undefined behaviour rather than an error.
1123 #[cfg(test)]
1124 // Its consumer is the per-tier agreement test, and the tiers worth
1125 // disagreeing are the x86 ones, so on other architectures nothing calls it.
1126 #[allow(dead_code)]
1127 pub(crate) fn decode_with_kernel_for_tests(&mut self, kernel: crate::cpu_kernel::CpuKernelTag) {
1128 self.kernel = kernel;
1129 }
1130
1131 /// Heap bytes currently held by the decoder's lazily-grown workspace:
1132 /// the decode-window buffer plus the per-block literal/content buffers
1133 /// and the entropy tables. Returns 0 before the first frame is initialised
1134 /// (no workspace allocated yet). The window allocation dominates and grows
1135 /// with the frame's window size; this is the value to track for decode-time
1136 /// memory pressure, mirroring the workspace term of upstream
1137 /// `ZSTD_sizeof_DCtx`. Shared dictionaries (ref-counted handles) are not
1138 /// counted, matching upstream excluding `refDDict` memory.
1139 pub fn workspace_size(&self) -> usize {
1140 self.state
1141 .as_ref()
1142 .map_or(0, |s| s.decoder_scratch.workspace_bytes())
1143 }
1144
1145 /// Select how the frame's optional content checksum is handled
1146 /// (compute, expose, verify, or skip). See [`ContentChecksum`].
1147 /// Default [`ContentChecksum::EmitOnly`]. Takes effect on the next
1148 /// decode; safe to call between frames on a reused decoder.
1149 pub fn set_content_checksum(&mut self, mode: ContentChecksum) {
1150 self.content_checksum = mode;
1151 }
1152
1153 /// Opt in to per-block XXH64 verification during decode.
1154 /// Default off; zero cost when disabled. Each block's decompressed
1155 /// bytes are XXH64-hashed (low 32 bits) and appended to
1156 /// [`Self::computed_block_checksums`] as the decode progresses.
1157 /// Callers compare the captured digests against externally-stored
1158 /// expected values (e.g. from a per-block sidecar in the
1159 /// containing application protocol).
1160 ///
1161 /// Behind `all(feature = "lsm", feature = "hash")` — the XXH64
1162 /// primitive lives behind the `hash` feature, so this method
1163 /// only compiles when both are enabled.
1164 #[cfg(all(feature = "lsm", feature = "hash"))]
1165 pub fn enable_per_block_checksums(&mut self) {
1166 self.per_block_checksums_enabled = true;
1167 }
1168
1169 /// Per-block XXH64 (low 32 bits) digests captured during the
1170 /// current frame's decode. Empty unless
1171 /// [`Self::enable_per_block_checksums`] was called before
1172 /// [`Self::decode_all`] / [`Self::reset`].
1173 ///
1174 /// Reset at the start of every new frame.
1175 ///
1176 /// Behind `all(feature = "lsm", feature = "hash")`.
1177 #[cfg(all(feature = "lsm", feature = "hash"))]
1178 pub fn computed_block_checksums(&self) -> &[u32] {
1179 &self.computed_block_checksums
1180 }
1181
1182 /// Pin the expected `Dictionary_ID` for the next frame.
1183 ///
1184 /// When `expected` is set, [`Self::init`] / [`Self::reset`]
1185 /// validate it against the parsed frame header BEFORE any
1186 /// block decode work runs. A mismatch returns
1187 /// [`crate::decoding::errors::FrameDecoderError::UnexpectedDictId`]
1188 /// before any block decode and before any output is produced.
1189 /// Scratch buffer allocation / reservation for the decode
1190 /// pipeline happens during frame-header parsing, which is
1191 /// already complete when this validation fires — the cost of
1192 /// scratch sizing is paid even on a mismatched header. The
1193 /// guarantee is "no block decode, no XXH64 init, no partial
1194 /// output", not "zero allocation".
1195 ///
1196 /// `Some(0)` is treated as "no dictionary expected": a frame
1197 /// whose header omits the optional `Dictionary_ID` field
1198 /// (flag value 0) passes the check; a frame that carries an
1199 /// explicit non-zero id fails.
1200 ///
1201 /// `None` (default) disables the check.
1202 ///
1203 /// Primary use case: post-AEAD-decrypt sanity check in
1204 /// wire-format consumers (e.g. lsm-tree's encrypted block
1205 /// format pins the `dict_id` baked into the AAD against the
1206 /// inner zstd frame's `dict_id` to defeat dict-substitution
1207 /// attacks).
1208 ///
1209 /// NOT a replacement for AEAD authentication. NOT the same
1210 /// semantic as upstream zstd `ZSTD_d_windowLogMax` (which is a
1211 /// ceiling-style limit, separate concern).
1212 #[cfg(feature = "lsm")]
1213 #[cfg_attr(docsrs, doc(cfg(feature = "lsm")))]
1214 pub fn expect_dict_id(&mut self, expected: Option<u32>) {
1215 self.expect_dict_id = expected;
1216 }
1217
1218 /// Pin the expected raw `Window_Descriptor` byte (RFC 8878
1219 /// §3.1.1.1.2 layout: `(exp << 3) | mantissa`) for the next
1220 /// frame.
1221 ///
1222 /// When `expected` is set, [`Self::init`] / [`Self::reset`]
1223 /// validate it against the parsed frame header BEFORE any
1224 /// block decode work runs. A mismatch returns
1225 /// [`crate::decoding::errors::FrameDecoderError::UnexpectedWindowDescriptor`].
1226 ///
1227 /// Single-segment frames omit the `Window_Descriptor` byte
1228 /// from the wire entirely. Setting an expectation while
1229 /// receiving a single-segment frame fails the check with
1230 /// `found: None` — there is no on-wire byte to match against,
1231 /// which is reported explicitly rather than silently passing.
1232 ///
1233 /// `None` (default) disables the check.
1234 ///
1235 /// Byte-exact equality, NOT a ceiling. Upstream zstd
1236 /// `ZSTD_d_windowLogMax` is a separate ceiling-style limit
1237 /// available through the C FFI surface; this method is for
1238 /// strict equality validation against a pinned expectation
1239 /// (e.g. lsm-tree's wire format pins the window descriptor
1240 /// from the AAD to defeat decompression-bomb-swap attacks).
1241 #[cfg(feature = "lsm")]
1242 #[cfg_attr(docsrs, doc(cfg(feature = "lsm")))]
1243 pub fn expect_window_descriptor(&mut self, expected: Option<u8>) {
1244 self.expect_window_descriptor = expected;
1245 }
1246
1247 /// Validate the just-parsed frame header against any pinned
1248 /// expectations set via [`Self::expect_dict_id`] /
1249 /// [`Self::expect_window_descriptor`].
1250 ///
1251 /// Returns the typed error variant on mismatch and leaves
1252 /// `self.state` in a re-resettable shape — a subsequent
1253 /// `reset()` will overwrite `frame_header` from the new source
1254 /// without needing intermediate cleanup.
1255 #[cfg(feature = "lsm")]
1256 fn validate_expectations(
1257 &self,
1258 frame_header: &frame::FrameHeader,
1259 ) -> Result<(), FrameDecoderError> {
1260 if let Some(expected) = self.expect_dict_id {
1261 let found = frame_header.dictionary_id();
1262 // `Some(0)` is the "no dictionary expected" sentinel —
1263 // matches a frame whose header omits the optional
1264 // dict_id field (which is reported as `None` by the
1265 // parser). All other values must match exactly.
1266 let matches = match (expected, found) {
1267 (0, None) => true,
1268 (e, Some(f)) => e == f,
1269 _ => false,
1270 };
1271 if !matches {
1272 return Err(FrameDecoderError::UnexpectedDictId {
1273 expected: Some(expected),
1274 found,
1275 });
1276 }
1277 }
1278 if let Some(expected) = self.expect_window_descriptor {
1279 let found = frame_header.window_descriptor();
1280 if found != Some(expected) {
1281 return Err(FrameDecoderError::UnexpectedWindowDescriptor { expected, found });
1282 }
1283 }
1284 Ok(())
1285 }
1286
1287 /// Enable or disable magicless frame format
1288 /// (`ZSTD_f_zstd1_magicless`). When set to `true`, subsequent
1289 /// [`Self::init`] / [`Self::reset`] calls expect the frame header to begin
1290 /// directly with the frame-header descriptor — no 4-byte magic
1291 /// number prefix. Default false. Must match the encoder's
1292 /// magicless setting; the format is unambiguous only when the
1293 /// caller knows it out-of-band.
1294 ///
1295 /// Note: magicless mode also disables skippable-frame detection.
1296 /// The `0x184D2A50..=0x184D2A5F` skippable-frame magic range is
1297 /// only recognised when the 4-byte magic prefix is consumed, so
1298 /// `decode_all` / `init` / `reset` will treat a skippable frame
1299 /// at the head of a magicless stream as a malformed frame header
1300 /// (bad descriptor / window-size error) instead of skipping it.
1301 /// Mixed-format streams that interleave skippable frames must be
1302 /// pre-split by the caller; `set_magicless(true)` is only safe
1303 /// when the entire stream is known to be magicless zstd frames.
1304 pub fn set_magicless(&mut self, magicless: bool) {
1305 self.magicless = magicless;
1306 }
1307
1308 #[cfg(target_has_atomic = "ptr")]
1309 fn shared_dict_exists(&self, dict_id: u32) -> bool {
1310 self.shared_dicts.contains_key(&dict_id)
1311 }
1312
1313 #[cfg(not(target_has_atomic = "ptr"))]
1314 fn shared_dict_exists(&self, _dict_id: u32) -> bool {
1315 false
1316 }
1317
1318 fn validate_registered_dictionary(dict: &Dictionary) -> Result<(), FrameDecoderError> {
1319 use crate::decoding::errors::DictionaryDecodeError as dict_err;
1320
1321 // Registration keys the dictionary by its ID, so a zero one has no
1322 // slot to occupy and no frame could ever select it.
1323 if dict.id == 0 {
1324 return Err(FrameDecoderError::from(dict_err::ZeroDictionaryId));
1325 }
1326 Self::validate_dictionary_content(dict)
1327 }
1328
1329 /// Checks a dictionary can be used at all, whatever route it arrived by.
1330 ///
1331 /// Separate from [`Self::validate_registered_dictionary`] because a
1332 /// dictionary supplied explicitly needs no ID: a raw-content dictionary
1333 /// has no header to carry one, and the frames built with it record none.
1334 fn validate_dictionary_content(dict: &Dictionary) -> Result<(), FrameDecoderError> {
1335 use crate::decoding::errors::DictionaryDecodeError as dict_err;
1336
1337 if let Some(index) = dict.offset_hist.iter().position(|&rep| rep == 0) {
1338 return Err(FrameDecoderError::from(
1339 dict_err::ZeroRepeatOffsetInDictionary { index: index as u8 },
1340 ));
1341 }
1342 Ok(())
1343 }
1344
1345 /// init() will allocate all needed buffers if it is the first time this decoder is used
1346 /// else they just reset these buffers with not further allocations
1347 ///
1348 /// Note that all bytes currently in the decodebuffer from any previous frame will be lost. Collect them with collect()/collect_to_writer()
1349 ///
1350 /// equivalent to reset()
1351 #[inline]
1352 pub fn init(&mut self, source: impl Read) -> Result<(), FrameDecoderError> {
1353 self.reset(source)
1354 }
1355
1356 /// Initialize the decoder for a new frame using a pre-parsed dictionary handle.
1357 ///
1358 /// If the frame header has a dictionary ID, this validates it against
1359 /// `dict.id()` and returns [`FrameDecoderError::DictIdMismatch`] on mismatch.
1360 ///
1361 /// If the header omits the optional dictionary ID, this still applies the
1362 /// provided dictionary handle.
1363 ///
1364 /// # Warning
1365 ///
1366 /// This method always applies `dict` unless the frame header contains a
1367 /// non-matching dictionary ID. Callers must only use this API when they
1368 /// already know the frame was encoded with the provided dictionary, even if
1369 /// the frame header omits the dictionary ID or encodes an explicit
1370 /// dictionary ID of `0`.
1371 ///
1372 /// Passing a dictionary for a frame that was not encoded with it can
1373 /// silently corrupt the decoded output.
1374 pub fn init_with_dict_handle(
1375 &mut self,
1376 source: impl Read,
1377 dict: &DictionaryHandle,
1378 ) -> Result<(), FrameDecoderError> {
1379 self.reset_with_dict_handle(source, dict)
1380 }
1381
1382 /// reset() will allocate all needed buffers if it is the first time this decoder is used
1383 /// else they just reset these buffers with not further allocations
1384 ///
1385 /// Note that all bytes currently in the decodebuffer from any previous frame will be lost. Collect them with collect()/collect_to_writer()
1386 ///
1387 /// equivalent to init()
1388 #[inline]
1389 pub fn reset(&mut self, source: impl Read) -> Result<(), FrameDecoderError> {
1390 use FrameDecoderError as err;
1391 // Fresh frame → start with an empty per-block checksum vec so
1392 // the values for the next frame don't carry over from the
1393 // previous one.
1394 #[cfg(all(feature = "lsm", feature = "hash"))]
1395 self.computed_block_checksums.clear();
1396 let magicless = self.magicless;
1397 let dict_id = match &mut self.state {
1398 Some(s) => {
1399 s.reset_with_format(source, magicless)?;
1400 s.frame_header.dictionary_id()
1401 }
1402 None => {
1403 self.state = Some(FrameDecoderState::new_with_format(source, magicless)?);
1404 self.state
1405 .as_ref()
1406 .and_then(|state| state.frame_header.dictionary_id())
1407 }
1408 };
1409 // Validate any pinned expectations BEFORE block decode work
1410 // runs. Catches dict_id substitution / window-descriptor
1411 // tampering on inputs already authenticated by an outer
1412 // layer (e.g. AEAD). Returning here leaves `self.state` in
1413 // a re-resettable shape — next `reset()` re-parses the
1414 // frame header without intermediate cleanup.
1415 #[cfg(feature = "lsm")]
1416 if let Some(state) = self.state.as_ref() {
1417 self.validate_expectations(&state.frame_header)?;
1418 }
1419 if let Some(dict_id) = dict_id {
1420 let state = self.state.as_mut().expect("state initialized");
1421 let owned_dicts = &self.owned_dicts;
1422 #[cfg(target_has_atomic = "ptr")]
1423 let shared_dicts = &self.shared_dicts;
1424 let dict = owned_dicts
1425 .get(&dict_id)
1426 .or_else(|| {
1427 #[cfg(target_has_atomic = "ptr")]
1428 {
1429 shared_dicts.get(&dict_id)
1430 }
1431 #[cfg(not(target_has_atomic = "ptr"))]
1432 {
1433 None
1434 }
1435 })
1436 .ok_or(err::DictNotProvided { dict_id })?;
1437 state.decoder_scratch.init_from_dict(dict);
1438 state.set_active_dict(dict);
1439 state.using_dict = Some(dict_id);
1440 }
1441 Ok(())
1442 }
1443
1444 /// Slice-direct equivalent of [`reset`](Self::reset) for the in-memory
1445 /// decode path: parses the frame header straight out of `*input` via
1446 /// [`frame::read_frame_header_from_slice`] (no `Read`-trait `read_exact`
1447 /// per field) and advances `*input` past it, then applies it through the
1448 /// shared parsed-header path. Behaviour — including skippable-frame and
1449 /// truncation errors, dictionary-id resolution, and pinned-expectation
1450 /// validation — is identical to `reset`; only the header read avoids the
1451 /// `io::impls` dispatch.
1452 pub(crate) fn reset_from_slice(&mut self, input: &mut &[u8]) -> Result<(), FrameDecoderError> {
1453 use FrameDecoderError as err;
1454 #[cfg(all(feature = "lsm", feature = "hash"))]
1455 self.computed_block_checksums.clear();
1456 let magicless = self.magicless;
1457 let (frame_header, header_size) = frame::read_frame_header_from_slice(input, magicless)?;
1458 let dict_id = match &mut self.state {
1459 Some(s) => {
1460 s.reset_with_parsed_header(frame_header, header_size)?;
1461 s.frame_header.dictionary_id()
1462 }
1463 None => {
1464 self.state = Some(FrameDecoderState::new_with_parsed_header(
1465 frame_header,
1466 header_size,
1467 )?);
1468 self.state
1469 .as_ref()
1470 .and_then(|state| state.frame_header.dictionary_id())
1471 }
1472 };
1473 #[cfg(feature = "lsm")]
1474 if let Some(state) = self.state.as_ref() {
1475 self.validate_expectations(&state.frame_header)?;
1476 }
1477 if let Some(dict_id) = dict_id {
1478 let state = self.state.as_mut().expect("state initialized");
1479 let owned_dicts = &self.owned_dicts;
1480 #[cfg(target_has_atomic = "ptr")]
1481 let shared_dicts = &self.shared_dicts;
1482 let dict = owned_dicts
1483 .get(&dict_id)
1484 .or_else(|| {
1485 #[cfg(target_has_atomic = "ptr")]
1486 {
1487 shared_dicts.get(&dict_id)
1488 }
1489 #[cfg(not(target_has_atomic = "ptr"))]
1490 {
1491 None
1492 }
1493 })
1494 .ok_or(err::DictNotProvided { dict_id })?;
1495 state.decoder_scratch.init_from_dict(dict);
1496 state.set_active_dict(dict);
1497 state.using_dict = Some(dict_id);
1498 }
1499 Ok(())
1500 }
1501
1502 /// Reset this decoder for a new frame using a pre-parsed dictionary handle.
1503 ///
1504 /// If the frame header has a dictionary ID, this validates it against
1505 /// `dict.id()` and returns [`FrameDecoderError::DictIdMismatch`] on mismatch.
1506 ///
1507 /// If the header omits the optional dictionary ID, this still applies the
1508 /// provided dictionary handle.
1509 ///
1510 /// # Warning
1511 ///
1512 /// This method always applies `dict` unless the frame header contains a
1513 /// non-matching dictionary ID. Callers must only use this API when they
1514 /// already know the frame was encoded with the provided dictionary, even if
1515 /// the frame header omits the dictionary ID or encodes an explicit
1516 /// dictionary ID of `0`.
1517 ///
1518 /// Passing a dictionary for a frame that was not encoded with it can
1519 /// silently corrupt the decoded output.
1520 pub fn reset_with_dict_handle(
1521 &mut self,
1522 source: impl Read,
1523 dict: &DictionaryHandle,
1524 ) -> Result<(), FrameDecoderError> {
1525 self.reset_frame_for_dict(source, dict)?;
1526 self.state
1527 .as_mut()
1528 .expect("state populated by reset_frame_for_dict")
1529 .set_active_dict(dict);
1530 Ok(())
1531 }
1532
1533 /// [`reset_with_dict_handle`](Self::reset_with_dict_handle) with the
1534 /// dictionary the decode state already holds, for the frames that follow
1535 /// a forced-dictionary frame in one stream. The handle is moved out and
1536 /// back rather than passed in, so no reference count is touched.
1537 pub(crate) fn reset_with_active_dict(
1538 &mut self,
1539 source: impl Read,
1540 ) -> Result<(), FrameDecoderError> {
1541 let dict = self
1542 .state
1543 .as_mut()
1544 .and_then(|state| state.active_dict.take())
1545 .expect("a forced-dictionary stream's decoder holds its dictionary");
1546 let reset = self.reset_frame_for_dict(source, &dict);
1547 // Back in place whatever the outcome, so a failed header leaves the
1548 // decoder holding its dictionary as before.
1549 self.state
1550 .as_mut()
1551 .expect("the state the dictionary was taken from")
1552 .active_dict = Some(dict);
1553 reset
1554 }
1555
1556 /// Everything [`reset_with_dict_handle`](Self::reset_with_dict_handle)
1557 /// does except storing the handle: parse the header, check the ID, and
1558 /// point the entropy tables at `dict`.
1559 fn reset_frame_for_dict(
1560 &mut self,
1561 source: impl Read,
1562 dict: &DictionaryHandle,
1563 ) -> Result<(), FrameDecoderError> {
1564 use FrameDecoderError as err;
1565 // Fresh frame → drop the previous frame's per-block checksum
1566 // digests so the next decode starts with an empty vec.
1567 // Mirrors the same clear in `reset()`; reset_with_dict_handle
1568 // is a parallel entry point so it needs its own call.
1569 #[cfg(all(feature = "lsm", feature = "hash"))]
1570 self.computed_block_checksums.clear();
1571 Self::validate_dictionary_content(dict.as_dict())?;
1572 let magicless = self.magicless;
1573 // Scope the &mut borrow of `self.state` to the header parse
1574 // alone, so the subsequent `validate_expectations(&self, ...)`
1575 // call below can take a fresh shared borrow of self without
1576 // tripping the borrow checker.
1577 match &mut self.state {
1578 Some(s) => s.reset_with_format(source, magicless)?,
1579 None => {
1580 self.state = Some(FrameDecoderState::new_with_format(source, magicless)?);
1581 }
1582 }
1583 // Single source of truth: route through the same
1584 // `validate_expectations` used by `reset()`. Routing through
1585 // the helper keeps the two code paths from drifting (e.g.,
1586 // if expect-semantics or error wiring changes later).
1587 #[cfg(feature = "lsm")]
1588 {
1589 let header = &self
1590 .state
1591 .as_ref()
1592 .expect("state populated by reset_with_format/new_with_format")
1593 .frame_header;
1594 self.validate_expectations(header)?;
1595 }
1596 let state = self
1597 .state
1598 .as_mut()
1599 .expect("state populated by reset_with_format/new_with_format");
1600 if let Some(dict_id) = state.frame_header.dictionary_id()
1601 && dict_id != dict.id()
1602 {
1603 return Err(err::DictIdMismatch {
1604 expected: dict_id,
1605 provided: dict.id(),
1606 });
1607 }
1608 state.decoder_scratch.init_from_dict(dict);
1609 state.using_dict = Some(dict.id());
1610 Ok(())
1611 }
1612
1613 /// Slice-direct equivalent of [`reset_with_dict_handle`](Self::reset_with_dict_handle)
1614 /// for the in-memory decode path: parses the frame header straight out of
1615 /// `*input` via [`frame::read_frame_header_from_slice`] (no `Read`-trait
1616 /// `read_exact` per field) and applies it through the shared parsed-header
1617 /// path, then attaches `dict`. Behaviour — dictionary-id mismatch, pinned
1618 /// expectations, scratch init — is identical to `reset_with_dict_handle`;
1619 /// only the header read avoids the `io::impls` dispatch.
1620 pub(crate) fn reset_from_slice_with_dict_handle(
1621 &mut self,
1622 input: &mut &[u8],
1623 dict: &DictionaryHandle,
1624 ) -> Result<(), FrameDecoderError> {
1625 use FrameDecoderError as err;
1626 #[cfg(all(feature = "lsm", feature = "hash"))]
1627 self.computed_block_checksums.clear();
1628 Self::validate_dictionary_content(dict.as_dict())?;
1629 let magicless = self.magicless;
1630 let (frame_header, header_size) = frame::read_frame_header_from_slice(input, magicless)?;
1631 match &mut self.state {
1632 Some(s) => s.reset_with_parsed_header(frame_header, header_size)?,
1633 None => {
1634 self.state = Some(FrameDecoderState::new_with_parsed_header(
1635 frame_header,
1636 header_size,
1637 )?);
1638 }
1639 }
1640 #[cfg(feature = "lsm")]
1641 {
1642 let header = &self
1643 .state
1644 .as_ref()
1645 .expect("state populated by reset_with_parsed_header/new_with_parsed_header")
1646 .frame_header;
1647 self.validate_expectations(header)?;
1648 }
1649 let state = self
1650 .state
1651 .as_mut()
1652 .expect("state populated by reset_with_parsed_header/new_with_parsed_header");
1653 if let Some(dict_id) = state.frame_header.dictionary_id()
1654 && dict_id != dict.id()
1655 {
1656 return Err(err::DictIdMismatch {
1657 expected: dict_id,
1658 provided: dict.id(),
1659 });
1660 }
1661 state.decoder_scratch.init_from_dict(dict);
1662 state.set_active_dict(dict);
1663 state.using_dict = Some(dict.id());
1664 Ok(())
1665 }
1666
1667 /// Add a dictionary that can be selected dynamically by frame dictionary ID.
1668 ///
1669 /// Returns [`FrameDecoderError::DictAlreadyRegistered`] if the ID is already
1670 /// registered (either as owned or shared).
1671 pub fn add_dict(&mut self, dict: Dictionary) -> Result<(), FrameDecoderError> {
1672 Self::validate_registered_dictionary(&dict)?;
1673 let dict_id = dict.id;
1674 if self.owned_dicts.contains_key(&dict_id) || self.shared_dict_exists(dict_id) {
1675 return Err(FrameDecoderError::DictAlreadyRegistered { dict_id });
1676 }
1677 self.owned_dicts
1678 .insert(dict_id, DictionaryHandle::from_dictionary(dict));
1679 Ok(())
1680 }
1681
1682 /// Parse and add a serialized dictionary blob.
1683 pub fn add_dict_from_bytes(&mut self, raw_dictionary: &[u8]) -> Result<(), FrameDecoderError> {
1684 let dict = Dictionary::decode_dict(raw_dictionary)?;
1685 self.add_dict(dict)
1686 }
1687
1688 /// Add a pre-parsed dictionary handle for reuse across decoders.
1689 ///
1690 /// This API is available on targets with pointer-width atomics
1691 /// (`target_has_atomic = "ptr"`).
1692 ///
1693 /// Returns [`FrameDecoderError::DictAlreadyRegistered`] if the ID is already
1694 /// registered (either as owned or shared).
1695 #[cfg(target_has_atomic = "ptr")]
1696 pub fn add_dict_handle(&mut self, dict: DictionaryHandle) -> Result<(), FrameDecoderError> {
1697 Self::validate_registered_dictionary(dict.as_dict())?;
1698 let dict_id = dict.id();
1699 if self.owned_dicts.contains_key(&dict_id) || self.shared_dicts.contains_key(&dict_id) {
1700 return Err(FrameDecoderError::DictAlreadyRegistered { dict_id });
1701 }
1702 self.shared_dicts.insert(dict_id, dict);
1703 Ok(())
1704 }
1705
1706 pub fn force_dict(&mut self, dict_id: u32) -> Result<(), FrameDecoderError> {
1707 use FrameDecoderError as err;
1708 let state = self.state.as_mut().ok_or(err::NotYetInitialized)?;
1709 let owned_dicts = &self.owned_dicts;
1710 #[cfg(target_has_atomic = "ptr")]
1711 let shared_dicts = &self.shared_dicts;
1712
1713 let dict = owned_dicts
1714 .get(&dict_id)
1715 .or_else(|| {
1716 #[cfg(target_has_atomic = "ptr")]
1717 {
1718 shared_dicts.get(&dict_id)
1719 }
1720 #[cfg(not(target_has_atomic = "ptr"))]
1721 {
1722 None
1723 }
1724 })
1725 .ok_or(err::DictNotProvided { dict_id })?;
1726 state.decoder_scratch.init_from_dict(dict);
1727 state.set_active_dict(dict);
1728 state.using_dict = Some(dict_id);
1729
1730 Ok(())
1731 }
1732
1733 /// Returns how many bytes the frame contains after decompression
1734 pub fn content_size(&self) -> u64 {
1735 match &self.state {
1736 None => 0,
1737 Some(s) => s.frame_header.frame_content_size(),
1738 }
1739 }
1740
1741 /// Returns the checksum that was read from the data. Only available after all bytes have been read. It is the last 4 bytes of a zstd-frame
1742 pub fn get_checksum_from_data(&self) -> Option<u32> {
1743 let state = self.state.as_ref()?;
1744
1745 state.check_sum
1746 }
1747
1748 /// Returns the checksum that was calculated while decoding.
1749 /// Only a sensible value after all decoded bytes have been collected/read from the FrameDecoder.
1750 /// Returns `None` when the frame header has `content_checksum_flag = 0`:
1751 /// no hash is computed for such frames (the post-decode XXH64 pass was a
1752 /// 63 % decode-wall hotspot on flag-off frames; skipping it when the
1753 /// frame format declares no trailing digest avoids that wasted work).
1754 #[cfg(feature = "hash")]
1755 pub fn get_calculated_checksum(&self) -> Option<u32> {
1756 let state = self.state.as_ref()?;
1757 // `ContentChecksum::None` skips the XXH64 pass entirely, so there is
1758 // no calculated digest to report.
1759 if self.content_checksum == ContentChecksum::None {
1760 return None;
1761 }
1762 if !state.frame_header.descriptor.content_checksum_flag() {
1763 return None;
1764 }
1765 let cksum_64bit = state.decoder_scratch.hash_finish();
1766 //truncate to lower 32bit because reasons...
1767 Some(cksum_64bit as u32)
1768 }
1769
1770 /// Compare the frame's stored content checksum against the digest the
1771 /// decoder computed, returning [`FrameDecoderError::ChecksumMismatch`] on
1772 /// disagreement. No-op unless the mode is [`ContentChecksum::Verify`] and
1773 /// the frame carries a trailing checksum.
1774 ///
1775 /// [`decode_all`](Self::decode_all) and the streaming reader call this
1776 /// automatically. Callers driving [`decode_blocks`](Self::decode_blocks)
1777 /// directly invoke it themselves once per frame, after the frame is fully
1778 /// decoded AND fully drained (e.g. via [`collect`](Self::collect)), so both
1779 /// the stored value and the running digest are final.
1780 #[cfg(feature = "hash")]
1781 pub fn verify_content_checksum(&self) -> Result<(), FrameDecoderError> {
1782 if self.content_checksum != ContentChecksum::Verify {
1783 return Ok(());
1784 }
1785 let Some(state) = self.state.as_ref() else {
1786 return Ok(());
1787 };
1788 if !state.frame_header.descriptor.content_checksum_flag() {
1789 return Ok(());
1790 }
1791 let Some(expected) = state.check_sum else {
1792 return Ok(());
1793 };
1794 let calculated = state.decoder_scratch.hash_finish() as u32;
1795 if expected != calculated {
1796 return Err(FrameDecoderError::ChecksumMismatch {
1797 expected,
1798 calculated,
1799 });
1800 }
1801 Ok(())
1802 }
1803
1804 /// Counter for how many bytes have been consumed while decoding the frame
1805 pub fn bytes_read_from_source(&self) -> u64 {
1806 let state = match &self.state {
1807 None => return 0,
1808 Some(s) => s,
1809 };
1810 state.bytes_read_counter
1811 }
1812
1813 /// Test-only: number of frames decoded through the single-copy direct
1814 /// path (`run_direct_decode`). Lets cross-module tests assert that a
1815 /// given decode took the decode-in-place path rather than the ring drain.
1816 #[cfg(test)]
1817 pub(crate) fn direct_frames(&self) -> u64 {
1818 self.direct_frames
1819 }
1820
1821 /// Test-only: whether the decode state currently holds an owning dictionary
1822 /// handle (`active_dict`). Every path that arms `Dict`-sourced scratch tables
1823 /// must also install this handle, or a later dict-table read resolves `None`.
1824 #[cfg(test)]
1825 pub(crate) fn active_dict_installed(&self) -> bool {
1826 self.state.as_ref().is_some_and(|s| s.active_dict.is_some())
1827 }
1828
1829 /// Whether the current frames last block has been decoded yet
1830 /// If this returns true you can call the drain* functions to get all content
1831 /// (the read() function will drain automatically if this returns true)
1832 pub fn is_finished(&self) -> bool {
1833 let state = match &self.state {
1834 None => return true,
1835 Some(s) => s,
1836 };
1837 if state.frame_header.descriptor.content_checksum_flag() {
1838 state.frame_finished && state.check_sum.is_some()
1839 } else {
1840 state.frame_finished
1841 }
1842 }
1843
1844 /// Counter for how many blocks have already been decoded
1845 pub fn blocks_decoded(&self) -> usize {
1846 let state = match &self.state {
1847 None => return 0,
1848 Some(s) => s,
1849 };
1850 state.block_counter
1851 }
1852
1853 /// Decodes blocks from a reader. It requires that the framedecoder has been initialized first.
1854 /// The Strategy influences how many blocks will be decoded before the function returns
1855 /// This is important if you want to manage memory consumption carefully. If you don't care
1856 /// about that you can just choose the strategy "All" and have all blocks of the frame decoded into the buffer
1857 pub fn decode_blocks(
1858 &mut self,
1859 mut source: impl Read,
1860 strat: BlockDecodingStrategy,
1861 ) -> Result<bool, FrameDecoderError> {
1862 use FrameDecoderError as err;
1863 // Apply the content-checksum mode to the streaming drain hash before
1864 // any block decodes into the ring. Hash only when a digest is both
1865 // wanted (mode != None) AND present in the frame (content_checksum_flag
1866 // set) — a flag-off frame has nothing to verify or expose, so hashing
1867 // it is wasted work. Mirrors the direct path and get_calculated_checksum.
1868 #[cfg(feature = "hash")]
1869 let checksum_mode = self.content_checksum;
1870 let state = self.state.as_mut().ok_or(err::NotYetInitialized)?;
1871 #[cfg(feature = "hash")]
1872 {
1873 let compute_hash = checksum_mode != ContentChecksum::None
1874 && state.frame_header.descriptor.content_checksum_flag();
1875 state.decoder_scratch.set_compute_hash(compute_hash);
1876 }
1877
1878 // Streaming entry point: pre-reserve the backing buffer to
1879 // the FCS-capped window plus a block so multi-block frames don't pay
1880 // repeated `reserve_amortized` grow steps (128 KiB → 256 KiB → ... →
1881 // window) as blocks accumulate, nor a copy of the window when it
1882 // fills. `decode_all` does the same up front in `decode_all_impl`;
1883 // this mirrors it for callers driving `decode_blocks` directly.
1884 // Idempotent — the backend's `reserve` early-returns when capacity
1885 // is already sufficient.
1886 state.reserve_decoding_buffer();
1887
1888 let mut block_dec = decoding::block_decoder::with_kernel(self.kernel);
1889
1890 let buffer_size_before = state.decoder_scratch.buffer_len();
1891 let block_counter_before = state.block_counter;
1892 loop {
1893 vprintln!("################");
1894 vprintln!("Next Block: {}", state.block_counter);
1895 vprintln!("################");
1896 // Capture the failing-block coordinates BEFORE the header read so
1897 // the error carries where it happened: `bytes_read_counter` is the
1898 // frame-absolute offset of this block's header (not yet advanced),
1899 // `block_counter` its 0-based index. Used by both the header- and
1900 // body-error builders below (block-precise recovery under `lsm`).
1901 let block_index = state.block_counter as u32;
1902 let block_frame_offset = state.bytes_read_counter as u32;
1903 let (block_header, block_header_size) =
1904 block_dec.read_block_header(&mut source).map_err(|source| {
1905 block_header_decode_error(source, block_index, block_frame_offset)
1906 })?;
1907 state.bytes_read_counter += u64::from(block_header_size);
1908
1909 vprintln!();
1910 vprintln!(
1911 "Found {} block with size: {}, which will be of size: {}",
1912 block_header.block_type,
1913 block_header.content_size,
1914 block_header.decompressed_size
1915 );
1916
1917 #[cfg(all(feature = "lsm", feature = "hash"))]
1918 let len_before_block: Option<usize> = if self.per_block_checksums_enabled {
1919 Some(state.decoder_scratch.buffer_len())
1920 } else {
1921 None
1922 };
1923 // Only expose the held dictionary while THIS frame is dict-backed
1924 // (`using_dict` is set per dict-apply, cleared on reset). A reused
1925 // decoder keeps `active_dict` across a no-dict frame for the
1926 // `ptr::eq` reuse-skip, so it must be gated here or a stray
1927 // out-of-window offset on a dictless frame would resolve against the
1928 // stale dictionary content instead of erroring.
1929 let dict_ref = if state.using_dict.is_some() {
1930 state.active_dict.as_ref().map(|h| h.as_dict())
1931 } else {
1932 None
1933 };
1934 let bytes_read_in_block_body = state
1935 .decoder_scratch
1936 .decode_block_content(&mut block_dec, &block_header, &mut source, dict_ref)
1937 .map_err(|source| {
1938 block_body_decode_error(
1939 source,
1940 block_index,
1941 block_frame_offset,
1942 &block_header,
1943 block_header_size,
1944 )
1945 })?;
1946 state.bytes_read_counter += bytes_read_in_block_body;
1947
1948 // Per-block XXH64 (low 32 bits) of the just-decompressed
1949 // bytes. Hashed from `last_n_as_slices` so RingBuffer wrap
1950 // is handled in-place, no extra copy.
1951 #[cfg(all(feature = "lsm", feature = "hash"))]
1952 if let Some(len_before_block) = len_before_block {
1953 let added = state.decoder_scratch.buffer_len() - len_before_block;
1954 let (s1, s2) = state.decoder_scratch.last_n_as_slices(added);
1955 let mut h = twox_hash::XxHash64::with_seed(0);
1956 use core::hash::Hasher;
1957 h.write(s1);
1958 h.write(s2);
1959 self.computed_block_checksums.push(h.finish() as u32);
1960 }
1961
1962 state.block_counter += 1;
1963
1964 vprintln!("Output: {}", state.decoder_scratch.buffer_len());
1965
1966 if block_header.last_block {
1967 state.frame_finished = true;
1968 if state.frame_header.descriptor.content_checksum_flag() {
1969 let mut chksum = [0u8; 4];
1970 source
1971 .read_exact(&mut chksum)
1972 .map_err(err::FailedToReadChecksum)?;
1973 state.bytes_read_counter += 4;
1974 let chksum = u32::from_le_bytes(chksum);
1975 state.check_sum = Some(chksum);
1976 }
1977 break;
1978 }
1979
1980 match strat {
1981 BlockDecodingStrategy::All => { /* keep going */ }
1982 BlockDecodingStrategy::UptoBlocks(n) => {
1983 if state.block_counter - block_counter_before >= n {
1984 break;
1985 }
1986 }
1987 BlockDecodingStrategy::UptoBytes(n) => {
1988 if state.decoder_scratch.buffer_len() - buffer_size_before >= n {
1989 break;
1990 }
1991 }
1992 }
1993 }
1994
1995 Ok(state.frame_finished)
1996 }
1997
1998 /// Decode the inner blocks `[start_block, end_block)` of the current
1999 /// frame and return their decompressed bytes as one contiguous buffer.
2000 ///
2001 /// Serves two consumer needs with one call:
2002 ///
2003 /// - **Range-query performance:** decode only the inner zstd blocks that
2004 /// cover a key range instead of the whole frame. Blocks before
2005 /// `start_block` are decoded into the window (zstd blocks share one
2006 /// window, so a leading block's bytes may be the match source for an
2007 /// in-range block and cannot simply be skipped) but their output is not
2008 /// returned; blocks at or after `end_block` are not decoded at all,
2009 /// which is the trailing-block work saving. Map a decompressed byte
2010 /// offset to a block index with
2011 /// [`FrameEmitInfo::decompressed_byte_range`].
2012 /// - **Best-effort recovery:** if a block decode fails, decoding stops,
2013 /// the clean prefix of in-range output is preserved in
2014 /// [`PartialDecode::data`], and the failure is reported via
2015 /// [`PartialDecode::stopped_at`]. Passing `(0, u32::MAX)` decodes the
2016 /// whole frame, stopping at the first corrupt block (pure recovery).
2017 ///
2018 /// `end_block` is exclusive; pass `u32::MAX` to decode to the end of the
2019 /// frame. Call on a freshly [`reset`](Self::reset) decoder (it decodes
2020 /// from the frame's first block).
2021 ///
2022 /// # Resume (cold incremental / top-up)
2023 ///
2024 /// A plain call drains its in-range output from the match window on return,
2025 /// so two consecutive calls cannot resume one another and growing a decoded
2026 /// extent would mean re-decoding the covering prefix from block 0
2027 /// (`O(extent)` per growth, `O(N²)` for a forward walk). The `resume` /
2028 /// `emit_resume` arguments make a symmetric one-call grow-loop possible:
2029 ///
2030 /// - `emit_resume = true` captures the cross-block carry-over state (entropy
2031 /// tables + repcode history + the next block index / output offset) into
2032 /// [`PartialDecode::resume_state`]. The entropy-table snapshot clone is
2033 /// only paid when this is set. The snapshot is `None` when the decode
2034 /// reaches the frame's last block ([`PartialDecode::frame_finished`]):
2035 /// there is no following block to resume from, so an incremental walk
2036 /// stops on `frame_finished` rather than on a `None` snapshot.
2037 /// - `resume = Some(`[`ResumeInput`]`)` continues from a previously emitted
2038 /// [`ResumeState`] WITHOUT re-decompressing the preceding blocks: the
2039 /// match window is primed from [`ResumeInput::window_prime`] and the
2040 /// entropy/repcode tables are restored from the state, so a `Repeat_Mode`
2041 /// resume block resolves byte-identically to a contiguous decode — even
2042 /// across a dropped (cold) decoder.
2043 ///
2044 /// When `resume` is `Some`, decoding resumes at
2045 /// [`ResumeState::block_index`] and the `start_block` argument is ignored
2046 /// (pass `resume.state.block_index()`); position `source` at that block's
2047 /// compressed frame offset
2048 /// ([`FrameEmitInfo::blocks`]`[block_index].offset_in_frame`). After a
2049 /// resumed call, [`bytes_read_from_source`](Self::bytes_read_from_source)
2050 /// and any `stopped_at` offsets are relative to the repositioned `source`.
2051 ///
2052 /// **Dictionaries:** [`ResumeState`] does NOT carry the dictionary content.
2053 /// For a dictionary frame, attach the dictionary to the resuming decoder the
2054 /// same way as for a fresh decode — [`reset`](Self::reset) with the
2055 /// dictionary registered (or
2056 /// [`reset_with_dict_handle`](Self::reset_with_dict_handle)) BEFORE this
2057 /// call — so dict-sourced matches near the frame start resolve. The caller
2058 /// already holds the dictionary (it supplied it at encode time), so
2059 /// re-supplying it on resume is free; storing it in the snapshot would only
2060 /// duplicate it. The resume guard records the applied dictionary's identity
2061 /// and rejects ([`FrameDecoderError::ResumeFrameMismatch`]) a resume whose
2062 /// active dictionary differs from the one the snapshot was captured under.
2063 ///
2064 /// # Errors
2065 ///
2066 /// Returns [`FrameDecoderError::NotYetInitialized`] if the decoder has not
2067 /// been reset, [`FrameDecoderError::InvalidBlockRange`] if the effective
2068 /// start exceeds `end_block`, [`FrameDecoderError::ResumeWindowTooShort`]
2069 /// if `resume`'s `window_prime` is shorter than the match window the resume
2070 /// block can reach back into (`min(window_size, output_offset)`), and
2071 /// [`FrameDecoderError::ResumeFrameMismatch`] if the snapshot was captured
2072 /// from a frame with a different decode shape / dictionary, or (with the
2073 /// `hash` feature) a `window_prime` whose content does not match what was
2074 /// captured — all rejected up front rather than silently mis-resolving
2075 /// matches. A corrupt block is NOT an `Err` here: it is reported via
2076 /// [`PartialDecode::stopped_at`] so the clean prefix survives.
2077 ///
2078 /// [`FrameEmitInfo::decompressed_byte_range`]: crate::encoding::frame_emit_info::FrameEmitInfo::decompressed_byte_range
2079 /// [`FrameEmitInfo::blocks`]: crate::encoding::frame_emit_info::FrameEmitInfo::blocks
2080 #[cfg(feature = "lsm")]
2081 #[cfg_attr(docsrs, doc(cfg(feature = "lsm")))]
2082 pub fn decode_blocks_partial(
2083 &mut self,
2084 mut source: impl Read,
2085 start_block: u32,
2086 end_block: u32,
2087 resume: Option<ResumeInput<'_>>,
2088 emit_resume: bool,
2089 ) -> Result<PartialDecode, FrameDecoderError> {
2090 use FrameDecoderError as err;
2091 #[cfg(feature = "hash")]
2092 let checksum_mode = self.content_checksum;
2093 let magicless = self.magicless;
2094 let state = self.state.as_mut().ok_or(err::NotYetInitialized)?;
2095
2096 // Honor the checksum mode before any drain/read can hash: `None` must
2097 // compute no XXH64. `decode_blocks` sets this; the partial path must too,
2098 // or a reused scratch keeps hashing with the default-enabled state.
2099 #[cfg(feature = "hash")]
2100 {
2101 let compute_hash = checksum_mode != ContentChecksum::None
2102 && state.frame_header.descriptor.content_checksum_flag();
2103 state.decoder_scratch.set_compute_hash(compute_hash);
2104 }
2105
2106 // A dictionary that carries no ID cannot be told from another one, and
2107 // the frame key records only that ID: two different raw-content
2108 // dictionaries key alike, so a snapshot would be restored under the
2109 // wrong one — and one taken here could later be. Both directions are
2110 // refused, and refused HERE, before a block is read or a buffer
2111 // reserved. An answer that does not depend on the blocks must not be
2112 // given after decoding them: it would discard the output they produced
2113 // and leave the source and the decoder somewhere the caller cannot
2114 // retry from.
2115 if (resume.is_some() || emit_resume) && state.using_dict == Some(0) {
2116 return Err(err::ResumeUnidentifiedDictionary);
2117 }
2118
2119 // Mirror `decode_blocks`: pre-reserve the backing buffer to the
2120 // FCS-capped window plus a block so multi-block frames don't pay
2121 // repeated grow steps. The RAW frame window stays separately bound —
2122 // the resume logic below bounds match reach by the frame's window
2123 // semantics, not by the (possibly smaller) reservation cap.
2124 let window_size = state.frame_header.window_size().unwrap_or(0) as usize;
2125 state.reserve_decoding_buffer();
2126
2127 // Cold resume: prime the match window + restore entropy/repcode state +
2128 // advance the block cursor BEFORE the loop, so the first in-range block
2129 // resolves its matches and `Repeat_Mode` tables against the caller's
2130 // persisted state instead of re-decoded prefix blocks. The effective
2131 // start is the resume state's block index (the passed `start_block` is
2132 // ignored in resume mode, per the doc).
2133 let effective_start = if let Some(r) = resume {
2134 // Reject a snapshot captured from a different frame shape BEFORE
2135 // touching any decoder state: restoring entropy/repcode tables that
2136 // belong to another frame would silently produce byte-wrong output.
2137 let current_key = FrameKey::from_state(state, magicless);
2138 if current_key != r.state.frame_key {
2139 return Err(err::ResumeFrameMismatch);
2140 }
2141 let output_offset = r.state.output_offset;
2142 // The window the resume block can reach back into is bounded by the
2143 // smaller of the frame's window_size and the bytes produced so far.
2144 let required = core::cmp::min(window_size as u64, output_offset) as usize;
2145 if r.window_prime.len() < required {
2146 return Err(err::ResumeWindowTooShort {
2147 got: r.window_prime.len(),
2148 need: required,
2149 });
2150 }
2151 // Only the most recent `window_size` bytes can ever back a match
2152 // (offset <= window_size by the frame invariant); load just those
2153 // even if the caller handed us a longer prefix, bounding resume
2154 // memory to one window regardless of the skipped prefix's size.
2155 let prime = if r.window_prime.len() > window_size {
2156 &r.window_prime[r.window_prime.len() - window_size..]
2157 } else {
2158 r.window_prime
2159 };
2160 // Content-exact identity: the primed window must hash to what was
2161 // captured at emit. Catches a same-shape-but-different-frame
2162 // snapshot and a wrong/corrupted window_prime (which FrameKey alone
2163 // cannot), before any state is restored. O(window) one-time per
2164 // resume — negligible next to the decode it guards.
2165 #[cfg(feature = "hash")]
2166 if xxh64_of(prime) != r.state.window_hash {
2167 return Err(err::ResumeFrameMismatch);
2168 }
2169 // Validate the effective range (resume mode begins at the resume
2170 // block, ignoring the caller's `start_block`) BEFORE mutating the
2171 // decoder: an inverted `end_block` must fail without priming the
2172 // window / entropy or advancing the cursor, leaving the decoder
2173 // re-resettable rather than in a half-resumed state.
2174 let effective_start = r.state.block_index;
2175 if effective_start > end_block {
2176 return Err(err::InvalidBlockRange {
2177 start_block: effective_start,
2178 end_block,
2179 });
2180 }
2181 state.decoder_scratch.restore_entropy(r.state);
2182 state.decoder_scratch.prime_window(prime, output_offset);
2183 state.block_counter = effective_start as usize;
2184 // The caller repositions `source` to the resume block; report
2185 // consumed bytes relative to that point (reset left this at the
2186 // frame-header size).
2187 state.bytes_read_counter = 0;
2188 effective_start
2189 } else {
2190 // Fresh decode: validate the caller's range (no state to mutate).
2191 if start_block > end_block {
2192 return Err(err::InvalidBlockRange {
2193 start_block,
2194 end_block,
2195 });
2196 }
2197 start_block
2198 };
2199
2200 let mut block_dec = decoding::block_decoder::with_kernel(self.kernel);
2201
2202 // Bytes of prefix-window output that physically precede the first
2203 // in-range block in the buffer. Captured at the prefix → in-range
2204 // transition (after leading blocks were dropped to the window) so we
2205 // can discard exactly those bytes once decoding is done. `None` until
2206 // the first in-range block is reached.
2207 let mut prefix_window_len: Option<usize> = None;
2208 // Exact count of clean in-range decompressed bytes (sum of per-block
2209 // length deltas of the in-range blocks that succeeded). Any partial
2210 // bytes of a failing in-range block are excluded — the fused executor
2211 // rolls the buffer back to the pre-block checkpoint on a sequence
2212 // error, and anything left over is never counted here, so it is not
2213 // drained into `data`.
2214 let mut subset_bytes: u64 = 0;
2215 let mut blocks_decoded: u32 = 0;
2216 let mut stopped_at: Option<(u32, FrameDecoderError)> = None;
2217
2218 loop {
2219 let block_index = state.block_counter as u32;
2220 // Stop before decoding `end_block`: the trailing blocks are never
2221 // touched (the perf win), and the frame's tail is left unread.
2222 if block_index >= end_block || state.frame_finished {
2223 break;
2224 }
2225 let in_range = block_index >= effective_start;
2226 // Snapshot the window length at the prefix → in-range boundary.
2227 if in_range && prefix_window_len.is_none() {
2228 prefix_window_len = Some(state.decoder_scratch.buffer_len());
2229 }
2230
2231 let block_frame_offset = state.bytes_read_counter as u32;
2232 let (block_header, block_header_size) = match block_dec.read_block_header(&mut source) {
2233 Ok(v) => v,
2234 Err(e) => {
2235 stopped_at = Some((
2236 block_index,
2237 block_header_decode_error(e, block_index, block_frame_offset),
2238 ));
2239 break;
2240 }
2241 };
2242 state.bytes_read_counter += u64::from(block_header_size);
2243
2244 let len_before = state.decoder_scratch.buffer_len();
2245 // Only expose the held dictionary while THIS frame is dict-backed
2246 // (`using_dict` is set per dict-apply, cleared on reset). A reused
2247 // decoder keeps `active_dict` across a no-dict frame for the
2248 // `ptr::eq` reuse-skip, so it must be gated here or a stray
2249 // out-of-window offset on a dictless frame would resolve against the
2250 // stale dictionary content instead of erroring.
2251 let dict_ref = if state.using_dict.is_some() {
2252 state.active_dict.as_ref().map(|h| h.as_dict())
2253 } else {
2254 None
2255 };
2256 match state.decoder_scratch.decode_block_content(
2257 &mut block_dec,
2258 &block_header,
2259 &mut source,
2260 dict_ref,
2261 ) {
2262 Ok(body_read) => state.bytes_read_counter += body_read,
2263 Err(e) => {
2264 stopped_at = Some((
2265 block_index,
2266 block_body_decode_error(
2267 e,
2268 block_index,
2269 block_frame_offset,
2270 &block_header,
2271 block_header_size,
2272 ),
2273 ));
2274 break;
2275 }
2276 }
2277 let produced = state.decoder_scratch.buffer_len() - len_before;
2278 // Per-block XXH64 capture, mirroring `decode_blocks`: hash this
2279 // block's just-decoded bytes BEFORE any window drop so the digest
2280 // count stays 1:1 with the blocks decoded on this path too. Covers
2281 // context (out-of-range) blocks as well, matching `decode_blocks`
2282 // which hashes every block it decodes.
2283 #[cfg(all(feature = "lsm", feature = "hash"))]
2284 if self.per_block_checksums_enabled {
2285 use core::hash::Hasher;
2286 let (s1, s2) = state.decoder_scratch.last_n_as_slices(produced);
2287 let mut h = twox_hash::XxHash64::with_seed(0);
2288 h.write(s1);
2289 h.write(s2);
2290 self.computed_block_checksums.push(h.finish() as u32);
2291 }
2292 state.block_counter += 1;
2293 if in_range {
2294 subset_bytes += produced as u64;
2295 blocks_decoded += 1;
2296 }
2297
2298 if block_header.last_block {
2299 state.frame_finished = true;
2300 if state.frame_header.descriptor.content_checksum_flag() {
2301 let mut chksum = [0u8; 4];
2302 match source.read_exact(&mut chksum) {
2303 Ok(()) => {
2304 state.bytes_read_counter += 4;
2305 state.check_sum = Some(u32::from_le_bytes(chksum));
2306 }
2307 // A trailing-checksum read failure does not invalidate
2308 // the decoded bytes; surface it so the caller knows the
2309 // frame tail was truncated, but keep `data`.
2310 Err(e) => {
2311 stopped_at = Some((block_index, err::FailedToReadChecksum(e)));
2312 }
2313 }
2314 }
2315 break;
2316 }
2317
2318 // Leading (out-of-range) block: bound memory to the window. We
2319 // must NOT drop once in-range, or the in-range output we are about
2320 // to return would be discarded.
2321 if !in_range {
2322 state.decoder_scratch.buffer_drop_to_window_size();
2323 }
2324 }
2325
2326 // Emit cross-block carry-over state for a later resume, if requested.
2327 // Captured AFTER the loop (entropy tables / repcode history are final)
2328 // but BEFORE the drain — the drain only touches the visible output, not
2329 // the entropy state or `total_output_counter`. `block_counter` /
2330 // `total_output()` give the resume coordinates: the next block to decode
2331 // and the cumulative decompressed offset before it (clean even after an
2332 // early stop, since a failed block rolls both back to its checkpoint).
2333 // Suppress the snapshot on the terminal block: `block_counter` is then
2334 // one past the last block (EOF), for which there is no next-block source
2335 // position to resume from. A resume needs a real following block.
2336 let resume_state = if emit_resume && !state.frame_finished {
2337 let dict_ref = if state.using_dict.is_some() {
2338 state.active_dict.as_ref().map(|h| h.as_dict())
2339 } else {
2340 None
2341 };
2342 let (fse, huf, offset_hist) = state.decoder_scratch.export_entropy(dict_ref);
2343 Some(ResumeState {
2344 frame_key: FrameKey::from_state(state, magicless),
2345 block_index: state.block_counter as u32,
2346 output_offset: state.decoder_scratch.total_output(),
2347 fse,
2348 huf,
2349 offset_hist,
2350 #[cfg(feature = "hash")]
2351 window_hash: state.decoder_scratch.window_tail_hash(window_size),
2352 })
2353 } else {
2354 None
2355 };
2356
2357 // The visible buffer is now `[prefix window][in-range clean][maybe
2358 // trailing garbage from a failed in-range block]`. Drop the prefix
2359 // window from the front (match resolution is complete, so it is no
2360 // longer needed), then drain exactly the clean in-range byte count.
2361 let w = prefix_window_len.unwrap_or(0);
2362 state.decoder_scratch.buffer_discard_front(w);
2363 let mut data = alloc::vec![0u8; subset_bytes as usize];
2364 state
2365 .decoder_scratch
2366 .buffer_read_all(&mut data)
2367 .map_err(err::FailedToDrainDecodebuffer)?;
2368
2369 // Clear anything still buffered so a later `read()`/`collect()` on this
2370 // decoder cannot surface out-of-range bytes: the leading-block window
2371 // when no in-range block was reached (`prefix_window_len` stayed
2372 // `None`, so `w` was 0), or trailing garbage from a failed in-range
2373 // block. Only the returned `data` is the partial decode's output.
2374 let residual = state.decoder_scratch.buffer_len();
2375 state.decoder_scratch.buffer_discard_front(residual);
2376
2377 Ok(PartialDecode {
2378 data,
2379 start_block: effective_start,
2380 blocks_decoded,
2381 stopped_at,
2382 frame_finished: state.frame_finished,
2383 resume_state,
2384 })
2385 }
2386
2387 /// Collect bytes and retain window_size bytes while decoding is still going on.
2388 /// After decoding of the frame (is_finished() == true) has finished it will collect all remaining bytes
2389 pub fn collect(&mut self) -> Option<Vec<u8>> {
2390 let finished = self.is_finished();
2391 let state = self.state.as_mut()?;
2392 if finished {
2393 Some(state.decoder_scratch.buffer_drain())
2394 } else {
2395 state.decoder_scratch.buffer_drain_to_window_size()
2396 }
2397 }
2398
2399 /// Collect bytes and retain window_size bytes while decoding is still going on.
2400 /// After decoding of the frame (is_finished() == true) has finished it will collect all remaining bytes
2401 pub fn collect_to_writer(&mut self, w: impl Write) -> Result<usize, Error> {
2402 let finished = self.is_finished();
2403 let state = match &mut self.state {
2404 None => return Ok(0),
2405 Some(s) => s,
2406 };
2407 if finished {
2408 state.decoder_scratch.buffer_drain_to_writer(w)
2409 } else {
2410 state.decoder_scratch.buffer_drain_to_window_size_writer(w)
2411 }
2412 }
2413
2414 /// How many bytes can currently be collected from the decodebuffer, while decoding is going on this will be lower than the actual decodbuffer size
2415 /// because window_size bytes need to be retained for decoding.
2416 /// After decoding of the frame (is_finished() == true) has finished it will report all remaining bytes
2417 pub fn can_collect(&self) -> usize {
2418 let finished = self.is_finished();
2419 let state = match &self.state {
2420 None => return 0,
2421 Some(s) => s,
2422 };
2423 if finished {
2424 state.decoder_scratch.buffer_can_drain()
2425 } else {
2426 state
2427 .decoder_scratch
2428 .buffer_can_drain_to_window_size()
2429 .unwrap_or(0)
2430 }
2431 }
2432
2433 /// Decodes blocks from the source slice and reads from the decodebuffer into the target slice, one block at a
2434 /// time: output is handed to `target` as each block completes, and no further block is decoded while `target`
2435 /// cannot take what is already decoded. The decode buffer so holds one window plus one block however much input
2436 /// is supplied; call again with the unread input to continue.
2437 /// The source slice may contain only parts of a frame but must contain at least one full block to make progress
2438 ///
2439 /// By all means use decode_blocks if you have a io.Reader available. This is just for compatibility with other decompressors
2440 /// which try to serve an old-style c api
2441 ///
2442 /// Returns (read, written). Both zero means the call made no progress: the
2443 /// source holds no full block and the buffer no drainable output, so the
2444 /// same input cannot advance. `read == 0` with `written > 0` is progress of
2445 /// the other kind: `target` filled from output already buffered, and the
2446 /// same input decodes further once the caller offers more room.
2447 ///
2448 /// Note that no kind of block can be bigger than 128kb.
2449 /// So to be safe use at least 128*1024 (max block content size) + 3 (block_header size) + 18 (max frame_header size) bytes as your source buffer
2450 ///
2451 /// You may call this function with an empty source after all bytes have been decoded. This is equivalent to just call decoder.read(&mut target)
2452 pub fn decode_from_to(
2453 &mut self,
2454 source: &[u8],
2455 target: &mut [u8],
2456 ) -> Result<(usize, usize), FrameDecoderError> {
2457 use FrameDecoderError as err;
2458 let bytes_read_at_start = match &self.state {
2459 Some(s) => s.bytes_read_counter,
2460 None => 0,
2461 };
2462 // Bytes already handed to `target` by the per-block drain below.
2463 let mut written = 0usize;
2464
2465 if !self.is_finished() || self.state.is_none() {
2466 let mut mt_source = source;
2467
2468 if self.state.is_none() {
2469 self.init(&mut mt_source)?;
2470 }
2471
2472 //pseudo block to scope "state" so we can borrow self again after the block
2473 {
2474 let kernel = self.kernel;
2475 let state = match &mut self.state {
2476 Some(s) => s,
2477 None => panic!("Bug in library"),
2478 };
2479 let mut block_dec = decoding::block_decoder::with_kernel(kernel);
2480
2481 // Honour the content-checksum mode on this hand-rolled decode
2482 // loop (it does not go through `decode_blocks`): hash only when
2483 // a digest is wanted and the frame carries one. `None` skips the
2484 // XXH64 pass; verification happens after the final drain below.
2485 #[cfg(feature = "hash")]
2486 {
2487 let compute_hash = self.content_checksum != ContentChecksum::None
2488 && state.frame_header.descriptor.content_checksum_flag();
2489 state.decoder_scratch.set_compute_hash(compute_hash);
2490 }
2491
2492 if state.frame_header.descriptor.content_checksum_flag()
2493 && state.frame_finished
2494 && state.check_sum.is_none()
2495 {
2496 // The trailing checksum arrived on a separate call (the last
2497 // block finished earlier). Consume it and fall through to the
2498 // shared `self.read` + post-drain verify below — NOT an early
2499 // return — so any output still buffered from a prior
2500 // small-`target` call is flushed on this call too, and the
2501 // checksum is verified through the one shared path.
2502 if mt_source.len() >= 4 {
2503 let chksum = mt_source[..4].try_into().expect("optimized away");
2504 state.bytes_read_counter += 4;
2505 let chksum = u32::from_le_bytes(chksum);
2506 state.check_sum = Some(chksum);
2507 mt_source = &mt_source[4..];
2508 }
2509 }
2510
2511 loop {
2512 // The frame is fully decoded (last block seen, trailer
2513 // consumed above); no more blocks to read. Any leftover
2514 // bytes are not a block header — stop before misreading them.
2515 if state.frame_finished {
2516 break;
2517 }
2518 // Hand what the window no longer needs to `target` before
2519 // decoding more, and decode no further while `target` cannot
2520 // take it: the buffer then holds one window plus the block
2521 // being decoded, whatever the caller supplies, as upstream
2522 // `ZSTD_decompressStream` flushes each block before the next.
2523 let (read, pending) = state
2524 .decoder_scratch
2525 .buffer_read_reporting_pending(&mut target[written..])
2526 .map_err(err::FailedToDrainDecodebuffer)?;
2527 written += read;
2528 // Stop on a full target as well as on output left behind:
2529 // a drain that empties the buffer into the last of `target`
2530 // leaves nothing pending, and decoding another block then
2531 // consumes input the caller cannot be handed the output of.
2532 // A target of no bytes is not full in that sense: it starts
2533 // at its own length, so the test would fire before any block
2534 // was read and a frame that produces nothing could never
2535 // reach the empty block that ends it. Such a frame decodes
2536 // here; one that does produce bytes buffers its first block
2537 // and stops on the `pending` arm of the next pass.
2538 if pending > 0 || (!target.is_empty() && written == target.len()) {
2539 break;
2540 }
2541 //check if there are enough bytes for the next header
2542 if mt_source.len() < 3 {
2543 break;
2544 }
2545 let block_index = state.block_counter as u32;
2546 let block_frame_offset = state.bytes_read_counter as u32;
2547 let (block_header, block_header_size) = block_dec
2548 .read_block_header(&mut mt_source)
2549 .map_err(|source| {
2550 block_header_decode_error(source, block_index, block_frame_offset)
2551 })?;
2552
2553 // check the needed size for the block before updating counters.
2554 // If not enough bytes are in the source, the header will have to be read again, so act like we never read it in the first place
2555 if mt_source.len() < block_header.content_size as usize {
2556 break;
2557 }
2558 state.bytes_read_counter += u64::from(block_header_size);
2559 // A frame that declares its size gets its buffer in one
2560 // allocation once its first block is in hand, as upstream
2561 // allocates its stream buffer per frame: growing it block by
2562 // block cost a fresh decoder several reallocations, copies
2563 // and page-fault passes per frame. Not on the header alone,
2564 // which would let a header followed by nothing reserve its
2565 // whole declared window. The size is content-capped, so a
2566 // small frame gets a small buffer; a frame of unknown size
2567 // keeps growing lazily rather than paying for its window.
2568 if state.block_counter == 0 && state.frame_header.fcs_declared() {
2569 state.reserve_decoding_buffer();
2570 }
2571
2572 // Only expose the held dictionary while THIS frame is dict-backed
2573 // (`using_dict` is set per dict-apply, cleared on reset). A reused
2574 // decoder keeps `active_dict` across a no-dict frame for the
2575 // `ptr::eq` reuse-skip, so it must be gated here or a stray
2576 // out-of-window offset on a dictless frame would resolve against the
2577 // stale dictionary content instead of erroring.
2578 let dict_ref = if state.using_dict.is_some() {
2579 state.active_dict.as_ref().map(|h| h.as_dict())
2580 } else {
2581 None
2582 };
2583 let bytes_read_in_block_body = state
2584 .decoder_scratch
2585 .decode_block_content(
2586 &mut block_dec,
2587 &block_header,
2588 &mut mt_source,
2589 dict_ref,
2590 )
2591 .map_err(|source| {
2592 block_body_decode_error(
2593 source,
2594 block_index,
2595 block_frame_offset,
2596 &block_header,
2597 block_header_size,
2598 )
2599 })?;
2600 state.bytes_read_counter += bytes_read_in_block_body;
2601 state.block_counter += 1;
2602
2603 if block_header.last_block {
2604 state.frame_finished = true;
2605 if state.frame_header.descriptor.content_checksum_flag() {
2606 //if there are enough bytes handle this here. Else the block at the start of this function will handle it at the next call
2607 if mt_source.len() >= 4 {
2608 let chksum = mt_source[..4].try_into().expect("optimized away");
2609 state.bytes_read_counter += 4;
2610 let chksum = u32::from_le_bytes(chksum);
2611 state.check_sum = Some(chksum);
2612 }
2613 }
2614 break;
2615 }
2616 }
2617 }
2618 }
2619
2620 let result_len = written
2621 + self
2622 .read(&mut target[written..])
2623 .map_err(err::FailedToDrainDecodebuffer)?;
2624 // Once the frame is fully decoded and drained, the running digest is
2625 // final: validate it in `Verify` mode (no-op otherwise). Same finish
2626 // point as the streaming reader.
2627 #[cfg(feature = "hash")]
2628 if self.is_finished() && self.can_collect() == 0 {
2629 self.verify_content_checksum()?;
2630 }
2631 let bytes_read_at_end = match &mut self.state {
2632 Some(s) => s.bytes_read_counter,
2633 None => panic!("Bug in library"),
2634 };
2635 let read_len = bytes_read_at_end - bytes_read_at_start;
2636 Ok((read_len as usize, result_len))
2637 }
2638
2639 /// Decode multiple frames into the output slice.
2640 ///
2641 /// `input` must contain an exact number of frames. Skippable frames are allowed and will be
2642 /// skipped during decode.
2643 ///
2644 /// `output` must be large enough to hold the decompressed data. If you don't know
2645 /// how large the output will be, use [`FrameDecoder::decode_blocks`] instead.
2646 ///
2647 /// This calls [`FrameDecoder::init`], and all bytes currently in the decoder will be lost.
2648 ///
2649 /// Returns the number of bytes written to `output`.
2650 pub fn decode_all(
2651 &mut self,
2652 input: &[u8],
2653 output: &mut [u8],
2654 ) -> Result<usize, FrameDecoderError> {
2655 #[cfg(not(feature = "lsm"))]
2656 {
2657 self.decode_all_impl(input, output, |this, src| this.reset_from_slice(src))
2658 }
2659 #[cfg(feature = "lsm")]
2660 {
2661 self.decode_all_impl(input, output, |this, src| this.reset_from_slice(src), None)
2662 }
2663 }
2664
2665 /// Decode multiple frames into the output slice, invoking `visitor`
2666 /// for every skippable frame encountered before advancing past it.
2667 ///
2668 /// `input` must contain an exact number of frames. Skippable frames
2669 /// (RFC 8878 §3.1.2 magic numbers `0x184D2A50..=0x184D2A5F`) are
2670 /// allowed and will be both visited AND skipped: the visitor gets
2671 /// `(magic_variant, payload)` where `magic_variant` is the low
2672 /// nibble of the magic (`magic - 0x184D2A50`, range `0..=15`) and
2673 /// `payload` is a borrowed slice of the on-wire payload bytes (the
2674 /// skippable frame's `Frame_Size` field worth of data) into
2675 /// `input` — no allocation.
2676 ///
2677 /// The visitor sees skippable frames in stream order; interleaved
2678 /// regular zstd frames continue to decompress into `output` exactly
2679 /// as `decode_all` does.
2680 ///
2681 /// `output` must be large enough to hold the decompressed data.
2682 /// Returns the number of bytes written to `output`.
2683 ///
2684 /// # Example
2685 ///
2686 /// ```ignore
2687 /// use structured_zstd::decoding::FrameDecoder;
2688 ///
2689 /// let mut decoder = FrameDecoder::new();
2690 /// let mut output = vec![0u8; 1024];
2691 /// let mut collected: Vec<(u8, Vec<u8>)> = Vec::new();
2692 /// let n = decoder.decode_all_with_skippable_visitor(
2693 /// input,
2694 /// &mut output,
2695 /// |variant, payload| collected.push((variant, payload.to_vec())),
2696 /// )?;
2697 /// ```
2698 #[cfg(feature = "lsm")]
2699 #[cfg_attr(docsrs, doc(cfg(feature = "lsm")))]
2700 pub fn decode_all_with_skippable_visitor<F>(
2701 &mut self,
2702 input: &[u8],
2703 output: &mut [u8],
2704 mut visitor: F,
2705 ) -> Result<usize, FrameDecoderError>
2706 where
2707 F: FnMut(u8, &[u8]),
2708 {
2709 self.decode_all_impl(
2710 input,
2711 output,
2712 |this, src| this.reset_from_slice(src),
2713 Some(&mut visitor),
2714 )
2715 }
2716
2717 /// Decode multiple frames into the output slice using a pre-parsed dictionary handle.
2718 ///
2719 /// `input` must contain an exact number of frames. Skippable frames are allowed and will be
2720 /// skipped during decode.
2721 ///
2722 /// `output` must be large enough to hold the decompressed data. If you don't know
2723 /// how large the output will be, use [`FrameDecoder::decode_blocks`] instead.
2724 ///
2725 /// This calls [`FrameDecoder::init_with_dict_handle`], and all bytes currently in the
2726 /// decoder will be lost.
2727 ///
2728 /// # Warning
2729 ///
2730 /// Each decoded frame is initialized with `dict`, even when a frame header
2731 /// omits the optional dictionary ID. Callers must only use this API when
2732 /// they already know the input frames were encoded with the provided
2733 /// dictionary; otherwise decoded output can be silently corrupted.
2734 pub fn decode_all_with_dict_handle(
2735 &mut self,
2736 input: &[u8],
2737 output: &mut [u8],
2738 dict: &DictionaryHandle,
2739 ) -> Result<usize, FrameDecoderError> {
2740 #[cfg(not(feature = "lsm"))]
2741 {
2742 self.decode_all_impl(input, output, |this, src| {
2743 this.reset_from_slice_with_dict_handle(src, dict)
2744 })
2745 }
2746 #[cfg(feature = "lsm")]
2747 {
2748 self.decode_all_impl(
2749 input,
2750 output,
2751 |this, src| this.reset_from_slice_with_dict_handle(src, dict),
2752 None,
2753 )
2754 }
2755 }
2756
2757 /// Whether the decoder sits at the very start of an initialised frame:
2758 /// the header has been read (state populated) but no block has been
2759 /// decoded and the frame is not finished. In this state the wrapped
2760 /// source is positioned exactly after the frame header, so
2761 /// [`Self::decode_current_frame_to_vec`] can decode the rest of the frame
2762 /// straight from the remaining source bytes.
2763 pub(crate) fn is_at_frame_start(&self) -> bool {
2764 self.state
2765 .as_ref()
2766 .is_some_and(|s| s.block_counter == 0 && !s.frame_finished)
2767 }
2768
2769 /// Decode the CURRENT (already-initialised) frame, APPENDING the
2770 /// decompressed bytes to `output`, and return the number appended.
2771 ///
2772 /// `input` must be the frame's post-header bytes (the wrapped source after
2773 /// `init` consumed the header). Unlike [`Self::decode_all_to_vec`] this
2774 /// neither re-reads a header nor requires the caller to pre-reserve
2775 /// capacity: a frame that declares its content size decodes DIRECTLY into
2776 /// freshly-grown `output` capacity via the single-copy direct path
2777 /// ([`Self::run_direct_decode`]) — bypassing the `Ring`/`FlatBuf` →
2778 /// `read()` drain copy the streaming loop pays — while an unsized frame
2779 /// falls back to the window-bounded ring drain (still one copy, into
2780 /// `output`). Backs [`StreamingDecoder`](crate::decoding::StreamingDecoder)'s
2781 /// `read_to_end` fast path; the caller must ensure
2782 /// [`Self::is_at_frame_start`].
2783 ///
2784 /// # Errors
2785 ///
2786 /// Propagates any [`FrameDecoderError`] from block decode, content-size
2787 /// mismatch, or (in `Verify` mode) checksum validation.
2788 pub(crate) fn decode_current_frame_to_vec(
2789 &mut self,
2790 mut input: &[u8],
2791 output: &mut Vec<u8>,
2792 keep_dictionary: bool,
2793 ) -> Result<usize, FrameDecoderError> {
2794 let start_len = output.len();
2795 // The current frame is already initialised (its header consumed by the
2796 // caller, WITH the dictionary applied if the decoder was constructed
2797 // with one). Decode it, then decode any FOLLOWING concatenated /
2798 // skippable frames in `input` so the whole source is consumed to EOF
2799 // and nothing is dropped (matching `read_to_end` semantics).
2800 self.decode_one_frame_to_vec(&mut input, output)?;
2801 self.decode_concatenated_frames_to_vec(&mut input, output, keep_dictionary)?;
2802 Ok(output.len() - start_len)
2803 }
2804
2805 /// Initialise and decode every frame remaining in `input` (concatenated /
2806 /// skippable), APPENDING to `output`. `input` is advanced as frames are
2807 /// consumed; on return it is empty. With `keep_dictionary` each following
2808 /// frame is initialised with the dictionary the decoder holds
2809 /// ([`Self::reset_with_active_dict`]), so a forced dictionary is preserved
2810 /// even for frames that omit the dictionary id (plain [`Self::init`] would
2811 /// resolve dictionaries by id only). Backs the `read_to_end` fast path (the
2812 /// frames after the current one) and its mid-frame fallback (the frames
2813 /// after the partially-read one).
2814 pub(crate) fn decode_concatenated_frames_to_vec(
2815 &mut self,
2816 input: &mut &[u8],
2817 output: &mut Vec<u8>,
2818 keep_dictionary: bool,
2819 ) -> Result<usize, FrameDecoderError> {
2820 let start_len = output.len();
2821 while !input.is_empty() {
2822 let init_result = if keep_dictionary {
2823 self.reset_with_active_dict(&mut *input)
2824 } else {
2825 self.init(&mut *input)
2826 };
2827 match init_result {
2828 Ok(_) => {}
2829 Err(FrameDecoderError::ReadFrameHeaderError(
2830 crate::decoding::errors::ReadFrameHeaderError::SkipFrame { length, .. },
2831 )) => {
2832 *input = input
2833 .get(length as usize..)
2834 .ok_or(FrameDecoderError::FailedToSkipFrame)?;
2835 continue;
2836 }
2837 Err(e) => return Err(e),
2838 }
2839 self.decode_one_frame_to_vec(&mut *input, output)?;
2840 }
2841 Ok(output.len() - start_len)
2842 }
2843
2844 /// Decode the single CURRENT (already-initialised) frame, APPENDING to
2845 /// `output`. Helper for [`Self::decode_current_frame_to_vec`].
2846 fn decode_one_frame_to_vec(
2847 &mut self,
2848 input: &mut &[u8],
2849 output: &mut Vec<u8>,
2850 ) -> Result<usize, FrameDecoderError> {
2851 let frame_start = output.len();
2852 let (content_size, fcs_declared) = {
2853 let s = self.state.as_ref().expect("frame is initialised");
2854 (
2855 s.frame_header.frame_content_size(),
2856 s.frame_header.fcs_declared(),
2857 )
2858 };
2859 // Direct path: a declared, non-empty content size that FITS in `usize`
2860 // (and whose end offset does not overflow). `usize::try_from` guards the
2861 // 32-bit / oversized-FCS truncation; an unrepresentable size falls
2862 // through to the window-bounded ring drain rather than allocating a
2863 // truncated buffer that would violate `run_direct_decode`'s precondition.
2864 //
2865 // Plausibility gate: the direct path `resize`s `output` to the declared
2866 // size up front, so a tiny/truncated frame declaring a huge (but
2867 // representable) FCS would allocate + zero that whole size before the
2868 // body is validated. zstd's per-block ceiling is MAX_BLOCK_SIZE from as
2869 // little as ~4 input bytes, so the declared size cannot legitimately
2870 // exceed `input.len() * (MAX_BLOCK_SIZE / 4)`. Anything larger falls
2871 // through to the ring drain, which grows only as real bytes are produced
2872 // and errors out cheaply on truncated input. `input` spans the remaining
2873 // source (this frame plus any following ones), so the bound only ever
2874 // over-permits — a legitimate frame is never forced off the direct path.
2875 // saturating_mul is intentional: an overflow means the available input
2876 // is so large that any representable FCS is plausible (cap = "no limit").
2877 const MAX_DECOMPRESSION_RATIO: usize = (crate::common::MAX_BLOCK_SIZE / 4) as usize;
2878 if content_size > 0
2879 && let Ok(cs) = usize::try_from(content_size)
2880 && cs <= input.len().saturating_mul(MAX_DECOMPRESSION_RATIO)
2881 && let Some(frame_end) = frame_start.checked_add(cs)
2882 {
2883 // Reserve exactly the frame's content and decode straight into it
2884 // (single copy, no ring). The direct path writes precisely
2885 // `content_size` bytes (erroring otherwise), so the grown region is
2886 // fully written.
2887 output.resize(frame_end, 0);
2888 // On error, drop the just-grown (zeroed) tail before propagating so
2889 // callers never observe bytes that were never decoded.
2890 let written = match self.run_direct_decode(
2891 &mut *input,
2892 &mut output[frame_start..],
2893 Some(content_size),
2894 ) {
2895 Ok(n) => n,
2896 Err(e) => {
2897 output.truncate(frame_start);
2898 return Err(e);
2899 }
2900 };
2901 output.truncate(frame_start + written);
2902 #[cfg(feature = "hash")]
2903 self.verify_content_checksum()?;
2904 return Ok(written);
2905 }
2906 // The ring-drain fallback below pre-reserves `useful_window_size()`
2907 // (= `window.min(FCS)`), which for a single-segment frame is the
2908 // declared FCS itself — so a truncated single-segment frame lying about
2909 // its size would still allocate the pledged window before the body
2910 // errors, sidestepping the direct-path gate above. Reject such a frame
2911 // up front when its declared (FCS-bearing) window exceeds what the
2912 // available input could plausibly produce. Frames without a declared
2913 // size keep their window-descriptor reservation (already capped at
2914 // `MAXIMUM_ALLOWED_WINDOW_SIZE` at init); a small-window multi-segment
2915 // frame still falls through to the ring drain, which errors cheaply on
2916 // the truncated body.
2917 if fcs_declared
2918 && let Some(state) = self.state.as_ref()
2919 && state.useful_window_size() > input.len().saturating_mul(MAX_DECOMPRESSION_RATIO)
2920 {
2921 return Err(FrameDecoderError::FrameContentSizeMismatch {
2922 declared: content_size,
2923 produced: 0,
2924 });
2925 }
2926 // No declared size, explicit FCS=0, or an unrepresentable FCS: window-
2927 // bounded ring drain, appended directly to `output` via
2928 // `collect_to_writer` (no staging buffer).
2929 loop {
2930 self.decode_blocks(&mut *input, BlockDecodingStrategy::UptoBytes(1024 * 1024))?;
2931 self.collect_to_writer(&mut *output)
2932 .map_err(FrameDecoderError::FailedToDrainDecodebuffer)?;
2933 if self.is_finished() {
2934 // Final flush of the retained window tail.
2935 self.collect_to_writer(&mut *output)
2936 .map_err(FrameDecoderError::FailedToDrainDecodebuffer)?;
2937 break;
2938 }
2939 }
2940 let produced = (output.len() - frame_start) as u64;
2941 // A declared content size MUST match what the body produced — otherwise
2942 // accept the same corrupt frames `decode_all_impl` rejects (e.g. an
2943 // explicit FCS=0 whose body emits bytes). Use `fcs_declared()` so an
2944 // on-wire FCS=0 is validated, while an unknown size is not.
2945 if fcs_declared && produced != content_size {
2946 return Err(FrameDecoderError::FrameContentSizeMismatch {
2947 declared: content_size,
2948 produced,
2949 });
2950 }
2951 #[cfg(feature = "hash")]
2952 self.verify_content_checksum()?;
2953 Ok(produced as usize)
2954 }
2955
2956 /// Default-feature decode_all_impl: no visitor parameter so the
2957 /// no-lsm build's call surface and codegen are byte-identical to
2958 /// the pre-#172 implementation. Compiles only when `lsm` is OFF.
2959 #[cfg(not(feature = "lsm"))]
2960 fn decode_all_impl(
2961 &mut self,
2962 mut input: &[u8],
2963 mut output: &mut [u8],
2964 mut init_frame: impl FnMut(&mut Self, &mut &[u8]) -> Result<(), FrameDecoderError>,
2965 ) -> Result<usize, FrameDecoderError> {
2966 let mut total_bytes_written = 0;
2967 while !input.is_empty() {
2968 match init_frame(self, &mut input) {
2969 Ok(_) => {}
2970 Err(FrameDecoderError::ReadFrameHeaderError(
2971 crate::decoding::errors::ReadFrameHeaderError::SkipFrame { length, .. },
2972 )) => {
2973 input = input
2974 .get(length as usize..)
2975 .ok_or(FrameDecoderError::FailedToSkipFrame)?;
2976 continue;
2977 }
2978 Err(e) => return Err(e),
2979 };
2980 // Per-frame direct-path dispatch. Now safe to route the
2981 // public `decode_all` here because
2982 // `UserSliceBackend::exec_sequence_inline` returns
2983 // `Result<(), ExecuteSequencesError>` instead of
2984 // panicking on capacity overflow; the error propagates
2985 // up as `FrameDecoderError`. Eligibility (FCS > 0,
2986 // remaining `output` slice holds the declared content)
2987 // puts the frame on the fast path that bypasses the
2988 // FlatBuf/Ring -> `read()` drain copy. Ineligible frames
2989 // (no FCS, output too small) fall through to the legacy
2990 // `decode_blocks` + `read` drain loop below. Dictionary
2991 // frames are eligible: `run_direct_decode` hands the
2992 // shared dict handle to its buffer, and beyond-prefix
2993 // offsets resolve through `repeat_from_dict`.
2994 let (content_size, fcs_declared) = {
2995 let state_ref = self.state.as_ref().expect("init populated state");
2996 (
2997 state_ref.frame_header.frame_content_size(),
2998 state_ref.frame_header.fcs_declared(),
2999 )
3000 };
3001 // Direct decode requires only that the caller slice holds the
3002 // declared content; the inline sequence-exec path no longer
3003 // needs `WILDCOPY_OVERLENGTH` trailing slack because the
3004 // trailing sequence(s) take the bounded (non-overshooting)
3005 // copy in `UserSliceBackend::exec_sequence_bounded`. This is
3006 // the universal "decode into an FCS-sized buffer" case (a
3007 // caller sizing `output` to exactly `frame_content_size`),
3008 // so dropping the slack requirement halves its peak alloc.
3009 //
3010 // Per-block checksums collected inside `run_direct_decode`
3011 // post-loop (over recorded (start, end) ranges of `output`)
3012 // so the direct path stays eligible AND keeps the
3013 // window-size cap (`drop_to_window_size`) between blocks
3014 // that the spec relies on for `offset <= window_size`
3015 // validation. Path choice no longer alters checksum
3016 // semantics.
3017 // A frame that declares no size decodes straight into the
3018 // caller's slice too, the slice being its limit, as upstream
3019 // `ZSTD_decompressDCtx` decodes into `dst`. The drain path
3020 // reserved the frame's whole declared window for it, which for a
3021 // streamed producer's small frame is megabytes for kilobytes.
3022 let declared_size = fcs_declared.then_some(content_size);
3023 let direct_eligible = match declared_size {
3024 Some(declared) => declared > 0 && (output.len() as u64) >= declared,
3025 None => true,
3026 };
3027 if direct_eligible {
3028 let written = self.run_direct_decode(&mut input, output, declared_size)?;
3029 output = &mut output[written..];
3030 total_bytes_written += written;
3031 // Per-frame content-checksum verification (no-op unless the
3032 // mode is `Verify` and the frame carries a checksum).
3033 #[cfg(feature = "hash")]
3034 self.verify_content_checksum()?;
3035 continue;
3036 }
3037 // Non-direct fallback: pre-reserve the backing buffer to
3038 // `window_size` in a single allocation before block decode
3039 // starts, so multi-segment frames don't pay repeated
3040 // `reserve_amortized` grow steps as blocks accumulate (each
3041 // block only reserves MAX_BLOCK_SIZE = 128 KiB, so a window
3042 // > 128 KiB otherwise grows through several intermediate
3043 // sizes with `alloc_zeroed + memcpy` each time).
3044 if let Some(state) = self.state.as_mut() {
3045 // FCS-capped via `decoding_buffer_size` — the same cap
3046 // `decode_blocks` applies, so its per-iteration reserve in
3047 // the loop below cannot grow the buffer back to the raw
3048 // frame window.
3049 state.reserve_decoding_buffer();
3050 }
3051 let frame_start_total = total_bytes_written;
3052 loop {
3053 self.decode_blocks(&mut input, BlockDecodingStrategy::UptoBytes(1024 * 1024))?;
3054 let bytes_written = self
3055 .read(output)
3056 .map_err(FrameDecoderError::FailedToDrainDecodebuffer)?;
3057 output = &mut output[bytes_written..];
3058 total_bytes_written += bytes_written;
3059 if self.can_collect() != 0 {
3060 return Err(FrameDecoderError::TargetTooSmall);
3061 }
3062 if self.is_finished() {
3063 break;
3064 }
3065 }
3066 // Per-frame FCS validation on the legacy fallback path.
3067 // Use `fcs_declared()` (NOT `content_size > 0`) so an
3068 // empty frame with explicit FCS=0 on the wire still gets
3069 // validated.
3070 if fcs_declared {
3071 let produced = (total_bytes_written - frame_start_total) as u64;
3072 if produced != content_size {
3073 return Err(FrameDecoderError::FrameContentSizeMismatch {
3074 declared: content_size,
3075 produced,
3076 });
3077 }
3078 }
3079 // Per-frame content-checksum verification on the drain path: the
3080 // frame is fully decoded and drained here (is_finished + nothing
3081 // left to collect), so the running digest and stored value are
3082 // final. No-op unless the mode is `Verify`.
3083 #[cfg(feature = "hash")]
3084 self.verify_content_checksum()?;
3085 }
3086
3087 Ok(total_bytes_written)
3088 }
3089
3090 /// `lsm`-feature decode_all_impl: adds the optional skippable
3091 /// visitor parameter consumed by
3092 /// [`Self::decode_all_with_skippable_visitor`]. Mirrors the no-lsm
3093 /// variant including the direct-path dispatch + FCS-validation
3094 /// rationale comments, so the two functions stay in sync; the only
3095 /// behavioral difference is the SkipFrame arm, which uses
3096 /// `split_at(length)` (single bounds check) instead of two
3097 /// separate `get(..length)` / `get(length..)` slices and invokes
3098 /// the visitor (when `Some`) on the borrowed payload before
3099 /// advancing past it.
3100 #[cfg(feature = "lsm")]
3101 #[allow(clippy::type_complexity)]
3102 fn decode_all_impl(
3103 &mut self,
3104 mut input: &[u8],
3105 mut output: &mut [u8],
3106 mut init_frame: impl FnMut(&mut Self, &mut &[u8]) -> Result<(), FrameDecoderError>,
3107 mut skippable_visitor: Option<&mut dyn FnMut(u8, &[u8])>,
3108 ) -> Result<usize, FrameDecoderError> {
3109 let mut total_bytes_written = 0;
3110 while !input.is_empty() {
3111 match init_frame(self, &mut input) {
3112 Ok(_) => {}
3113 Err(FrameDecoderError::ReadFrameHeaderError(
3114 crate::decoding::errors::ReadFrameHeaderError::SkipFrame {
3115 magic_number,
3116 length,
3117 },
3118 )) => {
3119 let length = length as usize;
3120 // Visitor sees the payload slice BEFORE we advance
3121 // past it. Borrowed slice — no allocation. The
3122 // variant is the low nibble of the magic number
3123 // (RFC 8878 §3.1.2). `read_frame_header` only emits
3124 // SkipFrame for magic in 0x184D2A50..=0x184D2A5F, so
3125 // the subtraction fits in 0..=15.
3126 if input.len() < length {
3127 return Err(FrameDecoderError::FailedToSkipFrame);
3128 }
3129 let (payload, rest) = input.split_at(length);
3130 if let Some(visitor) = skippable_visitor.as_mut() {
3131 let variant = (magic_number - 0x184D2A50) as u8;
3132 visitor(variant, payload);
3133 }
3134 input = rest;
3135 continue;
3136 }
3137 Err(e) => return Err(e),
3138 };
3139 // Per-frame direct-path dispatch. Now safe to route the
3140 // public `decode_all` here because
3141 // `UserSliceBackend::exec_sequence_inline` returns
3142 // `Result<(), ExecuteSequencesError>` instead of
3143 // panicking on capacity overflow; the error propagates
3144 // up as `FrameDecoderError`. Eligibility (FCS > 0,
3145 // remaining `output` slice holds the declared content)
3146 // puts the frame on the fast path that bypasses the
3147 // FlatBuf/Ring -> `read()` drain copy. Ineligible frames
3148 // (no FCS, output too small) fall through to the legacy
3149 // `decode_blocks` + `read` drain loop below. Dictionary
3150 // frames are eligible (see the no-lsm path above).
3151 let (content_size, fcs_declared) = {
3152 let state_ref = self.state.as_ref().expect("init populated state");
3153 (
3154 state_ref.frame_header.frame_content_size(),
3155 state_ref.frame_header.fcs_declared(),
3156 )
3157 };
3158 // Only `cap >= frame_content_size` needed; the trailing
3159 // sequence(s) take the bounded copy in
3160 // `UserSliceBackend::exec_sequence_bounded`, so no
3161 // `WILDCOPY_OVERLENGTH` trailing slack is required (see the
3162 // no-lsm path above).
3163 // A frame that declares no size decodes straight into the
3164 // caller's slice too, the slice being its limit, as upstream
3165 // `ZSTD_decompressDCtx` decodes into `dst`. The drain path
3166 // reserved the frame's whole declared window for it, which for a
3167 // streamed producer's small frame is megabytes for kilobytes.
3168 let declared_size = fcs_declared.then_some(content_size);
3169 let direct_eligible = match declared_size {
3170 Some(declared) => declared > 0 && (output.len() as u64) >= declared,
3171 None => true,
3172 };
3173 if direct_eligible {
3174 let written = self.run_direct_decode(&mut input, output, declared_size)?;
3175 output = &mut output[written..];
3176 total_bytes_written += written;
3177 // Per-frame content-checksum verification (no-op unless the
3178 // mode is `Verify` and the frame carries a checksum).
3179 #[cfg(feature = "hash")]
3180 self.verify_content_checksum()?;
3181 continue;
3182 }
3183 // Non-direct fallback: pre-reserve the backing buffer to
3184 // `window_size` once so the per-block growth cycle is
3185 // skipped (see same comment on the no-lsm path above).
3186 if let Some(state) = self.state.as_mut() {
3187 // FCS-capped via `decoding_buffer_size` — the same cap
3188 // `decode_blocks` applies, so its per-iteration reserve in
3189 // the loop below cannot grow the buffer back to the raw
3190 // frame window.
3191 state.reserve_decoding_buffer();
3192 }
3193 let frame_start_total = total_bytes_written;
3194 loop {
3195 self.decode_blocks(&mut input, BlockDecodingStrategy::UptoBytes(1024 * 1024))?;
3196 let bytes_written = self
3197 .read(output)
3198 .map_err(FrameDecoderError::FailedToDrainDecodebuffer)?;
3199 output = &mut output[bytes_written..];
3200 total_bytes_written += bytes_written;
3201 if self.can_collect() != 0 {
3202 return Err(FrameDecoderError::TargetTooSmall);
3203 }
3204 if self.is_finished() {
3205 break;
3206 }
3207 }
3208 // Per-frame FCS validation on the legacy fallback path.
3209 // Use `fcs_declared()` (NOT `content_size > 0`) so an
3210 // empty frame with explicit FCS=0 on the wire still gets
3211 // validated.
3212 if fcs_declared {
3213 let produced = (total_bytes_written - frame_start_total) as u64;
3214 if produced != content_size {
3215 return Err(FrameDecoderError::FrameContentSizeMismatch {
3216 declared: content_size,
3217 produced,
3218 });
3219 }
3220 }
3221 // Per-frame content-checksum verification on the drain path: the
3222 // frame is fully decoded and drained here (is_finished + nothing
3223 // left to collect), so the running digest and stored value are
3224 // final. No-op unless the mode is `Verify`.
3225 #[cfg(feature = "hash")]
3226 self.verify_content_checksum()?;
3227 }
3228
3229 Ok(total_bytes_written)
3230 }
3231
3232 /// Decode multiple frames into the output slice using a serialized dictionary.
3233 ///
3234 /// # Warning
3235 ///
3236 /// Each decoded frame is initialized with the parsed dictionary, even when a
3237 /// frame header omits the optional dictionary ID. Callers must only use this
3238 /// API when they already know the input frames were encoded with that
3239 /// dictionary; otherwise decoded output can be silently corrupted.
3240 pub fn decode_all_with_dict_bytes(
3241 &mut self,
3242 input: &[u8],
3243 output: &mut [u8],
3244 raw_dictionary: &[u8],
3245 ) -> Result<usize, FrameDecoderError> {
3246 let dict = DictionaryHandle::decode_dict(raw_dictionary)?;
3247 self.decode_all_with_dict_handle(input, output, &dict)
3248 }
3249
3250 /// Decode multiple frames into the extra capacity of the output vector.
3251 ///
3252 /// `input` must contain an exact number of frames.
3253 ///
3254 /// `output` must have enough spare capacity to hold the decompressed
3255 /// data. This adds no extra slack: exact-fit output is now eligible
3256 /// for the direct decode path, so a `Vec::with_capacity(fcs)` is
3257 /// decoded straight into without a growth/reallocation. It will NOT
3258 /// grow the vector to fit the decompressed payload itself; the
3259 /// caller's pre-allocated capacity must already cover the data. If
3260 /// you don't know how large the output will be, use
3261 /// [`FrameDecoder::decode_blocks`] instead.
3262 ///
3263 /// This calls [`FrameDecoder::init`], and all bytes currently in the decoder will be lost.
3264 ///
3265 /// The length of the output vector is updated to include the
3266 /// decompressed data. The length is not changed if an error occurs.
3267 pub fn decode_all_to_vec(
3268 &mut self,
3269 input: &[u8],
3270 output: &mut Vec<u8>,
3271 ) -> Result<(), FrameDecoderError> {
3272 let len = output.len();
3273 let cap = output.capacity();
3274 output.resize(cap, 0);
3275 match self.decode_all(input, &mut output[len..]) {
3276 Ok(bytes_written) => {
3277 let new_len = core::cmp::min(len + bytes_written, cap); // Sanitizes `bytes_written`.
3278 output.resize(new_len, 0);
3279 Ok(())
3280 }
3281 Err(e) => {
3282 output.resize(len, 0);
3283 Err(e)
3284 }
3285 }
3286 }
3287
3288 /// Single-frame direct-decode path. Decodes one zstd frame into
3289 /// `output[..content_size]` via a stack-local
3290 /// `DecodeBuffer<UserSliceBackend>`, bypassing the per-block
3291 /// FlatBuf/Ring -> `read()` drain copy.
3292 ///
3293 /// # Preconditions (caller-enforced)
3294 ///
3295 /// - `self.init` (or `init_with_dict_handle`) was called for
3296 /// this frame so `self.state` is populated.
3297 /// - `declared_size` is the frame's declared content size, `> 0`, with
3298 /// `output.len() >= declared_size` (the eligibility gate), or `None`
3299 /// for a frame that declares none. Then `output` itself is the limit,
3300 /// as upstream `ZSTD_decompressDCtx` decodes into `dst`, and a frame
3301 /// that does not fit is `TargetTooSmall` rather than a size mismatch.
3302 /// No `WILDCOPY_OVERLENGTH` trailing slack is required: the trailing
3303 /// sequence(s) take the bounded (non-overshooting) copy in
3304 /// [`UserSliceBackend::exec_sequence_bounded`](crate::decoding::user_slice_buf::UserSliceBackend::exec_sequence_bounded).
3305 ///
3306 /// Dictionary frames are supported: the scratch buffer's shared
3307 /// dict handle is forwarded to the stack-local `DecodeBuffer`, so
3308 /// offsets reaching past the frame's own output resolve through
3309 /// `repeat_from_dict` (the ext-dict slow path).
3310 ///
3311 /// On return, `input` points at the byte immediately after the
3312 /// frame's checksum (or after the last block, when the frame
3313 /// has `content_checksum_flag = 0`). `self.state.frame_finished`
3314 /// is set so [`Self::is_finished`] reports `true`.
3315 fn run_direct_decode(
3316 &mut self,
3317 input: &mut &[u8],
3318 output: &mut [u8],
3319 declared_size: Option<u64>,
3320 ) -> Result<usize, FrameDecoderError> {
3321 #[cfg(test)]
3322 {
3323 self.direct_frames += 1;
3324 }
3325 use super::block_decoder;
3326 use super::decode_buffer::DecodeBuffer;
3327 use super::scratch::DirectScratch;
3328 use super::user_slice_buf::UserSliceBackend;
3329 use crate::io::Read;
3330 use FrameDecoderError as err;
3331
3332 // The most the frame may write: its declared size, or the caller's
3333 // slice for a frame that declares none.
3334 let limit = declared_size.unwrap_or(output.len() as u64);
3335 // Output past `limit`: the frame lied about its size, or it does not
3336 // fit the caller's slice.
3337 let overflow = |produced: u64| match declared_size {
3338 Some(declared) => err::FrameContentSizeMismatch { declared, produced },
3339 None => err::TargetTooSmall,
3340 };
3341
3342 let kernel = self.kernel;
3343 let state = self
3344 .state
3345 .as_mut()
3346 .expect("caller ensures init populated state");
3347
3348 // Fast path: a frame that is a single RAW block spanning the whole
3349 // declared content. Upstream zstd handles this as one `ZSTD_copyRawBlock`
3350 // (a `memmove`) inside `ZSTD_decompressFrame`; do the same here — a direct
3351 // `copy_from_slice` into the caller's output slice — skipping the
3352 // `DirectScratch` / `DecodeBuffer` / `UserSliceBackend` wrapper
3353 // construction and the general per-block loop. Incompressible payloads
3354 // (random / already-compressed data) emit exactly this shape, so the win
3355 // lands on small high-entropy frames where the per-frame machinery, not
3356 // the 1-block copy, dominates.
3357 {
3358 let mut probe = *input;
3359 let mut header_dec = block_decoder::with_kernel(kernel);
3360 if let Ok((bh, hsize)) = header_dec.read_block_header(&mut probe) {
3361 let n = bh.decompressed_size as usize;
3362 // A frame that declares no size takes the shortcut too: the
3363 // slice is its limit, and holding the block means holding the
3364 // frame. Without this the probe parsed the header that the
3365 // general loop below parses again, on the very path (a small
3366 // frame from a streamed producer) this decode is for. The
3367 // block maximum is checked here as the general path checks it:
3368 // a block past it is malformed, and the shortcut must not be
3369 // the way around that.
3370 let window = state.frame_header.window_size().unwrap_or(0) as usize;
3371 if bh.last_block
3372 && matches!(bh.block_type, crate::blocks::block::BlockType::Raw)
3373 && declared_size.is_none_or(|declared| declared == n as u64)
3374 && n <= block_decoder::block_maximum(window)
3375 && probe.len() >= n
3376 && output.len() >= n
3377 {
3378 output[..n].copy_from_slice(&probe[..n]);
3379 *input = &probe[n..];
3380 state.bytes_read_counter += u64::from(hsize) + n as u64;
3381 state.block_counter += 1;
3382 // Consume the trailing 4-byte content checksum UNCONDITIONALLY
3383 // when the frame declares one — exactly like the general
3384 // direct loop and `decode_blocks`. Only the hash SEEDING is
3385 // `hash`-gated; the byte consumption / counter / `check_sum`
3386 // must not be, or a no-`hash` build leaves the 4 bytes in
3387 // `*input` (misparsed as the next frame) and never sets
3388 // `check_sum` (so `is_finished` stays false).
3389 if state.frame_header.descriptor.content_checksum_flag() {
3390 let mut chksum = [0u8; 4];
3391 Read::read_exact(input, &mut chksum).map_err(err::FailedToReadChecksum)?;
3392 state.bytes_read_counter += 4;
3393 state.check_sum = Some(u32::from_le_bytes(chksum));
3394 // Mirror the general path: seed the scratch hash so
3395 // `verify_content_checksum` / `get_calculated_checksum`
3396 // read the digest. Skipped under `ContentChecksum::None`.
3397 #[cfg(feature = "hash")]
3398 if self.content_checksum != ContentChecksum::None {
3399 use core::hash::Hasher;
3400 let mut h = twox_hash::XxHash64::with_seed(0);
3401 h.write(&output[..n]);
3402 match &mut state.decoder_scratch {
3403 DecoderScratchKind::Flat(s) => s.buffer.set_hash(h),
3404 DecoderScratchKind::Ring(s) => s.buffer.set_hash(h),
3405 }
3406 }
3407 }
3408 #[cfg(all(feature = "lsm", feature = "hash"))]
3409 if self.per_block_checksums_enabled {
3410 use core::hash::Hasher;
3411 let mut h = twox_hash::XxHash64::with_seed(0);
3412 h.write(&output[..n]);
3413 self.computed_block_checksums.push(h.finish() as u32);
3414 }
3415 state.frame_finished = true;
3416 return Ok(n);
3417 }
3418 }
3419 }
3420
3421 // Borrow persistent fields out of whichever scratch variant
3422 // `init` produced (Flat for single_segment, Ring for
3423 // multi-segment) — both expose the same HUF/FSE/Vec
3424 // fields; only `buffer` differs and we don't use that here.
3425 // Macro-style binding avoids the closure / generic
3426 // gymnastics of returning multiple `&mut` from a match arm.
3427 // Resolve the dictionary borrow for this frame BEFORE taking the
3428 // `&mut` field borrows below — `active_dict` is a disjoint field, so
3429 // the shared borrow coexists with the mutable scratch borrows. It is
3430 // threaded as a call-scoped argument into every `Dict`-sourced read
3431 // (the direct path's `repeat_from_dict` ext-dict slow path), mirroring
3432 // C's per-frame pointer hand-off with zero refcount churn.
3433 // Only expose the held dictionary while THIS frame is dict-backed
3434 // (`using_dict` is set per dict-apply, cleared on reset). A reused
3435 // decoder keeps `active_dict` across a no-dict frame for the
3436 // `ptr::eq` reuse-skip, so it must be gated here or a stray
3437 // out-of-window offset on a dictless frame would resolve against the
3438 // stale dictionary content instead of erroring.
3439 let dict_ref = if state.using_dict.is_some() {
3440 state.active_dict.as_ref().map(|h| h.as_dict())
3441 } else {
3442 None
3443 };
3444 let (huf, fse, offset_hist, literals_buffer, block_content_buffer, window_size) =
3445 match &mut state.decoder_scratch {
3446 DecoderScratchKind::Flat(s) => (
3447 &mut s.huf,
3448 &mut s.fse,
3449 &mut s.offset_hist,
3450 &mut s.literals_buffer,
3451 &mut s.block_content_buffer,
3452 s.buffer.window_size,
3453 ),
3454 DecoderScratchKind::Ring(s) => (
3455 &mut s.huf,
3456 &mut s.fse,
3457 &mut s.offset_hist,
3458 &mut s.literals_buffer,
3459 &mut s.block_content_buffer,
3460 s.buffer.window_size,
3461 ),
3462 };
3463 let backend = UserSliceBackend::from_slice(output);
3464 let buffer = DecodeBuffer::from_backend(backend, window_size);
3465 let mut direct = DirectScratch {
3466 huf,
3467 fse,
3468 offset_hist,
3469 literals_buffer,
3470 block_content_buffer,
3471 buffer,
3472 };
3473
3474 // Block loop. Mirrors `decode_blocks` (without the
3475 // strategy-bounded early exit — we always decode the whole
3476 // frame in one shot for the direct path). Keeps
3477 // `state.bytes_read_counter` / `state.block_counter` in
3478 // sync with `decode_blocks` so post-call accessors
3479 // (`bytes_read_from_source`, `blocks_decoded`) return
3480 // accurate values.
3481 let mut block_dec = block_decoder::with_kernel(kernel);
3482 // Track total output bytes against the declared
3483 // `frame_content_size` via the buffer's actual write
3484 // counter — `BlockHeader.decompressed_size` is 0 for
3485 // Compressed blocks (the header parser can't know the
3486 // expanded size before decoding the body), so per-header
3487 // tracking would always count 0 for those blocks and
3488 // miscount frames that aren't pure Raw/RLE.
3489 let mut produced: u64 = 0;
3490 // Per-block output ranges captured during the direct-path
3491 // loop. After the loop we re-borrow `output` (post-drop of
3492 // `direct`) and XXH64 each range into
3493 // `self.computed_block_checksums`, so the digests vector
3494 // stays consistent with the legacy `decode_blocks` path
3495 // regardless of which dispatch the frame took.
3496 // `Vec::new()` does not allocate, so this stays free when
3497 // `per_block_checksums_enabled` is false: the `push` and the
3498 // post-loop hashing loop are both gated by the same flag.
3499 #[cfg(all(feature = "lsm", feature = "hash"))]
3500 let mut block_ranges: alloc::vec::Vec<(usize, usize)> = alloc::vec::Vec::new();
3501 // Frame-level XXH64, accumulated PER BLOCK right after each block
3502 // decodes — the bytes are still cache-resident then. The previous
3503 // shape hashed the whole output once after the loop, which re-read
3504 // the entire frame cold: a full extra memory pass that the
3505 // reference implementation does not make (it hashes incrementally
3506 // per block). Invisible on outputs that fit L3, ~1.14x wall on a
3507 // 100 MiB all-raw decode and the dominant CI gap on
3508 // bandwidth-limited hosts.
3509 #[cfg(feature = "hash")]
3510 let mut running_hash: Option<twox_hash::XxHash64> =
3511 if state.frame_header.descriptor.content_checksum_flag()
3512 && self.content_checksum != ContentChecksum::None
3513 {
3514 Some(twox_hash::XxHash64::with_seed(0))
3515 } else {
3516 None
3517 };
3518 loop {
3519 #[cfg(all(feature = "lsm", feature = "hash"))]
3520 let produced_before: Option<usize> = if self.per_block_checksums_enabled {
3521 Some(produced as usize)
3522 } else {
3523 None
3524 };
3525 // Failing-block coordinates captured before the header read (see
3526 // the `decode_blocks` loop for the rationale).
3527 let block_index = state.block_counter as u32;
3528 let block_frame_offset = state.bytes_read_counter as u32;
3529 let (block_header, hsize) =
3530 block_dec.read_block_header(&mut *input).map_err(|source| {
3531 block_header_decode_error(source, block_index, block_frame_offset)
3532 })?;
3533 state.bytes_read_counter += u64::from(hsize);
3534 // Pre-flight FCS check ONLY for Raw / RLE blocks where
3535 // `decompressed_size` is the actual block output size.
3536 // For Compressed blocks the header field is 0; the
3537 // post-decode check below catches overflow via the
3538 // backend's actual write counter delta.
3539 let block_upper = u64::from(block_header.decompressed_size);
3540 if block_upper > 0 && produced + block_upper > limit {
3541 // Raw/RLE block headers claim more output than the FCS
3542 // allows (a corrupt frame) or the caller's slice holds.
3543 return Err(overflow(produced + block_upper));
3544 }
3545 // Slice-source fast path: consume the block body
3546 // straight from `input` without copying into the
3547 // persistent `block_content_buffer`.
3548 let body_consumed = match block_dec.decode_block_content_from_slice(
3549 &block_header,
3550 &mut direct,
3551 dict_ref,
3552 &mut *input,
3553 ) {
3554 Ok(n) => n,
3555 // Defense-in-depth: RLE / Raw block whose declared
3556 // `decompressed_size` slipped past the per-block
3557 // pre-flight above and tripped the backend's
3558 // fallible write surface.
3559 Err(crate::decoding::errors::DecodeBlockContentError::BackendOverflow {
3560 ..
3561 }) => {
3562 // Use saturating_add on the
3563 // `produced + decompressed_size` sum. Each block
3564 // is bounded by 128 KiB (MAX_BLOCK_SIZE), but
3565 // accumulated `produced` can grow toward
3566 // u64::MAX across adversarial frames. Saturating
3567 // avoids a panic on the error path itself.
3568 return Err(overflow(
3569 produced.saturating_add(u64::from(block_header.decompressed_size)),
3570 ));
3571 }
3572 // Compressed-block in-block overshoot: the sequence
3573 // executor (upstream zstd-inline path) or the match-repeat
3574 // fallback tripped the fixed-capacity backend's per-write
3575 // check. Unlike Raw/RLE, a Compressed block carries no
3576 // header-declared output size, so `produced` is computed
3577 // from the partial fill: `tail` bytes were written before
3578 // the failing op, and `requested` is what overflowed —
3579 // their sum is a strict lower bound on the frame's true
3580 // expanded size and is always > `limit`. With a declared
3581 // size the slice holds at least that much, so any overflow
3582 // means the frame exceeded its FCS, never a caller-undersized
3583 // buffer, and folds into the same `FrameContentSizeMismatch`
3584 // contract as Raw/RLE; without one the slice is the limit.
3585 // An overflow that stays within `limit` was refused by the
3586 // per-block output ceiling instead: a malformed block, which
3587 // takes the generic arm below.
3588 Err(crate::decoding::errors::DecodeBlockContentError::DecompressBlockError(
3589 crate::decoding::errors::DecompressBlockError::ExecuteSequencesError(ref e),
3590 )) if e.output_overflow_requested().is_some_and(|requested| {
3591 (direct.buffer.buffer_ref().tail() as u64).saturating_add(requested as u64)
3592 > limit
3593 }) =>
3594 {
3595 let requested = e
3596 .output_overflow_requested()
3597 .expect("guard guarantees Some") as u64;
3598 let tail = direct.buffer.buffer_ref().tail() as u64;
3599 return Err(overflow(tail.saturating_add(requested)));
3600 }
3601 // A no-sequence block's literals did not fit the slice. Every
3602 // direct-path entry holds `output.len() >= limit` (a declared
3603 // size is checked against the slice before the path is chosen,
3604 // and an undeclared frame's limit IS the slice), so a write
3605 // past the slice is a write past `limit`: the frame outgrew its
3606 // declared size, or the caller's target is short.
3607 Err(crate::decoding::errors::DecodeBlockContentError::DecompressBlockError(
3608 crate::decoding::errors::DecompressBlockError::LiteralsOutputOverflow {
3609 tail,
3610 requested,
3611 capacity,
3612 },
3613 )) => {
3614 debug_assert!(
3615 capacity as u64 >= limit,
3616 "direct path entered with a short slice"
3617 );
3618 return Err(overflow((tail as u64).saturating_add(requested as u64)));
3619 }
3620 Err(e) => {
3621 return Err(block_body_decode_error(
3622 e,
3623 block_index,
3624 block_frame_offset,
3625 &block_header,
3626 hsize,
3627 ));
3628 }
3629 };
3630 // Hash this block's freshly-written bytes while they are hot
3631 // (see `running_hash` above). `tail()` is the physical write
3632 // cursor: `drop_to_window_size` below only advances the head,
3633 // so `[prev_tail, tail)` is exactly this block's output.
3634 #[cfg(feature = "hash")]
3635 if let Some(hasher) = running_hash.as_mut() {
3636 use core::hash::Hasher;
3637 hasher.write(direct.buffer.buffer_ref().written_since(produced as usize));
3638 }
3639 produced = direct.buffer.buffer_ref().tail() as u64;
3640 // Post-decode FCS overflow check.
3641 if produced > limit {
3642 return Err(overflow(produced));
3643 }
3644 state.bytes_read_counter += body_consumed;
3645 state.block_counter += 1;
3646 #[cfg(all(feature = "lsm", feature = "hash"))]
3647 if let Some(produced_before) = produced_before {
3648 block_ranges.push((produced_before, produced as usize));
3649 }
3650 // Cap the visible buffer at window_size between blocks
3651 // so the next block's match-offset validation matches
3652 // the spec's `offset <= window_size` rule.
3653 direct.buffer.drop_to_window_size();
3654 if block_header.last_block {
3655 if state.frame_header.descriptor.content_checksum_flag() {
3656 let mut chksum = [0u8; 4];
3657 input
3658 .read_exact(&mut chksum)
3659 .map_err(err::FailedToReadChecksum)?;
3660 state.bytes_read_counter += 4;
3661 state.check_sum = Some(u32::from_le_bytes(chksum));
3662 }
3663 break;
3664 }
3665 }
3666 // Final sanity: blocks summed to exactly the declared size.
3667 if let Some(declared) = declared_size
3668 && produced != declared
3669 {
3670 return Err(err::FrameContentSizeMismatch { declared, produced });
3671 }
3672
3673 let written = produced as usize;
3674 state.frame_finished = true;
3675 // `direct`'s last use is in the decode loop above; NLL therefore
3676 // releases its `&mut output` borrow before here, freeing `output` for
3677 // the hash re-borrow below. No explicit `drop(direct)` is needed:
3678 // `DirectScratch` now holds only borrowed dict POINTERS (not an owned
3679 // `Arc`), so it is not a `Drop` type whose glue would hold the borrow
3680 // to end-of-scope.
3681 // Per-block XXH64 (low 32 bits) over the captured ranges.
3682 // Mirrors `decode_blocks`' per-block hashing so the digests
3683 // vector stays identical regardless of which dispatch path
3684 // the frame took. Ranges were recorded inside the loop while
3685 // `direct` held a mutable borrow on `output`; now that the
3686 // borrow is dropped we can read the slices directly.
3687 #[cfg(all(feature = "lsm", feature = "hash"))]
3688 if self.per_block_checksums_enabled {
3689 use core::hash::Hasher;
3690 for (start, end) in &block_ranges {
3691 let mut h = twox_hash::XxHash64::with_seed(0);
3692 h.write(&output[*start..*end]);
3693 self.computed_block_checksums.push(h.finish() as u32);
3694 }
3695 }
3696 #[cfg(feature = "hash")]
3697 if let Some(hasher) = running_hash {
3698 // Propagate the per-block-accumulated hasher state (see the
3699 // `running_hash` rationale above the loop) so the frame-tail
3700 // XXH64 check and `get_calculated_checksum()` read the digest.
3701 // `running_hash` is `None` for flag-off frames or
3702 // `ContentChecksum::None` — nothing to verify there, and
3703 // `get_calculated_checksum()` returns `None`, matching the skip.
3704 match &mut state.decoder_scratch {
3705 DecoderScratchKind::Flat(s) => s.buffer.set_hash(hasher),
3706 DecoderScratchKind::Ring(s) => s.buffer.set_hash(hasher),
3707 }
3708 }
3709 Ok(written)
3710 }
3711}
3712
3713/// Read bytes from the decode_buffer that are no longer needed. While the frame is not yet finished
3714/// this will retain window_size bytes, else it will drain it completely
3715impl Read for FrameDecoder {
3716 fn read(&mut self, target: &mut [u8]) -> Result<usize, Error> {
3717 let state = match &mut self.state {
3718 None => return Ok(0),
3719 Some(s) => s,
3720 };
3721 if state.frame_finished {
3722 state.decoder_scratch.buffer_read_all(target)
3723 } else {
3724 state.decoder_scratch.buffer_read(target)
3725 }
3726 }
3727}
3728
3729#[cfg(test)]
3730mod tests;