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