miroir/fuzz/fuzz_targets/canonical_json.rs
jedarden 7be2a0e9ec feat(tests): add property tests and fuzz targets for router, config, and parsers (plan §8, P9.6)
This commit adds comprehensive property-based tests and fuzzing coverage for
critical router and parser invariants as specified in plan §8.

**Property Tests (proptest) - router.rs:**
- `prop_write_targets_count`: Verifies |write_targets| == RG × RF
- `prop_write_targets_rf_per_group`: Verifies each group contributes exactly RF nodes
- `prop_covering_set_covers_all_shards`: Verifies covering set includes all shards
- `prop_reshuffle_bound_on_add`: Verifies reshuffle bounded by 4 × RF × ceil(S / Ng_new)
- `prop_determinism`: Verifies same inputs produce same outputs
- `prop_shard_for_key_uniformity`: Verifies uniform key distribution across shards

**Fuzz Targets (cargo-fuzz):**
- `config_parser.rs`: Feeds random UTF-8 to Config::from_yaml
- `filter_parser.rs`: Feeds random strings to query planner filter grammar
- `canonical_json.rs`: Roundtrips random JSON through canonicalizer

**Bug Fixes:**
- Fixed missing `mode_b_type` import in mode_b_coordinator.rs
- Fixed missing `Arc` import in scatter.rs

All property tests pass at 1024 cases per property. Fuzz targets are ready
for integration with weekly CI fuzz runs via Argo Workflow.

Closes: miroir-89x.6
2026-05-24 11:41:48 -04:00

50 lines
1.7 KiB
Rust

#![no_main]
use libfuzzer_sys::fuzz_target;
use serde_json::Value;
use std::collections::BTreeMap;
use twox_hash::XxHash64;
use std::hash::{Hash, Hasher};
/// Canonicalize a JSON value by sorting object keys.
fn canonicalize_json(value: &Value) -> String {
match value {
Value::Object(map) => {
let sorted: BTreeMap<_, _> = map.iter().collect();
serde_json::to_string(&sorted).unwrap_or_else(|_| "{}".to_string())
}
_ => serde_json::to_string(value).unwrap_or_else(|_| "null".to_string()),
}
}
/// Compute hash of canonicalized JSON.
fn hash_canonical(value: &Value) -> u64 {
let canonical = canonicalize_json(value);
let mut hasher = XxHash64::with_seed(0);
hasher.write(canonical.as_bytes());
hasher.finish()
}
fuzz_target!(|data: &[u8]| {
// Try to parse as JSON
let json1 = match serde_json::from_slice::<Value>(data) {
Ok(v) => v,
Err(_) => return, // Skip invalid JSON
};
// Canonicalize and hash - should never panic
let hash1 = hash_canonical(&json1);
// Round-trip through canonical string and verify
let canonical = canonicalize_json(&json1);
if let Ok(json2) = serde_json::from_str::<Value>(&canonical) {
let hash2 = hash_canonical(&json2);
// Hashes must be identical after roundtrip
assert_eq!(hash1, hash2, "Canonical JSON roundtrip produced different hash");
}
// Verify that parsing the canonical string again produces the same hash
if let Ok(json3) = serde_json::from_str::<Value>(&canonical) {
let hash3 = hash_canonical(&json3);
assert_eq!(hash1, hash3, "Second canonicalization produced different hash");
}
});