cargo-vet 0.3.1

Supply-chain security for Rust
use std::{path::PathBuf, str::FromStr};

use cargo_metadata::Version;
use clap::{Parser, Subcommand, ValueEnum};
use tracing::level_filters::LevelFilter;

use crate::format::{CriteriaName, PackageName, VersionReq};

#[derive(Parser)]
#[clap(version, about, long_about = None)]
#[clap(propagate_version = true)]
#[clap(bin_name = "cargo")]
pub enum FakeCli {
    Vet(Cli),
}

#[derive(clap::Args)]
#[clap(version)]
#[clap(bin_name = "cargo vet")]
#[clap(args_conflicts_with_subcommands = true)]
#[clap(global_setting(clap::AppSettings::DeriveDisplayOrder))]
/// Supply-chain security for Rust
///
/// When run without a subcommand, `cargo vet` will invoke the `check`
/// subcommand. See `cargo vet help check` for more details.
pub struct Cli {
    /// Subcommands ("no subcommand" defaults to `check`)
    #[clap(subcommand)]
    pub command: Option<Commands>,

    // Top-level flags
    /// Path to Cargo.toml
    #[clap(long, name = "PATH", parse(from_os_str))]
    #[clap(help_heading = "GLOBAL OPTIONS", global = true)]
    pub manifest_path: Option<PathBuf>,

    /// Don't use --all-features
    ///
    /// We default to passing --all-features to `cargo metadata`
    /// because we want to analyze your full dependency tree
    #[clap(long, action)]
    #[clap(help_heading = "GLOBAL OPTIONS", global = true)]
    pub no_all_features: bool,

    /// Do not activate the `default` feature
    #[clap(long, action)]
    #[clap(help_heading = "GLOBAL OPTIONS", global = true)]
    pub no_default_features: bool,

    /// Space-separated list of features to activate
    #[clap(long, action, require_value_delimiter = true, value_delimiter = ' ')]
    #[clap(help_heading = "GLOBAL OPTIONS", global = true)]
    pub features: Vec<String>,

    /// Do not fetch new imported audits.
    #[clap(long, action)]
    #[clap(help_heading = "GLOBAL OPTIONS", global = true)]
    pub locked: bool,

    /// Avoid the network entirely, requiring either that the cargo cache is
    /// populated or the dependencies are vendored. Requires --locked.
    #[clap(long, action)]
    #[clap(requires = "locked")]
    #[clap(help_heading = "GLOBAL OPTIONS", global = true)]
    pub frozen: bool,

    /// Prevent commands such as `check` and `certify` from automatically
    /// cleaning up unused exemptions.
    #[clap(long, action)]
    #[clap(help_heading = "GLOBAL OPTIONS", global = true)]
    pub no_minimize_exemptions: bool,

    /// How verbose logging should be (log level)
    #[clap(long, action)]
    #[clap(default_value_t = LevelFilter::WARN)]
    #[clap(possible_values = ["off", "error", "warn", "info", "debug", "trace"])]
    #[clap(help_heading = "GLOBAL OPTIONS", global = true)]
    pub verbose: LevelFilter,

    /// Instead of stdout, write output to this file
    #[clap(long, action)]
    #[clap(help_heading = "GLOBAL OPTIONS", global = true)]
    pub output_file: Option<PathBuf>,

    /// Instead of stderr, write logs to this file (only used after successful CLI parsing)
    #[clap(long, action)]
    #[clap(help_heading = "GLOBAL OPTIONS", global = true)]
    pub log_file: Option<PathBuf>,

    /// The format of the output
    #[clap(long, value_enum, action)]
    #[clap(default_value_t = OutputFormat::Human)]
    #[clap(help_heading = "GLOBAL OPTIONS", global = true)]
    pub output_format: OutputFormat,

