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