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