use std::collections::HashMap;
use std::sync::{Arc, LazyLock, RwLock};
const REGEX_SIZE_LIMIT_BYTES: usize = 64 << 20;
static PATTERN_CACHE: LazyLock<RwLock<HashMap<String, Option<Arc<CompiledPattern>>>>> =
LazyLock::new(|| RwLock::new(HashMap::new()));
#[derive(Debug)]
pub enum CompiledPattern {
Fast(regex::Regex),
Fancy(fancy_regex::Regex),
}
impl CompiledPattern {
#[must_use]
pub fn is_match(&self, text: &str) -> bool {
match self {
CompiledPattern::Fast(re) => re.is_match(text),
CompiledPattern::Fancy(re) => re.is_match(text).unwrap_or(false),
}
}
}
#[must_use]
pub fn compile(pattern: &str) -> Option<Arc<CompiledPattern>> {
if let Some(cached) = PATTERN_CACHE.read().unwrap_or_else(|poisoned| poisoned.into_inner()).get(pattern) {
return cached.clone();
}
let compiled = compile_uncached(pattern).map(Arc::new);
PATTERN_CACHE
.write()
.unwrap_or_else(|poisoned| poisoned.into_inner())
.insert(pattern.to_string(), compiled.clone());
compiled
}
fn compile_uncached(pattern: &str) -> Option<CompiledPattern> {
if let Ok(re) = build_fast(pattern) {
return Some(CompiledPattern::Fast(re));
}
let normalized = normalize(pattern);
if let Ok(re) = build_fast(&normalized) {
return Some(CompiledPattern::Fast(re));
}
if let Ok(re) = fancy_regex::Regex::new(pattern) {
return Some(CompiledPattern::Fancy(re));
}
if let Ok(re) = fancy_regex::Regex::new(&normalized) {
return Some(CompiledPattern::Fancy(re));
}
None
}
#[must_use]
pub fn is_service_valid(pattern: &str) -> bool {
compile(pattern).is_some()
}
#[must_use]
pub fn anchor_allowed_pattern(pattern: &str) -> String {
match (pattern.starts_with('^'), pattern.ends_with('$')) {
(true, true) => pattern.to_string(),
(true, false) => format!("{pattern}$"),
(false, true) => format!("^{pattern}"),
(false, false) => format!("^{pattern}$"),
}
}
#[must_use]
pub fn default_matches_pattern(pattern: &str, value: &str, is_comma_delimited: bool) -> Option<bool> {
let compiled = compile(&anchor_allowed_pattern(pattern))?;
if is_comma_delimited {
Some(value.split(',').all(|element| compiled.is_match(element.trim())))
} else {
Some(compiled.is_match(value))
}
}
fn build_fast(pattern: &str) -> Result<regex::Regex, regex::Error> {
regex::RegexBuilder::new(pattern).size_limit(REGEX_SIZE_LIMIT_BYTES).build()
}
static QUANTIFIED_LOOKAROUND: LazyLock<regex::Regex> =
LazyLock::new(|| regex::Regex::new(r"(\(\?[=!][^)]*\))[+*]").expect("QUANTIFIED_LOOKAROUND is a valid regex"));
fn normalize(pattern: &str) -> String {
let mut out = pattern.replace(r"\Z", r"\z");
out = convert_unicode_escapes(&out);
out = expand_posix_classes(&out);
out = rewrite_unicode_other_complement(&out);
out = repair_char_classes(&out);
QUANTIFIED_LOOKAROUND.replace_all(&out, "$1").into_owned()
}
fn convert_unicode_escapes(pattern: &str) -> String {
let chars: Vec<char> = pattern.chars().collect();
let mut out = String::with_capacity(pattern.len());
let mut index = 0;
let mut inside_class = false;
while index < chars.len() {
if let Some(code_point) = unicode_escape_at(&chars, index) {
if inside_class && is_surrogate(code_point) {
out.push_str(SURROGATE_PAIR_ASTRAL_RANGE);
index = end_of_surrogate_run(&chars, index);
} else {
out.push_str(&format!(r"\u{{{:04X}}}", nearest_scalar(code_point)));
index += UNICODE_ESCAPE_LEN;
}
continue;
}
let ch = chars[index];
match ch {
'\\' if index + 1 < chars.len() => {
out.push('\\');
out.push(chars[index + 1]);
index += 2;
continue;
}
'[' => inside_class = true,
']' => inside_class = false,
_ => {}
}
out.push(ch);
index += 1;
}
out
}
const SURROGATE_PAIR_ASTRAL_RANGE: &str = r"\u{10000}-\u{10FFFF}";
const UNICODE_ESCAPE_LEN: usize = 6;
fn is_surrogate(code_point: u32) -> bool {
(0xD800..=0xDFFF).contains(&code_point)
}
fn nearest_scalar(code_point: u32) -> u32 {
if !is_surrogate(code_point) {
code_point
} else if code_point <= 0xDBFF {
0xD7FF
} else {
0xE000
}
}
fn unicode_escape_at(chars: &[char], index: usize) -> Option<u32> {
if chars.get(index) != Some(&'\\') || chars.get(index + 1) != Some(&'u') {
return None;
}
let hex: String = chars.get(index + 2..index + UNICODE_ESCAPE_LEN)?.iter().collect();
if !hex.chars().all(|digit| digit.is_ascii_hexdigit()) {
return None;
}
u32::from_str_radix(&hex, 16).ok()
}
fn end_of_surrogate_run(chars: &[char], mut index: usize) -> usize {
loop {
index += UNICODE_ESCAPE_LEN;
if chars.get(index) == Some(&'-') && unicode_escape_at(chars, index + 1).is_some_and(is_surrogate) {
index += 1;
} else if !unicode_escape_at(chars, index).is_some_and(is_surrogate) {
return index;
}
}
}
fn expand_posix_classes(pattern: &str) -> String {
const GRAPH_TOKEN: &[char] = &['\\', 'p', '{', 'G', 'r', 'a', 'p', 'h', '}'];
const PRINT_TOKEN: &[char] = &['\\', 'p', '{', 'P', 'r', 'i', 'n', 't', '}'];
const GRAPH_MEMBERS: &str = r"\p{L}\p{M}\p{N}\p{P}\p{S}\p{Cf}\p{Co}";
const PRINT_MEMBERS: &str = r"\p{L}\p{M}\p{N}\p{P}\p{S}\p{Cf}\p{Co}\p{Zs}\x20";
let chars: Vec<char> = pattern.chars().collect();
let mut out = String::with_capacity(pattern.len());
let mut index = 0;
let mut inside_class = false;
while index < chars.len() {
if let Some(replacement) = posix_expansion(&chars, index, GRAPH_TOKEN, GRAPH_MEMBERS, inside_class) {
out.push_str(&replacement);
index += GRAPH_TOKEN.len();
continue;
}
if let Some(replacement) = posix_expansion(&chars, index, PRINT_TOKEN, PRINT_MEMBERS, inside_class) {
out.push_str(&replacement);
index += PRINT_TOKEN.len();
continue;
}
let ch = chars[index];
match ch {
'\\' if index + 1 < chars.len() => {
out.push('\\');
out.push(chars[index + 1]);
index += 2;
continue;
}
'[' => inside_class = true,
']' => inside_class = false,
_ => {}
}
out.push(ch);
index += 1;
}
out
}
fn posix_expansion(chars: &[char], index: usize, token: &[char], members: &str, inside_class: bool) -> Option<String> {
if !chars[index..].starts_with(token) {
return None;
}
Some(if inside_class { members.to_string() } else { format!("[{members}]") })
}
const UNICODE_OTHER_COMPLEMENT: &[char] = &['[', '^', '\\', 'p', '{', 'C', '}', ']'];
const UNICODE_OTHER_LOOKAHEAD: &str = r"(?:(?!\p{C})(?s:.))";
fn rewrite_unicode_other_complement(pattern: &str) -> String {
let chars: Vec<char> = pattern.chars().collect();
let mut out = String::with_capacity(pattern.len());
let mut index = 0;
let mut inside_class = false;
while index < chars.len() {
if chars[index] == '\\' && index + 1 < chars.len() {
out.push(chars[index]);
out.push(chars[index + 1]);
index += 2;
continue;
}
if !inside_class && chars[index..].starts_with(UNICODE_OTHER_COMPLEMENT) {
out.push_str(UNICODE_OTHER_LOOKAHEAD);
index += UNICODE_OTHER_COMPLEMENT.len();
continue;
}
match chars[index] {
'[' => inside_class = true,
']' => inside_class = false,
_ => {}
}
out.push(chars[index]);
index += 1;
}
out
}
fn repair_char_classes(pattern: &str) -> String {
let chars: Vec<char> = pattern.chars().collect();
let mut out = String::with_capacity(pattern.len() + 8);
let mut index = 0;
let mut inside_class = false;
let mut prev_was_shorthand = false;
while index < chars.len() {
let ch = chars[index];
if ch == '\\' && index + 1 < chars.len() {
let next = chars[index + 1];
out.push('\\');
out.push(next);
if (next == 'p' || next == 'P') && index + 2 < chars.len() && chars[index + 2] == '{' {
let mut end = index + 2;
while end < chars.len() && chars[end] != '}' {
out.push(chars[end]);
end += 1;
}
if end < chars.len() {
out.push('}');
}
index = end + 1;
prev_was_shorthand = inside_class;
continue;
}
prev_was_shorthand = inside_class && matches!(next, 'w' | 'd' | 's' | 'W' | 'D' | 'S');
index += 2;
continue;
}
if ch == '[' {
if inside_class {
out.push_str(r"\[");
index += 1;
prev_was_shorthand = false;
continue;
}
inside_class = true;
prev_was_shorthand = false;
out.push('[');
index += 1;
if index < chars.len() && chars[index] == '^' {
out.push('^');
index += 1;
}
if index < chars.len() && chars[index] == ']' {
out.push(']');
index += 1;
}
continue;
}
if ch == ']' && inside_class {
inside_class = false;
out.push(']');
index += 1;
prev_was_shorthand = false;
continue;
}
if ch == '-' && inside_class && prev_was_shorthand {
out.push_str(r"\-");
index += 1;
prev_was_shorthand = false;
continue;
}
out.push(ch);
prev_was_shorthand = false;
index += 1;
}
out
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn plain_pattern_compiles_on_fast_engine() {
let compiled = compile(r"^ami-[0-9a-f]{8,17}$").expect("plain pattern compiles");
assert!(matches!(*compiled, CompiledPattern::Fast(_)));
assert!(compiled.is_match("ami-0123abcd"));
assert!(!compiled.is_match("vpc-0123abcd"));
}
#[test]
fn compile_is_memoized_and_returns_the_same_instance() {
let first = compile(r"^cache-test-[0-9]+$").expect("compiles");
let second = compile(r"^cache-test-[0-9]+$").expect("compiles");
assert!(Arc::ptr_eq(&first, &second), "repeated compile must reuse the cached instance");
}
#[test]
fn print_class_matches_format_and_private_use_characters() {
let compiled = compile(r"^\p{Print}+$").expect("\\p{Print} compiles");
assert!(compiled.is_match("normal text"));
assert!(compiled.is_match("zero\u{200B}width"), "U+200B (Cf) must match \\p{{Print}}");
assert!(compiled.is_match("private\u{E000}use"), "U+E000 (Co) must match \\p{{Print}}");
}
#[test]
fn negative_lookahead_compiles_and_matches_like_pcre() {
let compiled = compile(r"^(?!aws:).+$").expect("lookahead pattern compiles");
assert!(matches!(*compiled, CompiledPattern::Fancy(_)));
assert!(compiled.is_match("my-tag"));
assert!(!compiled.is_match("aws:reserved"));
}
#[test]
fn uppercase_z_anchor_is_normalized_to_lowercase() {
let compiled = compile(r"\A[0-9a-fA-F]+\Z").expect("\\A..\\Z pattern compiles");
assert!(compiled.is_match("abc123"));
assert!(!compiled.is_match("xyz"));
}
#[test]
fn size_limit_pattern_compiles_without_rewrite() {
let compiled = compile(r"^[\w.-]{1,255}$").expect("size-limit pattern compiles");
assert!(matches!(*compiled, CompiledPattern::Fast(_)));
assert!(compiled.is_match("valid.name-1"));
}
#[test]
fn bounded_unicode_other_complement_preserves_category_and_length_constraints() {
let compiled = compile(r"^[^\p{C}]{1,2048}$").expect("bounded Unicode category pattern compiles");
assert!(compiled.is_match("cafe\u{301} 123 ☃!"));
assert!(!compiled.is_match(""), "the lower length boundary must be enforced");
for (category, value) in [
("Control", "line\nbreak"),
("Format", "zero\u{200B}width"),
("Private Use", "private\u{E000}use"),
("Unassigned", "unassigned\u{0378}"),
] {
assert!(!compiled.is_match(value), "Unicode {category} characters must be rejected");
}
let maximum_length = "a".repeat(2048);
let above_maximum_length = "a".repeat(2049);
assert!(compiled.is_match(&maximum_length), "the upper length boundary must be accepted");
assert!(!compiled.is_match(&above_maximum_length), "values above the upper length boundary must be rejected");
}
#[test]
fn print_class_still_matches_ordinary_text() {
let compiled = compile(r"^\p{Print}+$").expect("\\p{Print} pattern compiles");
for value in ["abc", "Hello World", "aws:reserved", "café", "a.b.c/x"] {
assert!(compiled.is_match(value), "\\p{{Print}} must match {value:?}");
}
assert!(!compiled.is_match(""));
}
#[test]
fn graph_class_inside_existing_class_compiles() {
let compiled = compile(r"^[\p{Graph}\x20]*$").expect("\\p{Graph} in class compiles");
assert!(compiled.is_match("visible text"));
}
#[test]
fn surrogate_pair_range_admits_astral_characters() {
let compiled = compile(r"^[ --�𐀀-\r\n\t]*$").expect("surrogate pattern compiles");
assert!(compiled.is_match("normal text"));
assert!(
compiled.is_match("emoji \u{1F600} and \u{10FFFF}"),
"astral characters must match the surrogate-pair range"
);
}
#[test]
fn low_then_high_surrogate_range_still_compiles() {
let compiled =
compile(r"^[ --�\uD800\uDBFF-\uDC00\uDFFF\t]*$").expect("reordered surrogate pattern compiles");
assert!(compiled.is_match("normal text"));
assert!(compiled.is_match("astral \u{1F4A9}"), "astral characters must match regardless of surrogate ordering");
}
#[test]
fn lone_surrogate_escape_outside_class_is_clamped_not_inverted() {
assert!(is_service_valid(r"^\uD800$"));
}
#[test]
fn hyphen_after_shorthand_is_treated_as_literal() {
let compiled = compile(r"[\w-.~]+").expect("hyphen-range pattern compiles");
assert!(compiled.is_match("a-b.c"));
}
#[test]
fn genuinely_malformed_pattern_is_not_service_valid() {
assert!(!is_service_valid(r"^(unbalanced"));
assert!(!is_service_valid(r"a{2,1}"));
}
#[test]
fn pcre_flavored_patterns_are_service_valid() {
assert!(is_service_valid(r"^(?!aws:).+$"));
assert!(is_service_valid(r"\A[0-9]+\Z"));
assert!(is_service_valid(r"^\p{Print}+$"));
assert!(is_service_valid(r"^[\w.-]{1,255}$"));
}
#[test]
fn normalize_preserves_plain_pattern_unchanged() {
assert_eq!(normalize(r"^[a-z0-9]+$"), r"^[a-z0-9]+$");
}
#[test]
fn anchor_allowed_pattern_adds_missing_anchors() {
assert_eq!(anchor_allowed_pattern("[a-z]+"), "^[a-z]+$");
assert_eq!(anchor_allowed_pattern("^[a-z]+"), "^[a-z]+$");
assert_eq!(anchor_allowed_pattern("[a-z]+$"), "^[a-z]+$");
assert_eq!(anchor_allowed_pattern("^[a-z]+$"), "^[a-z]+$");
}
#[test]
fn default_matches_pattern_scalar() {
assert_eq!(default_matches_pattern(r"[0-9]+", "123", false), Some(true));
assert_eq!(default_matches_pattern(r"[0-9]+", "12a", false), Some(false));
}
#[test]
fn default_matches_pattern_honors_pcre_syntax() {
assert_eq!(default_matches_pattern(r"\A[0-9a-fA-F]+\Z", "abc123", false), Some(true));
}
#[test]
fn default_matches_pattern_comma_delimited_checks_each_element() {
assert_eq!(default_matches_pattern(r"[a-z]+", "abc, def ,ghi", true), Some(true));
assert_eq!(default_matches_pattern(r"[a-z]+", "abc, DEF", true), Some(false));
}
#[test]
fn default_matches_pattern_returns_none_for_uncompilable() {
assert_eq!(default_matches_pattern(r"^(unbalanced", "x", false), None);
}
}