use super::super::CommandSpec;
use crate::config::CargoConfig;
use crate::eval::{CommandContext, Decision, RuleMatch};
use agent_shell_parser::parse::Word;
use std::collections::HashMap;
pub struct CargoSpec {
safe_subcommands: Vec<String>,
allowed_with_config: Vec<String>,
config_env: HashMap<String, String>,
}
impl CargoSpec {
pub fn from_config(config: &CargoConfig) -> Self {
Self {
safe_subcommands: config.safe_subcommands.clone(),
allowed_with_config: config.allowed_with_config.clone(),
config_env: config.config_env.clone(),
}
}
fn subcommand(ctx: &CommandContext) -> Option<&Word> {
let mut iter = ctx.words.iter();
for word in iter.by_ref() {
if word == "cargo" {
return iter.find(|w| !w.is_flag());
}
}
None
}
fn env_keys_display(&self) -> String {
let mut keys: Vec<&str> = self.config_env.keys().map(|k| k.as_str()).collect();
keys.sort();
keys.join(", ")
}
}
impl CommandSpec for CargoSpec {
fn evaluate(&self, ctx: &CommandContext) -> RuleMatch {
let sub_str: &str = Self::subcommand(ctx).map(|w| w.as_str()).unwrap_or("?");
if self.safe_subcommands.iter().any(|s| s == sub_str) {
if let Some(ref r) = ctx.redirection {
return RuleMatch {
decision: Decision::Ask,
reason: format!("cargo {sub_str} with {}", r),
};
}
return RuleMatch {
decision: Decision::Allow,
reason: format!("cargo {sub_str}"),
};
}
if self.allowed_with_config.iter().any(|s| s == sub_str) {
if !self.config_env.is_empty() && ctx.env_satisfies(&self.config_env) {
if let Some(ref r) = ctx.redirection {
return RuleMatch {
decision: Decision::Ask,
reason: format!("cargo {sub_str} with {}", r),
};
}
return RuleMatch {
decision: Decision::Allow,
reason: format!("cargo {sub_str} with {}", self.env_keys_display()),
};
}
return RuleMatch {
decision: Decision::Ask,
reason: format!("cargo {sub_str} requires confirmation"),
};
}
if ctx.has_any_flag(&["--version", "-V"]) {
return RuleMatch {
decision: Decision::Allow,
reason: "cargo --version".into(),
};
}
RuleMatch {
decision: Decision::Ask,
reason: format!("cargo {sub_str} requires confirmation"),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::config::Config;
fn spec() -> CargoSpec {
CargoSpec::from_config(&Config::default_config().cargo)
}
fn eval(cmd: &str) -> Decision {
let s = spec();
let ctx = CommandContext::from_command(cmd);
s.evaluate(&ctx).decision
}
#[test]
fn allow_build() {
assert_eq!(eval("cargo build --release"), Decision::Allow);
}
#[test]
fn allow_test() {
assert_eq!(eval("cargo test"), Decision::Allow);
}
#[test]
fn allow_clippy() {
assert_eq!(eval("cargo clippy"), Decision::Allow);
}
#[test]
fn allow_version() {
assert_eq!(eval("cargo --version"), Decision::Allow);
}
#[test]
fn allow_version_short() {
assert_eq!(eval("cargo -V"), Decision::Allow);
}
#[test]
fn ask_install() {
assert_eq!(eval("cargo install ripgrep"), Decision::Ask);
}
#[test]
fn ask_publish() {
assert_eq!(eval("cargo publish"), Decision::Ask);
}
#[test]
fn redir_build() {
assert_eq!(eval("cargo build --release > /tmp/log"), Decision::Ask);
}
fn spec_with_env_gate() -> CargoSpec {
CargoSpec::from_config(&CargoConfig {
safe_subcommands: vec!["build".into(), "check".into(), "test".into()],
allowed_with_config: vec!["install".into(), "publish".into()],
config_env: HashMap::from([("CARGO_INSTALL_ROOT".into(), "/tmp/bin".into())]),
})
}
fn eval_with_env_gate(cmd: &str) -> Decision {
let s = spec_with_env_gate();
let ctx = CommandContext::from_command(cmd);
s.evaluate(&ctx).decision
}
#[test]
fn env_gate_install_with_matching_value() {
assert_eq!(
eval_with_env_gate("CARGO_INSTALL_ROOT=/tmp/bin cargo install ripgrep"),
Decision::Allow
);
}
#[test]
fn env_gate_install_with_wrong_value() {
assert_eq!(
eval_with_env_gate("CARGO_INSTALL_ROOT=/usr/local cargo install ripgrep"),
Decision::Ask
);
}
#[test]
fn env_gate_install_no_config() {
assert_eq!(eval_with_env_gate("cargo install ripgrep"), Decision::Ask);
}
#[test]
fn env_gate_publish_with_config() {
assert_eq!(
eval_with_env_gate("CARGO_INSTALL_ROOT=/tmp/bin cargo publish"),
Decision::Allow
);
}
#[test]
fn env_gate_build_still_safe_no_env() {
assert_eq!(eval_with_env_gate("cargo build"), Decision::Allow);
}
}