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};
37pub use frame_decoder::{BlockDecodingStrategy, ContentChecksum, FrameDecoder};
38#[cfg(feature = "lsm")]
39pub use frame_decoder::{PartialDecode, ResumeInput, ResumeState};
40pub use streaming_decoder::StreamingDecoder;
41
42/// Decompressed size a frame declares in its header, as read by
43/// [`read_frame_content_size`] without decoding the frame body.
44#[derive(Copy, Clone, Debug, PartialEq, Eq)]
45pub enum FrameContentSize {
46    /// The header carried an explicit `Frame_Content_Size` field (in bytes).
47    Known(u64),
48    /// The header did not declare a content size; the true size is only
49    /// known after decoding (or from out-of-band knowledge).
50    Unknown,
51}
52
53/// Read the decompressed size a frame declares in its header, without
54/// decoding the frame body.
55///
56/// Parses only the leading frame header of `src`. Returns
57/// [`FrameContentSize::Known`] when the header carries an explicit
58/// `Frame_Content_Size`, or [`FrameContentSize::Unknown`] when it does not.
59/// This backs the C `ZSTD_getFrameContentSize` entry point, where the two
60/// variants map to a concrete size and `ZSTD_CONTENTSIZE_UNKNOWN`.
61///
62/// # Errors
63/// Returns [`ReadFrameHeaderError`](errors::ReadFrameHeaderError) when `src`
64/// is too short to hold a header, carries a bad magic number, or begins with
65/// a skippable frame.
66///
67/// ```rust
68/// use structured_zstd::encoding::{compress_slice_to_vec, CompressionLevel};
69/// use structured_zstd::decoding::{read_frame_content_size, FrameContentSize};
70/// let frame = compress_slice_to_vec(&[42u8; 100], CompressionLevel::Default);
71/// assert_eq!(read_frame_content_size(&frame).unwrap(), FrameContentSize::Known(100));
72/// ```
73pub fn read_frame_content_size(
74    src: &[u8],
75) -> Result<FrameContentSize, errors::ReadFrameHeaderError> {
76    let (header, _consumed) = frame::read_frame_header_with_format(src, false)?;
77    Ok(if header.fcs_declared() {
78        FrameContentSize::Known(header.frame_content_size())
79    } else {
80        FrameContentSize::Unknown
81    })
82}
83
84/// Error from [`find_frame_compressed_size`].
85#[derive(Debug)]
86pub enum FrameSizeError {
87    /// The frame header could not be parsed.
88    Header(errors::ReadFrameHeaderError),
89    /// The buffer ends before the frame's blocks (or trailing checksum) are
90    /// complete.
91    Truncated,
92    /// A block declared the reserved block type, which is invalid per RFC 8878.
93    ReservedBlock,
94    /// A block declared a `Block_Size` larger than the frame's
95    /// `Block_Maximum_Size` (`min(Window_Size, 128 KiB)`), which is invalid per
96    /// RFC 8878 §3.1.1.2. Accepting it would let a corrupt frame pass a size
97    /// query and make the no-`Frame_Content_Size` decompressed-bound
98    /// under-count (each block can regenerate at most `Block_Maximum_Size`).
99    OversizedBlock,
100}
101
102/// On-disk byte length of the FIRST frame in `src` — magic number, frame
103/// header, every block, and the trailing content checksum when present —
104/// computed by walking the block headers without decoding any block body.
105///
106/// For a skippable frame, returns its full `8 + Frame_Size` length. This backs
107/// the C `ZSTD_findFrameCompressedSize` entry point; the returned value is the
108/// offset at which a following concatenated frame would begin.
109///
110/// # Errors
111/// [`FrameSizeError`] when the header is unreadable, the buffer is truncated
112/// mid-frame, or a block uses the reserved type.
113///
114/// ```rust
115/// use structured_zstd::encoding::{compress_slice_to_vec, CompressionLevel};
116/// use structured_zstd::decoding::find_frame_compressed_size;
117/// let frame = compress_slice_to_vec(&[5u8; 256], CompressionLevel::Default);
118/// assert_eq!(find_frame_compressed_size(&frame).unwrap(), frame.len());
119/// ```
120pub fn find_frame_compressed_size(src: &[u8]) -> Result<usize, FrameSizeError> {
121    let (header, header_len) = match frame::read_frame_header_with_format(src, false) {
122        Ok(parsed) => parsed,
123        // Skippable frame: magic (4) + Frame_Size field (4) + payload.
124        Err(errors::ReadFrameHeaderError::SkipFrame { length, .. }) => {
125            return 8usize
126                .checked_add(length as usize)
127                .filter(|end| *end <= src.len())
128                .ok_or(FrameSizeError::Truncated);
129        }
130        Err(e) => return Err(FrameSizeError::Header(e)),
131    };
132
133    let walk = walk_blocks(src, header_len as usize, frame_block_size_max(&header))?;
134    if header.descriptor.content_checksum_flag() {
135        walk.end
136            .checked_add(4)
137            .filter(|end| *end <= src.len())
138            .ok_or(FrameSizeError::Truncated)
139    } else {
140        Ok(walk.end)
141    }
142}
143
144/// Result of walking the block sequence of one frame (between the header and
145/// the optional trailing checksum).
146struct BlockWalk {
147    /// Offset just past the last block (before any content checksum).
148    end: usize,
149    /// Number of blocks in the frame.
150    count: u64,
151}
152
153/// `Block_Maximum_Size` for the frame: `min(Window_Size, 128 KiB)`. Per RFC
154/// 8878 §3.1.1.2 every block's `Block_Size` is bounded by this, and each block
155/// regenerates at most this many bytes. Single-segment frames omit the
156/// `Window_Descriptor`; their window equals the declared content size.
157fn frame_block_size_max(header: &frame::FrameHeader) -> usize {
158    let window_size = match header.window_descriptor() {
159        Some(desc) => {
160            let exponent = u64::from(desc >> 3);
161            let mantissa = u64::from(desc & 0x7);
162            let window_base = 1u64 << (10 + exponent);
163            window_base + (window_base / 8) * mantissa
164        }
165        None => header.frame_content_size(),
166    };
167    // The 128 KiB cap keeps the result within usize on every target.
168    window_size.min(128 * 1024) as usize
169}
170
171/// Walk the block headers of a single frame starting at `start` (the offset of
172/// the first block header), validating each fits in `src` and declares a
173/// `Block_Size` no larger than `max_block_size` (the frame's
174/// `Block_Maximum_Size`). Does not consume the trailing content checksum.
175/// Shared by [`find_frame_compressed_size`] and [`frame_decompressed_bound`] so
176/// the on-disk-size and block-count views never diverge.
177fn walk_blocks(
178    src: &[u8],
179    start: usize,
180    max_block_size: usize,
181) -> Result<BlockWalk, FrameSizeError> {
182    let mut offset = start;
183    let mut count = 0u64;
184    loop {
185        // 3-byte block header (RFC 8878 §3.1.1.2): bit0 last-block flag,
186        // bits1-2 block type, bits3-23 Block_Size.
187        let hdr = src
188            .get(offset..offset + 3)
189            .ok_or(FrameSizeError::Truncated)?;
190        let raw = u32::from(hdr[0]) | (u32::from(hdr[1]) << 8) | (u32::from(hdr[2]) << 16);
191        let last_block = (raw & 1) != 0;
192        let block_type = (raw >> 1) & 0b11;
193        let block_size = (raw >> 3) as usize;
194        // On-disk bytes following the header: RLE stores a single byte
195        // regardless of the run length; Raw/Compressed store Block_Size bytes;
196        // the reserved type is invalid.
197        let on_disk = match block_type {
198            1 => 1,              // RLE
199            0 | 2 => block_size, // Raw / Compressed
200            _ => return Err(FrameSizeError::ReservedBlock),
201        };
202        // RFC 8878 §3.1.1.2: Block_Size MUST NOT exceed Block_Maximum_Size for
203        // any block type (it bounds both the on-disk Raw/Compressed payload and
204        // the RLE/Raw regenerated size). Reject rather than accept a corrupt
205        // declaration that would otherwise pass the size query and let the
206        // no-FCS bound under-count.
207        if block_size > max_block_size {
208            return Err(FrameSizeError::OversizedBlock);
209        }
210        offset = offset
211            .checked_add(3 + on_disk)
212            .filter(|end| *end <= src.len())
213            .ok_or(FrameSizeError::Truncated)?;
214        count += 1;
215        if last_block {
216            break;
217        }
218    }
219    Ok(BlockWalk { end: offset, count })
220}
221
222/// Upper bound on the decompressed size of the FIRST frame in `src`, without
223/// decoding the body. Backs the C `ZSTD_decompressBound` (per-frame term).
224///
225/// Returns the exact size when the header declares `Frame_Content_Size`;
226/// otherwise a valid (loose) bound of `block_count * block_size_max`, where
227/// `block_size_max = min(window_size, 128 KiB)` — every block decompresses to
228/// at most that many bytes. Skippable frames contribute `0`.
229///
230/// # Errors
231/// [`FrameSizeError`] on an unreadable header, truncation, or a reserved block.
232pub fn frame_decompressed_bound(src: &[u8]) -> Result<u64, FrameSizeError> {
233    let (header, header_len) = match frame::read_frame_header_with_format(src, false) {
234        Ok(parsed) => parsed,
235        // Skippable frame contributes 0, but its full payload must be present:
236        // truncation is an error per this function's contract.
237        Err(errors::ReadFrameHeaderError::SkipFrame { length, .. }) => {
238            return 8usize
239                .checked_add(length as usize)
240                .filter(|end| *end <= src.len())
241                .map(|_| 0)
242                .ok_or(FrameSizeError::Truncated);
243        }
244        Err(e) => return Err(FrameSizeError::Header(e)),
245    };
246
247    // Walk the blocks (and the optional checksum trailer) so a truncated frame
248    // is rejected even when Frame_Content_Size is declared — without this the
249    // declared-FCS path would return a bound for an incomplete buffer. The
250    // per-frame block maximum both bounds the walk and scales the no-FCS bound.
251    let block_size_max = frame_block_size_max(&header);
252    let walk = walk_blocks(src, header_len as usize, block_size_max)?;
253    if header.descriptor.content_checksum_flag() {
254        walk.end
255            .checked_add(4)
256            .filter(|end| *end <= src.len())
257            .ok_or(FrameSizeError::Truncated)?;
258    }
259
260    if header.fcs_declared() {
261        return Ok(header.frame_content_size());
262    }
263    // Saturating is intentional here: this is an UPPER bound, so capping at the
264    // maximum representable value is the correct ceiling for a pathologically
265    // large frame, not a masked arithmetic bug. Each of `walk.count` blocks
266    // regenerates at most `block_size_max` bytes (now enforced by `walk_blocks`,
267    // so the bound can no longer be undercut by an oversized block header).
268    Ok(walk.count.saturating_mul(block_size_max as u64))
269}
270
271/// Frame header fields decoded by [`read_frame_header_info`], mirroring the
272/// values the C `ZSTD_getFrameHeader` fills into a `ZSTD_FrameHeader`.
273#[derive(Copy, Clone, Debug)]
274pub struct FrameHeaderInfo {
275    /// Declared decompressed size, or [`FrameContentSize::Unknown`] when the
276    /// header omits the `Frame_Content_Size` field.
277    pub content_size: FrameContentSize,
278    /// Decoder window size in bytes (the minimum buffer needed to decode the
279    /// frame). For single-segment frames this equals the content size.
280    pub window_size: u64,
281    /// Dictionary id required to decode the frame, if the header carries one.
282    pub dictionary_id: Option<u32>,
283    /// Whether a 32-bit content checksum trails the frame.
284    pub content_checksum: bool,
285    /// Header length in bytes, measured in the parsed input format: it includes
286    /// the 4-byte magic number in the default format, but excludes it when
287    /// parsed as magicless (`read_frame_header_info(.., true)`), since those 4
288    /// bytes are not present on the wire in that mode.
289    pub header_size: usize,
290}
291
292/// Length in bytes of the frame header at the start of `src`, including the
293/// 4-byte magic number (the offset at which the first block begins). Backs the
294/// C `ZSTD_frameHeaderSize`.
295///
296/// # Errors
297/// [`ReadFrameHeaderError`](errors::ReadFrameHeaderError) when the header is
298/// too short, has a bad magic number, or is a skippable frame.
299pub fn frame_header_size(src: &[u8]) -> Result<usize, errors::ReadFrameHeaderError> {
300    let (_header, consumed) = frame::read_frame_header_with_format(src, false)?;
301    Ok(consumed as usize)
302}
303
304/// Decode the leading frame header fields of `src` without decoding the body.
305///
306/// Backs the C `ZSTD_getFrameHeader`. When `magicless` is `true` the 4-byte
307/// magic prefix is assumed absent (the `ZSTD_f_zstd1_magicless` format); the
308/// caller must know out-of-band that the stream is magicless. The reported
309/// [`FrameHeaderInfo::window_size`] is the raw value derived from the header
310/// (no maximum-window policy applied here; that bound is enforced at decode
311/// time), so callers see the frame's own declared window even when it exceeds
312/// a decoder limit.
313///
314/// # Errors
315/// As [`read_frame_content_size`].
316///
317/// ```rust
318/// use structured_zstd::encoding::{compress_slice_to_vec, CompressionLevel};
319/// use structured_zstd::decoding::{read_frame_header_info, FrameContentSize};
320/// let frame = compress_slice_to_vec(&[7u8; 512], CompressionLevel::Default);
321/// let info = read_frame_header_info(&frame, false).unwrap();
322/// assert_eq!(info.content_size, FrameContentSize::Known(512));
323/// assert!(info.window_size >= 512);
324/// ```
325pub fn read_frame_header_info(
326    src: &[u8],
327    magicless: bool,
328) -> Result<FrameHeaderInfo, errors::ReadFrameHeaderError> {
329    let (header, consumed) = frame::read_frame_header_with_format(src, magicless)?;
330    let content_size = if header.fcs_declared() {
331        FrameContentSize::Known(header.frame_content_size())
332    } else {
333        FrameContentSize::Unknown
334    };
335    // Compute the window size without the decode-time maximum-window check
336    // (RFC 8878 §3.1.1.1.2). `window_descriptor()` returns `None` for a
337    // single-segment frame, where the window equals the content size.
338    let window_size = match header.window_descriptor() {
339        Some(desc) => {
340            let exponent = u64::from(desc >> 3);
341            let mantissa = u64::from(desc & 0x7);
342            let window_base = 1u64 << (10 + exponent);
343            window_base + (window_base / 8) * mantissa
344        }
345        None => header.frame_content_size(),
346    };
347    Ok(FrameHeaderInfo {
348        content_size,
349        window_size,
350        dictionary_id: header.dictionary_id(),
351        content_checksum: header.descriptor.content_checksum_flag(),
352        header_size: consumed as usize,
353    })
354}
355
356pub(crate) mod block_decoder;
357pub(crate) mod buffer_backend;
358pub(crate) mod decode_buffer;
359pub(crate) mod dictionary;
360pub(crate) mod exec_sequence_inline;
361// FlatBuf is the compile-time-monomorphised "frame fits in window"
362// backend selected via `DecodeBuffer<FlatBuf>`. `FrameDecoder`'s
363// `DecoderScratchKind` picks it when the frame header has
364// `Single_Segment_flag` set; the ring backend remains the default
365// for multi-segment frames. See backlog item #132 for the wiring
366// rationale.
367pub(crate) mod flat_buf;
368pub(crate) mod frame;
369pub(crate) mod literals_section_decoder;
370pub(crate) mod prefetch;
371mod ringbuffer;
372#[allow(dead_code)]
373pub(crate) mod scratch;
374// Per-kernel monolithic sequence-section decoder entry points. Each
375// kernel has its own self-contained function with the full pipeline
376// (outer init, both arms, decode_one, execute_one) inlined inside one
377// `#[target_feature]`-scoped body. The dispatcher in
378// `sequence_section_decoder::decode_and_execute_sequences` selects the
379// kernel ONCE per call via cached `detect_cpu_kernel`. aarch64 Neon
380// and Sve still go through the K-generic
381// `decode_and_execute_sequences_impl` shared body until their own
382// monoliths land.
383//
384// The shared helpers (`decode_and_execute_sequences_impl`,
385// `run_pipelined_sequence_loop`, `decode_one_sequence_inline`, the
386// `execute_one_sequence_pipelined*` wrappers) live on aarch64
387// (Neon/Sve dispatch arms in `decode_and_execute_sequences`) and in
388// tests, but are orphan on x86_64 production builds where the
389// per-kernel monoliths bypass them entirely. Each carries
390// `#[allow(dead_code)]` so the `-D warnings` clippy gate stays green
391// on x86_64 without losing the cross-arch reuse. The vestigial
392// `_bmi2`/`_avx2`/`_vbmi2` variants are pre-R12 macro-dispatch
393// helpers with no remaining callers; they should be cleaned up in
394// a follow-up PR once the per-kernel monolithic shape is fully
395// settled.
396#[cfg(all(target_arch = "x86_64", feature = "kernel_avx2"))]
397pub(crate) mod seq_decoder_avx2;
398#[cfg(all(target_arch = "x86_64", feature = "kernel_bmi2"))]
399pub(crate) mod seq_decoder_bmi2;
400pub(crate) mod seq_decoder_scalar;
401#[cfg(all(target_arch = "x86_64", feature = "kernel_vbmi2"))]
402pub(crate) mod seq_decoder_vbmi2;
403pub(crate) mod sequence_execution;
404pub(crate) mod sequence_section_decoder;
405pub(crate) mod simd_copy;
406/// Diagnostic-only re-export of the copy-shape histogram counters. Public
407/// only when the `copy_shape_stats` feature is on (off in shipping builds).
408#[cfg(feature = "copy_shape_stats")]
409pub use simd_copy::shape_stats;
410// `UserSliceBackend` is the compile-time-monomorphised backend that
411// writes directly into the caller's `&mut [u8]` output slice, used
412// by the `FrameDecoder::decode_all` direct-decode path. It
413// eliminates the `FlatBuf` drain copy + anonymous-page-fault cost
414// on large literal sections. Wiring happens via
415// `DecodeBuffer<UserSliceBackend<'a>>`; the lifetime binds the
416// backend to the caller's slice for the call duration.
417pub(crate) mod user_slice_buf;
418
419#[cfg(feature = "bench_internals")]
420pub(crate) use self::simd_copy::copy_bytes_overshooting_for_bench;
421
422#[cfg(test)]
423mod frame_inspection_tests {
424    use super::{
425        FrameContentSize, FrameSizeError, find_frame_compressed_size, frame_decompressed_bound,
426        frame_header_size, read_frame_content_size, read_frame_header_info,
427    };
428    use crate::encoding::{CompressionLevel, compress_slice_to_vec};
429    use alloc::vec;
430    use alloc::vec::Vec;
431
432    fn frame(content: &[u8]) -> Vec<u8> {
433        compress_slice_to_vec(content, CompressionLevel::Default)
434    }
435
436    /// A hand-built single raw-block frame that omits `Frame_Content_Size`
437    /// (descriptor `0x00`: FCS_Flag=0, Single_Segment=0) so it carries a
438    /// Window_Descriptor instead — the only way to exercise the window-size
439    /// fallback in [`frame_decompressed_bound`] / [`read_frame_header_info`],
440    /// which the encoder (always declaring FCS) never produces.
441    fn no_fcs_frame() -> Vec<u8> {
442        vec![
443            0x28, 0xB5, 0x2F, 0xFD, // magic
444            0x00, // frame header descriptor: no FCS, multi-segment, no checksum
445            0x00, // window descriptor -> windowLog 10 -> 1024 bytes
446            0x19, 0x00, 0x00, // block header: last, raw, size 3
447            0xAA, 0xBB, 0xCC, // raw payload
448        ]
449    }
450
451    /// As [`no_fcs_frame`] but with the Content_Checksum_flag (descriptor bit 2)
452    /// set and a 4-byte trailer appended, to cover the checksum-trailer branch.
453    fn no_fcs_checksum_frame() -> Vec<u8> {
454        vec![
455            0x28, 0xB5, 0x2F, 0xFD, 0x04, // descriptor: checksum flag set
456            0x00, 0x19, 0x00, 0x00, 0xAA, 0xBB, 0xCC, // window + block + payload
457            0xDE, 0xAD, 0xBE, 0xEF, // content checksum trailer
458        ]
459    }
460
461    /// A skippable frame: magic `0x184D2A50`, 4-byte length, then `length` bytes.
462    fn skippable_frame(payload: &[u8]) -> Vec<u8> {
463        let mut f = vec![0x50, 0x2A, 0x4D, 0x18];
464        f.extend_from_slice(&(payload.len() as u32).to_le_bytes());
465        f.extend_from_slice(payload);
466        f
467    }
468
469    #[test]
470    fn read_frame_content_size_reports_declared_size() {
471        let f = frame(&[42u8; 100]);
472        assert_eq!(
473            read_frame_content_size(&f).unwrap(),
474            FrameContentSize::Known(100)
475        );
476    }
477
478    #[test]
479    fn read_frame_content_size_reports_unknown_without_fcs() {
480        assert_eq!(
481            read_frame_content_size(&no_fcs_frame()).unwrap(),
482            FrameContentSize::Unknown
483        );
484    }
485
486    #[test]
487    fn read_frame_content_size_errors_on_garbage() {
488        assert!(read_frame_content_size(&[0xAB; 16]).is_err());
489    }
490
491    #[test]
492    fn find_frame_compressed_size_spans_one_frame_then_the_next() {
493        let first = frame(&[5u8; 256]);
494        assert_eq!(find_frame_compressed_size(&first).unwrap(), first.len());
495
496        let mut two = first.clone();
497        two.extend_from_slice(&frame(&[9u8; 50]));
498        // Still reports only the first frame so a caller can step forward.
499        assert_eq!(find_frame_compressed_size(&two).unwrap(), first.len());
500    }
501
502    #[test]
503    fn find_frame_compressed_size_measures_skippable_frame() {
504        let skip = skippable_frame(&[1, 2, 3, 4]);
505        assert_eq!(find_frame_compressed_size(&skip).unwrap(), skip.len());
506    }
507
508    #[test]
509    fn find_frame_compressed_size_rejects_truncation() {
510        let f = frame(&[7u8; 512]);
511        // Drop the trailing block bytes mid-frame.
512        let err = find_frame_compressed_size(&f[..f.len() - 4]).unwrap_err();
513        assert!(matches!(err, FrameSizeError::Truncated));
514    }
515
516    #[test]
517    fn frame_header_size_matches_first_block_offset() {
518        let f = frame(&[3u8; 2048]);
519        let hdr = frame_header_size(&f).unwrap();
520        assert!((5..=18).contains(&hdr));
521        assert!(frame_header_size(&[0u8; 2]).is_err());
522    }
523
524    #[test]
525    fn read_frame_header_info_fills_declared_fields() {
526        let f = frame(&[7u8; 512]);
527        let info = read_frame_header_info(&f, false).unwrap();
528        assert_eq!(info.content_size, FrameContentSize::Known(512));
529        assert!(info.window_size >= 512);
530        assert_eq!(info.dictionary_id, None);
531    }
532
533    #[test]
534    fn read_frame_header_info_derives_window_without_fcs() {
535        let info = read_frame_header_info(&no_fcs_frame(), false).unwrap();
536        assert_eq!(info.content_size, FrameContentSize::Unknown);
537        assert_eq!(info.window_size, 1024);
538    }
539
540    #[test]
541    fn frame_decompressed_bound_returns_declared_size() {
542        let f = frame(&[4u8; 4096]);
543        assert_eq!(frame_decompressed_bound(&f).unwrap(), 4096);
544    }
545
546    #[test]
547    fn frame_decompressed_bound_uses_block_bound_without_fcs() {
548        // No declared FCS -> block_count(1) * block_size_max(min(1024,128K)).
549        assert_eq!(frame_decompressed_bound(&no_fcs_frame()).unwrap(), 1024);
550    }
551
552    #[test]
553    fn frame_decompressed_bound_accepts_present_checksum_trailer() {
554        assert_eq!(
555            frame_decompressed_bound(&no_fcs_checksum_frame()).unwrap(),
556            1024
557        );
558    }
559
560    #[test]
561    fn frame_decompressed_bound_rejects_missing_checksum_trailer() {
562        let mut f = no_fcs_checksum_frame();
563        f.truncate(f.len() - 4); // drop the 4-byte trailer the descriptor promises
564        assert!(matches!(
565            frame_decompressed_bound(&f).unwrap_err(),
566            FrameSizeError::Truncated
567        ));
568    }
569
570    /// A block header may declare a `Block_Size` larger than the frame's
571    /// `Block_Maximum_Size` (`min(window, 128 KiB)`). RFC 8878 forbids this;
572    /// accepting it lets a corrupt frame pass the size query and makes the
573    /// no-FCS decompressed bound under-count (the raw block regenerates more
574    /// bytes than `block_count * block_size_max`). Both helpers must reject it.
575    #[test]
576    fn size_helpers_reject_oversized_block_header() {
577        // Window 1024 (WD 0x00) -> Block_Maximum_Size = 1024. Declare a raw
578        // block of Block_Size 2000 with all 2000 payload bytes present, so the
579        // failure is the oversized declaration, not truncation.
580        let block_size = 2000usize;
581        let raw = ((block_size as u32) << 3) | 1; // last_block flag, Raw type (00)
582        let mut f = vec![
583            0x28,
584            0xB5,
585            0x2F,
586            0xFD, // magic
587            0x00, // descriptor: no FCS, multi-segment, no checksum
588            0x00, // window descriptor -> 1024 bytes
589            (raw & 0xFF) as u8,
590            ((raw >> 8) & 0xFF) as u8,
591            ((raw >> 16) & 0xFF) as u8,
592        ];
593        f.resize(f.len() + block_size, 0xAB);
594
595        assert!(matches!(
596            find_frame_compressed_size(&f).unwrap_err(),
597            FrameSizeError::OversizedBlock
598        ));
599        assert!(matches!(
600            frame_decompressed_bound(&f).unwrap_err(),
601            FrameSizeError::OversizedBlock
602        ));
603    }
604
605    #[test]
606    fn frame_decompressed_bound_handles_skippable_frame() {
607        assert_eq!(
608            frame_decompressed_bound(&skippable_frame(&[0u8; 8])).unwrap(),
609            0
610        );
611        // A skippable frame whose advertised payload is absent is truncation.
612        let mut short = skippable_frame(&[0u8; 8]);
613        short.truncate(short.len() - 2);
614        assert!(matches!(
615            frame_decompressed_bound(&short).unwrap_err(),
616            FrameSizeError::Truncated
617        ));
618    }
619
620    #[test]
621    fn frame_decompressed_bound_errors_on_garbage_header() {
622        assert!(frame_decompressed_bound(&[0xAB; 16]).is_err());
623    }
624}