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 into the first [`HEADER_SIZE`] bytes of `out`.
354    pub fn write(&self, out: &mut [u8]) {
355        debug_assert!(out.len() >= HEADER_SIZE);
356        out[0..4].copy_from_slice(&MAGIC);
357        out[4..6].copy_from_slice(&self.major.to_le_bytes());
358        out[6..8].copy_from_slice(&self.minor.to_le_bytes());
359        out[8..10].copy_from_slice(&self.flags.to_le_bytes());
360        out[10..12].copy_from_slice(&self.header_size.to_le_bytes());
361        out[12..16].copy_from_slice(&0u32.to_le_bytes());
362        out[16..48].copy_from_slice(&self.content_hash);
363        out[48..56].copy_from_slice(&self.dialect_hash.to_le_bytes());
364        out[56..64].copy_from_slice(&self.total_size.to_le_bytes());
365        for (index, span) in self.sections.iter().enumerate() {
366            let base = 64 + index * 8;
367            out[base..base + 4].copy_from_slice(&span.offset.to_le_bytes());
368            out[base + 4..base + 8].copy_from_slice(&span.count.to_le_bytes());
369        }
370    }
371}
372
373/// Decode limits applied to every untrusted buffer.
374///
375/// Defaults are generous for real modules (millions of nodes) and tight
376/// enough that a hostile 160-byte buffer cannot make a reader allocate
377/// gigabytes. Every field is a hard ceiling; `0` is rejected at construction
378/// (there is no unlimited mode).
379#[derive(Debug, Clone, Copy, PartialEq, Eq)]
380pub struct Limits {
381    pub max_nodes: u32,
382    pub max_edges: u32,
383    pub max_children: u32,
384    pub max_fields: u32,
385    pub max_parts: u32,
386    pub max_strings: u32,
387    pub max_string_bytes: u32,
388    pub max_blobs: u32,
389    pub max_blob_bytes: u32,
390    pub max_annotations: u32,
391    pub max_units: u32,
392    pub max_types: u32,
393    pub max_aux: u32,
394    pub max_numbers: u32,
395    pub max_secrets: u32,
396    pub max_secret_bytes: u32,
397    pub max_extensions: u32,
398    pub max_prov_entries: u32,
399    pub max_total_bytes: u64,
400    pub max_depth: u32,
401}
402
403impl Default for Limits {
404    fn default() -> Self {
405        Self {
406            max_nodes: 1 << 22,
407            max_edges: 1 << 24,
408            max_children: 1 << 24,
409            max_fields: 1 << 24,
410            max_parts: 1 << 22,
411            max_strings: 1 << 22,
412            max_string_bytes: 64 << 20,
413            max_blobs: 1 << 20,
414            max_blob_bytes: 512 << 20,
415            max_annotations: 1 << 22,
416            max_units: 1 << 16,
417            max_types: 1 << 20,
418            max_aux: 1 << 24,
419            max_numbers: 1 << 24,
420            max_secrets: 1 << 20,
421            max_secret_bytes: 16 << 20,
422            max_extensions: 1 << 16,
423            max_prov_entries: 1 << 16,
424            max_total_bytes: 1 << 30,
425            max_depth: 4096,
426        }
427    }
428}
429
430impl Limits {
431    /// A tight profile for tests and adversarial surfaces.
432    pub fn strict() -> Self {
433        Self {
434            max_nodes: 4096,
435            max_edges: 16384,
436            max_children: 16384,
437            max_fields: 16384,
438            max_parts: 4096,
439            max_strings: 4096,
440            max_string_bytes: 1 << 20,
441            max_blobs: 256,
442            max_blob_bytes: 1 << 20,
443            max_annotations: 4096,
444            max_units: 1024,
445            max_types: 1024,
446            max_aux: 16384,
447            max_numbers: 16384,
448            max_secrets: 256,
449            max_secret_bytes: 1 << 16,
450            max_extensions: 64,
451            max_prov_entries: 256,
452            max_total_bytes: 8 << 20,
453            max_depth: 128,
454        }
455    }
456
457    /// Reject a limits value that would disable every ceiling.
458    pub fn check(&self) -> Result<()> {
459        for (name, value) in [
460            ("max_nodes", self.max_nodes),
461            ("max_string_bytes", self.max_string_bytes),
462            ("max_blob_bytes", self.max_blob_bytes),
463            ("max_units", self.max_units),
464            ("max_depth", self.max_depth),
465        ] {
466            if value == 0 {
467                return Err(Error::InvalidArgument(match name {
468                    "max_nodes" => "max_nodes must be non-zero",
469                    "max_string_bytes" => "max_string_bytes must be non-zero",
470                    "max_blob_bytes" => "max_blob_bytes must be non-zero",
471                    "max_units" => "max_units must be non-zero",
472                    _ => "max_depth must be non-zero",
473                }));
474            }
475        }
476        Ok(())
477    }
478}
479
480/// Decode options.
481#[derive(Debug, Clone, Copy, PartialEq, Eq)]
482pub struct DecodeOpts {
483    pub limits: Limits,
484    /// Verify the stored content hash (always recommended; only tests that
485    /// intentionally mutate bytes turn it off).
486    pub verify_hash: bool,
487    /// Prove that the image is exactly the canonical encoding of its own
488    /// content. This is unconditional: an unflagged image fails, because the
489    /// canonical encoding of its content carries the canonical flag.
490    pub verify_canonical: bool,
491}
492
493impl Default for DecodeOpts {
494    fn default() -> Self {
495        Self {
496            limits: Limits::default(),
497            verify_hash: true,
498            verify_canonical: false,
499        }
500    }
501}
502
503impl DecodeOpts {
504    /// Everything on: the profile for untrusted network input.
505    pub fn paranoid() -> Self {
506        Self {
507            limits: Limits::strict(),
508            verify_hash: true,
509            verify_canonical: true,
510        }
511    }
512}
513
514/// Encode options.
515#[derive(Debug, Clone, Copy, PartialEq, Eq)]
516pub struct EncodeOpts {
517    /// Store the full 32-byte Blake3 content hash.
518    pub write_hash: bool,
519    /// Mark the image canonical (the encoder always writes canonical form).
520    pub mark_canonical: bool,
521    /// Preserve the `streamed` flag when re-encoding a reassembled module.
522    pub streamed: bool,
523}
524
525impl Default for EncodeOpts {
526    fn default() -> Self {
527        Self {
528            write_hash: true,
529            mark_canonical: true,
530            streamed: false,
531        }
532    }
533}
534
535/// Metadata describing a validated image, returned by `validate`.
536#[derive(Debug, Clone, Copy, PartialEq, Eq)]
537pub struct Meta {
538    pub flags: u16,
539    pub major: u16,
540    pub minor: u16,
541    pub dialect_hash: u64,
542    pub content_hash: [u8; 32],
543    pub total_size: u64,
544    pub nodes: u32,
545    pub edges: u32,
546    pub strings: u32,
547    pub string_bytes: u32,
548    pub blobs: u32,
549    pub blob_bytes: u32,
550    pub annotations: u32,
551    pub units: u32,
552    pub types: u32,
553    pub secrets: u32,
554    pub extensions: u32,
555}
556
557impl Meta {
558    pub fn has_annotations(&self) -> bool {
559        self.annotations > 0
560    }
561
562    pub fn has_provenance(&self) -> bool {
563        self.flags & FLAG_PROVENANCE != 0
564    }
565}
566
567/// Read a little-endian `u32`, mapping short input to a truncation error.
568pub fn read_u32(buf: &[u8], offset: usize) -> Result<u32> {
569    let end = offset + 4;
570    if end > buf.len() {
571        return Err(Error::Truncated {
572            needed: end as u64,
573            found: buf.len() as u64,
574        });
575    }
576    Ok(u32::from_le_bytes([
577        buf[offset],
578        buf[offset + 1],
579        buf[offset + 2],
580        buf[offset + 3],
581    ]))
582}
583
584/// Read a little-endian `u64`, mapping short input to a truncation error.
585pub fn read_u64(buf: &[u8], offset: usize) -> Result<u64> {
586    let end = offset + 8;
587    if end > buf.len() {
588        return Err(Error::Truncated {
589            needed: end as u64,
590            found: buf.len() as u64,
591        });
592    }
593    let mut bytes = [0u8; 8];
594    bytes.copy_from_slice(&buf[offset..end]);
595    Ok(u64::from_le_bytes(bytes))
596}
597
598/// Write helpers used by the encoder.
599pub fn put_u16(out: &mut Vec<u8>, value: u16) {
600    out.extend_from_slice(&value.to_le_bytes());
601}
602
603pub fn put_u32(out: &mut Vec<u8>, value: u32) {
604    out.extend_from_slice(&value.to_le_bytes());
605}
606
607pub fn put_u64(out: &mut Vec<u8>, value: u64) {
608    out.extend_from_slice(&value.to_le_bytes());
609}
610
611/// Pad `out` with zero bytes to the next 8-byte boundary.
612pub fn pad8(out: &mut Vec<u8>) {
613    while out.len() % 8 != 0 {
614        out.push(0);
615    }
616}
617
618/// The dialect this build implements: a hash over the format version and the
619/// semantic tables (unit discriminants, node kinds, aux tags). Stored in every
620/// header so a peer can refuse a module produced by an incompatible build
621/// before parsing a single record.
622pub fn dialect_hash() -> u64 {
623    let mut hasher = blake3::Hasher::new();
624    hasher.update(b"active-infinity/dialect");
625    hasher.update(&FORMAT_MAJOR.to_le_bytes());
626    hasher.update(&FORMAT_MINOR.to_le_bytes());
627    hasher.update(&crate::unit::UNIT_MAX_BUILTIN.to_le_bytes());
628    hasher.update(b"node-kinds:0..=24");
629    hasher.update(b"aux-tags:0,1,2,3,4,10,11,12,20,21,22,23,24,25,26,30,31");
630    let digest = hasher.finalize();
631    let mut bytes = [0u8; 8];
632    bytes.copy_from_slice(&digest.as_bytes()[..8]);
633    u64::from_le_bytes(bytes)
634}
635
636#[cfg(test)]
637mod tests {
638    use super::*;
639
640    #[test]
641    fn header_round_trips() {
642        let mut header = Header {
643            major: FORMAT_MAJOR,
644            minor: FORMAT_MINOR,
645            flags: FLAG_CANONICAL,
646            header_size: HEADER_SIZE as u16,
647            content_hash: [7u8; 32],
648            dialect_hash: dialect_hash(),
649            total_size: 512,
650            sections: [SectionSpan::default(); SECTION_COUNT],
651        };
652        header.span_mut(Section::Nodes).offset = 192;
653        header.span_mut(Section::Nodes).count = 3;
654        let mut bytes = vec![0u8; HEADER_SIZE];
655        header.write(&mut bytes);
656        let parsed = Header::parse(&bytes).expect("parse");
657        assert_eq!(parsed, header);
658    }
659
660    #[test]
661    fn rejects_short_and_misversioned_input() {
662        assert_eq!(Header::parse(b"AI"), Err(Error::Magic));
663        assert_eq!(Header::parse(b"XXXX").unwrap_err(), Error::Magic);
664        let mut bytes = vec![0u8; HEADER_SIZE];
665        bytes[0..4].copy_from_slice(&MAGIC);
666        // major defaults to 0 in the zeroed buffer
667        assert!(matches!(Header::parse(&bytes), Err(Error::VersionMajor(0))));
668        bytes[4..6].copy_from_slice(&FORMAT_MAJOR.to_le_bytes());
669        // A zeroed header-size field is refused before any section is read.
670        assert_eq!(Header::parse(&bytes).unwrap_err(), Error::HeaderSize(0));
671        bytes[10..12].copy_from_slice(&(HEADER_SIZE as u16).to_le_bytes());
672        assert!(Header::parse(&bytes).is_ok());
673    }
674
675    #[test]
676    fn alignment_and_rounding() {
677        assert_eq!(align8(0), 0);
678        assert_eq!(align8(1), 8);
679        assert_eq!(align8(8), 8);
680        assert_eq!(align8(9), 16);
681    }
682
683    #[test]
684    fn floats_canonicalize() {
685        assert_eq!(canonical_f64(-0.0).to_bits(), 0.0f64.to_bits());
686        assert_eq!(canonical_f64(f64::NAN).to_bits(), CANONICAL_NAN_BITS);
687        assert_eq!(canonical_f64(1.5), 1.5);
688    }
689
690    #[test]
691    fn dialect_hash_is_stable() {
692        assert_eq!(dialect_hash(), dialect_hash());
693        assert_ne!(dialect_hash(), 0);
694    }
695}