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