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