1pub 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
31pub fn make_or_fallback(and_query: &str) -> Option<String> {
37 let terms: Vec<&str> = and_query.split_whitespace().collect();
38 if terms.len() <= 1 {
39 return None;
40 }
41 Some(terms.join(" OR "))
42}
43
44#[cfg(test)]
45mod tests {
46 use super::*;
47
48 #[test]
49 fn sanitizes_normal_query() {
50 assert_eq!(sanitize_fts_query("hello world"), "\"hello\" \"world\"");
51 }
52
53 #[test]
54 fn strips_sql_injection() {
55 let result = sanitize_fts_query("'; DROP TABLE memories; --");
56 assert!(!result.contains(';'));
58 assert!(!result.contains('\''));
59 assert!(result.contains("\"DROP\""));
62 assert!(result.contains("\"TABLE\""));
63 assert!(result.contains("\"memories\""));
64 }
65
66 #[test]
67 fn strips_fts5_operators() {
68 let result = sanitize_fts_query("NEAR(password admin)");
69 assert!(!result.contains("NEAR"));
70 }
71
72 #[test]
73 fn strips_column_filters() {
74 let result = sanitize_fts_query("key:* OR 1=1");
75 assert!(!result.contains("key:"));
76 assert!(!result.contains("OR"));
77 }
78
79 #[test]
80 fn strips_special_chars() {
81 let result = sanitize_fts_query("test * ^ { } ( )");
82 assert_eq!(result, "\"test\"");
83 }
84
85 #[test]
86 fn empty_input_returns_empty() {
87 assert_eq!(sanitize_fts_query(""), "");
88 assert_eq!(sanitize_fts_query(" "), "");
89 }
90
91 #[test]
92 fn all_operators_returns_empty() {
93 assert_eq!(sanitize_fts_query("AND OR NOT"), "");
94 }
95
96 #[test]
97 fn preserves_hyphens_underscores_dots() {
98 let result = sanitize_fts_query("my-key some_thing file.rs");
99 assert_eq!(result, "\"my-key\" \"some_thing\" \"file.rs\"");
100 }
101
102 #[test]
103 fn make_or_fallback_joins_with_or() {
104 let and_q = sanitize_fts_query("authentication auth security");
105 assert_eq!(and_q, "\"authentication\" \"auth\" \"security\"");
106 let or_q = make_or_fallback(&and_q).unwrap();
107 assert_eq!(or_q, "\"authentication\" OR \"auth\" OR \"security\"");
108 }
109
110 #[test]
111 fn make_or_fallback_single_term_returns_none() {
112 let and_q = sanitize_fts_query("authentication");
113 assert!(make_or_fallback(&and_q).is_none());
114 }
115
116 #[test]
117 fn make_or_fallback_empty_returns_none() {
118 assert!(make_or_fallback("").is_none());
119 }
120}