186 lines
5.9 KiB
Rust
186 lines
5.9 KiB
Rust
|
|
#![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()
|
||
|
|
}
|