Skip to main content

brink_format/inkb/
read.rs

1//! Decoding (read) half of the `.inkb` binary format.
2
3use alloc::string::String;
4use alloc::vec::Vec;
5
6use crate::codec::{
7    crc32, read_def_id, read_i32, read_str, read_u8, read_u16, read_u32, read_u64, read_varint,
8};
9use crate::counting::CountingFlags;
10use crate::definition::{
11    AddressDef, AddressPath, AliasEntry, CallAtom, CapabilityParam, ContainerDef,
12    DebugContainerTable, DebugEntry, DebugFileEntry, DebugInfoSection, DebugLocalEntry,
13    DirectEffects, DispatchEntry, EffectRowEntry, ExternalFnDef, FileSurface, FrameShapeDef,
14    GlobalVarDef, LineEntry, ListDef, ListItemDef, ParamMeta, ScopeLineTable, SlotInfo,
15    SourceLocation, StructShapeDef,
16};
17use crate::id::{DefinitionId, NameId};
18use crate::line::{LineContent, LinePart, PluralCategory, SelectKey};
19use crate::opcode::DecodeError;
20use crate::story::StoryData;
21use crate::value::{
22    ClosureEnvEntry, ListValue, MAX_DECODE_DEPTH, MapKey, OrderedMap, ShapeId, Value, ValueType,
23};
24
25use super::write::{
26    ALIAS_TABLE_SECTION_VERSION, DEBUG_INFO_SECTION_VERSION, EFFECT_ROWS_SECTION_VERSION,
27    FRAME_SHAPES_SECTION_VERSION, LINE_VARIANT_GROUPS_SECTION_VERSION, LOCAL_FLAG_HAS_RANGE,
28    LOCAL_FLAG_SYNTHETIC, LOCAL_FLAGS_KNOWN,
29};
30use super::{
31    CAP_PARAM_ANY, CAT_FEW, CAT_MANY, CAT_ONE, CAT_OTHER, CAT_TWO, CAT_ZERO, HANDLE_PARAM_NONE,
32    HEADER_PREAMBLE, InkbIndex, KEY_CARDINAL, KEY_EXACT, KEY_KEYWORD, KEY_ORDINAL, LINE_PLAIN,
33    LINE_TEMPLATE, MAGIC, PART_LITERAL, PART_SELECT, PART_SLOT, PART_SPAN, PROJ_SEG_INDEX,
34    PROJ_SEG_KEY, SECTION_ENTRY_SIZE, SectionEntry, SectionKind, VAL_ARRAY, VAL_BOOL, VAL_CLOSURE,
35    VAL_DIVERT_TARGET, VAL_FLOAT, VAL_FN_REF, VAL_FRAGMENT_REF, VAL_HANDLE, VAL_INT, VAL_LIST,
36    VAL_MAP, VAL_MAT2, VAL_MAT3, VAL_MAT4, VAL_NULL, VAL_OPTION, VAL_PROJECTION, VAL_QUAT,
37    VAL_RANGE, VAL_RECORD, VAL_STRING, VAL_VAR_POINTER, VAL_VEC2, VAL_VEC3, VAL_VEC4, VAL_WEIGHTED,
38    VERSION, safe_capacity,
39};
40
41// ── Tier 1: Full story read ─────────────────────────────────────────────────
42
43/// Decode a [`StoryData`] from `.inkb` binary format.
44pub fn read_inkb(buf: &[u8]) -> Result<StoryData, DecodeError> {
45    let index = read_inkb_index(buf)?;
46
47    // Validate checksum.
48    let header_size = index.header_size();
49    let computed = crc32(&buf[header_size..]);
50    if computed != index.checksum {
51        return Err(DecodeError::ChecksumMismatch {
52            expected: index.checksum,
53            actual: computed,
54        });
55    }
56
57    let name_table = read_section_name_table(buf, &index)?;
58    let variables = read_section_variables(buf, &index)?;
59    let list_defs = read_section_list_defs(buf, &index)?;
60    let list_items = read_section_list_items(buf, &index)?;
61    let externals = read_section_externals(buf, &index)?;
62    let containers = read_section_containers(buf, &index)?;
63    let line_tables = read_section_line_tables(buf, &index)?;
64    let addresses = read_section_addresses(buf, &index)?;
65    let list_literals = read_section_list_literals(buf, &index)?;
66    let address_paths = read_section_address_paths(buf, &index)?;
67    let literal_pool = read_section_literal_pool(buf, &index)?;
68    let struct_shapes = read_section_struct_shapes(buf, &index)?;
69    let private_defs = read_section_visibility(buf, &index)?;
70    let alias_table = read_section_alias_table(buf, &index)?;
71    let effect_rows = read_section_effect_rows(buf, &index)?;
72    let frame_shapes = read_section_frame_shapes(buf, &index)?;
73    let debug_info = read_section_debug_info(buf, &index)?;
74    let line_variant_groups = read_section_line_variant_groups(buf, &index)?;
75
76    Ok(StoryData {
77        containers,
78        line_tables,
79        variables,
80        list_defs,
81        list_items,
82        externals,
83        addresses,
84        address_paths,
85        name_table,
86        list_literals,
87        literal_pool,
88        struct_shapes,
89        private_defs,
90        alias_table,
91        effect_rows,
92        frame_shapes,
93        debug_info,
94        line_variant_groups,
95        source_checksum: index.checksum,
96    })
97}
98
99// ── Tier 2: Index-only parse ────────────────────────────────────────────────
100
101/// Parse the `.inkb` header and offset table without touching section data.
102pub fn read_inkb_index(buf: &[u8]) -> Result<InkbIndex, DecodeError> {
103    if buf.len() < HEADER_PREAMBLE {
104        return Err(DecodeError::UnexpectedEof);
105    }
106
107    let magic: [u8; 4] = [buf[0], buf[1], buf[2], buf[3]];
108    if &magic != MAGIC {
109        return Err(DecodeError::BadMagic(magic));
110    }
111
112    let mut off = 4;
113    let version = read_u16(buf, &mut off)?;
114    if version != VERSION {
115        return Err(DecodeError::UnsupportedVersion(version));
116    }
117
118    let section_count = read_u8(buf, &mut off)?;
119    let _reserved = read_u8(buf, &mut off)?;
120    let file_size = read_u32(buf, &mut off)?;
121    let checksum = read_u32(buf, &mut off)?;
122
123    // Validate file size.
124    if file_size as usize != buf.len() {
125        return Err(DecodeError::FileSizeMismatch {
126            expected: file_size,
127            actual: buf.len(),
128        });
129    }
130
131    let total_header = HEADER_PREAMBLE + section_count as usize * SECTION_ENTRY_SIZE;
132    if buf.len() < total_header {
133        return Err(DecodeError::UnexpectedEof);
134    }
135
136    let mut sections = Vec::with_capacity(section_count as usize);
137    for _ in 0..section_count {
138        let kind_tag = read_u8(buf, &mut off)?;
139        let kind = SectionKind::from_u8(kind_tag)?;
140        let _reserved0 = read_u8(buf, &mut off)?;
141        let _reserved1 = read_u8(buf, &mut off)?;
142        let _reserved2 = read_u8(buf, &mut off)?;
143        let offset = read_u32(buf, &mut off)?;
144        sections.push(SectionEntry { kind, offset });
145    }
146
147    // Validate structural invariants so downstream code can trust the index:
148    //   1. Every offset >= header size (sections live after the header)
149    //   2. Offsets are strictly monotonically increasing
150    //   3. Every offset <= file_size (sections live within the file)
151    // Max value: 16 + 255*8 = 2056, always fits in u32.
152    #[expect(clippy::cast_possible_truncation)]
153    let header_size = total_header as u32;
154    let mut prev_offset = header_size;
155    for entry in &sections {
156        if entry.offset < header_size || entry.offset > file_size || entry.offset < prev_offset {
157            return Err(DecodeError::InvalidSectionOffset {
158                kind: entry.kind as u8,
159                offset: entry.offset,
160            });
161        }
162        prev_offset = entry.offset;
163    }
164
165    Ok(InkbIndex {
166        version,
167        file_size,
168        checksum,
169        sections,
170    })
171}
172
173// ── Tier 3: Section-level read ──────────────────────────────────────────────
174
175/// Read the name table from a complete `.inkb` file using its index.
176pub fn read_section_name_table(buf: &[u8], index: &InkbIndex) -> Result<Vec<String>, DecodeError> {
177    let range =
178        index
179            .section_range(SectionKind::NameTable)
180            .ok_or(DecodeError::MissingSectionKind(
181                SectionKind::NameTable as u8,
182            ))?;
183    let mut off = range.start;
184    let count = read_u32(buf, &mut off)? as usize;
185    let mut names = Vec::with_capacity(safe_capacity(count, buf.len(), off, 4));
186    for _ in 0..count {
187        names.push(read_str(buf, &mut off)?);
188    }
189    Ok(names)
190}
191
192/// Read the variables from a complete `.inkb` file using its index.
193pub fn read_section_variables(
194    buf: &[u8],
195    index: &InkbIndex,
196) -> Result<Vec<GlobalVarDef>, DecodeError> {
197    let range =
198        index
199            .section_range(SectionKind::Variables)
200            .ok_or(DecodeError::MissingSectionKind(
201                SectionKind::Variables as u8,
202            ))?;
203    let mut off = range.start;
204    let count = read_u32(buf, &mut off)? as usize;
205    let mut vars = Vec::with_capacity(safe_capacity(count, buf.len(), off, 12));
206    for _ in 0..count {
207        vars.push(decode_global_var(buf, &mut off)?);
208    }
209    Ok(vars)
210}
211
212/// Read the list definitions from a complete `.inkb` file using its index.
213pub fn read_section_list_defs(buf: &[u8], index: &InkbIndex) -> Result<Vec<ListDef>, DecodeError> {
214    let range = index
215        .section_range(SectionKind::ListDefs)
216        .ok_or(DecodeError::MissingSectionKind(SectionKind::ListDefs as u8))?;
217    let mut off = range.start;
218    let count = read_u32(buf, &mut off)? as usize;
219    let mut defs = Vec::with_capacity(safe_capacity(count, buf.len(), off, 14));
220    for _ in 0..count {
221        defs.push(decode_list_def(buf, &mut off)?);
222    }
223    Ok(defs)
224}
225
226/// Read the list items from a complete `.inkb` file using its index.
227pub fn read_section_list_items(
228    buf: &[u8],
229    index: &InkbIndex,
230) -> Result<Vec<ListItemDef>, DecodeError> {
231    let range =
232        index
233            .section_range(SectionKind::ListItems)
234            .ok_or(DecodeError::MissingSectionKind(
235                SectionKind::ListItems as u8,
236            ))?;
237    let mut off = range.start;
238    let count = read_u32(buf, &mut off)? as usize;
239    let mut items = Vec::with_capacity(safe_capacity(count, buf.len(), off, 20));
240    for _ in 0..count {
241        items.push(decode_list_item(buf, &mut off)?);
242    }
243    Ok(items)
244}
245
246/// Read the externals from a complete `.inkb` file using its index.
247pub fn read_section_externals(
248    buf: &[u8],
249    index: &InkbIndex,
250) -> Result<Vec<ExternalFnDef>, DecodeError> {
251    let range =
252        index
253            .section_range(SectionKind::Externals)
254            .ok_or(DecodeError::MissingSectionKind(
255                SectionKind::Externals as u8,
256            ))?;
257    let mut off = range.start;
258    let count = read_u32(buf, &mut off)? as usize;
259    let mut exts = Vec::with_capacity(safe_capacity(count, buf.len(), off, 12));
260    for _ in 0..count {
261        exts.push(decode_external(buf, &mut off)?);
262    }
263    Ok(exts)
264}
265
266/// Read the containers from a complete `.inkb` file using its index.
267pub fn read_section_containers(
268    buf: &[u8],
269    index: &InkbIndex,
270) -> Result<Vec<ContainerDef>, DecodeError> {
271    let range =
272        index
273            .section_range(SectionKind::Containers)
274            .ok_or(DecodeError::MissingSectionKind(
275                SectionKind::Containers as u8,
276            ))?;
277    let mut off = range.start;
278    let count = read_u32(buf, &mut off)? as usize;
279    let mut containers = Vec::with_capacity(safe_capacity(count, buf.len(), off, 21));
280    for _ in 0..count {
281        containers.push(decode_container(buf, &mut off)?);
282    }
283    Ok(containers)
284}
285
286/// Read the addresses from a complete `.inkb` file using its index.
287pub fn read_section_addresses(
288    buf: &[u8],
289    index: &InkbIndex,
290) -> Result<Vec<AddressDef>, DecodeError> {
291    let Some(range) = index.section_range(SectionKind::Labels) else {
292        // Addresses section is optional for backwards compatibility.
293        return Ok(Vec::new());
294    };
295    let mut off = range.start;
296    let count = read_u32(buf, &mut off)? as usize;
297    // Each address entry: def_id(8) + container_id(8) + byte_offset(4) = 20 bytes
298    let mut addresses = Vec::with_capacity(safe_capacity(count, buf.len(), off, 20));
299    for _ in 0..count {
300        let id = read_def_id(buf, &mut off)?;
301        let container_id = read_def_id(buf, &mut off)?;
302        let byte_offset = read_u32(buf, &mut off)?;
303        addresses.push(AddressDef {
304            id,
305            container_id,
306            byte_offset,
307        });
308    }
309    Ok(addresses)
310}
311
312/// Read the address-paths section using a pre-parsed index.
313pub fn read_section_address_paths(
314    buf: &[u8],
315    index: &InkbIndex,
316) -> Result<Vec<AddressPath>, DecodeError> {
317    let Some(range) = index.section_range(SectionKind::AddressPaths) else {
318        // AddressPaths section is optional for backwards compatibility
319        // (legacy `.inkb` and converter output omit it).
320        return Ok(Vec::new());
321    };
322    let mut off = range.start;
323    let count = read_u32(buf, &mut off)? as usize;
324    // Each entry: path NameId(2) + target def_id(8) = 10 bytes
325    let mut paths = Vec::with_capacity(safe_capacity(count, buf.len(), off, 10));
326    for _ in 0..count {
327        let path = NameId(read_u16(buf, &mut off)?);
328        let target = read_def_id(buf, &mut off)?;
329        paths.push(AddressPath { path, target });
330    }
331    Ok(paths)
332}
333
334// ── Decode helpers (private) ────────────────────────────────────────────────
335
336fn decode_global_var(buf: &[u8], off: &mut usize) -> Result<GlobalVarDef, DecodeError> {
337    let id = read_def_id(buf, off)?;
338    let name = NameId(read_u16(buf, off)?);
339    let value_type = decode_value_type(buf, off)?;
340    let default_value = decode_value(buf, off, 0)?;
341    let mutable = read_u8(buf, off)? != 0;
342    let local = read_u8(buf, off)? != 0;
343    Ok(GlobalVarDef {
344        id,
345        name,
346        value_type,
347        default_value,
348        mutable,
349        local,
350    })
351}
352
353fn decode_value_type(buf: &[u8], off: &mut usize) -> Result<ValueType, DecodeError> {
354    let tag = read_u8(buf, off)?;
355    match tag {
356        VAL_INT => Ok(ValueType::Int),
357        VAL_FLOAT => Ok(ValueType::Float),
358        VAL_BOOL => Ok(ValueType::Bool),
359        VAL_STRING => Ok(ValueType::String),
360        VAL_LIST => Ok(ValueType::List),
361        VAL_DIVERT_TARGET => Ok(ValueType::DivertTarget),
362        VAL_VAR_POINTER => Ok(ValueType::VariablePointer),
363        VAL_FRAGMENT_REF => Ok(ValueType::FragmentRef),
364        VAL_NULL => Ok(ValueType::Null),
365        VAL_ARRAY => Ok(ValueType::Array),
366        VAL_MAP => Ok(ValueType::Map),
367        VAL_RECORD => Ok(ValueType::Record),
368        VAL_FN_REF => Ok(ValueType::FnRef),
369        VAL_CLOSURE => Ok(ValueType::Closure),
370        VAL_HANDLE => Ok(ValueType::Handle),
371        VAL_PROJECTION => Ok(ValueType::Projection),
372        VAL_OPTION => Ok(ValueType::Option),
373        VAL_RANGE => Ok(ValueType::Range),
374        VAL_VEC2 => Ok(ValueType::Vec2),
375        VAL_VEC3 => Ok(ValueType::Vec3),
376        VAL_VEC4 => Ok(ValueType::Vec4),
377        VAL_QUAT => Ok(ValueType::Quat),
378        VAL_MAT2 => Ok(ValueType::Mat2),
379        VAL_MAT3 => Ok(ValueType::Mat3),
380        VAL_MAT4 => Ok(ValueType::Mat4),
381        VAL_WEIGHTED => Ok(ValueType::Weighted),
382        _ => Err(DecodeError::InvalidValueType(tag)),
383    }
384}
385
386/// NS-A8 (`docs/tower-mini-spec.md` T5): read `N` explicit little-endian
387/// f32 lanes — the hand-serialized tower wire form `write_f32_lanes`
388/// produced. The lanes are handed back as a plain array; the caller builds
389/// the glam value through its explicit `from_array`/`from_cols_array`
390/// constructor (never a memory-layout cast).
391fn read_f32_lanes<const N: usize>(buf: &[u8], off: &mut usize) -> Result<[f32; N], DecodeError> {
392    if *off + 4 * N > buf.len() {
393        return Err(DecodeError::UnexpectedEof);
394    }
395    let mut lanes = [0.0f32; N];
396    for lane in &mut lanes {
397        *lane = f32::from_le_bytes([buf[*off], buf[*off + 1], buf[*off + 2], buf[*off + 3]]);
398        *off += 4;
399    }
400    Ok(lanes)
401}
402
403#[expect(
404    clippy::too_many_lines,
405    reason = "one match arm per value tag — the NS-A1 VAL_OPTION arm pushed this past 100"
406)]
407fn decode_value(buf: &[u8], off: &mut usize, depth: usize) -> Result<Value, DecodeError> {
408    if depth > MAX_DECODE_DEPTH {
409        return Err(DecodeError::MaxDepthExceeded(MAX_DECODE_DEPTH));
410    }
411    let tag = read_u8(buf, off)?;
412    match tag {
413        VAL_INT => Ok(Value::Int(read_i32(buf, off)?)),
414        VAL_FLOAT => {
415            if *off + 4 > buf.len() {
416                return Err(DecodeError::UnexpectedEof);
417            }
418            let v = f32::from_le_bytes([buf[*off], buf[*off + 1], buf[*off + 2], buf[*off + 3]]);
419            *off += 4;
420            Ok(Value::Float(v))
421        }
422        VAL_BOOL => Ok(Value::Bool(read_u8(buf, off)? != 0)),
423        VAL_STRING => Ok(Value::String(read_str(buf, off)?.into())),
424        VAL_LIST => {
425            let item_count = read_u32(buf, off)? as usize;
426            let mut items = Vec::with_capacity(safe_capacity(item_count, buf.len(), *off, 8));
427            for _ in 0..item_count {
428                items.push(read_def_id(buf, off)?);
429            }
430            let origin_count = read_u32(buf, off)? as usize;
431            let mut origins = Vec::with_capacity(safe_capacity(origin_count, buf.len(), *off, 8));
432            for _ in 0..origin_count {
433                origins.push(read_def_id(buf, off)?);
434            }
435            Ok(Value::List(ListValue { items, origins }.into()))
436        }
437        VAL_DIVERT_TARGET => Ok(Value::DivertTarget(read_def_id(buf, off)?)),
438        VAL_VAR_POINTER => Ok(Value::VariablePointer(read_def_id(buf, off)?)),
439        VAL_FRAGMENT_REF => Ok(Value::FragmentRef(read_u32(buf, off)?)),
440        VAL_NULL => Ok(Value::Null),
441        VAL_ARRAY => {
442            let len = read_u32(buf, off)? as usize;
443            // Each element is at least one tag byte, so `len` can't exceed the
444            // remaining bytes — cap the pre-allocation against crafted inputs.
445            let mut items = Vec::with_capacity(safe_capacity(len, buf.len(), *off, 1));
446            for _ in 0..len {
447                items.push(decode_value(buf, off, depth + 1)?);
448            }
449            Ok(Value::array(items))
450        }
451        VAL_MAP => {
452            let len = read_u32(buf, off)? as usize;
453            let mut map = OrderedMap::with_capacity(safe_capacity(len, buf.len(), *off, 2));
454            for _ in 0..len {
455                let key = decode_map_key(buf, off)?;
456                let val = decode_value(buf, off, depth + 1)?;
457                // A repeated key would violate the content-based `OrderedMap`
458                // `Eq` (#909); reject rather than silently keeping the last
459                // occurrence (#985).
460                if map.contains_key(&key) {
461                    return Err(DecodeError::DuplicateMapKey);
462                }
463                map.insert(key, val);
464            }
465            Ok(Value::map(map))
466        }
467        VAL_RECORD => {
468            let shape = ShapeId(read_u32(buf, off)?);
469            let len = read_u32(buf, off)? as usize;
470            let mut fields = Vec::with_capacity(safe_capacity(len, buf.len(), *off, 1));
471            for _ in 0..len {
472                fields.push(decode_value(buf, off, depth + 1)?);
473            }
474            Ok(Value::record(shape, fields))
475        }
476        // Function values (T1c, `docs/format-v4-rfc.md` §1).
477        VAL_FN_REF => Ok(Value::FnRef(read_def_id(buf, off)?)),
478        VAL_CLOSURE => {
479            let target = read_def_id(buf, off)?;
480            let count = read_u16(buf, off)? as usize;
481            let mut env = Vec::with_capacity(safe_capacity(count, buf.len(), *off, 4));
482            for _ in 0..count {
483                let name = NameId(read_u16(buf, off)?);
484                let is_ref = read_u8(buf, off)? != 0;
485                let payload = decode_value(buf, off, depth + 1)?;
486                env.push(ClosureEnvEntry {
487                    name,
488                    is_ref,
489                    payload,
490                });
491            }
492            Ok(Value::closure(target, env))
493        }
494        // Handle values (T1d, `docs/format-v4-rfc.md` §1).
495        VAL_HANDLE => {
496            let kind = NameId(read_u16(buf, off)?);
497            let id = read_u64(buf, off)?;
498            Ok(Value::handle(kind, id))
499        }
500        // Projection values (T1e, `docs/format-v4-rfc.md` §1). Segment kind
501        // `2=range` is RESERVED — `decode_proj_segment` rejects it since no
502        // `ProjSegment` variant exists to decode into (`docs/t1e-spec.md` §3).
503        VAL_PROJECTION => {
504            let cell = read_def_id(buf, off)?;
505            let count = read_u8(buf, off)? as usize;
506            let mut segments = Vec::with_capacity(safe_capacity(count, buf.len(), *off, 1));
507            for _ in 0..count {
508                segments.push(decode_proj_segment(buf, off, depth + 1)?);
509            }
510            Ok(Value::projection(cell, segments))
511        }
512        // Option values (NS-A1, `docs/stdlib-spec.md` §1.4): flag byte
513        // (0 = none, 1 = some) then the inner value when some. Any other
514        // flag byte is corrupt input. Depth-counted like the collection
515        // tags — a crafted chain of nested `some`s is the same recursion
516        // shape as nested single-element arrays.
517        VAL_OPTION => match read_u8(buf, off)? {
518            0 => Ok(Value::none()),
519            1 => Ok(Value::some(decode_value(buf, off, depth + 1)?)),
520            other => Err(DecodeError::InvalidValueType(other)),
521        },
522        // Range values (NS-A5, F7): start i32, end i32, inclusive flag.
523        // Flat — a range holds two ints, never another value, so there is
524        // no recursion and no depth accounting. Any flag byte other than
525        // 0/1 is corrupt input.
526        VAL_RANGE => {
527            let start = read_i32(buf, off)?;
528            let end = read_i32(buf, off)?;
529            let inclusive = match read_u8(buf, off)? {
530                0 => false,
531                1 => true,
532                other => return Err(DecodeError::InvalidValueType(other)),
533            };
534            Ok(Value::range(start, end, inclusive))
535        }
536        // Tower values (NS-A8, `docs/tower-mini-spec.md` T5): explicit
537        // little-endian f32 lanes in the pinned order (vec/quat `x, y(, z,
538        // w)`; matrices column-major), rebuilt through glam's explicit
539        // array constructors. Fixed sizes — no counts, no recursion, no
540        // depth concerns (tower values are leaves).
541        VAL_VEC2 => Ok(Value::Vec2(glam::Vec2::from_array(read_f32_lanes::<2>(
542            buf, off,
543        )?))),
544        VAL_VEC3 => Ok(Value::Vec3(glam::Vec3::from_array(read_f32_lanes::<3>(
545            buf, off,
546        )?))),
547        VAL_VEC4 => Ok(Value::Vec4(glam::Vec4::from_array(read_f32_lanes::<4>(
548            buf, off,
549        )?))),
550        VAL_QUAT => Ok(Value::Quat(glam::Quat::from_array(read_f32_lanes::<4>(
551            buf, off,
552        )?))),
553        VAL_MAT2 => Ok(Value::Mat2(glam::Mat2::from_cols_array(&read_f32_lanes::<
554            4,
555        >(
556            buf, off
557        )?))),
558        VAL_MAT3 => Ok(Value::Mat3(glam::Mat3::from_cols_array(&read_f32_lanes::<
559            9,
560        >(
561            buf, off
562        )?))),
563        VAL_MAT4 => Ok(Value::Mat4(glam::Mat4::from_cols_array(&read_f32_lanes::<
564            16,
565        >(
566            buf, off
567        )?))),
568        // Weighted tables (NS-A7, `docs/stdlib-spec.md` §8): u32 entry
569        // count, then per entry an i32 weight and a recursively-decoded
570        // value. Depth-counted like the collection tags. The §8
571        // evidence-by-construction invariant is enforced HERE too: an
572        // empty table or a non-positive weight is corrupt input (a
573        // `Weighted` never enters the runtime invalid, even from a
574        // crafted file).
575        VAL_WEIGHTED => {
576            let count = read_u32(buf, off)?;
577            if count == 0 {
578                return Err(DecodeError::InvalidValueType(VAL_WEIGHTED));
579            }
580            let mut entries = Vec::with_capacity(safe_capacity(count as usize, buf.len(), *off, 5));
581            for _ in 0..count {
582                let weight = read_i32(buf, off)?;
583                if weight < 1 {
584                    return Err(DecodeError::InvalidValueType(VAL_WEIGHTED));
585                }
586                let value = decode_value(buf, off, depth + 1)?;
587                entries.push((weight, value));
588            }
589            Ok(Value::weighted(entries))
590        }
591        _ => Err(DecodeError::InvalidValueType(tag)),
592    }
593}
594
595/// Decode a single [`crate::ProjSegment`] written by `encode_proj_segment`.
596fn decode_proj_segment(
597    buf: &[u8],
598    off: &mut usize,
599    depth: usize,
600) -> Result<crate::ProjSegment, DecodeError> {
601    let kind = read_u8(buf, off)?;
602    match kind {
603        PROJ_SEG_INDEX => Ok(crate::ProjSegment::Index(read_i32(buf, off)?)),
604        PROJ_SEG_KEY => Ok(crate::ProjSegment::Key(decode_value(buf, off, depth)?)),
605        other => Err(DecodeError::InvalidProjSegmentKind(other)),
606    }
607}
608
609/// Decode a [`MapKey`] written by `encode_map_key`: a scalar `VAL_*` tag
610/// (`int`/`string`/`bool`) then its payload. The strict reader rejects any
611/// other tag — only the v1 key domain is permitted (`docs/value-model-spec.md` §4).
612fn decode_map_key(buf: &[u8], off: &mut usize) -> Result<MapKey, DecodeError> {
613    let tag = read_u8(buf, off)?;
614    match tag {
615        VAL_INT => Ok(MapKey::Int(read_i32(buf, off)?)),
616        VAL_STRING => Ok(MapKey::Str(read_str(buf, off)?.into())),
617        VAL_BOOL => Ok(MapKey::Bool(read_u8(buf, off)? != 0)),
618        _ => Err(DecodeError::InvalidValueType(tag)),
619    }
620}
621
622fn decode_list_def(buf: &[u8], off: &mut usize) -> Result<ListDef, DecodeError> {
623    let id = read_def_id(buf, off)?;
624    let name = NameId(read_u16(buf, off)?);
625    let item_count = read_u32(buf, off)? as usize;
626    let mut items = Vec::with_capacity(safe_capacity(item_count, buf.len(), *off, 6));
627    for _ in 0..item_count {
628        let name_id = NameId(read_u16(buf, off)?);
629        let ordinal = read_i32(buf, off)?;
630        items.push((name_id, ordinal));
631    }
632    Ok(ListDef { id, name, items })
633}
634
635fn decode_list_item(buf: &[u8], off: &mut usize) -> Result<ListItemDef, DecodeError> {
636    let id = read_def_id(buf, off)?;
637    let origin = read_def_id(buf, off)?;
638    let ordinal = read_i32(buf, off)?;
639    let name = NameId(read_u16(buf, off)?);
640    Ok(ListItemDef {
641        id,
642        origin,
643        ordinal,
644        name,
645    })
646}
647
648/// Read the list literals from a complete `.inkb` file using its index.
649pub fn read_section_list_literals(
650    buf: &[u8],
651    index: &InkbIndex,
652) -> Result<Vec<ListValue>, DecodeError> {
653    let Some(range) = index.section_range(SectionKind::ListLiterals) else {
654        return Ok(Vec::new());
655    };
656    let mut off = range.start;
657    let count = read_u32(buf, &mut off)? as usize;
658    let mut literals = Vec::with_capacity(safe_capacity(count, buf.len(), off, 8));
659    for _ in 0..count {
660        let item_count = read_u32(buf, &mut off)? as usize;
661        let mut items = Vec::with_capacity(safe_capacity(item_count, buf.len(), off, 8));
662        for _ in 0..item_count {
663            items.push(read_def_id(buf, &mut off)?);
664        }
665        let origin_count = read_u32(buf, &mut off)? as usize;
666        let mut origins = Vec::with_capacity(safe_capacity(origin_count, buf.len(), off, 8));
667        for _ in 0..origin_count {
668            origins.push(read_def_id(buf, &mut off)?);
669        }
670        literals.push(ListValue { items, origins });
671    }
672    Ok(literals)
673}
674
675/// Read the T1b literal pool from a complete `.inkb` file using its index.
676/// Absent section (older-shaped buffer within the same version) decodes as
677/// empty, mirroring [`read_section_list_literals`].
678pub fn read_section_literal_pool(buf: &[u8], index: &InkbIndex) -> Result<Vec<Value>, DecodeError> {
679    let Some(range) = index.section_range(SectionKind::LiteralPool) else {
680        return Ok(Vec::new());
681    };
682    let mut off = range.start;
683    let count = read_u32(buf, &mut off)? as usize;
684    let mut pool = Vec::with_capacity(safe_capacity(count, buf.len(), off, 1));
685    for _ in 0..count {
686        pool.push(decode_value(buf, &mut off, 0)?);
687    }
688    Ok(pool)
689}
690
691/// Read the TM-4 `StructShapes` section from a complete `.inkb` file using
692/// its index. Absent section decodes as empty, mirroring
693/// [`read_section_literal_pool`].
694pub fn read_section_struct_shapes(
695    buf: &[u8],
696    index: &InkbIndex,
697) -> Result<Vec<StructShapeDef>, DecodeError> {
698    let Some(range) = index.section_range(SectionKind::StructShapes) else {
699        return Ok(Vec::new());
700    };
701    let mut off = range.start;
702    let count = read_u32(buf, &mut off)? as usize;
703    let mut shapes = Vec::with_capacity(safe_capacity(count, buf.len(), off, 8));
704    for _ in 0..count {
705        let id = ShapeId(read_u32(buf, &mut off)?);
706        let name = NameId(read_u16(buf, &mut off)?);
707        let field_count = read_u16(buf, &mut off)? as usize;
708        let mut fields = Vec::with_capacity(safe_capacity(field_count, buf.len(), off, 2));
709        for _ in 0..field_count {
710            fields.push(NameId(read_u16(buf, &mut off)?));
711        }
712        shapes.push(StructShapeDef { id, name, fields });
713    }
714    Ok(shapes)
715}
716
717/// Read the M-2b `Visibility` section (tag `0x0E`) from a complete `.inkb`
718/// file using its index — the `DefinitionId`s of every `#@private`
719/// definition. Absent section decodes as empty (the all-public common case),
720/// mirroring [`read_section_struct_shapes`].
721pub fn read_section_visibility(
722    buf: &[u8],
723    index: &InkbIndex,
724) -> Result<Vec<DefinitionId>, DecodeError> {
725    let Some(range) = index.section_range(SectionKind::Visibility) else {
726        return Ok(Vec::new());
727    };
728    let mut off = range.start;
729    let count = read_u32(buf, &mut off)? as usize;
730    let mut ids = Vec::with_capacity(safe_capacity(count, buf.len(), off, 4));
731    for _ in 0..count {
732        ids.push(read_def_id(buf, &mut off)?);
733    }
734    Ok(ids)
735}
736
737/// Read the M-3 `AliasTable` section (`docs/modules-spec.md` §5) from a
738/// complete `.inkb` file using its index. Absent section (a pre-M-3 file, or
739/// a story with no `#@was` directives) decodes as empty, mirroring
740/// [`read_section_literal_pool`]. The section-local version byte is checked
741/// independently of the whole-file `VERSION` — see [`ALIAS_TABLE_SECTION_VERSION`].
742pub fn read_section_alias_table(
743    buf: &[u8],
744    index: &InkbIndex,
745) -> Result<Vec<AliasEntry>, DecodeError> {
746    let Some(range) = index.section_range(SectionKind::AliasTable) else {
747        return Ok(Vec::new());
748    };
749    let mut off = range.start;
750    let section_version = read_u8(buf, &mut off)?;
751    if section_version != ALIAS_TABLE_SECTION_VERSION {
752        return Err(DecodeError::UnsupportedSectionVersion {
753            section: SectionKind::AliasTable as u8,
754            version: section_version,
755        });
756    }
757    let count = read_u32(buf, &mut off)? as usize;
758    let mut entries = Vec::with_capacity(safe_capacity(count, buf.len(), off, 16));
759    for _ in 0..count {
760        let old = read_def_id(buf, &mut off)?;
761        let new = read_def_id(buf, &mut off)?;
762        entries.push(AliasEntry { old, new });
763    }
764    Ok(entries)
765}
766
767/// Read the T2-3 `EffectRows` section (`docs/effects-spec.md` §11) from a
768/// complete `.inkb` file using its index. Absent section (converter output, or
769/// a story compiled before this slice) decodes as empty, mirroring
770/// [`read_section_alias_table`]. The section-local version byte is checked
771/// independently of the whole-file `VERSION` — see [`EFFECT_ROWS_SECTION_VERSION`].
772pub fn read_section_effect_rows(
773    buf: &[u8],
774    index: &InkbIndex,
775) -> Result<Vec<EffectRowEntry>, DecodeError> {
776    let Some(range) = index.section_range(SectionKind::EffectRows) else {
777        return Ok(Vec::new());
778    };
779    let mut off = range.start;
780    let section_version = read_u8(buf, &mut off)?;
781    if section_version != EFFECT_ROWS_SECTION_VERSION {
782        return Err(DecodeError::UnsupportedSectionVersion {
783            section: SectionKind::EffectRows as u8,
784            version: section_version,
785        });
786    }
787    let count = read_u32(buf, &mut off)? as usize;
788    // Minimum per-entry footprint: def_id(8) + is_entry(1) +
789    // direct(3×u32 counts + opaque + dims) + dispatch count(4) =
790    // 8 + 1 + 13 + 1 + 4 = 27 bytes.
791    let mut rows = Vec::with_capacity(safe_capacity(count, buf.len(), off, 27));
792    for _ in 0..count {
793        let def = read_def_id(buf, &mut off)?;
794        // #882 freeze bit — see `EffectRowEntry::is_entry`'s doc.
795        let is_entry = read_u8(buf, &mut off)? != 0;
796        let direct = decode_direct_effects(buf, &mut off)?;
797        let dispatch_count = read_u32(buf, &mut off)? as usize;
798        let mut dispatches = Vec::with_capacity(safe_capacity(dispatch_count, buf.len(), off, 13));
799        for _ in 0..dispatch_count {
800            let cell = read_def_id(buf, &mut off)?;
801            let narrowable = read_u8(buf, &mut off)? != 0;
802            let fallback = decode_direct_effects(buf, &mut off)?;
803            dispatches.push(DispatchEntry {
804                cell,
805                narrowable,
806                fallback,
807            });
808        }
809        rows.push(EffectRowEntry {
810            def,
811            is_entry,
812            direct,
813            dispatches,
814        });
815    }
816    Ok(rows)
817}
818
819/// Read the FS-3 `FrameShapes` section (`docs/flow-suspension-spec.md`
820/// §4/§11) from a complete `.inkb` file using its index. Absent section (every
821/// story compiled behind the E052 fence, and all converter output) decodes as
822/// empty, mirroring [`read_section_visibility`]. The section-local version byte
823/// is checked independently of the whole-file `VERSION` — see
824/// [`FRAME_SHAPES_SECTION_VERSION`].
825pub fn read_section_frame_shapes(
826    buf: &[u8],
827    index: &InkbIndex,
828) -> Result<Vec<FrameShapeDef>, DecodeError> {
829    let Some(range) = index.section_range(SectionKind::FrameShapes) else {
830        return Ok(Vec::new());
831    };
832    let mut off = range.start;
833    let section_version = read_u8(buf, &mut off)?;
834    if section_version != FRAME_SHAPES_SECTION_VERSION {
835        return Err(DecodeError::UnsupportedSectionVersion {
836            section: SectionKind::FrameShapes as u8,
837            version: section_version,
838        });
839    }
840    let count = read_u32(buf, &mut off)? as usize;
841    // Minimum per-entry footprint: site def_id(8) + slot count(4) = 12 bytes.
842    let mut shapes = Vec::with_capacity(safe_capacity(count, buf.len(), off, 12));
843    for _ in 0..count {
844        let site = read_def_id(buf, &mut off)?;
845        let slot_count = read_u32(buf, &mut off)? as usize;
846        let mut slots = Vec::with_capacity(safe_capacity(slot_count, buf.len(), off, 2));
847        for _ in 0..slot_count {
848            slots.push(NameId(read_u16(buf, &mut off)?));
849        }
850        shapes.push(FrameShapeDef { site, slots });
851    }
852    Ok(shapes)
853}
854
855/// Read the D6 `DebugInfo` section (`docs/debugger-spec.md` §2) from a
856/// complete `.inkb` file using its index. Absent section (every story
857/// compiled without the debug flag) decodes as `None`, distinct from the
858/// other optional sections above, which decode absence as an empty `Vec` —
859/// Decode the `LineVariantGroups` section (#3273). Absent section decodes
860/// as an empty vec — the section is omitted-when-empty by contract, so
861/// absence and emptiness are the same statement.
862pub fn read_section_line_variant_groups(
863    buf: &[u8],
864    index: &InkbIndex,
865) -> Result<Vec<crate::definition::LineVariantGroup>, DecodeError> {
866    let Some(range) = index.section_range(SectionKind::LineVariantGroups) else {
867        return Ok(Vec::new());
868    };
869    let mut off = range.start;
870    let section_version = read_u8(buf, &mut off)?;
871    if section_version != LINE_VARIANT_GROUPS_SECTION_VERSION {
872        return Err(DecodeError::UnsupportedSectionVersion {
873            section: SectionKind::LineVariantGroups as u8,
874            version: section_version,
875        });
876    }
877    let count = read_u32(buf, &mut off)? as usize;
878    // Minimum per-group footprint: scope_id(8) + base(4) + dim-count(1)
879    // + one dim(2) = 15 bytes.
880    let mut groups = Vec::with_capacity(safe_capacity(count, buf.len(), off, 15));
881    for _ in 0..count {
882        let scope_id = read_def_id(buf, &mut off)?;
883        let base = read_u32(buf, &mut off)?;
884        let dim_count = read_u8(buf, &mut off)? as usize;
885        let mut dims = Vec::with_capacity(safe_capacity(dim_count, buf.len(), off, 2));
886        for _ in 0..dim_count {
887            dims.push(read_u16(buf, &mut off)?);
888        }
889        groups.push(crate::definition::LineVariantGroup {
890            scope_id,
891            base,
892            dims,
893        });
894    }
895    Ok(groups)
896}
897
898/// see [`crate::StoryData::debug_info`]'s doc for why presence itself is
899/// meaningful here.
900///
901/// Reserved `flags` bits are read through unmodified, never rejected — the
902/// section's explicit, ruled departure from this format's default
903/// strict-rejection posture (§2.2's "reserved-bit forward compatibility").
904/// A future revision needing genuinely new per-entry bytes still bumps
905/// [`DEBUG_INFO_SECTION_VERSION`] like any other section-local encoding
906/// change; nothing about this leniency exempts that.
907pub fn read_section_debug_info(
908    buf: &[u8],
909    index: &InkbIndex,
910) -> Result<Option<DebugInfoSection>, DecodeError> {
911    let Some(range) = index.section_range(SectionKind::DebugInfo) else {
912        return Ok(None);
913    };
914    let mut off = range.start;
915    let section_version = read_u8(buf, &mut off)?;
916    if section_version != DEBUG_INFO_SECTION_VERSION {
917        return Err(DecodeError::UnsupportedSectionVersion {
918            section: SectionKind::DebugInfo as u8,
919            version: section_version,
920        });
921    }
922
923    let file_count = read_u32(buf, &mut off)? as usize;
924    // Minimum per-entry footprint (#3261): surface(1) + path length
925    // prefix(4) + source_hash(8) + line-count varint(1) = 14 bytes.
926    let mut files = Vec::with_capacity(safe_capacity(file_count, buf.len(), off, 14));
927    for _ in 0..file_count {
928        let surface = FileSurface::from_u8(read_u8(buf, &mut off)?)?;
929        let path = read_str(buf, &mut off)?;
930        let source_hash = read_u64(buf, &mut off)?;
931        // A count that cannot fit in `usize` cannot be satisfied by any
932        // buffer we could be holding — malformed input, not a truncation.
933        let line_count =
934            usize::try_from(read_varint(buf, &mut off)?).map_err(|_| DecodeError::UnexpectedEof)?;
935        // Minimum per-line footprint: one varint byte.
936        let mut line_starts = Vec::with_capacity(safe_capacity(line_count, buf.len(), off, 1));
937        let mut prev: u32 = 0;
938        for _ in 0..line_count {
939            let delta = u32::try_from(read_varint(buf, &mut off)?)
940                .map_err(|_| DecodeError::UnexpectedEof)?;
941            // `wrapping_add` mirrors the entry table's decode tolerance: a
942            // malformed artifact yields nonsense offsets, never a panic.
943            prev = prev.wrapping_add(delta);
944            line_starts.push(prev);
945        }
946        files.push(DebugFileEntry {
947            surface,
948            path,
949            source_hash,
950            line_starts,
951        });
952    }
953
954    let container_count = read_u32(buf, &mut off)? as usize;
955    // Minimum per-container footprint: entry_count varint(1) + locals_count
956    // varint(1) = 2 bytes (both tables may legitimately be empty).
957    let mut containers = Vec::with_capacity(safe_capacity(container_count, buf.len(), off, 2));
958    for _ in 0..container_count {
959        // A count that cannot fit in `usize` cannot be satisfied by any
960        // buffer we could be holding — treat it as malformed input rather
961        // than truncating it into a plausible-looking small number.
962        let entry_count =
963            usize::try_from(read_varint(buf, &mut off)?).map_err(|_| DecodeError::UnexpectedEof)?;
964        // Minimum per-entry footprint: 4 varints of at least 1 byte each +
965        // kind_token u32(4) + flags u8(1) = 9 bytes.
966        let mut entries = Vec::with_capacity(safe_capacity(entry_count, buf.len(), off, 9));
967        let mut prev_offset: u32 = 0;
968        for _ in 0..entry_count {
969            #[expect(
970                clippy::cast_possible_truncation,
971                reason = "wire fields are u32-domain values re-widened to u64 for varint transport"
972            )]
973            let delta = read_varint(buf, &mut off)? as u32;
974            let bytecode_offset = prev_offset.wrapping_add(delta);
975            prev_offset = bytecode_offset;
976            #[expect(clippy::cast_possible_truncation)]
977            let file_idx = read_varint(buf, &mut off)? as u32;
978            #[expect(clippy::cast_possible_truncation)]
979            let range_start = read_varint(buf, &mut off)? as u32;
980            #[expect(clippy::cast_possible_truncation)]
981            let range_len = read_varint(buf, &mut off)? as u32;
982            let kind_token = read_u32(buf, &mut off)?;
983            // Reserved bits are carried through as-is — never masked,
984            // never rejected (§2.2's reserved-bit tolerance contract).
985            let flags = read_u8(buf, &mut off)?;
986            entries.push(DebugEntry {
987                bytecode_offset,
988                file_idx,
989                range_start,
990                range_len,
991                kind_token,
992                flags,
993            });
994        }
995
996        let local_count =
997            usize::try_from(read_varint(buf, &mut off)?).map_err(|_| DecodeError::UnexpectedEof)?;
998        // Minimum per-local footprint: slot u16(2) + name length prefix
999        // u32(4) + has_range u8(1) = 7 bytes.
1000        let mut locals = Vec::with_capacity(safe_capacity(local_count, buf.len(), off, 7));
1001        for _ in 0..local_count {
1002            let slot = read_u16(buf, &mut off)?;
1003            let name = read_str(buf, &mut off)?;
1004            // Flags byte (section version 2, #3395): bit 0 = a declaring
1005            // range follows, bit 1 = synthetic. Strict on the reserved bits,
1006            // like `DirectEffects`' extension-flags byte.
1007            let flags = read_u8(buf, &mut off)?;
1008            if flags & !LOCAL_FLAGS_KNOWN != 0 {
1009                return Err(DecodeError::InvalidDebugLocalFlags(flags));
1010            }
1011            let synthetic = flags & LOCAL_FLAG_SYNTHETIC != 0;
1012            let declaring_range = if flags & LOCAL_FLAG_HAS_RANGE == 0 {
1013                None
1014            } else {
1015                #[expect(clippy::cast_possible_truncation)]
1016                let file_idx = read_varint(buf, &mut off)? as u32;
1017                #[expect(clippy::cast_possible_truncation)]
1018                let range_start = read_varint(buf, &mut off)? as u32;
1019                #[expect(clippy::cast_possible_truncation)]
1020                let range_len = read_varint(buf, &mut off)? as u32;
1021                Some((file_idx, range_start, range_len))
1022            };
1023            locals.push(DebugLocalEntry {
1024                slot,
1025                name,
1026                declaring_range,
1027                synthetic,
1028            });
1029        }
1030
1031        containers.push(DebugContainerTable { entries, locals });
1032    }
1033
1034    Ok(Some(DebugInfoSection { files, containers }))
1035}
1036
1037/// Decode a [`DirectEffects`] block written by `encode_direct_effects`.
1038fn decode_direct_effects(buf: &[u8], off: &mut usize) -> Result<DirectEffects, DecodeError> {
1039    let read_count = read_u32(buf, off)? as usize;
1040    let mut reads = Vec::with_capacity(safe_capacity(read_count, buf.len(), *off, 8));
1041    for _ in 0..read_count {
1042        reads.push(read_def_id(buf, off)?);
1043    }
1044    let write_count = read_u32(buf, off)? as usize;
1045    let mut writes = Vec::with_capacity(safe_capacity(write_count, buf.len(), *off, 8));
1046    for _ in 0..write_count {
1047        writes.push(read_def_id(buf, off)?);
1048    }
1049    let call_count = read_u32(buf, off)? as usize;
1050    let mut calls = Vec::with_capacity(safe_capacity(call_count, buf.len(), *off, 4));
1051    for _ in 0..call_count {
1052        calls.push(decode_call_atom(buf, off)?);
1053    }
1054    let opaque = read_u8(buf, off)? != 0;
1055    // NS-A2 extension-flags byte (section version 3): the strict reader
1056    // rejects reserved bits (3–7) until a section version graduates them —
1057    // the same reservation discipline the capability/handle slots follow.
1058    let dims = read_u8(buf, off)?;
1059    if dims & !super::EFFECT_DIM_KNOWN_MASK != 0 {
1060        return Err(DecodeError::InvalidEffectDimensions(dims));
1061    }
1062    Ok(DirectEffects {
1063        reads,
1064        writes,
1065        calls,
1066        opaque,
1067        emits: dims & super::EFFECT_DIM_EMITS != 0,
1068        tags: dims & super::EFFECT_DIM_TAGS != 0,
1069        faults: dims & super::EFFECT_DIM_FAULTS != 0,
1070    })
1071}
1072
1073/// Decode a single [`CallAtom`] written by `encode_call_atom`. The strict
1074/// reader rejects a non-`Any` capability tag (path-granular is reserved, #826)
1075/// and a non-`None` handle-parameter slot (reserved, `docs/t1d-spec.md` §7) —
1076/// the same reservation discipline the projection range segment follows.
1077fn decode_call_atom(buf: &[u8], off: &mut usize) -> Result<CallAtom, DecodeError> {
1078    let name = NameId(read_u16(buf, off)?);
1079    let cap_tag = read_u8(buf, off)?;
1080    let capability = match cap_tag {
1081        CAP_PARAM_ANY => CapabilityParam::Any,
1082        other => return Err(DecodeError::InvalidEffectCapParam(other)),
1083    };
1084    let handle_tag = read_u8(buf, off)?;
1085    if handle_tag != HANDLE_PARAM_NONE {
1086        return Err(DecodeError::InvalidEffectHandleParam(handle_tag));
1087    }
1088    Ok(CallAtom {
1089        name,
1090        capability,
1091        handle_param: None,
1092    })
1093}
1094
1095fn decode_external(buf: &[u8], off: &mut usize) -> Result<ExternalFnDef, DecodeError> {
1096    let id = read_def_id(buf, off)?;
1097    let name = NameId(read_u16(buf, off)?);
1098    let arg_count = read_u8(buf, off)?;
1099    let has_fallback = read_u8(buf, off)? != 0;
1100    let fallback = if has_fallback {
1101        Some(read_def_id(buf, off)?)
1102    } else {
1103        None
1104    };
1105    Ok(ExternalFnDef {
1106        id,
1107        name,
1108        arg_count,
1109        fallback,
1110    })
1111}
1112
1113fn decode_container(buf: &[u8], off: &mut usize) -> Result<ContainerDef, DecodeError> {
1114    let id = read_def_id(buf, off)?;
1115    let scope_id = read_def_id(buf, off)?;
1116    let has_name = read_u8(buf, off)? != 0;
1117    let name = if has_name {
1118        Some(NameId(read_u16(buf, off)?))
1119    } else {
1120        None
1121    };
1122    let counting_bits = read_u8(buf, off)?;
1123    let counting_flags = CountingFlags::from_bits(counting_bits).unwrap_or(CountingFlags::empty());
1124    let path_hash = read_i32(buf, off)?;
1125    let param_count = read_u8(buf, off)?;
1126    let local = read_u8(buf, off)? != 0;
1127    // Per-param name/mode metadata (T1c, `docs/t1c-spec.md` §6).
1128    let param_meta_count = read_u16(buf, off)? as usize;
1129    let mut params = Vec::with_capacity(safe_capacity(param_meta_count, buf.len(), *off, 3));
1130    for _ in 0..param_meta_count {
1131        let name = NameId(read_u16(buf, off)?);
1132        let is_ref = read_u8(buf, off)? != 0;
1133        let slot = read_u16(buf, off)?;
1134        params.push(ParamMeta { name, is_ref, slot });
1135    }
1136    // `ContainerDef::params`'s doc invariant: `params.len()` always equals
1137    // `param_count` whenever per-param metadata is present at all (empty
1138    // `params` is the separate, legitimate "count only, no metadata" case).
1139    // A mutated `.inkb` asserting otherwise is malformed input, not
1140    // silently-acceptable data — mirrors the `.inkt` reader's guard (#745,
1141    // #954), rejecting with a decode error rather than constructing an
1142    // inconsistent `ContainerDef`.
1143    if !params.is_empty() && params.len() != usize::from(param_count) {
1144        return Err(DecodeError::ParamCountMismatch {
1145            declared: param_count,
1146            actual: params.len(),
1147        });
1148    }
1149
1150    let bytecode_len = read_u32(buf, off)? as usize;
1151    if *off + bytecode_len > buf.len() {
1152        return Err(DecodeError::UnexpectedEof);
1153    }
1154    let bytecode = buf[*off..*off + bytecode_len].to_vec();
1155    *off += bytecode_len;
1156
1157    Ok(ContainerDef {
1158        id,
1159        scope_id,
1160        name,
1161        bytecode,
1162        counting_flags,
1163        path_hash,
1164        param_count,
1165        params,
1166        local,
1167    })
1168}
1169
1170/// Read the line tables from a complete `.inkb` file using its index.
1171pub fn read_section_line_tables(
1172    buf: &[u8],
1173    index: &InkbIndex,
1174) -> Result<Vec<ScopeLineTable>, DecodeError> {
1175    let range =
1176        index
1177            .section_range(SectionKind::LineTables)
1178            .ok_or(DecodeError::MissingSectionKind(
1179                SectionKind::LineTables as u8,
1180            ))?;
1181    let mut off = range.start;
1182    let count = read_u32(buf, &mut off)? as usize;
1183    let mut tables = Vec::with_capacity(safe_capacity(count, buf.len(), off, 12));
1184    for _ in 0..count {
1185        tables.push(decode_scope_line_table(buf, &mut off)?);
1186    }
1187    Ok(tables)
1188}
1189
1190fn decode_scope_line_table(buf: &[u8], off: &mut usize) -> Result<ScopeLineTable, DecodeError> {
1191    let scope_id = read_def_id(buf, off)?;
1192    let line_count = read_u32(buf, off)? as usize;
1193    let mut lines = Vec::with_capacity(safe_capacity(line_count, buf.len(), *off, 9));
1194    for _ in 0..line_count {
1195        lines.push(decode_line_entry(buf, off)?);
1196    }
1197    Ok(ScopeLineTable { scope_id, lines })
1198}
1199
1200fn decode_line_entry(buf: &[u8], off: &mut usize) -> Result<LineEntry, DecodeError> {
1201    let content = decode_line_content(buf, off)?;
1202    let source_hash = read_u64(buf, off)?;
1203    let has_audio = read_u8(buf, off)? != 0;
1204    let audio_ref = if has_audio {
1205        Some(read_str(buf, off)?)
1206    } else {
1207        None
1208    };
1209    // Slot info
1210    let slot_count = read_u8(buf, off)? as usize;
1211    let mut slot_info = Vec::with_capacity(slot_count);
1212    for _ in 0..slot_count {
1213        let index = read_u8(buf, off)?;
1214        let name = read_str(buf, off)?;
1215        slot_info.push(SlotInfo { index, name });
1216    }
1217
1218    // Source location
1219    let has_source_loc = read_u8(buf, off)? != 0;
1220    let source_location = if has_source_loc {
1221        let file = read_str(buf, off)?;
1222        let range_start = read_u32(buf, off)?;
1223        let range_end = read_u32(buf, off)?;
1224        Some(SourceLocation {
1225            file,
1226            range_start,
1227            range_end,
1228        })
1229    } else {
1230        None
1231    };
1232
1233    let flags = crate::LineFlags::from_content(&content);
1234    Ok(LineEntry {
1235        content,
1236        flags,
1237        source_hash,
1238        audio_ref,
1239        slot_info,
1240        source_location,
1241    })
1242}
1243
1244pub(crate) fn decode_line_content(buf: &[u8], off: &mut usize) -> Result<LineContent, DecodeError> {
1245    let tag = read_u8(buf, off)?;
1246    match tag {
1247        LINE_PLAIN => Ok(LineContent::Plain(read_str(buf, off)?)),
1248        LINE_TEMPLATE => {
1249            let part_count = read_u32(buf, off)? as usize;
1250            let mut parts = Vec::with_capacity(safe_capacity(part_count, buf.len(), *off, 2));
1251            for _ in 0..part_count {
1252                parts.push(decode_line_part(buf, off, 0)?);
1253            }
1254            Ok(LineContent::Template(parts))
1255        }
1256        _ => Err(DecodeError::InvalidLineContent(tag)),
1257    }
1258}
1259
1260/// `depth` guards against a crafted file of deeply nested `LinePart::Span`s
1261/// (#1716) blowing the stack — the same `MAX_DECODE_DEPTH` cap
1262/// `decode_value` enforces for `VAL_ARRAY`/`VAL_MAP`/etc., since `Span` is
1263/// now the one `LinePart` shape that recurses.
1264fn decode_line_part(buf: &[u8], off: &mut usize, depth: usize) -> Result<LinePart, DecodeError> {
1265    if depth > MAX_DECODE_DEPTH {
1266        return Err(DecodeError::MaxDepthExceeded(MAX_DECODE_DEPTH));
1267    }
1268    let tag = read_u8(buf, off)?;
1269    match tag {
1270        PART_LITERAL => Ok(LinePart::Literal(read_str(buf, off)?)),
1271        PART_SLOT => Ok(LinePart::Slot(read_u8(buf, off)?)),
1272        PART_SELECT => {
1273            let slot = read_u8(buf, off)?;
1274            let variant_count = read_u32(buf, off)? as usize;
1275            let mut variants = Vec::with_capacity(safe_capacity(variant_count, buf.len(), *off, 6));
1276            for _ in 0..variant_count {
1277                let key = decode_select_key(buf, off)?;
1278                let text = read_str(buf, off)?;
1279                variants.push((key, text));
1280            }
1281            let default = read_str(buf, off)?;
1282            Ok(LinePart::Select {
1283                slot,
1284                variants,
1285                default,
1286            })
1287        }
1288        PART_SPAN => {
1289            let name = read_str(buf, off)?;
1290            let attr_count = read_u32(buf, off)? as usize;
1291            // Each attr is two `write_str`-encoded strings, minimum 4 bytes
1292            // (an empty string's length prefix) apiece.
1293            let attrs_cap = safe_capacity(attr_count, buf.len(), *off, 8);
1294            let mut attrs = Vec::with_capacity(attrs_cap);
1295            for _ in 0..attr_count {
1296                let k = read_str(buf, off)?;
1297                let v = read_str(buf, off)?;
1298                attrs.push((k, v));
1299            }
1300            let child_count = read_u32(buf, off)? as usize;
1301            let mut children = Vec::with_capacity(safe_capacity(child_count, buf.len(), *off, 2));
1302            for _ in 0..child_count {
1303                children.push(decode_line_part(buf, off, depth + 1)?);
1304            }
1305            Ok(LinePart::Span {
1306                name,
1307                attrs,
1308                children,
1309            })
1310        }
1311        _ => Err(DecodeError::InvalidLinePart(tag)),
1312    }
1313}
1314
1315fn decode_select_key(buf: &[u8], off: &mut usize) -> Result<SelectKey, DecodeError> {
1316    let tag = read_u8(buf, off)?;
1317    match tag {
1318        KEY_CARDINAL => Ok(SelectKey::Cardinal(decode_plural_category(buf, off)?)),
1319        KEY_ORDINAL => Ok(SelectKey::Ordinal(decode_plural_category(buf, off)?)),
1320        KEY_EXACT => Ok(SelectKey::Exact(read_i32(buf, off)?)),
1321        KEY_KEYWORD => Ok(SelectKey::Keyword(read_str(buf, off)?)),
1322        _ => Err(DecodeError::InvalidSelectKey(tag)),
1323    }
1324}
1325
1326fn decode_plural_category(buf: &[u8], off: &mut usize) -> Result<PluralCategory, DecodeError> {
1327    let tag = read_u8(buf, off)?;
1328    match tag {
1329        CAT_ZERO => Ok(PluralCategory::Zero),
1330        CAT_ONE => Ok(PluralCategory::One),
1331        CAT_TWO => Ok(PluralCategory::Two),
1332        CAT_FEW => Ok(PluralCategory::Few),
1333        CAT_MANY => Ok(PluralCategory::Many),
1334        CAT_OTHER => Ok(PluralCategory::Other),
1335        _ => Err(DecodeError::InvalidPluralCategory(tag)),
1336    }
1337}
1338
1339#[cfg(test)]
1340mod tests {
1341    use super::*;
1342    use crate::codec::{write_u8, write_u32};
1343
1344    /// Hand-build a `VAL_MAP` payload carrying the same `int` key twice — no
1345    /// legitimate encoder emits this (`OrderedMap::insert` de-duplicates on
1346    /// the write side), so this is the "crafted payload" scenario issue #985
1347    /// guards against: the reader must reject it with a decode error, never
1348    /// construct an `OrderedMap` that violates the content-based `Eq`
1349    /// invariant (#909) by silently keeping the last occurrence.
1350    fn duplicate_int_key_map_bytes() -> Vec<u8> {
1351        let mut buf = Vec::new();
1352        write_u8(&mut buf, VAL_MAP);
1353        write_u32(&mut buf, 2); // two entries
1354        // entry 0: key = int(0), value = int(1)
1355        write_u8(&mut buf, VAL_INT);
1356        buf.extend_from_slice(&0i32.to_le_bytes());
1357        write_u8(&mut buf, VAL_INT);
1358        buf.extend_from_slice(&1i32.to_le_bytes());
1359        // entry 1: key = int(0) again, value = int(2)
1360        write_u8(&mut buf, VAL_INT);
1361        buf.extend_from_slice(&0i32.to_le_bytes());
1362        write_u8(&mut buf, VAL_INT);
1363        buf.extend_from_slice(&2i32.to_le_bytes());
1364        buf
1365    }
1366
1367    #[test]
1368    fn decode_value_rejects_duplicate_map_key() {
1369        let buf = duplicate_int_key_map_bytes();
1370        let mut off = 0;
1371        assert_eq!(
1372            decode_value(&buf, &mut off, 0),
1373            Err(DecodeError::DuplicateMapKey)
1374        );
1375    }
1376
1377    #[test]
1378    fn decode_value_accepts_distinct_map_keys() {
1379        let mut buf = Vec::new();
1380        write_u8(&mut buf, VAL_MAP);
1381        write_u32(&mut buf, 2);
1382        write_u8(&mut buf, VAL_INT);
1383        buf.extend_from_slice(&0i32.to_le_bytes());
1384        write_u8(&mut buf, VAL_INT);
1385        buf.extend_from_slice(&1i32.to_le_bytes());
1386        write_u8(&mut buf, VAL_INT);
1387        buf.extend_from_slice(&5i32.to_le_bytes());
1388        write_u8(&mut buf, VAL_INT);
1389        buf.extend_from_slice(&2i32.to_le_bytes());
1390
1391        let mut off = 0;
1392        let value = decode_value(&buf, &mut off, 0).expect("distinct keys decode cleanly");
1393        let Value::Map(map) = value else {
1394            unreachable!("expected a map value");
1395        };
1396        assert_eq!(map.len(), 2);
1397    }
1398}