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