json-archive/fuzz/fuzz_targets/fuzz_apply_move.rs

186 lines
5.9 KiB
Rust
Raw Normal View History

#![no_main]
use arbitrary::{Arbitrary, Unstructured};
use json_archive::apply_move;
use libfuzzer_sys::fuzz_target;
use serde_json::{json, Value};
#[derive(Arbitrary, Debug)]
struct FuzzMoveInput {
structure: FuzzStructure,
moves: Vec<(u8, u8)>,
}
#[derive(Arbitrary, Debug)]
enum FuzzStructure {
// Direct array at root path
RootArray(Vec<FuzzValue>),
// Object with array field
ObjectWithArray {
field_name: String,
array: Vec<FuzzValue>,
},
// Nested object with array
NestedArray {
outer_field: String,
inner_field: String,
array: Vec<FuzzValue>,
},
// Non-array value (should error)
NonArray(FuzzValue),
}
#[derive(Arbitrary, Debug, Clone)]
enum FuzzValue {
Null,
Bool(bool),
SmallInt(i8),
String(String),
// Limit recursion depth
Array(Vec<SimpleValue>),
Object(Vec<(String, SimpleValue)>),
}
#[derive(Arbitrary, Debug, Clone)]
enum SimpleValue {
Null,
Bool(bool),
SmallInt(i8),
String(String),
}
impl SimpleValue {
fn to_json(&self) -> Value {
match self {
SimpleValue::Null => Value::Null,
SimpleValue::Bool(b) => Value::Bool(*b),
SimpleValue::SmallInt(n) => json!(n),
SimpleValue::String(s) => Value::String(s.clone()),
}
}
}
impl FuzzValue {
fn to_json(&self) -> Value {
match self {
FuzzValue::Null => Value::Null,
FuzzValue::Bool(b) => Value::Bool(*b),
FuzzValue::SmallInt(n) => json!(n),
FuzzValue::String(s) => Value::String(s.clone()),
FuzzValue::Array(arr) => Value::Array(arr.iter().map(|v| v.to_json()).collect()),
FuzzValue::Object(obj) => {
let map: serde_json::Map<String, Value> =
obj.iter().map(|(k, v)| (k.clone(), v.to_json())).collect();
Value::Object(map)
}
}
}
}
impl FuzzStructure {
fn to_json_and_path(&self) -> (Value, String) {
match self {
FuzzStructure::RootArray(arr) => {
let json_arr = Value::Array(arr.iter().map(|v| v.to_json()).collect());
(json!({"root": json_arr}), "/root".to_string())
}
FuzzStructure::ObjectWithArray { field_name, array } => {
let json_arr = Value::Array(array.iter().map(|v| v.to_json()).collect());
let path = format!("/{}", escape_json_pointer(field_name));
(json!({ field_name.clone(): json_arr }), path)
}
FuzzStructure::NestedArray {
outer_field,
inner_field,
array,
} => {
let json_arr = Value::Array(array.iter().map(|v| v.to_json()).collect());
let path = format!(
"/{}/{}",
escape_json_pointer(outer_field),
escape_json_pointer(inner_field)
);
(
json!({ outer_field.clone(): { inner_field.clone(): json_arr } }),
path,
)
}
FuzzStructure::NonArray(val) => {
(json!({"value": val.to_json()}), "/value".to_string())
}
}
}
}
fn escape_json_pointer(s: &str) -> String {
s.replace('~', "~0").replace('/', "~1")
}
fuzz_target!(|data: &[u8]| {
let mut u = Unstructured::new(data);
if let Ok(input) = FuzzMoveInput::arbitrary(&mut u) {
let (mut state, path) = input.structure.to_json_and_path();
let original_state = state.clone();
// Get actual array from original state to compare against
let original_array = get_array_at_path(&original_state, &path).cloned();
// Convert moves to usize
let moves: Vec<(usize, usize)> = input
.moves
.iter()
.map(|(from, to)| (*from as usize, *to as usize))
.collect();
let result = apply_move(&mut state, &path, moves.clone());
match result {
Ok(()) => {
// If successful, verify invariants using actual arrays from JSON
let new_array = get_array_at_path(&state, &path);
if let (Some(orig_arr), Some(new_arr)) = (&original_array, new_array) {
// 1. Array length should be preserved
assert_eq!(
new_arr.len(),
orig_arr.len(),
"Array length changed after move: was {}, now {}",
orig_arr.len(),
new_arr.len()
);
// 2. All original elements should still exist (as a multiset)
let mut orig_sorted: Vec<_> =
orig_arr.iter().map(|v| v.to_string()).collect();
let mut new_sorted: Vec<_> = new_arr.iter().map(|v| v.to_string()).collect();
orig_sorted.sort();
new_sorted.sort();
assert_eq!(
orig_sorted, new_sorted,
"Elements were lost or duplicated during move"
);
}
}
Err(diag) => {
// Error is expected for:
// - Non-array targets
// - Out of bounds indices
// - Invalid paths
// Just make sure we got a proper diagnostic
assert!(!diag.description.is_empty());
}
}
}
});
fn get_array_at_path<'a>(state: &'a Value, path: &str) -> Option<&'a Vec<Value>> {
let parts: Vec<&str> = path.split('/').filter(|s| !s.is_empty()).collect();
let mut current = state;
for part in parts {
let unescaped = part.replace("~1", "/").replace("~0", "~");
current = current.get(&unescaped)?;
}
current.as_array()
}