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}