Files
alktype/src/offset_map.rs
T
deepseek-v4-pro 62ed009281 Resolve review #003 nits N1, N3, N4
- N1: number_from_f64 now returns AlkTypeError::Access for non-finite
  floats instead of silently materializing Value::Null. A NaN/Inf in the
  buffer surfaces as a clear access error rather than a misleading
  'expected a number' validation error.
- N3: drop the dead Value::String arm from check_bytes; the materializer
  only ever emits bytes as an array of u8. Update the validation-model
  docs to match.
- N4: fix stale ADR references in doc comments (ADR-096 -> ADR-002,
  ADR-097 -> ADR-003, ADR-098 -> ADR-004, ADR-101 -> ADR-007).

N2 (BastType::alk_kind returning Struct for ) left as documented;
every current caller resolves the ref first, so forcing Option through
the call sites is churn without benefit.

Verification: cargo test --release (411 pass), cargo clippy --all-targets
-- -D warnings (clean), cargo doc --no-deps (clean).
2026-08-16 08:59:41 +00:00

876 lines
32 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, VariableEncoding};
/// 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)]
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
}
}
/// A flat table of `(field_path, byte_range)` 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.
#[derive(Debug)]
pub struct OffsetMap {
fields: Vec<(String, ByteRange)>,
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.
/// 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<'a>(doc: &'a BastDoc<'a>) -> 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()
)));
}
};
let mut ctx = ComputeCtx {
doc,
fields: Vec::new(),
offset: 0,
};
let struct_default_align = struct_node.align().unwrap_or(1).max(1);
let (total, _align) = ctx.compute_struct(struct_node, "", struct_default_align)?;
Ok(Self {
fields: ctx.fields,
total_size: total,
})
}
/// Look up a field's byte range 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<&ByteRange> {
self.fields
.iter()
.find(|(path, _)| path == field_path)
.map(|(_, range)| range)
}
/// 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, byte_range)` 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 = &(String, ByteRange)> {
self.fields.iter()
}
}
/// 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<'d>,
fields: Vec<(String, ByteRange)>,
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<'d>,
prefix: &str,
parent_struct_align: usize,
) -> 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 {
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. Use `maxLength` \
(fixed-size reservation) or \
`\"encoding\": \"offset-indirect\"`, or move this \
field to the last position in the struct. (ADR-006)"
),
});
}
}
}
let layout = self.compute_field(field, &field_path, struct_default_align)?;
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<'d>,
field_path: &str,
struct_default_align: usize,
) -> Result<FieldLayout, AlkTypeError> {
let ty = field.ty();
let resolved = self.doc.resolve_typeref(ty)?;
match &resolved {
BastType::Struct(s) => self.compute_struct_field(s, field, field_path, struct_default_align),
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),
BastType::Record(_) => {
self.compute_variable_field(field, field_path, struct_default_align)
}
BastType::Primitive(k) if k.is_variable_length() => {
self.compute_variable_field(field, field_path, struct_default_align)
}
BastType::Primitive(k) => {
self.compute_fixed_field(*k, field, field_path, struct_default_align)
}
BastType::Enum(_) => self.compute_fixed_field(
AlkTypeKind::Enum,
field,
field_path,
struct_default_align,
),
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<'d>,
field_path: &str,
struct_default_align: usize,
) -> 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);
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<'d>,
field: &BastField<'d>,
field_path: &str,
struct_default_align: usize,
) -> 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)?;
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, range) in probe.fields {
self.fields.push((
path,
ByteRange {
start: struct_start + range.start,
end: struct_start + range.end,
},
));
}
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<'d>,
field: &BastField<'d>,
field_path: &str,
struct_default_align: usize,
) -> 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();
let elem_align = element_alignment(&resolved_elem, struct_default_align, elem_natural);
let stride = round_up(elem_size, elem_align);
let count = array.count();
let array_align = field_alignment(field, struct_default_align, elem_align);
align_up(&mut self.offset, array_align);
let start = self.offset;
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);
}
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/Timestamp).
///
/// 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<'d>,
field_path: &str,
struct_default_align: usize,
) -> 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);
Ok(FieldLayout { align })
}
/// Push a `(field_path, ByteRange)` pair onto the fields vec.
fn push(&mut self, path: &str, start: usize, end: usize) {
self.fields
.push((path.to_string(), ByteRange { start, end }));
}
}
/// If the field's type is a variable-length primitive, return its kind.
fn field_variable_kind(field: &BastField<'_>) -> Option<AlkTypeKind> {
match field.ty() {
BastType::Primitive(k) if k.is_variable_length() => Some(*k),
_ => None,
}
}
/// 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)
}
/// Resolve an element type's alignment. Inline struct/array elements use
/// natural alignment 1; primitives use their natural alignment; field-level
/// `align` from the enclosing field node doesn't apply to inline element
/// schemas (BAST field-level annotations live on the field, not the
/// element TypeRef), so we fall back to the struct default.
fn element_alignment(
elem_ty: &BastType<'_>,
struct_default_align: usize,
natural: usize,
) -> usize {
match elem_ty {
BastType::Struct(_) | BastType::Union(_) | BastType::Array(_) => {
struct_default_align.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(&ByteRange { start: 0, end: 1 }));
assert_eq!(m.get("id"), Some(&ByteRange { start: 4, end: 8 }));
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(&ByteRange { start: 0, end: 1 }));
assert_eq!(m.get("b"), Some(&ByteRange { start: 4, end: 8 }));
}
#[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(&ByteRange { start: 0, end: 4 }));
assert_eq!(
m.get("header.version"),
Some(&ByteRange { start: 4, end: 5 })
);
assert_eq!(m.get("body"), Some(&ByteRange { start: 8, end: 12 }));
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(&ByteRange { start: 0, end: 4 }));
assert_eq!(m.get("vals[1]"), Some(&ByteRange { start: 4, end: 8 }));
assert_eq!(m.get("vals[2]"), Some(&ByteRange { start: 8, end: 12 }));
assert_eq!(m.total_size(), 12);
}
#[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(&ByteRange { start: 0, end: 4 }));
assert_eq!(m.get("name"), Some(&ByteRange { start: 4, end: 8 }));
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(&ByteRange { start: 0, end: 4 }));
assert_eq!(m.get("name"), Some(&ByteRange { start: 4, end: 260 }));
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(&ByteRange { start: 0, end: 4 }));
assert_eq!(m.get("blob"), Some(&ByteRange { start: 4, end: 12 }));
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 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(&ByteRange { start: 0, end: 4 }));
assert_eq!(m.get("name"), Some(&ByteRange { start: 4, end: 8 }));
}
#[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(&ByteRange { start: 0, end: 256 }));
assert_eq!(m.get("id"), Some(&ByteRange { start: 256, end: 260 }));
}
#[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(&ByteRange { start: 0, end: 8 }));
assert_eq!(m.get("id"), Some(&ByteRange { start: 8, end: 12 }));
}
#[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(&ByteRange { start: 0, end: 1 }));
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(&ByteRange { start: 0, end: 1 }));
assert_eq!(m.get("flag"), Some(&ByteRange { start: 16, end: 17 }));
assert_eq!(m.get("id"), Some(&ByteRange { start: 20, end: 24 }));
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(&ByteRange { start: 0, end: 1 }));
assert_eq!(m.get("b"), Some(&ByteRange { start: 1, end: 5 }));
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<&String> = m.iter().map(|(p, _)| p).collect();
assert_eq!(paths, vec!["a", "b"]);
}
#[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());
}
}