Skip to main content

active_infinity/
validate.rs

1//! Structural validation of untrusted images.
2//!
3//! Validation is total and fail-closed: every byte of the buffer is accounted
4//! for before any caller receives a node, a string, a blob or a
5//! [`crate::view::InfinityView`]. The checker enforces the canonical form as
6//! well as safety — sorted symbol tables, zeroed padding, zeroed unused record
7//! fields — so a validated image cannot carry covert payload in slack space,
8//! and a validated image re-encodes to itself.
9//!
10//! Work is bounded by [`Limits`]: counts are checked against the remaining
11//! bytes before allocation, traversals are iterative (no recursion), and every
12//! reference is bounds-checked.
13
14// Module-level: item docs for this layer live in docs/spec/layout.md and
15// docs/spec/streaming.md; the crate root keeps deny(missing_docs) for the
16// hand-written public surface.
17#![allow(missing_docs)]
18
19use crate::encode::{hash_image, NodeRec};
20use crate::error::{Error, Result};
21use crate::ir::{NodeId, ProvValue, Provenance};
22use crate::layout::{
23    align8, dialect_hash, read_u32, read_u64, Header, Limits, Meta, Section, BLOB_ENTRY_SIZE,
24    EXT_DIR_SIZE, FIELD_SIZE, FLAG_ANNOTATIONS, FLAG_EXTENSIONS, FLAG_PROVENANCE, FLAG_SECRETS,
25    HEADER_SIZE, NODE_FLAG_0, NODE_FLAG_1, NODE_FLAG_KNOWN, NODE_RECORD_SIZE, PROV_ENTRY_SIZE,
26    PROV_HEADER_SIZE, SECTION_COUNT,
27};
28use crate::unit::{UnitTag, UNIT_NAMED};
29
30/// A validated image plus zero-copy accessors.
31#[derive(Debug, Clone, Copy)]
32pub struct Validated<'a> {
33    buf: &'a [u8],
34    header: Header,
35    ends: [u64; SECTION_COUNT],
36    limits: Limits,
37}
38
39impl<'a> Validated<'a> {
40    /// Parse and validate `buf`.
41    pub fn new(buf: &'a [u8], opts: crate::layout::DecodeOpts) -> Result<Self> {
42        opts.limits.check()?;
43        let header = Header::parse(buf)?;
44        if header.dialect_hash != dialect_hash() {
45            return Err(Error::DialectMismatch(header.dialect_hash));
46        }
47        let limits = opts.limits;
48        if buf.len() as u64 > limits.max_total_bytes {
49            return Err(Error::Limit {
50                kind: "total bytes",
51                limit: limits.max_total_bytes,
52            });
53        }
54        validate_counts(&header, limits)?;
55        let ends = validate_layout(buf, &header)?;
56        let validated = Validated {
57            buf,
58            header,
59            ends,
60            limits,
61        };
62
63        if header.flags & FLAG_ANNOTATIONS != 0 && validated.span(Section::Annotations).count == 0 {
64            return Err(Error::Annotation {
65                id: 0,
66                reason: "annotations flag set with an empty table",
67            });
68        }
69        if header.flags & FLAG_ANNOTATIONS == 0 && validated.span(Section::Annotations).count != 0 {
70            return Err(Error::Annotation {
71                id: 0,
72                reason: "annotations present without the flag",
73            });
74        }
75        if header.flags & FLAG_PROVENANCE != 0 && validated.span(Section::Provenance).count == 0 {
76            return Err(Error::Provenance("provenance flag with an empty block"));
77        }
78        if header.flags & FLAG_PROVENANCE == 0 && validated.span(Section::Provenance).count != 0 {
79            return Err(Error::Provenance("provenance block without the flag"));
80        }
81        if header.flags & FLAG_EXTENSIONS != 0 && validated.span(Section::Extensions).count == 0 {
82            return Err(Error::Extension {
83                tag: 0,
84                reason: "extensions flag with an empty directory",
85            });
86        }
87        if header.flags & FLAG_EXTENSIONS == 0 && validated.span(Section::Extensions).count != 0 {
88            return Err(Error::Extension {
89                tag: 0,
90                reason: "extension blocks without the flag",
91            });
92        }
93        if header.flags & FLAG_SECRETS != 0 && validated.span(Section::Secrets).count == 0 {
94            return Err(Error::SecretEntry(0));
95        }
96        if header.flags & FLAG_SECRETS == 0 && validated.span(Section::Secrets).count != 0 {
97            return Err(Error::SecretEntry(0));
98        }
99
100        validated.check_strings()?;
101        validated.check_units(limits)?;
102        validated.check_types(limits)?;
103        validated.check_numbers()?;
104        validated.check_hashes()?;
105        validated.check_secrets(limits)?;
106        validated.check_blobs(limits)?;
107        validated.check_nodes(limits)?;
108        validated.check_fields()?;
109        validated.check_parts()?;
110        validated.check_edges()?;
111        validated.check_annotations()?;
112        validated.check_provenance(limits)?;
113        validated.check_extensions()?;
114        validated.check_reachability(limits)?;
115        validated.check_acyclic()?;
116        validated.check_depth(limits)?;
117
118        if opts.verify_hash {
119            let computed = hash_image(buf)?;
120            if computed != header.content_hash {
121                return Err(Error::HashMismatch);
122            }
123        }
124        if opts.verify_canonical {
125            let module = crate::decode::decode_validated(&validated)?;
126            let image = crate::encode::encode_with(
127                &module,
128                crate::layout::EncodeOpts {
129                    write_hash: true,
130                    mark_canonical: true,
131                    streamed: header.flags & crate::layout::FLAG_STREAMED != 0,
132                },
133            )?;
134            if image != buf {
135                return Err(Error::NotCanonical);
136            }
137        }
138        Ok(validated)
139    }
140
141    pub fn buf(&self) -> &'a [u8] {
142        self.buf
143    }
144
145    /// The limits this image was validated against.
146    pub fn limits_snapshot(&self) -> Limits {
147        self.limits
148    }
149
150    pub fn header(&self) -> &Header {
151        &self.header
152    }
153
154    pub fn limits_span(&self, section: Section) -> (u32, u32) {
155        let span = self.header.span(section);
156        (span.offset, span.count)
157    }
158
159    pub fn span(&self, section: Section) -> crate::layout::SectionSpan {
160        self.header.span(section)
161    }
162
163    /// Byte length of a section body (from its offset to its computed end).
164    pub fn section_len(&self, section: Section) -> u64 {
165        self.ends[section as usize] - self.header.span(section).offset as u64
166    }
167
168    pub fn section(&self, section: Section) -> &'a [u8] {
169        let (offset, _) = self.limits_span(section);
170        let end = self.ends[section as usize];
171        &self.buf[offset as usize..end as usize]
172    }
173
174    /// `Meta` describing this image.
175    pub fn meta(&self) -> Meta {
176        let header = &self.header;
177        Meta {
178            flags: header.flags,
179            major: header.major,
180            minor: header.minor,
181            dialect_hash: header.dialect_hash,
182            content_hash: header.content_hash,
183            total_size: header.total_size,
184            nodes: header.span(Section::Nodes).count,
185            edges: header.span(Section::Edges).count,
186            strings: header.span(Section::Strings).count,
187            string_bytes: self.string_heap_len(),
188            blobs: header.span(Section::Blobs).count,
189            blob_bytes: self.blob_heap_len(),
190            annotations: header.span(Section::Annotations).count,
191            units: header.span(Section::Units).count,
192            types: header.span(Section::Types).count,
193            secrets: header.span(Section::Secrets).count,
194            extensions: header.span(Section::Extensions).count,
195        }
196    }
197
198    pub fn node_count(&self) -> u32 {
199        self.header.span(Section::Nodes).count
200    }
201
202    /// Read a node record.
203    pub(crate) fn node(&self, id: NodeId) -> Result<NodeRec> {
204        let count = self.node_count();
205        if id.0 >= count {
206            return Err(Error::NodeRef(id.0));
207        }
208        let offset =
209            self.header.span(Section::Nodes).offset as usize + id.index() * NODE_RECORD_SIZE;
210        let rec = &self.buf[offset..offset + NODE_RECORD_SIZE];
211        Ok(NodeRec {
212            kind: rec[0],
213            flags: rec[1],
214            aux: u32::from_le_bytes([rec[4], rec[5], rec[6], rec[7]]),
215            a: u64::from_le_bytes(rec[8..16].try_into().expect("8 bytes")),
216            b: u64::from_le_bytes(rec[16..24].try_into().expect("8 bytes")),
217            c: u64::from_le_bytes(rec[24..32].try_into().expect("8 bytes")),
218        })
219    }
220
221    /// Read a child index entry.
222    pub fn child(&self, index: u32) -> Result<u32> {
223        let count = self.header.span(Section::Children).count;
224        if index >= count {
225            return Err(Error::Node {
226                id: index,
227                reason: "child index out of range",
228            });
229        }
230        let offset = self.header.span(Section::Children).offset as usize + index as usize * 4;
231        read_u32(self.buf, offset)
232    }
233
234    /// Read a field entry (string id, node id).
235    pub fn field(&self, index: u32) -> Result<(u32, NodeId)> {
236        let count = self.header.span(Section::Fields).count;
237        if index >= count {
238            return Err(Error::Node {
239                id: index,
240                reason: "field index out of range",
241            });
242        }
243        let offset =
244            self.header.span(Section::Fields).offset as usize + index as usize * FIELD_SIZE;
245        let name = read_u32(self.buf, offset)?;
246        let node = read_u32(self.buf, offset + 8)?;
247        Ok((name, NodeId(node)))
248    }
249
250    /// Read an interpolation part (tag, value).
251    pub fn part(&self, index: u32) -> Result<(u32, u32)> {
252        let count = self.header.span(Section::Parts).count;
253        if index >= count {
254            return Err(Error::Node {
255                id: index,
256                reason: "part index out of range",
257            });
258        }
259        let offset = self.header.span(Section::Parts).offset as usize + index as usize * 8;
260        Ok((read_u32(self.buf, offset)?, read_u32(self.buf, offset + 4)?))
261    }
262
263    /// Read an edge (from, to).
264    pub fn edge(&self, index: u32) -> Result<(u32, u32)> {
265        let count = self.header.span(Section::Edges).count;
266        if index >= count {
267            return Err(Error::Edge {
268                from: index,
269                to: index,
270            });
271        }
272        let offset = self.header.span(Section::Edges).offset as usize + index as usize * 8;
273        Ok((read_u32(self.buf, offset)?, read_u32(self.buf, offset + 4)?))
274    }
275
276    /// Byte length of the string heap (excluding the offset table).
277    pub fn string_heap_len(&self) -> u32 {
278        let count = self.header.span(Section::Strings).count;
279        if count == 0 {
280            return 0;
281        }
282        let offset = self.header.span(Section::Strings).offset as usize + count as usize * 4;
283        read_u32(self.buf, offset).unwrap_or(0)
284    }
285
286    /// Borrow string `id` directly from the buffer (zero copy).
287    pub fn string(&self, id: u32) -> Result<&'a str> {
288        let count = self.header.span(Section::Strings).count;
289        if id >= count {
290            return Err(Error::StringRef(id));
291        }
292        let table = self.header.span(Section::Strings).offset as usize;
293        let start = read_u32(self.buf, table + id as usize * 4)?;
294        let end = read_u32(self.buf, table + id as usize * 4 + 4)?;
295        let heap = table + (count as usize + 1) * 4;
296        let bytes = &self.buf[heap + start as usize..heap + end as usize];
297        std::str::from_utf8(bytes).map_err(|_| Error::Utf8 {
298            offset: (heap + start as usize) as u64,
299        })
300    }
301
302    /// Total blob payload heap length.
303    pub fn blob_heap_len(&self) -> u32 {
304        let count = self.header.span(Section::Blobs).count;
305        if count == 0 {
306            return 0;
307        }
308        let table = self.header.span(Section::Blobs).offset as usize;
309        let last = table + (count as usize - 1) * BLOB_ENTRY_SIZE;
310        let off = read_u32(self.buf, last + 32).unwrap_or(0);
311        let len = read_u32(self.buf, last + 36).unwrap_or(0);
312        off.saturating_add(len)
313    }
314
315    /// Read a blob index entry: (hash, offset-in-heap, length).
316    pub fn blob_entry(&self, id: u32) -> Result<([u8; 32], u32, u32)> {
317        let count = self.header.span(Section::Blobs).count;
318        if id >= count {
319            return Err(Error::Blob {
320                id,
321                reason: "index out of range",
322            });
323        }
324        let offset =
325            self.header.span(Section::Blobs).offset as usize + id as usize * BLOB_ENTRY_SIZE;
326        let mut hash = [0u8; 32];
327        hash.copy_from_slice(&self.buf[offset..offset + 32]);
328        Ok((
329            hash,
330            read_u32(self.buf, offset + 32)?,
331            read_u32(self.buf, offset + 36)?,
332        ))
333    }
334
335    /// Borrow a blob payload directly from the buffer (zero copy).
336    pub fn blob(&self, id: u32) -> Result<&'a [u8]> {
337        let count = self.header.span(Section::Blobs).count;
338        let (_, off, len) = self.blob_entry(id)?;
339        let heap =
340            self.header.span(Section::Blobs).offset as usize + count as usize * BLOB_ENTRY_SIZE;
341        let start = heap + off as usize;
342        let end = start + len as usize;
343        if end > self.ends[Section::Blobs as usize] as usize {
344            return Err(Error::Blob {
345                id,
346                reason: "payload out of range",
347            });
348        }
349        Ok(&self.buf[start..end])
350    }
351
352    /// Read an annotation record: (node, kind, a, b).
353    pub fn annotation(&self, index: u32) -> Result<(u32, u32, u32, u32)> {
354        let count = self.header.span(Section::Annotations).count;
355        if index >= count {
356            return Err(Error::Annotation {
357                id: index,
358                reason: "index out of range",
359            });
360        }
361        let offset = self.header.span(Section::Annotations).offset as usize + index as usize * 16;
362        Ok((
363            read_u32(self.buf, offset)?,
364            read_u32(self.buf, offset + 4)?,
365            read_u32(self.buf, offset + 8)?,
366            read_u32(self.buf, offset + 12)?,
367        ))
368    }
369
370    /// Resolve a unit table entry.
371    pub fn unit(&self, id: u32) -> Result<UnitTag> {
372        let count = self.header.span(Section::Units).count;
373        if id >= count {
374            return Err(Error::UnitRef(id));
375        }
376        let offset = self.header.span(Section::Units).offset as usize + id as usize * 8;
377        let disc = read_u32(self.buf, offset)?;
378        let name = read_u32(self.buf, offset + 4)?;
379        if disc == UNIT_NAMED {
380            Ok(UnitTag::Named(self.string(name)?.to_string()))
381        } else {
382            UnitTag::from_discriminant(disc).ok_or(Error::UnknownUnit(disc))
383        }
384    }
385
386    /// Read a type table entry: (code, flags, aux start, aux count).
387    pub fn type_entry(&self, id: u32) -> Result<(u32, u32, u32, u32)> {
388        let count = self.header.span(Section::Types).count;
389        if id >= count {
390            return Err(Error::Type {
391                id,
392                reason: "index out of range",
393            });
394        }
395        let offset = self.header.span(Section::Types).offset as usize + id as usize * 16;
396        Ok((
397            read_u32(self.buf, offset)?,
398            read_u32(self.buf, offset + 4)?,
399            read_u32(self.buf, offset + 8)?,
400            read_u32(self.buf, offset + 12)?,
401        ))
402    }
403
404    /// Read an aux record.
405    pub fn aux(&self, index: u32) -> Result<(u32, u32)> {
406        let count = self.header.span(Section::Aux).count;
407        if index >= count {
408            return Err(Error::Node {
409                id: index,
410                reason: "aux index out of range",
411            });
412        }
413        let offset = self.header.span(Section::Aux).offset as usize + index as usize * 8;
414        Ok((read_u32(self.buf, offset)?, read_u32(self.buf, offset + 4)?))
415    }
416
417    /// Read a number table entry.
418    pub fn number(&self, index: u32) -> Result<i64> {
419        let count = self.header.span(Section::Numbers).count;
420        if index >= count {
421            return Err(Error::Node {
422                id: index,
423                reason: "number index out of range",
424            });
425        }
426        let offset = self.header.span(Section::Numbers).offset as usize + index as usize * 8;
427        Ok(read_u64(self.buf, offset)? as i64)
428    }
429
430    /// Borrow a secret's raw bytes.
431    ///
432    /// Crate-internal on purpose: a public accessor would be an unlock API, and
433    /// the whole point of structural secrets is that they cannot be read out.
434    /// The decoder and the redaction paths are the only callers.
435    pub(crate) fn secret(&self, id: u32) -> Result<&'a [u8]> {
436        let count = self.header.span(Section::Secrets).count;
437        if id >= count {
438            return Err(Error::SecretEntry(id));
439        }
440        let table = self.header.span(Section::Secrets).offset as usize;
441        let off = read_u32(self.buf, table + id as usize * 8)?;
442        let len = read_u32(self.buf, table + id as usize * 8 + 4)?;
443        let heap = table + count as usize * 8;
444        let start = heap + off as usize;
445        let end = start + len as usize;
446        if end > self.ends[Section::Secrets as usize] as usize {
447            return Err(Error::SecretEntry(id));
448        }
449        Ok(&self.buf[start..end])
450    }
451
452    /// Read a hash table entry.
453    pub fn hash_entry(&self, index: u32) -> Result<[u8; 32]> {
454        let count = self.header.span(Section::Hashes).count;
455        if index >= count {
456            return Err(Error::Provenance("hash index out of range"));
457        }
458        let offset = self.header.span(Section::Hashes).offset as usize + index as usize * 32;
459        let mut hash = [0u8; 32];
460        hash.copy_from_slice(&self.buf[offset..offset + 32]);
461        Ok(hash)
462    }
463
464    /// Read one extension directory entry: (tag, payload offset, payload len).
465    pub fn extension_entry(&self, index: u32) -> Result<(u64, u32, u32)> {
466        let count = self.header.span(Section::Extensions).count;
467        if index >= count {
468            return Err(Error::Extension {
469                tag: index as u64,
470                reason: "index out of range",
471            });
472        }
473        let offset = self.header.span(Section::Extensions).offset as usize + index as usize * 16;
474        Ok((
475            read_u64(self.buf, offset)?,
476            read_u32(self.buf, offset + 8)?,
477            read_u32(self.buf, offset + 12)?,
478        ))
479    }
480
481    /// Borrow an extension payload (zero copy).
482    pub fn extension_payload(&self, offset: u32, len: u32) -> Result<&'a [u8]> {
483        let start = offset as usize;
484        let end = start.checked_add(len as usize).ok_or(Error::Extension {
485            tag: 0,
486            reason: "payload length overflows",
487        })?;
488        let section = self.header.span(Section::Extensions);
489        if start < section.offset as usize || end > self.ends[Section::Extensions as usize] as usize
490        {
491            return Err(Error::Extension {
492                tag: 0,
493                reason: "payload out of range",
494            });
495        }
496        Ok(&self.buf[start..end])
497    }
498
499    /// Parse the provenance block.
500    pub fn provenance(&self) -> Result<Option<Provenance>> {
501        let len = self.header.span(Section::Provenance).count as usize;
502        if len == 0 {
503            return Ok(None);
504        }
505        let start = self.header.span(Section::Provenance).offset as usize;
506        if len < PROV_HEADER_SIZE {
507            return Err(Error::Provenance("block shorter than its header"));
508        }
509        let block = &self.buf[start..start + len];
510        let version = read_u32(block, 0)?;
511        if version != 1 {
512            return Err(Error::Provenance("unsupported provenance version"));
513        }
514        let count = read_u32(block, 4)?;
515        let timestamp_ms = read_u64(block, 8)?;
516        let read_sid = |offset: usize| -> Result<Option<String>> {
517            let id = read_u32(block, offset)?;
518            if id == u32::MAX {
519                Ok(None)
520            } else {
521                Ok(Some(self.string(id)?.to_string()))
522            }
523        };
524        let producer = read_sid(16)?;
525        let model = read_sid(20)?;
526        let tool = read_sid(24)?;
527        let flags = read_u32(block, 28)?;
528        if flags & !1 != 0 {
529            return Err(Error::Provenance("unknown provenance flags"));
530        }
531        let parent = if flags & 1 != 0 {
532            let mut hash = [0u8; 32];
533            hash.copy_from_slice(&block[32..64]);
534            Some(hash)
535        } else {
536            if block[32..64].iter().any(|byte| *byte != 0) {
537                return Err(Error::Provenance("parent hash without the flag"));
538            }
539            None
540        };
541        if block[64..PROV_HEADER_SIZE].iter().any(|byte| *byte != 0) {
542            return Err(Error::Provenance("non-zero provenance reserved bytes"));
543        }
544        let mut entries = Vec::with_capacity(count.min(1024) as usize);
545        for index in 0..count as usize {
546            let offset = PROV_HEADER_SIZE + index * PROV_ENTRY_SIZE;
547            if offset + PROV_ENTRY_SIZE > block.len() {
548                return Err(Error::Provenance("entry exceeds block"));
549            }
550            let kind = read_u32(block, offset)?;
551            let key = self.string(read_u32(block, offset + 4)?)?.to_string();
552            let a = read_u32(block, offset + 8)?;
553            let b = read_u32(block, offset + 12)?;
554            if kind > 3 {
555                // Kinds 4+ are reserved. Accepting them would decode to a
556                // value that re-encodes as kind 3, i.e. a non-canonical image.
557                return Err(Error::Provenance("reserved provenance entry kind"));
558            }
559            if b != 0 {
560                return Err(Error::Provenance("non-zero entry reserved word"));
561            }
562            let value = match kind {
563                0 => ProvValue::Text(self.string(a)?.to_string()),
564                1 => ProvValue::Int(self.number(a)?),
565                2 => {
566                    if a > 1 {
567                        return Err(Error::Provenance("flag entry is not 0/1"));
568                    }
569                    ProvValue::Flag(a == 1)
570                }
571                3 => ProvValue::Hash(self.hash_entry(a)?),
572                _ => unreachable!("kind checked above"),
573            };
574            entries.push(crate::ir::ProvEntry { key, value });
575        }
576        if PROV_HEADER_SIZE + count as usize * PROV_ENTRY_SIZE != block.len() {
577            return Err(Error::Provenance("trailing bytes in provenance block"));
578        }
579        Ok(Some(Provenance {
580            producer,
581            timestamp_ms,
582            model,
583            tool,
584            parent,
585            entries,
586        }))
587    }
588
589    /// Module root recorded in the bindings index (row 0), if any.
590    pub fn root(&self) -> Result<Option<NodeId>> {
591        let count = self.header.span(Section::Bindings).count;
592        if count == 0 {
593            return Err(Error::Offset {
594                section: "bindings",
595                offset: self.header.span(Section::Bindings).offset as u64,
596            });
597        }
598        let offset = self.header.span(Section::Bindings).offset as usize;
599        let root = read_u32(self.buf, offset)?;
600        if root == u32::MAX {
601            Ok(None)
602        } else if root < self.node_count() {
603            Ok(Some(NodeId(root)))
604        } else {
605            Err(Error::NodeRef(root))
606        }
607    }
608
609    /// Name bindings in canonical order (sorted by name).
610    pub fn bindings(&self) -> Result<Vec<(&'a str, NodeId)>> {
611        let count = self.header.span(Section::Bindings).count;
612        if count == 0 {
613            return Err(Error::Offset {
614                section: "bindings",
615                offset: self.header.span(Section::Bindings).offset as u64,
616            });
617        }
618        let base = self.header.span(Section::Bindings).offset as usize;
619        let mut out = Vec::with_capacity(count.saturating_sub(1) as usize);
620        for index in 1..count {
621            let offset = base + index as usize * 8;
622            let name = read_u32(self.buf, offset)?;
623            let node = read_u32(self.buf, offset + 4)?;
624            if node >= self.node_count() {
625                return Err(Error::NodeRef(node));
626            }
627            out.push((self.string(name)?, NodeId(node)));
628        }
629        Ok(out)
630    }
631
632    fn check_bindings(&self) -> Result<()> {
633        let count = self.header.span(Section::Bindings).count;
634        if count == 0 {
635            return Err(Error::Offset {
636                section: "bindings",
637                offset: self.header.span(Section::Bindings).offset as u64,
638            });
639        }
640        let base = self.header.span(Section::Bindings).offset as usize;
641        if read_u32(self.buf, base + 4)? != 0 {
642            return Err(Error::ReservedNonZero {
643                section: "bindings",
644                offset: (base + 4) as u64,
645            });
646        }
647        if count as u64 > self.limits.max_nodes as u64 {
648            return Err(Error::Limit {
649                kind: "bindings",
650                limit: self.limits.max_nodes as u64,
651            });
652        }
653        let mut previous: Option<&str> = None;
654        for (name, _) in self.bindings()? {
655            if let Some(previous) = previous {
656                if previous >= name {
657                    return Err(Error::DuplicateBinding);
658                }
659            }
660            previous = Some(name);
661        }
662        Ok(())
663    }
664
665    // ---- checks -------------------------------------------------------
666
667    fn check_strings(&self) -> Result<()> {
668        let count = self.header.span(Section::Strings).count;
669        if count == 0 {
670            return Ok(());
671        }
672        let table = self.header.span(Section::Strings).offset as usize;
673        let heap_len = self.string_heap_len();
674        if heap_len as u64 > self.limits_snapshot().max_string_bytes as u64 {
675            return Err(Error::Limit {
676                kind: "string bytes",
677                limit: self.limits_snapshot().max_string_bytes as u64,
678            });
679        }
680        let heap_len = self.string_heap_len();
681        let heap = table + (count as usize + 1) * 4;
682        if heap + heap_len as usize > self.ends[Section::Strings as usize] as usize {
683            return Err(Error::Offset {
684                section: "strings",
685                offset: table as u64,
686            });
687        }
688        let mut previous: Option<&[u8]> = None;
689        let mut last_end = 0u32;
690        for id in 0..count {
691            let start = read_u32(self.buf, table + id as usize * 4)?;
692            let end = read_u32(self.buf, table + id as usize * 4 + 4)?;
693            if start != last_end || end < start || end > heap_len {
694                return Err(Error::Offset {
695                    section: "strings",
696                    offset: (heap + start as usize) as u64,
697                });
698            }
699            last_end = end;
700            let bytes = &self.buf[heap + start as usize..heap + end as usize];
701            std::str::from_utf8(bytes).map_err(|_| Error::Utf8 {
702                offset: (heap + start as usize) as u64,
703            })?;
704            if let Some(previous) = previous {
705                if previous >= bytes {
706                    return Err(Error::Offset {
707                        section: "strings",
708                        offset: (heap + start as usize) as u64,
709                    });
710                }
711            }
712            previous = Some(bytes);
713        }
714        if last_end != heap_len {
715            return Err(Error::Offset {
716                section: "strings",
717                offset: (heap + last_end as usize) as u64,
718            });
719        }
720        Ok(())
721    }
722
723    fn check_units(&self, limits: Limits) -> Result<()> {
724        let count = self.header.span(Section::Units).count;
725        if count > limits.max_units {
726            return Err(Error::Limit {
727                kind: "units",
728                limit: limits.max_units as u64,
729            });
730        }
731        let mut previous: Option<(u32, u32)> = None;
732        for id in 0..count {
733            let offset = self.header.span(Section::Units).offset as usize + id as usize * 8;
734            let disc = read_u32(self.buf, offset)?;
735            let name = read_u32(self.buf, offset + 4)?;
736            if disc == UNIT_NAMED {
737                if name >= self.header.span(Section::Strings).count {
738                    return Err(Error::UnitRef(name));
739                }
740                if self.string(name)?.is_empty() {
741                    return Err(Error::UnitRef(name));
742                }
743            } else {
744                if UnitTag::from_discriminant(disc).is_none() {
745                    return Err(Error::UnknownUnit(disc));
746                }
747                if name != 0 {
748                    return Err(Error::UnitRef(name));
749                }
750            }
751            if let Some(previous) = previous {
752                if previous >= (disc, name) {
753                    return Err(Error::Offset {
754                        section: "units",
755                        offset: offset as u64,
756                    });
757                }
758            }
759            previous = Some((disc, name));
760        }
761        Ok(())
762    }
763
764    fn check_types(&self, limits: Limits) -> Result<()> {
765        let count = self.header.span(Section::Types).count;
766        if count > limits.max_types {
767            return Err(Error::Limit {
768                kind: "types",
769                limit: limits.max_types as u64,
770            });
771        }
772        let aux_count = self.header.span(Section::Aux).count;
773        let mut previous: Option<(u32, u32, u32, u32)> = None;
774        for id in 0..count {
775            let offset = self.header.span(Section::Types).offset as usize + id as usize * 16;
776            let code = read_u32(self.buf, offset)?;
777            let flags = read_u32(self.buf, offset + 4)?;
778            let start = read_u32(self.buf, offset + 8)?;
779            let len = read_u32(self.buf, offset + 12)?;
780            if code > 11 {
781                return Err(Error::Type {
782                    id,
783                    reason: "unknown type code",
784                });
785            }
786            if code != 7 && flags != 0 {
787                return Err(Error::Type {
788                    id,
789                    reason: "flags on a non-table type",
790                });
791            }
792            if code == 7 && flags > 1 {
793                return Err(Error::Type {
794                    id,
795                    reason: "table closed flag must be 0 or 1",
796                });
797            }
798            if start > aux_count || len > aux_count - start {
799                return Err(Error::Type {
800                    id,
801                    reason: "aux range out of bounds",
802                });
803            }
804            for index in start..start + len {
805                let (tag, value) = self.aux(index)?;
806                match (code, tag) {
807                    (8, crate::layout::AUX_TYPE_ENUM_ITEM) => {
808                        if value >= self.header.span(Section::Strings).count {
809                            return Err(Error::StringRef(value));
810                        }
811                    }
812                    (7, crate::layout::AUX_TYPE_TABLE) => {
813                        if value > 1 {
814                            return Err(Error::Type {
815                                id,
816                                reason: "closed flag must be 0 or 1",
817                            });
818                        }
819                    }
820                    _ => {
821                        return Err(Error::Type {
822                            id,
823                            reason: "unexpected aux record",
824                        })
825                    }
826                }
827            }
828            if code == 8 && len == 0 {
829                return Err(Error::Type {
830                    id,
831                    reason: "enum type without members",
832                });
833            }
834            if let Some(previous) = previous {
835                if previous >= (code, flags, start, len) {
836                    return Err(Error::Offset {
837                        section: "types",
838                        offset: offset as u64,
839                    });
840                }
841            }
842            previous = Some((code, flags, start, len));
843        }
844        Ok(())
845    }
846
847    fn check_numbers(&self) -> Result<()> {
848        let count = self.header.span(Section::Numbers).count;
849        let base = self.header.span(Section::Numbers).offset as usize;
850        let mut previous: Option<i64> = None;
851        for index in 0..count {
852            let value = read_u64(self.buf, base + index as usize * 8)? as i64;
853            if let Some(previous) = previous {
854                if previous >= value {
855                    return Err(Error::Offset {
856                        section: "numbers",
857                        offset: (base + index as usize * 8) as u64,
858                    });
859                }
860            }
861            previous = Some(value);
862        }
863        Ok(())
864    }
865
866    fn check_hashes(&self) -> Result<()> {
867        let count = self.header.span(Section::Hashes).count;
868        if count as u64 > self.limits.max_nodes as u64 {
869            return Err(Error::Limit {
870                kind: "hashes",
871                limit: self.limits.max_nodes as u64,
872            });
873        }
874        let base = self.header.span(Section::Hashes).offset as usize;
875        let mut previous: Option<[u8; 32]> = None;
876        for index in 0..count {
877            let mut hash = [0u8; 32];
878            hash.copy_from_slice(
879                &self.buf[base + index as usize * 32..base + (index as usize + 1) * 32],
880            );
881            if let Some(previous) = previous {
882                if previous >= hash {
883                    return Err(Error::Offset {
884                        section: "hashes",
885                        offset: (base + index as usize * 32) as u64,
886                    });
887                }
888            }
889            previous = Some(hash);
890        }
891        Ok(())
892    }
893
894    fn check_secrets(&self, limits: Limits) -> Result<()> {
895        let count = self.header.span(Section::Secrets).count;
896        if count > limits.max_secrets {
897            return Err(Error::Limit {
898                kind: "secrets",
899                limit: limits.max_secrets as u64,
900            });
901        }
902        let table = self.header.span(Section::Secrets).offset as usize;
903        let heap = table + count as usize * 8;
904        let end = self.ends[Section::Secrets as usize] as usize;
905        let mut cursor = 0u32;
906        for id in 0..count {
907            let off = read_u32(self.buf, table + id as usize * 8)?;
908            let len = read_u32(self.buf, table + id as usize * 8 + 4)?;
909            if off != cursor || heap + off as usize + len as usize > end {
910                return Err(Error::SecretEntry(id));
911            }
912            cursor = cursor.checked_add(len).ok_or(Error::SecretEntry(id))?;
913        }
914        if cursor as u64 > limits.max_secret_bytes as u64 {
915            return Err(Error::Limit {
916                kind: "secret bytes",
917                limit: limits.max_secret_bytes as u64,
918            });
919        }
920        Ok(())
921    }
922
923    fn check_blobs(&self, limits: Limits) -> Result<()> {
924        let count = self.header.span(Section::Blobs).count;
925        if count > limits.max_blobs {
926            return Err(Error::Limit {
927                kind: "blobs",
928                limit: limits.max_blobs as u64,
929            });
930        }
931        let table = self.header.span(Section::Blobs).offset as usize;
932        let heap = table + count as usize * BLOB_ENTRY_SIZE;
933        let end = self.ends[Section::Blobs as usize] as usize;
934        let mut cursor = 0u32;
935        let mut previous: Option<([u8; 32], u32, u32)> = None;
936        for id in 0..count {
937            let offset = table + id as usize * BLOB_ENTRY_SIZE;
938            let mut hash = [0u8; 32];
939            hash.copy_from_slice(&self.buf[offset..offset + 32]);
940            let off = read_u32(self.buf, offset + 32)?;
941            let len = read_u32(self.buf, offset + 36)?;
942            if off != cursor || heap + off as usize + len as usize > end {
943                return Err(Error::Blob {
944                    id,
945                    reason: "payload out of range",
946                });
947            }
948            cursor = cursor.checked_add(len).ok_or(Error::Blob {
949                id,
950                reason: "payload heap overflows",
951            })?;
952            if let Some(previous) = previous {
953                if previous >= (hash, off, len) {
954                    return Err(Error::Offset {
955                        section: "blobs",
956                        offset: offset as u64,
957                    });
958                }
959            }
960            previous = Some((hash, off, len));
961        }
962        if cursor > limits.max_blob_bytes {
963            return Err(Error::Limit {
964                kind: "blob bytes",
965                limit: limits.max_blob_bytes as u64,
966            });
967        }
968        Ok(())
969    }
970
971    fn check_nodes(&self, limits: Limits) -> Result<()> {
972        let count = self.node_count();
973        if count > limits.max_nodes {
974            return Err(Error::Limit {
975                kind: "nodes",
976                limit: limits.max_nodes as u64,
977            });
978        }
979        let child_count = self.header.span(Section::Children).count;
980        let field_count = self.header.span(Section::Fields).count;
981        let part_count = self.header.span(Section::Parts).count;
982        let string_count = self.header.span(Section::Strings).count;
983        let unit_count = self.header.span(Section::Units).count;
984        let type_count = self.header.span(Section::Types).count;
985        let aux_count = self.header.span(Section::Aux).count;
986        let blob_count = self.header.span(Section::Blobs).count;
987        let secret_count = self.header.span(Section::Secrets).count;
988        for index in 0..count {
989            let id = NodeId(index);
990            let rec = self.node(id)?;
991            if rec.kind > 24 {
992                return Err(Error::Node {
993                    id: index,
994                    reason: "unknown node kind",
995                });
996            }
997            if rec.flags & !NODE_FLAG_KNOWN != 0 {
998                return Err(Error::Node {
999                    id: index,
1000                    reason: "reserved node flag set",
1001                });
1002            }
1003            let node = |value: u64| -> Result<()> {
1004                if value >= count as u64 {
1005                    Err(Error::NodeRef(value as u32))
1006                } else {
1007                    Ok(())
1008                }
1009            };
1010            let range = |start: u64, len: u64, total: u32, what: &'static str| -> Result<()> {
1011                if start > total as u64 || len > total as u64 - start {
1012                    return Err(Error::Node {
1013                        id: index,
1014                        reason: what,
1015                    });
1016                }
1017                Ok(())
1018            };
1019            let string = |value: u32| -> Result<()> {
1020                if value >= string_count {
1021                    Err(Error::StringRef(value))
1022                } else {
1023                    Ok(())
1024                }
1025            };
1026            let zero = |what: &'static str| -> Result<()> {
1027                if rec.aux != 0 || rec.a != 0 || rec.b != 0 || rec.c != 0 || rec.flags != 0 {
1028                    return Err(Error::Node {
1029                        id: index,
1030                        reason: what,
1031                    });
1032                }
1033                Ok(())
1034            };
1035            match rec.kind {
1036                0 => zero("unused fields in Null node")?,
1037                1 => {
1038                    if rec.a > 1 || rec.aux != 0 || rec.b != 0 || rec.c != 0 || rec.flags != 0 {
1039                        return Err(Error::Node {
1040                            id: index,
1041                            reason: "invalid Bool node",
1042                        });
1043                    }
1044                }
1045                2 => {
1046                    if rec.aux != 0 || rec.b != 0 || rec.c != 0 || rec.flags != 0 {
1047                        return Err(Error::Node {
1048                            id: index,
1049                            reason: "unused fields in Int node",
1050                        });
1051                    }
1052                }
1053                3 => {
1054                    let value = f64::from_bits(rec.a);
1055                    if rec.aux != 0 || rec.b != 0 || rec.c != 0 || rec.flags != 0 {
1056                        return Err(Error::Node {
1057                            id: index,
1058                            reason: "unused fields in Float node",
1059                        });
1060                    }
1061                    if value.is_nan() && !crate::canonical::is_canonical_nan(value) {
1062                        return Err(Error::Node {
1063                            id: index,
1064                            reason: "non-canonical NaN",
1065                        });
1066                    }
1067                    if value == 0.0 && value.is_sign_negative() {
1068                        return Err(Error::Node {
1069                            id: index,
1070                            reason: "negative zero must be canonicalized",
1071                        });
1072                    }
1073                }
1074                4 | 13 => {
1075                    string(rec.aux)?;
1076                    if rec.a != 0 || rec.b != 0 || rec.c != 0 || rec.flags != 0 {
1077                        return Err(Error::Node {
1078                            id: index,
1079                            reason: "unused fields in string node",
1080                        });
1081                    }
1082                }
1083                5 => {
1084                    if rec.aux != 0 || rec.b != 0 || rec.c != 0 || rec.flags != 0 {
1085                        return Err(Error::Node {
1086                            id: index,
1087                            reason: "unused fields in Duration node",
1088                        });
1089                    }
1090                }
1091                6 => {
1092                    node(rec.aux as u64)?;
1093                    if rec.a >= unit_count as u64 {
1094                        return Err(Error::UnitRef(rec.a as u32));
1095                    }
1096                    if rec.b != 0 || rec.c != 0 || rec.flags != 0 {
1097                        return Err(Error::Node {
1098                            id: index,
1099                            reason: "unused fields in Quantity node",
1100                        });
1101                    }
1102                }
1103                7 => {
1104                    range(rec.a, rec.b, child_count, "array range out of bounds")?;
1105                    if rec.aux != 0 || rec.c != 0 || rec.flags != 0 {
1106                        return Err(Error::Node {
1107                            id: index,
1108                            reason: "unused fields in Array node",
1109                        });
1110                    }
1111                    for offset in rec.a..rec.a + rec.b {
1112                        let child = self.child(offset as u32)?;
1113                        node(child as u64)?;
1114                    }
1115                }
1116                8 | 20 => {
1117                    range(rec.a, rec.b, field_count, "field range out of bounds")?;
1118                    if rec.aux != 0 || rec.c != 0 {
1119                        return Err(Error::Node {
1120                            id: index,
1121                            reason: "unused fields in table node",
1122                        });
1123                    }
1124                    if rec.kind == 8 && rec.flags != 0 {
1125                        return Err(Error::Node {
1126                            id: index,
1127                            reason: "flags on a data table",
1128                        });
1129                    }
1130                    if rec.kind == 20 && rec.flags & !NODE_FLAG_0 != 0 {
1131                        return Err(Error::Node {
1132                            id: index,
1133                            reason: "unknown schema-table flag",
1134                        });
1135                    }
1136                    let mut previous: Option<u32> = None;
1137                    for offset in rec.a..rec.a + rec.b {
1138                        let (name, node_id) = self.field(offset as u32)?;
1139                        string(name)?;
1140                        node(node_id.0 as u64)?;
1141                        if let Some(previous) = previous {
1142                            if previous == name {
1143                                return Err(Error::DuplicateField { node: index });
1144                            }
1145                            if previous > name {
1146                                return Err(Error::Node {
1147                                    id: index,
1148                                    reason: "table fields not sorted by key",
1149                                });
1150                            }
1151                        }
1152                        previous = Some(name);
1153                    }
1154                }
1155                9 => {
1156                    if rec.a >= secret_count as u64 {
1157                        return Err(Error::SecretEntry(rec.a as u32));
1158                    }
1159                    if rec.aux != 0 || rec.b != 0 || rec.c != 0 || rec.flags != 0 {
1160                        return Err(Error::Node {
1161                            id: index,
1162                            reason: "unused fields in Secret node",
1163                        });
1164                    }
1165                }
1166                10 | 11 | 21 => {
1167                    node(rec.a)?;
1168                    if rec.aux != 0 || rec.b != 0 || rec.c != 0 || rec.flags != 0 {
1169                        return Err(Error::Node {
1170                            id: index,
1171                            reason: "unused fields in reference node",
1172                        });
1173                    }
1174                }
1175                12 => {
1176                    string(rec.aux)?;
1177                    if rec.a != u32::MAX as u64 {
1178                        node(rec.a)?;
1179                    }
1180                    if rec.b != 0 || rec.c != 0 || rec.flags != 0 {
1181                        return Err(Error::Node {
1182                            id: index,
1183                            reason: "unused fields in Env node",
1184                        });
1185                    }
1186                }
1187                14 => {
1188                    range(rec.a, rec.b, part_count, "part range out of bounds")?;
1189                    if rec.aux != 0 || rec.c != 0 || rec.flags != 0 {
1190                        return Err(Error::Node {
1191                            id: index,
1192                            reason: "unused fields in Interpolate node",
1193                        });
1194                    }
1195                    for offset in rec.a..rec.a + rec.b {
1196                        let (tag, value) = self.part(offset as u32)?;
1197                        match tag {
1198                            crate::layout::PART_LITERAL => string(value)?,
1199                            crate::layout::PART_EXPR => node(value as u64)?,
1200                            _ => {
1201                                return Err(Error::Node {
1202                                    id: index,
1203                                    reason: "unknown interpolation part tag",
1204                                })
1205                            }
1206                        }
1207                    }
1208                }
1209                15 => {
1210                    if crate::ir::BinOp::from_code(rec.aux).is_none() {
1211                        return Err(Error::Node {
1212                            id: index,
1213                            reason: "unknown binary operator",
1214                        });
1215                    }
1216                    node(rec.a)?;
1217                    node(rec.b)?;
1218                    if rec.c != 0 || rec.flags != 0 {
1219                        return Err(Error::Node {
1220                            id: index,
1221                            reason: "unused fields in Binary node",
1222                        });
1223                    }
1224                }
1225                16 => {
1226                    if crate::ir::UnOp::from_code(rec.aux).is_none() {
1227                        return Err(Error::Node {
1228                            id: index,
1229                            reason: "unknown unary operator",
1230                        });
1231                    }
1232                    node(rec.a)?;
1233                    if rec.b != 0 || rec.c != 0 || rec.flags != 0 {
1234                        return Err(Error::Node {
1235                            id: index,
1236                            reason: "unused fields in Unary node",
1237                        });
1238                    }
1239                }
1240                17 => {
1241                    node(rec.a)?;
1242                    if rec.aux > crate::layout::CONSTRAINT_KIND_CUSTOM {
1243                        return Err(Error::Node {
1244                            id: index,
1245                            reason: "unknown constraint kind",
1246                        });
1247                    }
1248                    range(rec.b, rec.c, aux_count, "constraint aux out of range")?;
1249                    if rec.flags != 0 {
1250                        return Err(Error::Node {
1251                            id: index,
1252                            reason: "flags on a constraint node",
1253                        });
1254                    }
1255                    let mut seen_kinds = Vec::new();
1256                    for offset in rec.b..rec.b + rec.c {
1257                        let (tag, value) = self.aux(offset as u32)?;
1258                        seen_kinds.push(tag);
1259                        match tag {
1260                            crate::layout::AUX_CONSTRAINT_MIN
1261                            | crate::layout::AUX_CONSTRAINT_MAX => {
1262                                if value >= self.header.span(Section::Numbers).count {
1263                                    return Err(Error::Node {
1264                                        id: index,
1265                                        reason: "constraint bound out of range",
1266                                    });
1267                                }
1268                            }
1269                            crate::layout::AUX_CONSTRAINT_ENUM_ITEM
1270                            | crate::layout::AUX_CONSTRAINT_PATTERN
1271                            | crate::layout::AUX_CONSTRAINT_CUSTOM => string(value)?,
1272                            _ => {
1273                                return Err(Error::Node {
1274                                    id: index,
1275                                    reason: "unknown constraint aux tag",
1276                                })
1277                            }
1278                        }
1279                    }
1280                    let expected_matches = match rec.aux {
1281                        crate::layout::CONSTRAINT_KIND_MIN_MAX => seen_kinds.iter().all(|tag| {
1282                            *tag == crate::layout::AUX_CONSTRAINT_MIN
1283                                || *tag == crate::layout::AUX_CONSTRAINT_MAX
1284                        }),
1285                        crate::layout::CONSTRAINT_KIND_ENUM => seen_kinds
1286                            .iter()
1287                            .all(|tag| *tag == crate::layout::AUX_CONSTRAINT_ENUM_ITEM),
1288                        crate::layout::CONSTRAINT_KIND_PATTERN => {
1289                            seen_kinds == [crate::layout::AUX_CONSTRAINT_PATTERN]
1290                        }
1291                        crate::layout::CONSTRAINT_KIND_ODD => seen_kinds.is_empty(),
1292                        _ => seen_kinds == [crate::layout::AUX_CONSTRAINT_CUSTOM],
1293                    };
1294                    if !expected_matches {
1295                        return Err(Error::Node {
1296                            id: index,
1297                            reason: "constraint payload does not match its kind",
1298                        });
1299                    }
1300                    if rec.aux == crate::layout::CONSTRAINT_KIND_ENUM && rec.c == 0 {
1301                        return Err(Error::Node {
1302                            id: index,
1303                            reason: "empty enum constraint",
1304                        });
1305                    }
1306                }
1307                18 => {
1308                    if rec.aux > crate::layout::PATCH_KIND_RENAME {
1309                        return Err(Error::Node {
1310                            id: index,
1311                            reason: "unknown patch kind",
1312                        });
1313                    }
1314                    range(rec.b, rec.c, aux_count, "patch aux out of range")?;
1315                    if rec.a != 0 || rec.flags != 0 {
1316                        return Err(Error::Node {
1317                            id: index,
1318                            reason: "unused fields in Patch node",
1319                        });
1320                    }
1321                    let mut tags = Vec::new();
1322                    for offset in rec.b..rec.b + rec.c {
1323                        let (tag, value) = self.aux(offset as u32)?;
1324                        tags.push(tag);
1325                        match tag {
1326                            crate::layout::AUX_PATCH_PATH | crate::layout::AUX_PATCH_RENAME_TO => {
1327                                string(value)?
1328                            }
1329                            crate::layout::AUX_PATCH_VALUE => node(value as u64)?,
1330                            _ => {
1331                                return Err(Error::Node {
1332                                    id: index,
1333                                    reason: "unknown patch aux tag",
1334                                })
1335                            }
1336                        }
1337                    }
1338                    let ok = match rec.aux {
1339                        crate::layout::PATCH_KIND_SET => {
1340                            tags == [
1341                                crate::layout::AUX_PATCH_PATH,
1342                                crate::layout::AUX_PATCH_VALUE,
1343                            ]
1344                        }
1345                        crate::layout::PATCH_KIND_REMOVE => tags == [crate::layout::AUX_PATCH_PATH],
1346                        _ => {
1347                            tags == [
1348                                crate::layout::AUX_PATCH_PATH,
1349                                crate::layout::AUX_PATCH_RENAME_TO,
1350                            ]
1351                        }
1352                    };
1353                    if !ok {
1354                        return Err(Error::Node {
1355                            id: index,
1356                            reason: "patch payload does not match its kind",
1357                        });
1358                    }
1359                }
1360                19 => {
1361                    if rec.aux >= type_count {
1362                        return Err(Error::Type {
1363                            id: rec.aux,
1364                            reason: "schema field references a missing type",
1365                        });
1366                    }
1367                    range(rec.b, rec.c, aux_count, "schema aux out of range")?;
1368                    if rec.a != 0 || rec.flags != 0 {
1369                        return Err(Error::Node {
1370                            id: index,
1371                            reason: "unused fields in SchemaField node",
1372                        });
1373                    }
1374                    for offset in rec.b..rec.b + rec.c {
1375                        let (tag, value) = self.aux(offset as u32)?;
1376                        match tag {
1377                            crate::layout::AUX_SCHEMA_MIN
1378                            | crate::layout::AUX_SCHEMA_MAX
1379                            | crate::layout::AUX_SCHEMA_MIN_LEN
1380                            | crate::layout::AUX_SCHEMA_MAX_LEN
1381                            | crate::layout::AUX_SCHEMA_PATTERN
1382                            | crate::layout::AUX_SCHEMA_DEFAULT => node(value as u64)?,
1383                            crate::layout::AUX_SCHEMA_DEPRECATED => string(value)?,
1384                            _ => {
1385                                return Err(Error::Node {
1386                                    id: index,
1387                                    reason: "unknown schema aux tag",
1388                                })
1389                            }
1390                        }
1391                    }
1392                }
1393                22 => {
1394                    if rec.aux > 2 {
1395                        return Err(Error::Node {
1396                            id: index,
1397                            reason: "unknown semantic tag",
1398                        });
1399                    }
1400                    node(rec.a)?;
1401                    if rec.b != 0 || rec.c != 0 || rec.flags != 0 {
1402                        return Err(Error::Node {
1403                            id: index,
1404                            reason: "unused fields in Semantic node",
1405                        });
1406                    }
1407                }
1408                23 => {
1409                    if rec.a >= blob_count as u64 {
1410                        return Err(Error::Blob {
1411                            id: rec.a as u32,
1412                            reason: "node references a missing blob",
1413                        });
1414                    }
1415                    string(rec.aux)?;
1416                    if rec.c != 0 {
1417                        return Err(Error::Node {
1418                            id: index,
1419                            reason: "unused field in Blob node",
1420                        });
1421                    }
1422                    if rec.flags & !(NODE_FLAG_0 | NODE_FLAG_1) != 0 {
1423                        return Err(Error::Node {
1424                            id: index,
1425                            reason: "unknown blob flag",
1426                        });
1427                    }
1428                    let (hash, _, len) = self.blob_entry(rec.a as u32)?;
1429                    let tainted = rec.flags & NODE_FLAG_0 != 0;
1430                    let has_payload = rec.flags & NODE_FLAG_1 != 0;
1431                    if tainted && has_payload {
1432                        return Err(Error::Blob {
1433                            id: rec.a as u32,
1434                            reason: "tainted blob must not carry a payload",
1435                        });
1436                    }
1437                    let size = rec.b;
1438                    if has_payload {
1439                        let payload = self.blob(rec.a as u32)?;
1440                        if payload.len() as u64 != size {
1441                            return Err(Error::Blob {
1442                                id: rec.a as u32,
1443                                reason: "payload length disagrees with size",
1444                            });
1445                        }
1446                        let mut hasher = blake3::Hasher::new();
1447                        hasher.update(payload);
1448                        if hasher.finalize().as_bytes() != &hash {
1449                            return Err(Error::Blob {
1450                                id: rec.a as u32,
1451                                reason: "payload hash mismatch",
1452                            });
1453                        }
1454                    } else if len != 0 {
1455                        return Err(Error::Blob {
1456                            id: rec.a as u32,
1457                            reason: "payload present without the flag",
1458                        });
1459                    }
1460                }
1461                24 => {
1462                    node(rec.a)?;
1463                    node(rec.b)?;
1464                    if rec.aux != 0 || rec.c != 0 || rec.flags != 0 {
1465                        return Err(Error::Node {
1466                            id: index,
1467                            reason: "unused fields in ContentAddressed node",
1468                        });
1469                    }
1470                }
1471                _ => unreachable!("kind checked above"),
1472            }
1473        }
1474        Ok(())
1475    }
1476
1477    fn check_fields(&self) -> Result<()> {
1478        let count = self.header.span(Section::Fields).count;
1479        let base = self.header.span(Section::Fields).offset as usize;
1480        for index in 0..count {
1481            let offset = base + index as usize * FIELD_SIZE;
1482            if read_u32(self.buf, offset + 4)? != 0 || read_u32(self.buf, offset + 12)? != 0 {
1483                return Err(Error::ReservedNonZero {
1484                    section: "fields",
1485                    offset: (offset + 4) as u64,
1486                });
1487            }
1488        }
1489        Ok(())
1490    }
1491
1492    fn check_parts(&self) -> Result<()> {
1493        let count = self.header.span(Section::Parts).count;
1494        let base = self.header.span(Section::Parts).offset as usize;
1495        for index in 0..count {
1496            let tag = read_u32(self.buf, base + index as usize * 8)?;
1497            if tag > crate::layout::PART_EXPR {
1498                return Err(Error::Node {
1499                    id: index,
1500                    reason: "unknown part tag",
1501                });
1502            }
1503        }
1504        Ok(())
1505    }
1506
1507    fn check_edges(&self) -> Result<()> {
1508        let count = self.header.span(Section::Edges).count;
1509        let node_count = self.node_count();
1510        let base = self.header.span(Section::Edges).offset as usize;
1511        let mut previous: Option<(u32, u32)> = None;
1512        let mut edges = Vec::with_capacity(count.min(1 << 20) as usize);
1513        for index in 0..count {
1514            let from = read_u32(self.buf, base + index as usize * 8)?;
1515            let to = read_u32(self.buf, base + index as usize * 8 + 4)?;
1516            if from >= node_count || to >= node_count || from == to {
1517                return Err(Error::Edge { from, to });
1518            }
1519            if let Some(previous) = previous {
1520                if previous >= (from, to) {
1521                    return Err(Error::Edge { from, to });
1522                }
1523            }
1524            previous = Some((from, to));
1525            edges.push((from, to));
1526        }
1527        // The edge table must be exactly the derived graph: no extra edges,
1528        // no missing edges. This is what makes the graph unforgeable.
1529        let mut derived: Vec<(u32, u32)> = Vec::new();
1530        let mut scratch: Vec<u32> = Vec::new();
1531        for index in 0..node_count {
1532            let rec = self.node(NodeId(index))?;
1533            self.node_children_into(&rec, &mut scratch)?;
1534            for child in &scratch {
1535                derived.push((*child, index));
1536            }
1537        }
1538        derived.sort_unstable();
1539        derived.dedup();
1540        if derived != edges {
1541            return Err(Error::Edge { from: 0, to: 0 });
1542        }
1543        Ok(())
1544    }
1545
1546    /// Node references in canonical order, written into a reusable buffer.
1547    ///
1548    /// Validation walks every node three times (nodes, edges, reachability); a
1549    /// fresh `Vec` per node dominated the profile, so the hot paths reuse one
1550    /// scratch buffer per traversal.
1551    pub(crate) fn node_children_into(&self, rec: &NodeRec, out: &mut Vec<u32>) -> Result<()> {
1552        out.clear();
1553        match rec.kind {
1554            6 => out.push(rec.aux),
1555            7 => {
1556                for offset in rec.a..rec.a + rec.b {
1557                    out.push(self.child(offset as u32)?);
1558                }
1559            }
1560            8 | 20 => {
1561                for offset in rec.a..rec.a + rec.b {
1562                    out.push(self.field(offset as u32)?.1 .0);
1563                }
1564            }
1565            10 | 11 | 21 | 22 => out.push(rec.a as u32),
1566            12 => {
1567                if rec.a != u32::MAX as u64 {
1568                    out.push(rec.a as u32);
1569                }
1570            }
1571            14 => {
1572                for offset in rec.a..rec.a + rec.b {
1573                    let (tag, value) = self.part(offset as u32)?;
1574                    if tag == crate::layout::PART_EXPR {
1575                        out.push(value);
1576                    }
1577                }
1578            }
1579            15 => {
1580                out.push(rec.a as u32);
1581                out.push(rec.b as u32);
1582            }
1583            16 | 17 => out.push(rec.a as u32),
1584            18 => {
1585                for offset in rec.b..rec.b + rec.c {
1586                    let (tag, value) = self.aux(offset as u32)?;
1587                    if tag == crate::layout::AUX_PATCH_VALUE {
1588                        out.push(value);
1589                    }
1590                }
1591            }
1592            19 => {
1593                for offset in rec.b..rec.b + rec.c {
1594                    let (tag, value) = self.aux(offset as u32)?;
1595                    if tag != crate::layout::AUX_SCHEMA_DEPRECATED {
1596                        out.push(value);
1597                    }
1598                }
1599            }
1600            24 => {
1601                out.push(rec.a as u32);
1602                out.push(rec.b as u32);
1603            }
1604            _ => {}
1605        }
1606        Ok(())
1607    }
1608
1609    /// Node references in canonical order for a record.
1610    pub(crate) fn node_children(&self, _index: u32, rec: &NodeRec) -> Result<Vec<u32>> {
1611        let mut out = Vec::new();
1612        match rec.kind {
1613            6 => out.push(rec.aux),
1614            7 => {
1615                for offset in rec.a..rec.a + rec.b {
1616                    out.push(self.child(offset as u32)?);
1617                }
1618            }
1619            8 | 20 => {
1620                for offset in rec.a..rec.a + rec.b {
1621                    out.push(self.field(offset as u32)?.1 .0);
1622                }
1623            }
1624            10 | 11 | 21 | 22 => out.push(rec.a as u32),
1625            12 => {
1626                if rec.a != u32::MAX as u64 {
1627                    out.push(rec.a as u32);
1628                }
1629            }
1630            14 => {
1631                for offset in rec.a..rec.a + rec.b {
1632                    let (tag, value) = self.part(offset as u32)?;
1633                    if tag == crate::layout::PART_EXPR {
1634                        out.push(value);
1635                    }
1636                }
1637            }
1638            15 => {
1639                out.push(rec.a as u32);
1640                out.push(rec.b as u32);
1641            }
1642            16 => out.push(rec.a as u32),
1643            17 => out.push(rec.a as u32),
1644            18 => {
1645                for offset in rec.b..rec.b + rec.c {
1646                    let (tag, value) = self.aux(offset as u32)?;
1647                    if tag == crate::layout::AUX_PATCH_VALUE {
1648                        out.push(value);
1649                    }
1650                }
1651            }
1652            19 => {
1653                for offset in rec.b..rec.b + rec.c {
1654                    let (tag, value) = self.aux(offset as u32)?;
1655                    if tag != crate::layout::AUX_SCHEMA_DEPRECATED {
1656                        out.push(value);
1657                    }
1658                }
1659            }
1660            24 => {
1661                out.push(rec.a as u32);
1662                out.push(rec.b as u32);
1663            }
1664            _ => {}
1665        }
1666        Ok(out)
1667    }
1668
1669    fn check_annotations(&self) -> Result<()> {
1670        let count = self.header.span(Section::Annotations).count;
1671        let node_count = self.node_count();
1672        let mut previous: Option<(u32, u32, u32, u32)> = None;
1673        for index in 0..count {
1674            let (node, kind, a, b) = self.annotation(index)?;
1675            if node >= node_count {
1676                return Err(Error::Annotation {
1677                    id: index,
1678                    reason: "annotation subject out of range",
1679                });
1680            }
1681            match kind {
1682                crate::layout::ANN_KIND_INTENT => {
1683                    if a >= self.header.span(Section::Strings).count || b != 0 {
1684                        return Err(Error::Annotation {
1685                            id: index,
1686                            reason: "invalid intent annotation",
1687                        });
1688                    }
1689                }
1690                crate::layout::ANN_KIND_CONSTRAINT => {
1691                    if a >= self.header.span(Section::Strings).count || b >= node_count {
1692                        return Err(Error::Annotation {
1693                            id: index,
1694                            reason: "invalid constraint annotation",
1695                        });
1696                    }
1697                }
1698                crate::layout::ANN_KIND_AI_IMMUTABLE => {
1699                    if a > 1 || b != 0 {
1700                        return Err(Error::Annotation {
1701                            id: index,
1702                            reason: "invalid immutability annotation",
1703                        });
1704                    }
1705                }
1706                _ => {
1707                    return Err(Error::Annotation {
1708                        id: index,
1709                        reason: "unknown annotation kind",
1710                    })
1711                }
1712            }
1713            if let Some(previous) = previous {
1714                if previous >= (node, kind, a, b) {
1715                    return Err(Error::Annotation {
1716                        id: index,
1717                        reason: "annotations not sorted",
1718                    });
1719                }
1720            }
1721            previous = Some((node, kind, a, b));
1722        }
1723        Ok(())
1724    }
1725
1726    fn check_provenance(&self, limits: Limits) -> Result<()> {
1727        let len = self.header.span(Section::Provenance).count as usize;
1728        if len == 0 {
1729            return Ok(());
1730        }
1731        let start = self.header.span(Section::Provenance).offset as usize;
1732        let count = read_u32(self.buf, start + 4)?;
1733        if count > limits.max_prov_entries {
1734            return Err(Error::Limit {
1735                kind: "provenance entries",
1736                limit: limits.max_prov_entries as u64,
1737            });
1738        }
1739        let prov = self.provenance()?;
1740        if let Some(prov) = prov {
1741            let mut previous: Option<&str> = None;
1742            for entry in &prov.entries {
1743                if let Some(previous) = previous {
1744                    if previous >= entry.key.as_str() {
1745                        return Err(Error::Provenance("entries not sorted by key"));
1746                    }
1747                }
1748                previous = Some(entry.key.as_str());
1749            }
1750            if let Some(text) = &prov.producer {
1751                if text.is_empty() {
1752                    return Err(Error::Provenance("empty producer"));
1753                }
1754            }
1755        }
1756        Ok(())
1757    }
1758
1759    fn check_extensions(&self) -> Result<()> {
1760        let count = self.header.span(Section::Extensions).count;
1761        if count == 0 {
1762            return Ok(());
1763        }
1764        let start = self.header.span(Section::Extensions).offset as u64;
1765        let end = self.ends[Section::Extensions as usize];
1766        let dir_end = start + count as u64 * EXT_DIR_SIZE as u64;
1767        if dir_end > end {
1768            return Err(Error::Offset {
1769                section: "extensions",
1770                offset: start,
1771            });
1772        }
1773        let mut previous_tag: Option<u64> = None;
1774        let mut previous_end = dir_end;
1775        for index in 0..count {
1776            let offset = start as usize + index as usize * EXT_DIR_SIZE;
1777            let tag = read_u64(self.buf, offset)?;
1778            let payload_offset = read_u32(self.buf, offset + 8)? as u64;
1779            let payload_len = read_u32(self.buf, offset + 12)? as u64;
1780            if let Some(previous) = previous_tag {
1781                if previous >= tag {
1782                    return Err(Error::Extension {
1783                        tag,
1784                        reason: "extension tags not sorted",
1785                    });
1786                }
1787            }
1788            previous_tag = Some(tag);
1789            // Payloads are packed in tag order: the first starts right after the
1790            // directory and each later one starts at the next multiple of 8, so
1791            // there is no room for slack that a reader could not account for.
1792            let expected = align8(previous_end);
1793            if payload_offset != expected {
1794                return Err(Error::Extension {
1795                    tag,
1796                    reason: "payloads are not packed in tag order",
1797                });
1798            }
1799            for offset in previous_end..expected {
1800                if self.buf[offset as usize] != 0 {
1801                    return Err(Error::Padding { offset });
1802                }
1803            }
1804            let payload_end = payload_offset
1805                .checked_add(payload_len)
1806                .ok_or(Error::Extension {
1807                    tag,
1808                    reason: "payload length overflows",
1809                })?;
1810            if payload_end > end {
1811                return Err(Error::Extension {
1812                    tag,
1813                    reason: "payload out of range",
1814                });
1815            }
1816            previous_end = payload_end;
1817        }
1818        // `section_end` already reports the unaligned end of the last payload;
1819        // the padding up to `align8(end)` belongs to the inter-section gap and
1820        // is checked for zeroes by `validate_layout`.
1821        if previous_end != end {
1822            return Err(Error::Extension {
1823                tag: 0,
1824                reason: "extension section has trailing bytes",
1825            });
1826        }
1827        Ok(())
1828    }
1829
1830    /// Reject a dependency cycle with its own code.
1831    ///
1832    /// The depth ceiling would also stop a cycle, but it would report `depth`;
1833    /// a peer needs to distinguish "too deep" from "cyclic" (the text language
1834    /// treats a cycle as fatal before any value exists).
1835    fn check_acyclic(&self) -> Result<()> {
1836        let count = self.node_count();
1837        // Kahn's algorithm: peel nodes with no incoming edge. It is O(V + E)
1838        // with one child-list derivation per node, where a cursor-based depth
1839        // first search would re-derive a wide node's children once per edge and
1840        // turn a 64k-field table into a quadratic validation. Counts saturate:
1841        // an in-degree above `u32::MAX` needs more edges than `max_edges` allows.
1842        let mut incoming = vec![0u32; count as usize];
1843        let mut scratch: Vec<u32> = Vec::new();
1844        for node in 0..count {
1845            let rec = self.node(NodeId(node))?;
1846            self.node_children_into(&rec, &mut scratch)?;
1847            for child in &scratch {
1848                let slot = &mut incoming[*child as usize];
1849                *slot = slot.saturating_add(1);
1850            }
1851        }
1852        let mut queue: std::collections::VecDeque<u32> = incoming
1853            .iter()
1854            .enumerate()
1855            .filter(|(_, degree)| **degree == 0)
1856            .map(|(node, _)| node as u32)
1857            .collect();
1858        let mut processed = 0u32;
1859        while let Some(node) = queue.pop_front() {
1860            processed += 1;
1861            let rec = self.node(NodeId(node))?;
1862            self.node_children_into(&rec, &mut scratch)?;
1863            for child in &scratch {
1864                let slot = &mut incoming[*child as usize];
1865                *slot = slot.saturating_sub(1);
1866                if *slot == 0 {
1867                    queue.push_back(*child);
1868                }
1869            }
1870        }
1871        if processed != count {
1872            // Whatever the peel could not reach is on a cycle or depends on one
1873            // (`incoming` counts edges from nodes that never got peeled). Report
1874            // the lowest such node so the answer is stable.
1875            let node = incoming.iter().position(|degree| *degree > 0).unwrap_or(0) as u32;
1876            return Err(Error::Cycle { node });
1877        }
1878        Ok(())
1879    }
1880
1881    fn check_reachability(&self, limits: Limits) -> Result<()> {
1882        self.check_bindings()?;
1883        let count = self.node_count();
1884        if count == 0 {
1885            return Ok(());
1886        }
1887        let mut roots = Vec::new();
1888        if let Some(root) = self.root()? {
1889            roots.push(root.0);
1890        }
1891        for (_, id) in self.bindings()? {
1892            roots.push(id.0);
1893        }
1894        for index in 0..self.header.span(Section::Annotations).count {
1895            let (node, kind, _, b) = self.annotation(index)?;
1896            roots.push(node);
1897            if kind == crate::layout::ANN_KIND_CONSTRAINT {
1898                roots.push(b);
1899            }
1900        }
1901        roots.sort_unstable();
1902        roots.dedup();
1903        let mut seen = vec![false; count as usize];
1904        let mut stack: Vec<u32> = roots;
1905        let mut scratch: Vec<u32> = Vec::new();
1906        let mut visited = 0u32;
1907        while let Some(node) = stack.pop() {
1908            if seen[node as usize] {
1909                continue;
1910            }
1911            seen[node as usize] = true;
1912            visited += 1;
1913            let rec = self.node(NodeId(node))?;
1914            self.node_children_into(&rec, &mut scratch)?;
1915            for child in &scratch {
1916                if !seen[*child as usize] {
1917                    stack.push(*child);
1918                }
1919            }
1920        }
1921        if visited != count {
1922            return Err(Error::Node {
1923                id: (0..count).find(|id| !seen[*id as usize]).unwrap_or(0),
1924                reason: "unreachable node",
1925            });
1926        }
1927        let _ = limits;
1928        Ok(())
1929    }
1930
1931    fn check_depth(&self, limits: Limits) -> Result<()> {
1932        // `Limits::check` has already rejected a zero ceiling, so this function
1933        // never sees `max_depth == 0`.
1934        let count = self.node_count();
1935        if count == 0 {
1936            return Ok(());
1937        }
1938        // A topological sweep, so each edge is examined exactly once. The old
1939        // cursor walk re-derived a node's children every time its depth grew,
1940        // which is quadratic on wide nodes and polynomial on diamond graphs.
1941        let mut incoming = vec![0u32; count as usize];
1942        let mut scratch: Vec<u32> = Vec::new();
1943        for node in 0..count {
1944            let rec = self.node(NodeId(node))?;
1945            self.node_children_into(&rec, &mut scratch)?;
1946            for child in &scratch {
1947                let slot = &mut incoming[*child as usize];
1948                *slot = slot.saturating_add(1);
1949            }
1950        }
1951        // Every node with no incoming edge is a root (module root, bindings,
1952        // annotation subjects). Starting only at node 0 would let a second root
1953        // carry an arbitrarily deep chain past the ceiling.
1954        let mut depth = vec![1u32; count as usize];
1955        let mut queue: std::collections::VecDeque<u32> = incoming
1956            .iter()
1957            .enumerate()
1958            .filter(|(_, degree)| **degree == 0)
1959            .map(|(node, _)| node as u32)
1960            .collect();
1961        if queue.is_empty() {
1962            // Acyclicity runs first; stay total for a hand-built order and
1963            // report the same node the cycle check would.
1964            let node = incoming.iter().position(|degree| *degree > 0).unwrap_or(0) as u32;
1965            return Err(Error::Cycle { node });
1966        }
1967        let mut processed = 0u32;
1968        while let Some(node) = queue.pop_front() {
1969            processed += 1;
1970            let next = depth[node as usize] + 1;
1971            let rec = self.node(NodeId(node))?;
1972            self.node_children_into(&rec, &mut scratch)?;
1973            for child in &scratch {
1974                if next > limits.max_depth {
1975                    return Err(Error::Depth {
1976                        limit: limits.max_depth,
1977                    });
1978                }
1979                if next > depth[*child as usize] {
1980                    depth[*child as usize] = next;
1981                }
1982                let slot = &mut incoming[*child as usize];
1983                *slot = slot.saturating_sub(1);
1984                if *slot == 0 {
1985                    queue.push_back(*child);
1986                }
1987            }
1988        }
1989        if processed != count {
1990            let node = incoming.iter().position(|degree| *degree > 0).unwrap_or(0) as u32;
1991            return Err(Error::Cycle { node });
1992        }
1993        Ok(())
1994    }
1995}
1996
1997/// Check every count against the configured limits and the remaining bytes.
1998fn validate_counts(header: &Header, limits: Limits) -> Result<()> {
1999    let checks: [(Section, u32, u32); SECTION_COUNT] = [
2000        (
2001            Section::Nodes,
2002            header.span(Section::Nodes).count,
2003            limits.max_nodes,
2004        ),
2005        (
2006            Section::Children,
2007            header.span(Section::Children).count,
2008            limits.max_children,
2009        ),
2010        (
2011            Section::Fields,
2012            header.span(Section::Fields).count,
2013            limits.max_fields,
2014        ),
2015        (
2016            Section::Parts,
2017            header.span(Section::Parts).count,
2018            limits.max_parts,
2019        ),
2020        (
2021            Section::Edges,
2022            header.span(Section::Edges).count,
2023            limits.max_edges,
2024        ),
2025        (
2026            Section::Strings,
2027            header.span(Section::Strings).count,
2028            limits.max_strings,
2029        ),
2030        (
2031            Section::Blobs,
2032            header.span(Section::Blobs).count,
2033            limits.max_blobs,
2034        ),
2035        (
2036            Section::Annotations,
2037            header.span(Section::Annotations).count,
2038            limits.max_annotations,
2039        ),
2040        (
2041            Section::Units,
2042            header.span(Section::Units).count,
2043            limits.max_units,
2044        ),
2045        (
2046            Section::Types,
2047            header.span(Section::Types).count,
2048            limits.max_types,
2049        ),
2050        (
2051            Section::Aux,
2052            header.span(Section::Aux).count,
2053            limits.max_aux,
2054        ),
2055        (
2056            Section::Numbers,
2057            header.span(Section::Numbers).count,
2058            limits.max_numbers,
2059        ),
2060        (
2061            Section::Secrets,
2062            header.span(Section::Secrets).count,
2063            limits.max_secrets,
2064        ),
2065        (
2066            Section::Provenance,
2067            header.span(Section::Provenance).count,
2068            u32::MAX,
2069        ),
2070        (
2071            Section::Hashes,
2072            header.span(Section::Hashes).count,
2073            u32::MAX,
2074        ),
2075        (
2076            Section::Extensions,
2077            header.span(Section::Extensions).count,
2078            limits.max_extensions,
2079        ),
2080        (
2081            Section::Bindings,
2082            header.span(Section::Bindings).count,
2083            u32::MAX,
2084        ),
2085    ];
2086    for (section, count, limit) in checks {
2087        if count > limit {
2088            return Err(Error::Limit {
2089                kind: section.name(),
2090                limit: limit as u64,
2091            });
2092        }
2093    }
2094    Ok(())
2095}
2096
2097/// Check section ranges: canonical order, alignment, no overlap, zero padding,
2098/// and an exact total size.
2099fn validate_layout(buf: &[u8], header: &Header) -> Result<[u64; SECTION_COUNT]> {
2100    // Bound the buffer first: every later read is in range only after this.
2101    if header.total_size > buf.len() as u64 {
2102        return Err(Error::Truncated {
2103            needed: header.total_size,
2104            found: buf.len() as u64,
2105        });
2106    }
2107    if header.total_size < buf.len() as u64 {
2108        return Err(Error::TrailingBytes {
2109            at: header.total_size,
2110        });
2111    }
2112    let mut ends = [0u64; SECTION_COUNT];
2113    let mut cursor = HEADER_SIZE as u64;
2114    for section in Section::ALL {
2115        let span = header.span(section);
2116        if span.offset as u64 != align8(cursor) {
2117            return Err(Error::Offset {
2118                section: section.name(),
2119                offset: span.offset as u64,
2120            });
2121        }
2122        if span.offset as u64 > buf.len() as u64 {
2123            return Err(Error::Truncated {
2124                needed: span.offset as u64,
2125                found: buf.len() as u64,
2126            });
2127        }
2128        // Padding between the previous section and this one must be zero.
2129        for offset in cursor..span.offset as u64 {
2130            if buf[offset as usize] != 0 {
2131                return Err(Error::Padding { offset });
2132            }
2133        }
2134        let end = section_end(buf, header, section, span.offset as u64, span.count)?;
2135        if end > header.total_size || end > buf.len() as u64 {
2136            // The declared count cannot be satisfied by the image: a count
2137            // fault, distinct from an offset/alignment fault.
2138            return Err(Error::Count {
2139                field: section.name(),
2140                value: span.count as u64,
2141            });
2142        }
2143        ends[section as usize] = end;
2144        cursor = end;
2145    }
2146    if header.total_size != align8(cursor) {
2147        return Err(Error::Offset {
2148            section: "total-size",
2149            offset: header.total_size,
2150        });
2151    }
2152    if header.total_size != buf.len() as u64 {
2153        if header.total_size < buf.len() as u64 {
2154            return Err(Error::TrailingBytes {
2155                at: header.total_size,
2156            });
2157        }
2158        return Err(Error::Truncated {
2159            needed: header.total_size,
2160            found: buf.len() as u64,
2161        });
2162    }
2163    Ok(ends)
2164}
2165
2166fn section_end(
2167    buf: &[u8],
2168    _header: &Header,
2169    section: Section,
2170    offset: u64,
2171    count: u32,
2172) -> Result<u64> {
2173    let end = match section {
2174        Section::Strings => {
2175            if count == 0 {
2176                offset
2177            } else {
2178                let table = offset as usize;
2179                let heap_len = read_u32(buf, table + count as usize * 4)? as u64;
2180                offset + (count as u64 + 1) * 4 + heap_len
2181            }
2182        }
2183        Section::Blobs => {
2184            let table = offset as usize;
2185            let mut heap = 0u64;
2186            for index in 0..count {
2187                let len = read_u32(buf, table + index as usize * BLOB_ENTRY_SIZE + 36)? as u64;
2188                heap = heap.checked_add(len).ok_or(Error::Blob {
2189                    id: index,
2190                    reason: "payload heap overflows",
2191                })?;
2192            }
2193            offset + count as u64 * BLOB_ENTRY_SIZE as u64 + heap
2194        }
2195        Section::Provenance => offset + count as u64,
2196        Section::Secrets => {
2197            let table = offset as usize;
2198            let mut heap = 0u64;
2199            for index in 0..count {
2200                let len = read_u32(buf, table + index as usize * 8 + 4)? as u64;
2201                heap = heap.checked_add(len).ok_or(Error::SecretEntry(index))?;
2202            }
2203            offset + count as u64 * 8 + heap
2204        }
2205        Section::Extensions => {
2206            if count == 0 {
2207                offset
2208            } else {
2209                let mut max_end = offset + count as u64 * EXT_DIR_SIZE as u64;
2210                for index in 0..count {
2211                    let dir = offset as usize + index as usize * EXT_DIR_SIZE;
2212                    let payload_offset = read_u32(buf, dir + 8)? as u64;
2213                    let payload_len = read_u32(buf, dir + 12)? as u64;
2214                    max_end = max_end.max(payload_offset + payload_len);
2215                }
2216                max_end
2217            }
2218        }
2219        Section::Nodes => offset + count as u64 * NODE_RECORD_SIZE as u64,
2220        Section::Children => offset + count as u64 * 4,
2221        Section::Fields => offset + count as u64 * FIELD_SIZE as u64,
2222        Section::Parts => offset + count as u64 * 8,
2223        Section::Edges => offset + count as u64 * 8,
2224        Section::Annotations => offset + count as u64 * 16,
2225        Section::Units => offset + count as u64 * 8,
2226        Section::Types => offset + count as u64 * 16,
2227        Section::Aux => offset + count as u64 * 8,
2228        Section::Numbers => offset + count as u64 * 8,
2229        Section::Hashes => offset + count as u64 * 32,
2230        Section::Bindings => offset + count as u64 * 8,
2231    };
2232    Ok(end)
2233}
2234
2235/// Validate a buffer with the default options.
2236pub fn validate(buf: &[u8]) -> Result<Meta> {
2237    Validated::new(buf, crate::layout::DecodeOpts::default()).map(|validated| validated.meta())
2238}
2239
2240/// Validate a buffer with explicit options.
2241pub fn validate_with(buf: &[u8], opts: crate::layout::DecodeOpts) -> Result<Meta> {
2242    Validated::new(buf, opts).map(|validated| validated.meta())
2243}
2244
2245/// Validate and additionally prove `buf` is exactly the canonical encoding of
2246/// its own content.
2247///
2248/// This is the unconditional form of `DecodeOpts { verify_canonical: true }`:
2249/// an image that is canonical in structure but lacks the `canonical` flag fails
2250/// with `infinity/not-canonical`, because the canonical encoding of its content
2251/// carries the flag. The check re-encodes once, inside validation.
2252pub fn validate_canonical(buf: &[u8], limits: Limits) -> Result<Meta> {
2253    let opts = crate::layout::DecodeOpts {
2254        limits,
2255        verify_hash: true,
2256        verify_canonical: true,
2257    };
2258    Validated::new(buf, opts).map(|validated| validated.meta())
2259}
2260
2261/// Parse a provenance block from bytes already validated (used by tests).
2262pub fn provenance_hash_of(prov: Option<&Provenance>) -> Option<[u8; 32]> {
2263    prov.and_then(|prov| prov.parent)
2264}
2265
2266/// Report whether a value is a hash provenance entry (helper for projections).
2267pub fn is_hash_entry(entry: &crate::ir::ProvEntry) -> bool {
2268    matches!(entry.value, ProvValue::Hash(_))
2269}