Port alknet-typedef crate from alknet

Copy the binary struct engine (src/, tests/) verbatim from
alknet/crates/alknet-typedef and create a standalone Cargo.toml
(workspace-inherited fields inlined). Port the architecture docs
(specs, ADRs 095-102, OQs 069-071) from alknet's nested multi-crate
layout to a flat single-crate layout, fixing relative link paths.

Build, 295 tests, and clippy all pass clean.
This commit is contained in:
glm-5.2 committed 2026-08-02 05:59:12 +00:00
1 parent eac7ad88b3
commit 2c4a4994dc
36 files changed
+13805

No files matched your search

+873
View File
@@ -0,0 +1,873 @@
//! Aligned static `OffsetMap` — Mode 2 of the two layout modes (ADR-096).
//!
//! 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 schema JSON. 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::error::TypedefError;
use crate::schema::{
get_typedef_kind, get_typedef_kind_loose_enum, parse_align, parse_encoding,
parse_max_length, resolve_ref_or_inline, TypeDefKind, VariableEncoding,
};
use serde_json::Value;
/// 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 schema.
///
/// 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 schema JSON value.
///
/// Walks the schema recursively, computing byte positions for each
/// field based on type sizes, field order, and alignment. The
/// top-level schema must be a `TypeDef:Struct`.
///
/// # Errors
///
/// Returns [`TypedefError::Schema`] if the top-level schema is not a
/// `TypeDef:Struct` or has no `TypeDef:*` kind, or if the schema is
/// malformed (missing `properties`, unknown kind, etc.).
///
/// Returns [`TypedefError::Offset`] for unsupported type combinations
/// encountered during the walk.
pub fn compute(schema: &Value) -> Result<Self, TypedefError> {
let kind = get_typedef_kind(schema)
.and_then(|s| s.parse::<TypeDefKind>().ok())
.ok_or_else(|| {
TypedefError::Schema("top-level schema has no TypeDef:* kind".to_string())
})?;
if kind != TypeDefKind::Struct {
return Err(TypedefError::Schema(format!(
"OffsetMap::compute requires a TypeDef:Struct at the top level, got {kind}"
)));
}
let mut ctx = ComputeCtx {
root: schema,
fields: Vec::new(),
offset: 0,
};
let (total, _align) = ctx.compute_struct(schema, "", 1)?;
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 schema's `properties` order (preserved by
/// `serde_json`'s `preserve_order` feature). 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 root schema for `$ref` resolution. Grouping these
/// keeps the recursive helper signatures small.
struct ComputeCtx<'a> {
root: &'a Value,
fields: Vec<(String, ByteRange)>,
offset: usize,
}
/// Result of laying out a single field: its alignment.
struct FieldLayout {
align: usize,
}
impl<'a> ComputeCtx<'a> {
/// Recurse into a `TypeDef:Struct`, 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).
///
/// `struct_schema` is the schema of the struct to walk. `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_schema: &Value,
prefix: &str,
parent_struct_align: usize,
) -> Result<(usize, usize), TypedefError> {
let obj = struct_schema
.as_object()
.ok_or_else(|| TypedefError::Schema("struct schema is not an object".to_string()))?;
let properties = obj
.get("properties")
.and_then(|v| v.as_object())
.ok_or_else(|| {
TypedefError::Schema("struct schema has no 'properties' object".to_string())
})?;
let struct_default_align = parse_align(struct_schema).unwrap_or(parent_struct_align);
let mut max_align: usize = 1;
let struct_start = self.offset;
let field_schemas: Vec<(String, Value)> = properties
.iter()
.map(|(k, v)| (k.clone(), v.clone()))
.collect();
let field_count = field_schemas.len();
for (i, (field_name, field_schema)) in field_schemas.iter().enumerate() {
let field_path = if prefix.is_empty() {
field_name.clone()
} else {
format!("{prefix}.{field_name}")
};
// ADR-100: reject non-final inline length-prefixed variable fields.
// The OffsetMap reserves only 4 bytes (the length prefix), but
// data_access::write_string writes prefix+data inline — clobbering
// subsequent fields. Only allowed as the last field in the struct.
if i < field_count - 1 {
if let Some(kind) = get_typedef_kind_loose_enum(field_schema) {
if kind.is_variable_length() {
let keyword_value = field_schema
.as_object()
.and_then(|o| {
o.keys()
.find(|k| k.starts_with("TypeDef:"))
.and_then(|k| o.get(k))
})
.cloned()
.unwrap_or(Value::Bool(true));
let encoding = parse_encoding(&keyword_value);
let max_length = parse_max_length(field_schema);
let is_inline_length_prefixed =
encoding == VariableEncoding::LengthPrefixed && max_length.is_none();
if is_inline_length_prefixed {
return Err(TypedefError::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-100)"
),
});
}
}
}
}
let layout = self.compute_field(field_schema, &field_path, struct_default_align)?;
if layout.align > max_align {
max_align = layout.align;
}
}
let effective_align = parse_align(struct_schema).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_schema: &Value,
field_path: &str,
struct_default_align: usize,
) -> Result<FieldLayout, TypedefError> {
let kind = get_typedef_kind_loose_enum(field_schema).ok_or_else(|| TypedefError::Offset {
field_path: field_path.to_string(),
reason: "field schema has no TypeDef:* kind".to_string(),
})?;
match kind {
TypeDefKind::Struct => {
self.compute_struct_field(field_schema, field_path, struct_default_align)
}
TypeDefKind::Union => Err(TypedefError::Offset {
field_path: field_path.to_string(),
reason: "TUnion is not supported in aligned static mode (ADR-102). \
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(),
}),
TypeDefKind::Array => {
self.compute_array_field(field_schema, field_path, struct_default_align)
}
TypeDefKind::String
| TypeDefKind::Bytes
| TypeDefKind::Record
| TypeDefKind::Timestamp => {
self.compute_variable_field(field_schema, field_path, struct_default_align)
}
k if k.is_fixed_size() => {
self.compute_fixed_field(k, field_schema, field_path, struct_default_align)
}
_ => unreachable!("all TypeDefKind variants are covered above"),
}
}
/// Compute the layout for a fixed-size primitive field.
fn compute_fixed_field(
&mut self,
kind: TypeDefKind,
field_schema: &Value,
field_path: &str,
struct_default_align: usize,
) -> Result<FieldLayout, TypedefError> {
let size = kind.type_size().ok_or_else(|| TypedefError::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_schema, struct_default_align, natural);
align_up(&mut self.offset, align);
let start = self.offset;
self.offset += size;
self.push(field_path, start, start + size);
Ok(FieldLayout { align })
}
/// Compute the layout for a nested `TypeDef: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,
field_schema: &Value,
field_path: &str,
struct_default_align: usize,
) -> Result<FieldLayout, TypedefError> {
let inner_parent_align = parse_align(field_schema).unwrap_or(struct_default_align);
let mut probe = ComputeCtx {
root: self.root,
fields: Vec::new(),
offset: 0,
};
let (inner_total, inner_align) =
probe.compute_struct(field_schema, field_path, inner_parent_align)?;
let align = field_alignment(field_schema, struct_default_align, inner_align);
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 + inner_total;
Ok(FieldLayout { align })
}
/// Compute the layout for a `TypeDef:Array` field.
fn compute_array_field(
&mut self,
field_schema: &Value,
field_path: &str,
struct_default_align: usize,
) -> Result<FieldLayout, TypedefError> {
let obj = field_schema
.as_object()
.ok_or_else(|| TypedefError::Offset {
field_path: field_path.to_string(),
reason: "array schema is not an object".to_string(),
})?;
let items = obj.get("items").ok_or_else(|| TypedefError::Offset {
field_path: field_path.to_string(),
reason: "TArray is missing 'items'".to_string(),
})?;
let element_schema =
resolve_ref_or_inline(items, self.root).ok_or_else(|| TypedefError::Offset {
field_path: field_path.to_string(),
reason: "could not resolve TArray items schema".to_string(),
})?;
let elem_kind = get_typedef_kind(element_schema)
.and_then(|s| s.parse::<TypeDefKind>().ok())
.ok_or_else(|| TypedefError::Offset {
field_path: field_path.to_string(),
reason: "TArray element schema has no TypeDef:* kind".to_string(),
})?;
if !elem_kind.is_fixed_size() {
return Err(TypedefError::Offset {
field_path: field_path.to_string(),
reason: format!(
"TArray of variable-length element kind {elem_kind} is not supported (OQ-069)"
),
});
}
let elem_size = elem_kind.type_size().ok_or_else(|| TypedefError::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 = field_alignment(element_schema, struct_default_align, elem_natural);
let stride = round_up(elem_size, elem_align);
let min_items = obj
.get("minItems")
.and_then(|v| v.as_u64())
.map(|n| n as usize);
let max_items = obj
.get("maxItems")
.and_then(|v| v.as_u64())
.map(|n| n as usize);
let fixed_count = match (min_items, max_items) {
(Some(mn), Some(mx)) if mn == mx => Some(mn),
_ => None,
};
let array_align = field_alignment(field_schema, struct_default_align, elem_align);
if let Some(count) = fixed_count {
align_up(&mut self.offset, array_align);
let start = self.offset;
for i in 0..count {
let elem_start = start + i * stride;
let elem_end = elem_start + elem_size;
let elem_path = format!("{field_path}[{i}]");
self.push(&elem_path, elem_start, elem_end);
}
let array_size = count * stride;
self.offset = start + array_size;
Ok(FieldLayout { align: array_align })
} else {
let count_prefix_align = array_align.max(4);
align_up(&mut self.offset, count_prefix_align);
let start = self.offset;
self.push(field_path, start, start + 4);
self.offset = start + 4;
Ok(FieldLayout {
align: count_prefix_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_schema: &Value,
field_path: &str,
struct_default_align: usize,
) -> Result<FieldLayout, TypedefError> {
let keyword_value = field_schema
.as_object()
.and_then(|o| {
o.keys()
.find(|k| k.starts_with("TypeDef:"))
.and_then(|k| o.get(k))
})
.cloned()
.unwrap_or(Value::Bool(true));
let encoding = parse_encoding(&keyword_value);
let max_length = parse_max_length(field_schema);
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_schema, struct_default_align, natural);
align_up(&mut self.offset, align);
let start = self.offset;
self.offset += size;
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 }));
}
}
/// Resolve the field's alignment: field-level `align` annotation,
/// then the struct default, then the natural alignment.
fn field_alignment(field_schema: &Value, struct_default_align: usize, natural: usize) -> usize {
if let Some(a) = parse_align(field_schema) {
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(schema: &Value) -> OffsetMap {
OffsetMap::compute(schema).expect("offset map computation")
}
#[test]
fn simple_fixed_fields_natural_alignment() {
let schema = json!({
"TypeDef:Struct": true,
"properties": {
"flag": { "TypeDef:Uint8": true },
"id": { "TypeDef:Uint32": true }
}
});
let m = map(&schema);
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 schema = json!({
"TypeDef:Struct": true,
"properties": {
"a": { "TypeDef:Uint8": true },
"b": { "TypeDef:Uint32": true }
}
});
let m = map(&schema);
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 schema = json!({
"TypeDef:Struct": true,
"properties": {
"header": {
"TypeDef:Struct": true,
"properties": {
"magic": { "TypeDef:Uint32": true },
"version": { "TypeDef:Uint8": true }
}
},
"body": { "TypeDef:Uint32": true }
}
});
let m = map(&schema);
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 schema = json!({
"TypeDef:Struct": true,
"properties": {
"vals": {
"TypeDef:Array": true,
"items": { "TypeDef:Uint32": true },
"minItems": 3,
"maxItems": 3
}
}
});
let m = map(&schema);
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 array_variable_count_length_prefix() {
let schema = json!({
"TypeDef:Struct": true,
"properties": {
"vals": {
"TypeDef:Array": true,
"items": { "TypeDef:Uint32": true }
}
}
});
let m = map(&schema);
assert_eq!(m.get("vals"), Some(&ByteRange { start: 0, end: 4 }));
assert_eq!(m.total_size(), 4);
}
#[test]
fn variable_string_length_prefix_at_known_offset() {
let schema = json!({
"TypeDef:Struct": true,
"properties": {
"id": { "TypeDef:Uint32": true },
"name": { "TypeDef:String": true }
}
});
let m = map(&schema);
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 schema = json!({
"TypeDef:Struct": true,
"properties": {
"id": { "TypeDef:Uint32": true },
"name": { "TypeDef:String": true, "maxLength": 256 }
}
});
let m = map(&schema);
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 schema = json!({
"TypeDef:Struct": true,
"properties": {
"id": { "TypeDef:Uint32": true },
"blob": { "TypeDef:String": { "encoding": "offset-indirect" } }
}
});
let m = map(&schema);
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 schema = json!({
"TypeDef:Struct": true,
"properties": {
"payload": {
"TypeDef:Union": true,
"discriminator": {
"kind": "byte",
"offset": 0,
"type": "TypeDef:Uint8"
},
"mapping": {
"5": { "$ref": "#/$defs/Read" },
"6": { "$ref": "#/$defs/Write" }
}
}
},
"$defs": {
"Read": {
"TypeDef:Struct": true,
"properties": {
"handle": { "TypeDef:Uint32": true },
"length": { "TypeDef:Uint32": true }
}
},
"Write": {
"TypeDef:Struct": true,
"properties": {
"handle": { "TypeDef:Uint32": true },
"length": { "TypeDef:Uint32": true },
"data": { "TypeDef:Uint32": true }
}
}
}
});
let err = OffsetMap::compute(&schema).unwrap_err();
assert!(matches!(err, TypedefError::Offset { .. }), "got {err:?}");
let reason = match err {
TypedefError::Offset { reason, .. } => reason,
_ => unreachable!(),
};
assert!(reason.contains("ADR-102"), "reason: {reason}");
}
#[test]
fn union_field_name_discriminator_rejected_in_aligned_mode() {
let schema = json!({
"TypeDef:Struct": true,
"properties": {
"event": {
"TypeDef:Union": true,
"discriminator": { "kind": "field", "name": "type" },
"mapping": {
"read": { "$ref": "#/$defs/Read" },
"write": { "$ref": "#/$defs/Write" }
}
}
},
"$defs": {
"Read": {
"TypeDef:Struct": true,
"properties": {
"type": { "TypeDef:Uint8": true },
"handle": { "TypeDef:Uint32": true }
}
},
"Write": {
"TypeDef:Struct": true,
"properties": {
"type": { "TypeDef:Uint8": true },
"handle": { "TypeDef:Uint32": true },
"length": { "TypeDef:Uint32": true }
}
}
}
});
let err = OffsetMap::compute(&schema).unwrap_err();
assert!(matches!(err, TypedefError::Offset { .. }), "got {err:?}");
}
#[test]
fn non_final_inline_string_rejected_in_aligned_mode() {
let schema = json!({
"TypeDef:Struct": true,
"properties": {
"name": { "TypeDef:String": true },
"id": { "TypeDef:Uint32": true }
}
});
let err = OffsetMap::compute(&schema).unwrap_err();
match err {
TypedefError::Offset { field_path, reason } => {
assert_eq!(field_path, "name");
assert!(reason.contains("ADR-100"), "reason: {reason}");
}
other => panic!("expected Offset, got {other:?}"),
}
}
#[test]
fn final_inline_string_allowed_in_aligned_mode() {
let schema = json!({
"TypeDef:Struct": true,
"properties": {
"id": { "TypeDef:Uint32": true },
"name": { "TypeDef:String": true }
}
});
let m = map(&schema);
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 schema = json!({
"TypeDef:Struct": true,
"properties": {
"name": { "TypeDef:String": true, "maxLength": 256 },
"id": { "TypeDef:Uint32": true }
}
});
let m = map(&schema);
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 schema = json!({
"TypeDef:Struct": true,
"properties": {
"blob": { "TypeDef:String": { "encoding": "offset-indirect" } },
"id": { "TypeDef:Uint32": true }
}
});
let m = map(&schema);
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 schema = json!({
"TypeDef:Struct": true,
"align": 16,
"properties": {
"flag": { "TypeDef:Uint8": true }
}
});
let m = map(&schema);
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 schema = json!({
"TypeDef:Struct": true,
"align": 1,
"properties": {
"tag": { "TypeDef:Uint8": true },
"flag": { "TypeDef:Uint8": true, "align": 16 },
"id": { "TypeDef:Uint32": true }
}
});
let m = map(&schema);
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 schema = json!({
"TypeDef:Struct": true,
"align": 8,
"properties": {
"a": { "TypeDef:Uint8": true },
"b": { "TypeDef:Uint32": true, "align": 1 }
}
});
let m = map(&schema);
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 schema = json!({
"TypeDef:Struct": true,
"properties": {
"a": { "TypeDef:Uint8": true },
"b": { "TypeDef:Uint32": true }
}
});
let m = map(&schema);
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 schema =
json!({ "TypeDef:Union": true, "discriminator": { "kind": "byte" }, "mapping": {} });
let err = OffsetMap::compute(&schema).unwrap_err();
assert!(matches!(err, TypedefError::Schema(_)));
}
#[test]
fn compute_rejects_missing_typedef_kind() {
let schema = json!({ "type": "object", "properties": {} });
let err = OffsetMap::compute(&schema).unwrap_err();
assert!(matches!(err, TypedefError::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());
}
}