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'|') => {
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 super::is_destructive;
use crate::config::RegexWrapper;
use regex::Regex;
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 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("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));
}
}