Skip to main content

structured_zstd/decoding/
mod.rs

1//! RFC 8878 Zstandard decoder.
2//!
3//! Three entry points are exposed, each with progressively lower-level
4//! control:
5//!
6//! * [`StreamingDecoder`] — implements [`crate::io::Read`] over a compressed
7//!   byte stream, transparently parsing the frame header and concatenated
8//!   frames. The typical choice for application code.
9//! * [`FrameDecoder`] — single-frame interface; use when the caller manages
10//!   the input buffer manually (zero-copy slices, network framing, etc).
11//! * [`DictionaryHandle`] — pre-parsed dictionary handle. Parse the
12//!   dictionary bytes once with [`DictionaryHandle::decode_dict`] and reuse
13//!   the handle across every subsequent decode; saves the per-frame
14//!   dictionary parse cost when the same dictionary is used many times in a
15//!   row.
16//!
17//! Both decoders expose dictionary-aware constructors / methods,
18//! though the exact naming differs:
19//!
20//! * [`StreamingDecoder::new_with_dictionary_handle`] /
21//!   [`StreamingDecoder::new_with_dictionary_bytes`]
22//! * [`FrameDecoder::decode_all_with_dict_handle`] /
23//!   [`FrameDecoder::decode_all_with_dict_bytes`]
24//!
25//! The `_handle` variants reuse a previously parsed
26//! [`DictionaryHandle`]; the `_bytes` variants parse the dictionary
27//! per call (suitable for one-off decodes).
28//!
29//! Errors surface through [`errors::FrameDecoderError`] and the per-decoder
30//! error types in the [`errors`] submodule.
31
32pub mod errors;
33mod frame_decoder;
34mod streaming_decoder;
35
36pub use dictionary::{Dictionary, DictionaryHandle, MAGIC_NUM as DICTIONARY_MAGIC};
37pub use frame_decoder::{BlockDecodingStrategy, ContentChecksum, FrameDecoder};
38
39/// Largest window a frame may declare before a decoder that holds the window
40/// itself refuses it, unless [`FrameDecoder::set_max_window_size`] raised the
41/// ceiling.
42///
43/// The bound is what makes decoding untrusted input safe: a frame header can
44/// ask for a window far larger than the data behind it, and honouring that
45/// would let a few bytes of input demand gigabytes of memory.
46pub use crate::common::MAXIMUM_ALLOWED_WINDOW_SIZE;
47
48/// Largest window any decode accepts, the ceiling
49/// [`FrameDecoder::set_max_window_size`] can be raised to.
50pub use crate::common::MAX_DECODER_WINDOW_SIZE;
51#[cfg(feature = "lsm")]
52pub use frame_decoder::{PartialDecode, ResumeInput, ResumeState};
53pub use streaming_decoder::{DictionaryRefused, FinishedFrame, StreamingDecoder};
54
55/// Decompressed size a frame declares in its header, as read by
56/// [`read_frame_content_size`] without decoding the frame body.
57#[derive(Copy, Clone, Debug, PartialEq, Eq)]
58pub enum FrameContentSize {
59    /// The header carried an explicit `Frame_Content_Size` field (in bytes).
60    Known(u64),
61    /// The header did not declare a content size; the true size is only
62    /// known after decoding (or from out-of-band knowledge).
63    Unknown,
64}
65
66/// Read the decompressed size a frame declares in its header, without
67/// decoding the frame body.
68///
69/// Parses only the leading frame header of `src`. Returns
70/// [`FrameContentSize::Known`] when the header carries an explicit
71/// `Frame_Content_Size`, or [`FrameContentSize::Unknown`] when it does not.
72/// This backs the C `ZSTD_getFrameContentSize` entry point, where the two
73/// variants map to a concrete size and `ZSTD_CONTENTSIZE_UNKNOWN`.
74///
75/// # Errors
76/// Returns [`ReadFrameHeaderError`](errors::ReadFrameHeaderError) when `src`
77/// is too short to hold a header, carries a bad magic number, or begins with
78/// a skippable frame.
79///
80/// ```rust
81/// use structured_zstd::encoding::{compress_slice_to_vec, CompressionLevel};
82/// use structured_zstd::decoding::{read_frame_content_size, FrameContentSize};
83/// let frame = compress_slice_to_vec(&[42u8; 100], CompressionLevel::Default);
84/// assert_eq!(read_frame_content_size(&frame).unwrap(), FrameContentSize::Known(100));
85/// ```
86pub fn read_frame_content_size(
87    src: &[u8],
88) -> Result<FrameContentSize, errors::ReadFrameHeaderError> {
89    let (header, _consumed) = frame::read_frame_header_with_format(src, false)?;
90    Ok(if header.fcs_declared() {
91        FrameContentSize::Known(header.frame_content_size())
92    } else {
93        FrameContentSize::Unknown
94    })
95}
96
97/// Error from [`find_frame_compressed_size`].
98#[derive(Debug)]
99pub enum FrameSizeError {
100    /// The frame header could not be parsed.
101    Header(errors::ReadFrameHeaderError),
102    /// The buffer ends before the frame's blocks (or trailing checksum) are
103    /// complete.
104    Truncated,
105    /// A block declared the reserved block type, which is invalid per RFC 8878.
106    ReservedBlock,
107    /// A block declared a `Block_Size` larger than the frame's
108    /// `Block_Maximum_Size` (`min(Window_Size, 128 KiB)`), which is invalid per
109    /// RFC 8878 §3.1.1.2. Accepting it would let a corrupt frame pass a size
110    /// query and make the no-`Frame_Content_Size` decompressed-bound
111    /// under-count (each block can regenerate at most `Block_Maximum_Size`).
112    OversizedBlock,
113}
114
115/// On-disk byte length of the FIRST frame in `src` — magic number, frame
116/// header, every block, and the trailing content checksum when present —
117/// computed by walking the block headers without decoding any block body.
118///
119/// For a skippable frame, returns its full `8 + Frame_Size` length. This backs
120/// the C `ZSTD_findFrameCompressedSize` entry point; the returned value is the
121/// offset at which a following concatenated frame would begin.
122///
123/// # Errors
124/// [`FrameSizeError`] when the header is unreadable, the buffer is truncated
125/// mid-frame, or a block uses the reserved type.
126///
127/// ```rust
128/// use structured_zstd::encoding::{compress_slice_to_vec, CompressionLevel};
129/// use structured_zstd::decoding::find_frame_compressed_size;
130/// let frame = compress_slice_to_vec(&[5u8; 256], CompressionLevel::Default);
131/// assert_eq!(find_frame_compressed_size(&frame).unwrap(), frame.len());
132/// ```
133pub fn find_frame_compressed_size(src: &[u8]) -> Result<usize, FrameSizeError> {
134    let (header, header_len) = match frame::read_frame_header_with_format(src, false) {
135        Ok(parsed) => parsed,
136        // Skippable frame: magic (4) + Frame_Size field (4) + payload.
137        Err(errors::ReadFrameHeaderError::SkipFrame { length, .. }) => {
138            return 8usize
139                .checked_add(length as usize)
140                .filter(|end| *end <= src.len())
141                .ok_or(FrameSizeError::Truncated);
142        }
143        Err(e) => return Err(FrameSizeError::Header(e)),
144    };
145
146    let walk = walk_blocks(src, header_len as usize, frame_block_size_max(&header))?;
147    if header.descriptor.content_checksum_flag() {
148        walk.end
149            .checked_add(4)
150            .filter(|end| *end <= src.len())
151            .ok_or(FrameSizeError::Truncated)
152    } else {
153        Ok(walk.end)
154    }
155}
156
157/// Result of walking the block sequence of one frame (between the header and
158/// the optional trailing checksum).
159struct BlockWalk {
160    /// Offset just past the last block (before any content checksum).
161    end: usize,
162    /// Number of blocks in the frame.
163    count: u64,
164}
165
166/// `Block_Maximum_Size` for the frame: `min(Window_Size, 128 KiB)`. Per RFC
167/// 8878 §3.1.1.2 every block's `Block_Size` is bounded by this, and each block
168/// regenerates at most this many bytes. Single-segment frames omit the
169/// `Window_Descriptor`; their window equals the declared content size.
170fn frame_block_size_max(header: &frame::FrameHeader) -> usize {
171    let window_size = match header.window_descriptor() {
172        Some(desc) => {
173            let exponent = u64::from(desc >> 3);
174            let mantissa = u64::from(desc & 0x7);
175            let window_base = 1u64 << (10 + exponent);
176            window_base + (window_base / 8) * mantissa
177        }
178        None => header.frame_content_size(),
179    };
180    // The 128 KiB cap keeps the result within usize on every target.
181    window_size.min(128 * 1024) as usize
182}
183
184/// Walk the block headers of a single frame starting at `start` (the offset of
185/// the first block header), validating each fits in `src` and declares a
186/// `Block_Size` no larger than `max_block_size` (the frame's
187/// `Block_Maximum_Size`). Does not consume the trailing content checksum.
188/// Shared by [`find_frame_compressed_size`] and [`frame_decompressed_bound`] so
189/// the on-disk-size and block-count views never diverge.
190fn walk_blocks(
191    src: &[u8],
192    start: usize,
193    max_block_size: usize,
194) -> Result<BlockWalk, FrameSizeError> {
195    let mut offset = start;
196    let mut count = 0u64;
197    loop {
198        // 3-byte block header (RFC 8878 §3.1.1.2): bit0 last-block flag,
199        // bits1-2 block type, bits3-23 Block_Size.
200        let hdr = src
201            .get(offset..offset + 3)
202            .ok_or(FrameSizeError::Truncated)?;
203        let raw = u32::from(hdr[0]) | (u32::from(hdr[1]) << 8) | (u32::from(hdr[2]) << 16);
204        let last_block = (raw & 1) != 0;
205        let block_type = (raw >> 1) & 0b11;
206        let block_size = (raw >> 3) as usize;
207        // On-disk bytes following the header: RLE stores a single byte
208        // regardless of the run length; Raw/Compressed store Block_Size bytes;
209        // the reserved type is invalid.
210        let on_disk = match block_type {
211            1 => 1,              // RLE
212            0 | 2 => block_size, // Raw / Compressed
213            _ => return Err(FrameSizeError::ReservedBlock),
214        };
215        // RFC 8878 §3.1.1.2: Block_Size MUST NOT exceed Block_Maximum_Size for
216        // any block type (it bounds both the on-disk Raw/Compressed payload and
217        // the RLE/Raw regenerated size). Reject rather than accept a corrupt
218        // declaration that would otherwise pass the size query and let the
219        // no-FCS bound under-count.
220        if block_size > max_block_size {
221            return Err(FrameSizeError::OversizedBlock);
222        }
223        offset = offset
224            .checked_add(3 + on_disk)
225            .filter(|end| *end <= src.len())
226            .ok_or(FrameSizeError::Truncated)?;
227        count += 1;
228        if last_block {
229            break;
230        }
231    }
232    Ok(BlockWalk { end: offset, count })
233}
234
235/// Upper bound on the decompressed size of the FIRST frame in `src`, without
236/// decoding the body. Backs the C `ZSTD_decompressBound` (per-frame term).
237///
238/// Returns the exact size when the header declares `Frame_Content_Size`;
239/// otherwise a valid (loose) bound of `block_count * block_size_max`, where
240/// `block_size_max = min(window_size, 128 KiB)` — every block decompresses to
241/// at most that many bytes. Skippable frames contribute `0`.
242///
243/// # Errors
244/// [`FrameSizeError`] on an unreadable header, truncation, or a reserved block.
245pub fn frame_decompressed_bound(src: &[u8]) -> Result<u64, FrameSizeError> {
246    let (header, header_len) = match frame::read_frame_header_with_format(src, false) {
247        Ok(parsed) => parsed,
248        // Skippable frame contributes 0, but its full payload must be present:
249        // truncation is an error per this function's contract.
250        Err(errors::ReadFrameHeaderError::SkipFrame { length, .. }) => {
251            return 8usize
252                .checked_add(length as usize)
253                .filter(|end| *end <= src.len())
254                .map(|_| 0)
255                .ok_or(FrameSizeError::Truncated);
256        }
257        Err(e) => return Err(FrameSizeError::Header(e)),
258    };
259
260    // Walk the blocks (and the optional checksum trailer) so a truncated frame
261    // is rejected even when Frame_Content_Size is declared — without this the
262    // declared-FCS path would return a bound for an incomplete buffer. The
263    // per-frame block maximum both bounds the walk and scales the no-FCS bound.
264    let block_size_max = frame_block_size_max(&header);
265    let walk = walk_blocks(src, header_len as usize, block_size_max)?;
266    if header.descriptor.content_checksum_flag() {
267        walk.end
268            .checked_add(4)
269            .filter(|end| *end <= src.len())
270            .ok_or(FrameSizeError::Truncated)?;
271    }
272
273    if header.fcs_declared() {
274        return Ok(header.frame_content_size());
275    }
276    // Saturating is intentional here: this is an UPPER bound, so capping at the
277    // maximum representable value is the correct ceiling for a pathologically
278    // large frame, not a masked arithmetic bug. Each of `walk.count` blocks
279    // regenerates at most `block_size_max` bytes (now enforced by `walk_blocks`,
280    // so the bound can no longer be undercut by an oversized block header).
281    Ok(walk.count.saturating_mul(block_size_max as u64))
282}
283
284/// Frame header fields decoded by [`read_frame_header_info`], mirroring the
285/// values the C `ZSTD_getFrameHeader` fills into a `ZSTD_FrameHeader`.
286#[derive(Copy, Clone, Debug)]
287pub struct FrameHeaderInfo {
288    /// Declared decompressed size, or [`FrameContentSize::Unknown`] when the
289    /// header omits the `Frame_Content_Size` field.
290    pub content_size: FrameContentSize,
291    /// Decoder window size in bytes (the minimum buffer needed to decode the
292    /// frame). For single-segment frames this equals the content size.
293    pub window_size: u64,
294    /// Dictionary id required to decode the frame, if the header carries one.
295    pub dictionary_id: Option<u32>,
296    /// Whether a 32-bit content checksum trails the frame.
297    pub content_checksum: bool,
298    /// Header length in bytes, measured in the parsed input format: it includes
299    /// the 4-byte magic number in the default format, but excludes it when
300    /// parsed as magicless (`read_frame_header_info(.., true)`), since those 4
301    /// bytes are not present on the wire in that mode.
302    pub header_size: usize,
303}
304
305/// Length in bytes of the frame header at the start of `src`, including the
306/// 4-byte magic number (the offset at which the first block begins). Backs the
307/// C `ZSTD_frameHeaderSize`.
308///
309/// Only the magic number and the descriptor byte are needed: the length
310/// follows from the descriptor alone, so a caller holding just that five-byte
311/// prefix learns how much of the header is still to read. Upstream's
312/// `ZSTD_frameHeaderSize` measures it the same way
313/// (`zstd_decompress.c:416-429`), without looking at the descriptor's other
314/// bits, so a header this library refuses to decode is still measured.
315///
316/// # Errors
317/// [`ReadFrameHeaderError`](errors::ReadFrameHeaderError) when `src` is shorter
318/// than five bytes, has a bad magic number, or is a skippable frame.
319///
320/// ```rust
321/// use structured_zstd::decoding::frame_header_size;
322/// // Magic number and a descriptor announcing a window byte only.
323/// assert_eq!(frame_header_size(&[0x28, 0xB5, 0x2F, 0xFD, 0x00]).unwrap(), 6);
324/// ```
325pub fn frame_header_size(src: &[u8]) -> Result<usize, errors::ReadFrameHeaderError> {
326    frame::measure_header_from_slice(src)
327}
328
329/// Decode the leading frame header fields of `src` without decoding the body.
330///
331/// Backs the C `ZSTD_getFrameHeader`. When `magicless` is `true` the 4-byte
332/// magic prefix is assumed absent (the `ZSTD_f_zstd1_magicless` format); the
333/// caller must know out-of-band that the stream is magicless. The reported
334/// [`FrameHeaderInfo::window_size`] is the raw value derived from the header
335/// (no maximum-window policy applied here; that bound is enforced at decode
336/// time), so callers see the frame's own declared window even when it exceeds
337/// a decoder limit.
338///
339/// # Errors
340/// As [`read_frame_content_size`].
341///
342/// ```rust
343/// use structured_zstd::encoding::{compress_slice_to_vec, CompressionLevel};
344/// use structured_zstd::decoding::{read_frame_header_info, FrameContentSize};
345/// let frame = compress_slice_to_vec(&[7u8; 512], CompressionLevel::Default);
346/// let info = read_frame_header_info(&frame, false).unwrap();
347/// assert_eq!(info.content_size, FrameContentSize::Known(512));
348/// assert!(info.window_size >= 512);
349/// ```
350pub fn read_frame_header_info(
351    src: &[u8],
352    magicless: bool,
353) -> Result<FrameHeaderInfo, errors::ReadFrameHeaderError> {
354    let (header, consumed) = frame::read_frame_header_with_format(src, magicless)?;
355    let content_size = if header.fcs_declared() {
356        FrameContentSize::Known(header.frame_content_size())
357    } else {
358        FrameContentSize::Unknown
359    };
360    // Compute the window size without the decode-time maximum-window check
361    // (RFC 8878 §3.1.1.1.2). `window_descriptor()` returns `None` for a
362    // single-segment frame, where the window equals the content size.
363    let window_size = match header.window_descriptor() {
364        Some(desc) => {
365            let exponent = u64::from(desc >> 3);
366            let mantissa = u64::from(desc & 0x7);
367            let window_base = 1u64 << (10 + exponent);
368            window_base + (window_base / 8) * mantissa
369        }
370        None => header.frame_content_size(),
371    };
372    Ok(FrameHeaderInfo {
373        content_size,
374        window_size,
375        dictionary_id: header.dictionary_id(),
376        content_checksum: header.descriptor.content_checksum_flag(),
377        header_size: consumed as usize,
378    })
379}
380
381pub(crate) mod block_decoder;
382pub(crate) mod buffer_backend;
383pub(crate) mod decode_buffer;
384pub(crate) mod dictionary;
385pub(crate) mod exec_sequence_inline;
386// FlatBuf is the compile-time-monomorphised "frame fits in window"
387// backend selected via `DecodeBuffer<FlatBuf>`. `FrameDecoder`'s
388// `DecoderScratchKind` picks it when the frame header has
389// `Single_Segment_flag` set; the ring backend remains the default
390// for multi-segment frames. See backlog item #132 for the wiring
391// rationale.
392pub(crate) mod flat_buf;
393pub(crate) mod frame;
394pub(crate) mod literals_section_decoder;
395pub(crate) mod prefetch;
396mod ringbuffer;
397#[allow(dead_code)]
398pub(crate) mod scratch;
399// Per-kernel monolithic sequence-section decoder entry points. Each
400// kernel has its own self-contained function with the full pipeline
401// (outer init, both arms, decode_one, execute_one) inlined inside one
402// `#[target_feature]`-scoped body. The dispatcher in
403// `sequence_section_decoder::decode_and_execute_sequences` selects the
404// kernel ONCE per call via cached `detect_cpu_kernel`. aarch64 Neon
405// and Sve still go through the K-generic
406// `decode_and_execute_sequences_impl` shared body until their own
407// monoliths land.
408//
409// The shared helpers (`decode_and_execute_sequences_impl`,
410// `run_pipelined_sequence_loop`, `decode_one_sequence_inline`, the
411// `execute_one_sequence_pipelined*` wrappers) live on aarch64
412// (Neon/Sve dispatch arms in `decode_and_execute_sequences`) and in
413// tests, but are orphan on x86_64 production builds where the
414// per-kernel monoliths bypass them entirely. Each carries
415// `#[allow(dead_code)]` so the `-D warnings` clippy gate stays green
416// on x86_64 without losing the cross-arch reuse. The vestigial
417// `_bmi2`/`_avx2`/`_vbmi2` variants are pre-R12 macro-dispatch
418// helpers with no remaining callers; they should be cleaned up in
419// a follow-up PR once the per-kernel monolithic shape is fully
420// settled.
421#[cfg(all(target_arch = "x86_64", feature = "kernel-avx2"))]
422pub(crate) mod seq_decoder_avx2;
423#[cfg(all(target_arch = "x86_64", feature = "kernel-bmi2"))]
424pub(crate) mod seq_decoder_bmi2;
425pub(crate) mod seq_decoder_scalar;
426#[cfg(all(target_arch = "x86_64", feature = "kernel-vbmi2"))]
427pub(crate) mod seq_decoder_vbmi2;
428pub(crate) mod sequence_execution;
429pub(crate) mod sequence_section_decoder;
430pub(crate) mod simd_copy;
431/// Diagnostic-only re-export of the copy-shape histogram counters. Public
432/// only when the `copy-shape-stats` feature is on (off in shipping builds).
433#[cfg(feature = "copy-shape-stats")]
434pub use simd_copy::shape_stats;
435// `UserSliceBackend` is the compile-time-monomorphised backend that
436// writes directly into the caller's `&mut [u8]` output slice, used
437// by the `FrameDecoder::decode_all` direct-decode path. It
438// eliminates the `FlatBuf` drain copy + anonymous-page-fault cost
439// on large literal sections. Wiring happens via
440// `DecodeBuffer<UserSliceBackend<'a>>`; the lifetime binds the
441// backend to the caller's slice for the call duration.
442pub(crate) mod user_slice_buf;
443
444#[cfg(feature = "bench-internals")]
445pub(crate) use self::simd_copy::copy_bytes_overshooting_for_bench;
446
447#[cfg(test)]
448mod frame_inspection_tests;