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
@@ -42,4 +42,11 @@ test = false
|
||||
doc = false
|
||||
bench = false
|
||||
|
||||
[[bin]]
|
||||
name = "validate_pair"
|
||||
path = "fuzz_targets/validate_pair.rs"
|
||||
test = false
|
||||
doc = false
|
||||
bench = false
|
||||
|
||||
[workspace]
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,7 @@
|
||||
#![no_main]
|
||||
|
||||
use libfuzzer_sys::fuzz_target;
|
||||
|
||||
fuzz_target!(|data: &[u8]| {
|
||||
alktype_fuzz_shared::validate_pair::fuzz_validate_pair(data);
|
||||
});
|
||||
+174
-1
@@ -468,13 +468,186 @@ def layout_build_seeds():
|
||||
return i
|
||||
|
||||
|
||||
def validate_pair_seeds():
|
||||
# DocLane has 2 variants: Menu(u8), Raw(Vec<u8>). Shape has 4:
|
||||
# Raw, Empty, Truncate(u8), Padded.
|
||||
# PairInput = { lane: DocLane, mode: bool, shape: Shape,
|
||||
# buffer: Vec<u8> } — buffer is the take-rest field.
|
||||
# mode byte: 0 = packed, 1 = aligned.
|
||||
LANE = {name: variant_u32(i, 2) for i, name in enumerate(["menu", "raw"])}
|
||||
SHAPE = {name: variant_u32(i, 4) for i, name in enumerate(
|
||||
["raw", "empty", "trunc", "padded"])}
|
||||
|
||||
def shape(name, payload=None):
|
||||
b = bytearray(SHAPE[name])
|
||||
if name == "trunc":
|
||||
b.append(payload if payload is not None else 0)
|
||||
return bytes(b)
|
||||
|
||||
def pair(lane_bytes, aligned, shape_bytes, buffer):
|
||||
out = bytearray(lane_bytes)
|
||||
out.append(1 if aligned else 0)
|
||||
out.extend(shape_bytes)
|
||||
for b in buffer:
|
||||
out.append(0x01)
|
||||
out.append(b)
|
||||
out.append(0x00)
|
||||
return bytes(out)
|
||||
|
||||
def menu_lane(index):
|
||||
b = bytearray(LANE["menu"])
|
||||
b.append(index)
|
||||
return bytes(b)
|
||||
|
||||
def raw_lane(doc):
|
||||
b = bytearray(LANE["raw"])
|
||||
if isinstance(doc, str):
|
||||
doc = doc.encode()
|
||||
for byte in doc:
|
||||
b.append(0x01)
|
||||
b.append(byte)
|
||||
b.append(0x00)
|
||||
return bytes(b)
|
||||
|
||||
# --- Buffer builders (all little-endian unless noted) ---
|
||||
|
||||
def sandwich_packed(s):
|
||||
# Menu 0/1 packed: u8 a | u32 n | string s | u8 tail.
|
||||
return (struct.pack("<B", 0x11) + struct.pack("<I", 7)
|
||||
+ struct.pack("<I", len(s)) + s + struct.pack("<B", 0x22))
|
||||
|
||||
def sandwich_aligned(s):
|
||||
# Menu 1 aligned: s is a fixed 64-byte maxLength reservation
|
||||
# inline at the entry (8..72, prefix at the start); offsets
|
||||
# pinned by the crate test: a@0, n@4, s@8..72, tail@72.
|
||||
buf = bytearray(12 + 64 + 1)
|
||||
buf[0] = 0x11
|
||||
buf[4:8] = struct.pack("<I", 7)
|
||||
buf[8:12] = struct.pack("<I", len(s))
|
||||
buf[12:12 + len(s)] = s
|
||||
buf[72] = 0x22
|
||||
return bytes(buf)
|
||||
|
||||
def indirect(data):
|
||||
# Menu 2 aligned offset-indirect: u32 id | u32 data_off | u32
|
||||
# data_len | data region at data_off = 12.
|
||||
buf = bytearray(12 + len(data))
|
||||
buf[0:4] = struct.pack("<I", 0x0B)
|
||||
buf[4:8] = struct.pack("<I", 12)
|
||||
buf[8:12] = struct.pack("<I", len(data))
|
||||
buf[12:] = data
|
||||
return bytes(buf)
|
||||
|
||||
def record(count, entries):
|
||||
# Menu 3 packed: u32 k | u32 count | entries(key-prefixed).
|
||||
buf = bytearray()
|
||||
buf += struct.pack("<I", 1)
|
||||
buf += struct.pack("<I", count)
|
||||
for key, value in entries:
|
||||
buf += struct.pack("<I", len(key)) + key + struct.pack("<I", value)
|
||||
return bytes(buf)
|
||||
|
||||
def union(disc, v):
|
||||
# Menu 4 packed: u16 pre | disc byte | variant u8.
|
||||
return struct.pack("<H", 0x0102) + struct.pack("<B", disc) + struct.pack("<B", v)
|
||||
|
||||
def float64(bits):
|
||||
# Menu 5: u8 tag | f64 bits.
|
||||
return struct.pack("<B", 0x07) + struct.pack("<Q", bits)
|
||||
|
||||
def enum_val(index):
|
||||
# Menu 6: u32 enum index | u8 t.
|
||||
return struct.pack("<I", index) + struct.pack("<B", 0x09)
|
||||
|
||||
def big_endian(hdr, a, b):
|
||||
# Menu 7 big-endian: u32 hdr | u16 vals[0] | u16 vals[1].
|
||||
return struct.pack(">I", hdr) + struct.pack(">H", a) + struct.pack(">H", b)
|
||||
|
||||
i = 0
|
||||
|
||||
def w(data):
|
||||
nonlocal i
|
||||
write_seed("validate_pair", i, data)
|
||||
i += 1
|
||||
|
||||
# valid/valid: both modes, the happy pair.
|
||||
w(pair(menu_lane(0), False, shape("raw"), sandwich_packed(b"hi")))
|
||||
w(pair(menu_lane(1), True, shape("raw"), sandwich_aligned(b"hi")))
|
||||
w(pair(menu_lane(2), True, shape("raw"), indirect(b"data")))
|
||||
w(pair(menu_lane(3), False, shape("raw"),
|
||||
record(2, [(b"k1", 5), (b"k2", 6)])))
|
||||
w(pair(menu_lane(4), False, shape("raw"), union(0, 0x33)))
|
||||
be = big_endian(0x01020304, 0x0A0B, 0x0C0D)
|
||||
w(pair(menu_lane(7), False, shape("raw"), be))
|
||||
w(pair(menu_lane(7), True, shape("raw"), be))
|
||||
|
||||
# hostile-schema/valid-bytes: the reject menus (8 empty, 9 cycle)
|
||||
# and a raw-lane hostile doc; the buffer must never be driven.
|
||||
w(pair(menu_lane(8), False, shape("raw"), sandwich_packed(b"hi")))
|
||||
w(pair(menu_lane(9), True, shape("raw"), b"\xAA" * 8))
|
||||
w(pair(raw_lane('{"$defs":{}}'), False, shape("raw"), sandwich_packed(b"hi")))
|
||||
# Raw-lane valid doc with the root hint (drives the fused lane's
|
||||
# happy path).
|
||||
w(pair(raw_lane('{"root":"S","$defs":{"S":{"kind":"struct","fields":'
|
||||
'[{"name":"x","kind":"uint8"}]}}}'),
|
||||
False, shape("raw"), struct.pack("<B", 0x2A)))
|
||||
|
||||
# valid-schema/hostile-bytes: truncation sweep at every prefix of
|
||||
# the packed sandwich body.
|
||||
body = sandwich_packed(b"hello")
|
||||
for n in range(1, len(body)):
|
||||
w(pair(menu_lane(0), False, shape("trunc", n), body))
|
||||
|
||||
# Empty buffer + padded buffer under record and indirect lanes.
|
||||
w(pair(menu_lane(3), False, shape("empty"),
|
||||
record(2, [(b"k1", 5), (b"k2", 6)])))
|
||||
w(pair(menu_lane(3), False, shape("padded"),
|
||||
record(2, [(b"k1", 5), (b"k2", 6)])))
|
||||
w(pair(menu_lane(2), True, shape("empty"), indirect(b"data")))
|
||||
w(pair(menu_lane(2), True, shape("padded"), indirect(b"data")))
|
||||
|
||||
# Non-finite floats: NaN and Inf must surface Access, never a
|
||||
# silent 0.0/Null.
|
||||
w(pair(menu_lane(5), False, shape("raw"),
|
||||
float64(0x7FF8000000000000)))
|
||||
w(pair(menu_lane(5), True, shape("raw"),
|
||||
float64(0x7FF0000000000000)))
|
||||
|
||||
# Out-of-range enum index: materialize Ok, plan walk Validation.
|
||||
w(pair(menu_lane(6), False, shape("raw"), enum_val(99)))
|
||||
w(pair(menu_lane(6), True, shape("raw"), enum_val(99)))
|
||||
|
||||
# The §6 candidate-1 record spin fixtures, wave-3 form:
|
||||
# count = u32::MAX truncated right after the count (first
|
||||
# iteration has nothing to verify → immediate Access).
|
||||
hostile = record(0xFFFFFFFF, [])
|
||||
w(pair(menu_lane(3), False, shape("trunc", 8), hostile[:8]))
|
||||
# Zero-key huge count: every iteration must verify ≥ 4 bytes.
|
||||
zk = record(0x00010000, [])
|
||||
zk += b"\x00" * 16
|
||||
w(pair(menu_lane(3), False, shape("raw"), zk))
|
||||
|
||||
# Mode-agreement pins: TUnion in aligned mode fails (ADR-008);
|
||||
# non-final inline string fails aligned (ADR-006); both packed OK.
|
||||
w(pair(menu_lane(4), True, shape("raw"), union(0, 0x33)))
|
||||
w(pair(menu_lane(0), True, shape("raw"), sandwich_packed(b"hi")))
|
||||
|
||||
# Aligned truncation sweep over the fixed-shape menu 7 body.
|
||||
for n in range(1, len(be)):
|
||||
w(pair(menu_lane(7), True, shape("trunc", n), be))
|
||||
|
||||
return i
|
||||
|
||||
|
||||
def main():
|
||||
n1 = bast_compile_seeds()
|
||||
n2 = data_access_seeds()
|
||||
n3 = read_opseq_seeds()
|
||||
n4 = layout_build_seeds()
|
||||
n5 = validate_pair_seeds()
|
||||
print(f"bast_compile: {n1} seeds, data_access: {n2} seeds, "
|
||||
f"read_opseq: {n3} seeds, layout_build: {n4} seeds")
|
||||
f"read_opseq: {n3} seeds, layout_build: {n4} seeds, "
|
||||
f"validate_pair: {n5} seeds")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -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