    /// Use the following path as the diff-cache
    ///
    /// The diff-cache stores the summary results used by vet's suggestion machinery.
    /// This is automatically managed in vet's tempdir, but if you want to manually store
    /// it somewhere more reliable, you can.
    ///
    /// This mostly exists for testing vet itself.
    #[clap(long, action)]
    #[clap(help_heading = "GLOBAL OPTIONS", global = true)]
    pub diff_cache: Option<PathBuf>,

    /// Filter out different parts of the build graph and pretend that's the true graph
    ///
    /// Example: `--filter-graph="exclude(any(eq(is_dev_only(true)),eq(name(serde_derive))))"`
    ///
    /// This mostly exists to debug or reduce projects that cargo-vet is mishandling.
    /// Combining this with `cargo vet --output-format=json dump-graph` can produce an
    /// input that can be added to vet's test suite.
    ///
    ///
    /// The resulting graph is computed as follows:
    ///
    /// 1. First compute the original graph
    /// 2. Then apply the filters to find the new set of nodes
    /// 3. Create a new empty graph
    /// 4. For each workspace member that still exists, recursively add it and its dependencies
    ///
    /// This means that any non-workspace package that becomes "orphaned" by the filters will
    /// be implicitly discarded even if it passes the filters.
    ///
    /// Possible filters:
    ///
    /// * `include($query)`: only include packages that match this filter
    /// * `exclude($query)`: exclude packages that match this filter
    ///
    ///
    /// Possible queries:
    ///
    /// * `any($query1, $query2, ...)`: true if any of the listed queries are true
    /// * `all($query1, $query2, ...)`: true if all of the listed queries are true
    /// * `not($query)`: true if the query is false
    /// * `$property`: true if the package has this property
    ///
    ///
    /// Possible properties:
    ///
    /// * `name($string)`: the package's name (i.e. `serde`)
    /// * `version($version)`: the package's version (i.e. `1.2.0`)
    /// * `is_root($bool)`: whether it's a root in the original graph (ignoring dev-deps)
    /// * `is_workspace_member($bool)`: whether the package is a workspace-member (can be tested)
    /// * `is_third_party($bool)`: whether the package is considered third-party by vet
    /// * `is_dev_only($bool)`: whether it's only used by dev (test) builds in the original graph
    #[clap(long, action)]
    #[clap(verbatim_doc_comment)]
    #[clap(help_heading = "GLOBAL OPTIONS", global = true)]
    pub filter_graph: Option<Vec<GraphFilter>>,

    // Args for `Check` when the subcommand is not explicitly specified.
    //
    // These are exclusive with specifying a subcommand due to
    // `args_conflicts_with_subcommand`.
    #[clap(flatten)]
    pub check_args: CheckArgs,
}

#[derive(Subcommand)]
pub enum Commands {
    // Main commands:
    /// \[default\] Check that the current project has been vetted
    ///
    /// This is the default behaviour if no subcommand is specified.
    ///
    /// If the check fails due to lack of audits, we will do our best to explain why
    /// vetting failed, and what should be done to fix it. This can involve a certain
    /// amount of guesswork, as there are many possible solutions and we only want to recommend
    /// the "best" one to keep things simple.
    ///
    /// Failures and suggestions can either be "Certain" or "Speculative". Speculative items
    /// are greyed out and sorted lower to indicate that the Certain entries should be looked
    /// at first. Speculative items are for packages that probably need audits too, but
    /// only appear as transitive dependencies of Certain items.
    ///
    /// During review of Certain issues you may take various actions that change what's needed
    /// for the Speculative ones. For instance you may discover you're enabling a feature you
    /// don't need, and that's the only reason the Speculative package is in your tree. Or you
    /// may determine that the Certain package only needs to be safe-to-run, which may make
    /// the Speculative requirements weaker or completely resolved. For these reasons we
    /// recommend fixing problems "top down", and Certain items are The Top.
    ///
    /// Suggested fixes are grouped by the criteria they should be reviewed for and sorted by
    /// how easy the review should be (in terms of lines of code). We only ever suggest audits
    /// (and provide the command you need to run to do it), but there are other possible fixes
    /// like an `exemption` or `policy` change.
    ///
    /// The most aggressive solution is to run `cargo vet regenerate exemptions` which will
    /// add whatever exemptions necessary to make `check` pass (and remove uneeded ones).
    /// Ideally you should avoid doing this and prefer adding audits, but if you've done all
    /// the audits you plan on doing, that's the way to finish the job.
    #[clap(disable_version_flag = true)]
    Check(CheckArgs),

