Skip to main content

intelli_shell/utils/
destructive.rs

1use crate::config::RegexWrapper;
2
3/// Checks whether a command string is destructive based on its tags and regex patterns.
4pub fn is_destructive(command: &str, tags: &[String], patterns: &[RegexWrapper]) -> bool {
5    // 1. Tag-Based Detection (Always-On)
6    if tags.iter().any(|t| t == "#destructive") {
7        return true;
8    }
9
10    // 2. Config-Based Regex Detection
11    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        // Tag-based check is always-on and triggers if '#destructive' tag is present
107        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        // If '#destructive' is not present, it should not trigger without patterns
116        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        // Matches segment starting with rm or del
124        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        // Negative cases that should not match
130        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}