use crate::bash_danger_argv::substitution_bodies;
use crate::bash_danger_lexical::{self, RM_WORD};
use crate::bash_danger_removal::{self, OperandVerdict};
use regex::Regex;
use std::sync::LazyLock;
const SUBSTITUTION_BAIL: usize = 64;
const CMDSUB_FIXPOINT_ITERATIONS: usize = 16;
const CMDSUB_TOKEN: &str = "__CMDSUB__";
pub(crate) const TAIL_TOO_MANY: &str = "\u{2014} too many command substitutions to analyze";
pub(crate) const TAIL_IN_SUBSTITUTION: &str =
"on possibly-empty variable path inside command substitution";
static BRACE_COMMAND: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"\$\{[ \t\n|]").expect("u7e"));
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum CensusVerdict {
Bail { count: usize },
Lexical { verb: &'static str, target: String },
Structured { tail: &'static str, target: String },
NeedsFs,
Clear,
}
pub(crate) fn census(command: &str, gen3: bool) -> CensusVerdict {
let subs = collect_substitutions(command);
if subs.len() > SUBSTITUTION_BAIL {
if RM_WORD.is_match(command) {
return CensusVerdict::Bail { count: subs.len() };
}
return CensusVerdict::Clear;
}
let mut needs_fs = false;
let mut texts: Vec<String> = Vec::with_capacity(subs.len() + 1);
texts.push(command.to_string());
texts.extend(subs);
for text in texts {
for stmt in statements_of(&text) {
let inner = unwrap_group(stmt.trim());
let hit = if gen3 {
crate::bash_danger_out::out(&inner).map(|h| (h.command, h.target))
} else {
bash_danger_lexical::hnt(&inner).map(|h| (h.command, h.target))
};
if let Some((verb, target)) = hit {
return CensusVerdict::Lexical { verb, target };
}
let tokenised = cmdsub_fixpoint(&inner);
match bash_danger_removal::structured(&tokenised) {
OperandVerdict::Ask { tail, target } => {
return CensusVerdict::Structured { tail, target }
}
OperandVerdict::NeedsFs => needs_fs = true,
OperandVerdict::Clear => {}
}
}
}
if needs_fs {
CensusVerdict::NeedsFs
} else {
CensusVerdict::Clear
}
}
pub(crate) fn collect_substitutions(text: &str) -> Vec<String> {
let mut out = Vec::new();
let mut queue = vec![text.to_string()];
let mut guard = 0usize;
while let Some(t) = queue.pop() {
guard += 1;
if guard > 4096 {
break;
}
for body in substitution_bodies(&t) {
out.push(body.trim().to_string());
queue.push(body);
}
for m in BRACE_COMMAND.find_iter(&t) {
if let Some(body) = brace_command_body(&t, m.start()) {
out.push(body);
}
}
}
out
}
pub(crate) fn brace_command_body(text: &str, at: usize) -> Option<String> {
let rest = &text[at..];
let close = rest.find('}')?;
let inner = &rest[2..close];
Some(
inner
.trim()
.trim_start_matches('|')
.trim_end_matches(';')
.trim()
.to_string(),
)
}
pub(crate) fn statements_of(text: &str) -> Vec<String> {
match crate::bash_danger_shape::split_statements(text) {
Some(parts) => parts.iter().map(|s| (*s).to_string()).collect(),
None => vec![text.to_string()],
}
}
pub(crate) fn unwrap_group(s: &str) -> String {
let brace = s.starts_with('{') && s.trim_end().ends_with('}');
let paren = s.starts_with('(') && s.ends_with(')');
if brace || paren {
let inner = &s[1..];
let trimmed = inner.trim_end();
let cut = trimmed
.strip_suffix(['}', ')'])
.unwrap_or(trimmed)
.trim_end()
.trim_end_matches(';');
return cut.trim().to_string();
}
s.to_string()
}
pub(crate) fn cmdsub_fixpoint(s: &str) -> String {
let mut b = bash_danger_lexical::BACKTICKS
.replace_all(s, CMDSUB_TOKEN)
.into_owned();
let mut prev = String::new();
let mut z = 0usize;
while prev != b && z < CMDSUB_FIXPOINT_ITERATIONS {
prev.clone_from(&b);
b = bash_danger_lexical::DOLLAR_PAREN
.replace_all(&b, CMDSUB_TOKEN)
.into_owned();
z += 1;
}
b
}
#[cfg(test)]
#[path = "bash_danger_census_tests.rs"]
mod tests;