1use std::error::Error;
2use std::fmt::{Debug, Display};
3use std::path::PathBuf;
4use std::time::Duration;
5
6use getopts::{Fail, Matches, Options};
7
8pub const MAX_INPUT_CPLX_FLAG: &str = "max-cplx";
9pub const INPUT_FILE_FLAG: &str = "input-file";
10pub const IN_CORPUS_FLAG: &str = "in-corpus";
11pub const NO_IN_CORPUS_FLAG: &str = "no-in-corpus";
12pub const OUT_CORPUS_FLAG: &str = "out-corpus";
13pub const NO_OUT_CORPUS_FLAG: &str = "no-out-corpus";
14pub const ARTIFACTS_FLAG: &str = "artifacts";
15pub const NO_ARTIFACTS_FLAG: &str = "no-artifacts";
16pub const STATS_FLAG: &str = "stats";
17pub const NO_STATS_FLAG: &str = "no-stats";
18pub const COMMAND_FLAG: &str = "command";
19
20pub const MAX_DURATION_FLAG: &str = "stop-after-duration";
21pub const MAX_ITERATIONS_FLAG: &str = "stop-after-iterations";
22pub const STOP_AFTER_FIRST_FAILURE_FLAG: &str = "stop-after-first-failure";
23
24pub const DETECT_INFINITE_LOOP_FLAG: &str = "detect-infinite-loop";
25
26pub const COMMAND_FUZZ: &str = "fuzz";
27pub const COMMAND_MINIFY_INPUT: &str = "minify";
28pub const COMMAND_READ: &str = "read";
29
30#[derive(Clone)]
31pub struct DefaultArguments {
32 pub max_input_cplx: f64,
33}
34impl Default for DefaultArguments {
35 #[coverage(off)]
36 fn default() -> Self {
37 Self { max_input_cplx: 4096.0 }
38 }
39}
40
41#[derive(Debug, Clone)]
43pub enum FuzzerCommand {
44 Fuzz,
45 Read { input_file: PathBuf },
46 MinifyInput { input_file: PathBuf },
47}
48impl Default for FuzzerCommand {
49 fn default() -> Self {
50 Self::Fuzz
51 }
52}
53
54#[derive(Debug, Clone)]
56pub struct Arguments {
57 pub command: FuzzerCommand,
58 pub max_input_cplx: f64,
59 pub detect_infinite_loop: bool,
60 pub maximum_duration: Duration,
61 pub maximum_iterations: usize,
62 pub stop_after_first_failure: bool,
63 pub corpus_in: Option<PathBuf>,
64 pub corpus_out: Option<PathBuf>,
65 pub artifacts_folder: Option<PathBuf>,
66 pub stats_folder: Option<PathBuf>,
67}
68impl Arguments {
69 pub fn for_internal_documentation_test() -> Self {
70 Self {
71 command: FuzzerCommand::Fuzz,
72 max_input_cplx: 256.,
73 detect_infinite_loop: false,
74 maximum_duration: Duration::MAX,
75 maximum_iterations: usize::MAX,
76 stop_after_first_failure: true,
77 corpus_in: None,
78 corpus_out: None,
79 artifacts_folder: None,
80 stats_folder: None,
81 }
82 }
83}
84
85#[must_use]
87#[coverage(off)]
88pub fn options_parser() -> Options {
89 let mut options = Options::new();
90
91 let defaults = DefaultArguments::default();
92 options.optopt(
93 "",
94 COMMAND_FLAG,
95 &format!(
96 "the action to be performed (default: fuzz). --{} is required when using `{}`",
97 INPUT_FILE_FLAG, COMMAND_MINIFY_INPUT
98 ),
99 &format!("<{} | {}>", COMMAND_FUZZ, COMMAND_MINIFY_INPUT),
100 );
101 options.optopt(
102 "",
103 MAX_DURATION_FLAG,
104 "maximum duration of the fuzz test, in seconds",
105 "N",
106 );
107 options.optopt("", MAX_ITERATIONS_FLAG, "maximum number of iterations", "N");
108
109 options.optflag(
110 "",
111 DETECT_INFINITE_LOOP_FLAG,
112 "fail on tests running for more than one second",
113 );
114
115 options.optflag(
116 "",
117 STOP_AFTER_FIRST_FAILURE_FLAG,
118 "stop the fuzzer after the first test failure is found",
119 );
120
121 options.optopt("", IN_CORPUS_FLAG, "folder for the input corpus", "PATH");
122 options.optflag(
123 "",
124 NO_IN_CORPUS_FLAG,
125 format!(
126 "do not use an input corpus, overrides --{in_corpus}",
127 in_corpus = IN_CORPUS_FLAG
128 )
129 .as_str(),
130 );
131 options.optopt("", OUT_CORPUS_FLAG, "folder for the output corpus", "PATH");
132 options.optflag(
133 "",
134 NO_OUT_CORPUS_FLAG,
135 format!(
136 "do not use an output corpus, overrides --{out_corpus}",
137 out_corpus = OUT_CORPUS_FLAG
138 )
139 .as_str(),
140 );
141 options.optopt("", ARTIFACTS_FLAG, "folder where the artifacts will be written", "PATH");
142 options.optflag(
143 "",
144 NO_ARTIFACTS_FLAG,
145 format!(
146 "do not save artifacts, overrides --{artifacts}",
147 artifacts = ARTIFACTS_FLAG
148 )
149 .as_str(),
150 );
151 options.optopt("", STATS_FLAG, "folder where the statistics will be written", "PATH");
152 options.optflag(
153 "",
154 NO_STATS_FLAG,
155 format!("do not save statistics, overrides --{stats}", stats = STATS_FLAG).as_str(),
156 );
157 options.optopt("", INPUT_FILE_FLAG, "file containing a test case", "PATH");
158 options.optopt(
159 "",
160 MAX_INPUT_CPLX_FLAG,
161 format!(
162 "maximum allowed complexity of inputs (default: {default})",
163 default = defaults.max_input_cplx
164 )
165 .as_str(),
166 "N",
167 );
168 options.optflag("h", "help", "print this help menu");
169
170 options
171}
172
173impl Arguments {
174 #[coverage(off)]
181 pub fn from_matches(matches: &Matches, for_cargo_fuzzcheck: bool) -> Result<Self, ArgumentsError> {
182 if matches.opt_present("help") || matches.free.contains(&"help".to_owned()) {
183 return Err(ArgumentsError::WantsHelp);
184 }
185
186 if for_cargo_fuzzcheck && matches.free.is_empty() {
187 return Err(ArgumentsError::Validation(
188 "A fuzz target must be given to cargo fuzzcheck.".to_string(),
189 ));
190 }
191
192 let command = matches.opt_str(COMMAND_FLAG).unwrap_or_else(
193 #[coverage(off)]
194 || COMMAND_FUZZ.to_owned(),
195 );
196
197 let command = command.as_str();
198
199 if !matches!(command, COMMAND_FUZZ | COMMAND_READ | COMMAND_MINIFY_INPUT) {
200 return Err(ArgumentsError::Validation(format!(
201 r#"The command {c} is not supported. It can either be ‘{fuzz}’ or ‘{minify}’."#,
202 c = &matches.free[0],
203 fuzz = COMMAND_FUZZ,
204 minify = COMMAND_MINIFY_INPUT,
205 )));
206 }
207
208 let max_input_cplx: Option<f64> = matches
209 .opt_str(MAX_INPUT_CPLX_FLAG)
210 .and_then(
211 #[coverage(off)]
212 |x| x.parse::<usize>().ok(),
213 )
214 .map(
215 #[coverage(off)]
216 |x| x as f64,
217 );
218
219 let detect_infinite_loop = matches.opt_present(DETECT_INFINITE_LOOP_FLAG);
220
221 let corpus_in: Option<PathBuf> = matches.opt_str(IN_CORPUS_FLAG).and_then(
222 #[coverage(off)]
223 |x| x.parse::<PathBuf>().ok(),
224 );
225
226 let no_in_corpus = if matches.opt_present(NO_IN_CORPUS_FLAG) {
227 Some(())
228 } else {
229 None
230 };
231
232 let corpus_out: Option<PathBuf> = matches.opt_str(OUT_CORPUS_FLAG).and_then(
233 #[coverage(off)]
234 |x| x.parse::<PathBuf>().ok(),
235 );
236
237 let no_out_corpus = if matches.opt_present(NO_OUT_CORPUS_FLAG) {
238 Some(())
239 } else {
240 None
241 };
242
243 let artifacts_folder: Option<PathBuf> = matches.opt_str(ARTIFACTS_FLAG).and_then(
244 #[coverage(off)]
245 |x| x.parse::<PathBuf>().ok(),
246 );
247
248 let no_artifacts = if matches.opt_present(NO_ARTIFACTS_FLAG) {
249 Some(())
250 } else {
251 None
252 };
253
254 let stats_folder: Option<PathBuf> = matches.opt_str(STATS_FLAG).and_then(
255 #[coverage(off)]
256 |x| x.parse::<PathBuf>().ok(),
257 );
258
259 let no_stats = if matches.opt_present(NO_STATS_FLAG) {
260 Some(())
261 } else {
262 None
263 };
264
265 let input_file: Option<PathBuf> = matches.opt_str(INPUT_FILE_FLAG).and_then(
266 #[coverage(off)]
267 |x| x.parse::<PathBuf>().ok(),
268 );
269
270 let command = match command {
273 COMMAND_FUZZ => FuzzerCommand::Fuzz,
274 COMMAND_READ => {
275 let input_file = input_file.unwrap_or_else(
276 #[coverage(off)]
277 || {
278 panic!(
279 "An input file must be provided when reading a test case. Use --{}",
280 INPUT_FILE_FLAG
281 )
282 },
283 );
284 FuzzerCommand::Read { input_file }
285 }
286 COMMAND_MINIFY_INPUT => {
287 let input_file = input_file.unwrap_or_else(
288 #[coverage(off)]
289 || {
290 panic!(
291 "An input file must be provided when minifying a test case. Use --{}",
292 INPUT_FILE_FLAG
293 )
294 },
295 );
296 FuzzerCommand::MinifyInput { input_file }
297 }
298 _ => unreachable!(),
299 };
300
301 let maximum_duration = {
302 let seconds = matches
303 .opt_str(MAX_DURATION_FLAG)
304 .and_then(
305 #[coverage(off)]
306 |x| x.parse::<u64>().ok(),
307 )
308 .unwrap_or(u64::MAX);
309 Duration::new(seconds, 0)
310 };
311 let maximum_iterations = matches
312 .opt_str(MAX_ITERATIONS_FLAG)
313 .and_then(
314 #[coverage(off)]
315 |x| x.parse::<usize>().ok(),
316 )
317 .unwrap_or(usize::MAX);
318 let stop_after_first_failure = matches.opt_present(STOP_AFTER_FIRST_FAILURE_FLAG);
319
320 let defaults = DefaultArguments::default();
321 let max_input_cplx: f64 = max_input_cplx.unwrap_or(defaults.max_input_cplx as f64);
322 let corpus_in: Option<PathBuf> = if no_in_corpus.is_some() { None } else { corpus_in };
323 let corpus_out: Option<PathBuf> = if no_out_corpus.is_some() { None } else { corpus_out };
324
325 let artifacts_folder: Option<PathBuf> = if no_artifacts.is_some() { None } else { artifacts_folder };
326 let stats_folder: Option<PathBuf> = if no_stats.is_some() { None } else { stats_folder };
327
328 Ok(Arguments {
329 command,
330 detect_infinite_loop,
331 maximum_duration,
332 maximum_iterations,
333 stop_after_first_failure,
334 max_input_cplx,
335 corpus_in,
336 corpus_out,
337 artifacts_folder,
338 stats_folder,
339 })
340 }
341}
342
343#[coverage(off)]
345pub fn help(parser: &Options) -> String {
346 let mut help = r##"
347USAGE:
348 cargo-fuzzcheck <FUZZ_TEST> [OPTIONS]
349
350FUZZ_TEST:
351 The fuzz test is the exact path to the #[test] function that launches
352 fuzzcheck. For example, it can be "parser::tests::fuzz_test_1" if you have
353 the following snippet located at src/parser/mod.rs:
354
355 #[cfg(test)]
356 mod tests {{
357 #[test]
358 fn fuzz_test_1() {{
359 fuzzcheck::fuzz_test(some_function_to_test)
360 .default_options()
361 .launch();
362 }}
363 }}
364"##
365 .to_owned();
366 help += parser.usage("").as_str();
367 help += format!(
368 r#"
369
370EXAMPLES:
371
372cargo-fuzzcheck tests::fuzz_test1
373 Launch the fuzzer on "tests::fuzz_test1", located in the crate’s library, with default options.
374
375cargo-fuzzcheck tests::fuzz_bin --bin my_program
376 Launch the fuzzer on "tests::fuzz_bin", located in the "my_program" binary target, with default options.
377
378cargo-fuzzcheck fuzz_test2 --test my_integration_test
379 Launch the fuzzer on "fuzz_test2", located in the "my_integration_test" test target, with default options.
380
381cargo-fuzzcheck tests::fuzzit --{max_cplx} 4000 --{out_corpus} fuzz_results/out/
382 Fuzz "tests::fuzzit", generating inputs of complexity no greater than 4000,
383 and write the output corpus (i.e. the folder of most interesting test cases)
384 to fuzz_results/out/.
385
386cargo-fuzzcheck tests::fuzz --command {minify} --{input_file} "artifacts/crash.json"
387 Using the fuzz test located at "tests::fuzz_test", minify the test input defined
388 in the file "artifacts/crash.json". It will put minified inputs in the folder
389 artifacts/crash.minified/ and name them {{complexity}}-{{hash}}.json.
390 For example, artifacts/crash.minified/4213--8cd7777109b57b8c.json
391 is a minified input of complexity 42.13.
392"#,
393 minify = COMMAND_MINIFY_INPUT,
394 input_file = INPUT_FILE_FLAG,
395 max_cplx = MAX_INPUT_CPLX_FLAG,
396 out_corpus = OUT_CORPUS_FLAG,
397 )
398 .as_str();
399 help
400}
401
402#[derive(Clone)]
403pub enum ArgumentsError {
404 NoArgumentsGiven(String),
405 Parsing(Fail),
406 Validation(String),
407 WantsHelp,
408}
409
410impl Debug for ArgumentsError {
411 #[coverage(off)]
412 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
413 <Self as Display>::fmt(self, f)
414 }
415}
416impl Display for ArgumentsError {
417 #[coverage(off)]
418 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
419 match self {
420 ArgumentsError::NoArgumentsGiven(help) => {
421 write!(f, "No arguments were given.\nHelp:\n{}", help)
422 }
423 ArgumentsError::Parsing(e) => {
424 write!(
425 f,
426 "{}
427To display the help, run:
428 cargo fuzzcheck --help",
429 e
430 )
431 }
432 ArgumentsError::Validation(e) => {
433 write!(
434 f,
435 "{}
436To display the help, run:
437 cargo fuzzcheck --help",
438 e
439 )
440 }
441 ArgumentsError::WantsHelp => {
442 write!(f, "Help requested.")
443 }
444 }
445 }
446}
447impl Error for ArgumentsError {}
448
449impl From<Fail> for ArgumentsError {
450 #[coverage(off)]
451 fn from(e: Fail) -> Self {
452 Self::Parsing(e)
453 }
454}