Files
alktype/src/engine.rs
T
glm-5.3-flash a0dd3d2de4 fix: W3-3 — read_field/write_field misread aligned maxLength reservations
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.
2026-09-30 08:20:13 +00:00

1825 lines
73 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! `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"}})));
}
}