Skip to main content

memory_core/
search.rs

1/// Sanitize user input for FTS5 queries.
2/// Whitelist approach: only alphanumeric, spaces, hyphens, underscores, dots.
3/// All FTS5 operators (NEAR, AND, OR, NOT, *, ^, column:) are stripped.
4pub fn sanitize_fts_query(input: &str) -> String {
5    let cleaned: String = input
6        .chars()
7        .map(|c| {
8            if c.is_alphanumeric() || c == ' ' || c == '-' || c == '_' || c == '.' {
9                c
10            } else {
11                ' '
12            }
13        })
14        .collect();
15
16    let fts_operators = ["NEAR", "AND", "OR", "NOT"];
17
18    let terms: Vec<String> = cleaned
19        .split_whitespace()
20        .filter(|term| {
21            let upper = term.to_uppercase();
22            !fts_operators.contains(&upper.as_str())
23        })
24        .filter(|term| !term.contains(':'))
25        .map(|term| format!("\"{term}\""))
26        .collect();
27
28    terms.join(" ")
29}
30
31#[cfg(test)]
32mod tests {
33    use super::*;
34
35    #[test]
36    fn sanitizes_normal_query() {
37        assert_eq!(sanitize_fts_query("hello world"), "\"hello\" \"world\"");
38    }
39
40    #[test]
41    fn strips_sql_injection() {
42        let result = sanitize_fts_query("'; DROP TABLE memories; --");
43        // Special chars like ' and ; are stripped
44        assert!(!result.contains(';'));
45        assert!(!result.contains('\''));
46        // Remaining words are safely quoted as FTS5 search terms
47        // "--" becomes a quoted term (harmless in FTS5 MATCH)
48        assert!(result.contains("\"DROP\""));
49        assert!(result.contains("\"TABLE\""));
50        assert!(result.contains("\"memories\""));
51    }
52
53    #[test]
54    fn strips_fts5_operators() {
55        let result = sanitize_fts_query("NEAR(password admin)");
56        assert!(!result.contains("NEAR"));
57    }
58
59    #[test]
60    fn strips_column_filters() {
61        let result = sanitize_fts_query("key:* OR 1=1");
62        assert!(!result.contains("key:"));
63        assert!(!result.contains("OR"));
64    }
65
66    #[test]
67    fn strips_special_chars() {
68        let result = sanitize_fts_query("test * ^ { } ( )");
69        assert_eq!(result, "\"test\"");
70    }
71
72    #[test]
73    fn empty_input_returns_empty() {
74        assert_eq!(sanitize_fts_query(""), "");
75        assert_eq!(sanitize_fts_query("   "), "");
76    }
77
78    #[test]
79    fn all_operators_returns_empty() {
80        assert_eq!(sanitize_fts_query("AND OR NOT"), "");
81    }
82
83    #[test]
84    fn preserves_hyphens_underscores_dots() {
85        let result = sanitize_fts_query("my-key some_thing file.rs");
86        assert_eq!(result, "\"my-key\" \"some_thing\" \"file.rs\"");
87    }
88}