use super::architect_pane_parse::Word;
use super::architect_pane_verbs::DENY_VERBS;
use super::credential_print::{basename, code_operands, evaluator_name};
use super::floor_d4::{EXEC_FLAGS, program_positions, segments};
use super::heredoc::{blank_spans, data_bodies};
use super::shell_lex::{DASH_C_SHELLS, xargs_utility_index};
use crate::commands::hook_rewrite::{is_env_assignment, strip_wrapper_prefix};
use crate::commands::program_word::COMMAND_WRAPPERS;
const RUNNERS: &[&str] = &[
"watch",
"script",
"entr",
"chronic",
"unshare",
"systemd-run",
];
const MAX_RUNNER_SUFFIXES: usize = 16;
pub(super) fn runner_texts(argv: &[String], pos: usize) -> Vec<String> {
if !RUNNERS.contains(&basename(&argv[pos]).as_str()) {
return Vec::new();
}
let after = &argv[pos + 1..];
(0..after.len())
.filter(|&i| !after[i].starts_with('-'))
.take(MAX_RUNNER_SUFFIXES)
.map(|i| after[i..].join(" "))
.collect()
}
pub(super) fn without_data_bodies(command: &str) -> String {
let spans: Vec<(usize, usize)> = data_bodies(command)
.into_iter()
.filter(|b| {
let body = &command[b.span.0..b.span.1];
let line = &command[b.operator_line.0..b.operator_line.1];
let runs = !b.expands || !(body.contains("$(") || body.contains('`'));
runs && !names_evaluator(line)
})
.map(|b| b.span)
.collect();
let blanked = blank_spans(command, &spans);
if !spans.is_empty() && (names_evaluator(&blanked) || runs_dynamic_program(&blanked)) {
return command.to_owned();
}
blanked
}
fn names_evaluator(line: &str) -> bool {
line.split_whitespace().any(|token| {
let word = token.trim_matches(|c: char| "()<>|&;\"'".contains(c));
evaluator_name(&basename(word)).is_some()
})
}
fn runs_dynamic_program(text: &str) -> bool {
segments(text).iter().any(|seg| {
let Some(argv) = shlex::split(seg.text.trim()) else {
return false;
};
let words = with_dynamics(&seg.text, argv);
let texts: Vec<String> = words.iter().map(|w| w.text.clone()).collect();
program_positions(&texts)
.iter()
.any(|(pos, _)| dynamic_name(&words[*pos]) && !is_env_assignment(&texts[*pos]))
})
}
pub(super) fn dynamic_name(word: &Word) -> bool {
let name = word.text.rsplit('/').next().unwrap_or(&word.text);
word.dynamic && (!word.text.contains('/') || name.contains(['$', '`']))
}
pub(super) fn names_tmux(text: &str) -> bool {
let flat: String = text
.chars()
.filter(|c| !matches!(c, '\'' | '"' | '\\'))
.collect();
flat.split(|c: char| !(c.is_ascii_alphanumeric() || c == '-'))
.any(|w| w.eq_ignore_ascii_case("tmux") || DENY_VERBS.iter().any(|v| v.name == w))
}
pub(super) fn opaque_route(
argv: &[String],
pos: usize,
piped: bool,
command: &str,
) -> Option<&'static str> {
let program = basename(&argv[pos]);
let before = &argv[..pos];
let after = &argv[pos + 1..];
if program == "xargs" && after.iter().any(|w| basename(w) == "tmux") {
return Some("`xargs` appends arguments from stdin the guard cannot read");
}
if program == "xargs" && xargs_replaces(after) && names_tmux(command) {
return Some("`xargs` puts words from stdin the guard cannot read into its command");
}
if program == "xargs" && stdin_is_the_program(after) && names_tmux(command) {
return Some("`xargs` runs a wrapper whose program comes from stdin the guard cannot read");
}
if program == "parallel" && names_tmux(command) {
return Some("`parallel` runs commands built from inputs the guard cannot read");
}
if program == "tmux" {
let in_exec = before.iter().any(|w| EXEC_FLAGS.contains(&w.as_str()));
if in_exec && after.iter().any(|w| w == "+") {
return Some("`find -exec … +` appends paths the guard cannot read");
}
return None;
}
let evaluator = evaluator_name(&program)?;
let fed = piped
|| after.iter().any(|w| {
w.trim_start_matches(|c: char| c.is_ascii_digit())
.starts_with('<')
});
let reads_stdin = code_operands(evaluator, after).is_empty() && !runs_script(evaluator, after);
(fed && reads_stdin && names_tmux(command))
.then_some("a shell runs program text it reads on stdin")
}
fn stdin_is_the_program(args: &[String]) -> bool {
let utility = &args[xargs_utility_index(args)..];
let Some(first) = utility.first() else {
return false;
};
let program = basename(first);
if RUNNERS.contains(&program.as_str()) || program == "parallel" {
return true;
}
if let Some(evaluator) = evaluator_name(&program) {
return code_operands(evaluator, &utility[1..]).is_empty()
&& !runs_script(evaluator, &utility[1..]);
}
COMMAND_WRAPPERS.contains(&program.as_str())
&& strip_wrapper_prefix(utility).is_none_or(|at| at >= utility.len())
}
fn xargs_replaces(args: &[String]) -> bool {
let mut words = args.iter().map(String::as_str);
while let Some(word) = words.next() {
if word == "--" || word == "-" || !word.starts_with('-') {
return false;
}
if let Some(long) = word.strip_prefix("--") {
if long.starts_with("replace") {
return true;
}
if XARGS_LONG_VALUED.contains(&long) {
words.next();
}
continue;
}
for (k, c) in word.char_indices().skip(1) {
if "IJi".contains(c) {
return true;
}
if "adELnPsRS".contains(c) {
if word[k + 1..].is_empty() {
words.next();
}
break;
}
if "el".contains(c) {
break;
}
}
}
false
}
const XARGS_LONG_VALUED: &[&str] = &[
"arg-file",
"delimiter",
"max-args",
"max-procs",
"max-chars",
"process-slot-var",
];
fn runs_script(evaluator: &str, args: &[String]) -> bool {
let shell = DASH_C_SHELLS.contains(&evaluator) || evaluator == "fish";
let python = evaluator.starts_with("python");
let valued: &[char] = if shell {
&['o', 'O']
} else {
&['W', 'X', 'r', 'I', 'M', 'E', 'C']
};
let known: &[char] = if shell {
SHELL_FLAGS
} else if python {
PYTHON_FLAGS
} else {
&[]
};
let mut words = args.iter().map(String::as_str);
while let Some(word) = words.next() {
let redirect = word.trim_start_matches(|c: char| c.is_ascii_digit());
if redirect.starts_with("<(") || redirect.starts_with(">(") {
return false;
}
if redirect.starts_with(['<', '>']) || redirect.starts_with("&>") {
if redirect.trim_start_matches(['<', '>', '&']).is_empty() {
words.next();
}
continue;
}
if word == "--" {
return words.next().is_some();
}
if word == "-" {
return false;
}
let Some(cluster) = word.strip_prefix(['-', '+']) else {
return true;
};
if let Some(long) = cluster.strip_prefix('-') {
if ["rcfile", "init-file", "require"].contains(&long) {
words.next();
} else if !long.contains('=') && !(shell && SHELL_LONG_FLAGS.contains(&long)) {
return false;
}
continue;
}
if shell && cluster.contains('s') {
return false;
}
if python && cluster.contains('m') {
return true;
}
if cluster.ends_with(valued) {
words.next();
} else if !cluster
.chars()
.all(|c| known.contains(&c) || valued.contains(&c))
{
return false;
}
}
false
}
const SHELL_FLAGS: &[char] = &[
'a', 'b', 'e', 'f', 'h', 'i', 'k', 'l', 'm', 'n', 'p', 'r', 't', 'u', 'v', 'x', 'B', 'C', 'E',
'H', 'P', 'T',
];
const SHELL_LONG_FLAGS: &[&str] = &[
"login",
"noprofile",
"norc",
"posix",
"restricted",
"verbose",
"noediting",
"debugger",
"pretty-print",
"protected",
"help",
"version",
"interactive",
"no-config",
"private",
];
const PYTHON_FLAGS: &[char] = &[
'b', 'B', 'd', 'E', 'h', 'i', 'I', 'O', 'P', 'q', 's', 'S', 'u', 'v', 'V', 'x', 'R', '3',
];
pub(super) fn with_dynamics(segment: &str, argv: Vec<String>) -> Vec<Word> {
let flags = word_dynamics(segment.trim());
let aligned = flags.len() == argv.len();
argv.into_iter()
.enumerate()
.map(|(i, text)| {
let dynamic = if aligned {
flags[i]
} else {
text.contains(['$', '`'])
};
Word { text, dynamic }
})
.collect()
}
fn word_dynamics(segment: &str) -> Vec<bool> {
let (mut out, mut single, mut double, mut escaped) = (Vec::new(), false, false, false);
let mut current: Option<bool> = None;
for c in segment.chars() {
if escaped {
escaped = false;
current.get_or_insert(false);
continue;
}
let quoted = single || double;
if !quoted && c.is_whitespace() {
out.extend(current.take());
continue;
}
let at_start = current.is_none();
let live = current.get_or_insert(false);
match c {
'\\' if !single => escaped = true,
'\'' if !double => single = !single,
'"' if !single => double = !double,
'$' | '`' if !single => *live = true,
'~' if !quoted && at_start => *live = true,
_ => {}
}
}
out.extend(current);
out
}