use std::cell::RefCell;
use std::fmt;
use std::ops::{Index, Range};
use std::panic::AssertUnwindSafe;
use std::str::FromStr;
use std::sync::Arc;
use std::time::Duration;
use crate::encodings::utf8::ONIG_ENCODING_UTF8;
use crate::error::RegexError;
use crate::oniguruma::*;
use crate::regcomp::{onig_new, onig_new_with_backtracking_optimization};
use crate::regexec::{
MatchArg, OnigMatchParam, cache_msa, onig_name_to_backref_number, onig_new_match_param,
onig_search, onig_search_bounds, onig_search_with_param, take_cached_msa,
};
use crate::regint::RegexType;
use crate::regparse_types::NameTable;
use crate::regsyntax::{
OnigSyntaxASIS, OnigSyntaxEmacs, OnigSyntaxGnuRegex, OnigSyntaxGrep, OnigSyntaxJava,
OnigSyntaxOniguruma, OnigSyntaxPerl, OnigSyntaxPerl_NG, OnigSyntaxPosixBasic,
OnigSyntaxPosixExtended, OnigSyntaxPython, OnigSyntaxRuby,
};
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);
}
});
}
#[track_caller]
fn check_str_start(text: &str, start: usize) {
check_bytes_start(text.as_bytes(), start);
assert!(
text.is_char_boundary(start),
"start {start} is not a char boundary of the text"
);
}
#[track_caller]
fn check_bytes_start(text: &[u8], start: usize) {
assert!(
start <= text.len(),
"start {start} is past the end of the text ({} bytes)",
text.len()
);
}
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
}
}
struct Compiled {
program: AssertUnwindSafe<RegexType>,
pattern: Box<[u8]>,
}
#[derive(Clone)]
pub struct Regex {
inner: Arc<Compiled>,
}
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 program = onig_new(
pattern,
ONIG_OPTION_NONE,
&ONIG_ENCODING_UTF8,
&OnigSyntaxOniguruma,
)?;
Ok(Self::from_parts(program, pattern.into()))
}
fn from_parts(program: RegexType, pattern: Box<[u8]>) -> Regex {
Regex {
inner: Arc::new(Compiled {
program: AssertUnwindSafe(program),
pattern,
}),
}
}
fn program(&self) -> &RegexType {
&self.inner.program
}
pub fn as_str(&self) -> Option<&str> {
std::str::from_utf8(&self.inner.pattern).ok()
}
pub fn as_bytes(&self) -> &[u8] {
&self.inner.pattern
}
pub fn capture_names(&self) -> CaptureNames<'_> {
CaptureNames {
table: self.program().name_table.as_ref(),
next_group: 0,
end_group: self.captures_len() as i32 + 1,
}
}
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>> {
self.find_from(text, 0)
}
pub fn find_at<'t>(&self, text: &'t str, start: usize) -> Option<Match<'t>> {
check_str_start(text, start);
self.find_from(text.as_bytes(), start)
}
pub fn find_bytes_at<'t>(&self, text: &'t [u8], start: usize) -> Option<Match<'t>> {
check_bytes_start(text, start);
self.find_from(text, start)
}
#[inline]
fn find_from<'t>(&self, text: &'t [u8], start: usize) -> Option<Match<'t>> {
if !self.program().keep_moves_match_start
&& !self.program().options.contains(ONIG_OPTION_FIND_LONGEST)
{
let mut msa = take_cached_msa(self.program(), ONIG_OPTION_NONE, None, start);
let found = onig_search_bounds(self.program(), text, start, &mut msa);
cache_msa(msa);
let (match_start, match_end) = found.ok()??;
return (match_end <= text.len()).then_some(Match {
text,
start: match_start,
end: match_end,
});
}
let (result, region) = onig_search(
self.program(),
text,
text.len(),
start,
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> {
self.find_from_with(text, 0, options)
}
pub fn find_at_with<'t>(
&self,
text: &'t str,
start: usize,
options: SearchOptions,
) -> Result<Option<Match<'t>>, RegexError> {
check_str_start(text, start);
self.find_from_with(text.as_bytes(), start, options)
}
pub fn find_bytes_at_with<'t>(
&self,
text: &'t [u8],
start: usize,
options: SearchOptions,
) -> Result<Option<Match<'t>>, RegexError> {
check_bytes_start(text, start);
self.find_from_with(text, start, options)
}
fn find_from_with<'t>(
&self,
text: &'t [u8],
start: usize,
options: SearchOptions,
) -> Result<Option<Match<'t>>, RegexError> {
let (result, region) =
self.search_with(text, start, 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 {
self.is_match_from(text, 0)
}
pub fn is_match_at(&self, text: &str, start: usize) -> bool {
check_str_start(text, start);
self.is_match_from(text.as_bytes(), start)
}
pub fn is_match_bytes_at(&self, text: &[u8], start: usize) -> bool {
check_bytes_start(text, start);
self.is_match_from(text, start)
}
#[inline]
fn is_match_from(&self, text: &[u8], start: usize) -> bool {
let (result, _) = onig_search(
self.program(),
text,
text.len(),
start,
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> {
self.is_match_from_with(text, 0, options)
}
pub fn is_match_at_with(
&self,
text: &str,
start: usize,
options: SearchOptions,
) -> Result<bool, RegexError> {
check_str_start(text, start);
self.is_match_from_with(text.as_bytes(), start, options)
}
pub fn is_match_bytes_at_with(
&self,
text: &[u8],
start: usize,
options: SearchOptions,
) -> Result<bool, RegexError> {
check_bytes_start(text, start);
self.is_match_from_with(text, start, options)
}
fn is_match_from_with(
&self,
text: &[u8],
start: usize,
options: SearchOptions,
) -> Result<bool, RegexError> {
let (result, _) = self.search_with(text, start, text.len(), None, options)?;
Ok(result >= 0)
}
pub fn captures<'t>(&self, text: &'t str) -> Option<Captures<'t>> {
self.captures_bytes(text.as_bytes())
}
pub fn captures_bytes<'t>(&self, text: &'t [u8]) -> Option<Captures<'t>> {
self.captures_from(text, 0)
}
pub fn captures_at<'t>(&self, text: &'t str, start: usize) -> Option<Captures<'t>> {
check_str_start(text, start);
self.captures_from(text.as_bytes(), start)
}
pub fn captures_bytes_at<'t>(&self, text: &'t [u8], start: usize) -> Option<Captures<'t>> {
check_bytes_start(text, start);
self.captures_from(text, start)
}
#[inline]
fn captures_from<'t>(&self, text: &'t [u8], start: usize) -> Option<Captures<'t>> {
let (result, region) = onig_search(
self.program(),
text,
text.len(),
start,
text.len(),
Some(take_cached_region()),
ONIG_OPTION_NONE,
);
let region = region?;
if result < 0 {
cache_region(region);
return None;
}
Some(Captures {
text,
region,
compiled: Arc::clone(&self.inner),
})
}
pub fn captures_with<'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>(
&self,
text: &'t [u8],
options: SearchOptions,
) -> Result<Option<Captures<'t>>, RegexError> {
self.captures_from_with(text, 0, options)
}
pub fn captures_at_with<'t>(
&self,
text: &'t str,
start: usize,
options: SearchOptions,
) -> Result<Option<Captures<'t>>, RegexError> {
check_str_start(text, start);
self.captures_from_with(text.as_bytes(), start, options)
}
pub fn captures_bytes_at_with<'t>(
&self,
text: &'t [u8],
start: usize,
options: SearchOptions,
) -> Result<Option<Captures<'t>>, RegexError> {
check_bytes_start(text, start);
self.captures_from_with(text, start, options)
}
fn captures_from_with<'t>(
&self,
text: &'t [u8],
start: usize,
options: SearchOptions,
) -> Result<Option<Captures<'t>>, RegexError> {
let (result, region) =
self.search_with(text, start, 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,
compiled: Arc::clone(&self.inner),
}))
}
pub fn find_iter<'r, 't>(&'r self, text: &'t str) -> FindIter<'r, 't> {
self.find_iter_bytes(text.as_bytes())
}
pub fn find_iter_bytes<'r, 't>(&'r self, text: &'t [u8]) -> FindIter<'r, 't> {
let bounds_only = !self.program().keep_moves_match_start
&& !self.program().options.contains(ONIG_OPTION_FIND_LONGEST);
FindIter {
regex: self,
text,
cursor: MatchCursor::default(),
region: if bounds_only {
OnigRegion::new()
} else {
take_cached_region()
},
msa: bounds_only.then(|| take_cached_msa(self.program(), ONIG_OPTION_NONE, None, 0)),
}
}
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,
cursor: MatchCursor::default(),
region: take_cached_region(),
options,
finished: false,
}
}
pub fn captures_iter<'r, 't>(&'r self, text: &'t str) -> CaptureMatches<'r, 't> {
self.captures_iter_bytes(text.as_bytes())
}
pub fn captures_iter_bytes<'r, 't>(&'r self, text: &'t [u8]) -> CaptureMatches<'r, 't> {
CaptureMatches {
regex: self,
text,
cursor: MatchCursor::default(),
region: take_cached_region(),
}
}
pub fn captures_iter_with<'r, 't>(
&'r self,
text: &'t str,
options: SearchOptions,
) -> TryCaptureMatches<'r, 't> {
self.captures_iter_bytes_with(text.as_bytes(), options)
}
pub fn captures_iter_bytes_with<'r, 't>(
&'r self,
text: &'t [u8],
options: SearchOptions,
) -> TryCaptureMatches<'r, 't> {
TryCaptureMatches {
regex: self,
text,
cursor: MatchCursor::default(),
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.program(),
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.program().num_mem as usize
}
pub fn backtracking_warnings(&self) -> &[crate::backtrack_lint::BacktrackWarning] {
&self.program().backtrack_warnings
}
pub fn backtracking_rewrites(&self) -> &[crate::backtrack_rewrite::BacktrackingRewrite] {
&self.program().backtrack_rewrites
}
pub fn as_raw(&self) -> &RegexType {
self.program()
}
}
impl fmt::Debug for Regex {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Regex")
.field("pattern", &String::from_utf8_lossy(&self.inner.pattern))
.finish_non_exhaustive()
}
}
impl fmt::Display for Regex {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&String::from_utf8_lossy(&self.inner.pattern))
}
}
impl FromStr for Regex {
type Err = RegexError;
fn from_str(pattern: &str) -> Result<Self, Self::Err> {
Regex::new(pattern)
}
}
pub struct RegexBuilder {
pattern: Vec<u8>,
options: OnigOptionType,
syntax: &'static OnigSyntaxType,
reject_backtracking_risks: bool,
optimize_backtracking: bool,
}
impl RegexBuilder {
pub fn new(pattern: &str) -> Self {
Self::new_bytes(pattern.as_bytes())
}
pub fn new_bytes(pattern: &[u8]) -> Self {
RegexBuilder {
pattern: pattern.to_vec(),
options: ONIG_OPTION_NONE,
syntax: &OnigSyntaxOniguruma,
reject_backtracking_risks: false,
optimize_backtracking: false,
}
}
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 clear_option(mut self, flag: OnigOptionType) -> Self {
self.options &= !flag;
self
}
pub fn capture_group(mut self, yes: bool) -> Self {
if yes {
self.options |= ONIG_OPTION_CAPTURE_GROUP;
self.options &= !ONIG_OPTION_DONT_CAPTURE_GROUP;
} else {
self.options &= !ONIG_OPTION_CAPTURE_GROUP;
}
self
}
pub fn syntax(mut self, syntax: &'static OnigSyntaxType) -> Self {
self.syntax = syntax;
self
}
pub fn syntax_mode(mut self, syntax: Syntax) -> Self {
self.syntax = syntax.as_onig_syntax();
self
}
pub fn reject_backtracking_risks(mut self, yes: bool) -> Self {
self.reject_backtracking_risks = yes;
self
}
pub fn optimize_backtracking(mut self, yes: bool) -> Self {
self.optimize_backtracking = yes;
self
}
pub fn build(self) -> Result<Regex, RegexError> {
let inner = onig_new_with_backtracking_optimization(
&self.pattern,
self.options,
&ONIG_ENCODING_UTF8,
self.syntax,
self.optimize_backtracking,
)?;
if self.reject_backtracking_risks && !inner.backtrack_warnings.is_empty() {
return Err(ONIGERR_VERY_INEFFICIENT_PATTERN.into());
}
Ok(Regex::from_parts(inner, self.pattern.into_boxed_slice()))
}
}
impl fmt::Debug for RegexBuilder {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
const BUILT_IN: [Syntax; 12] = [
Syntax::Oniguruma,
Syntax::Asis,
Syntax::PosixBasic,
Syntax::PosixExtended,
Syntax::Emacs,
Syntax::Grep,
Syntax::GnuRegex,
Syntax::Java,
Syntax::Perl,
Syntax::PerlNg,
Syntax::Ruby,
Syntax::Python,
];
let syntax = BUILT_IN
.into_iter()
.find(|syntax| std::ptr::eq(syntax.as_onig_syntax(), self.syntax));
f.debug_struct("RegexBuilder")
.field("pattern", &String::from_utf8_lossy(&self.pattern))
.field("options", &self.options)
.field("syntax", &syntax)
.field("reject_backtracking_risks", &self.reject_backtracking_risks)
.field("optimize_backtracking", &self.optimize_backtracking)
.finish()
}
}
impl Regex {
pub fn builder_bytes(pattern: &[u8]) -> RegexBuilder {
RegexBuilder::new_bytes(pattern)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
#[non_exhaustive]
pub enum Syntax {
#[default]
Oniguruma,
Asis,
PosixBasic,
PosixExtended,
Emacs,
Grep,
GnuRegex,
Java,
Perl,
PerlNg,
Ruby,
Python,
}
impl Syntax {
pub(crate) fn as_onig_syntax(&self) -> &'static OnigSyntaxType {
match self {
Self::Oniguruma => &OnigSyntaxOniguruma,
Self::Asis => &OnigSyntaxASIS,
Self::PosixBasic => &OnigSyntaxPosixBasic,
Self::PosixExtended => &OnigSyntaxPosixExtended,
Self::Emacs => &OnigSyntaxEmacs,
Self::Grep => &OnigSyntaxGrep,
Self::GnuRegex => &OnigSyntaxGnuRegex,
Self::Java => &OnigSyntaxJava,
Self::Perl => &OnigSyntaxPerl,
Self::PerlNg => &OnigSyntaxPerl_NG,
Self::Ruby => &OnigSyntaxRuby,
Self::Python => &OnigSyntaxPython,
}
}
}
#[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,
compiled: Arc<Compiled>,
}
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.compiled.program,
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 Index<usize> for Captures<'_> {
type Output = str;
fn index(&self, i: usize) -> &str {
match self.get(i) {
Some(m) => m.as_str(),
None => panic!("no group at index '{i}'"),
}
}
}
impl<'n> Index<&'n str> for Captures<'_> {
type Output = str;
fn index(&self, name: &'n str) -> &str {
match self.name(name) {
Some(m) => m.as_str(),
None => panic!("no group named '{name}'"),
}
}
}
impl fmt::Debug for Captures<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> 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<'_, '_> {}
impl fmt::Debug for CapturesIter<'_, '_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("CapturesIter").finish_non_exhaustive()
}
}
pub struct CaptureNames<'r> {
table: Option<&'r NameTable>,
next_group: i32,
end_group: i32,
}
impl<'r> CaptureNames<'r> {
fn name_of(&self, group: i32) -> Option<&'r str> {
let table = self.table?;
let entry = table
.entries
.values()
.find(|entry| entry.back_refs.contains(&group))?;
std::str::from_utf8(&entry.name).ok()
}
}
impl<'r> Iterator for CaptureNames<'r> {
type Item = Option<&'r str>;
fn next(&mut self) -> Option<Self::Item> {
if self.next_group >= self.end_group {
return None;
}
let group = self.next_group;
self.next_group += 1;
Some(self.name_of(group))
}
fn size_hint(&self) -> (usize, Option<usize>) {
let remaining = (self.end_group - self.next_group) as usize;
(remaining, Some(remaining))
}
}
impl ExactSizeIterator for CaptureNames<'_> {}
impl std::iter::FusedIterator for CaptureNames<'_> {}
impl fmt::Debug for CaptureNames<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("CaptureNames").finish_non_exhaustive()
}
}
#[derive(Clone, Copy, Debug, Default)]
struct MatchCursor {
last_end: usize,
last_was_empty: bool,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum Verdict {
Yield,
Retry,
Finish,
}
impl MatchCursor {
#[inline]
fn accept(&mut self, program: &RegexType, text: &[u8], start: usize, end: usize) -> Verdict {
if start == end {
if self.last_was_empty {
if self.last_end >= text.len() {
return Verdict::Finish;
}
self.last_end += program.enc.mbc_enc_len(&text[self.last_end..]);
self.last_was_empty = false;
return Verdict::Retry;
}
self.last_was_empty = true;
} else {
self.last_was_empty = false;
}
self.last_end = end;
Verdict::Yield
}
}
pub struct FindIter<'r, 't> {
regex: &'r Regex,
text: &'t [u8],
cursor: MatchCursor,
region: OnigRegion,
msa: Option<Box<MatchArg>>,
}
impl<'r, 't> Iterator for FindIter<'r, 't> {
type Item = Match<'t>;
fn next(&mut self) -> Option<Match<'t>> {
if self.cursor.last_end > self.text.len() {
return None;
}
let m = if let Some(msa) = self.msa.as_deref_mut() {
let (start, end) =
onig_search_bounds(self.regex.program(), self.text, self.cursor.last_end, msa)
.ok()??;
(end <= self.text.len()).then_some(Match {
text: self.text,
start,
end,
})?
} else {
let (result, region) = onig_search(
self.regex.program(),
self.text,
self.text.len(),
self.cursor.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;
}
Match::from_region(self.text, self.region.beg[0], self.region.end[0])?
};
match self
.cursor
.accept(self.regex.program(), self.text, m.start, m.end)
{
Verdict::Yield => Some(m),
Verdict::Retry => self.next(),
Verdict::Finish => None,
}
}
}
impl Drop for FindIter<'_, '_> {
fn drop(&mut self) {
match self.msa.take() {
Some(msa) => cache_msa(msa),
None => cache_region(std::mem::take(&mut self.region)),
}
}
}
impl fmt::Debug for FindIter<'_, '_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("FindIter").finish_non_exhaustive()
}
}
pub struct TryFindIter<'r, 't> {
regex: &'r Regex,
text: &'t [u8],
cursor: MatchCursor,
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.cursor.last_end > self.text.len() {
self.finished = true;
return None;
}
let search = self.regex.search_with(
self.text,
self.cursor.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;
};
match self
.cursor
.accept(self.regex.program(), self.text, m.start, m.end)
{
Verdict::Yield => Some(Ok(m)),
Verdict::Retry => self.next(),
Verdict::Finish => {
self.finished = true;
None
}
}
}
}
impl std::iter::FusedIterator for TryFindIter<'_, '_> {}
impl Drop for TryFindIter<'_, '_> {
fn drop(&mut self) {
cache_region(std::mem::take(&mut self.region));
}
}
impl fmt::Debug for TryFindIter<'_, '_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("TryFindIter").finish_non_exhaustive()
}
}
pub struct CaptureMatches<'r, 't> {
regex: &'r Regex,
text: &'t [u8],
cursor: MatchCursor,
region: OnigRegion,
}
impl<'r, 't> Iterator for CaptureMatches<'r, 't> {
type Item = Captures<'t>;
fn next(&mut self) -> Option<Captures<'t>> {
if self.cursor.last_end > self.text.len() {
return None;
}
let (result, region) = onig_search(
self.regex.program(),
self.text,
self.text.len(),
self.cursor.last_end,
self.text.len(),
Some(std::mem::take(&mut self.region)),
ONIG_OPTION_NONE,
);
let region = region?;
if result < 0 || region.num_regs < 1 {
self.region = region;
return None;
}
let Some(m) = Match::from_region(self.text, region.beg[0], region.end[0]) else {
self.region = region;
return None;
};
match self
.cursor
.accept(self.regex.program(), self.text, m.start, m.end)
{
Verdict::Yield => Some(Captures {
text: self.text,
region,
compiled: Arc::clone(&self.regex.inner),
}),
Verdict::Retry => {
self.region = region;
self.next()
}
Verdict::Finish => {
self.region = region;
None
}
}
}
}
impl Drop for CaptureMatches<'_, '_> {
fn drop(&mut self) {
cache_region(std::mem::take(&mut self.region));
}
}
impl fmt::Debug for CaptureMatches<'_, '_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("CaptureMatches").finish_non_exhaustive()
}
}
pub struct TryCaptureMatches<'r, 't> {
regex: &'r Regex,
text: &'t [u8],
cursor: MatchCursor,
region: OnigRegion,
options: SearchOptions,
finished: bool,
}
impl<'r, 't> Iterator for TryCaptureMatches<'r, 't> {
type Item = Result<Captures<'t>, RegexError>;
fn next(&mut self) -> Option<Self::Item> {
if self.finished || self.cursor.last_end > self.text.len() {
self.finished = true;
return None;
}
let search = self.regex.search_with(
self.text,
self.cursor.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;
};
if result < 0 || region.num_regs < 1 {
self.finished = true;
self.region = region;
return None;
}
let Some(m) = Match::from_region(self.text, region.beg[0], region.end[0]) else {
self.finished = true;
self.region = region;
return None;
};
match self
.cursor
.accept(self.regex.program(), self.text, m.start, m.end)
{
Verdict::Yield => Some(Ok(Captures {
text: self.text,
region,
compiled: Arc::clone(&self.regex.inner),
})),
Verdict::Retry => {
self.region = region;
self.next()
}
Verdict::Finish => {
self.finished = true;
self.region = region;
None
}
}
}
}
impl std::iter::FusedIterator for TryCaptureMatches<'_, '_> {}
impl Drop for TryCaptureMatches<'_, '_> {
fn drop(&mut self) {
cache_region(std::mem::take(&mut self.region));
}
}
impl fmt::Debug for TryCaptureMatches<'_, '_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("TryCaptureMatches").finish_non_exhaustive()
}
}
#[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 public_types_implement_debug() {
use crate::scanner::{OnigString, Scanner, ScannerConfig, ScannerPatternCache};
let _limits = crate::regexec::shared_limits();
let re = Regex::new(r"(a)(?<n>b)").unwrap();
let text = "ab ab";
let captures = re.captures(text).unwrap();
let config = ScannerConfig::default();
let mut cache = ScannerPatternCache::new();
let _top =
Scanner::with_pattern_cache(&[r"\bfn\b", r#""[^"]*""#], &config, &mut cache).unwrap();
let _call =
Scanner::with_pattern_cache(&[r"\)", r#""[^"]*""#], &config, &mut cache).unwrap();
let scanner = Scanner::new(&[r"\d+", "[a-z]+"]).unwrap();
let custom_syntax: &'static OnigSyntaxType =
Box::leak(Box::new(crate::regsyntax::OnigSyntaxRuby.clone()));
let shown: [(String, &str); 15] = [
(
format!("{:?}", Regex::builder("a+").syntax_mode(Syntax::Ruby)),
"pattern: \"a+\", options:",
),
(
format!("{:?}", Regex::builder("a+").syntax(custom_syntax)),
"syntax: None",
),
(format!("{:?}", captures.iter()), "CapturesIter { .. }"),
(format!("{:?}", re.capture_names()), "CaptureNames { .. }"),
(format!("{:?}", re.find_iter(text)), "FindIter { .. }"),
(
format!("{:?}", re.find_iter_with(text, SearchOptions::new())),
"TryFindIter { .. }",
),
(
format!("{:?}", re.captures_iter(text)),
"CaptureMatches { .. }",
),
(
format!("{:?}", re.captures_iter_with(text, SearchOptions::new())),
"TryCaptureMatches { .. }",
),
(format!("{:?}", re.split(text)), "Split { .. }"),
(format!("{:?}", re.splitn(text, 2)), "SplitN { .. }"),
(
format!("{:?}", re.split_bytes(text.as_bytes())),
"SplitBytes { .. }",
),
(
format!("{:?}", re.splitn_bytes(text.as_bytes(), 2)),
"SplitNBytes { .. }",
),
(format!("{scanner:?}"), "Scanner { patterns: 2,"),
(format!("{cache:?}"), "ScannerPatternCache { len: 3, .. }"),
(
format!("{:?}", OnigString::new("a💻b")),
"OnigString { content: \"a💻b\", .. }",
),
];
for (shown, expected) in &shown {
assert!(
shown.contains(*expected),
"{shown} does not contain {expected}"
);
}
}
#[test]
fn find_reuses_the_thread_local_region_buffer() {
let _limits = crate::regexec::shared_limits();
clear_cached_region();
let re = Regex::new(r"(a)(b)\K(c)").unwrap();
assert_eq!(re.find("abc").unwrap().as_str(), "c");
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(), "c");
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() {
let _limits = crate::regexec::shared_limits();
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() {
let _limits = crate::regexec::shared_limits();
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() {
let _limits = crate::regexec::shared_limits();
clear_cached_region();
let re = Regex::new(r"(\w)\K\w+").unwrap();
let mut matches = re.find_iter("one two");
assert!(matches.msa.is_none());
assert_eq!(matches.next().unwrap().as_str(), "ne");
let first = matches.region.beg.as_ptr();
assert_eq!(matches.next().unwrap().as_str(), "wo");
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 find_iter_reuses_one_match_arg_without_a_region() {
let _limits = crate::regexec::shared_limits();
let re = Regex::new(r"(\w+)").unwrap();
let mut matches = re.find_iter("one two");
let msa: *const MatchArg = &**matches.msa.as_ref().expect("bounds-only iterator");
assert_eq!(matches.next().unwrap().as_str(), "one");
assert_eq!(matches.next().unwrap().as_str(), "two");
assert!(matches.next().is_none());
assert!(std::ptr::eq(&**matches.msa.as_ref().unwrap(), msa));
assert!(matches.region.beg.is_empty(), "no region was requested");
}
#[test]
fn find_iter_bounds_match_the_region_path() {
let _limits = crate::regexec::shared_limits();
let patterns = [
r"\d+",
r"(\w+)",
r"(a)|b",
r"\b(\w+)\s+\1\b",
r"(?<=@)\w+",
r"\w+(?=\()",
r"x*",
r"",
r"^",
r"$",
r"(?m)^\s*$",
r"alpha|beta|gamma|delta",
r"(alpha|beta|gamma|delta)",
r"(?i)(?:error|warn|fatal|panic)",
r"\b(?:if|else|for|while)\b",
r"(?>a+)b|a",
r"(a|ab)(c|bcd)(d*)",
r"[\w.+-]+@\w+\.\w+",
r"\p{L}+",
r"é|e",
r".*",
r"(?<n>\d)(?<m>\d)?",
];
let texts = [
"",
"a",
"one two two three",
"x@y.z foo(1) bar( a@b.cd",
"alpha beta gammadelta ERROR warn Panic",
"if else\nfor \n\nwhile",
"aab abcd abcbcdd",
"Grüße café éte 12 345",
"xxaxxb",
];
for pattern in patterns {
let re = Regex::new(pattern).unwrap();
for text in texts {
let bounds = re.find_iter(text);
assert!(bounds.msa.is_some(), "{pattern}");
let region_path = FindIter {
regex: &re,
text: text.as_bytes(),
cursor: MatchCursor::default(),
region: OnigRegion::new(),
msa: None,
};
let got: Vec<_> = bounds.map(|m| m.range()).collect();
let expected: Vec<_> = region_path.map(|m| m.range()).collect();
assert_eq!(got, expected, "{pattern:?} {text:?}");
assert_eq!(
re.find(text).map(|m| m.range()),
expected.first().cloned(),
"{pattern:?} {text:?}"
);
}
}
let keep = Regex::new(r"(\w)\K\w+").unwrap();
assert_eq!(keep.find("one two").map(|m| m.range()), Some(1..3));
}
#[test]
fn regex_new_and_find() {
let _limits = crate::regexec::shared_limits();
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 _limits = crate::regexec::shared_limits();
let re = Regex::new(r"\d+").unwrap();
assert!(re.find("no digits here").is_none());
}
#[test]
fn regex_is_match() {
let _limits = crate::regexec::shared_limits();
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 _limits = crate::regexec::shared_limits();
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 _limits = crate::regexec::shared_limits();
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 _limits = crate::regexec::shared_limits();
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 _limits = crate::regexec::shared_limits();
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 _limits = crate::regexec::exclusive_limits();
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 _limits = crate::regexec::shared_limits();
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 _limits = crate::regexec::shared_limits();
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 _limits = crate::regexec::shared_limits();
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 _limits = crate::regexec::shared_limits();
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 _limits = crate::regexec::shared_limits();
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 _limits = crate::regexec::shared_limits();
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 _limits = crate::regexec::shared_limits();
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 _limits = crate::regexec::shared_limits();
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 _limits = crate::regexec::shared_limits();
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);
}
}