1use std::cell::RefCell;
7use std::fmt;
8use std::ops::{Index, Range};
9use std::panic::AssertUnwindSafe;
10use std::str::FromStr;
11use std::sync::Arc;
12#[cfg(feature = "dfa-prefilter")]
13use std::sync::atomic::{AtomicBool, AtomicU32, Ordering};
14use std::time::Duration;
15
16#[cfg(feature = "dfa-prefilter")]
17use crate::dfa_prefilter::SingleSeek;
18use crate::encodings::utf8::ONIG_ENCODING_UTF8;
19use crate::error::RegexError;
20use crate::oniguruma::*;
21use crate::regcomp::{AcceptSeek, ScannerSeek, onig_new_for_single_seek};
22use crate::regexec::{
23 MatchArg, SearchLimits, cache_msa, onig_name_to_backref_number, onig_search,
24 onig_search_bounds, onig_search_captures, pure_literal_first, release_oversized_stack,
25 search_in_range, search_in_range_inner, take_cached_msa,
26};
27use crate::regint::RegexType;
28use crate::regparse_types::NameTable;
29use crate::regsyntax::{
30 OnigSyntaxASIS, OnigSyntaxEmacs, OnigSyntaxGnuRegex, OnigSyntaxGrep, OnigSyntaxJava,
31 OnigSyntaxOniguruma, OnigSyntaxPerl, OnigSyntaxPerl_NG, OnigSyntaxPosixBasic,
32 OnigSyntaxPosixExtended, OnigSyntaxPython, OnigSyntaxRuby,
33};
34
35thread_local! {
36 static CACHED_REGION: RefCell<Option<OnigRegion>> = const { RefCell::new(None) };
40}
41
42fn take_cached_region() -> OnigRegion {
43 CACHED_REGION
44 .try_with(|cached| cached.borrow_mut().take())
45 .ok()
46 .flatten()
47 .unwrap_or_default()
48}
49
50fn cache_region(region: OnigRegion) {
51 let _ = CACHED_REGION.try_with(|cached| {
54 let mut cached = cached.borrow_mut();
55 let region_capacity = region.beg.capacity() + region.end.capacity();
56 let should_replace = match cached.as_ref() {
57 Some(current) => region_capacity > current.beg.capacity() + current.end.capacity(),
58 None => true,
59 };
60 if should_replace {
61 *cached = Some(region);
62 }
63 });
64}
65
66#[track_caller]
69fn check_str_start(text: &str, start: usize) {
70 check_bytes_start(text.as_bytes(), start);
71 assert!(
72 text.is_char_boundary(start),
73 "start {start} is not a char boundary of the text"
74 );
75}
76
77#[track_caller]
79fn check_bytes_start(text: &[u8], start: usize) {
80 assert!(
81 start <= text.len(),
82 "start {start} is past the end of the text ({} bytes)",
83 text.len()
84 );
85}
86
87fn timeout_to_millis(timeout: Duration) -> u64 {
88 if timeout.is_zero() {
89 return 0;
90 }
91
92 let millis = timeout.as_millis();
93 let millis = millis + u128::from(!timeout.subsec_nanos().is_multiple_of(1_000_000));
94 millis.clamp(1, u64::MAX as u128) as u64
95}
96
97#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
122pub struct SearchOptions {
123 timeout: Option<Duration>,
124 retry_limit_in_match: Option<u64>,
125 retry_limit_in_search: Option<u64>,
126 match_stack_limit: Option<u32>,
127}
128
129impl SearchOptions {
130 pub const fn new() -> Self {
132 SearchOptions {
133 timeout: None,
134 retry_limit_in_match: None,
135 retry_limit_in_search: None,
136 match_stack_limit: None,
137 }
138 }
139
140 pub const fn timeout(mut self, timeout: Duration) -> Self {
154 self.timeout = Some(timeout);
155 self
156 }
157
158 pub const fn retry_limit_in_match(mut self, limit: u64) -> Self {
161 self.retry_limit_in_match = Some(limit);
162 self
163 }
164
165 pub const fn retry_limit_in_search(mut self, limit: u64) -> Self {
168 self.retry_limit_in_search = Some(limit);
169 self
170 }
171
172 pub const fn match_stack_limit(mut self, limit: u32) -> Self {
177 self.match_stack_limit = Some(limit);
178 self
179 }
180
181 #[inline]
184 fn limits(self) -> Option<SearchLimits> {
185 if self == Self::new() {
186 return None;
187 }
188 let process_wide = SearchLimits::process_wide();
189 Some(SearchLimits {
190 match_stack_limit: self
191 .match_stack_limit
192 .unwrap_or(process_wide.match_stack_limit),
193 retry_limit_in_match: self
194 .retry_limit_in_match
195 .unwrap_or(process_wide.retry_limit_in_match),
196 retry_limit_in_search: self
197 .retry_limit_in_search
198 .unwrap_or(process_wide.retry_limit_in_search),
199 time_limit: self
200 .timeout
201 .map_or(process_wide.time_limit, timeout_to_millis),
202 progress_callout: process_wide.progress_callout,
203 retraction_callout: process_wide.retraction_callout,
204 callout_user_data: process_wide.callout_user_data,
205 })
206 }
207}
208
209struct Compiled {
214 program: AssertUnwindSafe<RegexType>,
217 pattern: Box<[u8]>,
218 #[cfg(feature = "dfa-prefilter")]
223 seek: AssertUnwindSafe<LazySeek>,
224 #[cfg(feature = "dfa-prefilter")]
228 seek_gate: SeekGate,
229}
230
231#[cfg(feature = "dfa-prefilter")]
238struct LazySeek {
239 codes: std::sync::Mutex<Option<(crate::dfa_prefilter::Seek, AcceptSeek)>>,
241 automaton: std::sync::OnceLock<Option<Box<SingleSeek>>>,
243}
244
245#[cfg(feature = "dfa-prefilter")]
246impl LazySeek {
247 fn new(codes: Option<(crate::dfa_prefilter::Seek, AcceptSeek)>) -> LazySeek {
249 let automaton = std::sync::OnceLock::new();
250 if codes.is_none() {
251 let _ = automaton.set(None);
252 }
253 LazySeek {
254 codes: std::sync::Mutex::new(codes),
255 automaton,
256 }
257 }
258
259 #[inline]
262 fn get(&self) -> Option<&SingleSeek> {
263 self.automaton
264 .get_or_init(|| {
265 let codes = self
266 .codes
267 .lock()
268 .unwrap_or_else(std::sync::PoisonError::into_inner)
269 .take();
270 codes
271 .and_then(|(seek, accept)| SingleSeek::build(&seek, accept.as_ref()))
272 .map(Box::new)
273 })
274 .as_deref()
275 }
276
277 #[inline]
280 fn may_exist(&self) -> bool {
281 self.automaton.get().is_none_or(Option::is_some)
282 }
283
284 #[cfg(test)]
286 fn built(&self) -> bool {
287 self.automaton.get().is_some_and(Option::is_some)
288 }
289}
290
291#[cfg(feature = "dfa-prefilter")]
294const SEEK_GATE_PROBE_INTERVAL: u32 = 32;
295
296#[cfg(feature = "dfa-prefilter")]
297thread_local! {
298 static GATE_PROBE: std::cell::Cell<u32> = const { std::cell::Cell::new(0x9E37_79B9) };
301}
302
303#[cfg(feature = "dfa-prefilter")]
313fn probe_closed_gate() -> bool {
314 GATE_PROBE.with(|state| {
315 let mut x = state.get();
316 x ^= x << 13;
317 x ^= x >> 17;
318 x ^= x << 5;
319 state.set(x);
320 (u64::from(x) * u64::from(SEEK_GATE_PROBE_INTERVAL)) >> 32 == 0
322 })
323}
324
325#[cfg(feature = "dfa-prefilter")]
328const SEEK_CONSULT_COST_IR: u32 = 300;
329
330#[cfg(feature = "dfa-prefilter")]
335const SEEK_REJECT_SAVING_IR: u32 = 800;
336
337#[cfg(feature = "dfa-prefilter")]
340const SEEK_GATE_OPEN_PERMILLE: u32 = SEEK_CONSULT_COST_IR * 1000 / SEEK_REJECT_SAVING_IR;
341
342#[cfg(feature = "dfa-prefilter")]
346const SEEK_GATE_CLOSE_PERMILLE: u32 = SEEK_GATE_OPEN_PERMILLE * 2 / 3;
347
348#[cfg(feature = "dfa-prefilter")]
366struct SeekGate {
367 rate: AtomicU32,
369 open: AtomicBool,
371 #[cfg(test)]
374 forced: std::sync::atomic::AtomicU8,
375}
376
377#[cfg(feature = "dfa-prefilter")]
378impl SeekGate {
379 const fn new() -> Self {
380 SeekGate {
381 rate: AtomicU32::new(400),
382 open: AtomicBool::new(true),
383 #[cfg(test)]
384 forced: std::sync::atomic::AtomicU8::new(0),
385 }
386 }
387
388 #[inline]
390 fn consults(&self) -> bool {
391 #[cfg(test)]
392 match self.forced.load(Ordering::Relaxed) {
393 1 => return true,
394 2 => return false,
395 _ => {}
396 }
397 self.open.load(Ordering::Relaxed) || probe_closed_gate()
398 }
399
400 fn record(&self, rejected: bool) {
403 let sample = if rejected { 1000 } else { 0 };
404 let previous = self.rate.load(Ordering::Relaxed);
405 let rate = (previous * 7 + sample) / 8;
406 if rate != previous {
410 self.rate.store(rate, Ordering::Relaxed);
411 }
412 let open = self.open.load(Ordering::Relaxed);
413 let threshold = if open {
414 SEEK_GATE_CLOSE_PERMILLE
415 } else {
416 SEEK_GATE_OPEN_PERMILLE
417 };
418 if (rate >= threshold) != open {
419 self.open.store(!open, Ordering::Relaxed);
420 }
421 }
422
423 #[cfg(test)]
426 fn state(&self) -> (bool, u32) {
427 (
428 self.open.load(Ordering::Relaxed),
429 self.rate.load(Ordering::Relaxed),
430 )
431 }
432}
433
434const DFA_DEFAULT: bool = cfg!(feature = "dfa-prefilter");
437
438#[cfg(feature = "dfa-prefilter")]
441fn is_char_boundary(text: &[u8], start: usize) -> bool {
442 text.get(start).is_none_or(|&byte| (byte as i8) >= -0x40)
443}
444
445#[derive(Clone)]
485pub struct Regex {
486 inner: Arc<Compiled>,
487}
488
489impl Regex {
490 pub fn new(pattern: &str) -> Result<Regex, RegexError> {
492 Self::new_bytes(pattern.as_bytes())
493 }
494
495 pub fn new_bytes(pattern: &[u8]) -> Result<Regex, RegexError> {
502 let (program, seek, accept) = onig_new_for_single_seek(
503 pattern,
504 ONIG_OPTION_NONE,
505 &ONIG_ENCODING_UTF8,
506 &OnigSyntaxOniguruma,
507 false,
508 DFA_DEFAULT,
509 )?;
510 Ok(Self::from_parts(program, pattern.into(), seek, accept))
511 }
512
513 #[cfg_attr(not(feature = "dfa-prefilter"), allow(unused_variables))]
517 fn from_parts(
518 program: RegexType,
519 pattern: Box<[u8]>,
520 seek: ScannerSeek,
521 accept: AcceptSeek,
522 ) -> Regex {
523 #[cfg(feature = "dfa-prefilter")]
527 let seek = AssertUnwindSafe(LazySeek::new(if program.pure_literal {
528 None
529 } else {
530 seek.map(|seek| (seek, accept))
531 }));
532 Regex {
533 inner: Arc::new(Compiled {
534 program: AssertUnwindSafe(program),
535 pattern,
536 #[cfg(feature = "dfa-prefilter")]
537 seek,
538 #[cfg(feature = "dfa-prefilter")]
539 seek_gate: SeekGate::new(),
540 }),
541 }
542 }
543
544 fn program(&self) -> &RegexType {
546 &self.inner.program
547 }
548
549 #[cfg(feature = "dfa-prefilter")]
575 #[inline]
576 fn seek_answer(&self, text: &[u8], start: usize, mut utf8: bool, accept: bool) -> Option<bool> {
577 let seek = self.inner.seek.get()?;
578 if accept && seek.has_accept() {
579 if !(utf8 || std::str::from_utf8(text).is_ok()) || !is_char_boundary(text, start) {
580 return None;
581 }
582 utf8 = true;
583 if let Some(answer) = seek.accept_answer(text, start) {
584 #[cfg(feature = "prefilter-self-check")]
585 self.check_accept_answer(text, start, answer);
586 return Some(answer);
587 }
588 }
589 let found = seek.search(text, start)?;
590 if found && !(accept && seek.exact()) {
591 return None;
592 }
593 if !(utf8 || std::str::from_utf8(text).is_ok()) || !is_char_boundary(text, start) {
594 return None;
595 }
596 Some(found)
597 }
598
599 #[cfg(feature = "prefilter-self-check")]
603 fn check_accept_answer(&self, text: &[u8], start: usize, answer: bool) {
604 let (result, _) = onig_search(
605 self.program(),
606 text,
607 text.len(),
608 start,
609 text.len(),
610 None,
611 ONIG_OPTION_NONE,
612 );
613 if result >= 0 || result == ONIG_MISMATCH {
614 assert_eq!(
615 result >= 0,
616 answer,
617 "the accept seek of {:?} answers {answer} on {:?} from {start}",
618 String::from_utf8_lossy(self.as_bytes()),
619 String::from_utf8_lossy(text),
620 );
621 }
622 }
623
624 #[cfg(not(feature = "dfa-prefilter"))]
625 #[inline]
626 fn seek_answer(&self, text: &[u8], start: usize, utf8: bool, accept: bool) -> Option<bool> {
627 let _ = (text, start, utf8, accept);
628 None
629 }
630
631 #[cfg(feature = "dfa-prefilter")]
637 #[inline]
638 fn seek_rejects_search(&self, text: &[u8], start: usize, utf8: bool) -> bool {
639 if !self.inner.seek.may_exist() || !self.inner.seek_gate.consults() {
640 return false;
641 }
642 let rejected = self.seek_answer(text, start, utf8, false) == Some(false);
643 self.inner.seek_gate.record(rejected);
644 rejected
645 }
646
647 #[cfg(not(feature = "dfa-prefilter"))]
648 #[inline(always)]
649 fn seek_rejects_search(&self, text: &[u8], start: usize, utf8: bool) -> bool {
650 let _ = (text, start, utf8);
651 false
652 }
653
654 #[cfg(all(test, feature = "dfa-prefilter"))]
656 pub(crate) fn seek_present(&self) -> bool {
657 self.inner.seek.get().is_some()
658 }
659
660 #[cfg(all(test, feature = "dfa-prefilter"))]
662 pub(crate) fn seek_built(&self) -> bool {
663 self.inner.seek.built()
664 }
665
666 #[cfg(all(test, feature = "dfa-prefilter"))]
668 pub(crate) fn seek_exact(&self) -> Option<bool> {
669 self.inner.seek.get().map(SingleSeek::exact)
670 }
671
672 #[cfg(all(test, feature = "dfa-prefilter"))]
675 pub(crate) fn seek_decides_is_match(&self, text: &str) -> Option<bool> {
676 self.seek_answer(text.as_bytes(), 0, true, true)
677 }
678
679 #[cfg(all(test, feature = "dfa-prefilter"))]
681 pub(crate) fn accept_seek_present(&self) -> bool {
682 self.inner.seek.get().is_some_and(SingleSeek::has_accept)
683 }
684
685 #[cfg(all(test, feature = "dfa-prefilter"))]
688 pub(crate) fn accept_answer(&self, text: &str, start: usize) -> Option<bool> {
689 self.inner.seek.get()?.accept_answer(text.as_bytes(), start)
690 }
691
692 #[cfg(all(test, feature = "dfa-prefilter"))]
695 pub(crate) fn seek_memory_usage(&self) -> usize {
696 self.inner.seek.get().map_or(0, SingleSeek::memory_usage)
697 }
698
699 #[cfg(all(test, feature = "dfa-prefilter"))]
702 pub(crate) fn seek_gate_state(&self) -> (bool, u32) {
703 self.inner.seek_gate.state()
704 }
705
706 #[cfg(all(test, feature = "dfa-prefilter"))]
709 pub(crate) fn seek_gate_force(&self, mode: Option<bool>) {
710 let value = match mode {
711 None => 0,
712 Some(true) => 1,
713 Some(false) => 2,
714 };
715 self.inner
716 .seek_gate
717 .forced
718 .store(value, std::sync::atomic::Ordering::Relaxed);
719 }
720
721 pub fn as_str(&self) -> Option<&str> {
733 std::str::from_utf8(&self.inner.pattern).ok()
734 }
735
736 pub fn as_bytes(&self) -> &[u8] {
745 &self.inner.pattern
746 }
747
748 pub fn capture_names(&self) -> CaptureNames<'_> {
766 CaptureNames {
767 table: self.program().name_table.as_ref(),
768 next_group: 0,
769 end_group: self.captures_len() as i32 + 1,
770 }
771 }
772
773 pub fn builder(pattern: &str) -> RegexBuilder {
775 RegexBuilder::new(pattern)
776 }
777
778 pub fn find<'t>(&self, text: &'t str) -> Option<Match<'t>> {
784 self.find_from(Subject::Str(text), 0)
785 }
786
787 pub fn find_bytes<'t>(&self, text: &'t [u8]) -> Option<Match<'t>> {
789 self.find_from(Subject::Bytes(text), 0)
790 }
791
792 #[inline]
826 pub fn find_at<'t>(&self, text: &'t str, start: usize) -> Option<Match<'t>> {
827 check_str_start(text, start);
828 self.find_from(Subject::Str(text), start)
829 }
830
831 pub fn find_bytes_at<'t>(&self, text: &'t [u8], start: usize) -> Option<Match<'t>> {
841 check_bytes_start(text, start);
842 self.find_from(Subject::Bytes(text), start)
843 }
844
845 #[inline]
848 fn find_from<'t>(&self, subject: Subject<'t>, start: usize) -> Option<Match<'t>> {
849 if self.seek_rejects_search(subject.bytes(), start, subject.is_str()) {
853 return None;
854 }
855 self.find_from_limited(subject, start, None).ok().flatten()
857 }
858
859 #[inline]
862 fn find_from_limited<'t>(
863 &self,
864 subject: Subject<'t>,
865 start: usize,
866 limits: Option<SearchLimits>,
867 ) -> Result<Option<Match<'t>>, i32> {
868 let text = subject.bytes();
869 if !self.program().keep_moves_match_start
873 && !self.program().options.contains(ONIG_OPTION_FIND_LONGEST)
874 {
875 if let Some(found) = pure_literal_first(
878 self.program(),
879 text,
880 text.len(),
881 start,
882 text.len(),
883 text.len(),
884 self.program().options,
885 ) {
886 return Ok(found.map(|(match_start, match_end)| Match {
887 subject,
888 start: match_start,
889 end: match_end,
890 }));
891 }
892 let mut msa = take_cached_msa(self.program(), ONIG_OPTION_NONE, None, start, limits);
893 let found = onig_search_bounds(self.program(), text, start, &mut msa);
894 cache_msa(msa);
895 let Some((match_start, match_end)) = found? else {
896 return Ok(None);
897 };
898 return Ok((match_end <= text.len()).then_some(Match {
899 subject,
900 start: match_start,
901 end: match_end,
902 }));
903 }
904 let len = text.len();
905 let (result, region) = search_in_range(
906 self.program(),
907 text,
908 len,
909 start,
910 len,
911 len,
912 Some(take_cached_region()),
913 ONIG_OPTION_NONE,
914 limits,
915 );
916 let Some(region) = region else {
917 return Ok(None);
918 };
919 if result < 0 {
920 cache_region(region);
921 return if result == ONIG_MISMATCH {
922 Ok(None)
923 } else {
924 Err(result)
925 };
926 }
927 if region.num_regs < 1 {
928 cache_region(region);
929 return Ok(None);
930 }
931 let m = Match::from_region(subject, region.beg[0], region.end[0]);
932 cache_region(region);
933 Ok(m)
934 }
935
936 #[inline]
943 pub fn find_with<'t>(
944 &self,
945 text: &'t str,
946 options: SearchOptions,
947 ) -> Result<Option<Match<'t>>, RegexError> {
948 self.find_from_with(Subject::Str(text), 0, options)
949 }
950
951 #[inline]
957 pub fn find_bytes_with<'t>(
958 &self,
959 text: &'t [u8],
960 options: SearchOptions,
961 ) -> Result<Option<Match<'t>>, RegexError> {
962 self.find_from_with(Subject::Bytes(text), 0, options)
963 }
964
965 #[inline]
979 pub fn find_at_with<'t>(
980 &self,
981 text: &'t str,
982 start: usize,
983 options: SearchOptions,
984 ) -> Result<Option<Match<'t>>, RegexError> {
985 check_str_start(text, start);
986 self.find_from_with(Subject::Str(text), start, options)
987 }
988
989 #[inline]
1000 pub fn find_bytes_at_with<'t>(
1001 &self,
1002 text: &'t [u8],
1003 start: usize,
1004 options: SearchOptions,
1005 ) -> Result<Option<Match<'t>>, RegexError> {
1006 check_bytes_start(text, start);
1007 self.find_from_with(Subject::Bytes(text), start, options)
1008 }
1009
1010 fn find_from_with<'t>(
1011 &self,
1012 subject: Subject<'t>,
1013 start: usize,
1014 options: SearchOptions,
1015 ) -> Result<Option<Match<'t>>, RegexError> {
1016 self.find_from_limited(subject, start, options.limits())
1017 .map_err(RegexError::from)
1018 }
1019
1020 pub fn is_match(&self, text: &str) -> bool {
1027 self.is_match_from(text.as_bytes(), 0, true)
1028 }
1029
1030 pub fn is_match_bytes(&self, text: &[u8]) -> bool {
1032 self.is_match_from(text, 0, false)
1033 }
1034
1035 pub fn is_match_at(&self, text: &str, start: usize) -> bool {
1045 check_str_start(text, start);
1046 self.is_match_from(text.as_bytes(), start, true)
1047 }
1048
1049 pub fn is_match_bytes_at(&self, text: &[u8], start: usize) -> bool {
1056 check_bytes_start(text, start);
1057 self.is_match_from(text, start, false)
1058 }
1059
1060 #[inline]
1061 fn is_match_from(&self, text: &[u8], start: usize, utf8: bool) -> bool {
1062 if let Some(answer) = self.seek_answer(text, start, utf8, true) {
1063 return answer;
1064 }
1065 let (result, _) = onig_search(
1066 self.program(),
1067 text,
1068 text.len(),
1069 start,
1070 text.len(),
1071 None,
1072 ONIG_OPTION_NONE,
1073 );
1074 result >= 0
1075 }
1076
1077 #[inline]
1083 pub fn is_match_with(&self, text: &str, options: SearchOptions) -> Result<bool, RegexError> {
1084 self.is_match_bytes_with(text.as_bytes(), options)
1085 }
1086
1087 #[inline]
1093 pub fn is_match_bytes_with(
1094 &self,
1095 text: &[u8],
1096 options: SearchOptions,
1097 ) -> Result<bool, RegexError> {
1098 self.is_match_from_with(text, 0, options)
1099 }
1100
1101 #[inline]
1113 pub fn is_match_at_with(
1114 &self,
1115 text: &str,
1116 start: usize,
1117 options: SearchOptions,
1118 ) -> Result<bool, RegexError> {
1119 check_str_start(text, start);
1120 self.is_match_from_with(text.as_bytes(), start, options)
1121 }
1122
1123 #[inline]
1134 pub fn is_match_bytes_at_with(
1135 &self,
1136 text: &[u8],
1137 start: usize,
1138 options: SearchOptions,
1139 ) -> Result<bool, RegexError> {
1140 check_bytes_start(text, start);
1141 self.is_match_from_with(text, start, options)
1142 }
1143
1144 fn is_match_from_with(
1145 &self,
1146 text: &[u8],
1147 start: usize,
1148 options: SearchOptions,
1149 ) -> Result<bool, RegexError> {
1150 let len = text.len();
1151 let (result, _) = search_in_range(
1152 self.program(),
1153 text,
1154 len,
1155 start,
1156 len,
1157 len,
1158 None,
1159 ONIG_OPTION_NONE,
1160 options.limits(),
1161 );
1162 if result < 0 && result != ONIG_MISMATCH {
1163 return Err(RegexError::from(result));
1164 }
1165 Ok(result >= 0)
1166 }
1167
1168 pub fn captures<'t>(&self, text: &'t str) -> Option<Captures<'t>> {
1174 self.captures_from(Subject::Str(text), 0)
1175 }
1176
1177 pub fn captures_bytes<'t>(&self, text: &'t [u8]) -> Option<Captures<'t>> {
1179 self.captures_from(Subject::Bytes(text), 0)
1180 }
1181
1182 pub fn captures_at<'t>(&self, text: &'t str, start: usize) -> Option<Captures<'t>> {
1203 check_str_start(text, start);
1204 self.captures_from(Subject::Str(text), start)
1205 }
1206
1207 pub fn captures_bytes_at<'t>(&self, text: &'t [u8], start: usize) -> Option<Captures<'t>> {
1214 check_bytes_start(text, start);
1215 self.captures_from(Subject::Bytes(text), start)
1216 }
1217
1218 #[inline]
1221 fn captures_from<'t>(&self, subject: Subject<'t>, start: usize) -> Option<Captures<'t>> {
1222 let text = subject.bytes();
1223 if self.seek_rejects_search(text, start, subject.is_str()) {
1224 return None;
1225 }
1226 let (result, region) =
1227 onig_search_captures(self.program(), text, start, take_cached_region());
1228 let region = region?;
1229 if result < 0 {
1230 cache_region(region);
1231 return None;
1232 }
1233 Some(Captures {
1234 text: subject,
1235 region,
1236 compiled: name_lookup(&self.inner),
1237 })
1238 }
1239
1240 #[inline]
1247 pub fn captures_with<'t>(
1248 &self,
1249 text: &'t str,
1250 options: SearchOptions,
1251 ) -> Result<Option<Captures<'t>>, RegexError> {
1252 self.captures_from_with(Subject::Str(text), 0, options)
1253 }
1254
1255 #[inline]
1262 pub fn captures_bytes_with<'t>(
1263 &self,
1264 text: &'t [u8],
1265 options: SearchOptions,
1266 ) -> Result<Option<Captures<'t>>, RegexError> {
1267 self.captures_from_with(Subject::Bytes(text), 0, options)
1268 }
1269
1270 #[inline]
1282 pub fn captures_at_with<'t>(
1283 &self,
1284 text: &'t str,
1285 start: usize,
1286 options: SearchOptions,
1287 ) -> Result<Option<Captures<'t>>, RegexError> {
1288 check_str_start(text, start);
1289 self.captures_from_with(Subject::Str(text), start, options)
1290 }
1291
1292 #[inline]
1303 pub fn captures_bytes_at_with<'t>(
1304 &self,
1305 text: &'t [u8],
1306 start: usize,
1307 options: SearchOptions,
1308 ) -> Result<Option<Captures<'t>>, RegexError> {
1309 check_bytes_start(text, start);
1310 self.captures_from_with(Subject::Bytes(text), start, options)
1311 }
1312
1313 fn captures_from_with<'t>(
1314 &self,
1315 subject: Subject<'t>,
1316 start: usize,
1317 options: SearchOptions,
1318 ) -> Result<Option<Captures<'t>>, RegexError> {
1319 let text = subject.bytes();
1320 let len = text.len();
1321 let (result, region) = search_in_range(
1322 self.program(),
1323 text,
1324 len,
1325 start,
1326 len,
1327 len,
1328 Some(take_cached_region()),
1329 ONIG_OPTION_NONE,
1330 options.limits(),
1331 );
1332 let Some(region) = region else {
1333 return Ok(None);
1334 };
1335 if result < 0 {
1336 cache_region(region);
1337 return if result == ONIG_MISMATCH {
1338 Ok(None)
1339 } else {
1340 Err(RegexError::from(result))
1341 };
1342 }
1343 Ok(Some(Captures {
1344 text: subject,
1345 region,
1346 compiled: name_lookup(&self.inner),
1347 }))
1348 }
1349
1350 pub fn find_iter<'r, 't>(&'r self, text: &'t str) -> FindIter<'r, 't> {
1358 self.find_iter_limited(Subject::Str(text), None)
1359 }
1360
1361 pub fn find_iter_bytes<'r, 't>(&'r self, text: &'t [u8]) -> FindIter<'r, 't> {
1363 self.find_iter_limited(Subject::Bytes(text), None)
1364 }
1365
1366 fn find_iter_limited<'r, 't>(
1369 &'r self,
1370 subject: Subject<'t>,
1371 limits: Option<SearchLimits>,
1372 ) -> FindIter<'r, 't> {
1373 let bounds_only = !self.program().keep_moves_match_start
1377 && !self.program().options.contains(ONIG_OPTION_FIND_LONGEST);
1378 FindIter {
1379 regex: self,
1380 program: self.program(),
1381 text: subject.bytes(),
1382 subject,
1383 cursor: MatchCursor::default(),
1384 region: if bounds_only {
1385 OnigRegion::new()
1386 } else {
1387 take_cached_region()
1388 },
1389 msa: bounds_only
1390 .then(|| take_cached_msa(self.program(), ONIG_OPTION_NONE, None, 0, limits)),
1391 limits,
1392 }
1393 }
1394
1395 #[inline]
1401 pub fn find_iter_with<'r, 't>(
1402 &'r self,
1403 text: &'t str,
1404 options: SearchOptions,
1405 ) -> TryFindIter<'r, 't> {
1406 TryFindIter {
1407 iter: self.find_iter_limited(Subject::Str(text), options.limits()),
1408 finished: false,
1409 }
1410 }
1411
1412 #[inline]
1415 pub fn find_iter_bytes_with<'r, 't>(
1416 &'r self,
1417 text: &'t [u8],
1418 options: SearchOptions,
1419 ) -> TryFindIter<'r, 't> {
1420 TryFindIter {
1421 iter: self.find_iter_limited(Subject::Bytes(text), options.limits()),
1422 finished: false,
1423 }
1424 }
1425
1426 pub fn captures_iter<'r, 't>(&'r self, text: &'t str) -> CaptureMatches<'r, 't> {
1445 CaptureMatches::new(self, Subject::Str(text), None)
1446 }
1447
1448 pub fn captures_iter_bytes<'r, 't>(&'r self, text: &'t [u8]) -> CaptureMatches<'r, 't> {
1451 CaptureMatches::new(self, Subject::Bytes(text), None)
1452 }
1453
1454 #[inline]
1460 pub fn captures_iter_with<'r, 't>(
1461 &'r self,
1462 text: &'t str,
1463 options: SearchOptions,
1464 ) -> TryCaptureMatches<'r, 't> {
1465 TryCaptureMatches {
1466 iter: CaptureMatches::new(self, Subject::Str(text), options.limits()),
1467 finished: false,
1468 }
1469 }
1470
1471 #[inline]
1474 pub fn captures_iter_bytes_with<'r, 't>(
1475 &'r self,
1476 text: &'t [u8],
1477 options: SearchOptions,
1478 ) -> TryCaptureMatches<'r, 't> {
1479 TryCaptureMatches {
1480 iter: CaptureMatches::new(self, Subject::Bytes(text), options.limits()),
1481 finished: false,
1482 }
1483 }
1484
1485 pub fn captures_len(&self) -> usize {
1490 self.program().num_mem as usize
1491 }
1492
1493 pub fn backtracking_warnings(&self) -> &[crate::backtrack_lint::BacktrackWarning] {
1509 &self.program().backtrack_warnings
1510 }
1511
1512 pub fn backtracking_rewrites(&self) -> &[crate::backtrack_rewrite::BacktrackingRewrite] {
1516 &self.program().backtrack_rewrites
1517 }
1518
1519 pub fn as_raw(&self) -> &RegexType {
1521 self.program()
1522 }
1523}
1524
1525impl fmt::Debug for Regex {
1527 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1528 f.debug_struct("Regex")
1529 .field("pattern", &String::from_utf8_lossy(&self.inner.pattern))
1530 .finish_non_exhaustive()
1531 }
1532}
1533
1534impl fmt::Display for Regex {
1537 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1538 f.write_str(&String::from_utf8_lossy(&self.inner.pattern))
1539 }
1540}
1541
1542impl FromStr for Regex {
1552 type Err = RegexError;
1553
1554 fn from_str(pattern: &str) -> Result<Self, Self::Err> {
1555 Regex::new(pattern)
1556 }
1557}
1558
1559pub struct RegexBuilder {
1575 pattern: Vec<u8>,
1576 options: OnigOptionType,
1577 syntax: &'static OnigSyntaxType,
1578 reject_backtracking_risks: bool,
1579 optimize_backtracking: bool,
1580 dfa: bool,
1581}
1582
1583impl RegexBuilder {
1584 pub fn new(pattern: &str) -> Self {
1586 Self::new_bytes(pattern.as_bytes())
1587 }
1588
1589 pub fn new_bytes(pattern: &[u8]) -> Self {
1603 RegexBuilder {
1604 pattern: pattern.to_vec(),
1605 options: ONIG_OPTION_NONE,
1606 syntax: &OnigSyntaxOniguruma,
1607 reject_backtracking_risks: false,
1608 optimize_backtracking: false,
1609 dfa: DFA_DEFAULT,
1610 }
1611 }
1612
1613 pub fn case_insensitive(mut self, yes: bool) -> Self {
1615 if yes {
1616 self.options |= ONIG_OPTION_IGNORECASE;
1617 } else {
1618 self.options &= !ONIG_OPTION_IGNORECASE;
1619 }
1620 self
1621 }
1622
1623 pub fn dot_matches_newline(mut self, yes: bool) -> Self {
1625 if yes {
1626 self.options |= ONIG_OPTION_MULTILINE;
1627 } else {
1628 self.options &= !ONIG_OPTION_MULTILINE;
1629 }
1630 self
1631 }
1632
1633 pub fn multi_line_anchors(mut self, yes: bool) -> Self {
1639 if yes {
1640 self.options |= ONIG_OPTION_NEGATE_SINGLELINE;
1641 self.options &= !ONIG_OPTION_SINGLELINE;
1642 } else {
1643 self.options &= !ONIG_OPTION_NEGATE_SINGLELINE;
1644 self.options |= ONIG_OPTION_SINGLELINE;
1645 }
1646 self
1647 }
1648
1649 pub fn extended(mut self, yes: bool) -> Self {
1651 if yes {
1652 self.options |= ONIG_OPTION_EXTEND;
1653 } else {
1654 self.options &= !ONIG_OPTION_EXTEND;
1655 }
1656 self
1657 }
1658
1659 pub fn option(mut self, flag: OnigOptionType) -> Self {
1663 self.options |= flag;
1664 self
1665 }
1666
1667 pub fn clear_option(mut self, flag: OnigOptionType) -> Self {
1684 self.options &= !flag;
1685 self
1686 }
1687
1688 pub fn capture_group(mut self, yes: bool) -> Self {
1710 if yes {
1711 self.options |= ONIG_OPTION_CAPTURE_GROUP;
1712 self.options &= !ONIG_OPTION_DONT_CAPTURE_GROUP;
1713 } else {
1714 self.options &= !ONIG_OPTION_CAPTURE_GROUP;
1715 }
1716 self
1717 }
1718
1719 pub fn syntax(mut self, syntax: &'static OnigSyntaxType) -> Self {
1725 self.syntax = syntax;
1726 self
1727 }
1728
1729 pub fn syntax_mode(mut self, syntax: Syntax) -> Self {
1744 self.syntax = syntax.as_onig_syntax();
1745 self
1746 }
1747
1748 pub fn reject_backtracking_risks(mut self, yes: bool) -> Self {
1762 self.reject_backtracking_risks = yes;
1763 self
1764 }
1765
1766 pub fn optimize_backtracking(mut self, yes: bool) -> Self {
1783 self.optimize_backtracking = yes;
1784 self
1785 }
1786
1787 pub fn dfa(mut self, yes: bool) -> Self {
1836 self.dfa = yes;
1837 self
1838 }
1839
1840 pub fn build(self) -> Result<Regex, RegexError> {
1842 let (inner, seek, accept) = onig_new_for_single_seek(
1843 &self.pattern,
1844 self.options,
1845 &ONIG_ENCODING_UTF8,
1846 self.syntax,
1847 self.optimize_backtracking,
1848 self.dfa,
1849 )?;
1850 if self.reject_backtracking_risks && !inner.backtrack_warnings.is_empty() {
1851 return Err(ONIGERR_VERY_INEFFICIENT_PATTERN.into());
1852 }
1853 Ok(Regex::from_parts(
1854 inner,
1855 self.pattern.into_boxed_slice(),
1856 seek,
1857 accept,
1858 ))
1859 }
1860}
1861
1862impl fmt::Debug for RegexBuilder {
1865 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1866 const BUILT_IN: [Syntax; 12] = [
1868 Syntax::Oniguruma,
1869 Syntax::Asis,
1870 Syntax::PosixBasic,
1871 Syntax::PosixExtended,
1872 Syntax::Emacs,
1873 Syntax::Grep,
1874 Syntax::GnuRegex,
1875 Syntax::Java,
1876 Syntax::Perl,
1877 Syntax::PerlNg,
1878 Syntax::Ruby,
1879 Syntax::Python,
1880 ];
1881 let syntax = BUILT_IN
1882 .into_iter()
1883 .find(|syntax| std::ptr::eq(syntax.as_onig_syntax(), self.syntax));
1884 f.debug_struct("RegexBuilder")
1885 .field("pattern", &String::from_utf8_lossy(&self.pattern))
1886 .field("options", &self.options)
1887 .field("syntax", &syntax)
1888 .field("reject_backtracking_risks", &self.reject_backtracking_risks)
1889 .field("optimize_backtracking", &self.optimize_backtracking)
1890 .field("dfa", &self.dfa)
1891 .finish()
1892 }
1893}
1894
1895impl Regex {
1896 pub fn builder_bytes(pattern: &[u8]) -> RegexBuilder {
1907 RegexBuilder::new_bytes(pattern)
1908 }
1909}
1910
1911#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
1930#[non_exhaustive]
1931pub enum Syntax {
1932 #[default]
1934 Oniguruma,
1935 Asis,
1937 PosixBasic,
1939 PosixExtended,
1941 Emacs,
1943 Grep,
1945 GnuRegex,
1947 Java,
1949 Perl,
1951 PerlNg,
1953 Ruby,
1955 Python,
1957}
1958
1959impl Syntax {
1960 pub(crate) fn as_onig_syntax(&self) -> &'static OnigSyntaxType {
1961 match self {
1962 Self::Oniguruma => &OnigSyntaxOniguruma,
1963 Self::Asis => &OnigSyntaxASIS,
1964 Self::PosixBasic => &OnigSyntaxPosixBasic,
1965 Self::PosixExtended => &OnigSyntaxPosixExtended,
1966 Self::Emacs => &OnigSyntaxEmacs,
1967 Self::Grep => &OnigSyntaxGrep,
1968 Self::GnuRegex => &OnigSyntaxGnuRegex,
1969 Self::Java => &OnigSyntaxJava,
1970 Self::Perl => &OnigSyntaxPerl,
1971 Self::PerlNg => &OnigSyntaxPerl_NG,
1972 Self::Ruby => &OnigSyntaxRuby,
1973 Self::Python => &OnigSyntaxPython,
1974 }
1975 }
1976}
1977
1978#[derive(Clone, Copy)]
1985enum Subject<'t> {
1986 Str(&'t str),
1987 Bytes(&'t [u8]),
1988}
1989
1990impl<'t> Subject<'t> {
1991 #[inline(always)]
1992 fn bytes(self) -> &'t [u8] {
1993 match self {
1994 Subject::Str(text) => text.as_bytes(),
1995 Subject::Bytes(text) => text,
1996 }
1997 }
1998
1999 #[inline(always)]
2001 fn is_str(self) -> bool {
2002 matches!(self, Subject::Str(_))
2003 }
2004}
2005
2006#[derive(Clone, Copy)]
2010pub struct Match<'t> {
2011 subject: Subject<'t>,
2012 start: usize,
2013 end: usize,
2014}
2015
2016impl fmt::Debug for Match<'_> {
2017 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2018 f.debug_struct("Match")
2020 .field("text", &self.subject.bytes())
2021 .field("start", &self.start)
2022 .field("end", &self.end)
2023 .finish()
2024 }
2025}
2026
2027impl<'t> Match<'t> {
2028 fn from_region(subject: Subject<'t>, beg: i32, end: i32) -> Option<Self> {
2034 let start = usize::try_from(beg).ok()?;
2035 let end = usize::try_from(end).ok()?;
2036 (start <= end && end <= subject.bytes().len()).then_some(Match {
2037 subject,
2038 start,
2039 end,
2040 })
2041 }
2042
2043 pub fn start(&self) -> usize {
2045 self.start
2046 }
2047
2048 pub fn end(&self) -> usize {
2050 self.end
2051 }
2052
2053 pub fn range(&self) -> Range<usize> {
2055 self.start..self.end
2056 }
2057
2058 pub fn as_bytes(&self) -> &'t [u8] {
2060 &self.subject.bytes()[self.start..self.end]
2061 }
2062
2063 pub fn as_str(&self) -> &'t str {
2075 match self.subject {
2076 Subject::Str(text) => text.get(self.start..self.end),
2079 Subject::Bytes(text) => std::str::from_utf8(&text[self.start..self.end]).ok(),
2080 }
2081 .expect("match is not valid UTF-8")
2082 }
2083
2084 pub fn len(&self) -> usize {
2086 self.end - self.start
2087 }
2088
2089 pub fn is_empty(&self) -> bool {
2091 self.start == self.end
2092 }
2093}
2094
2095pub struct Captures<'t> {
2119 text: Subject<'t>,
2120 region: OnigRegion,
2121 compiled: Option<Arc<Compiled>>,
2126}
2127
2128fn name_lookup(inner: &Arc<Compiled>) -> Option<Arc<Compiled>> {
2138 (crate::regexec::onig_number_of_names(&inner.program) > 0).then(|| Arc::clone(inner))
2139}
2140
2141impl<'t> Captures<'t> {
2142 pub fn get(&self, i: usize) -> Option<Match<'t>> {
2155 if i >= self.region.num_regs as usize {
2156 return None;
2157 }
2158 Match::from_region(self.text, self.region.beg[i], self.region.end[i])
2159 }
2160
2161 pub fn name(&self, name: &str) -> Option<Match<'t>> {
2163 let compiled = self.compiled.as_deref()?;
2166 let num =
2167 onig_name_to_backref_number(&compiled.program, name.as_bytes(), Some(&self.region))
2168 .ok()?;
2169 self.get(num as usize)
2170 }
2171
2172 pub fn len(&self) -> usize {
2174 self.region.num_regs as usize
2175 }
2176
2177 pub fn is_empty(&self) -> bool {
2179 self.region.num_regs == 0
2180 }
2181
2182 pub fn iter(&self) -> CapturesIter<'_, 't> {
2184 CapturesIter {
2185 captures: self,
2186 index: 0,
2187 }
2188 }
2189}
2190
2191impl Index<usize> for Captures<'_> {
2208 type Output = str;
2209
2210 fn index(&self, i: usize) -> &str {
2211 match self.get(i) {
2212 Some(m) => m.as_str(),
2213 None => panic!("no group at index '{i}'"),
2214 }
2215 }
2216}
2217
2218impl<'n> Index<&'n str> for Captures<'_> {
2235 type Output = str;
2236
2237 fn index(&self, name: &'n str) -> &str {
2238 match self.name(name) {
2239 Some(m) => m.as_str(),
2240 None => panic!("no group named '{name}'"),
2241 }
2242 }
2243}
2244
2245impl fmt::Debug for Captures<'_> {
2246 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2247 let mut list = f.debug_list();
2248 for i in 0..self.len() {
2249 list.entry(&self.get(i));
2250 }
2251 list.finish()
2252 }
2253}
2254
2255impl Drop for Captures<'_> {
2256 fn drop(&mut self) {
2257 cache_region(std::mem::take(&mut self.region));
2258 }
2259}
2260
2261pub struct CapturesIter<'c, 't> {
2265 captures: &'c Captures<'t>,
2266 index: usize,
2267}
2268
2269impl<'c, 't> Iterator for CapturesIter<'c, 't> {
2270 type Item = Option<Match<'t>>;
2271
2272 fn next(&mut self) -> Option<Self::Item> {
2273 if self.index >= self.captures.len() {
2274 return None;
2275 }
2276 let m = self.captures.get(self.index);
2277 self.index += 1;
2278 Some(m)
2279 }
2280
2281 fn size_hint(&self) -> (usize, Option<usize>) {
2282 let remaining = self.captures.len() - self.index;
2283 (remaining, Some(remaining))
2284 }
2285}
2286
2287impl ExactSizeIterator for CapturesIter<'_, '_> {}
2288
2289impl fmt::Debug for CapturesIter<'_, '_> {
2290 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2291 f.debug_struct("CapturesIter").finish_non_exhaustive()
2292 }
2293}
2294
2295pub struct CaptureNames<'r> {
2300 table: Option<&'r NameTable>,
2301 next_group: i32,
2302 end_group: i32,
2303}
2304
2305impl<'r> CaptureNames<'r> {
2306 fn name_of(&self, group: i32) -> Option<&'r str> {
2309 let table = self.table?;
2310 let entry = table
2311 .entries
2312 .values()
2313 .find(|entry| entry.back_refs.contains(&group))?;
2314 std::str::from_utf8(&entry.name).ok()
2315 }
2316}
2317
2318impl<'r> Iterator for CaptureNames<'r> {
2319 type Item = Option<&'r str>;
2320
2321 fn next(&mut self) -> Option<Self::Item> {
2322 if self.next_group >= self.end_group {
2323 return None;
2324 }
2325 let group = self.next_group;
2326 self.next_group += 1;
2327 Some(self.name_of(group))
2328 }
2329
2330 fn size_hint(&self) -> (usize, Option<usize>) {
2331 let remaining = (self.end_group - self.next_group) as usize;
2332 (remaining, Some(remaining))
2333 }
2334}
2335
2336impl ExactSizeIterator for CaptureNames<'_> {}
2337
2338impl std::iter::FusedIterator for CaptureNames<'_> {}
2339
2340impl fmt::Debug for CaptureNames<'_> {
2341 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2342 f.debug_struct("CaptureNames").finish_non_exhaustive()
2343 }
2344}
2345
2346#[derive(Clone, Copy, Debug, Default)]
2353struct MatchCursor {
2354 last_end: usize,
2355 last_was_empty: bool,
2356}
2357
2358#[derive(Clone, Copy, Debug, PartialEq, Eq)]
2360enum Verdict {
2361 Yield,
2363 Retry,
2365 Finish,
2367}
2368
2369impl MatchCursor {
2370 #[inline]
2382 fn accept(&mut self, program: &RegexType, text: &[u8], start: usize, end: usize) -> Verdict {
2383 if start == end {
2384 if self.last_was_empty {
2385 if self.last_end >= text.len() {
2386 return Verdict::Finish;
2387 }
2388 self.last_end += program.enc.mbc_enc_len(&text[self.last_end..]);
2390 self.last_was_empty = false;
2391 return Verdict::Retry;
2392 }
2393 self.last_was_empty = true;
2394 } else {
2395 self.last_was_empty = false;
2396 }
2397
2398 self.last_end = end;
2399 if start == end && program.skip_repeated_empty_search {
2400 if end >= text.len() {
2401 self.last_end = text.len() + 1;
2404 } else {
2405 self.last_end += program.enc.mbc_enc_len(&text[end..]);
2406 self.last_was_empty = false;
2407 }
2408 }
2409 Verdict::Yield
2410 }
2411}
2412
2413pub struct FindIter<'r, 't> {
2417 regex: &'r Regex,
2418 program: &'r RegexType,
2420 text: &'t [u8],
2421 subject: Subject<'t>,
2423 cursor: MatchCursor,
2424 region: OnigRegion,
2425 msa: Option<Box<MatchArg>>,
2427 limits: Option<SearchLimits>,
2429}
2430
2431impl<'r, 't> FindIter<'r, 't> {
2432 fn try_next(&mut self) -> Result<Option<Match<'t>>, i32> {
2434 loop {
2435 if self.cursor.last_end > self.text.len() {
2436 return Ok(None);
2437 }
2438 let program = self.program;
2439 let m = if let Some(msa) = self.msa.as_deref_mut() {
2440 debug_assert!(msa.region.is_none(), "bounds-only searches keep no region");
2441 msa.reset_for_iteration(program, self.cursor.last_end, self.limits);
2442 let Some((start, end)) =
2443 onig_search_bounds(program, self.text, self.cursor.last_end, msa)?
2444 else {
2445 return Ok(None);
2446 };
2447 if end > self.text.len() {
2448 return Ok(None);
2449 }
2450 Match {
2451 subject: self.subject,
2452 start,
2453 end,
2454 }
2455 } else {
2456 let len = self.text.len();
2457 let (result, region) = search_in_range(
2458 program,
2459 self.text,
2460 len,
2461 self.cursor.last_end,
2462 len,
2463 len,
2464 Some(std::mem::take(&mut self.region)),
2465 ONIG_OPTION_NONE,
2466 self.limits,
2467 );
2468 let Some(region) = region else {
2469 return Ok(None);
2470 };
2471 self.region = region;
2472
2473 if result < 0 {
2474 return if result == ONIG_MISMATCH {
2475 Ok(None)
2476 } else {
2477 Err(result)
2478 };
2479 }
2480 if self.region.num_regs < 1 {
2481 return Ok(None);
2482 }
2483
2484 let Some(m) =
2485 Match::from_region(self.subject, self.region.beg[0], self.region.end[0])
2486 else {
2487 return Ok(None);
2488 };
2489 m
2490 };
2491
2492 match self.cursor.accept(program, self.text, m.start, m.end) {
2493 Verdict::Yield => return Ok(Some(m)),
2494 Verdict::Retry => continue,
2495 Verdict::Finish => return Ok(None),
2496 }
2497 }
2498 }
2499}
2500
2501impl<'r, 't> Iterator for FindIter<'r, 't> {
2502 type Item = Match<'t>;
2503
2504 fn next(&mut self) -> Option<Match<'t>> {
2505 self.try_next().ok().flatten()
2507 }
2508}
2509
2510impl Drop for FindIter<'_, '_> {
2511 fn drop(&mut self) {
2512 match self.msa.take() {
2513 Some(msa) => cache_msa(msa),
2514 None => cache_region(std::mem::take(&mut self.region)),
2515 }
2516 }
2517}
2518
2519impl fmt::Debug for FindIter<'_, '_> {
2520 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2521 f.debug_struct("FindIter").finish_non_exhaustive()
2522 }
2523}
2524
2525pub struct TryFindIter<'r, 't> {
2530 iter: FindIter<'r, 't>,
2531 finished: bool,
2532}
2533
2534impl<'r, 't> Iterator for TryFindIter<'r, 't> {
2535 type Item = Result<Match<'t>, RegexError>;
2536
2537 fn next(&mut self) -> Option<Self::Item> {
2538 if self.finished {
2539 return None;
2540 }
2541 match self.iter.try_next() {
2542 Ok(Some(m)) => Some(Ok(m)),
2543 Ok(None) => {
2544 self.finished = true;
2545 None
2546 }
2547 Err(code) => {
2548 self.finished = true;
2549 Some(Err(RegexError::from(code)))
2550 }
2551 }
2552 }
2553}
2554
2555impl std::iter::FusedIterator for TryFindIter<'_, '_> {}
2556
2557impl fmt::Debug for TryFindIter<'_, '_> {
2558 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2559 f.debug_struct("TryFindIter").finish_non_exhaustive()
2560 }
2561}
2562
2563pub struct CaptureMatches<'r, 't> {
2571 regex: &'r Regex,
2572 program: &'r RegexType,
2574 text: &'t [u8],
2575 subject: Subject<'t>,
2577 cursor: MatchCursor,
2578 msa: Option<Box<MatchArg>>,
2583 limits: Option<SearchLimits>,
2585}
2586
2587impl<'r, 't> CaptureMatches<'r, 't> {
2588 fn new(regex: &'r Regex, subject: Subject<'t>, limits: Option<SearchLimits>) -> Self {
2589 let program = regex.program();
2590 CaptureMatches {
2591 regex,
2592 program,
2593 text: subject.bytes(),
2594 subject,
2595 cursor: MatchCursor::default(),
2596 msa: Some(take_cached_msa(program, ONIG_OPTION_NONE, None, 0, limits)),
2597 limits,
2598 }
2599 }
2600
2601 fn next_region(&mut self) -> OnigRegion {
2606 let region = self
2607 .msa
2608 .as_deref_mut()
2609 .and_then(|msa| msa.region.take())
2610 .unwrap_or_default();
2611 if region.beg.capacity() == 0 {
2612 take_cached_region()
2613 } else {
2614 region
2615 }
2616 }
2617
2618 fn try_next(&mut self) -> Result<Option<Captures<'t>>, i32> {
2621 loop {
2622 if self.cursor.last_end > self.text.len() {
2623 return Ok(None);
2624 }
2625
2626 let program = self.program;
2627 let len = self.text.len();
2628 let start = self.cursor.last_end;
2629 let region = self.next_region();
2630 let Some(msa) = self.msa.as_deref_mut() else {
2631 return Ok(None);
2632 };
2633 msa.reset_for_iteration(program, start, self.limits);
2636 msa.region = Some(region);
2637 let result = search_in_range_inner(program, self.text, len, start, len, len, msa);
2638 let region = msa.region.take();
2639 release_oversized_stack(msa);
2640 let Some(region) = region else {
2641 return Ok(None);
2642 };
2643 if result < 0 {
2644 msa.region = Some(region);
2645 return if result == ONIG_MISMATCH {
2646 Ok(None)
2647 } else {
2648 Err(result)
2649 };
2650 }
2651 if region.num_regs < 1 {
2652 msa.region = Some(region);
2653 return Ok(None);
2654 }
2655 let Some(m) = Match::from_region(self.subject, region.beg[0], region.end[0]) else {
2656 msa.region = Some(region);
2657 return Ok(None);
2658 };
2659
2660 match self.cursor.accept(program, self.text, m.start, m.end) {
2661 Verdict::Yield => {
2662 return Ok(Some(Captures {
2663 text: self.subject,
2664 region,
2665 compiled: name_lookup(&self.regex.inner),
2666 }));
2667 }
2668 Verdict::Retry => msa.region = Some(region),
2669 Verdict::Finish => {
2670 msa.region = Some(region);
2671 return Ok(None);
2672 }
2673 }
2674 }
2675 }
2676}
2677
2678impl<'r, 't> Iterator for CaptureMatches<'r, 't> {
2679 type Item = Captures<'t>;
2680
2681 fn next(&mut self) -> Option<Captures<'t>> {
2682 self.try_next().ok().flatten()
2684 }
2685}
2686
2687impl Drop for CaptureMatches<'_, '_> {
2688 fn drop(&mut self) {
2689 if let Some(mut msa) = self.msa.take() {
2690 cache_region(msa.region.take().unwrap_or_default());
2691 cache_msa(msa);
2692 }
2693 }
2694}
2695
2696impl fmt::Debug for CaptureMatches<'_, '_> {
2697 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2698 f.debug_struct("CaptureMatches").finish_non_exhaustive()
2699 }
2700}
2701
2702pub struct TryCaptureMatches<'r, 't> {
2708 iter: CaptureMatches<'r, 't>,
2709 finished: bool,
2710}
2711
2712impl<'r, 't> Iterator for TryCaptureMatches<'r, 't> {
2713 type Item = Result<Captures<'t>, RegexError>;
2714
2715 fn next(&mut self) -> Option<Self::Item> {
2716 if self.finished {
2717 return None;
2718 }
2719 match self.iter.try_next() {
2720 Ok(Some(captures)) => Some(Ok(captures)),
2721 Ok(None) => {
2722 self.finished = true;
2723 None
2724 }
2725 Err(code) => {
2726 self.finished = true;
2727 Some(Err(RegexError::from(code)))
2728 }
2729 }
2730 }
2731}
2732
2733impl std::iter::FusedIterator for TryCaptureMatches<'_, '_> {}
2734
2735impl fmt::Debug for TryCaptureMatches<'_, '_> {
2736 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2737 f.debug_struct("TryCaptureMatches").finish_non_exhaustive()
2738 }
2739}
2740
2741#[cfg(test)]
2742mod tests {
2743 use super::*;
2744
2745 fn assert_send_sync<T: Send + Sync>() {}
2746
2747 fn clear_cached_region() {
2748 CACHED_REGION.with(|cached| *cached.borrow_mut() = None);
2749 }
2750
2751 fn cached_region_buffer() -> Option<(*const i32, usize)> {
2752 CACHED_REGION.with(|cached| {
2753 cached
2754 .borrow()
2755 .as_ref()
2756 .map(|region| (region.beg.as_ptr(), region.beg.capacity()))
2757 })
2758 }
2759
2760 #[cfg(feature = "dfa-prefilter")]
2765 #[test]
2766 fn seek_is_built_with_the_regex() {
2767 let _limits = crate::regexec::shared_limits();
2768 let re = Regex::new(r"^[0-9A-F]{8}$").unwrap();
2769 assert!(re.seek_present());
2770 assert_eq!(re.seek_exact(), Some(true));
2771 assert!(re.clone().seek_present());
2772 assert!(!re.is_match("zzzz"));
2773
2774 let folded = RegexBuilder::new("^k+$")
2777 .case_insensitive(true)
2778 .build()
2779 .unwrap();
2780 assert_eq!(folded.seek_exact(), Some(true));
2781 assert!(!folded.is_match("xxx"));
2782 assert!(folded.is_match("\u{212A}k"));
2783
2784 for pattern in ["GET", "a*"] {
2787 let re = Regex::new(pattern).unwrap();
2788 assert!(!re.seek_present(), "{pattern}");
2789 assert!(re.is_match("GET a"), "{pattern}");
2790 }
2791
2792 let off = RegexBuilder::new(r"^[0-9A-F]{8}$")
2793 .dfa(false)
2794 .build()
2795 .unwrap();
2796 assert!(!off.seek_present());
2797 assert!(off.is_match("0123ABCD"));
2798 }
2799
2800 #[cfg(feature = "dfa-prefilter")]
2804 #[test]
2805 fn exact_seek_answers_match_for_an_unbounded_repetition() {
2806 let _limits = crate::regexec::shared_limits();
2807 let pattern = r"^[a-z@.]+@[a-z.]+\.com$";
2808 let re = Regex::new(pattern).unwrap();
2809 let matcher = RegexBuilder::new(pattern).dfa(false).build().unwrap();
2810 assert_eq!(re.seek_exact(), Some(true));
2811 assert_eq!(re.seek_decides_is_match("john@doe.com"), Some(true));
2812 assert_eq!(re.seek_decides_is_match("john@doe.org"), Some(false));
2813 assert!(re.is_match("john@doe.com"));
2814 assert!(matcher.is_match("john@doe.com"));
2815 assert!(!re.is_match("john@doe.org"));
2816 }
2817
2818 #[cfg(feature = "dfa-prefilter")]
2823 #[test]
2824 fn automatic_possessive_repeats_keep_the_seek_exact() {
2825 let _limits = crate::regexec::shared_limits();
2826 let re = Regex::new(r"^[a-z]+@[a-z]+\.com$").unwrap();
2827 assert_eq!(re.seek_exact(), Some(true));
2828 assert_eq!(re.seek_decides_is_match("john@doe.com"), Some(true));
2829 assert_eq!(re.seek_decides_is_match("john@doe.org"), Some(false));
2830 assert!(re.is_match("john@doe.com"));
2831 assert!(!re.is_match("john@doe.org"));
2832
2833 let atomic = Regex::new(r"^[a-z]+@(?>[a-z]+)\.com$").unwrap();
2834 assert_eq!(atomic.seek_exact(), Some(false));
2835 assert_eq!(atomic.seek_decides_is_match("john@doe.com"), None);
2836 assert_eq!(atomic.seek_decides_is_match("john@doe.org"), Some(false));
2837 assert!(atomic.is_match("john@doe.com"));
2838 }
2839
2840 #[cfg(feature = "dfa-prefilter")]
2844 #[test]
2845 fn exact_seek_answers_where_a_tiny_retry_limit_stops_the_matcher() {
2846 let _limits = crate::regexec::exclusive_limits();
2847 let re = Regex::new(r"^[a-z]+ab\z").unwrap();
2848 let text = "xaaab";
2849 assert_eq!(re.seek_exact(), Some(true));
2850 crate::regexec::onig_set_retry_limit_in_match(1);
2851 assert!(
2853 re.is_match_bytes_at_with(text.as_bytes(), 0, SearchOptions::new())
2854 .is_err()
2855 );
2856 assert_eq!(re.seek_decides_is_match(text), Some(true));
2857 assert!(re.is_match(text));
2858 crate::regexec::onig_set_retry_limit_in_match(0);
2860 assert!(
2861 re.is_match_bytes_at_with(text.as_bytes(), 0, SearchOptions::new())
2862 .unwrap()
2863 );
2864 crate::regexec::onig_set_retry_limit_in_match(1);
2866 assert!(!re.is_match("xaaac"));
2867 }
2868
2869 #[cfg(feature = "dfa-prefilter")]
2873 #[test]
2874 fn catalog_validators_have_exact_seeks() {
2875 let _limits = crate::regexec::shared_limits();
2876 let cases: &[(&str, &str, bool, &str)] = &[
2877 (
2878 "uuid_shape",
2879 "^[0-9A-F]{8}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{12}$",
2880 true,
2881 "0123ABCD-0123-0123-0123-0123456789AB",
2882 ),
2883 (
2884 "uuid_v4",
2885 "^[0-9A-F]{8}-[0-9A-F]{4}-4[0-9A-F]{3}-[89AB][0-9A-F]{3}-[0-9A-F]{12}$",
2886 true,
2887 "0123ABCD-0123-4123-8123-0123456789AB",
2888 ),
2889 (
2890 "ipv4_validation",
2891 "^((?:[0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])[.]){3}(?:[0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])$",
2892 false,
2893 "192.168.0.1",
2894 ),
2895 (
2896 "ipv6_validation",
2897 "^((?:(?:[0-9a-fA-F]{1,4}):){7}(?:(?:[0-9a-fA-F]{1,4})|:)|(?:(?:[0-9a-fA-F]{1,4}):){6}(?:((?:[0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])[.]){3}(?:[0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])|:(?:[0-9a-fA-F]{1,4})|:)|(?:(?:[0-9a-fA-F]{1,4}):){5}(?::((?:[0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])[.]){3}(?:[0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])|(:(?:[0-9a-fA-F]{1,4})){1,2}|:)|(?:(?:[0-9a-fA-F]{1,4}):){4}(?:(:(?:[0-9a-fA-F]{1,4})){0,1}:((?:[0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])[.]){3}(?:[0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])|(:(?:[0-9a-fA-F]{1,4})){1,3}|:)|(?:(?:[0-9a-fA-F]{1,4}):){3}(?:(:(?:[0-9a-fA-F]{1,4})){0,2}:((?:[0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])[.]){3}(?:[0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])|(:(?:[0-9a-fA-F]{1,4})){1,4}|:)|(?:(?:[0-9a-fA-F]{1,4}):){2}(?:(:(?:[0-9a-fA-F]{1,4})){0,3}:((?:[0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])[.]){3}(?:[0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])|(:(?:[0-9a-fA-F]{1,4})){1,5}|:)|(?:(?:[0-9a-fA-F]{1,4}):){1}(?:(:(?:[0-9a-fA-F]{1,4})){0,4}:((?:[0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])[.]){3}(?:[0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])|(:(?:[0-9a-fA-F]{1,4})){1,6}|:)|(?::((?::(?:[0-9a-fA-F]{1,4})){0,5}:((?:[0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])[.]){3}(?:[0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])|(?::(?:[0-9a-fA-F]{1,4})){1,7}|:)))(%[0-9a-zA-Z.]{1,})?$",
2898 false,
2899 "fe80::1",
2900 ),
2901 (
2902 "django_email_local_part",
2903 "(^[-!#$%&'*+/=?^_`{}|~0-9A-Z]+(\\.[-!#$%&'*+/=?^_`{}|~0-9A-Z]+)*\\z|^\"([\\001-\\010\\013\\014\\016-\\037!#-\\[\\]-\\177]|\\\\[\\001-\\011\\013\\014\\016-\\177])*\"\\z)",
2904 true,
2905 "john.doe",
2906 ),
2907 ];
2908 for &(name, pattern, ignore_case, matching) in cases {
2909 let re = RegexBuilder::new(pattern)
2910 .case_insensitive(ignore_case)
2911 .multi_line_anchors(false)
2912 .build()
2913 .unwrap();
2914 assert_eq!(re.seek_exact(), Some(true), "{name}");
2915 assert_eq!(re.seek_decides_is_match(matching), Some(true), "{name}");
2916 }
2917 }
2918
2919 #[cfg(feature = "dfa-prefilter")]
2922 #[test]
2923 fn seek_path_leaves_invalid_utf8_and_inner_starts_to_the_matcher() {
2924 let _limits = crate::regexec::shared_limits();
2925 let re = Regex::new(r"[^a]").unwrap();
2926 let text = "é".as_bytes();
2927 for start in 0..=text.len() {
2928 let expected = re
2929 .is_match_bytes_at_with(text, start, SearchOptions::new())
2930 .unwrap();
2931 assert_eq!(re.is_match_bytes_at(text, start), expected, "start {start}");
2932 }
2933 let invalid = [0xFF_u8];
2934 let expected = re
2935 .is_match_bytes_at_with(&invalid, 0, SearchOptions::new())
2936 .unwrap();
2937 assert_eq!(re.is_match_bytes(&invalid), expected);
2938 assert_eq!(re.find_bytes(&invalid).is_some(), expected);
2939 }
2940
2941 fn capture_spans(found: Option<Captures<'_>>) -> Option<Vec<Option<Range<usize>>>> {
2943 found.map(|caps| {
2944 (0..caps.len())
2945 .map(|group| caps.get(group).map(|m| m.range()))
2946 .collect()
2947 })
2948 }
2949
2950 #[cfg(feature = "dfa-prefilter")]
2954 #[test]
2955 fn capture_gate_never_changes_a_capture_result() {
2956 let _limits = crate::regexec::shared_limits();
2957 let patterns = [
2958 r#"\A(?:('[^'\\]*(?:\\.[^'\\]*)*')|("[^"\\]*(?:\\.[^"\\]*)*"))"#,
2959 r"(\d+)-(\d+)",
2960 r"x(?=y)(.)",
2961 r"\Gab(c)?",
2962 r"^(\w+)\s*=\s*(\S*)$",
2963 r"(?<=é)a(b)?",
2964 r"\A(a|b)\1",
2965 r"(?i)\bfoo\b(.)",
2966 ];
2967 let texts = [
2968 "",
2969 "x",
2970 "ab",
2971 "abc",
2972 "'it''s'",
2973 "\"q\\\"\"",
2974 "12-34 x",
2975 "xy",
2976 "é",
2977 "éab",
2978 "Foo foo",
2979 "a=b",
2980 "bb",
2981 "a\u{1F600}b",
2982 ];
2983 for pattern in patterns {
2984 let re = Regex::new(pattern).unwrap();
2985 for text in texts {
2986 for start in (0..=text.len()).filter(|&at| text.is_char_boundary(at)) {
2987 let expected = re
2988 .captures_at_with(text, start, SearchOptions::new())
2989 .unwrap();
2990 let expected = capture_spans(expected);
2991 let expected_find = re
2992 .find_at_with(text, start, SearchOptions::new())
2993 .unwrap()
2994 .map(|found| found.range());
2995 for mode in [Some(true), Some(false), None] {
2996 re.seek_gate_force(mode);
2997 assert_eq!(
2998 capture_spans(re.captures_at(text, start)),
2999 expected,
3000 "{pattern} on {text:?} from {start}, gate {mode:?}"
3001 );
3002 assert_eq!(
3003 re.find_at(text, start).map(|found| found.range()),
3004 expected_find,
3005 "find: {pattern} on {text:?} from {start}, gate {mode:?}"
3006 );
3007 }
3008 re.seek_gate_force(None);
3009 }
3010 let expected = capture_spans(re.captures_with(text, SearchOptions::new()).unwrap());
3012 let expected_bytes = capture_spans(re.captures_bytes(text.as_bytes()));
3013 assert_eq!(expected_bytes, expected, "{pattern} on {text:?}");
3014 assert_eq!(
3015 capture_spans(re.captures(text)),
3016 expected,
3017 "{pattern} on {text:?}"
3018 );
3019 }
3020 }
3021 }
3022
3023 #[test]
3029 fn anchored_captures_match_the_general_path_through_the_api() {
3030 let _limits = crate::regexec::shared_limits();
3031 let patterns = [
3032 r"\A(\d+)-(\w+)?",
3033 r"\A",
3034 r"\Gab(c)?",
3035 r"^(\w+)\s*=\s*(\S*)$",
3036 r"\A(?:(a)|b)",
3037 r"(\d+)",
3038 r"\Ab",
3039 ];
3040 let texts = ["", "12-ab", "a=b", "ab", "abc", "bb", "é", "x\nab", "abab"];
3041 for text in texts {
3042 for pattern in patterns {
3043 let re = Regex::new(pattern).unwrap();
3044 for start in (0..=text.len()).filter(|&at| text.is_char_boundary(at)) {
3045 let expected = capture_spans(
3046 re.captures_at_with(text, start, SearchOptions::new())
3047 .unwrap(),
3048 );
3049 assert_eq!(
3050 capture_spans(re.captures_at(text, start)),
3051 expected,
3052 "{pattern} on {text:?} from {start}"
3053 );
3054 assert_eq!(
3055 capture_spans(re.captures_bytes_at(text.as_bytes(), start)),
3056 expected,
3057 "{pattern} on {text:?} from {start} (bytes)"
3058 );
3059 }
3060 let expected = capture_spans(re.captures_with(text, SearchOptions::new()).unwrap());
3061 assert_eq!(
3062 capture_spans(re.captures(text)),
3063 expected,
3064 "{pattern} on {text:?}"
3065 );
3066 }
3067 }
3068 }
3069
3070 #[cfg(feature = "dfa-prefilter")]
3074 #[test]
3075 fn capture_gate_follows_the_rejection_rate() {
3076 let _limits = crate::regexec::shared_limits();
3077 let rejecting = Regex::new(r"\Aabc(d)").unwrap();
3078 for _ in 0..32 {
3079 assert!(rejecting.captures("xyz").is_none());
3080 }
3081 let (open, rate) = rejecting.seek_gate_state();
3082 assert!(open, "rate {rate}");
3083 assert!(rate >= SEEK_GATE_OPEN_PERMILLE, "rate {rate}");
3084
3085 let accepting = Regex::new(r"x\d").unwrap();
3087 for _ in 0..32 {
3088 assert!(accepting.captures("x1").is_some());
3089 }
3090 let (open, rate) = accepting.seek_gate_state();
3091 assert!(!open, "rate {rate}");
3092 assert!(rate < SEEK_GATE_CLOSE_PERMILLE, "rate {rate}");
3093 for _ in 0..SEEK_GATE_PROBE_INTERVAL * 16 {
3096 assert!(accepting.captures("y").is_none());
3097 }
3098 let (open, rate) = accepting.seek_gate_state();
3099 assert!(open, "rate {rate}");
3100
3101 let original = Regex::new(r"\Aabc").unwrap();
3102 let clone = original.clone();
3103 let before = original.seek_gate_state();
3104 assert!(clone.captures("xyz").is_none());
3105 assert_ne!(original.seek_gate_state(), before);
3106 assert_eq!(original.seek_gate_state(), clone.seek_gate_state());
3107 }
3108
3109 #[cfg(feature = "dfa-prefilter")]
3112 #[test]
3113 fn capture_gate_is_shared_and_thread_safe() {
3114 let _limits = crate::regexec::shared_limits();
3115 let re = Regex::new(r"\Aabc(d)?").unwrap();
3116 std::thread::scope(|scope| {
3117 for thread in 0..4 {
3118 let re = re.clone();
3119 scope.spawn(move || {
3120 for round in 0..500 {
3121 let (text, matches) = if (round + thread) % 3 == 0 {
3122 ("abcd", true)
3123 } else {
3124 ("xyz", false)
3125 };
3126 let found = re.captures(text);
3127 assert_eq!(found.is_some(), matches, "{text}");
3128 let expected = re
3129 .captures_with(text, SearchOptions::new())
3130 .unwrap()
3131 .is_some();
3132 assert_eq!(found.is_some(), expected, "{text}");
3133 }
3134 });
3135 }
3136 });
3137 assert_eq!(re.seek_gate_state(), re.clone().seek_gate_state());
3138 }
3139
3140 #[cfg(feature = "dfa-prefilter")]
3144 #[test]
3145 fn seek_automaton_is_built_by_the_first_search_that_consults_it() {
3146 let _limits = crate::regexec::shared_limits();
3147 let pattern = r"\b[a-z]+ing\b";
3148 let text = "nothing here but sing and ring";
3149 let re = Regex::new(pattern).unwrap();
3150 assert!(!re.seek_built());
3151 assert_eq!(re.find_iter(text).count(), 3);
3152 assert!(
3153 re.captures_with(text, SearchOptions::new())
3154 .unwrap()
3155 .is_some()
3156 );
3157 assert!(!re.seek_built());
3158 let clone = re.clone();
3159 assert!(re.is_match(text));
3160 assert!(re.seek_built() && clone.seek_built());
3161
3162 let literal = Regex::new("sing").unwrap();
3165 assert!(literal.is_match(text));
3166 assert!(!literal.seek_built() && !literal.seek_present());
3167 let unseekable = RegexBuilder::new(pattern).dfa(false).build().unwrap();
3168 assert!(unseekable.is_match(text));
3169 assert!(!unseekable.seek_built());
3170 }
3171
3172 #[cfg(feature = "dfa-prefilter")]
3175 #[test]
3176 fn seek_automaton_is_built_once_for_racing_threads() {
3177 let _limits = crate::regexec::shared_limits();
3178 let re = Regex::new(r"\b[a-z]+ing\b").unwrap();
3179 std::thread::scope(|scope| {
3180 for _ in 0..4 {
3181 let re = re.clone();
3182 scope.spawn(move || {
3183 assert!(re.is_match("nothing here but sing and ring"));
3184 assert!(!re.is_match("no match here"));
3185 });
3186 }
3187 });
3188 assert!(re.seek_built());
3189 }
3190
3191 #[test]
3195 fn str_matches_slice_what_validation_returns() {
3196 let _limits = crate::regexec::shared_limits();
3197 let re = Regex::new(r"(\w+)(?:é|ü)?").unwrap();
3198 let text = "Grüße café x 12 naïve";
3199 let validated = |m: Match<'_>| std::str::from_utf8(m.as_bytes()).unwrap().to_owned();
3200 for m in re.find_iter(text) {
3201 assert_eq!(m.as_str(), validated(m));
3202 }
3203 for caps in re.captures_iter(text) {
3204 for m in caps.iter().flatten() {
3205 assert_eq!(m.as_str(), validated(m));
3206 }
3207 }
3208 let m = re.find_at(text, 7).unwrap();
3209 assert_eq!(m.as_str(), validated(m));
3210 let caps = re
3211 .captures_at_with(text, 7, SearchOptions::new())
3212 .unwrap()
3213 .unwrap();
3214 assert_eq!(&caps[1], validated(caps.get(1).unwrap()));
3215 let bytes = re.find_bytes(text.as_bytes()).unwrap();
3216 assert_eq!(bytes.as_str(), "Grüße");
3217 assert_eq!(
3218 format!("{:?}", Regex::new("ab").unwrap().find("xab").unwrap()),
3219 "Match { text: [120, 97, 98], start: 1, end: 3 }"
3220 );
3221 }
3222
3223 #[test]
3227 fn captures_hold_the_regex_only_for_named_groups() {
3228 let _limits = crate::regexec::shared_limits();
3229 let unnamed = Regex::new(r"<(\w+)>").unwrap();
3230 let held: Vec<Captures<'_>> = unnamed.captures_iter("<a><b><c>").collect();
3231 assert_eq!(held.len(), 3);
3232 assert_eq!(Arc::strong_count(&unnamed.inner), 1);
3233 let single = unnamed.captures("<d>").unwrap();
3234 assert_eq!(Arc::strong_count(&unnamed.inner), 1);
3235 assert!(single.name("tag").is_none());
3236 assert_eq!(&single[1], "d");
3237
3238 let named = Regex::new(r"<(?<tag>\w+)>").unwrap();
3239 let held: Vec<Captures<'_>> = named.captures_iter("<a><b>").collect();
3240 assert_eq!(Arc::strong_count(&named.inner), 3);
3241 let tags: Vec<&str> = held
3242 .iter()
3243 .map(|caps| caps.name("tag").unwrap().as_str())
3244 .collect();
3245 assert_eq!(tags, ["a", "b"]);
3246 drop(held);
3247 assert_eq!(Arc::strong_count(&named.inner), 1);
3248 }
3249
3250 #[test]
3251 fn compiled_regex_types_are_send_and_sync() {
3252 assert_send_sync::<Regex>();
3253 assert_send_sync::<crate::regset::OnigRegSet>();
3254 assert_send_sync::<crate::scanner::Scanner>();
3255 }
3256
3257 #[test]
3258 fn public_types_implement_debug() {
3259 use crate::scanner::{OnigString, Scanner, ScannerConfig, ScannerPatternCache};
3260 let _limits = crate::regexec::shared_limits();
3261
3262 let re = Regex::new(r"(a)(?<n>b)").unwrap();
3263 let text = "ab ab";
3264 let captures = re.captures(text).unwrap();
3265 let config = ScannerConfig::default();
3266 let mut cache = ScannerPatternCache::new();
3267 let _top =
3268 Scanner::with_pattern_cache(&[r"\bfn\b", r#""[^"]*""#], &config, &mut cache).unwrap();
3269 let _call =
3270 Scanner::with_pattern_cache(&[r"\)", r#""[^"]*""#], &config, &mut cache).unwrap();
3271 let scanner = Scanner::new(&[r"\d+", "[a-z]+"]).unwrap();
3272 let custom_syntax: &'static OnigSyntaxType =
3273 Box::leak(Box::new(crate::regsyntax::OnigSyntaxRuby.clone()));
3274
3275 let shown: [(String, &str); 15] = [
3276 (
3277 format!("{:?}", Regex::builder("a+").syntax_mode(Syntax::Ruby)),
3278 "pattern: \"a+\", options:",
3279 ),
3280 (
3281 format!("{:?}", Regex::builder("a+").syntax(custom_syntax)),
3282 "syntax: None",
3283 ),
3284 (format!("{:?}", captures.iter()), "CapturesIter { .. }"),
3285 (format!("{:?}", re.capture_names()), "CaptureNames { .. }"),
3286 (format!("{:?}", re.find_iter(text)), "FindIter { .. }"),
3287 (
3288 format!("{:?}", re.find_iter_with(text, SearchOptions::new())),
3289 "TryFindIter { .. }",
3290 ),
3291 (
3292 format!("{:?}", re.captures_iter(text)),
3293 "CaptureMatches { .. }",
3294 ),
3295 (
3296 format!("{:?}", re.captures_iter_with(text, SearchOptions::new())),
3297 "TryCaptureMatches { .. }",
3298 ),
3299 (format!("{:?}", re.split(text)), "Split { .. }"),
3300 (format!("{:?}", re.splitn(text, 2)), "SplitN { .. }"),
3301 (
3302 format!("{:?}", re.split_bytes(text.as_bytes())),
3303 "SplitBytes { .. }",
3304 ),
3305 (
3306 format!("{:?}", re.splitn_bytes(text.as_bytes(), 2)),
3307 "SplitNBytes { .. }",
3308 ),
3309 (format!("{scanner:?}"), "Scanner { patterns: 2,"),
3310 (format!("{cache:?}"), "ScannerPatternCache { len: 3, .. }"),
3311 (
3312 format!("{:?}", OnigString::new("a💻b")),
3313 "OnigString { content: \"a💻b\", .. }",
3314 ),
3315 ];
3316 for (shown, expected) in &shown {
3317 assert!(
3318 shown.contains(*expected),
3319 "{shown} does not contain {expected}"
3320 );
3321 }
3322 }
3323
3324 #[test]
3325 fn find_reuses_the_thread_local_region_buffer() {
3326 let _limits = crate::regexec::shared_limits();
3327 clear_cached_region();
3328 let re = Regex::new(r"(a)(b)\K(c)").unwrap();
3330
3331 assert_eq!(re.find("abc").unwrap().as_str(), "c");
3332 let first = cached_region_buffer().expect("find should return its region to the cache");
3333 assert!(first.1 >= 4);
3334
3335 assert_eq!(re.find("abc").unwrap().as_str(), "c");
3336 let second = cached_region_buffer().expect("find should preserve the cached region");
3337 assert_eq!(second, first);
3338 }
3339
3340 #[test]
3341 fn captures_returns_its_region_buffer_when_dropped() {
3342 let _limits = crate::regexec::shared_limits();
3343 clear_cached_region();
3344 let re = Regex::new(r"(a)(b)(c)").unwrap();
3345 let captures = re.captures("abc").unwrap();
3346 let buffer = captures.region.beg.as_ptr();
3347
3348 assert!(cached_region_buffer().is_none());
3349 drop(captures);
3350
3351 let cached = cached_region_buffer().expect("drop should return the captures region");
3352 assert_eq!(cached.0, buffer);
3353 assert!(cached.1 >= 4);
3354 }
3355
3356 #[test]
3357 fn result_drop_during_thread_local_teardown_does_not_panic() {
3358 let _limits = crate::regexec::shared_limits();
3359 thread_local! {
3360 static LATE_RESULT: RefCell<Option<Captures<'static>>> = const { RefCell::new(None) };
3361 }
3362
3363 std::thread::spawn(|| {
3364 LATE_RESULT.with(|result| assert!(result.borrow().is_none()));
3367 let regex = Box::leak(Box::new(Regex::new(r"(a)").unwrap()));
3368 let captures = regex.captures("a").unwrap();
3369 LATE_RESULT.with(|result| *result.borrow_mut() = Some(captures));
3370 })
3371 .join()
3372 .expect("result drop during thread teardown must not panic");
3373 }
3374
3375 #[test]
3376 fn find_iter_reuses_one_region_for_every_step() {
3377 let _limits = crate::regexec::shared_limits();
3378 clear_cached_region();
3379 let re = Regex::new(r"(\w)\K\w+").unwrap();
3382 let mut matches = re.find_iter("one two");
3383 assert!(matches.msa.is_none());
3384
3385 assert_eq!(matches.next().unwrap().as_str(), "ne");
3386 let first = matches.region.beg.as_ptr();
3387 assert_eq!(matches.next().unwrap().as_str(), "wo");
3388 assert_eq!(matches.region.beg.as_ptr(), first);
3389 drop(matches);
3390
3391 let cached = cached_region_buffer().expect("iterator drop should return its region");
3392 assert_eq!(cached.0, first);
3393 }
3394
3395 #[test]
3396 fn find_iter_reuses_one_match_arg_without_a_region() {
3397 let _limits = crate::regexec::shared_limits();
3398 let re = Regex::new(r"(\w+)").unwrap();
3399 let mut matches = re.find_iter("one two");
3400 let msa: *const MatchArg = &**matches.msa.as_ref().expect("bounds-only iterator");
3401 assert_eq!(matches.next().unwrap().as_str(), "one");
3402 assert_eq!(matches.next().unwrap().as_str(), "two");
3403 assert!(matches.next().is_none());
3404 assert!(std::ptr::eq(&**matches.msa.as_ref().unwrap(), msa));
3405 assert!(matches.region.beg.is_empty(), "no region was requested");
3406 }
3407
3408 #[test]
3410 fn find_iter_bounds_match_the_region_path() {
3411 let _limits = crate::regexec::shared_limits();
3412 let patterns = [
3413 r"\d+",
3414 r"(\w+)",
3415 r"(a)|b",
3416 r"\b(\w+)\s+\1\b",
3417 r"(?<=@)\w+",
3418 r"\w+(?=\()",
3419 r"x*",
3420 r"",
3421 r"^",
3422 r"$",
3423 r"(?m)^\s*$",
3424 r"alpha|beta|gamma|delta",
3425 r"(alpha|beta|gamma|delta)",
3426 r"(?i)(?:error|warn|fatal|panic)",
3427 r"\b(?:if|else|for|while)\b",
3428 r"(?>a+)b|a",
3429 r"(a|ab)(c|bcd)(d*)",
3430 r"[\w.+-]+@\w+\.\w+",
3431 r"\p{L}+",
3432 r"é|e",
3433 r".*",
3434 r"(?<n>\d)(?<m>\d)?",
3435 ];
3436 let texts = [
3437 "",
3438 "a",
3439 "one two two three",
3440 "x@y.z foo(1) bar( a@b.cd",
3441 "alpha beta gammadelta ERROR warn Panic",
3442 "if else\nfor \n\nwhile",
3443 "aab abcd abcbcdd",
3444 "Grüße café éte 12 345",
3445 "xxaxxb",
3446 ];
3447 for pattern in patterns {
3448 let re = Regex::new(pattern).unwrap();
3449 for text in texts {
3450 let bounds = re.find_iter(text);
3451 assert!(bounds.msa.is_some(), "{pattern}");
3452 let region_path = FindIter {
3453 regex: &re,
3454 program: re.program(),
3455 text: text.as_bytes(),
3456 subject: Subject::Str(text),
3457 cursor: MatchCursor::default(),
3458 region: OnigRegion::new(),
3459 msa: None,
3460 limits: None,
3461 };
3462 let got: Vec<_> = bounds.map(|m| m.range()).collect();
3463 let expected: Vec<_> = region_path.map(|m| m.range()).collect();
3464 assert_eq!(got, expected, "{pattern:?} {text:?}");
3465 assert_eq!(
3466 re.find(text).map(|m| m.range()),
3467 expected.first().cloned(),
3468 "{pattern:?} {text:?}"
3469 );
3470 }
3471 }
3472 let keep = Regex::new(r"(\w)\K\w+").unwrap();
3474 assert_eq!(keep.find("one two").map(|m| m.range()), Some(1..3));
3475 }
3476
3477 #[test]
3481 fn find_iter_map_candidates_at_and_after_the_start() {
3482 let _limits = crate::regexec::shared_limits();
3483 let re = Regex::new(r"a+|b").unwrap();
3485 let ranges: Vec<_> = re.find_iter("aabxba").map(|m| m.range()).collect();
3486 assert_eq!(ranges, vec![0..2, 2..3, 4..5, 5..6]);
3487 let ranges: Vec<_> = re.find_iter("xa").map(|m| m.range()).collect();
3489 assert_eq!(ranges, vec![1..2]);
3490 let ranges: Vec<_> = re.find_iter("éaé").map(|m| m.range()).collect();
3493 assert_eq!(ranges, vec![2..3]);
3494 let re = Regex::new(r"é+|a").unwrap();
3495 let ranges: Vec<_> = re.find_iter("éaé").map(|m| m.range()).collect();
3496 assert_eq!(ranges, vec![0..2, 2..3, 3..5]);
3497 assert_eq!(re.find_at("éaé", 2).map(|m| m.range()), Some(2..3));
3499 assert!(re.find_at("éaé", 5).is_none());
3501 let re = Regex::new(r"x*|a").unwrap();
3503 let ranges: Vec<_> = re.find_iter("ab").map(|m| m.range()).collect();
3504 assert_eq!(ranges, vec![0..0, 1..1, 2..2]);
3505 let re = Regex::new(r"(?<=a)b|c+").unwrap();
3507 let ranges: Vec<_> = re.find_iter("abcc b").map(|m| m.range()).collect();
3508 assert_eq!(ranges, vec![1..2, 2..4]);
3509 let re = Regex::new(r"\Ga|b").unwrap();
3511 let ranges: Vec<_> = re.find_iter("aba").map(|m| m.range()).collect();
3512 assert_eq!(ranges, vec![0..1, 1..2, 2..3]);
3513 }
3514
3515 #[test]
3516 fn regex_new_and_find() {
3517 let _limits = crate::regexec::shared_limits();
3518 let re = Regex::new(r"\d+").unwrap();
3519 let m = re.find("hello 42 world").unwrap();
3520 assert_eq!(m.as_str(), "42");
3521 assert_eq!(m.start(), 6);
3522 assert_eq!(m.end(), 8);
3523 assert_eq!(m.range(), 6..8);
3524 assert_eq!(m.len(), 2);
3525 assert!(!m.is_empty());
3526 }
3527
3528 #[test]
3529 fn regex_no_match() {
3530 let _limits = crate::regexec::shared_limits();
3531 let re = Regex::new(r"\d+").unwrap();
3532 assert!(re.find("no digits here").is_none());
3533 }
3534
3535 #[test]
3536 fn regex_is_match() {
3537 let _limits = crate::regexec::shared_limits();
3538 let re = Regex::new(r"hello").unwrap();
3539 assert!(re.is_match("say hello"));
3540 assert!(!re.is_match("say goodbye"));
3541 }
3542
3543 #[test]
3544 fn search_options_methods_return_matches_and_captures() {
3545 let _limits = crate::regexec::shared_limits();
3546 let re = Regex::new(r"(\w+)").unwrap();
3547 let options = SearchOptions::new().timeout(Duration::from_secs(1));
3548
3549 assert_eq!(
3550 re.find_with("hello 42", options).unwrap().unwrap().as_str(),
3551 "hello"
3552 );
3553 assert_eq!(
3554 re.find_bytes_with(b"hello 42", options)
3555 .unwrap()
3556 .unwrap()
3557 .as_str(),
3558 "hello"
3559 );
3560 assert!(re.is_match_with("hello", options).unwrap());
3561 assert!(!re.is_match_bytes_with(b"!!!", options).unwrap());
3562 assert_eq!(
3563 re.captures_with("hello 42", options)
3564 .unwrap()
3565 .unwrap()
3566 .get(1)
3567 .unwrap()
3568 .as_str(),
3569 "hello"
3570 );
3571 assert!(re.captures_bytes_with(b"!!!", options).unwrap().is_none());
3572 }
3573
3574 #[test]
3575 fn search_options_surface_time_limit_errors() {
3576 let _limits = crate::regexec::shared_limits();
3577 let re = Regex::new(r"(a+)+b").unwrap();
3578 let text = "a".repeat(40);
3579 let options = SearchOptions::new()
3580 .timeout(Duration::from_millis(1))
3581 .retry_limit_in_match(0)
3582 .retry_limit_in_search(0);
3583
3584 assert!(matches!(
3585 re.find_with(&text, options),
3586 Err(RegexError::TimeLimitOver)
3587 ));
3588 assert!(matches!(
3589 re.is_match_with(&text, options),
3590 Err(RegexError::TimeLimitOver)
3591 ));
3592 assert!(matches!(
3593 re.captures_with(&text, options),
3594 Err(RegexError::TimeLimitOver)
3595 ));
3596 }
3597
3598 #[test]
3599 fn search_options_timeout_spans_every_start_position() {
3600 let _limits = crate::regexec::shared_limits();
3601 let re = Regex::new(r"a{1,400}?(?=b)").unwrap();
3604 let text = "a".repeat(500_000);
3605 let options = SearchOptions::new().timeout(Duration::from_millis(10));
3606
3607 let started = std::time::Instant::now();
3608 assert!(matches!(
3609 re.find_with(&text, options),
3610 Err(RegexError::TimeLimitOver)
3611 ));
3612 assert!(started.elapsed() < Duration::from_secs(1));
3613 }
3614
3615 #[test]
3620 fn skip_gates_keep_the_retry_limit_boundary() {
3621 let _limits = crate::regexec::shared_limits();
3622 let compile = |pattern: &str, gated: bool| {
3623 crate::regcomp::SKIP_GATES_DISABLED.with(|disabled| disabled.set(!gated));
3624 let re = Regex::new(pattern);
3625 crate::regcomp::SKIP_GATES_DISABLED.with(|disabled| disabled.set(false));
3626 re.unwrap()
3627 };
3628 let text = format!("{}ss!", "ß".repeat(4));
3629 let plain = compile(r"(?i)^[[:word:]]{0,40}s!", false);
3630 let gated = compile(r"(?i)^[[:word:]]{0,40}s!", true);
3631 let at = |re: &Regex, limit: u64| {
3632 re.find_with(&text, SearchOptions::new().retry_limit_in_match(limit))
3633 .map(|m| m.map(|m| (m.start(), m.end())))
3634 };
3635 let boundary = (1..=100_000u64)
3636 .find(|&limit| matches!(at(&plain, limit), Ok(Some(_))))
3637 .expect("the match fits in the default limit");
3638 assert!(boundary > 1, "the search backtracks");
3639 for limit in [boundary - 1, boundary] {
3640 assert_eq!(at(&gated, limit), at(&plain, limit), "limit {limit}");
3641 }
3642 assert!(matches!(
3643 at(&plain, boundary - 1),
3644 Err(RegexError::RetryLimitInMatchOver)
3645 ));
3646 }
3647
3648 #[test]
3649 fn search_options_surface_retry_limit_errors() {
3650 let _limits = crate::regexec::shared_limits();
3651 let re = Regex::new(r"(a+)+b").unwrap();
3652 let text = "a".repeat(30);
3653
3654 assert!(matches!(
3655 re.find_with(&text, SearchOptions::new().retry_limit_in_match(1_000)),
3656 Err(RegexError::RetryLimitInMatchOver)
3657 ));
3658 assert!(matches!(
3659 re.find_with(
3660 &text,
3661 SearchOptions::new()
3662 .retry_limit_in_match(0)
3663 .retry_limit_in_search(1_000)
3664 ),
3665 Err(RegexError::RetryLimitInSearchOver)
3666 ));
3667 }
3668
3669 #[test]
3673 fn search_options_limits_do_not_leak_into_standard_searches() {
3674 let _limits = crate::regexec::shared_limits();
3675 let re = Regex::new(r"a*?b").unwrap();
3676 let text = format!("{}b", "a".repeat(50));
3677 let tight = SearchOptions::new().retry_limit_in_match(3);
3678
3679 assert!(matches!(
3680 re.find_with(&text, tight),
3681 Err(RegexError::RetryLimitInMatchOver)
3682 ));
3683 assert!(matches!(
3684 re.is_match_with(&text, tight),
3685 Err(RegexError::RetryLimitInMatchOver)
3686 ));
3687 assert!(matches!(
3688 re.captures_with(&text, tight),
3689 Err(RegexError::RetryLimitInMatchOver)
3690 ));
3691 assert!(matches!(
3692 re.find_iter_with(&text, tight).next(),
3693 Some(Err(RegexError::RetryLimitInMatchOver))
3694 ));
3695 assert!(matches!(
3696 re.captures_iter_with(&text, tight).next(),
3697 Some(Err(RegexError::RetryLimitInMatchOver))
3698 ));
3699
3700 assert_eq!(re.find(&text).map(|m| m.range()), Some(0..51));
3701 assert!(re.is_match(&text));
3702 assert!(re.captures(&text).is_some());
3703 assert_eq!(re.find_iter(&text).count(), 1);
3704 assert_eq!(
3705 re.find_with(&text, SearchOptions::new())
3706 .unwrap()
3707 .map(|m| m.range()),
3708 Some(0..51)
3709 );
3710 }
3711
3712 #[test]
3716 fn bounds_and_region_paths_report_the_same_limit_errors() {
3717 let _limits = crate::regexec::shared_limits();
3718 let patterns = [
3719 r"a*?b",
3720 r"(a+)+b",
3721 r"(?:a|aa)*c",
3722 r"\w+\s+\w+x",
3723 r"(\w+)@(\w+)\.com",
3724 r"x*y|a+z",
3725 r"(?>a+)b|a",
3726 ];
3727 let texts = [
3730 format!("{}b", "a".repeat(16)),
3731 format!("{}c", "a".repeat(15)),
3732 "a".repeat(16),
3733 "one two three four five six".to_owned(),
3734 "mail user@example.com and more".to_owned(),
3735 format!("{}z", "a".repeat(20)),
3736 format!("{}b", "a".repeat(400)),
3739 ];
3740 let limits = [
3741 SearchOptions::new().retry_limit_in_match(1),
3742 SearchOptions::new().retry_limit_in_match(3),
3743 SearchOptions::new().retry_limit_in_match(20),
3744 SearchOptions::new().retry_limit_in_search(5),
3745 SearchOptions::new().retry_limit_in_search(50),
3746 SearchOptions::new().match_stack_limit(4),
3747 SearchOptions::new().match_stack_limit(40),
3748 SearchOptions::new().match_stack_limit(320),
3749 ];
3750 for pattern in patterns {
3751 let re = Regex::new(pattern).unwrap();
3752 for text in &texts {
3753 for options in limits {
3754 let bounds = re.find_with(text, options).map(|m| m.map(|m| m.range()));
3755 let region = re
3756 .captures_with(text, options)
3757 .map(|c| c.map(|c| c.get(0).unwrap().range()));
3758 assert_eq!(bounds, region, "{pattern:?} {text:?} {options:?}");
3759 let is_match = re.is_match_with(text, options);
3760 assert_eq!(
3761 is_match,
3762 bounds.as_ref().map(Option::is_some).map_err(Clone::clone),
3763 "{pattern:?} {text:?} {options:?}"
3764 );
3765 }
3766 }
3767 }
3768 }
3769
3770 #[test]
3771 fn search_options_leave_unset_limits_process_wide() {
3772 let _limits = crate::regexec::exclusive_limits();
3773 let old_retry = crate::regexec::onig_get_retry_limit_in_match();
3774 crate::regexec::onig_set_retry_limit_in_match(1_000);
3775 let re = Regex::new(r"(a+)+b").unwrap();
3776 let text = "a".repeat(30);
3777
3778 let unset = re.find_with(&text, SearchOptions::new());
3779 let disabled = re.find_with(
3780 &text,
3781 SearchOptions::new()
3782 .retry_limit_in_match(0)
3783 .retry_limit_in_search(0)
3784 .timeout(Duration::from_millis(50)),
3785 );
3786 crate::regexec::onig_set_retry_limit_in_match(old_retry);
3787
3788 assert!(matches!(unset, Err(RegexError::RetryLimitInMatchOver)));
3789 assert!(matches!(disabled, Err(RegexError::TimeLimitOver)));
3790 }
3791
3792 #[test]
3793 fn search_options_iterator_yields_matches_and_then_finishes() {
3794 let _limits = crate::regexec::shared_limits();
3795 let re = Regex::new(r"\w+").unwrap();
3796 let options = SearchOptions::new().timeout(Duration::from_secs(1));
3797 let mut matches = re.find_iter_with("one two", options);
3798
3799 assert_eq!(matches.next().unwrap().unwrap().as_str(), "one");
3800 assert_eq!(matches.next().unwrap().unwrap().as_str(), "two");
3801 assert!(matches.next().is_none());
3802 assert!(matches.next().is_none());
3803 let bytes: Vec<_> = re
3804 .find_iter_bytes_with(b"a b", options)
3805 .map(|m| m.unwrap().as_bytes().to_vec())
3806 .collect();
3807 assert_eq!(bytes, vec![b"a".to_vec(), b"b".to_vec()]);
3808 }
3809
3810 #[test]
3811 fn search_options_iterator_surfaces_an_error_once() {
3812 let _limits = crate::regexec::shared_limits();
3813 let re = Regex::new(r"(a+)+b").unwrap();
3814 let text = "a".repeat(40);
3815 let options = SearchOptions::new()
3816 .timeout(Duration::from_millis(1))
3817 .retry_limit_in_match(0)
3818 .retry_limit_in_search(0);
3819 let mut matches = re.find_iter_with(&text, options);
3820
3821 assert!(matches!(
3822 matches.next().unwrap(),
3823 Err(RegexError::TimeLimitOver)
3824 ));
3825 assert!(matches.next().is_none());
3826 }
3827
3828 #[test]
3829 fn search_options_default_keeps_every_limit_unset() {
3830 assert_eq!(SearchOptions::default(), SearchOptions::new());
3831 assert_send_sync::<SearchOptions>();
3832 assert_send_sync::<TryFindIter<'static, 'static>>();
3833 }
3834
3835 #[test]
3836 fn timeout_is_rounded_up_to_milliseconds() {
3837 assert_eq!(timeout_to_millis(Duration::ZERO), 0);
3838 assert_eq!(timeout_to_millis(Duration::from_nanos(1)), 1);
3839 assert_eq!(timeout_to_millis(Duration::from_millis(1)), 1);
3840 assert_eq!(timeout_to_millis(Duration::from_nanos(1_000_001)), 2);
3841 assert_eq!(timeout_to_millis(Duration::MAX), u64::MAX);
3842 }
3843
3844 #[test]
3845 fn regex_captures() {
3846 let _limits = crate::regexec::shared_limits();
3847 let re = Regex::new(r"(\d{4})-(\d{2})-(\d{2})").unwrap();
3848 let caps = re.captures("date: 2026-02-14").unwrap();
3849 assert_eq!(caps.get(0).unwrap().as_str(), "2026-02-14");
3850 assert_eq!(caps.get(1).unwrap().as_str(), "2026");
3851 assert_eq!(caps.get(2).unwrap().as_str(), "02");
3852 assert_eq!(caps.get(3).unwrap().as_str(), "14");
3853 assert!(caps.get(4).is_none());
3854 assert_eq!(caps.len(), 4);
3855 }
3856
3857 #[test]
3858 fn regex_captures_len() {
3859 let re = Regex::new(r"(a)(b)(c)").unwrap();
3860 assert_eq!(re.captures_len(), 3);
3861 }
3862
3863 #[test]
3864 fn regex_find_iter() {
3865 let _limits = crate::regexec::shared_limits();
3866 let re = Regex::new(r"\d+").unwrap();
3867 let matches: Vec<&str> = re.find_iter("1 + 22 = 333").map(|m| m.as_str()).collect();
3868 assert_eq!(matches, vec!["1", "22", "333"]);
3869 }
3870
3871 #[test]
3872 fn regex_builder_case_insensitive() {
3873 let _limits = crate::regexec::shared_limits();
3874 let re = Regex::builder(r"hello")
3875 .case_insensitive(true)
3876 .build()
3877 .unwrap();
3878 assert!(re.is_match("HELLO"));
3879 assert!(re.is_match("Hello"));
3880 }
3881
3882 #[test]
3883 fn regex_invalid_pattern() {
3884 let err = Regex::new(r"(unclosed").unwrap_err();
3885 assert!(matches!(err, RegexError::Syntax { .. }));
3886 }
3887
3888 #[test]
3889 fn match_as_bytes() {
3890 let _limits = crate::regexec::shared_limits();
3891 let re = Regex::new(r"world").unwrap();
3892 let m = re.find("hello world").unwrap();
3893 assert_eq!(m.as_bytes(), b"world");
3894 }
3895
3896 #[test]
3897 fn captures_iter() {
3898 let _limits = crate::regexec::shared_limits();
3899 let re = Regex::new(r"(a)(b)?").unwrap();
3900 let caps = re.captures("a").unwrap();
3901 let items: Vec<_> = caps.iter().collect();
3902 assert_eq!(items.len(), 3);
3904 assert!(items[0].is_some());
3905 assert!(items[1].is_some());
3906 assert!(items[2].is_none());
3907 }
3908
3909 #[test]
3910 fn named_captures() {
3911 let _limits = crate::regexec::shared_limits();
3912 let re = Regex::new(r"(?<year>\d{4})-(?<month>\d{2})").unwrap();
3913 let caps = re.captures("2026-02").unwrap();
3914 assert_eq!(caps.name("year").unwrap().as_str(), "2026");
3915 assert_eq!(caps.name("month").unwrap().as_str(), "02");
3916 assert!(caps.name("day").is_none());
3917 }
3918
3919 #[test]
3920 fn empty_match_find_iter() {
3921 let _limits = crate::regexec::shared_limits();
3922 let re = Regex::new(r"").unwrap();
3923 let matches: Vec<_> = re.find_iter("ab").collect();
3924 assert_eq!(matches.len(), 3);
3926 assert_eq!(matches[0].start(), 0);
3927 assert_eq!(matches[1].start(), 1);
3928 assert_eq!(matches[2].start(), 2);
3929 }
3930
3931 fn repeated_search_iteration<T>(
3937 re: &Regex,
3938 text: &[u8],
3939 search: impl Fn(usize) -> Option<(usize, usize, T)>,
3940 ) -> Vec<T> {
3941 let mut found = Vec::new();
3942 let mut last_end = 0;
3943 let mut last_was_empty = false;
3944 while last_end <= text.len() {
3945 let Some((start, end, item)) = search(last_end) else {
3946 break;
3947 };
3948 if start == end {
3949 if last_was_empty {
3950 if last_end >= text.len() {
3951 break;
3952 }
3953 last_end += re.program().enc.mbc_enc_len(&text[last_end..]);
3954 last_was_empty = false;
3955 continue;
3956 }
3957 last_was_empty = true;
3958 } else {
3959 last_was_empty = false;
3960 }
3961 last_end = end;
3962 found.push(item);
3963 }
3964 found
3965 }
3966
3967 type Groups = Vec<Option<(usize, usize)>>;
3970
3971 fn group_spans(caps: &Captures<'_>) -> Groups {
3972 caps.iter()
3973 .map(|group| group.map(|m| (m.start(), m.end())))
3974 .collect()
3975 }
3976
3977 const EMPTY_MATCH_PATTERNS: &[&str] = &[
3980 "",
3981 "a*",
3982 "a*?",
3983 "x*",
3984 r"\b",
3985 r"\B",
3986 "(?=x)",
3987 "(?!x)",
3988 "|",
3989 "a|",
3990 "|a",
3991 "(a|)",
3992 "(a)?(b*)",
3993 r"(\w*)",
3994 "(?<=a)",
3995 "^",
3996 "$",
3997 r"\b\w*",
3998 "(?:)",
3999 r"\d*",
4000 "é*",
4001 "(?i)k*",
4002 r"a*?$",
4003 r"(?=\w)|\s",
4004 r"\b|é",
4005 r"(?<=a)(?!\G)|b",
4009 r"\Ga*",
4010 r"\G",
4011 r"\Gx*",
4012 r"a\K",
4014 r"\K",
4015 r"x*\K",
4016 r"(a)\K|b*",
4017 ];
4018
4019 const EMPTY_MATCH_TEXTS: &[&str] = &[
4020 "",
4021 "a",
4022 "ab",
4023 "aXa",
4024 "xéx日本",
4025 "a\nb\n",
4026 "ééé",
4027 "\u{1F600}x",
4028 "bbb",
4029 "x x",
4030 ];
4031
4032 #[test]
4033 fn empty_match_step_only_where_the_program_allows_it() {
4034 let _limits = crate::regexec::shared_limits();
4035 for pattern in ["", "a*", r"\b", "(?=x)", "|", r"(\w*)", "(a)?(b*)"] {
4036 let re = Regex::new(pattern).unwrap();
4037 assert!(
4038 re.program().skip_repeated_empty_search,
4039 "{pattern:?} has no \\G, \\K or callout"
4040 );
4041 }
4042 for pattern in [r"\Ga*", r"\G", r"a\K", r"\K", r"(a)\K|b*"] {
4043 let re = Regex::new(pattern).unwrap();
4044 assert!(
4045 !re.program().skip_repeated_empty_search,
4046 "{pattern:?} reads the search start or keeps the match start"
4047 );
4048 }
4049 }
4050
4051 #[test]
4054 fn empty_match_step_yields_what_the_repeated_search_yields() {
4055 let _limits = crate::regexec::shared_limits();
4056 for pattern in EMPTY_MATCH_PATTERNS {
4057 let re = Regex::new(pattern).unwrap();
4058 for text in EMPTY_MATCH_TEXTS {
4059 let bytes = text.as_bytes();
4060 let expected = repeated_search_iteration(&re, bytes, |at| {
4061 re.find_bytes_at(bytes, at)
4062 .map(|m| (m.start(), m.end(), (m.start(), m.end())))
4063 });
4064 let found: Vec<_> = re.find_iter(text).map(|m| (m.start(), m.end())).collect();
4065 assert_eq!(found, expected, "find_iter: {pattern:?} on {text:?}");
4066 let found: Vec<_> = re
4067 .find_iter_bytes(bytes)
4068 .map(|m| (m.start(), m.end()))
4069 .collect();
4070 assert_eq!(found, expected, "find_iter_bytes: {pattern:?} on {text:?}");
4071
4072 let expected = repeated_search_iteration(&re, bytes, |at| {
4073 re.captures_bytes_at(bytes, at).map(|caps| {
4074 let spans = group_spans(&caps);
4075 let (start, end) = spans[0].expect("whole match");
4076 (start, end, spans)
4077 })
4078 });
4079 let found: Vec<_> = re.captures_iter(text).map(|c| group_spans(&c)).collect();
4080 assert_eq!(found, expected, "captures_iter: {pattern:?} on {text:?}");
4081 }
4082 }
4083 }
4084
4085 #[test]
4088 fn empty_match_step_on_invalid_utf8() {
4089 let _limits = crate::regexec::shared_limits();
4090 let text: &[u8] = b"a\xffb\xe6\x97";
4091 for pattern in ["", "a*", r"\b", "x*", r"(\w*)"] {
4092 let re = Regex::new(pattern).unwrap();
4093 let expected = repeated_search_iteration(&re, text, |at| {
4094 re.find_bytes_at(text, at)
4095 .map(|m| (m.start(), m.end(), (m.start(), m.end())))
4096 });
4097 let found: Vec<_> = re
4098 .find_iter_bytes(text)
4099 .map(|m| (m.start(), m.end()))
4100 .collect();
4101 assert_eq!(found, expected, "{pattern:?} on invalid UTF-8");
4102 }
4103 }
4104
4105 #[test]
4108 fn empty_match_step_takes_one_search_per_match() {
4109 let _limits = crate::regexec::shared_limits();
4110 let re = Regex::new("x*").unwrap();
4111 let before = crate::regexec::VM_ATTEMPTS.with(std::cell::Cell::get);
4112 let matches = re.find_iter("abcd").count();
4113 let attempts = crate::regexec::VM_ATTEMPTS.with(std::cell::Cell::get) - before;
4114 assert_eq!(matches, 5);
4115 assert_eq!(attempts, 5);
4116 }
4117
4118 #[test]
4122 fn captures_iter_records_the_captures_of_every_match() {
4123 let _limits = crate::regexec::shared_limits();
4124 let re = Regex::new(r"#(\w+)").unwrap();
4125 let found: Vec<_> = re
4126 .captures_iter("x #ab #c")
4127 .map(|c| group_spans(&c))
4128 .collect();
4129 assert_eq!(
4130 found,
4131 vec![
4132 vec![Some((2, 5)), Some((3, 5))],
4133 vec![Some((6, 8)), Some((7, 8))],
4134 ]
4135 );
4136
4137 let re = Regex::new(r"(a)|(b)").unwrap();
4138 let found: Vec<_> = re.captures_iter("cba").map(|c| group_spans(&c)).collect();
4139 assert_eq!(
4140 found,
4141 vec![
4142 vec![Some((1, 2)), None, Some((1, 2))],
4143 vec![Some((2, 3)), Some((2, 3)), None],
4144 ]
4145 );
4146
4147 let re = Regex::new(r"(\d)(\d)?").unwrap();
4148 let found: Vec<_> = re
4149 .captures_iter("a1 22b")
4150 .map(|c| group_spans(&c))
4151 .collect();
4152 assert_eq!(
4153 found,
4154 vec![
4155 vec![Some((1, 2)), Some((1, 2)), None],
4156 vec![Some((3, 5)), Some((3, 4)), Some((4, 5))],
4157 ]
4158 );
4159
4160 let cases: [(&str, &str, Vec<Groups>); 4] = [
4163 (
4164 r"(a)b|(c)",
4165 "axc ac",
4166 vec![
4167 vec![Some((2, 3)), None, Some((2, 3))],
4168 vec![Some((5, 6)), None, Some((5, 6))],
4169 ],
4170 ),
4171 (
4172 r"(?<=z)(a)|(b)",
4173 "zaxb",
4174 vec![
4175 vec![Some((1, 2)), Some((1, 2)), None],
4176 vec![Some((3, 4)), None, Some((3, 4))],
4177 ],
4178 ),
4179 (
4180 r"\w(\d)|(x)",
4181 "1x2",
4182 vec![vec![Some((1, 3)), Some((2, 3)), None]],
4183 ),
4184 (
4185 r"(a)(b)?c|(d)e",
4186 "ab de",
4187 vec![vec![Some((3, 5)), None, None, Some((3, 4))]],
4188 ),
4189 ];
4190 for (pattern, text, expected) in cases {
4191 let re = Regex::new(pattern).unwrap();
4192 let found: Vec<_> = re.captures_iter(text).map(|c| group_spans(&c)).collect();
4193 assert_eq!(found, expected, "{pattern:?} on {text:?}");
4194 }
4195 }
4196
4197 #[test]
4202 fn find_iter_reads_the_process_wide_limits_for_each_search() {
4203 let _limits = crate::regexec::exclusive_limits();
4204 crate::regexec::onig_set_retry_limit_in_search(0);
4205 let re = Regex::new(r"x|a+c").unwrap();
4206 let mut matches = re.find_iter("x aabaac");
4207 assert_eq!(matches.next().unwrap().range(), 0..1);
4208
4209 crate::regexec::onig_set_retry_limit_in_search(1);
4210 assert!(matches.next().is_none());
4213 }
4214
4215 #[test]
4218 fn captures_iter_reads_the_process_wide_limits_for_each_search() {
4219 let _limits = crate::regexec::exclusive_limits();
4220 crate::regexec::onig_set_retry_limit_in_search(0);
4221 let re = Regex::new(r"(x)|(a+c)").unwrap();
4222 let mut matches = re.captures_iter("x aabaac");
4223 assert_eq!(matches.next().unwrap().get(0).unwrap().range(), 0..1);
4224
4225 crate::regexec::onig_set_retry_limit_in_search(1);
4226 assert!(matches.next().is_none());
4227 }
4228
4229 #[test]
4232 fn captures_iter_reuses_the_region_of_a_dropped_match() {
4233 let _limits = crate::regexec::shared_limits();
4234 clear_cached_region();
4235 let re = Regex::new(r"(\w)(\w?)").unwrap();
4236 let mut matches = re.captures_iter("ab cd ef");
4237 let first = matches.next().unwrap().region.beg.as_ptr();
4238 let second = matches.next().unwrap().region.beg.as_ptr();
4239 assert_eq!(second, first);
4240 let third = matches.next().unwrap().region.beg.as_ptr();
4241 assert_eq!(third, first);
4242 }
4243}