    /// Suggest some low-hanging fruit to review
    ///
    /// This is essentially the same as `check` but with all your `exemptions` temporarily
    /// removed as a way to inspect your "review backlog". As such, we recommend against
    /// running this command while `check` is failing, because this will just give you worse
    /// information.
    ///
    /// If you don't consider an exemption to be "backlog", add `suggest = false` to its
    /// entry and we won't remove it while suggesting.
    ///
    /// See also `regenerate exemptions`, which can be used to "garbage collect"
    /// your backlog (if you run it while `check` is passing).
    #[clap(disable_version_flag = true)]
    Suggest(SuggestArgs),

    /// Initialize cargo-vet for your project
    ///
    /// This will add `exemptions` and `audit-as-crates-io = false` for all packages that
    /// need it to make `check` pass immediately and make it easy to start using vet with
    /// your project.
    ///
    /// At this point you can either configure your project further or start working on your
    /// review backlog with `suggest`.
    #[clap(disable_version_flag = true)]
    Init(InitArgs),

    // Fetch Commands
    /// Fetch the source of a package
    ///
    /// We will attempt to guess what criteria you want to audit the package for
    /// based on the current check/suggest status, and show you the meaning of
    /// those criteria ahead of time.
    #[clap(disable_version_flag = true)]
    Inspect(InspectArgs),

    /// Yield a diff against the last reviewed version
    ///
    /// We will attempt to guess what criteria you want to audit the package for
    /// based on the current check/suggest status, and show you the meaning of
    /// those criteria ahead of time.
    #[clap(disable_version_flag = true)]
    Diff(DiffArgs),

    // Update State Commands
    /// Mark a package as audited
    ///
    /// This command will do its best to guess what you want to be certifying.
    ///
    /// If invoked with no args, it will try to certify the last thing you looked at
    /// with `inspect` or `diff`. Otherwise you must either supply the package name
    /// and one version (for a full audit) or two versions (for a delta audit).
    ///
    /// Once the package+version(s) have been selected, we will try to guess what
    /// criteria to certify it for. First we will `check`, and if the check fails
    /// and your audit would seemingly fix this package, we will use the criteria
    /// recommended for that fix. If `check` passes, we will assume you are working
    /// on your backlog and instead use the recommendations of `suggest`.
    ///
    /// If this removes the need for an `exemption` will we automatically remove it.
    #[clap(disable_version_flag = true)]
    Certify(CertifyArgs),

    /// Explicitly regenerate various pieces of information
    ///
    /// There are several things that `cargo vet` *can* do for you automatically
    /// but we choose to make manual just to keep a human in the loop of those
    /// decisions. Some of these might one day become automatic if we agree they're
    /// boring/reliable enough.
    ///
    /// See the subcommands for specifics.
    #[clap(disable_version_flag = true)]
    #[clap(subcommand)]
    Regenerate(RegenerateSubcommands),

    /// Mark a package as exempted from review
    ///
    /// Exemptions are *usually* just "backlog" and the expectation is that you will review
    /// them "eventually". You should usually only be trying to remove them, but sometimes
    /// additions are necessary to make progress.
    ///
    /// `regenerate exemptions` will do this for your automatically to make `check` pass
    /// (and remove any unnecessary ones), so we recommend using that over `add-exemption`.
    /// This command mostly exists as "plumbing" for building tools on top of `cargo vet`.
    #[clap(disable_version_flag = true)]
    AddExemption(AddExemptionArgs),

