intelli_shell/utils/
destructive.rs1use crate::config::RegexWrapper;
2
3pub fn is_destructive(command: &str, tags: &[String], patterns: &[RegexWrapper]) -> bool {
5 if tags.iter().any(|t| t == "#destructive") {
7 return true;
8 }
9
10 if patterns.is_empty() {
12 return false;
13 }
14
15 let segments = split_shell_segments(command);
16 for pattern in patterns {
17 for segment in &segments {
18 let trimmed = segment.trim();
19 if !trimmed.is_empty() && pattern.is_match(trimmed) {
20 return true;
21 }
22 }
23 }
24
25 false
26}
27
28fn split_shell_segments(command: &str) -> Vec<&str> {
29 let bytes = command.as_bytes();
30 let mut segments = Vec::new();
31 let mut start = 0;
32 let mut index = 0;
33 let mut quote: Option<u8> = None;
34 let mut escaped = false;
35
36 while index < bytes.len() {
37 let byte = bytes[index];
38
39 if escaped {
40 escaped = false;
41 index += 1;
42 continue;
43 }
44
45 if let Some(active_quote) = quote {
46 if byte == b'\\' && active_quote == b'"' {
47 escaped = true;
48 } else if byte == active_quote {
49 quote = None;
50 }
51 index += 1;
52 continue;
53 }
54
55 match byte {
56 b'\\' => {
57 escaped = true;
58 index += 1;
59 }
60 b'\'' | b'"' => {
61 quote = Some(byte);
62 index += 1;
63 }
64 b';' | b'\n' => {
65 segments.push(&command[start..index]);
66 start = index + 1;
67 index += 1;
68 }
69 b'&' if bytes.get(index + 1) == Some(&b'&') => {
70 segments.push(&command[start..index]);
71 start = index + 2;
72 index += 2;
73 }
74 b'|' if bytes.get(index + 1) == Some(&b'|') => {
75 segments.push(&command[start..index]);
76 start = index + 2;
77 index += 2;
78 }
79 b'|' => {
80 segments.push(&command[start..index]);
81 start = index + 1;
82 index += 1;
83 }
84 _ => index += 1,
85 }
86 }
87
88 segments.push(&command[start..]);
89 segments
90}
91
92#[cfg(test)]
93mod tests {
94 use super::is_destructive;
95 use crate::config::RegexWrapper;
96 use regex::Regex;
97
98 fn make_patterns(pats: &[&str]) -> Vec<RegexWrapper> {
99 pats.iter()
100 .map(|p| RegexWrapper::new(Regex::new(p).unwrap()))
101 .collect()
102 }
103
104 #[test]
105 fn test_tag_based_detection() {
106 assert!(is_destructive("echo safe", &["#destructive".to_string()], &[]));
108 assert!(is_destructive("rm -rf /", &["#destructive".to_string()], &[]));
109 assert!(is_destructive(
110 "some-command",
111 &["#other".to_string(), "#destructive".to_string()],
112 &[]
113 ));
114
115 assert!(!is_destructive("rm -rf /", &["#safe".to_string()], &[]));
117 }
118
119 #[test]
120 fn test_regex_patterns_detection() {
121 let patterns = make_patterns(&["^rm\\b", "^del\\b"]);
122
123 assert!(is_destructive("rm -rf /", &[], &patterns));
125 assert!(is_destructive("del file.txt", &[], &patterns));
126 assert!(is_destructive("echo ok && rm -rf /", &[], &patterns));
127 assert!(is_destructive("rm -rf / | echo", &[], &patterns));
128
129 assert!(!is_destructive("echo rm file", &[], &patterns));
131 assert!(!is_destructive("docker run --rm image", &[], &patterns));
132 assert!(!is_destructive("rmdir_backup", &[], &patterns));
133 }
134}