use crate::adapter::fuzz_common::{run_structural_fuzz, FuzzOutcome};
use crate::adapter::tier1::tests::{synth_split, SplitKind};
use arbitrary::{Arbitrary, Unstructured};
use cviz::model::{
FuncSignature, InstanceInterface, InterfaceType, TypeArena, ValueType, ValueTypeId,
};
use std::collections::BTreeMap;
const FUZZ_MAX_DEPTH: u32 = 1;
const FUZZ_TARGET: &str = "test:fuzz/iface@1.0.0";
const FUZZ_HOOKS: &[&str] = &["splicer:tier2/before", "splicer:tier2/after"];
fn sig(
is_async: bool,
param_names: &[&str],
params: Vec<ValueTypeId>,
results: Vec<ValueTypeId>,
) -> FuncSignature {
FuncSignature {
is_async,
param_names: param_names.iter().map(|s| s.to_string()).collect(),
params,
results,
}
}
fn fuzz_primitive(u: &mut Unstructured<'_>) -> arbitrary::Result<ValueType> {
let ctors: &[fn() -> ValueType] = &[
|| ValueType::Bool,
|| ValueType::S8,
|| ValueType::U8,
|| ValueType::S16,
|| ValueType::U16,
|| ValueType::S32,
|| ValueType::U32,
|| ValueType::S64,
|| ValueType::U64,
|| ValueType::F32,
|| ValueType::F64,
|| ValueType::Char,
|| ValueType::String,
];
Ok(ctors[u.choose_index(ctors.len())?]())
}
fn fuzz_value_type(
u: &mut Unstructured<'_>,
arena: &mut TypeArena,
depth: u32,
need_export: &mut Vec<ValueTypeId>,
) -> arbitrary::Result<ValueTypeId> {
if depth == 0 {
return Ok(arena.intern_val(fuzz_primitive(u)?));
}
match u.choose_index(11)? {
0 => Ok(arena.intern_val(fuzz_primitive(u)?)),
1 => {
let inner = fuzz_value_type(u, arena, depth - 1, need_export)?;
Ok(arena.intern_val(ValueType::List(inner)))
}
2 => {
let inner = fuzz_value_type(u, arena, depth - 1, need_export)?;
let n = u.int_in_range::<u32>(1..=4)?;
Ok(arena.intern_val(ValueType::FixedSizeList(inner, n)))
}
3 => {
let count = u.int_in_range(2..=3)?;
let mut ids = Vec::with_capacity(count);
for _ in 0..count {
ids.push(fuzz_value_type(u, arena, depth - 1, need_export)?);
}
Ok(arena.intern_val(ValueType::Tuple(ids)))
}
4 => {
let inner = fuzz_value_type(u, arena, depth - 1, need_export)?;
Ok(arena.intern_val(ValueType::Option(inner)))
}
5 => {
let ok = if bool::arbitrary(u)? {
Some(fuzz_value_type(u, arena, depth - 1, need_export)?)
} else {
None
};
let err = if bool::arbitrary(u)? {
Some(fuzz_value_type(u, arena, depth - 1, need_export)?)
} else {
None
};
Ok(arena.intern_val(ValueType::Result { ok, err }))
}
6 => {
let count = u.int_in_range(1..=3)?;
let mut fields = Vec::with_capacity(count);
for i in 0..count {
let fid = fuzz_value_type(u, arena, depth - 1, need_export)?;
fields.push((format!("f{i}"), fid));
}
let id = arena.intern_val(ValueType::Record(fields));
need_export.push(id);
Ok(id)
}
7 => {
let count = u.int_in_range(1..=3)?;
let mut cases = Vec::with_capacity(count);
for i in 0..count {
let payload = if bool::arbitrary(u)? {
Some(fuzz_value_type(u, arena, depth - 1, need_export)?)
} else {
None
};
cases.push((format!("c{i}"), payload));
}
let id = arena.intern_val(ValueType::Variant(cases));
need_export.push(id);
Ok(id)
}
8 => {
let count = u.int_in_range(1..=4)?;
let tags: Vec<String> = (0..count).map(|i| format!("t{i}")).collect();
let id = arena.intern_val(ValueType::Enum(tags));
need_export.push(id);
Ok(id)
}
9 => {
let count = u.int_in_range::<usize>(1..=8)?;
let labels: Vec<String> = (0..count).map(|i| format!("fl{i}")).collect();
let id = arena.intern_val(ValueType::Flags(labels));
need_export.push(id);
Ok(id)
}
_ => Ok(arena.intern_val(fuzz_primitive(u)?)),
}
}
fn fuzz_is_expected_bail(msg: &str) -> bool {
msg.contains("flat-slot count")
|| msg.contains("cell count")
|| msg.contains("exceeds i32 budget")
|| msg.contains("exceeds tier-2 layout budget")
|| msg.contains("requires N >= 1")
|| msg.contains("MAX_FLAT_ASYNC_PARAMS")
|| msg.contains("unsupported type")
|| msg.contains("interface has no functions")
|| (msg.contains("declares resource") && msg.contains("inline"))
|| msg.contains("ComponentEncoder")
}
fn gen_tier2_adapter(
target: &str,
hooks: &[String],
iface: &InterfaceType,
arena: &TypeArena,
) -> anyhow::Result<Vec<u8>> {
let tmp = tempfile::tempdir().unwrap();
let split = synth_split(target, iface, arena, SplitKind::Consumer);
let split_path = split.path().to_str().unwrap();
let path = crate::adapter::generate_tier2_adapter(
"fuzz-mdl",
target,
hooks,
tmp.path().to_str().unwrap(),
split_path,
)?;
std::fs::read(&path).map_err(Into::into)
}
#[test]
fn fuzz_structural_shapes() {
run_structural_fuzz("tier2-fuzz", |bytes| {
let mut u = Unstructured::new(bytes);
let mut arena = TypeArena::default();
let mut need_export: Vec<ValueTypeId> = Vec::new();
let result_id = fuzz_value_type(&mut u, &mut arena, FUZZ_MAX_DEPTH, &mut need_export)
.map_err(|_| "ran out of random bytes".to_string())?;
let shape = arena.canonical_val(result_id);
let type_exports: BTreeMap<String, ValueTypeId> = need_export
.iter()
.enumerate()
.map(|(idx, id)| (format!("ty{idx}"), *id))
.collect();
let iface = InterfaceType::Instance(InstanceInterface {
functions: BTreeMap::from([(
"get".to_string(),
sig(true, &[], vec![], vec![result_id]),
)]),
type_exports,
});
let hooks: Vec<String> = FUZZ_HOOKS.iter().map(|s| s.to_string()).collect();
match gen_tier2_adapter(FUZZ_TARGET, &hooks, &iface, &arena) {
Ok(bytes) => {
let mut validator =
wasmparser::Validator::new_with_features(wasmparser::WasmFeatures::all());
validator
.validate_all(&bytes)
.map_err(|e| format!("invalid component for shape `{shape}`: {e}"))?;
Ok(FuzzOutcome::Passed)
}
Err(e) => {
let msg = format!("{e:#}");
if fuzz_is_expected_bail(&msg) {
Ok(FuzzOutcome::ExpectedBail)
} else {
Err(format!("unexpected bail for shape `{shape}`: {msg}"))
}
}
}
});
}