    /// Declare that some versions of a package violate certain audit criteria
    ///
    /// **IMPORTANT**: violations take *VersionReqs* not *Versions*. This is the same
    /// syntax used by Cargo.toml when specifying dependencies. A bare `1.0.0` actually
    /// means `^1.0.0`. If you want to forbid a *specific* version, use `=1.0.0`.
    /// This command can be a bit awkward because syntax like `*` has special meaning
    /// in scripts and terminals. It's probably easier to just manually add the entry
    /// to your audits.toml, but the command's here in case you want it.
    ///
    /// Violations are essentially treated as integrity constraints on your supply-chain,
    /// and will only result in errors if you have `exemptions` or `audits` (including
    /// imported ones) that claim criteria that are contradicted by the `violation`.
    /// It is not inherently an error to depend on a package with a `violation`.
    ///
    /// For instance, someone may review a package and determine that it's horribly
    /// unsound in the face of untrusted inputs, and therefore *un*safe-to-deploy. They
    /// would then add a "safe-to-deploy" violation for whatever versions of that
    /// package seem to have that problem. But if the package basically works fine
    /// on trusted inputs, it might still be safe-to-run. So if you use it in your
    /// tests and have an audit that only claims safe-to-run, we won't mention it.
    ///
    /// When a violation *does* cause an integrity error, it's up to you and your
    /// peers to figure out what to do about it. There isn't yet a mechanism for
    /// dealing with disagreements with a peer's published violations.
    #[clap(disable_version_flag = true)]
    RecordViolation(RecordViolationArgs),

    // Plumbing/Debug Commands
    /// Reformat all of vet's files (in case you hand-edited them)
    ///
    /// Most commands will implicitly do this, so this mostly exists as "plumbing"
    /// for building tools on top of vet, or in case you don't want to run another command.
    #[clap(disable_version_flag = true)]
    Fmt(FmtArgs),

    /// Explicitly fetch the imports (foreign audit files)
    ///
    /// `cargo vet check` will implicitly do this, so this mostly exists as "plumbing"
    /// for building tools on top of vet.
    #[clap(disable_version_flag = true)]
    FetchImports(FetchImportsArgs),

    /// Print the cargo build graph as understood by `cargo vet`
    ///
    /// This is a debugging command, the output's format is not guaranteed.
    /// Use `cargo metadata` to get a stable version of what *cargo* thinks the
    /// build graph is. Our graph is based on that result.
    ///
    /// With `--output-format=human` (the default) this will print out mermaid-js
    /// diagrams, which things like github natively support rendering of.
    ///
    /// With `--output-format=json` we will print out more raw statistics for you
    /// to search/analyze.
    ///
    /// Most projects will have unreadably complex build graphs, so you may want to
    /// use the global `--filter-graph` argument to narrow your focus on an interesting
    /// subgraph. `--filter-graph` is applied *before* doing any semantic analysis,
    /// so if you filter out a package and it was the problem, the problem will disappear.
    /// This can be used to bisect a problem if you get ambitious enough with your filters.
    #[clap(disable_version_flag = true)]
    DumpGraph(DumpGraphArgs),

    /// Print --help as markdown (for generating docs)
    ///
    /// The output of this is not stable or guaranteed.
    #[clap(disable_version_flag = true)]
    #[clap(hide = true)]
    HelpMarkdown(HelpMarkdownArgs),

    /// Clean up old packages from the vet cache
    ///
    /// Removes packages which haven't been accessed in a while, and deletes
    /// any extra files which aren't recognized by cargo-vet.
    ///
    /// In the future, many cargo-vet subcommands will implicitly do this.
    #[clap(disable_version_flag = true)]
    Gc(GcArgs),
}

#[derive(Subcommand)]
pub enum RegenerateSubcommands {
    /// Regenerate your exemptions to make `check` pass minimally
    ///
    /// This command can be used for two purposes: to force your supply-chain to pass `check`
    /// when it's currently failing, or to minimize/garbage-collect your exemptions when it's
    /// already passing. These are ultimately the same operation.
    ///
    /// We will try our best to preserve existing exemptions, removing only those that
    /// aren't needed, and adding only those that are needed. Exemptions that are overbroad
    /// may also be weakened (i.e. safe-to-deploy may be reduced to safe-to-run).
    #[clap(disable_version_flag = true)]
    Exemptions(RegenerateExemptionsArgs),

