brink_format/inkb/mod.rs
1//! Binary (.inkb) writer and reader for [`StoryData`].
2//!
3//! The `.inkb` format is a compact, little-endian binary encoding designed for
4//! fast loading by the runtime.
5//!
6//! ## Header layout
7//!
8//! ```text
9//! Offset Size Field
10//! ------ ----- ------
11//! 0 4 Magic: b"INKB"
12//! 4 2 Version: u16 LE (= 1)
13//! 6 1 Section count: u8 (N entries in offset table)
14//! 7 1 Reserved: 0x00
15//! 8 4 File size: u32 LE (total bytes)
16//! 12 4 Content checksum: u32 LE (CRC-32 of all bytes after header)
17//! 16 N*8 Offset table entries
18//! ```
19//!
20//! Each offset table entry (8 bytes):
21//! ```text
22//! 0 1 SectionKind: u8 tag
23//! 1 3 Reserved: 3 bytes of 0x00
24//! 4 4 Offset: u32 LE (byte offset from start of file)
25//! ```
26
27pub(crate) mod read;
28pub(crate) mod write;
29
30pub use read::{
31 read_inkb, read_inkb_index, read_section_address_paths, read_section_addresses,
32 read_section_alias_table, read_section_containers, read_section_debug_info,
33 read_section_effect_rows, read_section_externals, read_section_frame_shapes,
34 read_section_line_tables, read_section_list_defs, read_section_list_items,
35 read_section_list_literals, read_section_literal_pool, read_section_name_table,
36 read_section_struct_shapes, read_section_variables, read_section_visibility,
37};
38pub use write::{
39 assemble_inkb, write_inkb, write_section_address_paths, write_section_addresses,
40 write_section_alias_table, write_section_containers, write_section_debug_info,
41 write_section_effect_rows, write_section_externals, write_section_frame_shapes,
42 write_section_line_tables, write_section_list_defs, write_section_list_items,
43 write_section_list_literals, write_section_literal_pool, write_section_name_table,
44 write_section_struct_shapes, write_section_variables, write_section_visibility,
45};
46
47use core::ops::Range;
48
49use alloc::vec::Vec;
50
51use crate::opcode::DecodeError;
52
53// ── Constants ───────────────────────────────────────────────────────────────
54
55pub(crate) const MAGIC: &[u8; 4] = b"INKB";
56/// On-the-wire format version. Bumped on any byte-layout change; the reader
57/// hard-rejects an unrecognized version (see `docs/format-spec.md` § Versioning).
58/// v2 added `ContainerDef::param_count` to the Containers section.
59/// v3 added the `local` scope bit to `GlobalVarDef` (Variables section) and
60/// `ContainerDef` (Containers section) — see `docs/directive-annotations-spec.md`.
61/// v4 added the collection value tags `VAL_ARRAY`/`VAL_MAP` (tree encoding) and
62/// froze the reserved Tier-1 value-tag/section/opcode surface (the §9 one-bump
63/// rule of `docs/value-model-spec.md`) — see `docs/format-v4-rfc.md`.
64/// (Also on the v4 line: the optional `Visibility` section, M-2b,
65/// `docs/modules-spec.md` §4, tag `0x0E` — omitted when empty, so it didn't
66/// need a bump of its own.)
67/// v5 added the `AliasTable` section (`docs/modules-spec.md` §5, M-3): this
68/// section was not part of the v4 RFC's frozen inventory (unlike
69/// `StructShapes`/`EffectRows`, which were pre-reserved and only needed
70/// their *encoding* materialized without a bump), so a brand-new *mandatory*
71/// section is its own one-bump event. The section itself carries a
72/// one-byte section-local version so its *row encoding* can still evolve
73/// without a further format bump, matching the `EffectRows` precedent.
74/// `AliasTable` takes tag `0x0F` (the next free tag after `Visibility`).
75/// v6 added the `PART_SPAN` `LinePart` tag (#1716, `docs/prose-dialect-spec.md`
76/// §4.4/§4.5): like `AliasTable`, `PART_SPAN` was not part of the v4 RFC's
77/// pre-reserved inventory (that inventory covers `VAL_ARRAY`/`VAL_MAP`-style
78/// *value* tags and the `StructShapes`/`EffectRows` sections — it never
79/// reserved a `LinePart` tag), so introducing it is its own one-bump event,
80/// not a free ride on the `VAL_VEC2`/`VAL_WEIGHTED` no-bump precedent (see
81/// `PART_SPAN`'s doc comment below). Ruled directly by issue #1716's own
82/// ⚠ ("`LinePart::Span` is a v6 payload") and coordinated with #1683 (the
83/// v6 bump manifest); this PR lands only the `Span` payload of that
84/// manifest, so `VERSION` 6 stays open to absorb #1683's remaining payloads
85/// (element kind/data, universal block id, choice captured environment)
86/// without a further bump, the same way v4 absorbed its later Tier-1
87/// milestones — a single bump event, not a bump per payload.
88/// v7 added the first peephole superinstruction, `EmitLineNl` (`0x6C`,
89/// `docs/optimizer-peephole.md`): a brand-new opcode discriminant, not one of
90/// the reserved ones, so — like `PART_SPAN` — its own one-bump event. The
91/// optimizer is the only producer; codegen output is unchanged, and the
92/// reader still hard-rejects the previous version rather than translating.
93/// v8 added the second superinstruction family — `BinaryImm` (`0x6D`),
94/// `BinaryJumpIfFalse` (`0x6E`), `BinaryImmJumpIfFalse` (`0x6F`), each with a
95/// `BinaryKind` operator byte — under the same one-bump-per-new-opcode-event
96/// rule and the same optimizer-only provenance.
97/// v9 added the third family — `GetTempBinaryImm` (`0x70`),
98/// `GetTempBinaryImmJumpIfFalse` (`0x71`), `DuplicateBinaryImmJumpIfFalse`
99/// (`0x74`): the v8 forms with their left operand's producer folded in.
100/// v10 removed the parameter-binding prologue: codegen no longer emits a
101/// leading `DeclareTemp` run for a container's parameters, because the VM
102/// binds them into the frame at every entry instead
103/// (`docs/compiler-spec.md` §"Parameter binding"). Unlike v7-v9 this is not
104/// a new opcode — it is a change of meaning for existing bytecode, and the
105/// dangerous direction is a v9 artifact read by a v10 runtime: its prologue
106/// would decode perfectly and re-bind parameters from an empty stack. The
107/// bump is what turns that into a hard rejection.
108pub(crate) const VERSION: u16 = 10;
109/// Fixed-size preamble: magic + version + section count + reserved + file size + checksum.
110pub(crate) const HEADER_PREAMBLE: usize = 16;
111/// Each offset table entry: kind(1) + reserved(3) + offset(4)
112pub(crate) const SECTION_ENTRY_SIZE: usize = 8;
113/// Number of *mandatory* sections in the current format (always present,
114/// including the possibly-empty `AliasTable` and `EffectRows`). The optional
115/// `Visibility` section (M-2b) adds one more entry to the offset table when
116/// non-empty.
117pub(crate) const SECTION_COUNT: u8 = 14;
118
119// Value type tags
120pub(crate) const VAL_INT: u8 = 0x00;
121pub(crate) const VAL_FLOAT: u8 = 0x01;
122pub(crate) const VAL_BOOL: u8 = 0x02;
123pub(crate) const VAL_STRING: u8 = 0x03;
124pub(crate) const VAL_LIST: u8 = 0x04;
125pub(crate) const VAL_DIVERT_TARGET: u8 = 0x05;
126pub(crate) const VAL_NULL: u8 = 0x06;
127pub(crate) const VAL_VAR_POINTER: u8 = 0x07;
128pub(crate) const VAL_FRAGMENT_REF: u8 = 0x08;
129// v4 collection tags (`docs/format-v4-rfc.md` §1). Tree encoding: sharing is
130// not preserved on the wire — a snapshot serializes as a plain nested tree.
131pub(crate) const VAL_ARRAY: u8 = 0x09;
132pub(crate) const VAL_MAP: u8 = 0x0A;
133// TM-4 (`docs/typed-mode-spec.md` §6 / `docs/value-model-spec.md` §11c):
134// closed-shape records. `docs/format-v4-rfc.md` §1: `ShapeId (u32 into
135// StructShapes), then field values in shape order`.
136pub(crate) const VAL_RECORD: u8 = 0x0F;
137// T1c (`docs/t1c-spec.md` §6, `docs/format-v4-rfc.md` §1): function values.
138// `VAL_FN_REF` = the zero-bound case (a `DefinitionId`); `VAL_CLOSURE` =
139// `DefinitionId`, u16 env count, then env entries `{NameId, kind u8 (0=val,
140// 1=ref), value}`. Numeric assignments were frozen by the one-bump rule; this
141// PR (T1c-2) materializes them.
142pub(crate) const VAL_FN_REF: u8 = 0x0B;
143pub(crate) const VAL_CLOSURE: u8 = 0x0C;
144// T1d (`docs/t1d-spec.md` §2, `docs/format-v4-rfc.md` §1): opaque
145// host-resource tokens. `kind NameId, u64 id` — no live pointer, no
146// dedicated opcode; handles enter the script world only via bindings.
147pub(crate) const VAL_HANDLE: u8 = 0x0D;
148// T1e (`docs/t1e-spec.md` §3, `docs/format-v4-rfc.md` §1): symbolic path
149// projections. `cell reference (= VAL_VAR_POINTER payload shape), u8 segment
150// count, then segments (u8 kind: 0=index i32 / 1=key value)`. Segment kind
151// `2=range` is RESERVED — never emitted (icebox #829). First emission of
152// this reserved tag.
153pub(crate) const VAL_PROJECTION: u8 = 0x0E;
154// NS-A1 (`docs/stdlib-spec.md` §1.1/§1.4, ruled 2026-07-18): the compiler-
155// owned `Option[T]` enum. Wire form: one flag byte (0 = `none`, 1 =
156// `some`), then the inner value when `some` — the enum's two variants,
157// nothing more. Next free tag after `VAL_RECORD` (0x0F); this PR's own
158// reservation, same "assigned here" precedent as the record/handle/
159// projection tags above. Recursion counts toward `MAX_DECODE_DEPTH`
160// exactly like the collection tags (a crafted chain of nested `some`s is
161// the same stack-overflow shape as nested single-element arrays).
162pub(crate) const VAL_OPTION: u8 = 0x10;
163// NS-A5 (`docs/stdlib-spec.md` §7, F7 ruled 2026-07-19): the integer range
164// value kind. Wire form: start i32, end i32, one flag byte (0 = `..`
165// exclusive, 1 = `..=` inclusive) — flat, no recursion, so it does NOT
166// count toward `MAX_DECODE_DEPTH` (a range holds two ints, never another
167// value). Next free tag after `VAL_OPTION` (0x10); same "assigned here"
168// reservation precedent as its neighbors. Distinct from the RESERVED
169// projection-*segment* kind 0x02 below — that is a different namespace.
170pub(crate) const VAL_RANGE: u8 = 0x11;
171// NS-A8 (`docs/tower-mini-spec.md` T5, issue #1114): the numeric tower.
172// Wire form is hand-serialized **explicit little-endian f32 lanes** — never
173// glam's memory layout (it varies with SIMD features and versions) and never
174// serde-through-glam. Lane order: vectors and the quat `x, y(, z, w)`;
175// matrices column-major, column-by-column. Fixed payload sizes (no counts,
176// no recursion — tower values are leaves, so like `VAL_RANGE` they do NOT
177// count toward `MAX_DECODE_DEPTH`): vec2 = 8 bytes, vec3 = 12,
178// vec4/quat/mat2 = 16, mat3 = 36, mat4 = 64. Next free tags after
179// `VAL_RANGE` (0x11); this PR's own reservation, the same "assigned here"
180// precedent as the record/handle/projection/option/range tags above. No
181// format `VERSION` bump — additive value tags follow the NS-A1 `VAL_OPTION`
182// precedent (an old reader rejects the unknown tag; an old file simply
183// never contains one).
184pub(crate) const VAL_VEC2: u8 = 0x12;
185pub(crate) const VAL_VEC3: u8 = 0x13;
186pub(crate) const VAL_VEC4: u8 = 0x14;
187pub(crate) const VAL_QUAT: u8 = 0x15;
188pub(crate) const VAL_MAT2: u8 = 0x16;
189pub(crate) const VAL_MAT3: u8 = 0x17;
190pub(crate) const VAL_MAT4: u8 = 0x18;
191// NS-A7 (`docs/stdlib-spec.md` §8, issue #1113): the weighted table.
192// Wire form: u32 entry count, then per entry an i32 weight followed by a
193// recursively-encoded value. Values recurse, so decoding counts toward
194// `MAX_DECODE_DEPTH` exactly like the collection tags. The reader enforces
195// the §8 evidence-by-construction invariant (non-empty, weights ≥ 1) — a
196// violating payload is a decode error, so a `Weighted` never enters the
197// runtime invalid, even from a crafted file. Next free tag after
198// `VAL_MAT4` (0x18); this PR's own reservation, additive per the NS-A1
199// `VAL_OPTION` precedent (no `VERSION` bump).
200pub(crate) const VAL_WEIGHTED: u8 = 0x19;
201/// Wire kind for a [`crate::ProjSegment::Index`] segment.
202pub(crate) const PROJ_SEG_INDEX: u8 = 0x00;
203/// Wire kind for a [`crate::ProjSegment::Key`] segment.
204pub(crate) const PROJ_SEG_KEY: u8 = 0x01;
205// Segment kind 0x02 (range: start i32, end i32) is RESERVED — sequence
206// slices/ranges (icebox #829). Never emitted; the reader rejects it
207// (`InvalidProjSegmentKind`) since no `ProjSegment` variant exists to decode
208// into, the same discipline the value-tag reservations above follow.
209
210// EffectRows call-atom slots (T2-3, `docs/effects-spec.md` §11). The
211// capability-parameter slot is populated `(any)` in v1; the handle-parameter
212// slot is reserved (`docs/t1d-spec.md` §7) and always `None`.
213/// Capability-parameter slot value: the whole capability, unrefined. The only
214/// value v1 emits; path-granular tags (#826) are reserved (reader rejects).
215pub(crate) const CAP_PARAM_ANY: u8 = 0x00;
216/// Handle-parameter slot value: no bound handle. The only value v1 emits;
217/// a non-zero slot is the reserved handle-parameterized form (reader rejects).
218pub(crate) const HANDLE_PARAM_NONE: u8 = 0x00;
219
220/// NS-A2 (issue #1108): bit assignments for the `DirectEffects` extension
221/// flags byte (`EffectRows` section version 3). Bits 3–7 are RESERVED —
222/// the strict reader rejects a nonzero reserved bit until a section version
223/// graduates it (the same discipline as the capability/handle slots).
224pub(crate) const EFFECT_DIM_EMITS: u8 = 0b0000_0001;
225pub(crate) const EFFECT_DIM_TAGS: u8 = 0b0000_0010;
226pub(crate) const EFFECT_DIM_FAULTS: u8 = 0b0000_0100;
227pub(crate) const EFFECT_DIM_KNOWN_MASK: u8 = EFFECT_DIM_EMITS | EFFECT_DIM_TAGS | EFFECT_DIM_FAULTS;
228
229// LineContent tags
230pub(crate) const LINE_PLAIN: u8 = 0x00;
231pub(crate) const LINE_TEMPLATE: u8 = 0x01;
232
233// LinePart tags
234pub(crate) const PART_LITERAL: u8 = 0x00;
235pub(crate) const PART_SLOT: u8 = 0x01;
236pub(crate) const PART_SELECT: u8 = 0x02;
237// #1716 (`docs/prose-dialect-spec.md` §4.4/§4.5): the inline markup span.
238// Structurally, adding this tag is one match arm on the existing `u8`
239// part-tag dispatch — an old reader hard-rejects the unknown tag
240// (`decode_line_part`'s `_ => Err(InvalidLinePart)`) and an old file simply
241// never contains one. That is *not* the same as the `VAL_VEC2`/
242// `VAL_WEIGHTED` no-`VERSION`-bump precedent, though: those value tags sit
243// in the v4 RFC's pre-reserved, frozen tag inventory (`docs/format-v4-rfc.md`,
244// the §9 one-bump rule), so materializing their encoding was already paid
245// for by v4's bump. `PART_SPAN` was never part of that reservation — issue
246// #1716 rules it explicitly as **"a v6 payload"**, coordinated with #1683
247// (the v6 bump manifest). It IS its own one-bump event: `VERSION` bumped to
248// 6 (see `VERSION`'s doc comment). `.inkl` shares this encoder/decoder
249// (`inkl::{read,write}` call straight through to
250// `encode_line_content`/`decode_line_content`), so both formats gain the
251// tag from this one bump.
252pub(crate) const PART_SPAN: u8 = 0x03;
253
254// SelectKey tags
255pub(crate) const KEY_CARDINAL: u8 = 0x00;
256pub(crate) const KEY_ORDINAL: u8 = 0x01;
257pub(crate) const KEY_EXACT: u8 = 0x02;
258pub(crate) const KEY_KEYWORD: u8 = 0x03;
259
260// PluralCategory tags
261pub(crate) const CAT_ZERO: u8 = 0x00;
262pub(crate) const CAT_ONE: u8 = 0x01;
263pub(crate) const CAT_TWO: u8 = 0x02;
264pub(crate) const CAT_FEW: u8 = 0x03;
265pub(crate) const CAT_MANY: u8 = 0x04;
266pub(crate) const CAT_OTHER: u8 = 0x05;
267
268// ── Section types ───────────────────────────────────────────────────────────
269
270/// Identifies a section within an `.inkb` file.
271#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
272#[repr(u8)]
273pub enum SectionKind {
274 NameTable = 0x01,
275 Variables = 0x02,
276 ListDefs = 0x03,
277 ListItems = 0x04,
278 Externals = 0x05,
279 Containers = 0x06,
280 LineTables = 0x07,
281 Labels = 0x08,
282 ListLiterals = 0x09,
283 AddressPaths = 0x0A,
284 /// T1b `LiteralPool` (`docs/format-v4-rfc.md` §2): content-hash
285 /// deduplicated constant values referenced by `PushLiteral(idx)`.
286 /// Additive alongside `ListLiterals` — see the T1b-2 PR description for
287 /// why the RFC's `ListLiterals` absorption is deferred, not done here.
288 LiteralPool = 0x0B,
289 /// TM-4 `StructShapes` (`docs/typed-mode-spec.md` §6): one entry per
290 /// declared `STRUCT` — shape id, name, ordered field names. Reserved
291 /// (count always 0) through 4.0; this PR lands the section's real
292 /// encoding at the format layer only — nothing in the compiler emits a
293 /// non-empty table yet (see the PR description's scope note).
294 StructShapes = 0x0C,
295 /// T2-3 `EffectRows` (`docs/effects-spec.md` §11, `docs/format-v4-rfc.md`
296 /// §2): the `DefinitionId → row` table of factored effect rows — one per
297 /// knot/stitch (the resume-scheduling estimate, §12.1). Section-locally
298 /// versioned (one prefix byte) so the row encoding can grow without a
299 /// format-wide bump — the reservation this graduates was made for exactly
300 /// this, so no `VERSION` bump accompanies it. Always present (possibly
301 /// empty). Was reserved (count-0) through v5; this slice lands the real
302 /// encoding **and** first emission (rows are inert metadata the runtime
303 /// does not yet read).
304 EffectRows = 0x0D,
305 /// M-2b `Visibility` (`docs/modules-spec.md` §4, `docs/format-spec.md`):
306 /// the `DefinitionId`s of every `#@private` definition, sorted ascending.
307 /// **Omitted entirely when empty** (the common all-public case), so
308 /// public-only stories stay byte-identical for that section — the
309 /// section is purely additive and self-framed in the offset table.
310 /// `0x0D` is claimed by `EffectRows`, so this takes the next free tag.
311 Visibility = 0x0E,
312 /// M-3 `AliasTable` (`docs/modules-spec.md` §5): old→new `DefinitionId`
313 /// rename records from `#@was(old_name)` directives. Section-locally
314 /// versioned (one prefix byte) so the row encoding can grow without a
315 /// format-wide bump. Always present (possibly empty) from v5 onward.
316 /// `0x0E` was claimed by `Visibility` (M-2b), so this takes the next
317 /// free tag.
318 AliasTable = 0x0F,
319 /// FS-3 `FrameShapes` (`docs/flow-suspension-spec.md` §4/§11): one
320 /// name-keyed frame shape per `await` site — the static crossing-locals
321 /// description the runtime spills/restores around a park.
322 /// Section-locally versioned (one prefix byte) so the shape encoding can
323 /// grow without a format-wide bump. **Omitted entirely when empty** (the
324 /// common case — and, behind the E052 fence, the *only* case today, since
325 /// no `await` compiles yet), so all existing stories stay byte-identical
326 /// and no `VERSION` bump is needed. `0x0F` was claimed by `AliasTable`, so
327 /// this takes the next free tag.
328 FrameShapes = 0x10,
329 /// D6 `DebugInfo` (`docs/debugger-spec.md` §2, issue #3184): the
330 /// bytecode-offset → source-range map, plus its section-local file
331 /// table. Section-locally versioned (one prefix byte) so the entry
332 /// encoding can grow (e.g. the reserved `NodeId` column, §1.3) without a
333 /// format-wide bump. **Omitted entirely when not requested** — the
334 /// ship-policy default (§1.2): a release-exported story never carries
335 /// this section, so every existing story stays byte-identical and no
336 /// `VERSION` bump is needed. `0x10` was claimed by `FrameShapes`, so
337 /// this takes the next free tag — the test this graduates,
338 /// `from_u8_rejects_unclaimed_section_tag`, is updated alongside this
339 /// variant (not deleted) to pin the *new* next-free tag (`0x12`).
340 DebugInfo = 0x11,
341 /// Stage 1 of the shared-alternatives track (issue #3273,
342 /// `docs/decision-log.md` 2026-08-29): [`crate::LineVariantGroup`]
343 /// records tying runs of consecutive line-table entries back to one
344 /// authored line whose inline alternatives were enumerated at
345 /// recognition time. Section-locally versioned (one prefix byte) so the
346 /// record encoding can grow without a format-wide bump. **Omitted
347 /// entirely when empty** — nothing emits a non-empty table until the
348 /// stage-2 flip (#3274), so every existing story stays byte-identical
349 /// and no `VERSION` bump is needed. `0x11` is claimed by `DebugInfo`,
350 /// so this takes the next free tag.
351 LineVariantGroups = 0x12,
352}
353
354// All v4-reserved section kinds have now graduated: `LiteralPool` (0x0B),
355// `StructShapes` (0x0C), and `EffectRows` (0x0D, T2-3). `Visibility` (0x0E)
356// and `AliasTable` (0x0F) were later one-bump additions past the reserved
357// gap; `LineVariantGroups` (0x12, #3273) is the newest.
358
359impl SectionKind {
360 pub(crate) fn from_u8(tag: u8) -> Result<Self, DecodeError> {
361 match tag {
362 0x01 => Ok(Self::NameTable),
363 0x02 => Ok(Self::Variables),
364 0x03 => Ok(Self::ListDefs),
365 0x04 => Ok(Self::ListItems),
366 0x05 => Ok(Self::Externals),
367 0x06 => Ok(Self::Containers),
368 0x07 => Ok(Self::LineTables),
369 0x08 => Ok(Self::Labels),
370 0x09 => Ok(Self::ListLiterals),
371 0x0A => Ok(Self::AddressPaths),
372 0x0B => Ok(Self::LiteralPool),
373 0x0C => Ok(Self::StructShapes),
374 0x0D => Ok(Self::EffectRows),
375 0x0E => Ok(Self::Visibility),
376 0x0F => Ok(Self::AliasTable),
377 0x10 => Ok(Self::FrameShapes),
378 0x11 => Ok(Self::DebugInfo),
379 0x12 => Ok(Self::LineVariantGroups),
380 _ => Err(DecodeError::InvalidSectionKind(tag)),
381 }
382 }
383}
384
385/// An entry in the `.inkb` offset table.
386#[derive(Debug, Clone, Copy, PartialEq, Eq)]
387pub struct SectionEntry {
388 pub kind: SectionKind,
389 pub offset: u32,
390}
391
392/// Parsed header + offset table from an `.inkb` file.
393///
394/// Allows selective reads without parsing section data.
395#[derive(Debug, Clone, PartialEq, Eq)]
396pub struct InkbIndex {
397 pub version: u16,
398 pub file_size: u32,
399 pub checksum: u32,
400 pub sections: Vec<SectionEntry>,
401}
402
403impl InkbIndex {
404 /// Total header size in bytes (preamble + offset table).
405 pub fn header_size(&self) -> usize {
406 HEADER_PREAMBLE + self.sections.len() * SECTION_ENTRY_SIZE
407 }
408
409 /// Returns `(offset, length)` for a section, computing length from the
410 /// next section's offset (or `file_size` for the last section).
411 ///
412 /// Subtraction is safe because `read_inkb_index` validates that offsets
413 /// are monotonically increasing and within `[header_size, file_size]`.
414 pub fn section_range(&self, kind: SectionKind) -> Option<Range<usize>> {
415 let idx = self.sections.iter().position(|e| e.kind == kind)?;
416 let start = self.sections[idx].offset as usize;
417 let end = self
418 .sections
419 .get(idx + 1)
420 .map_or(self.file_size, |e| e.offset) as usize;
421 Some(start..end)
422 }
423}
424
425/// Cap `Vec::with_capacity` allocations against remaining buffer bytes to avoid
426/// OOM on crafted inputs with huge count fields. Each element occupies at least
427/// `min_element_size` bytes, so the count can't exceed `remaining / min`.
428pub(crate) fn safe_capacity(
429 count: usize,
430 buf_len: usize,
431 offset: usize,
432 min_element_size: usize,
433) -> usize {
434 let remaining = buf_len.saturating_sub(offset);
435 let max_possible = remaining.checked_div(min_element_size).unwrap_or(remaining);
436 count.min(max_possible)
437}
438
439#[cfg(test)]
440mod tests {
441 use super::*;
442
443 /// Every v4-reserved section tag has now graduated to a real
444 /// `SectionKind` variant — `LiteralPool` (0x0B, T1b-2 #570),
445 /// `StructShapes` (0x0C, TM-4 #620), and `EffectRows` (0x0D, T2-3 #862).
446 /// `LineVariantGroups` (0x12, #3273) is the newest tag; the next
447 /// unclaimed tag (0x13) is still rejected. This pin previously named
448 /// `0x12` before `LineVariantGroups` claimed it, and `0x11` before
449 /// D6 claimed it — flipped here, not deleted, per
450 /// `docs/debugger-spec.md` §1.1's explicit instruction.
451 #[test]
452 fn from_u8_rejects_unclaimed_section_tag() {
453 let tag = 0x13u8;
454 let err = SectionKind::from_u8(tag).unwrap_err();
455 assert_eq!(err, DecodeError::InvalidSectionKind(tag));
456 }
457
458 #[test]
459 fn from_u8_accepts_all_current_sections() {
460 // 0x01..=0x0D are contiguous real sections (EffectRows graduated 0x0D).
461 for tag in 0x01u8..=0x0D {
462 assert!(SectionKind::from_u8(tag).is_ok());
463 }
464 assert!(SectionKind::from_u8(0x0E).is_ok(), "Visibility (M-2b)");
465 assert!(SectionKind::from_u8(0x0F).is_ok(), "AliasTable (M-3)");
466 assert!(SectionKind::from_u8(0x10).is_ok(), "FrameShapes (FS-3c)");
467 assert!(SectionKind::from_u8(0x11).is_ok(), "DebugInfo (D6 #3184)");
468 assert!(
469 SectionKind::from_u8(0x12).is_ok(),
470 "LineVariantGroups (#3273)"
471 );
472 }
473}