#![doc = include_str!("../README.md")]
#![warn(clippy::pedantic)]
#![allow(clippy::doc_markdown)]
use std::{
cell::UnsafeCell,
error::Error,
ffi::c_int,
fmt::Display,
mem::MaybeUninit,
ops::{BitAnd, BitOr, Not},
ptr::{NonNull, null_mut},
range::Range,
};
use minrx_sys::{
minrx_regcomp_flags_t, minrx_regcomp_flags_t_MINRX_REG_BRACE_COMPAT,
minrx_regcomp_flags_t_MINRX_REG_BRACK_ESCAPE, minrx_regcomp_flags_t_MINRX_REG_EXTENDED,
minrx_regcomp_flags_t_MINRX_REG_EXTENSIONS_BSD, minrx_regcomp_flags_t_MINRX_REG_EXTENSIONS_GNU,
minrx_regcomp_flags_t_MINRX_REG_ICASE, minrx_regcomp_flags_t_MINRX_REG_MINDISABLE,
minrx_regcomp_flags_t_MINRX_REG_MINIMAL, minrx_regcomp_flags_t_MINRX_REG_NATIVE1B,
minrx_regcomp_flags_t_MINRX_REG_NEWLINE, minrx_regcomp_flags_t_MINRX_REG_NOSUB, minrx_regerror,
minrx_regex_t, minrx_regexec_flags_t, minrx_regexec_flags_t_MINRX_REG_FIRSTSUB,
minrx_regexec_flags_t_MINRX_REG_NOFIRSTBYTES, minrx_regexec_flags_t_MINRX_REG_NOSUBRESET,
minrx_regexec_flags_t_MINRX_REG_NOTBOL, minrx_regexec_flags_t_MINRX_REG_NOTEOL,
minrx_regexec_flags_t_MINRX_REG_RESUME, minrx_regfree, minrx_regmatch_t, minrx_regncomp,
minrx_regnexec, minrx_result_t_MINRX_REG_BADBR, minrx_result_t_MINRX_REG_BADPAT,
minrx_result_t_MINRX_REG_BADRPT, minrx_result_t_MINRX_REG_EBRACE,
minrx_result_t_MINRX_REG_EBRACK, minrx_result_t_MINRX_REG_ECOLLATE,
minrx_result_t_MINRX_REG_ECTYPE, minrx_result_t_MINRX_REG_EESCAPE,
minrx_result_t_MINRX_REG_EPAREN, minrx_result_t_MINRX_REG_ERANGE,
minrx_result_t_MINRX_REG_ESPACE, minrx_result_t_MINRX_REG_ESUBREG,
minrx_result_t_MINRX_REG_NOMATCH, minrx_result_t_MINRX_REG_SUCCESS,
minrx_result_t_MINRX_REG_UNKNOWN,
};
#[repr(transparent)]
#[must_use = "This value does nothing on its own -- you must call its methods
to start matching."]
pub struct Regex(UnsafeCell<minrx_regex_t>);
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct Match {
pub start: usize,
pub end: usize,
}
#[must_use = "This value does nothing on its own -- you must advance the \
iterator to start matching."]
pub struct MatchIter<'r, 'h> {
regex: &'r Regex,
haystack: &'h [u8],
rm: minrx_regmatch_t,
options: MatchOptions,
resuming: bool,
is_done: bool,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
#[repr(transparent)]
#[must_use = "This value does nothing on its own -- you must consume it with \
`Self::build()`. It is also `Copy` and its setters return \
`Self`, so you must assign it or consume their values."]
pub struct RegexBuilder(minrx_regcomp_flags_t);
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
#[repr(transparent)]
#[must_use = "This value does nothing on its own -- you must consume it with \
`Regex::*_with()`. It is also `Copy` and its setters return \
`Self`, so you must assign it or consume their values."]
pub struct MatchOptions(minrx_regexec_flags_t);
#[repr(u32)]
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum BuildError {
BadPattern(String) = minrx_result_t_MINRX_REG_BADPAT,
BadBracket(String) = minrx_result_t_MINRX_REG_BADBR,
BadRepetition(String) = minrx_result_t_MINRX_REG_BADRPT,
UnbalancedBrace(String) = minrx_result_t_MINRX_REG_EBRACE,
UnbalancedBracket(String) = minrx_result_t_MINRX_REG_EBRACK,
InvalidCollate(String) = minrx_result_t_MINRX_REG_ECOLLATE,
InvalidClass(String) = minrx_result_t_MINRX_REG_ECTYPE,
InvalidEscape(String) = minrx_result_t_MINRX_REG_EESCAPE,
UnbalancedParen(String) = minrx_result_t_MINRX_REG_EPAREN,
InvalidEndpoint(String) = minrx_result_t_MINRX_REG_ERANGE,
AllocError(String) = minrx_result_t_MINRX_REG_ESPACE,
InvalidDigitEscape(String) = minrx_result_t_MINRX_REG_ESUBREG,
Unknown(String) = minrx_result_t_MINRX_REG_UNKNOWN,
}
#[repr(u32)]
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum MatchError {
AllocError(String) = minrx_result_t_MINRX_REG_ESPACE,
Unknown(String) = minrx_result_t_MINRX_REG_UNKNOWN,
}
impl Regex {
pub fn new(pattern: impl AsRef<[u8]>) -> Result<Self, BuildError> {
RegexBuilder::new().build(pattern)
}
#[must_use]
pub fn capture_len(&self) -> usize {
unsafe { &*self.0.get() }.re_nsub
}
pub fn find(&self, haystack: impl AsRef<[u8]>) -> Result<Option<Match>, MatchError> {
self.find_with(haystack, MatchOptions::new())
}
pub fn find_with(
&self,
haystack: impl AsRef<[u8]>,
options: MatchOptions,
) -> Result<Option<Match>, MatchError> {
let mut buf = MaybeUninit::<minrx_regmatch_t>::uninit();
let res = unsafe {
self.regnexec(
haystack.as_ref(),
Some(non_null_slice(&mut buf, 1)),
options,
)
};
res.map(|found| {
found.then(|| {
let rm = unsafe { buf.assume_init() };
Match {
start: rm.rm_so.cast_unsigned(),
end: rm.rm_eo.cast_unsigned(),
}
})
})
}
pub fn is_match(&self, haystack: impl AsRef<[u8]>) -> Result<bool, MatchError> {
self.is_match_with(haystack, MatchOptions::new())
}
pub fn captures(
&self,
haystack: impl AsRef<[u8]>,
) -> Result<Option<Box<[Option<Match>]>>, MatchError> {
self.captures_with(haystack, MatchOptions::new())
}
pub fn captures_with(
&self,
haystack: impl AsRef<[u8]>,
options: MatchOptions,
) -> Result<Option<Box<[Option<Match>]>>, MatchError> {
let haystack = haystack.as_ref();
let n_matches = self.capture_len() + 1;
let mut buf = Vec::with_capacity(n_matches);
let slice = non_null_slice(buf.as_mut_slice(), n_matches);
let res = unsafe { self.regnexec(haystack, Some(slice), options) };
res.map(|found| {
found.then(|| {
unsafe { buf.set_len(n_matches) };
buf.into_iter()
.map(|m: minrx_regmatch_t| {
Some(Match {
start: m.rm_so.try_into().ok()?,
end: m.rm_eo.try_into().ok()?,
})
})
.collect()
})
})
}
pub fn is_match_with(
&self,
haystack: impl AsRef<[u8]>,
options: MatchOptions,
) -> Result<bool, MatchError> {
unsafe { self.regnexec(haystack.as_ref(), None, options) }
}
pub fn find_iter<'r, 'h>(
&'r self,
haystack: &'h (impl AsRef<[u8]> + ?Sized),
) -> MatchIter<'r, 'h> {
self.find_iter_with(haystack, MatchOptions::new())
}
pub fn find_iter_with<'r, 'h>(
&'r self,
haystack: &'h (impl AsRef<[u8]> + ?Sized),
options: MatchOptions,
) -> MatchIter<'r, 'h> {
MatchIter {
regex: self,
haystack: haystack.as_ref(),
rm: minrx_regmatch_t { rm_so: 0, rm_eo: 0 },
options,
resuming: false,
is_done: false,
}
}
#[inline]
unsafe fn regnexec(
&self,
haystack: &[u8],
buf: Option<NonNull<[minrx_regmatch_t]>>,
options: MatchOptions,
) -> Result<bool, MatchError> {
let (buf_ptr, buf_cap) = buf.map_or((null_mut(), 0), |b| (b.as_ptr().cast(), b.len()));
let res = unsafe {
minrx_regnexec(
self.0.get(),
haystack.len(),
haystack.as_ptr().cast(),
buf_cap,
buf_ptr,
options.as_c_int(),
)
};
MatchError::from_raw(res, self)
}
}
impl RegexBuilder {
pub fn new() -> Self {
Self(minrx_regcomp_flags_t_MINRX_REG_EXTENDED)
}
pub fn build(self, pattern: impl AsRef<[u8]>) -> Result<Regex, BuildError> {
let pattern = pattern.as_ref();
let mut regex = MaybeUninit::zeroed();
let res = unsafe {
minrx_regncomp(
regex.as_mut_ptr(),
pattern.len(),
pattern.as_ptr().cast(),
self.as_c_int(),
)
};
BuildError::from_raw(res, &mut regex)?;
let regex = unsafe { regex.assume_init() };
Ok(Regex(regex.into()))
}
pub fn extended(self, _enable: bool) -> Self {
self
}
pub fn case_insensitive(mut self, enable: bool) -> Self {
self.0 = mask(self.0, minrx_regcomp_flags_t_MINRX_REG_ICASE, enable);
self
}
pub fn swap_greed(mut self, enable: bool) -> Self {
self.0 = mask(self.0, minrx_regcomp_flags_t_MINRX_REG_MINIMAL, enable);
self
}
pub fn multi_line(mut self, enable: bool) -> Self {
self.0 = mask(self.0, minrx_regcomp_flags_t_MINRX_REG_NEWLINE, enable);
self
}
pub fn no_substrings(mut self, enable: bool) -> Self {
self.0 = mask(self.0, minrx_regcomp_flags_t_MINRX_REG_NOSUB, enable);
self
}
pub fn brace_compat(mut self, enable: bool) -> Self {
self.0 = mask(self.0, minrx_regcomp_flags_t_MINRX_REG_BRACE_COMPAT, enable);
self
}
pub fn escapes_in_brackets(mut self, enable: bool) -> Self {
self.0 = mask(self.0, minrx_regcomp_flags_t_MINRX_REG_BRACK_ESCAPE, enable);
self
}
pub fn bsd_extensions(mut self, enable: bool) -> Self {
self.0 = mask(
self.0,
minrx_regcomp_flags_t_MINRX_REG_EXTENSIONS_BSD,
enable,
);
self
}
pub fn gnu_extensions(mut self, enable: bool) -> Self {
self.0 = mask(
self.0,
minrx_regcomp_flags_t_MINRX_REG_EXTENSIONS_GNU,
enable,
);
self
}
pub fn native_encoding(mut self, enable: bool) -> Self {
self.0 = mask(self.0, minrx_regcomp_flags_t_MINRX_REG_NATIVE1B, enable);
self
}
pub fn disable_min_reps(mut self, enable: bool) -> Self {
self.0 = mask(self.0, minrx_regcomp_flags_t_MINRX_REG_MINDISABLE, enable);
self
}
fn as_c_int(self) -> c_int {
self.0.cast_signed()
}
}
impl MatchOptions {
fn as_c_int(self) -> c_int {
self.0.cast_signed()
}
pub fn new() -> Self {
Self(0)
}
pub fn not_bol(mut self, enable: bool) -> Self {
self.0 = mask(self.0, minrx_regexec_flags_t_MINRX_REG_NOTBOL, enable);
self
}
pub fn not_eol(mut self, enable: bool) -> Self {
self.0 = mask(self.0, minrx_regexec_flags_t_MINRX_REG_NOTEOL, enable);
self
}
pub fn first_subexpr(mut self, enable: bool) -> Self {
self.0 = mask(self.0, minrx_regexec_flags_t_MINRX_REG_FIRSTSUB, enable);
self
}
pub fn no_subexpr_reset(mut self, enable: bool) -> Self {
self.0 = mask(self.0, minrx_regexec_flags_t_MINRX_REG_NOSUBRESET, enable);
self
}
fn resume(mut self, enable: bool) -> Self {
self.0 = mask(self.0, minrx_regexec_flags_t_MINRX_REG_RESUME, enable);
self
}
pub fn no_first_bytes(mut self, enable: bool) -> Self {
self.0 = mask(self.0, minrx_regexec_flags_t_MINRX_REG_NOFIRSTBYTES, enable);
self
}
}
impl Default for RegexBuilder {
fn default() -> Self {
Self::new()
}
}
impl Default for MatchOptions {
fn default() -> Self {
Self::new()
}
}
impl BuildError {
fn from_raw(res: c_int, regex: &mut MaybeUninit<minrx_regex_t>) -> Result<(), Self> {
let err = || regerror(res, regex.as_ptr());
let err = match res.cast_unsigned() {
res if res == minrx_result_t_MINRX_REG_SUCCESS => return Ok(()),
res if res == minrx_result_t_MINRX_REG_BADPAT => Err(Self::BadPattern(err())),
res if res == minrx_result_t_MINRX_REG_BADBR => Err(Self::BadBracket(err())),
res if res == minrx_result_t_MINRX_REG_BADRPT => Err(Self::BadRepetition(err())),
res if res == minrx_result_t_MINRX_REG_EBRACE => Err(Self::UnbalancedBrace(err())),
res if res == minrx_result_t_MINRX_REG_EBRACK => Err(Self::UnbalancedBracket(err())),
res if res == minrx_result_t_MINRX_REG_ECOLLATE => Err(Self::InvalidCollate(err())),
res if res == minrx_result_t_MINRX_REG_ECTYPE => Err(Self::InvalidClass(err())),
res if res == minrx_result_t_MINRX_REG_EESCAPE => Err(Self::InvalidEscape(err())),
res if res == minrx_result_t_MINRX_REG_EPAREN => Err(Self::UnbalancedParen(err())),
res if res == minrx_result_t_MINRX_REG_ERANGE => Err(Self::InvalidEndpoint(err())),
res if res == minrx_result_t_MINRX_REG_ESPACE => return Err(Self::AllocError(err())),
res if res == minrx_result_t_MINRX_REG_ESUBREG => Err(Self::InvalidDigitEscape(err())),
_ => Err(Self::Unknown(err())),
};
drop(Regex(unsafe { regex.assume_init().into() }));
err
}
}
impl MatchError {
fn from_raw(res: c_int, regex: &Regex) -> Result<bool, Self> {
let err = || regerror(res, regex.0.get());
match res.cast_unsigned() {
res if res == minrx_result_t_MINRX_REG_SUCCESS => Ok(true),
res if res == minrx_result_t_MINRX_REG_NOMATCH => Ok(false),
res if res == minrx_result_t_MINRX_REG_ESPACE => Err(Self::AllocError(err())),
_ => Err(Self::Unknown(err())),
}
}
}
impl Iterator for MatchIter<'_, '_> {
type Item = Result<Match, MatchError>;
fn next(&mut self) -> Option<Self::Item> {
if self.is_done {
return None;
}
self.options = self.options.resume(self.resuming);
let res = unsafe {
self.regex.regnexec(
self.haystack,
Some(non_null_slice(&mut self.rm, 1)),
self.options,
)
};
match res {
Ok(true) => {
let so = self.rm.rm_so.cast_unsigned();
let eo = self.rm.rm_eo.cast_unsigned();
if so == eo {
if eo >= self.haystack.len() {
self.is_done = true;
} else {
self.rm.rm_eo += 1;
}
}
self.resuming = true;
Some(Ok(Match { start: so, end: eo }))
}
Ok(false) => {
self.is_done = true;
None
}
Err(e) => {
self.is_done = true;
Some(Err(e))
}
}
}
}
#[inline]
fn mask<T>(set: T, bit: T, enable: bool) -> T
where
T: BitOr<Output = T> + BitAnd<Output = T> + Not<Output = T>,
{
if enable { set | bit } else { set & !bit }
}
fn regerror(res: c_int, regex: *const minrx_regex_t) -> String {
let mut buf = Vec::<u8>::with_capacity(53);
let new_len = unsafe { minrx_regerror(res, regex, buf.as_mut_ptr().cast(), buf.capacity()) };
if new_len > buf.capacity() {
buf.reserve_exact(new_len);
unsafe { minrx_regerror(res, regex, buf.as_mut_ptr().cast(), buf.capacity()) };
}
unsafe { buf.set_len(new_len.saturating_sub(1)) }; String::from_utf8_lossy(&buf).to_string()
}
impl Drop for Regex {
fn drop(&mut self) {
unsafe { minrx_regfree(self.0.get()) };
}
}
unsafe impl Send for Regex {}
impl From<Match> for std::ops::Range<usize> {
fn from(value: Match) -> Self {
value.start..value.end
}
}
impl Match {
#[must_use]
pub fn range(&self) -> Range<usize> {
(self.start..self.end).into()
}
}
impl From<Match> for Range<usize> {
fn from(value: Match) -> Self {
value.range()
}
}
fn non_null_slice<T: ?Sized, U>(ptr: impl Into<NonNull<T>>, len: usize) -> NonNull<[U]> {
NonNull::slice_from_raw_parts(ptr.into().cast::<U>(), len)
}
impl Display for BuildError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str("Regex parse error: ")?;
match self {
BuildError::BadPattern(s)
| BuildError::BadBracket(s)
| BuildError::BadRepetition(s)
| BuildError::UnbalancedBrace(s)
| BuildError::UnbalancedBracket(s)
| BuildError::InvalidCollate(s)
| BuildError::InvalidClass(s)
| BuildError::InvalidEscape(s)
| BuildError::UnbalancedParen(s)
| BuildError::InvalidEndpoint(s)
| BuildError::AllocError(s)
| BuildError::InvalidDigitEscape(s)
| BuildError::Unknown(s) => f.write_str(s),
}
}
}
impl Display for MatchError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str("Regex execution error: ")?;
match self {
MatchError::AllocError(s) | MatchError::Unknown(s) => f.write_str(s),
}
}
}
impl Error for BuildError {}
impl Error for MatchError {}