    /// Regenerate your imports and accept changes to criteria
    ///
    /// This is equivalent to `cargo vet fetch-imports` but it won't produce an error if
    /// the descriptions of foreign criteria change.
    #[clap(disable_version_flag = true)]
    Imports(RegenerateImportsArgs),

    /// Regenerate you audit-as-crates-io entries to make `check` pass
    ///
    /// This will just set any problematic entries to `audit-as-crates-io = false`.
    #[clap(disable_version_flag = true)]
    AuditAsCratesIo(RegenerateAuditAsCratesIoArgs),
}

#[derive(clap::Args)]
pub struct CheckArgs {
    /// Avoid suggesting audits for dependencies of unaudited dependencies.
    ///
    /// By default, if a dependency doesn't have sufficient audits for *itself*
    /// then we try to speculate that its dependencies require the criteria.
    /// This flag disables that behaviour, causing only suggestions which we're
    /// certain of the requirements for to be emitted.
    #[clap(long, action)]
    pub shallow: bool,
}

#[derive(clap::Args)]
pub struct InitArgs {}

/// Fetches the crate to a temp location and pushd's to it
#[derive(clap::Args)]
pub struct InspectArgs {
    /// The package to inspect
    #[clap(action)]
    pub package: PackageName,
    /// The version to inspect
    #[clap(action)]
    pub version: Version,
    /// How to inspect the source
    #[clap(long, action, default_value = "sourcegraph")]
    pub mode: FetchMode,
}

/// Emits a diff of the two versions
#[derive(clap::Args)]
pub struct DiffArgs {
    /// The package to diff
    #[clap(action)]
    pub package: PackageName,
    /// The base version to diff
    #[clap(action)]
    pub version1: Version,
    /// The target version to diff
    #[clap(action)]
    pub version2: Version,
    /// How to inspect the source
    #[clap(long, action, default_value = "sourcegraph")]
    pub mode: FetchMode,
}

/// Certifies a package as audited
#[derive(clap::Args)]
pub struct CertifyArgs {
    /// The package to certify as audited
    #[clap(action)]
    pub package: Option<PackageName>,
    /// The version to certify as audited
    #[clap(action)]
    pub version1: Option<Version>,
    /// If present, instead certify a diff from version1->version2
    #[clap(action)]
    pub version2: Option<Version>,
    /// The criteria to certify for this audit
    ///
    /// If not provided, we will prompt you for this information.
    #[clap(long, action)]
    pub criteria: Vec<CriteriaName>,
    /// The dependency-criteria to require for this audit to be valid
    ///
    /// If not provided, we will still implicitly require dependencies to satisfy `criteria`.
    #[clap(long, action)]
    pub dependency_criteria: Vec<DependencyCriteriaArg>,
    /// Who to name as the auditor
    ///
    /// If not provided, we will collect this information from the local git.
    #[clap(long, action)]
    pub who: Option<String>,
    /// A free-form string to include with the new audit entry
    ///
    /// If not provided, there will be no notes.
    #[clap(long, action)]
    pub notes: Option<String>,
    /// Accept all criteria without an interactive prompt
    #[clap(long, action)]
    pub accept_all: bool,
    /// Force the command to ignore whether the package/version makes sense
    ///
    /// To catch typos/mistakes, we check if the thing you're trying to
    /// talk about is part of your current build, but this flag disables that.
    #[clap(long, action)]
    pub force: bool,
}

