Skip to main content

mnesis_store/
wire.rs

1//! Wire-format frame builder shared by `mnesis-fjall` and `mnesis-store::testing`.
2//!
3//! One canonical implementation of the on-disk frame format: a fixed
4//! header, then event-type bytes, optional metadata bytes, alignment
5//! padding, and finally payload. The padding makes the payload pointer
6//! 16-byte aligned in the resulting [`Bytes`] buffer, which is a
7//! wire-format invariant — every adapter must use [`encode_frame`] to
8//! encode frames, every decoder may rely on the alignment.
9//!
10//! Layout (V2 — the current format every frame is encoded in):
11//!
12//! ```text
13//! [u8 frame_format_version][u32 LE schema_version]
14//! [u16 LE event_type_len][u32 LE meta_len][event_type bytes]
15//! [metadata bytes if any][padding zero-bytes][payload bytes]
16//! ```
17//!
18//! `meta_len == u32::MAX` is the absent-metadata sentinel
19//! (distinguishes `None` from `Some(empty)`).
20//!
21//! V2 dropped the `[u64 LE global_seq]` field that V1 carried after the version
22//! byte: a store-local `$all` position is now adapter-defined and surfaced
23//! alongside `$all` events, not stamped into every row (#266). The leading
24//! version byte makes this evolvable — `decode_frame` still reads any V1 frame
25//! (skipping its `global_seq`) during the pre-freeze transition; the V1 decode
26//! branch is removed before the 1.0 freeze.
27//!
28//! Pipeline: [`encode_frame`] is `plan(...).map(execute)` — `plan` does the
29//! layout math (fallible only on `FrameLengthOverflow`), `execute` does
30//! the buffer fill (infallible). Each stage is independently testable.
31//!
32//! # Implicit couplings (deliberate, but worth knowing)
33//!
34//! - **The leading byte is the frame-format version.** `decode_frame`
35//!   reads it first and branches on layout; an unknown version is a
36//!   typed `DecodeError::UnsupportedFrameVersion`, never a misparse.
37//! - **Payload length is not stored.** It's derived as
38//!   `value.len() - (header + event_type + metadata + padding)`. Saves
39//!   four bytes per row but means truncation that lops bytes off the
40//!   *end* of a frame is structurally undetectable here. Storage layers
41//!   that wrap [`encode_frame`] output (fjall, snapshots) own
42//!   value-integrity guarantees.
43//! - **Decode recomputes the padding** via the same [`align_padding`]
44//!   formula the encoder used; there is no padding-length field. Any
45//!   future change to the alignment formula is a wire break — both
46//!   sides must change together.
47//!
48//! # Validation home
49//!
50//! All field invariants (`event_type` UTF-8 + size cap, payload size cap,
51//! metadata non-empty + size cap, `schema_version` > 0) are owned by the
52//! value newtypes in [`crate::value`]. By the time bytes reach
53//! [`encode_frame`], they have been validated at the value newtype
54//! boundary, so the only failure mode here is [`WireError::FrameLengthOverflow`]
55//! — pure arithmetic. On the read path, [`decode_frame`] reconstructs
56//! the `schema_version` through [`crate::value::SchemaVersion::from_u32`]
57//! so a corrupt on-disk zero surfaces as [`DecodeError::CorruptSchemaVersion`]
58//! rather than slipping through into a panic-on-conversion downstream.
59
60use aligned_vec::{AVec, ConstAlign};
61use bytes::Bytes;
62use core::ops::Range;
63use thiserror::Error;
64
65use crate::value::{EventType, Metadata, Payload, SchemaVersion};
66
67/// Payload alignment in bytes. Wire-format invariant.
68pub const PAYLOAD_ALIGN: usize = 16;
69
70/// Fixed header size in bytes (V2 — the current encode format).
71///
72/// Fields: `frame_format_version` (1), `schema_version` (4), `et_len` (2),
73/// `meta_len` (4) = 11. V1's 19 (it carried an 8-byte `global_seq`) lives in
74/// [`HEADER_FIXED_SIZE_V1`] for the decode-only transition path.
75pub(crate) const HEADER_FIXED_SIZE: usize = 11;
76
77/// Offset of the `frame_format_version` byte (same in every format version).
78pub(crate) const VERSION_OFFSET: usize = 0;
79
80/// Offset of the `schema_version` field in a V2 header.
81pub(crate) const SCHEMA_VERSION_OFFSET: usize = 1;
82
83/// Offset of the `event_type_len` field in a V2 header.
84pub(crate) const EVENT_TYPE_LEN_OFFSET: usize = 5;
85
86/// Offset of the `meta_len` field in a V2 header.
87pub(crate) const META_LEN_OFFSET: usize = 7;
88
89/// Fixed header size of a **V1** frame (decode-only, pre-freeze transition):
90/// `frame_format_version` (1) + `global_seq` (8) + `schema_version` (4)
91/// + `et_len` (2) + `meta_len` (4) = 19.
92const HEADER_FIXED_SIZE_V1: usize = 19;
93
94/// V1 fixed-field offsets (decode-only). The 8-byte `global_seq` at offset 1 is
95/// read and discarded — V2 drops it, and `DecodedFrame` no longer carries it.
96const SCHEMA_VERSION_OFFSET_V1: usize = 9;
97const EVENT_TYPE_LEN_OFFSET_V1: usize = 13;
98const META_LEN_OFFSET_V1: usize = 15;
99
100/// Sentinel `meta_len` value meaning "no metadata field present".
101pub(crate) const META_LEN_ABSENT: u32 = u32::MAX;
102
103/// Bytes needed after `offset` to reach the next multiple of `align`.
104///
105/// Returns 0 when `offset` is already aligned. `align` must be a non-zero
106/// power of two; callers pass [`PAYLOAD_ALIGN`].
107#[inline]
108const fn align_padding(offset: usize, align: usize) -> usize {
109    (align - (offset % align)) % align
110}
111
112/// On-disk frame-format version — the byte-layout tag, distinct from the
113/// per-event `schema_version`.
114///
115/// Read first by the decoder so a future layout (different alignment, a CRC,
116/// a stored payload length) can coexist with older rows. Exhaustive on purpose:
117/// adding the next variant is a compile-error-forcing one-liner here and in
118/// `decode_frame`.
119#[derive(Debug, Clone, Copy, PartialEq, Eq)]
120pub(crate) enum FrameFormatVersion {
121    /// The original layout, carrying an 8-byte `global_seq`. Decode-only
122    /// (pre-freeze transition); removed before the 1.0 freeze.
123    V1,
124    /// The current layout: `global_seq` dropped (#266). See the module diagram.
125    V2,
126}
127
128impl FrameFormatVersion {
129    /// The version every freshly-encoded frame is stamped with.
130    pub(crate) const CURRENT: Self = Self::V2;
131
132    /// On-wire byte for this version.
133    #[inline]
134    const fn to_u8(self) -> u8 {
135        match self {
136            Self::V1 => 1,
137            Self::V2 => 2,
138        }
139    }
140
141    /// Map an on-wire byte to a known version, or `None` if unrecognized.
142    /// The caller turns `None` into `DecodeError::UnsupportedFrameVersion`,
143    /// so this stays decoupled from the error type.
144    #[inline]
145    const fn from_u8(byte: u8) -> Option<Self> {
146        match byte {
147            1 => Some(Self::V1),
148            2 => Some(Self::V2),
149            _ => None,
150        }
151    }
152}
153
154// ---------------------------------------------------------------------
155// FrameHeader
156//
157// The four fixed-position V2 fields packed at the start of every frame.
158// `write_into` serializes to exactly 11 bytes (V2); `read_from` is its
159// inverse and *also* parses a V1 frame (discarding its `global_seq`).
160// Stores `event_type_len`/`metadata_len` directly as the wire-format
161// integer widths (`u16` / `Option<u32>`) — there are no length newtypes
162// to enforce caps because the value newtypes (EventType / Metadata /
163// Payload / SchemaVersion) own those invariants at construction time.
164// ---------------------------------------------------------------------
165
166/// Fixed-position frame header (11 bytes on the wire for V2).
167///
168/// Carries the header fields together so they serialize and
169/// deserialize as a unit. Use [`FrameHeader::write_into`] from the build
170/// path and [`FrameHeader::read_from`] from the decode path. Holds no
171/// `global_seq` — V2 dropped it and a V1 decode discards it.
172#[derive(Debug, Clone, Copy)]
173pub(crate) struct FrameHeader {
174    pub(crate) format_version: FrameFormatVersion,
175    pub(crate) schema_version: u32,
176    event_type_len: u16,
177    metadata_len: Option<u32>,
178}
179
180impl FrameHeader {
181    /// Header size in bytes. Matches [`HEADER_FIXED_SIZE`].
182    pub(crate) const SIZE: usize = HEADER_FIXED_SIZE;
183
184    /// Construct a header from already-validated raw lengths.
185    ///
186    /// Caller guarantees: `event_type_len` fits in `u16`, and
187    /// `metadata_len` (if `Some`) fits in `u32`. The value newtypes
188    /// (`EventType` / `Metadata`) provide these guarantees by
189    /// construction — they reject byte slices that would not fit the
190    /// wire field at their `from_bytes` constructors.
191    fn from_validated_lengths(
192        format_version: FrameFormatVersion,
193        schema_version: u32,
194        event_type_len: usize,
195        metadata_len: Option<usize>,
196    ) -> Self {
197        #[allow(
198            clippy::expect_used,
199            reason = "validated by EventType::from_bytes invariant: length ≤ u16::MAX"
200        )]
201        let event_type_len_u16 = u16::try_from(event_type_len)
202            .expect("event_type length validated by EventType invariant");
203        let metadata_len_u32 = metadata_len.map(|n| {
204            #[allow(
205                clippy::expect_used,
206                reason = "validated by Metadata::from_bytes invariant: length ≤ MAX_METADATA_LEN"
207            )]
208            let v = u32::try_from(n).expect("metadata length validated by Metadata invariant");
209            v
210        });
211        Self {
212            format_version,
213            schema_version,
214            event_type_len: event_type_len_u16,
215            metadata_len: metadata_len_u32,
216        }
217    }
218
219    /// Serialize this header into the start of `buf` (writes exactly 11 bytes:
220    /// the version byte then the three V2 fixed fields). Inverse of the V2 arm
221    /// of `read_from`. Only ever called for `CURRENT` (V2) on the encode path.
222    fn write_into(&self, buf: &mut AVec<u8, ConstAlign<PAYLOAD_ALIGN>>) {
223        let meta_field = self.metadata_len.unwrap_or(META_LEN_ABSENT);
224        buf.extend_from_slice(&[self.format_version.to_u8()]);
225        buf.extend_from_slice(&self.schema_version.to_le_bytes());
226        buf.extend_from_slice(&self.event_type_len.to_le_bytes());
227        buf.extend_from_slice(&meta_field.to_le_bytes());
228    }
229
230    /// Read the fixed header from the start of `value`.
231    ///
232    /// Reads the leading version byte, then parses the version's fixed fields
233    /// (`schema_version` / `event_type_len` / `meta_len`) at that version's
234    /// offsets. A V1 frame's 8-byte `global_seq` (offset 1) is skipped — V2
235    /// dropped it and [`DecodedFrame`] no longer carries it. The body/padding
236    /// offsets a per-version decoder then computes hang off the version's header
237    /// size ([`HEADER_FIXED_SIZE`] for V2, [`HEADER_FIXED_SIZE_V1`] for V1).
238    ///
239    /// # Errors
240    ///
241    /// - [`DecodeError::ValueTooShort`] if `value` is shorter than the version's
242    ///   fixed header.
243    /// - [`DecodeError::UnsupportedFrameVersion`] if the leading version byte
244    ///   is not a known [`FrameFormatVersion`].
245    pub(crate) fn read_from(value: &[u8]) -> Result<Self, DecodeError> {
246        // Enough bytes to read the version byte + the smallest (V2) header.
247        if value.len() < Self::SIZE {
248            return Err(DecodeError::ValueTooShort {
249                min: Self::SIZE,
250                actual: value.len(),
251            });
252        }
253        let version_byte = value[VERSION_OFFSET];
254        let format_version = FrameFormatVersion::from_u8(version_byte).ok_or(
255            DecodeError::UnsupportedFrameVersion {
256                version: version_byte,
257            },
258        )?;
259        let (schema_off, et_len_off, meta_off) = match format_version {
260            FrameFormatVersion::V1 => {
261                if value.len() < HEADER_FIXED_SIZE_V1 {
262                    return Err(DecodeError::ValueTooShort {
263                        min: HEADER_FIXED_SIZE_V1,
264                        actual: value.len(),
265                    });
266                }
267                (
268                    SCHEMA_VERSION_OFFSET_V1,
269                    EVENT_TYPE_LEN_OFFSET_V1,
270                    META_LEN_OFFSET_V1,
271                )
272            }
273            FrameFormatVersion::V2 => (
274                SCHEMA_VERSION_OFFSET,
275                EVENT_TYPE_LEN_OFFSET,
276                META_LEN_OFFSET,
277            ),
278        };
279        let schema_version = u32::from_le_bytes([
280            value[schema_off],
281            value[schema_off + 1],
282            value[schema_off + 2],
283            value[schema_off + 3],
284        ]);
285        let event_type_len = u16::from_le_bytes([value[et_len_off], value[et_len_off + 1]]);
286        let meta_field = u32::from_le_bytes([
287            value[meta_off],
288            value[meta_off + 1],
289            value[meta_off + 2],
290            value[meta_off + 3],
291        ]);
292        let metadata_len = if meta_field == META_LEN_ABSENT {
293            None
294        } else {
295            Some(meta_field)
296        };
297        Ok(Self {
298            format_version,
299            schema_version,
300            event_type_len,
301            metadata_len,
302        })
303    }
304}
305
306// ---------------------------------------------------------------------
307// FrameLayout — pure arithmetic (no buffer touches)
308// ---------------------------------------------------------------------
309
310/// Byte layout of one frame: where each field lives and how big the buffer is.
311///
312/// Produced by [`FrameLayout::compute_from_validated_lengths`] from raw
313/// lengths whose fit-the-wire-field invariant is owned upstream by the
314/// value newtypes. The build path uses every field; the decode path uses
315/// only the padding formula via [`align_padding`].
316#[derive(Debug, Clone)]
317struct FrameLayout {
318    event_type: Range<u32>,
319    metadata: Option<Range<u32>>,
320    payload: Range<u32>,
321    padding: usize,
322    total: usize,
323}
324
325/// Build a [`WireError::FrameLengthOverflow`] from its three diagnostic fields.
326#[inline]
327const fn length_overflow(header: usize, padding: usize, payload: usize) -> WireError {
328    WireError::FrameLengthOverflow {
329        header,
330        padding,
331        payload,
332    }
333}
334
335impl FrameLayout {
336    /// Compute the layout from already-validated raw lengths.
337    ///
338    /// Callers must guarantee `event_type_len <= u16::MAX`,
339    /// `metadata_len <= u32::MAX - 1` (the absent sentinel is reserved),
340    /// and `payload_len <= u32::MAX`. The value newtypes uphold these
341    /// invariants at construction time.
342    ///
343    /// # Errors
344    ///
345    /// Returns [`WireError::FrameLengthOverflow`] if combining the
346    /// fields would overflow `usize` on the target platform or any
347    /// computed offset would not fit in `u32`.
348    fn compute_from_validated_lengths(
349        event_type_len: usize,
350        metadata_len: Option<usize>,
351        payload_len: usize,
352    ) -> Result<Self, WireError> {
353        let meta_len_usize = metadata_len.unwrap_or(0);
354
355        let pre_payload_len = HEADER_FIXED_SIZE
356            .checked_add(event_type_len)
357            .and_then(|n| n.checked_add(meta_len_usize))
358            .ok_or_else(|| length_overflow(HEADER_FIXED_SIZE, 0, payload_len))?;
359
360        let padding = align_padding(pre_payload_len, PAYLOAD_ALIGN);
361        let total = pre_payload_len
362            .checked_add(padding)
363            .and_then(|n| n.checked_add(payload_len))
364            .ok_or_else(|| length_overflow(pre_payload_len, padding, payload_len))?;
365
366        let overflow = || length_overflow(pre_payload_len, padding, payload_len);
367
368        let event_type_start = u32::try_from(HEADER_FIXED_SIZE).map_err(|_| overflow())?;
369        let event_type_len_u32 = u32::try_from(event_type_len).map_err(|_| overflow())?;
370        let event_type_end = event_type_start
371            .checked_add(event_type_len_u32)
372            .ok_or_else(overflow)?;
373
374        let metadata_range = metadata_len
375            .map(|n| -> Result<Range<u32>, WireError> {
376                let n_u32 = u32::try_from(n).map_err(|_| overflow())?;
377                let end = event_type_end.checked_add(n_u32).ok_or_else(overflow)?;
378                Ok(event_type_end..end)
379            })
380            .transpose()?;
381
382        let payload_start_usize = pre_payload_len.checked_add(padding).ok_or_else(overflow)?;
383        let payload_start = u32::try_from(payload_start_usize).map_err(|_| overflow())?;
384        let payload_len_u32 = u32::try_from(payload_len).map_err(|_| overflow())?;
385        let payload_end = payload_start
386            .checked_add(payload_len_u32)
387            .ok_or_else(overflow)?;
388
389        Ok(Self {
390            event_type: event_type_start..event_type_end,
391            metadata: metadata_range,
392            payload: payload_start..payload_end,
393            padding,
394            total,
395        })
396    }
397}
398
399// ---------------------------------------------------------------------
400// Public output / error types
401// ---------------------------------------------------------------------
402
403/// Output of [`encode_frame`]: the assembled buffer plus byte ranges into it.
404#[derive(Debug)]
405pub struct EncodedFrame {
406    pub value: Bytes,
407    pub offsets: FrameOffsets,
408}
409
410/// Byte ranges for each variable-width field within an [`EncodedFrame::value`] buffer.
411///
412/// Fixed-position header fields (`schema_version`, `event_type_len`,
413/// `meta_len`) are read from constant offsets and have no ranges here.
414#[derive(Debug, Clone)]
415pub struct FrameOffsets {
416    pub event_type: Range<u32>,
417    pub metadata: Option<Range<u32>>,
418    pub payload: Range<u32>,
419}
420
421/// Errors from [`encode_frame`].
422///
423/// The only failure mode is arithmetic overflow when combining lengths.
424/// All field-shape invariants (`event_type` UTF-8 + cap, payload cap,
425/// metadata non-empty + cap, `schema_version` > 0) are upheld at the
426/// value newtype boundary in [`crate::value`].
427#[derive(Debug, Error)]
428#[non_exhaustive]
429pub enum WireError {
430    #[error(
431        "frame length overflow combining header={header}, padding={padding}, payload={payload}"
432    )]
433    FrameLengthOverflow {
434        header: usize,
435        padding: usize,
436        payload: usize,
437    },
438}
439
440/// Output of [`decode_frame`]: header fields plus byte ranges into the input value.
441#[derive(Debug)]
442pub struct DecodedFrame {
443    pub schema_version: SchemaVersion,
444    pub offsets: FrameOffsets,
445}
446
447/// Errors from [`decode_frame`].
448#[derive(Debug, Error)]
449#[non_exhaustive]
450pub enum DecodeError {
451    #[error("value too short: need at least {min} bytes, got {actual}")]
452    ValueTooShort { min: usize, actual: usize },
453    /// The leading frame-format-version byte holds a value this build does
454    /// not understand. Distinct from `ValueTooShort` (not enough bytes) and
455    /// `CorruptSchemaVersion` (per-event schema) — its own failure domain.
456    #[error(
457        "unsupported frame format version on wire: got {version}, this build supports up to {}",
458        FrameFormatVersion::CURRENT.to_u8()
459    )]
460    UnsupportedFrameVersion { version: u8 },
461    #[error("event type length {et_len} extends past value (len={value_len})")]
462    EventTypeTruncated { et_len: usize, value_len: usize },
463    #[error("metadata length {meta_len} extends past value (len={value_len})")]
464    MetadataTruncated { meta_len: u32, value_len: usize },
465    #[error("computed offset overflows u32 (value len={value_len})")]
466    OffsetOverflow { value_len: usize },
467    /// Corrupt on-disk frame with `schema_version == 0`. The build path
468    /// uses [`SchemaVersion`], which makes this value structurally
469    /// unrepresentable — so the only way for a decoder to encounter zero
470    /// is bit-rot, truncation, or tampering of persisted data.
471    #[error("corrupt schema_version on wire: got 0, must be > 0")]
472    CorruptSchemaVersion,
473}
474
475// ---------------------------------------------------------------------
476// FramePlan + plan/execute
477//
478// `plan` does the layout math.
479// `execute` is infallible — given a plan, fill the buffer.
480// ---------------------------------------------------------------------
481
482/// Everything needed to materialize one frame's bytes.
483///
484/// Construction via [`plan`] guarantees: the layout has been computed
485/// without overflow, and the borrowed slices are the body bytes the
486/// layout describes.
487#[derive(Debug)]
488struct FramePlan<'a> {
489    header: FrameHeader,
490    event_type_bytes: &'a [u8],
491    metadata: Option<&'a [u8]>,
492    payload: &'a [u8],
493    layout: FrameLayout,
494}
495
496/// Compute the layout and header for one frame from validated value newtypes.
497fn plan<'a>(
498    schema_version: SchemaVersion,
499    event_type: &'a EventType,
500    payload: &'a Payload,
501    metadata: Option<&'a Metadata>,
502) -> Result<FramePlan<'a>, WireError> {
503    let event_type_bytes = event_type.as_bytes();
504    let metadata_bytes = metadata.map(Metadata::as_slice);
505    let payload_bytes = payload.as_slice();
506
507    let layout = FrameLayout::compute_from_validated_lengths(
508        event_type_bytes.len(),
509        metadata_bytes.map(<[u8]>::len),
510        payload_bytes.len(),
511    )?;
512    let header = FrameHeader::from_validated_lengths(
513        FrameFormatVersion::CURRENT,
514        schema_version.get(),
515        event_type_bytes.len(),
516        metadata_bytes.map(<[u8]>::len),
517    );
518    Ok(FramePlan {
519        header,
520        event_type_bytes,
521        metadata: metadata_bytes,
522        payload: payload_bytes,
523        layout,
524    })
525}
526
527/// Materialize a plan into an aligned buffer. Infallible.
528fn execute(plan: FramePlan<'_>) -> EncodedFrame {
529    let mut buf: AVec<u8, ConstAlign<PAYLOAD_ALIGN>> =
530        AVec::with_capacity(PAYLOAD_ALIGN, plan.layout.total);
531    plan.header.write_into(&mut buf);
532    buf.extend_from_slice(plan.event_type_bytes);
533    if let Some(m) = plan.metadata {
534        buf.extend_from_slice(m);
535    }
536    buf.resize(buf.len() + plan.layout.padding, 0u8);
537    buf.extend_from_slice(plan.payload);
538
539    EncodedFrame {
540        value: Bytes::from_owner(buf),
541        offsets: FrameOffsets {
542            event_type: plan.layout.event_type,
543            metadata: plan.layout.metadata,
544            payload: plan.layout.payload,
545        },
546    }
547}
548
549// ---------------------------------------------------------------------
550// Public API
551// ---------------------------------------------------------------------
552
553/// Build one frame buffer with payload aligned to [`PAYLOAD_ALIGN`].
554///
555/// Argument order: `(schema_version, &event_type, &payload, metadata)`.
556/// `payload` precedes `metadata` to keep the optional argument trailing per
557/// Rust API conventions. Emits the current format (V2).
558///
559/// Layout:
560///
561/// ```text
562/// [u8 frame_format_version][u32 LE schema_version]
563/// [u16 LE event_type_len][u32 LE meta_len][event_type bytes]
564/// [metadata bytes if any][padding zero-bytes][payload bytes]
565/// ```
566///
567/// `meta_len == u32::MAX` is the absent-metadata sentinel.
568///
569/// All field invariants (UTF-8, size caps, `schema_version` > 0) live on
570/// the value newtypes ([`EventType`], [`Payload`], [`Metadata`],
571/// [`SchemaVersion`]) — by the time inputs reach this function they are
572/// already wire-encodable. The only remaining failure mode is arithmetic
573/// overflow when combining lengths into the final frame size.
574///
575/// # Errors
576///
577/// Returns [`WireError::FrameLengthOverflow`] if the assembled frame
578/// would overflow `usize` on the target platform.
579pub fn encode_frame(
580    schema_version: SchemaVersion,
581    event_type: &EventType,
582    payload: &Payload,
583    metadata: Option<&Metadata>,
584) -> Result<EncodedFrame, WireError> {
585    plan(schema_version, event_type, payload, metadata).map(execute)
586}
587
588/// Decode a frame value built by [`encode_frame`].
589///
590/// Reads the fixed header (including the leading format-version byte),
591/// dispatches to the appropriate per-version decoder, recovers
592/// event-type and metadata ranges, and computes the payload range
593/// honoring the 16-byte alignment padding. The `schema_version` is
594/// reconstructed through [`SchemaVersion::from_u32`] so a corrupt
595/// on-disk zero surfaces as [`DecodeError::CorruptSchemaVersion`].
596///
597/// # Errors
598///
599/// - [`DecodeError::ValueTooShort`] if `value` is shorter than the fixed header.
600/// - [`DecodeError::UnsupportedFrameVersion`] if the leading version byte is unrecognized.
601/// - [`DecodeError::EventTypeTruncated`] if the event-type length runs past the buffer.
602/// - [`DecodeError::MetadataTruncated`] if `meta_len` claims bytes past the buffer end.
603/// - [`DecodeError::OffsetOverflow`] if any computed offset would not fit in `u32`.
604/// - [`DecodeError::CorruptSchemaVersion`] if the on-disk `schema_version` is 0.
605pub fn decode_frame(value: &[u8]) -> Result<DecodedFrame, DecodeError> {
606    let header = FrameHeader::read_from(value)?;
607    // Each version's body offsets hang off its fixed header size; everything
608    // after the header (event_type / metadata / padding / payload) is laid out
609    // identically, so one body decoder serves both.
610    let header_size = match header.format_version {
611        FrameFormatVersion::V1 => HEADER_FIXED_SIZE_V1,
612        FrameFormatVersion::V2 => HEADER_FIXED_SIZE,
613    };
614    decode_frame_body(value, header, header_size)
615}
616
617/// Decode the body (`event_type` / `metadata` / padding / `payload` ranges) of
618/// a frame given its already-validated header and that version's header size.
619fn decode_frame_body(
620    value: &[u8],
621    header: FrameHeader,
622    header_size: usize,
623) -> Result<DecodedFrame, DecodeError> {
624    let schema_version = SchemaVersion::from_u32(header.schema_version)
625        .map_err(|_| DecodeError::CorruptSchemaVersion)?;
626    let et_len = usize::from(header.event_type_len);
627
628    let et_start = header_size;
629    let et_end = et_start
630        .checked_add(et_len)
631        .ok_or(DecodeError::OffsetOverflow {
632            value_len: value.len(),
633        })?;
634    if value.len() < et_end {
635        return Err(DecodeError::EventTypeTruncated {
636            et_len,
637            value_len: value.len(),
638        });
639    }
640
641    let (metadata_range, post_meta) = match header.metadata_len {
642        None => (None, et_end),
643        Some(meta_len) => {
644            let meta_len_usize =
645                usize::try_from(meta_len).map_err(|_| DecodeError::OffsetOverflow {
646                    value_len: value.len(),
647                })?;
648            let meta_end =
649                et_end
650                    .checked_add(meta_len_usize)
651                    .ok_or(DecodeError::OffsetOverflow {
652                        value_len: value.len(),
653                    })?;
654            if value.len() < meta_end {
655                return Err(DecodeError::MetadataTruncated {
656                    meta_len,
657                    value_len: value.len(),
658                });
659            }
660            let m_start_u32 = u32::try_from(et_end).map_err(|_| DecodeError::OffsetOverflow {
661                value_len: value.len(),
662            })?;
663            let m_end_u32 = u32::try_from(meta_end).map_err(|_| DecodeError::OffsetOverflow {
664                value_len: value.len(),
665            })?;
666            (Some(m_start_u32..m_end_u32), meta_end)
667        }
668    };
669
670    let padding = align_padding(post_meta, PAYLOAD_ALIGN);
671    let payload_start = post_meta
672        .checked_add(padding)
673        .ok_or(DecodeError::OffsetOverflow {
674            value_len: value.len(),
675        })?;
676    let payload_end = value.len();
677    if payload_start > payload_end {
678        return Err(DecodeError::OffsetOverflow {
679            value_len: value.len(),
680        });
681    }
682
683    let et_start_u32 = u32::try_from(et_start).map_err(|_| DecodeError::OffsetOverflow {
684        value_len: value.len(),
685    })?;
686    let et_end_u32 = u32::try_from(et_end).map_err(|_| DecodeError::OffsetOverflow {
687        value_len: value.len(),
688    })?;
689    let payload_start_u32 =
690        u32::try_from(payload_start).map_err(|_| DecodeError::OffsetOverflow {
691            value_len: value.len(),
692        })?;
693    let payload_end_u32 = u32::try_from(payload_end).map_err(|_| DecodeError::OffsetOverflow {
694        value_len: value.len(),
695    })?;
696
697    Ok(DecodedFrame {
698        schema_version,
699        offsets: FrameOffsets {
700            event_type: et_start_u32..et_end_u32,
701            metadata: metadata_range,
702            payload: payload_start_u32..payload_end_u32,
703        },
704    })
705}
706
707#[cfg(test)]
708#[allow(
709    clippy::as_conversions,
710    clippy::cast_possible_truncation,
711    clippy::panic,
712    clippy::redundant_clone,
713    clippy::single_match_else,
714    reason = "test code: index arithmetic, prop_assert_eq macro expansions, \
715              and `panic!(\"expected X, got {other:?}\")` arms surface failing test diagnostics"
716)]
717mod tests {
718    use super::*;
719    use crate::value::{MAX_EVENT_TYPE_LEN, MAX_METADATA_LEN, MAX_PAYLOAD_LEN};
720    use proptest::prelude::*;
721
722    fn payload_ptr_aligned(frame: &EncodedFrame) -> bool {
723        let start = usize::try_from(frame.offsets.payload.start).expect("u32 fits usize");
724        let end = usize::try_from(frame.offsets.payload.end).expect("u32 fits usize");
725        let payload_slice = &frame.value[start..end];
726        payload_slice.as_ptr().addr().is_multiple_of(PAYLOAD_ALIGN)
727    }
728
729    /// Build an [`EventType`] from arbitrary bytes for testing.
730    /// Panics on cap violation; tests choose inputs within the cap.
731    fn et(s: &str) -> EventType {
732        EventType::from_bytes(Bytes::copy_from_slice(s.as_bytes())).expect("test event_type valid")
733    }
734
735    /// Build a [`Payload`] from arbitrary bytes for testing.
736    fn pl(b: &[u8]) -> Payload {
737        Payload::from_bytes(Bytes::copy_from_slice(b)).expect("test payload valid")
738    }
739
740    /// Build a [`Metadata`] from arbitrary non-empty bytes for testing.
741    fn md(b: &[u8]) -> Metadata {
742        Metadata::from_bytes(Bytes::copy_from_slice(b)).expect("test metadata non-empty + valid")
743    }
744
745    fn sv1() -> SchemaVersion {
746        SchemaVersion::INITIAL
747    }
748
749    // -----------------------------------------------------------------
750    // Reusable strategy helpers
751    //
752    // Each follows the project's "include 0, 1, MAX-1, MAX via prop_oneof!
753    // alongside the interior range" rule. Weights are 1 per boundary and
754    // 10 for the interior — boundaries are still always hit (~28% of
755    // runs collectively) without choking interior coverage.
756    // -----------------------------------------------------------------
757
758    /// Couples align (power of 2) with a boundary-rich offset for that
759    /// align. Generated jointly via `prop_flat_map` so shrinking can
760    /// narrow to the minimum `(align, offset)` pair that violates an
761    /// invariant — see the book's "Higher-Order Strategies" chapter.
762    fn align_and_offset() -> impl Strategy<Value = (usize, usize)> {
763        (0u32..16).prop_flat_map(|align_pow| {
764            let align = 1usize << align_pow;
765            // align - 1 may equal 0 when align == 1; duplicates Just(0).
766            let offset = prop_oneof![
767                1 => Just(0usize),
768                1 => Just(1usize),
769                1 => Just(align.saturating_sub(1)),
770                1 => Just(align),
771                1 => Just(align + 1),
772                10 => 0usize..1_000_000,
773            ];
774            (Just(align), offset)
775        })
776    }
777
778    fn u32_strategy() -> impl Strategy<Value = u32> {
779        prop_oneof![
780            1 => Just(0u32),
781            1 => Just(1u32),
782            1 => Just(u32::MAX - 1),
783            1 => Just(u32::MAX),
784            10 => any::<u32>(),
785        ]
786    }
787
788    /// Nonzero `schema_version` strategy — mirrors the [`SchemaVersion`]
789    /// invariant. Boundaries follow the project's `0/1/MAX-1/MAX via
790    /// prop_oneof!` rule, adjusted for the nonzero domain.
791    fn schema_version_strategy() -> impl Strategy<Value = SchemaVersion> {
792        prop_oneof![
793            1 => Just(1u32),
794            1 => Just(2u32),
795            1 => Just(u32::MAX - 1),
796            1 => Just(u32::MAX),
797            10 => 1u32..=u32::MAX,
798        ]
799        .prop_map(|v| SchemaVersion::from_u32(v).expect("nonzero strategy"))
800    }
801
802    fn u16_strategy() -> impl Strategy<Value = u16> {
803        prop_oneof![
804            1 => Just(0u16),
805            1 => Just(1u16),
806            1 => Just(u16::MAX - 1),
807            1 => Just(u16::MAX),
808            10 => any::<u16>(),
809        ]
810    }
811
812    /// Bounded length strategy for layout-time tests where the input is
813    /// also a `Vec<u8>` allocation; capped low to keep tests fast while
814    /// preserving the boundary cases that matter for layout arithmetic.
815    fn frame_body_length() -> impl Strategy<Value = usize> {
816        prop_oneof![
817            1 => Just(0usize),
818            1 => Just(1usize),
819            1 => Just(PAYLOAD_ALIGN - 1),
820            1 => Just(PAYLOAD_ALIGN),
821            1 => Just(PAYLOAD_ALIGN + 1),
822            10 => 0usize..=4096,
823        ]
824    }
825
826    /// UTF-8 event-type strings with explicit boundary anchors plus a
827    /// Unicode-complete interior. `any::<char>()` covers the full code
828    /// point space, which the wire format accepts (the only constraint
829    /// is byte length under [`MAX_EVENT_TYPE_LEN`]).
830    fn event_type_str_strategy() -> impl Strategy<Value = String> {
831        prop_oneof![
832            1 => Just(String::new()),
833            1 => Just("a".to_owned()),
834            10 => prop::collection::vec(any::<char>(), 0..=256)
835                .prop_map(|chars| chars.into_iter().collect::<String>()),
836        ]
837    }
838
839    fn metadata_bytes_strategy() -> impl Strategy<Value = Option<Vec<u8>>> {
840        // Metadata::from_bytes rejects empty — so when generating Some,
841        // start at length 1 to keep the strategy inside the value-newtype
842        // domain (the wire layer no longer rejects on its own).
843        prop_oneof![
844            1 => Just(None),
845            1 => Just(Some(vec![0u8])),
846            10 => prop::option::of(prop::collection::vec(any::<u8>(), 1..512)),
847        ]
848    }
849
850    fn payload_bytes_strategy() -> impl Strategy<Value = Vec<u8>> {
851        prop_oneof![
852            1 => Just(Vec::<u8>::new()),
853            1 => Just(vec![0u8]),
854            10 => prop::collection::vec(any::<u8>(), 0..2048),
855        ]
856    }
857
858    // Composite: every input to `encode_frame` joined into one strategy
859    // via `prop_compose!` (the book's pattern for named composites).
860    // Shrinking remains coordinated across the four components.
861    prop_compose! {
862        fn valid_frame_inputs()(
863            schema_version in schema_version_strategy(),
864            event_type in event_type_str_strategy(),
865            metadata in metadata_bytes_strategy(),
866            payload in payload_bytes_strategy(),
867        ) -> (SchemaVersion, String, Option<Vec<u8>>, Vec<u8>) {
868            (schema_version, event_type, metadata, payload)
869        }
870    }
871
872    proptest! {
873        #[test]
874        fn payload_pointer_is_16_aligned(
875            (schema_version, event_type, metadata, payload) in valid_frame_inputs(),
876        ) {
877            let et_v = et(&event_type);
878            let pl_v = pl(&payload);
879            let md_v = metadata.as_deref().map(md);
880            let frame = encode_frame(schema_version, &et_v, &pl_v, md_v.as_ref())
881                .expect("encode_frame succeeds on bounded inputs");
882            prop_assert!(payload_ptr_aligned(&frame));
883        }
884
885        #[test]
886        fn ranges_recover_each_field(
887            (schema_version, event_type, metadata, payload) in valid_frame_inputs(),
888        ) {
889            let et_v = et(&event_type);
890            let pl_v = pl(&payload);
891            let md_v = metadata.as_deref().map(md);
892            let frame = encode_frame(schema_version, &et_v, &pl_v, md_v.as_ref())
893                .expect("encode_frame succeeds on bounded inputs");
894            let v = &frame.value;
895            prop_assert_eq!(
896                &v[frame.offsets.event_type.start as usize..frame.offsets.event_type.end as usize],
897                event_type.as_bytes()
898            );
899            prop_assert_eq!(
900                &v[frame.offsets.payload.start as usize..frame.offsets.payload.end as usize],
901                payload.as_slice()
902            );
903            if let (Some(meta), Some(range)) = (metadata.as_deref(), frame.offsets.metadata) {
904                prop_assert_eq!(
905                    &v[range.start as usize..range.end as usize],
906                    meta
907                );
908            }
909        }
910
911        #[test]
912        fn header_fields_are_recoverable(
913            (schema_version, event_type, metadata, payload) in valid_frame_inputs(),
914        ) {
915            let et_v = et(&event_type);
916            let pl_v = pl(&payload);
917            let md_v = metadata.as_deref().map(md);
918            let frame = encode_frame(schema_version, &et_v, &pl_v, md_v.as_ref())
919                .expect("encode_frame succeeds on bounded inputs");
920            let v = &frame.value;
921
922            let mut sv_buf = [0u8; 4];
923            sv_buf.copy_from_slice(&v[SCHEMA_VERSION_OFFSET..EVENT_TYPE_LEN_OFFSET]);
924            prop_assert_eq!(u32::from_le_bytes(sv_buf), schema_version.get());
925
926            let mut et_len_buf = [0u8; 2];
927            et_len_buf.copy_from_slice(&v[EVENT_TYPE_LEN_OFFSET..EVENT_TYPE_LEN_OFFSET + 2]);
928            prop_assert_eq!(usize::from(u16::from_le_bytes(et_len_buf)), event_type.len());
929
930            let mut ml_buf = [0u8; 4];
931            ml_buf.copy_from_slice(&v[META_LEN_OFFSET..META_LEN_OFFSET + 4]);
932            let ml = u32::from_le_bytes(ml_buf);
933            match metadata.as_deref() {
934                Some(m) => prop_assert_eq!(usize::try_from(ml).unwrap(), m.len()),
935                None => prop_assert_eq!(ml, META_LEN_ABSENT),
936            }
937        }
938
939        #[test]
940        fn encoded_frame_carries_v2_version_byte(
941            (schema_version, event_type, metadata, payload) in valid_frame_inputs(),
942        ) {
943            let et_v = et(&event_type);
944            let pl_v = pl(&payload);
945            let md_v = metadata.as_deref().map(md);
946            let frame = encode_frame(schema_version, &et_v, &pl_v, md_v.as_ref())
947                .expect("encode_frame succeeds on bounded inputs");
948            // Sequence: the leading byte is the version tag, == 2 (V2 is current).
949            prop_assert_eq!(frame.value[VERSION_OFFSET], 2);
950            // And the header reader recovers it as the typed V2.
951            let header = FrameHeader::read_from(&frame.value)
952                .expect("header reads back from a freshly built frame");
953            prop_assert_eq!(header.format_version, FrameFormatVersion::V2);
954        }
955    }
956
957    #[test]
958    fn empty_payload_still_aligned() {
959        let frame = encode_frame(sv1(), &et("X"), &pl(b""), None).expect("trivial frame builds");
960        assert!(payload_ptr_aligned(&frame));
961        assert_eq!(frame.offsets.payload.start, frame.offsets.payload.end);
962    }
963
964    #[test]
965    fn empty_event_type_permitted() {
966        let frame = encode_frame(sv1(), &et(""), &pl(b"data"), None)
967            .expect("empty event_type accepted at wire layer");
968        assert!(payload_ptr_aligned(&frame));
969    }
970
971    #[test]
972    fn max_event_type_accepted() {
973        let huge = "a".repeat(MAX_EVENT_TYPE_LEN);
974        encode_frame(sv1(), &et(&huge), &pl(b"d"), None).expect("max-length event_type accepted");
975    }
976
977    #[test]
978    fn meta_len_u32_max_is_absent_sentinel() {
979        let frame =
980            encode_frame(sv1(), &et("X"), &pl(b"d"), None).expect("none-metadata frame builds");
981        let mut ml_buf = [0u8; 4];
982        ml_buf.copy_from_slice(&frame.value[META_LEN_OFFSET..META_LEN_OFFSET + 4]);
983        assert_eq!(u32::from_le_bytes(ml_buf), META_LEN_ABSENT);
984        assert!(frame.offsets.metadata.is_none());
985    }
986
987    proptest! {
988        #[test]
989        fn build_then_decode_round_trips(
990            (schema_version, event_type, metadata, payload) in valid_frame_inputs(),
991        ) {
992            let et_v = et(&event_type);
993            let pl_v = pl(&payload);
994            let md_v = metadata.as_deref().map(md);
995            let frame = encode_frame(schema_version, &et_v, &pl_v, md_v.as_ref())
996                .expect("encode_frame succeeds on bounded inputs");
997            let decoded = decode_frame(&frame.value).expect("decode_frame succeeds on a built frame");
998            prop_assert_eq!(decoded.schema_version, schema_version);
999            prop_assert_eq!(decoded.offsets.event_type.clone(), frame.offsets.event_type.clone());
1000            prop_assert_eq!(decoded.offsets.metadata.clone(), frame.offsets.metadata.clone());
1001            prop_assert_eq!(decoded.offsets.payload.clone(), frame.offsets.payload.clone());
1002        }
1003    }
1004
1005    #[test]
1006    fn decode_rejects_truncated_value() {
1007        let too_short = vec![0u8; HEADER_FIXED_SIZE - 1];
1008        assert!(matches!(
1009            decode_frame(&too_short),
1010            Err(DecodeError::ValueTooShort { .. })
1011        ));
1012    }
1013
1014    #[test]
1015    fn decode_rejects_truncated_event_type() {
1016        // Header claims et_len = 100 but no event-type bytes follow. V2 frame
1017        // (version byte 2) so the V2 fixed-field offsets/header size apply.
1018        let mut buf = vec![0u8; HEADER_FIXED_SIZE];
1019        buf[VERSION_OFFSET] = 2;
1020        buf[SCHEMA_VERSION_OFFSET..EVENT_TYPE_LEN_OFFSET].copy_from_slice(&1u32.to_le_bytes());
1021        buf[EVENT_TYPE_LEN_OFFSET..EVENT_TYPE_LEN_OFFSET + 2]
1022            .copy_from_slice(&100u16.to_le_bytes());
1023        buf[META_LEN_OFFSET..META_LEN_OFFSET + 4].copy_from_slice(&META_LEN_ABSENT.to_le_bytes());
1024        assert!(matches!(
1025            decode_frame(&buf),
1026            Err(DecodeError::EventTypeTruncated { .. })
1027        ));
1028    }
1029
1030    #[test]
1031    fn decode_rejects_truncated_metadata() {
1032        // Header claims meta_len = 100 but no metadata bytes follow. V2 frame.
1033        let mut buf = vec![0u8; HEADER_FIXED_SIZE];
1034        buf[VERSION_OFFSET] = 2;
1035        buf[SCHEMA_VERSION_OFFSET..EVENT_TYPE_LEN_OFFSET].copy_from_slice(&1u32.to_le_bytes());
1036        buf[EVENT_TYPE_LEN_OFFSET..EVENT_TYPE_LEN_OFFSET + 2].copy_from_slice(&0u16.to_le_bytes());
1037        buf[META_LEN_OFFSET..META_LEN_OFFSET + 4].copy_from_slice(&100u32.to_le_bytes());
1038        assert!(matches!(
1039            decode_frame(&buf),
1040            Err(DecodeError::MetadataTruncated { .. })
1041        ));
1042    }
1043
1044    // -----------------------------------------------------------------
1045    // schema_version corruption surfacing — read path must reject the
1046    // structurally-impossible-to-encode value with a typed error rather
1047    // than panicking on the SchemaVersion conversion downstream.
1048    // -----------------------------------------------------------------
1049
1050    #[test]
1051    fn decode_rejects_corrupt_schema_version_zero() {
1052        // The encoder cannot produce schema_version=0 (its input is
1053        // SchemaVersion, which is NonZeroU32). Simulate corrupt disk
1054        // bytes by hand-zeroing the header field.
1055        let frame = encode_frame(sv1(), &et("X"), &pl(b"p"), None).expect("encode");
1056        let mut bytes_vec = frame.value.to_vec();
1057        bytes_vec[SCHEMA_VERSION_OFFSET..EVENT_TYPE_LEN_OFFSET].fill(0);
1058        let tampered = Bytes::from(bytes_vec);
1059        assert!(matches!(
1060            decode_frame(&tampered),
1061            Err(DecodeError::CorruptSchemaVersion)
1062        ));
1063    }
1064
1065    // -----------------------------------------------------------------
1066    // decode_frame panic-freedom — adversarial input
1067    //
1068    // Wire-format frames stored at rest may be corrupt (disk bit-rot,
1069    // truncation, malicious tampering). decode_frame must surface every
1070    // failure as a typed DecodeError; a panic would crash the host
1071    // process on a single bad row. proptest catches panics as failures,
1072    // so the absence of `prop_assert!`/`assert!` in the body is
1073    // intentional — the test asserts "did not panic" by surviving.
1074    // -----------------------------------------------------------------
1075
1076    /// Buffers shaped to exercise every branch of [`decode_frame`].
1077    ///
1078    /// - empty / 1-byte: trigger the `ValueTooShort` early-return.
1079    /// - lengths around `HEADER_FIXED_SIZE`: pin the exact threshold.
1080    /// - raw random bytes: most random headers claim huge `et_len`, so
1081    ///   they exercise the `EventTypeTruncated` path well.
1082    /// - header-shaped: bound `et_len`/`meta_len` to plausible values
1083    ///   so random bodies reach the metadata- and payload-range arms
1084    ///   that raw random would skip ~94% of the time.
1085    fn adversarial_decode_bytes() -> impl Strategy<Value = Vec<u8>> {
1086        // Header-shaped V2: leading version byte (mostly the valid 2, sometimes
1087        // random to exercise the version-reject path), then small et_len /
1088        // meta_len, random body. Drives the deeper code paths that raw
1089        // random rarely reaches.
1090        let header_shaped = (
1091            prop_oneof![10 => Just(2u8), 1 => any::<u8>()],
1092            any::<u32>(),
1093            0u16..=64,
1094            prop_oneof![Just(META_LEN_ABSENT), 0u32..=64],
1095            prop::collection::vec(any::<u8>(), 0..=512),
1096        )
1097            .prop_map(|(version, sv, et_len, meta_len, body)| {
1098                let mut buf = Vec::with_capacity(HEADER_FIXED_SIZE + body.len());
1099                buf.extend_from_slice(&[version]);
1100                buf.extend_from_slice(&sv.to_le_bytes());
1101                buf.extend_from_slice(&et_len.to_le_bytes());
1102                buf.extend_from_slice(&meta_len.to_le_bytes());
1103                buf.extend_from_slice(&body);
1104                buf
1105            });
1106
1107        prop_oneof![
1108            1 => Just(Vec::<u8>::new()),
1109            1 => Just(vec![0u8]),
1110            1 => prop::collection::vec(any::<u8>(), HEADER_FIXED_SIZE - 1..=HEADER_FIXED_SIZE - 1),
1111            1 => prop::collection::vec(any::<u8>(), HEADER_FIXED_SIZE..=HEADER_FIXED_SIZE),
1112            1 => prop::collection::vec(any::<u8>(), HEADER_FIXED_SIZE + 1..=HEADER_FIXED_SIZE + 1),
1113            5 => prop::collection::vec(any::<u8>(), 0..=4096),
1114            5 => header_shaped,
1115        ]
1116    }
1117
1118    proptest! {
1119        #[test]
1120        fn decode_never_panics(bytes in adversarial_decode_bytes()) {
1121            // The assertion is structural: proptest treats panics as
1122            // failures, so reaching the end of the closure with any
1123            // Result is a pass. Every fallible step in decode_frame is
1124            // a `?` to a typed DecodeError variant — this test pins
1125            // that claim end-to-end on arbitrary input.
1126            let _ = decode_frame(&bytes);
1127        }
1128
1129        /// Stronger claim: when `decode_frame` succeeds on adversarial
1130        /// input, the returned ranges must be in-bounds. A bug that
1131        /// returns out-of-range offsets is just as dangerous as a panic
1132        /// — the next slice index by a consumer would panic instead.
1133        #[test]
1134        fn decode_offsets_in_bounds_on_success(bytes in adversarial_decode_bytes()) {
1135            if let Ok(decoded) = decode_frame(&bytes) {
1136                let len_u32 = u32::try_from(bytes.len()).unwrap_or(u32::MAX);
1137                prop_assert!(decoded.offsets.event_type.start <= decoded.offsets.event_type.end);
1138                prop_assert!(decoded.offsets.event_type.end <= len_u32);
1139                if let Some(meta) = decoded.offsets.metadata {
1140                    prop_assert!(meta.start <= meta.end);
1141                    prop_assert!(meta.end <= len_u32);
1142                }
1143                prop_assert!(decoded.offsets.payload.start <= decoded.offsets.payload.end);
1144                prop_assert!(decoded.offsets.payload.end <= len_u32);
1145            }
1146        }
1147    }
1148
1149    // -----------------------------------------------------------------
1150    // align_padding
1151    // -----------------------------------------------------------------
1152
1153    #[test]
1154    fn align_padding_zero_offset_yields_zero() {
1155        assert_eq!(align_padding(0, PAYLOAD_ALIGN), 0);
1156    }
1157
1158    #[test]
1159    fn align_padding_one_below_boundary_yields_one() {
1160        assert_eq!(align_padding(15, PAYLOAD_ALIGN), 1);
1161    }
1162
1163    #[test]
1164    fn align_padding_on_boundary_yields_zero() {
1165        assert_eq!(align_padding(PAYLOAD_ALIGN, PAYLOAD_ALIGN), 0);
1166    }
1167
1168    #[test]
1169    fn align_padding_one_above_boundary_yields_fifteen() {
1170        assert_eq!(align_padding(PAYLOAD_ALIGN + 1, PAYLOAD_ALIGN), 15);
1171    }
1172
1173    proptest! {
1174        // `align_and_offset()` generates (align, offset) jointly via
1175        // `prop_flat_map`, so when an invariant breaks proptest shrinks
1176        // to the minimum failing pair — not just a seed value the loop
1177        // happened to derive.
1178        #[test]
1179        fn align_padding_invariants(
1180            (align, offset) in align_and_offset(),
1181        ) {
1182            let pad = align_padding(offset, align);
1183
1184            // Invariant I1: offset + pad is a multiple of align.
1185            prop_assert!(
1186                (offset + pad).is_multiple_of(align),
1187                "offset={offset} align={align} pad={pad} not multiple",
1188            );
1189
1190            // Invariant I2: pad < align — the function returns the
1191            // *minimum* padding to reach the next multiple, never more.
1192            prop_assert!(pad < align, "pad {pad} >= align {align}");
1193
1194            // Invariant I3: pad == 0 iff offset is already aligned.
1195            prop_assert_eq!(pad == 0, offset.is_multiple_of(align));
1196        }
1197    }
1198
1199    // -----------------------------------------------------------------
1200    // FrameHeader
1201    // -----------------------------------------------------------------
1202
1203    fn fresh_buf() -> AVec<u8, ConstAlign<PAYLOAD_ALIGN>> {
1204        AVec::with_capacity(PAYLOAD_ALIGN, 64)
1205    }
1206
1207    #[test]
1208    fn frame_header_write_into_writes_all_fields_at_correct_offsets() {
1209        // Distinct byte patterns per field so a mis-offset would show up.
1210        let header = FrameHeader {
1211            format_version: FrameFormatVersion::V2,
1212            schema_version: 0x090A_0B0C,
1213            event_type_len: 0x0D0E,
1214            metadata_len: Some(0x0F10_1112),
1215        };
1216        let mut buf = fresh_buf();
1217        header.write_into(&mut buf);
1218
1219        // Invariant: writes exactly SIZE bytes (V2 header = 11).
1220        assert_eq!(buf.len(), FrameHeader::SIZE);
1221
1222        // Invariant: version byte is at offset 0 (V2 == 2).
1223        assert_eq!(buf[VERSION_OFFSET], 2);
1224
1225        // Invariant: every field lives at its declared V2 constant offset
1226        // in little-endian. Asserting all three catches mis-offset bugs
1227        // a spot check would miss.
1228        assert_eq!(
1229            &buf[SCHEMA_VERSION_OFFSET..EVENT_TYPE_LEN_OFFSET],
1230            &0x090A_0B0Cu32.to_le_bytes(),
1231        );
1232        assert_eq!(
1233            &buf[EVENT_TYPE_LEN_OFFSET..EVENT_TYPE_LEN_OFFSET + 2],
1234            &0x0D0Eu16.to_le_bytes(),
1235        );
1236        assert_eq!(
1237            &buf[META_LEN_OFFSET..META_LEN_OFFSET + 4],
1238            &0x0F10_1112u32.to_le_bytes(),
1239        );
1240    }
1241
1242    #[test]
1243    fn frame_header_none_metadata_encodes_sentinel() {
1244        let header = FrameHeader {
1245            format_version: FrameFormatVersion::V2,
1246            schema_version: 1,
1247            event_type_len: 0,
1248            metadata_len: None,
1249        };
1250        let mut buf = fresh_buf();
1251        header.write_into(&mut buf);
1252        let mut ml = [0u8; 4];
1253        ml.copy_from_slice(&buf[META_LEN_OFFSET..META_LEN_OFFSET + 4]);
1254        // Invariant: None metadata serializes to the absent sentinel,
1255        // distinguishing it from Some(empty).
1256        assert_eq!(u32::from_le_bytes(ml), META_LEN_ABSENT);
1257        let read = FrameHeader::read_from(&buf).expect("read back");
1258        assert!(read.metadata_len.is_none());
1259    }
1260
1261    #[test]
1262    fn frame_header_some_zero_metadata_distinct_from_none() {
1263        // Some(0) — empty metadata field — must NOT encode as the
1264        // absent sentinel.
1265        let with_empty = FrameHeader {
1266            format_version: FrameFormatVersion::V2,
1267            schema_version: 1,
1268            event_type_len: 0,
1269            metadata_len: Some(0),
1270        };
1271        let mut buf = fresh_buf();
1272        with_empty.write_into(&mut buf);
1273        let mut ml = [0u8; 4];
1274        ml.copy_from_slice(&buf[META_LEN_OFFSET..META_LEN_OFFSET + 4]);
1275        assert_eq!(u32::from_le_bytes(ml), 0);
1276        assert_ne!(u32::from_le_bytes(ml), META_LEN_ABSENT);
1277
1278        let read = FrameHeader::read_from(&buf).expect("read back");
1279        assert_eq!(read.metadata_len, Some(0));
1280    }
1281
1282    #[test]
1283    fn frame_header_read_from_rejects_buffer_below_size() {
1284        // Every length in [0, SIZE) must be rejected with ValueTooShort.
1285        for too_short_len in 0..FrameHeader::SIZE {
1286            let buf = vec![0u8; too_short_len];
1287            match FrameHeader::read_from(&buf) {
1288                Err(DecodeError::ValueTooShort { min, actual }) => {
1289                    assert_eq!(min, FrameHeader::SIZE);
1290                    assert_eq!(actual, too_short_len);
1291                }
1292                other => panic!("expected ValueTooShort for len={too_short_len}, got {other:?}"),
1293            }
1294        }
1295    }
1296
1297    #[test]
1298    fn frame_header_read_from_accepts_exactly_size() {
1299        let mut buf = vec![0u8; FrameHeader::SIZE];
1300        buf[VERSION_OFFSET] = 2;
1301        let header = FrameHeader::read_from(&buf).expect("accepts at SIZE");
1302        assert_eq!(header.format_version, FrameFormatVersion::V2);
1303        assert_eq!(header.schema_version, 0);
1304        assert_eq!(header.event_type_len, 0);
1305        assert_eq!(header.metadata_len, Some(0));
1306    }
1307
1308    proptest! {
1309        #[test]
1310        fn frame_header_round_trip(
1311            schema_version in u32_strategy(),
1312            et_raw in u16_strategy(),
1313            meta_choice in 0u32..4,
1314        ) {
1315            // meta_choice selects: None, Some(0), Some(MAX-1=u32::MAX-2), Some(arbitrary <= u32::MAX-1).
1316            let metadata_len = match meta_choice {
1317                0 => None,
1318                1 => Some(0u32),
1319                2 => Some(u32::MAX - 2),
1320                _ => Some((u32::MAX - 1) / 2),
1321            };
1322            let original = FrameHeader {
1323                format_version: FrameFormatVersion::V2,
1324                schema_version,
1325                event_type_len: et_raw,
1326                metadata_len,
1327            };
1328            let mut buf = fresh_buf();
1329            original.write_into(&mut buf);
1330            prop_assert_eq!(buf.len(), FrameHeader::SIZE);
1331
1332            let read = FrameHeader::read_from(&buf).expect("round-trip read");
1333            prop_assert_eq!(read.format_version, original.format_version);
1334            prop_assert_eq!(read.schema_version, original.schema_version);
1335            prop_assert_eq!(read.event_type_len, original.event_type_len);
1336            prop_assert_eq!(read.metadata_len, original.metadata_len);
1337        }
1338    }
1339
1340    // -----------------------------------------------------------------
1341    // FrameLayout — structural invariants
1342    // -----------------------------------------------------------------
1343
1344    #[test]
1345    fn layout_concrete_no_metadata_example() {
1346        // Anchored example to nail down the exact arithmetic the
1347        // proptest checks structurally (V2 header = 11): pre_payload = 11 + 2 =
1348        // 13; padding = 3; payload starts at 16.
1349        let layout = FrameLayout::compute_from_validated_lengths(2, None, 1).expect("ok");
1350        assert_eq!(layout.padding, 3);
1351        assert_eq!(layout.event_type, 11..13);
1352        assert_eq!(layout.metadata, None);
1353        assert_eq!(layout.payload, 16..17);
1354        assert_eq!(layout.total, 17);
1355    }
1356
1357    #[test]
1358    fn layout_concrete_with_metadata_example() {
1359        // V2 header = 11: pre_payload = 11 + 2 + 3 = 16; padding = 0; payload
1360        // starts at 16.
1361        let layout = FrameLayout::compute_from_validated_lengths(2, Some(3), 4).expect("ok");
1362        assert_eq!(layout.event_type, 11..13);
1363        assert_eq!(layout.metadata, Some(13..16));
1364        assert_eq!(layout.padding, 0);
1365        assert_eq!(layout.payload, 16..20);
1366        assert_eq!(layout.total, 20);
1367    }
1368
1369    proptest! {
1370        #[test]
1371        fn layout_structural_invariants(
1372            et_len_raw in frame_body_length(),
1373            meta in prop::option::of(frame_body_length()),
1374            payload_len in frame_body_length(),
1375        ) {
1376            // Cap event_type at its actual ceiling.
1377            let et_len = et_len_raw.min(MAX_EVENT_TYPE_LEN);
1378            // Cap metadata at its actual ceiling.
1379            let meta_capped = meta.map(|n| n.min(MAX_METADATA_LEN));
1380            // Cap payload at its actual ceiling.
1381            let payload_len_capped = payload_len.min(MAX_PAYLOAD_LEN);
1382            let layout = FrameLayout::compute_from_validated_lengths(
1383                et_len,
1384                meta_capped,
1385                payload_len_capped,
1386            ).expect("bounded inputs compute");
1387
1388            // I1: event_type starts immediately after the fixed header.
1389            prop_assert_eq!(
1390                usize::try_from(layout.event_type.start).unwrap(),
1391                HEADER_FIXED_SIZE,
1392            );
1393
1394            // I2: each variable-width range has length equal to its input.
1395            prop_assert_eq!(
1396                (layout.event_type.end - layout.event_type.start) as usize,
1397                et_len,
1398            );
1399            match (meta_capped, layout.metadata.clone()) {
1400                (None, None) => {},
1401                (Some(meta_len), Some(range)) => {
1402                    prop_assert_eq!((range.end - range.start) as usize, meta_len);
1403                }
1404                _ => prop_assert!(false, "metadata Option mismatch between input and layout"),
1405            }
1406            prop_assert_eq!(
1407                (layout.payload.end - layout.payload.start) as usize,
1408                payload_len_capped,
1409            );
1410
1411            // I3: ranges are non-overlapping and properly ordered.
1412            if let Some(m) = layout.metadata.clone() {
1413                prop_assert!(layout.event_type.end <= m.start);
1414                prop_assert!(m.end <= layout.payload.start);
1415            } else {
1416                prop_assert!(layout.event_type.end <= layout.payload.start);
1417            }
1418
1419            // I4: payload starts on a PAYLOAD_ALIGN boundary
1420            //     (the wire-format invariant zero-copy decoders rely on).
1421            let payload_start = usize::try_from(layout.payload.start).unwrap();
1422            prop_assert!(payload_start.is_multiple_of(PAYLOAD_ALIGN));
1423
1424            // I5: padding < align — the alignment math produces the
1425            //     minimum padding, never more than align - 1.
1426            prop_assert!(layout.padding < PAYLOAD_ALIGN);
1427
1428            // I6: total == payload.end as usize.
1429            prop_assert_eq!(layout.total, usize::try_from(layout.payload.end).unwrap());
1430
1431            // I7: total accounts exactly for header + bodies + padding.
1432            let body_total = et_len
1433                + meta_capped.unwrap_or(0)
1434                + layout.padding
1435                + payload_len_capped;
1436            prop_assert_eq!(layout.total, HEADER_FIXED_SIZE + body_total);
1437        }
1438    }
1439
1440    // -----------------------------------------------------------------
1441    // plan / execute
1442    // -----------------------------------------------------------------
1443
1444    #[test]
1445    fn plan_then_execute_matches_encode_frame_concrete() {
1446        // Anchored equivalence; the proptest below generalizes.
1447        let sv = SchemaVersion::from_u32(2).expect("nonzero");
1448        let et_v = et("Evt");
1449        let pl_v = pl(b"payload");
1450        let md_v = md(b"meta");
1451        let one_shot = encode_frame(sv, &et_v, &pl_v, Some(&md_v)).expect("ok");
1452        let staged = execute(plan(sv, &et_v, &pl_v, Some(&md_v)).expect("plan ok"));
1453        assert_eq!(one_shot.value.as_ref(), staged.value.as_ref());
1454        assert_eq!(one_shot.offsets.event_type, staged.offsets.event_type);
1455        assert_eq!(one_shot.offsets.metadata, staged.offsets.metadata);
1456        assert_eq!(one_shot.offsets.payload, staged.offsets.payload);
1457    }
1458
1459    // execute() invariants.
1460
1461    #[test]
1462    fn execute_buffer_length_equals_layout_total() {
1463        let cases: Vec<(EventType, Option<Metadata>, Payload)> = vec![
1464            (et(""), None, pl(b"")),
1465            (et("X"), None, pl(b"")),
1466            (et("Evt"), Some(md(b"meta")), pl(b"payload")),
1467            (et("LongerType"), Some(md(b"x")), pl(b"x")),
1468        ];
1469        for (et_v, md_v, pl_v) in cases {
1470            let p = plan(sv1(), &et_v, &pl_v, md_v.as_ref()).expect("plan ok");
1471            let total = p.layout.total;
1472            let frame = execute(p);
1473            assert_eq!(frame.value.len(), total);
1474        }
1475    }
1476
1477    #[test]
1478    fn execute_padding_bytes_are_zero() {
1479        // Choose inputs where padding > 0: 19 + 1 (et) = 20, padding = 12.
1480        let frame = encode_frame(sv1(), &et("x"), &pl(b"payload"), None).expect("ok");
1481        let pad_start = usize::try_from(frame.offsets.event_type.end).unwrap();
1482        let pad_end = usize::try_from(frame.offsets.payload.start).unwrap();
1483        assert!(pad_end > pad_start, "expected at least one padding byte");
1484        for (i, byte) in frame.value[pad_start..pad_end].iter().enumerate() {
1485            assert_eq!(
1486                *byte,
1487                0,
1488                "padding byte at offset {} is {:#x}",
1489                pad_start + i,
1490                byte
1491            );
1492        }
1493    }
1494
1495    proptest! {
1496        #[test]
1497        fn plan_execute_equals_encode_frame(
1498            (schema_version, event_type, metadata, payload) in valid_frame_inputs(),
1499        ) {
1500            let et_v = et(&event_type);
1501            let pl_v = pl(&payload);
1502            let md_v = metadata.as_deref().map(md);
1503            let one_shot = encode_frame(
1504                schema_version, &et_v, &pl_v, md_v.as_ref(),
1505            ).expect("valid inputs encode");
1506            let staged = execute(
1507                plan(schema_version, &et_v, &pl_v, md_v.as_ref())
1508                    .expect("valid inputs plan"),
1509            );
1510            // Whole-buffer equality is the strongest equivalence.
1511            prop_assert_eq!(one_shot.value.as_ref(), staged.value.as_ref());
1512            prop_assert_eq!(one_shot.offsets.event_type, staged.offsets.event_type);
1513            prop_assert_eq!(one_shot.offsets.metadata, staged.offsets.metadata);
1514            prop_assert_eq!(one_shot.offsets.payload, staged.offsets.payload);
1515        }
1516
1517        #[test]
1518        fn execute_invariants(
1519            (schema_version, event_type, metadata, payload) in valid_frame_inputs(),
1520        ) {
1521            let et_v = et(&event_type);
1522            let pl_v = pl(&payload);
1523            let md_v = metadata.as_deref().map(md);
1524            let p = plan(schema_version, &et_v, &pl_v, md_v.as_ref())
1525                .expect("valid inputs plan");
1526            let layout_total = p.layout.total;
1527            let event_type_range = p.layout.event_type.clone();
1528            let metadata_range = p.layout.metadata.clone();
1529            let payload_range = p.layout.payload.clone();
1530            let frame = execute(p);
1531
1532            // I1: buffer length equals layout.total.
1533            prop_assert_eq!(frame.value.len(), layout_total);
1534
1535            // I2: payload pointer is 16-byte aligned.
1536            let payload_slice_start = usize::try_from(payload_range.start).unwrap();
1537            let ptr = frame.value[payload_slice_start..].as_ptr().addr();
1538            prop_assert!(ptr.is_multiple_of(PAYLOAD_ALIGN));
1539
1540            // I3: each body byte lands at its layout offset.
1541            let et_start = usize::try_from(event_type_range.start).unwrap();
1542            let et_end = usize::try_from(event_type_range.end).unwrap();
1543            prop_assert_eq!(&frame.value[et_start..et_end], event_type.as_bytes());
1544            if let (Some(range), Some(meta)) = (metadata_range.clone(), metadata.as_deref()) {
1545                let s = usize::try_from(range.start).unwrap();
1546                let e = usize::try_from(range.end).unwrap();
1547                prop_assert_eq!(&frame.value[s..e], meta);
1548            }
1549            let p_start = usize::try_from(payload_range.start).unwrap();
1550            let p_end = usize::try_from(payload_range.end).unwrap();
1551            prop_assert_eq!(&frame.value[p_start..p_end], payload.as_slice());
1552
1553            // I4: padding bytes (between event_type/metadata end and payload start) are zero.
1554            let pad_start = metadata_range
1555                .as_ref()
1556                .map_or(et_end, |r| usize::try_from(r.end).unwrap());
1557            for byte in &frame.value[pad_start..p_start] {
1558                prop_assert_eq!(*byte, 0u8);
1559            }
1560        }
1561    }
1562
1563    // -----------------------------------------------------------------
1564    // Value-newtype input acceptance + corrupt-disk schema_version
1565    // -----------------------------------------------------------------
1566
1567    #[test]
1568    fn encode_frame_accepts_value_newtypes() {
1569        let et_v = EventType::from_static_str("UserCreated");
1570        let payload = Payload::from_bytes(Bytes::from_static(b"hello")).expect("valid");
1571        let metadata = Metadata::from_bytes(Bytes::from_static(b"m")).expect("valid");
1572        let sv = SchemaVersion::INITIAL;
1573        let frame = encode_frame(sv, &et_v, &payload, Some(&metadata)).expect("valid frame");
1574        let decoded = decode_frame(&frame.value).expect("decodes");
1575        assert_eq!(decoded.schema_version, sv);
1576    }
1577
1578    #[test]
1579    fn decode_frame_rejects_corrupt_schema_version_zero() {
1580        // Hand-craft a frame with schema_version=0 on the wire, simulating
1581        // corrupt on-disk data. Going through encode_frame with a
1582        // SchemaVersion is structurally impossible.
1583        let et_v = EventType::from_static_str("X");
1584        let payload = Payload::from_bytes(Bytes::from_static(b"p")).expect("valid");
1585        let sv_one = SchemaVersion::INITIAL;
1586        let frame = encode_frame(sv_one, &et_v, &payload, None).expect("valid frame for tamper");
1587        let mut bytes_vec = frame.value.to_vec();
1588        bytes_vec[SCHEMA_VERSION_OFFSET..EVENT_TYPE_LEN_OFFSET].fill(0);
1589        let tampered = Bytes::from(bytes_vec);
1590        let err = decode_frame(&tampered).expect_err("schema_version=0 on wire rejected");
1591        assert!(matches!(err, DecodeError::CorruptSchemaVersion));
1592    }
1593
1594    // -----------------------------------------------------------------
1595    // Version-byte: 4 mandatory test categories
1596    // -----------------------------------------------------------------
1597
1598    #[test]
1599    fn decode_rejects_every_unknown_version_byte() {
1600        // A valid V2 frame, then flip offset 0 to each byte that is neither a
1601        // known V1 (1) nor V2 (2) tag — every one must be rejected as
1602        // unsupported, never misparsed.
1603        let frame = encode_frame(sv1(), &et("Evt"), &pl(b"payload"), Some(&md(b"m")))
1604            .expect("valid frame for tamper base");
1605        for bad in (0u8..=u8::MAX).filter(|b| *b != 1 && *b != 2) {
1606            let mut bytes_vec = frame.value.to_vec();
1607            bytes_vec[VERSION_OFFSET] = bad;
1608            let tampered = Bytes::from(bytes_vec);
1609            match decode_frame(&tampered) {
1610                Err(DecodeError::UnsupportedFrameVersion { version }) => {
1611                    assert_eq!(version, bad);
1612                }
1613                other => panic!("version byte {bad} should be rejected, got {other:?}"),
1614            }
1615        }
1616    }
1617
1618    #[test]
1619    fn decode_reads_a_v1_frame_dropping_its_global_seq() {
1620        // Transition guarantee: a hand-built V1 frame (version byte 1, with the
1621        // 8-byte global_seq the format no longer encodes) still decodes — the
1622        // global_seq is read and discarded, schema/event_type/payload recover.
1623        // Layout: [1][u64 global_seq][u32 schema][u16 et_len][u32 meta_len]
1624        //         [event_type][padding][payload]
1625        let event_type = b"Created";
1626        let payload = b"data-bytes";
1627        let mut buf = Vec::new();
1628        buf.push(1u8); // V1 version tag
1629        buf.extend_from_slice(&999u64.to_le_bytes()); // global_seq (to be discarded)
1630        buf.extend_from_slice(&7u32.to_le_bytes()); // schema_version
1631        buf.extend_from_slice(&u16::try_from(event_type.len()).unwrap().to_le_bytes()); // et_len
1632        buf.extend_from_slice(&META_LEN_ABSENT.to_le_bytes()); // no metadata
1633        buf.extend_from_slice(event_type);
1634        // Pad so the payload lands on the 16-byte boundary the format promises.
1635        let post_et = buf.len();
1636        buf.resize(post_et + align_padding(post_et, PAYLOAD_ALIGN), 0u8);
1637        buf.extend_from_slice(payload);
1638
1639        let decoded = decode_frame(&buf).expect("a well-formed V1 frame must still decode");
1640        assert_eq!(decoded.schema_version.get(), 7);
1641        let et = &buf
1642            [decoded.offsets.event_type.start as usize..decoded.offsets.event_type.end as usize];
1643        assert_eq!(et, event_type);
1644        let pl = &buf[decoded.offsets.payload.start as usize..decoded.offsets.payload.end as usize];
1645        assert_eq!(pl, payload);
1646    }
1647
1648    #[test]
1649    fn decode_empty_buffer_is_too_short_not_version_error() {
1650        match decode_frame(&[]) {
1651            Err(DecodeError::ValueTooShort { min, actual }) => {
1652                assert_eq!(min, HEADER_FIXED_SIZE);
1653                assert_eq!(actual, 0);
1654            }
1655            other => panic!("empty buffer should be ValueTooShort, got {other:?}"),
1656        }
1657    }
1658
1659    #[test]
1660    fn corrupt_version_byte_surfaces_unsupported_not_panic() {
1661        // Simulate on-disk bit-rot of byte 0 of a persisted frame.
1662        let frame = encode_frame(sv1(), &et("X"), &pl(b"p"), None).expect("encode");
1663        let mut bytes_vec = frame.value.to_vec();
1664        bytes_vec[VERSION_OFFSET] = 0xFF;
1665        let tampered = Bytes::from(bytes_vec);
1666        let err = decode_frame(&tampered).expect_err("corrupt version rejected");
1667        assert!(matches!(
1668            err,
1669            DecodeError::UnsupportedFrameVersion { version: 0xFF }
1670        ));
1671    }
1672}