use std::cell::RefCell;
use std::ops::Range;
use std::time::Duration;
use crate::encodings::utf8::ONIG_ENCODING_UTF8;
use crate::error::RegexError;
use crate::oniguruma::*;
use crate::regcomp::onig_new;
use crate::regexec::{
OnigMatchParam, onig_name_to_backref_number, onig_new_match_param, onig_search,
onig_search_with_param,
};
use crate::regint::RegexType;
use crate::regsyntax::OnigSyntaxOniguruma;
thread_local! {
static CACHED_REGION: RefCell<Option<OnigRegion>> = const { RefCell::new(None) };
}
fn take_cached_region() -> OnigRegion {
CACHED_REGION
.try_with(|cached| cached.borrow_mut().take())
.ok()
.flatten()
.unwrap_or_default()
}
fn cache_region(region: OnigRegion) {
let _ = CACHED_REGION.try_with(|cached| {
let mut cached = cached.borrow_mut();
let region_capacity = region.beg.capacity() + region.end.capacity();
let should_replace = match cached.as_ref() {
Some(current) => region_capacity > current.beg.capacity() + current.end.capacity(),
None => true,
};
if should_replace {
*cached = Some(region);
}
});
}
fn timeout_to_millis(timeout: Duration) -> u64 {
if timeout.is_zero() {
return 0;
}
let millis = timeout.as_millis();
let millis = millis + u128::from(!timeout.subsec_nanos().is_multiple_of(1_000_000));
millis.clamp(1, u64::MAX as u128) as u64
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct SearchOptions {
timeout: Option<Duration>,
retry_limit_in_match: Option<u64>,
retry_limit_in_search: Option<u64>,
match_stack_limit: Option<u32>,
}
impl SearchOptions {
pub const fn new() -> Self {
SearchOptions {
timeout: None,
retry_limit_in_match: None,
retry_limit_in_search: None,
match_stack_limit: None,
}
}
pub const fn timeout(mut self, timeout: Duration) -> Self {
self.timeout = Some(timeout);
self
}
pub const fn retry_limit_in_match(mut self, limit: u64) -> Self {
self.retry_limit_in_match = Some(limit);
self
}
pub const fn retry_limit_in_search(mut self, limit: u64) -> Self {
self.retry_limit_in_search = Some(limit);
self
}
pub const fn match_stack_limit(mut self, limit: u32) -> Self {
self.match_stack_limit = Some(limit);
self
}
fn match_param(self) -> OnigMatchParam {
let mut match_param = onig_new_match_param();
if let Some(timeout) = self.timeout {
match_param.time_limit = timeout_to_millis(timeout);
}
if let Some(limit) = self.retry_limit_in_match {
match_param.retry_limit_in_match = limit;
}
if let Some(limit) = self.retry_limit_in_search {
match_param.retry_limit_in_search = limit;
}
if let Some(limit) = self.match_stack_limit {
match_param.match_stack_limit = limit;
}
match_param
}
}
pub struct Regex {
inner: RegexType,
}
impl Regex {
pub fn new(pattern: &str) -> Result<Regex, RegexError> {
Self::new_bytes(pattern.as_bytes())
}
pub fn new_bytes(pattern: &[u8]) -> Result<Regex, RegexError> {
let inner = onig_new(
pattern,
ONIG_OPTION_NONE,
&ONIG_ENCODING_UTF8,
&OnigSyntaxOniguruma,
)?;
Ok(Regex { inner })
}
pub fn builder(pattern: &str) -> RegexBuilder {
RegexBuilder::new(pattern)
}
pub fn find<'t>(&self, text: &'t str) -> Option<Match<'t>> {
self.find_bytes(text.as_bytes())
}
pub fn find_bytes<'t>(&self, text: &'t [u8]) -> Option<Match<'t>> {
let (result, region) = onig_search(
&self.inner,
text,
text.len(),
0,
text.len(),
Some(take_cached_region()),
ONIG_OPTION_NONE,
);
let region = region?;
if result < 0 {
cache_region(region);
return None;
}
if region.num_regs < 1 {
cache_region(region);
return None;
}
let m = Match::from_region(text, region.beg[0], region.end[0]);
cache_region(region);
m
}
pub fn find_with<'t>(
&self,
text: &'t str,
options: SearchOptions,
) -> Result<Option<Match<'t>>, RegexError> {
self.find_bytes_with(text.as_bytes(), options)
}
pub fn find_bytes_with<'t>(
&self,
text: &'t [u8],
options: SearchOptions,
) -> Result<Option<Match<'t>>, RegexError> {
let (result, region) =
self.search_with(text, 0, text.len(), Some(take_cached_region()), options)?;
let Some(region) = region else {
return Ok(None);
};
if result < 0 || region.num_regs < 1 {
cache_region(region);
return Ok(None);
}
let m = Match::from_region(text, region.beg[0], region.end[0]);
cache_region(region);
Ok(m)
}
pub fn is_match(&self, text: &str) -> bool {
self.is_match_bytes(text.as_bytes())
}
pub fn is_match_bytes(&self, text: &[u8]) -> bool {
let (result, _) = onig_search(
&self.inner,
text,
text.len(),
0,
text.len(),
None,
ONIG_OPTION_NONE,
);
result >= 0
}
pub fn is_match_with(&self, text: &str, options: SearchOptions) -> Result<bool, RegexError> {
self.is_match_bytes_with(text.as_bytes(), options)
}
pub fn is_match_bytes_with(
&self,
text: &[u8],
options: SearchOptions,
) -> Result<bool, RegexError> {
let (result, _) = self.search_with(text, 0, text.len(), None, options)?;
Ok(result >= 0)
}
pub fn captures<'t>(&'t self, text: &'t str) -> Option<Captures<'t>> {
self.captures_bytes(text.as_bytes())
}
pub fn captures_bytes<'t>(&'t self, text: &'t [u8]) -> Option<Captures<'t>> {
let (result, region) = onig_search(
&self.inner,
text,
text.len(),
0,
text.len(),
Some(take_cached_region()),
ONIG_OPTION_NONE,
);
let region = region?;
if result < 0 {
cache_region(region);
return None;
}
Some(Captures {
text,
region,
regex: self,
})
}
pub fn captures_with<'t>(
&'t self,
text: &'t str,
options: SearchOptions,
) -> Result<Option<Captures<'t>>, RegexError> {
self.captures_bytes_with(text.as_bytes(), options)
}
pub fn captures_bytes_with<'t>(
&'t self,
text: &'t [u8],
options: SearchOptions,
) -> Result<Option<Captures<'t>>, RegexError> {
let (result, region) =
self.search_with(text, 0, text.len(), Some(take_cached_region()), options)?;
let Some(region) = region else {
return Ok(None);
};
if result < 0 {
cache_region(region);
return Ok(None);
}
Ok(Some(Captures {
text,
region,
regex: self,
}))
}
pub fn find_iter<'r, 't>(&'r self, text: &'t str) -> FindIter<'r, 't> {
FindIter {
regex: self,
text: text.as_bytes(),
last_end: 0,
last_was_empty: false,
region: take_cached_region(),
}
}
pub fn find_iter_bytes<'r, 't>(&'r self, text: &'t [u8]) -> FindIter<'r, 't> {
FindIter {
regex: self,
text,
last_end: 0,
last_was_empty: false,
region: take_cached_region(),
}
}
pub fn find_iter_with<'r, 't>(
&'r self,
text: &'t str,
options: SearchOptions,
) -> TryFindIter<'r, 't> {
self.find_iter_bytes_with(text.as_bytes(), options)
}
pub fn find_iter_bytes_with<'r, 't>(
&'r self,
text: &'t [u8],
options: SearchOptions,
) -> TryFindIter<'r, 't> {
TryFindIter {
regex: self,
text,
last_end: 0,
last_was_empty: false,
region: take_cached_region(),
options,
finished: false,
}
}
fn search_with(
&self,
text: &[u8],
start: usize,
range: usize,
region: Option<OnigRegion>,
options: SearchOptions,
) -> Result<(i32, Option<OnigRegion>), RegexError> {
let (result, region) = onig_search_with_param(
&self.inner,
text,
text.len(),
start,
range,
region,
ONIG_OPTION_NONE,
&options.match_param(),
);
if result < 0 && result != ONIG_MISMATCH {
if let Some(region) = region {
cache_region(region);
}
return Err(RegexError::from(result));
}
Ok((result, region))
}
pub fn captures_len(&self) -> usize {
self.inner.num_mem as usize
}
pub fn as_raw(&self) -> &RegexType {
&self.inner
}
}
impl std::fmt::Debug for Regex {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Regex").finish_non_exhaustive()
}
}
pub struct RegexBuilder {
pattern: Vec<u8>,
options: OnigOptionType,
syntax: &'static OnigSyntaxType,
}
impl RegexBuilder {
pub fn new(pattern: &str) -> Self {
RegexBuilder {
pattern: pattern.as_bytes().to_vec(),
options: ONIG_OPTION_NONE,
syntax: &OnigSyntaxOniguruma,
}
}
pub fn case_insensitive(mut self, yes: bool) -> Self {
if yes {
self.options |= ONIG_OPTION_IGNORECASE;
} else {
self.options &= !ONIG_OPTION_IGNORECASE;
}
self
}
pub fn dot_matches_newline(mut self, yes: bool) -> Self {
if yes {
self.options |= ONIG_OPTION_MULTILINE;
} else {
self.options &= !ONIG_OPTION_MULTILINE;
}
self
}
pub fn multi_line_anchors(mut self, yes: bool) -> Self {
if yes {
self.options |= ONIG_OPTION_NEGATE_SINGLELINE;
self.options &= !ONIG_OPTION_SINGLELINE;
} else {
self.options &= !ONIG_OPTION_NEGATE_SINGLELINE;
self.options |= ONIG_OPTION_SINGLELINE;
}
self
}
pub fn extended(mut self, yes: bool) -> Self {
if yes {
self.options |= ONIG_OPTION_EXTEND;
} else {
self.options &= !ONIG_OPTION_EXTEND;
}
self
}
pub fn option(mut self, flag: OnigOptionType) -> Self {
self.options |= flag;
self
}
pub fn syntax(mut self, syntax: &'static OnigSyntaxType) -> Self {
self.syntax = syntax;
self
}
pub fn build(self) -> Result<Regex, RegexError> {
let inner = onig_new(
&self.pattern,
self.options,
&ONIG_ENCODING_UTF8,
self.syntax,
)?;
Ok(Regex { inner })
}
}
#[derive(Debug, Clone, Copy)]
pub struct Match<'t> {
text: &'t [u8],
start: usize,
end: usize,
}
impl<'t> Match<'t> {
fn from_region(text: &'t [u8], beg: i32, end: i32) -> Option<Self> {
let start = usize::try_from(beg).ok()?;
let end = usize::try_from(end).ok()?;
(start <= end && end <= text.len()).then_some(Match { text, start, end })
}
pub fn start(&self) -> usize {
self.start
}
pub fn end(&self) -> usize {
self.end
}
pub fn range(&self) -> Range<usize> {
self.start..self.end
}
pub fn as_bytes(&self) -> &'t [u8] {
&self.text[self.start..self.end]
}
pub fn as_str(&self) -> &'t str {
std::str::from_utf8(self.as_bytes()).expect("match is not valid UTF-8")
}
pub fn len(&self) -> usize {
self.end - self.start
}
pub fn is_empty(&self) -> bool {
self.start == self.end
}
}
pub struct Captures<'t> {
text: &'t [u8],
region: OnigRegion,
regex: &'t Regex,
}
impl<'t> Captures<'t> {
pub fn get(&self, i: usize) -> Option<Match<'t>> {
if i >= self.region.num_regs as usize {
return None;
}
Match::from_region(self.text, self.region.beg[i], self.region.end[i])
}
pub fn name(&self, name: &str) -> Option<Match<'t>> {
let num =
onig_name_to_backref_number(&self.regex.inner, name.as_bytes(), Some(&self.region))
.ok()?;
self.get(num as usize)
}
pub fn len(&self) -> usize {
self.region.num_regs as usize
}
pub fn is_empty(&self) -> bool {
self.region.num_regs == 0
}
pub fn iter(&self) -> CapturesIter<'_, 't> {
CapturesIter {
captures: self,
index: 0,
}
}
}
impl std::fmt::Debug for Captures<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let mut list = f.debug_list();
for i in 0..self.len() {
list.entry(&self.get(i));
}
list.finish()
}
}
impl Drop for Captures<'_> {
fn drop(&mut self) {
cache_region(std::mem::take(&mut self.region));
}
}
pub struct CapturesIter<'c, 't> {
captures: &'c Captures<'t>,
index: usize,
}
impl<'c, 't> Iterator for CapturesIter<'c, 't> {
type Item = Option<Match<'t>>;
fn next(&mut self) -> Option<Self::Item> {
if self.index >= self.captures.len() {
return None;
}
let m = self.captures.get(self.index);
self.index += 1;
Some(m)
}
fn size_hint(&self) -> (usize, Option<usize>) {
let remaining = self.captures.len() - self.index;
(remaining, Some(remaining))
}
}
impl ExactSizeIterator for CapturesIter<'_, '_> {}
pub struct FindIter<'r, 't> {
regex: &'r Regex,
text: &'t [u8],
last_end: usize,
last_was_empty: bool,
region: OnigRegion,
}
impl<'r, 't> Iterator for FindIter<'r, 't> {
type Item = Match<'t>;
fn next(&mut self) -> Option<Match<'t>> {
if self.last_end > self.text.len() {
return None;
}
let (result, region) = onig_search(
&self.regex.inner,
self.text,
self.text.len(),
self.last_end,
self.text.len(),
Some(std::mem::take(&mut self.region)),
ONIG_OPTION_NONE,
);
self.region = region?;
if result < 0 {
return None;
}
if self.region.num_regs < 1 {
return None;
}
let m = Match::from_region(self.text, self.region.beg[0], self.region.end[0])?;
let (start, end) = (m.start, m.end);
if start == end {
if self.last_was_empty {
if self.last_end >= self.text.len() {
return None;
}
self.last_end += self
.regex
.inner
.enc
.mbc_enc_len(&self.text[self.last_end..]);
self.last_was_empty = false;
return self.next();
}
self.last_was_empty = true;
} else {
self.last_was_empty = false;
}
self.last_end = end;
Some(m)
}
}
impl Drop for FindIter<'_, '_> {
fn drop(&mut self) {
cache_region(std::mem::take(&mut self.region));
}
}
pub struct TryFindIter<'r, 't> {
regex: &'r Regex,
text: &'t [u8],
last_end: usize,
last_was_empty: bool,
region: OnigRegion,
options: SearchOptions,
finished: bool,
}
impl<'r, 't> Iterator for TryFindIter<'r, 't> {
type Item = Result<Match<'t>, RegexError>;
fn next(&mut self) -> Option<Self::Item> {
if self.finished || self.last_end > self.text.len() {
self.finished = true;
return None;
}
let search = self.regex.search_with(
self.text,
self.last_end,
self.text.len(),
Some(std::mem::take(&mut self.region)),
self.options,
);
let (result, region) = match search {
Ok(result) => result,
Err(error) => {
self.finished = true;
return Some(Err(error));
}
};
let Some(region) = region else {
self.finished = true;
return None;
};
self.region = region;
if result < 0 {
self.finished = true;
return None;
}
if self.region.num_regs < 1 {
self.finished = true;
return None;
}
let Some(m) = Match::from_region(self.text, self.region.beg[0], self.region.end[0]) else {
self.finished = true;
return None;
};
let (start, end) = (m.start, m.end);
if start == end {
if self.last_was_empty {
if self.last_end >= self.text.len() {
self.finished = true;
return None;
}
self.last_end += self
.regex
.inner
.enc
.mbc_enc_len(&self.text[self.last_end..]);
self.last_was_empty = false;
return self.next();
}
self.last_was_empty = true;
} else {
self.last_was_empty = false;
}
self.last_end = end;
Some(Ok(m))
}
}
impl std::iter::FusedIterator for TryFindIter<'_, '_> {}
impl Drop for TryFindIter<'_, '_> {
fn drop(&mut self) {
cache_region(std::mem::take(&mut self.region));
}
}
#[cfg(test)]
mod tests {
use super::*;
fn assert_send_sync<T: Send + Sync>() {}
fn clear_cached_region() {
CACHED_REGION.with(|cached| *cached.borrow_mut() = None);
}
fn cached_region_buffer() -> Option<(*const i32, usize)> {
CACHED_REGION.with(|cached| {
cached
.borrow()
.as_ref()
.map(|region| (region.beg.as_ptr(), region.beg.capacity()))
})
}
#[test]
fn compiled_regex_types_are_send_and_sync() {
assert_send_sync::<Regex>();
assert_send_sync::<crate::regset::OnigRegSet>();
assert_send_sync::<crate::scanner::Scanner>();
}
#[test]
fn find_reuses_the_thread_local_region_buffer() {
clear_cached_region();
let re = Regex::new(r"(a)(b)(c)").unwrap();
assert_eq!(re.find("abc").unwrap().as_str(), "abc");
let first = cached_region_buffer().expect("find should return its region to the cache");
assert!(first.1 >= 4);
assert_eq!(re.find("abc").unwrap().as_str(), "abc");
let second = cached_region_buffer().expect("find should preserve the cached region");
assert_eq!(second, first);
}
#[test]
fn captures_returns_its_region_buffer_when_dropped() {
clear_cached_region();
let re = Regex::new(r"(a)(b)(c)").unwrap();
let captures = re.captures("abc").unwrap();
let buffer = captures.region.beg.as_ptr();
assert!(cached_region_buffer().is_none());
drop(captures);
let cached = cached_region_buffer().expect("drop should return the captures region");
assert_eq!(cached.0, buffer);
assert!(cached.1 >= 4);
}
#[test]
fn result_drop_during_thread_local_teardown_does_not_panic() {
thread_local! {
static LATE_RESULT: RefCell<Option<Captures<'static>>> = const { RefCell::new(None) };
}
std::thread::spawn(|| {
LATE_RESULT.with(|result| assert!(result.borrow().is_none()));
let regex = Box::leak(Box::new(Regex::new(r"(a)").unwrap()));
let captures = regex.captures("a").unwrap();
LATE_RESULT.with(|result| *result.borrow_mut() = Some(captures));
})
.join()
.expect("result drop during thread teardown must not panic");
}
#[test]
fn find_iter_reuses_one_region_for_every_step() {
clear_cached_region();
let re = Regex::new(r"(\w+)").unwrap();
let mut matches = re.find_iter("one two");
assert_eq!(matches.next().unwrap().as_str(), "one");
let first = matches.region.beg.as_ptr();
assert_eq!(matches.next().unwrap().as_str(), "two");
assert_eq!(matches.region.beg.as_ptr(), first);
drop(matches);
let cached = cached_region_buffer().expect("iterator drop should return its region");
assert_eq!(cached.0, first);
}
#[test]
fn regex_new_and_find() {
let re = Regex::new(r"\d+").unwrap();
let m = re.find("hello 42 world").unwrap();
assert_eq!(m.as_str(), "42");
assert_eq!(m.start(), 6);
assert_eq!(m.end(), 8);
assert_eq!(m.range(), 6..8);
assert_eq!(m.len(), 2);
assert!(!m.is_empty());
}
#[test]
fn regex_no_match() {
let re = Regex::new(r"\d+").unwrap();
assert!(re.find("no digits here").is_none());
}
#[test]
fn regex_is_match() {
let re = Regex::new(r"hello").unwrap();
assert!(re.is_match("say hello"));
assert!(!re.is_match("say goodbye"));
}
#[test]
fn search_options_methods_return_matches_and_captures() {
let _lock = crate::regexec::LIMIT_TEST_LOCK.lock().unwrap();
let re = Regex::new(r"(\w+)").unwrap();
let options = SearchOptions::new().timeout(Duration::from_secs(1));
assert_eq!(
re.find_with("hello 42", options).unwrap().unwrap().as_str(),
"hello"
);
assert_eq!(
re.find_bytes_with(b"hello 42", options)
.unwrap()
.unwrap()
.as_str(),
"hello"
);
assert!(re.is_match_with("hello", options).unwrap());
assert!(!re.is_match_bytes_with(b"!!!", options).unwrap());
assert_eq!(
re.captures_with("hello 42", options)
.unwrap()
.unwrap()
.get(1)
.unwrap()
.as_str(),
"hello"
);
assert!(re.captures_bytes_with(b"!!!", options).unwrap().is_none());
}
#[test]
fn search_options_surface_time_limit_errors() {
let _lock = crate::regexec::LIMIT_TEST_LOCK.lock().unwrap();
let re = Regex::new(r"(a+)+b").unwrap();
let text = "a".repeat(40);
let options = SearchOptions::new()
.timeout(Duration::from_millis(1))
.retry_limit_in_match(0)
.retry_limit_in_search(0);
assert!(matches!(
re.find_with(&text, options),
Err(RegexError::TimeLimitOver)
));
assert!(matches!(
re.is_match_with(&text, options),
Err(RegexError::TimeLimitOver)
));
assert!(matches!(
re.captures_with(&text, options),
Err(RegexError::TimeLimitOver)
));
}
#[test]
fn search_options_timeout_spans_every_start_position() {
let _lock = crate::regexec::LIMIT_TEST_LOCK.lock().unwrap();
let re = Regex::new(r"a{1,400}?(?=b)").unwrap();
let text = "a".repeat(500_000);
let options = SearchOptions::new().timeout(Duration::from_millis(10));
let started = std::time::Instant::now();
assert!(matches!(
re.find_with(&text, options),
Err(RegexError::TimeLimitOver)
));
assert!(started.elapsed() < Duration::from_secs(1));
}
#[test]
fn search_options_surface_retry_limit_errors() {
let _lock = crate::regexec::LIMIT_TEST_LOCK.lock().unwrap();
let re = Regex::new(r"(a+)+b").unwrap();
let text = "a".repeat(30);
assert!(matches!(
re.find_with(&text, SearchOptions::new().retry_limit_in_match(1_000)),
Err(RegexError::RetryLimitInMatchOver)
));
assert!(matches!(
re.find_with(
&text,
SearchOptions::new()
.retry_limit_in_match(0)
.retry_limit_in_search(1_000)
),
Err(RegexError::RetryLimitInSearchOver)
));
}
#[test]
fn search_options_leave_unset_limits_process_wide() {
let _lock = crate::regexec::LIMIT_TEST_LOCK.lock().unwrap();
let old_retry = crate::regexec::onig_get_retry_limit_in_match();
crate::regexec::onig_set_retry_limit_in_match(1_000);
let re = Regex::new(r"(a+)+b").unwrap();
let text = "a".repeat(30);
let unset = re.find_with(&text, SearchOptions::new());
let disabled = re.find_with(
&text,
SearchOptions::new()
.retry_limit_in_match(0)
.retry_limit_in_search(0)
.timeout(Duration::from_millis(50)),
);
crate::regexec::onig_set_retry_limit_in_match(old_retry);
assert!(matches!(unset, Err(RegexError::RetryLimitInMatchOver)));
assert!(matches!(disabled, Err(RegexError::TimeLimitOver)));
}
#[test]
fn search_options_iterator_yields_matches_and_then_finishes() {
let _lock = crate::regexec::LIMIT_TEST_LOCK.lock().unwrap();
let re = Regex::new(r"\w+").unwrap();
let options = SearchOptions::new().timeout(Duration::from_secs(1));
let mut matches = re.find_iter_with("one two", options);
assert_eq!(matches.next().unwrap().unwrap().as_str(), "one");
assert_eq!(matches.next().unwrap().unwrap().as_str(), "two");
assert!(matches.next().is_none());
assert!(matches.next().is_none());
let bytes: Vec<_> = re
.find_iter_bytes_with(b"a b", options)
.map(|m| m.unwrap().as_bytes().to_vec())
.collect();
assert_eq!(bytes, vec![b"a".to_vec(), b"b".to_vec()]);
}
#[test]
fn search_options_iterator_surfaces_an_error_once() {
let _lock = crate::regexec::LIMIT_TEST_LOCK.lock().unwrap();
let re = Regex::new(r"(a+)+b").unwrap();
let text = "a".repeat(40);
let options = SearchOptions::new()
.timeout(Duration::from_millis(1))
.retry_limit_in_match(0)
.retry_limit_in_search(0);
let mut matches = re.find_iter_with(&text, options);
assert!(matches!(
matches.next().unwrap(),
Err(RegexError::TimeLimitOver)
));
assert!(matches.next().is_none());
}
#[test]
fn search_options_default_keeps_every_limit_unset() {
assert_eq!(SearchOptions::default(), SearchOptions::new());
assert_send_sync::<SearchOptions>();
assert_send_sync::<TryFindIter<'static, 'static>>();
}
#[test]
fn timeout_is_rounded_up_to_milliseconds() {
assert_eq!(timeout_to_millis(Duration::ZERO), 0);
assert_eq!(timeout_to_millis(Duration::from_nanos(1)), 1);
assert_eq!(timeout_to_millis(Duration::from_millis(1)), 1);
assert_eq!(timeout_to_millis(Duration::from_nanos(1_000_001)), 2);
assert_eq!(timeout_to_millis(Duration::MAX), u64::MAX);
}
#[test]
fn regex_captures() {
let re = Regex::new(r"(\d{4})-(\d{2})-(\d{2})").unwrap();
let caps = re.captures("date: 2026-02-14").unwrap();
assert_eq!(caps.get(0).unwrap().as_str(), "2026-02-14");
assert_eq!(caps.get(1).unwrap().as_str(), "2026");
assert_eq!(caps.get(2).unwrap().as_str(), "02");
assert_eq!(caps.get(3).unwrap().as_str(), "14");
assert!(caps.get(4).is_none());
assert_eq!(caps.len(), 4);
}
#[test]
fn regex_captures_len() {
let re = Regex::new(r"(a)(b)(c)").unwrap();
assert_eq!(re.captures_len(), 3);
}
#[test]
fn regex_find_iter() {
let re = Regex::new(r"\d+").unwrap();
let matches: Vec<&str> = re.find_iter("1 + 22 = 333").map(|m| m.as_str()).collect();
assert_eq!(matches, vec!["1", "22", "333"]);
}
#[test]
fn regex_builder_case_insensitive() {
let re = Regex::builder(r"hello")
.case_insensitive(true)
.build()
.unwrap();
assert!(re.is_match("HELLO"));
assert!(re.is_match("Hello"));
}
#[test]
fn regex_invalid_pattern() {
let err = Regex::new(r"(unclosed").unwrap_err();
assert!(matches!(err, RegexError::Syntax { .. }));
}
#[test]
fn match_as_bytes() {
let re = Regex::new(r"world").unwrap();
let m = re.find("hello world").unwrap();
assert_eq!(m.as_bytes(), b"world");
}
#[test]
fn captures_iter() {
let re = Regex::new(r"(a)(b)?").unwrap();
let caps = re.captures("a").unwrap();
let items: Vec<_> = caps.iter().collect();
assert_eq!(items.len(), 3);
assert!(items[0].is_some());
assert!(items[1].is_some());
assert!(items[2].is_none());
}
#[test]
fn named_captures() {
let re = Regex::new(r"(?<year>\d{4})-(?<month>\d{2})").unwrap();
let caps = re.captures("2026-02").unwrap();
assert_eq!(caps.name("year").unwrap().as_str(), "2026");
assert_eq!(caps.name("month").unwrap().as_str(), "02");
assert!(caps.name("day").is_none());
}
#[test]
fn empty_match_find_iter() {
let re = Regex::new(r"").unwrap();
let matches: Vec<_> = re.find_iter("ab").collect();
assert_eq!(matches.len(), 3);
assert_eq!(matches[0].start(), 0);
assert_eq!(matches[1].start(), 1);
assert_eq!(matches[2].start(), 2);
}
}