/// Forbids the given version
#[derive(clap::Args)]
pub struct RecordViolationArgs {
    /// The package to forbid
    #[clap(action)]
    pub package: PackageName,
    /// The versions to forbid
    #[clap(action)]
    pub versions: VersionReq,
    /// The criteria that have failed to be satisfied.
    ///
    /// If not provided, we will prompt you for this information(?)
    #[clap(long, action)]
    pub criteria: Vec<CriteriaName>,
    /// Who to name as the auditor
    ///
    /// If not provided, we will collect this information from the local git.
    #[clap(long, action)]
    pub who: Option<String>,
    /// A free-form string to include with the new forbid entry
    ///
    /// If not provided, there will be no notes.
    #[clap(long, action)]
    pub notes: Option<String>,
    /// Force the command to ignore whether the package/version makes sense
    ///
    /// To catch typos/mistakes, we check if the thing you're trying to
    /// talk about is part of your current build, but this flag disables that.
    #[clap(long, action)]
    pub force: bool,
}

/// Certifies the given version
#[derive(clap::Args)]
pub struct AddExemptionArgs {
    /// The package to mark as exempted
    #[clap(action)]
    pub package: PackageName,
    /// The version to mark as exempted
    #[clap(action)]
    pub version: Version,
    /// The criteria to assume (trust)
    ///
    /// If not provided, we will prompt you for this information.
    #[clap(long, action)]
    pub criteria: Vec<CriteriaName>,
    /// The dependency-criteria to require for this exemption to be valid
    ///
    /// If not provided, we will still implicitly require dependencies to satisfy `criteria`.
    #[clap(long, action)]
    pub dependency_criteria: Vec<DependencyCriteriaArg>,
    /// A free-form string to include with the new forbid entry
    ///
    /// If not provided, there will be no notes.
    #[clap(long, action)]
    pub notes: Option<String>,
    /// Suppress suggesting this exemption for review
    #[clap(long, action)]
    pub no_suggest: bool,
    /// Force the command to ignore whether the package/version makes sense
    ///
    /// To catch typos/mistakes, we check if the thing you're trying to
    /// talk about is part of your current build, but this flag disables that.
    #[clap(long, action)]
    pub force: bool,
}

#[derive(clap::Args)]
pub struct SuggestArgs {
    /// Avoid suggesting audits for dependencies of unaudited dependencies.
    ///
    /// By default, if a dependency doesn't have sufficient audits for *itself*
    /// then we try to speculate that its dependencies require the criteria.
    /// This flag disables that behaviour, causing only suggestions which we're
    /// certain of the requirements for to be emitted.
    #[clap(long, action)]
    pub shallow: bool,
}

#[derive(clap::Args)]
pub struct FmtArgs {}

#[derive(clap::Args)]
pub struct FetchImportsArgs {}

#[derive(clap::Args)]
pub struct RegenerateExemptionsArgs {}

#[derive(clap::Args)]
pub struct RegenerateImportsArgs {}

#[derive(clap::Args)]
pub struct RegenerateAuditAsCratesIoArgs {}

#[derive(clap::Args)]
pub struct HelpMarkdownArgs {}

#[derive(clap::Args)]
pub struct GcArgs {
    /// Packages in the vet cache which haven't been used for this many days
    /// will be removed.
    #[clap(long, action)]
    #[clap(default_value_t = 30.0)]
    pub max_package_age_days: f64,

    /// Remove the entire cache directory, forcing it to be regenerated next
    /// time you use cargo vet.
    #[clap(long, action)]
    pub clean: bool,
}

#[derive(clap::Args)]
pub struct DumpGraphArgs {
    /// The depth of the graph to print (for a large project, the full graph is a HUGE MESS).
    #[clap(long, value_enum, action)]
    #[clap(default_value_t = DumpGraphDepth::FirstParty)]
    pub depth: DumpGraphDepth,
}

#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum)]
pub enum DumpGraphDepth {
    Roots,
    Workspace,
    FirstParty,
    FirstPartyAndDirects,
    Full,
}

/// Logging verbosity levels
#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum)]
pub enum Verbose {
    Off,
    Error,
    Warn,
    Info,
    Debug,
    Trace,
}

