use crate::error::{Error, fault};
use crate::matches::{CaptureMatches, Captures, GroupSpans, Match, Matches, Split, crate_sequence};
use crate::pool::{Lease, Pool, Recipe};
use crate::sys;
use std::ptr::NonNull;
const FIRST_WINDOW: usize = 4096;
pub struct Regex {
pool: Pool<Spell>,
groups: Result<usize, Error>,
names: Box<[(Box<str>, usize)]>,
}
impl Regex {
pub fn new(pattern: &str) -> Result<Self, Error> {
RegexBuilder::new(pattern).build()
}
fn compile(pattern: &str, flags: u32) -> Result<Self, Error> {
let pool = Pool::new(Spell {
pattern: pattern.into(),
flags,
})?;
let lease = pool.lease()?;
let mut count: u32 = 0;
let status = unsafe { sys::irgx_group_count(lease.raw(), &raw mut count) };
let groups = if status < 0 {
Err(fault(status, |status, detail| Error::Groups {
pattern: pattern.to_owned(),
status,
detail,
}))
} else {
Ok(count as usize)
};
let names = match groups {
Ok(n) if n > 0 => name_table(&lease, count),
_ => Box::default(),
};
drop(lease);
Ok(Self {
pool,
groups,
names,
})
}
#[must_use]
pub fn as_str(&self) -> &str {
&self.pool.recipe().pattern
}
#[must_use]
pub fn groups(&self) -> Option<usize> {
self.groups.as_ref().ok().copied()
}
pub fn group_names(&self) -> impl ExactSizeIterator<Item = (&str, usize)> {
self.names.iter().map(|(name, at)| (&**name, *at))
}
#[must_use]
pub fn group_index(&self, name: &str) -> Option<usize> {
self.names
.iter()
.find(|(known, _)| &**known == name)
.map(|(_, at)| *at)
}
#[must_use]
pub fn windows(&self) -> bool {
self.pool.lease().is_ok_and(|lease| {
let windows = unsafe { sys::irgx_pattern_windows(lease.raw()) };
windows == 1
})
}
#[must_use]
pub fn earliest(&self) -> bool {
self.pool.lease().is_ok_and(|lease| {
let earliest = unsafe { sys::irgx_pattern_earliest(lease.raw()) };
earliest == 1
})
}
pub(crate) fn with_handle<T>(&self, f: impl FnOnce(*mut sys::Regex) -> T) -> Result<T, Error> {
let lease = self.pool.lease()?;
Ok(f(lease.raw()))
}
#[must_use]
pub fn is_match(&self, text: &str) -> bool {
expect(self.try_is_match(text))
}
pub fn try_is_match(&self, text: &str) -> Result<bool, Error> {
let lease = self.pool.lease()?;
let body = text.as_bytes();
let status = unsafe { sys::irgx_is_match(lease.raw(), body.as_ptr(), body.len()) };
if status < 0 {
return Err(fault(status, |status, detail| Error::Search {
status,
detail,
}));
}
Ok(status == sys::MATCH)
}
#[must_use]
pub fn find<'t>(&self, text: &'t str) -> Option<Match<'t>> {
expect(self.try_find(text))
}
pub fn try_find<'t>(&self, text: &'t str) -> Result<Option<Match<'t>>, Error> {
let Some(span) = self.first(text, 0)? else {
return Ok(None);
};
let (start, end) = self.checked(text, span)?;
Ok(Some(Match::new(text, start, end)))
}
#[must_use]
pub fn find_at<'t>(&self, text: &'t str, start: usize) -> Option<Match<'t>> {
expect(self.try_find_at(text, start))
}
pub fn try_find_at<'t>(&self, text: &'t str, start: usize) -> Result<Option<Match<'t>>, Error> {
self.reachable(text, start)?;
let Some(span) = self.first(text, start)? else {
return Ok(None);
};
let (from, end) = self.checked(text, span)?;
Ok(Some(Match::new(text, from, end)))
}
#[must_use]
pub fn is_match_at(&self, text: &str, start: usize) -> bool {
expect(self.try_is_match_at(text, start))
}
pub fn try_is_match_at(&self, text: &str, start: usize) -> Result<bool, Error> {
self.reachable(text, start)?;
let lease = self.pool.lease()?;
let body = text.as_bytes();
let status = unsafe {
sys::irgx_is_match_in(lease.raw(), body.as_ptr(), body.len(), start, body.len())
};
if status < 0 {
return Err(fault(status, |status, detail| Error::Search {
status,
detail,
}));
}
Ok(status == sys::MATCH)
}
#[must_use]
pub fn is_match_within(&self, text: &str, start: usize, end: usize) -> bool {
expect(self.try_is_match_within(text, start, end))
}
pub fn try_is_match_within(&self, text: &str, start: usize, end: usize) -> Result<bool, Error> {
self.reachable(text, start)?;
self.reachable(text, end)?;
if end < start {
return Err(Error::BadWindow { start, end });
}
let lease = self.pool.lease()?;
let body = text.as_bytes();
let status =
unsafe { sys::irgx_is_match_in(lease.raw(), body.as_ptr(), body.len(), start, end) };
if status < 0 {
return Err(fault(status, |status, detail| Error::Search {
status,
detail,
}));
}
Ok(status == sys::MATCH)
}
fn reachable(&self, text: &str, start: usize) -> Result<(), Error> {
if text.is_char_boundary(start) {
return Ok(());
}
Err(Error::NotCharBoundary { offset: start })
}
#[must_use]
pub fn find_iter<'t>(&self, text: &'t str) -> Matches<'t> {
expect(self.try_find_iter(text))
}
pub fn try_find_iter<'t>(&self, text: &'t str) -> Result<Matches<'t>, Error> {
Ok(Matches::new(text, self.find_all(text)?))
}
#[must_use]
pub fn captures<'r, 't>(&'r self, text: &'t str) -> Option<Captures<'r, 't>> {
expect(self.try_captures(text))
}
pub fn try_captures<'r, 't>(
&'r self,
text: &'t str,
) -> Result<Option<Captures<'r, 't>>, Error> {
let Some(found) = self.try_find(text)? else {
return Ok(None);
};
let spans = self.captures_at(text, found.start(), found.end())?;
Ok(Some(Captures::new(self, text, spans)))
}
#[must_use]
pub fn captures_iter<'r, 't>(&'r self, text: &'t str) -> CaptureMatches<'r, 't> {
expect(self.try_captures_iter(text))
}
pub fn try_captures_iter<'r, 't>(
&'r self,
text: &'t str,
) -> Result<CaptureMatches<'r, 't>, Error> {
self.groups.clone()?;
Ok(CaptureMatches::new(self, text, self.find_all(text)?))
}
#[must_use]
pub fn split<'t>(&self, text: &'t str) -> Split<'t> {
Split::new(text, self.find_iter(text), usize::MAX)
}
#[must_use]
pub fn splitn<'t>(&self, text: &'t str, limit: usize) -> Split<'t> {
Split::new(text, self.find_iter(text), limit)
}
fn scan(&self, text: &str, from: usize, out: &mut [sys::Span]) -> Result<usize, Error> {
let lease = self.pool.lease()?;
let body = text.as_bytes();
let mut written: usize = 0;
let status = unsafe {
sys::irgx_find_all_in(
lease.raw(),
body.as_ptr(),
body.len(),
from,
body.len(),
out.as_mut_ptr(),
out.len(),
&raw mut written,
)
};
if status < 0 {
return Err(fault(status, |status, detail| Error::Search {
status,
detail,
}));
}
Ok(written)
}
fn first(&self, text: &str, from: usize) -> Result<Option<sys::Span>, Error> {
let lease = self.pool.lease()?;
let body = text.as_bytes();
let mut span = sys::Span::default();
let status = unsafe {
sys::irgx_find_first_in(
lease.raw(),
body.as_ptr(),
body.len(),
from,
body.len(),
&raw mut span,
)
};
if status < 0 {
return Err(fault(status, |status, detail| Error::Search {
status,
detail,
}));
}
Ok((status == sys::MATCH).then_some(span))
}
fn find_all(&self, text: &str) -> Result<Vec<(usize, usize)>, Error> {
let mut out = vec![sys::Span::default(); FIRST_WINDOW.min(text.len() + 1)];
let mut total = self.scan(text, 0, &mut out)?;
if total > out.len() {
out = vec![sys::Span::default(); total];
total = self.scan(text, 0, &mut out)?;
}
out.truncate(total.min(out.len()));
let raw = out
.into_iter()
.map(|span| self.set(span))
.collect::<Result<_, _>>()?;
crate_sequence(raw, text)
.into_iter()
.map(|span| self.boundaries(text, span))
.collect()
}
pub(crate) fn captures_at(
&self,
text: &str,
start: usize,
end: usize,
) -> Result<GroupSpans, Error> {
let groups = self.groups.clone()?;
if groups == 0 {
return Ok(Box::new([Some((start, end))]));
}
let body = text.as_bytes();
let mut window = groups + 1;
let (out, written) = loop {
let lease = self.pool.lease()?;
let mut out = vec![sys::Span::default(); window];
let mut written: usize = 0;
let status = unsafe {
sys::irgx_captures(
lease.raw(),
body.as_ptr(),
body.len(),
start,
out.as_mut_ptr(),
out.len(),
&raw mut written,
)
};
drop(lease);
if status < 0 {
return Err(fault(status, |status, detail| Error::Groups {
pattern: self.as_str().to_owned(),
status,
detail,
}));
}
if status != sys::MATCH {
return Err(Error::Inconsistent {
message: format!(
"find_all reported a match at byte {start} for `{}`, but captures \
found none",
self.as_str()
),
});
}
if written <= window {
break (out, written);
}
window = written;
};
let whole = out[0].range();
if whole != Some((start, end)) {
return Err(Error::Inconsistent {
message: format!(
"find_all reported ({start}, {end}) for `{}`, but captures reported \
{whole:?} from the same offset",
self.as_str()
),
});
}
out[..window.min(written)]
.iter()
.map(|span| match span.range() {
None => Ok(None),
Some(_) => self.checked(text, *span).map(Some),
})
.collect()
}
fn checked(&self, text: &str, span: sys::Span) -> Result<(usize, usize), Error> {
self.boundaries(text, self.set(span)?)
}
fn set(&self, span: sys::Span) -> Result<(usize, usize), Error> {
span.range().ok_or_else(|| Error::Inconsistent {
message: format!(
"the whole-match span for `{}` came back unset ({}, {})",
self.as_str(),
span.start,
span.end
),
})
}
fn boundaries(&self, text: &str, span: (usize, usize)) -> Result<(usize, usize), Error> {
for offset in [span.0, span.1] {
if !text.is_char_boundary(offset) {
return Err(Error::NotCharBoundary { offset });
}
}
Ok(span)
}
}
pub(crate) struct Spell {
pattern: Box<str>,
flags: u32,
}
impl Recipe for Spell {
type Raw = sys::Regex;
fn compile(&self) -> Result<NonNull<sys::Regex>, Error> {
let body = self.pattern.as_bytes();
let mut out: *mut sys::Regex = std::ptr::null_mut();
let status =
unsafe { sys::irgx_compile(body.as_ptr(), body.len(), self.flags, &raw mut out) };
if status < 0 {
return Err(crate::error::compile_refusal(status, &self.pattern));
}
crate::pool::wrote(out, "irgx_compile")
}
unsafe fn release(raw: NonNull<sys::Regex>) {
unsafe { sys::irgx_free(raw.as_ptr()) }
}
}
pub(crate) fn expect<T>(result: Result<T, Error>) -> T {
result.unwrap_or_else(|why| panic!("{why}"))
}
fn name_table(lease: &Lease<'_, Spell>, count: u32) -> Box<[(Box<str>, usize)]> {
let mut found: Vec<(Box<str>, usize)> = Vec::new();
for index in 1..=count {
let mut name = sys::Text::default();
let status = unsafe { sys::irgx_group_name(lease.raw(), index, &raw mut name) };
if status != sys::MATCH || name.ptr.is_null() {
continue;
}
let bytes = unsafe { std::slice::from_raw_parts(name.ptr, name.len) };
if let Ok(text) = std::str::from_utf8(bytes) {
found.push((text.into(), index as usize));
}
}
found.into()
}
impl Clone for Regex {
fn clone(&self) -> Self {
let spell = self.pool.recipe();
expect(Self::compile(&spell.pattern, spell.flags))
}
}
impl std::fmt::Debug for Regex {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
std::fmt::Debug::fmt(self.as_str(), f)
}
}
impl std::fmt::Display for Regex {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.as_str())
}
}
impl std::str::FromStr for Regex {
type Err = Error;
fn from_str(pattern: &str) -> Result<Self, Error> {
Self::new(pattern)
}
}
#[derive(Clone, Debug)]
pub struct RegexBuilder {
pattern: String,
flags: u32,
}
impl RegexBuilder {
#[must_use]
pub fn new(pattern: &str) -> Self {
Self {
pattern: pattern.to_owned(),
flags: 0,
}
}
pub fn fixed(&mut self, yes: bool) -> &mut Self {
self.set(sys::FIXED, yes)
}
pub fn ignore_case(&mut self, yes: bool) -> &mut Self {
self.set(sys::IGNORE_CASE, yes)
}
pub fn word(&mut self, yes: bool) -> &mut Self {
self.set(sys::WORD, yes)
}
pub fn smart_case(&mut self, yes: bool) -> &mut Self {
self.set(sys::SMART_CASE, yes)
}
pub fn unicode(&mut self, yes: bool) -> &mut Self {
self.set(sys::NO_UNICODE, !yes)
}
pub fn multi_line(&mut self, yes: bool) -> &mut Self {
self.set(sys::MULTILINE, yes)
}
pub fn dot_matches_new_line(&mut self, yes: bool) -> &mut Self {
self.set(sys::DOTALL, yes)
}
pub fn pcre(&mut self, yes: bool) -> &mut Self {
self.set(sys::PCRE, yes)
}
pub fn build(&self) -> Result<Regex, Error> {
Regex::compile(&self.pattern, self.flags)
}
fn set(&mut self, bit: u32, yes: bool) -> &mut Self {
if yes {
self.flags |= bit;
} else {
self.flags &= !bit;
}
self
}
}