fuzz: wave 3 — validate_pair two-input harness, 44 seeds, release-budget campaigns next
Target 5 (§3): compile an attacker schema (10-lane menu incl. raw JSON bytes lane) in both modes, then hammer the hostile buffer through validate_bytes, an independent materialize_packed/materialize_aligned, read_field over every offset-map leaf, junk field paths, and the packed sequential walk under the spin bound. Invariants coded (per §3 target 5): mode agreement (aligned Ok ⇒ packed Ok; packed-Ok/aligned-Err only for the documented ADR-006/ADR-008/ offset-indirect rejections), the materialize⇄validate_bytes verdict lattice with verbatim error propagation, unknown-path echo, serde round-trip of materialized output, non-finite-float Access pinning, out-of-range enum Validation pinning, and the record-count spin bound. 44 committed seeds (menu/raw lanes × valid/valid, hostile-schema/ valid-bytes, valid-schema/hostile-bytes incl. a per-prefix truncation sweep, NaN/Inf, enum 99, spin fixtures, mode-agreement pins), hand- encoded against the pinned arbitrary 1.4.2 derive layout and pinned by decode tests. The aligned maxLength-reservation offset pin (s@8..72, tail@72, total 76) caught a fixture assumption error pre-commit. Verification: corpus replay 29/29 green (44 new seeds decode+replay), main crate 570 tests pass, clippy -D warnings clean (crate + shared), wasm clean, cargo fuzz build clean. Hand-run drives (indirect pair escape, enum-Validation, unknown discriminator, trailing garbage) all held.
This commit is contained in:
1 parent
16b9023f60
commit
aef8d9f6ab
49 files changed
+1037
-2
No files matched your search
@@ -1,4 +1,4 @@
|
||||
//! Fuzz invariant logic for alktype's wave-1 targets. Kept out of the
|
||||
//! Fuzz invariant logic for alktype's fuzz targets. Kept out of the
|
||||
//! fuzz-target binaries so the corpus replay unit tests in the release
|
||||
//! verification checklist can exercise the same invariant checks
|
||||
//! against every committed corpus entry (the quinn CI pattern) without
|
||||
@@ -8,3 +8,4 @@ pub mod bast_compile;
|
||||
pub mod data_access;
|
||||
pub mod layout_build;
|
||||
pub mod read_opseq;
|
||||
pub mod validate_pair;
|
||||
@@ -0,0 +1,847 @@
|
||||
//! Invariants for `validate_pair` (wave 3, target 5) — the integration
|
||||
//! two-input harness: schema and bytes are both adversarial. Compile
|
||||
//! once per exec in the fuzzer-picked mode (plus a cross-mode compile
|
||||
//! for the mode-agreement lattice), then hammer the hostile buffer
|
||||
//! through `validate_bytes`, an independent `materialize_*`, leaf
|
||||
//! `read_field`s over the offset map, junk field paths, and (packed
|
||||
//! mode) the sequential walk under the compiled plan.
|
||||
//!
|
||||
//! Pinned contract (read off the `validate_bytes` implementation —
|
||||
//! `engine.rs:308`: materialize then plan-validate, errors propagated
|
||||
//! verbatim):
|
||||
//! - no-panic for any (doc, bytes) pair, either mode;
|
||||
//! - mode agreement: aligned Ok ⇒ packed Ok; packed Ok with aligned
|
||||
//! Err allowed only for the documented aligned-mode rejections
|
||||
//! (ADR-006 non-final inline length-prefixed, ADR-008 TUnion, the
|
||||
//! record offset-indirect rejection) surfacing as `Offset`;
|
||||
//! - agreement lattice: `materialize` Err ⇒ `validate_bytes` Err with
|
||||
//! the identical payload; `materialize` Ok ⇒ `validate_bytes`
|
||||
//! equals the plan walk over the independently materialized `Value`;
|
||||
//! - `validate_bytes` Ok ⇒ every offset-map leaf `read_field` over the
|
||||
//! same buffer is Ok;
|
||||
//! - unknown field paths always error (Offset in aligned, Access in
|
||||
//! packed) echoing `field_path` verbatim, never panic;
|
||||
//! - a successful materialization round-trips through `serde_json`
|
||||
//! (structural equality — `preserve_order` keeps key order);
|
||||
//! - the record-count spin bound holds through materialize too: a
|
||||
//! hostile count fails fast with `Access` (fixtures, target-3 form).
|
||||
|
||||
use alktype::bast::BastDoc;
|
||||
use alktype::materialize;
|
||||
use alktype::read_plan::ReadPlan;
|
||||
use alktype::{AlkTypeEngine, AlkTypeError, LayoutMode};
|
||||
use arbitrary::Arbitrary;
|
||||
use serde_json::Value;
|
||||
|
||||
/// The schema menu. Lanes 0–7 are well-formed and compile in at least
|
||||
/// one mode; lanes 8–9 are hostile docs (missing root, `$ref` cycle).
|
||||
/// Together they cover: the classic variable-length sandwich (aligned
|
||||
/// only via `maxLength`), the aligned offset-indirect pair, the record
|
||||
/// (the count-loop candidate), a TUnion (aligned-rejected), the
|
||||
/// non-finite-float lane, an enum (the value-range Validation lane),
|
||||
/// an all-fixed shape (both modes), and the reject lanes.
|
||||
const MENUS: [&str; 10] = [
|
||||
// 0: variable field sandwiched between tails — packed-valid,
|
||||
// aligned-rejected (ADR-006, `s` is a non-final inline
|
||||
// length-prefixed field).
|
||||
r##"{"$defs": {"S": {"kind": "struct", "endian": "little", "fields": [
|
||||
{ "name": "a", "kind": "uint8" },
|
||||
{ "name": "n", "kind": "uint32" },
|
||||
{ "name": "s", "kind": "string" },
|
||||
{ "name": "tail", "kind": "uint8" }]}}}"##,
|
||||
// 1: the same shape with a declared `maxLength` — valid in both
|
||||
// modes (aligned: fixed reservation).
|
||||
r##"{"$defs": {"S": {"kind": "struct", "endian": "little", "fields": [
|
||||
{ "name": "a", "kind": "uint8" },
|
||||
{ "name": "n", "kind": "uint32" },
|
||||
{ "name": "s", "kind": "string", "maxLength": 64 },
|
||||
{ "name": "tail", "kind": "uint8" }]}}}"##,
|
||||
// 2: aligned offset-indirect bytes field (the §6 candidate-2 pair,
|
||||
// end-to-end: the pair at the entry, the data region elsewhere in
|
||||
// the buffer).
|
||||
r##"{"$defs": {"S": {"kind": "struct", "endian": "little", "fields": [
|
||||
{ "name": "id", "kind": "uint32" },
|
||||
{ "name": "b", "kind": "bytes", "encoding": "offset-indirect",
|
||||
"maxLength": 64 }]}}}"##,
|
||||
// 3: record with string values — the materializer's count-loop.
|
||||
r##"{"$defs": {"S": {"kind": "struct", "endian": "little", "fields": [
|
||||
{ "name": "k", "kind": "uint32" },
|
||||
{ "name": "rec", "kind": { "kind": "record", "values": "string" } }]}}}"##,
|
||||
// 4: TUnion under a byte discriminator — packed-valid,
|
||||
// aligned-rejected (ADR-008).
|
||||
r##"{"$defs": {"S": {"kind": "struct", "endian": "little", "fields": [
|
||||
{ "name": "pre", "kind": "uint16" },
|
||||
{ "name": "un", "kind": { "$ref": "#/$defs/U" } }],
|
||||
},
|
||||
"U": {"kind": "union",
|
||||
"discriminator": {"kind": "byte", "offset": 0, "type": "uint8"},
|
||||
"mapping": {"0": {"$ref": "#/$defs/Small"}}},
|
||||
"Small": {"kind": "struct", "fields": [{ "name": "v", "kind": "uint8" }]}}}"##,
|
||||
// 5: float64 — the non-finite lane (NaN/Inf bytes must surface as
|
||||
// Access, never silent 0.0/Null).
|
||||
r##"{"$defs": {"S": {"kind": "struct", "endian": "little", "fields": [
|
||||
{ "name": "tag", "kind": "uint8" },
|
||||
{ "name": "f", "kind": "float64" }]}}}"##,
|
||||
// 6: enum — the value-domain lane (out-of-range wire index
|
||||
// materializes but must fail the plan walk with `Validation`).
|
||||
r##"{"$defs": {"S": {"kind": "struct", "endian": "little", "fields": [
|
||||
{ "name": "e", "kind": { "kind": "enum", "values": ["r", "w", "x"] } },
|
||||
{ "name": "t", "kind": "uint8" }]}}}"##,
|
||||
// 7: all-fixed array shape — valid in both modes, truncation menu
|
||||
// host.
|
||||
r##"{"$defs": {"S": {"kind": "struct", "endian": "big", "fields": [
|
||||
{ "name": "hdr", "kind": "uint32" },
|
||||
{ "name": "vals", "kind": { "kind": "array", "element": "uint16", "count": 2 } }]}}}"##,
|
||||
// 8: hostile — empty `$defs` (no root definition).
|
||||
r##"{"$defs": {}}"##,
|
||||
// 9: hostile — `$ref` cycle through two definitions.
|
||||
r##"{"$defs": {"S": {"kind": "struct", "fields": [
|
||||
{ "name": "a", "kind": {"$ref": "#/$defs/T"} }],
|
||||
},
|
||||
"T": {"kind": "struct", "fields": [
|
||||
{ "name": "b", "kind": {"$ref": "#/$defs/S"} }]}}}"##,
|
||||
];
|
||||
|
||||
/// Junk field paths: never declared, must always error (Offset in
|
||||
/// aligned, Access in packed) echoing the path back in `field_path`.
|
||||
const JUNK_PATHS: [&str; 5] = ["", "__nope__", "a.", "s.payload", "x.y.z"];
|
||||
|
||||
#[derive(Debug, Arbitrary)]
|
||||
pub enum DocLane {
|
||||
Menu(u8),
|
||||
Raw(Vec<u8>),
|
||||
}
|
||||
|
||||
/// Post-decode transform of the hostile bytes (the read_opseq shape
|
||||
/// set: raw, empty, truncate at n, fixed 64-byte zero pad).
|
||||
#[derive(Debug, Arbitrary, Clone, Copy)]
|
||||
pub enum Shape {
|
||||
Raw,
|
||||
Empty,
|
||||
Truncate(u8),
|
||||
Padded,
|
||||
}
|
||||
|
||||
/// The typed fuzz input. `buffer` (the take-rest field) carries the
|
||||
/// hostile buffer body.
|
||||
#[derive(Debug, Arbitrary)]
|
||||
pub struct PairInput {
|
||||
lane: DocLane,
|
||||
mode: bool,
|
||||
shape: Shape,
|
||||
buffer: Vec<u8>,
|
||||
}
|
||||
|
||||
pub fn fuzz_validate_pair(data: &[u8]) {
|
||||
let Ok(input) = PairInput::arbitrary_take_rest(arbitrary::Unstructured::new(data)) else {
|
||||
return;
|
||||
};
|
||||
|
||||
let (doc, root_name) = match &input.lane {
|
||||
DocLane::Menu(i) => {
|
||||
let text = MENUS[(*i as usize) % MENUS.len()];
|
||||
let doc = match serde_json::from_str::<Value>(text) {
|
||||
Ok(v) => v,
|
||||
Err(_) => return,
|
||||
};
|
||||
(doc, "S".to_string())
|
||||
}
|
||||
DocLane::Raw(bytes) => {
|
||||
// The genuinely fused lane: the attacker's doc is bytes of
|
||||
// the same provenance as the buffer. Anything serde_json
|
||||
// rejects is not an alktype surface.
|
||||
let doc = match serde_json::from_slice::<Value>(bytes) {
|
||||
Ok(v) => v,
|
||||
Err(_) => return,
|
||||
};
|
||||
let root = doc
|
||||
.get("root")
|
||||
.and_then(Value::as_str)
|
||||
.unwrap_or("S")
|
||||
.to_string();
|
||||
(doc, root)
|
||||
}
|
||||
};
|
||||
let mode = if input.mode { LayoutMode::Packed } else { LayoutMode::Aligned };
|
||||
|
||||
let packed = AlkTypeEngine::compile(&doc, &root_name, LayoutMode::Packed, None);
|
||||
let aligned = AlkTypeEngine::compile(&doc, &root_name, LayoutMode::Aligned, None);
|
||||
assert_mode_agreement(&packed, &aligned);
|
||||
|
||||
let engine = match mode {
|
||||
LayoutMode::Packed => packed,
|
||||
LayoutMode::Aligned => aligned,
|
||||
};
|
||||
let Ok(engine) = engine else {
|
||||
return;
|
||||
};
|
||||
assert_engine_shape(&engine);
|
||||
|
||||
let buffer = shape_buffer(&input.shape, &input.buffer);
|
||||
junk_paths(&engine, &root_name);
|
||||
|
||||
drive_pair(&engine, &doc, &root_name, &buffer);
|
||||
|
||||
if engine.mode() == LayoutMode::Packed {
|
||||
packed_walk(&engine, &buffer);
|
||||
}
|
||||
}
|
||||
|
||||
/// Cross-mode gate agreement (the wave-1 invariant, re-pinned over the
|
||||
/// hostile-doc lanes at the pair level): the parse layers are
|
||||
/// mode-independent, so aligned Ok ⇒ packed Ok; a packed Ok with
|
||||
/// aligned Err is allowed only when the aligned walk rejects for its
|
||||
/// documented mode-specific reasons (ADR-006/ADR-008/record
|
||||
/// offset-indirect), surfacing as `Offset`.
|
||||
fn assert_mode_agreement(
|
||||
packed: &Result<AlkTypeEngine, AlkTypeError>,
|
||||
aligned: &Result<AlkTypeEngine, AlkTypeError>,
|
||||
) {
|
||||
match (packed, aligned) {
|
||||
(Ok(_), Ok(_)) => {}
|
||||
(Err(p), Err(a)) => {
|
||||
assert_clean(p);
|
||||
assert_clean(a);
|
||||
}
|
||||
(Ok(_), Err(a)) => match a {
|
||||
AlkTypeError::Offset { reason, .. } => assert!(
|
||||
reason.contains("ADR-006")
|
||||
|| reason.contains("ADR-008")
|
||||
|| reason.contains("offset-indirect"),
|
||||
"aligned compile rejection after a packed Ok must name a \
|
||||
documented aligned-mode rejection (ADR-006/ADR-008 or the \
|
||||
record offset-indirect rule), got: {reason}"
|
||||
),
|
||||
other => panic!(
|
||||
"aligned compile rejection after a packed Ok must be an \
|
||||
Offset-class mode rejection, got {other:?}"
|
||||
),
|
||||
},
|
||||
(Err(_), Ok(_)) => {
|
||||
panic!("aligned Ok ⇒ packed Ok must hold (the aligned walk is a \
|
||||
strict subset of the packed walkers)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn assert_clean(err: &AlkTypeError) {
|
||||
match err {
|
||||
AlkTypeError::Schema(msg) => {
|
||||
assert!(!msg.is_empty(), "Schema error carries a message");
|
||||
}
|
||||
AlkTypeError::Offset { reason, .. } | AlkTypeError::Access { reason, .. } => {
|
||||
assert!(!reason.is_empty(), "Offset/Access errors carry a reason");
|
||||
}
|
||||
AlkTypeError::Validation(_) => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn assert_engine_shape(engine: &AlkTypeEngine) {
|
||||
match engine.mode() {
|
||||
LayoutMode::Packed => {
|
||||
assert!(engine.offset_map().is_none());
|
||||
assert!(engine.sequential_reader().is_some(), "packed factory");
|
||||
}
|
||||
LayoutMode::Aligned => {
|
||||
assert!(engine.offset_map().is_some());
|
||||
assert!(engine.sequential_reader().is_none(), "aligned factory");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Unknown paths must always error, echo the path, and never panic.
|
||||
fn junk_paths(engine: &AlkTypeEngine, _root: &str) {
|
||||
for junk in JUNK_PATHS {
|
||||
let err = match engine.read_field(&[], junk) {
|
||||
Ok(_) => panic!("junk path {junk:?} must never resolve"),
|
||||
Err(e) => e,
|
||||
};
|
||||
match &err {
|
||||
AlkTypeError::Offset { field_path, .. } => {
|
||||
assert_eq!(field_path, junk, "aligned unknown path echoes back");
|
||||
}
|
||||
AlkTypeError::Access { field_path, .. } => {
|
||||
assert_eq!(field_path, junk, "packed read_field echoes back");
|
||||
}
|
||||
other => panic!("junk path must be Offset/Access, got {other:?}"),
|
||||
}
|
||||
assert_clean(&err);
|
||||
}
|
||||
}
|
||||
|
||||
fn shape_buffer(shape: &Shape, bytes: &[u8]) -> Vec<u8> {
|
||||
match shape {
|
||||
Shape::Raw => bytes.to_vec(),
|
||||
Shape::Empty => Vec::new(),
|
||||
Shape::Truncate(n) => bytes[..bytes.len().min(*n as usize)].to_vec(),
|
||||
Shape::Padded => {
|
||||
let mut v = bytes.to_vec();
|
||||
v.extend_from_slice(&[0u8; 64]);
|
||||
v
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The pair lane: validate_bytes and an independent materialize must
|
||||
/// compose to the same result, byte-leaf reads must agree under a
|
||||
/// successful validate, and the materialized `Value` must be
|
||||
/// serde-safe.
|
||||
fn drive_pair(engine: &AlkTypeEngine, doc: &Value, root: &str, buffer: &[u8]) {
|
||||
let vb = engine.validate_bytes(buffer);
|
||||
|
||||
let independent: Result<Value, AlkTypeError> = match engine.mode() {
|
||||
LayoutMode::Packed => {
|
||||
let plan = match ReadPlan::compile(doc, root) {
|
||||
Ok(p) => p,
|
||||
Err(e) => panic!("a compiled engine implies an equal plan compile: {e:?}"),
|
||||
};
|
||||
materialize::materialize_packed(&plan, buffer)
|
||||
}
|
||||
LayoutMode::Aligned => {
|
||||
let offset_map = engine
|
||||
.offset_map()
|
||||
.expect("an aligned engine carries its offset map");
|
||||
let bast_doc = match BastDoc::new(doc, root) {
|
||||
Ok(d) => d,
|
||||
Err(e) => panic!("a compiled engine implies an equal BastDoc: {e:?}"),
|
||||
};
|
||||
materialize::materialize_aligned(&bast_doc, buffer, offset_map)
|
||||
}
|
||||
};
|
||||
|
||||
match independent {
|
||||
Err(e_mat) => {
|
||||
assert_clean(&e_mat);
|
||||
let Err(e_vb) = vb else {
|
||||
panic!(
|
||||
"materialize Err ⇒ validate_bytes Err must hold, got \
|
||||
validate_bytes Ok while materializing failed: {e_mat:?}"
|
||||
)
|
||||
};
|
||||
assert_eq!(
|
||||
format!("{e_vb:?}"),
|
||||
format!("{e_mat:?}"),
|
||||
"validate_bytes propagates the materializer's error verbatim"
|
||||
);
|
||||
}
|
||||
Ok(value) => {
|
||||
let plan_res = engine.validation_plan().validate(&value);
|
||||
match plan_res {
|
||||
Ok(()) => assert!(
|
||||
vb.is_ok(),
|
||||
"materialize Ok + plan Ok ⇒ validate_bytes Ok, got {vb:?}"
|
||||
),
|
||||
Err(e_val) => {
|
||||
assert_clean(&e_val);
|
||||
let Err(ref e_vb) = vb else {
|
||||
panic!(
|
||||
"materialize Ok + plan Err ⇒ validate_bytes Err, got Ok"
|
||||
)
|
||||
};
|
||||
assert_eq!(
|
||||
format!("{e_vb:?}"),
|
||||
format!("{e_val:?}"),
|
||||
"validate_bytes on a materialized value is exactly the \
|
||||
plan walk's verdict"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// validate_bytes Ok ⇒ every offset-map leaf reads back
|
||||
// through `read_field` over the same buffer.
|
||||
if vb.is_ok() {
|
||||
if let Some(map) = engine.offset_map() {
|
||||
for (path, entry) in map.iter() {
|
||||
if matches!(
|
||||
entry.meta.kind,
|
||||
alktype::AlkTypeKind::Struct
|
||||
| alktype::AlkTypeKind::Union
|
||||
| alktype::AlkTypeKind::Array
|
||||
| alktype::AlkTypeKind::Record
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
match engine.read_field(buffer, path) {
|
||||
Ok(_) => {}
|
||||
Err(e) => panic!(
|
||||
"validate_bytes Ok ⇒ read_field({path}) Ok, got {e:?}"
|
||||
),
|
||||
}
|
||||
assert!(
|
||||
entry.range.end <= buffer.len(),
|
||||
"a validated leaf sits inside the buffer"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// serde-safety: the output survives to_vec + parse back,
|
||||
// structurally equal (preserve_order keeps key order).
|
||||
let bytes = serde_json::to_vec(&value)
|
||||
.expect("a materialized Value is serde-serializable");
|
||||
let back = serde_json::from_slice::<Value>(&bytes)
|
||||
.expect("a materialized Value survives its own JSON encoding");
|
||||
assert_eq!(value, back, "materialize output serde round-trips");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The packed walk (packed mode only): plan order over a possibly
|
||||
/// hostile buffer under the spin bound, errors always clean.
|
||||
fn packed_walk(engine: &AlkTypeEngine, buffer: &[u8]) {
|
||||
let Some(mut reader) = engine.sequential_reader() else {
|
||||
return;
|
||||
};
|
||||
let field_count = reader.plan().fields().len();
|
||||
let bound = walk_bound(buffer.len(), field_count);
|
||||
|
||||
let mut steps = 0usize;
|
||||
let mut names = Vec::new();
|
||||
loop {
|
||||
steps += 1;
|
||||
if steps > bound {
|
||||
panic!(
|
||||
"the pair walk exceeded the spin bound at step {steps} over a \
|
||||
{len}-byte buffer with {fields} fields",
|
||||
len = buffer.len(),
|
||||
fields = field_count
|
||||
);
|
||||
}
|
||||
match reader.read_next(buffer) {
|
||||
Ok(Some((name, _))) => {
|
||||
assert!(reader.position() <= buffer.len(), "cursor in bounds");
|
||||
names.push(name.to_string());
|
||||
}
|
||||
Ok(None) => break,
|
||||
Err(e) => {
|
||||
assert_clean(&e);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if names.len() == field_count {
|
||||
let expected: Vec<String> =
|
||||
reader.plan().fields().iter().map(|f| f.name().to_string()).collect();
|
||||
assert_eq!(
|
||||
names, expected,
|
||||
"a full walk over a validating buffer reports fields in plan order"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Same bound formula as the read_opseq target: a successful step
|
||||
/// advances the cursor, and every inner iteration verifies ≥ 4 bytes.
|
||||
fn walk_bound(buffer_len: usize, field_count: usize) -> usize {
|
||||
buffer_len.saturating_add(field_count).saturating_add(2)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod corpus_replay {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn committed_corpus_replays_through_the_invariants() {
|
||||
let corpus =
|
||||
std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../corpus/validate_pair");
|
||||
let mut count = 0usize;
|
||||
for entry in std::fs::read_dir(&corpus).expect("corpus directory is committed") {
|
||||
let path = entry.expect("corpus entry readable").path();
|
||||
let data = std::fs::read(&path).expect("corpus entry readable");
|
||||
fuzz_validate_pair(&data);
|
||||
count += 1;
|
||||
}
|
||||
assert!(
|
||||
count >= 10,
|
||||
"committed seed corpus is present, found {count}"
|
||||
);
|
||||
}
|
||||
|
||||
/// Byte encodings against the pinned arbitrary 1.4.2 derive layout:
|
||||
/// field order { lane: DocLane, mode: bool, shape: Shape, buffer:
|
||||
/// Vec<u8> } with buffer the take-rest field. DocLane has 2
|
||||
/// variants (Menu(u8), Raw(Vec<u8>)); Shape has 4 (Raw, Empty,
|
||||
/// Truncate(u8), Padded). Enum variant selection is the 4-byte LE
|
||||
/// multiply-shift u32; Vec<u8> elements are keep-going byte +
|
||||
/// element bytes with a 0x00 stop; bools are one byte (& 1).
|
||||
mod enc {
|
||||
pub fn variant_u32(k: u64, count: u64) -> [u8; 4] {
|
||||
let mut v = (k << 32).div_ceil(count);
|
||||
while (v.wrapping_mul(count) >> 32) != k {
|
||||
v += 1;
|
||||
}
|
||||
u32::try_from(v).expect("k < count always admits a < 2^32 v").to_le_bytes()
|
||||
}
|
||||
|
||||
pub const LANE_MENU: u64 = 0;
|
||||
pub const LANE_RAW: u64 = 1;
|
||||
|
||||
pub const SHAPE_RAW: u64 = 0;
|
||||
pub const SHAPE_EMPTY: u64 = 1;
|
||||
pub const SHAPE_TRUNCATE: u64 = 2;
|
||||
pub const SHAPE_PADDED: u64 = 3;
|
||||
|
||||
pub fn lane_menu(index: u8) -> Vec<u8> {
|
||||
let mut v = variant_u32(LANE_MENU, 2).to_vec();
|
||||
v.push(index);
|
||||
v
|
||||
}
|
||||
|
||||
pub fn lane_raw(doc: &[u8]) -> Vec<u8> {
|
||||
let mut v = variant_u32(LANE_RAW, 2).to_vec();
|
||||
for b in doc {
|
||||
v.push(0x01);
|
||||
v.push(*b);
|
||||
}
|
||||
v.push(0x00);
|
||||
v
|
||||
}
|
||||
|
||||
pub fn shape_field(tag: u64) -> Vec<u8> {
|
||||
variant_u32(tag, 4).to_vec()
|
||||
}
|
||||
|
||||
pub fn shape_truncate(n: u8) -> Vec<u8> {
|
||||
let mut v = shape_field(SHAPE_TRUNCATE);
|
||||
v.push(n);
|
||||
v
|
||||
}
|
||||
|
||||
pub fn pair(lane: Vec<u8>, aligned: bool, shape: Vec<u8>, buffer: &[u8]) -> Vec<u8> {
|
||||
let mut v = lane;
|
||||
v.push(u8::from(aligned)); // mode: false = packed, true = aligned
|
||||
v.extend_from_slice(&shape);
|
||||
for b in buffer {
|
||||
v.push(0x01);
|
||||
v.push(*b);
|
||||
}
|
||||
v.push(0x00);
|
||||
v
|
||||
}
|
||||
}
|
||||
|
||||
/// Buffers matching the menu schemas (little-endian where declared;
|
||||
/// menu 7 is big-endian).
|
||||
mod buffers {
|
||||
/// Menu 0/1 packed body: u8 a | u32 n | string s | u8 tail.
|
||||
pub fn sandwich_packed(s: &[u8]) -> Vec<u8> {
|
||||
let mut buf = Vec::new();
|
||||
buf.push(0x11);
|
||||
buf.extend_from_slice(&7u32.to_le_bytes());
|
||||
buf.extend_from_slice(&(s.len() as u32).to_le_bytes());
|
||||
buf.extend_from_slice(s);
|
||||
buf.push(0x22);
|
||||
buf
|
||||
}
|
||||
|
||||
/// Menu 1 aligned body with `s` as a fixed `maxLength` region
|
||||
/// (64 bytes reserved at the entry, 8..72): a at 0, n at 4,
|
||||
/// s prefix at 8, s data inline 12..12+len, tail at 72 (the
|
||||
/// offset pin test below keeps these honest).
|
||||
pub fn sandwich_aligned(s: &[u8]) -> Vec<u8> {
|
||||
let mut buf = vec![0u8; 12 + 64 + 1];
|
||||
buf[0] = 0x11;
|
||||
buf[4..8].copy_from_slice(&7u32.to_le_bytes());
|
||||
buf[8..12].copy_from_slice(&(s.len() as u32).to_le_bytes());
|
||||
buf[12..12 + s.len()].copy_from_slice(s);
|
||||
buf[72] = 0x22;
|
||||
buf
|
||||
}
|
||||
|
||||
/// Menu 2 aligned offset-indirect body: u32 id | u32 data_off |
|
||||
/// u32 data_len | data region at data_off.
|
||||
pub fn indirect(data: &[u8]) -> Vec<u8> {
|
||||
let mut buf = vec![0u8; 12 + data.len()];
|
||||
buf[0..4].copy_from_slice(&0x0Bu32.to_le_bytes());
|
||||
buf[4..8].copy_from_slice(&12u32.to_le_bytes());
|
||||
buf[8..12].copy_from_slice(&(data.len() as u32).to_le_bytes());
|
||||
buf[12..].copy_from_slice(data);
|
||||
buf
|
||||
}
|
||||
|
||||
/// Menu 3 packed body: u32 k | record count | entries.
|
||||
pub fn record(count: u32, entries: &[(&[u8], u32)]) -> Vec<u8> {
|
||||
let mut buf = Vec::new();
|
||||
buf.extend_from_slice(&1u32.to_le_bytes());
|
||||
buf.extend_from_slice(&count.to_le_bytes());
|
||||
for (key, value) in entries {
|
||||
buf.extend_from_slice(&(key.len() as u32).to_le_bytes());
|
||||
buf.extend_from_slice(key);
|
||||
buf.extend_from_slice(&value.to_le_bytes());
|
||||
}
|
||||
buf
|
||||
}
|
||||
|
||||
/// Menu 4 packed body: u16 pre | disc byte | variant u8.
|
||||
pub fn union(disc: u8, v: u8) -> Vec<u8> {
|
||||
let mut buf = Vec::new();
|
||||
buf.extend_from_slice(&0x0102u16.to_le_bytes());
|
||||
buf.push(disc);
|
||||
buf.push(v);
|
||||
buf
|
||||
}
|
||||
|
||||
/// Menu 5 body: u8 tag | f64 (chosen bit pattern).
|
||||
pub fn float(bits: u64) -> Vec<u8> {
|
||||
let mut buf = Vec::new();
|
||||
buf.push(0x07);
|
||||
buf.extend_from_slice(&bits.to_le_bytes());
|
||||
buf
|
||||
}
|
||||
|
||||
/// Menu 6 body: u32 enum index | u8 t.
|
||||
pub fn enum_val(index: u32) -> Vec<u8> {
|
||||
let mut buf = Vec::new();
|
||||
buf.extend_from_slice(&index.to_le_bytes());
|
||||
buf.push(0x09);
|
||||
buf
|
||||
}
|
||||
|
||||
/// Menu 7 big-endian body: u32 hdr | u16 vals[0] | u16 vals[1].
|
||||
pub fn big_endian(hdr: u32, a: u16, b: u16) -> Vec<u8> {
|
||||
let mut buf = Vec::new();
|
||||
buf.extend_from_slice(&hdr.to_be_bytes());
|
||||
buf.extend_from_slice(&a.to_be_bytes());
|
||||
buf.extend_from_slice(&b.to_be_bytes());
|
||||
buf
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn decode_lands_on_the_intended_variants() {
|
||||
fn decode(data: &[u8]) -> (String, bool, String, usize) {
|
||||
let input = PairInput::arbitrary_take_rest(arbitrary::Unstructured::new(data))
|
||||
.expect("the encoding decodes");
|
||||
let lane = match &input.lane {
|
||||
DocLane::Menu(i) => format!("M{i}"),
|
||||
DocLane::Raw(_) => "Raw".to_string(),
|
||||
};
|
||||
let shape = match input.shape {
|
||||
Shape::Raw => "Raw".to_string(),
|
||||
Shape::Empty => "Empty".to_string(),
|
||||
Shape::Truncate(n) => format!("T{n}"),
|
||||
Shape::Padded => "P".to_string(),
|
||||
};
|
||||
(lane, input.mode, shape, input.buffer.len())
|
||||
}
|
||||
|
||||
let seed =
|
||||
enc::pair(enc::lane_menu(3), true, enc::shape_truncate(5), &[9, 9, 9]);
|
||||
assert_eq!(decode(&seed), ("M3".to_string(), true, "T5".to_string(), 3));
|
||||
let seed = enc::pair(
|
||||
enc::lane_raw(br#"{"$defs":{}}"#),
|
||||
false,
|
||||
enc::shape_field(enc::SHAPE_PADDED),
|
||||
&[1],
|
||||
);
|
||||
assert_eq!(decode(&seed), ("Raw".to_string(), false, "P".to_string(), 1));
|
||||
let seed = enc::pair(enc::lane_menu(0), false, enc::shape_field(enc::SHAPE_RAW), &[]);
|
||||
assert_eq!(decode(&seed), ("M0".to_string(), false, "Raw".to_string(), 0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn valid_valid_both_modes_agree_and_validate() {
|
||||
// Menu 0 packed with a complete body: validate Ok, independent
|
||||
// materialize agrees, leaves read back, serde round-trips.
|
||||
fuzz_validate_pair(&enc::pair(
|
||||
enc::lane_menu(0),
|
||||
false,
|
||||
enc::shape_field(enc::SHAPE_RAW),
|
||||
&buffers::sandwich_packed(b"hi"),
|
||||
));
|
||||
// Menu 1 (maxLength reservation) in aligned mode.
|
||||
fuzz_validate_pair(&enc::pair(
|
||||
enc::lane_menu(1),
|
||||
true,
|
||||
enc::shape_field(enc::SHAPE_RAW),
|
||||
&buffers::sandwich_aligned(b"hi"),
|
||||
));
|
||||
// Menu 2 aligned offset-indirect: the full pair contract.
|
||||
fuzz_validate_pair(&enc::pair(
|
||||
enc::lane_menu(2),
|
||||
true,
|
||||
enc::shape_field(enc::SHAPE_RAW),
|
||||
&buffers::indirect(b"data"),
|
||||
));
|
||||
// Menu 3 packed record with two entries.
|
||||
fuzz_validate_pair(&enc::pair(
|
||||
enc::lane_menu(3),
|
||||
false,
|
||||
enc::shape_field(enc::SHAPE_RAW),
|
||||
&buffers::record(2, &[(b"k1", 5), (b"k2", 6)]),
|
||||
));
|
||||
// Menu 4 packed union (disc 0 → Small). Valid in packed.
|
||||
fuzz_validate_pair(&enc::pair(
|
||||
enc::lane_menu(4),
|
||||
false,
|
||||
enc::shape_field(enc::SHAPE_RAW),
|
||||
&buffers::union(0, 0x33),
|
||||
));
|
||||
// Menu 7 big-endian fixed shape, both modes.
|
||||
let be = buffers::big_endian(0x0102_0304, 0x0A0B, 0x0C0D);
|
||||
fuzz_validate_pair(&enc::pair(enc::lane_menu(7), false, enc::shape_field(enc::SHAPE_RAW), &be));
|
||||
fuzz_validate_pair(&enc::pair(enc::lane_menu(7), true, enc::shape_field(enc::SHAPE_RAW), &be));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hostile_schema_with_valid_bytes_is_a_clean_rejection() {
|
||||
// Menus 8/9 never compile; the byte lane must not be driven.
|
||||
fuzz_validate_pair(&enc::pair(
|
||||
enc::lane_menu(8),
|
||||
false,
|
||||
enc::shape_field(enc::SHAPE_RAW),
|
||||
&buffers::sandwich_packed(b"hi"),
|
||||
));
|
||||
fuzz_validate_pair(&enc::pair(
|
||||
enc::lane_menu(9),
|
||||
true,
|
||||
enc::shape_field(enc::SHAPE_RAW),
|
||||
&[0xAA; 8],
|
||||
));
|
||||
// Raw-lane hostile doc with a valid sibling buffer.
|
||||
fuzz_validate_pair(&enc::pair(
|
||||
enc::lane_raw(br#"{"$defs":{}}"#),
|
||||
false,
|
||||
enc::shape_field(enc::SHAPE_RAW),
|
||||
&buffers::sandwich_packed(b"hi"),
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn valid_schema_with_hostile_bytes_agrees_on_rejection() {
|
||||
// Truncation sweep over the packed sandwich: every prefix must
|
||||
// materialize-fail with the error propagated verbatim by
|
||||
// validate_bytes.
|
||||
let body = buffers::sandwich_packed(b"hello");
|
||||
for n in 1..body.len() {
|
||||
fuzz_validate_pair(&enc::pair(
|
||||
enc::lane_menu(0),
|
||||
false,
|
||||
enc::shape_truncate(n as u8),
|
||||
&body,
|
||||
));
|
||||
}
|
||||
// Aligned truncated at the record/union boundaries (empty +
|
||||
// padded shapes over menu 2 and 3).
|
||||
fuzz_validate_pair(&enc::pair(
|
||||
enc::lane_menu(2),
|
||||
true,
|
||||
enc::shape_field(enc::SHAPE_EMPTY),
|
||||
&buffers::indirect(b"data"),
|
||||
));
|
||||
fuzz_validate_pair(&enc::pair(
|
||||
enc::lane_menu(3),
|
||||
false,
|
||||
enc::shape_field(enc::SHAPE_PADDED),
|
||||
&buffers::record(2, &[(b"k1", 5), (b"k2", 6)]),
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_aligned_sandwich_uses_the_documented_offsets() {
|
||||
// Pin the aligned layout offsets of menu 1 so the
|
||||
// `sandwich_aligned` fixture stays honest across engine
|
||||
// changes: a (u8), n (u32 aligned to 4), s reservation
|
||||
// (maxLength 64 reserved inline at the entry, 8..72), tail at
|
||||
// 72. The prefix sits at the reservation start; the string
|
||||
// data follows it.
|
||||
let doc: serde_json::Value = serde_json::from_str(MENUS[1])
|
||||
.expect("menu 1 parses");
|
||||
let engine = AlkTypeEngine::compile(&doc, "S", LayoutMode::Aligned, None)
|
||||
.expect("menu 1 compiles aligned");
|
||||
let map = engine.offset_map().expect("aligned engine");
|
||||
assert_eq!(map.get("a").expect("a").range.start, 0);
|
||||
assert_eq!(map.get("n").expect("n").range.start, 4);
|
||||
let s = map.get("s").expect("s");
|
||||
assert_eq!(s.range, alktype::ByteRange { start: 8, end: 72 });
|
||||
assert_eq!(map.get("tail").expect("tail").range.start, 72);
|
||||
assert_eq!(map.total_size(), 76);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn non_finite_floats_are_access_not_silent_values() {
|
||||
// NaN through materialize must be an Access error naming the
|
||||
// reason — never a silent 0.0/Null in the output (the plan's
|
||||
// §3 target-5 non-finite invariant, src/materialize.rs's
|
||||
// number_from_f64).
|
||||
fuzz_validate_pair(&enc::pair(
|
||||
enc::lane_menu(5),
|
||||
false,
|
||||
enc::shape_field(enc::SHAPE_RAW),
|
||||
&buffers::float(0x7FF8_0000_0000_0000), // f64 NaN
|
||||
));
|
||||
fuzz_validate_pair(&enc::pair(
|
||||
enc::lane_menu(5),
|
||||
true,
|
||||
enc::shape_field(enc::SHAPE_RAW),
|
||||
&buffers::float(0x7FF0_0000_0000_0000), // f64 Inf
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn out_of_range_enum_is_a_validation_error_not_materialization_loss() {
|
||||
// Enum index 99: bytes decode fine (u32), materialize Ok, the
|
||||
// plan walk must reject with Validation — and validate_bytes
|
||||
// must surface exactly that.
|
||||
fuzz_validate_pair(&enc::pair(
|
||||
enc::lane_menu(6),
|
||||
false,
|
||||
enc::shape_field(enc::SHAPE_RAW),
|
||||
&buffers::enum_val(99),
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hostile_record_count_fails_fast_with_access() {
|
||||
// The §6 candidate-1 wave-3 form: count far above the wire can
|
||||
// afford. materialize (both directly and through
|
||||
// validate_bytes) must fail fast with Access naming the
|
||||
// record — the spin bound lives in the target's walk bound on
|
||||
// the sequential side and in this fixture's fast-fail on the
|
||||
// materialize side.
|
||||
fuzz_validate_pair(&enc::pair(
|
||||
enc::lane_menu(3),
|
||||
false,
|
||||
// Truncate right after the count: the first iteration has
|
||||
// nothing to verify.
|
||||
enc::shape_truncate(8),
|
||||
&{
|
||||
let mut buf =
|
||||
buffers::record(0xFFFF_FFFF, &[]);
|
||||
buf.truncate(8);
|
||||
buf
|
||||
},
|
||||
));
|
||||
// count = 65536 with zero everything: every iteration must
|
||||
// verify ≥ 4 bytes, so it fails inside the byte budget.
|
||||
fuzz_validate_pair(&enc::pair(
|
||||
enc::lane_menu(3),
|
||||
false,
|
||||
enc::shape_field(enc::SHAPE_RAW),
|
||||
&{
|
||||
let mut buf = buffers::record(0x0001_0000, &[]);
|
||||
buf.extend_from_slice(&[0u8; 16]);
|
||||
buf
|
||||
},
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn packed_lane_rejects_read_field_and_union_aligns_reject() {
|
||||
// Mode-agreement pins at the harness level: menu 4 (TUnion)
|
||||
// compiles packed but must fail aligned (ADR-008); menu 0 must
|
||||
// fail aligned (ADR-006); both must compile-packed with a
|
||||
// valid body.
|
||||
fuzz_validate_pair(&enc::pair(
|
||||
enc::lane_menu(4),
|
||||
true,
|
||||
enc::shape_field(enc::SHAPE_RAW),
|
||||
&buffers::union(0, 0x33),
|
||||
));
|
||||
fuzz_validate_pair(&enc::pair(
|
||||
enc::lane_menu(0),
|
||||
true,
|
||||
enc::shape_field(enc::SHAPE_RAW),
|
||||
&buffers::sandwich_packed(b"hi"),
|
||||
));
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user