#[derive(Clone, Debug)]
pub struct DependencyCriteriaArg {
    pub dependency: PackageName,
    pub criteria: CriteriaName,
}

impl FromStr for DependencyCriteriaArg {
    // the error must be owned as well
    type Err = String;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        use nom::{
            bytes::complete::{is_not, tag},
            combinator::all_consuming,
            error::{convert_error, VerboseError},
            sequence::tuple,
            Finish, IResult,
        };
        type ParseResult<I, O> = IResult<I, O, VerboseError<I>>;

        fn parse(input: &str) -> ParseResult<&str, DependencyCriteriaArg> {
            let (rest, (dependency, _, criteria)) =
                all_consuming(tuple((is_not(":"), tag(":"), is_not(":"))))(input)?;
            Ok((
                rest,
                DependencyCriteriaArg {
                    dependency: dependency.to_string(),
                    criteria: criteria.to_string(),
                },
            ))
        }

        match parse(s).finish() {
            Ok((_remaining, val)) => Ok(val),
            Err(e) => Err(convert_error(s, e)),
        }
    }
}

#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum)]
pub enum FetchMode {
    Local,
    Sourcegraph,
}

#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum)]
pub enum OutputFormat {
    Human,
    Json,
}

#[derive(Clone, Debug)]
pub enum GraphFilter {
    Include(GraphFilterQuery),
    Exclude(GraphFilterQuery),
}

#[derive(Clone, Debug)]
pub enum GraphFilterQuery {
    Any(Vec<GraphFilterQuery>),
    All(Vec<GraphFilterQuery>),
    Not(Box<GraphFilterQuery>),
    Prop(GraphFilterProperty),
}

#[derive(Clone, Debug)]
pub enum GraphFilterProperty {
    Name(PackageName),
    Version(Version),
    IsRoot(bool),
    IsWorkspaceMember(bool),
    IsThirdParty(bool),
    IsDevOnly(bool),
}

impl FromStr for GraphFilter {
    type Err = String;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        use nom::{
            branch::alt,
            bytes::complete::{is_not, tag},
            character::complete::multispace0,
            combinator::{all_consuming, cut},
            error::{convert_error, ParseError, VerboseError, VerboseErrorKind},
            multi::separated_list1,
            sequence::delimited,
            Finish, IResult,
        };
        type ParseResult<I, O> = IResult<I, O, VerboseError<I>>;

