#![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), // Object with array field ObjectWithArray { field_name: String, array: Vec, }, // Nested object with array NestedArray { outer_field: String, inner_field: String, array: Vec, }, // 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), 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 = 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> { 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() }