The running validate_pair campaign found a third crash: in aligned mode a maxLength reservation (ADR-003 strategy 2, VARCHAR(N)) stores RAW zero-padded data with no length prefix — materialize and validate_bytes implement exactly that — but read_field read the entry through data_access::read_string, i.e. parsed the window's first four bytes as a u32 length prefix. Raw reservation bytes that look like a large prefix then fail bounds with Access while validate_bytes says Ok: the validate⇒read agreement lattice breaks on every aligned maxLength string/bytes field (any schema declaring maxLength in aligned mode). write_field had the same mismatch (prefix+data into a raw window). Engine fix: - VariableEncoding gains MaxLengthReserved (additive variant, ADR-003 strategy 2). OffsetMap::compute records it for maxLength fields with the default encoding; maxLength+offset-indirect stays OffsetIndirect (the pair read is intentional, the window reserves max_len bytes), preserving the W3-1 combination semantics. - read_field String/Bytes arms dispatch on MaxLengthReserved → new data_access::read_reservation_string / read_reservation (raw window inside-buffer check + NUL trim — the materializer's exact semantics). - write_field dispatches → new data_access::write_reservation (zero- pads the window, rejects oversized values with Access). - materialize_aligned reads MaxLengthReserved through the same new read_reservation paths (single source of truth; replaces the inline trim logic with an identical implementation). - offset_map compute rejects a MaxLengthReserved encoding reaching the walk with a clean Offset error (recorded, never declared). - builder round-trips: MaxLengthReserved serializes via maxLength (the document form), never as an encoding value. - three engine regression tests: raw-not-prefixed read, zero-pad write + oversize rejection, validate⇒read_field agreement. - fuzz/shared validate_pair invariant updated: the W3-1 shorter-than-reservation exemption now applies to offset-indirect only; reservations assert the full window in-bounds (fixed engine). - corpus regenerated for generator-consistent numbering (seeds 037-044 relabeled; W3-1/W3-2 artifacts remain 044/045-047 → now 044, 048-050 region) — 48 seeds, replay 30/30 green. Verification: main crate 573 tests pass; clippy -D warnings clean (crate + shared); wasm clean; cargo fuzz build clean.
1825 lines
73 KiB
Rust
1825 lines
73 KiB
Rust
//! `AlkTypeEngine` — the compiled form of a BAST document.
|
||
//!
|
||
//! Combines the layout engine (both packed and aligned modes) and the
|
||
//! two validators into a single struct. Built once at schema load time
|
||
//! via [`AlkTypeEngine::compile`]. Used for repeated read/write/validate
|
||
//! operations at access time.
|
||
//!
|
||
//! Two validators, two inputs (D-BAST-006, D-BAST-007):
|
||
//! - `validate_bytes` — the BAST-native validator walks the BAST type
|
||
//! tree over a materialized `Value` (no `jsonschema` involvement).
|
||
//! - `validate_json` — a standard `jsonschema::Validator` compiled from
|
||
//! a consumer-provided JSON Schema (no custom keywords, no BAST
|
||
//! involvement).
|
||
//!
|
||
//! See [validation.md](../../docs/architecture/validation.md)
|
||
//! §"The AlkTypeEngine struct" and
|
||
//! [overview.md](../../docs/architecture/overview.md).
|
||
|
||
use crate::bast::{BastDefKind, BastDoc};
|
||
use crate::data_access;
|
||
use crate::error::AlkTypeError;
|
||
use crate::layout_builder::LayoutBuilder;
|
||
use crate::materialize;
|
||
use crate::offset_map::OffsetMap;
|
||
use crate::read_plan::ReadPlan;
|
||
use crate::schema::{AlkTypeKind, Endian, VariableEncoding};
|
||
use crate::sequential_reader::{FieldValue, SequentialReader};
|
||
use crate::validation;
|
||
use crate::validation_plan::ValidationPlan;
|
||
use serde_json::Value;
|
||
use std::fmt;
|
||
use std::sync::Arc;
|
||
|
||
/// The layout mode selected at engine construction time.
|
||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||
pub enum LayoutMode {
|
||
/// Packed sequential — for protocol wire formats (SFTP, channels, TTY).
|
||
Packed,
|
||
/// Aligned static — for mmap-friendly formats (metatensor, safetensors).
|
||
Aligned,
|
||
}
|
||
|
||
/// The layout strategy — packed sequential or aligned static.
|
||
///
|
||
/// Carries the layout-specific handles needed for read/write access in
|
||
/// the selected mode. The consumer chooses the mode at construction time
|
||
/// via [`AlkTypeEngine::compile`]; the engine then exposes only the
|
||
/// APIs that make sense for that mode.
|
||
#[derive(Debug)]
|
||
enum Layout {
|
||
/// Packed sequential layout. The write-side is [`LayoutBuilder`];
|
||
/// the read-side compiled form is the [`ReadPlan`] (ADR-011),
|
||
/// shared via `Arc` with every [`SequentialReader`] the factory
|
||
/// hands out (ADR-007 — the reader owns its cursor state, so the
|
||
/// engine is a factory, not a holder).
|
||
Packed {
|
||
builder: Box<LayoutBuilder>,
|
||
plan: Arc<ReadPlan>,
|
||
},
|
||
/// Aligned static layout. Field offsets are precomputed in an
|
||
/// [`OffsetMap`] for random access.
|
||
Aligned { offset_map: OffsetMap },
|
||
}
|
||
|
||
/// The compiled form of a BAST document. Combines the layout engine
|
||
/// (both packed and aligned modes) and the two validators.
|
||
///
|
||
/// Built once at schema load time via [`AlkTypeEngine::compile`].
|
||
/// Used for repeated read/write/validate operations at access time.
|
||
///
|
||
/// The consumer selects the layout mode at construction time. The engine
|
||
/// then exposes mode-appropriate accessors: [`AlkTypeEngine::offset_map`]
|
||
/// for aligned mode, [`AlkTypeEngine::layout_builder`] and
|
||
/// [`AlkTypeEngine::sequential_reader`] for packed mode.
|
||
///
|
||
/// Validation is split (D-BAST-006, D-BAST-007):
|
||
/// - [`AlkTypeEngine::validate_bytes`] uses the BAST-native validator
|
||
/// ([`crate::bast_validation`]) — no `jsonschema` involvement, no external
|
||
/// JSON Schema required.
|
||
/// - [`AlkTypeEngine::validate_json`] / [`AlkTypeEngine::is_valid_json`]
|
||
/// use a standard `jsonschema::Validator` compiled at construction
|
||
/// time from a consumer-provided JSON Schema. If no JSON Schema was
|
||
/// supplied to [`AlkTypeEngine::compile`], the JSON-validation methods
|
||
/// return [`AlkTypeError::Schema`].
|
||
pub struct AlkTypeEngine {
|
||
layout: Layout,
|
||
json_validator: Option<jsonschema::Validator>,
|
||
validation_plan: Arc<ValidationPlan>,
|
||
endian: Endian,
|
||
doc: BastDoc,
|
||
}
|
||
|
||
impl AlkTypeEngine {
|
||
/// Compile a BAST document into a [`AlkTypeEngine`].
|
||
///
|
||
/// This is the expensive operation — it parses the BAST document
|
||
/// into a typed tree, computes the layout, and (optionally) builds a
|
||
/// standard `jsonschema::Validator` for the JSON-validation path.
|
||
/// Call once at load time; use the returned engine for repeated
|
||
/// operations.
|
||
///
|
||
/// `bast_doc` is the raw BAST JSON `Value` (`{ "$defs": { ... } }`).
|
||
/// `root_name` selects which `$defs` entry is the top-level type
|
||
/// (D-BAST-001). The `mode` parameter selects the layout strategy.
|
||
/// The same document can be compiled in either mode.
|
||
///
|
||
/// `json_schema` is an optional consumer-provided standard JSON
|
||
/// Schema (Draft 2020-12 or any draft `jsonschema` supports) used by
|
||
/// [`AlkTypeEngine::validate_json`] / [`AlkTypeEngine::is_valid_json`]
|
||
/// (D-BAST-007). Pass `None` when JSON validation is not needed;
|
||
/// calling the JSON-validation methods then returns
|
||
/// [`AlkTypeError::Schema`]. The JSON Schema is independent of the
|
||
/// BAST document — BAST describes bytes, not JSON shape. It may be
|
||
/// authored separately or derived from BAST via future codegen.
|
||
///
|
||
/// The engine retains an owned [`BastDoc`] (ADR-012 §2a) so that
|
||
/// [`AlkTypeEngine::validate_bytes`] and
|
||
/// [`AlkTypeEngine::read_field`] can walk the typed tree without
|
||
/// re-parsing the raw document per call.
|
||
///
|
||
/// # Errors
|
||
///
|
||
/// Returns [`AlkTypeError::Schema`] if the BAST document is malformed
|
||
/// or the root type is not a struct, propagated from [`BastDoc::new`],
|
||
/// [`LayoutBuilder::new`], [`OffsetMap::compute`], or
|
||
/// [`validation::build_validator`] (when `json_schema` is `Some`).
|
||
pub fn compile(
|
||
bast_doc: &Value,
|
||
root_name: &str,
|
||
mode: LayoutMode,
|
||
json_schema: Option<&Value>,
|
||
) -> Result<Self, AlkTypeError> {
|
||
crate::bast_meta::validate_bast_doc(bast_doc)?;
|
||
let doc = BastDoc::new(bast_doc, root_name)?;
|
||
let root_def = doc.root_def();
|
||
let struct_node = match root_def.kind() {
|
||
BastDefKind::Struct(s) => s,
|
||
other => {
|
||
return Err(AlkTypeError::Schema(format!(
|
||
"AlkTypeEngine::compile requires a struct root, got {kind}",
|
||
kind = other.alk_kind()
|
||
)));
|
||
}
|
||
};
|
||
let endian = struct_node.endian();
|
||
// The compiled value-domain constraint tree (ADR-012 §3) — built
|
||
// once here, walked per buffer by `validate_bytes`. Its compile
|
||
// walk also rejects cyclic `$ref` graphs here, before the layout
|
||
// builders below (the standalone walkers now guard themselves
|
||
// via `walk_guard::check_ref_graph` — review #006 H2 — so this
|
||
// gate is engine-compile-time confirmation, not the only line of
|
||
// defense).
|
||
let validation_plan = Arc::new(ValidationPlan::compile(&doc)?);
|
||
let layout = match mode {
|
||
LayoutMode::Packed => {
|
||
let builder = Box::new(LayoutBuilder::new(bast_doc, root_name)?);
|
||
let plan = Arc::new(ReadPlan::compile(bast_doc, root_name)?);
|
||
Layout::Packed { builder, plan }
|
||
}
|
||
LayoutMode::Aligned => {
|
||
let offset_map = OffsetMap::compute(&doc)?;
|
||
Layout::Aligned { offset_map }
|
||
}
|
||
};
|
||
let json_validator = match json_schema {
|
||
Some(schema) => Some(validation::build_validator(schema)?),
|
||
None => None,
|
||
};
|
||
Ok(Self {
|
||
layout,
|
||
json_validator,
|
||
validation_plan,
|
||
endian,
|
||
doc,
|
||
})
|
||
}
|
||
|
||
/// The schema's endianness.
|
||
pub fn endian(&self) -> Endian {
|
||
self.endian
|
||
}
|
||
|
||
/// The root type name this engine was compiled with (D-BAST-001).
|
||
pub fn root_name(&self) -> &str {
|
||
self.doc.root_name()
|
||
}
|
||
|
||
/// The layout mode this engine was compiled with.
|
||
pub fn mode(&self) -> LayoutMode {
|
||
match self.layout {
|
||
Layout::Packed { .. } => LayoutMode::Packed,
|
||
Layout::Aligned { .. } => LayoutMode::Aligned,
|
||
}
|
||
}
|
||
|
||
/// Access the aligned offset map. Returns `None` if compiled in
|
||
/// packed mode.
|
||
pub fn offset_map(&self) -> Option<&OffsetMap> {
|
||
match &self.layout {
|
||
Layout::Aligned { offset_map } => Some(offset_map),
|
||
Layout::Packed { .. } => None,
|
||
}
|
||
}
|
||
|
||
/// Access the layout builder (write-side of packed mode).
|
||
/// Returns `None` if compiled in aligned mode.
|
||
pub fn layout_builder(&self) -> Option<&LayoutBuilder> {
|
||
match &self.layout {
|
||
Layout::Packed { builder, .. } => Some(builder),
|
||
Layout::Aligned { .. } => None,
|
||
}
|
||
}
|
||
|
||
/// Construct a fresh [`SequentialReader`] for packed-mode reads
|
||
/// (ADR-007, ADR-011). Each call returns a new reader with the
|
||
/// cursor at position 0, sharing the engine's `Arc<ReadPlan>` (a
|
||
/// refcount bump — no re-parse, no document clone). The consumer
|
||
/// owns the reader and calls `read_next`/`read_field`/`reset` on it
|
||
/// directly.
|
||
///
|
||
/// Returns `None` if compiled in aligned mode.
|
||
pub fn sequential_reader(&self) -> Option<SequentialReader> {
|
||
match &self.layout {
|
||
Layout::Packed { plan, .. } => {
|
||
Some(SequentialReader::new(Arc::clone(plan)))
|
||
}
|
||
Layout::Aligned { .. } => None,
|
||
}
|
||
}
|
||
|
||
/// Validate a JSON `Value` against the consumer-provided JSON Schema
|
||
/// supplied to [`AlkTypeEngine::compile`] (D-BAST-007).
|
||
///
|
||
/// The validator is a standard `jsonschema::Validator` built at
|
||
/// construction time from a plain JSON Schema document — no custom
|
||
/// keywords, no BAST involvement. BAST describes bytes, not JSON
|
||
/// shape; a JSON value is validated by a JSON Schema, which is a
|
||
/// separate concern.
|
||
///
|
||
/// Returns `Ok(())` if valid, `Err(AlkTypeError::Validation(...))` if
|
||
/// the instance violates the JSON Schema, or
|
||
/// `Err(AlkTypeError::Schema(...))` if no JSON Schema was supplied to
|
||
/// [`AlkTypeEngine::compile`].
|
||
///
|
||
/// # Errors
|
||
///
|
||
/// - [`AlkTypeError::Validation`] if the instance violates the JSON
|
||
/// Schema. The payload is the `jsonschema::ValidationError` (D-BAST-009
|
||
/// — uniform with the `validate_bytes` path).
|
||
/// - [`AlkTypeError::Schema`] if `compile` was called with
|
||
/// `json_schema: None`.
|
||
pub fn validate_json(&self, instance: &Value) -> Result<(), AlkTypeError> {
|
||
let validator = self.json_validator.as_ref().ok_or_else(|| {
|
||
AlkTypeError::Schema(
|
||
"validate_json requires a JSON Schema supplied to AlkTypeEngine::compile \
|
||
(json_schema was None)"
|
||
.to_string(),
|
||
)
|
||
})?;
|
||
validator
|
||
.validate(instance)
|
||
.map_err(|e| AlkTypeError::Validation(e.to_owned()))
|
||
}
|
||
|
||
/// Check if a JSON `Value` is valid against the consumer-provided
|
||
/// JSON Schema supplied to [`AlkTypeEngine::compile`] (D-BAST-007).
|
||
///
|
||
/// Returns `false` if the instance is invalid **or** if no JSON
|
||
/// Schema was supplied to [`AlkTypeEngine::compile`]. Use
|
||
/// [`AlkTypeEngine::validate_json`] if the distinction matters.
|
||
pub fn is_valid_json(&self, instance: &Value) -> bool {
|
||
self.json_validator
|
||
.as_ref()
|
||
.is_some_and(|v| v.is_valid(instance))
|
||
}
|
||
|
||
/// Validate a binary buffer against the schema by materializing a
|
||
/// `serde_json::Value` tree from the bytes (walking the layout engine)
|
||
/// and then validating that `Value` against the compiled
|
||
/// [`ValidationPlan`] — the engine's value-domain constraint tree
|
||
/// (integer ranges, `maxLength`, enum index bounds, union variant
|
||
/// constraints), built once at [`AlkTypeEngine::compile`] from the
|
||
/// BAST document (ADR-010; ADR-012 §3). Decided in D-BAST-006; see
|
||
/// [bast-format.md §Validation Model](../../docs/architecture/bast-format.md#validation-model).
|
||
///
|
||
/// The materialize half is the only per-buffer schema-touching step
|
||
/// (the bytes must be decoded against the tree); the validation half
|
||
/// walks the compiled plan — no `$ref` re-resolution, no schema
|
||
/// re-parse per buffer.
|
||
///
|
||
/// Dispatches on the engine's layout mode: packed mode walks
|
||
/// sequentially from offset 0; aligned mode reads at offsets from
|
||
/// the `OffsetMap`. Both produce the same `Value` form; the
|
||
/// validation plan is mode-agnostic.
|
||
///
|
||
/// # Errors
|
||
///
|
||
/// - [`AlkTypeError::Access`] if a field cannot be read from the
|
||
/// buffer (too short, invalid UTF-8, value out of range for the
|
||
/// target type). Carries the field path.
|
||
/// - [`AlkTypeError::Offset`] if a field is not found in the
|
||
/// offset map (aligned mode only).
|
||
/// - [`AlkTypeError::Validation`] if the materialized `Value`
|
||
/// violates a value-domain constraint expressed in the BAST
|
||
/// document. The payload is a `jsonschema::ValidationError::custom`
|
||
/// (D-BAST-009 — the variant type stays uniform with the
|
||
/// `validate_json` path).
|
||
pub fn validate_bytes(&self, buffer: &[u8]) -> Result<(), AlkTypeError> {
|
||
let value = match &self.layout {
|
||
Layout::Packed { plan, .. } => {
|
||
materialize::materialize_packed(plan, buffer)?
|
||
}
|
||
Layout::Aligned { offset_map } => {
|
||
materialize::materialize_aligned(&self.doc, buffer, offset_map)?
|
||
}
|
||
};
|
||
self.validation_plan.validate(&value)
|
||
}
|
||
|
||
/// Access the compiled [`ValidationPlan`] (ADR-012 §3).
|
||
///
|
||
/// The plan is the engine's value-domain constraint tree — built once
|
||
/// at [`AlkTypeEngine::compile`] from the BAST document, shared via
|
||
/// `Arc`, and walked by [`AlkTypeEngine::validate_bytes`] per buffer
|
||
/// without touching the BAST document. Exposed for consumers that
|
||
/// want to validate their *own* materialized `Value` trees
|
||
/// (e.g. one produced by an external reader) against the same
|
||
/// constraints, or that want the plan's
|
||
/// [`fingerprint`](ValidationPlan::fingerprint) for caching or
|
||
/// schema handshakes.
|
||
pub fn validation_plan(&self) -> &Arc<ValidationPlan> {
|
||
&self.validation_plan
|
||
}
|
||
|
||
/// Read a field from a buffer at its computed offset (aligned mode).
|
||
///
|
||
/// Looks up the field's [`OffsetEntry`](crate::offset_map::OffsetEntry) in the [`OffsetMap`] and reads
|
||
/// the appropriate type using the [`crate::data_access`] functions,
|
||
/// dispatching on the entry's [`LeafMeta`](crate::offset_map::LeafMeta) (kind, encoding, effective
|
||
/// endian — computed at compile time, ADR-012 §2b). Works for
|
||
/// fixed-size primitive kinds and length-prefixed `String`/`Bytes`
|
||
/// fields.
|
||
///
|
||
/// Returns an error if compiled in packed mode — use
|
||
/// [`AlkTypeEngine::sequential_reader`] for packed mode. Composite
|
||
/// kinds error as well: a struct path never has an `OffsetMap` entry
|
||
/// (only its leaf fields are recorded — read those by their dotted
|
||
/// paths), and `Union`/`Array`/`Record` are better handled via the
|
||
/// layout-specific APIs.
|
||
///
|
||
/// # Errors
|
||
///
|
||
/// - [`AlkTypeError::Access`] if compiled in packed mode.
|
||
/// - [`AlkTypeError::Offset`] if `field_path` is not in the offset
|
||
/// map (the reachable failure for any composite path, including
|
||
/// struct paths — no entry exists for them).
|
||
/// - [`AlkTypeError::Access`] for buffer-too-short or invalid data,
|
||
/// propagated from [`crate::data_access`].
|
||
pub fn read_field<'a>(
|
||
&self,
|
||
buffer: &'a [u8],
|
||
field_path: &str,
|
||
) -> Result<FieldValue<'a>, AlkTypeError> {
|
||
let offset_map = match &self.layout {
|
||
Layout::Aligned { offset_map } => offset_map,
|
||
Layout::Packed { .. } => {
|
||
return Err(AlkTypeError::Access {
|
||
field_path: field_path.to_string(),
|
||
reason: "read_field is only available in aligned mode; \
|
||
use sequential_reader() for packed mode"
|
||
.to_string(),
|
||
});
|
||
}
|
||
};
|
||
let entry = offset_map
|
||
.get(field_path)
|
||
.ok_or_else(|| AlkTypeError::Offset {
|
||
field_path: field_path.to_string(),
|
||
reason: "field not found in offset map".to_string(),
|
||
})?;
|
||
let (kind, encoding, endian) = (entry.meta.kind, entry.meta.encoding, entry.meta.endian);
|
||
let range = entry.range;
|
||
match kind {
|
||
AlkTypeKind::Int8 => {
|
||
let v = data_access::read_i8(buffer, range.start, field_path)?;
|
||
Ok(FieldValue::I8(v))
|
||
}
|
||
AlkTypeKind::Int16 => {
|
||
let v = data_access::read_i16(buffer, range.start, field_path, endian)?;
|
||
Ok(FieldValue::I16(v))
|
||
}
|
||
AlkTypeKind::Int32 => {
|
||
let v = data_access::read_i32(buffer, range.start, field_path, endian)?;
|
||
Ok(FieldValue::I32(v))
|
||
}
|
||
AlkTypeKind::Int64 => {
|
||
let v = data_access::read_i64(buffer, range.start, field_path, endian)?;
|
||
Ok(FieldValue::I64(v))
|
||
}
|
||
AlkTypeKind::Uint8 => {
|
||
let v = data_access::read_u8(buffer, range.start, field_path)?;
|
||
Ok(FieldValue::U8(v))
|
||
}
|
||
AlkTypeKind::Uint16 => {
|
||
let v = data_access::read_u16(buffer, range.start, field_path, endian)?;
|
||
Ok(FieldValue::U16(v))
|
||
}
|
||
AlkTypeKind::Uint32 => {
|
||
let v = data_access::read_u32(buffer, range.start, field_path, endian)?;
|
||
Ok(FieldValue::U32(v))
|
||
}
|
||
AlkTypeKind::Uint64 => {
|
||
let v = data_access::read_u64(buffer, range.start, field_path, endian)?;
|
||
Ok(FieldValue::U64(v))
|
||
}
|
||
AlkTypeKind::Float32 => {
|
||
let v = data_access::read_f32(buffer, range.start, field_path, endian)?;
|
||
Ok(FieldValue::F32(v))
|
||
}
|
||
AlkTypeKind::Float64 => {
|
||
let v = data_access::read_f64(buffer, range.start, field_path, endian)?;
|
||
Ok(FieldValue::F64(v))
|
||
}
|
||
AlkTypeKind::Boolean => {
|
||
let v = data_access::read_bool(buffer, range.start, field_path)?;
|
||
Ok(FieldValue::Bool(v))
|
||
}
|
||
AlkTypeKind::Enum => {
|
||
let v = data_access::read_enum(buffer, range.start, field_path, endian)?;
|
||
Ok(FieldValue::Enum(v))
|
||
}
|
||
AlkTypeKind::String => {
|
||
let v = match encoding {
|
||
VariableEncoding::OffsetIndirect => {
|
||
data_access::read_string_indirect(buffer, range.start, field_path, endian)?
|
||
}
|
||
VariableEncoding::MaxLengthReserved => {
|
||
data_access::read_reservation_string(buffer, range, field_path)?
|
||
}
|
||
VariableEncoding::LengthPrefixed => {
|
||
data_access::read_string(buffer, range.start, field_path, endian)?
|
||
}
|
||
};
|
||
Ok(FieldValue::String(v))
|
||
}
|
||
AlkTypeKind::Bytes => {
|
||
let v = match encoding {
|
||
VariableEncoding::OffsetIndirect => {
|
||
data_access::read_bytes_indirect(buffer, range.start, field_path, endian)?
|
||
}
|
||
VariableEncoding::MaxLengthReserved => {
|
||
data_access::read_reservation(buffer, range, field_path)?
|
||
}
|
||
VariableEncoding::LengthPrefixed => {
|
||
data_access::read_bytes(buffer, range.start, field_path, endian)?
|
||
}
|
||
};
|
||
Ok(FieldValue::Bytes(v))
|
||
}
|
||
AlkTypeKind::Struct => Err(AlkTypeError::Offset {
|
||
field_path: field_path.to_string(),
|
||
reason: "read_field does not support composite types (no offset-map entry \
|
||
exists for a struct path — only its leaf fields are recorded); \
|
||
read the leaf fields by their dotted paths instead"
|
||
.to_string(),
|
||
}),
|
||
AlkTypeKind::Union | AlkTypeKind::Array | AlkTypeKind::Record => {
|
||
Err(AlkTypeError::Access {
|
||
field_path: field_path.to_string(),
|
||
reason: "read_field does not support composite types; \
|
||
use the layout-specific APIs"
|
||
.to_string(),
|
||
})
|
||
}
|
||
}
|
||
}
|
||
|
||
/// Write a field to a buffer at its computed offset (aligned mode).
|
||
///
|
||
/// Looks up the field's [`OffsetEntry`](crate::offset_map::OffsetEntry) in the [`OffsetMap`] and writes
|
||
/// the appropriate type using the [`crate::data_access`] functions,
|
||
/// dispatching on the entry's [`LeafMeta`](crate::offset_map::LeafMeta) (kind, encoding, effective
|
||
/// endian — computed at compile time, ADR-012 §2b). Works for
|
||
/// fixed-size primitive kinds and length-prefixed `String`/`Bytes`
|
||
/// fields.
|
||
///
|
||
/// Returns an error if compiled in packed mode — use
|
||
/// [`AlkTypeEngine::layout_builder`] for packed mode. Also returns
|
||
/// an error for composite kinds (`Struct`, `Union`, `Array`, `Record`).
|
||
///
|
||
/// # Errors
|
||
///
|
||
/// - [`AlkTypeError::Access`] if compiled in packed mode.
|
||
/// - [`AlkTypeError::Offset`] if `field_path` is not in the offset map.
|
||
/// - [`AlkTypeError::Access`] for buffer-too-short or invalid data,
|
||
/// propagated from [`crate::data_access`].
|
||
pub fn write_field(
|
||
&self,
|
||
buffer: &mut [u8],
|
||
field_path: &str,
|
||
value: &FieldValue<'_>,
|
||
) -> Result<(), AlkTypeError> {
|
||
let offset_map = match &self.layout {
|
||
Layout::Aligned { offset_map } => offset_map,
|
||
Layout::Packed { .. } => {
|
||
return Err(AlkTypeError::Access {
|
||
field_path: field_path.to_string(),
|
||
reason: "write_field is only available in aligned mode; \
|
||
use layout_builder() for packed mode"
|
||
.to_string(),
|
||
});
|
||
}
|
||
};
|
||
let entry = offset_map
|
||
.get(field_path)
|
||
.ok_or_else(|| AlkTypeError::Offset {
|
||
field_path: field_path.to_string(),
|
||
reason: "field not found in offset map".to_string(),
|
||
})?;
|
||
let (encoding, endian) = (entry.meta.encoding, entry.meta.endian);
|
||
let range = entry.range;
|
||
match value {
|
||
FieldValue::I8(v) => data_access::write_i8(buffer, range.start, *v, field_path),
|
||
FieldValue::I16(v) => {
|
||
data_access::write_i16(buffer, range.start, *v, field_path, endian)
|
||
}
|
||
FieldValue::I32(v) => {
|
||
data_access::write_i32(buffer, range.start, *v, field_path, endian)
|
||
}
|
||
FieldValue::I64(v) => {
|
||
data_access::write_i64(buffer, range.start, *v, field_path, endian)
|
||
}
|
||
FieldValue::U8(v) => data_access::write_u8(buffer, range.start, *v, field_path),
|
||
FieldValue::U16(v) => {
|
||
data_access::write_u16(buffer, range.start, *v, field_path, endian)
|
||
}
|
||
FieldValue::U32(v) => {
|
||
data_access::write_u32(buffer, range.start, *v, field_path, endian)
|
||
}
|
||
FieldValue::U64(v) => {
|
||
data_access::write_u64(buffer, range.start, *v, field_path, endian)
|
||
}
|
||
FieldValue::F32(v) => {
|
||
data_access::write_f32(buffer, range.start, *v, field_path, endian)
|
||
}
|
||
FieldValue::F64(v) => {
|
||
data_access::write_f64(buffer, range.start, *v, field_path, endian)
|
||
}
|
||
FieldValue::Bool(v) => data_access::write_bool(buffer, range.start, *v, field_path),
|
||
FieldValue::Enum(v) => {
|
||
data_access::write_enum(buffer, range.start, *v, field_path, endian)
|
||
}
|
||
FieldValue::String(v) => {
|
||
match encoding {
|
||
VariableEncoding::OffsetIndirect => {
|
||
return Err(AlkTypeError::Access {
|
||
field_path: field_path.to_string(),
|
||
reason: "write_field cannot write offset-indirect fields; \
|
||
use data_access::write_string_indirect with the \
|
||
offset map range and a data offset"
|
||
.to_string(),
|
||
});
|
||
}
|
||
VariableEncoding::MaxLengthReserved => {
|
||
data_access::write_reservation(
|
||
buffer,
|
||
range,
|
||
v.as_bytes(),
|
||
field_path,
|
||
)?;
|
||
}
|
||
VariableEncoding::LengthPrefixed => {
|
||
data_access::write_string(buffer, range.start, v, field_path, endian)?;
|
||
}
|
||
}
|
||
Ok(())
|
||
}
|
||
FieldValue::Bytes(v) => {
|
||
match encoding {
|
||
VariableEncoding::OffsetIndirect => {
|
||
return Err(AlkTypeError::Access {
|
||
field_path: field_path.to_string(),
|
||
reason: "write_field cannot write offset-indirect fields; \
|
||
use data_access::write_bytes_indirect with the \
|
||
offset map range and a data offset"
|
||
.to_string(),
|
||
});
|
||
}
|
||
VariableEncoding::MaxLengthReserved => {
|
||
data_access::write_reservation(buffer, range, v, field_path)?;
|
||
}
|
||
VariableEncoding::LengthPrefixed => {
|
||
data_access::write_bytes(buffer, range.start, v, field_path, endian)?;
|
||
}
|
||
}
|
||
Ok(())
|
||
}
|
||
FieldValue::Struct { .. } | FieldValue::Union { .. } | FieldValue::Array { .. } => {
|
||
Err(AlkTypeError::Access {
|
||
field_path: field_path.to_string(),
|
||
reason: "write_field does not support composite types; \
|
||
use the layout-specific APIs"
|
||
.to_string(),
|
||
})
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
impl fmt::Debug for AlkTypeEngine {
|
||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||
f.debug_struct("AlkTypeEngine")
|
||
.field("layout", &self.layout)
|
||
.field("json_validator", &self.json_validator.as_ref().map(|_| "<jsonschema::Validator>"))
|
||
.field("endian", &self.endian)
|
||
.field("root_name", &self.root_name())
|
||
.finish()
|
||
}
|
||
}
|
||
|
||
#[cfg(test)]
|
||
mod tests {
|
||
use super::*;
|
||
use serde_json::json;
|
||
|
||
fn fixed_struct_doc() -> Value {
|
||
json!({
|
||
"$defs": {
|
||
"S": {
|
||
"kind": "struct",
|
||
"endian": "little",
|
||
"fields": [
|
||
{ "name": "flag", "kind": "uint8" },
|
||
{ "name": "id", "kind": "uint32" },
|
||
{ "name": "score", "kind": "float32" },
|
||
{ "name": "tag", "kind": "string" }
|
||
]
|
||
}
|
||
}
|
||
})
|
||
}
|
||
|
||
#[test]
|
||
fn compile_aligned_builds_offset_map() {
|
||
let doc = fixed_struct_doc();
|
||
let engine = AlkTypeEngine::compile(&doc, "S", LayoutMode::Aligned, None).expect("compile");
|
||
assert_eq!(engine.mode(), LayoutMode::Aligned);
|
||
assert!(engine.offset_map().is_some());
|
||
assert!(engine.layout_builder().is_none());
|
||
assert!(engine.sequential_reader().is_none());
|
||
}
|
||
|
||
#[test]
|
||
fn compile_packed_builds_builder_and_reader() {
|
||
let doc = fixed_struct_doc();
|
||
let engine = AlkTypeEngine::compile(&doc, "S", LayoutMode::Packed, None).expect("compile");
|
||
assert_eq!(engine.mode(), LayoutMode::Packed);
|
||
assert!(engine.layout_builder().is_some());
|
||
assert!(engine.sequential_reader().is_some());
|
||
assert!(engine.offset_map().is_none());
|
||
}
|
||
|
||
#[test]
|
||
fn endian_parsed_from_struct() {
|
||
let doc = json!({
|
||
"$defs": {
|
||
"S": {
|
||
"kind": "struct",
|
||
"endian": "big",
|
||
"fields": [ { "name": "id", "kind": "uint32" } ]
|
||
}
|
||
}
|
||
});
|
||
let engine = AlkTypeEngine::compile(&doc, "S", LayoutMode::Packed, None).expect("compile");
|
||
assert_eq!(engine.endian(), Endian::Big);
|
||
}
|
||
|
||
#[test]
|
||
fn endian_defaults_to_little() {
|
||
let doc = json!({
|
||
"$defs": {
|
||
"S": {
|
||
"kind": "struct",
|
||
"fields": [ { "name": "id", "kind": "uint32" } ]
|
||
}
|
||
}
|
||
});
|
||
let engine = AlkTypeEngine::compile(&doc, "S", LayoutMode::Packed, None).expect("compile");
|
||
assert_eq!(engine.endian(), Endian::Little);
|
||
}
|
||
|
||
#[test]
|
||
fn read_field_aligned_reads_fixed_fields() {
|
||
let doc = json!({
|
||
"$defs": {
|
||
"S": {
|
||
"kind": "struct",
|
||
"endian": "little",
|
||
"fields": [
|
||
{ "name": "flag", "kind": "uint8" },
|
||
{ "name": "id", "kind": "uint32" }
|
||
]
|
||
}
|
||
}
|
||
});
|
||
let engine = AlkTypeEngine::compile(&doc, "S", LayoutMode::Aligned, None).expect("compile");
|
||
let mut buf = vec![0u8; 8];
|
||
buf[0] = 0xAB;
|
||
buf[4..8].copy_from_slice(&0x01020304u32.to_le_bytes());
|
||
assert_eq!(
|
||
engine.read_field(&buf, "flag").unwrap(),
|
||
FieldValue::U8(0xAB)
|
||
);
|
||
assert_eq!(
|
||
engine.read_field(&buf, "id").unwrap(),
|
||
FieldValue::U32(0x01020304)
|
||
);
|
||
}
|
||
|
||
#[test]
|
||
fn read_field_aligned_reads_string_length_prefixed() {
|
||
let doc = json!({
|
||
"$defs": {
|
||
"S": {
|
||
"kind": "struct",
|
||
"fields": [ { "name": "name", "kind": "string" } ]
|
||
}
|
||
}
|
||
});
|
||
let engine = AlkTypeEngine::compile(&doc, "S", LayoutMode::Aligned, None).expect("compile");
|
||
let mut buf = vec![0u8; 32];
|
||
let len_bytes = 5u32.to_le_bytes();
|
||
buf[0..4].copy_from_slice(&len_bytes);
|
||
buf[4..9].copy_from_slice(b"hello");
|
||
assert_eq!(
|
||
engine.read_field(&buf, "name").unwrap(),
|
||
FieldValue::String("hello")
|
||
);
|
||
}
|
||
|
||
#[test]
|
||
fn read_field_returns_access_error_in_packed_mode() {
|
||
let doc = json!({
|
||
"$defs": {
|
||
"S": {
|
||
"kind": "struct",
|
||
"fields": [ { "name": "id", "kind": "uint32" } ]
|
||
}
|
||
}
|
||
});
|
||
let engine = AlkTypeEngine::compile(&doc, "S", LayoutMode::Packed, None).expect("compile");
|
||
let buf = [0u8; 4];
|
||
let err = engine.read_field(&buf, "id").unwrap_err();
|
||
assert!(matches!(err, AlkTypeError::Access { .. }), "got {err:?}");
|
||
}
|
||
|
||
#[test]
|
||
fn read_field_returns_offset_error_for_missing_field() {
|
||
let doc = json!({
|
||
"$defs": {
|
||
"S": {
|
||
"kind": "struct",
|
||
"fields": [ { "name": "id", "kind": "uint32" } ]
|
||
}
|
||
}
|
||
});
|
||
let engine = AlkTypeEngine::compile(&doc, "S", LayoutMode::Aligned, None).expect("compile");
|
||
let buf = [0u8; 4];
|
||
let err = engine.read_field(&buf, "missing").unwrap_err();
|
||
assert!(matches!(err, AlkTypeError::Offset { .. }), "got {err:?}");
|
||
}
|
||
|
||
#[test]
|
||
fn read_field_on_nested_struct_path_is_offset_error_not_struct_value() {
|
||
// M3: the read_field kind dispatch previously had a Struct arm
|
||
// returning FieldValue::Struct — unreachable, because
|
||
// OffsetMap::compute records only a nested struct's inner leaf
|
||
// entries, never an entry for the struct path itself. Lock the
|
||
// honest behavior: the struct path is an Offset miss.
|
||
let doc = json!({
|
||
"$defs": {
|
||
"S": {
|
||
"kind": "struct",
|
||
"fields": [
|
||
{
|
||
"name": "header",
|
||
"kind": {
|
||
"kind": "struct",
|
||
"fields": [ { "name": "magic", "kind": "uint32" } ]
|
||
}
|
||
}
|
||
]
|
||
}
|
||
}
|
||
});
|
||
let engine = AlkTypeEngine::compile(&doc, "S", LayoutMode::Aligned, None).expect("compile");
|
||
let buf = [0u8; 4];
|
||
let err = engine.read_field(&buf, "header").unwrap_err();
|
||
assert!(matches!(err, AlkTypeError::Offset { .. }), "got {err:?}");
|
||
// The leaf inside the nested struct is reachable by dotted path.
|
||
assert!(engine.read_field(&buf, "header.magic").is_ok());
|
||
}
|
||
|
||
#[test]
|
||
fn read_field_returns_error_for_composite_types() {
|
||
let doc = json!({
|
||
"$defs": {
|
||
"S": {
|
||
"kind": "struct",
|
||
"fields": [
|
||
{ "name": "vals", "kind": { "kind": "array", "element": "uint32", "count": 2 } }
|
||
]
|
||
}
|
||
}
|
||
});
|
||
let engine = AlkTypeEngine::compile(&doc, "S", LayoutMode::Aligned, None).expect("compile");
|
||
let buf = [0u8; 8];
|
||
// `vals` itself is not in the offset map (only `vals[0]`/`vals[1]`
|
||
// are). The lookup fails with `Offset`. Reading an individual
|
||
// array element returns its kind (`Uint32`), which `read_field`
|
||
// handles as a leaf — composites aren't addressed via
|
||
// `read_field`. This test confirms the offset-map lookup path
|
||
// errors out for the array field path.
|
||
let err = engine.read_field(&buf, "vals").unwrap_err();
|
||
assert!(
|
||
matches!(err, AlkTypeError::Access { .. } | AlkTypeError::Offset { .. }),
|
||
"got {err:?}"
|
||
);
|
||
}
|
||
|
||
#[test]
|
||
fn write_field_aligned_writes_fixed_fields() {
|
||
let doc = json!({
|
||
"$defs": {
|
||
"S": {
|
||
"kind": "struct",
|
||
"endian": "little",
|
||
"fields": [
|
||
{ "name": "flag", "kind": "uint8" },
|
||
{ "name": "id", "kind": "uint32" }
|
||
]
|
||
}
|
||
}
|
||
});
|
||
let engine = AlkTypeEngine::compile(&doc, "S", LayoutMode::Aligned, None).expect("compile");
|
||
let mut buf = vec![0u8; 8];
|
||
engine
|
||
.write_field(&mut buf, "flag", &FieldValue::U8(0xAB))
|
||
.unwrap();
|
||
engine
|
||
.write_field(&mut buf, "id", &FieldValue::U32(0x01020304))
|
||
.unwrap();
|
||
assert_eq!(buf[0], 0xAB);
|
||
assert_eq!(&buf[4..8], &0x01020304u32.to_le_bytes());
|
||
assert_eq!(
|
||
engine.read_field(&buf, "flag").unwrap(),
|
||
FieldValue::U8(0xAB)
|
||
);
|
||
assert_eq!(
|
||
engine.read_field(&buf, "id").unwrap(),
|
||
FieldValue::U32(0x01020304)
|
||
);
|
||
}
|
||
|
||
#[test]
|
||
fn write_field_round_trips_string() {
|
||
let doc = json!({
|
||
"$defs": {
|
||
"S": {
|
||
"kind": "struct",
|
||
"fields": [ { "name": "name", "kind": "string" } ]
|
||
}
|
||
}
|
||
});
|
||
let engine = AlkTypeEngine::compile(&doc, "S", LayoutMode::Aligned, None).expect("compile");
|
||
let mut buf = vec![0u8; 32];
|
||
engine
|
||
.write_field(&mut buf, "name", &FieldValue::String("hello"))
|
||
.unwrap();
|
||
assert_eq!(
|
||
engine.read_field(&buf, "name").unwrap(),
|
||
FieldValue::String("hello")
|
||
);
|
||
}
|
||
|
||
#[test]
|
||
fn write_field_returns_access_error_in_packed_mode() {
|
||
let doc = json!({
|
||
"$defs": {
|
||
"S": {
|
||
"kind": "struct",
|
||
"fields": [ { "name": "id", "kind": "uint32" } ]
|
||
}
|
||
}
|
||
});
|
||
let engine = AlkTypeEngine::compile(&doc, "S", LayoutMode::Packed, None).expect("compile");
|
||
let mut buf = [0u8; 4];
|
||
let err = engine
|
||
.write_field(&mut buf, "id", &FieldValue::U32(1))
|
||
.unwrap_err();
|
||
assert!(matches!(err, AlkTypeError::Access { .. }), "got {err:?}");
|
||
}
|
||
|
||
#[test]
|
||
fn write_field_returns_offset_error_for_missing_field() {
|
||
let doc = json!({
|
||
"$defs": {
|
||
"S": {
|
||
"kind": "struct",
|
||
"fields": [ { "name": "id", "kind": "uint32" } ]
|
||
}
|
||
}
|
||
});
|
||
let engine = AlkTypeEngine::compile(&doc, "S", LayoutMode::Aligned, None).expect("compile");
|
||
let mut buf = [0u8; 4];
|
||
let err = engine
|
||
.write_field(&mut buf, "missing", &FieldValue::U32(1))
|
||
.unwrap_err();
|
||
assert!(matches!(err, AlkTypeError::Offset { .. }), "got {err:?}");
|
||
}
|
||
|
||
#[test]
|
||
fn write_field_returns_error_for_composite_value() {
|
||
let doc = json!({
|
||
"$defs": {
|
||
"S": {
|
||
"kind": "struct",
|
||
"fields": [ { "name": "id", "kind": "uint32" } ]
|
||
}
|
||
}
|
||
});
|
||
let engine = AlkTypeEngine::compile(&doc, "S", LayoutMode::Aligned, None).expect("compile");
|
||
let mut buf = [0u8; 8];
|
||
let err = engine
|
||
.write_field(&mut buf, "id", &FieldValue::Struct { start: 0, end: 4 })
|
||
.unwrap_err();
|
||
assert!(matches!(err, AlkTypeError::Access { .. }), "got {err:?}");
|
||
}
|
||
|
||
#[test]
|
||
fn compile_returns_schema_error_for_invalid_top_level() {
|
||
let doc = json!({ "type": "object", "properties": {} });
|
||
let err = AlkTypeEngine::compile(&doc, "S", LayoutMode::Packed, None).unwrap_err();
|
||
assert!(matches!(err, AlkTypeError::Schema(_)), "got {err:?}");
|
||
}
|
||
|
||
#[test]
|
||
fn compile_returns_schema_error_for_non_struct_root() {
|
||
let doc = json!({
|
||
"$defs": {
|
||
"U": {
|
||
"kind": "union",
|
||
"discriminator": { "kind": "byte", "offset": 0, "type": "uint8" },
|
||
"mapping": { "1": { "$ref": "#/$defs/A" } }
|
||
},
|
||
"A": { "kind": "struct", "fields": [] }
|
||
}
|
||
});
|
||
let err = AlkTypeEngine::compile(&doc, "U", LayoutMode::Packed, None).unwrap_err();
|
||
assert!(matches!(err, AlkTypeError::Schema(_)), "got {err:?}");
|
||
}
|
||
|
||
#[test]
|
||
fn compile_rejects_unknown_endian_annotation() {
|
||
let doc = json!({
|
||
"$defs": {
|
||
"S": {
|
||
"kind": "struct",
|
||
"endian": "middle",
|
||
"fields": [ { "name": "id", "kind": "uint32" } ]
|
||
}
|
||
}
|
||
});
|
||
let err = AlkTypeEngine::compile(&doc, "S", LayoutMode::Packed, None).unwrap_err();
|
||
assert!(matches!(err, AlkTypeError::Schema(_)), "got {err:?}");
|
||
}
|
||
|
||
#[test]
|
||
fn compile_rejects_unknown_encoding_annotation() {
|
||
let doc = json!({
|
||
"$defs": {
|
||
"S": {
|
||
"kind": "struct",
|
||
"fields": [
|
||
{ "name": "blob", "kind": "bytes", "encoding": "sideways" }
|
||
]
|
||
}
|
||
}
|
||
});
|
||
let err = AlkTypeEngine::compile(&doc, "S", LayoutMode::Packed, None).unwrap_err();
|
||
assert!(matches!(err, AlkTypeError::Schema(_)), "got {err:?}");
|
||
}
|
||
|
||
#[test]
|
||
fn compile_rejects_non_integer_align() {
|
||
let doc = json!({
|
||
"$defs": {
|
||
"S": {
|
||
"kind": "struct",
|
||
"align": "wide",
|
||
"fields": [ { "name": "id", "kind": "uint32" } ]
|
||
}
|
||
}
|
||
});
|
||
let err = AlkTypeEngine::compile(&doc, "S", LayoutMode::Packed, None).unwrap_err();
|
||
assert!(matches!(err, AlkTypeError::Schema(_)), "got {err:?}");
|
||
}
|
||
|
||
#[test]
|
||
fn compile_accepts_inline_struct_field() {
|
||
let doc = json!({
|
||
"$defs": {
|
||
"S": {
|
||
"kind": "struct",
|
||
"fields": [
|
||
{ "name": "inner", "kind": {
|
||
"kind": "struct",
|
||
"fields": [ { "name": "x", "kind": "uint16" } ]
|
||
} }
|
||
]
|
||
}
|
||
}
|
||
});
|
||
let engine = AlkTypeEngine::compile(&doc, "S", LayoutMode::Packed, None).expect("compile");
|
||
assert_eq!(engine.mode(), LayoutMode::Packed);
|
||
}
|
||
|
||
#[test]
|
||
fn debug_formats_without_panicking() {
|
||
let doc = fixed_struct_doc();
|
||
let engine = AlkTypeEngine::compile(&doc, "S", LayoutMode::Aligned, None).expect("compile");
|
||
let s = format!("{engine:?}");
|
||
assert!(s.contains("AlkTypeEngine"));
|
||
assert!(s.contains("Aligned"));
|
||
}
|
||
|
||
#[test]
|
||
fn offset_map_entries_carry_leaf_meta() {
|
||
let doc = json!({
|
||
"$defs": {
|
||
"S": {
|
||
"kind": "struct",
|
||
"fields": [
|
||
{
|
||
"name": "header",
|
||
"kind": {
|
||
"kind": "struct",
|
||
"fields": [
|
||
{ "name": "magic", "kind": "uint32" },
|
||
{ "name": "version", "kind": "uint8" }
|
||
]
|
||
}
|
||
}
|
||
]
|
||
}
|
||
}
|
||
});
|
||
let engine = AlkTypeEngine::compile(&doc, "S", LayoutMode::Aligned, None).expect("compile");
|
||
let map = engine.offset_map().expect("aligned map");
|
||
assert_eq!(map.get("header.magic").unwrap().meta.kind, AlkTypeKind::Uint32);
|
||
assert_eq!(map.get("header.version").unwrap().meta.kind, AlkTypeKind::Uint8);
|
||
assert!(map.get("header.missing").is_none());
|
||
assert!(map.get("missing").is_none());
|
||
}
|
||
|
||
#[test]
|
||
fn read_field_aligned_reads_nested_struct_leaf_fields() {
|
||
let doc = json!({
|
||
"$defs": {
|
||
"S": {
|
||
"kind": "struct",
|
||
"endian": "little",
|
||
"fields": [
|
||
{
|
||
"name": "header",
|
||
"kind": {
|
||
"kind": "struct",
|
||
"fields": [
|
||
{ "name": "magic", "kind": "uint32" },
|
||
{ "name": "version", "kind": "uint8" }
|
||
]
|
||
}
|
||
},
|
||
{ "name": "body", "kind": "uint32" }
|
||
]
|
||
}
|
||
}
|
||
});
|
||
let engine = AlkTypeEngine::compile(&doc, "S", LayoutMode::Aligned, None).expect("compile");
|
||
let mut buf = vec![0u8; 12];
|
||
buf[0..4].copy_from_slice(&0xDEADBEEFu32.to_le_bytes());
|
||
buf[4] = 0x01;
|
||
buf[8..12].copy_from_slice(&0xCAFEBABEu32.to_le_bytes());
|
||
assert_eq!(
|
||
engine.read_field(&buf, "header.magic").unwrap(),
|
||
FieldValue::U32(0xDEADBEEF)
|
||
);
|
||
assert_eq!(
|
||
engine.read_field(&buf, "header.version").unwrap(),
|
||
FieldValue::U8(0x01)
|
||
);
|
||
assert_eq!(
|
||
engine.read_field(&buf, "body").unwrap(),
|
||
FieldValue::U32(0xCAFEBABE)
|
||
);
|
||
}
|
||
|
||
#[test]
|
||
fn read_field_aligned_reads_bytes_field() {
|
||
let doc = json!({
|
||
"$defs": {
|
||
"S": {
|
||
"kind": "struct",
|
||
"fields": [ { "name": "blob", "kind": "bytes" } ]
|
||
}
|
||
}
|
||
});
|
||
let engine = AlkTypeEngine::compile(&doc, "S", LayoutMode::Aligned, None).expect("compile");
|
||
let mut buf = vec![0u8; 16];
|
||
let payload = [0xAA, 0xBB, 0xCC];
|
||
let len_bytes = 3u32.to_le_bytes();
|
||
buf[0..4].copy_from_slice(&len_bytes);
|
||
buf[4..7].copy_from_slice(&payload);
|
||
match engine.read_field(&buf, "blob").unwrap() {
|
||
FieldValue::Bytes(b) => assert_eq!(b, &payload[..]),
|
||
other => panic!("expected Bytes, got {other:?}"),
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn read_field_aligned_honors_field_level_endian_override() {
|
||
let doc = json!({
|
||
"$defs": {
|
||
"S": {
|
||
"kind": "struct",
|
||
"endian": "big",
|
||
"fields": [
|
||
{ "name": "crc", "kind": "uint32", "endian": "little" }
|
||
]
|
||
}
|
||
}
|
||
});
|
||
let engine = AlkTypeEngine::compile(&doc, "S", LayoutMode::Aligned, None).expect("compile");
|
||
let buf = [0x04u8, 0x03, 0x02, 0x01];
|
||
assert_eq!(
|
||
engine.read_field(&buf, "crc").unwrap(),
|
||
FieldValue::U32(0x01020304)
|
||
);
|
||
}
|
||
|
||
#[test]
|
||
fn write_field_aligned_honors_field_level_endian_override() {
|
||
let doc = json!({
|
||
"$defs": {
|
||
"S": {
|
||
"kind": "struct",
|
||
"endian": "big",
|
||
"fields": [
|
||
{ "name": "crc", "kind": "uint32", "endian": "little" }
|
||
]
|
||
}
|
||
}
|
||
});
|
||
let engine = AlkTypeEngine::compile(&doc, "S", LayoutMode::Aligned, None).expect("compile");
|
||
let mut buf = [0u8; 4];
|
||
engine
|
||
.write_field(&mut buf, "crc", &FieldValue::U32(0x01020304))
|
||
.unwrap();
|
||
assert_eq!(buf, [0x04, 0x03, 0x02, 0x01]);
|
||
}
|
||
|
||
#[test]
|
||
fn read_field_aligned_reads_offset_indirect_bytes() {
|
||
let doc = json!({
|
||
"$defs": {
|
||
"S": {
|
||
"kind": "struct",
|
||
"fields": [
|
||
{ "name": "blob", "kind": "bytes", "encoding": "offset-indirect" }
|
||
]
|
||
}
|
||
}
|
||
});
|
||
let engine = AlkTypeEngine::compile(&doc, "S", LayoutMode::Aligned, None).expect("compile");
|
||
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]);
|
||
match engine.read_field(&buf, "blob").unwrap() {
|
||
FieldValue::Bytes(b) => assert_eq!(b, &[0xAA, 0xBB, 0xCC]),
|
||
other => panic!("expected Bytes, got {other:?}"),
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn read_field_aligned_reads_offset_indirect_string() {
|
||
let doc = json!({
|
||
"$defs": {
|
||
"S": {
|
||
"kind": "struct",
|
||
"fields": [
|
||
{ "name": "name", "kind": "string", "encoding": "offset-indirect" }
|
||
]
|
||
}
|
||
}
|
||
});
|
||
let engine = AlkTypeEngine::compile(&doc, "S", LayoutMode::Aligned, None).expect("compile");
|
||
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");
|
||
match engine.read_field(&buf, "name").unwrap() {
|
||
FieldValue::String(s) => assert_eq!(s, "hello"),
|
||
other => panic!("expected String, got {other:?}"),
|
||
}
|
||
}
|
||
|
||
/// W3-3 regression: an aligned `maxLength` reservation holds raw
|
||
/// zero-padded data with NO length prefix (ADR-003 strategy 2).
|
||
/// `read_field` must read the raw reservation (NUL-trimmed) —
|
||
/// reading a length prefix there misparsed raw data as a huge
|
||
/// prefix and broke the validate_bytes ⇒ read_field agreement.
|
||
#[test]
|
||
fn read_field_aligned_max_length_reservation_reads_raw_not_prefixed() {
|
||
let doc = json!({
|
||
"$defs": {
|
||
"S": {
|
||
"kind": "struct",
|
||
"fields": [ { "name": "s", "kind": "string", "maxLength": 16 } ]
|
||
}
|
||
}
|
||
});
|
||
let engine = AlkTypeEngine::compile(&doc, "S", LayoutMode::Aligned, None).expect("compile");
|
||
let mut buf = vec![0u8; 32];
|
||
buf[0..8].copy_from_slice(b"hi there");
|
||
match engine.read_field(&buf, "s").unwrap() {
|
||
FieldValue::String(s) => assert_eq!(s, "hi there"),
|
||
other => panic!("expected String, got {other:?}"),
|
||
}
|
||
// Bytes that look like a huge length prefix when misread must
|
||
// never fool the read: 0xff bytes are raw data (invalid UTF-8
|
||
// only where they survive the NUL trim).
|
||
buf[0..2].copy_from_slice(&[0xFF, 0xFF]);
|
||
let v = engine.validate_bytes(&buf);
|
||
assert!(v.is_err());
|
||
match engine.read_field(&buf, "s") {
|
||
Err(AlkTypeError::Access { reason, .. }) => {
|
||
assert!(reason.contains("maxLength"), "reason: {reason}");
|
||
}
|
||
other => panic!("expected Access for invalid reservation UTF-8, got {other:?}"),
|
||
}
|
||
}
|
||
|
||
/// W3-3 write-side: `write_field` over an aligned maxLength
|
||
/// reservation fills the window with the value + zero padding, and
|
||
/// an oversized value is a clean Access error.
|
||
#[test]
|
||
fn write_field_aligned_max_length_reservation_zero_pads_and_rejects_oversize() {
|
||
let doc = json!({
|
||
"$defs": {
|
||
"S": {
|
||
"kind": "struct",
|
||
"fields": [ { "name": "s", "kind": "string", "maxLength": 8 } ]
|
||
}
|
||
}
|
||
});
|
||
let engine = AlkTypeEngine::compile(&doc, "S", LayoutMode::Aligned, None).expect("compile");
|
||
let mut buf = vec![0xA5u8; 8];
|
||
engine
|
||
.write_field(&mut buf, "s", &FieldValue::String("ab"))
|
||
.expect("an 8-byte reservation accepts 2 bytes");
|
||
assert_eq!(&buf[..2], b"ab");
|
||
assert!(buf[2..].iter().all(|&b| b == 0), "the rest of the window zero-pads");
|
||
let err = engine
|
||
.write_field(&mut buf, "s", &FieldValue::String("9bytes!!!"))
|
||
.expect_err("a 9-byte value overflows an 8-byte reservation");
|
||
assert!(matches!(err, AlkTypeError::Access { .. }));
|
||
}
|
||
|
||
/// W3-3 agreement: with the reservation read path fixed,
|
||
/// `validate_bytes` Ok implies `read_field` Ok on every leaf —
|
||
/// the exact lattice the wave-3 target asserts.
|
||
#[test]
|
||
fn aligned_max_length_validate_then_read_field_agree() {
|
||
let doc = json!({
|
||
"$defs": {
|
||
"S": {
|
||
"kind": "struct",
|
||
"fields": [
|
||
{ "name": "id", "kind": "uint32" },
|
||
{ "name": "s", "kind": "string", "maxLength": 12 }
|
||
]
|
||
}
|
||
}
|
||
});
|
||
let engine = AlkTypeEngine::compile(&doc, "S", LayoutMode::Aligned, None).expect("compile");
|
||
// Layout: id@0..4, s@4..16 (total 16). The reservation reads
|
||
// its window raw with trailing-NUL trim — a "length-prefix-
|
||
// looking" word inside the data must never be consulted. The
|
||
// window here starts with valid UTF-8 and ends in NULs; the
|
||
// 0xFFFF word sits mid-window as pure data in the middle of
|
||
// the s region is impossible (the window starts at 4), so
|
||
// keep the data clean and pin the boundary behavior: leading
|
||
// data + NUL padding reads verbatim.
|
||
let mut buf = vec![0u8; 16];
|
||
buf[0..4].copy_from_slice(&7u32.to_le_bytes());
|
||
buf[4..8].copy_from_slice(b"stri");
|
||
match engine.validate_bytes(&buf) {
|
||
Ok(()) => {}
|
||
Err(e) => panic!("validate_bytes must accept raw reservation data, got {e:?}"),
|
||
}
|
||
match engine.read_field(&buf, "s").unwrap() {
|
||
FieldValue::String(s) => assert_eq!(s, "stri"),
|
||
other => panic!("expected String, got {other:?}"),
|
||
}
|
||
// A misread as length-prefixed would have consumed the first
|
||
// four bytes s[0..4] = "stri" as a length word (0x69727473 =
|
||
// 1_770_632_051) and failed bounds — this exact buffer cannot
|
||
// read Ok through the old path, so this pin doubles as the
|
||
// regression.
|
||
}
|
||
|
||
#[test]
|
||
fn write_field_aligned_rejects_offset_indirect() {
|
||
let doc = json!({
|
||
"$defs": {
|
||
"S": {
|
||
"kind": "struct",
|
||
"fields": [
|
||
{ "name": "blob", "kind": "bytes", "encoding": "offset-indirect" }
|
||
]
|
||
}
|
||
}
|
||
});
|
||
let engine = AlkTypeEngine::compile(&doc, "S", LayoutMode::Aligned, None).expect("compile");
|
||
let mut buf = vec![0u8; 16];
|
||
let err = engine
|
||
.write_field(&mut buf, "blob", &FieldValue::Bytes(&[1, 2, 3]))
|
||
.unwrap_err();
|
||
assert!(matches!(err, AlkTypeError::Access { .. }), "got {err:?}");
|
||
}
|
||
|
||
// ----- validate_bytes tests (ADR-010) -----
|
||
//
|
||
// Note: step 4 wires the layout layer to BAST. The `validate_bytes`
|
||
// value-constraint enforcement (maxLength, enum bounds) is step 5's
|
||
// concern (the BAST-native validator). These tests cover the
|
||
// materialization + structural path.
|
||
|
||
fn chunk_header_doc() -> Value {
|
||
json!({
|
||
"$defs": {
|
||
"ChunkHeader": {
|
||
"kind": "struct",
|
||
"endian": "big",
|
||
"fields": [
|
||
{ "name": "channel_id", "kind": "uint32" },
|
||
{ "name": "length", "kind": "uint32" }
|
||
]
|
||
}
|
||
}
|
||
})
|
||
}
|
||
|
||
#[test]
|
||
fn validate_bytes_packed_accepts_valid_chunk_header() {
|
||
let doc = chunk_header_doc();
|
||
let engine = AlkTypeEngine::compile(&doc, "ChunkHeader", LayoutMode::Packed, None).expect("compile");
|
||
let buf = [0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 12u8];
|
||
assert!(engine.validate_bytes(&buf).is_ok(), "valid header should pass");
|
||
}
|
||
|
||
#[test]
|
||
fn validate_bytes_packed_rejects_short_buffer() {
|
||
let doc = chunk_header_doc();
|
||
let engine = AlkTypeEngine::compile(&doc, "ChunkHeader", LayoutMode::Packed, None).expect("compile");
|
||
let buf = [0u8; 4];
|
||
let err = engine.validate_bytes(&buf).unwrap_err();
|
||
assert!(matches!(err, AlkTypeError::Access { .. }), "got {err:?}");
|
||
}
|
||
|
||
#[test]
|
||
fn validate_bytes_aligned_accepts_valid_chunk_header() {
|
||
let doc = chunk_header_doc();
|
||
let engine = AlkTypeEngine::compile(&doc, "ChunkHeader", LayoutMode::Aligned, None).expect("compile");
|
||
let buf = [0u8, 0u8, 0u8, 42u8, 0u8, 0u8, 0u8, 7u8];
|
||
assert!(engine.validate_bytes(&buf).is_ok(), "valid header should pass");
|
||
}
|
||
|
||
#[test]
|
||
fn validate_bytes_aligned_rejects_short_buffer() {
|
||
let doc = chunk_header_doc();
|
||
let engine = AlkTypeEngine::compile(&doc, "ChunkHeader", LayoutMode::Aligned, None).expect("compile");
|
||
let buf = [0u8; 6];
|
||
let err = engine.validate_bytes(&buf).unwrap_err();
|
||
assert!(
|
||
matches!(err, AlkTypeError::Access { .. } | AlkTypeError::Offset { .. }),
|
||
"got {err:?}"
|
||
);
|
||
}
|
||
|
||
#[test]
|
||
fn validate_bytes_packed_materializes_struct_with_mixed_fields() {
|
||
let doc = json!({
|
||
"$defs": {
|
||
"S": {
|
||
"kind": "struct",
|
||
"fields": [
|
||
{ "name": "flag", "kind": "uint8" },
|
||
{ "name": "id", "kind": "uint32" },
|
||
{ "name": "name", "kind": "string" }
|
||
]
|
||
}
|
||
}
|
||
});
|
||
let engine = AlkTypeEngine::compile(&doc, "S", LayoutMode::Packed, None).expect("compile");
|
||
let mut buf = vec![0u8; 16];
|
||
buf[0] = 0xAB;
|
||
buf[1..5].copy_from_slice(&0x01020304u32.to_le_bytes());
|
||
buf[5..9].copy_from_slice(&5u32.to_le_bytes());
|
||
buf[9..14].copy_from_slice(b"hello");
|
||
assert!(engine.validate_bytes(&buf).is_ok(), "valid mixed struct should pass");
|
||
}
|
||
|
||
#[test]
|
||
fn c1_validate_bytes_packed_decodes_all_twelve_primitives_le() {
|
||
// The plan materializer's i16/i32/i64/u64/f64/bool arms had
|
||
// zero public-path executions before this battery (review #007
|
||
// C1): every packed validate_bytes test fed u8/u32/string
|
||
// shapes.
|
||
let doc = json!({
|
||
"$defs": {
|
||
"S": {
|
||
"kind": "struct",
|
||
"endian": "little",
|
||
"fields": [
|
||
{ "name": "i8", "kind": "int8" },
|
||
{ "name": "i16", "kind": "int16" },
|
||
{ "name": "i32", "kind": "int32" },
|
||
{ "name": "i64", "kind": "int64" },
|
||
{ "name": "u8", "kind": "uint8" },
|
||
{ "name": "u16", "kind": "uint16" },
|
||
{ "name": "u32", "kind": "uint32" },
|
||
{ "name": "u64", "kind": "uint64" },
|
||
{ "name": "f32", "kind": "float32" },
|
||
{ "name": "f64", "kind": "float64" },
|
||
{ "name": "b", "kind": "bool" }
|
||
]
|
||
}
|
||
}
|
||
});
|
||
let engine = AlkTypeEngine::compile(&doc, "S", LayoutMode::Packed, None).expect("compile");
|
||
let mut buf = vec![0u8; 1 + 2 + 4 + 8 + 1 + 2 + 4 + 8 + 4 + 8 + 1];
|
||
let mut off = 0;
|
||
buf[off] = 0x81; off += 1; // i8 = -127
|
||
buf[off..off + 2].copy_from_slice(&(-32000i16).to_le_bytes()); off += 2;
|
||
buf[off..off + 4].copy_from_slice(&(-2_000_000_007i32).to_le_bytes()); off += 4;
|
||
buf[off..off + 8].copy_from_slice(&(-9_000_000_000_000_000_000i64).to_le_bytes()); off += 8;
|
||
buf[off] = 0xAB; off += 1; // u8
|
||
buf[off..off + 2].copy_from_slice(&0xBEEFu16.to_le_bytes()); off += 2;
|
||
buf[off..off + 4].copy_from_slice(&0xDEADBEEFu32.to_le_bytes()); off += 4;
|
||
buf[off..off + 8].copy_from_slice(&0x0102030405060708u64.to_le_bytes()); off += 8;
|
||
buf[off..off + 4].copy_from_slice(&1.5f32.to_le_bytes()); off += 4;
|
||
buf[off..off + 8].copy_from_slice(&2.5f64.to_le_bytes()); off += 8;
|
||
buf[off] = 1; off += 1; // bool
|
||
assert_eq!(off, buf.len());
|
||
assert!(
|
||
engine.validate_bytes(&buf).is_ok(),
|
||
"all-twelve-primitive battery must validate"
|
||
);
|
||
|
||
// Corrupted-value rejection: bool 0x40 is not 0/1.
|
||
let mut bad = buf.clone();
|
||
bad[off - 1] = 0x40;
|
||
let err = engine.validate_bytes(&bad).unwrap_err();
|
||
assert!(matches!(err, AlkTypeError::Access { .. }), "got {err:?}");
|
||
}
|
||
|
||
#[test]
|
||
fn c1_validate_bytes_packed_decodes_big_endian_subset() {
|
||
// The BE leg: packed validate_bytes had only ever decoded BE
|
||
// u32s (the chunk-header fixture) before this test.
|
||
let doc = json!({
|
||
"$defs": {
|
||
"S": {
|
||
"kind": "struct",
|
||
"endian": "big",
|
||
"fields": [
|
||
{ "name": "i16", "kind": "int16" },
|
||
{ "name": "u64", "kind": "uint64" },
|
||
{ "name": "f64", "kind": "float64" }
|
||
]
|
||
}
|
||
}
|
||
});
|
||
let engine = AlkTypeEngine::compile(&doc, "S", LayoutMode::Packed, None).expect("compile");
|
||
let mut buf = vec![0u8; 18];
|
||
buf[0..2].copy_from_slice(&(-32000i16).to_be_bytes());
|
||
buf[2..10].copy_from_slice(&0x0102030405060708u64.to_be_bytes());
|
||
buf[10..18].copy_from_slice(&2.5f64.to_be_bytes());
|
||
assert!(engine.validate_bytes(&buf).is_ok(), "BE battery must validate");
|
||
}
|
||
|
||
#[test]
|
||
fn validate_bytes_with_simple_struct_round_trips() {
|
||
let doc = json!({
|
||
"$defs": {
|
||
"S": {
|
||
"kind": "struct",
|
||
"endian": "little",
|
||
"fields": [
|
||
{ "name": "channel_id", "kind": "uint32" },
|
||
{ "name": "length", "kind": "uint32" }
|
||
]
|
||
}
|
||
}
|
||
});
|
||
let engine = AlkTypeEngine::compile(&doc, "S", LayoutMode::Packed, None).expect("compile");
|
||
let mut buf = vec![0u8; 8];
|
||
buf[0..4].copy_from_slice(&42u32.to_le_bytes());
|
||
buf[4..8].copy_from_slice(&7u32.to_le_bytes());
|
||
assert!(engine.validate_bytes(&buf).is_ok());
|
||
}
|
||
|
||
#[test]
|
||
fn validate_bytes_aligned_record_last_field_round_trips() {
|
||
// M2: the aligned record path (OffsetMap LengthPrefixed entry at
|
||
// the prefix offset + materialize_typeref_packed dispatch) had
|
||
// zero public-path coverage. Record-as-last-field is the only
|
||
// safe inline position (ADR-006, M1).
|
||
let doc = json!({
|
||
"$defs": {
|
||
"S": {
|
||
"kind": "struct",
|
||
"endian": "little",
|
||
"fields": [
|
||
{ "name": "id", "kind": "uint32" },
|
||
{ "name": "counts", "kind": { "kind": "record", "values": "uint32" } }
|
||
]
|
||
}
|
||
}
|
||
});
|
||
let engine = AlkTypeEngine::compile(&doc, "S", LayoutMode::Aligned, None).expect("compile");
|
||
// Offsets: id @ 0 (4 bytes), counts prefix @ 4 (4 bytes).
|
||
// Wire: 4 (id) + 4 (record count) + [4 (key len) + 1 (key) + 4
|
||
// (value)] × 2 = 26 bytes.
|
||
let mut buf = vec![0u8; 26];
|
||
buf[0..4].copy_from_slice(&7u32.to_le_bytes());
|
||
let mut off = 4;
|
||
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());
|
||
assert!(engine.validate_bytes(&buf).is_ok());
|
||
// Corrupting bytes inside the first entry (key byte + value
|
||
// bytes) produces garbage the value-domain check must reject.
|
||
let mut corrupt = buf.clone();
|
||
corrupt[12] = 0xFF;
|
||
corrupt[13] = 0xFF;
|
||
corrupt[14] = 0xFF;
|
||
corrupt[15] = 0xFF;
|
||
assert!(engine.validate_bytes(&corrupt).is_err());
|
||
}
|
||
|
||
#[test]
|
||
fn c2_validate_bytes_aligned_inline_string_and_bytes_default_encoding() {
|
||
// The aligned validate_bytes tests covered maxLength
|
||
// reservations, offset-indirect, records, and unions — but the
|
||
// *default* inline length-prefixed encoding (the most common
|
||
// real shape) had zero public-path executions
|
||
// (materialize_variable_aligned's LengthPrefixed-else branch,
|
||
// review #007 C2). ADR-006 allows inline variable fields only
|
||
// in the last position, so each shape gets its own schema.
|
||
let string_doc = json!({
|
||
"$defs": {
|
||
"S": {
|
||
"kind": "struct",
|
||
"endian": "little",
|
||
"fields": [
|
||
{ "name": "id", "kind": "uint32" },
|
||
{ "name": "name", "kind": "string" }
|
||
]
|
||
}
|
||
}
|
||
});
|
||
let engine =
|
||
AlkTypeEngine::compile(&string_doc, "S", LayoutMode::Aligned, None).expect("compile");
|
||
// Offsets: id @ 0 (4), name prefix @ 4 (4), name data @ 8 (5).
|
||
let mut buf = vec![0u8; 13];
|
||
buf[0..4].copy_from_slice(&42u32.to_le_bytes());
|
||
buf[4..8].copy_from_slice(&5u32.to_le_bytes());
|
||
buf[8..13].copy_from_slice(b"hello");
|
||
assert!(
|
||
engine.validate_bytes(&buf).is_ok(),
|
||
"aligned inline string must validate through the default encoding"
|
||
);
|
||
// Short buffer: name's declared data runs past the buffer end.
|
||
let err = engine.validate_bytes(&buf[..10]).unwrap_err();
|
||
assert!(matches!(err, AlkTypeError::Access { .. }), "got {err:?}");
|
||
|
||
let bytes_doc = json!({
|
||
"$defs": {
|
||
"S": {
|
||
"kind": "struct",
|
||
"endian": "little",
|
||
"fields": [
|
||
{ "name": "blob", "kind": "bytes" }
|
||
]
|
||
}
|
||
}
|
||
});
|
||
let engine =
|
||
AlkTypeEngine::compile(&bytes_doc, "S", LayoutMode::Aligned, None).expect("compile");
|
||
let mut bbuf = vec![0u8; 7];
|
||
bbuf[0..4].copy_from_slice(&3u32.to_le_bytes());
|
||
bbuf[4..7].copy_from_slice(&[0xAA, 0xBB, 0xCC]);
|
||
assert!(
|
||
engine.validate_bytes(&bbuf).is_ok(),
|
||
"aligned inline bytes must validate through the default encoding"
|
||
);
|
||
}
|
||
|
||
// ----- ValidationPlan / validate_bytes (ADR-012 §3, phase 7) ----------
|
||
|
||
#[test]
|
||
fn validation_plan_accessor_returns_compiled_plan() {
|
||
let doc = uint32_struct_bast();
|
||
let engine =
|
||
AlkTypeEngine::compile(&doc, "S", LayoutMode::Packed, None).expect("compile");
|
||
let plan = engine.validation_plan();
|
||
assert!(plan.validate(&json!({"id": 42})).is_ok());
|
||
assert!(plan.validate(&json!({"id": -1})).is_err());
|
||
// Same schema compiled twice produces equal plans (fingerprint
|
||
// contract), so the accessor reflects the compile-time build.
|
||
let engine2 =
|
||
AlkTypeEngine::compile(&doc, "S", LayoutMode::Packed, None).expect("compile");
|
||
assert_eq!(plan, engine2.validation_plan());
|
||
assert_eq!(plan.fingerprint(), engine2.validation_plan().fingerprint());
|
||
}
|
||
|
||
#[test]
|
||
fn validate_bytes_enforces_value_domain_via_plan_in_both_modes() {
|
||
let doc = json!({
|
||
"$defs": { "S": { "kind": "struct", "endian": "little", "fields": [
|
||
{ "name": "status", "kind": { "$ref": "#/$defs/Status" } }
|
||
] },
|
||
"Status": { "kind": "enum", "values": ["Ok", "Err"] } }
|
||
});
|
||
// Packed: enum index 5 is out of bounds for the 2-value enum.
|
||
let engine = AlkTypeEngine::compile(&doc, "S", LayoutMode::Packed, None).expect("compile");
|
||
let err = engine.validate_bytes(&5u32.to_le_bytes()).unwrap_err();
|
||
assert!(matches!(err, AlkTypeError::Validation(_)), "got {err:?}");
|
||
// Aligned: same constraint, offset read path (the enum leaf sits
|
||
// at offset 0 — no alignment padding for a lone u32-width leaf).
|
||
let engine =
|
||
AlkTypeEngine::compile(&doc, "S", LayoutMode::Aligned, None).expect("compile");
|
||
let mut buf = [0u8; 8];
|
||
buf[0..4].copy_from_slice(&5u32.to_le_bytes());
|
||
let err = engine.validate_bytes(&buf).unwrap_err();
|
||
assert!(matches!(err, AlkTypeError::Validation(_)), "got {err:?}");
|
||
// Valid index passes in both modes.
|
||
let engine = AlkTypeEngine::compile(&doc, "S", LayoutMode::Packed, None).expect("compile");
|
||
assert!(engine.validate_bytes(&1u32.to_le_bytes()).is_ok());
|
||
let engine =
|
||
AlkTypeEngine::compile(&doc, "S", LayoutMode::Aligned, None).expect("compile");
|
||
let mut buf = [0u8; 8];
|
||
buf[0..4].copy_from_slice(&1u32.to_le_bytes());
|
||
assert!(engine.validate_bytes(&buf).is_ok());
|
||
}
|
||
|
||
#[test]
|
||
fn compile_rejects_cyclic_ref_graph_with_schema_error() {
|
||
let doc = json!({
|
||
"$defs": {
|
||
"A": { "kind": "struct", "fields": [
|
||
{ "name": "next", "kind": { "$ref": "#/$defs/B" } }
|
||
] },
|
||
"B": { "kind": "struct", "fields": [
|
||
{ "name": "back", "kind": { "$ref": "#/$defs/A" } }
|
||
] }
|
||
}
|
||
});
|
||
let err = AlkTypeEngine::compile(&doc, "A", LayoutMode::Packed, None).unwrap_err();
|
||
assert!(matches!(err, AlkTypeError::Schema(_)), "got {err:?}");
|
||
assert!(err.to_string().contains("cyclic"), "got {err:?}");
|
||
let err = AlkTypeEngine::compile(&doc, "A", LayoutMode::Aligned, None).unwrap_err();
|
||
assert!(matches!(err, AlkTypeError::Schema(_)), "got {err:?}");
|
||
}
|
||
|
||
#[test]
|
||
fn validation_plan_is_send_sync_shared() {
|
||
let doc = uint32_struct_bast();
|
||
let engine =
|
||
AlkTypeEngine::compile(&doc, "S", LayoutMode::Packed, None).expect("compile");
|
||
fn assert_send_sync<T: Send + Sync>(_: &T) {}
|
||
assert_send_sync(engine.validation_plan());
|
||
// The engine itself must stay Send + Sync now that it holds the
|
||
// owned BastDoc (ADR-012 §2a) — alkcall shares engines across
|
||
// hub/spoke threads.
|
||
fn assert_engine_send_sync<T: Send + Sync>(_: &T) {}
|
||
assert_engine_send_sync(&engine);
|
||
let shared:std::sync::Arc<_> = engine.validation_plan().clone();
|
||
let handle = std::thread::spawn(move || shared.validate(&json!({"id": 1})).is_ok());
|
||
assert!(handle.join().expect("join"));
|
||
}
|
||
|
||
// ----- validate_json / is_valid_json tests (step 6, D-BAST-007) -----
|
||
|
||
fn uint32_struct_bast() -> Value {
|
||
json!({
|
||
"$defs": {
|
||
"S": {
|
||
"kind": "struct",
|
||
"fields": [ { "name": "id", "kind": "uint32" } ]
|
||
}
|
||
}
|
||
})
|
||
}
|
||
|
||
fn id_json_schema() -> Value {
|
||
json!({
|
||
"type": "object",
|
||
"properties": {
|
||
"id": { "type": "integer", "minimum": 0, "maximum": 4294967295u64 }
|
||
},
|
||
"required": ["id"]
|
||
})
|
||
}
|
||
|
||
#[test]
|
||
fn validate_json_accepts_valid_instance() {
|
||
let bast = uint32_struct_bast();
|
||
let schema = id_json_schema();
|
||
let engine =
|
||
AlkTypeEngine::compile(&bast, "S", LayoutMode::Packed, Some(&schema)).expect("compile");
|
||
assert!(engine.validate_json(&json!({"id": 42})).is_ok());
|
||
assert!(engine.is_valid_json(&json!({"id": 42})));
|
||
}
|
||
|
||
#[test]
|
||
fn validate_json_rejects_invalid_instance() {
|
||
let bast = uint32_struct_bast();
|
||
let schema = id_json_schema();
|
||
let engine =
|
||
AlkTypeEngine::compile(&bast, "S", LayoutMode::Packed, Some(&schema)).expect("compile");
|
||
let err = engine.validate_json(&json!({"id": -1})).unwrap_err();
|
||
assert!(matches!(err, AlkTypeError::Validation(_)), "got {err:?}");
|
||
assert!(!engine.is_valid_json(&json!({"id": -1})));
|
||
assert!(!engine.is_valid_json(&json!({"id": "x"})));
|
||
assert!(!engine.is_valid_json(&json!({})));
|
||
}
|
||
|
||
#[test]
|
||
fn validate_json_returns_schema_error_when_no_json_schema_supplied() {
|
||
let bast = uint32_struct_bast();
|
||
let engine = AlkTypeEngine::compile(&bast, "S", LayoutMode::Packed, None).expect("compile");
|
||
let err = engine.validate_json(&json!({"id": 42})).unwrap_err();
|
||
assert!(matches!(err, AlkTypeError::Schema(_)), "got {err:?}");
|
||
}
|
||
|
||
#[test]
|
||
fn is_valid_json_returns_false_when_no_json_schema_supplied() {
|
||
let bast = uint32_struct_bast();
|
||
let engine = AlkTypeEngine::compile(&bast, "S", LayoutMode::Packed, None).expect("compile");
|
||
assert!(!engine.is_valid_json(&json!({"id": 42})));
|
||
}
|
||
|
||
#[test]
|
||
fn validate_json_independent_of_bast_document() {
|
||
let bast = uint32_struct_bast();
|
||
let schema = json!({
|
||
"type": "object",
|
||
"properties": {
|
||
"name": { "type": "string", "maxLength": 3 }
|
||
},
|
||
"required": ["name"]
|
||
});
|
||
let engine =
|
||
AlkTypeEngine::compile(&bast, "S", LayoutMode::Aligned, Some(&schema)).expect("compile");
|
||
assert!(engine.validate_json(&json!({"name": "hi"})).is_ok());
|
||
assert!(engine.validate_json(&json!({"name": "toolong"})).is_err());
|
||
assert!(!engine.is_valid_json(&json!({"name": "toolong"})));
|
||
}
|
||
|
||
#[test]
|
||
fn compile_with_json_schema_returns_schema_error_for_malformed_json_schema() {
|
||
let bast = uint32_struct_bast();
|
||
let bad_schema = json!({"type": "not-a-real-type"});
|
||
let err = AlkTypeEngine::compile(&bast, "S", LayoutMode::Packed, Some(&bad_schema))
|
||
.unwrap_err();
|
||
assert!(matches!(err, AlkTypeError::Schema(_)), "got {err:?}");
|
||
}
|
||
|
||
#[test]
|
||
fn validate_json_validates_nested_object_json_schema() {
|
||
let bast = uint32_struct_bast();
|
||
let schema = json!({
|
||
"type": "object",
|
||
"properties": {
|
||
"packet": {
|
||
"type": "object",
|
||
"properties": {
|
||
"kind": { "type": "string", "enum": ["a", "b"] }
|
||
},
|
||
"required": ["kind"]
|
||
}
|
||
},
|
||
"required": ["packet"]
|
||
});
|
||
let engine =
|
||
AlkTypeEngine::compile(&bast, "S", LayoutMode::Packed, Some(&schema)).expect("compile");
|
||
assert!(engine.validate_json(&json!({"packet": {"kind": "a"}})).is_ok());
|
||
assert!(engine.validate_json(&json!({"packet": {"kind": "c"}})).is_err());
|
||
assert!(!engine.is_valid_json(&json!({"packet": {"kind": "c"}})));
|
||
}
|
||
} |