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