diff --git a/src/data_access.rs b/src/data_access.rs index f05b6d8..1ebac52 100644 --- a/src/data_access.rs +++ b/src/data_access.rs @@ -712,6 +712,43 @@ mod tests { assert!(matches!(err, AlkTypeError::Access { .. })); } + #[test] + fn m4_write_bytes_indirect_pair_at_nonzero_offset_data_ok() { + // The indirect write's pair offset and data offset are + // independent; pin the pair landing mid-buffer with the data + // elsewhere (review #006 M4 item 3 — the write_bytes_indirect + // guard family is the canonical overflow-guard pattern, review + // #002 M2, and its nonzero-offset paths were uncovered). + let mut buf = vec![0u8; 24]; + let written = write_bytes_indirect(&mut buf, 4, 16, b"xyz", "blob", BE).unwrap(); + assert_eq!(written, 8); + assert_eq!(&buf[4..8], &16u32.to_be_bytes()); + assert_eq!(&buf[8..12], &3u32.to_be_bytes()); + assert_eq!(&buf[16..19], b"xyz"); + let bytes = read_bytes_indirect(&buf, 4, "blob", BE).unwrap(); + assert_eq!(bytes, b"xyz"); + } + + #[test] + fn m4_write_bytes_indirect_data_length_exceeds_bounds() { + // The data-region bounds check in write_bytes_indirect: the pair + // fits, but the {data_offset, length} target runs past the buffer + // end — the write must refuse without corrupting the pair. + let mut buf = vec![0u8; 20]; + let err = write_bytes_indirect(&mut buf, 0, 16, b"hello", "blob", LE).unwrap_err(); + match err { + AlkTypeError::Access { field_path, reason } => { + assert_eq!(field_path, "blob"); + assert!(reason.contains("bounds"), "reason: {reason}"); + } + other => panic!("expected Access, got {other:?}"), + } + // The pair was already written (offset+length at 0..8) — that's + // fine; the error refuses the data copy only. + assert_eq!(&buf[0..4], &16u32.to_le_bytes()); + assert_eq!(&buf[4..8], &5u32.to_le_bytes()); + } + #[test] fn read_at_nonzero_offset() { let mut buf = vec![0u8; 16]; diff --git a/src/materialize.rs b/src/materialize.rs index f435c01..b757c0f 100644 --- a/src/materialize.rs +++ b/src/materialize.rs @@ -727,18 +727,42 @@ fn materialize_union_packed( } BastDiscriminator::Field { name } => { let fields = union_node.fields(); - let disc_field = fields - .iter() - .find(|f| f.name() == *name) - .ok_or_else(|| { - AlkTypeError::Schema(format!( - "materialize: union at {field_path} has no discriminator field '{name}'" - )) - })?; - let disc_path = format!("{field_path}.{name}"); - let disc_value = - materialize_field_packed(doc, disc_field, &disc_path, endian, buffer, offset)?; - let key = union_discriminator_key(&disc_value, field_path)?; + if fields.is_empty() { + return Err(AlkTypeError::Schema(format!( + "materialize: union at {field_path} has no shared fields (the \ + discriminator field must be declared first)" + ))); + } + // The shared-then-variant wire convention (H3): walk ALL + // declared `fields` in order — capturing the disc value at its + // real position — and start the variant after the whole shared + // walk. (Review #006 M6: the pre-fix arm materialized only the + // disc field and started the variant immediately after it, + // silently reading the remaining shared fields' bytes as + // variant data whenever the union had any.) + let mut key = None; + let mut disc_value: Option = None; + let mut shared_values: Vec<(String, Value)> = Vec::new(); + for field in fields { + let path = format!("{field_path}.{}", field.name()); + let value = materialize_field_packed(doc, field, &path, endian, buffer, offset)?; + if field.name() == name.as_str() { + key = Some(union_discriminator_key(&value, field_path)?); + disc_value = Some(value); + } else { + shared_values.push((field.name().to_string(), value)); + } + } + let key = key.ok_or_else(|| { + AlkTypeError::Schema(format!( + "materialize: union at {field_path} has no discriminator field '{name}'" + )) + })?; + let disc_value = disc_value.ok_or_else(|| { + AlkTypeError::Schema(format!( + "internal: union {field_path} discriminator field {name:?} not walked" + )) + })?; let mapping = union_node.mapping(); let variant_ty = mapping .iter() @@ -760,6 +784,9 @@ fn materialize_union_packed( let mut obj = Map::new(); obj.insert(DISCRIMINATOR_KEY.to_string(), Value::String(key.clone())); 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)) } @@ -1029,6 +1056,7 @@ fn materialize_leaf_at( #[cfg(test)] mod tests { use super::*; + use crate::offset_map::ByteRange; use serde_json::json; fn doc_from(root: &Value, name: &str) -> BastDoc { @@ -1648,6 +1676,410 @@ mod tests { assert_eq!(v["crc"], json!(0x01020304u32)); } + // ----- M4 item 1: aligned nested structs + error arms ---------------- + + #[test] + fn m4_aligned_nested_struct_recursion_roundtrips() { + // The nested-struct recursion (materialize_struct_aligned entered + // with a non-empty path_prefix) had 0 executions in the review's + // coverage run — the heart of aligned validate_bytes was never + // exercised beyond a flat 2-field struct. This drives a + // three-level nesting through OffsetMap + materialize_aligned. + let root = json!({ + "$defs": { + "S": { + "kind": "struct", + "fields": [ + { + "name": "header", + "kind": { + "kind": "struct", + "fields": [ + { "name": "magic", "kind": "uint32" }, + { + "name": "meta", + "kind": { + "kind": "struct", + "fields": [ + { "name": "ver", "kind": "uint16" }, + { "name": "flag", "kind": "uint8" } + ] + } + } + ] + } + }, + { "name": "body", "kind": "uint32" } + ] + } + } + }); + let doc = doc_from(&root, "S"); + let map = crate::offset_map::OffsetMap::compute(&doc).expect("map"); + // Layout: header.magic @0..4, header.meta.ver @4..6, flag @6..7, + // (pad 1) body @8..12 — total 12. + assert_eq!(map.get("header.magic").map(|e| e.range), Some(ByteRange { start: 0, end: 4 })); + assert_eq!(map.get("header.meta.ver").map(|e| e.range), Some(ByteRange { start: 4, end: 6 })); + assert_eq!(map.get("header.meta.flag").map(|e| e.range), Some(ByteRange { start: 6, end: 7 })); + assert_eq!(map.get("body").map(|e| e.range), Some(ByteRange { start: 8, end: 12 })); + assert_eq!(map.total_size(), 12); + let mut buf = vec![0u8; 12]; + buf[0..4].copy_from_slice(&0xDEADBEEFu32.to_le_bytes()); + buf[4..6].copy_from_slice(&7u16.to_le_bytes()); + buf[6] = 1; + buf[8..12].copy_from_slice(&99u32.to_le_bytes()); + let v = materialize_aligned(&doc, &buf, &map).expect("materialize"); + assert_eq!(v["header"]["magic"], json!(0xDEADBEEFu32)); + assert_eq!(v["header"]["meta"]["ver"], json!(7)); + assert_eq!(v["header"]["meta"]["flag"], json!(1)); + assert_eq!(v["body"], json!(99)); + } + + #[test] + fn m4_aligned_nested_struct_maxlength_string_deep() { + // maxLength trim inside nested aligned structs — the trim arm is + // shared with the flat case, but the nested entry lookup is not. + let root = json!({ + "$defs": { + "S": { + "kind": "struct", + "fields": [ + { + "name": "inner", + "kind": { + "kind": "struct", + "fields": [ + { "name": "name", "kind": "string", "maxLength": 8 } + ] + } + }, + { "name": "id", "kind": "uint32" } + ] + } + } + }); + let doc = doc_from(&root, "S"); + let mut buf = vec![0u8; 12]; + buf[..5].copy_from_slice(b"hello"); + buf[8..12].copy_from_slice(&5u32.to_le_bytes()); + let v = materialize_aligned_strict(&doc, &buf).expect("materialize"); + assert_eq!(v["inner"]["name"], json!("hello")); + assert_eq!(v["id"], json!(5)); + } + + #[test] + fn m4_aligned_max_length_string_invalid_utf8_is_access_error() { + // The UTF-8 error arm of the maxLength trim (0-execution in the + // review's coverage run): garbage bytes in the reservation must + // be a clean Access error, not a panic or a silent String. + let root = json!({ + "$defs": { + "S": { + "kind": "struct", + "fields": [ + { "name": "name", "kind": "string", "maxLength": 8 } + ] + } + } + }); + let doc = doc_from(&root, "S"); + let mut buf = vec![0u8; 8]; + buf[..4].copy_from_slice(&[0xFF, 0xFE, 0x00, 0x80]); + let err = materialize_aligned_strict(&doc, &buf).unwrap_err(); + match err { + AlkTypeError::Access { field_path, reason } => { + assert_eq!(field_path, "name"); + assert!(reason.contains("UTF-8"), "reason: {reason}"); + } + other => panic!("expected Access, got {other:?}"), + } + } + + #[test] + fn m4_aligned_offset_indirect_out_of_bounds_is_access_error() { + // The offset-indirect arms' error path: the {offset, length} pair + // points outside the buffer — data_access's bounds check rejects. + let root = json!({ + "$defs": { + "S": { + "kind": "struct", + "fields": [ + { "name": "name", "kind": "string", "encoding": "offset-indirect" } + ] + } + } + }); + let doc = doc_from(&root, "S"); + let mut buf = vec![0u8; 8]; + buf[0..4].copy_from_slice(&1000u32.to_le_bytes()); + buf[4..8].copy_from_slice(&4u32.to_le_bytes()); + let err = materialize_aligned_strict(&doc, &buf).unwrap_err(); + assert!(matches!(err, AlkTypeError::Access { .. }), "got {err:?}"); + } + + #[test] + fn m4_aligned_offset_indirect_string_roundtrips_after_sibling() { + // Offset-indirect with a following field — the indirect pair's + // data can point anywhere in the buffer (that is the point of + // the encoding); pin the data landing after the sibling field. + let root = json!({ + "$defs": { + "S": { + "kind": "struct", + "fields": [ + { "name": "id", "kind": "uint32" }, + { "name": "name", "kind": "string", "encoding": "offset-indirect" }, + { "name": "tail", "kind": "uint32" } + ] + } + } + }); + let doc = doc_from(&root, "S"); + // Layout: id @0..4, pair @4..12, tail @12..16 (total 16); + // string data at 16..21. + let map = crate::offset_map::OffsetMap::compute(&doc).expect("map"); + assert_eq!(map.get("name").map(|e| e.range), Some(ByteRange { start: 4, end: 12 })); + assert_eq!(map.get("tail").map(|e| e.range), Some(ByteRange { start: 12, end: 16 })); + let mut buf = vec![0u8; 21]; + buf[0..4].copy_from_slice(&3u32.to_le_bytes()); + buf[4..8].copy_from_slice(&16u32.to_le_bytes()); + buf[8..12].copy_from_slice(&5u32.to_le_bytes()); + buf[12..16].copy_from_slice(&7u32.to_le_bytes()); + buf[16..21].copy_from_slice(b"hello"); + let v = materialize_aligned(&doc, &buf, &map).expect("materialize"); + assert_eq!(v["id"], json!(3)); + assert_eq!(v["name"], json!("hello")); + assert_eq!(v["tail"], json!(7)); + } + + #[test] + fn m4_aligned_array_of_fixed_struct_elements_materializes() { + // Struct elements are rejected in aligned mode (their kind is + // `Struct`, which is_fixed_size() refuses — OQ-001's conservative + // gate; the packed reader supports fixed-struct arrays via the + // plan's computed stride but the aligned map does not). Pin the + // aligned array walk over scalar fixed elements + the map's + // per-element entries, which is the aligned-mode shape. + let root = json!({ + "$defs": { + "S": { + "kind": "struct", + "fields": [ + { + "name": "points", + "kind": { + "kind": "array", + "element": "uint32", + "count": 2 + } + } + ] + } + } + }); + let doc = doc_from(&root, "S"); + let map = crate::offset_map::OffsetMap::compute(&doc).expect("map"); + assert_eq!(map.get("points[0]").map(|e| e.range), Some(ByteRange { start: 0, end: 4 })); + assert_eq!(map.get("points[1]").map(|e| e.range), Some(ByteRange { start: 4, end: 8 })); + let buf = [1u8, 0, 0, 0, 2, 0, 0, 0]; + let v = materialize_aligned(&doc, &buf, &map).expect("materialize"); + assert_eq!(v["points"], json!([1, 2])); + } + + #[test] + fn m4_aligned_record_with_struct_values_walks_nested_fields() { + // A record whose values are a struct: the aligned record arm + // drops into the packed BAST walker (materialize_typeref_packed → + // materialize_struct_packed), which was 0-execution in the review's + // coverage run. Pin the recursion + wire arithmetic. + let root = json!({ + "$defs": { + "S": { + "kind": "struct", + "fields": [ + { "name": "meta", "kind": { "kind": "record", "values": { "$ref": "#/$defs/V" } } } + ] + }, + "V": { + "kind": "struct", + "fields": [ + { "name": "tag", "kind": "uint8" }, + { "name": "val", "kind": "uint32" } + ] + } + } + }); + let doc = doc_from(&root, "S"); + let map = crate::offset_map::OffsetMap::compute(&doc).expect("map"); + // The record's internal walk is the packed walker (no padding): + // count@0..4 + entry0: keylen@4..8=1, key@8='a', tag@9, val@10..14 + // = 14 bytes total. + let mut buf = vec![0u8; 14]; + buf[0..4].copy_from_slice(&1u32.to_le_bytes()); + buf[4..8].copy_from_slice(&1u32.to_le_bytes()); + buf[8] = b'a'; + buf[9] = 7; + buf[10..14].copy_from_slice(&4242u32.to_le_bytes()); + let v = materialize_aligned(&doc, &buf, &map).expect("materialize"); + assert_eq!(v["meta"]["a"], json!({ "tag": 7, "val": 4242 })); + } + + #[test] + fn m4_aligned_record_with_array_values_walks_elements() { + // Record values that are an array of fixed primitives — the + // packed BAST walker's array arm through the aligned record path. + let root = json!({ + "$defs": { + "S": { + "kind": "struct", + "fields": [ + { "name": "bins", "kind": { "kind": "record", "values": { "kind": "array", "element": "uint16", "count": 2 } } } + ] + } + } + }); + let doc = doc_from(&root, "S"); + let map = crate::offset_map::OffsetMap::compute(&doc).expect("map"); + // count@0..4=1, keylen@4..8=1, key@8='a', elems@9..13 (2×u16). + let mut buf = vec![0u8; 13]; + buf[0..4].copy_from_slice(&1u32.to_le_bytes()); + buf[4..8].copy_from_slice(&1u32.to_le_bytes()); + buf[8] = b'a'; + buf[9..11].copy_from_slice(&10u16.to_le_bytes()); + buf[11..13].copy_from_slice(&20u16.to_le_bytes()); + let v = materialize_aligned(&doc, &buf, &map).expect("materialize"); + assert_eq!(v["bins"], json!({ "a": [10, 20] })); + } + + #[test] + fn m4_aligned_record_with_byte_disc_union_values_dispatches() { + // Record values that are a byte-disc union — the packed BAST + // walker's union arm through the aligned record path. + let root = json!({ + "$defs": { + "S": { + "kind": "struct", + "fields": [ + { "name": "events", "kind": { "kind": "record", "values": { "$ref": "#/$defs/Ev" } } } + ] + }, + "Ev": { + "kind": "union", + "discriminator": { "kind": "byte", "offset": 0, "type": "uint8" }, + "mapping": { "3": { "$ref": "#/$defs/Read" } } + }, + "Read": { "kind": "struct", "fields": [ { "name": "n", "kind": "uint32" } ] } + } + }); + let doc = doc_from(&root, "S"); + let map = crate::offset_map::OffsetMap::compute(&doc).expect("map"); + // count@0..4=1, keylen@4..8=1, key@8='a', disc@9=3, n@10..14. + let mut buf = vec![0u8; 14]; + buf[0..4].copy_from_slice(&1u32.to_le_bytes()); + buf[4..8].copy_from_slice(&1u32.to_le_bytes()); + buf[8] = b'a'; + buf[9] = 3; + buf[10..14].copy_from_slice(&77u32.to_le_bytes()); + let v = materialize_aligned(&doc, &buf, &map).expect("materialize"); + assert_eq!(v["events"]["a"]["__discriminator"], json!(3)); + assert_eq!(v["events"]["a"]["n"], json!(77)); + } + + #[test] + fn m4_aligned_record_with_field_disc_union_values_dispatches() { + // A field-disc union as record values — the union walker's field + // arm (shared walk + variant flatten) through the aligned record + // path. + let root = json!({ + "$defs": { + "S": { + "kind": "struct", + "fields": [ + { "name": "events", "kind": { "kind": "record", "values": { "$ref": "#/$defs/Ev" } } } + ] + }, + "Ev": { + "kind": "union", + "discriminator": { "kind": "field", "name": "type" }, + "fields": [ { "name": "type", "kind": "uint8" }, { "name": "seq", "kind": "uint32" } ], + "mapping": { "1": { "$ref": "#/$defs/Read" } } + }, + "Read": { "kind": "struct", "fields": [ { "name": "handle", "kind": "uint32" } ] } + } + }); + let doc = doc_from(&root, "S"); + let map = crate::offset_map::OffsetMap::compute(&doc).expect("map"); + // count@0..4=1, keylen@4..8=1, key@8='a', shared: type@9, seq@10..14, + // variant: handle@14..18 → 18 bytes. + let mut buf = vec![0u8; 18]; + buf[0..4].copy_from_slice(&1u32.to_le_bytes()); + buf[4..8].copy_from_slice(&1u32.to_le_bytes()); + buf[8] = b'a'; + buf[9] = 1; + buf[10..14].copy_from_slice(&5u32.to_le_bytes()); + buf[14..18].copy_from_slice(&77u32.to_le_bytes()); + let v = materialize_aligned(&doc, &buf, &map).expect("materialize"); + let ev = &v["events"]["a"]; + assert_eq!(ev["type"], json!(1)); + assert_eq!(ev["seq"], json!(5)); + assert_eq!(ev["handle"], json!(77)); + assert_eq!(ev["__discriminator"], json!("1")); + } + + #[test] + fn m4_aligned_record_with_wide_primitive_values_walks_all_arms() { + // The packed BAST walker's remaining primitive arms (i8..bool, + // u64, floats, string, bytes) as record values — 0-execution in + // the review's coverage run. + let root = json!({ + "$defs": { + "S": { + "kind": "struct", + "fields": [ + { "name": "vals", "kind": { "kind": "record", "values": { "$ref": "#/$defs/V" } } } + ] + }, + "V": { + "kind": "struct", + "fields": [ + { "name": "i8", "kind": "int8" }, + { "name": "u64", "kind": "uint64" }, + { "name": "f32", "kind": "float32" }, + { "name": "f64", "kind": "float64" }, + { "name": "b", "kind": "bool" }, + { "name": "s", "kind": "string" }, + { "name": "raw", "kind": "bytes" } + ] + } + } + }); + let doc = doc_from(&root, "S"); + let map = crate::offset_map::OffsetMap::compute(&doc).expect("map"); + let mut buf = vec![0u8; 64]; + buf[0..4].copy_from_slice(&1u32.to_le_bytes()); + buf[4..8].copy_from_slice(&1u32.to_le_bytes()); + buf[8] = b'a'; + let mut off = 9; + buf[off] = 0xF3; off += 1; // i8 = -13 + buf[off..off+8].copy_from_slice(&0x0102030405060708u64.to_le_bytes()); off += 8; + buf[off..off+4].copy_from_slice(&1.5f32.to_le_bytes()); off += 4; + buf[off..off+8].copy_from_slice(&2.5f64.to_le_bytes()); off += 8; + buf[off] = 0x01; off += 1; // bool = true + buf[off..off+4].copy_from_slice(&1u32.to_le_bytes()); off += 4; // string len + buf[off] = b'z'; off += 1; + buf[off..off+4].copy_from_slice(&2u32.to_le_bytes()); off += 4; // bytes len + buf[off] = 0xAA; + buf[off+1] = 0xBB; + let v = materialize_aligned(&doc, &buf, &map).expect("materialize"); + let obj = &v["vals"]["a"]; + assert_eq!(obj["i8"], json!(-13)); + assert_eq!(obj["u64"], json!(0x0102030405060708u64)); + assert_eq!(obj["f32"], json!(1.5)); + assert_eq!(obj["f64"], json!(2.5)); + assert_eq!(obj["s"], json!("z")); + assert_eq!(obj["raw"], json!([0xAA, 0xBB])); + } + #[test] fn materialize_packed_rejects_non_finite_float() { let root = json!({ diff --git a/src/sequential_reader.rs b/src/sequential_reader.rs index 5ebe2a3..428f4e9 100644 --- a/src/sequential_reader.rs +++ b/src/sequential_reader.rs @@ -1464,6 +1464,259 @@ mod tests { assert!(matches!(err, AlkTypeError::Access { .. }), "got {err:?}"); } + // ----- M4 item 2: disc-kind arms, nested-union walk, accessors ------- + + #[test] + fn m4_field_disc_uint16_dispatches_on_wide_value() { + let root = json!({ + "$defs": { + "S": { + "kind": "struct", + "fields": [ + { "name": "event", "kind": { "$ref": "#/$defs/Event" } } + ] + }, + "Event": { + "kind": "union", + "discriminator": { "kind": "field", "name": "type" }, + "fields": [ { "name": "type", "kind": "uint16" } ], + "mapping": { + "514": { "$ref": "#/$defs/Read" } + } + }, + "Read": { + "kind": "struct", + "fields": [ { "name": "x", "kind": "uint8" } ] + } + } + }); + let mut buf = vec![0u8; 4]; + buf[0..2].copy_from_slice(&514u16.to_le_bytes()); + buf[2] = 42; + let mut reader = reader(&root, "S"); + let (name, value) = reader.read_next(&buf).unwrap().unwrap(); + assert_eq!(name, "event"); + match value { + FieldValue::Union { discriminator, variant_start } => { + assert_eq!(discriminator, "514"); + assert_eq!(variant_start, 2); + } + other => panic!("expected Union, got {other:?}"), + } + assert_eq!(reader.position(), 3); + } + + #[test] + fn m4_field_disc_uint32_dispatches_on_wide_value() { + let root = json!({ + "$defs": { + "S": { + "kind": "struct", + "fields": [ + { "name": "event", "kind": { "$ref": "#/$defs/Event" } } + ] + }, + "Event": { + "kind": "union", + "discriminator": { "kind": "field", "name": "type" }, + "fields": [ { "name": "type", "kind": "uint32" } ], + "mapping": { + "258": { "$ref": "#/$defs/Read" } + } + }, + "Read": { + "kind": "struct", + "fields": [ { "name": "x", "kind": "uint8" } ] + } + } + }); + let mut buf = vec![0u8; 6]; + buf[0..4].copy_from_slice(&258u32.to_le_bytes()); + buf[4] = 42; + let mut reader = reader(&root, "S"); + let (name, value) = reader.read_next(&buf).unwrap().unwrap(); + assert_eq!(name, "event"); + match value { + FieldValue::Union { discriminator, variant_start } => { + assert_eq!(discriminator, "258"); + assert_eq!(variant_start, 4); + } + other => panic!("expected Union, got {other:?}"), + } + assert_eq!(reader.position(), 5); + } + + #[test] + fn m4_field_disc_enum_dispatches_on_index() { + let root = json!({ + "$defs": { + "S": { + "kind": "struct", + "fields": [ + { "name": "event", "kind": { "$ref": "#/$defs/Event" } } + ] + }, + "Event": { + "kind": "union", + "discriminator": { "kind": "field", "name": "kind" }, + "fields": [ { "name": "kind", "kind": { "$ref": "#/$defs/Kind" } } ], + "mapping": { + "3": { "$ref": "#/$defs/Read" } + } + }, + "Kind": { "kind": "enum", "values": ["A", "B", "C", "D"] }, + "Read": { + "kind": "struct", + "fields": [ { "name": "x", "kind": "uint8" } ] + } + } + }); + let mut buf = vec![0u8; 8]; + buf[0..4].copy_from_slice(&3u32.to_le_bytes()); + buf[4] = 42; + let mut reader = reader(&root, "S"); + let (name, value) = reader.read_next(&buf).unwrap().unwrap(); + assert_eq!(name, "event"); + match value { + FieldValue::Union { discriminator, variant_start } => { + assert_eq!(discriminator, "3"); + assert_eq!(variant_start, 4); + } + other => panic!("expected Union, got {other:?}"), + } + assert_eq!(reader.position(), 5); + } + + #[test] + fn m4_byte_disc_uint16_and_uint32_arms() { + // The plan reader's byte-disc uint16/uint32 arms (0-execution in + // the review's coverage run — wire-visible dispatch widths). + let u16_root = json!({ + "$defs": { + "S": { + "kind": "struct", + "fields": [ + { "name": "packet", "kind": { "$ref": "#/$defs/P16" } } + ] + }, + "P16": { + "kind": "union", + "discriminator": { "kind": "byte", "offset": 0, "type": "uint16" }, + "mapping": { "515": { "$ref": "#/$defs/Read" } } + }, + "Read": { "kind": "struct", "fields": [ { "name": "x", "kind": "uint8" } ] } + } + }); + let mut buf = vec![0u8; 4]; + buf[0..2].copy_from_slice(&515u16.to_le_bytes()); + buf[2] = 42; + let mut reader16 = reader(&u16_root, "S"); + let (_, value) = reader16.read_next(&buf).unwrap().unwrap(); + match value { + FieldValue::Union { discriminator, variant_start } => { + assert_eq!(discriminator, "515"); + assert_eq!(variant_start, 2); + } + other => panic!("expected Union, got {other:?}"), + } + assert_eq!(reader16.position(), 3); + + let u32_root = json!({ + "$defs": { + "S": { + "kind": "struct", + "fields": [ + { "name": "packet", "kind": { "$ref": "#/$defs/P32" } } + ] + }, + "P32": { + "kind": "union", + "discriminator": { "kind": "byte", "offset": 0, "type": "uint32" }, + "mapping": { "258": { "$ref": "#/$defs/Read" } } + }, + "Read": { "kind": "struct", "fields": [ { "name": "x", "kind": "uint8" } ] } + } + }); + let mut buf = vec![0u8; 6]; + buf[0..4].copy_from_slice(&258u32.to_le_bytes()); + buf[4] = 42; + let mut reader32 = reader(&u32_root, "S"); + let (_, value) = reader32.read_next(&buf).unwrap().unwrap(); + match value { + FieldValue::Union { discriminator, variant_start } => { + assert_eq!(discriminator, "258"); + assert_eq!(variant_start, 4); + } + other => panic!("expected Union, got {other:?}"), + } + assert_eq!(reader32.position(), 5); + } + + #[test] + fn m4_nested_union_variant_size_walk() { + // A byte-disc union whose variant is itself a union — the + // plan_walk_variant_size union arm (0-execution in the review's + // coverage run; the capability phase 1 restored). The inner + // union dispatches on its own discriminator after the outer's. + let root = json!({ + "$defs": { + "S": { + "kind": "struct", + "fields": [ + { "name": "outer", "kind": { "$ref": "#/$defs/Outer" } } + ] + }, + "Outer": { + "kind": "union", + "discriminator": { "kind": "byte", "offset": 0, "type": "uint8" }, + "mapping": { "1": { "$ref": "#/$defs/Inner" } } + }, + "Inner": { + "kind": "union", + "discriminator": { "kind": "byte", "offset": 0, "type": "uint8" }, + "mapping": { "7": { "$ref": "#/$defs/Leaf" } } + }, + "Leaf": { "kind": "struct", "fields": [ { "name": "v", "kind": "uint32" } ] } + } + }); + // Wire: outer disc (1) + inner disc (7) + u32 — 6 bytes. + let mut buf = vec![0u8; 6]; + buf[0] = 1; + buf[1] = 7; + buf[2..6].copy_from_slice(&42u32.to_le_bytes()); + let mut reader = reader(&root, "S"); + let (name, value) = reader.read_next(&buf).unwrap().unwrap(); + assert_eq!(name, "outer"); + match value { + FieldValue::Union { discriminator, variant_start } => { + assert_eq!(discriminator, "1"); + assert_eq!(variant_start, 1); + } + other => panic!("expected Union, got {other:?}"), + } + assert_eq!(reader.position(), 6, "nested union's full size walked"); + } + + #[test] + fn m4_public_schema_and_plan_accessors_return_compiled_doc() { + // The public accessors (phase-2 deliverables) had 0 test calls: + // `schema()` returns the compiled document, `plan()` the compiled + // ReadPlan handle. + let root = json!({ + "$defs": { + "S": { "kind": "struct", "fields": [ { "name": "a", "kind": "uint8" } ] } + } + }); + let plan = ReadPlan::compile(&root, "S").expect("plan"); + let reader = SequentialReader::new(Arc::new(plan)); + let schema = reader.schema(); + assert_eq!(schema["$defs"]["S"]["fields"][0]["name"], json!("a")); + let p = reader.plan(); + assert_eq!(p.fields().len(), 1); + assert_eq!(p.fields()[0].name(), "a"); + assert_eq!(p.schema()["$defs"]["S"]["fields"][0]["name"], json!("a")); + } + #[test] fn rejects_schema_with_missing_root() { let root = json!({ "$defs": { "Other": { "kind": "struct", "fields": [] } } });