SequentialReader now walks Arc<ReadPlan> instead of re-parsing the BAST typed tree per field (the 400x read-path gap, review #004 H1); materialize_packed walks the same plan, unifying the two packed read-side consumers on one compiled form. - SequentialReader::new(Arc<ReadPlan>) -> Self, infallible: the fallible BastDoc parse moved to ReadPlan::compile (phase 1). The reader holds the plan Arc + cursor state only; schema() returns the Arc<Value> retained on the plan (review #005 H2 — no self-referential struct); new plan() accessor exposes the shared plan. - ReadPlan carries schema: Arc<Value> (set at compile; sub-plans hold a Null placeholder — only the root plan is handed out). - materialize_packed(&ReadPlan, &[u8]): plan-walking packed materializer. The aligned path keeps walking BastDoc with the retained dummy_field_for/ty_source/materialize_typeref_packed helpers (phase 5 Scope Boundary: aligned structure walk is the permanent 0.3.0 design). - Engine: Layout::Packed stores Arc<ReadPlan> alongside the builder; sequential_reader() is an Arc::clone (was a full-document Value clone); packed validate_bytes calls materialize_packed(&self.plan). - Stride (deferred decision 4): FieldValue::Array now reports the true stride for fixed-size struct/nested-array elements (0.2.0 returned 0); doc comment documents the behavioral change; no existing test asserted the 0, so none needed changing. - Two parity subtleties found and preserved: (a) materialize_plan_composite unwraps the plan's anonymous single-field wrapper for primitive array elements/record values — without it, materialized records nest each leaf under a synthetic object (caught by the record parity test); (b) field-disc unions keep 0.2.0's materialized key order (__discriminator first), observable under preserve_order. Both are now covered by plan-phase tests or construction. Bench (alktty wire_vs_bast, 1024 chunks/stream): packed read 2.27 us/chunk (review #004) -> 98 ns/chunk p64 / 100 ns/chunk p4k (~23x; the 400x gap closes to ~17x vs hand-rolled 5.6 ns/chunk). Residual gap is the per-field String allocation mandated by the unchanged (String, FieldValue) read_next signature (2 allocs/chunk) plus data_access bounds checks. sequential_reader() construction: 15.7 ns (was a whole-document clone). Verification: 465 tests pass unchanged (the existing reader/ materialize/engine suites drive the rewrite through the public API — only constructor call sites moved to ReadPlan::compile); clippy -D warnings clean; cargo doc zero warnings; wasm32 release build green.
1599 lines
58 KiB
Rust
1599 lines
58 KiB
Rust
//! Materialize a `serde_json::Value` tree from a binary buffer. Used by
|
|
//! [`crate::engine::AlkTypeEngine::validate_bytes`] (ADR-010) to collapse
|
|
//! the two-step dance (read bytes -> `Value`, then validate `Value`) into
|
|
//! a single call.
|
|
//!
|
|
//! Packed mode walks the compiled [`ReadPlan`]
|
|
//! (ADR-011) — the same compiled form [`crate::sequential_reader`] walks,
|
|
//! unifying the two packed read-side consumers on one plan. Aligned mode
|
|
//! walks the BAST typed tree, reading leaves at offsets from the
|
|
//! [`crate::offset_map::OffsetMap`] (the permanent 0.3.0 design — see the
|
|
//! phase 5 Scope Boundary in the 0.3.0 plan). Both produce the same
|
|
//! `Value` form; the validator is mode-agnostic.
|
|
//!
|
|
//! ## `$ref` resolution
|
|
//!
|
|
//! Packed mode: resolved eagerly at `ReadPlan::compile` — the per-buffer
|
|
//! walk never touches the BAST document. Aligned mode: the BAST document
|
|
//! ([`crate::bast::BastDoc`]) is threaded through every recursive call so
|
|
//! `$ref` pointers (e.g. `"#/$defs/Read"`) in composite variants resolve
|
|
//! via [`crate::bast::BastDoc::resolve_typeref`] at walk time —
|
|
//! load-bearing for unions and arrays whose variants/elements are `$ref`s
|
|
//! into `$defs`.
|
|
|
|
use crate::bast::{
|
|
BastArray, BastDefKind, BastDiscriminator, BastDoc, BastField, BastRecord, BastStruct,
|
|
BastType, BastUnion,
|
|
};
|
|
use crate::data_access;
|
|
use crate::error::AlkTypeError;
|
|
use crate::read_plan::{
|
|
CompositePlan, DiscriminatorPlan, FieldPlan, ReadKind, ReadPlan,
|
|
};
|
|
use crate::schema::{AlkTypeKind, Endian, VariableEncoding};
|
|
use serde_json::{Map, Value};
|
|
|
|
const U32_SIZE: usize = 4;
|
|
const DISCRIMINATOR_KEY: &str = "__discriminator";
|
|
|
|
/// Materialize a `Value` tree from `buffer` by walking the compiled
|
|
/// [`ReadPlan`] in packed mode (sequential, from offset 0).
|
|
///
|
|
/// The plan's root must be a struct (enforced at `ReadPlan::compile`).
|
|
/// Endianness is baked per node at compile time.
|
|
pub fn materialize_packed(
|
|
plan: &ReadPlan,
|
|
buffer: &[u8],
|
|
) -> Result<Value, AlkTypeError> {
|
|
let mut offset = 0usize;
|
|
let mut obj = Map::new();
|
|
for field in plan.fields() {
|
|
let value = materialize_plan_field(plan, field, field.name(), buffer, &mut offset)?;
|
|
obj.insert(field.name().to_string(), value);
|
|
}
|
|
Ok(Value::Object(obj))
|
|
}
|
|
|
|
/// Materialize one planned field (packed mode): dispatch on the
|
|
/// field's [`ReadKind`], reading leaves via [`crate::data_access`] and
|
|
/// recursing into the compiled composite body. `endian` is the field's
|
|
/// effective endianness baked into the plan.
|
|
fn materialize_plan_field(
|
|
plan: &ReadPlan,
|
|
field: &FieldPlan,
|
|
field_path: &str,
|
|
buffer: &[u8],
|
|
offset: &mut usize,
|
|
) -> Result<Value, AlkTypeError> {
|
|
let _ = plan;
|
|
match field.kind() {
|
|
ReadKind::Primitive(kind) => {
|
|
materialize_plan_primitive(*kind, field.endian(), field_path, buffer, offset)
|
|
}
|
|
ReadKind::Enum => {
|
|
let v = data_access::read_enum(buffer, *offset, field_path, field.endian())?;
|
|
*offset += 4;
|
|
Ok(Value::from(v))
|
|
}
|
|
ReadKind::Struct => {
|
|
let body = match field.body() {
|
|
Some(CompositePlan::Struct(p)) => p,
|
|
_ => {
|
|
return Err(AlkTypeError::Schema(format!(
|
|
"internal: struct field {field_path} has no Struct body"
|
|
)))
|
|
}
|
|
};
|
|
materialize_plan_struct(body, field_path, buffer, offset)
|
|
}
|
|
ReadKind::Union => {
|
|
let body = field.body().ok_or_else(|| AlkTypeError::Schema(format!(
|
|
"internal: union field {field_path} has no body"
|
|
)))?;
|
|
materialize_plan_union(body, field_path, field.endian(), buffer, offset)
|
|
}
|
|
ReadKind::Array => {
|
|
let body = field.body().ok_or_else(|| AlkTypeError::Schema(format!(
|
|
"internal: array field {field_path} has no body"
|
|
)))?;
|
|
materialize_plan_array(body, field_path, field.endian(), buffer, offset)
|
|
}
|
|
ReadKind::Record => {
|
|
let body = field.body().ok_or_else(|| AlkTypeError::Schema(format!(
|
|
"internal: record field {field_path} has no body"
|
|
)))?;
|
|
materialize_plan_record(body, field_path, field.endian(), buffer, offset)
|
|
}
|
|
}
|
|
}
|
|
|
|
fn materialize_plan_primitive(
|
|
kind: AlkTypeKind,
|
|
endian: Endian,
|
|
field_path: &str,
|
|
buffer: &[u8],
|
|
offset: &mut usize,
|
|
) -> Result<Value, AlkTypeError> {
|
|
match kind {
|
|
AlkTypeKind::Int8 => {
|
|
let v = data_access::read_i8(buffer, *offset, field_path)?;
|
|
*offset += 1;
|
|
Ok(Value::from(v))
|
|
}
|
|
AlkTypeKind::Int16 => {
|
|
let v = data_access::read_i16(buffer, *offset, field_path, endian)?;
|
|
*offset += 2;
|
|
Ok(Value::from(v))
|
|
}
|
|
AlkTypeKind::Int32 => {
|
|
let v = data_access::read_i32(buffer, *offset, field_path, endian)?;
|
|
*offset += 4;
|
|
Ok(Value::from(v))
|
|
}
|
|
AlkTypeKind::Int64 => {
|
|
let v = data_access::read_i64(buffer, *offset, field_path, endian)?;
|
|
*offset += 8;
|
|
Ok(Value::from(v))
|
|
}
|
|
AlkTypeKind::Uint8 => {
|
|
let v = data_access::read_u8(buffer, *offset, field_path)?;
|
|
*offset += 1;
|
|
Ok(Value::from(v))
|
|
}
|
|
AlkTypeKind::Uint16 => {
|
|
let v = data_access::read_u16(buffer, *offset, field_path, endian)?;
|
|
*offset += 2;
|
|
Ok(Value::from(v))
|
|
}
|
|
AlkTypeKind::Uint32 => {
|
|
let v = data_access::read_u32(buffer, *offset, field_path, endian)?;
|
|
*offset += 4;
|
|
Ok(Value::from(v))
|
|
}
|
|
AlkTypeKind::Uint64 => {
|
|
let v = data_access::read_u64(buffer, *offset, field_path, endian)?;
|
|
*offset += 8;
|
|
Ok(Value::from(v))
|
|
}
|
|
AlkTypeKind::Float32 => {
|
|
let v = data_access::read_f32(buffer, *offset, field_path, endian)?;
|
|
*offset += 4;
|
|
Ok(number_from_f64(v as f64, field_path)?)
|
|
}
|
|
AlkTypeKind::Float64 => {
|
|
let v = data_access::read_f64(buffer, *offset, field_path, endian)?;
|
|
*offset += 8;
|
|
Ok(number_from_f64(v, field_path)?)
|
|
}
|
|
AlkTypeKind::Boolean => {
|
|
let v = data_access::read_bool(buffer, *offset, field_path)?;
|
|
*offset += 1;
|
|
Ok(Value::Bool(v))
|
|
}
|
|
AlkTypeKind::String => {
|
|
let s = data_access::read_string(buffer, *offset, field_path, endian)?;
|
|
let len = U32_SIZE
|
|
.checked_add(s.len())
|
|
.ok_or_else(|| AlkTypeError::Access {
|
|
field_path: field_path.to_string(),
|
|
reason: "string size 4 + len overflows usize".to_string(),
|
|
})?;
|
|
*offset = checked_add_at(*offset, len, field_path, "string")?;
|
|
Ok(Value::String(s.to_string()))
|
|
}
|
|
AlkTypeKind::Bytes => {
|
|
let b = data_access::read_bytes(buffer, *offset, field_path, endian)?;
|
|
let len = U32_SIZE
|
|
.checked_add(b.len())
|
|
.ok_or_else(|| AlkTypeError::Access {
|
|
field_path: field_path.to_string(),
|
|
reason: "bytes size 4 + len overflows usize".to_string(),
|
|
})?;
|
|
*offset = checked_add_at(*offset, len, field_path, "bytes")?;
|
|
let arr: Vec<Value> = b.iter().map(|&byte| Value::from(u32::from(byte))).collect();
|
|
Ok(Value::Array(arr))
|
|
}
|
|
other => Err(AlkTypeError::Schema(format!(
|
|
"materialize: unsupported primitive kind {other} at {field_path}"
|
|
))),
|
|
}
|
|
}
|
|
|
|
fn checked_add_at(
|
|
offset: usize,
|
|
size: usize,
|
|
field_path: &str,
|
|
what: &str,
|
|
) -> Result<usize, AlkTypeError> {
|
|
offset.checked_add(size).ok_or_else(|| AlkTypeError::Access {
|
|
field_path: field_path.to_string(),
|
|
reason: format!("{what} offset {offset} + {size} overflows usize"),
|
|
})
|
|
}
|
|
|
|
fn materialize_plan_struct(
|
|
plan: &ReadPlan,
|
|
field_path: &str,
|
|
buffer: &[u8],
|
|
offset: &mut usize,
|
|
) -> Result<Value, AlkTypeError> {
|
|
let mut obj = Map::new();
|
|
for field in plan.fields() {
|
|
let path = format!("{field_path}.{}", field.name());
|
|
let value = materialize_plan_field(plan, field, &path, buffer, offset)?;
|
|
obj.insert(field.name().to_string(), value);
|
|
}
|
|
Ok(Value::Object(obj))
|
|
}
|
|
|
|
fn materialize_plan_array(
|
|
body: &CompositePlan,
|
|
field_path: &str,
|
|
endian: Endian,
|
|
buffer: &[u8],
|
|
offset: &mut usize,
|
|
) -> Result<Value, AlkTypeError> {
|
|
let (element, count) = match body {
|
|
CompositePlan::Array {
|
|
element,
|
|
count,
|
|
..
|
|
} => (element, *count),
|
|
_ => {
|
|
return Err(AlkTypeError::Schema(format!(
|
|
"internal: array body at {field_path} is not CompositePlan::Array"
|
|
)))
|
|
}
|
|
};
|
|
let mut arr = Vec::with_capacity(count);
|
|
for i in 0..count {
|
|
let path = format!("{field_path}[{i}]");
|
|
let value = materialize_plan_composite(element, &path, endian, buffer, offset)?;
|
|
arr.push(value);
|
|
}
|
|
Ok(Value::Array(arr))
|
|
}
|
|
|
|
/// Materialize a composite body used as a TypeRef (array element,
|
|
/// record value, union variant).
|
|
///
|
|
/// Primitive/enum elements and values were wrapped at compile time in an
|
|
/// anonymous single-field struct plan (the wrap-leaf convention) so the
|
|
/// read loop has a uniform node; the materializer unwraps that wrapper
|
|
/// and emits the leaf value directly — the 0.2.0 materializer never
|
|
/// wrapped (real schema field names cannot be empty, so an empty name
|
|
/// uniquely identifies the wrapper).
|
|
fn materialize_plan_composite(
|
|
body: &CompositePlan,
|
|
field_path: &str,
|
|
endian: Endian,
|
|
buffer: &[u8],
|
|
offset: &mut usize,
|
|
) -> Result<Value, AlkTypeError> {
|
|
if let CompositePlan::Struct(plan) = body {
|
|
if let Some(only) = plan.fields().first() {
|
|
if plan.fields().len() == 1
|
|
&& only.name().is_empty()
|
|
&& matches!(only.kind(), ReadKind::Primitive(_) | ReadKind::Enum)
|
|
{
|
|
return materialize_plan_field(plan, only, field_path, buffer, offset);
|
|
}
|
|
}
|
|
}
|
|
match body {
|
|
CompositePlan::Struct(plan) => materialize_plan_struct(plan, field_path, buffer, offset),
|
|
CompositePlan::Union { .. } => {
|
|
materialize_plan_union(body, field_path, endian, buffer, offset)
|
|
}
|
|
CompositePlan::Array { .. } => {
|
|
materialize_plan_array(body, field_path, endian, buffer, offset)
|
|
}
|
|
CompositePlan::Record { .. } => {
|
|
materialize_plan_record(body, field_path, endian, buffer, offset)
|
|
}
|
|
}
|
|
}
|
|
|
|
fn materialize_plan_union(
|
|
body: &CompositePlan,
|
|
field_path: &str,
|
|
endian: Endian,
|
|
buffer: &[u8],
|
|
offset: &mut usize,
|
|
) -> Result<Value, AlkTypeError> {
|
|
let (disc, shared, variants) = match body {
|
|
CompositePlan::Union {
|
|
disc,
|
|
shared,
|
|
variants,
|
|
} => (disc, shared, variants),
|
|
_ => {
|
|
return Err(AlkTypeError::Schema(format!(
|
|
"internal: union body at {field_path} is not CompositePlan::Union"
|
|
)))
|
|
}
|
|
};
|
|
match disc {
|
|
DiscriminatorPlan::Byte {
|
|
offset: disc_rel,
|
|
disc_type,
|
|
} => {
|
|
let disc_abs = checked_add_at(*offset, *disc_rel, field_path, "discriminator")?;
|
|
let disc_value = match disc_type {
|
|
AlkTypeKind::Uint8 => {
|
|
data_access::read_u8(buffer, disc_abs, field_path)? as u32
|
|
}
|
|
AlkTypeKind::Uint16 => {
|
|
data_access::read_u16(buffer, disc_abs, field_path, endian)? as u32
|
|
}
|
|
AlkTypeKind::Uint32 => {
|
|
data_access::read_u32(buffer, disc_abs, field_path, endian)?
|
|
}
|
|
other => {
|
|
return Err(AlkTypeError::Schema(format!(
|
|
"internal: union discriminator type {other} is not a supported byte \
|
|
discriminator (parser should have rejected this)"
|
|
)));
|
|
}
|
|
};
|
|
let key = disc_value.to_string();
|
|
let variant = variants
|
|
.iter()
|
|
.find(|(k, _)| *k == key)
|
|
.map(|(_, v)| v)
|
|
.ok_or_else(|| AlkTypeError::Access {
|
|
field_path: field_path.to_string(),
|
|
reason: format!("union discriminator value {key} not in mapping"),
|
|
})?;
|
|
let disc_size = disc_type.type_size().unwrap_or(1);
|
|
let variant_offset =
|
|
checked_add_at(disc_abs, disc_size, field_path, "variant offset")?;
|
|
*offset = variant_offset;
|
|
let variant_value =
|
|
materialize_plan_composite(variant, field_path, endian, buffer, offset)?;
|
|
Ok(tag_union_value_number(disc_value, variant_value))
|
|
}
|
|
DiscriminatorPlan::Field { name, field_index } => {
|
|
let shared_plan = shared.as_ref().ok_or_else(|| AlkTypeError::Schema(format!(
|
|
"internal: field-disc union at {field_path} has no shared plan"
|
|
)))?;
|
|
let disc_field = shared_plan.fields().get(*field_index).ok_or_else(|| {
|
|
AlkTypeError::Schema(format!(
|
|
"internal: union {field_path} discriminator index {field_index} out of range"
|
|
))
|
|
})?;
|
|
let _ = disc_field;
|
|
// Walk the whole shared plan; capture the discriminator
|
|
// field's value for the mapping key as we pass it. The
|
|
// result object preserves the 0.2.0 key order:
|
|
// `__discriminator` first, then the shared fields, then the
|
|
// variant's fields.
|
|
let mut disc_value: Option<Value> = None;
|
|
let mut shared_values: Vec<(String, Value)> = Vec::new();
|
|
for field in shared_plan.fields() {
|
|
let path = format!("{field_path}.{}", field.name());
|
|
let value = materialize_plan_field(shared_plan, field, &path, buffer, offset)?;
|
|
if field.name() == name.as_str() {
|
|
disc_value = Some(value);
|
|
} else {
|
|
shared_values.push((field.name().to_string(), value));
|
|
}
|
|
}
|
|
let disc_value = disc_value.ok_or_else(|| AlkTypeError::Schema(format!(
|
|
"internal: union {field_path} discriminator field {name:?} not walked"
|
|
)))?;
|
|
let key = plan_value_discriminator_key(&disc_value, field_path)?;
|
|
let variant = variants
|
|
.iter()
|
|
.find(|(k, _)| *k == key)
|
|
.map(|(_, v)| v)
|
|
.ok_or_else(|| AlkTypeError::Access {
|
|
field_path: field_path.to_string(),
|
|
reason: format!("union discriminator value {key} not in mapping"),
|
|
})?;
|
|
let variant_value =
|
|
materialize_plan_composite(variant, field_path, endian, buffer, offset)?;
|
|
let mut obj = Map::new();
|
|
obj.insert(DISCRIMINATOR_KEY.to_string(), Value::String(key));
|
|
obj.insert((*name).to_string(), disc_value);
|
|
for (k, v) in shared_values {
|
|
obj.insert(k, v);
|
|
}
|
|
flatten_variant_into(&mut obj, variant_value);
|
|
Ok(Value::Object(obj))
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Stringify a materialized discriminator field value for mapping
|
|
/// lookup. Mirrors the 0.2.0 `union_discriminator_key` for the Value form.
|
|
fn plan_value_discriminator_key(value: &Value, field_path: &str) -> Result<String, AlkTypeError> {
|
|
match value {
|
|
Value::String(s) => Ok(s.clone()),
|
|
Value::Number(n) => Ok(n.to_string()),
|
|
_ => Err(AlkTypeError::Schema(format!(
|
|
"union {field_path} has unsupported discriminator value kind: {value}"
|
|
))),
|
|
}
|
|
}
|
|
|
|
fn materialize_plan_record(
|
|
body: &CompositePlan,
|
|
field_path: &str,
|
|
endian: Endian,
|
|
buffer: &[u8],
|
|
offset: &mut usize,
|
|
) -> Result<Value, AlkTypeError> {
|
|
let value_body = match body {
|
|
CompositePlan::Record { value } => value,
|
|
_ => {
|
|
return Err(AlkTypeError::Schema(format!(
|
|
"internal: record body at {field_path} is not CompositePlan::Record"
|
|
)))
|
|
}
|
|
};
|
|
let count = data_access::read_u32(buffer, *offset, field_path, endian)? as usize;
|
|
*offset += U32_SIZE;
|
|
let mut obj = Map::new();
|
|
for i in 0..count {
|
|
let key_path = format!("{field_path}[{i}].key");
|
|
let key_str = data_access::read_string(buffer, *offset, &key_path, endian)?;
|
|
let key_len = U32_SIZE
|
|
.checked_add(key_str.len())
|
|
.ok_or_else(|| AlkTypeError::Access {
|
|
field_path: key_path.clone(),
|
|
reason: "key size 4 + len overflows usize".to_string(),
|
|
})?;
|
|
*offset = checked_add_at(*offset, key_len, &key_path, "key")?;
|
|
let val_path = format!("{field_path}[{i}].value");
|
|
let val = materialize_plan_composite(value_body, &val_path, endian, buffer, offset)?;
|
|
obj.insert(key_str.to_string(), val);
|
|
}
|
|
Ok(Value::Object(obj))
|
|
}
|
|
|
|
|
|
/// Materialize a `Value` tree from `buffer` by walking the BAST root type
|
|
/// in aligned mode (offsets from `offset_map`).
|
|
///
|
|
/// The root type must be a struct. `offset_map` must have been computed
|
|
/// from the same BAST document. Endianness is read from the root struct's
|
|
/// `endian` annotation (defaults to little-endian).
|
|
pub fn materialize_aligned(
|
|
doc: &BastDoc<'_>,
|
|
buffer: &[u8],
|
|
offset_map: &crate::offset_map::OffsetMap,
|
|
) -> Result<Value, AlkTypeError> {
|
|
let root_def = doc.root_def();
|
|
let struct_node = match root_def.kind() {
|
|
BastDefKind::Struct(s) => s,
|
|
other => {
|
|
return Err(AlkTypeError::Schema(format!(
|
|
"materialize_aligned: expected a struct at the root, got {kind}",
|
|
kind = other.alk_kind()
|
|
)));
|
|
}
|
|
};
|
|
let effective_endian = struct_node.endian();
|
|
materialize_struct_aligned(doc, struct_node, "", offset_map, effective_endian, buffer)
|
|
}
|
|
|
|
|
|
fn materialize_struct_packed(
|
|
doc: &BastDoc<'_>,
|
|
struct_node: &BastStruct<'_>,
|
|
path_prefix: &str,
|
|
endian: Endian,
|
|
buffer: &[u8],
|
|
offset: &mut usize,
|
|
) -> Result<Value, AlkTypeError> {
|
|
let mut obj = Map::new();
|
|
for field in struct_node.fields() {
|
|
let path = if path_prefix.is_empty() {
|
|
field.name().to_string()
|
|
} else {
|
|
format!("{path_prefix}.{}", field.name())
|
|
};
|
|
let field_endian = field.effective_endian(endian);
|
|
let value = materialize_field_packed(doc, field, &path, field_endian, buffer, offset)?;
|
|
obj.insert(field.name().to_string(), value);
|
|
}
|
|
Ok(Value::Object(obj))
|
|
}
|
|
|
|
fn materialize_field_packed(
|
|
doc: &BastDoc<'_>,
|
|
field: &BastField<'_>,
|
|
field_path: &str,
|
|
endian: Endian,
|
|
buffer: &[u8],
|
|
offset: &mut usize,
|
|
) -> Result<Value, AlkTypeError> {
|
|
let field_endian = field.effective_endian(endian);
|
|
let ty = field.ty();
|
|
let resolved = doc.resolve_typeref(ty)?;
|
|
materialize_typeref_packed(doc, &resolved, field, field_path, field_endian, buffer, offset)
|
|
}
|
|
|
|
fn materialize_typeref_packed(
|
|
doc: &BastDoc<'_>,
|
|
ty: &BastType<'_>,
|
|
_field: &BastField<'_>,
|
|
field_path: &str,
|
|
endian: Endian,
|
|
buffer: &[u8],
|
|
offset: &mut usize,
|
|
) -> Result<Value, AlkTypeError> {
|
|
match ty {
|
|
BastType::Primitive(AlkTypeKind::Int8) => {
|
|
let v = data_access::read_i8(buffer, *offset, field_path)?;
|
|
*offset += 1;
|
|
Ok(Value::from(v))
|
|
}
|
|
BastType::Primitive(AlkTypeKind::Int16) => {
|
|
let v = data_access::read_i16(buffer, *offset, field_path, endian)?;
|
|
*offset += 2;
|
|
Ok(Value::from(v))
|
|
}
|
|
BastType::Primitive(AlkTypeKind::Int32) => {
|
|
let v = data_access::read_i32(buffer, *offset, field_path, endian)?;
|
|
*offset += 4;
|
|
Ok(Value::from(v))
|
|
}
|
|
BastType::Primitive(AlkTypeKind::Int64) => {
|
|
let v = data_access::read_i64(buffer, *offset, field_path, endian)?;
|
|
*offset += 8;
|
|
Ok(Value::from(v))
|
|
}
|
|
BastType::Primitive(AlkTypeKind::Uint8) => {
|
|
let v = data_access::read_u8(buffer, *offset, field_path)?;
|
|
*offset += 1;
|
|
Ok(Value::from(v))
|
|
}
|
|
BastType::Primitive(AlkTypeKind::Uint16) => {
|
|
let v = data_access::read_u16(buffer, *offset, field_path, endian)?;
|
|
*offset += 2;
|
|
Ok(Value::from(v))
|
|
}
|
|
BastType::Primitive(AlkTypeKind::Uint32) => {
|
|
let v = data_access::read_u32(buffer, *offset, field_path, endian)?;
|
|
*offset += 4;
|
|
Ok(Value::from(v))
|
|
}
|
|
BastType::Primitive(AlkTypeKind::Uint64) => {
|
|
let v = data_access::read_u64(buffer, *offset, field_path, endian)?;
|
|
*offset += 8;
|
|
Ok(Value::from(v))
|
|
}
|
|
BastType::Primitive(AlkTypeKind::Float32) => {
|
|
let v = data_access::read_f32(buffer, *offset, field_path, endian)?;
|
|
*offset += 4;
|
|
Ok(number_from_f64(v as f64, field_path)?)
|
|
}
|
|
BastType::Primitive(AlkTypeKind::Float64) => {
|
|
let v = data_access::read_f64(buffer, *offset, field_path, endian)?;
|
|
*offset += 8;
|
|
Ok(number_from_f64(v, field_path)?)
|
|
}
|
|
BastType::Primitive(AlkTypeKind::Boolean) => {
|
|
let v = data_access::read_bool(buffer, *offset, field_path)?;
|
|
*offset += 1;
|
|
Ok(Value::Bool(v))
|
|
}
|
|
BastType::Enum(_) => {
|
|
let v = data_access::read_enum(buffer, *offset, field_path, endian)?;
|
|
*offset += 4;
|
|
Ok(Value::from(v))
|
|
}
|
|
BastType::Primitive(AlkTypeKind::String) => {
|
|
let s = data_access::read_string(buffer, *offset, field_path, endian)?;
|
|
let len = U32_SIZE + s.len();
|
|
*offset = offset.checked_add(len).ok_or_else(|| AlkTypeError::Access {
|
|
field_path: field_path.to_string(),
|
|
reason: format!("string offset {len} overflows usize"),
|
|
})?;
|
|
Ok(Value::String(s.to_string()))
|
|
}
|
|
BastType::Primitive(AlkTypeKind::Bytes) => {
|
|
let b = data_access::read_bytes(buffer, *offset, field_path, endian)?;
|
|
let len = U32_SIZE + b.len();
|
|
*offset = offset.checked_add(len).ok_or_else(|| AlkTypeError::Access {
|
|
field_path: field_path.to_string(),
|
|
reason: format!("bytes offset {len} overflows usize"),
|
|
})?;
|
|
let arr: Vec<Value> = b.iter().map(|&byte| Value::from(u32::from(byte))).collect();
|
|
Ok(Value::Array(arr))
|
|
}
|
|
BastType::Struct(s) => {
|
|
materialize_struct_packed(doc, s, field_path, endian, buffer, offset)
|
|
}
|
|
BastType::Array(a) => {
|
|
materialize_array_packed(doc, a, field_path, endian, buffer, offset)
|
|
}
|
|
BastType::Union(u) => {
|
|
materialize_union_packed(doc, u, field_path, endian, buffer, offset)
|
|
}
|
|
BastType::Record(r) => {
|
|
materialize_record_packed(doc, r, field_path, endian, buffer, offset)
|
|
}
|
|
BastType::Ref(_) => Err(AlkTypeError::Schema(format!(
|
|
"materialize: unresolved $ref at {field_path}"
|
|
))),
|
|
BastType::Primitive(k) => Err(AlkTypeError::Schema(format!(
|
|
"materialize: unsupported primitive kind {k} at {field_path}"
|
|
))),
|
|
}
|
|
}
|
|
|
|
fn materialize_array_packed(
|
|
doc: &BastDoc<'_>,
|
|
array: &BastArray<'_>,
|
|
field_path: &str,
|
|
endian: Endian,
|
|
buffer: &[u8],
|
|
offset: &mut usize,
|
|
) -> Result<Value, AlkTypeError> {
|
|
let element_ty = array.element();
|
|
let count = array.count();
|
|
let mut arr = Vec::with_capacity(count);
|
|
for i in 0..count {
|
|
let path = format!("{field_path}[{i}]");
|
|
let resolved_elem = doc.resolve_typeref(element_ty)?;
|
|
let value = materialize_typeref_packed(
|
|
doc,
|
|
&resolved_elem,
|
|
&dummy_field_for(&resolved_elem),
|
|
&path,
|
|
endian,
|
|
buffer,
|
|
offset,
|
|
)?;
|
|
arr.push(value);
|
|
}
|
|
Ok(Value::Array(arr))
|
|
}
|
|
|
|
fn materialize_union_packed(
|
|
doc: &BastDoc<'_>,
|
|
union_node: &BastUnion<'_>,
|
|
field_path: &str,
|
|
endian: Endian,
|
|
buffer: &[u8],
|
|
offset: &mut usize,
|
|
) -> Result<Value, AlkTypeError> {
|
|
let disc = union_node.discriminator();
|
|
match disc {
|
|
BastDiscriminator::Byte { offset: disc_rel, disc_type } => {
|
|
let disc_abs = offset.checked_add(*disc_rel).ok_or_else(|| AlkTypeError::Access {
|
|
field_path: field_path.to_string(),
|
|
reason: format!("discriminator offset {disc_rel} overflows"),
|
|
})?;
|
|
let disc_value = match disc_type {
|
|
AlkTypeKind::Uint8 => {
|
|
data_access::read_u8(buffer, disc_abs, field_path)? as u32
|
|
}
|
|
AlkTypeKind::Uint16 => {
|
|
data_access::read_u16(buffer, disc_abs, field_path, endian)? as u32
|
|
}
|
|
AlkTypeKind::Uint32 => {
|
|
data_access::read_u32(buffer, disc_abs, field_path, endian)?
|
|
}
|
|
other => {
|
|
return Err(AlkTypeError::Schema(format!(
|
|
"internal: union discriminator type {other} is not a supported byte \
|
|
discriminator (parser should have rejected this)"
|
|
)));
|
|
}
|
|
};
|
|
let mapping = union_node.mapping();
|
|
let key = disc_value.to_string();
|
|
let variant_ty = mapping
|
|
.iter()
|
|
.find(|(k, _)| *k == key)
|
|
.map(|(_, v)| v)
|
|
.ok_or_else(|| AlkTypeError::Access {
|
|
field_path: field_path.to_string(),
|
|
reason: format!("union discriminator value {key} not in mapping"),
|
|
})?;
|
|
let disc_size = disc_type.type_size().unwrap_or(1);
|
|
let variant_offset = disc_abs.checked_add(disc_size).ok_or_else(|| {
|
|
AlkTypeError::Access {
|
|
field_path: field_path.to_string(),
|
|
reason: format!(
|
|
"variant offset {disc_abs} + disc size {disc_size} overflows usize"
|
|
),
|
|
}
|
|
})?;
|
|
*offset = variant_offset;
|
|
let resolved_variant = doc.resolve_typeref(variant_ty)?;
|
|
let variant_value = materialize_typeref_packed(
|
|
doc,
|
|
&resolved_variant,
|
|
&dummy_field_for(&resolved_variant),
|
|
field_path,
|
|
endian,
|
|
buffer,
|
|
offset,
|
|
)?;
|
|
Ok(tag_union_value_number(disc_value, variant_value))
|
|
}
|
|
BastDiscriminator::Field { name } => {
|
|
let fields = union_node.fields();
|
|
let disc_field = fields
|
|
.iter()
|
|
.find(|f| f.name() == *name)
|
|
.ok_or_else(|| {
|
|
AlkTypeError::Schema(format!(
|
|
"materialize: union at {field_path} has no discriminator field '{name}'"
|
|
))
|
|
})?;
|
|
let disc_path = format!("{field_path}.{name}");
|
|
let disc_value =
|
|
materialize_field_packed(doc, disc_field, &disc_path, endian, buffer, offset)?;
|
|
let key = union_discriminator_key(&disc_value, field_path)?;
|
|
let mapping = union_node.mapping();
|
|
let variant_ty = mapping
|
|
.iter()
|
|
.find(|(k, _)| *k == key)
|
|
.map(|(_, v)| v)
|
|
.ok_or_else(|| AlkTypeError::Access {
|
|
field_path: field_path.to_string(),
|
|
reason: format!("union discriminator value {key} not in mapping"),
|
|
})?;
|
|
let resolved_variant = doc.resolve_typeref(variant_ty)?;
|
|
let variant_value = materialize_typeref_packed(
|
|
doc,
|
|
&resolved_variant,
|
|
&dummy_field_for(&resolved_variant),
|
|
field_path,
|
|
endian,
|
|
buffer,
|
|
offset,
|
|
)?;
|
|
let mut obj = Map::new();
|
|
obj.insert(DISCRIMINATOR_KEY.to_string(), Value::String(key.clone()));
|
|
obj.insert((*name).to_string(), disc_value);
|
|
flatten_variant_into(&mut obj, variant_value);
|
|
Ok(Value::Object(obj))
|
|
}
|
|
}
|
|
}
|
|
|
|
fn materialize_record_packed(
|
|
doc: &BastDoc<'_>,
|
|
record: &BastRecord<'_>,
|
|
field_path: &str,
|
|
endian: Endian,
|
|
buffer: &[u8],
|
|
offset: &mut usize,
|
|
) -> Result<Value, AlkTypeError> {
|
|
let values_ty = record.values();
|
|
let count = data_access::read_u32(buffer, *offset, field_path, endian)? as usize;
|
|
*offset += U32_SIZE;
|
|
let mut obj = Map::new();
|
|
for i in 0..count {
|
|
let key_path = format!("{field_path}[{i}].key");
|
|
let key_str = data_access::read_string(buffer, *offset, &key_path, endian)?;
|
|
let key_len = U32_SIZE + key_str.len();
|
|
*offset = offset.checked_add(key_len).ok_or_else(|| AlkTypeError::Access {
|
|
field_path: key_path.clone(),
|
|
reason: "key offset overflows".to_string(),
|
|
})?;
|
|
let val_path = format!("{field_path}[{i}].value");
|
|
let resolved_values = doc.resolve_typeref(values_ty)?;
|
|
let val = materialize_typeref_packed(
|
|
doc,
|
|
&resolved_values,
|
|
&dummy_field_for(&resolved_values),
|
|
&val_path,
|
|
endian,
|
|
buffer,
|
|
offset,
|
|
)?;
|
|
obj.insert(key_str.to_string(), val);
|
|
}
|
|
Ok(Value::Object(obj))
|
|
}
|
|
|
|
|
|
fn tag_union_value_number(disc_value: u32, variant_value: Value) -> Value {
|
|
let mut obj = Map::new();
|
|
obj.insert(DISCRIMINATOR_KEY.to_string(), Value::from(disc_value));
|
|
flatten_variant_into(&mut obj, variant_value);
|
|
Value::Object(obj)
|
|
}
|
|
|
|
/// Flatten a variant's materialized object fields into `obj` (in place).
|
|
/// If the variant is not an object (e.g. a leaf kind), nest it under
|
|
/// `"__variant"`.
|
|
fn flatten_variant_into(obj: &mut Map<String, Value>, variant_value: Value) {
|
|
if let Some(variant_obj) = variant_value.as_object() {
|
|
for (k, v) in variant_obj.iter() {
|
|
obj.insert(k.clone(), v.clone());
|
|
}
|
|
} else {
|
|
obj.insert("__variant".to_string(), variant_value);
|
|
}
|
|
}
|
|
|
|
/// Stringify a discriminator field value for mapping lookup. Mirrors
|
|
/// `sequential_reader::discriminator_string_value` for the Value form.
|
|
fn union_discriminator_key(value: &Value, field_path: &str) -> Result<String, AlkTypeError> {
|
|
match value {
|
|
Value::String(s) => Ok(s.clone()),
|
|
Value::Number(n) => Ok(n.to_string()),
|
|
_ => Err(AlkTypeError::Schema(format!(
|
|
"union {field_path} has unsupported discriminator value kind: {value}"
|
|
))),
|
|
}
|
|
}
|
|
|
|
fn number_from_f64(f: f64, field_path: &str) -> Result<Value, AlkTypeError> {
|
|
serde_json::Number::from_f64(f)
|
|
.map(Value::Number)
|
|
.ok_or_else(|| AlkTypeError::Access {
|
|
field_path: field_path.to_string(),
|
|
reason: format!("non-finite float {f} cannot be represented as JSON"),
|
|
})
|
|
}
|
|
|
|
/// Aligned-mode materialization: walk the BAST typed tree, but read
|
|
/// leaves at offsets from the `OffsetMap` and fall back to packed-style
|
|
/// sequential walks for composites (Record) whose internal layout can't
|
|
/// be precomputed into the static offset map.
|
|
fn materialize_struct_aligned(
|
|
doc: &BastDoc<'_>,
|
|
struct_node: &BastStruct<'_>,
|
|
path_prefix: &str,
|
|
offset_map: &crate::offset_map::OffsetMap,
|
|
endian: Endian,
|
|
buffer: &[u8],
|
|
) -> Result<Value, AlkTypeError> {
|
|
let mut obj = Map::new();
|
|
for field in struct_node.fields() {
|
|
let path = if path_prefix.is_empty() {
|
|
field.name().to_string()
|
|
} else {
|
|
format!("{path_prefix}.{}", field.name())
|
|
};
|
|
let ty = field.ty();
|
|
let resolved = doc.resolve_typeref(ty)?;
|
|
let kind = resolved.alk_kind();
|
|
let field_endian = field.effective_endian(endian);
|
|
let value = if kind.is_fixed_size() {
|
|
let range = offset_map.get(&path).ok_or_else(|| AlkTypeError::Offset {
|
|
field_path: path.clone(),
|
|
reason: "field not found in offset map".to_string(),
|
|
})?;
|
|
materialize_leaf_at(doc, &resolved, &path, range.start, field_endian, buffer)?
|
|
} else if kind == AlkTypeKind::Struct {
|
|
if let BastType::Struct(s) = &resolved {
|
|
materialize_struct_aligned(doc, s, &path, offset_map, field_endian, buffer)?
|
|
} else {
|
|
return Err(AlkTypeError::Schema(format!(
|
|
"materialize: resolved kind Struct but type was {resolved} at {path}"
|
|
)));
|
|
}
|
|
} else if kind == AlkTypeKind::Array {
|
|
if let BastType::Array(a) = &resolved {
|
|
materialize_array_aligned(doc, a, &path, offset_map, field_endian, buffer)?
|
|
} else {
|
|
return Err(AlkTypeError::Schema(format!(
|
|
"materialize: resolved kind Array but type was {resolved} at {path}"
|
|
)));
|
|
}
|
|
} else if kind == AlkTypeKind::Record {
|
|
let range = offset_map.get(&path).ok_or_else(|| AlkTypeError::Offset {
|
|
field_path: path.clone(),
|
|
reason: "field not found in offset map".to_string(),
|
|
})?;
|
|
let mut offset = range.start;
|
|
materialize_typeref_packed(
|
|
doc,
|
|
&resolved,
|
|
field,
|
|
&path,
|
|
field_endian,
|
|
buffer,
|
|
&mut offset,
|
|
)?
|
|
} else if kind.is_variable_length() {
|
|
let range = offset_map.get(&path).ok_or_else(|| AlkTypeError::Offset {
|
|
field_path: path.clone(),
|
|
reason: "field not found in offset map".to_string(),
|
|
})?;
|
|
materialize_variable_aligned(doc, field, &resolved, &path, range, field_endian, buffer)?
|
|
} else {
|
|
return Err(AlkTypeError::Schema(format!(
|
|
"materialize: unsupported kind {kind} at {path} in aligned mode"
|
|
)));
|
|
};
|
|
obj.insert(field.name().to_string(), value);
|
|
}
|
|
Ok(Value::Object(obj))
|
|
}
|
|
|
|
/// Materialize a fixed-size array in aligned mode by reading each element
|
|
/// at its `vals[i]` offset-map entry. Array elements are always fixed-size
|
|
/// in aligned mode (the offset map rejects variable-length elements).
|
|
fn materialize_array_aligned(
|
|
doc: &BastDoc<'_>,
|
|
array: &BastArray<'_>,
|
|
field_path: &str,
|
|
offset_map: &crate::offset_map::OffsetMap,
|
|
endian: Endian,
|
|
buffer: &[u8],
|
|
) -> Result<Value, AlkTypeError> {
|
|
let element_ty = array.element();
|
|
let resolved_elem = doc.resolve_typeref(element_ty)?;
|
|
let count = array.count();
|
|
let mut arr = Vec::with_capacity(count);
|
|
for i in 0..count {
|
|
let elem_path = format!("{field_path}[{i}]");
|
|
let range = offset_map.get(&elem_path).ok_or_else(|| AlkTypeError::Offset {
|
|
field_path: elem_path.clone(),
|
|
reason: "array element not found in offset map".to_string(),
|
|
})?;
|
|
let value = materialize_leaf_at(doc, &resolved_elem, &elem_path, range.start, endian, buffer)?;
|
|
arr.push(value);
|
|
}
|
|
Ok(Value::Array(arr))
|
|
}
|
|
|
|
/// Materialize a variable-length leaf (String/Bytes) in aligned
|
|
/// mode, dispatching on the field's encoding strategy:
|
|
///
|
|
/// - `offset-indirect`: an 8-byte `{offset, length}` pair at `start`,
|
|
/// pointing absolutely into the same buffer.
|
|
/// - `maxLength` reservation: `end - start` bytes of zero-padded data;
|
|
/// trailing NUL bytes are trimmed.
|
|
/// - inline length-prefixed (default): a 4-byte length prefix at `start`.
|
|
fn materialize_variable_aligned(
|
|
doc: &BastDoc<'_>,
|
|
field: &BastField<'_>,
|
|
ty: &BastType<'_>,
|
|
field_path: &str,
|
|
range: &crate::offset_map::ByteRange,
|
|
endian: Endian,
|
|
buffer: &[u8],
|
|
) -> Result<Value, AlkTypeError> {
|
|
let start = range.start;
|
|
match field.encoding() {
|
|
VariableEncoding::OffsetIndirect => match ty {
|
|
BastType::Primitive(AlkTypeKind::String) => {
|
|
let s = data_access::read_string_indirect(buffer, start, field_path, endian)?;
|
|
Ok(Value::String(s.to_string()))
|
|
}
|
|
BastType::Primitive(AlkTypeKind::Bytes) => {
|
|
let b = data_access::read_bytes_indirect(buffer, start, field_path, endian)?;
|
|
let arr: Vec<Value> = b.iter().map(|&byte| Value::from(u32::from(byte))).collect();
|
|
Ok(Value::Array(arr))
|
|
}
|
|
_ => Err(AlkTypeError::Schema(format!(
|
|
"materialize: offset-indirect encoding on non-variable kind at {field_path}"
|
|
))),
|
|
},
|
|
VariableEncoding::LengthPrefixed => {
|
|
if let Some(_max_len) = field.max_length() {
|
|
let data = buffer.get(start..range.end).ok_or_else(|| AlkTypeError::Access {
|
|
field_path: field_path.to_string(),
|
|
reason: format!("reservation [{start}..{}) unavailable", range.end),
|
|
})?;
|
|
let trimmed_len = data
|
|
.iter()
|
|
.rposition(|&b| b != 0)
|
|
.map(|i| i + 1)
|
|
.unwrap_or(0);
|
|
let trimmed = &data[..trimmed_len];
|
|
match ty {
|
|
BastType::Primitive(AlkTypeKind::String) => {
|
|
let s = std::str::from_utf8(trimmed).map_err(|e| AlkTypeError::Access {
|
|
field_path: field_path.to_string(),
|
|
reason: format!("invalid UTF-8 in maxLength string: {e}"),
|
|
})?;
|
|
Ok(Value::String(s.to_string()))
|
|
}
|
|
BastType::Primitive(AlkTypeKind::Bytes) => {
|
|
let arr: Vec<Value> =
|
|
trimmed.iter().map(|&byte| Value::from(u32::from(byte))).collect();
|
|
Ok(Value::Array(arr))
|
|
}
|
|
_ => Err(AlkTypeError::Schema(format!(
|
|
"materialize: maxLength on non-variable kind at {field_path}"
|
|
))),
|
|
}
|
|
} else {
|
|
materialize_leaf_at(doc, ty, field_path, start, endian, buffer)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
fn materialize_leaf_at(
|
|
doc: &BastDoc<'_>,
|
|
ty: &BastType<'_>,
|
|
field_path: &str,
|
|
offset: usize,
|
|
endian: Endian,
|
|
buffer: &[u8],
|
|
) -> Result<Value, AlkTypeError> {
|
|
let mut pos = offset;
|
|
materialize_typeref_packed(
|
|
doc,
|
|
ty,
|
|
&dummy_field_for(ty),
|
|
field_path,
|
|
endian,
|
|
buffer,
|
|
&mut pos,
|
|
)
|
|
}
|
|
|
|
/// Construct a synthetic `BastField` borrowing from a resolved
|
|
/// `BastType`'s source node. Used when materializing array elements /
|
|
/// record values / union variants — these are TypeRefs, not fields, so
|
|
/// they don't carry field-level annotations. The synthetic field
|
|
/// inherits no annotations (which is correct: `maxLength`/`encoding`/
|
|
/// `endian`/`align` are field-level properties, and array elements and
|
|
/// record values don't have them in BAST).
|
|
///
|
|
/// The `BastField::parse` path validates the node; this helper bypasses
|
|
/// re-validation for a node we've already resolved. The returned field
|
|
/// borrows from the `BastType`'s source.
|
|
fn dummy_field_for<'d>(ty: &BastType<'d>) -> BastField<'d> {
|
|
let source = ty_source(ty);
|
|
BastField::synthetic(ty.clone(), source)
|
|
}
|
|
|
|
fn ty_source<'d>(ty: &BastType<'d>) -> &'d serde_json::Value {
|
|
match ty {
|
|
BastType::Struct(s) => s.source(),
|
|
BastType::Union(u) => u.source(),
|
|
BastType::Array(a) => a.source(),
|
|
BastType::Record(r) => r.source(),
|
|
_ => &serde_json::Value::Null,
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use serde_json::json;
|
|
|
|
fn doc_from<'a>(root: &'a Value, name: &'a str) -> BastDoc<'a> {
|
|
BastDoc::new(root, name).expect("bast doc")
|
|
}
|
|
|
|
fn materialize_packed_strict(
|
|
root: &Value,
|
|
doc: &BastDoc<'_>,
|
|
buffer: &[u8],
|
|
) -> Result<Value, AlkTypeError> {
|
|
let plan = ReadPlan::compile(root, doc.root_name())?;
|
|
materialize_packed(&plan, buffer)
|
|
}
|
|
|
|
#[test]
|
|
fn materialize_bytes_packed_returns_array_of_u8() {
|
|
let root = json!({
|
|
"$defs": {
|
|
"S": {
|
|
"kind": "struct",
|
|
"fields": [ { "name": "blob", "kind": "bytes" } ]
|
|
}
|
|
}
|
|
});
|
|
let doc = doc_from(&root, "S");
|
|
let mut buf = vec![0u8; 7];
|
|
buf[0..4].copy_from_slice(&3u32.to_le_bytes());
|
|
buf[4] = 0x41;
|
|
buf[5] = 0x42;
|
|
buf[6] = 0xC3;
|
|
let v = materialize_packed_strict(&root, &doc, &buf).expect("materialize");
|
|
assert_eq!(v["blob"], json!([65, 66, 195]));
|
|
}
|
|
|
|
#[test]
|
|
fn materialize_bytes_packed_non_utf8_round_trips() {
|
|
let root = json!({
|
|
"$defs": {
|
|
"S": {
|
|
"kind": "struct",
|
|
"fields": [ { "name": "raw", "kind": "bytes" } ]
|
|
}
|
|
}
|
|
});
|
|
let doc = doc_from(&root, "S");
|
|
let mut buf = vec![0u8; 8];
|
|
buf[0..4].copy_from_slice(&4u32.to_le_bytes());
|
|
buf[4] = 0xFF;
|
|
buf[5] = 0xFE;
|
|
buf[6] = 0x00;
|
|
buf[7] = 0x80;
|
|
let v = materialize_packed_strict(&root, &doc, &buf).expect("materialize");
|
|
assert_eq!(v["raw"], json!([255, 254, 0, 128]));
|
|
}
|
|
|
|
#[test]
|
|
fn materialize_record_packed_count_prefixed_pairs() {
|
|
let root = json!({
|
|
"$defs": {
|
|
"S": {
|
|
"kind": "struct",
|
|
"fields": [
|
|
{ "name": "counts", "kind": { "kind": "record", "values": "uint32" } }
|
|
]
|
|
}
|
|
}
|
|
});
|
|
let doc = doc_from(&root, "S");
|
|
let mut buf = vec![0u8; 22];
|
|
let mut off = 0usize;
|
|
buf[off..off + 4].copy_from_slice(&2u32.to_le_bytes());
|
|
off += 4;
|
|
buf[off..off + 4].copy_from_slice(&1u32.to_le_bytes());
|
|
off += 4;
|
|
buf[off] = b'b';
|
|
off += 1;
|
|
buf[off..off + 4].copy_from_slice(&10u32.to_le_bytes());
|
|
off += 4;
|
|
buf[off..off + 4].copy_from_slice(&1u32.to_le_bytes());
|
|
off += 4;
|
|
buf[off] = b'a';
|
|
off += 1;
|
|
buf[off..off + 4].copy_from_slice(&20u32.to_le_bytes());
|
|
let v = materialize_packed_strict(&root, &doc, &buf).expect("materialize");
|
|
assert_eq!(v["counts"], json!({ "b": 10, "a": 20 }));
|
|
}
|
|
|
|
#[test]
|
|
fn materialize_record_packed_zero_entries() {
|
|
let root = json!({
|
|
"$defs": {
|
|
"S": {
|
|
"kind": "struct",
|
|
"fields": [
|
|
{ "name": "counts", "kind": { "kind": "record", "values": "uint32" } }
|
|
]
|
|
}
|
|
}
|
|
});
|
|
let doc = doc_from(&root, "S");
|
|
let mut buf = vec![0u8; 4];
|
|
buf[0..4].copy_from_slice(&0u32.to_le_bytes());
|
|
let v = materialize_packed_strict(&root, &doc, &buf).expect("materialize");
|
|
assert_eq!(v["counts"], json!({}));
|
|
}
|
|
|
|
#[test]
|
|
fn materialize_record_packed_rejects_short_buffer() {
|
|
let root = json!({
|
|
"$defs": {
|
|
"S": {
|
|
"kind": "struct",
|
|
"fields": [
|
|
{ "name": "counts", "kind": { "kind": "record", "values": "uint32" } }
|
|
]
|
|
}
|
|
}
|
|
});
|
|
let doc = doc_from(&root, "S");
|
|
let buf = [0u8; 2];
|
|
let err = materialize_packed_strict(&root, &doc, &buf).unwrap_err();
|
|
assert!(matches!(err, AlkTypeError::Access { .. }), "got {err:?}");
|
|
}
|
|
|
|
#[test]
|
|
fn materialize_union_byte_discriminator_packs_variant_fields() {
|
|
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" }
|
|
}
|
|
},
|
|
"Read": {
|
|
"kind": "struct",
|
|
"fields": [ { "name": "id", "kind": "uint32" } ]
|
|
}
|
|
}
|
|
});
|
|
let doc = doc_from(&root, "S");
|
|
let mut buf = vec![0u8; 5];
|
|
buf[0] = 5;
|
|
buf[1..5].copy_from_slice(&42u32.to_le_bytes());
|
|
let v = materialize_packed_strict(&root, &doc, &buf).expect("materialize");
|
|
let payload = &v["payload"];
|
|
assert_eq!(payload["__discriminator"], json!(5));
|
|
assert_eq!(payload["id"], json!(42));
|
|
}
|
|
|
|
#[test]
|
|
fn materialize_union_field_discriminator_includes_typed_disc_value() {
|
|
let root = json!({
|
|
"$defs": {
|
|
"S": {
|
|
"kind": "struct",
|
|
"fields": [
|
|
{
|
|
"name": "payload",
|
|
"kind": { "$ref": "#/$defs/Event" }
|
|
}
|
|
]
|
|
},
|
|
"Event": {
|
|
"kind": "union",
|
|
"discriminator": { "kind": "field", "name": "type" },
|
|
"fields": [ { "name": "type", "kind": "string" } ],
|
|
"mapping": {
|
|
"read": { "$ref": "#/$defs/Read" }
|
|
}
|
|
},
|
|
"Read": {
|
|
"kind": "struct",
|
|
"fields": [ { "name": "n", "kind": "uint32" } ]
|
|
}
|
|
}
|
|
});
|
|
let doc = doc_from(&root, "S");
|
|
let mut buf = vec![0u8; 12];
|
|
buf[0..4].copy_from_slice(&4u32.to_le_bytes());
|
|
buf[4..8].copy_from_slice(b"read");
|
|
buf[8..12].copy_from_slice(&7u32.to_le_bytes());
|
|
let v = materialize_packed_strict(&root, &doc, &buf).expect("materialize");
|
|
let payload = &v["payload"];
|
|
assert_eq!(payload["__discriminator"], json!("read"));
|
|
assert_eq!(payload["type"], json!("read"));
|
|
assert_eq!(payload["n"], json!(7));
|
|
}
|
|
|
|
#[test]
|
|
fn materialize_union_field_discriminator_advances_offset_correctly() {
|
|
let root = json!({
|
|
"$defs": {
|
|
"S": {
|
|
"kind": "struct",
|
|
"fields": [
|
|
{
|
|
"name": "payload",
|
|
"kind": { "$ref": "#/$defs/Event" }
|
|
},
|
|
{ "name": "trailer", "kind": "uint32" }
|
|
]
|
|
},
|
|
"Event": {
|
|
"kind": "union",
|
|
"discriminator": { "kind": "field", "name": "type" },
|
|
"fields": [ { "name": "type", "kind": "string" } ],
|
|
"mapping": {
|
|
"read": { "$ref": "#/$defs/Read" }
|
|
}
|
|
},
|
|
"Read": {
|
|
"kind": "struct",
|
|
"fields": [ { "name": "n", "kind": "uint32" } ]
|
|
}
|
|
}
|
|
});
|
|
let doc = doc_from(&root, "S");
|
|
let mut buf = vec![0u8; 16];
|
|
buf[0..4].copy_from_slice(&4u32.to_le_bytes());
|
|
buf[4..8].copy_from_slice(b"read");
|
|
buf[8..12].copy_from_slice(&7u32.to_le_bytes());
|
|
buf[12..16].copy_from_slice(&99u32.to_le_bytes());
|
|
let v = materialize_packed_strict(&root, &doc, &buf).expect("materialize");
|
|
assert_eq!(v["trailer"], json!(99));
|
|
assert_eq!(v["payload"]["n"], json!(7));
|
|
}
|
|
|
|
#[test]
|
|
fn materialize_union_byte_discriminator_unknown_value_is_access_error() {
|
|
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" }
|
|
}
|
|
},
|
|
"Read": {
|
|
"kind": "struct",
|
|
"fields": []
|
|
}
|
|
}
|
|
});
|
|
let doc = doc_from(&root, "S");
|
|
let buf = [99u8];
|
|
let err = materialize_packed_strict(&root, &doc, &buf).unwrap_err();
|
|
assert!(matches!(err, AlkTypeError::Access { .. }), "got {err:?}");
|
|
}
|
|
|
|
#[test]
|
|
fn materialize_union_with_ref_variant_resolves_via_root() {
|
|
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" }
|
|
}
|
|
},
|
|
"Read": {
|
|
"kind": "struct",
|
|
"fields": [ { "name": "id", "kind": "uint32" } ]
|
|
}
|
|
}
|
|
});
|
|
let doc = doc_from(&root, "S");
|
|
let mut buf = vec![0u8; 5];
|
|
buf[0] = 5;
|
|
buf[1..5].copy_from_slice(&42u32.to_le_bytes());
|
|
let v = materialize_packed_strict(&root, &doc, &buf).expect("materialize");
|
|
assert_eq!(v["payload"]["__discriminator"], json!(5));
|
|
assert_eq!(v["payload"]["id"], json!(42));
|
|
}
|
|
|
|
#[test]
|
|
fn materialize_array_with_ref_element_resolves_via_root() {
|
|
let root = json!({
|
|
"$defs": {
|
|
"S": {
|
|
"kind": "struct",
|
|
"fields": [
|
|
{
|
|
"name": "items",
|
|
"kind": {
|
|
"kind": "array",
|
|
"element": { "$ref": "#/$defs/Point" },
|
|
"count": 2
|
|
}
|
|
}
|
|
]
|
|
},
|
|
"Point": {
|
|
"kind": "struct",
|
|
"fields": [
|
|
{ "name": "x", "kind": "uint16" },
|
|
{ "name": "y", "kind": "uint16" }
|
|
]
|
|
}
|
|
}
|
|
});
|
|
let doc = doc_from(&root, "S");
|
|
let mut buf = vec![0u8; 8];
|
|
buf[0..2].copy_from_slice(&1u16.to_le_bytes());
|
|
buf[2..4].copy_from_slice(&2u16.to_le_bytes());
|
|
buf[4..6].copy_from_slice(&3u16.to_le_bytes());
|
|
buf[6..8].copy_from_slice(&4u16.to_le_bytes());
|
|
let v = materialize_packed_strict(&root, &doc, &buf).expect("materialize");
|
|
assert_eq!(v["items"], json!([{ "x": 1, "y": 2 }, { "x": 3, "y": 4 }]));
|
|
}
|
|
|
|
#[test]
|
|
fn materialize_struct_with_nested_struct_then_field() {
|
|
let root = json!({
|
|
"$defs": {
|
|
"S": {
|
|
"kind": "struct",
|
|
"fields": [
|
|
{
|
|
"name": "header",
|
|
"kind": {
|
|
"kind": "struct",
|
|
"fields": [
|
|
{ "name": "a", "kind": "uint8" },
|
|
{ "name": "b", "kind": "uint8" }
|
|
]
|
|
}
|
|
},
|
|
{ "name": "after", "kind": "uint32" }
|
|
]
|
|
}
|
|
}
|
|
});
|
|
let doc = doc_from(&root, "S");
|
|
let mut buf = vec![0u8; 6];
|
|
buf[0] = 1;
|
|
buf[1] = 2;
|
|
buf[2..6].copy_from_slice(&0x03030303u32.to_le_bytes());
|
|
let v = materialize_packed_strict(&root, &doc, &buf).expect("materialize");
|
|
assert_eq!(v["header"]["a"], json!(1));
|
|
assert_eq!(v["header"]["b"], json!(2));
|
|
assert_eq!(v["after"], json!(0x03030303u32));
|
|
}
|
|
|
|
#[test]
|
|
fn materialize_chunk_header_packed() {
|
|
let root = json!({
|
|
"$defs": {
|
|
"S": {
|
|
"kind": "struct",
|
|
"endian": "big",
|
|
"fields": [
|
|
{ "name": "channel_id", "kind": "uint32" },
|
|
{ "name": "length", "kind": "uint32" }
|
|
]
|
|
}
|
|
}
|
|
});
|
|
let doc = doc_from(&root, "S");
|
|
let buf = [0u8, 0u8, 0u8, 42u8, 0u8, 0u8, 0u8, 7u8];
|
|
let v = materialize_packed_strict(&root, &doc, &buf).expect("materialize");
|
|
assert_eq!(v["channel_id"], json!(42));
|
|
assert_eq!(v["length"], json!(7));
|
|
}
|
|
|
|
// ----- Aligned-mode materialization (M2 fixes) ----------------------
|
|
|
|
fn materialize_aligned_strict(
|
|
doc: &BastDoc<'_>,
|
|
buffer: &[u8],
|
|
) -> Result<Value, AlkTypeError> {
|
|
let offset_map = crate::offset_map::OffsetMap::compute(doc)?;
|
|
materialize_aligned(doc, buffer, &offset_map)
|
|
}
|
|
|
|
#[test]
|
|
fn materialize_aligned_fixed_size_array() {
|
|
let root = json!({
|
|
"$defs": {
|
|
"S": {
|
|
"kind": "struct",
|
|
"fields": [
|
|
{ "name": "vals", "kind": { "kind": "array", "element": "uint32", "count": 2 } }
|
|
]
|
|
}
|
|
}
|
|
});
|
|
let doc = doc_from(&root, "S");
|
|
let buf = [1u8, 0, 0, 0, 2, 0, 0, 0];
|
|
let v = materialize_aligned_strict(&doc, &buf).expect("materialize");
|
|
assert_eq!(v["vals"], json!([1, 2]));
|
|
}
|
|
|
|
#[test]
|
|
fn materialize_aligned_max_length_string_trims_padding() {
|
|
let root = json!({
|
|
"$defs": {
|
|
"S": {
|
|
"kind": "struct",
|
|
"fields": [
|
|
{ "name": "name", "kind": "string", "maxLength": 8 }
|
|
]
|
|
}
|
|
}
|
|
});
|
|
let doc = doc_from(&root, "S");
|
|
let mut buf = vec![0u8; 8];
|
|
buf[..5].copy_from_slice(b"hello");
|
|
let v = materialize_aligned_strict(&doc, &buf).expect("materialize");
|
|
assert_eq!(v["name"], json!("hello"));
|
|
}
|
|
|
|
#[test]
|
|
fn materialize_aligned_max_length_bytes_trims_padding() {
|
|
let root = json!({
|
|
"$defs": {
|
|
"S": {
|
|
"kind": "struct",
|
|
"fields": [
|
|
{ "name": "blob", "kind": "bytes", "maxLength": 8 }
|
|
]
|
|
}
|
|
}
|
|
});
|
|
let doc = doc_from(&root, "S");
|
|
let mut buf = vec![0u8; 8];
|
|
buf[..3].copy_from_slice(&[0xAA, 0xBB, 0xCC]);
|
|
let v = materialize_aligned_strict(&doc, &buf).expect("materialize");
|
|
assert_eq!(v["blob"], json!([0xAA, 0xBB, 0xCC]));
|
|
}
|
|
|
|
#[test]
|
|
fn materialize_aligned_offset_indirect_bytes() {
|
|
let root = json!({
|
|
"$defs": {
|
|
"S": {
|
|
"kind": "struct",
|
|
"fields": [
|
|
{ "name": "blob", "kind": "bytes", "encoding": "offset-indirect" }
|
|
]
|
|
}
|
|
}
|
|
});
|
|
let doc = doc_from(&root, "S");
|
|
let mut buf = vec![0u8; 8 + 3];
|
|
buf[0..4].copy_from_slice(&8u32.to_le_bytes());
|
|
buf[4..8].copy_from_slice(&3u32.to_le_bytes());
|
|
buf[8..11].copy_from_slice(&[0xAA, 0xBB, 0xCC]);
|
|
let v = materialize_aligned_strict(&doc, &buf).expect("materialize");
|
|
assert_eq!(v["blob"], json!([0xAA, 0xBB, 0xCC]));
|
|
}
|
|
|
|
#[test]
|
|
fn materialize_aligned_offset_indirect_string() {
|
|
let root = json!({
|
|
"$defs": {
|
|
"S": {
|
|
"kind": "struct",
|
|
"fields": [
|
|
{ "name": "name", "kind": "string", "encoding": "offset-indirect" }
|
|
]
|
|
}
|
|
}
|
|
});
|
|
let doc = doc_from(&root, "S");
|
|
let mut buf = vec![0u8; 8 + 5];
|
|
buf[0..4].copy_from_slice(&8u32.to_le_bytes());
|
|
buf[4..8].copy_from_slice(&5u32.to_le_bytes());
|
|
buf[8..13].copy_from_slice(b"hello");
|
|
let v = materialize_aligned_strict(&doc, &buf).expect("materialize");
|
|
assert_eq!(v["name"], json!("hello"));
|
|
}
|
|
|
|
#[test]
|
|
fn materialize_aligned_honors_field_level_endian_override() {
|
|
let root = json!({
|
|
"$defs": {
|
|
"S": {
|
|
"kind": "struct",
|
|
"endian": "big",
|
|
"fields": [
|
|
{ "name": "crc", "kind": "uint32", "endian": "little" }
|
|
]
|
|
}
|
|
}
|
|
});
|
|
let doc = doc_from(&root, "S");
|
|
let buf = [0x04u8, 0x03, 0x02, 0x01];
|
|
let v = materialize_aligned_strict(&doc, &buf).expect("materialize");
|
|
assert_eq!(v["crc"], json!(0x01020304u32));
|
|
}
|
|
|
|
#[test]
|
|
fn materialize_packed_rejects_non_finite_float() {
|
|
let root = json!({
|
|
"$defs": {
|
|
"S": {
|
|
"kind": "struct",
|
|
"fields": [ { "name": "f", "kind": "float32" } ]
|
|
}
|
|
}
|
|
});
|
|
let doc = doc_from(&root, "S");
|
|
let buf = f32::NAN.to_le_bytes();
|
|
let err = materialize_packed_strict(&root, &doc, &buf).unwrap_err();
|
|
assert!(matches!(err, AlkTypeError::Access { .. }), "got {err:?}");
|
|
}
|
|
} |