use crate::tools::permissions::PermissionManager;
pub(super) const COMPOUND_KEYWORDS: &[&str] = &[
"do", "done", "then", "else", "elif", "fi", "case", "esac", "{", "}", "(",
];
pub(super) const COMPOUND_HEADERS_WITH_BODY: &[&str] = &["while", "until", "if"];
pub(super) const COMPOUND_HEADERS_NO_BODY: &[&str] = &["for"];
const DANGEROUS_ENV_KEYS: &[&str] = &[
"PATH",
"LD_PRELOAD",
"LD_LIBRARY_PATH",
"NODE_PATH",
"PYTHONPATH",
];
pub(super) fn leading_dangerous_env_prefix(segment: &str) -> Option<&'static str> {
for tok in segment.split_whitespace() {
if !is_env_assignment(tok) {
return None;
}
let key = tok.split_once('=').map(|(k, _)| k).unwrap_or(tok);
let upper = key.to_ascii_uppercase();
if upper.starts_with("DYLD_") {
return Some("DYLD_*");
}
if let Some(name) = DANGEROUS_ENV_KEYS.iter().find(|d| **d == upper.as_str()) {
return Some(*name);
}
}
None
}
pub(super) fn is_env_assignment(tok: &str) -> bool {
let Some((key, _)) = tok.split_once('=') else {
return false;
};
let mut chars = key.chars();
let Some(first) = chars.next() else {
return false;
};
(first.is_ascii_alphabetic() || first == '_')
&& chars.all(|c| c.is_ascii_alphanumeric() || c == '_')
}
pub(super) fn clean_base_token(tok: &str) -> String {
let stripped = tok.trim_start_matches(['(', '{', '\\', '\'', '"']);
let stripped = stripped.trim_end_matches([')', '}', '\'', '"']);
let after_unix = stripped.rsplit('/').next().unwrap_or(stripped);
let after_win = after_unix.rsplit('\\').next().unwrap_or(after_unix);
after_win.to_string()
}
pub(super) fn command_lookup_key(base: &str) -> String {
#[cfg(windows)]
{
base.to_ascii_lowercase()
}
#[cfg(not(windows))]
{
base.to_string()
}
}
impl PermissionManager {
pub(super) fn extract_base_command(command: &str) -> String {
let command = command.trim();
let without_prefix = if let Some(stripped) = command.strip_prefix("Bash(") {
if let Some(end) = stripped.find(')') {
&stripped[..end]
} else {
stripped
}
} else {
command
};
let raw = without_prefix
.split_whitespace()
.find(|tok| !is_env_assignment(tok))
.or_else(|| without_prefix.split_whitespace().next())
.unwrap_or(without_prefix);
clean_base_token(raw)
}
pub(super) fn split_compound_command(command: &str) -> Vec<String> {
let mut segments: Vec<String> = Vec::new();
let mut current = String::new();
let mut chars = command.chars().peekable();
let mut quote: Option<char> = None;
while let Some(c) = chars.next() {
if let Some(q) = quote {
current.push(c);
if c == q {
quote = None;
}
continue;
}
match c {
'\'' | '"' => {
quote = Some(c);
current.push(c);
}
';' | '\n' => Self::push_segment(&mut segments, &mut current),
'|' => {
if chars.peek() == Some(&'|') {
chars.next();
}
Self::push_segment(&mut segments, &mut current);
}
'&' => {
if chars.peek() == Some(&'&') {
chars.next();
Self::push_segment(&mut segments, &mut current);
} else if matches!(prev_non_space(¤t), Some('>') | Some('<')) {
current.push(c);
} else {
Self::push_segment(&mut segments, &mut current);
}
}
_ => current.push(c),
}
}
Self::push_segment(&mut segments, &mut current);
segments
}
fn push_segment(segments: &mut Vec<String>, current: &mut String) {
let trimmed = current.trim();
if !trimmed.is_empty() {
segments.push(trimmed.to_string());
}
current.clear();
}
}
fn prev_non_space(current: &str) -> Option<char> {
current.chars().rev().find(|c| !c.is_whitespace())
}
impl PermissionManager {
pub(super) fn extract_segment_base_with_args(segment: &str) -> Option<(&str, Vec<&str>)> {
let mut tokens = segment.split_whitespace().peekable();
while let Some(&tok) = tokens.peek() {
if COMPOUND_HEADERS_NO_BODY.contains(&tok) {
return None;
}
if is_env_assignment(tok)
|| COMPOUND_KEYWORDS.contains(&tok)
|| COMPOUND_HEADERS_WITH_BODY.contains(&tok)
{
tokens.next();
continue;
}
break;
}
let base = tokens.next()?;
if base.starts_with('#') {
return None;
}
let args: Vec<&str> = tokens.collect();
Some((base, args))
}
pub(super) fn enumerate_compound_bases(command: &str) -> Vec<String> {
Self::split_compound_command(command)
.iter()
.filter_map(|seg| {
Self::extract_segment_base_with_args(seg).map(|(base, _)| clean_base_token(base))
})
.collect()
}
}