diff --git a/docs/architecture/bast-format.md b/docs/architecture/bast-format.md index 303ffe1..1cb20ab 100644 --- a/docs/architecture/bast-format.md +++ b/docs/architecture/bast-format.md @@ -540,7 +540,7 @@ BAST type tree that checks exactly these: | Int64/Uint64 (full range) | `validate_int64`/`validate_uint64` (JSON precision caveat per ADR-005) | | Float finiteness (Float32/64) | `validate_float` with `as_f64().is_finite()` | | String `maxLength` (byte length) | `check_string` reads the field-level `maxLength` | -| Bytes `maxLength` (array length) | `check_bytes` accepts `Value::String` and `Value::Array` forms | +| Bytes `maxLength` (array length) | `check_bytes` accepts the `Value::Array` form (the materializer emits bytes as an array of u8) | | RFC 3339 timestamp shape | `validate_timestamp` — same non-strict check as v0.1.0 | | Enum index bounds | `validate_enum` checks `idx < values.len()` — **fixes the v0.1.0 dead constraint** | | Union variant dispatch | `validate_union` reads `__discriminator`, looks up the variant, recurses via `validate_typeref` | diff --git a/docs/architecture/validation.md b/docs/architecture/validation.md index 0b1a023..95b8c92 100644 --- a/docs/architecture/validation.md +++ b/docs/architecture/validation.md @@ -47,7 +47,7 @@ tree ([`crate::bast::BastDoc`]/[`BastType`]) that checks exactly these: | Int64/Uint64 (full range) | `validate_int64`/`validate_uint64` (JSON precision caveat per ADR-005) | | Float finiteness (Float32/64) | `validate_float` with `as_f64().is_finite()` | | String `maxLength` (byte length) | `check_string` reads the field-level `maxLength` | -| Bytes `maxLength` (array length) | `check_bytes` accepts `Value::String` and `Value::Array` forms | +| Bytes `maxLength` (array length) | `check_bytes` accepts the `Value::Array` form (the materializer emits bytes as an array of u8) | | RFC 3339 timestamp shape | `validate_timestamp` — non-strict check (matching v0.1.0) | | Enum index bounds | `validate_enum` checks `idx < values.len()` — **fixes the v0.1.0 dead constraint** | | Union variant dispatch | `validate_union` reads `__discriminator`, looks up the variant, recurses via `validate_typeref` | diff --git a/src/bast_validation.rs b/src/bast_validation.rs index ed146f1..c8f22ba 100644 --- a/src/bast_validation.rs +++ b/src/bast_validation.rs @@ -211,48 +211,32 @@ fn check_string(value: &Value, path: &str, max_length: Option) -> Result< } fn check_bytes(value: &Value, path: &str, max_length: Option) -> Result<(), AlkTypeError> { - match value { - Value::String(s) => { - if let Some(max) = max_length { - if s.len() > max { - return Err(validation_err( - path, - format!("bytes length {} exceeds maxLength {max}", s.len()), - )); - } - } - Ok(()) + let arr = value + .as_array() + .ok_or_else(|| validation_err(path, "expected an array of u8 for bytes"))?; + if let Some(max) = max_length { + if arr.len() > max { + return Err(validation_err( + path, + format!("bytes array length {} exceeds maxLength {max}", arr.len()), + )); } - Value::Array(arr) => { - if let Some(max) = max_length { - if arr.len() > max { - return Err(validation_err( - path, - format!("bytes array length {} exceeds maxLength {max}", arr.len()), - )); - } - } - for (i, entry) in arr.iter().enumerate() { - let n = entry.as_u64().ok_or_else(|| { - validation_err( - path, - format!("bytes array entry {i} is not a non-negative integer"), - ) - })?; - if n > 255 { - return Err(validation_err( - path, - format!("bytes array entry {i} = {n} is not a u8 (0..=255)"), - )); - } - } - Ok(()) - } - _ => Err(validation_err( - path, - "expected a string or array of u8 for bytes", - )), } + for (i, entry) in arr.iter().enumerate() { + let n = entry.as_u64().ok_or_else(|| { + validation_err( + path, + format!("bytes array entry {i} is not a non-negative integer"), + ) + })?; + if n > 255 { + return Err(validation_err( + path, + format!("bytes array entry {i} = {n} is not a u8 (0..=255)"), + )); + } + } + Ok(()) } fn validate_timestamp(value: &Value, path: &str) -> Result<(), AlkTypeError> { diff --git a/src/engine.rs b/src/engine.rs index 40fe84b..3e064bd 100644 --- a/src/engine.rs +++ b/src/engine.rs @@ -48,7 +48,7 @@ pub enum LayoutMode { 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 + /// (ADR-007 — the reader has mutable cursor state that the consumer /// owns, so the engine is a factory, not a holder). Packed { builder: LayoutBuilder, @@ -194,7 +194,7 @@ impl AlkTypeEngine { } /// Construct a fresh [`SequentialReader`] for packed-mode reads - /// (ADR-101). Each call returns a new reader with the cursor at + /// (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. /// diff --git a/src/error.rs b/src/error.rs index fd80702..289ef70 100644 --- a/src/error.rs +++ b/src/error.rs @@ -1,6 +1,6 @@ //! Error types for the alktype engine. //! -//! Decided in ADR-098: a single `AlkTypeError` enum covers all error +//! Decided in ADR-004: a single `AlkTypeError` enum covers all error //! conditions across the engine's three phases (schema parsing, offset //! computation, read/write) plus validation. diff --git a/src/layout_builder.rs b/src/layout_builder.rs index cda9c7a..87c1266 100644 --- a/src/layout_builder.rs +++ b/src/layout_builder.rs @@ -1,4 +1,4 @@ -//! Packed sequential `LayoutBuilder` — Mode 1 write-side (ADR-096). +//! Packed sequential `LayoutBuilder` — Mode 1 write-side (ADR-002). //! //! Fields are packed with no alignment padding. Variable-length fields //! shift all subsequent fields. The consumer provides actual data sizes diff --git a/src/materialize.rs b/src/materialize.rs index 9f569c4..0e74af1 100644 --- a/src/materialize.rs +++ b/src/materialize.rs @@ -172,12 +172,12 @@ fn materialize_typeref_packed( BastType::Primitive(AlkTypeKind::Float32) => { let v = data_access::read_f32(buffer, *offset, field_path, endian)?; *offset += 4; - Ok(number_from_f64(v as f64)) + Ok(number_from_f64(v as f64, field_path)?) } BastType::Primitive(AlkTypeKind::Float64) => { let v = data_access::read_f64(buffer, *offset, field_path, endian)?; *offset += 8; - Ok(number_from_f64(v)) + Ok(number_from_f64(v, field_path)?) } BastType::Primitive(AlkTypeKind::Boolean) => { let v = data_access::read_bool(buffer, *offset, field_path)?; @@ -444,10 +444,13 @@ fn union_discriminator_key(value: &Value, field_path: &str) -> Result Value { +fn number_from_f64(f: f64, field_path: &str) -> Result { serde_json::Number::from_f64(f) .map(Value::Number) - .unwrap_or(Value::Null) + .ok_or_else(|| AlkTypeError::Access { + field_path: field_path.to_string(), + reason: format!("non-finite float {f} cannot be represented as JSON"), + }) } /// Aligned-mode materialization: walk the BAST typed tree, but read @@ -1193,4 +1196,20 @@ mod tests { let v = materialize_aligned_strict(&doc, &buf).expect("materialize"); assert_eq!(v["crc"], json!(0x01020304u32)); } + + #[test] + fn materialize_packed_rejects_non_finite_float() { + let root = json!({ + "$defs": { + "S": { + "kind": "struct", + "fields": [ { "name": "f", "kind": "float32" } ] + } + } + }); + let doc = doc_from(&root, "S"); + let buf = f32::NAN.to_le_bytes(); + let err = materialize_packed_strict(&doc, &buf).unwrap_err(); + assert!(matches!(err, AlkTypeError::Access { .. }), "got {err:?}"); + } } \ No newline at end of file diff --git a/src/offset_map.rs b/src/offset_map.rs index 962520f..970a86e 100644 --- a/src/offset_map.rs +++ b/src/offset_map.rs @@ -1,4 +1,4 @@ -//! Aligned static `OffsetMap` — Mode 2 of the two layout modes (ADR-096). +//! Aligned static `OffsetMap` — Mode 2 of the two layout modes (ADR-002). //! //! Fields have fixed positions with natural alignment padding. //! Variable-length fields get a 4-byte length prefix at a known offset; diff --git a/src/sequential_reader.rs b/src/sequential_reader.rs index 3d58f3f..ebe81ea 100644 --- a/src/sequential_reader.rs +++ b/src/sequential_reader.rs @@ -1,4 +1,4 @@ -//! Packed sequential `SequentialReader` — Mode 1 read-side (ADR-096). +//! Packed sequential `SequentialReader` — Mode 1 read-side (ADR-002). //! //! Walks a buffer field-by-field according to the BAST schema, reading //! length prefixes to determine variable-length data positions. Used at diff --git a/src/tunion.rs b/src/tunion.rs index 71112e7..2fe19c9 100644 --- a/src/tunion.rs +++ b/src/tunion.rs @@ -1,4 +1,4 @@ -//! TUnion discriminator dispatch (ADR-097 §4). +//! TUnion discriminator dispatch (ADR-003 §4). //! //! TUnion supports two discriminator kinds: byte-offset (protocol //! dispatch, e.g., SFTP type bytes) and field-name (typedef.ts string