Skip to main content

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