Skip to main content

brink_format/inkb/
write.rs

1//! Encoding (write) half of the `.inkb` binary format.
2
3use crate::codec::{
4    crc32, write_def_id, write_i32, write_str, write_u8, write_u16, write_u32, write_u64,
5};
6use crate::definition::{
7    AddressDef, AddressPath, ContainerDef, ExternalFnDef, GlobalVarDef, LineEntry, ListDef,
8    ListItemDef, ScopeLineTable,
9};
10use crate::line::{LineContent, LinePart, PluralCategory, SelectKey};
11use crate::story::StoryData;
12use crate::value::{ListValue, Value, ValueType};
13
14use super::{
15    CAT_FEW, CAT_MANY, CAT_ONE, CAT_OTHER, CAT_TWO, CAT_ZERO, HEADER_PREAMBLE, KEY_CARDINAL,
16    KEY_EXACT, KEY_KEYWORD, KEY_ORDINAL, LINE_PLAIN, LINE_TEMPLATE, MAGIC, PART_LITERAL,
17    PART_SELECT, PART_SLOT, SECTION_COUNT, SECTION_ENTRY_SIZE, SectionKind, VAL_BOOL,
18    VAL_DIVERT_TARGET, VAL_FLOAT, VAL_FRAGMENT_REF, VAL_INT, VAL_LIST, VAL_NULL, VAL_STRING,
19    VAL_VAR_POINTER, VERSION,
20};
21
22// ── Tier 1: Full story write ────────────────────────────────────────────────
23
24/// Encode a [`StoryData`] into the `.inkb` binary format with sectioned header.
25#[expect(clippy::cast_possible_truncation)]
26pub fn write_inkb(story: &StoryData, buf: &mut Vec<u8>) {
27    let base = buf.len();
28    let header_size = HEADER_PREAMBLE + SECTION_COUNT as usize * SECTION_ENTRY_SIZE;
29
30    // Write placeholder header (zeros) — we'll patch it after writing sections.
31    buf.resize(base + header_size, 0);
32
33    // Track section offsets as we write each section.
34    let section_kinds = [
35        SectionKind::NameTable,
36        SectionKind::Variables,
37        SectionKind::ListDefs,
38        SectionKind::ListItems,
39        SectionKind::Externals,
40        SectionKind::Containers,
41        SectionKind::LineTables,
42        SectionKind::Labels,
43        SectionKind::ListLiterals,
44        SectionKind::AddressPaths,
45    ];
46    let mut section_offsets = [0u32; 10];
47
48    // 1. NameTable
49    section_offsets[0] = (buf.len() - base) as u32;
50    write_section_name_table(&story.name_table, buf);
51
52    // 2. Variables
53    section_offsets[1] = (buf.len() - base) as u32;
54    write_section_variables(&story.variables, buf);
55
56    // 3. ListDefs
57    section_offsets[2] = (buf.len() - base) as u32;
58    write_section_list_defs(&story.list_defs, buf);
59
60    // 4. ListItems
61    section_offsets[3] = (buf.len() - base) as u32;
62    write_section_list_items(&story.list_items, buf);
63
64    // 5. Externals
65    section_offsets[4] = (buf.len() - base) as u32;
66    write_section_externals(&story.externals, buf);
67
68    // 6. Containers
69    section_offsets[5] = (buf.len() - base) as u32;
70    write_section_containers(&story.containers, buf);
71
72    // 7. LineTables
73    section_offsets[6] = (buf.len() - base) as u32;
74    write_section_line_tables(&story.line_tables, buf);
75
76    // 8. Addresses (Labels section)
77    section_offsets[7] = (buf.len() - base) as u32;
78    write_section_addresses(&story.addresses, buf);
79
80    // 9. ListLiterals
81    section_offsets[8] = (buf.len() - base) as u32;
82    write_section_list_literals(&story.list_literals, buf);
83
84    // 10. AddressPaths
85    section_offsets[9] = (buf.len() - base) as u32;
86    write_section_address_paths(&story.address_paths, buf);
87
88    let file_size = (buf.len() - base) as u32;
89    let checksum = crc32(&buf[base + header_size..]);
90
91    // Patch header in-place.
92    let h = &mut buf[base..];
93    h[0..4].copy_from_slice(MAGIC);
94    h[4..6].copy_from_slice(&VERSION.to_le_bytes());
95    h[6] = SECTION_COUNT;
96    h[7] = 0; // reserved
97    h[8..12].copy_from_slice(&file_size.to_le_bytes());
98    h[12..16].copy_from_slice(&checksum.to_le_bytes());
99
100    for (i, kind) in section_kinds.iter().enumerate() {
101        let entry_base = HEADER_PREAMBLE + i * SECTION_ENTRY_SIZE;
102        h[entry_base] = *kind as u8;
103        h[entry_base + 1] = 0; // reserved
104        h[entry_base + 2] = 0;
105        h[entry_base + 3] = 0;
106        h[entry_base + 4..entry_base + 8].copy_from_slice(&section_offsets[i].to_le_bytes());
107    }
108}
109
110// ── Assembly ────────────────────────────────────────────────────────────────
111
112/// Assemble a complete `.inkb` file from pre-encoded section buffers.
113///
114/// Sections should be provided in the canonical order matching [`SectionKind`]
115/// tags. The header (with offsets and checksum) is computed automatically.
116#[expect(clippy::cast_possible_truncation)]
117pub fn assemble_inkb(sections: &[(SectionKind, &[u8])], out: &mut Vec<u8>) {
118    let base = out.len();
119    let section_count = sections.len() as u8;
120    let header_size = HEADER_PREAMBLE + sections.len() * SECTION_ENTRY_SIZE;
121
122    // Placeholder header.
123    out.resize(base + header_size, 0);
124
125    // Append section data and record offsets.
126    let mut entries: Vec<(SectionKind, u32)> = Vec::with_capacity(sections.len());
127    for (kind, data) in sections {
128        let offset = (out.len() - base) as u32;
129        entries.push((*kind, offset));
130        out.extend_from_slice(data);
131    }
132
133    let file_size = (out.len() - base) as u32;
134    let checksum = crc32(&out[base + header_size..]);
135
136    // Patch header.
137    let h = &mut out[base..];
138    h[0..4].copy_from_slice(MAGIC);
139    h[4..6].copy_from_slice(&VERSION.to_le_bytes());
140    h[6] = section_count;
141    h[7] = 0;
142    h[8..12].copy_from_slice(&file_size.to_le_bytes());
143    h[12..16].copy_from_slice(&checksum.to_le_bytes());
144
145    for (i, (kind, offset)) in entries.iter().enumerate() {
146        let entry_base = HEADER_PREAMBLE + i * SECTION_ENTRY_SIZE;
147        h[entry_base] = *kind as u8;
148        h[entry_base + 1] = 0;
149        h[entry_base + 2] = 0;
150        h[entry_base + 3] = 0;
151        h[entry_base + 4..entry_base + 8].copy_from_slice(&offset.to_le_bytes());
152    }
153}
154
155// ── Section writers ─────────────────────────────────────────────────────────
156
157/// Write the name table section (no header framing).
158#[expect(clippy::cast_possible_truncation)]
159pub fn write_section_name_table(names: &[String], buf: &mut Vec<u8>) {
160    write_u32(buf, names.len() as u32);
161    for name in names {
162        write_str(buf, name);
163    }
164}
165
166/// Write the variables section (no header framing).
167#[expect(clippy::cast_possible_truncation)]
168pub fn write_section_variables(variables: &[GlobalVarDef], buf: &mut Vec<u8>) {
169    write_u32(buf, variables.len() as u32);
170    for var in variables {
171        encode_global_var(var, buf);
172    }
173}
174
175/// Write the list definitions section (no header framing).
176#[expect(clippy::cast_possible_truncation)]
177pub fn write_section_list_defs(list_defs: &[ListDef], buf: &mut Vec<u8>) {
178    write_u32(buf, list_defs.len() as u32);
179    for ld in list_defs {
180        encode_list_def(ld, buf);
181    }
182}
183
184/// Write the list items section (no header framing).
185#[expect(clippy::cast_possible_truncation)]
186pub fn write_section_list_items(list_items: &[ListItemDef], buf: &mut Vec<u8>) {
187    write_u32(buf, list_items.len() as u32);
188    for li in list_items {
189        encode_list_item(li, buf);
190    }
191}
192
193/// Write the externals section (no header framing).
194#[expect(clippy::cast_possible_truncation)]
195pub fn write_section_externals(externals: &[ExternalFnDef], buf: &mut Vec<u8>) {
196    write_u32(buf, externals.len() as u32);
197    for ext in externals {
198        encode_external(ext, buf);
199    }
200}
201
202/// Write the containers section (no header framing).
203#[expect(clippy::cast_possible_truncation)]
204pub fn write_section_containers(containers: &[ContainerDef], buf: &mut Vec<u8>) {
205    write_u32(buf, containers.len() as u32);
206    for c in containers {
207        encode_container(c, buf);
208    }
209}
210
211/// Write the addresses section (no header framing).
212#[expect(clippy::cast_possible_truncation)]
213pub fn write_section_addresses(addresses: &[AddressDef], buf: &mut Vec<u8>) {
214    write_u32(buf, addresses.len() as u32);
215    for addr in addresses {
216        write_def_id(buf, addr.id);
217        write_def_id(buf, addr.container_id);
218        write_u32(buf, addr.byte_offset);
219    }
220}
221
222/// Write the address-paths section (no header framing).
223#[expect(clippy::cast_possible_truncation)]
224pub fn write_section_address_paths(address_paths: &[AddressPath], buf: &mut Vec<u8>) {
225    write_u32(buf, address_paths.len() as u32);
226    for ap in address_paths {
227        write_u16(buf, ap.path.0);
228        write_def_id(buf, ap.target);
229    }
230}
231
232// ── Encode helpers (private) ────────────────────────────────────────────────
233
234fn encode_global_var(v: &GlobalVarDef, buf: &mut Vec<u8>) {
235    write_def_id(buf, v.id);
236    write_u16(buf, v.name.0);
237    encode_value_type(v.value_type, buf);
238    encode_value(&v.default_value, buf);
239    write_u8(buf, u8::from(v.mutable));
240    write_u8(buf, u8::from(v.local));
241}
242
243fn encode_value_type(vt: ValueType, buf: &mut Vec<u8>) {
244    let tag = match vt {
245        ValueType::Int => VAL_INT,
246        ValueType::Float => VAL_FLOAT,
247        ValueType::Bool => VAL_BOOL,
248        ValueType::String => VAL_STRING,
249        ValueType::List => VAL_LIST,
250        ValueType::DivertTarget => VAL_DIVERT_TARGET,
251        ValueType::VariablePointer => VAL_VAR_POINTER,
252        // TempPointer is runtime-only and should never appear in .inkb files.
253        ValueType::FragmentRef => VAL_FRAGMENT_REF,
254        ValueType::TempPointer | ValueType::Null => VAL_NULL,
255    };
256    write_u8(buf, tag);
257}
258
259#[expect(clippy::cast_possible_truncation)]
260fn encode_value(v: &Value, buf: &mut Vec<u8>) {
261    match v {
262        Value::Int(n) => {
263            write_u8(buf, VAL_INT);
264            write_i32(buf, *n);
265        }
266        Value::Float(n) => {
267            write_u8(buf, VAL_FLOAT);
268            buf.extend_from_slice(&n.to_le_bytes());
269        }
270        Value::Bool(b) => {
271            write_u8(buf, VAL_BOOL);
272            write_u8(buf, u8::from(*b));
273        }
274        Value::String(s) => {
275            write_u8(buf, VAL_STRING);
276            write_str(buf, s);
277        }
278        Value::List(lv) => {
279            write_u8(buf, VAL_LIST);
280            write_u32(buf, lv.items.len() as u32);
281            for item in &lv.items {
282                write_def_id(buf, *item);
283            }
284            write_u32(buf, lv.origins.len() as u32);
285            for origin in &lv.origins {
286                write_def_id(buf, *origin);
287            }
288        }
289        Value::DivertTarget(id) => {
290            write_u8(buf, VAL_DIVERT_TARGET);
291            write_def_id(buf, *id);
292        }
293        Value::VariablePointer(id) => {
294            write_u8(buf, VAL_VAR_POINTER);
295            write_def_id(buf, *id);
296        }
297        Value::FragmentRef(idx) => {
298            write_u8(buf, VAL_FRAGMENT_REF);
299            write_u32(buf, *idx);
300        }
301        // TempPointer is runtime-only and should never appear in .inkb files.
302        Value::TempPointer { .. } | Value::Null => {
303            write_u8(buf, VAL_NULL);
304        }
305    }
306}
307
308#[expect(clippy::cast_possible_truncation)]
309fn encode_list_def(ld: &ListDef, buf: &mut Vec<u8>) {
310    write_def_id(buf, ld.id);
311    write_u16(buf, ld.name.0);
312    write_u32(buf, ld.items.len() as u32);
313    for (name_id, ordinal) in &ld.items {
314        write_u16(buf, name_id.0);
315        write_i32(buf, *ordinal);
316    }
317}
318
319fn encode_list_item(li: &ListItemDef, buf: &mut Vec<u8>) {
320    write_def_id(buf, li.id);
321    write_def_id(buf, li.origin);
322    write_i32(buf, li.ordinal);
323    write_u16(buf, li.name.0);
324}
325
326/// Write the list literals section (no header framing).
327#[expect(clippy::cast_possible_truncation)]
328pub fn write_section_list_literals(list_literals: &[ListValue], buf: &mut Vec<u8>) {
329    write_u32(buf, list_literals.len() as u32);
330    for lv in list_literals {
331        write_u32(buf, lv.items.len() as u32);
332        for item in &lv.items {
333            write_def_id(buf, *item);
334        }
335        write_u32(buf, lv.origins.len() as u32);
336        for origin in &lv.origins {
337            write_def_id(buf, *origin);
338        }
339    }
340}
341
342fn encode_external(ext: &ExternalFnDef, buf: &mut Vec<u8>) {
343    write_def_id(buf, ext.id);
344    write_u16(buf, ext.name.0);
345    write_u8(buf, ext.arg_count);
346    match ext.fallback {
347        Some(fb) => {
348            write_u8(buf, 1);
349            write_def_id(buf, fb);
350        }
351        None => {
352            write_u8(buf, 0);
353        }
354    }
355}
356
357#[expect(clippy::cast_possible_truncation)]
358fn encode_container(c: &ContainerDef, buf: &mut Vec<u8>) {
359    write_def_id(buf, c.id);
360    write_def_id(buf, c.scope_id);
361    match c.name {
362        Some(name_id) => {
363            write_u8(buf, 1);
364            write_u16(buf, name_id.0);
365        }
366        None => {
367            write_u8(buf, 0);
368        }
369    }
370    write_u8(buf, c.counting_flags.bits());
371    write_i32(buf, c.path_hash);
372    write_u8(buf, c.param_count);
373    write_u8(buf, u8::from(c.local));
374    write_u32(buf, c.bytecode.len() as u32);
375    buf.extend_from_slice(&c.bytecode);
376}
377
378/// Write the line tables section (no header framing).
379#[expect(clippy::cast_possible_truncation)]
380pub fn write_section_line_tables(line_tables: &[ScopeLineTable], buf: &mut Vec<u8>) {
381    write_u32(buf, line_tables.len() as u32);
382    for lt in line_tables {
383        encode_scope_line_table(lt, buf);
384    }
385}
386
387#[expect(clippy::cast_possible_truncation)]
388fn encode_scope_line_table(lt: &ScopeLineTable, buf: &mut Vec<u8>) {
389    write_def_id(buf, lt.scope_id);
390    write_u32(buf, lt.lines.len() as u32);
391    for entry in &lt.lines {
392        encode_line_entry(entry, buf);
393    }
394}
395
396fn encode_line_entry(entry: &LineEntry, buf: &mut Vec<u8>) {
397    encode_line_content(&entry.content, buf);
398    write_u64(buf, entry.source_hash);
399    match &entry.audio_ref {
400        Some(audio) => {
401            write_u8(buf, 1);
402            write_str(buf, audio);
403        }
404        None => {
405            write_u8(buf, 0);
406        }
407    }
408
409    // Slot info
410    #[expect(clippy::cast_possible_truncation)]
411    write_u8(buf, entry.slot_info.len() as u8);
412    for slot in &entry.slot_info {
413        write_u8(buf, slot.index);
414        write_str(buf, &slot.name);
415    }
416
417    // Source location
418    match &entry.source_location {
419        Some(loc) => {
420            write_u8(buf, 1);
421            write_str(buf, &loc.file);
422            write_u32(buf, loc.range_start);
423            write_u32(buf, loc.range_end);
424        }
425        None => {
426            write_u8(buf, 0);
427        }
428    }
429}
430
431#[expect(clippy::cast_possible_truncation)]
432pub(crate) fn encode_line_content(content: &LineContent, buf: &mut Vec<u8>) {
433    match content {
434        LineContent::Plain(s) => {
435            write_u8(buf, LINE_PLAIN);
436            write_str(buf, s);
437        }
438        LineContent::Template(parts) => {
439            write_u8(buf, LINE_TEMPLATE);
440            write_u32(buf, parts.len() as u32);
441            for part in parts {
442                encode_line_part(part, buf);
443            }
444        }
445    }
446}
447
448#[expect(clippy::cast_possible_truncation)]
449fn encode_line_part(part: &LinePart, buf: &mut Vec<u8>) {
450    match part {
451        LinePart::Literal(s) => {
452            write_u8(buf, PART_LITERAL);
453            write_str(buf, s);
454        }
455        LinePart::Slot(idx) => {
456            write_u8(buf, PART_SLOT);
457            write_u8(buf, *idx);
458        }
459        LinePart::Select {
460            slot,
461            variants,
462            default,
463        } => {
464            write_u8(buf, PART_SELECT);
465            write_u8(buf, *slot);
466            write_u32(buf, variants.len() as u32);
467            for (key, text) in variants {
468                encode_select_key(key, buf);
469                write_str(buf, text);
470            }
471            write_str(buf, default);
472        }
473    }
474}
475
476fn encode_select_key(key: &SelectKey, buf: &mut Vec<u8>) {
477    match key {
478        SelectKey::Cardinal(cat) => {
479            write_u8(buf, KEY_CARDINAL);
480            encode_plural_category(*cat, buf);
481        }
482        SelectKey::Ordinal(cat) => {
483            write_u8(buf, KEY_ORDINAL);
484            encode_plural_category(*cat, buf);
485        }
486        SelectKey::Exact(n) => {
487            write_u8(buf, KEY_EXACT);
488            write_i32(buf, *n);
489        }
490        SelectKey::Keyword(k) => {
491            write_u8(buf, KEY_KEYWORD);
492            write_str(buf, k);
493        }
494    }
495}
496
497fn encode_plural_category(cat: PluralCategory, buf: &mut Vec<u8>) {
498    let tag = match cat {
499        PluralCategory::Zero => CAT_ZERO,
500        PluralCategory::One => CAT_ONE,
501        PluralCategory::Two => CAT_TWO,
502        PluralCategory::Few => CAT_FEW,
503        PluralCategory::Many => CAT_MANY,
504        PluralCategory::Other => CAT_OTHER,
505    };
506    write_u8(buf, tag);
507}