structured-zstd 0.0.41

Pure Rust zstd implementation — managed fork of ruzstd. Dictionary decompression, no FFI.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
//! RFC 8878 Zstandard decoder.
//!
//! Three entry points are exposed, each with progressively lower-level
//! control:
//!
//! * [`StreamingDecoder`] — implements [`crate::io::Read`] over a compressed
//!   byte stream, transparently parsing the frame header and concatenated
//!   frames. The typical choice for application code.
//! * [`FrameDecoder`] — single-frame interface; use when the caller manages
//!   the input buffer manually (zero-copy slices, network framing, etc).
//! * [`DictionaryHandle`] — pre-parsed dictionary handle. Parse the
//!   dictionary bytes once with [`DictionaryHandle::decode_dict`] and reuse
//!   the handle across every subsequent decode; saves the per-frame
//!   dictionary parse cost when the same dictionary is used many times in a
//!   row.
//!
//! Both decoders expose dictionary-aware constructors / methods,
//! though the exact naming differs:
//!
//! * [`StreamingDecoder::new_with_dictionary_handle`] /
//!   [`StreamingDecoder::new_with_dictionary_bytes`]
//! * [`FrameDecoder::decode_all_with_dict_handle`] /
//!   [`FrameDecoder::decode_all_with_dict_bytes`]
//!
//! The `_handle` variants reuse a previously parsed
//! [`DictionaryHandle`]; the `_bytes` variants parse the dictionary
//! per call (suitable for one-off decodes).
//!
//! Errors surface through [`errors::FrameDecoderError`] and the per-decoder
//! error types in the [`errors`] submodule.

pub mod errors;
mod frame_decoder;
mod streaming_decoder;

pub use dictionary::{Dictionary, DictionaryHandle};
pub use frame_decoder::{BlockDecodingStrategy, ContentChecksum, FrameDecoder};
#[cfg(feature = "lsm")]
pub use frame_decoder::{PartialDecode, ResumeInput, ResumeState};
pub use streaming_decoder::StreamingDecoder;

/// Decompressed size a frame declares in its header, as read by
/// [`read_frame_content_size`] without decoding the frame body.
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub enum FrameContentSize {
    /// The header carried an explicit `Frame_Content_Size` field (in bytes).
    Known(u64),
    /// The header did not declare a content size; the true size is only
    /// known after decoding (or from out-of-band knowledge).
    Unknown,
}

/// Read the decompressed size a frame declares in its header, without
/// decoding the frame body.
///
/// Parses only the leading frame header of `src`. Returns
/// [`FrameContentSize::Known`] when the header carries an explicit
/// `Frame_Content_Size`, or [`FrameContentSize::Unknown`] when it does not.
/// This backs the C `ZSTD_getFrameContentSize` entry point, where the two
/// variants map to a concrete size and `ZSTD_CONTENTSIZE_UNKNOWN`.
///
/// # Errors
/// Returns [`ReadFrameHeaderError`](errors::ReadFrameHeaderError) when `src`
/// is too short to hold a header, carries a bad magic number, or begins with
/// a skippable frame.
///
/// ```rust
/// use structured_zstd::encoding::{compress_slice_to_vec, CompressionLevel};
/// use structured_zstd::decoding::{read_frame_content_size, FrameContentSize};
/// let frame = compress_slice_to_vec(&[42u8; 100], CompressionLevel::Default);
/// assert_eq!(read_frame_content_size(&frame).unwrap(), FrameContentSize::Known(100));
/// ```
pub fn read_frame_content_size(
    src: &[u8],
) -> Result<FrameContentSize, errors::ReadFrameHeaderError> {
    let (header, _consumed) = frame::read_frame_header_with_format(src, false)?;
    Ok(if header.fcs_declared() {
        FrameContentSize::Known(header.frame_content_size())
    } else {
        FrameContentSize::Unknown
    })
}

