Skip to main content

active_infinity/
layout.rs

1//! Wire layout primitives: the fixed header, node records, section table and
2//! the limits that bound untrusted input.
3//!
4//! Everything here is a pure description of bytes; the normative document is
5//! `docs/spec/layout.md`, and `tests/doc_gate.rs` fails the build if constants
6//! and prose drift apart.
7
8// Field names mirror the wire header exactly; `docs/spec/layout.md` is the
9// normative description of every byte.
10#![allow(missing_docs)]
11
12use crate::error::{Error, Result};
13
14/// Magic at offset 0: `A I N F`.
15pub const MAGIC: [u8; 4] = *b"AINF";
16/// Stream frame magic: `A I N S`.
17pub const STREAM_MAGIC: [u8; 4] = *b"AINS";
18/// Major format version implemented by this crate.
19pub const FORMAT_MAJOR: u16 = 1;
20/// Minor format version implemented by this crate.
21pub const FORMAT_MINOR: u16 = 0;
22/// Fixed header size in bytes.
23pub const HEADER_SIZE: usize = 224;
24/// Fixed node record size in bytes.
25pub const NODE_RECORD_SIZE: usize = 32;
26/// Child index entry size.
27pub const CHILD_SIZE: usize = 4;
28/// Table field entry size: name string id + node id + two pad words.
29pub const FIELD_SIZE: usize = 16;
30/// Interpolation part entry size.
31pub const PART_SIZE: usize = 8;
32/// Edge entry size.
33pub const EDGE_SIZE: usize = 8;
34/// Blob index entry size.
35pub const BLOB_ENTRY_SIZE: usize = 40;
36/// Annotation entry size.
37pub const ANNOTATION_SIZE: usize = 16;
38/// Unit table entry size.
39pub const UNIT_SIZE: usize = 8;
40/// Type table entry size.
41pub const TYPE_SIZE: usize = 16;
42/// Aux record size.
43pub const AUX_SIZE: usize = 8;
44/// Numeric side-table entry size.
45pub const NUM_SIZE: usize = 8;
46/// Extension directory entry size.
47pub const EXT_DIR_SIZE: usize = 16;
48/// Provenance block fixed header size.
49pub const PROV_HEADER_SIZE: usize = 96;
50/// Provenance entry size.
51pub const PROV_ENTRY_SIZE: usize = 16;
52/// Secret table entry size.
53pub const SECRET_ENTRY_SIZE: usize = 8;
54
55// ---- aux record tags (wire contract) ----------------------------------
56/// Constraint `min` bound; value is a `numbers` index.
57pub const AUX_CONSTRAINT_MIN: u32 = 0;
58/// Constraint `max` bound; value is a `numbers` index.
59pub const AUX_CONSTRAINT_MAX: u32 = 1;
60/// Constraint enum member; value is a string id.
61pub const AUX_CONSTRAINT_ENUM_ITEM: u32 = 2;
62/// Constraint regex pattern; value is a string id.
63pub const AUX_CONSTRAINT_PATTERN: u32 = 3;
64/// Custom constraint payload; value is a string id.
65pub const AUX_CONSTRAINT_CUSTOM: u32 = 4;
66/// Patch target path; value is a string id.
67pub const AUX_PATCH_PATH: u32 = 10;
68/// Patch value node; value is a node id.
69pub const AUX_PATCH_VALUE: u32 = 11;
70/// Patch rename destination; value is a string id.
71pub const AUX_PATCH_RENAME_TO: u32 = 12;
72/// Schema field `min` node.
73pub const AUX_SCHEMA_MIN: u32 = 20;
74/// Schema field `max` node.
75pub const AUX_SCHEMA_MAX: u32 = 21;
76/// Schema field `min_len` node.
77pub const AUX_SCHEMA_MIN_LEN: u32 = 22;
78/// Schema field `max_len` node.
79pub const AUX_SCHEMA_MAX_LEN: u32 = 23;
80/// Schema field `pattern` node.
81pub const AUX_SCHEMA_PATTERN: u32 = 24;
82/// Schema field `default` node.
83pub const AUX_SCHEMA_DEFAULT: u32 = 25;
84/// Schema field `deprecated` message; value is a string id.
85pub const AUX_SCHEMA_DEPRECATED: u32 = 26;
86/// Enum type member; value is a string id.
87pub const AUX_TYPE_ENUM_ITEM: u32 = 30;
88/// Table type closed flag; value is `0` or `1`.
89pub const AUX_TYPE_TABLE: u32 = 31;
90
91/// Constraint kind: `MinMax`.
92pub const CONSTRAINT_KIND_MIN_MAX: u32 = 0;
93/// Constraint kind: `Enum`.
94pub const CONSTRAINT_KIND_ENUM: u32 = 1;
95/// Constraint kind: `Pattern`.
96pub const CONSTRAINT_KIND_PATTERN: u32 = 2;
97/// Constraint kind: `Odd`.
98pub const CONSTRAINT_KIND_ODD: u32 = 3;
99/// Constraint kind: `Custom`.
100pub const CONSTRAINT_KIND_CUSTOM: u32 = 4;
101
102/// Patch kind: `Set`.
103pub const PATCH_KIND_SET: u32 = 0;
104/// Patch kind: `Remove`.
105pub const PATCH_KIND_REMOVE: u32 = 1;
106/// Patch kind: `Rename`.
107pub const PATCH_KIND_RENAME: u32 = 2;
108
109/// Annotation kind: `Intent`.
110pub const ANN_KIND_INTENT: u32 = 0;
111/// Annotation kind: `Constraint`.
112pub const ANN_KIND_CONSTRAINT: u32 = 1;
113/// Annotation kind: `AiImmutable`.
114pub const ANN_KIND_AI_IMMUTABLE: u32 = 2;
115
116/// Interpolation part: literal string; value is a string id.
117pub const PART_LITERAL: u32 = 0;
118/// Interpolation part: expression; value is a node id.
119pub const PART_EXPR: u32 = 1;
120
121/// Header flag: the image is in canonical form (self-declaration the reader
122/// verifies).
123pub const FLAG_CANONICAL: u16 = 1 << 0;
124/// Header flag: an annotation table is present.
125pub const FLAG_ANNOTATIONS: u16 = 1 << 1;
126/// Header flag: a provenance block is present.
127pub const FLAG_PROVENANCE: u16 = 1 << 2;
128/// Header flag: an extension directory is present.
129pub const FLAG_EXTENSIONS: u16 = 1 << 3;
130/// Header flag: the module was reassembled from a stream.
131pub const FLAG_STREAMED: u16 = 1 << 4;
132/// Header flag: a secret table is present.
133pub const FLAG_SECRETS: u16 = 1 << 5;
134/// All flags this reader understands.
135pub const FLAG_KNOWN_MASK: u16 = FLAG_CANONICAL
136    | FLAG_ANNOTATIONS
137    | FLAG_PROVENANCE
138    | FLAG_EXTENSIONS
139    | FLAG_STREAMED
140    | FLAG_SECRETS;
141
142/// Node flag bit 0 (meaning depends on kind).
143pub const NODE_FLAG_0: u8 = 1 << 0;
144/// Node flag bit 1.
145pub const NODE_FLAG_1: u8 = 1 << 1;
146/// Node flag bit 2.
147pub const NODE_FLAG_2: u8 = 1 << 2;
148/// Node flag bit 3.
149pub const NODE_FLAG_3: u8 = 1 << 3;
150/// Node flag bit 4.
151pub const NODE_FLAG_4: u8 = 1 << 4;
152/// Node flag bit 5.
153pub const NODE_FLAG_5: u8 = 1 << 5;
154/// Node flag bit 6.
155pub const NODE_FLAG_6: u8 = 1 << 6;
156/// All node flag bits (bit 7 is reserved and must be zero).
157pub const NODE_FLAG_KNOWN: u8 =
158    NODE_FLAG_0 | NODE_FLAG_1 | NODE_FLAG_2 | NODE_FLAG_3 | NODE_FLAG_4 | NODE_FLAG_5 | NODE_FLAG_6;
159
160/// Number of sections in the canonical image.
161pub const SECTION_COUNT: usize = 17;
162
163/// Ordinal of a section in the canonical image. Sections appear in this exact
164/// order, each starting at an 8-byte aligned offset with no gaps.
165#[derive(Debug, Clone, Copy, PartialEq, Eq)]
166pub enum Section {
167    Nodes,
168    Children,
169    Fields,
170    Parts,
171    Edges,
172    Strings,
173    Blobs,
174    Annotations,
175    Provenance,
176    Extensions,
177    Units,
178    Types,
179    Aux,
180    Numbers,
181    Secrets,
182    Hashes,
183    /// Module index (root id) and name bindings.
184    Bindings,
185}
186
187impl Section {
188    pub const ALL: [Section; SECTION_COUNT] = [
189        Section::Nodes,
190        Section::Children,
191        Section::Fields,
192        Section::Parts,
193        Section::Edges,
194        Section::Strings,
195        Section::Blobs,
196        Section::Annotations,
197        Section::Provenance,
198        Section::Extensions,
199        Section::Units,
200        Section::Types,
201        Section::Aux,
202        Section::Numbers,
203        Section::Secrets,
204        Section::Hashes,
205        Section::Bindings,
206    ];
207
208    pub const fn name(self) -> &'static str {
209        match self {
210            Section::Nodes => "nodes",
211            Section::Children => "children",
212            Section::Fields => "fields",
213            Section::Parts => "parts",
214            Section::Edges => "edges",
215            Section::Strings => "strings",
216            Section::Blobs => "blobs",
217            Section::Annotations => "annotations",
218            Section::Provenance => "provenance",
219            Section::Extensions => "extensions",
220            Section::Units => "units",
221            Section::Types => "types",
222            Section::Aux => "aux",
223            Section::Numbers => "numbers",
224            Section::Secrets => "secrets",
225            Section::Hashes => "hashes",
226            Section::Bindings => "bindings",
227        }
228    }
229}
230
231/// Round `value` up to the next multiple of 8.
232pub const fn align8(value: u64) -> u64 {
233    (value + 7) & !7
234}
235
236/// Canonical form of a float: `-0.0` becomes `+0.0`; every NaN becomes the
237/// single quiet NaN `0x7ff8_0000_0000_0000`.
238pub fn canonical_f64(value: f64) -> f64 {
239    if value.is_nan() {
240        f64::from_bits(CANONICAL_NAN_BITS)
241    } else if value == 0.0 {
242        0.0
243    } else {
244        value
245    }
246}
247
248/// The one NaN bit pattern the format permits.
249pub const CANONICAL_NAN_BITS: u64 = 0x7ff8_0000_0000_0000;
250
251/// Parsed fixed header.
252#[derive(Debug, Clone, Copy, PartialEq, Eq)]
253pub struct Header {
254    pub major: u16,
255    pub minor: u16,
256    pub flags: u16,
257    pub header_size: u16,
258    pub content_hash: [u8; 32],
259    pub dialect_hash: u64,
260    pub total_size: u64,
261    pub sections: [SectionSpan; SECTION_COUNT],
262}
263
264/// Offset and element count (or byte length) of one section.
265#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
266pub struct SectionSpan {
267    pub offset: u32,
268    pub count: u32,
269}
270
271impl SectionSpan {
272    pub fn is_empty(&self) -> bool {
273        self.count == 0
274    }
275
276    /// Byte length of the section body for a fixed-stride section.
277    pub fn bytes(&self, stride: usize) -> u64 {
278        self.count as u64 * stride as u64
279    }
280}
281
282impl Header {
283    pub fn span(&self, section: Section) -> SectionSpan {
284        self.sections[section as usize]
285    }
286
287    pub fn span_mut(&mut self, section: Section) -> &mut SectionSpan {
288        &mut self.sections[section as usize]
289    }
290
291    /// Parse and structurally check the fixed header.
292    ///
293    /// This checks the prefix, versions and flags only. Section ranges are
294    /// validated by [`crate::validate::validate`], which owns the full invariants.
295    pub fn parse(buf: &[u8]) -> Result<Header> {
296        if buf.len() < 4 || buf[0..4] != MAGIC {
297            return Err(Error::Magic);
298        }
299        if buf.len() < HEADER_SIZE {
300            return Err(Error::HeaderTruncated);
301        }
302        let major = u16::from_le_bytes([buf[4], buf[5]]);
303        let minor = u16::from_le_bytes([buf[6], buf[7]]);
304        let flags = u16::from_le_bytes([buf[8], buf[9]]);
305        let header_size = u16::from_le_bytes([buf[10], buf[11]]);
306        if major != FORMAT_MAJOR {
307            return Err(Error::VersionMajor(major));
308        }
309        if minor > FORMAT_MINOR {
310            return Err(Error::VersionMinor(minor));
311        }
312        if header_size as usize != HEADER_SIZE {
313            return Err(Error::HeaderSize(header_size as u8));
314        }
315        if flags & !FLAG_KNOWN_MASK != 0 {
316            return Err(Error::UnknownFlags(flags & !FLAG_KNOWN_MASK));
317        }
318        if read_u32(buf, 12)? != 0 {
319            return Err(Error::ReservedNonZero {
320                section: "header",
321                offset: 12,
322            });
323        }
324        let mut content_hash = [0u8; 32];
325        content_hash.copy_from_slice(&buf[16..48]);
326        let dialect_hash = read_u64(buf, 48)?;
327        let total_size = read_u64(buf, 56)?;
328        let mut sections = [SectionSpan::default(); SECTION_COUNT];
329        for (index, span) in sections.iter_mut().enumerate() {
330            let base = 64 + index * 8;
331            span.offset = read_u32(buf, base)?;
332            span.count = read_u32(buf, base + 4)?;
333        }
334        let reserved_start = 64 + SECTION_COUNT * 8;
335        if let Some(position) = (reserved_start..HEADER_SIZE).find(|offset| buf[*offset] != 0) {
336            return Err(Error::ReservedNonZero {
337                section: "header",
338                offset: position as u64,
339            });
340        }
341        Ok(Header {
342            major,
343            minor,
344            flags,
345            header_size,
346            content_hash,
347            dialect_hash,
348            total_size,
349            sections,
350        })
351    }
352
353    /// Serialize this header, returning an error when `out` is too short.
354    ///
355    /// Prefer this over [`Header::write`] in code that accepts a caller-owned
356    /// buffer: the failure is a value, not a panic.
357    pub fn try_write(&self, out: &mut [u8]) -> crate::Result<()> {
358        if out.len() < HEADER_SIZE {
359            return Err(crate::Error::Encode("header buffer is too short"));
360        }
361        self.write(out);
362        Ok(())
363    }
364
365    /// Serialize this header into the first [`HEADER_SIZE`] bytes of `out`.
366    ///
367    /// # Panics
368    ///
369    /// Panics when `out` is shorter than [`HEADER_SIZE`]. Use
370    /// [`Header::try_write`] for a fallible form.
371    pub fn write(&self, out: &mut [u8]) {
372        // Enforced in release too: a short output buffer is a caller bug, not a
373        // debug-only precondition. The audit in `tests/redteam_battery.rs`
374        // refuses any debug-only security branch.
375        assert!(out.len() >= HEADER_SIZE, "header buffer is too short");
376        out[0..4].copy_from_slice(&MAGIC);
377        out[4..6].copy_from_slice(&self.major.to_le_bytes());
378        out[6..8].copy_from_slice(&self.minor.to_le_bytes());
379        out[8..10].copy_from_slice(&self.flags.to_le_bytes());
380        out[10..12].copy_from_slice(&self.header_size.to_le_bytes());
381        out[12..16].copy_from_slice(&0u32.to_le_bytes());
382        out[16..48].copy_from_slice(&self.content_hash);
383        out[48..56].copy_from_slice(&self.dialect_hash.to_le_bytes());
384        out[56..64].copy_from_slice(&self.total_size.to_le_bytes());
385        for (index, span) in self.sections.iter().enumerate() {
386            let base = 64 + index * 8;
387            out[base..base + 4].copy_from_slice(&span.offset.to_le_bytes());
388            out[base + 4..base + 8].copy_from_slice(&span.count.to_le_bytes());
389        }
390    }
391}
392
393/// Decode limits applied to every untrusted buffer.
394///
395/// Defaults are generous for real modules (millions of nodes) and tight
396/// enough that a hostile 160-byte buffer cannot make a reader allocate
397/// gigabytes. Every field is a hard ceiling; `0` is rejected at construction
398/// (there is no unlimited mode).
399#[derive(Debug, Clone, Copy, PartialEq, Eq)]
400pub struct Limits {
401    pub max_nodes: u32,
402    pub max_edges: u32,
403    pub max_children: u32,
404    pub max_fields: u32,
405    pub max_parts: u32,
406    pub max_strings: u32,
407    pub max_string_bytes: u32,
408    pub max_blobs: u32,
409    pub max_blob_bytes: u32,
410    pub max_annotations: u32,
411    pub max_units: u32,
412    pub max_types: u32,
413    pub max_aux: u32,
414    pub max_numbers: u32,
415    pub max_secrets: u32,
416    pub max_secret_bytes: u32,
417    pub max_extensions: u32,
418    pub max_prov_entries: u32,
419    pub max_total_bytes: u64,
420    pub max_depth: u32,
421}
422
423impl Default for Limits {
424    fn default() -> Self {
425        Self {
426            max_nodes: 1 << 22,
427            max_edges: 1 << 24,
428            max_children: 1 << 24,
429            max_fields: 1 << 24,
430            max_parts: 1 << 22,
431            max_strings: 1 << 22,
432            max_string_bytes: 64 << 20,
433            max_blobs: 1 << 20,
434            max_blob_bytes: 512 << 20,
435            max_annotations: 1 << 22,
436            max_units: 1 << 16,
437            max_types: 1 << 20,
438            max_aux: 1 << 24,
439            max_numbers: 1 << 24,
440            max_secrets: 1 << 20,
441            max_secret_bytes: 16 << 20,
442            max_extensions: 1 << 16,
443            max_prov_entries: 1 << 16,
444            max_total_bytes: 1 << 30,
445            max_depth: 4096,
446        }
447    }
448}
449
450impl Limits {
451    /// A tight profile for tests and adversarial surfaces.
452    pub fn strict() -> Self {
453        Self {
454            max_nodes: 4096,
455            max_edges: 16384,
456            max_children: 16384,
457            max_fields: 16384,
458            max_parts: 4096,
459            max_strings: 4096,
460            max_string_bytes: 1 << 20,
461            max_blobs: 256,
462            max_blob_bytes: 1 << 20,
463            max_annotations: 4096,
464            max_units: 1024,
465            max_types: 1024,
466            max_aux: 16384,
467            max_numbers: 16384,
468            max_secrets: 256,
469            max_secret_bytes: 1 << 16,
470            max_extensions: 64,
471            max_prov_entries: 256,
472            max_total_bytes: 8 << 20,
473            max_depth: 128,
474        }
475    }
476
477    /// Reject a limits value that would disable every ceiling.
478    pub fn check(&self) -> Result<()> {
479        for (name, value) in [
480            ("max_nodes", self.max_nodes),
481            ("max_string_bytes", self.max_string_bytes),
482            ("max_blob_bytes", self.max_blob_bytes),
483            ("max_units", self.max_units),
484            ("max_depth", self.max_depth),
485        ] {
486            if value == 0 {
487                return Err(Error::InvalidArgument(match name {
488                    "max_nodes" => "max_nodes must be non-zero",
489                    "max_string_bytes" => "max_string_bytes must be non-zero",
490                    "max_blob_bytes" => "max_blob_bytes must be non-zero",
491                    "max_units" => "max_units must be non-zero",
492                    _ => "max_depth must be non-zero",
493                }));
494            }
495        }
496        Ok(())
497    }
498}
499
500/// Decode options.
501#[derive(Debug, Clone, Copy, PartialEq, Eq)]
502pub struct DecodeOpts {
503    pub limits: Limits,
504    /// Verify the stored content hash (always recommended; only tests that
505    /// intentionally mutate bytes turn it off).
506    pub verify_hash: bool,
507    /// Prove that the image is exactly the canonical encoding of its own
508    /// content. This is unconditional: an unflagged image fails, because the
509    /// canonical encoding of its content carries the canonical flag.
510    pub verify_canonical: bool,
511}
512
513impl Default for DecodeOpts {
514    fn default() -> Self {
515        Self {
516            limits: Limits::default(),
517            verify_hash: true,
518            verify_canonical: false,
519        }
520    }
521}
522
523impl DecodeOpts {
524    /// Everything on: the profile for untrusted network input.
525    pub fn paranoid() -> Self {
526        Self {
527            limits: Limits::strict(),
528            verify_hash: true,
529            verify_canonical: true,
530        }
531    }
532}
533
534/// Encode options.
535#[derive(Debug, Clone, Copy, PartialEq, Eq)]
536pub struct EncodeOpts {
537    /// Store the full 32-byte Blake3 content hash.
538    pub write_hash: bool,
539    /// Mark the image canonical (the encoder always writes canonical form).
540    pub mark_canonical: bool,
541    /// Preserve the `streamed` flag when re-encoding a reassembled module.
542    pub streamed: bool,
543}
544
545impl Default for EncodeOpts {
546    fn default() -> Self {
547        Self {
548            write_hash: true,
549            mark_canonical: true,
550            streamed: false,
551        }
552    }
553}
554
555/// Metadata describing a validated image, returned by `validate`.
556#[derive(Debug, Clone, Copy, PartialEq, Eq)]
557pub struct Meta {
558    pub flags: u16,
559    pub major: u16,
560    pub minor: u16,
561    pub dialect_hash: u64,
562    pub content_hash: [u8; 32],
563    pub total_size: u64,
564    pub nodes: u32,
565    pub edges: u32,
566    pub strings: u32,
567    pub string_bytes: u32,
568    pub blobs: u32,
569    pub blob_bytes: u32,
570    pub annotations: u32,
571    pub units: u32,
572    pub types: u32,
573    pub secrets: u32,
574    pub extensions: u32,
575}
576
577impl Meta {
578    pub fn has_annotations(&self) -> bool {
579        self.annotations > 0
580    }
581
582    pub fn has_provenance(&self) -> bool {
583        self.flags & FLAG_PROVENANCE != 0
584    }
585}
586
587/// Read a little-endian `u32`, mapping short input to a truncation error.
588pub fn read_u32(buf: &[u8], offset: usize) -> Result<u32> {
589    let end = offset + 4;
590    if end > buf.len() {
591        return Err(Error::Truncated {
592            needed: end as u64,
593            found: buf.len() as u64,
594        });
595    }
596    Ok(u32::from_le_bytes([
597        buf[offset],
598        buf[offset + 1],
599        buf[offset + 2],
600        buf[offset + 3],
601    ]))
602}
603
604/// Read a little-endian `u64`, mapping short input to a truncation error.
605pub fn read_u64(buf: &[u8], offset: usize) -> Result<u64> {
606    let end = offset + 8;
607    if end > buf.len() {
608        return Err(Error::Truncated {
609            needed: end as u64,
610            found: buf.len() as u64,
611        });
612    }
613    let mut bytes = [0u8; 8];
614    bytes.copy_from_slice(&buf[offset..end]);
615    Ok(u64::from_le_bytes(bytes))
616}
617
618/// Write helpers used by the encoder.
619pub fn put_u16(out: &mut Vec<u8>, value: u16) {
620    out.extend_from_slice(&value.to_le_bytes());
621}
622
623pub fn put_u32(out: &mut Vec<u8>, value: u32) {
624    out.extend_from_slice(&value.to_le_bytes());
625}
626
627pub fn put_u64(out: &mut Vec<u8>, value: u64) {
628    out.extend_from_slice(&value.to_le_bytes());
629}
630
631/// Pad `out` with zero bytes to the next 8-byte boundary.
632pub fn pad8(out: &mut Vec<u8>) {
633    while out.len() % 8 != 0 {
634        out.push(0);
635    }
636}
637
638/// The dialect this build implements: a hash over the format version and the
639/// semantic tables (unit discriminants, node kinds, aux tags). Stored in every
640/// header so a peer can refuse a module produced by an incompatible build
641/// before parsing a single record.
642pub fn dialect_hash() -> u64 {
643    let mut hasher = blake3::Hasher::new();
644    hasher.update(b"active-infinity/dialect");
645    hasher.update(&FORMAT_MAJOR.to_le_bytes());
646    hasher.update(&FORMAT_MINOR.to_le_bytes());
647    hasher.update(&crate::unit::UNIT_MAX_BUILTIN.to_le_bytes());
648    hasher.update(b"node-kinds:0..=24");
649    hasher.update(b"aux-tags:0,1,2,3,4,10,11,12,20,21,22,23,24,25,26,30,31");
650    let digest = hasher.finalize();
651    let mut bytes = [0u8; 8];
652    bytes.copy_from_slice(&digest.as_bytes()[..8]);
653    u64::from_le_bytes(bytes)
654}
655
656#[cfg(test)]
657mod tests {
658    use super::*;
659
660    #[test]
661    fn header_round_trips() {
662        let mut header = Header {
663            major: FORMAT_MAJOR,
664            minor: FORMAT_MINOR,
665            flags: FLAG_CANONICAL,
666            header_size: HEADER_SIZE as u16,
667            content_hash: [7u8; 32],
668            dialect_hash: dialect_hash(),
669            total_size: 512,
670            sections: [SectionSpan::default(); SECTION_COUNT],
671        };
672        header.span_mut(Section::Nodes).offset = 192;
673        header.span_mut(Section::Nodes).count = 3;
674        let mut bytes = vec![0u8; HEADER_SIZE];
675        header.write(&mut bytes);
676        let parsed = Header::parse(&bytes).expect("parse");
677        assert_eq!(parsed, header);
678    }
679
680    #[test]
681    fn rejects_short_and_misversioned_input() {
682        assert_eq!(Header::parse(b"AI"), Err(Error::Magic));
683        assert_eq!(Header::parse(b"XXXX").unwrap_err(), Error::Magic);
684        let mut bytes = vec![0u8; HEADER_SIZE];
685        bytes[0..4].copy_from_slice(&MAGIC);
686        // major defaults to 0 in the zeroed buffer
687        assert!(matches!(Header::parse(&bytes), Err(Error::VersionMajor(0))));
688        bytes[4..6].copy_from_slice(&FORMAT_MAJOR.to_le_bytes());
689        // A zeroed header-size field is refused before any section is read.
690        assert_eq!(Header::parse(&bytes).unwrap_err(), Error::HeaderSize(0));
691        bytes[10..12].copy_from_slice(&(HEADER_SIZE as u16).to_le_bytes());
692        assert!(Header::parse(&bytes).is_ok());
693    }
694
695    #[test]
696    fn alignment_and_rounding() {
697        assert_eq!(align8(0), 0);
698        assert_eq!(align8(1), 8);
699        assert_eq!(align8(8), 8);
700        assert_eq!(align8(9), 16);
701    }
702
703    #[test]
704    fn floats_canonicalize() {
705        assert_eq!(canonical_f64(-0.0).to_bits(), 0.0f64.to_bits());
706        assert_eq!(canonical_f64(f64::NAN).to_bits(), CANONICAL_NAN_BITS);
707        assert_eq!(canonical_f64(1.5), 1.5);
708    }
709
710    #[test]
711    fn dialect_hash_is_stable() {
712        assert_eq!(dialect_hash(), dialect_hash());
713        assert_ne!(dialect_hash(), 0);
714    }
715}