Wire packed read path through ReadPlan (ADR-011 steps 2-4, plan phase 2)
SequentialReader now walks Arc<ReadPlan> instead of re-parsing the BAST typed tree per field (the 400x read-path gap, review #004 H1); materialize_packed walks the same plan, unifying the two packed read-side consumers on one compiled form. - SequentialReader::new(Arc<ReadPlan>) -> Self, infallible: the fallible BastDoc parse moved to ReadPlan::compile (phase 1). The reader holds the plan Arc + cursor state only; schema() returns the Arc<Value> retained on the plan (review #005 H2 — no self-referential struct); new plan() accessor exposes the shared plan. - ReadPlan carries schema: Arc<Value> (set at compile; sub-plans hold a Null placeholder — only the root plan is handed out). - materialize_packed(&ReadPlan, &[u8]): plan-walking packed materializer. The aligned path keeps walking BastDoc with the retained dummy_field_for/ty_source/materialize_typeref_packed helpers (phase 5 Scope Boundary: aligned structure walk is the permanent 0.3.0 design). - Engine: Layout::Packed stores Arc<ReadPlan> alongside the builder; sequential_reader() is an Arc::clone (was a full-document Value clone); packed validate_bytes calls materialize_packed(&self.plan). - Stride (deferred decision 4): FieldValue::Array now reports the true stride for fixed-size struct/nested-array elements (0.2.0 returned 0); doc comment documents the behavioral change; no existing test asserted the 0, so none needed changing. - Two parity subtleties found and preserved: (a) materialize_plan_composite unwraps the plan's anonymous single-field wrapper for primitive array elements/record values — without it, materialized records nest each leaf under a synthetic object (caught by the record parity test); (b) field-disc unions keep 0.2.0's materialized key order (__discriminator first), observable under preserve_order. Both are now covered by plan-phase tests or construction. Bench (alktty wire_vs_bast, 1024 chunks/stream): packed read 2.27 us/chunk (review #004) -> 98 ns/chunk p64 / 100 ns/chunk p4k (~23x; the 400x gap closes to ~17x vs hand-rolled 5.6 ns/chunk). Residual gap is the per-field String allocation mandated by the unchanged (String, FieldValue) read_next signature (2 allocs/chunk) plus data_access bounds checks. sequential_reader() construction: 15.7 ns (was a whole-document clone). Verification: 465 tests pass unchanged (the existing reader/ materialize/engine suites drive the rewrite through the public API — only constructor call sites moved to ReadPlan::compile); clippy -D warnings clean; cargo doc zero warnings; wasm32 release build green.
This commit is contained in:
1 parent
ff85258d03
commit
e5f1b9d825
8 files changed
+1101
-644
No files matched your search
@@ -214,7 +214,51 @@ Sync` so a future change can't break it silently — mirror the POC's
|
||||
|
||||
---
|
||||
|
||||
## Phase 2 — `SequentialReader` + `materialize_packed` consume `ReadPlan` (ADR-011 steps 2–4)
|
||||
## Phase 2 — `SequentialReader` + `materialize_packed` consume `ReadPlan` (ADR-011 steps 2–4) — **DONE (2026-09-02)**
|
||||
|
||||
> **Status: implemented.** The packed read loop walks `Arc<ReadPlan>`:
|
||||
> `SequentialReader::new(Arc<ReadPlan>) -> Self` (infallible; the old
|
||||
> fallible constructor's work moved to `ReadPlan::compile`), the reader
|
||||
> holds `plan: Arc<ReadPlan>` + cursor only, `schema()` returns the
|
||||
> `Arc<Value>` retained on the plan (review #005 H2 closed — no
|
||||
> self-referential struct), and a new `plan()` accessor exposes the
|
||||
> shared plan. `materialize_packed(&ReadPlan, &[u8])` walks the same
|
||||
> plan; the aligned materialize path keeps walking `BastDoc` with the
|
||||
> retained `dummy_field_for`/`ty_source`/`materialize_typeref_packed`
|
||||
> helpers (phase 5 Scope Boundary). Engine: `Layout::Packed` carries
|
||||
> `Arc<ReadPlan>`; `sequential_reader()` is an `Arc::clone` (15.7 ns,
|
||||
> was a whole-document `Value` clone); packed `validate_bytes` calls
|
||||
> `materialize_packed(&self.plan, ...)`. The temporary validation
|
||||
> bridge (reconstruct `BastDoc` for the validator) is still in place —
|
||||
> phase 7 already retired it on `main`'s ValidationPlan; this phase's
|
||||
> `validate_bytes` edit merged cleanly onto that state.
|
||||
>
|
||||
> **Stride (deferred decision 4):** fixed struct/nested-array elements
|
||||
> now report their true stride through `FieldValue::Array`
|
||||
> (0.2.0 returned `0`); doc comment updated; no existing test asserted
|
||||
> the `0`, so no test needed changing — the plan-compile tests lock the
|
||||
> new values.
|
||||
>
|
||||
> **Two parity subtleties found and preserved** (both invisible to the
|
||||
> existing test suite, both now locked by tests or by construction):
|
||||
> (a) the materializer unwraps the plan's anonymous single-field
|
||||
> wrapper for primitive array elements/record values — without this,
|
||||
> materialized records/arrays would nest each leaf under a synthetic
|
||||
> object and `validate_bytes` would fail its own parity suite (caught
|
||||
> by `materialize_record_packed_count_prefixed_pairs`); (b) the
|
||||
> field-disc union's materialized key order keeps `__discriminator`
|
||||
> first (matching 0.2.0's `Map` insertion order, observable under
|
||||
> `preserve_order`).
|
||||
>
|
||||
> **Bench (alktty `wire_vs_bast`, 1024 chunks/stream):** read p64
|
||||
> 2.27 µs/chunk (review #004) → **98 ns/chunk** (~23x; gap 400x →
|
||||
> ~17x vs hand-rolled's 5.6 ns); read p4k → 100 ns/chunk. `engine.
|
||||
> sequential_reader()` construction 15.7 ns (was a full `Value` clone).
|
||||
> The residual gap is dominated by the per-field `String` allocation
|
||||
> mandated by the unchanged `(String, FieldValue)` `read_next` return
|
||||
> signature (2 allocs/chunk) plus `data_access` bounds checks — both
|
||||
> outside this phase's scope (the signature is pinned by the Semver
|
||||
> Contract).
|
||||
|
||||
**Goal:** Rewrite the packed read loop to walk `&ReadPlan` instead of
|
||||
reconstructing `BastDoc`. `SequentialReader` stores `Arc<ReadPlan>` +
|
||||
|
||||
+21
-18
@@ -82,6 +82,7 @@ pub(crate) fn validation_err(path: &str, reason: impl Into<String>) -> AlkTypeEr
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::materialize::materialize_packed;
|
||||
use crate::read_plan::ReadPlan;
|
||||
use serde_json::json;
|
||||
|
||||
fn doc_from<'a>(root: &'a Value, name: &'a str) -> BastDoc<'a> {
|
||||
@@ -99,10 +100,12 @@ mod tests {
|
||||
}
|
||||
|
||||
fn materialize_and_validate(
|
||||
root: &Value,
|
||||
doc: &BastDoc<'_>,
|
||||
buffer: &[u8],
|
||||
) -> Result<(), AlkTypeError> {
|
||||
let value = materialize_packed(doc, buffer)?;
|
||||
let plan = ReadPlan::compile(root, doc.root_name())?;
|
||||
let value = materialize_packed(&plan, buffer)?;
|
||||
validate_value(doc, &value)
|
||||
}
|
||||
|
||||
@@ -121,8 +124,8 @@ mod tests {
|
||||
] } }
|
||||
});
|
||||
let d = doc_from(&root, "S");
|
||||
assert!(materialize_and_validate(&d, &u32_le(0)).is_ok());
|
||||
assert!(materialize_and_validate(&d, &u32_le(0xFFFF_FFFF)).is_ok());
|
||||
assert!(materialize_and_validate(&root, &d, &u32_le(0)).is_ok());
|
||||
assert!(materialize_and_validate(&root, &d, &u32_le(0xFFFF_FFFF)).is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -133,8 +136,8 @@ mod tests {
|
||||
] } }
|
||||
});
|
||||
let d = doc_from(&root, "S");
|
||||
assert!(materialize_and_validate(&d, &[127u8]).is_ok());
|
||||
assert!(materialize_and_validate(&d, &[128u8]).is_ok());
|
||||
assert!(materialize_and_validate(&root, &d, &[127u8]).is_ok());
|
||||
assert!(materialize_and_validate(&root, &d, &[128u8]).is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -190,8 +193,8 @@ mod tests {
|
||||
] } }
|
||||
});
|
||||
let d = doc_from(&root, "S");
|
||||
assert!(materialize_and_validate(&d, &prefixed_str_le("hi")).is_ok());
|
||||
let err = materialize_and_validate(&d, &prefixed_str_le("hello")).unwrap_err();
|
||||
assert!(materialize_and_validate(&root, &d, &prefixed_str_le("hi")).is_ok());
|
||||
let err = materialize_and_validate(&root, &d, &prefixed_str_le("hello")).unwrap_err();
|
||||
assert!(matches!(err, AlkTypeError::Validation(_)), "got {err:?}");
|
||||
}
|
||||
|
||||
@@ -205,10 +208,10 @@ mod tests {
|
||||
let d = doc_from(&root, "S");
|
||||
let mut buf = u32_le(2);
|
||||
buf.extend_from_slice(&[0xAA, 0xBB]);
|
||||
assert!(materialize_and_validate(&d, &buf).is_ok());
|
||||
assert!(materialize_and_validate(&root, &d, &buf).is_ok());
|
||||
let mut buf = u32_le(3);
|
||||
buf.extend_from_slice(&[0xAA, 0xBB, 0xCC]);
|
||||
let err = materialize_and_validate(&d, &buf).unwrap_err();
|
||||
let err = materialize_and_validate(&root, &d, &buf).unwrap_err();
|
||||
assert!(matches!(err, AlkTypeError::Validation(_)), "got {err:?}");
|
||||
}
|
||||
|
||||
@@ -238,8 +241,8 @@ mod tests {
|
||||
}
|
||||
});
|
||||
let d = doc_from(&root, "S");
|
||||
assert!(materialize_and_validate(&d, &u32_le(0)).is_ok());
|
||||
assert!(materialize_and_validate(&d, &u32_le(2)).is_ok());
|
||||
assert!(materialize_and_validate(&root, &d, &u32_le(0)).is_ok());
|
||||
assert!(materialize_and_validate(&root, &d, &u32_le(2)).is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -253,7 +256,7 @@ mod tests {
|
||||
}
|
||||
});
|
||||
let d = doc_from(&root, "S");
|
||||
let err = materialize_and_validate(&d, &u32_le(5)).unwrap_err();
|
||||
let err = materialize_and_validate(&root, &d, &u32_le(5)).unwrap_err();
|
||||
assert!(matches!(err, AlkTypeError::Validation(_)), "got {err:?}");
|
||||
}
|
||||
|
||||
@@ -284,16 +287,16 @@ mod tests {
|
||||
let mut buf = vec![2u8];
|
||||
buf.extend_from_slice(&u32_le(2));
|
||||
buf.extend_from_slice(&[0xAA, 0xBB]);
|
||||
assert!(materialize_and_validate(&d, &buf).is_ok());
|
||||
assert!(materialize_and_validate(&root, &d, &buf).is_ok());
|
||||
|
||||
let mut buf = vec![2u8];
|
||||
buf.extend_from_slice(&u32_le(3));
|
||||
buf.extend_from_slice(&[0xAA, 0xBB, 0xCC]);
|
||||
let err = materialize_and_validate(&d, &buf).unwrap_err();
|
||||
let err = materialize_and_validate(&root, &d, &buf).unwrap_err();
|
||||
assert!(matches!(err, AlkTypeError::Validation(_)), "got {err:?}");
|
||||
|
||||
let buf = vec![1u8, 7u8];
|
||||
assert!(materialize_and_validate(&d, &buf).is_ok());
|
||||
assert!(materialize_and_validate(&root, &d, &buf).is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -318,7 +321,7 @@ mod tests {
|
||||
let mut buf = prefixed_str_le("data");
|
||||
buf.extend_from_slice(&u32_le(2));
|
||||
buf.extend_from_slice(&[0xFF, 0xFE]);
|
||||
let err = materialize_and_validate(&d, &buf).unwrap_err();
|
||||
let err = materialize_and_validate(&root, &d, &buf).unwrap_err();
|
||||
assert!(matches!(err, AlkTypeError::Validation(_)), "got {err:?}");
|
||||
}
|
||||
|
||||
@@ -383,7 +386,7 @@ mod tests {
|
||||
buf.extend_from_slice(&2u16.to_le_bytes());
|
||||
buf.extend_from_slice(&3u16.to_le_bytes());
|
||||
buf.extend_from_slice(&4u16.to_le_bytes());
|
||||
assert!(materialize_and_validate(&d, &buf).is_ok());
|
||||
assert!(materialize_and_validate(&root, &d, &buf).is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -427,7 +430,7 @@ mod tests {
|
||||
] } }
|
||||
});
|
||||
let d = doc_from(&root, "S");
|
||||
let err = materialize_and_validate(&d, &[0u8; 2]).unwrap_err();
|
||||
let err = materialize_and_validate(&root, &d, &[0u8; 2]).unwrap_err();
|
||||
assert!(matches!(err, AlkTypeError::Access { .. }), "got {err:?}");
|
||||
}
|
||||
|
||||
|
||||
+19
-11
@@ -22,6 +22,7 @@ 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;
|
||||
@@ -47,12 +48,14 @@ pub enum LayoutMode {
|
||||
/// 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-007 — the reader has mutable cursor state that the consumer
|
||||
/// owns, so the engine is a factory, not a holder).
|
||||
/// 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: LayoutBuilder,
|
||||
plan: Arc<ReadPlan>,
|
||||
},
|
||||
/// Aligned static layout. Field offsets are precomputed in an
|
||||
/// [`OffsetMap`] for random access.
|
||||
@@ -151,7 +154,8 @@ impl AlkTypeEngine {
|
||||
let layout = match mode {
|
||||
LayoutMode::Packed => {
|
||||
let builder = LayoutBuilder::new(bast_doc, root_name)?;
|
||||
Layout::Packed { builder }
|
||||
let plan = Arc::new(ReadPlan::compile(bast_doc, root_name)?);
|
||||
Layout::Packed { builder, plan }
|
||||
}
|
||||
LayoutMode::Aligned => {
|
||||
let offset_map = OffsetMap::compute(&doc)?;
|
||||
@@ -204,15 +208,17 @@ impl AlkTypeEngine {
|
||||
}
|
||||
|
||||
/// Construct a fresh [`SequentialReader`] for packed-mode reads
|
||||
/// (ADR-007). 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.
|
||||
/// (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 { .. } => {
|
||||
SequentialReader::new(&self.bast_doc, &self.root_name).ok()
|
||||
Layout::Packed { plan, .. } => {
|
||||
Some(SequentialReader::new(Arc::clone(plan)))
|
||||
}
|
||||
Layout::Aligned { .. } => None,
|
||||
}
|
||||
@@ -298,7 +304,9 @@ impl AlkTypeEngine {
|
||||
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::Packed { plan, .. } => {
|
||||
materialize::materialize_packed(plan, buffer)?
|
||||
}
|
||||
Layout::Aligned { offset_map } => {
|
||||
materialize::materialize_aligned(&doc, buffer, offset_map)?
|
||||
}
|
||||
|
||||
+445
-50
@@ -1,27 +1,25 @@
|
||||
//! Materialize a `serde_json::Value` tree from a binary buffer by walking
|
||||
//! the BAST typed tree. Used by
|
||||
//! Materialize a `serde_json::Value` tree from a binary buffer. Used by
|
||||
//! [`crate::engine::AlkTypeEngine::validate_bytes`] (ADR-010) to collapse
|
||||
//! the two-step dance (read bytes -> `Value`, then validate `Value`) into
|
||||
//! a single call.
|
||||
//!
|
||||
//! The materializer reuses the [`crate::data_access`] read functions for
|
||||
//! leaf kinds and recurses into composites (`Struct`, `Array`, `Record`,
|
||||
//! `Union`). For `Union`, it dispatches on the discriminator and recurses
|
||||
//! into the variant. Errors carry the field path (ADR-004).
|
||||
//!
|
||||
//! Packed mode walks sequentially from offset 0; aligned mode reads at
|
||||
//! offsets from the [`crate::offset_map::OffsetMap`]. Both produce the
|
||||
//! same `Value` form; the validator is mode-agnostic.
|
||||
//! Packed mode walks the compiled [`ReadPlan`]
|
||||
//! (ADR-011) — the same compiled form [`crate::sequential_reader`] walks,
|
||||
//! unifying the two packed read-side consumers on one plan. Aligned mode
|
||||
//! walks the BAST typed tree, reading leaves at offsets from the
|
||||
//! [`crate::offset_map::OffsetMap`] (the permanent 0.3.0 design — see the
|
||||
//! phase 5 Scope Boundary in the 0.3.0 plan). Both produce the same
|
||||
//! `Value` form; the validator is mode-agnostic.
|
||||
//!
|
||||
//! ## `$ref` resolution
|
||||
//!
|
||||
//! The BAST document ([`crate::bast::BastDoc`]) is threaded through every
|
||||
//! recursive call so that `$ref` pointers (e.g. `"#/$defs/Read"`) in
|
||||
//! composite variants can be resolved via
|
||||
//! [`crate::bast::BastDoc::resolve_typeref`]. This is load-bearing for
|
||||
//! unions and arrays whose variants/elements are `$ref`s into `$defs`
|
||||
//! (the SFTP Packet shape: a struct wrapping a union with `$ref`
|
||||
//! variants).
|
||||
//! Packed mode: resolved eagerly at `ReadPlan::compile` — the per-buffer
|
||||
//! walk never touches the BAST document. Aligned mode: the BAST document
|
||||
//! ([`crate::bast::BastDoc`]) is threaded through every recursive call so
|
||||
//! `$ref` pointers (e.g. `"#/$defs/Read"`) in composite variants resolve
|
||||
//! via [`crate::bast::BastDoc::resolve_typeref`] at walk time —
|
||||
//! load-bearing for unions and arrays whose variants/elements are `$ref`s
|
||||
//! into `$defs`.
|
||||
|
||||
use crate::bast::{
|
||||
BastArray, BastDefKind, BastDiscriminator, BastDoc, BastField, BastRecord, BastStruct,
|
||||
@@ -29,35 +27,432 @@ use crate::bast::{
|
||||
};
|
||||
use crate::data_access;
|
||||
use crate::error::AlkTypeError;
|
||||
use crate::read_plan::{
|
||||
CompositePlan, DiscriminatorPlan, FieldPlan, ReadKind, ReadPlan,
|
||||
};
|
||||
use crate::schema::{AlkTypeKind, Endian, VariableEncoding};
|
||||
use serde_json::{Map, Value};
|
||||
|
||||
const U32_SIZE: usize = 4;
|
||||
const DISCRIMINATOR_KEY: &str = "__discriminator";
|
||||
|
||||
/// Materialize a `Value` tree from `buffer` by walking the BAST root type
|
||||
/// in packed mode (sequential, from offset 0).
|
||||
/// Materialize a `Value` tree from `buffer` by walking the compiled
|
||||
/// [`ReadPlan`] in packed mode (sequential, from offset 0).
|
||||
///
|
||||
/// The root type must be a struct. Endianness is read from the root
|
||||
/// struct's `endian` annotation (defaults to little-endian).
|
||||
/// The plan's root must be a struct (enforced at `ReadPlan::compile`).
|
||||
/// Endianness is baked per node at compile time.
|
||||
pub fn materialize_packed(
|
||||
doc: &BastDoc<'_>,
|
||||
plan: &ReadPlan,
|
||||
buffer: &[u8],
|
||||
) -> Result<Value, AlkTypeError> {
|
||||
let root_def = doc.root_def();
|
||||
let struct_node = match root_def.kind() {
|
||||
BastDefKind::Struct(s) => s,
|
||||
other => {
|
||||
let mut offset = 0usize;
|
||||
let mut obj = Map::new();
|
||||
for field in plan.fields() {
|
||||
let value = materialize_plan_field(plan, field, field.name(), buffer, &mut offset)?;
|
||||
obj.insert(field.name().to_string(), value);
|
||||
}
|
||||
Ok(Value::Object(obj))
|
||||
}
|
||||
|
||||
/// Materialize one planned field (packed mode): dispatch on the
|
||||
/// field's [`ReadKind`], reading leaves via [`crate::data_access`] and
|
||||
/// recursing into the compiled composite body. `endian` is the field's
|
||||
/// effective endianness baked into the plan.
|
||||
fn materialize_plan_field(
|
||||
plan: &ReadPlan,
|
||||
field: &FieldPlan,
|
||||
field_path: &str,
|
||||
buffer: &[u8],
|
||||
offset: &mut usize,
|
||||
) -> Result<Value, AlkTypeError> {
|
||||
let _ = plan;
|
||||
match field.kind() {
|
||||
ReadKind::Primitive(kind) => {
|
||||
materialize_plan_primitive(*kind, field.endian(), field_path, buffer, offset)
|
||||
}
|
||||
ReadKind::Enum => {
|
||||
let v = data_access::read_enum(buffer, *offset, field_path, field.endian())?;
|
||||
*offset += 4;
|
||||
Ok(Value::from(v))
|
||||
}
|
||||
ReadKind::Struct => {
|
||||
let body = match field.body() {
|
||||
Some(CompositePlan::Struct(p)) => p,
|
||||
_ => {
|
||||
return Err(AlkTypeError::Schema(format!(
|
||||
"internal: struct field {field_path} has no Struct body"
|
||||
)))
|
||||
}
|
||||
};
|
||||
materialize_plan_struct(body, field_path, buffer, offset)
|
||||
}
|
||||
ReadKind::Union => {
|
||||
let body = field.body().ok_or_else(|| AlkTypeError::Schema(format!(
|
||||
"internal: union field {field_path} has no body"
|
||||
)))?;
|
||||
materialize_plan_union(body, field_path, field.endian(), buffer, offset)
|
||||
}
|
||||
ReadKind::Array => {
|
||||
let body = field.body().ok_or_else(|| AlkTypeError::Schema(format!(
|
||||
"internal: array field {field_path} has no body"
|
||||
)))?;
|
||||
materialize_plan_array(body, field_path, field.endian(), buffer, offset)
|
||||
}
|
||||
ReadKind::Record => {
|
||||
let body = field.body().ok_or_else(|| AlkTypeError::Schema(format!(
|
||||
"internal: record field {field_path} has no body"
|
||||
)))?;
|
||||
materialize_plan_record(body, field_path, field.endian(), buffer, offset)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn materialize_plan_primitive(
|
||||
kind: AlkTypeKind,
|
||||
endian: Endian,
|
||||
field_path: &str,
|
||||
buffer: &[u8],
|
||||
offset: &mut usize,
|
||||
) -> Result<Value, AlkTypeError> {
|
||||
match kind {
|
||||
AlkTypeKind::Int8 => {
|
||||
let v = data_access::read_i8(buffer, *offset, field_path)?;
|
||||
*offset += 1;
|
||||
Ok(Value::from(v))
|
||||
}
|
||||
AlkTypeKind::Int16 => {
|
||||
let v = data_access::read_i16(buffer, *offset, field_path, endian)?;
|
||||
*offset += 2;
|
||||
Ok(Value::from(v))
|
||||
}
|
||||
AlkTypeKind::Int32 => {
|
||||
let v = data_access::read_i32(buffer, *offset, field_path, endian)?;
|
||||
*offset += 4;
|
||||
Ok(Value::from(v))
|
||||
}
|
||||
AlkTypeKind::Int64 => {
|
||||
let v = data_access::read_i64(buffer, *offset, field_path, endian)?;
|
||||
*offset += 8;
|
||||
Ok(Value::from(v))
|
||||
}
|
||||
AlkTypeKind::Uint8 => {
|
||||
let v = data_access::read_u8(buffer, *offset, field_path)?;
|
||||
*offset += 1;
|
||||
Ok(Value::from(v))
|
||||
}
|
||||
AlkTypeKind::Uint16 => {
|
||||
let v = data_access::read_u16(buffer, *offset, field_path, endian)?;
|
||||
*offset += 2;
|
||||
Ok(Value::from(v))
|
||||
}
|
||||
AlkTypeKind::Uint32 => {
|
||||
let v = data_access::read_u32(buffer, *offset, field_path, endian)?;
|
||||
*offset += 4;
|
||||
Ok(Value::from(v))
|
||||
}
|
||||
AlkTypeKind::Uint64 => {
|
||||
let v = data_access::read_u64(buffer, *offset, field_path, endian)?;
|
||||
*offset += 8;
|
||||
Ok(Value::from(v))
|
||||
}
|
||||
AlkTypeKind::Float32 => {
|
||||
let v = data_access::read_f32(buffer, *offset, field_path, endian)?;
|
||||
*offset += 4;
|
||||
Ok(number_from_f64(v as f64, field_path)?)
|
||||
}
|
||||
AlkTypeKind::Float64 => {
|
||||
let v = data_access::read_f64(buffer, *offset, field_path, endian)?;
|
||||
*offset += 8;
|
||||
Ok(number_from_f64(v, field_path)?)
|
||||
}
|
||||
AlkTypeKind::Boolean => {
|
||||
let v = data_access::read_bool(buffer, *offset, field_path)?;
|
||||
*offset += 1;
|
||||
Ok(Value::Bool(v))
|
||||
}
|
||||
AlkTypeKind::String => {
|
||||
let s = data_access::read_string(buffer, *offset, field_path, endian)?;
|
||||
let len = U32_SIZE
|
||||
.checked_add(s.len())
|
||||
.ok_or_else(|| AlkTypeError::Access {
|
||||
field_path: field_path.to_string(),
|
||||
reason: "string size 4 + len overflows usize".to_string(),
|
||||
})?;
|
||||
*offset = checked_add_at(*offset, len, field_path, "string")?;
|
||||
Ok(Value::String(s.to_string()))
|
||||
}
|
||||
AlkTypeKind::Bytes => {
|
||||
let b = data_access::read_bytes(buffer, *offset, field_path, endian)?;
|
||||
let len = U32_SIZE
|
||||
.checked_add(b.len())
|
||||
.ok_or_else(|| AlkTypeError::Access {
|
||||
field_path: field_path.to_string(),
|
||||
reason: "bytes size 4 + len overflows usize".to_string(),
|
||||
})?;
|
||||
*offset = checked_add_at(*offset, len, field_path, "bytes")?;
|
||||
let arr: Vec<Value> = b.iter().map(|&byte| Value::from(u32::from(byte))).collect();
|
||||
Ok(Value::Array(arr))
|
||||
}
|
||||
other => Err(AlkTypeError::Schema(format!(
|
||||
"materialize: unsupported primitive kind {other} at {field_path}"
|
||||
))),
|
||||
}
|
||||
}
|
||||
|
||||
fn checked_add_at(
|
||||
offset: usize,
|
||||
size: usize,
|
||||
field_path: &str,
|
||||
what: &str,
|
||||
) -> Result<usize, AlkTypeError> {
|
||||
offset.checked_add(size).ok_or_else(|| AlkTypeError::Access {
|
||||
field_path: field_path.to_string(),
|
||||
reason: format!("{what} offset {offset} + {size} overflows usize"),
|
||||
})
|
||||
}
|
||||
|
||||
fn materialize_plan_struct(
|
||||
plan: &ReadPlan,
|
||||
field_path: &str,
|
||||
buffer: &[u8],
|
||||
offset: &mut usize,
|
||||
) -> Result<Value, AlkTypeError> {
|
||||
let mut obj = Map::new();
|
||||
for field in plan.fields() {
|
||||
let path = format!("{field_path}.{}", field.name());
|
||||
let value = materialize_plan_field(plan, field, &path, buffer, offset)?;
|
||||
obj.insert(field.name().to_string(), value);
|
||||
}
|
||||
Ok(Value::Object(obj))
|
||||
}
|
||||
|
||||
fn materialize_plan_array(
|
||||
body: &CompositePlan,
|
||||
field_path: &str,
|
||||
endian: Endian,
|
||||
buffer: &[u8],
|
||||
offset: &mut usize,
|
||||
) -> Result<Value, AlkTypeError> {
|
||||
let (element, count) = match body {
|
||||
CompositePlan::Array {
|
||||
element,
|
||||
count,
|
||||
..
|
||||
} => (element, *count),
|
||||
_ => {
|
||||
return Err(AlkTypeError::Schema(format!(
|
||||
"materialize_packed: expected a struct at the root, got {kind}",
|
||||
kind = other.alk_kind()
|
||||
)));
|
||||
"internal: array body at {field_path} is not CompositePlan::Array"
|
||||
)))
|
||||
}
|
||||
};
|
||||
let effective_endian = struct_node.endian();
|
||||
materialize_struct_packed(doc, struct_node, "", effective_endian, buffer, &mut 0)
|
||||
let mut arr = Vec::with_capacity(count);
|
||||
for i in 0..count {
|
||||
let path = format!("{field_path}[{i}]");
|
||||
let value = materialize_plan_composite(element, &path, endian, buffer, offset)?;
|
||||
arr.push(value);
|
||||
}
|
||||
Ok(Value::Array(arr))
|
||||
}
|
||||
|
||||
/// Materialize a composite body used as a TypeRef (array element,
|
||||
/// record value, union variant).
|
||||
///
|
||||
/// Primitive/enum elements and values were wrapped at compile time in an
|
||||
/// anonymous single-field struct plan (the wrap-leaf convention) so the
|
||||
/// read loop has a uniform node; the materializer unwraps that wrapper
|
||||
/// and emits the leaf value directly — the 0.2.0 materializer never
|
||||
/// wrapped (real schema field names cannot be empty, so an empty name
|
||||
/// uniquely identifies the wrapper).
|
||||
fn materialize_plan_composite(
|
||||
body: &CompositePlan,
|
||||
field_path: &str,
|
||||
endian: Endian,
|
||||
buffer: &[u8],
|
||||
offset: &mut usize,
|
||||
) -> Result<Value, AlkTypeError> {
|
||||
if let CompositePlan::Struct(plan) = body {
|
||||
if let Some(only) = plan.fields().first() {
|
||||
if plan.fields().len() == 1
|
||||
&& only.name().is_empty()
|
||||
&& matches!(only.kind(), ReadKind::Primitive(_) | ReadKind::Enum)
|
||||
{
|
||||
return materialize_plan_field(plan, only, field_path, buffer, offset);
|
||||
}
|
||||
}
|
||||
}
|
||||
match body {
|
||||
CompositePlan::Struct(plan) => materialize_plan_struct(plan, field_path, buffer, offset),
|
||||
CompositePlan::Union { .. } => {
|
||||
materialize_plan_union(body, field_path, endian, buffer, offset)
|
||||
}
|
||||
CompositePlan::Array { .. } => {
|
||||
materialize_plan_array(body, field_path, endian, buffer, offset)
|
||||
}
|
||||
CompositePlan::Record { .. } => {
|
||||
materialize_plan_record(body, field_path, endian, buffer, offset)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn materialize_plan_union(
|
||||
body: &CompositePlan,
|
||||
field_path: &str,
|
||||
endian: Endian,
|
||||
buffer: &[u8],
|
||||
offset: &mut usize,
|
||||
) -> Result<Value, AlkTypeError> {
|
||||
let (disc, shared, variants) = match body {
|
||||
CompositePlan::Union {
|
||||
disc,
|
||||
shared,
|
||||
variants,
|
||||
} => (disc, shared, variants),
|
||||
_ => {
|
||||
return Err(AlkTypeError::Schema(format!(
|
||||
"internal: union body at {field_path} is not CompositePlan::Union"
|
||||
)))
|
||||
}
|
||||
};
|
||||
match disc {
|
||||
DiscriminatorPlan::Byte {
|
||||
offset: disc_rel,
|
||||
disc_type,
|
||||
} => {
|
||||
let disc_abs = checked_add_at(*offset, *disc_rel, field_path, "discriminator")?;
|
||||
let disc_value = match disc_type {
|
||||
AlkTypeKind::Uint8 => {
|
||||
data_access::read_u8(buffer, disc_abs, field_path)? as u32
|
||||
}
|
||||
AlkTypeKind::Uint16 => {
|
||||
data_access::read_u16(buffer, disc_abs, field_path, endian)? as u32
|
||||
}
|
||||
AlkTypeKind::Uint32 => {
|
||||
data_access::read_u32(buffer, disc_abs, field_path, endian)?
|
||||
}
|
||||
other => {
|
||||
return Err(AlkTypeError::Schema(format!(
|
||||
"internal: union discriminator type {other} is not a supported byte \
|
||||
discriminator (parser should have rejected this)"
|
||||
)));
|
||||
}
|
||||
};
|
||||
let key = disc_value.to_string();
|
||||
let variant = variants
|
||||
.iter()
|
||||
.find(|(k, _)| *k == key)
|
||||
.map(|(_, v)| v)
|
||||
.ok_or_else(|| AlkTypeError::Access {
|
||||
field_path: field_path.to_string(),
|
||||
reason: format!("union discriminator value {key} not in mapping"),
|
||||
})?;
|
||||
let disc_size = disc_type.type_size().unwrap_or(1);
|
||||
let variant_offset =
|
||||
checked_add_at(disc_abs, disc_size, field_path, "variant offset")?;
|
||||
*offset = variant_offset;
|
||||
let variant_value =
|
||||
materialize_plan_composite(variant, field_path, endian, buffer, offset)?;
|
||||
Ok(tag_union_value_number(disc_value, variant_value))
|
||||
}
|
||||
DiscriminatorPlan::Field { name, field_index } => {
|
||||
let shared_plan = shared.as_ref().ok_or_else(|| AlkTypeError::Schema(format!(
|
||||
"internal: field-disc union at {field_path} has no shared plan"
|
||||
)))?;
|
||||
let disc_field = shared_plan.fields().get(*field_index).ok_or_else(|| {
|
||||
AlkTypeError::Schema(format!(
|
||||
"internal: union {field_path} discriminator index {field_index} out of range"
|
||||
))
|
||||
})?;
|
||||
let _ = disc_field;
|
||||
// Walk the whole shared plan; capture the discriminator
|
||||
// field's value for the mapping key as we pass it. The
|
||||
// result object preserves the 0.2.0 key order:
|
||||
// `__discriminator` first, then the shared fields, then the
|
||||
// variant's fields.
|
||||
let mut disc_value: Option<Value> = None;
|
||||
let mut shared_values: Vec<(String, Value)> = Vec::new();
|
||||
for field in shared_plan.fields() {
|
||||
let path = format!("{field_path}.{}", field.name());
|
||||
let value = materialize_plan_field(shared_plan, field, &path, buffer, offset)?;
|
||||
if field.name() == name.as_str() {
|
||||
disc_value = Some(value);
|
||||
} else {
|
||||
shared_values.push((field.name().to_string(), value));
|
||||
}
|
||||
}
|
||||
let disc_value = disc_value.ok_or_else(|| AlkTypeError::Schema(format!(
|
||||
"internal: union {field_path} discriminator field {name:?} not walked"
|
||||
)))?;
|
||||
let key = plan_value_discriminator_key(&disc_value, field_path)?;
|
||||
let variant = variants
|
||||
.iter()
|
||||
.find(|(k, _)| *k == key)
|
||||
.map(|(_, v)| v)
|
||||
.ok_or_else(|| AlkTypeError::Access {
|
||||
field_path: field_path.to_string(),
|
||||
reason: format!("union discriminator value {key} not in mapping"),
|
||||
})?;
|
||||
let variant_value =
|
||||
materialize_plan_composite(variant, field_path, endian, buffer, offset)?;
|
||||
let mut obj = Map::new();
|
||||
obj.insert(DISCRIMINATOR_KEY.to_string(), Value::String(key));
|
||||
obj.insert((*name).to_string(), disc_value);
|
||||
for (k, v) in shared_values {
|
||||
obj.insert(k, v);
|
||||
}
|
||||
flatten_variant_into(&mut obj, variant_value);
|
||||
Ok(Value::Object(obj))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Stringify a materialized discriminator field value for mapping
|
||||
/// lookup. Mirrors the 0.2.0 `union_discriminator_key` for the Value form.
|
||||
fn plan_value_discriminator_key(value: &Value, field_path: &str) -> Result<String, AlkTypeError> {
|
||||
match value {
|
||||
Value::String(s) => Ok(s.clone()),
|
||||
Value::Number(n) => Ok(n.to_string()),
|
||||
_ => Err(AlkTypeError::Schema(format!(
|
||||
"union {field_path} has unsupported discriminator value kind: {value}"
|
||||
))),
|
||||
}
|
||||
}
|
||||
|
||||
fn materialize_plan_record(
|
||||
body: &CompositePlan,
|
||||
field_path: &str,
|
||||
endian: Endian,
|
||||
buffer: &[u8],
|
||||
offset: &mut usize,
|
||||
) -> Result<Value, AlkTypeError> {
|
||||
let value_body = match body {
|
||||
CompositePlan::Record { value } => value,
|
||||
_ => {
|
||||
return Err(AlkTypeError::Schema(format!(
|
||||
"internal: record body at {field_path} is not CompositePlan::Record"
|
||||
)))
|
||||
}
|
||||
};
|
||||
let count = data_access::read_u32(buffer, *offset, field_path, endian)? as usize;
|
||||
*offset += U32_SIZE;
|
||||
let mut obj = Map::new();
|
||||
for i in 0..count {
|
||||
let key_path = format!("{field_path}[{i}].key");
|
||||
let key_str = data_access::read_string(buffer, *offset, &key_path, endian)?;
|
||||
let key_len = U32_SIZE
|
||||
.checked_add(key_str.len())
|
||||
.ok_or_else(|| AlkTypeError::Access {
|
||||
field_path: key_path.clone(),
|
||||
reason: "key size 4 + len overflows usize".to_string(),
|
||||
})?;
|
||||
*offset = checked_add_at(*offset, key_len, &key_path, "key")?;
|
||||
let val_path = format!("{field_path}[{i}].value");
|
||||
let val = materialize_plan_composite(value_body, &val_path, endian, buffer, offset)?;
|
||||
obj.insert(key_str.to_string(), val);
|
||||
}
|
||||
Ok(Value::Object(obj))
|
||||
}
|
||||
|
||||
|
||||
/// Materialize a `Value` tree from `buffer` by walking the BAST root type
|
||||
/// in aligned mode (offsets from `offset_map`).
|
||||
///
|
||||
@@ -83,6 +478,7 @@ pub fn materialize_aligned(
|
||||
materialize_struct_aligned(doc, struct_node, "", offset_map, effective_endian, buffer)
|
||||
}
|
||||
|
||||
|
||||
fn materialize_struct_packed(
|
||||
doc: &BastDoc<'_>,
|
||||
struct_node: &BastStruct<'_>,
|
||||
@@ -399,10 +795,7 @@ fn materialize_record_packed(
|
||||
Ok(Value::Object(obj))
|
||||
}
|
||||
|
||||
/// Build the materialized `Value` for a byte-offset union: an object
|
||||
/// `{ "__discriminator": <disc_value>, ...variant-fields }`. If the
|
||||
/// variant materialized as a non-object (a leaf), it is nested under
|
||||
/// `"__variant"`.
|
||||
|
||||
fn tag_union_value_number(disc_value: u32, variant_value: Value) -> Value {
|
||||
let mut obj = Map::new();
|
||||
obj.insert(DISCRIMINATOR_KEY.to_string(), Value::from(disc_value));
|
||||
@@ -672,10 +1065,12 @@ mod tests {
|
||||
}
|
||||
|
||||
fn materialize_packed_strict(
|
||||
root: &Value,
|
||||
doc: &BastDoc<'_>,
|
||||
buffer: &[u8],
|
||||
) -> Result<Value, AlkTypeError> {
|
||||
materialize_packed(doc, buffer)
|
||||
let plan = ReadPlan::compile(root, doc.root_name())?;
|
||||
materialize_packed(&plan, buffer)
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -694,7 +1089,7 @@ mod tests {
|
||||
buf[4] = 0x41;
|
||||
buf[5] = 0x42;
|
||||
buf[6] = 0xC3;
|
||||
let v = materialize_packed_strict(&doc, &buf).expect("materialize");
|
||||
let v = materialize_packed_strict(&root, &doc, &buf).expect("materialize");
|
||||
assert_eq!(v["blob"], json!([65, 66, 195]));
|
||||
}
|
||||
|
||||
@@ -715,7 +1110,7 @@ mod tests {
|
||||
buf[5] = 0xFE;
|
||||
buf[6] = 0x00;
|
||||
buf[7] = 0x80;
|
||||
let v = materialize_packed_strict(&doc, &buf).expect("materialize");
|
||||
let v = materialize_packed_strict(&root, &doc, &buf).expect("materialize");
|
||||
assert_eq!(v["raw"], json!([255, 254, 0, 128]));
|
||||
}
|
||||
|
||||
@@ -747,7 +1142,7 @@ mod tests {
|
||||
buf[off] = b'a';
|
||||
off += 1;
|
||||
buf[off..off + 4].copy_from_slice(&20u32.to_le_bytes());
|
||||
let v = materialize_packed_strict(&doc, &buf).expect("materialize");
|
||||
let v = materialize_packed_strict(&root, &doc, &buf).expect("materialize");
|
||||
assert_eq!(v["counts"], json!({ "b": 10, "a": 20 }));
|
||||
}
|
||||
|
||||
@@ -766,7 +1161,7 @@ mod tests {
|
||||
let doc = doc_from(&root, "S");
|
||||
let mut buf = vec![0u8; 4];
|
||||
buf[0..4].copy_from_slice(&0u32.to_le_bytes());
|
||||
let v = materialize_packed_strict(&doc, &buf).expect("materialize");
|
||||
let v = materialize_packed_strict(&root, &doc, &buf).expect("materialize");
|
||||
assert_eq!(v["counts"], json!({}));
|
||||
}
|
||||
|
||||
@@ -784,7 +1179,7 @@ mod tests {
|
||||
});
|
||||
let doc = doc_from(&root, "S");
|
||||
let buf = [0u8; 2];
|
||||
let err = materialize_packed_strict(&doc, &buf).unwrap_err();
|
||||
let err = materialize_packed_strict(&root, &doc, &buf).unwrap_err();
|
||||
assert!(matches!(err, AlkTypeError::Access { .. }), "got {err:?}");
|
||||
}
|
||||
|
||||
@@ -818,7 +1213,7 @@ mod tests {
|
||||
let mut buf = vec![0u8; 5];
|
||||
buf[0] = 5;
|
||||
buf[1..5].copy_from_slice(&42u32.to_le_bytes());
|
||||
let v = materialize_packed_strict(&doc, &buf).expect("materialize");
|
||||
let v = materialize_packed_strict(&root, &doc, &buf).expect("materialize");
|
||||
let payload = &v["payload"];
|
||||
assert_eq!(payload["__discriminator"], json!(5));
|
||||
assert_eq!(payload["id"], json!(42));
|
||||
@@ -856,7 +1251,7 @@ mod tests {
|
||||
buf[0..4].copy_from_slice(&4u32.to_le_bytes());
|
||||
buf[4..8].copy_from_slice(b"read");
|
||||
buf[8..12].copy_from_slice(&7u32.to_le_bytes());
|
||||
let v = materialize_packed_strict(&doc, &buf).expect("materialize");
|
||||
let v = materialize_packed_strict(&root, &doc, &buf).expect("materialize");
|
||||
let payload = &v["payload"];
|
||||
assert_eq!(payload["__discriminator"], json!("read"));
|
||||
assert_eq!(payload["type"], json!("read"));
|
||||
@@ -897,7 +1292,7 @@ mod tests {
|
||||
buf[4..8].copy_from_slice(b"read");
|
||||
buf[8..12].copy_from_slice(&7u32.to_le_bytes());
|
||||
buf[12..16].copy_from_slice(&99u32.to_le_bytes());
|
||||
let v = materialize_packed_strict(&doc, &buf).expect("materialize");
|
||||
let v = materialize_packed_strict(&root, &doc, &buf).expect("materialize");
|
||||
assert_eq!(v["trailer"], json!(99));
|
||||
assert_eq!(v["payload"]["n"], json!(7));
|
||||
}
|
||||
@@ -930,7 +1325,7 @@ mod tests {
|
||||
});
|
||||
let doc = doc_from(&root, "S");
|
||||
let buf = [99u8];
|
||||
let err = materialize_packed_strict(&doc, &buf).unwrap_err();
|
||||
let err = materialize_packed_strict(&root, &doc, &buf).unwrap_err();
|
||||
assert!(matches!(err, AlkTypeError::Access { .. }), "got {err:?}");
|
||||
}
|
||||
|
||||
@@ -964,7 +1359,7 @@ mod tests {
|
||||
let mut buf = vec![0u8; 5];
|
||||
buf[0] = 5;
|
||||
buf[1..5].copy_from_slice(&42u32.to_le_bytes());
|
||||
let v = materialize_packed_strict(&doc, &buf).expect("materialize");
|
||||
let v = materialize_packed_strict(&root, &doc, &buf).expect("materialize");
|
||||
assert_eq!(v["payload"]["__discriminator"], json!(5));
|
||||
assert_eq!(v["payload"]["id"], json!(42));
|
||||
}
|
||||
@@ -1001,7 +1396,7 @@ mod tests {
|
||||
buf[2..4].copy_from_slice(&2u16.to_le_bytes());
|
||||
buf[4..6].copy_from_slice(&3u16.to_le_bytes());
|
||||
buf[6..8].copy_from_slice(&4u16.to_le_bytes());
|
||||
let v = materialize_packed_strict(&doc, &buf).expect("materialize");
|
||||
let v = materialize_packed_strict(&root, &doc, &buf).expect("materialize");
|
||||
assert_eq!(v["items"], json!([{ "x": 1, "y": 2 }, { "x": 3, "y": 4 }]));
|
||||
}
|
||||
|
||||
@@ -1032,7 +1427,7 @@ mod tests {
|
||||
buf[0] = 1;
|
||||
buf[1] = 2;
|
||||
buf[2..6].copy_from_slice(&0x03030303u32.to_le_bytes());
|
||||
let v = materialize_packed_strict(&doc, &buf).expect("materialize");
|
||||
let v = materialize_packed_strict(&root, &doc, &buf).expect("materialize");
|
||||
assert_eq!(v["header"]["a"], json!(1));
|
||||
assert_eq!(v["header"]["b"], json!(2));
|
||||
assert_eq!(v["after"], json!(0x03030303u32));
|
||||
@@ -1054,7 +1449,7 @@ mod tests {
|
||||
});
|
||||
let doc = doc_from(&root, "S");
|
||||
let buf = [0u8, 0u8, 0u8, 42u8, 0u8, 0u8, 0u8, 7u8];
|
||||
let v = materialize_packed_strict(&doc, &buf).expect("materialize");
|
||||
let v = materialize_packed_strict(&root, &doc, &buf).expect("materialize");
|
||||
assert_eq!(v["channel_id"], json!(42));
|
||||
assert_eq!(v["length"], json!(7));
|
||||
}
|
||||
@@ -1198,7 +1593,7 @@ mod tests {
|
||||
});
|
||||
let doc = doc_from(&root, "S");
|
||||
let buf = f32::NAN.to_le_bytes();
|
||||
let err = materialize_packed_strict(&doc, &buf).unwrap_err();
|
||||
let err = materialize_packed_strict(&root, &doc, &buf).unwrap_err();
|
||||
assert!(matches!(err, AlkTypeError::Access { .. }), "got {err:?}");
|
||||
}
|
||||
}
|
||||
+16
-1
@@ -44,6 +44,7 @@ use crate::error::AlkTypeError;
|
||||
use crate::schema::{AlkTypeKind, Endian, VariableEncoding};
|
||||
use serde_json::Value;
|
||||
use std::collections::{BTreeMap, BTreeSet};
|
||||
use std::sync::Arc;
|
||||
|
||||
/// Maximum recursion depth for the compile walk. Bounds deeply-nested
|
||||
/// (or cyclically-referencing) adversarial schemas; pairs with the
|
||||
@@ -65,6 +66,7 @@ pub struct ReadPlan {
|
||||
endian: Endian,
|
||||
fields: Vec<FieldPlan>,
|
||||
by_name: BTreeMap<String, usize>,
|
||||
schema: Arc<Value>,
|
||||
}
|
||||
|
||||
/// One compiled struct field: the read dispatch kind, the effective
|
||||
@@ -171,7 +173,10 @@ impl ReadPlan {
|
||||
}
|
||||
};
|
||||
let mut seen = BTreeSet::new();
|
||||
compile_struct(&doc, struct_node, struct_node.endian(), "", 0, &mut seen)
|
||||
compile_struct(&doc, struct_node, struct_node.endian(), "", 0, &mut seen).map(|mut plan| {
|
||||
plan.schema = Arc::new(bast_doc.clone());
|
||||
plan
|
||||
})
|
||||
}
|
||||
|
||||
/// The root container default endianness.
|
||||
@@ -188,6 +193,13 @@ impl ReadPlan {
|
||||
pub fn field_index(&self, name: &str) -> Option<usize> {
|
||||
self.by_name.get(name).copied()
|
||||
}
|
||||
|
||||
/// The BAST document this plan was compiled from (retained as
|
||||
/// `Arc<Value>` — the reader's `schema()` accessor returns it, and
|
||||
/// phase 6's `Hash` derive covers it via `serde_json::Value: Hash`).
|
||||
pub fn schema(&self) -> &Value {
|
||||
&self.schema
|
||||
}
|
||||
}
|
||||
|
||||
impl FieldPlan {
|
||||
@@ -264,6 +276,7 @@ fn compile_struct(
|
||||
endian: container_endian,
|
||||
fields,
|
||||
by_name,
|
||||
schema: Arc::new(Value::Null),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -428,6 +441,7 @@ fn compile_union(
|
||||
endian: container_endian,
|
||||
fields: plan,
|
||||
by_name,
|
||||
schema: Arc::new(Value::Null),
|
||||
}))
|
||||
}
|
||||
};
|
||||
@@ -568,6 +582,7 @@ fn wrap_leaf(
|
||||
endian: container_endian,
|
||||
fields: vec![field],
|
||||
by_name: BTreeMap::new(),
|
||||
schema: Arc::new(Value::Null),
|
||||
}))
|
||||
}
|
||||
_ => Err(AlkTypeError::Schema(format!(
|
||||
|
||||
+546
-558
File diff suppressed because it is too large.
Load diff
@@ -376,7 +376,8 @@ fn sequential_reader_buffer_too_short_returns_access_error() {
|
||||
}
|
||||
});
|
||||
let buffer = [0u8; 2];
|
||||
let mut reader = SequentialReader::new(&root, "S").unwrap();
|
||||
let plan = ReadPlan::compile(&root, "S").unwrap();
|
||||
let mut reader = SequentialReader::new(std::sync::Arc::new(plan));
|
||||
let err = reader.read_next(&buffer).unwrap_err();
|
||||
assert!(matches!(err, AlkTypeError::Access { .. }), "got {err:?}");
|
||||
}
|
||||
@@ -392,7 +393,8 @@ fn sequential_reader_unknown_field_returns_schema_error() {
|
||||
}
|
||||
});
|
||||
let buffer = [0u8; 4];
|
||||
let mut reader = SequentialReader::new(&root, "S").unwrap();
|
||||
let plan = ReadPlan::compile(&root, "S").unwrap();
|
||||
let mut reader = SequentialReader::new(std::sync::Arc::new(plan));
|
||||
let err = reader.read_field(&buffer, "missing").unwrap_err();
|
||||
assert!(matches!(err, AlkTypeError::Schema(_)), "got {err:?}");
|
||||
}
|
||||
@@ -409,7 +411,7 @@ fn sequential_reader_new_non_struct_returns_schema_error() {
|
||||
"A": { "kind": "struct", "fields": [] }
|
||||
}
|
||||
});
|
||||
let err = SequentialReader::new(&root, "U").unwrap_err();
|
||||
let err = ReadPlan::compile(&root, "U").unwrap_err();
|
||||
assert!(matches!(err, AlkTypeError::Schema(_)), "got {err:?}");
|
||||
}
|
||||
|
||||
|
||||
@@ -459,7 +459,8 @@ fn sequential_reader_round_trip_packed_buffer() -> Result<(), AlkTypeError> {
|
||||
let after = 1 + 4 + payload.len();
|
||||
data_access::write_u8(&mut buffer, after, 99, "tail")?;
|
||||
|
||||
let mut reader = SequentialReader::new(&root, "S")?;
|
||||
let plan = ReadPlan::compile(&root, "S")?;
|
||||
let mut reader = SequentialReader::new(std::sync::Arc::new(plan));
|
||||
assert_eq!(reader.endian(), Endian::Little);
|
||||
assert_eq!(reader.position(), 0);
|
||||
|
||||
@@ -502,7 +503,8 @@ fn sequential_reader_read_field_walks_preceding_fields() -> Result<(), AlkTypeEr
|
||||
data_access::write_u32(&mut buffer, 1, 0xDEADBEEF, "b", Endian::Little)?;
|
||||
data_access::write_u8(&mut buffer, 5, 9, "c")?;
|
||||
|
||||
let mut reader = SequentialReader::new(&root, "S")?;
|
||||
let plan = ReadPlan::compile(&root, "S")?;
|
||||
let mut reader = SequentialReader::new(std::sync::Arc::new(plan));
|
||||
let value = reader.read_field(&buffer, "c")?;
|
||||
assert_eq!(value, FieldValue::U8(9));
|
||||
assert_eq!(reader.position(), 6);
|
||||
|
||||
Reference in new issue
Block a user