/// Error from [`find_frame_compressed_size`].
#[derive(Debug)]
pub enum FrameSizeError {
    /// The frame header could not be parsed.
    Header(errors::ReadFrameHeaderError),
    /// The buffer ends before the frame's blocks (or trailing checksum) are
    /// complete.
    Truncated,
    /// A block declared the reserved block type, which is invalid per RFC 8878.
    ReservedBlock,
    /// A block declared a `Block_Size` larger than the frame's
    /// `Block_Maximum_Size` (`min(Window_Size, 128 KiB)`), which is invalid per
    /// RFC 8878 §3.1.1.2. Accepting it would let a corrupt frame pass a size
    /// query and make the no-`Frame_Content_Size` decompressed-bound
    /// under-count (each block can regenerate at most `Block_Maximum_Size`).
    OversizedBlock,
}

/// On-disk byte length of the FIRST frame in `src` — magic number, frame
/// header, every block, and the trailing content checksum when present —
/// computed by walking the block headers without decoding any block body.
///
/// For a skippable frame, returns its full `8 + Frame_Size` length. This backs
/// the C `ZSTD_findFrameCompressedSize` entry point; the returned value is the
/// offset at which a following concatenated frame would begin.
///
/// # Errors
/// [`FrameSizeError`] when the header is unreadable, the buffer is truncated
/// mid-frame, or a block uses the reserved type.
///
/// ```rust
/// use structured_zstd::encoding::{compress_slice_to_vec, CompressionLevel};
/// use structured_zstd::decoding::find_frame_compressed_size;
/// let frame = compress_slice_to_vec(&[5u8; 256], CompressionLevel::Default);
/// assert_eq!(find_frame_compressed_size(&frame).unwrap(), frame.len());
/// ```
pub fn find_frame_compressed_size(src: &[u8]) -> Result<usize, FrameSizeError> {
    let (header, header_len) = match frame::read_frame_header_with_format(src, false) {
        Ok(parsed) => parsed,
        // Skippable frame: magic (4) + Frame_Size field (4) + payload.
        Err(errors::ReadFrameHeaderError::SkipFrame { length, .. }) => {
            return 8usize
                .checked_add(length as usize)
                .filter(|end| *end <= src.len())
                .ok_or(FrameSizeError::Truncated);
        }
        Err(e) => return Err(FrameSizeError::Header(e)),
    };

    let walk = walk_blocks(src, header_len as usize, frame_block_size_max(&header))?;
    if header.descriptor.content_checksum_flag() {
        walk.end
            .checked_add(4)
            .filter(|end| *end <= src.len())
            .ok_or(FrameSizeError::Truncated)
    } else {
        Ok(walk.end)
    }
}

/// Result of walking the block sequence of one frame (between the header and
/// the optional trailing checksum).
struct BlockWalk {
    /// Offset just past the last block (before any content checksum).
    end: usize,
    /// Number of blocks in the frame.
    count: u64,
}

/// `Block_Maximum_Size` for the frame: `min(Window_Size, 128 KiB)`. Per RFC
/// 8878 §3.1.1.2 every block's `Block_Size` is bounded by this, and each block
/// regenerates at most this many bytes. Single-segment frames omit the
/// `Window_Descriptor`; their window equals the declared content size.
fn frame_block_size_max(header: &frame::FrameHeader) -> usize {
    let window_size = match header.window_descriptor() {
        Some(desc) => {
            let exponent = u64::from(desc >> 3);
            let mantissa = u64::from(desc & 0x7);
            let window_base = 1u64 << (10 + exponent);
            window_base + (window_base / 8) * mantissa
        }
        None => header.frame_content_size(),
    };
    // The 128 KiB cap keeps the result within usize on every target.
    window_size.min(128 * 1024) as usize
}

