Skip to main content

structured_zstd/decoding/
mod.rs

1//! RFC 8878 Zstandard decoder.
2//!
3//! The entry points:
4//!
5//! * [`StreamingDecoder`] — implements [`crate::io::Read`] over a compressed
6//!   byte stream, transparently parsing the frame header and concatenated
7//!   frames. The typical choice for application code.
8//! * [`FrameDecoder`] — single-frame interface; use when the caller manages
9//!   the input buffer manually (zero-copy slices, network framing, etc).
10//! * `workspace::WorkspaceDecoder` (feature `caller-workspace`): streaming
11//!   decoder over a byte slice the caller sizes and owns, with no allocation
12//!   and limits on window, output and work. For consumers that account their
13//!   memory themselves, such as browser bundles verifying a download.
14//! * [`DictionaryHandle`] — pre-parsed dictionary handle. Parse the
15//!   dictionary bytes once with [`DictionaryHandle::decode_dict`] and reuse
16//!   the handle across every subsequent decode; saves the per-frame
17//!   dictionary parse cost when the same dictionary is used many times in a
18//!   row.
19//!
20//! Both decoders expose dictionary-aware constructors / methods,
21//! though the exact naming differs:
22//!
23//! * [`StreamingDecoder::new_with_dictionary_handle`] /
24//!   [`StreamingDecoder::new_with_dictionary_bytes`]
25//! * [`FrameDecoder::decode_all_with_dict_handle`] /
26//!   [`FrameDecoder::decode_all_with_dict_bytes`]
27//!
28//! The `_handle` variants reuse a previously parsed
29//! [`DictionaryHandle`]; the `_bytes` variants parse the dictionary
30//! per call (suitable for one-off decodes).
31//!
32//! Errors surface through [`errors::FrameDecoderError`] and the per-decoder
33//! error types in the [`errors`] submodule.
34
35pub mod errors;
36mod frame_decoder;
37mod streaming_decoder;
38#[cfg(feature = "caller-workspace")]
39#[cfg_attr(docsrs, doc(cfg(feature = "caller-workspace")))]
40pub mod workspace;
41
42pub use dictionary::{Dictionary, DictionaryHandle, MAGIC_NUM as DICTIONARY_MAGIC};
43pub use frame_decoder::{BlockDecodingStrategy, ContentChecksum, FrameDecoder};
44#[cfg(feature = "legacy")]
45#[cfg_attr(docsrs, doc(cfg(feature = "legacy")))]
46pub use legacy::LegacyVersion;
47
48/// Largest window a frame may declare before a decoder that holds the window
49/// itself refuses it, unless [`FrameDecoder::set_max_window_size`] raised the
50/// ceiling.
51///
52/// The bound is what makes decoding untrusted input safe: a frame header can
53/// ask for a window far larger than the data behind it, and honouring that
54/// would let a few bytes of input demand gigabytes of memory.
55pub use crate::common::MAXIMUM_ALLOWED_WINDOW_SIZE;
56
57/// Largest window any decode accepts, the ceiling
58/// [`FrameDecoder::set_max_window_size`] can be raised to.
59pub use crate::common::MAX_DECODER_WINDOW_SIZE;
60#[cfg(feature = "lsm")]
61pub use frame_decoder::{PartialDecode, ResumeInput, ResumeState};
62pub use streaming_decoder::{DictionaryRefused, FinishedFrame, StreamingDecoder};
63
64/// Decompressed size a frame declares in its header, as read by
65/// [`read_frame_content_size`] without decoding the frame body.
66#[derive(Copy, Clone, Debug, PartialEq, Eq)]
67pub enum FrameContentSize {
68    /// The header carried an explicit `Frame_Content_Size` field (in bytes).
69    Known(u64),
70    /// The header did not declare a content size; the true size is only
71    /// known after decoding (or from out-of-band knowledge).
72    Unknown,
73}
74
75/// Read the decompressed size a frame declares in its header, without
76/// decoding the frame body.
77///
78/// Parses only the leading frame header of `src`. Returns
79/// [`FrameContentSize::Known`] when the header carries an explicit
80/// `Frame_Content_Size`, or [`FrameContentSize::Unknown`] when it does not.
81/// This backs the C `ZSTD_getFrameContentSize` entry point, where the two
82/// variants map to a concrete size and `ZSTD_CONTENTSIZE_UNKNOWN`.
83///
84/// With the `legacy` feature a v0.5-v0.7 frame reports the size its header
85/// declares, and [`FrameContentSize::Unknown`] when it declares none or its
86/// header does not read, as libzstd does: the field is advisory in those
87/// formats.
88///
89/// # Errors
90/// Returns [`ReadFrameHeaderError`](errors::ReadFrameHeaderError) when `src`
91/// is too short to hold a header, carries a bad magic number, or begins with
92/// a skippable frame.
93///
94/// ```rust
95/// use structured_zstd::encoding::{compress_slice_to_vec, CompressionLevel};
96/// use structured_zstd::decoding::{read_frame_content_size, FrameContentSize};
97/// let frame = compress_slice_to_vec(&[42u8; 100], CompressionLevel::Default);
98/// assert_eq!(read_frame_content_size(&frame).unwrap(), FrameContentSize::Known(100));
99/// ```
100pub fn read_frame_content_size(
101    src: &[u8],
102) -> Result<FrameContentSize, errors::ReadFrameHeaderError> {
103    #[cfg(feature = "legacy")]
104    if LegacyVersion::of(src).is_some() {
105        return Ok(legacy::declared_content_size(src)
106            .map_or(FrameContentSize::Unknown, FrameContentSize::Known));
107    }
108    let (header, _consumed) = frame::read_frame_header_with_format(src, false)?;
109    Ok(if header.fcs_declared() {
110        FrameContentSize::Known(header.frame_content_size())
111    } else {
112        FrameContentSize::Unknown
113    })
114}
115
116/// Error from [`find_frame_compressed_size`].
117#[derive(Debug)]
118pub enum FrameSizeError {
119    /// The frame header could not be parsed.
120    Header(errors::ReadFrameHeaderError),
121    /// The buffer ends before the frame's blocks (or trailing checksum) are
122    /// complete.
123    Truncated,
124    /// A block declared the reserved block type, which is invalid per RFC 8878.
125    ReservedBlock,
126    /// A block declared a `Block_Size` larger than the frame's
127    /// `Block_Maximum_Size` (`min(Window_Size, 128 KiB)`), which is invalid per
128    /// RFC 8878 §3.1.1.2. Accepting it would let a corrupt frame pass a size
129    /// query and make the no-`Frame_Content_Size` decompressed-bound
130    /// under-count (each block can regenerate at most `Block_Maximum_Size`).
131    OversizedBlock,
132}
133
134/// On-disk byte length of the FIRST frame in `src` — magic number, frame
135/// header, every block, and the trailing content checksum when present —
136/// computed by walking the block headers without decoding any block body.
137///
138/// For a skippable frame, returns its full `8 + Frame_Size` length, and with
139/// the `legacy` feature a v0.5-v0.7 frame's length through its end block. This
140/// backs the C `ZSTD_findFrameCompressedSize` entry point; the returned value
141/// is the offset at which a following concatenated frame would begin.
142///
143/// # Errors
144/// [`FrameSizeError`] when the header is unreadable, the buffer is truncated
145/// mid-frame, or a block uses the reserved type.
146///
147/// ```rust
148/// use structured_zstd::encoding::{compress_slice_to_vec, CompressionLevel};
149/// use structured_zstd::decoding::find_frame_compressed_size;
150/// let frame = compress_slice_to_vec(&[5u8; 256], CompressionLevel::Default);
151/// assert_eq!(find_frame_compressed_size(&frame).unwrap(), frame.len());
152/// ```
153pub fn find_frame_compressed_size(src: &[u8]) -> Result<usize, FrameSizeError> {
154    #[cfg(feature = "legacy")]
155    if let Some(version) = LegacyVersion::of(src) {
156        return legacy::frame_size(version, src).map(|(size, _)| size);
157    }
158    let (header, header_len) = match frame::read_frame_header_with_format(src, false) {
159        Ok(parsed) => parsed,
160        // Skippable frame: magic (4) + Frame_Size field (4) + payload.
161        Err(errors::ReadFrameHeaderError::SkipFrame { length, .. }) => {
162            return 8usize
163                .checked_add(length as usize)
164                .filter(|end| *end <= src.len())
165                .ok_or(FrameSizeError::Truncated);
166        }
167        Err(e) => return Err(FrameSizeError::Header(e)),
168    };
169
170    let walk = walk_blocks(src, header_len as usize, frame_block_size_max(&header))?;
171    if header.descriptor.content_checksum_flag() {
172        walk.end
173            .checked_add(4)
174            .filter(|end| *end <= src.len())
175            .ok_or(FrameSizeError::Truncated)
176    } else {
177        Ok(walk.end)
178    }
179}
180
181/// Error from [`find_decompressed_size`].
182#[derive(Debug)]
183pub enum DecompressedSizeError {
184    /// A frame header could not be parsed.
185    Header(errors::ReadFrameHeaderError),
186    /// A frame's blocks could not be walked to the start of the next one.
187    Frame(FrameSizeError),
188    /// The declared sizes add up past `u64::MAX`.
189    Overflow,
190}
191
192/// Total decompressed size the frames in `src` declare, without decoding any
193/// of them: [`FrameContentSize::Known`] with the sum, skippable frames counting
194/// zero, or [`FrameContentSize::Unknown`] as soon as one frame does not declare
195/// its size. Backs the C `ZSTD_findDecompressedSize` entry point.
196///
197/// # Errors
198/// [`DecompressedSizeError`] when a frame is malformed or truncated, or the
199/// sizes overflow `u64`.
200///
201/// ```rust
202/// use structured_zstd::encoding::{compress_slice_to_vec, CompressionLevel};
203/// use structured_zstd::decoding::{find_decompressed_size, FrameContentSize};
204/// let mut frames = compress_slice_to_vec(&[1u8; 300], CompressionLevel::Default);
205/// frames.extend(compress_slice_to_vec(&[2u8; 200], CompressionLevel::Default));
206/// assert_eq!(find_decompressed_size(&frames).unwrap(), FrameContentSize::Known(500));
207/// ```
208pub fn find_decompressed_size(src: &[u8]) -> Result<FrameContentSize, DecompressedSizeError> {
209    let mut rest = src;
210    let mut total: u64 = 0;
211    while !rest.is_empty() {
212        // A v0.5-v0.7 frame's size is advisory, so a header too short to read
213        // passes as unknown on its own; here a tail shorter than a frame's
214        // first five bytes is an error, as upstream's `ZSTD_findDecompressedSize`
215        // walks frames only while five remain.
216        #[cfg(feature = "legacy")]
217        if rest.len() < 5 && LegacyVersion::of(rest).is_some() {
218            return Err(DecompressedSizeError::Frame(FrameSizeError::Truncated));
219        }
220        match read_frame_content_size(rest) {
221            Ok(FrameContentSize::Known(size)) => {
222                total = total
223                    .checked_add(size)
224                    .ok_or(DecompressedSizeError::Overflow)?;
225            }
226            Ok(FrameContentSize::Unknown) => return Ok(FrameContentSize::Unknown),
227            // A skippable frame regenerates nothing.
228            Err(errors::ReadFrameHeaderError::SkipFrame { .. }) => {}
229            Err(err) => return Err(DecompressedSizeError::Header(err)),
230        }
231        let consumed = find_frame_compressed_size(rest).map_err(DecompressedSizeError::Frame)?;
232        rest = &rest[consumed..];
233    }
234    Ok(FrameContentSize::Known(total))
235}
236
237/// Result of walking the block sequence of one frame (between the header and
238/// the optional trailing checksum).
239struct BlockWalk {
240    /// Offset just past the last block (before any content checksum).
241    end: usize,
242    /// Number of blocks in the frame.
243    count: u64,
244}
245
246/// `Block_Maximum_Size` for the frame: `min(Window_Size, 128 KiB)`. Per RFC
247/// 8878 §3.1.1.2 every block's `Block_Size` is bounded by this, and each block
248/// regenerates at most this many bytes. Single-segment frames omit the
249/// `Window_Descriptor`; their window equals the declared content size.
250fn frame_block_size_max(header: &frame::FrameHeader) -> usize {
251    let window_size = match header.window_descriptor() {
252        Some(desc) => {
253            let exponent = u64::from(desc >> 3);
254            let mantissa = u64::from(desc & 0x7);
255            let window_base = 1u64 << (10 + exponent);
256            window_base + (window_base / 8) * mantissa
257        }
258        None => header.frame_content_size(),
259    };
260    // The 128 KiB cap keeps the result within usize on every target.
261    window_size.min(128 * 1024) as usize
262}
263
264/// Walk the block headers of a single frame starting at `start` (the offset of
265/// the first block header), validating each fits in `src` and declares a
266/// `Block_Size` no larger than `max_block_size` (the frame's
267/// `Block_Maximum_Size`). Does not consume the trailing content checksum.
268/// Shared by [`find_frame_compressed_size`] and [`frame_decompressed_bound`] so
269/// the on-disk-size and block-count views never diverge.
270fn walk_blocks(
271    src: &[u8],
272    start: usize,
273    max_block_size: usize,
274) -> Result<BlockWalk, FrameSizeError> {
275    let mut offset = start;
276    let mut count = 0u64;
277    loop {
278        // 3-byte block header (RFC 8878 §3.1.1.2): bit0 last-block flag,
279        // bits1-2 block type, bits3-23 Block_Size.
280        let hdr = src
281            .get(offset..offset + 3)
282            .ok_or(FrameSizeError::Truncated)?;
283        let raw = u32::from(hdr[0]) | (u32::from(hdr[1]) << 8) | (u32::from(hdr[2]) << 16);
284        let last_block = (raw & 1) != 0;
285        let block_type = (raw >> 1) & 0b11;
286        let block_size = (raw >> 3) as usize;
287        // On-disk bytes following the header: RLE stores a single byte
288        // regardless of the run length; Raw/Compressed store Block_Size bytes;
289        // the reserved type is invalid.
290        let on_disk = match block_type {
291            1 => 1,              // RLE
292            0 | 2 => block_size, // Raw / Compressed
293            _ => return Err(FrameSizeError::ReservedBlock),
294        };
295        // RFC 8878 §3.1.1.2: Block_Size MUST NOT exceed Block_Maximum_Size for
296        // any block type (it bounds both the on-disk Raw/Compressed payload and
297        // the RLE/Raw regenerated size). Reject rather than accept a corrupt
298        // declaration that would otherwise pass the size query and let the
299        // no-FCS bound under-count.
300        if block_size > max_block_size {
301            return Err(FrameSizeError::OversizedBlock);
302        }
303        offset = offset
304            .checked_add(3 + on_disk)
305            .filter(|end| *end <= src.len())
306            .ok_or(FrameSizeError::Truncated)?;
307        count += 1;
308        if last_block {
309            break;
310        }
311    }
312    Ok(BlockWalk { end: offset, count })
313}
314
315/// Upper bound on the decompressed size of the FIRST frame in `src`, without
316/// decoding the body. Backs the C `ZSTD_decompressBound` (per-frame term).
317///
318/// Returns the exact size when the header declares `Frame_Content_Size`;
319/// otherwise a valid (loose) bound of `block_count * block_size_max`, where
320/// `block_size_max = min(window_size, 128 KiB)` — every block decompresses to
321/// at most that many bytes. Skippable frames contribute `0`. With the `legacy`
322/// feature a v0.5-v0.7 frame is bounded by its blocks whatever its header
323/// declares, since those formats do not hold a frame to its declared size:
324/// 128 KiB per compressed block, as libzstd bounds it, and a stored block's
325/// own size, which can be more.
326///
327/// # Errors
328/// [`FrameSizeError`] on an unreadable header, truncation, or a reserved block.
329pub fn frame_decompressed_bound(src: &[u8]) -> Result<u64, FrameSizeError> {
330    #[cfg(feature = "legacy")]
331    if let Some(version) = LegacyVersion::of(src) {
332        return legacy::frame_size(version, src).map(|(_, bound)| bound);
333    }
334    let (header, header_len) = match frame::read_frame_header_with_format(src, false) {
335        Ok(parsed) => parsed,
336        // Skippable frame contributes 0, but its full payload must be present:
337        // truncation is an error per this function's contract.
338        Err(errors::ReadFrameHeaderError::SkipFrame { length, .. }) => {
339            return 8usize
340                .checked_add(length as usize)
341                .filter(|end| *end <= src.len())
342                .map(|_| 0)
343                .ok_or(FrameSizeError::Truncated);
344        }
345        Err(e) => return Err(FrameSizeError::Header(e)),
346    };
347
348    // Walk the blocks (and the optional checksum trailer) so a truncated frame
349    // is rejected even when Frame_Content_Size is declared — without this the
350    // declared-FCS path would return a bound for an incomplete buffer. The
351    // per-frame block maximum both bounds the walk and scales the no-FCS bound.
352    let block_size_max = frame_block_size_max(&header);
353    let walk = walk_blocks(src, header_len as usize, block_size_max)?;
354    if header.descriptor.content_checksum_flag() {
355        walk.end
356            .checked_add(4)
357            .filter(|end| *end <= src.len())
358            .ok_or(FrameSizeError::Truncated)?;
359    }
360
361    if header.fcs_declared() {
362        return Ok(header.frame_content_size());
363    }
364    // Saturating is intentional here: this is an UPPER bound, so capping at the
365    // maximum representable value is the correct ceiling for a pathologically
366    // large frame, not a masked arithmetic bug. Each of `walk.count` blocks
367    // regenerates at most `block_size_max` bytes (now enforced by `walk_blocks`,
368    // so the bound can no longer be undercut by an oversized block header).
369    Ok(walk.count.saturating_mul(block_size_max as u64))
370}
371
372/// What the first four bytes of a buffer announce, read by [`frame_magic`].
373#[derive(Copy, Clone, Debug, PartialEq, Eq)]
374pub enum FrameMagic {
375    /// A Zstandard frame (RFC 8878 §3.1.1).
376    Zstd,
377    /// A skippable frame (RFC 8878 §3.1.2) with its magic variant, `0..=15`.
378    Skippable(u8),
379}
380
381/// Which frame `src` starts with, judged by its magic number alone. Backs the
382/// C `ZSTD_isFrame` / `ZSTD_isSkippableFrame`. `None` when `src` is shorter
383/// than four bytes or the magic is no frame's.
384///
385/// ```rust
386/// use structured_zstd::decoding::{frame_magic, FrameMagic};
387/// assert_eq!(frame_magic(&[0x28, 0xB5, 0x2F, 0xFD]), Some(FrameMagic::Zstd));
388/// assert_eq!(frame_magic(&[0x53, 0x2A, 0x4D, 0x18]), Some(FrameMagic::Skippable(3)));
389/// assert_eq!(frame_magic(b"zip"), None);
390/// ```
391pub fn frame_magic(src: &[u8]) -> Option<FrameMagic> {
392    let magic = u32::from_le_bytes(src.get(..4)?.try_into().ok()?);
393    if magic == crate::common::MAGIC_NUM {
394        Some(FrameMagic::Zstd)
395    } else if magic & 0xFFFF_FFF0 == crate::skippable::SKIPPABLE_MAGIC_START {
396        Some(FrameMagic::Skippable((magic & 0xF) as u8))
397    } else {
398        None
399    }
400}
401
402/// Whether `src` starts with a frame this build decodes or skips: a Zstandard
403/// frame, a skippable frame, or with the `legacy` feature a v0.5-v0.7 frame.
404/// Backs the C `ZSTD_isFrame`, which answers the same for upstream's default
405/// build.
406///
407/// ```rust
408/// use structured_zstd::decoding::is_frame;
409/// assert!(is_frame(&[0x28, 0xB5, 0x2F, 0xFD]));
410/// assert!(is_frame(&[0x50, 0x2A, 0x4D, 0x18]));
411/// assert!(!is_frame(b"zip"));
412/// ```
413pub fn is_frame(src: &[u8]) -> bool {
414    #[cfg(feature = "legacy")]
415    if LegacyVersion::of(src).is_some() {
416        return true;
417    }
418    frame_magic(src).is_some()
419}
420
421/// Bytes the compressed input must trail the end of the output by for every
422/// frame in `src` to decode in place: the compressed data sits at the end of
423/// the output buffer and the decoder writes from its start. Backs the C
424/// `ZSTD_decompressionMargin`.
425///
426/// A frame costs its header, its checksum and three bytes per block, a
427/// skippable frame its whole length, and one largest block is added on top:
428/// a block may expand to its full size before its compressed bytes are
429/// consumed (upstream `zstd_decompress.c:838-879`).
430///
431/// # Errors
432/// [`FrameSizeError`] on an unreadable header, truncation, or an invalid block.
433///
434/// ```rust
435/// use structured_zstd::encoding::{compress_slice_to_vec, CompressionLevel};
436/// use structured_zstd::decoding::decompression_margin;
437/// let frame = compress_slice_to_vec(&[9u8; 4096], CompressionLevel::Default);
438/// assert!(decompression_margin(&frame).unwrap() >= frame.len().min(4096));
439/// ```
440pub fn decompression_margin(src: &[u8]) -> Result<usize, FrameSizeError> {
441    let mut rest = src;
442    let mut margin = 0usize;
443    let mut max_block_size = 0usize;
444    while !rest.is_empty() {
445        let consumed = match frame::read_frame_header_with_format(rest, false) {
446            Ok((header, header_len)) => {
447                let block_size_max = frame_block_size_max(&header);
448                let walk = walk_blocks(rest, header_len as usize, block_size_max)?;
449                let checksum = if header.descriptor.content_checksum_flag() {
450                    4
451                } else {
452                    0
453                };
454                let end = walk
455                    .end
456                    .checked_add(checksum)
457                    .filter(|end| *end <= rest.len())
458                    .ok_or(FrameSizeError::Truncated)?;
459                // Header, block headers and checksum all lie inside
460                // `..end <= rest.len()`, so the margin never exceeds the input.
461                margin += header_len as usize + checksum + 3 * walk.count as usize;
462                max_block_size = max_block_size.max(block_size_max);
463                end
464            }
465            Err(errors::ReadFrameHeaderError::SkipFrame { .. }) => {
466                let end = find_frame_compressed_size(rest)?;
467                margin += end;
468                end
469            }
470            Err(e) => return Err(FrameSizeError::Header(e)),
471        };
472        rest = &rest[consumed..];
473    }
474    Ok(margin + max_block_size)
475}
476
477/// Smallest output buffer a block-by-block decode of a frame can run in:
478/// the window, plus room for a block written after it and split literals,
479/// capped by the content size when the frame declares one. Backs the C
480/// `ZSTD_decodingBufferSize_min`; `None` when the result does not fit `usize`.
481///
482/// The two 32-byte terms are the over-copy allowance the sequence executor's
483/// wide copies may write past a match (upstream `WILDCOPY_OVERLENGTH`,
484/// `zstd_decompress.c:1965-1986`).
485///
486/// ```rust
487/// use structured_zstd::decoding::min_decoding_buffer_size;
488/// // A 1 MiB window with no declared content size.
489/// assert_eq!(min_decoding_buffer_size(1 << 20, None), Some((1 << 20) + 2 * 131_072 + 64));
490/// // A 1 KiB window holds no block over 1 KiB, so both block terms shrink.
491/// assert_eq!(min_decoding_buffer_size(1 << 10, None), Some((1 << 10) + 2 * 1024 + 64));
492/// // A frame whose whole content is smaller than that.
493/// assert_eq!(min_decoding_buffer_size(1 << 20, Some(1000)), Some(1000));
494/// ```
495pub fn min_decoding_buffer_size(
496    window_size: u64,
497    frame_content_size: Option<u64>,
498) -> Option<usize> {
499    const WILDCOPY_OVERLENGTH: u64 = 32;
500    // Both block terms are the largest block the frame can hold, not a fixed
501    // 128 KiB, since no block exceeds the window: RFC 8878 3.1.1.2.4,
502    // Block_Maximum_Size = min(Window_Size, 128 KiB); upstream
503    // zstd_decompress.c:1972-1980 computes window + 2 * that block + 64.
504    let block_size = window_size.min(crate::common::MAX_BLOCK_SIZE as u64);
505    let needed = window_size.checked_add(2 * block_size + 2 * WILDCOPY_OVERLENGTH);
506    let size = match (frame_content_size, needed) {
507        (Some(content), Some(needed)) => content.min(needed),
508        // A window past `u64` still decodes in a buffer the content fits.
509        (Some(content), None) => content,
510        (None, Some(needed)) => needed,
511        (None, None) => return None,
512    };
513    usize::try_from(size).ok()
514}
515
516/// Frame header fields decoded by [`read_frame_header_info`], mirroring the
517/// values the C `ZSTD_getFrameHeader` fills into a `ZSTD_FrameHeader`.
518#[derive(Copy, Clone, Debug)]
519pub struct FrameHeaderInfo {
520    /// Declared decompressed size, or [`FrameContentSize::Unknown`] when the
521    /// header omits the `Frame_Content_Size` field.
522    pub content_size: FrameContentSize,
523    /// Decoder window size in bytes (the minimum buffer needed to decode the
524    /// frame). For single-segment frames this equals the content size.
525    pub window_size: u64,
526    /// Dictionary id required to decode the frame, if the header carries one.
527    pub dictionary_id: Option<u32>,
528    /// Whether a 32-bit content checksum trails the frame.
529    pub content_checksum: bool,
530    /// Header length in bytes, measured in the parsed input format: it includes
531    /// the 4-byte magic number in the default format, but excludes it when
532    /// parsed as magicless (`read_frame_header_info(.., true)`), since those 4
533    /// bytes are not present on the wire in that mode.
534    pub header_size: usize,
535}
536
537/// Length in bytes of the frame header at the start of `src`, including the
538/// 4-byte magic number (the offset at which the first block begins). Backs the
539/// C `ZSTD_frameHeaderSize`.
540///
541/// Only the magic number and the descriptor byte are needed: the length
542/// follows from the descriptor alone, so a caller holding just that five-byte
543/// prefix learns how much of the header is still to read. Upstream's
544/// `ZSTD_frameHeaderSize` measures it the same way
545/// (`zstd_decompress.c:416-429`), without looking at the descriptor's other
546/// bits, so a header this library refuses to decode is still measured. With
547/// the `legacy` feature a v0.5-v0.7 header is measured by its own format's
548/// size fields, as [`LegacyVersion::header_size`] does.
549///
550/// # Errors
551/// [`ReadFrameHeaderError`](errors::ReadFrameHeaderError) when `src` is shorter
552/// than five bytes, has a bad magic number, or is a skippable frame.
553///
554/// ```rust
555/// use structured_zstd::decoding::frame_header_size;
556/// // Magic number and a descriptor announcing a window byte only.
557/// assert_eq!(frame_header_size(&[0x28, 0xB5, 0x2F, 0xFD, 0x00]).unwrap(), 6);
558/// ```
559pub fn frame_header_size(src: &[u8]) -> Result<usize, errors::ReadFrameHeaderError> {
560    // A v0.5-v0.7 header is measured by its own format's size fields. Upstream
561    // reads every header with RFC 8878's layout, which only v0.7 shares.
562    #[cfg(feature = "legacy")]
563    if let Some(version) = LegacyVersion::of(src) {
564        return version
565            .header_size(src)
566            .ok_or_else(|| errors::ReadFrameHeaderError::FrameDescriptorReadError(frame::eof()));
567    }
568    frame::measure_header_from_slice(src)
569}
570
571/// [`frame_header_size`] for a frame in either format: with `magicless` the
572/// header starts at its descriptor byte and its length counts no magic
573/// number.
574///
575/// ```rust
576/// use structured_zstd::decoding::frame_header_size_in_format;
577/// // A descriptor announcing a window byte only.
578/// assert_eq!(frame_header_size_in_format(&[0x00], true).unwrap(), 2);
579/// assert_eq!(frame_header_size_in_format(&[0x28, 0xB5, 0x2F, 0xFD, 0x00], false).unwrap(), 6);
580/// ```
581///
582/// # Errors
583/// As [`frame_header_size`]; a magicless input fails only when it is empty.
584pub fn frame_header_size_in_format(
585    src: &[u8],
586    magicless: bool,
587) -> Result<usize, errors::ReadFrameHeaderError> {
588    frame::measure_header_with_format(src, magicless)
589}
590
591/// How many bytes from its start the frame header beginning `src` takes to
592/// read whole, judged from what `src` holds of it: the bytes that hold the
593/// descriptor until they are all here (five, or one without the magic
594/// number), then the header's own length, and eight for a skippable frame.
595/// A caller reading a header in pieces asks this before
596/// [`read_frame_header_info`]; upstream's `ZSTD_getFrameHeader_advanced`
597/// returns the same counts.
598///
599/// ```rust
600/// use structured_zstd::decoding::frame_header_input_needed;
601/// assert_eq!(frame_header_input_needed(&[0x28, 0xB5], false).unwrap(), 5);
602/// // Magic number and a descriptor announcing a window byte only.
603/// assert_eq!(frame_header_input_needed(&[0x28, 0xB5, 0x2F, 0xFD, 0x00], false).unwrap(), 6);
604/// assert!(frame_header_input_needed(&[0x00, 0x01], false).is_err());
605/// ```
606///
607/// # Errors
608/// [`ReadFrameHeaderError::BadMagicNumber`](errors::ReadFrameHeaderError::BadMagicNumber)
609/// when the bytes start no known magic number.
610pub fn frame_header_input_needed(
611    src: &[u8],
612    magicless: bool,
613) -> Result<usize, errors::ReadFrameHeaderError> {
614    frame::header_input_needed(src, magicless)
615}
616
617/// Decode the leading frame header fields of `src` without decoding the body.
618///
619/// Backs the C `ZSTD_getFrameHeader`. When `magicless` is `true` the 4-byte
620/// magic prefix is assumed absent (the `ZSTD_f_zstd1_magicless` format); the
621/// caller must know out-of-band that the stream is magicless. The reported
622/// [`FrameHeaderInfo::window_size`] is the raw value derived from the header
623/// (no maximum-window policy applied here; that bound is enforced at decode
624/// time), so callers see the frame's own declared window even when it exceeds
625/// a decoder limit.
626///
627/// # Errors
628/// As [`read_frame_content_size`].
629///
630/// ```rust
631/// use structured_zstd::encoding::{compress_slice_to_vec, CompressionLevel};
632/// use structured_zstd::decoding::{read_frame_header_info, FrameContentSize};
633/// let frame = compress_slice_to_vec(&[7u8; 512], CompressionLevel::Default);
634/// let info = read_frame_header_info(&frame, false).unwrap();
635/// assert_eq!(info.content_size, FrameContentSize::Known(512));
636/// assert!(info.window_size >= 512);
637/// ```
638pub fn read_frame_header_info(
639    src: &[u8],
640    magicless: bool,
641) -> Result<FrameHeaderInfo, errors::ReadFrameHeaderError> {
642    let (header, consumed) = frame::read_frame_header_with_format(src, magicless)?;
643    let content_size = if header.fcs_declared() {
644        FrameContentSize::Known(header.frame_content_size())
645    } else {
646        FrameContentSize::Unknown
647    };
648    // Compute the window size without the decode-time maximum-window check
649    // (RFC 8878 §3.1.1.1.2). `window_descriptor()` returns `None` for a
650    // single-segment frame, where the window equals the content size.
651    let window_size = match header.window_descriptor() {
652        Some(desc) => {
653            let exponent = u64::from(desc >> 3);
654            let mantissa = u64::from(desc & 0x7);
655            let window_base = 1u64 << (10 + exponent);
656            window_base + (window_base / 8) * mantissa
657        }
658        None => header.frame_content_size(),
659    };
660    Ok(FrameHeaderInfo {
661        content_size,
662        window_size,
663        dictionary_id: header.dictionary_id(),
664        content_checksum: header.descriptor.content_checksum_flag(),
665        header_size: consumed as usize,
666    })
667}
668
669pub(crate) mod block_decoder;
670pub(crate) mod buffer_backend;
671pub(crate) mod decode_buffer;
672pub(crate) mod dictionary;
673pub(crate) mod exec_sequence_inline;
674// FlatBuf is the compile-time-monomorphised "frame fits in window"
675// backend selected via `DecodeBuffer<FlatBuf>`. `FrameDecoder`'s
676// `DecoderScratchKind` picks it when the frame header has
677// `Single_Segment_flag` set; the ring backend remains the default
678// for multi-segment frames. See backlog item #132 for the wiring
679// rationale.
680pub(crate) mod flat_buf;
681pub(crate) mod frame;
682#[cfg(feature = "legacy")]
683pub(crate) mod legacy;
684pub(crate) mod literals_section_decoder;
685pub(crate) mod prefetch;
686mod ringbuffer;
687#[allow(dead_code)]
688pub(crate) mod scratch;
689// Per-kernel monolithic sequence-section decoder entry points. Each
690// kernel has its own self-contained function with the full pipeline
691// (outer init, both arms, decode_one, execute_one) inlined inside one
692// `#[target_feature]`-scoped body. The dispatcher in
693// `sequence_section_decoder::decode_and_execute_sequences` selects the
694// kernel ONCE per call via cached `detect_cpu_kernel`. aarch64 Neon
695// and Sve still go through the K-generic
696// `decode_and_execute_sequences_impl` shared body until their own
697// monoliths land.
698//
699// The shared helpers (`decode_and_execute_sequences_impl`,
700// `run_pipelined_sequence_loop`, `decode_one_sequence_inline`, the
701// `execute_one_sequence_pipelined*` wrappers) live on aarch64
702// (Neon/Sve dispatch arms in `decode_and_execute_sequences`) and in
703// tests, but are orphan on x86_64 production builds where the
704// per-kernel monoliths bypass them entirely. Each carries
705// `#[allow(dead_code)]` so the `-D warnings` clippy gate stays green
706// on x86_64 without losing the cross-arch reuse. The vestigial
707// `_bmi2`/`_avx2`/`_vbmi2` variants are pre-R12 macro-dispatch
708// helpers with no remaining callers; they should be cleaned up in
709// a follow-up PR once the per-kernel monolithic shape is fully
710// settled.
711#[cfg(all(target_arch = "x86_64", feature = "kernel-avx2"))]
712pub(crate) mod seq_decoder_avx2;
713#[cfg(all(target_arch = "x86_64", feature = "kernel-bmi2"))]
714pub(crate) mod seq_decoder_bmi2;
715pub(crate) mod seq_decoder_scalar;
716#[cfg(all(target_arch = "x86_64", feature = "kernel-vbmi2"))]
717pub(crate) mod seq_decoder_vbmi2;
718pub(crate) mod sequence_execution;
719pub(crate) mod sequence_section_decoder;
720pub(crate) mod simd_copy;
721/// Diagnostic-only re-export of the copy-shape histogram counters. Public
722/// only when the `copy-shape-stats` feature is on (off in shipping builds).
723#[cfg(feature = "copy-shape-stats")]
724pub use simd_copy::shape_stats;
725// `UserSliceBackend` is the compile-time-monomorphised backend that
726// writes directly into the caller's `&mut [u8]` output slice, used
727// by the `FrameDecoder::decode_all` direct-decode path. It
728// eliminates the `FlatBuf` drain copy + anonymous-page-fault cost
729// on large literal sections. Wiring happens via
730// `DecodeBuffer<UserSliceBackend<'a>>`; the lifetime binds the
731// backend to the caller's slice for the call duration.
732pub(crate) mod user_slice_buf;
733
734#[cfg(feature = "bench-internals")]
735pub(crate) use self::simd_copy::copy_bytes_overshooting_for_bench;
736
737#[cfg(test)]
738mod block_api_tests;
739#[cfg(test)]
740mod frame_inspection_tests;