use std::collections::hash_map::DefaultHasher;
use std::collections::{HashMap, HashSet};
use std::hash::{Hash, Hasher};
use serde::{Deserialize, Serialize};
use serde_json::Value;
use super::detection::{FuzzResponse, FuzzResponseResult};
use super::input::FuzzInput;
pub struct CoverageTracker {
seen_paths: HashSet<u64>,
edge_counts: HashMap<u64, u32>,
total_inputs: u64,
coverage_inputs: u64,
method_coverage: HashMap<String, HashSet<u64>>,
}
impl Default for CoverageTracker {
fn default() -> Self {
Self::new()
}
}
impl CoverageTracker {
pub fn new() -> Self {
Self {
seen_paths: HashSet::new(),
edge_counts: HashMap::new(),
total_inputs: 0,
coverage_inputs: 0,
method_coverage: HashMap::new(),
}
}
pub fn hash_response(&self, input: &FuzzInput, response: &FuzzResponse) -> u64 {
let mut hasher = DefaultHasher::new();
input.method.hash(&mut hasher);
match &response.result {
FuzzResponseResult::Success(v) => {
"success".hash(&mut hasher);
self.hash_json_structure(v, &mut hasher);
}
FuzzResponseResult::Error(e) => {
"error".hash(&mut hasher);
e.code.hash(&mut hasher);
e.message
.chars()
.take(50)
.collect::<String>()
.hash(&mut hasher);
}
FuzzResponseResult::Timeout => {
"timeout".hash(&mut hasher);
}
FuzzResponseResult::ConnectionLost(reason) => {
"connection_lost".hash(&mut hasher);
reason
.chars()
.take(20)
.collect::<String>()
.hash(&mut hasher);
}
FuzzResponseResult::ProcessExit(code) => {
"exit".hash(&mut hasher);
code.hash(&mut hasher);
}
}
let time_bucket = response.response_time_ms / 100; time_bucket.hash(&mut hasher);
hasher.finish()
}
#[allow(clippy::only_used_in_recursion)]
fn hash_json_structure(&self, value: &Value, hasher: &mut impl Hasher) {
match value {
Value::Null => "null".hash(hasher),
Value::Bool(_) => "bool".hash(hasher),
Value::Number(_) => "number".hash(hasher),
Value::String(_) => "string".hash(hasher),
Value::Array(arr) => {
"array".hash(hasher);
arr.len().hash(hasher);
for item in arr.iter().take(3) {
self.hash_json_structure(item, hasher);
}
}
Value::Object(obj) => {
"object".hash(hasher);
obj.len().hash(hasher);
let mut keys: Vec<_> = obj.keys().collect();
keys.sort();
for key in keys.iter().take(10) {
key.hash(hasher);
if let Some(v) = obj.get(*key) {
self.hash_json_structure(v, hasher);
}
}
}
}
}
pub fn record(&mut self, input: &FuzzInput, response: &FuzzResponse) -> bool {
let hash = self.hash_response(input, response);
self.total_inputs += 1;
let method_paths = self
.method_coverage
.entry(input.method.clone())
.or_default();
method_paths.insert(hash);
if self.seen_paths.insert(hash) {
self.coverage_inputs += 1;
*self.edge_counts.entry(hash).or_insert(0) += 1;
true
} else {
*self.edge_counts.get_mut(&hash).unwrap() += 1;
false
}
}
pub fn stats(&self) -> CoverageStats {
let total_edges = self.edge_counts.len();
let edge_coverage = if total_edges > 0 {
(total_edges as f64 / 100.0).min(1.0)
} else {
0.0
};
let new_coverage_rate = if self.total_inputs > 0 {
self.coverage_inputs as f64 / self.total_inputs as f64
} else {
0.0
};
CoverageStats {
paths_explored: self.seen_paths.len(),
edge_coverage,
new_coverage_rate,
}
}
pub fn unique_paths(&self) -> usize {
self.seen_paths.len()
}
pub fn total_inputs(&self) -> u64 {
self.total_inputs
}
pub fn method_coverage(&self) -> &HashMap<String, HashSet<u64>> {
&self.method_coverage
}
pub fn hit_count(&self, hash: u64) -> u32 {
self.edge_counts.get(&hash).copied().unwrap_or(0)
}
pub fn has_seen(&self, hash: u64) -> bool {
self.seen_paths.contains(&hash)
}
pub fn reset(&mut self) {
self.seen_paths.clear();
self.edge_counts.clear();
self.total_inputs = 0;
self.coverage_inputs = 0;
self.method_coverage.clear();
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CoverageStats {
pub paths_explored: usize,
pub edge_coverage: f64,
pub new_coverage_rate: f64,
}
impl Default for CoverageStats {
fn default() -> Self {
Self {
paths_explored: 0,
edge_coverage: 0.0,
new_coverage_rate: 0.0,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn new_tracker() {
let tracker = CoverageTracker::new();
assert_eq!(tracker.unique_paths(), 0);
assert_eq!(tracker.total_inputs(), 0);
}
#[test]
fn record_new_coverage() {
let mut tracker = CoverageTracker::new();
let input = FuzzInput::tools_list();
let response = FuzzResponse::success(serde_json::json!({"tools": []}));
let is_new = tracker.record(&input, &response);
assert!(is_new);
assert_eq!(tracker.unique_paths(), 1);
}
#[test]
fn duplicate_not_new() {
let mut tracker = CoverageTracker::new();
let input = FuzzInput::tools_list();
let response1 = FuzzResponse::success(serde_json::json!({"tools": []}));
let response2 = FuzzResponse::success(serde_json::json!({"tools": []}));
tracker.record(&input, &response1);
let is_new = tracker.record(&input, &response2);
assert!(!is_new);
assert_eq!(tracker.unique_paths(), 1);
assert_eq!(tracker.total_inputs(), 2);
}
#[test]
fn different_responses_different_coverage() {
let mut tracker = CoverageTracker::new();
let input = FuzzInput::tools_list();
let success = FuzzResponse::success(serde_json::json!({"tools": []}));
let error = FuzzResponse::error(-32601, "Method not found");
tracker.record(&input, &success);
tracker.record(&input, &error);
assert_eq!(tracker.unique_paths(), 2);
}
#[test]
fn different_methods_different_coverage() {
let mut tracker = CoverageTracker::new();
let input1 = FuzzInput::tools_list();
let input2 = FuzzInput::resources_list();
let response = FuzzResponse::success(serde_json::json!({}));
tracker.record(&input1, &response);
tracker.record(&input2, &response);
assert_eq!(tracker.unique_paths(), 2);
}
#[test]
fn coverage_stats() {
let mut tracker = CoverageTracker::new();
let input = FuzzInput::tools_list();
let response1 = FuzzResponse::success(serde_json::json!({"a": 1}));
let response2 = FuzzResponse::success(serde_json::json!({"b": 2}));
let response3 = FuzzResponse::success(serde_json::json!({"a": 1}));
tracker.record(&input, &response1);
tracker.record(&input, &response2);
tracker.record(&input, &response3);
let stats = tracker.stats();
assert_eq!(stats.paths_explored, 2);
assert!(stats.new_coverage_rate > 0.0);
assert!(stats.new_coverage_rate <= 1.0);
}
#[test]
fn method_coverage_tracking() {
let mut tracker = CoverageTracker::new();
let tools = FuzzInput::tools_list();
let resources = FuzzInput::resources_list();
let response = FuzzResponse::success(serde_json::json!({}));
tracker.record(&tools, &response);
tracker.record(&tools, &response);
tracker.record(&resources, &response);
let method_cov = tracker.method_coverage();
assert!(method_cov.contains_key("tools/list"));
assert!(method_cov.contains_key("resources/list"));
}
#[test]
fn tracker_default() {
let tracker = CoverageTracker::default();
assert_eq!(tracker.unique_paths(), 0);
assert_eq!(tracker.total_inputs(), 0);
}
#[test]
fn tracker_reset() {
let mut tracker = CoverageTracker::new();
let input = FuzzInput::tools_list();
let response = FuzzResponse::success(serde_json::json!({}));
tracker.record(&input, &response);
assert_eq!(tracker.unique_paths(), 1);
assert_eq!(tracker.total_inputs(), 1);
tracker.reset();
assert_eq!(tracker.unique_paths(), 0);
assert_eq!(tracker.total_inputs(), 0);
}
#[test]
fn has_seen() {
let mut tracker = CoverageTracker::new();
let input = FuzzInput::tools_list();
let response = FuzzResponse::success(serde_json::json!({}));
let hash = tracker.hash_response(&input, &response);
assert!(!tracker.has_seen(hash));
tracker.record(&input, &response);
assert!(tracker.has_seen(hash));
}
#[test]
fn hit_count() {
let mut tracker = CoverageTracker::new();
let input = FuzzInput::tools_list();
let response = FuzzResponse::success(serde_json::json!({}));
let hash = tracker.hash_response(&input, &response);
tracker.record(&input, &response);
assert_eq!(tracker.hit_count(hash), 1);
tracker.record(&input, &response);
assert_eq!(tracker.hit_count(hash), 2);
}
#[test]
fn hit_count_unknown_hash() {
let tracker = CoverageTracker::new();
assert_eq!(tracker.hit_count(12345), 0);
}
#[test]
fn hash_timeout_response() {
let tracker = CoverageTracker::new();
let input = FuzzInput::tools_list();
let response = FuzzResponse::timeout();
let hash = tracker.hash_response(&input, &response);
assert!(hash > 0);
}
#[test]
fn hash_connection_lost_response() {
let tracker = CoverageTracker::new();
let input = FuzzInput::tools_list();
let response = FuzzResponse::connection_lost("connection reset");
let hash = tracker.hash_response(&input, &response);
assert!(hash > 0);
}
#[test]
fn hash_process_exit_response() {
let tracker = CoverageTracker::new();
let input = FuzzInput::tools_list();
let response = FuzzResponse::process_exit(1);
let hash = tracker.hash_response(&input, &response);
assert!(hash > 0);
}
#[test]
fn hash_error_response() {
let tracker = CoverageTracker::new();
let input = FuzzInput::tools_list();
let response = FuzzResponse::error(-32601, "Method not found");
let hash = tracker.hash_response(&input, &response);
assert!(hash > 0);
}
#[test]
fn hash_different_error_codes() {
let tracker = CoverageTracker::new();
let input = FuzzInput::tools_list();
let err1 = FuzzResponse::error(-32601, "Method not found");
let err2 = FuzzResponse::error(-32602, "Invalid params");
let hash1 = tracker.hash_response(&input, &err1);
let hash2 = tracker.hash_response(&input, &err2);
assert_ne!(hash1, hash2);
}
#[test]
fn hash_json_structure_array() {
let mut tracker = CoverageTracker::new();
let input = FuzzInput::tools_list();
let response1 = FuzzResponse::success(serde_json::json!({"items": [1, 2, 3]}));
let response2 = FuzzResponse::success(serde_json::json!({"items": [4, 5, 6]}));
tracker.record(&input, &response1);
let is_new = tracker.record(&input, &response2);
assert!(!is_new);
}
#[test]
fn coverage_stats_default() {
let stats = CoverageStats::default();
assert_eq!(stats.paths_explored, 0);
assert_eq!(stats.edge_coverage, 0.0);
assert_eq!(stats.new_coverage_rate, 0.0);
}
#[test]
fn response_time_affects_hash() {
let tracker = CoverageTracker::new();
let input = FuzzInput::tools_list();
let response1 = FuzzResponse::success(serde_json::json!({})).with_time(50);
let response2 = FuzzResponse::success(serde_json::json!({})).with_time(150);
let hash1 = tracker.hash_response(&input, &response1);
let hash2 = tracker.hash_response(&input, &response2);
assert_ne!(hash1, hash2);
}
#[test]
fn hash_nested_objects() {
let tracker = CoverageTracker::new();
let input = FuzzInput::tools_list();
let response = FuzzResponse::success(serde_json::json!({
"level1": {
"level2": {
"value": 123
}
}
}));
let hash = tracker.hash_response(&input, &response);
assert!(hash > 0);
}
#[test]
fn edge_coverage_capped_at_one() {
let mut tracker = CoverageTracker::new();
let input = FuzzInput::tools_list();
for i in 0..150 {
let response = FuzzResponse::success(serde_json::json!({"index": i}));
tracker.record(&input, &response);
}
let stats = tracker.stats();
assert!(stats.edge_coverage <= 1.0);
}
}