Skip to main content

structured_zstd/decoding/
frame_decoder.rs

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