fuzz-target-payload-codec: the §3.1 target — all five invariants in shared, thin fuzz_target wrapper, 97 committed seeds + gitignore negation + README row; placeholder bin replaced; serde (derive) added to shared

This commit is contained in:
glm-5.3-flash committed 2026-10-10 21:42:41 +00:00
1 parent 21ab6fe564
commit 1f6cc0970a
108 files changed
+583 -24

No files matched your search

+1
View File
@@ -6,6 +6,7 @@ edition = "2021"
[dependencies]
alkstore = { path = "../../alkstore" }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
[dependencies.arbitrary]
+1
View File
@@ -4,3 +4,4 @@
//! CI pattern) without a nightly toolchain.
pub mod arbitrary_value;
pub mod payload_codec;
+295
View File
@@ -0,0 +1,295 @@
//! Invariant logic for the payload codec target (ADR-020 §4 and
//! ADR-023 §1, `alkstore/src/payload.rs`): no-panic on any byte
//! sequence, the typed-failure partition (every decode failure is
//! `Error::Codec`), deep-input loudness at serde_json's recursion
//! limit, the `Value` round-trip with serde_json's canonicalization
//! pinned-not-regressed, and the typed round-trip for a
//! representative payload set.
use std::collections::BTreeMap;
use alkstore::{encode_payload, payload_as, Error};
use arbitrary::{Arbitrary, Unstructured};
use serde_json::{json, Map, Value};
use crate::arbitrary_value::JsonValue;
/// serde_json's default recursion limit — the depth at which decode
/// must fail loud and typed.
const RECURSION_LIMIT: usize = 128;
/// The representative typed payload for the `payload_as` side: a
/// struct in the shape consumers actually construct, covering
/// string/int/vec/option/map fields.
#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
struct TestJobPayload {
queue: String,
attempt: i64,
tags: Vec<String>,
note: Option<String>,
meta: BTreeMap<String, Value>,
}
fn test_payload_batteries() -> Vec<TestJobPayload> {
vec![
TestJobPayload {
queue: "orders".to_owned(),
attempt: 3,
tags: vec!["alpha".to_owned(), "beta".to_owned()],
note: Some("queued by the bridge".to_owned()),
meta: BTreeMap::from([
("source".to_owned(), json!("outbox")),
("count".to_owned(), json!(7)),
]),
},
TestJobPayload {
queue: String::new(),
attempt: i64::MIN,
tags: Vec::new(),
note: None,
meta: BTreeMap::new(),
},
TestJobPayload {
queue: "unicode-é-😀".to_owned(),
attempt: i64::MAX,
tags: vec!["tab\tquote\"slash\\/".to_owned()],
note: Some(String::new()),
meta: BTreeMap::from([("nested".to_owned(), json!([1, null, true]))]),
},
]
}
pub fn fuzz_payload_codec(data: &[u8]) {
// The baseline: no panic on any byte sequence, and every decode
// failure across the input space is the typed `Error::Codec` —
// over `Value` and over representative typed targets alike.
assert_decode_partition::<Value>(data);
assert_decode_partition::<TestJobPayload>(data);
assert_decode_partition::<String>(data);
assert_decode_partition::<Vec<Value>>(data);
// A successfully-decoded `Value` re-encodes byte-stably: the Value
// round-trip closes over the raw input, with serde_json's
// canonicalization (duplicate-key last-write-wins, number-to-NormalizedNumber,
// key ordering) pinned — not re-derived, just not regressed.
if let Ok(v) = payload_as::<Value>(data) {
let bytes = encode_payload(&v).expect("a decoded `Value` must re-encode");
let back: Value = payload_as(&bytes).expect("the encoded `Value` must decode");
assert_eq!(back, v, "the `Value` round-trip must close over raw input");
}
assert_deep_input_loudness();
assert_empty_byte_store_loudness();
assert_value_round_trip_battery();
assert_typed_round_trip_battery();
assert_arbitrary_value_round_trip(data);
}
fn assert_decode_partition<T: serde::de::DeserializeOwned>(data: &[u8]) {
match payload_as::<T>(data) {
Ok(_) | Err(Error::Codec(_)) => {}
Err(other) => panic!("the decode failure must be `Error::Codec`, got {other:?}"),
}
}
/// Deep inputs decode loud and typed: past serde_json's recursion
/// limit, deserialization fails with `Error::Codec` — never a stack
/// overflow, never an untyped panic. The encode side handles deep
/// (but sub-limit) data as the normal round-trip.
fn assert_deep_input_loudness() {
for depth in [RECURSION_LIMIT * 4, 4096] {
let deep_bytes = nested_array_bytes(depth);
match payload_as::<Value>(&deep_bytes) {
Err(Error::Codec(_)) => {}
Err(other) => {
panic!("deep deserialization failure must be `Error::Codec`, got {other:?}")
}
Ok(_) => panic!("nesting at depth {depth} must hit serde_json's recursion limit"),
}
match payload_as::<TestJobPayload>(&deep_bytes) {
Err(Error::Codec(_)) => {}
Err(other) => {
panic!("deep typed deserialization must be `Error::Codec`, got {other:?}")
}
Ok(_) => panic!("deep bytes must not decode to the typed target at depth {depth}"),
}
let deep_value = nest_value(Value::Null, RECURSION_LIMIT + 1);
let deep_encoded =
encode_payload(&deep_value).expect("a sub-limit-depth `Value` must encode");
match payload_as::<Value>(&deep_encoded) {
Err(Error::Codec(_)) => {}
Err(other) => {
panic!("the stored deep form must decode as typed `Codec`, got {other:?}")
}
Ok(_) => {
panic!("stored bytes nested past the decode limit must decode loud, not silent")
}
}
}
let shallow_bytes = nested_array_bytes(8);
let shallow_value = nest_value(Value::from(0u8), 8);
let shallow_encoded = encode_payload(&shallow_value).expect("a shallow `Value` must encode");
let back: Value = payload_as(&shallow_encoded).expect("the shallow form must decode");
let shallow_parsed: Value = payload_as(&shallow_bytes).expect("the shallow doc must decode");
assert_eq!(back, shallow_value, "the shallow `Value` round-trip holds");
assert_eq!(shallow_parsed, shallow_value, "nesting shape is preserved");
}
fn assert_empty_byte_store_loudness() {
match payload_as::<Value>(b"") {
Err(Error::Codec(_)) => {}
Err(other) => {
panic!("the empty-byte-store failure must be `Codec`, got {other:?}")
}
Ok(_) => panic!("the empty-byte store must never decode to a silent `Null` (ADR-023 §1)"),
}
}
fn assert_value_round_trip_battery() {
for v in value_battery() {
let bytes = encode_payload(&v).expect(
"encode_payload is only type-theoretically fallible; `Value` variants all serialize",
);
let back: Value = payload_as(&bytes).expect("the encoded battery `Value` must decode");
assert_eq!(back, v, "the `Value` round-trip must be exact");
}
}
fn assert_typed_round_trip_battery() {
for payload in test_payload_batteries() {
assert_typed_round_trip(&payload);
}
assert_typed_round_trip(&String::from("hello"));
assert_typed_round_trip(&String::new());
assert_typed_round_trip(&String::from("unicode é 😀"));
assert_typed_round_trip(&42i64);
assert_typed_round_trip(&i64::MAX);
assert_typed_round_trip(&i64::MIN);
assert_typed_round_trip(&true);
assert_typed_round_trip(&());
assert_typed_round_trip(&Option::<String>::None);
assert_typed_round_trip(&Some(String::from("some")));
assert_typed_round_trip(&vec![1i64, -2, 3]);
assert_typed_round_trip(&Vec::<Value>::new());
assert_typed_round_trip(&BTreeMap::from([
(String::from("a"), String::from("1")),
(String::from("b"), String::from("2")),
]));
}
fn assert_typed_round_trip<T>(payload: &T)
where
T: serde::Serialize + serde::de::DeserializeOwned + PartialEq + std::fmt::Debug,
{
let value =
serde_json::to_value(payload).expect("the battery payload must serialize to a `Value`");
let bytes = encode_payload(&value).expect("the battery `Value` must encode");
let back: T = match payload_as(&bytes) {
Ok(payload) => payload,
Err(other) => panic!("the typed round-trip decode holds, got {other:?}"),
};
assert_eq!(&back, payload, "the typed round-trip must be exact");
}
fn assert_arbitrary_value_round_trip(data: &[u8]) {
let mut u = Unstructured::new(data);
for _ in 0..2 {
let Ok(jv) = JsonValue::arbitrary(&mut u) else {
return;
};
let v: Value = jv.into();
let bytes = encode_payload(&v).expect("any arbitrary-derived `Value` must encode");
let back: Value = payload_as(&bytes).expect("the encoded arbitrary `Value` must decode");
assert_eq!(
back, v,
"the `Value` round-trip must hold for every arbitrary `Value`"
);
match payload_as::<TestJobPayload>(&bytes) {
Ok(typed) => {
let retrip = serde_json::to_value(&typed)
.expect("a decoded typed payload must re-serialize");
let bytes2 =
encode_payload(&retrip).expect("the re-serialized typed payload must encode");
let back: TestJobPayload =
payload_as(&bytes2).expect("the typed re-encoded form must decode");
assert_eq!(back, typed, "the typed round-trip is stable");
}
Err(Error::Codec(_)) => {}
Err(other) => {
panic!("the typed decode of arbitrary `Value` bytes must be `Codec`, got {other:?}")
}
}
}
}
fn value_battery() -> Vec<Value> {
vec![
Value::Null,
json!(true),
json!(false),
json!(0),
json!(-1),
json!(i64::MIN),
json!(i64::MAX),
Value::from(u64::MAX),
json!(1.5),
json!(-2.25),
json!(1e10),
json!(""),
json!("hello world"),
json!("unicode é 😀"),
json!("tab\tnewline\nquote\"slash\\/"),
json!("null"),
Value::Array(vec![]),
json!([1, "two", null, true, {"three": 3}, []]),
Value::Object(Map::new()),
json!({"a": 1, "nested": {"deep": [true, null, 1.5]}}),
json!({"unicode-é-key": "value"}),
]
}
fn nested_array_bytes(depth: usize) -> Vec<u8> {
let mut s = String::with_capacity(depth * 2 + 1);
s.push_str(&"[".repeat(depth));
s.push('0');
s.push_str(&"]".repeat(depth));
s.into_bytes()
}
fn nest_value(inner: Value, depth: usize) -> Value {
let mut v = inner;
for _ in 0..depth {
v = Value::Array(vec![v]);
}
v
}
#[cfg(test)]
mod corpus_replay {
use super::*;
use std::path::PathBuf;
#[test]
fn committed_corpus_replays_through_the_invariants() {
let corpus = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../corpus/payload_codec");
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_payload_codec(&data);
count += 1;
}
assert!(
count >= 64,
"committed seed corpus is present, found {count}"
);
}
#[test]
fn invariants_hold_without_corpus() {
fuzz_payload_codec(b"");
fuzz_payload_codec(b"null");
fuzz_payload_codec(&nested_array_bytes(RECURSION_LIMIT + 1));
}
}