1use std::collections::HashMap;
19use std::sync::{Arc, LazyLock, RwLock};
20
21const REGEX_SIZE_LIMIT_BYTES: usize = 64 << 20;
26
27static PATTERN_CACHE: LazyLock<RwLock<HashMap<String, Option<Arc<CompiledPattern>>>>> =
33 LazyLock::new(|| RwLock::new(HashMap::new()));
34
35#[derive(Debug)]
40pub enum CompiledPattern {
41 Fast(regex::Regex),
42 Fancy(fancy_regex::Regex),
43}
44
45impl CompiledPattern {
46 #[must_use]
51 pub fn is_match(&self, text: &str) -> bool {
52 match self {
53 CompiledPattern::Fast(re) => re.is_match(text),
54 CompiledPattern::Fancy(re) => re.is_match(text).unwrap_or(false),
55 }
56 }
57}
58
59#[must_use]
67pub fn compile(pattern: &str) -> Option<Arc<CompiledPattern>> {
68 if let Some(cached) = PATTERN_CACHE.read().unwrap_or_else(|poisoned| poisoned.into_inner()).get(pattern) {
69 return cached.clone();
70 }
71 let compiled = compile_uncached(pattern).map(Arc::new);
72 PATTERN_CACHE
73 .write()
74 .unwrap_or_else(|poisoned| poisoned.into_inner())
75 .insert(pattern.to_string(), compiled.clone());
76 compiled
77}
78
79fn compile_uncached(pattern: &str) -> Option<CompiledPattern> {
80 if let Ok(re) = build_fast(pattern) {
81 return Some(CompiledPattern::Fast(re));
82 }
83 let normalized = normalize(pattern);
84 let normalization_changed_pattern = normalized != pattern;
85 if normalization_changed_pattern && let Ok(re) = build_fast(&normalized) {
86 return Some(CompiledPattern::Fast(re));
87 }
88 if let Ok(re) = fancy_regex::Regex::new(pattern) {
89 return Some(CompiledPattern::Fancy(re));
90 }
91 if normalization_changed_pattern && let Ok(re) = fancy_regex::Regex::new(&normalized) {
92 return Some(CompiledPattern::Fancy(re));
93 }
94 let compact_bounded_dots = rewrite_bounded_dot_repetitions(&normalized);
95 if compact_bounded_dots != normalized
96 && let Ok(re) = fancy_regex::Regex::new(&compact_bounded_dots)
97 {
98 return Some(CompiledPattern::Fancy(re));
99 }
100 None
101}
102
103#[must_use]
109pub fn is_service_valid(pattern: &str) -> bool {
110 compile(pattern).is_some()
111}
112
113#[must_use]
117pub fn anchor_allowed_pattern(pattern: &str) -> String {
118 match (pattern.starts_with('^'), pattern.ends_with('$')) {
119 (true, true) => pattern.to_string(),
120 (true, false) => format!("{pattern}$"),
121 (false, true) => format!("^{pattern}"),
122 (false, false) => format!("^{pattern}$"),
123 }
124}
125
126#[must_use]
131pub fn default_matches_pattern(pattern: &str, value: &str, is_comma_delimited: bool) -> Option<bool> {
132 let compiled = compile(&anchor_allowed_pattern(pattern))?;
133 if is_comma_delimited {
134 Some(value.split(',').all(|element| compiled.is_match(element.trim())))
135 } else {
136 Some(compiled.is_match(value))
137 }
138}
139
140fn build_fast(pattern: &str) -> Result<regex::Regex, regex::Error> {
141 regex::RegexBuilder::new(pattern).size_limit(REGEX_SIZE_LIMIT_BYTES).build()
142}
143
144static QUANTIFIED_LOOKAROUND: LazyLock<regex::Regex> =
145 LazyLock::new(|| regex::Regex::new(r"(\(\?[=!][^)]*\))[+*]").expect("QUANTIFIED_LOOKAROUND is a valid regex"));
146
147fn normalize(pattern: &str) -> String {
151 let mut out = pattern.replace(r"\Z", r"\z");
152 out = convert_unicode_escapes(&out);
153 out = expand_posix_classes(&out);
154 out = rewrite_unicode_other_complement(&out);
155 out = repair_char_classes(&out);
156 QUANTIFIED_LOOKAROUND.replace_all(&out, "$1").into_owned()
157}
158
159const COMPACT_DOT_ATOM: &str = r"(?:(?=.)(?s:.))";
160
161fn rewrite_bounded_dot_repetitions(pattern: &str) -> String {
167 let chars: Vec<char> = pattern.chars().collect();
168 let mut out = String::with_capacity(pattern.len());
169 let mut index = 0;
170 let mut inside_class = false;
171 let mut has_rewritten_dot = false;
172 while index < chars.len() {
173 let character = chars[index];
174 if character == '\\' && index + 1 < chars.len() {
175 out.push(character);
176 out.push(chars[index + 1]);
177 index += 2;
178 continue;
179 }
180 if character == '[' && !inside_class {
181 inside_class = true;
182 out.push(character);
183 index += 1;
184 if chars.get(index) == Some(&'^') {
185 out.push('^');
186 index += 1;
187 }
188 if chars.get(index) == Some(&']') {
189 out.push(']');
190 index += 1;
191 }
192 continue;
193 }
194 if character == ']' && inside_class {
195 inside_class = false;
196 } else if character == '.' && !inside_class && bounded_quantifier_follows(&chars, index + 1) {
197 if has_rewritten_dot {
198 return pattern.to_string();
199 }
200 has_rewritten_dot = true;
201 out.push_str(COMPACT_DOT_ATOM);
202 index += 1;
203 continue;
204 }
205 out.push(character);
206 index += 1;
207 }
208 out
209}
210
211fn bounded_quantifier_follows(chars: &[char], index: usize) -> bool {
212 if chars.get(index) != Some(&'{') {
213 return false;
214 }
215
216 let mut cursor = index + 1;
217 let Some(minimum) = parse_quantifier_bound(chars, &mut cursor) else {
218 return false;
219 };
220
221 match chars.get(cursor) {
222 Some('}') => true,
223 Some(',') => {
224 cursor += 1;
225 parse_quantifier_bound(chars, &mut cursor)
226 .is_some_and(|maximum| minimum <= maximum && chars.get(cursor) == Some(&'}'))
227 }
228 _ => false,
229 }
230}
231
232fn parse_quantifier_bound(chars: &[char], cursor: &mut usize) -> Option<u64> {
233 let start = *cursor;
234 let mut bound = 0_u64;
235 while let Some(character) = chars.get(*cursor).filter(|character| character.is_ascii_digit()) {
236 bound = bound.checked_mul(10)?.checked_add(u64::from(character.to_digit(10)?))?;
237 *cursor += 1;
238 }
239 (*cursor > start).then_some(bound)
240}
241
242fn convert_unicode_escapes(pattern: &str) -> String {
255 let chars: Vec<char> = pattern.chars().collect();
256 let mut out = String::with_capacity(pattern.len());
257 let mut index = 0;
258 let mut inside_class = false;
259 while index < chars.len() {
260 if let Some(code_point) = unicode_escape_at(&chars, index) {
261 if inside_class && is_surrogate(code_point) {
262 out.push_str(SURROGATE_PAIR_ASTRAL_RANGE);
263 index = end_of_surrogate_run(&chars, index);
264 } else {
265 out.push_str(&format!(r"\u{{{:04X}}}", nearest_scalar(code_point)));
266 index += UNICODE_ESCAPE_LEN;
267 }
268 continue;
269 }
270 let ch = chars[index];
271 match ch {
272 '\\' if index + 1 < chars.len() => {
273 out.push('\\');
274 out.push(chars[index + 1]);
275 index += 2;
276 continue;
277 }
278 '[' => inside_class = true,
279 ']' => inside_class = false,
280 _ => {}
281 }
282 out.push(ch);
283 index += 1;
284 }
285 out
286}
287
288const SURROGATE_PAIR_ASTRAL_RANGE: &str = r"\u{10000}-\u{10FFFF}";
290
291const UNICODE_ESCAPE_LEN: usize = 6;
293
294fn is_surrogate(code_point: u32) -> bool {
295 (0xD800..=0xDFFF).contains(&code_point)
296}
297
298fn nearest_scalar(code_point: u32) -> u32 {
303 if !is_surrogate(code_point) {
304 code_point
305 } else if code_point <= 0xDBFF {
306 0xD7FF
307 } else {
308 0xE000
309 }
310}
311
312fn unicode_escape_at(chars: &[char], index: usize) -> Option<u32> {
314 if chars.get(index) != Some(&'\\') || chars.get(index + 1) != Some(&'u') {
315 return None;
316 }
317 let hex: String = chars.get(index + 2..index + UNICODE_ESCAPE_LEN)?.iter().collect();
318 if !hex.chars().all(|digit| digit.is_ascii_hexdigit()) {
319 return None;
320 }
321 u32::from_str_radix(&hex, 16).ok()
322}
323
324fn end_of_surrogate_run(chars: &[char], mut index: usize) -> usize {
328 loop {
329 index += UNICODE_ESCAPE_LEN;
330 if chars.get(index) == Some(&'-') && unicode_escape_at(chars, index + 1).is_some_and(is_surrogate) {
331 index += 1;
332 } else if !unicode_escape_at(chars, index).is_some_and(is_surrogate) {
333 return index;
334 }
335 }
336}
337
338fn expand_posix_classes(pattern: &str) -> String {
345 const GRAPH_TOKEN: &[char] = &['\\', 'p', '{', 'G', 'r', 'a', 'p', 'h', '}'];
346 const PRINT_TOKEN: &[char] = &['\\', 'p', '{', 'P', 'r', 'i', 'n', 't', '}'];
347 const GRAPH_MEMBERS: &str = r"\p{L}\p{M}\p{N}\p{P}\p{S}\p{Cf}\p{Co}";
348 const PRINT_MEMBERS: &str = r"\p{L}\p{M}\p{N}\p{P}\p{S}\p{Cf}\p{Co}\p{Zs}\x20";
349 let chars: Vec<char> = pattern.chars().collect();
350 let mut out = String::with_capacity(pattern.len());
351 let mut index = 0;
352 let mut inside_class = false;
353 while index < chars.len() {
354 if let Some(replacement) = posix_expansion(&chars, index, GRAPH_TOKEN, GRAPH_MEMBERS, inside_class) {
355 out.push_str(&replacement);
356 index += GRAPH_TOKEN.len();
357 continue;
358 }
359 if let Some(replacement) = posix_expansion(&chars, index, PRINT_TOKEN, PRINT_MEMBERS, inside_class) {
360 out.push_str(&replacement);
361 index += PRINT_TOKEN.len();
362 continue;
363 }
364 let ch = chars[index];
365 match ch {
366 '\\' if index + 1 < chars.len() => {
367 out.push('\\');
368 out.push(chars[index + 1]);
369 index += 2;
370 continue;
371 }
372 '[' => inside_class = true,
373 ']' => inside_class = false,
374 _ => {}
375 }
376 out.push(ch);
377 index += 1;
378 }
379 out
380}
381
382fn posix_expansion(chars: &[char], index: usize, token: &[char], members: &str, inside_class: bool) -> Option<String> {
386 if !chars[index..].starts_with(token) {
387 return None;
388 }
389 Some(if inside_class { members.to_string() } else { format!("[{members}]") })
390}
391
392const UNICODE_OTHER_COMPLEMENT: &[char] = &['[', '^', '\\', 'p', '{', 'C', '}', ']'];
393const UNICODE_OTHER_LOOKAHEAD: &str = r"(?:(?!\p{C})(?s:.))";
394
395fn rewrite_unicode_other_complement(pattern: &str) -> String {
400 let chars: Vec<char> = pattern.chars().collect();
401 let mut out = String::with_capacity(pattern.len());
402 let mut index = 0;
403 let mut inside_class = false;
404 while index < chars.len() {
405 if chars[index] == '\\' && index + 1 < chars.len() {
406 out.push(chars[index]);
407 out.push(chars[index + 1]);
408 index += 2;
409 continue;
410 }
411 if !inside_class && chars[index..].starts_with(UNICODE_OTHER_COMPLEMENT) {
412 out.push_str(UNICODE_OTHER_LOOKAHEAD);
413 index += UNICODE_OTHER_COMPLEMENT.len();
414 continue;
415 }
416 match chars[index] {
417 '[' => inside_class = true,
418 ']' => inside_class = false,
419 _ => {}
420 }
421 out.push(chars[index]);
422 index += 1;
423 }
424 out
425}
426
427fn repair_char_classes(pattern: &str) -> String {
432 let chars: Vec<char> = pattern.chars().collect();
433 let mut out = String::with_capacity(pattern.len() + 8);
434 let mut index = 0;
435 let mut inside_class = false;
436 let mut prev_was_shorthand = false;
437 while index < chars.len() {
438 let ch = chars[index];
439 if ch == '\\' && index + 1 < chars.len() {
440 let next = chars[index + 1];
441 out.push('\\');
442 out.push(next);
443 if (next == 'p' || next == 'P') && index + 2 < chars.len() && chars[index + 2] == '{' {
444 let mut end = index + 2;
445 while end < chars.len() && chars[end] != '}' {
446 out.push(chars[end]);
447 end += 1;
448 }
449 if end < chars.len() {
450 out.push('}');
451 }
452 index = end + 1;
453 prev_was_shorthand = inside_class;
454 continue;
455 }
456 prev_was_shorthand = inside_class && matches!(next, 'w' | 'd' | 's' | 'W' | 'D' | 'S');
457 index += 2;
458 continue;
459 }
460 if ch == '[' {
461 if inside_class {
462 out.push_str(r"\[");
463 index += 1;
464 prev_was_shorthand = false;
465 continue;
466 }
467 inside_class = true;
468 prev_was_shorthand = false;
469 out.push('[');
470 index += 1;
471 if index < chars.len() && chars[index] == '^' {
472 out.push('^');
473 index += 1;
474 }
475 if index < chars.len() && chars[index] == ']' {
476 out.push(']');
477 index += 1;
478 }
479 continue;
480 }
481 if ch == ']' && inside_class {
482 inside_class = false;
483 out.push(']');
484 index += 1;
485 prev_was_shorthand = false;
486 continue;
487 }
488 if ch == '-' && inside_class && prev_was_shorthand {
489 out.push_str(r"\-");
490 index += 1;
491 prev_was_shorthand = false;
492 continue;
493 }
494 out.push(ch);
495 prev_was_shorthand = false;
496 index += 1;
497 }
498 out
499}
500
501#[cfg(test)]
502mod tests {
503 use super::*;
504
505 #[test]
506 fn plain_pattern_compiles_on_fast_engine() {
507 let compiled = compile(r"^ami-[0-9a-f]{8,17}$").expect("plain pattern compiles");
508 assert!(matches!(*compiled, CompiledPattern::Fast(_)));
509 assert!(compiled.is_match("ami-0123abcd"));
510 assert!(!compiled.is_match("vpc-0123abcd"));
511 }
512
513 #[test]
514 fn compile_is_memoized_and_returns_the_same_instance() {
515 let first = compile(r"^cache-test-[0-9]+$").expect("compiles");
516 let second = compile(r"^cache-test-[0-9]+$").expect("compiles");
517 assert!(Arc::ptr_eq(&first, &second), "repeated compile must reuse the cached instance");
518 }
519
520 #[test]
521 fn print_class_matches_format_and_private_use_characters() {
522 let compiled = compile(r"^\p{Print}+$").expect("\\p{Print} compiles");
525 assert!(compiled.is_match("normal text"));
526 assert!(compiled.is_match("zero\u{200B}width"), "U+200B (Cf) must match \\p{{Print}}");
527 assert!(compiled.is_match("private\u{E000}use"), "U+E000 (Co) must match \\p{{Print}}");
528 }
529
530 #[test]
531 fn negative_lookahead_compiles_and_matches_like_pcre() {
532 let compiled = compile(r"^(?!aws:).+$").expect("lookahead pattern compiles");
533 assert!(matches!(*compiled, CompiledPattern::Fancy(_)));
534 assert!(compiled.is_match("my-tag"));
535 assert!(!compiled.is_match("aws:reserved"));
536 }
537
538 #[test]
539 fn uppercase_z_anchor_is_normalized_to_lowercase() {
540 let compiled = compile(r"\A[0-9a-fA-F]+\Z").expect("\\A..\\Z pattern compiles");
541 assert!(compiled.is_match("abc123"));
542 assert!(!compiled.is_match("xyz"));
543 }
544
545 #[test]
546 fn size_limit_pattern_compiles_without_rewrite() {
547 let compiled = compile(r"^[\w.-]{1,255}$").expect("size-limit pattern compiles");
548 assert!(matches!(*compiled, CompiledPattern::Fast(_)));
549 assert!(compiled.is_match("valid.name-1"));
550 }
551
552 #[test]
553 fn bounded_dot_pattern_uses_other_normalizations_before_the_compact_fallback() {
554 let compiled = compile(r"\A.{1,64}\Z").expect("normalized bounded dot pattern compiles");
555 assert!(matches!(*compiled, CompiledPattern::Fast(_)));
556 assert!(!compiled.is_match("line\n"));
557 assert_eq!(normalize(r"^(?=.{1,5})+abc$"), r"^(?=.{1,5})abc$");
558 }
559
560 #[test]
561 fn large_bounded_dot_repetition_preserves_length_anchors_and_dot_semantics() {
562 const MAXIMUM_LENGTH: usize = 65_535;
563
564 let compiled = compile(r"^.{1,65535}$").expect("large bounded dot repetition compiles");
565 let CompiledPattern::Fancy(regex) = compiled.as_ref() else {
566 panic!("large bounded dot repetition must use the compact backtracking representation");
567 };
568 assert!(matches!(regex.is_match("é"), Ok(true)), "one Unicode scalar must count as one character");
569 assert!(matches!(regex.is_match(""), Ok(false)), "the lower length boundary must be enforced");
570 assert!(
571 matches!(regex.is_match("line\nbreak"), Ok(false)),
572 "the compact atom must preserve ordinary dot behavior"
573 );
574 assert!(
575 matches!(regex.is_match("line\n"), Ok(false)),
576 "the compact representation must preserve the engine's anchor behavior"
577 );
578
579 let maximum_length = "a".repeat(MAXIMUM_LENGTH);
580 let above_maximum_length = "a".repeat(MAXIMUM_LENGTH + 1);
581 assert!(matches!(regex.is_match(&maximum_length), Ok(true)), "the upper length boundary must be accepted");
582 assert!(
583 matches!(regex.is_match(&above_maximum_length), Ok(false)),
584 "values above the upper boundary must be rejected without exhausting the backtracking budget"
585 );
586
587 let dot_all = compile(r"(?s:^.{1,65535}$)").expect("dot-all bounded repetition compiles");
588 let dot_all_matches_line_feed = match dot_all.as_ref() {
589 CompiledPattern::Fast(regex) => regex.is_match("\n"),
590 CompiledPattern::Fancy(regex) => matches!(regex.is_match("\n"), Ok(true)),
591 };
592 assert!(dot_all_matches_line_feed, "the compiled pattern must preserve surrounding dot-all behavior");
593 }
594
595 #[test]
596 fn bounded_dot_rewrite_ignores_escaped_dots_and_character_classes() {
597 assert_eq!(
598 rewrite_bounded_dot_repetitions(r"^.{1,65535}\.{1,65535}[.]{1,65535}$"),
599 r"^(?:(?=.)(?s:.)){1,65535}\.{1,65535}[.]{1,65535}$"
600 );
601 for character_class_pattern in [r"^[]a.{1,2}]$", r"^[^]a.{1,2}]$"] {
602 assert_eq!(rewrite_bounded_dot_repetitions(character_class_pattern), character_class_pattern);
603 }
604
605 let adjacent_broad_repetitions = r"^.{1,30000}x.{1,30000}$";
606 assert_eq!(
607 rewrite_bounded_dot_repetitions(adjacent_broad_repetitions),
608 adjacent_broad_repetitions,
609 "the fallback must not admit ambiguous adjacent broad repetitions"
610 );
611 }
612
613 #[test]
614 fn bounded_unicode_other_complement_preserves_category_and_length_constraints() {
615 let compiled = compile(r"^[^\p{C}]{1,2048}$").expect("bounded Unicode category pattern compiles");
616
617 assert!(compiled.is_match("cafe\u{301} 123 ☃!"));
618 assert!(!compiled.is_match(""), "the lower length boundary must be enforced");
619 for (category, value) in [
620 ("Control", "line\nbreak"),
621 ("Format", "zero\u{200B}width"),
622 ("Private Use", "private\u{E000}use"),
623 ("Unassigned", "unassigned\u{0378}"),
624 ] {
625 assert!(!compiled.is_match(value), "Unicode {category} characters must be rejected");
626 }
627
628 let maximum_length = "a".repeat(2048);
629 let above_maximum_length = "a".repeat(2049);
630 assert!(compiled.is_match(&maximum_length), "the upper length boundary must be accepted");
631 assert!(!compiled.is_match(&above_maximum_length), "values above the upper length boundary must be rejected");
632 }
633
634 #[test]
635 fn print_class_still_matches_ordinary_text() {
636 let compiled = compile(r"^\p{Print}+$").expect("\\p{Print} pattern compiles");
637 for value in ["abc", "Hello World", "aws:reserved", "café", "a.b.c/x"] {
638 assert!(compiled.is_match(value), "\\p{{Print}} must match {value:?}");
639 }
640 assert!(!compiled.is_match(""));
641 }
642
643 #[test]
644 fn graph_class_inside_existing_class_compiles() {
645 let compiled = compile(r"^[\p{Graph}\x20]*$").expect("\\p{Graph} in class compiles");
646 assert!(compiled.is_match("visible text"));
647 }
648
649 #[test]
650 fn surrogate_pair_range_admits_astral_characters() {
651 let compiled = compile(r"^[ --�𐀀-\r\n\t]*$").expect("surrogate pattern compiles");
655 assert!(compiled.is_match("normal text"));
656 assert!(
657 compiled.is_match("emoji \u{1F600} and \u{10FFFF}"),
658 "astral characters must match the surrogate-pair range"
659 );
660 }
661
662 #[test]
663 fn low_then_high_surrogate_range_still_compiles() {
664 let compiled =
667 compile(r"^[ --�\uD800\uDBFF-\uDC00\uDFFF\t]*$").expect("reordered surrogate pattern compiles");
668 assert!(compiled.is_match("normal text"));
669 assert!(compiled.is_match("astral \u{1F4A9}"), "astral characters must match regardless of surrogate ordering");
670 }
671
672 #[test]
673 fn lone_surrogate_escape_outside_class_is_clamped_not_inverted() {
674 assert!(is_service_valid(r"^\uD800$"));
677 }
678
679 #[test]
680 fn hyphen_after_shorthand_is_treated_as_literal() {
681 let compiled = compile(r"[\w-.~]+").expect("hyphen-range pattern compiles");
682 assert!(compiled.is_match("a-b.c"));
683 }
684
685 #[test]
686 fn genuinely_malformed_pattern_is_not_service_valid() {
687 assert!(!is_service_valid(r"^(unbalanced"));
688 assert!(!is_service_valid(r"a{2,1}"));
689 assert!(!is_service_valid(r"^.{2,1}$"));
690 }
691
692 #[test]
693 fn pcre_flavored_patterns_are_service_valid() {
694 assert!(is_service_valid(r"^(?!aws:).+$"));
695 assert!(is_service_valid(r"\A[0-9]+\Z"));
696 assert!(is_service_valid(r"^\p{Print}+$"));
697 assert!(is_service_valid(r"^[\w.-]{1,255}$"));
698 }
699
700 #[test]
701 fn normalize_preserves_plain_pattern_unchanged() {
702 assert_eq!(normalize(r"^[a-z0-9]+$"), r"^[a-z0-9]+$");
703 }
704
705 #[test]
706 fn anchor_allowed_pattern_adds_missing_anchors() {
707 assert_eq!(anchor_allowed_pattern("[a-z]+"), "^[a-z]+$");
708 assert_eq!(anchor_allowed_pattern("^[a-z]+"), "^[a-z]+$");
709 assert_eq!(anchor_allowed_pattern("[a-z]+$"), "^[a-z]+$");
710 assert_eq!(anchor_allowed_pattern("^[a-z]+$"), "^[a-z]+$");
711 }
712
713 #[test]
714 fn default_matches_pattern_scalar() {
715 assert_eq!(default_matches_pattern(r"[0-9]+", "123", false), Some(true));
716 assert_eq!(default_matches_pattern(r"[0-9]+", "12a", false), Some(false));
717 }
718
719 #[test]
720 fn default_matches_pattern_honors_pcre_syntax() {
721 assert_eq!(default_matches_pattern(r"\A[0-9a-fA-F]+\Z", "abc123", false), Some(true));
723 }
724
725 #[test]
726 fn default_matches_pattern_comma_delimited_checks_each_element() {
727 assert_eq!(default_matches_pattern(r"[a-z]+", "abc, def ,ghi", true), Some(true));
728 assert_eq!(default_matches_pattern(r"[a-z]+", "abc, DEF", true), Some(false));
729 }
730
731 #[test]
732 fn default_matches_pattern_returns_none_for_uncompilable() {
733 assert_eq!(default_matches_pattern(r"^(unbalanced", "x", false), None);
734 }
735}