/// Walk the block headers of a single frame starting at `start` (the offset of
/// the first block header), validating each fits in `src` and declares a
/// `Block_Size` no larger than `max_block_size` (the frame's
/// `Block_Maximum_Size`). Does not consume the trailing content checksum.
/// Shared by [`find_frame_compressed_size`] and [`frame_decompressed_bound`] so
/// the on-disk-size and block-count views never diverge.
fn walk_blocks(
    src: &[u8],
    start: usize,
    max_block_size: usize,
) -> Result<BlockWalk, FrameSizeError> {
    let mut offset = start;
    let mut count = 0u64;
    loop {
        // 3-byte block header (RFC 8878 §3.1.1.2): bit0 last-block flag,
        // bits1-2 block type, bits3-23 Block_Size.
        let hdr = src
            .get(offset..offset + 3)
            .ok_or(FrameSizeError::Truncated)?;
        let raw = u32::from(hdr[0]) | (u32::from(hdr[1]) << 8) | (u32::from(hdr[2]) << 16);
        let last_block = (raw & 1) != 0;
        let block_type = (raw >> 1) & 0b11;
        let block_size = (raw >> 3) as usize;
        // On-disk bytes following the header: RLE stores a single byte
        // regardless of the run length; Raw/Compressed store Block_Size bytes;
        // the reserved type is invalid.
        let on_disk = match block_type {
            1 => 1,              // RLE
            0 | 2 => block_size, // Raw / Compressed
            _ => return Err(FrameSizeError::ReservedBlock),
        };
        // RFC 8878 §3.1.1.2: Block_Size MUST NOT exceed Block_Maximum_Size for
        // any block type (it bounds both the on-disk Raw/Compressed payload and
        // the RLE/Raw regenerated size). Reject rather than accept a corrupt
        // declaration that would otherwise pass the size query and let the
        // no-FCS bound under-count.
        if block_size > max_block_size {
            return Err(FrameSizeError::OversizedBlock);
        }
        offset = offset
            .checked_add(3 + on_disk)
            .filter(|end| *end <= src.len())
            .ok_or(FrameSizeError::Truncated)?;
        count += 1;
        if last_block {
            break;
        }
    }
    Ok(BlockWalk { end: offset, count })
}

/// Upper bound on the decompressed size of the FIRST frame in `src`, without
/// decoding the body. Backs the C `ZSTD_decompressBound` (per-frame term).
///
/// Returns the exact size when the header declares `Frame_Content_Size`;
/// otherwise a valid (loose) bound of `block_count * block_size_max`, where
/// `block_size_max = min(window_size, 128 KiB)` — every block decompresses to
/// at most that many bytes. Skippable frames contribute `0`.
///
/// # Errors
/// [`FrameSizeError`] on an unreadable header, truncation, or a reserved block.
pub fn frame_decompressed_bound(src: &[u8]) -> Result<u64, FrameSizeError> {
    let (header, header_len) = match frame::read_frame_header_with_format(src, false) {
        Ok(parsed) => parsed,
        // Skippable frame contributes 0, but its full payload must be present:
        // truncation is an error per this function's contract.
        Err(errors::ReadFrameHeaderError::SkipFrame { length, .. }) => {
            return 8usize
                .checked_add(length as usize)
                .filter(|end| *end <= src.len())
                .map(|_| 0)
                .ok_or(FrameSizeError::Truncated);
        }
        Err(e) => return Err(FrameSizeError::Header(e)),
    };

    // Walk the blocks (and the optional checksum trailer) so a truncated frame
    // is rejected even when Frame_Content_Size is declared — without this the
    // declared-FCS path would return a bound for an incomplete buffer. The
    // per-frame block maximum both bounds the walk and scales the no-FCS bound.
    let block_size_max = frame_block_size_max(&header);
    let walk = walk_blocks(src, header_len as usize, block_size_max)?;
    if header.descriptor.content_checksum_flag() {
        walk.end
            .checked_add(4)
            .filter(|end| *end <= src.len())
            .ok_or(FrameSizeError::Truncated)?;
    }

    if header.fcs_declared() {
        return Ok(header.frame_content_size());
    }
    // Saturating is intentional here: this is an UPPER bound, so capping at the
    // maximum representable value is the correct ceiling for a pathologically
    // large frame, not a masked arithmetic bug. Each of `walk.count` blocks
    // regenerates at most `block_size_max` bytes (now enforced by `walk_blocks`,
    // so the bound can no longer be undercut by an oversized block header).
    Ok(walk.count.saturating_mul(block_size_max as u64))
}

