Skip to main content

structured_zstd/
skippable.rs

1//! Typed Rust API for zstd skippable frames (RFC 8878 §3.1).
2//!
3//! Skippable frames carry an arbitrary application payload alongside
4//! a zstd data stream. Spec layout, byte-compatible with donor
5//! `ZSTD_writeSkippableFrame`
6//! (`lib/compress/zstd_compress.c:4751-4763` in zstd v1.5.7):
7//!
8//! ```text
9//! +----------+-----------+----------------+
10//! | 4 bytes  | 4 bytes   | payload bytes  |
11//! | magic LE | length LE | (size = length)|
12//! +----------+-----------+----------------+
13//! ```
14//!
15//! - `magic = 0x184D2A50 + magic_variant`, with `magic_variant` in
16//!   `0..=15` — 16 application-claimed magic numbers in the
17//!   skippable-magic range `0x184D2A50..=0x184D2A5F`.
18//! - `length` is the payload byte count as a little-endian `u32`,
19//!   so payloads above `u32::MAX` are not representable on the wire
20//!   (the validation in [`SkippableFrame::new`] / [`write_skippable_frame`]
21//!   surfaces this as [`SkippableFrameError::PayloadTooLarge`]).
22//!
23//! # Primary use case
24//!
25//! Embedded metadata sidecars in storage formats. The first canonical
26//! consumer is the lsm-tree v1 encrypted wire format
27//! (<https://github.com/structured-world/coordinode-lsm-tree>), which
28//! stacks `MetadataFrame` / `BodyFrame` / `EccFrame` skippable frames
29//! around an inner zstd frame. Any storage-format author needing to
30//! interleave metadata with zstd data can use the same shape — the
31//! API takes a generic `magic_variant: u8` and leaves the per-variant
32//! semantics to the application.
33//!
34//! # Magic variant allocation policy
35//!
36//! Magic variants `0x184D2A50..=0x184D2A5F` are an **application-protocol**
37//! concern, NOT a structured-zstd concern. This crate accepts
38//! `magic_variant: u8` in `0..=15` and validates only that bound. No
39//! per-variant constants are baked into the source — applications are
40//! responsible for documenting which variants they claim and
41//! coordinating with other ecosystem consumers to avoid collisions.
42
43extern crate alloc;
44
45use alloc::vec::Vec;
46
47use crate::io::{Error, Read, Write};
48
49/// First magic number in the skippable-frame range (RFC 8878 §3.1.2).
50/// Variants 0..=15 correspond to magics in `[0x184D2A50, 0x184D2A5F]`.
51pub const SKIPPABLE_MAGIC_START: u32 = 0x184D_2A50;
52
53/// Number of bytes the skippable-frame header occupies on the wire:
54/// 4 bytes magic + 4 bytes length.
55pub const SKIPPABLE_HEADER_SIZE: usize = 8;
56
57/// Upper bound on the variant nibble. Variants are constrained to the
58/// low 4 bits of the magic number so [`SKIPPABLE_MAGIC_START`] +
59/// `variant` stays inside the spec's `0x184D2A50..=0x184D2A5F` band.
60pub const SKIPPABLE_MAGIC_MAX_VARIANT: u8 = 15;
61
62/// A typed skippable-frame value.
63///
64/// Construct via [`SkippableFrame::new`] (validates the variant bound
65/// and payload size up front) or [`SkippableFrame::decode_from`].
66/// Round-trip a frame via [`SkippableFrame::encode_into`].
67#[derive(Debug, Clone, PartialEq, Eq)]
68pub struct SkippableFrame {
69    magic_variant: u8,
70    payload: Vec<u8>,
71}
72
73impl SkippableFrame {
74    /// Build a `SkippableFrame` from its components. Validates:
75    /// - `magic_variant <= 15`
76    ///   ([`SkippableFrameError::InvalidMagicVariant`]).
77    /// - `payload.len() <= u32::MAX as usize`
78    ///   ([`SkippableFrameError::PayloadTooLarge`]) — unreachable on
79    ///   32-bit and smaller targets but enforced uniformly so 64-bit
80    ///   callers cannot smuggle through an overlong payload.
81    pub fn new(magic_variant: u8, payload: Vec<u8>) -> Result<Self, SkippableFrameError> {
82        validate_magic_variant(magic_variant)?;
83        validate_payload_size(payload.len())?;
84        Ok(Self {
85            magic_variant,
86            payload,
87        })
88    }
89
90    /// The 4-bit variant nibble. Combined with [`SKIPPABLE_MAGIC_START`]
91    /// to form the on-wire magic number (`magic = START + variant`).
92    pub fn magic_variant(&self) -> u8 {
93        self.magic_variant
94    }
95
96    /// Full 32-bit magic number this frame serialises with.
97    pub fn magic_number(&self) -> u32 {
98        SKIPPABLE_MAGIC_START + u32::from(self.magic_variant)
99    }
100
101    /// Payload bytes carried by the frame (without the 8-byte header).
102    pub fn payload(&self) -> &[u8] {
103        &self.payload
104    }
105
106    /// Move the payload out, consuming the frame.
107    pub fn into_payload(self) -> Vec<u8> {
108        self.payload
109    }
110
111    /// Total serialised size of this frame on the wire:
112    /// `payload.len() + 8` (8 = 4-byte magic + 4-byte length).
113    pub fn serialized_size(&self) -> usize {
114        self.payload.len() + SKIPPABLE_HEADER_SIZE
115    }
116
117    /// Serialise this frame into `writer`. Writes
118    /// `serialized_size()` bytes total: 4-byte magic LE,
119    /// 4-byte length LE, payload bytes.
120    pub fn encode_into<W: Write>(&self, writer: &mut W) -> Result<(), Error> {
121        write_skippable_frame_to(self.magic_variant, &self.payload, writer).map(|_| ())
122    }
123
124    /// Read one skippable frame from `reader`. Consumes
125    /// 4-byte magic + 4-byte length + `length` payload bytes. The
126    /// caller is responsible for positioning the reader at a frame
127    /// boundary; this method does not scan past unknown content.
128    ///
129    /// Three layers of protection against crafted-`length` DoS:
130    ///
131    /// 1. Validates that `length` is representable on the target
132    ///    pointer width (`length + SKIPPABLE_HEADER_SIZE` must not
133    ///    overflow `usize`). On 32-bit targets a wire `length` near
134    ///    `u32::MAX` would otherwise overflow `serialized_size()` and
135    ///    `write_skippable_frame_to`. Returns
136    ///    [`DecodeSkippableFrameError::PayloadTooLarge`] up front.
137    ///
138    /// 2. Reserves the address space via [`Vec::try_reserve_exact`],
139    ///    converting alloc-failure into typed
140    ///    [`DecodeSkippableFrameError::AllocationFailed`] instead of
141    ///    process abort.
142    ///
143    /// 3. Reads the payload in fixed-size chunks via a stack scratch
144    ///    buffer, so the OS only commits pages for bytes the reader
145    ///    actually delivers. A crafted `length` near `u32::MAX` on a
146    ///    reader that terminates early surfaces as
147    ///    `DecodeSkippableFrameError::Payload` without ever
148    ///    committing the full allocation — on OSes with memory
149    ///    overcommit (Linux default) where step 2 would otherwise
150    ///    succeed for any nominal size, this is what makes the
151    ///    "no abort on huge length" guarantee actually reliable.
152    ///
153    /// Callers handling untrusted streams should additionally cap
154    /// the acceptable payload size at the application layer; this
155    /// method itself imposes no upper bound beyond the wire-format
156    /// `u32::MAX` plus target-representability.
157    pub fn decode_from<R: Read>(reader: &mut R) -> Result<Self, DecodeSkippableFrameError> {
158        let mut magic_buf = [0u8; 4];
159        reader
160            .read_exact(&mut magic_buf)
161            .map_err(DecodeSkippableFrameError::Magic)?;
162        let magic_number = u32::from_le_bytes(magic_buf);
163
164        let variant = magic_number.wrapping_sub(SKIPPABLE_MAGIC_START);
165        if !(0..=u32::from(SKIPPABLE_MAGIC_MAX_VARIANT)).contains(&variant) {
166            return Err(DecodeSkippableFrameError::BadMagicNumber(magic_number));
167        }
168
169        let mut len_buf = [0u8; 4];
170        reader
171            .read_exact(&mut len_buf)
172            .map_err(DecodeSkippableFrameError::Length)?;
173        let length_u32 = u32::from_le_bytes(len_buf);
174
175        // Convert the wire-format u32 length to `usize` via
176        // `TryFrom` (NOT `as usize`). On 16-bit pointer-width
177        // targets (e.g. MSP430) the bare `as usize` would silently
178        // truncate any value above `u16::MAX`, leaving the
179        // subsequent allocation + `read_exact` to consume far fewer
180        // bytes than the wire declared and leaving the reader
181        // mis-aligned at a junk position in the stream. Surface
182        // unrepresentable lengths as `PayloadTooLarge` BEFORE any
183        // allocation. The error variant carries the raw wire-format
184        // `u32` so the diagnostic reports the declared value
185        // verbatim — no narrowing cast where it would matter most
186        // (the 16-bit target).
187        let length = usize::try_from(length_u32)
188            .map_err(|_| DecodeSkippableFrameError::PayloadTooLarge { length: length_u32 })?;
189
190        // Reject lengths that the `new()` / `write_skippable_frame()`
191        // path would also reject up front. On 32-bit targets this
192        // catches `length + SKIPPABLE_HEADER_SIZE` overflowing
193        // `usize` when the declared length sits near `u32::MAX`.
194        // On 64-bit the check is a no-op (every u32 length is
195        // representable). On 16-bit the upstream `try_from` already
196        // rejected everything above `u16::MAX`, so this is also
197        // a no-op there.
198        if length.checked_add(SKIPPABLE_HEADER_SIZE).is_none() {
199            return Err(DecodeSkippableFrameError::PayloadTooLarge { length: length_u32 });
200        }
201
202        let mut payload: Vec<u8> = Vec::new();
203        payload
204            .try_reserve_exact(length)
205            .map_err(|_| DecodeSkippableFrameError::AllocationFailed { requested: length })?;
206
207        // Read in chunks via a stack scratch buffer instead of
208        // `resize(length, 0) + read_exact(&mut payload)`. The
209        // resize-then-read path eagerly zero-fills the entire
210        // address range up front, which on overcommit OSes
211        // (Linux default) triggers the OOM killer the moment the
212        // crafted-`length` worth of pages get committed — even
213        // though `try_reserve_exact` succeeded earlier. Chunked
214        // reads commit pages only as the reader delivers bytes,
215        // so a 4 GiB-declared payload on a 12-byte stream commits
216        // ~one page, surfaces `Payload`, and exits.
217        // 1 KiB scratch — small enough to live comfortably on a
218        // Cortex-M0 4 KiB default stack while still amortising the
219        // per-read overhead vs byte-by-byte reads. Larger sizes
220        // (16 KiB) realistically overflow small-stack embedded
221        // targets that this crate explicitly supports via the
222        // no-std + alloc build.
223        const CHUNK: usize = 1024;
224        let mut scratch = [0u8; CHUNK];
225        let mut remaining = length;
226        while remaining > 0 {
227            let take = remaining.min(CHUNK);
228            reader
229                .read_exact(&mut scratch[..take])
230                .map_err(DecodeSkippableFrameError::Payload)?;
231            payload.extend_from_slice(&scratch[..take]);
232            remaining -= take;
233        }
234
235        Ok(Self {
236            magic_variant: variant as u8,
237            payload,
238        })
239    }
240}
241
242/// Free function for callers that want to write a skippable frame
243/// directly into a sink without constructing a temporary
244/// [`SkippableFrame`]. Shape mirrors donor
245/// `ZSTD_writeSkippableFrame(dst, dstCapacity, src, srcSize,
246/// magicVariant)` — same validation, same byte-level output.
247///
248/// On success returns the number of bytes written
249/// (`payload.len() + 8`).
250pub fn write_skippable_frame<W: Write>(
251    magic_variant: u8,
252    payload: &[u8],
253    writer: &mut W,
254) -> Result<usize, SkippableFrameError> {
255    validate_magic_variant(magic_variant)?;
256    validate_payload_size(payload.len())?;
257    write_skippable_frame_to(magic_variant, payload, writer).map_err(SkippableFrameError::Io)
258}
259
260/// Internal raw writer. Skips validation (caller must have validated
261/// `magic_variant` and `payload.len()` first) and propagates raw I/O
262/// errors. Used by both the typed [`SkippableFrame::encode_into`] and
263/// the free [`write_skippable_frame`].
264fn write_skippable_frame_to<W: Write>(
265    magic_variant: u8,
266    payload: &[u8],
267    writer: &mut W,
268) -> Result<usize, Error> {
269    let magic = SKIPPABLE_MAGIC_START + u32::from(magic_variant);
270    let length = payload.len() as u32;
271
272    writer.write_all(&magic.to_le_bytes())?;
273    writer.write_all(&length.to_le_bytes())?;
274    writer.write_all(payload)?;
275    Ok(payload.len() + SKIPPABLE_HEADER_SIZE)
276}
277
278#[inline]
279fn validate_magic_variant(magic_variant: u8) -> Result<(), SkippableFrameError> {
280    if magic_variant > SKIPPABLE_MAGIC_MAX_VARIANT {
281        Err(SkippableFrameError::InvalidMagicVariant(magic_variant))
282    } else {
283        Ok(())
284    }
285}
286
287#[inline]
288fn validate_payload_size(len: usize) -> Result<(), SkippableFrameError> {
289    // The on-wire length field is u32; payloads beyond u32::MAX are
290    // not representable. The `as u64` cast is needed to compare on
291    // 32-bit targets where `u32::MAX as usize == usize::MAX` and the
292    // condition trivially folds away (correct: no payload on 32-bit
293    // can exceed the limit).
294    if (len as u64) > u64::from(u32::MAX) {
295        return Err(SkippableFrameError::PayloadTooLarge(len));
296    }
297    // On 32-bit targets `usize` IS `u32` so the wire-format limit
298    // (`u32::MAX`) is identical to `usize::MAX`. Computing the total
299    // serialised size as `len + SKIPPABLE_HEADER_SIZE` would then
300    // overflow `usize` when `len` sits at the wire-format ceiling.
301    // Reject those borderline-sized payloads up front so
302    // `serialized_size()` and `write_skippable_frame_to` stay
303    // unconditionally panic-free across target widths.
304    if len.checked_add(SKIPPABLE_HEADER_SIZE).is_none() {
305        return Err(SkippableFrameError::PayloadTooLarge(len));
306    }
307    Ok(())
308}
309
310/// Errors surfaced when constructing or writing a [`SkippableFrame`].
311#[derive(Debug)]
312#[non_exhaustive]
313pub enum SkippableFrameError {
314    /// `magic_variant` outside the spec's `0..=15` range.
315    InvalidMagicVariant(u8),
316    /// `payload.len()` exceeds `u32::MAX`, the on-wire length field
317    /// width, OR would overflow `usize` when combined with the
318    /// 8-byte skippable-frame header (32-bit targets).
319    PayloadTooLarge(usize),
320    /// Underlying I/O error from the writer.
321    Io(Error),
322}
323
324/// Errors surfaced when reading a [`SkippableFrame`] from a stream.
325#[derive(Debug)]
326#[non_exhaustive]
327pub enum DecodeSkippableFrameError {
328    /// I/O error while reading the 4-byte magic prefix.
329    Magic(Error),
330    /// First 4 bytes are not a skippable-frame magic in the
331    /// `0x184D2A50..=0x184D2A5F` range.
332    BadMagicNumber(u32),
333    /// I/O error while reading the 4-byte length field.
334    Length(Error),
335    /// I/O error while reading the payload bytes.
336    Payload(Error),
337    /// Allocation of the payload buffer failed (e.g. a crafted
338    /// length field requested more memory than is available).
339    /// `requested` is the byte count the on-wire length field
340    /// asked for.
341    AllocationFailed { requested: usize },
342    /// Wire-format `length` field is not representable on this
343    /// target's `usize` width: either `usize::try_from(length)`
344    /// fails outright (16-bit targets where the declared length
345    /// exceeds `u16::MAX`) or `length + SKIPPABLE_HEADER_SIZE`
346    /// would overflow `usize` (32-bit targets where the declared
347    /// length sits near `u32::MAX`). On 64-bit every u32 length
348    /// is representable and this variant is unreachable.
349    ///
350    /// `length` is the raw wire-format `u32` value from the
351    /// length field — preserved exactly so callers can diagnose
352    /// what the stream declared, without any narrowing cast.
353    PayloadTooLarge { length: u32 },
354}
355
356impl core::fmt::Display for SkippableFrameError {
357    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
358        match self {
359            Self::InvalidMagicVariant(v) => {
360                write!(
361                    f,
362                    "skippable frame magic_variant {v} out of range 0..={}",
363                    SKIPPABLE_MAGIC_MAX_VARIANT
364                )
365            }
366            Self::PayloadTooLarge(n) => write!(
367                f,
368                "skippable frame payload size {n} not representable: either exceeds u32::MAX (wire-format length-field ceiling) or overflows usize when combined with the 8-byte header (32-bit targets)"
369            ),
370            Self::Io(e) => write!(f, "skippable frame I/O error: {e}"),
371        }
372    }
373}
374
375#[cfg(feature = "std")]
376impl std::error::Error for SkippableFrameError {
377    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
378        match self {
379            Self::Io(e) => Some(e),
380            Self::InvalidMagicVariant(_) | Self::PayloadTooLarge(_) => None,
381        }
382    }
383}
384
385impl From<Error> for SkippableFrameError {
386    fn from(value: Error) -> Self {
387        Self::Io(value)
388    }
389}
390
391impl core::fmt::Display for DecodeSkippableFrameError {
392    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
393        match self {
394            Self::Magic(e) => write!(f, "skippable frame: error reading magic number: {e}"),
395            Self::BadMagicNumber(m) => write!(
396                f,
397                "skippable frame: magic 0x{m:08X} is not in the skippable range 0x184D2A50..=0x184D2A5F"
398            ),
399            Self::Length(e) => write!(f, "skippable frame: error reading length field: {e}"),
400            Self::Payload(e) => write!(f, "skippable frame: error reading payload bytes: {e}"),
401            Self::AllocationFailed { requested } => write!(
402                f,
403                "skippable frame: failed to allocate {requested} bytes for payload"
404            ),
405            Self::PayloadTooLarge { length } => write!(
406                f,
407                "skippable frame: declared length {length} not representable on this target (length > usize::MAX on 16-bit, or length + 8 byte header overflows usize on 32-bit)"
408            ),
409        }
410    }
411}
412
413#[cfg(feature = "std")]
414impl std::error::Error for DecodeSkippableFrameError {
415    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
416        match self {
417            Self::Magic(e) | Self::Length(e) | Self::Payload(e) => Some(e),
418            Self::BadMagicNumber(_)
419            | Self::AllocationFailed { .. }
420            | Self::PayloadTooLarge { .. } => None,
421        }
422    }
423}
424
425#[cfg(all(test, feature = "std"))]
426mod tests {
427    use super::*;
428
429    fn build_donor_skippable(magic_variant: u8, payload: &[u8]) -> Vec<u8> {
430        // Donor `ZSTD_writeSkippableFrame` (zstd v1.5.7
431        // `lib/compress/zstd_compress.c:4751-4763`) emits exactly
432        // `4-byte LE magic || 4-byte LE size || payload`. Mirror that
433        // here as the byte-parity oracle. Re-implementing the donor
434        // layout in the test (rather than calling out to zstd-sys)
435        // keeps this test independent of the dev-dep wiring and
436        // makes the parity expectation visible inline.
437        let magic = (SKIPPABLE_MAGIC_START + u32::from(magic_variant)).to_le_bytes();
438        let size = (payload.len() as u32).to_le_bytes();
439        let mut out = Vec::with_capacity(payload.len() + SKIPPABLE_HEADER_SIZE);
440        out.extend_from_slice(&magic);
441        out.extend_from_slice(&size);
442        out.extend_from_slice(payload);
443        out
444    }
445
446    #[test]
447    fn round_trip_all_sixteen_variants() {
448        for variant in 0u8..=15 {
449            let payload = alloc::vec![variant; 32 + variant as usize];
450            let frame = SkippableFrame::new(variant, payload.clone()).expect("variant in range");
451            let mut wire = Vec::new();
452            frame
453                .encode_into(&mut wire)
454                .expect("encode into Vec succeeds");
455
456            let mut cursor: &[u8] = wire.as_slice();
457            let decoded = SkippableFrame::decode_from(&mut cursor).expect("round-trip decode");
458            assert_eq!(decoded.magic_variant(), variant);
459            assert_eq!(
460                decoded.magic_number(),
461                SKIPPABLE_MAGIC_START + u32::from(variant)
462            );
463            assert_eq!(decoded.payload(), payload.as_slice());
464            assert!(
465                cursor.is_empty(),
466                "decode_from must consume exactly the frame bytes, no overshoot or undershoot"
467            );
468        }
469    }
470
471    #[test]
472    fn empty_payload_round_trips() {
473        let frame = SkippableFrame::new(7, Vec::new()).expect("empty payload OK");
474        assert_eq!(frame.serialized_size(), SKIPPABLE_HEADER_SIZE);
475
476        let mut wire = Vec::new();
477        frame.encode_into(&mut wire).unwrap();
478        assert_eq!(wire.len(), SKIPPABLE_HEADER_SIZE);
479
480        let mut cursor: &[u8] = wire.as_slice();
481        let decoded = SkippableFrame::decode_from(&mut cursor).unwrap();
482        assert!(decoded.payload().is_empty());
483        assert_eq!(decoded.magic_variant(), 7);
484    }
485
486    #[test]
487    fn large_payload_round_trips() {
488        // 1 MiB so the 4-byte length field carries a non-trivial
489        // value (0x100000) — the byte-parity test below verifies the
490        // LE serialisation explicitly.
491        let payload = alloc::vec![0xABu8; 1024 * 1024];
492        let frame = SkippableFrame::new(0, payload.clone()).unwrap();
493        let mut wire = Vec::new();
494        frame.encode_into(&mut wire).unwrap();
495        assert_eq!(wire.len(), payload.len() + SKIPPABLE_HEADER_SIZE);
496
497        let mut cursor: &[u8] = wire.as_slice();
498        let decoded = SkippableFrame::decode_from(&mut cursor).unwrap();
499        assert_eq!(decoded.payload().len(), payload.len());
500        assert!(decoded.payload() == payload.as_slice());
501    }
502
503    #[test]
504    fn new_rejects_variant_sixteen() {
505        let err = SkippableFrame::new(16, Vec::new()).expect_err("variant 16 out of range");
506        match err {
507            SkippableFrameError::InvalidMagicVariant(v) => assert_eq!(v, 16),
508            other => panic!("expected InvalidMagicVariant(16), got {other:?}"),
509        }
510    }
511
512    #[test]
513    fn new_rejects_variant_max() {
514        // u8::MAX = 255 — clearly outside the spec's 0..=15 range.
515        let err = SkippableFrame::new(255, Vec::new()).unwrap_err();
516        match err {
517            SkippableFrameError::InvalidMagicVariant(v) => assert_eq!(v, 255),
518            other => panic!("expected InvalidMagicVariant(255), got {other:?}"),
519        }
520    }
521
522    #[test]
523    fn write_function_rejects_invalid_variant() {
524        let mut sink: Vec<u8> = Vec::new();
525        let err = write_skippable_frame(16, b"x", &mut sink).unwrap_err();
526        assert!(matches!(err, SkippableFrameError::InvalidMagicVariant(16)));
527        assert!(
528            sink.is_empty(),
529            "no bytes must be written on rejected input"
530        );
531    }
532
533    #[test]
534    fn byte_parity_with_donor_layout() {
535        // For every variant + a handful of payload sizes, our output
536        // bytes must equal the donor's `ZSTD_writeSkippableFrame`
537        // layout byte-for-byte. This locks the wire-format contract
538        // against future drift.
539        for &payload_len in &[0usize, 1, 8, 256, 4096] {
540            let payload: Vec<u8> = (0..payload_len).map(|i| (i % 251) as u8).collect();
541            for variant in 0u8..=15 {
542                let expected = build_donor_skippable(variant, &payload);
543
544                let mut via_struct = Vec::new();
545                SkippableFrame::new(variant, payload.clone())
546                    .unwrap()
547                    .encode_into(&mut via_struct)
548                    .unwrap();
549                assert_eq!(
550                    via_struct, expected,
551                    "struct encode mismatch: variant={variant} len={payload_len}"
552                );
553
554                let mut via_free = Vec::new();
555                let written = write_skippable_frame(variant, &payload, &mut via_free).unwrap();
556                assert_eq!(written, expected.len());
557                assert_eq!(
558                    via_free, expected,
559                    "free-fn encode mismatch: variant={variant} len={payload_len}"
560                );
561            }
562        }
563    }
564
565    #[test]
566    fn decode_rejects_non_skippable_magic() {
567        // Zstd-1 magic 0xFD2FB528 is NOT in the skippable range.
568        let mut wire = Vec::new();
569        wire.extend_from_slice(&0xFD2F_B528u32.to_le_bytes());
570        wire.extend_from_slice(&0u32.to_le_bytes());
571        let mut cursor: &[u8] = wire.as_slice();
572        let err = SkippableFrame::decode_from(&mut cursor).unwrap_err();
573        match err {
574            DecodeSkippableFrameError::BadMagicNumber(m) => assert_eq!(m, 0xFD2F_B528),
575            other => panic!("expected BadMagicNumber, got {other:?}"),
576        }
577    }
578
579    #[test]
580    fn decode_rejects_magic_above_band() {
581        // 0x184D2A60 is one past the skippable band — must be
582        // rejected via BadMagicNumber, not silently accepted as
583        // variant 16.
584        let mut wire = Vec::new();
585        wire.extend_from_slice(&0x184D_2A60u32.to_le_bytes());
586        wire.extend_from_slice(&0u32.to_le_bytes());
587        let mut cursor: &[u8] = wire.as_slice();
588        let err = SkippableFrame::decode_from(&mut cursor).unwrap_err();
589        assert!(matches!(
590            err,
591            DecodeSkippableFrameError::BadMagicNumber(0x184D_2A60)
592        ));
593    }
594
595    #[test]
596    fn decode_truncated_magic_surfaces_typed_error() {
597        // Three bytes (one less than a magic) — must fail on the
598        // magic read step, not panic.
599        let wire = [0x50u8, 0x2A, 0x4D];
600        let mut cursor: &[u8] = &wire;
601        let err = SkippableFrame::decode_from(&mut cursor).unwrap_err();
602        assert!(
603            matches!(err, DecodeSkippableFrameError::Magic(_)),
604            "expected Magic, got {err:?}"
605        );
606    }
607
608    #[test]
609    fn decode_truncated_length_surfaces_typed_error() {
610        // Magic OK, but length field is short (3 bytes instead of 4).
611        let mut wire = Vec::new();
612        wire.extend_from_slice(&SKIPPABLE_MAGIC_START.to_le_bytes());
613        wire.extend_from_slice(&[0u8, 0, 0]);
614        let mut cursor: &[u8] = wire.as_slice();
615        let err = SkippableFrame::decode_from(&mut cursor).unwrap_err();
616        assert!(
617            matches!(err, DecodeSkippableFrameError::Length(_)),
618            "expected Length, got {err:?}"
619        );
620    }
621
622    #[test]
623    fn decode_truncated_payload_surfaces_typed_error() {
624        // Header claims 16-byte payload but only 4 bytes follow.
625        // The error must point at the PAYLOAD read step, not get
626        // misreported as a header / descriptor read.
627        let mut wire = Vec::new();
628        wire.extend_from_slice(&SKIPPABLE_MAGIC_START.to_le_bytes());
629        wire.extend_from_slice(&16u32.to_le_bytes());
630        wire.extend_from_slice(&[0u8; 4]);
631        let mut cursor: &[u8] = wire.as_slice();
632        let err = SkippableFrame::decode_from(&mut cursor).unwrap_err();
633        assert!(
634            matches!(err, DecodeSkippableFrameError::Payload(_)),
635            "expected Payload, got {err:?}"
636        );
637    }
638
639    #[test]
640    fn serialized_size_matches_encoded_length() {
641        for payload_len in [0usize, 1, 7, 8, 9, 255, 256, 1023, 1024] {
642            let payload = alloc::vec![0u8; payload_len];
643            let frame = SkippableFrame::new(3, payload).unwrap();
644            let mut wire = Vec::new();
645            frame.encode_into(&mut wire).unwrap();
646            assert_eq!(
647                wire.len(),
648                frame.serialized_size(),
649                "serialized_size() must match actual encode length for payload_len={payload_len}"
650            );
651        }
652    }
653
654    #[test]
655    fn decode_huge_length_returns_typed_error_not_oom_abort() {
656        // Crafted wire declares a u32::MAX payload but provides
657        // zero payload bytes. The decoder must surface a typed
658        // error rather than aborting the process or panicking.
659        // Three paths are acceptable, each gated by the host's
660        // ABI / allocator behaviour:
661        //
662        // - `PayloadTooLarge { length }` — 32-bit host, where
663        //   `length + 8` overflows `usize`. The decoder rejects
664        //   the length before allocating.
665        // - `AllocationFailed { requested }` — 64-bit host, no
666        //   memory overcommit (Windows / configured Linux):
667        //   `try_reserve_exact` reports failure.
668        // - `Payload(io_err)` — 64-bit host, memory overcommit
669        //   (Linux default / macOS): allocation succeeds for the
670        //   address range, chunked read on truncated stream
671        //   surfaces the I/O error after committing one page
672        //   for the scratch buffer.
673        //
674        // What it must NOT do: abort the process on OOM or panic
675        // via Vec::with_capacity / Vec::resize.
676        let huge: u32 = u32::MAX;
677        let mut wire = Vec::new();
678        wire.extend_from_slice(&SKIPPABLE_MAGIC_START.to_le_bytes());
679        wire.extend_from_slice(&huge.to_le_bytes());
680        let mut cursor: &[u8] = wire.as_slice();
681        let err = SkippableFrame::decode_from(&mut cursor).unwrap_err();
682        match err {
683            DecodeSkippableFrameError::PayloadTooLarge { length } => {
684                assert_eq!(length, huge);
685            }
686            DecodeSkippableFrameError::AllocationFailed { requested } => {
687                assert_eq!(requested, huge as usize);
688            }
689            DecodeSkippableFrameError::Payload(_) => {
690                // Chunked read on the truncated payload surfaced
691                // the I/O error after the OS overcommitted the
692                // address range. Also acceptable.
693            }
694            other => panic!("expected PayloadTooLarge / AllocationFailed / Payload, got {other:?}"),
695        }
696    }
697
698    #[test]
699    fn payload_too_large_check_branches_on_pointer_width() {
700        // The `validate_payload_size` invariant is twofold:
701        //
702        // 1. `len > u32::MAX` is rejected on every target (the
703        //    on-wire length field is u32).
704        // 2. `len + SKIPPABLE_HEADER_SIZE` overflowing `usize` is
705        //    rejected on every target. On 64-bit this is
706        //    unreachable because `u32::MAX + 8 < usize::MAX`. On
707        //    32-bit `len == u32::MAX` itself trips condition 2:
708        //    `u32::MAX + 8` wraps `usize`.
709        //
710        // Branch the boundary expectation on pointer width so the
711        // test passes on both i686 (CI cross-i686 shard) and
712        // x86_64 hosts.
713        #[cfg(target_pointer_width = "64")]
714        {
715            let result = validate_payload_size(u32::MAX as usize + 1);
716            assert!(matches!(
717                result,
718                Err(SkippableFrameError::PayloadTooLarge(_))
719            ));
720            let ok = validate_payload_size(u32::MAX as usize);
721            assert!(ok.is_ok(), "u32::MAX representable on 64-bit");
722        }
723
724        #[cfg(target_pointer_width = "32")]
725        {
726            // `u32::MAX + 1` literally cannot be expressed as
727            // `usize` on 32-bit — `u32::MAX as usize + 1` wraps
728            // to 0. So construct the test only through values
729            // that are validly representable.
730            let result = validate_payload_size(u32::MAX as usize);
731            assert!(
732                matches!(result, Err(SkippableFrameError::PayloadTooLarge(_))),
733                "u32::MAX overflows when combined with the 8-byte header on 32-bit"
734            );
735            let ok = validate_payload_size((u32::MAX as usize) - SKIPPABLE_HEADER_SIZE);
736            assert!(ok.is_ok(), "below the header-overflow boundary on 32-bit");
737        }
738    }
739}