Files
alktype/src/engine.rs
T
deepseek-v4-pro 230345a867 Validate BAST documents against the meta-schema at compile time
Resolves review #003 finding M3.

- Add bast_meta::validate_bast_doc and call it from AlkTypeEngine::compile
  before parsing. Malformed annotations (unknown endian/encoding strings,
  non-integer align/maxLength, missing required properties) now surface as
  AlkTypeError::Schema instead of being silently tolerated by the parser.
- Align the meta-schema TypeRef with the parser: allow inline struct/union/
  enum as TypeRefs (the parser and builder already accepted them; the
  meta-schema and spec did not). Update bast-format.md TypeRef table and
  the BastType doc comment to the seven-form vocabulary.

Verification: cargo test --release (410 pass), cargo clippy --all-targets
-- -D warnings (clean), cargo doc --no-deps (clean), cargo build --target
wasm32-unknown-unknown --release (clean).
2026-08-16 08:54:57 +00:00

1425 lines
54 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, BastStruct, BastType};
use crate::bast_validation;
use crate::data_access;
use crate::error::AlkTypeError;
use crate::layout_builder::LayoutBuilder;
use crate::materialize;
use crate::offset_map::OffsetMap;
use crate::schema::{AlkTypeKind, Endian, VariableEncoding};
use crate::sequential_reader::{FieldValue, SequentialReader};
use crate::validation;
use serde_json::Value;
use std::fmt;
/// 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 is a fresh [`SequentialReader`] constructed on demand
/// (ADR-101 — the reader has mutable cursor state that the consumer
/// owns, so the engine is a factory, not a holder).
Packed {
builder: LayoutBuilder,
},
/// 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
/// ([`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>,
endian: Endian,
bast_doc: Value,
root_name: String,
}
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 a clone of the BAST `Value` so that
/// [`AlkTypeEngine::sequential_reader`] and
/// [`AlkTypeEngine::read_field`] can re-parse the typed tree on
/// demand without lifetime entanglement with the caller's `Value`.
///
/// # 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();
let layout = match mode {
LayoutMode::Packed => {
let builder = LayoutBuilder::new(bast_doc, root_name)?;
Layout::Packed { builder }
}
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,
endian,
bast_doc: bast_doc.clone(),
root_name: root_name.to_string(),
})
}
/// The schema's endianness.
pub fn endian(&self) -> Endian {
self.endian
}
/// 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-101). Each call returns a new reader with the cursor at
/// position 0. 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 { .. } => {
SequentialReader::new(&self.bast_doc, &self.root_name).ok()
}
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 BAST-native validator
/// — a recursive walker over the BAST type tree that checks the
/// value-domain constraints the materializer does not (integer
/// ranges, `maxLength`, timestamp shape, enum index bounds, union
/// variant constraints). Decided in D-BAST-006; see
/// [bast-format.md §Validation Model](../../docs/architecture/bast-format.md#validation-model).
///
/// 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
/// BAST-native validator 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 doc = BastDoc::new(&self.bast_doc, &self.root_name)?;
let value = match &self.layout {
Layout::Packed { .. } => materialize::materialize_packed(&doc, buffer)?,
Layout::Aligned { offset_map } => {
materialize::materialize_aligned(&doc, buffer, offset_map)?
}
};
bast_validation::validate_value(&doc, &value)
}
/// Read a field from a buffer at its computed offset (aligned mode).
///
/// Looks up the field's byte range in the [`OffsetMap`] and reads the
/// appropriate type using the [`crate::data_access`] functions. Works
/// for fixed-size primitive kinds and length-prefixed `String`/
/// `Bytes`/`Timestamp` fields.
///
/// Returns an error if compiled in packed mode — use
/// [`AlkTypeEngine::sequential_reader`] for packed mode. Also
/// returns an error for composite kinds (`Struct`, `Union`, `Array`,
/// `Record`) — those 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.
/// - [`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 range = 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 doc = BastDoc::new(&self.bast_doc, &self.root_name)?;
let leaf = lookup_leaf_field(&doc, field_path).ok_or_else(|| AlkTypeError::Offset {
field_path: field_path.to_string(),
reason: "field schema not found in BAST tree or has no primitive kind".to_string(),
})?;
let kind = leaf.kind;
let endian = leaf.endian;
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 leaf.encoding {
VariableEncoding::OffsetIndirect => {
data_access::read_string_indirect(buffer, range.start, field_path, endian)?
}
VariableEncoding::LengthPrefixed => {
data_access::read_string(buffer, range.start, field_path, endian)?
}
};
Ok(FieldValue::String(v))
}
AlkTypeKind::Bytes => {
let v = match leaf.encoding {
VariableEncoding::OffsetIndirect => {
data_access::read_bytes_indirect(buffer, range.start, field_path, endian)?
}
VariableEncoding::LengthPrefixed => {
data_access::read_bytes(buffer, range.start, field_path, endian)?
}
};
Ok(FieldValue::Bytes(v))
}
AlkTypeKind::Timestamp => {
let v = data_access::read_string(buffer, range.start, field_path, endian)?;
Ok(FieldValue::String(v))
}
AlkTypeKind::Struct => Ok(FieldValue::Struct {
start: range.start,
end: range.end,
}),
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 byte range in the [`OffsetMap`] and writes the
/// appropriate type using the [`crate::data_access`] functions. Works
/// for fixed-size primitive kinds and length-prefixed `String`/
/// `Bytes`/`Timestamp` 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 range = 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 doc = BastDoc::new(&self.bast_doc, &self.root_name)?;
let leaf = lookup_leaf_field(&doc, field_path).ok_or_else(|| AlkTypeError::Offset {
field_path: field_path.to_string(),
reason: "field schema not found in BAST tree or has no primitive kind".to_string(),
})?;
let endian = leaf.endian;
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) => {
if leaf.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(),
});
}
data_access::write_string(buffer, range.start, v, field_path, endian)?;
Ok(())
}
FieldValue::Bytes(v) => {
if leaf.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(),
});
}
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()
}
}
/// The resolved leaf-field metadata needed by `read_field`/`write_field`:
/// the field's kind, its variable-length encoding, and its effective
/// endianness (field override, else the enclosing struct's default).
struct LeafFieldInfo {
kind: AlkTypeKind,
encoding: VariableEncoding,
endian: Endian,
}
/// Walk the BAST typed tree to find the leaf field for a dotted field
/// path. Returns `None` if any segment is missing or the resolved type is
/// a `$ref` that can't be resolved.
///
/// Splits `field_path` on `.` and descends into the root struct's
/// `fields` at each step, resolving `$ref`s via [`BastDoc::resolve_typeref`].
/// Array element segments (`field[i]`) are not handled here —
/// `read_field`/`write_field` only address leaf fields.
fn lookup_leaf_field(doc: &BastDoc<'_>, field_path: &str) -> Option<LeafFieldInfo> {
let root_def = doc.root_def();
let mut current_struct: BastStruct<'_> = match root_def.kind() {
BastDefKind::Struct(s) => s.clone(),
_ => return None,
};
let mut struct_endian = current_struct.endian();
let segments: Vec<&str> = field_path.split('.').collect();
let last = segments.len();
for (i, segment) in segments.iter().enumerate() {
let field = current_struct.fields().iter().find(|f| f.name() == *segment)?;
let ty = field.ty();
let resolved = doc.resolve_typeref(ty).ok()?;
let field_endian = field.effective_endian(struct_endian);
if i + 1 == last {
return Some(LeafFieldInfo {
kind: resolved.alk_kind(),
encoding: field.encoding(),
endian: field_endian,
});
}
match &resolved {
BastType::Struct(s) => {
current_struct = s.clone();
struct_endian = s.endian();
}
_ => return None,
}
}
None
}
#[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_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 lookup_leaf_field_walks_dotted_path() {
let doc = json!({
"$defs": {
"S": {
"kind": "struct",
"fields": [
{
"name": "header",
"kind": {
"kind": "struct",
"fields": [
{ "name": "magic", "kind": "uint32" },
{ "name": "version", "kind": "uint8" }
]
}
}
]
}
}
});
let d = BastDoc::new(&doc, "S").expect("doc");
assert_eq!(lookup_leaf_field(&d, "header.magic").unwrap().kind, AlkTypeKind::Uint32);
assert_eq!(lookup_leaf_field(&d, "header.version").unwrap().kind, AlkTypeKind::Uint8);
assert!(lookup_leaf_field(&d, "header.missing").is_none());
assert!(lookup_leaf_field(&d, "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_reads_timestamp_as_string() {
let doc = json!({
"$defs": {
"S": {
"kind": "struct",
"fields": [ { "name": "ts", "kind": "timestamp" } ]
}
}
});
let engine = AlkTypeEngine::compile(&doc, "S", LayoutMode::Aligned, None).expect("compile");
let mut buf = vec![0u8; 32];
let stamp = "2026-07-20T15:30:00Z";
let len_bytes = (stamp.len() as u32).to_le_bytes();
buf[0..4].copy_from_slice(&len_bytes);
buf[4..4 + stamp.len()].copy_from_slice(stamp.as_bytes());
match engine.read_field(&buf, "ts").unwrap() {
FieldValue::String(s) => assert_eq!(s, stamp),
other => panic!("expected String, 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:?}"),
}
}
#[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 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());
}
// ----- 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"}})));
}
}