/// Frame header fields decoded by [`read_frame_header_info`], mirroring the
/// values the C `ZSTD_getFrameHeader` fills into a `ZSTD_FrameHeader`.
#[derive(Copy, Clone, Debug)]
pub struct FrameHeaderInfo {
    /// Declared decompressed size, or [`FrameContentSize::Unknown`] when the
    /// header omits the `Frame_Content_Size` field.
    pub content_size: FrameContentSize,
    /// Decoder window size in bytes (the minimum buffer needed to decode the
    /// frame). For single-segment frames this equals the content size.
    pub window_size: u64,
    /// Dictionary id required to decode the frame, if the header carries one.
    pub dictionary_id: Option<u32>,
    /// Whether a 32-bit content checksum trails the frame.
    pub content_checksum: bool,
    /// Header length in bytes, measured in the parsed input format: it includes
    /// the 4-byte magic number in the default format, but excludes it when
    /// parsed as magicless (`read_frame_header_info(.., true)`), since those 4
    /// bytes are not present on the wire in that mode.
    pub header_size: usize,
}

/// Length in bytes of the frame header at the start of `src`, including the
/// 4-byte magic number (the offset at which the first block begins). Backs the
/// C `ZSTD_frameHeaderSize`.
///
/// # Errors
/// [`ReadFrameHeaderError`](errors::ReadFrameHeaderError) when the header is
/// too short, has a bad magic number, or is a skippable frame.
pub fn frame_header_size(src: &[u8]) -> Result<usize, errors::ReadFrameHeaderError> {
    let (_header, consumed) = frame::read_frame_header_with_format(src, false)?;
    Ok(consumed as usize)
}

/// Decode the leading frame header fields of `src` without decoding the body.
///
/// Backs the C `ZSTD_getFrameHeader`. When `magicless` is `true` the 4-byte
/// magic prefix is assumed absent (the `ZSTD_f_zstd1_magicless` format); the
/// caller must know out-of-band that the stream is magicless. The reported
/// [`FrameHeaderInfo::window_size`] is the raw value derived from the header
/// (no maximum-window policy applied here; that bound is enforced at decode
/// time), so callers see the frame's own declared window even when it exceeds
/// a decoder limit.
///
/// # Errors
/// As [`read_frame_content_size`].
///
/// ```rust
/// use structured_zstd::encoding::{compress_slice_to_vec, CompressionLevel};
/// use structured_zstd::decoding::{read_frame_header_info, FrameContentSize};
/// let frame = compress_slice_to_vec(&[7u8; 512], CompressionLevel::Default);
/// let info = read_frame_header_info(&frame, false).unwrap();
/// assert_eq!(info.content_size, FrameContentSize::Known(512));
/// assert!(info.window_size >= 512);
/// ```
pub fn read_frame_header_info(
    src: &[u8],
    magicless: bool,
) -> Result<FrameHeaderInfo, errors::ReadFrameHeaderError> {
    let (header, consumed) = frame::read_frame_header_with_format(src, magicless)?;
    let content_size = if header.fcs_declared() {
        FrameContentSize::Known(header.frame_content_size())
    } else {
        FrameContentSize::Unknown
    };
    // Compute the window size without the decode-time maximum-window check
    // (RFC 8878 §3.1.1.1.2). `window_descriptor()` returns `None` for a
    // single-segment frame, where the window equals the content size.
    let window_size = match header.window_descriptor() {
        Some(desc) => {
            let exponent = u64::from(desc >> 3);
            let mantissa = u64::from(desc & 0x7);
            let window_base = 1u64 << (10 + exponent);
            window_base + (window_base / 8) * mantissa
        }
        None => header.frame_content_size(),
    };
    Ok(FrameHeaderInfo {
        content_size,
        window_size,
        dictionary_id: header.dictionary_id(),
        content_checksum: header.descriptor.content_checksum_flag(),
        header_size: consumed as usize,
    })
}

