Files
alktype/src/offset_map.rs
T
glm-5.3-flash 844c199fb8 Fix F1/F2 from review #007: zero-progress guard parity + maxLength cap
- F1: materialize_plan_array (validate_bytes' packed path) now carries
  the zero-progress array guard the reader and legacy walker already
  had; validate_bytes no longer accepts an empty buffer against a
  stride-0 empty-struct-element array that SequentialReader rejects.
  Cross-consumer agreement test added (review #007 probe transcript).
- F2: MAX_LENGTH = 2^26 cap on the maxLength annotation — the N2
  dual-layer pattern (clean Schema parse error naming value+maximum,
  meta-schema "maximum": 67108864 so the published contract matches).
  Also closes the silent usize-overflow drop in parse_max_length.
- docs/reviews/007-coverage-audit.md records the full audit: per-file
  numbers, all classifications, and the N3a dead-surface list deferred
  to the pre-release review.

Verification: 477 lib + 78 integration tests green, clippy -D warnings
clean, wasm32-unknown-unknown build green.
2026-09-03 06:39:12 +00:00

1596 lines
64 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! Aligned static `OffsetMap` — Mode 2 of the two layout modes (ADR-002).
//!
//! Fields have fixed positions with natural alignment padding.
//! Variable-length fields get a 4-byte length prefix at a known offset;
//! the variable data is not included in the static layout. Used for
//! mmap-friendly formats (metatensor, safetensors).
//!
//! The offset computation is a recursive walk of the BAST typed tree
//! ([`crate::bast::BastDoc`]). Nested structs propagate field path
//! prefixes (producing dotted paths like `"header.version"`). Alignment
//! padding is inserted before each field to satisfy the field's alignment
//! requirement (natural alignment by default, overridable via the
//! `"align"` annotation).
use crate::bast::{
BastArray, BastDefKind, BastDoc, BastField, BastStruct, BastType,
};
use crate::error::AlkTypeError;
use crate::schema::{AlkTypeKind, Endian, VariableEncoding, MAX_ARRAY_BYTES};
use std::collections::BTreeMap;
/// A byte range within a buffer.
///
/// Produced by [`OffsetMap::compute`] for each field in a schema. The
/// range is half-open: `start..end`. `end - start` is the field's byte
/// size in the static layout (for variable-length fields, this is the
/// size of the length prefix, the `{offset, length}` pair, or the
/// `maxLength` reservation — not the variable data itself).
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct ByteRange {
/// Inclusive start byte offset.
pub start: usize,
/// Exclusive end byte offset.
pub end: usize,
}
impl ByteRange {
/// Byte length of the range (`end - start`).
pub fn len(&self) -> usize {
self.end - self.start
}
/// True if the range covers zero bytes.
pub fn is_empty(&self) -> bool {
self.end == self.start
}
}
/// The resolved leaf metadata needed to read or write a field's bytes:
/// the field's kind, its variable-length encoding, and its effective
/// endianness (field override, else the enclosing struct/union default,
/// propagated the same way the aligned materializer propagates it — a
/// nested container inherits the *referring field's* effective endian).
///
/// Computed at [`OffsetMap::compute`] time so `read_field`/`write_field`
/// don't re-walk the BAST tree per access (ADR-012 §2b).
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct LeafMeta {
/// The leaf's type kind.
pub kind: AlkTypeKind,
/// The variable-length encoding strategy (meaningful for
/// `String`/`Bytes`/`Record` leaves).
pub encoding: VariableEncoding,
/// The effective endianness for reads/writes at this leaf.
pub endian: Endian,
}
/// A field's offset-map entry: its byte range plus its [`LeafMeta`].
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct OffsetEntry {
/// The field's byte range in the static layout.
pub range: ByteRange,
/// The resolved leaf metadata (kind, encoding, effective endian).
pub meta: LeafMeta,
}
impl OffsetEntry {
/// The entry's start byte offset.
pub fn start(&self) -> usize {
self.range.start
}
/// The entry's exclusive end byte offset.
pub fn end(&self) -> usize {
self.range.end
}
}
/// A flat table of `(field_path, OffsetEntry)` pairs computed from a
/// BAST document.
///
/// Fields have fixed positions with natural alignment padding.
/// Used for mmap-friendly formats (metatensor, safetensors) where random
/// access by field path is required — the consumer can read field N
/// without reading fields `0..N-1` first.
///
/// Construct via [`OffsetMap::compute`]. Variable-length fields appear
/// in the table as their fixed-position portion only (length prefix,
/// `{offset, length}` pair, or `maxLength` reservation); the variable
/// data lives outside the static layout.
///
/// `PartialEq`/`Eq`/`Hash`: two equal maps produce identical
/// read/write behavior over identical buffers (the compute is a pure
/// function of the document).
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct OffsetMap {
fields: Vec<(String, OffsetEntry)>,
index: BTreeMap<String, usize>,
total_size: usize,
}
impl OffsetMap {
/// Compute the offset map from a BAST document.
///
/// Walks the BAST typed tree recursively, computing byte positions
/// for each field based on type sizes, field order, and alignment.
/// The root type must be a struct.
///
/// # Errors
///
/// Returns [`AlkTypeError::Schema`] if the root type is not a struct,
/// or if the reference graph nests deeper than the walk depth cap
/// (128) or contains a `$ref` cycle (review #006 H2 — the compute
/// walk's struct recursion is unguarded, so cyclic input must be
/// rejected before the walk, not during it).
/// Returns [`AlkTypeError::Offset`] for unsupported type combinations
/// encountered during the walk (e.g. unions, which are rejected in
/// aligned mode per ADR-008).
pub fn compute(doc: &BastDoc) -> Result<Self, AlkTypeError> {
let root_def = doc.root_def();
let struct_node = match root_def.kind() {
BastDefKind::Struct(s) => s,
other => {
return Err(AlkTypeError::Schema(format!(
"OffsetMap::compute requires a struct at the root, got {kind}",
kind = other.alk_kind()
)));
}
};
crate::walk_guard::check_ref_graph(doc)?;
let mut ctx = ComputeCtx {
doc,
fields: Vec::new(),
offset: 0,
};
let struct_default_align = struct_node.align().unwrap_or(1).max(1);
let endian = struct_node.endian();
let (total, _align) = ctx.compute_struct(struct_node, "", struct_default_align, endian)?;
let index = Self::build_index(&ctx.fields);
Ok(Self {
fields: ctx.fields,
index,
total_size: total,
})
}
/// Look up a field's entry by dotted path (e.g., `"header.version"`).
///
/// Returns `None` if no field with the given path was recorded. For
/// TUnion byte-offset discriminators, the discriminator is recorded
/// under the synthetic path `"__discriminator"` (qualified by the
/// union field's path, e.g. `"payload.__discriminator"`).
pub fn get(&self, field_path: &str) -> Option<&OffsetEntry> {
let idx = *self.index.get(field_path)?;
self.fields.get(idx).map(|(_, entry)| entry)
}
/// The total size of the struct in bytes (including trailing alignment padding).
pub fn total_size(&self) -> usize {
self.total_size
}
/// Iterate over all `(field_path, OffsetEntry)` pairs in insertion order.
///
/// Field order matches the BAST `fields` array order. Nested struct
/// fields appear after their parent's path prefix.
pub fn iter(&self) -> impl Iterator<Item = (&str, &OffsetEntry)> {
self.fields.iter().map(|(path, entry)| (path.as_str(), entry))
}
/// A stable-within-version hash of the map (ADR-012 §1/§4).
///
/// Two maps with equal fingerprints (equal hashes) produce identical
/// read/write behavior over identical buffers. Cross-version
/// stability is a non-goal (ADR-012). Uses `DefaultHasher` (std, no
/// new dep); the fingerprint is not hot.
pub fn fingerprint(&self) -> u64 {
use std::hash::{Hash, Hasher};
let mut h = std::hash::DefaultHasher::new();
self.hash(&mut h);
h.finish()
}
/// Build the path→index lookup table over the insertion-ordered
/// `fields` vec. First occurrence wins on duplicate paths (matching
/// the linear-scan `find` this index replaced — `BastStruct::parse`
/// does not reject duplicate field names, so the semantic must be
/// preserved, review #006 L4).
fn build_index(fields: &[(String, OffsetEntry)]) -> BTreeMap<String, usize> {
let mut index = BTreeMap::new();
for (i, (path, _)) in fields.iter().enumerate() {
index.entry(path.clone()).or_insert(i);
}
index
}
}
/// Mutable context threaded through the recursive offset computation.
///
/// Carries the running `offset`, the accumulating `fields` vec, and a
/// reference to the BAST doc for `$ref` resolution. Grouping these
/// keeps the recursive helper signatures small.
struct ComputeCtx<'d> {
doc: &'d BastDoc,
fields: Vec<(String, OffsetEntry)>,
offset: usize,
}
/// Result of laying out a single field: its alignment.
struct FieldLayout {
align: usize,
}
impl<'d> ComputeCtx<'d> {
/// Recurse into a `BastStruct`, appending `(field_path, ByteRange)`
/// pairs to `self.fields` and advancing `self.offset`.
///
/// Returns `(total_size, alignment)` where `total_size` includes
/// trailing alignment padding and `alignment` is the struct's
/// effective alignment (its own `align` annotation, or the max of its
/// fields' alignments).
///
/// `prefix` is the dotted path prefix for nested fields (empty at the
/// top level). `parent_struct_align` is the default alignment a field
/// inherits when it specifies neither its own `align` annotation nor
/// a natural alignment larger than the default.
fn compute_struct(
&mut self,
struct_node: &BastStruct,
prefix: &str,
parent_struct_align: usize,
endian: Endian,
) -> Result<(usize, usize), AlkTypeError> {
let struct_default_align = struct_node.align().unwrap_or(parent_struct_align).max(1);
let mut max_align: usize = 1;
let struct_start = self.offset;
let fields = struct_node.fields();
let field_count = fields.len();
for (i, field) in fields.iter().enumerate() {
let field_path = if prefix.is_empty() {
field.name().to_string()
} else {
format!("{prefix}.{}", field.name())
};
if i < field_count - 1 {
if let Some(kind) = field_variable_kind(field) {
let encoding = field.encoding();
let max_length = field.max_length();
let is_inline_length_prefixed =
encoding == VariableEncoding::LengthPrefixed && max_length.is_none();
if is_inline_length_prefixed {
let remedy = if kind == AlkTypeKind::Record {
// For records both annotated remedies are
// dead ends: `maxLength` is rejected at
// parse (review #006 N3) and
// `offset-indirect` is rejected right
// below (review #006 M5) — moving to the
// last position is the only fix.
"move this field to the last position in the struct (for a \
record field, neither `maxLength` nor `offset-indirect` \
is available: `maxLength` is rejected at parse — review \
#006 N3 — and `offset-indirect` is rejected for records \
in aligned mode — review #006 M5)"
} else {
"Use `maxLength` (fixed-size reservation) or \
`\"encoding\": \"offset-indirect\"`, or move this \
field to the last position in the struct"
};
return Err(AlkTypeError::Offset {
field_path: field_path.clone(),
reason: format!(
"non-final inline length-prefixed variable field \
({kind}) in aligned mode: the variable data would \
clobber subsequent fields. {remedy}. (ADR-006)"
),
});
}
}
}
let layout = self.compute_field(field, &field_path, struct_default_align, endian)?;
if layout.align > max_align {
max_align = layout.align;
}
}
let effective_align = struct_node.align().unwrap_or(max_align).max(1);
align_up(&mut self.offset, effective_align);
let total = self.offset - struct_start;
Ok((total, effective_align))
}
/// Compute the layout for a single field, advancing `self.offset`
/// and appending any field paths to `self.fields`.
fn compute_field(
&mut self,
field: &BastField,
field_path: &str,
struct_default_align: usize,
container_endian: Endian,
) -> Result<FieldLayout, AlkTypeError> {
let ty = field.ty();
let resolved = self.doc.resolve_typeref(ty)?;
let field_endian = field.effective_endian(container_endian);
match &resolved {
BastType::Struct(s) => self.compute_struct_field(s, field, field_path, struct_default_align, field_endian),
BastType::Union(_) => Err(AlkTypeError::Offset {
field_path: field_path.to_string(),
reason: "TUnion is not supported in aligned static mode (ADR-008). \
Unions are the protocol dispatch pattern — use packed sequential \
mode (LayoutMode::Packed) for TUnion fields, or restructure as \
a struct with an explicit discriminator field."
.to_string(),
}),
BastType::Array(a) => self.compute_array_field(a, field, field_path, struct_default_align, field_endian),
BastType::Record(_) => {
if field.encoding() == VariableEncoding::OffsetIndirect {
return Err(AlkTypeError::Offset {
field_path: field_path.to_string(),
reason: "record field with `encoding: offset-indirect` is not supported \
in aligned mode: the materializer walks the record's inline \
count-prefixed form from the entry start, so an offset-indirect \
record would read from the wrong wire shape. Use the default \
inline length-prefixing, or packed mode."
.to_string(),
});
}
self.compute_variable_field(field, field_path, struct_default_align, resolved.alk_kind(), field_endian)
}
BastType::Primitive(k) if k.is_variable_length() => {
self.compute_variable_field(field, field_path, struct_default_align, *k, field_endian)
}
BastType::Primitive(k) => {
self.compute_fixed_field(*k, field, field_path, struct_default_align, field_endian)
}
BastType::Enum(_) => self.compute_fixed_field(
AlkTypeKind::Enum,
field,
field_path,
struct_default_align,
field_endian,
),
BastType::Ref(_) => Err(AlkTypeError::Offset {
field_path: field_path.to_string(),
reason: "internal: resolve_typeref returned a Ref".to_string(),
}),
}
}
/// Compute the layout for a fixed-size primitive field.
fn compute_fixed_field(
&mut self,
kind: AlkTypeKind,
field: &BastField,
field_path: &str,
struct_default_align: usize,
endian: Endian,
) -> Result<FieldLayout, AlkTypeError> {
let size = kind.type_size().ok_or_else(|| AlkTypeError::Offset {
field_path: field_path.to_string(),
reason: format!("type_size returned None for fixed kind {kind}"),
})?;
let natural = kind.natural_alignment();
let align = field_alignment(field, struct_default_align, natural);
align_up(&mut self.offset, align);
let start = self.offset;
self.offset = start
.checked_add(size)
.ok_or_else(|| AlkTypeError::Offset {
field_path: field_path.to_string(),
reason: format!("offset {start} + size {size} overflows usize"),
})?;
self.push(field_path, start, start + size, kind, field.encoding(), endian);
Ok(FieldLayout { align })
}
/// Compute the layout for a nested struct field.
///
/// Probes the nested struct's layout at a temporary offset of 0 to
/// determine its total size and alignment, aligns the parent offset,
/// then shifts the nested fields to their final positions.
fn compute_struct_field(
&mut self,
struct_node: &BastStruct,
field: &BastField,
field_path: &str,
struct_default_align: usize,
endian: Endian,
) -> Result<FieldLayout, AlkTypeError> {
let inner_parent_align = field.align().unwrap_or(struct_default_align);
let mut probe = ComputeCtx {
doc: self.doc,
fields: Vec::new(),
offset: 0,
};
let (inner_total, inner_align) =
probe.compute_struct(struct_node, field_path, inner_parent_align, endian)?;
let natural = inner_align;
let align = field_alignment(field, struct_default_align, natural);
align_up(&mut self.offset, align);
let struct_start = self.offset;
for (path, entry) in probe.fields {
let range = ByteRange {
start: struct_start + entry.range.start,
end: struct_start + entry.range.end,
};
self.fields.push((
path,
OffsetEntry {
range,
meta: entry.meta,
},
));
}
self.offset = struct_start
.checked_add(inner_total)
.ok_or_else(|| AlkTypeError::Offset {
field_path: field_path.to_string(),
reason: format!("struct start {struct_start} + inner total {inner_total} overflows usize"),
})?;
Ok(FieldLayout { align })
}
/// Compute the layout for a `BastArray` field.
fn compute_array_field(
&mut self,
array: &BastArray,
field: &BastField,
field_path: &str,
struct_default_align: usize,
endian: Endian,
) -> Result<FieldLayout, AlkTypeError> {
let element_ty = array.element();
let resolved_elem = self.doc.resolve_typeref(element_ty)?;
let elem_kind = resolved_elem.alk_kind();
if !elem_kind.is_fixed_size() {
return Err(AlkTypeError::Offset {
field_path: field_path.to_string(),
reason: format!(
"array of variable-length element kind {elem_kind} is not supported (OQ-001)"
),
});
}
let elem_size = elem_kind.type_size().ok_or_else(|| AlkTypeError::Offset {
field_path: field_path.to_string(),
reason: format!("element kind {elem_kind} has no fixed size"),
})?;
let elem_natural = elem_kind.natural_alignment();
// Composites (struct/union/array/record) never reach here — the
// OQ-001 rejection above refuses every non-fixed-size element
// kind — so the element alignment is the struct default vs the
// fixed kind's natural alignment (review #006 M4 item 4: the
// former composite arms of this lookup were dead).
let elem_align = struct_default_align.max(elem_natural).max(1);
let stride = round_up(elem_size, elem_align);
let count = array.count();
let array_bytes = count
.checked_mul(stride)
.ok_or_else(|| AlkTypeError::Offset {
field_path: field_path.to_string(),
reason: format!("array size {count} × stride {stride} overflows usize"),
})?;
if array_bytes > MAX_ARRAY_BYTES {
return Err(AlkTypeError::Offset {
field_path: field_path.to_string(),
reason: format!(
"array size {count} × stride {stride} = {array_bytes} bytes exceeds the \
compile-time limit of {MAX_ARRAY_BYTES} bytes"
),
});
}
let array_align = field_alignment(field, struct_default_align, elem_align);
align_up(&mut self.offset, array_align);
let start = self.offset;
let elem_encoding = field.encoding();
let elem_endian = field_endian_for_element(&resolved_elem, endian);
for i in 0..count {
let elem_start = start
.checked_add(
i.checked_mul(stride)
.ok_or_else(|| AlkTypeError::Offset {
field_path: field_path.to_string(),
reason: format!("element index {i} × stride {stride} overflows usize"),
})?,
)
.ok_or_else(|| AlkTypeError::Offset {
field_path: field_path.to_string(),
reason: format!("element offset {start} + {i}×{stride} overflows usize"),
})?;
let elem_end = elem_start
.checked_add(elem_size)
.ok_or_else(|| AlkTypeError::Offset {
field_path: field_path.to_string(),
reason: format!("element end {elem_start} + {elem_size} overflows usize"),
})?;
let elem_path = format!("{field_path}[{i}]");
self.push(&elem_path, elem_start, elem_end, elem_kind, elem_encoding, elem_endian);
}
let array_size = count
.checked_mul(stride)
.ok_or_else(|| AlkTypeError::Offset {
field_path: field_path.to_string(),
reason: format!("array size {count} × stride {stride} overflows usize"),
})?;
self.offset = start
.checked_add(array_size)
.ok_or_else(|| AlkTypeError::Offset {
field_path: field_path.to_string(),
reason: format!("offset {start} + array size {array_size} overflows usize"),
})?;
Ok(FieldLayout { align: array_align })
}
/// Compute the layout for a variable-length field (String/Bytes/Record).
///
/// In aligned static mode, three strategies are supported:
/// - `maxLength` reservation: `maxLength` bytes at a fixed offset.
/// - `offset-indirect` encoding: an 8-byte `{offset: u32, length: u32}` pair.
/// - inline length-prefixing (default): a 4-byte length prefix.
fn compute_variable_field(
&mut self,
field: &BastField,
field_path: &str,
struct_default_align: usize,
kind: AlkTypeKind,
endian: Endian,
) -> Result<FieldLayout, AlkTypeError> {
let encoding = field.encoding();
let max_length = field.max_length();
let (size, natural) = match (max_length, encoding) {
(Some(max_len), _) => (max_len, 1),
(None, VariableEncoding::OffsetIndirect) => (8, 4),
(None, VariableEncoding::LengthPrefixed) => (4, 4),
};
let align = field_alignment(field, struct_default_align, natural);
align_up(&mut self.offset, align);
let start = self.offset;
self.offset = start
.checked_add(size)
.ok_or_else(|| AlkTypeError::Offset {
field_path: field_path.to_string(),
reason: format!("offset {start} + size {size} overflows usize"),
})?;
self.push(field_path, start, start + size, kind, encoding, endian);
Ok(FieldLayout { align })
}
/// Push an `OffsetEntry` onto the fields vec.
fn push(&mut self, path: &str, start: usize, end: usize, kind: AlkTypeKind, encoding: VariableEncoding, endian: Endian) {
self.fields.push((
path.to_string(),
OffsetEntry {
range: ByteRange { start, end },
meta: LeafMeta {
kind,
encoding,
endian,
},
},
));
}
}
/// If the field's type is a variable-length kind, return its kind.
/// Matches primitive variable-length kinds (String/Bytes) *and*
/// `Record` — whose inline length-prefixed form has the same
/// 4-byte-prefix-then-variable-data shape and the same clobbering
/// hazard the ADR-006 check exists for (review #006 M1: records
/// previously slipped past the check).
fn field_variable_kind(field: &BastField) -> Option<AlkTypeKind> {
match field.ty() {
BastType::Primitive(k) if k.is_variable_length() => Some(*k),
BastType::Record(_) => Some(AlkTypeKind::Record),
_ => None,
}
}
/// The effective endianness for an array element's reads/writes. The
/// element TypeRef doesn't carry a field-level override (BAST field
/// annotations live on the field, not the element), so the referring
/// field's effective endian applies — the same propagation the aligned
/// materializer uses. Only fixed-size element kinds reach here (the
/// OQ-001 rejection upstream refuses composites), so the former
/// `Struct`/`Union` composite arms were dead (review #006 M4 item 4).
fn field_endian_for_element(elem_ty: &BastType, field_endian: Endian) -> Endian {
match elem_ty {
BastType::Enum(_) | BastType::Array(_) | BastType::Record(_) => field_endian,
BastType::Primitive(_) | BastType::Ref(_) => field_endian,
BastType::Struct(_) | BastType::Union(_) => field_endian,
}
}
/// Resolve the field's alignment: field-level `align` annotation,
/// then the struct default, then the natural alignment.
fn field_alignment(field: &BastField, struct_default_align: usize, natural: usize) -> usize {
if let Some(a) = field.align() {
return a.max(1);
}
struct_default_align.max(natural).max(1)
}
/// Round `offset` up to the next multiple of `align`. No-op if `align <= 1`.
fn align_up(offset: &mut usize, align: usize) {
if align <= 1 {
return;
}
let rem = *offset % align;
if rem != 0 {
*offset += align - rem;
}
}
/// Round `n` up to the next multiple of `align`.
fn round_up(n: usize, align: usize) -> usize {
if align <= 1 {
return n;
}
let rem = n % align;
if rem == 0 {
n
} else {
n + align - rem
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
fn map(root: &serde_json::Value, name: &str) -> OffsetMap {
let doc = BastDoc::new(root, name).expect("bast doc");
OffsetMap::compute(&doc).expect("offset map computation")
}
#[test]
fn simple_fixed_fields_natural_alignment() {
let root = json!({
"$defs": {
"S": {
"kind": "struct",
"fields": [
{ "name": "flag", "kind": "uint8" },
{ "name": "id", "kind": "uint32" }
]
}
}
});
let m = map(&root, "S");
assert_eq!(m.get("flag"), Some(&OffsetEntry { range: ByteRange { start: 0, end: 1 }, meta: LeafMeta { kind: AlkTypeKind::Uint8, encoding: VariableEncoding::LengthPrefixed, endian: Endian::Little } }));
assert_eq!(m.get("id"), Some(&OffsetEntry { range: ByteRange { start: 4, end: 8 }, meta: LeafMeta { kind: AlkTypeKind::Uint32, encoding: VariableEncoding::LengthPrefixed, endian: Endian::Little } }));
assert_eq!(m.total_size(), 8);
}
#[test]
fn u8_then_u32_three_bytes_padding() {
let root = json!({
"$defs": {
"S": {
"kind": "struct",
"fields": [
{ "name": "a", "kind": "uint8" },
{ "name": "b", "kind": "uint32" }
]
}
}
});
let m = map(&root, "S");
assert_eq!(m.get("a"), Some(&OffsetEntry { range: ByteRange { start: 0, end: 1 }, meta: LeafMeta { kind: AlkTypeKind::Uint8, encoding: VariableEncoding::LengthPrefixed, endian: Endian::Little } }));
assert_eq!(m.get("b"), Some(&OffsetEntry { range: ByteRange { start: 4, end: 8 }, meta: LeafMeta { kind: AlkTypeKind::Uint32, encoding: VariableEncoding::LengthPrefixed, endian: Endian::Little } }));
}
#[test]
fn nested_struct_dotted_paths() {
let root = json!({
"$defs": {
"S": {
"kind": "struct",
"fields": [
{
"name": "header",
"kind": {
"kind": "struct",
"fields": [
{ "name": "magic", "kind": "uint32" },
{ "name": "version", "kind": "uint8" }
]
}
},
{ "name": "body", "kind": "uint32" }
]
}
}
});
let m = map(&root, "S");
assert_eq!(m.get("header.magic"), Some(&OffsetEntry { range: ByteRange { start: 0, end: 4 }, meta: LeafMeta { kind: AlkTypeKind::Uint32, encoding: VariableEncoding::LengthPrefixed, endian: Endian::Little } }));
assert_eq!(
m.get("header.version"),
Some(&OffsetEntry { range: ByteRange { start: 4, end: 5 }, meta: LeafMeta { kind: AlkTypeKind::Uint8, encoding: VariableEncoding::LengthPrefixed, endian: Endian::Little } })
);
assert_eq!(m.get("body"), Some(&OffsetEntry { range: ByteRange { start: 8, end: 12 }, meta: LeafMeta { kind: AlkTypeKind::Uint32, encoding: VariableEncoding::LengthPrefixed, endian: Endian::Little } }));
assert_eq!(m.total_size(), 12);
}
#[test]
fn array_fixed_count_element_offsets() {
let root = json!({
"$defs": {
"S": {
"kind": "struct",
"fields": [
{ "name": "vals", "kind": { "kind": "array", "element": "uint32", "count": 3 } }
]
}
}
});
let m = map(&root, "S");
assert_eq!(m.get("vals[0]"), Some(&OffsetEntry { range: ByteRange { start: 0, end: 4 }, meta: LeafMeta { kind: AlkTypeKind::Uint32, encoding: VariableEncoding::LengthPrefixed, endian: Endian::Little } }));
assert_eq!(m.get("vals[1]"), Some(&OffsetEntry { range: ByteRange { start: 4, end: 8 }, meta: LeafMeta { kind: AlkTypeKind::Uint32, encoding: VariableEncoding::LengthPrefixed, endian: Endian::Little } }));
assert_eq!(m.get("vals[2]"), Some(&OffsetEntry { range: ByteRange { start: 8, end: 12 }, meta: LeafMeta { kind: AlkTypeKind::Uint32, encoding: VariableEncoding::LengthPrefixed, endian: Endian::Little } }));
assert_eq!(m.total_size(), 12);
}
// ----- H1: array caps bound untrusted schemas at compute time ------
#[test]
fn n2_struct_align_above_cap_rejected_at_parse() {
let root = json!({
"$defs": {
"S": {
"kind": "struct",
"align": 4097u64,
"fields": [ { "name": "v", "kind": "uint8" } ]
}
}
});
let err = BastDoc::new(&root, "S").unwrap_err();
match err {
AlkTypeError::Schema(reason) => {
assert!(reason.contains("align"), "reason: {reason}");
assert!(reason.contains("maximum"), "reason: {reason}");
}
other => panic!("expected Schema error, got {other:?}"),
}
}
#[test]
fn n2_field_align_above_cap_rejected_at_parse() {
let root = json!({
"$defs": {
"S": {
"kind": "struct",
"fields": [
{ "name": "v", "kind": "uint8", "align": 8192 }
]
}
}
});
let err = BastDoc::new(&root, "S").unwrap_err();
assert!(matches!(err, AlkTypeError::Schema(_)), "got {err:?}");
}
#[test]
fn n2_align_at_cap_accepted() {
let root = json!({
"$defs": {
"S": {
"kind": "struct",
"align": 4096u64,
"fields": [ { "name": "v", "kind": "uint8" } ]
}
}
});
let m = map(&root, "S");
assert_eq!(m.total_size(), 4096);
}
#[test]
fn h1_array_count_above_cap_rejected_at_parse() {
let root = json!({
"$defs": {
"S": {
"kind": "struct",
"fields": [
{ "name": "vals", "kind": { "kind": "array", "element": "uint8", "count": 2000000000 } }
]
}
}
});
let err = BastDoc::new(&root, "S").unwrap_err();
match err {
AlkTypeError::Schema(reason) => {
assert!(reason.contains("compile-time limit"), "reason: {reason}");
}
other => panic!("expected Schema error, got {other:?}"),
}
}
// ----- F2 (review #007): unbounded maxLength — the N2 pattern -------
#[test]
fn f2_max_length_above_cap_rejected_at_parse() {
let root = json!({
"$defs": {
"S": {
"kind": "struct",
"fields": [
{ "name": "blob", "kind": "bytes", "maxLength": 1099511627776u64 }
]
}
}
});
let err = BastDoc::new(&root, "S").unwrap_err();
match err {
AlkTypeError::Schema(reason) => {
assert!(reason.contains("maxLength"), "reason: {reason}");
assert!(reason.contains("maximum"), "reason: {reason}");
}
other => panic!("expected Schema error, got {other:?}"),
}
}
#[test]
fn f2_max_length_string_above_cap_rejected_at_parse() {
let root = json!({
"$defs": {
"S": {
"kind": "struct",
"fields": [
{ "name": "name", "kind": "string", "maxLength": 67108865u64 }
]
}
}
});
let err = BastDoc::new(&root, "S").unwrap_err();
assert!(matches!(err, AlkTypeError::Schema(_)), "got {err:?}");
}
#[test]
fn f2_max_length_at_cap_accepted() {
let root = json!({
"$defs": {
"S": {
"kind": "struct",
"endian": "little",
"fields": [
{ "name": "blob", "kind": "bytes", "maxLength": 67108864u64 }
]
}
}
});
let m = map(&root, "S");
assert_eq!(m.total_size(), 67108864);
}
#[test]
fn f2_max_length_u64_max_rejected_not_silently_dropped() {
// The old parse silently returned None for values it could not
// handle; a u64::MAX-scale maxLength must be a clean Schema
// error (the cap arm on 64-bit, the usize-overflow arm on
// 32-bit), never a silent layout change.
let root = json!({
"$defs": {
"S": {
"kind": "struct",
"fields": [
{ "name": "blob", "kind": "bytes", "maxLength": 18446744073709551615u64 }
]
}
}
});
let err = BastDoc::new(&root, "S").unwrap_err();
match err {
AlkTypeError::Schema(reason) => {
assert!(reason.contains("maxLength"), "reason: {reason}");
}
other => panic!("expected Schema error, got {other:?}"),
}
}
#[test]
fn f2_meta_schema_rejects_max_length_above_cap() {
// The published contract matches the parser (N2 dual-layer
// pattern).
let doc = json!({
"$defs": {
"S": {
"kind": "struct",
"fields": [
{ "name": "blob", "kind": "bytes", "maxLength": 67108865u64 }
]
}
}
});
assert!(
crate::bast_meta::validate_bast_doc(&doc).is_err(),
"meta-schema must reject maxLength above the cap"
);
let ok = json!({
"$defs": {
"S": {
"kind": "struct",
"fields": [
{ "name": "blob", "kind": "bytes", "maxLength": 67108864u64 }
]
}
}
});
assert!(
crate::bast_meta::validate_bast_doc(&ok).is_ok(),
"meta-schema must accept maxLength at the cap"
);
}
#[test]
fn h1_array_bytes_above_cap_rejected_at_compute() {
// 65536 elements × stride 4096 (a u8 array in a struct with the
// maximum legal align, N2) = 2^28 > 2^26 — hits the byte cap
// while staying under the element cap. (A struct/array element
// would be rejected earlier as variable-length, OQ-001, so the
// reachable way over the byte cap is a large stride, not a huge
// element.)
let root = json!({
"$defs": {
"S": {
"kind": "struct",
"align": 4096u64,
"fields": [
{ "name": "vals", "kind": { "kind": "array", "element": "uint8", "count": 65536 } }
]
}
}
});
let doc = BastDoc::new(&root, "S").expect("bast doc (under element cap)");
let err = OffsetMap::compute(&doc).unwrap_err();
match err {
AlkTypeError::Offset { field_path, reason } => {
assert_eq!(field_path, "vals");
assert!(reason.contains("exceeds the compile-time limit"), "reason: {reason}");
}
other => panic!("expected Offset error, got {other:?}"),
}
}
#[test]
fn variable_string_length_prefix_at_known_offset() {
let root = json!({
"$defs": {
"S": {
"kind": "struct",
"fields": [
{ "name": "id", "kind": "uint32" },
{ "name": "name", "kind": "string" }
]
}
}
});
let m = map(&root, "S");
assert_eq!(m.get("id"), Some(&OffsetEntry { range: ByteRange { start: 0, end: 4 }, meta: LeafMeta { kind: AlkTypeKind::Uint32, encoding: VariableEncoding::LengthPrefixed, endian: Endian::Little } }));
assert_eq!(m.get("name"), Some(&OffsetEntry { range: ByteRange { start: 4, end: 8 }, meta: LeafMeta { kind: AlkTypeKind::String, encoding: VariableEncoding::LengthPrefixed, endian: Endian::Little } }));
assert_eq!(m.total_size(), 8);
}
#[test]
fn variable_string_max_length_reservation() {
let root = json!({
"$defs": {
"S": {
"kind": "struct",
"fields": [
{ "name": "id", "kind": "uint32" },
{ "name": "name", "kind": "string", "maxLength": 256 }
]
}
}
});
let m = map(&root, "S");
assert_eq!(m.get("id"), Some(&OffsetEntry { range: ByteRange { start: 0, end: 4 }, meta: LeafMeta { kind: AlkTypeKind::Uint32, encoding: VariableEncoding::LengthPrefixed, endian: Endian::Little } }));
assert_eq!(m.get("name"), Some(&OffsetEntry { range: ByteRange { start: 4, end: 260 }, meta: LeafMeta { kind: AlkTypeKind::String, encoding: VariableEncoding::LengthPrefixed, endian: Endian::Little } }));
assert_eq!(m.total_size(), 260);
}
#[test]
fn variable_string_offset_indirect_eight_bytes() {
let root = json!({
"$defs": {
"S": {
"kind": "struct",
"fields": [
{ "name": "id", "kind": "uint32" },
{ "name": "blob", "kind": "string", "encoding": "offset-indirect" }
]
}
}
});
let m = map(&root, "S");
assert_eq!(m.get("id"), Some(&OffsetEntry { range: ByteRange { start: 0, end: 4 }, meta: LeafMeta { kind: AlkTypeKind::Uint32, encoding: VariableEncoding::LengthPrefixed, endian: Endian::Little } }));
assert_eq!(m.get("blob"), Some(&OffsetEntry { range: ByteRange { start: 4, end: 12 }, meta: LeafMeta { kind: AlkTypeKind::String, encoding: VariableEncoding::OffsetIndirect, endian: Endian::Little } }));
assert_eq!(m.total_size(), 12);
}
#[test]
fn union_byte_discriminator_rejected_in_aligned_mode() {
let root = json!({
"$defs": {
"S": {
"kind": "struct",
"fields": [
{
"name": "payload",
"kind": { "$ref": "#/$defs/Packet" }
}
]
},
"Packet": {
"kind": "union",
"discriminator": { "kind": "byte", "offset": 0, "type": "uint8" },
"mapping": {
"5": { "$ref": "#/$defs/Read" },
"6": { "$ref": "#/$defs/Write" }
}
},
"Read": {
"kind": "struct",
"fields": [
{ "name": "handle", "kind": "uint32" },
{ "name": "length", "kind": "uint32" }
]
},
"Write": {
"kind": "struct",
"fields": [
{ "name": "handle", "kind": "uint32" },
{ "name": "length", "kind": "uint32" },
{ "name": "data", "kind": "uint32" }
]
}
}
});
let doc = BastDoc::new(&root, "S").expect("doc");
let err = OffsetMap::compute(&doc).unwrap_err();
assert!(matches!(err, AlkTypeError::Offset { .. }), "got {err:?}");
let reason = match err {
AlkTypeError::Offset { reason, .. } => reason,
_ => unreachable!(),
};
assert!(reason.contains("ADR-008"), "reason: {reason}");
}
#[test]
fn h2_cyclic_ref_rejected_at_compute_not_stack_overflow() {
let root = json!({
"$defs": {
"S": { "kind": "struct", "fields": [
{ "name": "me", "kind": { "$ref": "#/$defs/S" } }
] }
}
});
let doc = BastDoc::new(&root, "S").expect("cyclic doc parses");
let err = OffsetMap::compute(&doc).unwrap_err();
match err {
AlkTypeError::Schema(reason) => {
assert!(reason.contains("cyclic"), "reason: {reason}");
}
other => panic!("expected Schema error, got {other:?}"),
}
}
#[test]
fn h2_two_def_cycle_rejected_at_compute() {
let root = json!({
"$defs": {
"A": { "kind": "struct", "fields": [
{ "name": "next", "kind": { "$ref": "#/$defs/B" } }
] },
"B": { "kind": "struct", "fields": [
{ "name": "back", "kind": { "$ref": "#/$defs/A" } }
] }
}
});
let doc = BastDoc::new(&root, "A").expect("cyclic doc parses");
let err = OffsetMap::compute(&doc).unwrap_err();
assert!(matches!(err, AlkTypeError::Schema(_)), "got {err:?}");
}
#[test]
fn h2_cycle_through_ref_field_of_nested_struct_rejected() {
// The cycle sits behind an inline struct + array hop: the walk
// guard must see through composite carriers, not just top-level
// fields.
let root = json!({
"$defs": {
"S": { "kind": "struct", "fields": [
{ "name": "inner", "kind": { "kind": "struct", "fields": [
{ "name": "items", "kind": {
"kind": "array", "element": { "$ref": "#/$defs/S" }, "count": 1
} }
] } }
] }
}
});
let doc = BastDoc::new(&root, "S").expect("cyclic doc parses");
let err = OffsetMap::compute(&doc).unwrap_err();
assert!(
matches!(err, AlkTypeError::Schema(_)),
"expected Schema error, got {err:?}"
);
}
#[test]
fn h2_diamond_refs_still_compute() {
let root = json!({
"$defs": {
"S": { "kind": "struct", "fields": [
{ "name": "a", "kind": { "$ref": "#/$defs/Point" } },
{ "name": "b", "kind": { "$ref": "#/$defs/Point" } }
] },
"Point": { "kind": "struct", "fields": [
{ "name": "x", "kind": "uint16" }
] }
}
});
let doc = BastDoc::new(&root, "S").expect("doc");
let m = OffsetMap::compute(&doc).expect("diamond refs compute");
assert_eq!(m.get("a.x").map(|e| e.range), Some(ByteRange { start: 0, end: 2 }));
assert_eq!(m.get("b.x").map(|e| e.range), Some(ByteRange { start: 2, end: 4 }));
}
// ----- M4 item 4: array-element endian propagation + OQ-001 gate ----
#[test]
fn m4_array_of_struct_element_rejected_oq001() {
// The gate that makes field_endian_for_element's composite arms
// dead: a struct element kind is variable-length, so it hits the
// OQ-001 rejection before the endian lookup.
let root = json!({
"$defs": {
"S": { "kind": "struct", "fields": [
{ "name": "points", "kind": { "kind": "array", "element": { "$ref": "#/$defs/Point" }, "count": 2 } }
] },
"Point": { "kind": "struct", "fields": [ { "name": "x", "kind": "uint16" } ] }
}
});
let doc = BastDoc::new(&root, "S").expect("doc");
let err = OffsetMap::compute(&doc).unwrap_err();
match err {
AlkTypeError::Offset { field_path, reason } => {
assert_eq!(field_path, "points");
assert!(reason.contains("OQ-001"), "reason: {reason}");
}
other => panic!("expected Offset, got {other:?}"),
}
}
#[test]
fn m4_array_element_endian_inherits_referring_field_not_element_own() {
// Phase-5 parity rule on the aligned path: the element's effective
// endian is the referring field's, not the element struct's own
// annotation (the pre-M4-cleanup code consulted the element
// struct's `endian` in a dead arm — dead because composites are
// rejected by OQ-001 upstream; this test pins the reachable
// propagation for fixed elements).
let root = json!({
"$defs": {
"S": {
"kind": "struct",
"endian": "big",
"fields": [
{
"name": "vals",
"kind": { "kind": "array", "element": "uint32", "count": 2 },
"endian": "little"
}
]
}
}
});
let m = map(&root, "S");
let e0 = m.get("vals[0]").expect("vals[0]");
assert_eq!(e0.meta.endian, Endian::Little, "field-level endian override propagates to elements");
let root_be = json!({
"$defs": {
"S": {
"kind": "struct",
"endian": "big",
"fields": [
{ "name": "vals", "kind": { "kind": "array", "element": "uint32", "count": 2 } }
]
}
}
});
let m_be = map(&root_be, "S");
assert_eq!(m_be.get("vals[0]").expect("vals[0]").meta.endian, Endian::Big, "struct default propagates to elements");
}
#[test]
fn non_final_record_rejected_in_aligned_mode() {
// M1: a Record field's inline length-prefixed form has the same
// 4-byte-prefix-then-variable-data shape as a string's, so a
// non-final record field must hit the ADR-006 rejection too
// (previously it computed silently corrupt offsets).
let root = json!({
"$defs": {
"S": {
"kind": "struct",
"fields": [
{ "name": "counts", "kind": { "kind": "record", "values": "uint32" } },
{ "name": "id", "kind": "uint32" }
]
}
}
});
let doc = BastDoc::new(&root, "S").expect("doc");
let err = OffsetMap::compute(&doc).unwrap_err();
match err {
AlkTypeError::Offset { field_path, reason } => {
assert_eq!(field_path, "counts");
assert!(reason.contains("ADR-006"), "reason: {reason}");
assert!(reason.contains("record"), "reason: {reason}");
}
other => panic!("expected Offset, got {other:?}"),
}
}
#[test]
fn final_inline_record_allowed_in_aligned_mode() {
let root = json!({
"$defs": {
"S": {
"kind": "struct",
"fields": [
{ "name": "id", "kind": "uint32" },
{ "name": "counts", "kind": { "kind": "record", "values": "uint32" } }
]
}
}
});
let m = map(&root, "S");
assert_eq!(m.get("id"), Some(&OffsetEntry { range: ByteRange { start: 0, end: 4 }, meta: LeafMeta { kind: AlkTypeKind::Uint32, encoding: VariableEncoding::LengthPrefixed, endian: Endian::Little } }));
assert_eq!(m.get("counts"), Some(&OffsetEntry { range: ByteRange { start: 4, end: 8 }, meta: LeafMeta { kind: AlkTypeKind::Record, encoding: VariableEncoding::LengthPrefixed, endian: Endian::Little } }));
assert_eq!(m.total_size(), 8);
}
// ----- M5: record offset-indirect rejected in aligned mode; record
// maxLength now rejected earlier, at parse (N3) — see the n3_ family
// in bast.rs
#[test]
fn m5_record_offset_indirect_rejected_in_aligned_mode() {
// Same walk-shape mismatch: the materializer reads the inline
// count-prefixed form, not the {offset, length} pair the map
// would have recorded.
let root = json!({
"$defs": {
"S": {
"kind": "struct",
"fields": [
{ "name": "counts", "kind": { "kind": "record", "values": "uint16" }, "encoding": "offset-indirect" },
{ "name": "id", "kind": "uint32" }
]
}
}
});
let doc = BastDoc::new(&root, "S").expect("doc parses");
let err = OffsetMap::compute(&doc).unwrap_err();
match err {
AlkTypeError::Offset { field_path, reason } => {
assert_eq!(field_path, "counts");
assert!(reason.contains("offset-indirect"), "reason: {reason}");
assert!(reason.contains("record"), "reason: {reason}");
}
other => panic!("expected Offset, got {other:?}"),
}
}
#[test]
fn non_final_inline_string_rejected_in_aligned_mode() {
let root = json!({
"$defs": {
"S": {
"kind": "struct",
"fields": [
{ "name": "name", "kind": "string" },
{ "name": "id", "kind": "uint32" }
]
}
}
});
let doc = BastDoc::new(&root, "S").expect("doc");
let err = OffsetMap::compute(&doc).unwrap_err();
match err {
AlkTypeError::Offset { field_path, reason } => {
assert_eq!(field_path, "name");
assert!(reason.contains("ADR-006"), "reason: {reason}");
}
other => panic!("expected Offset, got {other:?}"),
}
}
#[test]
fn final_inline_string_allowed_in_aligned_mode() {
let root = json!({
"$defs": {
"S": {
"kind": "struct",
"fields": [
{ "name": "id", "kind": "uint32" },
{ "name": "name", "kind": "string" }
]
}
}
});
let m = map(&root, "S");
assert_eq!(m.get("id"), Some(&OffsetEntry { range: ByteRange { start: 0, end: 4 }, meta: LeafMeta { kind: AlkTypeKind::Uint32, encoding: VariableEncoding::LengthPrefixed, endian: Endian::Little } }));
assert_eq!(m.get("name"), Some(&OffsetEntry { range: ByteRange { start: 4, end: 8 }, meta: LeafMeta { kind: AlkTypeKind::String, encoding: VariableEncoding::LengthPrefixed, endian: Endian::Little } }));
}
#[test]
fn non_final_maxlength_string_allowed_in_aligned_mode() {
let root = json!({
"$defs": {
"S": {
"kind": "struct",
"fields": [
{ "name": "name", "kind": "string", "maxLength": 256 },
{ "name": "id", "kind": "uint32" }
]
}
}
});
let m = map(&root, "S");
assert_eq!(m.get("name"), Some(&OffsetEntry { range: ByteRange { start: 0, end: 256 }, meta: LeafMeta { kind: AlkTypeKind::String, encoding: VariableEncoding::LengthPrefixed, endian: Endian::Little } }));
assert_eq!(m.get("id"), Some(&OffsetEntry { range: ByteRange { start: 256, end: 260 }, meta: LeafMeta { kind: AlkTypeKind::Uint32, encoding: VariableEncoding::LengthPrefixed, endian: Endian::Little } }));
}
#[test]
fn non_final_offset_indirect_string_allowed_in_aligned_mode() {
let root = json!({
"$defs": {
"S": {
"kind": "struct",
"fields": [
{ "name": "blob", "kind": "string", "encoding": "offset-indirect" },
{ "name": "id", "kind": "uint32" }
]
}
}
});
let m = map(&root, "S");
assert_eq!(m.get("blob"), Some(&OffsetEntry { range: ByteRange { start: 0, end: 8 }, meta: LeafMeta { kind: AlkTypeKind::String, encoding: VariableEncoding::OffsetIndirect, endian: Endian::Little } }));
assert_eq!(m.get("id"), Some(&OffsetEntry { range: ByteRange { start: 8, end: 12 }, meta: LeafMeta { kind: AlkTypeKind::Uint32, encoding: VariableEncoding::LengthPrefixed, endian: Endian::Little } }));
}
#[test]
fn struct_level_align_rounds_up_total() {
let root = json!({
"$defs": {
"S": {
"kind": "struct",
"align": 16,
"fields": [
{ "name": "flag", "kind": "uint8" }
]
}
}
});
let m = map(&root, "S");
assert_eq!(m.get("flag"), Some(&OffsetEntry { range: ByteRange { start: 0, end: 1 }, meta: LeafMeta { kind: AlkTypeKind::Uint8, encoding: VariableEncoding::LengthPrefixed, endian: Endian::Little } }));
assert_eq!(m.total_size(), 16);
}
#[test]
fn field_level_align_overrides_struct_default() {
let root = json!({
"$defs": {
"S": {
"kind": "struct",
"align": 1,
"fields": [
{ "name": "tag", "kind": "uint8" },
{ "name": "flag", "kind": "uint8", "align": 16 },
{ "name": "id", "kind": "uint32" }
]
}
}
});
let m = map(&root, "S");
assert_eq!(m.get("tag"), Some(&OffsetEntry { range: ByteRange { start: 0, end: 1 }, meta: LeafMeta { kind: AlkTypeKind::Uint8, encoding: VariableEncoding::LengthPrefixed, endian: Endian::Little } }));
assert_eq!(m.get("flag"), Some(&OffsetEntry { range: ByteRange { start: 16, end: 17 }, meta: LeafMeta { kind: AlkTypeKind::Uint8, encoding: VariableEncoding::LengthPrefixed, endian: Endian::Little } }));
assert_eq!(m.get("id"), Some(&OffsetEntry { range: ByteRange { start: 20, end: 24 }, meta: LeafMeta { kind: AlkTypeKind::Uint32, encoding: VariableEncoding::LengthPrefixed, endian: Endian::Little } }));
assert_eq!(m.total_size(), 24);
}
#[test]
fn field_align_smaller_than_struct_default() {
let root = json!({
"$defs": {
"S": {
"kind": "struct",
"align": 8,
"fields": [
{ "name": "a", "kind": "uint8" },
{ "name": "b", "kind": "uint32", "align": 1 }
]
}
}
});
let m = map(&root, "S");
assert_eq!(m.get("a"), Some(&OffsetEntry { range: ByteRange { start: 0, end: 1 }, meta: LeafMeta { kind: AlkTypeKind::Uint8, encoding: VariableEncoding::LengthPrefixed, endian: Endian::Little } }));
assert_eq!(m.get("b"), Some(&OffsetEntry { range: ByteRange { start: 1, end: 5 }, meta: LeafMeta { kind: AlkTypeKind::Uint32, encoding: VariableEncoding::LengthPrefixed, endian: Endian::Little } }));
assert_eq!(m.total_size(), 8);
}
#[test]
fn iter_returns_all_paths_in_order() {
let root = json!({
"$defs": {
"S": {
"kind": "struct",
"fields": [
{ "name": "a", "kind": "uint8" },
{ "name": "b", "kind": "uint32" }
]
}
}
});
let m = map(&root, "S");
let paths: Vec<&str> = m.iter().map(|(p, _)| p).collect();
assert_eq!(paths, vec!["a", "b"]);
}
// ----- L4: path-indexed random access --------------------------------
#[test]
fn l4_get_is_indexed_random_access_over_large_nested_schema() {
// The map's doc contract promises random access by field path
// ("read field N without reading fields 0..N-1 first"); L4 made
// that true with a BTreeMap index instead of a linear scan. A
// wide nested schema exercises dotted-path lookups that arrive
// late in insertion order.
let mut fields = Vec::new();
for i in 0..40 {
fields.push(json!({
"name": format!("blk{i}"),
"kind": {
"kind": "struct",
"fields": [
{ "name": "id", "kind": "uint32" },
{ "name": "tag", "kind": "uint8" },
{ "name": "val", "kind": "uint64" }
]
}
}));
}
let root = json!({
"$defs": { "S": { "kind": "struct", "fields": fields } }
});
let m = map(&root, "S");
assert_eq!(m.iter().count(), 120);
let last = m.get("blk39.val").expect("late dotted-path lookup");
assert_eq!(last.range, ByteRange { start: 39 * 16 + 8, end: 39 * 16 + 16 });
assert_eq!(last.meta.kind, AlkTypeKind::Uint64);
let mid = m.get("blk20.tag").expect("mid lookup");
assert_eq!(mid.range, ByteRange { start: 20 * 16 + 4, end: 20 * 16 + 5 });
assert_eq!(m.get("blk7.id").map(|e| e.range), Some(ByteRange { start: 7 * 16, end: 7 * 16 + 4 }));
assert_eq!(m.get("nope"), None);
assert_eq!(m.get("blk"), None);
assert_eq!(m.get("blk20"), None);
}
#[test]
fn l4_duplicate_field_paths_first_occurrence_wins() {
// BastStruct::parse does not reject duplicate field names, so two
// same-named siblings produce two entries with the same path. The
// pre-L4 linear scan returned the first; the index preserves that
// semantic.
let root = json!({
"$defs": {
"S": {
"kind": "struct",
"fields": [
{ "name": "a", "kind": "uint8" },
{ "name": "a", "kind": "uint16" }
]
}
}
});
let m = map(&root, "S");
let first = m.get("a").expect("duplicate path resolves");
assert_eq!(first.range, ByteRange { start: 0, end: 1 });
assert_eq!(first.meta.kind, AlkTypeKind::Uint8);
assert_eq!(m.iter().count(), 2);
}
// ----- Fingerprint contract (ADR-012 §1/§4) ---------------------------
#[test]
fn fingerprint_is_equal_for_equal_maps() {
let root = json!({ "$defs": { "S": { "kind": "struct", "fields": [
{ "name": "a", "kind": "uint8" },
{ "name": "b", "kind": "uint32" }
]}}});
let m1 = map(&root, "S");
let m2 = map(&root, "S");
assert_eq!(m1, m2);
assert_eq!(m1.fingerprint(), m2.fingerprint());
}
#[test]
fn fingerprint_changes_when_a_field_changes() {
let root_a = json!({ "$defs": { "S": { "kind": "struct", "fields": [
{ "name": "a", "kind": "uint8" },
{ "name": "b", "kind": "uint32" }
]}}});
let root_b = json!({ "$defs": { "S": { "kind": "struct", "fields": [
{ "name": "a", "kind": "uint16" },
{ "name": "b", "kind": "uint32" }
]}}});
let ma = map(&root_a, "S");
let mb = map(&root_b, "S");
assert_ne!(ma, mb);
assert_ne!(ma.fingerprint(), mb.fingerprint());
}
#[test]
fn fingerprint_changes_when_field_order_changes() {
let root_a = json!({ "$defs": { "S": { "kind": "struct", "fields": [
{ "name": "a", "kind": "uint8" },
{ "name": "b", "kind": "uint8" }
]}}});
let root_b = json!({ "$defs": { "S": { "kind": "struct", "fields": [
{ "name": "b", "kind": "uint8" },
{ "name": "a", "kind": "uint8" }
]}}});
let ma = map(&root_a, "S");
let mb = map(&root_b, "S");
assert_ne!(ma, mb);
assert_ne!(ma.fingerprint(), mb.fingerprint());
}
#[test]
fn fingerprint_changes_when_endianness_changes() {
let root_a = json!({ "$defs": { "S": { "kind": "struct", "endian": "little", "fields": [
{ "name": "id", "kind": "uint32" }
]}}});
let root_b = json!({ "$defs": { "S": { "kind": "struct", "endian": "big", "fields": [
{ "name": "id", "kind": "uint32" }
]}}});
let ma = map(&root_a, "S");
let mb = map(&root_b, "S");
assert_ne!(ma, mb);
assert_ne!(ma.fingerprint(), mb.fingerprint());
}
#[test]
fn compute_rejects_non_struct_top_level() {
let root = json!({
"$defs": {
"U": {
"kind": "union",
"discriminator": { "kind": "byte", "offset": 0, "type": "uint8" },
"mapping": { "1": { "$ref": "#/$defs/A" } }
},
"A": { "kind": "struct", "fields": [] }
}
});
let doc = BastDoc::new(&root, "U").expect("doc");
let err = OffsetMap::compute(&doc).unwrap_err();
assert!(matches!(err, AlkTypeError::Schema(_)));
}
#[test]
fn byte_range_len_and_is_empty() {
let r = ByteRange { start: 4, end: 8 };
assert_eq!(r.len(), 4);
assert!(!r.is_empty());
let empty = ByteRange { start: 5, end: 5 };
assert_eq!(empty.len(), 0);
assert!(empty.is_empty());
}
}