|
| 1 | +#![no_main] |
| 2 | + |
| 3 | +use arbitrary::{Arbitrary, Unstructured}; |
| 4 | +use libfuzzer_sys::fuzz_target; |
| 5 | +use rustworkx_core::err::ContractError; |
| 6 | +use rustworkx_core::graph_ext::*; |
| 7 | +use rustworkx_core::petgraph::graph::NodeIndex; |
| 8 | +use rustworkx_core::petgraph::prelude::*; |
| 9 | + |
| 10 | +#[derive(Debug, Arbitrary)] |
| 11 | +struct ContractFuzzInput { |
| 12 | + edges: Vec<(usize, usize, usize)>, |
| 13 | + node_count: usize, |
| 14 | + contract_indices: Vec<usize>, |
| 15 | + replacement_weight: char, |
| 16 | +} |
| 17 | + |
| 18 | +fuzz_target!(|data: &[u8]| { |
| 19 | + if let Ok(input) = ContractFuzzInput::arbitrary(&mut Unstructured::new(data)) { |
| 20 | + fuzz_contract_nodes(input); |
| 21 | + } |
| 22 | +}); |
| 23 | + |
| 24 | +fn fuzz_contract_nodes(input: ContractFuzzInput) { |
| 25 | + if input.node_count == 0 || input.node_count > 500 || input.edges.len() > 5000 { |
| 26 | + return; |
| 27 | + } |
| 28 | + |
| 29 | + let mut graph: StableDiGraph<char, usize> = StableDiGraph::new(); |
| 30 | + let mut nodes = Vec::with_capacity(input.node_count); |
| 31 | + for i in 0..input.node_count { |
| 32 | + let label = (b'a' + ((i % 26) as u8)) as char; |
| 33 | + nodes.push(graph.add_node(label)); |
| 34 | + } |
| 35 | + |
| 36 | + for (u, v, w) in input.edges { |
| 37 | + if u < input.node_count && v < input.node_count && w > 0 { |
| 38 | + graph.add_edge(nodes[u], nodes[v], w); |
| 39 | + } |
| 40 | + } |
| 41 | + |
| 42 | + let to_contract: Vec<NodeIndex> = input |
| 43 | + .contract_indices |
| 44 | + .into_iter() |
| 45 | + .filter_map(|i| nodes.get(i).copied()) |
| 46 | + .collect(); |
| 47 | + |
| 48 | + if to_contract.len() < 2 { |
| 49 | + return; |
| 50 | + } |
| 51 | + |
| 52 | + let mut graph_no_check = graph.clone(); |
| 53 | + |
| 54 | + // Run contraction without cycle check (should never fail) |
| 55 | + let _ = graph_no_check.contract_nodes(to_contract.clone(), input.replacement_weight, false); |
| 56 | + |
| 57 | + // Run contraction with cycle check, match on the result |
| 58 | + #[allow(unreachable_patterns)] |
| 59 | + match graph.contract_nodes(to_contract.clone(), input.replacement_weight, true) { |
| 60 | + Ok(_) => { |
| 61 | + // Idempotency: running again should not panic |
| 62 | + let _ = graph.contract_nodes(to_contract, input.replacement_weight, true); |
| 63 | + } |
| 64 | + Err(ContractError::DAGWouldCycle) => { |
| 65 | + // Expected error — no-op |
| 66 | + } |
| 67 | + Err(err) => { |
| 68 | + panic!("Unexpected error during node contraction: {:?}", err); |
| 69 | + } |
| 70 | + } |
| 71 | +} |
0 commit comments