Skip to main content

ox_mf2_parser/snapshot/
format.rs

1// @license MIT
2// @author kazuya kawaguchi (a.k.a. kazupon)
3
4//! Binary AST snapshot wire format constants and little-endian helpers.
5//!
6//! This module pins the v0.1 format contract: magic bytes, version, fixed
7//! record sizes, section kinds, alignment, and the canonical none sentinel
8//! values. Wire encoding helpers are intentionally tiny and explicit so the
9//! format never accidentally adopts Rust struct layout, padding, or
10//! platform alignment.
11
12use core::convert::TryFrom;
13
14/// Magic prefix written at offset 0 of every v0.1 snapshot buffer.
15pub const SNAPSHOT_MAGIC: [u8; 8] = *b"OXMF2AST";
16
17/// v0.1 major version.
18pub const SNAPSHOT_MAJOR_VERSION: u16 = 0;
19
20/// v0.1 minor version. v0.x uses exact version matching, so this is the
21/// only minor accepted by the v0.1 decoder.
22pub const SNAPSHOT_MINOR_VERSION: u16 = 1;
23
24/// v0.1 `feature_flags` value — fully reserved, only `0` is allowed.
25pub const SNAPSHOT_FEATURE_FLAGS: u32 = 0;
26
27/// Wire size of [`crate::snapshot`] `SnapshotHeader`, in bytes.
28pub const HEADER_SIZE: u32 = 32;
29
30/// Wire size of one `SectionRecord` entry, in bytes.
31pub const SECTION_RECORD_SIZE: u32 = 20;
32
33/// Wire size of one `RootRecord`, in bytes.
34pub const ROOT_RECORD_SIZE: u32 = 16;
35
36/// Wire size of one `StringOffsetRecord`, in bytes.
37pub const STRING_OFFSET_RECORD_SIZE: u32 = 8;
38
39/// Wire size of one `SourceRecord`, in bytes.
40pub const SOURCE_RECORD_SIZE: u32 = 32;
41
42/// Wire size of one `NodeRecord`, in bytes.
43pub const NODE_RECORD_SIZE: u32 = 24;
44
45/// Wire size of one `EdgeRecord`, in bytes.
46pub const EDGE_RECORD_SIZE: u32 = 8;
47
48/// Wire size of one `TokenRecord`, in bytes.
49pub const TOKEN_RECORD_SIZE: u32 = 36;
50
51/// Wire size of one `TriviaRecord`, in bytes.
52pub const TRIVIA_RECORD_SIZE: u32 = 16;
53
54/// Wire size of one `DiagnosticRecord`, in bytes.
55pub const DIAGNOSTIC_RECORD_SIZE: u32 = 28;
56
57/// Wire size of one `DiagnosticLabelRecord`, in bytes.
58pub const DIAGNOSTIC_LABEL_RECORD_SIZE: u32 = 16;
59
60/// Wire size of one `ExtendedDataHeader`, in bytes.
61pub const EXTENDED_DATA_HEADER_SIZE: u32 = 8;
62
63/// Section alignment in bytes. Every emitted section starts at an offset
64/// that is a multiple of this constant.
65pub const SECTION_ALIGNMENT: u32 = 8;
66
67/// Sentinel value for optional `u32` references in the snapshot wire
68/// format. Required `RootId`/`NodeId`/`TokenId`/`TriviaId`/`SourceId`
69/// indexes never use this value.
70pub const NONE_REF: u32 = u32::MAX;
71
72/// `SectionRecord.flags` bit indicating that this section is required
73/// for the snapshot to be interpretable.
74pub const SECTION_FLAG_REQUIRED: u16 = 1;
75
76/// Edge kind: child reference points at a `NodeRecord`.
77pub const EDGE_KIND_NODE: u16 = 0;
78/// Edge kind: child reference points at a `TokenRecord`.
79pub const EDGE_KIND_TOKEN: u16 = 1;
80
81/// Stable numeric `SectionKind` identifiers.
82///
83/// Numbers are part of the v0.1 wire contract. Once assigned, a kind
84/// number is not reused. Changing the meaning of a section incompatibly
85/// requires a major version bump.
86#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
87#[repr(u16)]
88pub enum SectionKind {
89    Roots = 1,
90    Sources = 2,
91    Nodes = 3,
92    Edges = 4,
93    Tokens = 5,
94    Trivia = 6,
95    Diagnostics = 7,
96    DiagnosticLabels = 8,
97    StringOffsets = 9,
98    StringData = 10,
99    SourceTextData = 11,
100    ExtendedData = 12,
101}
102
103impl SectionKind {
104    /// Numeric wire value.
105    #[inline]
106    pub const fn as_u16(self) -> u16 {
107        self as u16
108    }
109
110    /// Returns the matching `SectionKind` for a wire value, or `None`
111    /// when the value is `0` (reserved) or otherwise unknown to v0.1.
112    pub const fn from_u16(value: u16) -> Option<Self> {
113        Some(match value {
114            1 => Self::Roots,
115            2 => Self::Sources,
116            3 => Self::Nodes,
117            4 => Self::Edges,
118            5 => Self::Tokens,
119            6 => Self::Trivia,
120            7 => Self::Diagnostics,
121            8 => Self::DiagnosticLabels,
122            9 => Self::StringOffsets,
123            10 => Self::StringData,
124            11 => Self::SourceTextData,
125            12 => Self::ExtendedData,
126            _ => return None,
127        })
128    }
129
130    /// `true` for sections that must always be present in a v0.1
131    /// snapshot with `SectionFlags.required = true`.
132    pub const fn is_core(self) -> bool {
133        matches!(
134            self,
135            Self::Roots
136                | Self::Sources
137                | Self::Nodes
138                | Self::Edges
139                | Self::Tokens
140                | Self::StringOffsets
141                | Self::StringData
142        )
143    }
144
145    /// Wire `record_size` for fixed-record sections. Raw byte sections
146    /// (`StringData`, `SourceTextData`, `ExtendedData`) return `0`.
147    pub const fn record_size(self) -> u16 {
148        match self {
149            Self::Roots => ROOT_RECORD_SIZE as u16,
150            Self::Sources => SOURCE_RECORD_SIZE as u16,
151            Self::Nodes => NODE_RECORD_SIZE as u16,
152            Self::Edges => EDGE_RECORD_SIZE as u16,
153            Self::Tokens => TOKEN_RECORD_SIZE as u16,
154            Self::Trivia => TRIVIA_RECORD_SIZE as u16,
155            Self::Diagnostics => DIAGNOSTIC_RECORD_SIZE as u16,
156            Self::DiagnosticLabels => DIAGNOSTIC_LABEL_RECORD_SIZE as u16,
157            Self::StringOffsets => STRING_OFFSET_RECORD_SIZE as u16,
158            Self::StringData | Self::SourceTextData | Self::ExtendedData => 0,
159        }
160    }
161
162    /// Stable emission order for the v0.1 writer. The decoder reads
163    /// sections through the section table and does not require this
164    /// physical order, but the writer emits them in this order so that
165    /// binary fixtures and byte-level diffs stay reviewable.
166    pub const ALL_IN_ORDER: [Self; 12] = [
167        Self::Roots,
168        Self::Sources,
169        Self::Nodes,
170        Self::Edges,
171        Self::Tokens,
172        Self::Trivia,
173        Self::Diagnostics,
174        Self::DiagnosticLabels,
175        Self::StringOffsets,
176        Self::StringData,
177        Self::SourceTextData,
178        Self::ExtendedData,
179    ];
180}
181
182/// Snapshot-local root identifier. Indexes into the roots section.
183#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
184#[repr(transparent)]
185pub struct RootId(pub u32);
186
187impl RootId {
188    /// Construct a `RootId` from a raw wire value.
189    #[inline]
190    pub const fn new(raw: u32) -> Self {
191        Self(raw)
192    }
193
194    /// Raw wire value.
195    #[inline]
196    pub const fn raw(self) -> u32 {
197        self.0
198    }
199
200    /// `usize` index.
201    #[inline]
202    pub const fn index(self) -> usize {
203        self.0 as usize
204    }
205}
206
207impl From<u32> for RootId {
208    #[inline]
209    fn from(value: u32) -> Self {
210        Self(value)
211    }
212}
213
214impl From<RootId> for u32 {
215    #[inline]
216    fn from(value: RootId) -> Self {
217        value.0
218    }
219}
220
221/// Snapshot-local string identifier. Indexes into the string offsets
222/// section. `0xFFFF_FFFF` is the optional-reference sentinel and must
223/// not be dereferenced.
224#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
225#[repr(transparent)]
226pub struct StringId(pub u32);
227
228impl StringId {
229    /// `0xFFFF_FFFF` — optional string is absent.
230    pub const NONE: Self = Self(NONE_REF);
231
232    #[inline]
233    pub const fn new(raw: u32) -> Self {
234        Self(raw)
235    }
236
237    #[inline]
238    pub const fn raw(self) -> u32 {
239        self.0
240    }
241
242    #[inline]
243    pub const fn is_none(self) -> bool {
244        self.0 == NONE_REF
245    }
246}
247
248// ── Little-endian read / write helpers ──────────────────────────────
249//
250// Every wire field is encoded with explicit little-endian operations.
251// Helpers panic only on programmer error (slice shorter than the field
252// width) and never read uninitialised memory. Decoders pre-check the
253// slice length, so the panic paths are unreachable from validated
254// input.
255
256#[inline]
257pub(crate) fn write_u8(buf: &mut Vec<u8>, value: u8) {
258    buf.push(value);
259}
260
261#[inline]
262pub(crate) fn write_u16_le(buf: &mut Vec<u8>, value: u16) {
263    buf.extend_from_slice(&value.to_le_bytes());
264}
265
266#[inline]
267pub(crate) fn write_u32_le(buf: &mut Vec<u8>, value: u32) {
268    buf.extend_from_slice(&value.to_le_bytes());
269}
270
271#[inline]
272pub(crate) fn read_u16_le(bytes: &[u8], offset: usize) -> u16 {
273    let arr: [u8; 2] = bytes[offset..offset + 2].try_into().unwrap();
274    u16::from_le_bytes(arr)
275}
276
277#[inline]
278pub(crate) fn read_u32_le(bytes: &[u8], offset: usize) -> u32 {
279    let arr: [u8; 4] = bytes[offset..offset + 4].try_into().unwrap();
280    u32::from_le_bytes(arr)
281}
282
283/// Round `offset` up to the next multiple of `SECTION_ALIGNMENT`.
284/// Returns `None` on `u32` overflow so callers can surface
285/// `SectionTooLarge` instead of panicking on `usize` truncation.
286#[inline]
287pub(crate) fn align_up(offset: u32) -> Option<u32> {
288    let mask = SECTION_ALIGNMENT - 1;
289    let extra = offset & mask;
290    if extra == 0 {
291        Some(offset)
292    } else {
293        let padding = SECTION_ALIGNMENT - extra;
294        offset.checked_add(padding)
295    }
296}
297
298/// Checked `usize` -> `u32` conversion that returns `None` instead of
299/// truncating. v0.1 writer surfaces this as the matching `TooMany*` /
300/// `SectionTooLarge` error code.
301#[inline]
302pub(crate) fn checked_u32(value: usize) -> Option<u32> {
303    u32::try_from(value).ok()
304}
305
306#[cfg(test)]
307mod tests {
308    use super::*;
309
310    #[test]
311    fn record_sizes_match_design_table() {
312        assert_eq!(HEADER_SIZE, 32);
313        assert_eq!(SECTION_RECORD_SIZE, 20);
314        assert_eq!(ROOT_RECORD_SIZE, 16);
315        assert_eq!(STRING_OFFSET_RECORD_SIZE, 8);
316        assert_eq!(SOURCE_RECORD_SIZE, 32);
317        assert_eq!(NODE_RECORD_SIZE, 24);
318        assert_eq!(EDGE_RECORD_SIZE, 8);
319        assert_eq!(TOKEN_RECORD_SIZE, 36);
320        assert_eq!(TRIVIA_RECORD_SIZE, 16);
321        assert_eq!(DIAGNOSTIC_RECORD_SIZE, 28);
322        assert_eq!(DIAGNOSTIC_LABEL_RECORD_SIZE, 16);
323        assert_eq!(EXTENDED_DATA_HEADER_SIZE, 8);
324    }
325
326    #[test]
327    fn section_kind_numeric_values_are_stable() {
328        assert_eq!(SectionKind::Roots.as_u16(), 1);
329        assert_eq!(SectionKind::Sources.as_u16(), 2);
330        assert_eq!(SectionKind::Nodes.as_u16(), 3);
331        assert_eq!(SectionKind::Edges.as_u16(), 4);
332        assert_eq!(SectionKind::Tokens.as_u16(), 5);
333        assert_eq!(SectionKind::Trivia.as_u16(), 6);
334        assert_eq!(SectionKind::Diagnostics.as_u16(), 7);
335        assert_eq!(SectionKind::DiagnosticLabels.as_u16(), 8);
336        assert_eq!(SectionKind::StringOffsets.as_u16(), 9);
337        assert_eq!(SectionKind::StringData.as_u16(), 10);
338        assert_eq!(SectionKind::SourceTextData.as_u16(), 11);
339        assert_eq!(SectionKind::ExtendedData.as_u16(), 12);
340    }
341
342    #[test]
343    fn section_kind_from_u16_rejects_zero_and_unknown() {
344        assert!(SectionKind::from_u16(0).is_none());
345        assert!(SectionKind::from_u16(13).is_none());
346        assert_eq!(SectionKind::from_u16(1), Some(SectionKind::Roots));
347    }
348
349    #[test]
350    fn section_kind_emission_order_is_stable() {
351        let order = SectionKind::ALL_IN_ORDER.map(SectionKind::as_u16);
352        assert_eq!(order, [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]);
353    }
354
355    #[test]
356    fn core_section_predicate_matches_design() {
357        for kind in SectionKind::ALL_IN_ORDER {
358            let expected = matches!(
359                kind,
360                SectionKind::Roots
361                    | SectionKind::Sources
362                    | SectionKind::Nodes
363                    | SectionKind::Edges
364                    | SectionKind::Tokens
365                    | SectionKind::StringOffsets
366                    | SectionKind::StringData
367            );
368            assert_eq!(kind.is_core(), expected, "{kind:?}");
369        }
370    }
371
372    #[test]
373    fn align_up_rounds_to_eight_byte_boundary() {
374        assert_eq!(align_up(0), Some(0));
375        assert_eq!(align_up(1), Some(8));
376        assert_eq!(align_up(7), Some(8));
377        assert_eq!(align_up(8), Some(8));
378        assert_eq!(align_up(9), Some(16));
379        assert_eq!(align_up(u32::MAX - 6), None);
380    }
381
382    #[test]
383    fn checked_u32_surfaces_overflow_as_none() {
384        assert_eq!(checked_u32(0), Some(0));
385        assert_eq!(checked_u32(u32::MAX as usize), Some(u32::MAX));
386        if usize::BITS > 32 {
387            assert_eq!(checked_u32(u32::MAX as usize + 1), None);
388        }
389    }
390
391    #[test]
392    fn little_endian_helpers_round_trip() {
393        let mut buf = Vec::new();
394        write_u8(&mut buf, 0xAB);
395        write_u16_le(&mut buf, 0x1234);
396        write_u32_le(&mut buf, 0xDEAD_BEEF);
397        assert_eq!(buf.len(), 7);
398        assert_eq!(buf[0], 0xAB);
399        assert_eq!(read_u16_le(&buf, 1), 0x1234);
400        assert_eq!(read_u32_le(&buf, 3), 0xDEAD_BEEF);
401    }
402
403    #[test]
404    fn magic_is_oxmf2ast() {
405        assert_eq!(&SNAPSHOT_MAGIC, b"OXMF2AST");
406    }
407
408    #[test]
409    fn header_values_lock_initial_v01() {
410        assert_eq!(SNAPSHOT_MAJOR_VERSION, 0);
411        assert_eq!(SNAPSHOT_MINOR_VERSION, 1);
412        assert_eq!(SNAPSHOT_FEATURE_FLAGS, 0);
413        assert_eq!(SECTION_ALIGNMENT, 8);
414    }
415}