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
488/// A `map<K, V>` read lazily from the buffer. Entries are stored sorted by key
489/// (canonical order), so [`get`](MapReader::get) yields them in that order.
490#[derive(Clone, Debug)]
491pub struct MapReader<'b, 'r> {
492    buf: &'b [u8],
493    resolver: &'r Resolver,
494    plan: &'r MapPlan,
495    entries_base: u64,
496    count: u32,
497    budget: Option<&'r Budget>,
498}
499
500impl<'b, 'r> MapReader<'b, 'r> {
501    pub fn len(&self) -> u32 {
502        self.count
503    }
504
505    pub fn is_empty(&self) -> bool {
506        self.count == 0
507    }
508
509    /// The `(key, value)` of entry `index` (in canonical key order).
510    pub fn get(&self, index: u32) -> Result<(Ref<'b, 'r>, Ref<'b, 'r>)> {
511        if index >= self.count {
512            return Err(Error::IndexOutOfBounds);
513        }
514        let entry = self.entries_base + index as u64 * self.plan.stride as u64;
515        let key = load_at(
516            self.buf,
517            self.resolver,
518            &self.plan.key,
519            entry + self.plan.key_off as u64,
520            self.budget,
521        )?;
522        let value = load_at(
523            self.buf,
524            self.resolver,
525            &self.plan.value,
526            entry + self.plan.value_off as u64,
527            self.budget,
528        )?;
529        Ok((key, value))
530    }
531
532    pub fn iter(&self) -> impl Iterator<Item = Result<(Ref<'b, 'r>, Ref<'b, 'r>)>> + '_ {
533        (0..self.count).map(move |i| self.get(i))
534    }
535}
536
537/// A `union<…>` value read lazily: a variant [`tag`](UnionReader::tag) and the
538/// selected variant's [`value`](UnionReader::value).
539#[derive(Clone, Debug)]
540pub struct UnionReader<'b, 'r> {
541    buf: &'b [u8],
542    resolver: &'r Resolver,
543    plan: &'r UnionPlan,
544    base: u64,
545    tag: u32,
546    budget: Option<&'r Budget>,
547}
548
549impl<'b, 'r> UnionReader<'b, 'r> {
550    /// The variant tag (index into the union's variant list).
551    pub fn tag(&self) -> u32 {
552        self.tag
553    }
554
555    /// The selected variant's value. Errors with [`Error::BadUnionTag`] if the
556    /// wire tag is out of range for the reader schema's variant list.
557    pub fn value(&self) -> Result<Ref<'b, 'r>> {
558        let vp = self
559            .plan
560            .variants
561            .get(self.tag as usize)
562            .ok_or(Error::BadUnionTag(self.tag))?;
563        load_at(
564            self.buf,
565            self.resolver,
566            &vp.load,
567            self.base + vp.payload_off as u64,
568            self.budget,
569        )
570    }
571}
572
573/// Materialize a scalar [`Default`] into a [`Ref`]. Defaults are by-value
574/// scalars, so the returned `Ref` is valid for any lifetimes.
575fn default_to_ref<'b, 'r>(d: Default) -> Ref<'b, 'r> {
576    match d {
577        Default::Bool(x) => Ref::Bool(x),
578        Default::U8(x) => Ref::U8(x),
579        Default::U16(x) => Ref::U16(x),
580        Default::U32(x) => Ref::U32(x),
581        Default::U64(x) => Ref::U64(x),
582        Default::I8(x) => Ref::I8(x),
583        Default::I16(x) => Ref::I16(x),
584        Default::I32(x) => Ref::I32(x),
585        Default::I64(x) => Ref::I64(x),
586        Default::F32(bits) => Ref::F32(f32::from_bits(bits)),
587        Default::F64(bits) => Ref::F64(f64::from_bits(bits)),
588        Default::Enum(x) => Ref::Enum(x),
589    }
590}
591
592/// Depth ceiling for [`Message::verify`], matching `dump_json`. Bounds forged
593/// offset *cycles* (which the byte budget alone would only stop after burning
594/// the whole budget); far deeper than any real data.
595const MAX_VERIFY_DEPTH: u32 = 128;
596
597fn verify_struct(sr: &StructReader, depth: u32) -> Result<()> {
598    if depth > MAX_VERIFY_DEPTH {
599        return Err(Error::DepthLimitExceeded);
600    }
601    // `struct_def` comes from the reader schema; every present field is read
602    // (charging the budget via the bounded reader) and recursed into.
603    let ids: Vec<u16> = sr.struct_def().fields.iter().map(|f| f.id).collect();
604    for id in ids {
605        if let Some(v) = sr.get(id)? {
606            verify_ref(&v, depth)?;
607        }
608    }
609    Ok(())
610}
611
612fn verify_ref(v: &Ref, depth: u32) -> Result<()> {
613    match v {
614        Ref::Struct(s) => verify_struct(s, depth + 1),
615        Ref::List(l) => {
616            for i in 0..l.len() {
617                verify_ref(&l.get(i)?, depth + 1)?;
618            }
619            Ok(())
620        }
621        Ref::Map(m) => {
622            for i in 0..m.len() {
623                let (k, v) = m.get(i)?;
624                verify_ref(&k, depth + 1)?;
625                verify_ref(&v, depth + 1)?;
626            }
627            Ok(())
628        }
629        Ref::Union(u) => verify_ref(&u.value()?, depth + 1),
630        _ => Ok(()),
631    }
632}
633
634// ---------------------------------------------------------------------------
635// Raw bounds-checked loads
636// ---------------------------------------------------------------------------
637
638/// Bounds-checked slice fetch. On a bounded read (`budget = Some`) it also
639/// charges the touched bytes against the budget — since *every* buffer access
640/// funnels through here, that alone caps total traversal work: an aliased
641/// sub-object re-read costs its bytes again each time (the wire spec §5.2).
642fn get_slice<'b>(buf: &'b [u8], off: u64, len: u64, budget: Option<&Budget>) -> Result<&'b [u8]> {
643    charge(budget, len)?;
644    let start = usize::try_from(off).map_err(|_| Error::OutOfBounds)?;
645    let len = usize::try_from(len).map_err(|_| Error::OutOfBounds)?;
646    let end = start.checked_add(len).ok_or(Error::OutOfBounds)?;
647    buf.get(start..end).ok_or(Error::OutOfBounds)
648}
649
650fn read_u8(buf: &[u8], off: u64, budget: Option<&Budget>) -> Result<u8> {
651    Ok(get_slice(buf, off, 1, budget)?[0])
652}
653
654/// Load a packed struct's presence bitmap (1..=8 bytes, little-endian) into a
655/// u64 for popcount rank queries. A validated schema guarantees
656/// `bitmap_bytes <= 8` (packed structs cap at 64 fields), but we check anyway
657/// so a corrupt layout can never index past the word or panic.
658fn read_bitmap(buf: &[u8], base: u64, bitmap_bytes: u32, budget: Option<&Budget>) -> Result<u64> {
659    if bitmap_bytes > 8 {
660        return Err(Error::Internal("packed bitmap wider than 8 bytes"));
661    }
662    let bytes = get_slice(buf, base, bitmap_bytes as u64, budget)?;
663    let mut word = [0u8; 8];
664    word[..bytes.len()].copy_from_slice(bytes);
665    Ok(u64::from_le_bytes(word))
666}
667
668fn read_u32(buf: &[u8], off: u64, budget: Option<&Budget>) -> Result<u32> {
669    Ok(u32::from_le_bytes(
670        get_slice(buf, off, 4, budget)?.try_into().unwrap(),
671    ))
672}
673
674enum Wide {
675    U(u64),
676    I(i64),
677    F(f64),
678}
679
680fn read_wide(buf: &[u8], at: u64, kind: NumKind, budget: Option<&Budget>) -> Result<Wide> {
681    Ok(match kind {
682        NumKind::U8 => Wide::U(read_u8(buf, at, budget)? as u64),
683        NumKind::U16 => {
684            Wide::U(u16::from_le_bytes(get_slice(buf, at, 2, budget)?.try_into().unwrap()) as u64)
685        }
686        NumKind::U32 => Wide::U(read_u32(buf, at, budget)? as u64),
687        NumKind::U64 => Wide::U(u64::from_le_bytes(
688            get_slice(buf, at, 8, budget)?.try_into().unwrap(),
689        )),
690        NumKind::I8 => Wide::I(read_u8(buf, at, budget)? as i8 as i64),
691        NumKind::I16 => {
692            Wide::I(i16::from_le_bytes(get_slice(buf, at, 2, budget)?.try_into().unwrap()) as i64)
693        }
694        NumKind::I32 => {
695            Wide::I(i32::from_le_bytes(get_slice(buf, at, 4, budget)?.try_into().unwrap()) as i64)
696        }
697        NumKind::I64 => Wide::I(i64::from_le_bytes(
698            get_slice(buf, at, 8, budget)?.try_into().unwrap(),
699        )),
700        NumKind::F32 => {
701            Wide::F(f32::from_le_bytes(get_slice(buf, at, 4, budget)?.try_into().unwrap()) as f64)
702        }
703        NumKind::F64 => Wide::F(f64::from_le_bytes(
704            get_slice(buf, at, 8, budget)?.try_into().unwrap(),
705        )),
706    })
707}
708
709fn num_ref<'b, 'r>(to: NumKind, wide: Wide) -> Result<Ref<'b, 'r>> {
710    Ok(match (to, wide) {
711        (NumKind::U8, Wide::U(x)) => Ref::U8(x as u8),
712        (NumKind::U16, Wide::U(x)) => Ref::U16(x as u16),
713        (NumKind::U32, Wide::U(x)) => Ref::U32(x as u32),
714        (NumKind::U64, Wide::U(x)) => Ref::U64(x),
715        (NumKind::I8, Wide::I(x)) => Ref::I8(x as i8),
716        (NumKind::I16, Wide::I(x)) => Ref::I16(x as i16),
717        (NumKind::I32, Wide::I(x)) => Ref::I32(x as i32),
718        (NumKind::I64, Wide::I(x)) => Ref::I64(x),
719        (NumKind::F32, Wide::F(x)) => Ref::F32(x as f32),
720        (NumKind::F64, Wide::F(x)) => Ref::F64(x),
721        _ => return Err(Error::Internal("num kind mismatch in access plan")),
722    })
723}
724
725/// Materialize a value whose slot (or list element position) is at `at`.
726/// For heap loads, `at` holds a u32 absolute offset to the object.
727fn load_at<'b, 'r>(
728    buf: &'b [u8],
729    resolver: &'r Resolver,
730    load: &'r Load,
731    at: u64,
732    budget: Option<&'r Budget>,
733) -> Result<Ref<'b, 'r>> {
734    match load {
735        Load::Bool => Ok(Ref::Bool(read_u8(buf, at, budget)? != 0)),
736        Load::Num { from, to } => num_ref(*to, read_wide(buf, at, *from, budget)?),
737        Load::Enum => Ok(Ref::Enum(read_u32(buf, at, budget)?)),
738        Load::Str => {
739            let off = read_u32(buf, at, budget)? as u64;
740            let len = read_u32(buf, off, budget)? as u64;
741            let bytes = get_slice(buf, off + 4, len, budget)?;
742            let s = std::str::from_utf8(bytes).map_err(|_| Error::BadUtf8)?;
743            Ok(Ref::Str(s))
744        }
745        Load::Bytes => {
746            let off = read_u32(buf, at, budget)? as u64;
747            let len = read_u32(buf, off, budget)? as u64;
748            Ok(Ref::Bytes(get_slice(buf, off + 4, len, budget)?))
749        }
750        Load::Struct(plan_idx) => {
751            let off = read_u32(buf, at, budget)?;
752            Ok(Ref::Struct(StructReader {
753                buf,
754                base: off,
755                plan: resolver.plan(*plan_idx),
756                resolver,
757                budget,
758            }))
759        }
760        Load::List(elem) => {
761            let off = read_u32(buf, at, budget)?;
762            let count = read_u32(buf, off as u64, budget)?;
763            // u64 arithmetic: off + 4 rounded up to the element alignment
764            // cannot overflow here even at the 4 GiB message limit.
765            let x = off as u64 + 4;
766            let a = elem.align as u64;
767            let elems_base = (x + a - 1) & !(a - 1);
768            Ok(Ref::List(ListReader {
769                buf,
770                resolver,
771                elem,
772                elems_base,
773                count,
774                budget,
775            }))
776        }
777        Load::Map(plan) => {
778            let off = read_u32(buf, at, budget)?;
779            let count = read_u32(buf, off as u64, budget)?;
780            let x = off as u64 + 4;
781            let a = plan.align as u64;
782            let entries_base = (x + a - 1) & !(a - 1);
783            Ok(Ref::Map(MapReader {
784                buf,
785                resolver,
786                plan,
787                entries_base,
788                count,
789                budget,
790            }))
791        }
792        Load::Union(plan) => {
793            let off = read_u32(buf, at, budget)? as u64;
794            let tag = read_u32(buf, off, budget)?;
795            Ok(Ref::Union(UnionReader {
796                buf,
797                resolver,
798                plan,
799                base: off,
800                tag,
801                budget,
802            }))
803        }
804    }
805}