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