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.
53pub(crate) const VERSION: u16 = 2;
54/// Fixed-size preamble: magic + version + section count + reserved + file size + checksum.
55pub(crate) const HEADER_PREAMBLE: usize = 16;
56/// Each offset table entry: kind(1) + reserved(3) + offset(4)
57pub(crate) const SECTION_ENTRY_SIZE: usize = 8;
58/// Number of sections in the current format.
59pub(crate) const SECTION_COUNT: u8 = 10;
60
61// Value type tags
62pub(crate) const VAL_INT: u8 = 0x00;
63pub(crate) const VAL_FLOAT: u8 = 0x01;
64pub(crate) const VAL_BOOL: u8 = 0x02;
65pub(crate) const VAL_STRING: u8 = 0x03;
66pub(crate) const VAL_LIST: u8 = 0x04;
67pub(crate) const VAL_DIVERT_TARGET: u8 = 0x05;
68pub(crate) const VAL_NULL: u8 = 0x06;
69pub(crate) const VAL_VAR_POINTER: u8 = 0x07;
70pub(crate) const VAL_FRAGMENT_REF: u8 = 0x08;
71
72// LineContent tags
73pub(crate) const LINE_PLAIN: u8 = 0x00;
74pub(crate) const LINE_TEMPLATE: u8 = 0x01;
75
76// LinePart tags
77pub(crate) const PART_LITERAL: u8 = 0x00;
78pub(crate) const PART_SLOT: u8 = 0x01;
79pub(crate) const PART_SELECT: u8 = 0x02;
80
81// SelectKey tags
82pub(crate) const KEY_CARDINAL: u8 = 0x00;
83pub(crate) const KEY_ORDINAL: u8 = 0x01;
84pub(crate) const KEY_EXACT: u8 = 0x02;
85pub(crate) const KEY_KEYWORD: u8 = 0x03;
86
87// PluralCategory tags
88pub(crate) const CAT_ZERO: u8 = 0x00;
89pub(crate) const CAT_ONE: u8 = 0x01;
90pub(crate) const CAT_TWO: u8 = 0x02;
91pub(crate) const CAT_FEW: u8 = 0x03;
92pub(crate) const CAT_MANY: u8 = 0x04;
93pub(crate) const CAT_OTHER: u8 = 0x05;
94
95// ── Section types ───────────────────────────────────────────────────────────
96
97/// Identifies a section within an `.inkb` file.
98#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
99#[repr(u8)]
100pub enum SectionKind {
101    NameTable = 0x01,
102    Variables = 0x02,
103    ListDefs = 0x03,
104    ListItems = 0x04,
105    Externals = 0x05,
106    Containers = 0x06,
107    LineTables = 0x07,
108    Labels = 0x08,
109    ListLiterals = 0x09,
110    AddressPaths = 0x0A,
111}
112
113impl SectionKind {
114    pub(crate) fn from_u8(tag: u8) -> Result<Self, DecodeError> {
115        match tag {
116            0x01 => Ok(Self::NameTable),
117            0x02 => Ok(Self::Variables),
118            0x03 => Ok(Self::ListDefs),
119            0x04 => Ok(Self::ListItems),
120            0x05 => Ok(Self::Externals),
121            0x06 => Ok(Self::Containers),
122            0x07 => Ok(Self::LineTables),
123            0x08 => Ok(Self::Labels),
124            0x09 => Ok(Self::ListLiterals),
125            0x0A => Ok(Self::AddressPaths),
126            _ => Err(DecodeError::InvalidSectionKind(tag)),
127        }
128    }
129}
130
131/// An entry in the `.inkb` offset table.
132#[derive(Debug, Clone, Copy, PartialEq, Eq)]
133pub struct SectionEntry {
134    pub kind: SectionKind,
135    pub offset: u32,
136}
137
138/// Parsed header + offset table from an `.inkb` file.
139///
140/// Allows selective reads without parsing section data.
141#[derive(Debug, Clone, PartialEq, Eq)]
142pub struct InkbIndex {
143    pub version: u16,
144    pub file_size: u32,
145    pub checksum: u32,
146    pub sections: Vec<SectionEntry>,
147}
148
149impl InkbIndex {
150    /// Total header size in bytes (preamble + offset table).
151    pub fn header_size(&self) -> usize {
152        HEADER_PREAMBLE + self.sections.len() * SECTION_ENTRY_SIZE
153    }
154
155    /// Returns `(offset, length)` for a section, computing length from the
156    /// next section's offset (or `file_size` for the last section).
157    ///
158    /// Subtraction is safe because `read_inkb_index` validates that offsets
159    /// are monotonically increasing and within `[header_size, file_size]`.
160    pub fn section_range(&self, kind: SectionKind) -> Option<Range<usize>> {
161        let idx = self.sections.iter().position(|e| e.kind == kind)?;
162        let start = self.sections[idx].offset as usize;
163        let end = self
164            .sections
165            .get(idx + 1)
166            .map_or(self.file_size, |e| e.offset) as usize;
167        Some(start..end)
168    }
169}
170
171/// Cap `Vec::with_capacity` allocations against remaining buffer bytes to avoid
172/// OOM on crafted inputs with huge count fields. Each element occupies at least
173/// `min_element_size` bytes, so the count can't exceed `remaining / min`.
174pub(crate) fn safe_capacity(
175    count: usize,
176    buf_len: usize,
177    offset: usize,
178    min_element_size: usize,
179) -> usize {
180    let remaining = buf_len.saturating_sub(offset);
181    let max_possible = remaining.checked_div(min_element_size).unwrap_or(remaining);
182    count.min(max_possible)
183}