use crate::types::ValidationStatus;
use regex::Regex;
use std::sync::LazyLock;
static DANGEROUS_CHARS: LazyLock<Regex> = LazyLock::new(|| {
Regex::new(r"[;|&`<>\n\r\x00-\x1f]|\$\(|\$\{|&&|\|\|").expect("Invalid dangerous chars regex")
});
static ENV_VAR_PATTERN: LazyLock<Regex> = LazyLock::new(|| {
Regex::new(r"\$\{?\w+\}?").expect("Invalid env var regex")
});
static WRITE_FLAGS: LazyLock<Regex> = LazyLock::new(|| {
Regex::new(r"(?i)(^|\s)(--force|--delete|--remove|--prune)(\s|$)")
.expect("Invalid write flags regex")
});
static WRITE_KEYWORDS: LazyLock<Regex> = LazyLock::new(|| {
Regex::new(r"(?i)\b(repair|destroy|drop|truncate)\b").expect("Invalid write keywords regex")
});
static QUOTED_STRING: LazyLock<Regex> = LazyLock::new(|| {
Regex::new(r#""(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*'"#).expect("Invalid quoted string regex")
});
#[must_use]
pub fn contains_dangerous_chars(command: &str) -> Option<ValidationStatus> {
if ENV_VAR_PATTERN.is_match(command) {
return Some(ValidationStatus::RejectedEnvVar);
}
if DANGEROUS_CHARS.is_match(command) {
return Some(ValidationStatus::RejectedMetachar);
}
None
}
fn strip_quoted_sections(command: &str) -> String {
QUOTED_STRING.replace_all(command, " ").to_string()
}
#[must_use]
pub fn contains_write_operation(command: &str) -> bool {
if WRITE_FLAGS.is_match(command) {
return true;
}
let unquoted = strip_quoted_sections(command);
WRITE_KEYWORDS.is_match(&unquoted)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_safe_command() {
assert!(contains_dangerous_chars("sqry query \"foo\"").is_none());
}
#[test]
fn test_semicolon() {
assert_eq!(
contains_dangerous_chars("foo; bar"),
Some(ValidationStatus::RejectedMetachar)
);
}
#[test]
fn test_pipe() {
assert_eq!(
contains_dangerous_chars("foo | bar"),
Some(ValidationStatus::RejectedMetachar)
);
}
#[test]
fn test_backtick() {
assert_eq!(
contains_dangerous_chars("foo `whoami`"),
Some(ValidationStatus::RejectedMetachar)
);
}
#[test]
fn test_command_substitution() {
assert_eq!(
contains_dangerous_chars("foo $(whoami)"),
Some(ValidationStatus::RejectedMetachar)
);
}
#[test]
fn test_env_var_dollar() {
assert_eq!(
contains_dangerous_chars("$HOME/foo"),
Some(ValidationStatus::RejectedEnvVar)
);
}
#[test]
fn test_env_var_braces() {
assert_eq!(
contains_dangerous_chars("${HOME}/foo"),
Some(ValidationStatus::RejectedEnvVar)
);
}
#[test]
fn test_and_and() {
assert_eq!(
contains_dangerous_chars("foo && bar"),
Some(ValidationStatus::RejectedMetachar)
);
}
#[test]
fn test_or_or() {
assert_eq!(
contains_dangerous_chars("foo || bar"),
Some(ValidationStatus::RejectedMetachar)
);
}
#[test]
fn test_write_operation_flags() {
assert!(contains_write_operation("sqry index --force"));
assert!(contains_write_operation("sqry index --delete"));
assert!(contains_write_operation("sqry index --prune"));
assert!(!contains_write_operation("sqry query \"force\""));
}
#[test]
fn test_write_operation_keywords_unquoted() {
assert!(contains_write_operation("sqry index repair"));
assert!(contains_write_operation("sqry index drop"));
assert!(contains_write_operation("sqry index truncate"));
assert!(contains_write_operation("sqry index destroy"));
}
#[test]
fn test_write_operation_keywords_quoted() {
assert!(!contains_write_operation("sqry query \"drop_table\""));
assert!(!contains_write_operation("sqry query \"truncate_string\""));
assert!(!contains_write_operation(
"sqry query \"repair_connection\""
));
assert!(!contains_write_operation("sqry search \"destroy_session\""));
assert!(!contains_write_operation("sqry query 'drop_column'"));
}
#[test]
fn test_strip_quoted_sections() {
assert_eq!(strip_quoted_sections("hello \"world\" foo"), "hello foo");
assert_eq!(strip_quoted_sections("hello 'world' foo"), "hello foo");
assert_eq!(strip_quoted_sections("no quotes"), "no quotes");
assert_eq!(
strip_quoted_sections(r#"hello "wor\"ld" foo"#),
"hello foo"
);
}
}