pub(crate) mod block_decoder;
pub(crate) mod buffer_backend;
pub(crate) mod decode_buffer;
pub(crate) mod dictionary;
pub(crate) mod exec_sequence_inline;
// FlatBuf is the compile-time-monomorphised "frame fits in window"
// backend selected via `DecodeBuffer<FlatBuf>`. `FrameDecoder`'s
// `DecoderScratchKind` picks it when the frame header has
// `Single_Segment_flag` set; the ring backend remains the default
// for multi-segment frames. See backlog item #132 for the wiring
// rationale.
pub(crate) mod flat_buf;
pub(crate) mod frame;
pub(crate) mod literals_section_decoder;
pub(crate) mod prefetch;
mod ringbuffer;
#[allow(dead_code)]
pub(crate) mod scratch;
// Per-kernel monolithic sequence-section decoder entry points. Each
// kernel has its own self-contained function with the full pipeline
// (outer init, both arms, decode_one, execute_one) inlined inside one
// `#[target_feature]`-scoped body. The dispatcher in
// `sequence_section_decoder::decode_and_execute_sequences` selects the
// kernel ONCE per call via cached `detect_cpu_kernel`. aarch64 Neon
// and Sve still go through the K-generic
// `decode_and_execute_sequences_impl` shared body until their own
// monoliths land.
//
// The shared helpers (`decode_and_execute_sequences_impl`,
// `run_pipelined_sequence_loop`, `decode_one_sequence_inline`, the
// `execute_one_sequence_pipelined*` wrappers) live on aarch64
// (Neon/Sve dispatch arms in `decode_and_execute_sequences`) and in
// tests, but are orphan on x86_64 production builds where the
// per-kernel monoliths bypass them entirely. Each carries
// `#[allow(dead_code)]` so the `-D warnings` clippy gate stays green
// on x86_64 without losing the cross-arch reuse. The vestigial
// `_bmi2`/`_avx2`/`_vbmi2` variants are pre-R12 macro-dispatch
// helpers with no remaining callers; they should be cleaned up in
// a follow-up PR once the per-kernel monolithic shape is fully
// settled.
#[cfg(all(target_arch = "x86_64", feature = "kernel_avx2"))]
pub(crate) mod seq_decoder_avx2;
#[cfg(all(target_arch = "x86_64", feature = "kernel_bmi2"))]
pub(crate) mod seq_decoder_bmi2;
pub(crate) mod seq_decoder_scalar;
#[cfg(all(target_arch = "x86_64", feature = "kernel_vbmi2"))]
pub(crate) mod seq_decoder_vbmi2;
pub(crate) mod sequence_execution;
pub(crate) mod sequence_section_decoder;
pub(crate) mod simd_copy;
/// Diagnostic-only re-export of the copy-shape histogram counters. Public
/// only when the `copy_shape_stats` feature is on (off in shipping builds).
#[cfg(feature = "copy_shape_stats")]
pub use simd_copy::shape_stats;
// `UserSliceBackend` is the compile-time-monomorphised backend that
// writes directly into the caller's `&mut [u8]` output slice, used
// by the `FrameDecoder::decode_all` direct-decode path. It
// eliminates the `FlatBuf` drain copy + anonymous-page-fault cost
// on large literal sections. Wiring happens via
// `DecodeBuffer<UserSliceBackend<'a>>`; the lifetime binds the
// backend to the caller's slice for the call duration.
pub(crate) mod user_slice_buf;

#[cfg(feature = "bench_internals")]
pub(crate) use self::simd_copy::copy_bytes_overshooting_for_bench;

#[cfg(test)]
mod frame_inspection_tests {
    use super::{
        FrameContentSize, FrameSizeError, find_frame_compressed_size, frame_decompressed_bound,
        frame_header_size, read_frame_content_size, read_frame_header_info,
    };
    use crate::encoding::{CompressionLevel, compress_slice_to_vec};
    use alloc::vec;
    use alloc::vec::Vec;

