|  | #!/usr/bin/env python2.7 | 
|  | # Copyright 2019 The Fuchsia Authors. All rights reserved. | 
|  | # Use of this source code is governed by a BSD-style license that can be | 
|  | # found in the LICENSE file. | 
|  |  | 
|  | import argparse | 
|  | import re | 
|  | import sys | 
|  |  | 
|  | import command | 
|  | from host import Host | 
|  |  | 
|  | # This file contains the arguments parsing for "fx fuzz". | 
|  |  | 
|  |  | 
|  | class ArgParser(argparse.ArgumentParser): | 
|  | """Wrapper to ArgumentParser that suppresses help and usage messages. | 
|  |  | 
|  | argparse adds a lot of benefit, but its help and usage messages don't fit | 
|  | well with the style of other "fx" utilities. As a result, this class | 
|  | overrides the error() and exit() methods to suppress printing those | 
|  | messages. As an added bonus, the incorporation of Host | 
|  | makes it possible to test these conditions. | 
|  |  | 
|  | Attributes: | 
|  | host:       The host object representing the current system. | 
|  | """ | 
|  |  | 
|  | # These are used in parse_args() to identify and extract libFuzzer arguments. | 
|  | LIBFUZZER_OPT_RE = re.compile(r'^-(\w+)=(.*)$') | 
|  | SHORT_OPT_RE = re.compile(r'^-\w$') | 
|  | LONG_OPT_RE = re.compile(r'^--\w+$') | 
|  | POSITIONAL_RE = re.compile(r'^[^-]') | 
|  |  | 
|  | def __init__(self, **kwargs): | 
|  | # Don't use argparse's built-in help. | 
|  | kwargs['usage'] = '' | 
|  | kwargs['add_help'] = False | 
|  | super(ArgParser, self).__init__(**kwargs) | 
|  | self._parsers = {} | 
|  | self._unique_options = [] | 
|  | self._options_help = [] | 
|  | self._arguments_help = [] | 
|  | self._has_libfuzzer_extras = False | 
|  |  | 
|  | @property | 
|  | def host(self): | 
|  | """The system interface for user interactions.""" | 
|  | assert self._host, 'Host not set.' | 
|  | return self._host | 
|  |  | 
|  | @host.setter | 
|  | def host(self, host): | 
|  | self._host = host | 
|  |  | 
|  | def add_parsers(self): | 
|  | """Configure a top-level parser with subparsers. | 
|  |  | 
|  | Each subcommand should add a parser using the special SubparserAction | 
|  | returned by argparse.ArgumentParser.add_subparsers(). See | 
|  | https://docs.python.org/2/library/argparse.html#sub-commands. | 
|  | """ | 
|  | self.usage = '[SUBCOMMAND] [...]' | 
|  | self.description = [ | 
|  | 'Manage Fuchsia fuzzers. SUBCOMMAND defaults to "start" if omitted.' | 
|  | ] | 
|  |  | 
|  | # fx fuzz help | 
|  | help_parser = self._add_parser('help') | 
|  | help_parser.help = 'Print this message and exit.' | 
|  | help_parser.description = [ | 
|  | 'Prints the detailed help for SUBCOMMAND if provided, or a general help message.' | 
|  | ] | 
|  | help_parser._add_argument( | 
|  | 'subcommand', | 
|  | nargs='?', | 
|  | help=['Subcommand for which to print detailed help.']) | 
|  |  | 
|  | # fx fuzz list [name] | 
|  | list_parser = self._add_parser('list') | 
|  | list_parser.help = 'List available fuzzers in the current build.' | 
|  | list_parser.description = [ | 
|  | 'Lists fuzzers matching NAME if provided, or all fuzzers.' | 
|  | ] | 
|  | list_parser._add_verbose_flag() | 
|  | list_parser._add_name_argument(required=False) | 
|  | list_parser.set_defaults(command=command.list_fuzzers) | 
|  |  | 
|  | # fx fuzz start [-d] [-f] [-m] [-o <output>] <name> | 
|  | start_parser = self._add_parser('start') | 
|  | start_parser.help = 'Start a specific fuzzer.' | 
|  | start_parser.description = ['Starts the named fuzzer.'] | 
|  | start_parser._add_debug_flag() | 
|  | start_parser._add_flag( | 
|  | '-f', '--foreground', help=['Display fuzzer output.']) | 
|  | start_parser._add_flag('-m', '--monitor') | 
|  | start_parser._add_output_option() | 
|  | start_parser._add_verbose_flag() | 
|  | start_parser._add_name_argument(required=True) | 
|  | start_parser._add_libfuzzer_extras() | 
|  | start_parser.set_defaults(command=command.start_fuzzer) | 
|  |  | 
|  | # fx fuzz check [name] | 
|  | check_parser = self._add_parser('check') | 
|  | check_parser.description = [ | 
|  | 'Reports status for the fuzzer matching NAME if provided, or for all running', | 
|  | 'fuzzers. Status includes execution state, corpus size, and number of artifacts.', | 
|  | ] | 
|  | check_parser.help = 'Check on the status of one or more fuzzers.' | 
|  | check_parser._add_verbose_flag() | 
|  | check_parser._add_name_argument(required=False) | 
|  | check_parser.set_defaults(command=command.check_fuzzer) | 
|  |  | 
|  | # fx fuzz stop <name> | 
|  | stop_parser = self._add_parser('stop') | 
|  | stop_parser.description = ['Stops the named fuzzer.'] | 
|  | stop_parser.help = 'Stop a specific fuzzer.' | 
|  | stop_parser._add_verbose_flag() | 
|  | stop_parser._add_name_argument(required=True) | 
|  | stop_parser.set_defaults(command=command.stop_fuzzer) | 
|  |  | 
|  | # fx fuzz repro [-d] [-o <output>] <name> <file>... | 
|  | repro_parser = self._add_parser('repro') | 
|  | repro_parser.description = [ | 
|  | 'Runs the named fuzzer on provided test units.' | 
|  | ] | 
|  | repro_parser.help = 'Reproduce fuzzer findings by replaying test units.' | 
|  | repro_parser._add_debug_flag() | 
|  | repro_parser._add_output_option() | 
|  | repro_parser._add_verbose_flag() | 
|  | repro_parser._add_name_argument(required=True) | 
|  | repro_parser._add_argument( | 
|  | 'libfuzzer_inputs', | 
|  | metavar='unit', | 
|  | nargs='+', | 
|  | help=[ | 
|  | 'File containing a fuzzer input, such as an artifact from a', | 
|  | 'previous fuzzer run. Artifacts are typically named by the', | 
|  | 'type of artifact and a digest of the fuzzer input, e.g.', | 
|  | 'crash-2c5d0d1831b242b107a4c42bba2fa3f6d85edc35', | 
|  | ]) | 
|  | repro_parser._add_libfuzzer_extras() | 
|  | repro_parser.set_defaults(command=command.repro_units) | 
|  |  | 
|  | # fx fuzz analyze [-c <dir>] [-d <file>] [-l] [-o <output>] <name> | 
|  | analyze_parser = self._add_parser('analyze') | 
|  | analyze_parser.description = [ | 
|  | 'Analyze the corpus and/or dictionary for the given fuzzer.' | 
|  | ] | 
|  |  | 
|  | analyze_parser.help = 'Report coverage info for a given corpus and/or dictionary.' | 
|  | analyze_parser.add_option( | 
|  | '-c', | 
|  | '--corpus', | 
|  | dest='corpora', | 
|  | help=['Path to additional corpus elements. May be repeated.']) | 
|  | analyze_parser.add_option( | 
|  | '-d', | 
|  | '--dict', | 
|  | unique=True, | 
|  | help=['Path to a fuzzer dictionary. Replaces the package default.']) | 
|  | analyze_parser._add_flag( | 
|  | '-l', '--local', help=['Exclude corpus elements from Clusterfuzz.']) | 
|  | analyze_parser._add_output_option() | 
|  | analyze_parser._add_verbose_flag() | 
|  | analyze_parser._add_name_argument(required=True) | 
|  | analyze_parser._add_libfuzzer_extras() | 
|  | analyze_parser.set_defaults(command=command.analyze_fuzzer) | 
|  |  | 
|  | update_parser = self._add_parser('update') | 
|  | update_parser.description = [ | 
|  | 'Update the BUILD.gn file for a fuzzer corpus.' | 
|  | ] | 
|  |  | 
|  | update_parser.help = 'Update the BUILD.gn file for a fuzzer corpus.' | 
|  | update_parser._add_output_option() | 
|  | update_parser._add_verbose_flag() | 
|  | update_parser._add_name_argument(required=True) | 
|  | update_parser.set_defaults(command=command.update_corpus) | 
|  |  | 
|  | unittest_parser = self._add_parser('unittest') | 
|  | unittest_parser.description = [ | 
|  | 'Run the unittests for this tool. This runs all tests from all test cases. To run', | 
|  | 'a single test, use "python <path/to/test.py> <test_name>" instead.' | 
|  | ] | 
|  | unittest_parser._add_verbose_flag() | 
|  | unittest_parser.help = 'Run the unittests for this tool.' | 
|  | unittest_parser.set_defaults(command=command.run_unittests) | 
|  |  | 
|  | e2e_test_parser = self._add_parser('e2etest') | 
|  | e2e_test_parser.description = [ | 
|  | 'Run the end-to-end test for this tool. If a fuzzer is named, it', | 
|  | 'will be run. If none is specified, several example fuzzers will be', | 
|  | 'used. This requires the fuzzer(s) to have already been built and', | 
|  | 'deployed to a running device.' | 
|  | ] | 
|  | e2e_test_parser._add_flag( | 
|  | '-l', '--local', help=['Exclude corpus elements from Clusterfuzz.']) | 
|  | e2e_test_parser._add_verbose_flag() | 
|  | e2e_test_parser._add_name_argument(required=False) | 
|  | e2e_test_parser.help = 'Run the end-to-end test for this tool.' | 
|  | e2e_test_parser.set_defaults(command=command.run_e2e_test) | 
|  |  | 
|  | # TODO(fxb/24828) Once this is ready, merge with analyze or another tool | 
|  | coverage_parser = self._add_parser('coverage') | 
|  | coverage_parser.description = [ | 
|  | '[EXPERIMENTAL] Generates a coverage report for a set of tests.', | 
|  | 'Requires --variant profile to be set via fx set to generate the', | 
|  | 'necessary symbols. It is suggested to run with --no-goma in order', | 
|  | 'to preserve linking to files in the report.', | 
|  | ] | 
|  | coverage_parser._add_verbose_flag() | 
|  | coverage_parser._add_name_argument(required=True) | 
|  | coverage_parser._add_output_option() | 
|  | coverage_parser.help = 'Generate a coverage report for a test.' | 
|  | coverage_parser.set_defaults(command=command.measure_coverage) | 
|  |  | 
|  | self.epilog = [ | 
|  | 'See "fx fuzz help [SUBCOMMAND]" for details on each subcommand.', | 
|  | 'See also "fx help fuzz" for global "fx" options.', | 
|  | 'See https://fuchsia.dev/fuchsia-src/development/testing/fuzzing/libfuzzer', | 
|  | 'for details on writing and building fuzzers.', | 
|  | ] | 
|  |  | 
|  | def _add_parser(self, subcommand): | 
|  | """Return a subparser for a specific subcommand.""" | 
|  | parser = ArgParser(prog=subcommand) | 
|  | parser.host = self.host | 
|  | self._parsers[subcommand] = parser | 
|  | return parser | 
|  |  | 
|  | def _format_help(self, item, help_msg): | 
|  | """Format a help message for an argument or option. | 
|  |  | 
|  | If help_msg is None, the item will be suppressed from the help entirely. | 
|  | """ | 
|  | lines = [] | 
|  | if help_msg: | 
|  | lines += ['  {}'.format(item).ljust(22) + help_msg[0]] | 
|  | lines += [(' ' * 22) + line for line in help_msg[1:]] | 
|  | return lines | 
|  |  | 
|  | def _add_flag(self, short_opt, long_opt, **kwargs): | 
|  | """Add an optional command line boolean flag. | 
|  |  | 
|  | If help_msg is None, the option is suppressed from the help message. | 
|  | """ | 
|  | help_msg = kwargs.pop('help', None) | 
|  | item = '{},{}'.format(short_opt, long_opt) | 
|  | self._options_help += self._format_help(item, help_msg) | 
|  | kwargs['action'] = 'store_true' | 
|  | self.add_argument(short_opt, long_opt, **kwargs) | 
|  |  | 
|  | def add_option(self, short_opt, long_opt, **kwargs): | 
|  | """Add an command line option that takes a parameter. | 
|  |  | 
|  | If unique is False, the option may be repeated. | 
|  | If help_msg is None, the option is suppressed from the help message. | 
|  | """ | 
|  | help_msg = kwargs.pop('help', None) | 
|  | metavar = kwargs.pop('metavar', long_opt[2:].upper()) | 
|  | unique = kwargs.pop('unique', False) | 
|  | item = '{},{} {}'.format(short_opt, long_opt, metavar) | 
|  | self._options_help += self._format_help(item, help_msg) | 
|  | if unique: | 
|  | self._unique_options.append(long_opt[2:]) | 
|  | kwargs['action'] = 'append' | 
|  | self.add_argument(short_opt, long_opt, **kwargs) | 
|  |  | 
|  | def _add_argument(self, name, **kwargs): | 
|  | """Add a positional command line argument. | 
|  |  | 
|  | If help is None, the argument is suppressed from the help message. | 
|  | """ | 
|  | help_msg = kwargs.pop('help', None) | 
|  | metavar = kwargs.pop('metavar', name).upper() | 
|  | nargs = kwargs.get('nargs', None) | 
|  | usage = metavar | 
|  | if nargs == '+' or nargs == '*': | 
|  | usage = '{}...'.format(usage) | 
|  | if nargs == '?' or nargs == '*': | 
|  | usage = '[{}]'.format(usage) | 
|  | self.usage += ' {}'.format(usage) | 
|  | self._arguments_help += self._format_help(metavar, help_msg) | 
|  | self.add_argument(name, **kwargs) | 
|  |  | 
|  | def _add_name_argument(self, required=False): | 
|  | """Adds a fuzzer "NAME" argument. | 
|  |  | 
|  | If required is True, a fuzzer name must be supplied. | 
|  | """ | 
|  | self._add_argument( | 
|  | 'name', | 
|  | nargs=None if required else '?', | 
|  | help=[ | 
|  | 'Fuzzer name to match.  This can be part of the package', | 
|  | 'and/or target name, e.g. "foo", "bar", and "foo/bar" all', | 
|  | 'match "foo_package/bar_target".', | 
|  | ]) | 
|  |  | 
|  | def _add_debug_flag(self): | 
|  | """Adds a "--debug" flag.""" | 
|  | self._add_flag( | 
|  | '-g', | 
|  | '--debug', | 
|  | help=['Disable exception handling so a debugger can be attached']) | 
|  |  | 
|  | def _add_output_option(self): | 
|  | """Adds an "--output OUTPUT" option.""" | 
|  | self.add_option( | 
|  | '-o', | 
|  | '--output', | 
|  | unique=True, | 
|  | help=['Path under which to store results.']) | 
|  |  | 
|  | def _add_verbose_flag(self): | 
|  | """Adds a "--verbose" flag.""" | 
|  | self._add_flag('-v', '--verbose', help=['Display additional output.']) | 
|  |  | 
|  | def _add_libfuzzer_extras(self): | 
|  | """Adds liFuzzer options and subprocess arguments. | 
|  |  | 
|  | If a name is given, it will be listed as an argument. | 
|  | """ | 
|  | if not self._has_libfuzzer_extras: | 
|  | self._has_libfuzzer_extras = True | 
|  | self.usage += ' [...]' | 
|  | self.epilog = [ | 
|  | 'Additional options and/or arguments are passed through to libFuzzer.', | 
|  | 'See https://llvm.org/docs/LibFuzzer.html for details.', | 
|  | ] | 
|  |  | 
|  | def parse_args(self, args=None): | 
|  | """Parse args either as a top-level parser or subcommand.""" | 
|  | if args == None: | 
|  | args = sys.argv[1:] | 
|  | if self._parsers: | 
|  | return self._dispatch_to_subcommand(args) | 
|  | else: | 
|  | return self._parse_subcommand_args(args) | 
|  |  | 
|  | def _dispatch_to_subcommand(self, args): | 
|  | """Invokes the correct subcommand subparser.""" | 
|  | if not args or args[0] in ['-h', '--help']: | 
|  | subcommand = 'help' | 
|  | args = [] | 
|  | elif args[0] not in self._parsers: | 
|  | subcommand = 'start' | 
|  | else: | 
|  | subcommand = args[0] | 
|  | args = args[1:] | 
|  | self.host.trace('fx fuzz {} {}'.format(subcommand, ' '.join(args))) | 
|  | args = self._parsers[subcommand].parse_args(args) | 
|  | if subcommand != 'help': | 
|  | return args | 
|  | if not args.subcommand: | 
|  | self.host.echo(*self.generate_help()) | 
|  | elif args.subcommand in self._parsers: | 
|  | self.host.echo(*self._parsers[args.subcommand].generate_help()) | 
|  | else: | 
|  | self.error('Unrecognized subcommand: "{}".'.format(args.subcommand)) | 
|  | self.exit() | 
|  |  | 
|  | def _parse_subcommand_args(self, args): | 
|  | """Extract libFuzzer arguments and parse the rest. | 
|  |  | 
|  | This method extracts the libfuzzer options (of the form "-key=val") and | 
|  | the subprocess arguments (which follow "--") to avoid them getting | 
|  | mangled by argparse. The positional libFuzzer inputs are collected as | 
|  | expected, see _add_libfuzzer_inputs(). | 
|  | """ | 
|  | if self._has_libfuzzer_extras: | 
|  | libfuzzer_opts = {} | 
|  | pass_to_subprocess = False | 
|  | subprocess_args = [] | 
|  | valid = [] | 
|  | for arg in args: | 
|  | libfuzzer_opt = ArgParser.LIBFUZZER_OPT_RE.match(arg) | 
|  | if pass_to_subprocess: | 
|  | subprocess_args.append(arg) | 
|  | elif arg == '--': | 
|  | pass_to_subprocess = True | 
|  | elif libfuzzer_opt: | 
|  | libfuzzer_opts[libfuzzer_opt.group( | 
|  | 1)] = libfuzzer_opt.group(2) | 
|  | elif (ArgParser.SHORT_OPT_RE.match(arg) or | 
|  | ArgParser.LONG_OPT_RE.match(arg) or | 
|  | ArgParser.POSITIONAL_RE.match(arg)): | 
|  | valid.append(arg) | 
|  | else: | 
|  | self.error('Unrecognized option: {}'.format(arg)) | 
|  | args = valid | 
|  |  | 
|  | # Parse! | 
|  | args = super(ArgParser, self).parse_args(args) | 
|  |  | 
|  | # Check for options that were incorrectly repeated | 
|  | for key in self._unique_options: | 
|  | val = getattr(args, key, None) | 
|  | if not val: | 
|  | continue | 
|  | if len(val) == 1: | 
|  | setattr(args, key, val[0]) | 
|  | else: | 
|  | self.error('Repeated option: {}'.format(key)) | 
|  |  | 
|  | # Forward libFuzzer arguments | 
|  | if self._has_libfuzzer_extras: | 
|  | args.libfuzzer_opts = libfuzzer_opts | 
|  | args.subprocess_args = subprocess_args | 
|  |  | 
|  | return args | 
|  |  | 
|  | def generate_help(self): | 
|  | """Builds the help message as a list of lines. | 
|  |  | 
|  | Example output: | 
|  | Usage: fx fuzz foobar [OPTIONS] NAME [...] | 
|  |  | 
|  | Arguments: | 
|  | NAME         The thing it's called | 
|  |  | 
|  | Options: | 
|  | --foo BAR    A baz, and then some. | 
|  | """ | 
|  | lines = [''] | 
|  | usage = 'Usage: fx fuzz ' | 
|  | if not self._parsers: | 
|  | usage += self.prog | 
|  | if self._options_help: | 
|  | usage += ' [OPTIONS]' | 
|  | usage += self.usage | 
|  | lines += [usage] | 
|  |  | 
|  | if self.description: | 
|  | lines += [''] | 
|  | lines += self.description | 
|  |  | 
|  | if self._parsers: | 
|  | lines += [''] | 
|  | lines += ['Subcommands:'] | 
|  | for prog, subcommand in sorted(self._parsers.iteritems()): | 
|  | lines += self._format_help(prog, [subcommand.help]) | 
|  |  | 
|  | if self._arguments_help: | 
|  | lines += [''] | 
|  | lines += ['Arguments:'] | 
|  | lines += self._arguments_help | 
|  |  | 
|  | if self._options_help: | 
|  | lines += [''] | 
|  | lines += ['Options:'] | 
|  | lines += self._options_help | 
|  |  | 
|  | if self.epilog: | 
|  | lines += [''] | 
|  | lines += self.epilog | 
|  |  | 
|  | lines += [''] | 
|  |  | 
|  | for line in lines: | 
|  | assert len(line) <= 80, 'Line is too long:\n"{}"'.format(line) | 
|  |  | 
|  | return lines | 
|  |  | 
|  | def exit(self, status=0, message=None): | 
|  | if message: | 
|  | self.host.error(message, 'Try "fx fuzz help".') | 
|  | sys.exit(status) | 
|  |  | 
|  | def error(self, message): | 
|  | """Prints an error message and exits. | 
|  |  | 
|  | Cleans up the argparse messages before passing them to exit, e.g. | 
|  | argparse's messages are lowercase and unpunctuated. | 
|  | """ | 
|  | if ':' not in message and not message.endswith('.'): | 
|  | message += '.' | 
|  | self.exit(2, message.capitalize()) |