use crate::error::{Error, fault};
use crate::matches::{CaptureMatches, Captures, GroupSpans, Match, Matches, Split};
use crate::pool::Pool;
use crate::sys;
const FIRST_WINDOW: usize = 4096;
pub struct Regex {
pool: Pool,
pattern: Box<str>,
flags: u32,
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(pattern.as_bytes(), 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,
pattern: pattern.into(),
flags,
groups,
names,
})
}
#[must_use]
pub fn as_str(&self) -> &str {
&self.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 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 mut span = [sys::Span::default()];
if self.scan(text, &mut span)? == 0 {
return Ok(None);
}
let (start, end) = self.checked(text, span[0])?;
Ok(Some(Match::new(text, start, end)))
}
#[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, 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(
lease.raw(),
body.as_ptr(),
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 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, &mut out)?;
if total > out.len() {
out = vec![sys::Span::default(); total];
total = self.scan(text, &mut out)?;
}
out.truncate(total.min(out.len()));
out.into_iter()
.map(|span| self.checked(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.pattern.to_string(),
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.pattern
),
});
}
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.pattern
),
});
}
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> {
let Some((start, end)) = span.range() else {
return Err(Error::Inconsistent {
message: format!(
"the whole-match span for `{}` came back unset ({}, {})",
self.pattern, span.start, span.end
),
});
};
for offset in [start, end] {
if !text.is_char_boundary(offset) {
return Err(Error::NotCharBoundary { offset });
}
}
Ok((start, end))
}
}
pub(crate) fn expect<T>(result: Result<T, Error>) -> T {
result.unwrap_or_else(|why| panic!("{why}"))
}
fn name_table(lease: &crate::pool::Lease<'_>, 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 {
expect(Self::compile(&self.pattern, self.flags))
}
}
impl std::fmt::Debug for Regex {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
std::fmt::Debug::fmt(&self.pattern, f)
}
}
impl std::fmt::Display for Regex {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.pattern)
}
}
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 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
}
}