fuzzcheck_common/
arg.rs

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/// The task that the fuzzer is asked to perform.
42#[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/// Various arguments given to the fuzzer, typically provided by the `cargo fuzzcheck` command line tool.
55#[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/// The command line argument parser used by the fuzz target and `cargo fuzzcheck`
86#[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    /// Create an `Arguments` from the parsed result of [`options_parser()`].
175    ///
176    /// ### Arguments
177    /// * `for_cargo_fuzzcheck` : true if this method is called within `cargo fuzzcheck`, false otherwise.
178    ///   This is because `cargo fuzzcheck` also needs a fuzz target as argument, while the fuzzed binary
179    ///   does not.
180    #[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        // verify all the right options are here
271
272        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/// The “help” output of cargo-fuzzcheck
344#[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}