use serde_json::Value;
use crate::interpreter_code::flat_strings;
use crate::validate::{is_shell, program_stem};
struct Spec {
value_opts: &'static [&'static str],
positionals: usize,
}
fn spec(stem: &str) -> Option<Spec> {
let (value_opts, positionals): (&'static [&'static str], usize) = match stem {
"env" => (&["-u", "--unset", "-C", "--chdir"], 0),
"xargs" => (
&["-I", "-n", "-L", "-P", "-s", "-d", "-E", "-a", "--arg-file"],
0,
),
"nohup" | "setsid" | "unbuffer" | "busybox" => (&[], 0),
"nice" => (&["-n", "--adjustment"], 0),
"ionice" => (&["-c", "-n", "--class", "--classdata"], 0),
"timeout" => (&["-s", "--signal", "-k", "--kill-after"], 1),
"stdbuf" => (&["-i", "-o", "-e", "--input", "--output", "--error"], 0),
"sudo" | "doas" => (&["-u", "-g", "-C", "-p", "-h", "-U", "-r", "-t", "-D"], 0),
"chroot" => (&["--userspec", "--groups"], 1),
"time" => (&["-o", "-f", "--output", "--format"], 0),
"caffeinate" => (&["-t", "-w"], 0),
"taskset" | "chrt" => (&[], 1),
"watch" => (&["-n", "--interval"], 0),
_ => return None,
};
Some(Spec {
value_opts,
positionals,
})
}
enum Runs<'a> {
Program(usize, &'a str),
Shell(&'static str),
}
pub(crate) fn wrapper_problems(command: &str, args: Option<&Value>) -> Vec<String> {
problems_for(command, &flat_strings(args), 0)
}
fn problems_for(command: &str, args: &[&str], depth: usize) -> Vec<String> {
let mut problems = Vec::new();
for runs in what_runs(&program_stem(command), args) {
match runs {
Runs::Shell(why) => problems.push(format!(
"'{command}' {why}, which shell-run doesn't allow. Run the tool directly"
)),
Runs::Program(_, program) if bnto_core::placeholders::mentions_data(program) => {
problems.push(format!(
"'{command}' would run '{program}', which lets data choose the \
program. Name the tool literally"
));
}
Runs::Program(_, program) if is_shell(program) => problems.push(format!(
"'{command}' would run the shell '{program}', which shell-run \
doesn't allow. Run the tool directly"
)),
Runs::Program(at, program) => {
let rest = &args[at + 1..];
problems.extend(crate::interpreter_code::code_problems_in(program, rest));
if depth < 4 {
problems.extend(problems_for(program, rest, depth + 1));
}
}
}
}
problems
}
fn what_runs<'a>(stem: &str, args: &[&'a str]) -> Vec<Runs<'a>> {
match stem {
"script" => vec![Runs::Shell("runs its command through a shell")],
"watch" if !args.iter().any(|a| matches!(*a, "-x" | "--exec")) => {
vec![Runs::Shell(
"runs its command through sh -c unless given -x",
)]
}
"find" => find_exec_programs(args),
_ => spec(stem)
.and_then(|s| program_after_options(stem, &s, args))
.into_iter()
.collect(),
}
}
fn find_exec_programs<'a>(args: &[&'a str]) -> Vec<Runs<'a>> {
args.iter()
.enumerate()
.filter(|(_, a)| matches!(**a, "-exec" | "-execdir" | "-ok" | "-okdir"))
.filter_map(|(i, _)| args.get(i + 1).map(|p| Runs::Program(i + 1, p)))
.collect()
}
fn program_after_options<'a>(stem: &str, spec: &Spec, args: &[&'a str]) -> Option<Runs<'a>> {
let (mut i, mut skipped, mut options) = (0, 0, true);
while let Some(arg) = args.get(i).copied() {
if options && arg == "--" {
options = false;
} else if options && arg.starts_with('-') && arg.len() > 1 {
if let Some(runs) = shell_option(stem, arg, args.get(i + 1).copied()) {
return Some(runs);
}
i += usize::from(spec.value_opts.contains(&arg));
} else if stem == "env" && arg.contains('=') {
} else if skipped < spec.positionals {
skipped += 1;
} else {
return Some(Runs::Program(i, arg));
}
i += 1;
}
(stem == "chroot").then_some(Runs::Shell("runs a shell when given no program"))
}
fn shell_option<'a>(stem: &str, arg: &str, next: Option<&'a str>) -> Option<Runs<'a>> {
match (stem, arg) {
("sudo" | "doas", "-s" | "-i" | "--shell" | "--login") => {
Some(Runs::Shell("starts a shell with -s / -i"))
}
("env", "-S" | "--split-string") => {
let first = next?.split_whitespace().next()?;
is_shell(first).then_some(Runs::Shell(
"splits -S into a command line that runs a shell",
))
}
_ => None,
}
}
#[cfg(test)]
mod tests;