wave 7: fuzz sidecar scaffold (fuzz/ workspace, shared crate, runner, dict, seeds skeleton)

This commit is contained in:
glm-5.3-flash committed 2026-10-10 21:02:06 +00:00
1 parent 67d1d7e34f
commit 62ff18d1ed
14 files changed
+614 -15

No files matched your search

+13
View File
@@ -0,0 +1,13 @@
[package]
name = "alkstore-fuzz-shared"
version = "0.0.0"
publish = false
edition = "2021"
[dependencies]
alkstore = { path = "../../alkstore" }
serde_json = "1"
[dependencies.arbitrary]
version = "1"
features = ["derive"]
+108
View File
@@ -0,0 +1,108 @@
//! `Arbitrary` support for `serde_json::Value` — a bounded-depth
//! recursive generator shared by the semantic targets. Depth and size
//! bounds keep exec/s high (serde_json's own 128-depth recursion limit
//! is the decode-side guard; this generator explores structure).
use arbitrary::{Arbitrary, Unstructured};
use serde_json::{Map, Number, Value};
#[derive(Debug, arbitrary::Arbitrary, Clone)]
#[allow(dead_code)]
enum JsonShape {
Null,
Bool(bool),
Number {
mantissa: u32,
negative: bool,
},
String {
len: u8,
byte: u8,
},
Array {
len: u8,
items: Vec<JsonShape>,
},
Object {
len: u8,
keys: Vec<(u8, u8)>,
values: Vec<JsonShape>,
},
}
/// Generate a `Value` with bounded depth: arrays/objects nest at most
/// 6 levels, each holding at most 6 items. The `u8`-bounded shapes
/// keep the generator cheap; the semantic target's value space is
/// structure, not size.
impl<'a> Arbitrary<'a> for JsonValue {
fn arbitrary(u: &mut Unstructured<'a>) -> arbitrary::Result<Self> {
let shape = JsonShape::arbitrary(u)?;
Ok(JsonValue(shape_to_value(shape, 6)))
}
}
fn shape_to_value(shape: JsonShape, depth: u32) -> Value {
if depth == 0 {
return Value::Null;
}
match shape {
JsonShape::Null => Value::Null,
JsonShape::Bool(b) => Value::Bool(b),
JsonShape::Number { mantissa, negative } => {
let n = i64::from(mantissa % 1_000_000);
Number::from(if negative { -n } else { n }).into()
}
JsonShape::String { len, byte } => {
// Repeat a printable-ish char; embedded control bytes and
// invalid UTF-8 come from the fuzzer's byte mutations of
// the raw input in the payload targets, not here.
let n = (len % 64) as usize;
std::iter::repeat_n((0x41u8 + (byte % 26)) as char, n)
.collect::<String>()
.into()
}
JsonShape::Array { len, items } => {
let items: Vec<Value> = items
.into_iter()
.take(6)
.map(|s| shape_to_value(s, depth - 1))
.collect();
let _ = len;
Value::Array(items)
}
JsonShape::Object { keys, values, .. } => {
let mut map = Map::new();
for (i, (k_len, k_byte)) in keys.into_iter().take(6).enumerate() {
let key: String = std::iter::repeat_n(
(0x61u8 + (k_byte % 26)) as char,
(k_len % 16) as usize + 1,
)
.collect();
let value = values
.get(i)
.map(|s| shape_to_value(s.clone(), depth - 1))
.unwrap_or(Value::Null);
map.insert(key, value);
}
Value::Object(map)
}
}
}
/// Newtype so `impl Arbitrary` applies (serde_json re-exports `Value`
/// as `serde_json::Value`; a foreign-type impl is not allowed).
#[derive(Debug, Clone, PartialEq)]
pub struct JsonValue(pub Value);
impl std::ops::Deref for JsonValue {
type Target = Value;
fn deref(&self) -> &Value {
&self.0
}
}
impl From<JsonValue> for Value {
fn from(value: JsonValue) -> Value {
value.0
}
}
+6
View File
@@ -0,0 +1,6 @@
//! Invariant logic shared by alkstore's fuzz targets. Kept out of the
//! fuzz-target binaries so the corpus replay unit tests can exercise the
//! same invariant checks against every committed corpus entry (the quinn
//! CI pattern) without a nightly toolchain.
pub mod arbitrary_value;