173 lines · 5.6 KB · Rust
RawDownload
use crate::shell_parser::extract_commands;use url::Url; /// Extracts the command name from a shell command using the shell parser.////// This parses the command properly to extract just the command name (first word),/// handling shell syntax correctly. Returns `None` if parsing fails or if the/// command name contains path separators (for security reasons).fn extract_command_name(command: &str) -> Option<String> {    let commands = extract_commands(command)?;    let first_command = commands.first()?;     let first_token = first_command.split_whitespace().next()?;     // Only allow alphanumeric commands with hyphens/underscores.    // Reject paths like "./script.sh" or "/usr/bin/python" to prevent    // users from accidentally allowing arbitrary script execution.    if first_token        .chars()        .all(|c| c.is_alphanumeric() || c == '-' || c == '_')    {        Some(first_token.to_string())    } else {        None    }} /// Extracts a regex pattern from a terminal command based on the first token (command name).////// Returns `None` for commands starting with `./`, `/`, or other path-like prefixes./// This is a deliberate security decision: we only allow pattern-based "always allow"/// rules for well-known command names (like `cargo`, `npm`, `git`), not for arbitrary/// scripts or absolute paths which could be manipulated by an attacker.pub fn extract_terminal_pattern(command: &str) -> Option<String> {    let command_name = extract_command_name(command)?;    Some(format!("^{}\\b", regex::escape(&command_name)))} pub fn extract_terminal_pattern_display(command: &str) -> Option<String> {    extract_command_name(command)} pub fn extract_path_pattern(path: &str) -> Option<String> {    let parent = std::path::Path::new(path).parent()?;    let parent_str = parent.to_str()?;    if parent_str.is_empty() || parent_str == "/" {        return None;    }    Some(format!("^{}/", regex::escape(parent_str)))} pub fn extract_path_pattern_display(path: &str) -> Option<String> {    let parent = std::path::Path::new(path).parent()?;    let parent_str = parent.to_str()?;    if parent_str.is_empty() || parent_str == "/" {        return None;    }    Some(format!("{}/", parent_str))} pub fn extract_url_pattern(url: &str) -> Option<String> {    let parsed = Url::parse(url).ok()?;    let domain = parsed.host_str()?;    Some(format!("^https?://{}", regex::escape(domain)))} pub fn extract_url_pattern_display(url: &str) -> Option<String> {    let parsed = Url::parse(url).ok()?;    let domain = parsed.host_str()?;    Some(domain.to_string())} #[cfg(test)]mod tests {    use super::*;     #[test]    fn test_extract_terminal_pattern() {        assert_eq!(            extract_terminal_pattern("cargo build --release"),            Some("^cargo\\b".to_string())        );        assert_eq!(            extract_terminal_pattern("npm install"),            Some("^npm\\b".to_string())        );        assert_eq!(            extract_terminal_pattern("git-lfs pull"),            Some("^git\\-lfs\\b".to_string())        );        assert_eq!(            extract_terminal_pattern("my_script arg"),            Some("^my_script\\b".to_string())        );        assert_eq!(extract_terminal_pattern("./script.sh arg"), None);        assert_eq!(extract_terminal_pattern("/usr/bin/python arg"), None);    }     #[test]    fn test_extract_terminal_pattern_display() {        assert_eq!(            extract_terminal_pattern_display("cargo build --release"),            Some("cargo".to_string())        );        assert_eq!(            extract_terminal_pattern_display("npm install"),            Some("npm".to_string())        );    }     #[test]    fn test_extract_path_pattern() {        assert_eq!(            extract_path_pattern("/Users/alice/project/src/main.rs"),            Some("^/Users/alice/project/src/".to_string())        );        assert_eq!(            extract_path_pattern("src/lib.rs"),            Some("^src/".to_string())        );        assert_eq!(extract_path_pattern("file.txt"), None);        assert_eq!(extract_path_pattern("/file.txt"), None);    }     #[test]    fn test_extract_path_pattern_display() {        assert_eq!(            extract_path_pattern_display("/Users/alice/project/src/main.rs"),            Some("/Users/alice/project/src/".to_string())        );        assert_eq!(            extract_path_pattern_display("src/lib.rs"),            Some("src/".to_string())        );    }     #[test]    fn test_extract_url_pattern() {        assert_eq!(            extract_url_pattern("https://github.com/user/repo"),            Some("^https?://github\\.com".to_string())        );        assert_eq!(            extract_url_pattern("http://example.com/path?query=1"),            Some("^https?://example\\.com".to_string())        );        assert_eq!(extract_url_pattern("not a url"), None);    }     #[test]    fn test_extract_url_pattern_display() {        assert_eq!(            extract_url_pattern_display("https://github.com/user/repo"),            Some("github.com".to_string())        );        assert_eq!(            extract_url_pattern_display("http://api.example.com/v1/users"),            Some("api.example.com".to_string())        );    }     #[test]    fn test_special_chars_are_escaped() {        assert_eq!(            extract_path_pattern("/path/with (parens)/file.txt"),            Some("^/path/with \\(parens\\)/".to_string())        );        assert_eq!(            extract_url_pattern("https://test.example.com/path"),            Some("^https?://test\\.example\\.com".to_string())        );    }}