    fn frame(content: &[u8]) -> Vec<u8> {
        compress_slice_to_vec(content, CompressionLevel::Default)
    }

    /// A hand-built single raw-block frame that omits `Frame_Content_Size`
    /// (descriptor `0x00`: FCS_Flag=0, Single_Segment=0) so it carries a
    /// Window_Descriptor instead — the only way to exercise the window-size
    /// fallback in [`frame_decompressed_bound`] / [`read_frame_header_info`],
    /// which the encoder (always declaring FCS) never produces.
    fn no_fcs_frame() -> Vec<u8> {
        vec![
            0x28, 0xB5, 0x2F, 0xFD, // magic
            0x00, // frame header descriptor: no FCS, multi-segment, no checksum
            0x00, // window descriptor -> windowLog 10 -> 1024 bytes
            0x19, 0x00, 0x00, // block header: last, raw, size 3
            0xAA, 0xBB, 0xCC, // raw payload
        ]
    }

    /// As [`no_fcs_frame`] but with the Content_Checksum_flag (descriptor bit 2)
    /// set and a 4-byte trailer appended, to cover the checksum-trailer branch.
    fn no_fcs_checksum_frame() -> Vec<u8> {
        vec![
            0x28, 0xB5, 0x2F, 0xFD, 0x04, // descriptor: checksum flag set
            0x00, 0x19, 0x00, 0x00, 0xAA, 0xBB, 0xCC, // window + block + payload
            0xDE, 0xAD, 0xBE, 0xEF, // content checksum trailer
        ]
    }

    /// A skippable frame: magic `0x184D2A50`, 4-byte length, then `length` bytes.
    fn skippable_frame(payload: &[u8]) -> Vec<u8> {
        let mut f = vec![0x50, 0x2A, 0x4D, 0x18];
        f.extend_from_slice(&(payload.len() as u32).to_le_bytes());
        f.extend_from_slice(payload);
        f
    }

    #[test]
    fn read_frame_content_size_reports_declared_size() {
        let f = frame(&[42u8; 100]);
        assert_eq!(
            read_frame_content_size(&f).unwrap(),
            FrameContentSize::Known(100)
        );
    }

    #[test]
    fn read_frame_content_size_reports_unknown_without_fcs() {
        assert_eq!(
            read_frame_content_size(&no_fcs_frame()).unwrap(),
            FrameContentSize::Unknown
        );
    }

    #[test]
    fn read_frame_content_size_errors_on_garbage() {
        assert!(read_frame_content_size(&[0xAB; 16]).is_err());
    }

    #[test]
    fn find_frame_compressed_size_spans_one_frame_then_the_next() {
        let first = frame(&[5u8; 256]);
        assert_eq!(find_frame_compressed_size(&first).unwrap(), first.len());

        let mut two = first.clone();
        two.extend_from_slice(&frame(&[9u8; 50]));
        // Still reports only the first frame so a caller can step forward.
        assert_eq!(find_frame_compressed_size(&two).unwrap(), first.len());
    }

    #[test]
    fn find_frame_compressed_size_measures_skippable_frame() {
        let skip = skippable_frame(&[1, 2, 3, 4]);
        assert_eq!(find_frame_compressed_size(&skip).unwrap(), skip.len());
    }

    #[test]
    fn find_frame_compressed_size_rejects_truncation() {
        let f = frame(&[7u8; 512]);
        // Drop the trailing block bytes mid-frame.
        let err = find_frame_compressed_size(&f[..f.len() - 4]).unwrap_err();
        assert!(matches!(err, FrameSizeError::Truncated));
    }

    #[test]
    fn frame_header_size_matches_first_block_offset() {
        let f = frame(&[3u8; 2048]);
        let hdr = frame_header_size(&f).unwrap();
        assert!((5..=18).contains(&hdr));
        assert!(frame_header_size(&[0u8; 2]).is_err());
    }

