use crate::parse::Token;
use crate::registry;
use crate::verdict::{SafetyLevel, Verdict};
const EXEC_FLAGS: &[&str] = &["-x", "--exec", "-X", "--exec-batch"];
pub fn check_fd(tokens: &[Token]) -> Verdict {
let Some(xi) = tokens.iter().position(|t| EXEC_FLAGS.contains(&t.as_str())) else {
return registry::try_fallback_grammar("fd", tokens).unwrap_or(Verdict::Denied);
};
match registry::try_fallback_grammar("fd", &tokens[..xi]) {
Some(Verdict::Allowed(_)) => {}
_ => return Verdict::Denied,
}
let cmd_start = xi + 1;
let cmd_end = tokens[cmd_start..]
.iter()
.position(|t| *t == ";" || *t == "+")
.map(|p| cmd_start + p)
.unwrap_or(tokens.len());
if cmd_start >= cmd_end {
return Verdict::Denied;
}
let mut bases: Vec<&str> = tokens[1..xi]
.iter()
.map(Token::as_str)
.filter(|s| !s.starts_with('-'))
.collect();
bases.push(".");
let has_placeholder =
tokens[cmd_start..cmd_end].iter().any(|t| t.as_str().contains('{'));
let mut level = SafetyLevel::Inert;
for base in &bases {
let bound = crate::engine::resolve::locus::traversal_item(base);
let mut words: Vec<String> = tokens[cmd_start..cmd_end]
.iter()
.map(|t| synthetic_operand(&bind_placeholders(t.as_str(), &bound), t.as_str()))
.collect();
if !has_placeholder {
words.push(bound.clone());
}
match crate::command_verdict(&shell_words::join(&words)) {
Verdict::Denied => return Verdict::Denied,
Verdict::Allowed(l) => level = level.max(l),
}
}
Verdict::Allowed(level)
}
pub(crate) fn synthetic_operand(bound_word: &str, original: &str) -> String {
use crate::engine::resolve::locus::{UNKNOWABLE_ITEM, read_locus};
if bound_word != original && read_locus(bound_word) >= crate::engine::facet::LocalLocus::User {
return UNKNOWABLE_ITEM.to_string();
}
bound_word.to_string()
}
fn bind_placeholders(tok: &str, bound: &str) -> String {
tok.replace("{//}", bound)
.replace("{/.}", bound)
.replace("{/}", bound)
.replace("{.}", bound)
.replace("{}", bound)
}
#[cfg(test)]
mod tests {
use crate::is_safe_command;
fn check(cmd: &str) -> bool {
is_safe_command(cmd)
}
safe! {
bare: "fd",
pattern: "fd pattern",
flags: "fd --type f --hidden pattern",
pattern_and_path: "fd pattern src",
ext_valued: "fd -e rs",
exec_echo: "fd -x echo {}",
exec_wc: "fd pattern -x wc -l {}",
exec_batch_cat_worktree: "fd -X cat",
exec_rm_worktree: "fd -x rm {}", exec_placeholder_variants: "fd -x cat {//}/{/}",
}
denied! {
exec_system_read: "fd /etc -x cat {}",
exec_batch_system: "fd /etc -X cat",
exec_system_glued_placeholder: "fd -x rm /etc/{}",
exec_search_path_flag_system: "fd --search-path /etc -X cat",
unknown_flag: "fd --evil",
unknown_flag_before_exec: "fd --evil -x echo {}",
empty_exec: "fd -x",
}
use proptest::prelude::*;
proptest! {
#[test]
fn fd_exec_follows_the_inner_command_locus(
inner in prop::sample::select(vec!["cat", "od", "base64", "wc -l"]),
) {
let sys = format!("fd /etc -x {} {{}}", inner);
prop_assert!(!crate::is_safe_command(&sys), "system search must deny: {}", sys);
let wt = format!("fd src -x {} {{}}", inner);
prop_assert!(crate::is_safe_command(&wt), "worktree search must allow: {}", wt);
}
#[test]
fn fd_batch_without_placeholder_gates_the_base(
reader in prop::sample::select(vec!["cat", "od", "base64"]),
) {
let sys = format!("fd /etc -X {}", reader);
prop_assert!(!crate::is_safe_command(&sys), "batch system read must deny: {}", sys);
}
}
}