        fn parse(input: &str) -> ParseResult<&str, GraphFilter> {
            all_consuming(alt((include_filter, exclude_filter)))(input)
        }
        fn include_filter(input: &str) -> ParseResult<&str, GraphFilter> {
            let (rest, val) =
                delimited(ws(tag("include(")), cut(filter_query), ws(tag(")")))(input)?;
            Ok((rest, GraphFilter::Include(val)))
        }
        fn exclude_filter(input: &str) -> ParseResult<&str, GraphFilter> {
            let (rest, val) =
                delimited(ws(tag("exclude(")), cut(filter_query), ws(tag(")")))(input)?;
            Ok((rest, GraphFilter::Exclude(val)))
        }
        fn filter_query(input: &str) -> ParseResult<&str, GraphFilterQuery> {
            alt((any_query, all_query, not_query, prop_query))(input)
        }
        fn any_query(input: &str) -> ParseResult<&str, GraphFilterQuery> {
            let (rest, val) = delimited(
                ws(tag("any(")),
                cut(separated_list1(tag(","), cut(filter_query))),
                ws(tag(")")),
            )(input)?;
            Ok((rest, GraphFilterQuery::Any(val)))
        }
        fn all_query(input: &str) -> ParseResult<&str, GraphFilterQuery> {
            let (rest, val) = delimited(
                ws(tag("all(")),
                cut(separated_list1(tag(","), cut(filter_query))),
                ws(tag(")")),
            )(input)?;
            Ok((rest, GraphFilterQuery::All(val)))
        }
        fn not_query(input: &str) -> ParseResult<&str, GraphFilterQuery> {
            let (rest, val) = delimited(ws(tag("not(")), cut(filter_query), ws(tag(")")))(input)?;
            Ok((rest, GraphFilterQuery::Not(Box::new(val))))
        }
        fn prop_query(input: &str) -> ParseResult<&str, GraphFilterQuery> {
            let (rest, val) = filter_property(input)?;
            Ok((rest, GraphFilterQuery::Prop(val)))
        }
        fn filter_property(input: &str) -> ParseResult<&str, GraphFilterProperty> {
            alt((
                prop_name,
                prop_version,
                prop_is_root,
                prop_is_workspace_member,
                prop_is_third_party,
                prop_is_dev_only,
            ))(input)
        }
        fn prop_name(input: &str) -> ParseResult<&str, GraphFilterProperty> {
            let (rest, val) =
                delimited(ws(tag("name(")), cut(val_package_name), ws(tag(")")))(input)?;
            Ok((rest, GraphFilterProperty::Name(val.to_string())))
        }
        fn prop_version(input: &str) -> ParseResult<&str, GraphFilterProperty> {
            let (rest, val) =
                delimited(ws(tag("version(")), cut(val_version), ws(tag(")")))(input)?;
            Ok((rest, GraphFilterProperty::Version(val)))
        }
        fn prop_is_root(input: &str) -> ParseResult<&str, GraphFilterProperty> {
            let (rest, val) = delimited(ws(tag("is_root(")), cut(val_bool), ws(tag(")")))(input)?;
            Ok((rest, GraphFilterProperty::IsRoot(val)))
        }
        fn prop_is_workspace_member(input: &str) -> ParseResult<&str, GraphFilterProperty> {
            let (rest, val) =
                delimited(ws(tag("is_workspace_member(")), cut(val_bool), ws(tag(")")))(input)?;
            Ok((rest, GraphFilterProperty::IsWorkspaceMember(val)))
        }
        fn prop_is_third_party(input: &str) -> ParseResult<&str, GraphFilterProperty> {
            let (rest, val) =
                delimited(ws(tag("is_third_party(")), cut(val_bool), ws(tag(")")))(input)?;
            Ok((rest, GraphFilterProperty::IsThirdParty(val)))
        }
        fn prop_is_dev_only(input: &str) -> ParseResult<&str, GraphFilterProperty> {
            let (rest, val) =
                delimited(ws(tag("is_dev_only(")), cut(val_bool), ws(tag(")")))(input)?;
            Ok((rest, GraphFilterProperty::IsDevOnly(val)))
        }
        fn val_bool(input: &str) -> ParseResult<&str, bool> {
            alt((val_true, val_false))(input)
        }
        fn val_true(input: &str) -> ParseResult<&str, bool> {
            let (rest, _val) = ws(tag("true"))(input)?;
            Ok((rest, true))
        }
        fn val_false(input: &str) -> ParseResult<&str, bool> {
            let (rest, _val) = ws(tag("false"))(input)?;
            Ok((rest, false))
        }
        fn val_package_name(input: &str) -> ParseResult<&str, &str> {
            is_not(") ")(input)
        }
        fn val_version(input: &str) -> ParseResult<&str, Version> {
            let (rest, val) = is_not(") ")(input)?;
            let val = Version::from_str(val).map_err(|_e| {
                nom::Err::Failure(VerboseError {
                    errors: vec![(val, VerboseErrorKind::Context("version parse error"))],
                })
            })?;
            Ok((rest, val))
        }
        fn ws<'a, F: 'a, O, E: ParseError<&'a str>>(
            inner: F,
        ) -> impl FnMut(&'a str) -> IResult<&'a str, O, E>
        where
            F: Fn(&'a str) -> IResult<&'a str, O, E>,
        {
            delimited(multispace0, inner, multispace0)
        }

        match parse(s).finish() {
            Ok((_remaining, val)) => Ok(val),
            Err(e) => Err(convert_error(s, e)),
        }
    }
}