guard_core_engine/
compiler.rs1use std::num::NonZeroUsize;
2
3use lru::LruCache;
4use regex::Regex;
5
6const DANGEROUS_PATTERNS: &[&str] = &[
7 r"\(\.\*\)\+",
8 r"\(\.\+\)\+",
9 r"\([^)]*\*\)\+",
10 r"\([^)]*\+\)\+",
11 r"(?:\.\*){2,}",
12 r"(?:\.\+){2,}",
13];
14
15pub struct PatternCache {
20 cache: LruCache<String, Regex>,
21}
22
23impl PatternCache {
24 #[must_use]
26 pub fn new(capacity: usize) -> Self {
27 let cap = capacity.clamp(1, 5000);
28 Self {
29 cache: LruCache::new(NonZeroUsize::new(cap).expect("clamped to >= 1")),
30 }
31 }
32
33 pub fn get_or_compile(&mut self, pattern: &str) -> Result<&Regex, regex::Error> {
35 self.cache
37 .try_get_or_insert_mut(pattern.to_owned(), || {
38 Regex::new(&format!("(?im){pattern}"))
39 })
40 .map(|r| &*r)
41 }
42
43 pub fn clear(&mut self) {
45 self.cache.clear();
46 }
47
48 #[must_use]
50 pub fn len(&self) -> usize {
51 self.cache.len()
52 }
53
54 #[must_use]
56 pub fn is_empty(&self) -> bool {
57 self.cache.is_empty()
58 }
59}
60
61pub fn compile(pattern: &str) -> Result<Regex, regex::Error> {
63 Regex::new(&format!("(?im){pattern}"))
64}
65
66#[must_use]
73pub fn validate_pattern_safety(pattern: &str) -> (bool, &'static str) {
74 for &dangerous in DANGEROUS_PATTERNS {
75 if let Ok(checker) = Regex::new(dangerous)
76 && checker.is_match(pattern)
77 {
78 return (false, dangerous);
79 }
80 }
81
82 (true, "pattern appears safe")
83}
84
85#[must_use]
88pub fn batch_compile(patterns: &[&str], validate: bool) -> Vec<(String, Regex)> {
89 patterns
90 .iter()
91 .filter_map(|&pat| {
92 if validate && !validate_pattern_safety(pat).0 {
93 return None;
94 }
95 compile(pat).ok().map(|re| (pat.to_owned(), re))
96 })
97 .collect()
98}
99
100#[cfg(test)]
101mod tests {
102 use super::*;
103
104 #[test]
105 fn cache_basic_operations() {
106 let mut cache = PatternCache::new(10);
107 assert!(cache.is_empty());
108
109 let re = cache.get_or_compile(r"\d+").unwrap();
110 assert!(re.is_match("123"));
111 assert_eq!(cache.len(), 1);
112
113 let r1 = cache.get_or_compile(r"\d+").unwrap().as_str().to_owned();
115 let r2 = cache.get_or_compile(r"\d+").unwrap().as_str().to_owned();
116 assert_eq!(r1, r2);
117 assert_eq!(cache.len(), 1);
118
119 cache.clear();
120 assert!(cache.is_empty());
121 }
122
123 #[test]
124 fn cache_eviction_and_lru_order() {
125 let mut cache = PatternCache::new(2);
126 cache.get_or_compile("aaa").unwrap();
127 cache.get_or_compile("bbb").unwrap();
128 cache.get_or_compile("ccc").unwrap();
129 assert_eq!(cache.len(), 2);
130
131 let mut cache = PatternCache::new(3);
133 for i in 0..3 {
134 cache.get_or_compile(&format!("pattern_{i}")).unwrap();
135 }
136 let _ = cache.get_or_compile("pattern_0").unwrap();
137 cache.get_or_compile("pattern_new").unwrap();
138 assert_eq!(cache.len(), 3);
139 }
140
141 #[test]
142 fn cache_capacity_clamping() {
143 assert_eq!(PatternCache::new(0).cache.cap().get(), 1);
144 assert_eq!(PatternCache::new(99999).cache.cap().get(), 5000);
145 }
146
147 #[test]
148 fn dangerous_patterns_detected() {
149 for pat in [
150 r"(.*)+",
151 r"(.+)+",
152 r"([a-z]*)+",
153 r"([a-z]+)+",
154 r".*.*",
155 r".+.+",
156 ] {
157 let (safe, _) = validate_pattern_safety(pat);
158 assert!(!safe, "should flag: {pat}");
159 }
160 }
161
162 #[test]
163 fn safe_patterns_pass() {
164 let (safe, msg) = validate_pattern_safety(r"test\d+");
165 assert!(safe);
166 assert_eq!(msg, "pattern appears safe");
167
168 for pat in [r"<script[^>]*>", r"\d{3}-\d{3}-\d{4}", r"[a-zA-Z0-9]+"] {
169 assert!(validate_pattern_safety(pat).0, "should pass: {pat}");
170 }
171 }
172
173 #[test]
174 fn compile_flags_and_errors() {
175 assert!(compile(r"select").unwrap().is_match("SELECT"));
177 assert!(
178 compile(r"^hello")
179 .unwrap()
180 .is_match("first line\nhello world")
181 );
182 assert!(compile(r"invalid(pattern").is_err());
184 }
185
186 #[test]
187 fn batch_compile_filters_invalid_and_dangerous() {
188 let results = batch_compile(&[r"\d+", r"invalid(", r"[a-z]+"], false);
189 assert_eq!(results.len(), 2);
190
191 let results = batch_compile(&[r"\d+", r"(.*)+", r"[a-z]+"], true);
192 assert_eq!(results.len(), 2);
193 assert!(results.iter().all(|(p, _)| p != "(.*)+"));
194
195 assert!(batch_compile(&[r"invalid(", r"[unclosed"], false).is_empty());
196 }
197}