Skip to main content

minrx/
lib.rs

1use std::{
2    error::Error,
3    fmt::Display,
4    mem::MaybeUninit,
5    ops::{BitAnd, BitOr, Not},
6    ptr::null_mut,
7    range::Range,
8};
9
10use minrx_sys::{
11    minrx_regcomp_flags_t, minrx_regcomp_flags_t_MINRX_REG_BRACE_COMPAT,
12    minrx_regcomp_flags_t_MINRX_REG_BRACK_ESCAPE, minrx_regcomp_flags_t_MINRX_REG_EXTENDED,
13    minrx_regcomp_flags_t_MINRX_REG_EXTENSIONS_BSD, minrx_regcomp_flags_t_MINRX_REG_EXTENSIONS_GNU,
14    minrx_regcomp_flags_t_MINRX_REG_ICASE, minrx_regcomp_flags_t_MINRX_REG_MINDISABLE,
15    minrx_regcomp_flags_t_MINRX_REG_MINIMAL, minrx_regcomp_flags_t_MINRX_REG_NATIVE1B,
16    minrx_regcomp_flags_t_MINRX_REG_NEWLINE, minrx_regcomp_flags_t_MINRX_REG_NOSUB, minrx_regerror,
17    minrx_regex_t, minrx_regexec_flags_t, minrx_regexec_flags_t_MINRX_REG_FIRSTSUB,
18    minrx_regexec_flags_t_MINRX_REG_NOFIRSTBYTES, minrx_regexec_flags_t_MINRX_REG_NOSUBRESET,
19    minrx_regexec_flags_t_MINRX_REG_NOTBOL, minrx_regexec_flags_t_MINRX_REG_NOTEOL,
20    minrx_regexec_flags_t_MINRX_REG_RESUME, minrx_regfree, minrx_regmatch_t, minrx_regncomp,
21    minrx_regnexec, minrx_result_t, minrx_result_t_MINRX_REG_BADBR,
22    minrx_result_t_MINRX_REG_BADPAT, minrx_result_t_MINRX_REG_BADRPT,
23    minrx_result_t_MINRX_REG_EBRACE, minrx_result_t_MINRX_REG_EBRACK,
24    minrx_result_t_MINRX_REG_ECOLLATE, minrx_result_t_MINRX_REG_ECTYPE,
25    minrx_result_t_MINRX_REG_EESCAPE, minrx_result_t_MINRX_REG_EPAREN,
26    minrx_result_t_MINRX_REG_ERANGE, minrx_result_t_MINRX_REG_ESPACE,
27    minrx_result_t_MINRX_REG_ESUBREG, minrx_result_t_MINRX_REG_NOMATCH,
28    minrx_result_t_MINRX_REG_SUCCESS, minrx_result_t_MINRX_REG_UNKNOWN,
29};
30
31/// A MinRX regex matcher. [`Send`] but not [`Sync`].
32#[repr(transparent)]
33pub struct Regex(minrx_regex_t);
34
35/// A match on some haystack. It is a wrapper of [`std::range::Range<usize>`]
36/// with some convenience methods and derives.
37#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
38pub struct Match {
39    pub start: usize,
40    pub end: usize,
41}
42
43/// An iterator over all matches on a haystack.
44pub struct MatchIter<'r, 'h> {
45    regex: &'r mut Regex,
46    haystack: &'h [u8],
47    rm: minrx_regmatch_t,
48    options: MatchOptions,
49    resuming: bool,
50    is_done: bool,
51}
52
53#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
54pub struct RegexBuilder(minrx_regcomp_flags_t);
55
56#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
57pub struct MatchOptions(minrx_regexec_flags_t);
58
59#[repr(u32)]
60#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
61pub enum BuildError {
62    BadPattern(String) = minrx_result_t_MINRX_REG_BADPAT,
63    BadBracket(String) = minrx_result_t_MINRX_REG_BADBR,
64    BadRepetition(String) = minrx_result_t_MINRX_REG_BADRPT,
65    UnbalancedBrace(String) = minrx_result_t_MINRX_REG_EBRACE,
66    UnbalancedBracket(String) = minrx_result_t_MINRX_REG_EBRACK,
67    InvalidCollate(String) = minrx_result_t_MINRX_REG_ECOLLATE,
68    InvalidClass(String) = minrx_result_t_MINRX_REG_ECTYPE,
69    InvalidEscape(String) = minrx_result_t_MINRX_REG_EESCAPE,
70    UnbalancedParen(String) = minrx_result_t_MINRX_REG_EPAREN,
71    InvalidEndpoint(String) = minrx_result_t_MINRX_REG_ERANGE,
72    AllocError(String) = minrx_result_t_MINRX_REG_ESPACE,
73    InvalidDigitEscape(String) = minrx_result_t_MINRX_REG_ESUBREG,
74    Unknown(String) = minrx_result_t_MINRX_REG_UNKNOWN,
75}
76
77#[repr(u32)]
78#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
79pub enum MatchError {
80    AllocError(String) = minrx_result_t_MINRX_REG_ESPACE,
81    Unknown(String) = minrx_result_t_MINRX_REG_UNKNOWN,
82}
83
84impl Regex {
85    /// Constructs a new [`Regex`] with the default options. Look at
86    /// [`RegexBuilder`] for information about configuration options.
87    pub fn new(pattern: impl AsRef<[u8]>) -> Result<Self, BuildError> {
88        RegexBuilder::new().build(pattern)
89    }
90
91    /// Returns the number of captures the pattern generated.
92    pub fn capture_count(&self) -> usize {
93        self.0.re_nsub as _
94    }
95
96    /// Returns matches for all captures, if any. Its behavior can be customized
97    /// with [`Self::find_matches_with`].
98    pub fn find_matches(
99        &mut self,
100        subject: impl AsRef<[u8]>,
101    ) -> Result<Option<Box<[Option<Match>]>>, MatchError> {
102        self.find_matches_with(subject, MatchOptions::new())
103    }
104
105    /// Returns if the pattern matches any substring. It is recommended you
106    /// toggle [`RegexBuilder::no_substrings`] for better performance, if all
107    /// you need is to check existance. Its behavior can be customized with
108    /// [`Self::is_match_with`].
109    pub fn is_match(&mut self, subject: impl AsRef<[u8]>) -> Result<bool, MatchError> {
110        self.is_match_with(subject, MatchOptions::new())
111    }
112
113    /// Returns matches for all captures, if any. Allows for some execution
114    /// options.
115    pub fn find_matches_with(
116        &mut self,
117        haystack: impl AsRef<[u8]>,
118        options: MatchOptions,
119    ) -> Result<Option<Box<[Option<Match>]>>, MatchError> {
120        let subject = haystack.as_ref();
121        let mut buf = Vec::with_capacity(self.0.re_nsub + 1);
122        let res = unsafe {
123            minrx_regnexec(
124                &raw mut self.0,
125                subject.len(),
126                subject.as_ptr(),
127                buf.capacity(),
128                buf.as_mut_ptr(),
129                options.0 as _,
130            )
131        } as minrx_result_t;
132
133        MatchError::from_raw(res, self).map(|found| {
134            found.then(|| {
135                unsafe { buf.set_len(buf.capacity()) };
136                buf.into_iter()
137                    .map(|m| {
138                        Some(Match {
139                            start: m.rm_so.try_into().ok()?,
140                            end: m.rm_eo.try_into().ok()?,
141                        })
142                    })
143                    .collect()
144            })
145        })
146    }
147
148    /// Returns if the pattern matches any substring. It is recommended you
149    /// toggle [`RegexBuilder::no_substrings`] for better performance, if all
150    /// you need is to check existance. Allows for some execution options.
151    pub fn is_match_with(
152        &mut self,
153        haystack: impl AsRef<[u8]>,
154        options: MatchOptions,
155    ) -> Result<bool, MatchError> {
156        let subject = haystack.as_ref();
157        let res = unsafe {
158            minrx_regnexec(
159                &raw mut self.0,
160                subject.len(),
161                subject.as_ptr(),
162                0,
163                null_mut(),
164                options.0 as _,
165            )
166        } as minrx_result_t;
167
168        MatchError::from_raw(res, self)
169    }
170
171    /// Returns an iterator over all matches of the pattern. Its behavior can
172    /// be customized with [`Self::find_matches_with`].
173    pub fn find_iter<'r, 'h>(
174        &'r mut self,
175        haystack: &'h (impl AsRef<[u8]> + ?Sized),
176    ) -> MatchIter<'r, 'h> {
177        self.find_iter_with_flags(haystack, MatchOptions::new())
178    }
179
180    /// Returns an iterator over all matches of the pattern. Allows for some
181    /// execution options.
182    pub fn find_iter_with_flags<'r, 'h>(
183        &'r mut self,
184        haystack: &'h (impl AsRef<[u8]> + ?Sized),
185        options: MatchOptions,
186    ) -> MatchIter<'r, 'h> {
187        MatchIter {
188            regex: self,
189            haystack: haystack.as_ref(),
190            rm: minrx_regmatch_t { rm_so: 0, rm_eo: 0 },
191            options,
192            resuming: false,
193            is_done: false,
194        }
195    }
196}
197
198impl RegexBuilder {
199    /// Creates a new [`RegexBuilder`] that can be freely reused. This struct
200    /// configures [`Regex`], and constructs it with [`RegexBuilder::build`].
201    pub fn new() -> Self {
202        Self(minrx_regcomp_flags_t_MINRX_REG_EXTENDED)
203    }
204
205    /// Attempts to build a new [`Regex`] with the given pattern and options.
206    pub fn build(&self, pattern: impl AsRef<[u8]>) -> Result<Regex, BuildError> {
207        let pattern = pattern.as_ref();
208        let mut regex = MaybeUninit::uninit();
209        let res = unsafe {
210            minrx_regncomp(
211                regex.as_mut_ptr(),
212                pattern.len(),
213                pattern.as_ptr(),
214                self.0 as _,
215            )
216        } as minrx_result_t;
217        BuildError::from_raw(res, &mut regex)?;
218
219        let regex = unsafe { regex.assume_init() };
220        Ok(Regex(regex))
221    }
222
223    /// Uses extended POSIX syntax. This is enabled by default, and in the
224    /// current version of MinRX, disabling it is a no-op. In the meantime,
225    /// this function is a placeholder.
226    pub fn extended(&mut self, _enable: bool) -> &mut Self {
227        self
228    }
229
230    /// Ignore case in both pattern and search.
231    pub fn case_insensitive(&mut self, enable: bool) -> &mut Self {
232        self.0 = mask(self.0, minrx_regcomp_flags_t_MINRX_REG_ICASE, enable);
233        self
234    }
235
236    /// Swap meaning of operators `?` and `??`, `*` and `*?`, and `+` and `+?`.
237    pub fn swap_greed(&mut self, enable: bool) -> &mut Self {
238        self.0 = mask(self.0, minrx_regcomp_flags_t_MINRX_REG_MINIMAL, enable);
239        self
240    }
241
242    /// Excludes `\n` from `.` and `[^...]`; treat as boundary for `^` and `$`.
243    pub fn multi_line(&mut self, enable: bool) -> &mut Self {
244        self.0 = mask(self.0, minrx_regcomp_flags_t_MINRX_REG_NEWLINE, enable);
245        self
246    }
247
248    /// Output true/false results only; no [`Match`] substring results.
249    pub fn no_substrings(&mut self, enable: bool) -> &mut Self {
250        self.0 = mask(self.0, minrx_regcomp_flags_t_MINRX_REG_NOSUB, enable);
251        self
252    }
253
254    /// `{` begins interval expression only when followed by digit.
255    pub fn brace_compat(&mut self, enable: bool) -> &mut Self {
256        self.0 = mask(self.0, minrx_regcomp_flags_t_MINRX_REG_BRACE_COMPAT, enable);
257        self
258    }
259
260    /// Bracket expressions `[...]` allow backslash escapes.
261    pub fn escapes_in_brackets(&mut self, enable: bool) -> &mut Self {
262        self.0 = mask(self.0, minrx_regcomp_flags_t_MINRX_REG_BRACK_ESCAPE, enable);
263        self
264    }
265
266    /// Enable BSD extensions: `\<` and `\>`.
267    pub fn bsd_extensions(&mut self, enable: bool) -> &mut Self {
268        self.0 = mask(
269            self.0,
270            minrx_regcomp_flags_t_MINRX_REG_EXTENSIONS_BSD,
271            enable,
272        );
273        self
274    }
275
276    /// Enable GNU extensions: `\b`, `\B`, `\s`, `\S`, `\w`, `\W`.
277    pub fn gnu_extensions(&mut self, enable: bool) -> &mut Self {
278        self.0 = mask(
279            self.0,
280            minrx_regcomp_flags_t_MINRX_REG_EXTENSIONS_GNU,
281            enable,
282        );
283        self
284    }
285
286    /// Use native encoding for 8-bit character sets.
287    pub fn native_encoding(&mut self, enable: bool) -> &mut Self {
288        self.0 = mask(self.0, minrx_regcomp_flags_t_MINRX_REG_NATIVE1B, enable);
289        self
290    }
291
292    /// Disable POSIX 2024 minimal repetitions.
293    pub fn disable_min_reps(&mut self, enable: bool) -> &mut Self {
294        self.0 = mask(self.0, minrx_regcomp_flags_t_MINRX_REG_MINDISABLE, enable);
295        self
296    }
297}
298
299impl MatchOptions {
300    /// Creates a new [`MatchOptions`] that can be freely reused. This struct
301    /// configures [`Regex`] matching, and can be used with
302    /// [`Regex::find_matches_with`] and [`Regex::is_match_with`]. Their
303    /// non-`_with` counterparts use the default value of this.
304    pub fn new() -> Self {
305        Self(0)
306    }
307
308    /// Disables matching `^` at the beginning of the string.
309    pub fn not_bol(&mut self, enable: bool) -> &mut Self {
310        self.0 = mask(self.0, minrx_regexec_flags_t_MINRX_REG_NOTBOL, enable);
311        self
312    }
313
314    /// Disables matching `$` at the end of the string.
315    pub fn not_eol(&mut self, enable: bool) -> &mut Self {
316        self.0 = mask(self.0, minrx_regexec_flags_t_MINRX_REG_NOTEOL, enable);
317        self
318    }
319
320    /// repeated subexpressions capture their first occurrence (rather than last).
321    pub fn first_subexpr(&mut self, enable: bool) -> &mut Self {
322        self.0 = mask(self.0, minrx_regexec_flags_t_MINRX_REG_FIRSTSUB, enable);
323        self
324    }
325
326    /// Repeated subexpressions don't clear their contained subexpressions.
327    pub fn no_subexpr_reset(&mut self, enable: bool) -> &mut Self {
328        self.0 = mask(self.0, minrx_regexec_flags_t_MINRX_REG_NOSUBRESET, enable);
329        self
330    }
331
332    /// Resumes matching at the end of the last match. Private on purpose;
333    /// cannot be safely used without extra care.
334    fn resume(&mut self, enable: bool) -> &mut Self {
335        self.0 = mask(self.0, minrx_regexec_flags_t_MINRX_REG_RESUME, enable);
336        self
337    }
338
339    /// Disables rapid skip-ahead over impossible first bytes.
340    pub fn no_first_bytes(&mut self, enable: bool) -> &mut Self {
341        self.0 = mask(self.0, minrx_regexec_flags_t_MINRX_REG_NOFIRSTBYTES, enable);
342        self
343    }
344}
345
346impl Default for RegexBuilder {
347    fn default() -> Self {
348        Self::new()
349    }
350}
351
352impl Default for MatchOptions {
353    fn default() -> Self {
354        Self::new()
355    }
356}
357
358impl BuildError {
359    fn from_raw(res: minrx_result_t, regex: &mut MaybeUninit<minrx_regex_t>) -> Result<(), Self> {
360        let err = || regerror(res, regex.as_ptr());
361        let err = match res {
362            res if res == minrx_result_t_MINRX_REG_SUCCESS => return Ok(()),
363            res if res == minrx_result_t_MINRX_REG_BADPAT => Err(Self::BadPattern(err())),
364            res if res == minrx_result_t_MINRX_REG_BADBR => Err(Self::BadBracket(err())),
365            res if res == minrx_result_t_MINRX_REG_BADRPT => Err(Self::BadRepetition(err())),
366            res if res == minrx_result_t_MINRX_REG_EBRACE => Err(Self::UnbalancedBrace(err())),
367            res if res == minrx_result_t_MINRX_REG_EBRACK => Err(Self::UnbalancedBracket(err())),
368            res if res == minrx_result_t_MINRX_REG_ECOLLATE => Err(Self::InvalidCollate(err())),
369            res if res == minrx_result_t_MINRX_REG_ECTYPE => Err(Self::InvalidClass(err())),
370            res if res == minrx_result_t_MINRX_REG_EESCAPE => Err(Self::InvalidEscape(err())),
371            res if res == minrx_result_t_MINRX_REG_EPAREN => Err(Self::UnbalancedParen(err())),
372            res if res == minrx_result_t_MINRX_REG_ERANGE => Err(Self::InvalidEndpoint(err())),
373            res if res == minrx_result_t_MINRX_REG_ESPACE => Err(Self::AllocError(err())),
374            res if res == minrx_result_t_MINRX_REG_ESUBREG => Err(Self::InvalidDigitEscape(err())),
375            _ => Err(Self::Unknown(err())),
376        };
377        drop(Regex(unsafe { regex.assume_init() }));
378        err
379    }
380}
381
382impl MatchError {
383    fn from_raw(res: minrx_result_t, regex: &Regex) -> Result<bool, Self> {
384        let err = || regerror(res, &raw const regex.0);
385        match res {
386            res if res == minrx_result_t_MINRX_REG_SUCCESS => Ok(true),
387            res if res == minrx_result_t_MINRX_REG_NOMATCH => Ok(false),
388            res if res == minrx_result_t_MINRX_REG_ESPACE => Err(Self::AllocError(err())),
389            _ => Err(Self::Unknown(err())),
390        }
391    }
392}
393
394impl<'r, 'h> Iterator for MatchIter<'r, 'h> {
395    type Item = Result<Match, MatchError>;
396
397    fn next(&mut self) -> Option<Self::Item> {
398        if self.is_done {
399            return None;
400        }
401
402        self.options.resume(self.resuming);
403
404        let res = unsafe {
405            minrx_regnexec(
406                &raw mut self.regex.0,
407                self.haystack.len(),
408                self.haystack.as_ptr(),
409                1,
410                &raw mut self.rm,
411                self.options.0 as _,
412            )
413        };
414
415        match MatchError::from_raw(res as _, self.regex) {
416            Ok(true) => {
417                let so = self.rm.rm_so as usize;
418                let eo = self.rm.rm_eo as usize;
419
420                if so == eo {
421                    if eo >= self.haystack.len() {
422                        self.is_done = true;
423                    } else {
424                        // MINRX_REG_RESUME only repositions when `rm_eo > 0`,
425                        // so an empty match would otherwise be found again
426                        // forever. This bumps it forward one character.
427                        self.rm.rm_eo = (eo + 1) as _;
428                    }
429                }
430
431                self.resuming = true;
432                Some(Ok(Match { start: so, end: eo }))
433            }
434            Ok(false) => {
435                self.is_done = true;
436                None
437            }
438            Err(e) => {
439                self.is_done = true;
440                Some(Err(e))
441            }
442        }
443    }
444}
445
446#[inline]
447fn mask<T>(set: T, bit: T, enable: bool) -> T
448where
449    T: BitOr<Output = T> + BitAnd<Output = T> + Not<Output = T>,
450{
451    if enable { set | bit } else { set & !bit }
452}
453
454fn regerror(res: minrx_result_t, regex: *const minrx_regex_t) -> String {
455    let mut buf = Vec::with_capacity(53);
456    let new_len = unsafe { minrx_regerror(res as _, regex, buf.as_mut_ptr(), buf.capacity()) };
457
458    if new_len > buf.capacity() {
459        buf.reserve_exact(new_len);
460        unsafe { minrx_regerror(res as _, regex, buf.as_mut_ptr(), buf.capacity()) };
461    }
462
463    unsafe { buf.set_len(new_len.saturating_sub(1)) }; // Set len; remove null-terminator.
464    String::from_utf8_lossy(&buf).to_string()
465}
466
467impl Drop for Regex {
468    fn drop(&mut self) {
469        unsafe { minrx_regfree(&raw mut self.0) };
470    }
471}
472
473unsafe impl Send for Regex {}
474
475impl From<Match> for std::ops::Range<usize> {
476    fn from(value: Match) -> Self {
477        value.start..value.end
478    }
479}
480
481impl Match {
482    pub fn range(&self) -> Range<usize> {
483        (self.start..self.end).into()
484    }
485}
486
487impl From<Match> for Range<usize> {
488    fn from(value: Match) -> Self {
489        value.range()
490    }
491}
492
493impl Display for BuildError {
494    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
495        match self {
496            BuildError::BadPattern(s)
497            | BuildError::BadBracket(s)
498            | BuildError::BadRepetition(s)
499            | BuildError::UnbalancedBrace(s)
500            | BuildError::UnbalancedBracket(s)
501            | BuildError::InvalidCollate(s)
502            | BuildError::InvalidClass(s)
503            | BuildError::InvalidEscape(s)
504            | BuildError::UnbalancedParen(s)
505            | BuildError::InvalidEndpoint(s)
506            | BuildError::AllocError(s)
507            | BuildError::InvalidDigitEscape(s)
508            | BuildError::Unknown(s) => f.write_str(s),
509        }
510    }
511}
512
513impl Display for MatchError {
514    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
515        match self {
516            MatchError::AllocError(s) | MatchError::Unknown(s) => f.write_str(s),
517        }
518    }
519}
520
521impl Error for BuildError {}
522impl Error for MatchError {}