1#![cfg_attr(docsrs, feature(doc_cfg))]
34#![warn(missing_docs)]
35#![warn(rust_2018_idioms)]
36
37use std::time::Duration;
38
39use dev_report::{CheckResult, Evidence, Report, Severity};
40
41#[derive(Debug, Clone, Copy, PartialEq, Eq)]
43pub enum FuzzFindingKind {
44 Crash,
46 Timeout,
48 OutOfMemory,
50}
51
52#[derive(Debug, Clone, Copy)]
54pub enum FuzzBudget {
55 Time(Duration),
57 Executions(u64),
59}
60
61impl FuzzBudget {
62 pub fn time(d: Duration) -> Self {
64 Self::Time(d)
65 }
66
67 pub fn executions(n: u64) -> Self {
69 Self::Executions(n)
70 }
71}
72
73#[derive(Debug, Clone)]
75pub struct FuzzRun {
76 target: String,
77 version: String,
78 budget: FuzzBudget,
79}
80
81impl FuzzRun {
82 pub fn new(target: impl Into<String>, version: impl Into<String>) -> Self {
84 Self {
85 target: target.into(),
86 version: version.into(),
87 budget: FuzzBudget::Time(Duration::from_secs(60)),
88 }
89 }
90
91 pub fn budget(mut self, budget: FuzzBudget) -> Self {
93 self.budget = budget;
94 self
95 }
96
97 pub fn fuzz_budget(&self) -> FuzzBudget {
99 self.budget
100 }
101
102 pub fn execute(&self) -> Result<FuzzResult, FuzzError> {
106 Ok(FuzzResult {
107 target: self.target.clone(),
108 version: self.version.clone(),
109 executions: 0,
110 findings: Vec::new(),
111 })
112 }
113}
114
115#[derive(Debug, Clone)]
117pub struct FuzzFinding {
118 pub kind: FuzzFindingKind,
120 pub reproducer_path: String,
122 pub summary: String,
124}
125
126#[derive(Debug, Clone)]
128pub struct FuzzResult {
129 pub target: String,
131 pub version: String,
133 pub executions: u64,
135 pub findings: Vec<FuzzFinding>,
137}
138
139impl FuzzResult {
140 pub fn into_report(self) -> Report {
142 let mut report = Report::new(&self.target, &self.version).with_producer("dev-fuzz");
143 if self.findings.is_empty() {
144 report.push(
145 CheckResult::pass(format!("fuzz::{}", self.target))
146 .with_detail(format!("{} executions, 0 findings", self.executions))
147 .with_evidence(Evidence::numeric_int("executions", self.executions as i64)),
148 );
149 } else {
150 for f in &self.findings {
151 let sev = match f.kind {
152 FuzzFindingKind::Crash => Severity::Critical,
153 FuzzFindingKind::Timeout => Severity::Warning,
154 FuzzFindingKind::OutOfMemory => Severity::Error,
155 };
156 let kind_str = match f.kind {
157 FuzzFindingKind::Crash => "crash",
158 FuzzFindingKind::Timeout => "timeout",
159 FuzzFindingKind::OutOfMemory => "oom",
160 };
161 report.push(
162 CheckResult::fail(format!("fuzz::{}::{kind_str}", self.target), sev)
163 .with_detail(f.summary.clone())
164 .with_evidence(Evidence::file_ref("reproducer", &f.reproducer_path)),
165 );
166 }
167 }
168 report.finish();
169 report
170 }
171}
172
173#[derive(Debug)]
175pub enum FuzzError {
176 ToolNotInstalled,
178 NightlyRequired,
180 SubprocessFailed(String),
182 TargetNotFound(String),
184}
185
186impl std::fmt::Display for FuzzError {
187 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
188 match self {
189 Self::ToolNotInstalled => write!(f, "cargo-fuzz is not installed"),
190 Self::NightlyRequired => write!(f, "nightly Rust is required"),
191 Self::SubprocessFailed(s) => write!(f, "subprocess failed: {s}"),
192 Self::TargetNotFound(s) => write!(f, "fuzz target not found: {s}"),
193 }
194 }
195}
196
197impl std::error::Error for FuzzError {}
198
199#[cfg(test)]
200mod tests {
201 use super::*;
202
203 #[test]
204 fn run_builds() {
205 let r = FuzzRun::new("parse", "0.1.0");
206 match r.fuzz_budget() {
207 FuzzBudget::Time(_) => {}
208 _ => panic!("default should be time-based"),
209 }
210 }
211
212 #[test]
213 fn executions_budget() {
214 let r = FuzzRun::new("parse", "0.1.0").budget(FuzzBudget::executions(1_000_000));
215 match r.fuzz_budget() {
216 FuzzBudget::Executions(n) => assert_eq!(n, 1_000_000),
217 _ => panic!("expected executions budget"),
218 }
219 }
220
221 #[test]
222 fn empty_findings_passes() {
223 let r = FuzzResult {
224 target: "parse".into(),
225 version: "0.1.0".into(),
226 executions: 1_000_000,
227 findings: Vec::new(),
228 };
229 let report = r.into_report();
230 assert!(report.passed());
231 }
232
233 #[test]
234 fn crash_finding_is_critical() {
235 let r = FuzzResult {
236 target: "parse".into(),
237 version: "0.1.0".into(),
238 executions: 500,
239 findings: vec![FuzzFinding {
240 kind: FuzzFindingKind::Crash,
241 reproducer_path: "fuzz/artifacts/crash-deadbeef".into(),
242 summary: "panic in parse_input".into(),
243 }],
244 };
245 let report = r.into_report();
246 assert!(report.failed());
247 assert_eq!(report.checks[0].severity, Some(Severity::Critical));
248 }
249}