Skip to main content

verit_core/
message.rs

1//! Zero-copy message access. [`Message::parse`] only reads the 24-byte
2//! header; nothing else is touched until a field is asked for, and every
3//! field read is a bounds-checked load straight out of the buffer through a
4//! precompiled [`Resolver`] plan. Strings and byte fields are returned as
5//! `&str` / `&[u8]` borrowing the message buffer — no allocation, no copy.
6
7use std::cell::Cell;
8
9use crate::encode::{FLAG_INLINE_SCHEMA, HEADER_LEN, MESSAGE_MAGIC};
10use crate::error::{Error, Result};
11use crate::resolve::{
12    ElemPlan, FieldSource, Load, MapPlan, NumKind, Resolver, StructPlan, UnionPlan,
13};
14use crate::schema::{Default, Schema, StructDef};
15
16/// A per-read **traversal budget** — an opt-in guard against amplification-DoS
17/// on untrusted messages (the wire spec §5.2). Veritate's offsets are absolute and
18/// may alias, so a small hostile message can point many fields at the same
19/// large sub-object and make a naive full read do work super-linear in the
20/// message's own size. Memory safety (bounds, depth, allocation) always holds;
21/// a `Budget` additionally caps *total work*.
22///
23/// Create one per message read and open the root with
24/// [`Message::root_bounded`]. Every byte a read touches through the buffer is
25/// charged against the budget; because aliased sub-objects are charged on every
26/// visit, total work is capped regardless of how offsets alias. When exhausted,
27/// reads fail with [`Error::TraversalBudgetExceeded`] instead of running away.
28/// A good starting limit is [`Message::suggested_budget`].
29///
30/// ```
31/// # use verit_core::*;
32/// # fn go(buf: &[u8], resolver: &Resolver) -> Result<()> {
33/// let msg = Message::parse(buf)?;
34/// let budget = Budget::new(msg.suggested_budget());
35/// let root = msg.root_bounded(resolver, &budget)?;
36/// // ... traverse `root`; any amplification trips TraversalBudgetExceeded ...
37/// # Ok(()) }
38/// ```
39#[derive(Debug)]
40pub struct Budget {
41    remaining: Cell<u64>,
42}
43
44impl Budget {
45    /// A budget that allows `limit` total bytes of buffer access before
46    /// tripping.
47    pub fn new(limit: u64) -> Self {
48        Budget {
49            remaining: Cell::new(limit),
50        }
51    }
52
53    /// Bytes still allowed before the budget trips.
54    pub fn remaining(&self) -> u64 {
55        self.remaining.get()
56    }
57
58    /// Charge `bytes` against the budget, returning
59    /// [`Error::TraversalBudgetExceeded`] if it would go negative. Public so
60    /// generated code (see [`crate::wire`]) can charge its own buffer accesses.
61    #[inline]
62    pub fn charge(&self, bytes: u64) -> Result<()> {
63        match self.remaining.get().checked_sub(bytes) {
64            Some(rem) => {
65                self.remaining.set(rem);
66                Ok(())
67            }
68            None => Err(Error::TraversalBudgetExceeded),
69        }
70    }
71}
72
73#[inline]
74fn charge(budget: Option<&Budget>, bytes: u64) -> Result<()> {
75    match budget {
76        Some(b) => b.charge(bytes),
77        None => Ok(()),
78    }
79}
80
81#[derive(Clone, Debug)]
82pub struct Message<'b> {
83    buf: &'b [u8],
84    schema_id: u128,
85    root_offset: u32,
86    schema_range: Option<(usize, usize)>,
87}
88
89impl<'b> Message<'b> {
90    pub fn parse(buf: &'b [u8]) -> Result<Message<'b>> {
91        if buf.len() < HEADER_LEN {
92            return Err(Error::Truncated);
93        }
94        // Magic is "VRT" + a version digit. A message from a different major
95        // version fails with UnsupportedVersion (never a silent misread); a
96        // buffer that is not a Veritate message at all fails with BadMagic.
97        if &buf[0..4] != MESSAGE_MAGIC {
98            if buf[0..3] == MESSAGE_MAGIC[0..3] {
99                return Err(Error::UnsupportedVersion {
100                    found: buf[3],
101                    supported: MESSAGE_MAGIC[3],
102                });
103            }
104            return Err(Error::BadMagic);
105        }
106        let flags = u16::from_le_bytes(buf[4..6].try_into().unwrap());
107        // Reject header bits this version does not define, so a message that
108        // relies on semantics we don't implement is refused rather than
109        // misinterpreted. Only bit 0 (inline schema) is defined in v2.
110        if flags & !FLAG_INLINE_SCHEMA != 0 {
111            return Err(Error::MalformedHeader("unknown flag bit set"));
112        }
113        let reserved = u16::from_le_bytes(buf[6..8].try_into().unwrap());
114        if reserved != 0 {
115            return Err(Error::MalformedHeader("reserved header field is not zero"));
116        }
117        let schema_id = u128::from_le_bytes(buf[8..24].try_into().unwrap());
118        let root_offset = u32::from_le_bytes(buf[24..28].try_into().unwrap());
119        let schema_len = u32::from_le_bytes(buf[28..32].try_into().unwrap()) as usize;
120        let schema_range = if flags & FLAG_INLINE_SCHEMA != 0 {
121            let end = HEADER_LEN.checked_add(schema_len).ok_or(Error::Truncated)?;
122            if end > buf.len() {
123                return Err(Error::Truncated);
124            }
125            Some((HEADER_LEN, end))
126        } else {
127            None
128        };
129        Ok(Message {
130            buf,
131            schema_id,
132            root_offset,
133            schema_range,
134        })
135    }
136
137    pub fn buffer(&self) -> &'b [u8] {
138        self.buf
139    }
140
141    pub fn schema_id(&self) -> u128 {
142        self.schema_id
143    }
144
145    /// Absolute offset of the root struct block. Exposed for generated code
146    /// (the codegen identity fast path reads at constant offsets from here).
147    pub fn root_offset(&self) -> u32 {
148        self.root_offset
149    }
150
151    pub fn has_inline_schema(&self) -> bool {
152        self.schema_range.is_some()
153    }
154
155    /// Decode the inline writer schema, if the message carries one. The
156    /// decoded schema's content hash must match the header's schema id.
157    pub fn writer_schema(&self) -> Result<Option<Schema>> {
158        match self.schema_range {
159            None => Ok(None),
160            Some((start, end)) => {
161                let schema = Schema::from_canonical(&self.buf[start..end])?;
162                if schema.id() != self.schema_id {
163                    return Err(Error::SchemaIdMismatch {
164                        message: self.schema_id,
165                        expected: schema.id(),
166                    });
167                }
168                Ok(Some(schema))
169            }
170        }
171    }
172
173    /// Open the root struct through a resolver whose writer schema matches
174    /// this message's schema id. **Unbounded**: reads are memory-safe but do
175    /// not cap total traversal work — use this for trusted data or after an
176    /// upstream size cap. For untrusted input, prefer [`Self::root_bounded`].
177    pub fn root<'r>(&self, resolver: &'r Resolver) -> Result<StructReader<'b, 'r>> {
178        self.open_root(resolver, None)
179    }
180
181    /// Open the root struct with a [`Budget`] that caps total traversal work,
182    /// guarding against amplification-DoS on untrusted input (the wire spec §5.2).
183    /// The `budget` outlives the returned readers, which charge every byte they
184    /// touch against it.
185    pub fn root_bounded<'r>(
186        &self,
187        resolver: &'r Resolver,
188        budget: &'r Budget,
189    ) -> Result<StructReader<'b, 'r>> {
190        self.open_root(resolver, Some(budget))
191    }
192
193    /// Walk the whole message once under a [`Budget`], touching every reachable
194    /// field and element, and return `Ok(())` iff the traversal stays within
195    /// budget (and depth). This is the guard for the **fast/codegen** path:
196    /// verify untrusted bytes *once*, and if it passes, a single subsequent
197    /// zero-copy scan — via the generated readers or [`Self::root`] — cannot
198    /// amplify beyond the budget, because a full scan touches no more than this
199    /// walk did. Trusted data skips it and pays nothing. (Analogous to
200    /// FlatBuffers' verified `root`, but Veritate needs no separate encoding —
201    /// the same bytes are then read directly.)
202    pub fn verify(&self, resolver: &Resolver, budget: &Budget) -> Result<()> {
203        let root = self.root_bounded(resolver, budget)?;
204        verify_struct(&root, 0)
205    }
206
207    /// A sensible default traversal-budget limit for this message:
208    /// `max(64 KiB, 64 × message length)`. Scales with legitimate message size
209    /// (a well-formed message touches ~its own size once), leaving generous
210    /// headroom while capping amplification at ~64× the bytes on the wire.
211    pub fn suggested_budget(&self) -> u64 {
212        (self.buf.len() as u64).saturating_mul(64).max(64 * 1024)
213    }
214
215    fn open_root<'r>(
216        &self,
217        resolver: &'r Resolver,
218        budget: Option<&'r Budget>,
219    ) -> Result<StructReader<'b, 'r>> {
220        if resolver.writer_id() != self.schema_id {
221            return Err(Error::SchemaIdMismatch {
222                message: self.schema_id,
223                expected: resolver.writer_id(),
224            });
225        }
226        Ok(StructReader {
227            buf: self.buf,
228            base: self.root_offset,
229            plan: resolver.plan(resolver.root_plan_index()),
230            resolver,
231            budget,
232        })
233    }
234}
235
236/// A field value read from the buffer. Scalars are by value; strings, bytes,
237/// structs, and lists borrow the message buffer (`'b`).
238#[derive(Clone, Debug)]
239pub enum Ref<'b, 'r> {
240    Bool(bool),
241    U8(u8),
242    U16(u16),
243    U32(u32),
244    U64(u64),
245    I8(i8),
246    I16(i16),
247    I32(i32),
248    I64(i64),
249    F32(f32),
250    F64(f64),
251    Str(&'b str),
252    Bytes(&'b [u8]),
253    Enum(u32),
254    Struct(StructReader<'b, 'r>),
255    List(ListReader<'b, 'r>),
256    Map(MapReader<'b, 'r>),
257    Union(UnionReader<'b, 'r>),
258}
259
260impl<'b, 'r> Ref<'b, 'r> {
261    pub fn kind(&self) -> &'static str {
262        match self {
263            Ref::Bool(_) => "bool",
264            Ref::U8(_) => "u8",
265            Ref::U16(_) => "u16",
266            Ref::U32(_) => "u32",
267            Ref::U64(_) => "u64",
268            Ref::I8(_) => "i8",
269            Ref::I16(_) => "i16",
270            Ref::I32(_) => "i32",
271            Ref::I64(_) => "i64",
272            Ref::F32(_) => "f32",
273            Ref::F64(_) => "f64",
274            Ref::Str(_) => "string",
275            Ref::Bytes(_) => "bytes",
276            Ref::Enum(_) => "enum",
277            Ref::Struct(_) => "struct",
278            Ref::List(_) => "list",
279            Ref::Map(_) => "map",
280            Ref::Union(_) => "union",
281        }
282    }
283}
284
285#[derive(Clone, Debug)]
286pub struct StructReader<'b, 'r> {
287    buf: &'b [u8],
288    base: u32,
289    plan: &'r StructPlan,
290    resolver: &'r Resolver,
291    /// `Some` on a bounded read (via [`Message::root_bounded`]); charged for
292    /// every byte this reader and its children touch. `None` = unbounded.
293    budget: Option<&'r Budget>,
294}
295
296macro_rules! typed_getter {
297    ($doc:literal, $name:ident, $variant:ident, $ret:ty) => {
298        #[doc = $doc]
299        pub fn $name(&self, id: u16) -> Result<Option<$ret>> {
300            match self.get(id)? {
301                None => Ok(None),
302                Some(Ref::$variant(x)) => Ok(Some(x)),
303                Some(other) => Err(Error::TypeMismatch {
304                    expected: stringify!($variant).to_lowercase(),
305                    got: other.kind().into(),
306                }),
307            }
308        }
309    };
310}
311
312impl<'b, 'r> StructReader<'b, 'r> {
313    /// Read a field by its stable ID. `Ok(None)` means absent — either the
314    /// writer didn't set it, or the writer's schema doesn't have it at all.
315    pub fn get(&self, id: u16) -> Result<Option<Ref<'b, 'r>>> {
316        let pos = match self.plan.fields.binary_search_by_key(&id, |f| f.id) {
317            Ok(pos) => pos,
318            Err(_) => return Err(Error::UnknownFieldId(id)),
319        };
320        match &self.plan.fields[pos].source {
321            FieldSource::Absent => Ok(None),
322            FieldSource::Slot {
323                offset,
324                presence_byte,
325                presence_mask,
326                load,
327            } => {
328                // Mask 0 = dense writer struct: the field is always present.
329                if *presence_mask != 0 {
330                    let pbyte = read_u8(
331                        self.buf,
332                        self.base as u64 + *presence_byte as u64,
333                        self.budget,
334                    )?;
335                    if pbyte & presence_mask == 0 {
336                        return Ok(None);
337                    }
338                }
339                let at = self.base as u64 + *offset as u64;
340                load_at(self.buf, self.resolver, load, at, self.budget).map(Some)
341            }
342            FieldSource::Packed { writer_pos, load } => {
343                // Packed writer struct: recover presence and offset from the
344                // per-message bitmap via the writer's popcount layout.
345                let lay = self
346                    .resolver
347                    .writer_schema()
348                    .packed_layout_unchecked(self.plan.writer_type);
349                let bitmap =
350                    read_bitmap(self.buf, self.base as u64, lay.bitmap_bytes, self.budget)?;
351                if bitmap & (1u64 << writer_pos) == 0 {
352                    return Ok(None);
353                }
354                let at = self.base as u64 + lay.field_offset(bitmap, *writer_pos as usize) as u64;
355                load_at(self.buf, self.resolver, load, at, self.budget).map(Some)
356            }
357        }
358    }
359
360    /// Like [`get`](Self::get), but if the field is absent and the **reader
361    /// schema** gives it a custom default, return that default value instead of
362    /// `None`. Presence is unchanged — [`get`](Self::get) still reports the raw
363    /// wire state — so this is a read-time convenience layered on top.
364    pub fn get_or_default(&self, id: u16) -> Result<Option<Ref<'b, 'r>>> {
365        if let Some(v) = self.get(id)? {
366            return Ok(Some(v));
367        }
368        Ok(self
369            .struct_def()
370            .fields
371            .iter()
372            .find(|f| f.id == id)
373            .and_then(|f| f.default)
374            .map(default_to_ref))
375    }
376
377    /// The reader-schema definition of this struct (field names for
378    /// self-description).
379    pub fn struct_def(&self) -> &'r StructDef {
380        self.resolver
381            .reader_schema()
382            .struct_def_unchecked(self.plan.reader_type)
383    }
384
385    typed_getter!("Typed getter for `bool` fields.", get_bool, Bool, bool);
386    typed_getter!("Typed getter for `u8` fields.", get_u8, U8, u8);
387    typed_getter!("Typed getter for `u16` fields.", get_u16, U16, u16);
388    typed_getter!("Typed getter for `u32` fields.", get_u32, U32, u32);
389    typed_getter!("Typed getter for `u64` fields.", get_u64, U64, u64);
390    typed_getter!("Typed getter for `i8` fields.", get_i8, I8, i8);
391    typed_getter!("Typed getter for `i16` fields.", get_i16, I16, i16);
392    typed_getter!("Typed getter for `i32` fields.", get_i32, I32, i32);
393    typed_getter!("Typed getter for `i64` fields.", get_i64, I64, i64);
394    typed_getter!("Typed getter for `f32` fields.", get_f32, F32, f32);
395    typed_getter!("Typed getter for `f64` fields.", get_f64, F64, f64);
396    typed_getter!(
397        "Typed getter for string fields (borrows the buffer).",
398        get_str,
399        Str,
400        &'b str
401    );
402    typed_getter!(
403        "Typed getter for bytes fields (borrows the buffer).",
404        get_bytes,
405        Bytes,
406        &'b [u8]
407    );
408    typed_getter!(
409        "Typed getter for enum fields (raw open value).",
410        get_enum,
411        Enum,
412        u32
413    );
414    typed_getter!(
415        "Typed getter for nested struct fields.",
416        get_struct,
417        Struct,
418        StructReader<'b, 'r>
419    );
420    typed_getter!(
421        "Typed getter for list fields.",
422        get_list,
423        List,
424        ListReader<'b, 'r>
425    );
426    typed_getter!(
427        "Typed getter for map fields.",
428        get_map,
429        Map,
430        MapReader<'b, 'r>
431    );
432    typed_getter!(
433        "Typed getter for union fields.",
434        get_union,
435        Union,
436        UnionReader<'b, 'r>
437    );
438}
439
440#[derive(Clone, Debug)]
441pub struct ListReader<'b, 'r> {
442    buf: &'b [u8],
443    resolver: &'r Resolver,
444    elem: &'r ElemPlan,
445    elems_base: u64,
446    count: u32,
447    budget: Option<&'r Budget>,
448}
449
450impl<'b, 'r> ListReader<'b, 'r> {
451    pub fn len(&self) -> u32 {
452        self.count
453    }
454
455    pub fn is_empty(&self) -> bool {
456        self.count == 0
457    }
458
459    pub fn get(&self, index: u32) -> Result<Ref<'b, 'r>> {
460        if index >= self.count {
461            return Err(Error::IndexOutOfBounds);
462        }
463        let at = self.elems_base + index as u64 * self.elem.stride as u64;
464        match &self.elem.load {
465            // Fixed struct elements are stored inline: the element position IS
466            // the block. Packed struct elements are variable-size and stored
467            // by u32 offset (struct_inline = false), so they fall through to
468            // load_at, which follows the offset.
469            Load::Struct(plan_idx) if self.elem.struct_inline => {
470                let base = u32::try_from(at).map_err(|_| Error::OutOfBounds)?;
471                Ok(Ref::Struct(StructReader {
472                    buf: self.buf,
473                    base,
474                    plan: self.resolver.plan(*plan_idx),
475                    resolver: self.resolver,
476                    budget: self.budget,
477                }))
478            }
479            other => load_at(self.buf, self.resolver, other, at, self.budget),
480        }
481    }
482
483    pub fn iter(&self) -> impl Iterator<Item = Result<Ref<'b, 'r>>> + '_ {
484        (0..self.count).map(move |i| self.get(i))
485    }
486
487    /// Borrow a `list<u8>` as a slice of the message buffer — no copy, no
488    /// conversion.
489    ///
490    /// `u8` is the one numeric element type with no endianness and no
491    /// alignment to satisfy, so it is the only one that can be handed back as a
492    /// borrow. Every wider type needs a little-endian decode, which safe Rust
493    /// cannot do in place; use the `copy_*` / `to_vec_*` methods for those.
494    pub fn as_u8_slice(&self) -> Result<&'b [u8]> {
495        match &self.elem.load {
496            Load::Num {
497                from: NumKind::U8,
498                to: NumKind::U8,
499            } if self.elem.stride == 1 => {
500                get_slice(self.buf, self.elems_base, self.count as u64, self.budget)
501            }
502            other => Err(Error::TypeMismatch {
503                expected: "list<u8>".into(),
504                got: elem_kind(other).into(),
505            }),
506        }
507    }
508}
509
510/// Name an element plan for a type-mismatch error.
511fn elem_kind(load: &Load) -> &'static str {
512    match load {
513        Load::Bool => "list<bool>",
514        Load::Num { to, .. } => match to {
515            NumKind::U8 => "list<u8>",
516            NumKind::U16 => "list<u16>",
517            NumKind::U32 => "list<u32>",
518            NumKind::U64 => "list<u64>",
519            NumKind::I8 => "list<i8>",
520            NumKind::I16 => "list<i16>",
521            NumKind::I32 => "list<i32>",
522            NumKind::I64 => "list<i64>",
523            NumKind::F32 => "list<f32>",
524            NumKind::F64 => "list<f64>",
525        },
526        Load::Enum => "list<enum>",
527        Load::Str => "list<string>",
528        Load::Bytes => "list<bytes>",
529        Load::Struct(_) => "list<struct>",
530        Load::List(_) => "list<list>",
531        Load::Map(_) => "list<map>",
532        Load::Union(_) => "list<union>",
533    }
534}
535
536/// Bulk readers for numeric lists — the read-side peer of the bulk list
537/// *writer* fast path (ADR-0010).
538///
539/// A `list<f32>` of 1,536 elements is the shape that matters here (an embedding
540/// vector), and reading it element-at-a-time through [`Ref`] pays a match and a
541/// bounds check per element. These methods take one bounds check and one budget
542/// charge for the whole run, then decode in a tight loop the optimiser can
543/// vectorize.
544///
545/// Widening still works: a `list<u16>` written by an older schema reads through
546/// `copy_u32` into a reader that widened the field, at the cost of falling back
547/// to the per-element path.
548macro_rules! bulk_num {
549    ($ty:ty, $kind:ident, $variant:ident, $copy:ident, $to_vec:ident, $name:literal) => {
550        impl<'b, 'r> ListReader<'b, 'r> {
551            #[doc = concat!("Bulk-copy a `", $name, "` list into `out`, returning how many elements were written.")]
552            ///
553            /// Copies `min(len(), out.len())` elements — a short `out` is not an
554            /// error, so a caller can read a prefix deliberately.
555            pub fn $copy(&self, out: &mut [$ty]) -> Result<usize> {
556                const WIDTH: usize = std::mem::size_of::<$ty>();
557                let Load::Num { from, to } = &self.elem.load else {
558                    return Err(Error::TypeMismatch {
559                        expected: concat!("list<", $name, ">").into(),
560                        got: elem_kind(&self.elem.load).into(),
561                    });
562                };
563                if *to != NumKind::$kind {
564                    return Err(Error::TypeMismatch {
565                        expected: concat!("list<", $name, ">").into(),
566                        got: elem_kind(&self.elem.load).into(),
567                    });
568                }
569                let n = (self.count as usize).min(out.len());
570                if n == 0 {
571                    return Ok(0);
572                }
573
574                // Fast path: the writer stored exactly this type, packed at its
575                // natural stride. One range check and one budget charge for the
576                // whole run, then a straight little-endian decode.
577                if *from == NumKind::$kind && self.elem.stride as usize == WIDTH {
578                    let span = (n as u64) * WIDTH as u64;
579                    let bytes = get_slice(self.buf, self.elems_base, span, self.budget)?;
580                    for (slot, chunk) in out.iter_mut().zip(bytes.chunks_exact(WIDTH)) {
581                        *slot = <$ty>::from_le_bytes(chunk.try_into().unwrap());
582                    }
583                    return Ok(n);
584                }
585
586                // Slow path: the writer used a narrower type and the reader
587                // widened it, so each element needs converting.
588                for (i, slot) in out.iter_mut().enumerate().take(n) {
589                    let at = self.elems_base + i as u64 * self.elem.stride as u64;
590                    *slot = match num_ref(*to, read_wide(self.buf, at, *from, self.budget)?)? {
591                        Ref::$variant(x) => x,
592                        _ => return Err(Error::Internal("num kind mismatch in bulk list read")),
593                    };
594                }
595                Ok(n)
596            }
597
598            #[doc = concat!("Read a whole `", $name, "` list into a new `Vec`.")]
599            ///
600            /// Allocates once, at the exact length. Prefer
601            #[doc = concat!("[`", stringify!($copy), "`](Self::", stringify!($copy), ")")]
602            /// with a reused buffer on a hot path.
603            pub fn $to_vec(&self) -> Result<Vec<$ty>> {
604                // `count` is attacker-controlled, so the elements are proven to
605                // be inside the buffer *before* anything is sized by it — a
606                // forged count must fail on arithmetic, not on an OOM kill
607                // (File Format Specification §10, wire spec §5).
608                let span = (self.count as u64)
609                    .checked_mul(self.elem.stride as u64)
610                    .ok_or(Error::OutOfBounds)?;
611                get_slice(self.buf, self.elems_base, span, None)?;
612
613                let mut out: Vec<$ty> = Vec::new();
614                out.try_reserve_exact(self.count as usize)
615                    .map_err(|_| Error::OutOfBounds)?;
616                out.resize(self.count as usize, <$ty>::default());
617                let n = self.$copy(&mut out)?;
618                out.truncate(n);
619                Ok(out)
620            }
621        }
622    };
623}
624
625bulk_num!(u8, U8, U8, copy_u8, to_vec_u8, "u8");
626bulk_num!(u16, U16, U16, copy_u16, to_vec_u16, "u16");
627bulk_num!(u32, U32, U32, copy_u32, to_vec_u32, "u32");
628bulk_num!(u64, U64, U64, copy_u64, to_vec_u64, "u64");
629bulk_num!(i8, I8, I8, copy_i8, to_vec_i8, "i8");
630bulk_num!(i16, I16, I16, copy_i16, to_vec_i16, "i16");
631bulk_num!(i32, I32, I32, copy_i32, to_vec_i32, "i32");
632bulk_num!(i64, I64, I64, copy_i64, to_vec_i64, "i64");
633bulk_num!(f32, F32, F32, copy_f32, to_vec_f32, "f32");
634bulk_num!(f64, F64, F64, copy_f64, to_vec_f64, "f64");
635
636/// A `map<K, V>` read lazily from the buffer. Entries are stored sorted by key
637/// (canonical order), so [`get`](MapReader::get) yields them in that order.
638#[derive(Clone, Debug)]
639pub struct MapReader<'b, 'r> {
640    buf: &'b [u8],
641    resolver: &'r Resolver,
642    plan: &'r MapPlan,
643    entries_base: u64,
644    count: u32,
645    budget: Option<&'r Budget>,
646}
647
648impl<'b, 'r> MapReader<'b, 'r> {
649    pub fn len(&self) -> u32 {
650        self.count
651    }
652
653    pub fn is_empty(&self) -> bool {
654        self.count == 0
655    }
656
657    /// The `(key, value)` of entry `index` (in canonical key order).
658    pub fn get(&self, index: u32) -> Result<(Ref<'b, 'r>, Ref<'b, 'r>)> {
659        if index >= self.count {
660            return Err(Error::IndexOutOfBounds);
661        }
662        let entry = self.entries_base + index as u64 * self.plan.stride as u64;
663        let key = load_at(
664            self.buf,
665            self.resolver,
666            &self.plan.key,
667            entry + self.plan.key_off as u64,
668            self.budget,
669        )?;
670        let value = load_at(
671            self.buf,
672            self.resolver,
673            &self.plan.value,
674            entry + self.plan.value_off as u64,
675            self.budget,
676        )?;
677        Ok((key, value))
678    }
679
680    pub fn iter(&self) -> impl Iterator<Item = Result<(Ref<'b, 'r>, Ref<'b, 'r>)>> + '_ {
681        (0..self.count).map(move |i| self.get(i))
682    }
683}
684
685/// A `union<…>` value read lazily: a variant [`tag`](UnionReader::tag) and the
686/// selected variant's [`value`](UnionReader::value).
687#[derive(Clone, Debug)]
688pub struct UnionReader<'b, 'r> {
689    buf: &'b [u8],
690    resolver: &'r Resolver,
691    plan: &'r UnionPlan,
692    base: u64,
693    tag: u32,
694    budget: Option<&'r Budget>,
695}
696
697impl<'b, 'r> UnionReader<'b, 'r> {
698    /// The variant tag (index into the union's variant list).
699    pub fn tag(&self) -> u32 {
700        self.tag
701    }
702
703    /// The selected variant's value. Errors with [`Error::BadUnionTag`] if the
704    /// wire tag is out of range for the reader schema's variant list.
705    pub fn value(&self) -> Result<Ref<'b, 'r>> {
706        let vp = self
707            .plan
708            .variants
709            .get(self.tag as usize)
710            .ok_or(Error::BadUnionTag(self.tag))?;
711        load_at(
712            self.buf,
713            self.resolver,
714            &vp.load,
715            self.base + vp.payload_off as u64,
716            self.budget,
717        )
718    }
719}
720
721/// Materialize a scalar [`Default`] into a [`Ref`]. Defaults are by-value
722/// scalars, so the returned `Ref` is valid for any lifetimes.
723fn default_to_ref<'b, 'r>(d: Default) -> Ref<'b, 'r> {
724    match d {
725        Default::Bool(x) => Ref::Bool(x),
726        Default::U8(x) => Ref::U8(x),
727        Default::U16(x) => Ref::U16(x),
728        Default::U32(x) => Ref::U32(x),
729        Default::U64(x) => Ref::U64(x),
730        Default::I8(x) => Ref::I8(x),
731        Default::I16(x) => Ref::I16(x),
732        Default::I32(x) => Ref::I32(x),
733        Default::I64(x) => Ref::I64(x),
734        Default::F32(bits) => Ref::F32(f32::from_bits(bits)),
735        Default::F64(bits) => Ref::F64(f64::from_bits(bits)),
736        Default::Enum(x) => Ref::Enum(x),
737    }
738}
739
740/// Depth ceiling for [`Message::verify`], matching `dump_json`. Bounds forged
741/// offset *cycles* (which the byte budget alone would only stop after burning
742/// the whole budget); far deeper than any real data.
743const MAX_VERIFY_DEPTH: u32 = 128;
744
745fn verify_struct(sr: &StructReader, depth: u32) -> Result<()> {
746    if depth > MAX_VERIFY_DEPTH {
747        return Err(Error::DepthLimitExceeded);
748    }
749    // `struct_def` comes from the reader schema; every present field is read
750    // (charging the budget via the bounded reader) and recursed into.
751    let ids: Vec<u16> = sr.struct_def().fields.iter().map(|f| f.id).collect();
752    for id in ids {
753        if let Some(v) = sr.get(id)? {
754            verify_ref(&v, depth)?;
755        }
756    }
757    Ok(())
758}
759
760fn verify_ref(v: &Ref, depth: u32) -> Result<()> {
761    match v {
762        Ref::Struct(s) => verify_struct(s, depth + 1),
763        Ref::List(l) => {
764            for i in 0..l.len() {
765                verify_ref(&l.get(i)?, depth + 1)?;
766            }
767            Ok(())
768        }
769        Ref::Map(m) => {
770            for i in 0..m.len() {
771                let (k, v) = m.get(i)?;
772                verify_ref(&k, depth + 1)?;
773                verify_ref(&v, depth + 1)?;
774            }
775            Ok(())
776        }
777        Ref::Union(u) => verify_ref(&u.value()?, depth + 1),
778        _ => Ok(()),
779    }
780}
781
782// ---------------------------------------------------------------------------
783// Raw bounds-checked loads
784// ---------------------------------------------------------------------------
785
786/// Bounds-checked slice fetch. On a bounded read (`budget = Some`) it also
787/// charges the touched bytes against the budget — since *every* buffer access
788/// funnels through here, that alone caps total traversal work: an aliased
789/// sub-object re-read costs its bytes again each time (the wire spec §5.2).
790fn get_slice<'b>(buf: &'b [u8], off: u64, len: u64, budget: Option<&Budget>) -> Result<&'b [u8]> {
791    charge(budget, len)?;
792    let start = usize::try_from(off).map_err(|_| Error::OutOfBounds)?;
793    let len = usize::try_from(len).map_err(|_| Error::OutOfBounds)?;
794    let end = start.checked_add(len).ok_or(Error::OutOfBounds)?;
795    buf.get(start..end).ok_or(Error::OutOfBounds)
796}
797
798fn read_u8(buf: &[u8], off: u64, budget: Option<&Budget>) -> Result<u8> {
799    Ok(get_slice(buf, off, 1, budget)?[0])
800}
801
802/// Load a packed struct's presence bitmap (1..=8 bytes, little-endian) into a
803/// u64 for popcount rank queries. A validated schema guarantees
804/// `bitmap_bytes <= 8` (packed structs cap at 64 fields), but we check anyway
805/// so a corrupt layout can never index past the word or panic.
806fn read_bitmap(buf: &[u8], base: u64, bitmap_bytes: u32, budget: Option<&Budget>) -> Result<u64> {
807    if bitmap_bytes > 8 {
808        return Err(Error::Internal("packed bitmap wider than 8 bytes"));
809    }
810    let bytes = get_slice(buf, base, bitmap_bytes as u64, budget)?;
811    let mut word = [0u8; 8];
812    word[..bytes.len()].copy_from_slice(bytes);
813    Ok(u64::from_le_bytes(word))
814}
815
816fn read_u32(buf: &[u8], off: u64, budget: Option<&Budget>) -> Result<u32> {
817    Ok(u32::from_le_bytes(
818        get_slice(buf, off, 4, budget)?.try_into().unwrap(),
819    ))
820}
821
822enum Wide {
823    U(u64),
824    I(i64),
825    F(f64),
826}
827
828fn read_wide(buf: &[u8], at: u64, kind: NumKind, budget: Option<&Budget>) -> Result<Wide> {
829    Ok(match kind {
830        NumKind::U8 => Wide::U(read_u8(buf, at, budget)? as u64),
831        NumKind::U16 => {
832            Wide::U(u16::from_le_bytes(get_slice(buf, at, 2, budget)?.try_into().unwrap()) as u64)
833        }
834        NumKind::U32 => Wide::U(read_u32(buf, at, budget)? as u64),
835        NumKind::U64 => Wide::U(u64::from_le_bytes(
836            get_slice(buf, at, 8, budget)?.try_into().unwrap(),
837        )),
838        NumKind::I8 => Wide::I(read_u8(buf, at, budget)? as i8 as i64),
839        NumKind::I16 => {
840            Wide::I(i16::from_le_bytes(get_slice(buf, at, 2, budget)?.try_into().unwrap()) as i64)
841        }
842        NumKind::I32 => {
843            Wide::I(i32::from_le_bytes(get_slice(buf, at, 4, budget)?.try_into().unwrap()) as i64)
844        }
845        NumKind::I64 => Wide::I(i64::from_le_bytes(
846            get_slice(buf, at, 8, budget)?.try_into().unwrap(),
847        )),
848        NumKind::F32 => {
849            Wide::F(f32::from_le_bytes(get_slice(buf, at, 4, budget)?.try_into().unwrap()) as f64)
850        }
851        NumKind::F64 => Wide::F(f64::from_le_bytes(
852            get_slice(buf, at, 8, budget)?.try_into().unwrap(),
853        )),
854    })
855}
856
857fn num_ref<'b, 'r>(to: NumKind, wide: Wide) -> Result<Ref<'b, 'r>> {
858    Ok(match (to, wide) {
859        (NumKind::U8, Wide::U(x)) => Ref::U8(x as u8),
860        (NumKind::U16, Wide::U(x)) => Ref::U16(x as u16),
861        (NumKind::U32, Wide::U(x)) => Ref::U32(x as u32),
862        (NumKind::U64, Wide::U(x)) => Ref::U64(x),
863        (NumKind::I8, Wide::I(x)) => Ref::I8(x as i8),
864        (NumKind::I16, Wide::I(x)) => Ref::I16(x as i16),
865        (NumKind::I32, Wide::I(x)) => Ref::I32(x as i32),
866        (NumKind::I64, Wide::I(x)) => Ref::I64(x),
867        (NumKind::F32, Wide::F(x)) => Ref::F32(x as f32),
868        (NumKind::F64, Wide::F(x)) => Ref::F64(x),
869        _ => return Err(Error::Internal("num kind mismatch in access plan")),
870    })
871}
872
873/// Materialize a value whose slot (or list element position) is at `at`.
874/// For heap loads, `at` holds a u32 absolute offset to the object.
875fn load_at<'b, 'r>(
876    buf: &'b [u8],
877    resolver: &'r Resolver,
878    load: &'r Load,
879    at: u64,
880    budget: Option<&'r Budget>,
881) -> Result<Ref<'b, 'r>> {
882    match load {
883        Load::Bool => Ok(Ref::Bool(read_u8(buf, at, budget)? != 0)),
884        Load::Num { from, to } => num_ref(*to, read_wide(buf, at, *from, budget)?),
885        Load::Enum => Ok(Ref::Enum(read_u32(buf, at, budget)?)),
886        Load::Str => {
887            let off = read_u32(buf, at, budget)? as u64;
888            let len = read_u32(buf, off, budget)? as u64;
889            let bytes = get_slice(buf, off + 4, len, budget)?;
890            let s = std::str::from_utf8(bytes).map_err(|_| Error::BadUtf8)?;
891            Ok(Ref::Str(s))
892        }
893        Load::Bytes => {
894            let off = read_u32(buf, at, budget)? as u64;
895            let len = read_u32(buf, off, budget)? as u64;
896            Ok(Ref::Bytes(get_slice(buf, off + 4, len, budget)?))
897        }
898        Load::Struct(plan_idx) => {
899            let off = read_u32(buf, at, budget)?;
900            Ok(Ref::Struct(StructReader {
901                buf,
902                base: off,
903                plan: resolver.plan(*plan_idx),
904                resolver,
905                budget,
906            }))
907        }
908        Load::List(elem) => {
909            let off = read_u32(buf, at, budget)?;
910            let count = read_u32(buf, off as u64, budget)?;
911            // u64 arithmetic: off + 4 rounded up to the element alignment
912            // cannot overflow here even at the 4 GiB message limit.
913            let x = off as u64 + 4;
914            let a = elem.align as u64;
915            let elems_base = (x + a - 1) & !(a - 1);
916            Ok(Ref::List(ListReader {
917                buf,
918                resolver,
919                elem,
920                elems_base,
921                count,
922                budget,
923            }))
924        }
925        Load::Map(plan) => {
926            let off = read_u32(buf, at, budget)?;
927            let count = read_u32(buf, off as u64, budget)?;
928            let x = off as u64 + 4;
929            let a = plan.align as u64;
930            let entries_base = (x + a - 1) & !(a - 1);
931            Ok(Ref::Map(MapReader {
932                buf,
933                resolver,
934                plan,
935                entries_base,
936                count,
937                budget,
938            }))
939        }
940        Load::Union(plan) => {
941            let off = read_u32(buf, at, budget)? as u64;
942            let tag = read_u32(buf, off, budget)?;
943            Ok(Ref::Union(UnionReader {
944                buf,
945                resolver,
946                plan,
947                base: off,
948                tag,
949                budget,
950            }))
951        }
952    }
953}