fuzzcheck_arg_parser/
lib.rs

1use getopts::Options;
2use std::path::PathBuf;
3
4#[derive(Debug, Clone, Copy)]
5pub enum FuzzerCommand {
6    MinifyInput,
7    Fuzz,
8    Read,
9    MinifyCorpus,
10}
11
12pub const TIMEOUT_FLAG: &str = "timeout";
13pub const MAX_NBR_RUNS_FLAG: &str = "max-iter";
14pub const MAX_INPUT_CPLX_FLAG: &str = "max-cplx";
15pub const INPUT_FILE_FLAG: &str = "input-file";
16pub const IN_CORPUS_FLAG: &str = "in-corpus";
17pub const NO_IN_CORPUS_FLAG: &str = "no-in-corpus";
18pub const OUT_CORPUS_FLAG: &str = "out-corpus";
19pub const NO_OUT_CORPUS_FLAG: &str = "no-out-corpus";
20pub const ARTIFACTS_FLAG: &str = "artifacts";
21pub const NO_ARTIFACTS_FLAG: &str = "no-artifacts";
22pub const CORPUS_SIZE_FLAG: &str = "corpus-size";
23
24pub const COMMAND_FUZZ: &str = "fuzz";
25pub const COMMAND_MINIFY_INPUT: &str = "tmin";
26pub const COMMAND_MINIFY_CORPUS: &str = "cmin";
27pub const COMMAND_READ: &str = "read";
28
29#[derive(Clone)]
30pub struct DefaultArguments<'a> {
31    pub command: FuzzerCommand,
32    pub in_corpus: &'a str,
33    pub out_corpus: &'a str,
34    pub artifacts: &'a str,
35    pub max_nbr_of_runs: usize,
36    pub max_input_cplx: usize,
37    pub timeout: usize,
38    pub corpus_size: usize,
39}
40
41pub const DEFAULT_ARGUMENTS: DefaultArguments<'static> = DefaultArguments {
42    command: FuzzerCommand::Fuzz,
43    in_corpus: "corpus",
44    out_corpus: "corpus",
45    artifacts: "artifacts",
46    max_nbr_of_runs: core::usize::MAX,
47    max_input_cplx: 4096,
48    timeout: 0,
49    corpus_size: 100,
50};
51
52#[derive(Debug, Clone)]
53pub struct ResolvedCommandLineArguments {
54    pub command: FuzzerCommand,
55    pub max_nbr_of_runs: usize,
56    pub max_input_cplx: f64,
57    pub timeout: usize,
58    pub corpus_size: usize,
59    pub input_file: Option<PathBuf>,
60    pub corpus_in: Option<PathBuf>,
61    pub corpus_out: Option<PathBuf>,
62    pub artifacts_folder: Option<PathBuf>,
63}
64
65#[derive(Debug, Clone)]
66pub struct CommandLineArguments {
67    pub command: FuzzerCommand,
68    pub max_nbr_of_runs: Option<usize>,
69    pub max_input_cplx: Option<f64>,
70    pub timeout: Option<usize>,
71    pub corpus_size: Option<usize>,
72    pub input_file: Option<PathBuf>,
73    pub corpus_in: Option<PathBuf>,
74    pub corpus_out: Option<PathBuf>,
75    pub artifacts_folder: Option<PathBuf>,
76
77    pub no_in_corpus: Option<()>,
78    pub no_out_corpus: Option<()>,
79    pub no_artifacts: Option<()>,
80}
81
82#[must_use]
83pub fn options_parser() -> Options {
84    let mut options = Options::new();
85
86    options.long_only(true);
87    options.optopt("", IN_CORPUS_FLAG, "folder for the input corpus", "PATH");
88    options.optflag(
89        "",
90        NO_IN_CORPUS_FLAG,
91        format!(
92            "do not use an input corpus, overrides --{in_corpus}",
93            in_corpus = IN_CORPUS_FLAG
94        )
95        .as_str(),
96    );
97    options.optopt("", OUT_CORPUS_FLAG, "folder for the output corpus", "PATH");
98    options.optflag(
99        "",
100        NO_OUT_CORPUS_FLAG,
101        format!(
102            "do not use an output corpus, overrides --{out_corpus}",
103            out_corpus = OUT_CORPUS_FLAG
104        )
105        .as_str(),
106    );
107    options.optopt("", ARTIFACTS_FLAG, "folder where the artifacts will be written", "PATH");
108    options.optflag(
109        "",
110        NO_ARTIFACTS_FLAG,
111        format!(
112            "do not save artifacts, overrides --{artifacts}",
113            artifacts = ARTIFACTS_FLAG
114        )
115        .as_str(),
116    );
117    options.optopt("", INPUT_FILE_FLAG, "file containing a JSON-encoded input", "PATH");
118    options.optopt(
119        "",
120        CORPUS_SIZE_FLAG,
121        format!(
122            "target size of the corpus (default: {default})",
123            default = DEFAULT_ARGUMENTS.corpus_size
124        )
125        .as_str(),
126        "N",
127    );
128    options.optopt(
129        "",
130        MAX_INPUT_CPLX_FLAG,
131        format!(
132            "maximum allowed complexity of inputs (default: {default})",
133            default = DEFAULT_ARGUMENTS.max_input_cplx
134        )
135        .as_str(),
136        "N",
137    );
138    options.optopt("", MAX_NBR_RUNS_FLAG, "maximum number of iterations", "N");
139    options.optopt(
140        "",
141        TIMEOUT_FLAG,
142        format!(
143            "maximum allowed time in milliseconds for a single run to finish, or 0 for no limit (default: {default})",
144            default = DEFAULT_ARGUMENTS.timeout
145        )
146        .as_str(),
147        "N",
148    );
149    options.optflag("", "help", "print this help menu");
150
151    options
152}
153
154impl CommandLineArguments {
155    pub fn from_parser(options: &Options, args: &[String]) -> Result<Self, String> {
156        let matches = options.parse(args).map_err(|e| e.to_string())?;
157
158        // TODO: factor that out and make it prettier/more useful
159        if matches.opt_present("help") || args.is_empty() {
160            return Err("".to_owned());
161        }
162
163        let command: FuzzerCommand = match args[0].as_str() {
164            COMMAND_FUZZ => FuzzerCommand::Fuzz,
165            COMMAND_READ => FuzzerCommand::Read,
166            COMMAND_MINIFY_INPUT => FuzzerCommand::MinifyInput,
167            COMMAND_MINIFY_CORPUS => FuzzerCommand::MinifyCorpus,
168            _ => Err(format!(
169                r#"The command {c} is not supported. It can either be ‘{fuzz}’, ‘{tmin}’, or ‘{cmin}’."#,
170                c = args[0],
171                fuzz = COMMAND_FUZZ,
172                tmin = COMMAND_MINIFY_INPUT,
173                cmin = COMMAND_MINIFY_CORPUS
174            ))?,
175        };
176
177        let max_input_cplx: Option<f64> = matches
178            .opt_str(MAX_INPUT_CPLX_FLAG)
179            .and_then(|x| x.parse::<usize>().ok())
180            .map(|x| x as f64);
181
182        let input_file: Option<PathBuf> = matches.opt_str(INPUT_FILE_FLAG).and_then(|x| x.parse::<PathBuf>().ok());
183
184        let corpus_size: Option<usize> = matches.opt_str(CORPUS_SIZE_FLAG).and_then(|x| x.parse::<usize>().ok());
185
186        let corpus_in: Option<PathBuf> = matches.opt_str(IN_CORPUS_FLAG).and_then(|x| x.parse::<PathBuf>().ok());
187
188        let no_in_corpus = if matches.opt_present(NO_IN_CORPUS_FLAG) {
189            Some(())
190        } else {
191            None
192        };
193
194        let corpus_out: Option<PathBuf> = matches.opt_str(OUT_CORPUS_FLAG).and_then(|x| x.parse::<PathBuf>().ok());
195
196        let no_out_corpus = if matches.opt_present(NO_OUT_CORPUS_FLAG) {
197            Some(())
198        } else {
199            None
200        };
201
202        let artifacts_folder: Option<PathBuf> = matches.opt_str(ARTIFACTS_FLAG).and_then(|x| x.parse::<PathBuf>().ok());
203
204        let no_artifacts = if matches.opt_present(NO_ARTIFACTS_FLAG) {
205            Some(())
206        } else {
207            None
208        };
209
210        let max_nbr_of_runs: Option<usize> = matches.opt_str(MAX_NBR_RUNS_FLAG).and_then(|x| x.parse::<usize>().ok());
211
212        let timeout: Option<usize> = matches.opt_str(TIMEOUT_FLAG).and_then(|x| x.parse::<usize>().ok());
213
214        Ok(Self {
215            command,
216            max_nbr_of_runs,
217            max_input_cplx,
218            timeout,
219            corpus_size,
220            input_file,
221            corpus_in,
222            corpus_out,
223            artifacts_folder,
224            no_in_corpus,
225            no_out_corpus,
226            no_artifacts,
227        })
228    }
229
230    pub fn resolved(&self, defaults: DefaultArguments) -> ResolvedCommandLineArguments {
231        let command = self.command;
232
233        let max_input_cplx: f64 = self.max_input_cplx.unwrap_or(defaults.max_input_cplx as f64);
234
235        let input_file: Option<PathBuf> = self.input_file.clone();
236
237        let corpus_size: usize = self.corpus_size.unwrap_or(defaults.corpus_size);
238
239        let corpus_in: Option<PathBuf> = if self.no_in_corpus.is_some() {
240            None
241        } else {
242            self.corpus_in.clone()
243        };
244
245        match (command, &input_file, &corpus_in) {
246            (FuzzerCommand::MinifyInput, &None, _) => {
247                panic!("An input file must be given when minifying a test case".to_owned())
248                // TODO: return error for that
249            }
250            (FuzzerCommand::MinifyCorpus, _, &None) => {
251                panic!("An input corpus must be given when minifying a corpus".to_owned())
252            }
253            _ => (),
254        }
255
256        let corpus_out: Option<PathBuf> = if self.no_out_corpus.is_some() {
257            None
258        } else {
259            self.corpus_out.clone()
260        };
261
262        let artifacts_folder: Option<PathBuf> = if self.no_artifacts.is_some() {
263            None
264        } else {
265            self.artifacts_folder.clone()
266        };
267
268        let max_nbr_of_runs: usize = self.max_nbr_of_runs.unwrap_or(core::usize::MAX);
269
270        let timeout: usize = self.timeout.unwrap_or(defaults.timeout);
271
272        ResolvedCommandLineArguments {
273            command,
274            max_nbr_of_runs,
275            max_input_cplx,
276            timeout,
277            corpus_size,
278            input_file,
279            corpus_in,
280            corpus_out,
281            artifacts_folder,
282        }
283    }
284}
285
286impl ResolvedCommandLineArguments {
287    /// Get the command line arguments to the fuzzer from the option parser
288    /// # Errors
289    /// TODO
290    pub fn from_parser(options: &Options, args: &[String]) -> Result<Self, String> {
291        Ok(CommandLineArguments::from_parser(options, args)?.resolved(DEFAULT_ARGUMENTS))
292    }
293}