use crate::config::RegexWrapper;
pub fn is_destructive(command: &str, tags: &[String], patterns: &[RegexWrapper]) -> bool {
if tags.iter().any(|t| t == "#destructive") {
return true;
}
if patterns.is_empty() {
return false;
}
let segments = split_shell_segments(command);
for pattern in patterns {
for segment in &segments {
let trimmed = segment.trim();
if !trimmed.is_empty() && pattern.is_match(trimmed) {
return true;
}
}
}
false
}
fn split_shell_segments(command: &str) -> Vec<&str> {
let bytes = command.as_bytes();
let mut segments = Vec::new();
let mut start = 0;
let mut index = 0;
let mut quote: Option<u8> = None;
let mut escaped = false;
while index < bytes.len() {
let byte = bytes[index];
if escaped {
escaped = false;
index += 1;
continue;
}
if let Some(active_quote) = quote {
if byte == b'\\' && active_quote == b'"' {
escaped = true;
} else if byte == active_quote {
quote = None;
}
index += 1;
continue;
}
match byte {
b'\\' => {
escaped = true;
index += 1;
}
b'\'' | b'"' => {
quote = Some(byte);
index += 1;
}
b';' | b'\n' => {
segments.push(&command[start..index]);
start = index + 1;
index += 1;
}
b'&' if bytes.get(index + 1) == Some(&b'&') => {
segments.push(&command[start..index]);
start = index + 2;
index += 2;
}
b'&' if bytes.get(index + 1) == Some(&b'>') || (index > 0 && bytes[index - 1] == b'>') => {
index += 1;
}
b'&' => {
segments.push(&command[start..index]);
start = index + 1;
index += 1;
}
b'|' if bytes.get(index + 1) == Some(&b'|') => {
segments.push(&command[start..index]);
start = index + 2;
index += 2;
}
b'|' => {
segments.push(&command[start..index]);
start = index + 1;
index += 1;
}
_ => index += 1,
}
}
segments.push(&command[start..]);
segments
}
#[cfg(test)]
mod tests {
use regex::Regex;
use super::{is_destructive, split_shell_segments};
use crate::config::RegexWrapper;
fn make_patterns(pats: &[&str]) -> Vec<RegexWrapper> {
pats.iter().map(|p| RegexWrapper::new(Regex::new(p).unwrap())).collect()
}
#[test]
fn test_tag_based_detection() {
assert!(is_destructive("echo safe", &["#destructive".to_string()], &[]));
assert!(is_destructive("rm -rf /", &["#destructive".to_string()], &[]));
assert!(is_destructive(
"some-command",
&["#other".to_string(), "#destructive".to_string()],
&[]
));
assert!(!is_destructive("rm -rf /", &["#safe".to_string()], &[]));
}
#[test]
fn redirection_ampersands_do_not_split_segments() {
for command in ["rm -rf / 2>&1", "rm -rf / &>output", "rm -rf / >&output"] {
assert_eq!(split_shell_segments(command), vec![command]);
}
assert_eq!(split_shell_segments("echo ok & rm -rf / 2>&1"), vec!["echo ok ", " rm -rf / 2>&1"]);
}
#[test]
fn test_regex_patterns_detection() {
let patterns = make_patterns(&["^rm\\b", "^del\\b"]);
assert!(is_destructive("rm -rf /", &[], &patterns));
assert!(is_destructive("del file.txt", &[], &patterns));
assert!(is_destructive("echo ok && rm -rf /", &[], &patterns));
assert!(is_destructive("echo ok & rm -rf /", &[], &patterns));
assert!(is_destructive("rm -rf / | echo", &[], &patterns));
assert!(!is_destructive("echo rm file", &[], &patterns));
assert!(!is_destructive("docker run --rm image", &[], &patterns));
assert!(!is_destructive("rmdir_backup", &[], &patterns));
}
}