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    /// When the header declares canonical form, verify re-encoding equality.
488    pub verify_canonical: bool,
489}
490
491impl Default for DecodeOpts {
492    fn default() -> Self {
493        Self {
494            limits: Limits::default(),
495            verify_hash: true,
496            verify_canonical: false,
497        }
498    }
499}
500
501impl DecodeOpts {
502    /// Everything on: the profile for untrusted network input.
503    pub fn paranoid() -> Self {
504        Self {
505            limits: Limits::strict(),
506            verify_hash: true,
507            verify_canonical: true,
508        }
509    }
510}
511
512/// Encode options.
513#[derive(Debug, Clone, Copy, PartialEq, Eq)]
514pub struct EncodeOpts {
515    /// Store the full 32-byte Blake3 content hash.
516    pub write_hash: bool,
517    /// Mark the image canonical (the encoder always writes canonical form).
518    pub mark_canonical: bool,
519    /// Preserve the `streamed` flag when re-encoding a reassembled module.
520    pub streamed: bool,
521}
522
523impl Default for EncodeOpts {
524    fn default() -> Self {
525        Self {
526            write_hash: true,
527            mark_canonical: true,
528            streamed: false,
529        }
530    }
531}
532
533/// Metadata describing a validated image, returned by `validate`.
534#[derive(Debug, Clone, Copy, PartialEq, Eq)]
535pub struct Meta {
536    pub flags: u16,
537    pub major: u16,
538    pub minor: u16,
539    pub dialect_hash: u64,
540    pub content_hash: [u8; 32],
541    pub total_size: u64,
542    pub nodes: u32,
543    pub edges: u32,
544    pub strings: u32,
545    pub string_bytes: u32,
546    pub blobs: u32,
547    pub blob_bytes: u32,
548    pub annotations: u32,
549    pub units: u32,
550    pub types: u32,
551    pub secrets: u32,
552    pub extensions: u32,
553}
554
555impl Meta {
556    pub fn has_annotations(&self) -> bool {
557        self.annotations > 0
558    }
559
560    pub fn has_provenance(&self) -> bool {
561        self.flags & FLAG_PROVENANCE != 0
562    }
563}
564
565/// Read a little-endian `u32`, mapping short input to a truncation error.
566pub fn read_u32(buf: &[u8], offset: usize) -> Result<u32> {
567    let end = offset + 4;
568    if end > buf.len() {
569        return Err(Error::Truncated {
570            needed: end as u64,
571            found: buf.len() as u64,
572        });
573    }
574    Ok(u32::from_le_bytes([
575        buf[offset],
576        buf[offset + 1],
577        buf[offset + 2],
578        buf[offset + 3],
579    ]))
580}
581
582/// Read a little-endian `u64`, mapping short input to a truncation error.
583pub fn read_u64(buf: &[u8], offset: usize) -> Result<u64> {
584    let end = offset + 8;
585    if end > buf.len() {
586        return Err(Error::Truncated {
587            needed: end as u64,
588            found: buf.len() as u64,
589        });
590    }
591    let mut bytes = [0u8; 8];
592    bytes.copy_from_slice(&buf[offset..end]);
593    Ok(u64::from_le_bytes(bytes))
594}
595
596/// Write helpers used by the encoder.
597pub fn put_u16(out: &mut Vec<u8>, value: u16) {
598    out.extend_from_slice(&value.to_le_bytes());
599}
600
601pub fn put_u32(out: &mut Vec<u8>, value: u32) {
602    out.extend_from_slice(&value.to_le_bytes());
603}
604
605pub fn put_u64(out: &mut Vec<u8>, value: u64) {
606    out.extend_from_slice(&value.to_le_bytes());
607}
608
609/// Pad `out` with zero bytes to the next 8-byte boundary.
610pub fn pad8(out: &mut Vec<u8>) {
611    while out.len() % 8 != 0 {
612        out.push(0);
613    }
614}
615
616/// The dialect this build implements: a hash over the format version and the
617/// semantic tables (unit discriminants, node kinds, aux tags). Stored in every
618/// header so a peer can refuse a module produced by an incompatible build
619/// before parsing a single record.
620pub fn dialect_hash() -> u64 {
621    let mut hasher = blake3::Hasher::new();
622    hasher.update(b"active-infinity/dialect");
623    hasher.update(&FORMAT_MAJOR.to_le_bytes());
624    hasher.update(&FORMAT_MINOR.to_le_bytes());
625    hasher.update(&crate::unit::UNIT_MAX_BUILTIN.to_le_bytes());
626    hasher.update(b"node-kinds:0..=24");
627    hasher.update(b"aux-tags:0,1,2,3,4,10,11,12,20,21,22,23,24,25,26,30,31");
628    let digest = hasher.finalize();
629    let mut bytes = [0u8; 8];
630    bytes.copy_from_slice(&digest.as_bytes()[..8]);
631    u64::from_le_bytes(bytes)
632}
633
634#[cfg(test)]
635mod tests {
636    use super::*;
637
638    #[test]
639    fn header_round_trips() {
640        let mut header = Header {
641            major: FORMAT_MAJOR,
642            minor: FORMAT_MINOR,
643            flags: FLAG_CANONICAL,
644            header_size: HEADER_SIZE as u16,
645            content_hash: [7u8; 32],
646            dialect_hash: dialect_hash(),
647            total_size: 512,
648            sections: [SectionSpan::default(); SECTION_COUNT],
649        };
650        header.span_mut(Section::Nodes).offset = 192;
651        header.span_mut(Section::Nodes).count = 3;
652        let mut bytes = vec![0u8; HEADER_SIZE];
653        header.write(&mut bytes);
654        let parsed = Header::parse(&bytes).expect("parse");
655        assert_eq!(parsed, header);
656    }
657
658    #[test]
659    fn rejects_short_and_misversioned_input() {
660        assert_eq!(Header::parse(b"AI"), Err(Error::Magic));
661        assert_eq!(Header::parse(b"XXXX").unwrap_err(), Error::Magic);
662        let mut bytes = vec![0u8; HEADER_SIZE];
663        bytes[0..4].copy_from_slice(&MAGIC);
664        // major defaults to 0 in the zeroed buffer
665        assert!(matches!(Header::parse(&bytes), Err(Error::VersionMajor(0))));
666        bytes[4..6].copy_from_slice(&FORMAT_MAJOR.to_le_bytes());
667        // A zeroed header-size field is refused before any section is read.
668        assert_eq!(Header::parse(&bytes).unwrap_err(), Error::HeaderSize(0));
669        bytes[10..12].copy_from_slice(&(HEADER_SIZE as u16).to_le_bytes());
670        assert!(Header::parse(&bytes).is_ok());
671    }
672
673    #[test]
674    fn alignment_and_rounding() {
675        assert_eq!(align8(0), 0);
676        assert_eq!(align8(1), 8);
677        assert_eq!(align8(8), 8);
678        assert_eq!(align8(9), 16);
679    }
680
681    #[test]
682    fn floats_canonicalize() {
683        assert_eq!(canonical_f64(-0.0).to_bits(), 0.0f64.to_bits());
684        assert_eq!(canonical_f64(f64::NAN).to_bits(), CANONICAL_NAN_BITS);
685        assert_eq!(canonical_f64(1.5), 1.5);
686    }
687
688    #[test]
689    fn dialect_hash_is_stable() {
690        assert_eq!(dialect_hash(), dialect_hash());
691        assert_ne!(dialect_hash(), 0);
692    }
693}