    #[test]
    fn read_frame_header_info_fills_declared_fields() {
        let f = frame(&[7u8; 512]);
        let info = read_frame_header_info(&f, false).unwrap();
        assert_eq!(info.content_size, FrameContentSize::Known(512));
        assert!(info.window_size >= 512);
        assert_eq!(info.dictionary_id, None);
    }

    #[test]
    fn read_frame_header_info_derives_window_without_fcs() {
        let info = read_frame_header_info(&no_fcs_frame(), false).unwrap();
        assert_eq!(info.content_size, FrameContentSize::Unknown);
        assert_eq!(info.window_size, 1024);
    }

    #[test]
    fn frame_decompressed_bound_returns_declared_size() {
        let f = frame(&[4u8; 4096]);
        assert_eq!(frame_decompressed_bound(&f).unwrap(), 4096);
    }

    #[test]
    fn frame_decompressed_bound_uses_block_bound_without_fcs() {
        // No declared FCS -> block_count(1) * block_size_max(min(1024,128K)).
        assert_eq!(frame_decompressed_bound(&no_fcs_frame()).unwrap(), 1024);
    }

    #[test]
    fn frame_decompressed_bound_accepts_present_checksum_trailer() {
        assert_eq!(
            frame_decompressed_bound(&no_fcs_checksum_frame()).unwrap(),
            1024
        );
    }

    #[test]
    fn frame_decompressed_bound_rejects_missing_checksum_trailer() {
        let mut f = no_fcs_checksum_frame();
        f.truncate(f.len() - 4); // drop the 4-byte trailer the descriptor promises
        assert!(matches!(
            frame_decompressed_bound(&f).unwrap_err(),
            FrameSizeError::Truncated
        ));
    }

    /// A block header may declare a `Block_Size` larger than the frame's
    /// `Block_Maximum_Size` (`min(window, 128 KiB)`). RFC 8878 forbids this;
    /// accepting it lets a corrupt frame pass the size query and makes the
    /// no-FCS decompressed bound under-count (the raw block regenerates more
    /// bytes than `block_count * block_size_max`). Both helpers must reject it.
    #[test]
    fn size_helpers_reject_oversized_block_header() {
        // Window 1024 (WD 0x00) -> Block_Maximum_Size = 1024. Declare a raw
        // block of Block_Size 2000 with all 2000 payload bytes present, so the
        // failure is the oversized declaration, not truncation.
        let block_size = 2000usize;
        let raw = ((block_size as u32) << 3) | 1; // last_block flag, Raw type (00)
        let mut f = vec![
            0x28,
            0xB5,
            0x2F,
            0xFD, // magic
            0x00, // descriptor: no FCS, multi-segment, no checksum
            0x00, // window descriptor -> 1024 bytes
            (raw & 0xFF) as u8,
            ((raw >> 8) & 0xFF) as u8,
            ((raw >> 16) & 0xFF) as u8,
        ];
        f.resize(f.len() + block_size, 0xAB);

        assert!(matches!(
            find_frame_compressed_size(&f).unwrap_err(),
            FrameSizeError::OversizedBlock
        ));
        assert!(matches!(
            frame_decompressed_bound(&f).unwrap_err(),
            FrameSizeError::OversizedBlock
        ));
    }

    #[test]
    fn frame_decompressed_bound_handles_skippable_frame() {
        assert_eq!(
            frame_decompressed_bound(&skippable_frame(&[0u8; 8])).unwrap(),
            0
        );
        // A skippable frame whose advertised payload is absent is truncation.
        let mut short = skippable_frame(&[0u8; 8]);
        short.truncate(short.len() - 2);
        assert!(matches!(
            frame_decompressed_bound(&short).unwrap_err(),
            FrameSizeError::Truncated
        ));
    }

    #[test]
    fn frame_decompressed_bound_errors_on_garbage_header() {
        assert!(frame_decompressed_bound(&[0xAB; 16]).is_err());
    }
}