use regex::{Regex, RegexBuilder};
use std::collections::{HashMap, VecDeque};
use std::sync::{Arc, LazyLock, RwLock, RwLockReadGuard, RwLockWriteGuard};
const CACHE_CAPACITY: usize = 128;
const REGEX_SIZE_LIMIT: usize = 2 * 1024 * 1024;
#[derive(Default)]
struct CacheEntries {
values: HashMap<String, Arc<Regex>>,
insertion_order: VecDeque<String>,
}
struct RegexCache {
capacity: usize,
entries: RwLock<CacheEntries>,
}
impl RegexCache {
fn new(capacity: usize) -> Self {
Self {
capacity,
entries: RwLock::new(CacheEntries::default()),
}
}
fn read(&self) -> RwLockReadGuard<'_, CacheEntries> {
self.entries
.read()
.unwrap_or_else(|poison| poison.into_inner())
}
fn write(&self) -> RwLockWriteGuard<'_, CacheEntries> {
self.entries
.write()
.unwrap_or_else(|poison| poison.into_inner())
}
fn get_or_compile(&self, pattern: &str) -> Result<Arc<Regex>, regex::Error> {
if let Some(cached) = self.read().values.get(pattern) {
return Ok(Arc::clone(cached));
}
let compiled = Arc::new(
RegexBuilder::new(pattern)
.size_limit(REGEX_SIZE_LIMIT)
.build()?,
);
if self.capacity == 0 {
return Ok(compiled);
}
let mut entries = self.write();
if let Some(cached) = entries.values.get(pattern) {
return Ok(Arc::clone(cached));
}
while entries.values.len() >= self.capacity {
if let Some(oldest) = entries.insertion_order.pop_front() {
entries.values.remove(&oldest);
} else if let Some(oldest) = entries.values.keys().next().cloned() {
entries.values.remove(&oldest);
} else {
break;
}
}
entries.insertion_order.push_back(pattern.to_owned());
entries
.values
.insert(pattern.to_owned(), Arc::clone(&compiled));
Ok(compiled)
}
}
const OPERATOR_PREFIX: &str = "Invalid regular expression ";
const FUNCTION_INFIX: &str = "() invalid pattern: ";
pub(super) fn operator_compile_error(pattern: &str, err: ®ex::Error) -> String {
format!("{OPERATOR_PREFIX}'{pattern}': {err}")
}
pub(super) fn function_compile_error(function: &str, err: ®ex::Error) -> String {
format!("{function}{FUNCTION_INFIX}{err}")
}
pub(super) fn is_compile_error(message: &str) -> bool {
message.starts_with(OPERATOR_PREFIX) || message.contains(FUNCTION_INFIX)
}
static CACHE: LazyLock<RegexCache> = LazyLock::new(|| RegexCache::new(CACHE_CAPACITY));
fn anchor(pattern: &str) -> String {
format!("^(?:{pattern})$")
}
fn compile_anchored(pattern: &str) -> Result<Arc<Regex>, regex::Error> {
CACHE.get_or_compile(&anchor(pattern)).map_err(|wrapped| {
RegexBuilder::new(pattern)
.size_limit(REGEX_SIZE_LIMIT)
.build()
.err()
.unwrap_or(wrapped)
})
}
const LOCAL_CAPACITY: usize = 8;
thread_local! {
static LOCAL_ANCHORED: std::cell::RefCell<Vec<(String, Regex)>> =
const { std::cell::RefCell::new(Vec::new()) };
}
fn ensure_local(pattern: &str) -> Result<(), regex::Error> {
if LOCAL_ANCHORED.with(|local| local.borrow().iter().any(|(cached, _)| cached == pattern)) {
return Ok(());
}
let compiled = compile_anchored(pattern)?;
LOCAL_ANCHORED.with(|local| {
let mut local = local.borrow_mut();
if local.len() >= LOCAL_CAPACITY {
local.remove(0);
}
local.push((pattern.to_owned(), (*compiled).clone()));
});
Ok(())
}
pub fn with_compiled_anchored<R>(
pattern: &str,
f: impl FnOnce(&Regex) -> R,
) -> Result<R, regex::Error> {
ensure_local(pattern)?;
Ok(LOCAL_ANCHORED.with(|local| {
let local = local.borrow();
let compiled = local
.iter()
.find(|(cached, _)| cached == pattern)
.map(|(_, compiled)| compiled)
.expect("invariant: ensure_local just inserted this pattern");
f(compiled)
}))
}
pub fn get_or_compile(pattern: &str) -> Result<Arc<Regex>, regex::Error> {
CACHE.get_or_compile(pattern)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn compiles_and_caches() {
let cache = RegexCache::new(4);
let re1 = cache.get_or_compile(r"^\d+$").unwrap();
let re2 = cache.get_or_compile(r"^\d+$").unwrap();
assert!(Arc::ptr_eq(&re1, &re2));
assert!(re1.is_match("12345"));
assert!(!re1.is_match("abc"));
}
#[test]
fn evicts_at_capacity_and_recompiles() {
let cache = RegexCache::new(2);
let first = cache.get_or_compile("first").unwrap();
cache.get_or_compile("second").unwrap();
cache.get_or_compile("third").unwrap();
let entries = cache.read();
assert_eq!(entries.values.len(), 2);
assert!(!entries.values.contains_key("first"));
drop(entries);
let recompiled = cache.get_or_compile("first").unwrap();
assert!(!Arc::ptr_eq(&first, &recompiled));
}
#[test]
fn zero_capacity_compiles_without_storing() {
let cache = RegexCache::new(0);
let first = cache.get_or_compile("x").unwrap();
let second = cache.get_or_compile("x").unwrap();
assert!(!Arc::ptr_eq(&first, &second));
assert!(cache.read().values.is_empty());
}
#[test]
fn invalid_pattern_errors_without_consuming_capacity() {
let cache = RegexCache::new(1);
assert!(cache.get_or_compile(r"(?P<bad").is_err());
assert!(cache.read().values.is_empty());
}
#[test]
fn compile_error_messages_are_recognised() {
let bad = String::from("[");
let err = Regex::new(&bad).expect_err("'[' must not compile");
let operator = operator_compile_error(&bad, &err);
let function = function_compile_error("text_match_regex", &err);
assert!(operator.starts_with("Invalid regular expression '['"));
assert!(function.starts_with("text_match_regex() invalid pattern: "));
assert!(is_compile_error(&operator));
assert!(is_compile_error(&function));
}
#[test]
fn other_evaluation_errors_are_not_compile_errors() {
assert!(!is_compile_error("Missing parameter: $min"));
assert!(!is_compile_error(
"Cannot evaluate aggregate function in this context"
));
assert!(!is_compile_error("Variable 'x' not bound"));
assert!(!is_compile_error(""));
}
#[test]
fn anchored_matches_the_whole_subject_only() {
assert!(with_compiled_anchored("active", |re| re.is_match("active")).unwrap());
assert!(!with_compiled_anchored("active", |re| re.is_match("inactive")).unwrap());
assert!(!with_compiled_anchored("b", |re| re.is_match("abc")).unwrap());
assert!(with_compiled_anchored("^A.*", |re| re.is_match("Alice")).unwrap());
}
#[test]
fn anchored_alternation_binds_as_a_unit() {
assert!(with_compiled_anchored("cat|dog", |re| re.is_match("cat")).unwrap());
assert!(with_compiled_anchored("cat|dog", |re| re.is_match("dog")).unwrap());
assert!(!with_compiled_anchored("cat|dog", |re| re.is_match("catx")).unwrap());
assert!(!with_compiled_anchored("cat|dog", |re| re.is_match("xdog")).unwrap());
}
#[test]
fn anchored_keeps_inline_flags() {
assert!(with_compiled_anchored("(?i)active", |re| re.is_match("ACTIVE")).unwrap());
assert!(!with_compiled_anchored("active", |re| re.is_match("ACTIVE")).unwrap());
}
#[test]
fn anchored_compile_error_names_the_users_pattern() {
let bad = String::from("[");
let err = with_compiled_anchored(&bad, |_| ()).expect_err("'[' must not compile");
let message = operator_compile_error(&bad, &err);
assert!(
message.starts_with("Invalid regular expression '['"),
"{message}"
);
assert!(!message.contains("^(?:"), "{message}");
}
#[test]
fn inline_flags_work() {
let cache = RegexCache::new(1);
let re = cache.get_or_compile(r"(?i)hello").unwrap();
assert!(re.is_match("HELLO"));
assert!(re.is_match("Hello"));
}
#[test]
fn concurrent_misses_publish_one_cached_value() {
let cache = Arc::new(RegexCache::new(4));
let threads: Vec<_> = (0..8)
.map(|_| {
let cache = Arc::clone(&cache);
std::thread::spawn(move || cache.get_or_compile("concurrent").unwrap())
})
.collect();
let values: Vec<_> = threads.into_iter().map(|t| t.join().unwrap()).collect();
let cached = cache.get_or_compile("concurrent").unwrap();
assert!(values.iter().any(|value| Arc::ptr_eq(value, &cached)));
assert_eq!(cache.read().values.len(), 1);
}
#[test]
fn poisoned_lock_is_recovered() {
let cache = Arc::new(RegexCache::new(2));
let poisoner = Arc::clone(&cache);
let _ = std::thread::spawn(move || {
let _guard = poisoner.entries.write().unwrap();
panic!("poison cache lock");
})
.join();
assert!(cache
.get_or_compile("after-poison")
.unwrap()
.is_match("after-poison"));
}
#[test]
fn inconsistent_eviction_order_cannot_break_capacity() {
let cache = RegexCache::new(1);
cache.get_or_compile("first").unwrap();
cache.write().insertion_order.clear();
cache.get_or_compile("second").unwrap();
assert_eq!(cache.read().values.len(), 1);
}
}