Skip to main content

brink_format/inkb/
mod.rs

1//! Binary (.inkb) writer and reader for [`StoryData`].
2//!
3//! The `.inkb` format is a compact, little-endian binary encoding designed for
4//! fast loading by the runtime.
5//!
6//! ## Header layout
7//!
8//! ```text
9//! Offset  Size   Field
10//! ------  -----  ------
11//! 0       4      Magic: b"INKB"
12//! 4       2      Version: u16 LE (= 1)
13//! 6       1      Section count: u8 (N entries in offset table)
14//! 7       1      Reserved: 0x00
15//! 8       4      File size: u32 LE (total bytes)
16//! 12      4      Content checksum: u32 LE (CRC-32 of all bytes after header)
17//! 16      N*8    Offset table entries
18//! ```
19//!
20//! Each offset table entry (8 bytes):
21//! ```text
22//! 0       1      SectionKind: u8 tag
23//! 1       3      Reserved: 3 bytes of 0x00
24//! 4       4      Offset: u32 LE (byte offset from start of file)
25//! ```
26
27pub(crate) mod read;
28pub(crate) mod write;
29
30pub use read::{
31    read_inkb, read_inkb_index, read_section_address_paths, read_section_addresses,
32    read_section_containers, read_section_externals, read_section_line_tables,
33    read_section_list_defs, read_section_list_items, read_section_list_literals,
34    read_section_name_table, read_section_variables,
35};
36pub use write::{
37    assemble_inkb, write_inkb, write_section_address_paths, write_section_addresses,
38    write_section_containers, write_section_externals, write_section_line_tables,
39    write_section_list_defs, write_section_list_items, write_section_list_literals,
40    write_section_name_table, write_section_variables,
41};
42
43use std::ops::Range;
44
45use crate::opcode::DecodeError;
46
47// ── Constants ───────────────────────────────────────────────────────────────
48
49pub(crate) const MAGIC: &[u8; 4] = b"INKB";
50/// On-the-wire format version. Bumped on any byte-layout change; the reader
51/// hard-rejects an unrecognized version (see `docs/format-spec.md` § Versioning).
52/// v2 added `ContainerDef::param_count` to the Containers section.
53/// v3 added the `local` scope bit to `GlobalVarDef` (Variables section) and
54/// `ContainerDef` (Containers section) — see `docs/directive-annotations-spec.md`.
55pub(crate) const VERSION: u16 = 3;
56/// Fixed-size preamble: magic + version + section count + reserved + file size + checksum.
57pub(crate) const HEADER_PREAMBLE: usize = 16;
58/// Each offset table entry: kind(1) + reserved(3) + offset(4)
59pub(crate) const SECTION_ENTRY_SIZE: usize = 8;
60/// Number of sections in the current format.
61pub(crate) const SECTION_COUNT: u8 = 10;
62
63// Value type tags
64pub(crate) const VAL_INT: u8 = 0x00;
65pub(crate) const VAL_FLOAT: u8 = 0x01;
66pub(crate) const VAL_BOOL: u8 = 0x02;
67pub(crate) const VAL_STRING: u8 = 0x03;
68pub(crate) const VAL_LIST: u8 = 0x04;
69pub(crate) const VAL_DIVERT_TARGET: u8 = 0x05;
70pub(crate) const VAL_NULL: u8 = 0x06;
71pub(crate) const VAL_VAR_POINTER: u8 = 0x07;
72pub(crate) const VAL_FRAGMENT_REF: u8 = 0x08;
73
74// LineContent tags
75pub(crate) const LINE_PLAIN: u8 = 0x00;
76pub(crate) const LINE_TEMPLATE: u8 = 0x01;
77
78// LinePart tags
79pub(crate) const PART_LITERAL: u8 = 0x00;
80pub(crate) const PART_SLOT: u8 = 0x01;
81pub(crate) const PART_SELECT: u8 = 0x02;
82
83// SelectKey tags
84pub(crate) const KEY_CARDINAL: u8 = 0x00;
85pub(crate) const KEY_ORDINAL: u8 = 0x01;
86pub(crate) const KEY_EXACT: u8 = 0x02;
87pub(crate) const KEY_KEYWORD: u8 = 0x03;
88
89// PluralCategory tags
90pub(crate) const CAT_ZERO: u8 = 0x00;
91pub(crate) const CAT_ONE: u8 = 0x01;
92pub(crate) const CAT_TWO: u8 = 0x02;
93pub(crate) const CAT_FEW: u8 = 0x03;
94pub(crate) const CAT_MANY: u8 = 0x04;
95pub(crate) const CAT_OTHER: u8 = 0x05;
96
97// ── Section types ───────────────────────────────────────────────────────────
98
99/// Identifies a section within an `.inkb` file.
100#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
101#[repr(u8)]
102pub enum SectionKind {
103    NameTable = 0x01,
104    Variables = 0x02,
105    ListDefs = 0x03,
106    ListItems = 0x04,
107    Externals = 0x05,
108    Containers = 0x06,
109    LineTables = 0x07,
110    Labels = 0x08,
111    ListLiterals = 0x09,
112    AddressPaths = 0x0A,
113}
114
115impl SectionKind {
116    pub(crate) fn from_u8(tag: u8) -> Result<Self, DecodeError> {
117        match tag {
118            0x01 => Ok(Self::NameTable),
119            0x02 => Ok(Self::Variables),
120            0x03 => Ok(Self::ListDefs),
121            0x04 => Ok(Self::ListItems),
122            0x05 => Ok(Self::Externals),
123            0x06 => Ok(Self::Containers),
124            0x07 => Ok(Self::LineTables),
125            0x08 => Ok(Self::Labels),
126            0x09 => Ok(Self::ListLiterals),
127            0x0A => Ok(Self::AddressPaths),
128            _ => Err(DecodeError::InvalidSectionKind(tag)),
129        }
130    }
131}
132
133/// An entry in the `.inkb` offset table.
134#[derive(Debug, Clone, Copy, PartialEq, Eq)]
135pub struct SectionEntry {
136    pub kind: SectionKind,
137    pub offset: u32,
138}
139
140/// Parsed header + offset table from an `.inkb` file.
141///
142/// Allows selective reads without parsing section data.
143#[derive(Debug, Clone, PartialEq, Eq)]
144pub struct InkbIndex {
145    pub version: u16,
146    pub file_size: u32,
147    pub checksum: u32,
148    pub sections: Vec<SectionEntry>,
149}
150
151impl InkbIndex {
152    /// Total header size in bytes (preamble + offset table).
153    pub fn header_size(&self) -> usize {
154        HEADER_PREAMBLE + self.sections.len() * SECTION_ENTRY_SIZE
155    }
156
157    /// Returns `(offset, length)` for a section, computing length from the
158    /// next section's offset (or `file_size` for the last section).
159    ///
160    /// Subtraction is safe because `read_inkb_index` validates that offsets
161    /// are monotonically increasing and within `[header_size, file_size]`.
162    pub fn section_range(&self, kind: SectionKind) -> Option<Range<usize>> {
163        let idx = self.sections.iter().position(|e| e.kind == kind)?;
164        let start = self.sections[idx].offset as usize;
165        let end = self
166            .sections
167            .get(idx + 1)
168            .map_or(self.file_size, |e| e.offset) as usize;
169        Some(start..end)
170    }
171}
172
173/// Cap `Vec::with_capacity` allocations against remaining buffer bytes to avoid
174/// OOM on crafted inputs with huge count fields. Each element occupies at least
175/// `min_element_size` bytes, so the count can't exceed `remaining / min`.
176pub(crate) fn safe_capacity(
177    count: usize,
178    buf_len: usize,
179    offset: usize,
180    min_element_size: usize,
181) -> usize {
182    let remaining = buf_len.saturating_sub(offset);
183    let max_possible = remaining.checked_div(min_element_size).unwrap_or(remaining);
184    count.min(max_possible)
185}