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'>') || (index > 0 && bytes[index - 1] == b'>') => {
75 index += 1;
76 }
77 b'&' => {
78 segments.push(&command[start..index]);
79 start = index + 1;
80 index += 1;
81 }
82 b'|' if bytes.get(index + 1) == Some(&b'|') => {
83 segments.push(&command[start..index]);
84 start = index + 2;
85 index += 2;
86 }
87 b'|' => {
88 segments.push(&command[start..index]);
89 start = index + 1;
90 index += 1;
91 }
92 _ => index += 1,
93 }
94 }
95
96 segments.push(&command[start..]);
97 segments
98}
99
100#[cfg(test)]
101mod tests {
102 use regex::Regex;
103
104 use super::{is_destructive, split_shell_segments};
105 use crate::config::RegexWrapper;
106
107 fn make_patterns(pats: &[&str]) -> Vec<RegexWrapper> {
108 pats.iter().map(|p| RegexWrapper::new(Regex::new(p).unwrap())).collect()
109 }
110
111 #[test]
112 fn test_tag_based_detection() {
113 assert!(is_destructive("echo safe", &["#destructive".to_string()], &[]));
115 assert!(is_destructive("rm -rf /", &["#destructive".to_string()], &[]));
116 assert!(is_destructive(
117 "some-command",
118 &["#other".to_string(), "#destructive".to_string()],
119 &[]
120 ));
121
122 assert!(!is_destructive("rm -rf /", &["#safe".to_string()], &[]));
124 }
125
126 #[test]
127 fn redirection_ampersands_do_not_split_segments() {
128 for command in ["rm -rf / 2>&1", "rm -rf / &>output", "rm -rf / >&output"] {
129 assert_eq!(split_shell_segments(command), vec![command]);
130 }
131 assert_eq!(split_shell_segments("echo ok & rm -rf / 2>&1"), vec!["echo ok ", " rm -rf / 2>&1"]);
132 }
133
134 #[test]
135 fn test_regex_patterns_detection() {
136 let patterns = make_patterns(&["^rm\\b", "^del\\b"]);
137
138 assert!(is_destructive("rm -rf /", &[], &patterns));
140 assert!(is_destructive("del file.txt", &[], &patterns));
141 assert!(is_destructive("echo ok && rm -rf /", &[], &patterns));
142 assert!(is_destructive("echo ok & rm -rf /", &[], &patterns));
143 assert!(is_destructive("rm -rf / | echo", &[], &patterns));
144
145 assert!(!is_destructive("echo rm file", &[], &patterns));
147 assert!(!is_destructive("docker run --rm image", &[], &patterns));
148 assert!(!is_destructive("rmdir_backup", &[], &patterns));
149 }
150}