Skip to main content

tre_regex/
approx.rs

1// SPDX-License-Identifier: BSD-2-Clause
2// See LICENSE file in the project root for full license text.
3
4use std::ffi::c_int;
5
6use crate::{
7    Regex, RegexecFlags,
8    err::{BindingErrorCode, ErrorKind, RegexError, Result},
9    exec::match_offset,
10    tre,
11};
12
13pub type RegApproxMatchStr<'a> = RegApproxMatch<'a, str>;
14pub type RegApproxMatchBytes<'a> = RegApproxMatch<'a, [u8]>;
15
16/// Regex params passed to approximate matching functions such as [`regaexec`]
17#[cfg(feature = "approx")]
18#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, Hash)]
19pub struct RegApproxParams {
20    cost_ins: i32,
21    cost_del: i32,
22    cost_subst: i32,
23    max_cost: i32,
24    max_ins: i32,
25    max_del: i32,
26    max_subst: i32,
27    max_err: i32,
28}
29
30impl RegApproxParams {
31    /// Creates a new empty [`RegApproxParams`] object.
32    #[must_use]
33    #[inline]
34    pub const fn new() -> Self {
35        Self {
36            cost_ins: 0,
37            cost_del: 0,
38            cost_subst: 0,
39            max_cost: 0,
40            max_ins: 0,
41            max_del: 0,
42            max_subst: 0,
43            max_err: 0,
44        }
45    }
46
47    /// Sets the [`cost_ins`](tre_regex_sys::regaparams_t::cost_ins) element.
48    #[must_use]
49    #[inline]
50    pub const fn cost_ins(self, cost_ins: i32) -> Self {
51        let mut copy = self;
52        copy.cost_ins = cost_ins;
53        copy
54    }
55
56    /// Sets the [`cost_del`](tre_regex_sys::regaparams_t::cost_del) element.
57    #[must_use]
58    #[inline]
59    pub const fn cost_del(self, cost_del: i32) -> Self {
60        let mut copy = self;
61        copy.cost_del = cost_del;
62        copy
63    }
64
65    /// Sets the [`cost_subst`](tre_regex_sys::regaparams_t::cost_subst) element.
66    #[must_use]
67    #[inline]
68    pub const fn cost_subst(self, cost_subst: i32) -> Self {
69        let mut copy = self;
70        copy.cost_subst = cost_subst;
71        copy
72    }
73
74    /// Sets the [`max_cost`](tre_regex_sys::regaparams_t::max_cost) element.
75    #[must_use]
76    #[inline]
77    pub const fn max_cost(self, max_cost: i32) -> Self {
78        let mut copy = self;
79        copy.max_cost = max_cost;
80        copy
81    }
82
83    /// Sets the [`max_ins`](tre_regex_sys::regaparams_t::max_ins) element.
84    #[must_use]
85    #[inline]
86    pub const fn max_ins(self, max_ins: i32) -> Self {
87        let mut copy = self;
88        copy.max_ins = max_ins;
89        copy
90    }
91
92    /// Sets the [`max_del`](tre_regex_sys::regaparams_t::max_del) element.
93    #[must_use]
94    #[inline]
95    pub const fn max_del(self, max_del: i32) -> Self {
96        let mut copy = self;
97        copy.max_del = max_del;
98        copy
99    }
100
101    /// Sets the [`max_subst`](tre_regex_sys::regaparams_t::max_subst) element.
102    #[must_use]
103    #[inline]
104    pub const fn max_subst(self, max_subst: i32) -> Self {
105        let mut copy = self;
106        copy.max_subst = max_subst;
107        copy
108    }
109
110    /// Sets the [`max_err`](tre_regex_sys::regaparams_t::max_err) element.
111    #[must_use]
112    #[inline]
113    pub const fn max_err(self, max_err: i32) -> Self {
114        let mut copy = self;
115        copy.max_err = max_err;
116        copy
117    }
118
119    pub(crate) fn to_raw(self) -> Result<tre::regaparams_t> {
120        fn convert(value: i32) -> Result<c_int> {
121            c_int::try_from(value).map_err(|error| {
122                RegexError::new(
123                    ErrorKind::Binding(BindingErrorCode::INVALID_APPROX_PARAM),
124                    &format!("Approximate matching parameter is out of range: {error}"),
125                )
126            })
127        }
128
129        Ok(tre::regaparams_t {
130            cost_ins: convert(self.cost_ins)?,
131            cost_del: convert(self.cost_del)?,
132            cost_subst: convert(self.cost_subst)?,
133            max_cost: convert(self.max_cost)?,
134            max_ins: convert(self.max_ins)?,
135            max_del: convert(self.max_del)?,
136            max_subst: convert(self.max_subst)?,
137            max_err: convert(self.max_err)?,
138        })
139    }
140}
141
142/// This struct is returned by [`regaexec`].
143///
144/// The match results from this function are very complex. See the [TRE documentation] for details
145/// on how this all works and corresponding fields, and what they mean.
146///
147/// This structure should never be instantiated outside the library.
148///
149/// [TRE documentation]: <https://laurikari.net/tre/documentation/regaexec/>
150#[derive(Clone, Debug, PartialEq, Eq)]
151pub struct RegApproxMatch<'a, T: ?Sized> {
152    data: &'a T,
153    matches: Vec<Option<&'a T>>,
154    cost: i32,
155    num_ins: i32,
156    num_del: i32,
157    num_subst: i32,
158}
159
160impl<'a, T: ?Sized> RegApproxMatch<'a, T> {
161    pub(crate) const fn new(
162        data: &'a T,
163        matches: Vec<Option<&'a T>>,
164        amatch: tre::regamatch_t,
165    ) -> Self {
166        Self {
167            data,
168            matches,
169            cost: amatch.cost,
170            num_ins: amatch.num_ins,
171            num_del: amatch.num_del,
172            num_subst: amatch.num_subst,
173        }
174    }
175
176    /// Gets the cost of the match
177    #[must_use]
178    pub const fn cost(&self) -> i32 {
179        self.cost
180    }
181
182    /// Gets the number of insertions if the match
183    #[must_use]
184    pub const fn num_ins(&self) -> i32 {
185        self.num_ins
186    }
187
188    /// Gets the number of deletions if the match
189    #[must_use]
190    pub const fn num_del(&self) -> i32 {
191        self.num_del
192    }
193
194    /// Get the number of substitutions in the match
195    #[must_use]
196    pub const fn num_subst(&self) -> i32 {
197        self.num_subst
198    }
199
200    /// Gets an immutable reference to the underlying data
201    #[must_use]
202    pub const fn get_orig_data(&self) -> &'a T {
203        self.data
204    }
205
206    /// Gets the matches returned by this, as references to the data
207    #[must_use]
208    pub fn get_matches(&self) -> &[Option<&'a T>] {
209        &self.matches
210    }
211
212    /// Consumes this result and returns its matches.
213    #[must_use]
214    pub fn into_matches(self) -> Vec<Option<&'a T>> {
215        self.matches
216    }
217}
218
219impl Regex {
220    /// Performs an approximate regex search on the passed string, returning `nmatches` results.
221    ///
222    /// Non-matching subexpressions or patterns will return `None` in the results.
223    ///
224    /// # Arguments
225    /// * `string`: string to match against `compiled_reg`
226    /// * `params`: see [`RegApproxParams`]
227    /// * `nmatches`: number of matches to return
228    /// * `flags`: [`RegexecFlags`] to pass to [`tre_reganexec`](tre_regex_sys::tre_reganexec).
229    ///
230    /// # Returns
231    /// If no error was found, a [`Vec`] of [`Option`]s will be returned.
232    ///
233    /// If a given match index is empty, its `Option` is `None`; otherwise it contains a borrowed
234    /// substring of the input.
235    ///
236    /// # Errors
237    /// Returns a [`RegexError`] if matching fails or TRE returns offsets that do not fall on UTF-8
238    /// character boundaries.
239    ///
240    /// # Caveats
241    /// Unless copied, the match results must live at least as long as `string`. This is because they are
242    /// slices into `string` under the hood, for efficiency.
243    ///
244    /// # Examples
245    /// ```
246    /// # use tre_regex::Result;
247    /// # fn main() -> Result<()> {
248    /// use tre_regex::{RegcompFlags, RegexecFlags, RegApproxParams, Regex};
249    ///
250    /// let regcomp_flags = RegcompFlags::new()
251    ///     .add(RegcompFlags::EXTENDED)
252    ///     .add(RegcompFlags::ICASE);
253    /// let regaexec_flags = RegexecFlags::new().add(RegexecFlags::NONE);
254    /// let regaexec_params = RegApproxParams::new()
255    ///     .cost_ins(1)
256    ///     .cost_del(1)
257    ///     .cost_subst(1)
258    ///     .max_cost(2)
259    ///     .max_del(2)
260    ///     .max_ins(2)
261    ///     .max_subst(2)
262    ///     .max_err(2);
263    ///
264    /// let compiled_reg = Regex::new("^(hello).*(world)$", regcomp_flags)?;
265    /// let result = compiled_reg.regaexec(
266    ///     "hello world",      // String to match against
267    ///     &regaexec_params,   // Matching parameters
268    ///     3,                  // Number of matches we want
269    ///     regaexec_flags      // Flags
270    /// )?;
271    ///
272    /// for (i, matched) in result.get_matches().into_iter().enumerate() {
273    ///     match matched {
274    ///         Some(substr) => println!("Match {i}: {substr}"),
275    ///         None => println!("Match {i}: <None>"),
276    ///     }
277    /// }
278    /// # Ok(())
279    /// # }
280    /// ```
281    #[inline]
282    pub fn regaexec<'a>(
283        &self,
284        string: &'a str,
285        params: &RegApproxParams,
286        nmatches: usize,
287        flags: RegexecFlags,
288    ) -> Result<RegApproxMatchStr<'a>> {
289        let Some(compiled_reg_obj) = self.as_raw() else {
290            return Err(RegexError::new(
291                ErrorKind::Binding(BindingErrorCode::REGEX_VACANT),
292                "Attempted to unwrap a vacant Regex object",
293            ));
294        };
295        let data = string.as_bytes();
296        let mut match_vec = vec![tre::regmatch_t::default(); nmatches];
297        let mut amatch = tre::regamatch_t {
298            nmatch: nmatches,
299            pmatch: match_vec.as_mut_ptr(),
300            ..Default::default()
301        };
302
303        // SAFETY: the regex is initialised, data is valid for its supplied length, and amatch
304        // points to nmatches writable entries.
305        let result_code = unsafe {
306            tre::tre_reganexec(
307                compiled_reg_obj,
308                data.as_ptr().cast(),
309                data.len(),
310                &raw mut amatch,
311                params.to_raw()?,
312                flags.bits(),
313            )
314        };
315        if result_code != 0 {
316            return Err(self.regerror(result_code));
317        }
318
319        let mut result = Vec::with_capacity(nmatches);
320        for pmatch in match_vec {
321            if pmatch.rm_so < 0 || pmatch.rm_eo < 0 {
322                result.push(None);
323                continue;
324            }
325
326            let start_offset = match_offset(pmatch.rm_so)?;
327            let end_offset = match_offset(pmatch.rm_eo)?;
328            let matched = string.get(start_offset..end_offset).ok_or_else(|| {
329                RegexError::new(
330                    ErrorKind::Binding(BindingErrorCode::ENCODING),
331                    "TRE returned match offsets that are not UTF-8 character boundaries",
332                )
333            })?;
334            result.push(Some(matched));
335        }
336
337        Ok(RegApproxMatchStr::new(string, result, amatch))
338    }
339
340    /// Performs an approximate regex search on the passed bytes, returning `nmatches` results.
341    ///
342    /// This function should only be used if you need to match raw bytes, or bytes which may not be
343    /// UTF-8 compliant. Otherwise, [`regaexec`] is recommended instead.
344    ///
345    /// # Arguments
346    /// * `data`: [`u8`] slice to match against `compiled_reg`
347    /// * `params`: see [`RegApproxParams`]
348    /// * `nmatches`: number of matches to return
349    /// * `flags`: [`RegexecFlags`] to pass to [`tre_regaexecb`](tre_regex_sys::tre_regaexecb).
350    ///
351    /// # Returns
352    /// If no error was found, a [`Vec`] of [`Option`]s will be returned.
353    ///
354    /// If a given match index is empty, The `Option` will be `None`. Otherwise, [`u8`] slices will be
355    /// returned.
356    ///
357    /// # Errors
358    /// If an error is encountered during matching, it returns a [`RegexError`].
359    ///
360    /// # Caveats
361    /// Unless copied, the match results must live at least as long as `data`. This is because they are
362    /// slices into `data` under the hood, for efficiency. TRE's approximate byte matcher is
363    /// NUL-terminated, so an embedded NUL marks the end of the searchable data.
364    ///
365    /// # Examples
366    /// ```
367    /// # use tre_regex::Result;
368    /// # fn main() -> Result<()> {
369    /// use tre_regex::{RegcompFlags, RegexecFlags, RegApproxParams, Regex};
370    ///
371    /// let regcomp_flags = RegcompFlags::new()
372    ///     .add(RegcompFlags::EXTENDED)
373    ///     .add(RegcompFlags::ICASE);
374    /// let regaexec_flags = RegexecFlags::new().add(RegexecFlags::NONE);
375    /// let regaexec_params = RegApproxParams::new()
376    ///     .cost_ins(1)
377    ///     .cost_del(1)
378    ///     .cost_subst(1)
379    ///     .max_cost(2)
380    ///     .max_del(2)
381    ///     .max_ins(2)
382    ///     .max_subst(2)
383    ///     .max_err(2);
384    ///
385    /// let compiled_reg = Regex::new_bytes(b"^(hello).*(world)$", regcomp_flags)?;
386    /// let result = compiled_reg.regaexec_bytes(
387    ///     b"hello world",     // Bytes to match against
388    ///     &regaexec_params,   // Matching parameters
389    ///     3,                  // Number of matches we want
390    ///     regaexec_flags      // Flags
391    /// )?;
392    ///
393    /// for (i, matched) in result.get_matches().into_iter().enumerate() {
394    ///     match matched {
395    ///         Some(substr) => println!(
396    ///             "Match {i}: {}",
397    ///             std::str::from_utf8(substr).unwrap()
398    ///         ),
399    ///         None => println!("Match {i}: <None>"),
400    ///     }
401    /// }
402    /// # Ok(())
403    /// # }
404    /// ```
405    pub fn regaexec_bytes<'a>(
406        &self,
407        data: &'a [u8],
408        params: &RegApproxParams,
409        nmatches: usize,
410        flags: RegexecFlags,
411    ) -> Result<RegApproxMatchBytes<'a>> {
412        let Some(compiled_reg_obj) = self.as_raw() else {
413            return Err(RegexError::new(
414                ErrorKind::Binding(BindingErrorCode::REGEX_VACANT),
415                "Attempted to unwrap a vacant Regex object",
416            ));
417        };
418        let mut match_vec: Vec<tre::regmatch_t> =
419            vec![tre::regmatch_t { rm_so: 0, rm_eo: 0 }; nmatches];
420        let mut amatch = tre::regamatch_t {
421            nmatch: nmatches,
422            pmatch: match_vec.as_mut_ptr(),
423            ..Default::default()
424        };
425        let mut nul_terminated_data = Vec::with_capacity(data.len() + 1);
426        nul_terminated_data.extend_from_slice(data);
427        nul_terminated_data.push(0);
428
429        // SAFETY: compiled_reg is initialised, nul_terminated_data has an explicit trailing NUL,
430        // and amatch points to nmatches writable entries.
431        let result = unsafe {
432            tre::tre_regaexecb(
433                compiled_reg_obj,
434                nul_terminated_data.as_ptr().cast(),
435                &raw mut amatch,
436                params.to_raw()?,
437                flags.bits(),
438            )
439        };
440        if result != 0 {
441            return Err(self.regerror(result));
442        }
443
444        let mut result = Vec::with_capacity(nmatches);
445        for pmatch in match_vec {
446            if pmatch.rm_so < 0 || pmatch.rm_eo < 0 {
447                result.push(None);
448                continue;
449            }
450
451            let start_offset = match_offset(pmatch.rm_so)?;
452            let end_offset = match_offset(pmatch.rm_eo)?;
453
454            result.push(Some(&data[start_offset..end_offset]));
455        }
456
457        Ok(RegApproxMatchBytes::new(data, result, amatch))
458    }
459}
460
461/// Performs an approximate regex search on the passed string, returning `nmatches` results.
462///
463/// This is a thin wrapper around [`Regex::regaexec`].
464///
465/// Non-matching subexpressions or patterns will return `None` in the results.
466///
467/// # Arguments
468/// * `compiled_reg`: the compiled [`Regex`] object.
469/// * `string`: string to match against `compiled_reg`
470/// * `params`: see [`RegApproxParams`]
471/// * `nmatches`: number of matches to return
472/// * `flags`: [`RegexecFlags`] to pass to [`tre_reganexec`](tre_regex_sys::tre_reganexec).
473///
474/// # Returns
475/// If no error was found, a [`Vec`] of [`Option`]s will be returned.
476///
477/// If a given match index is empty, its `Option` is `None`; otherwise it contains a borrowed
478/// substring of the input.
479///
480/// # Errors
481/// Returns a [`RegexError`] if matching fails or TRE returns offsets that do not fall on UTF-8
482/// character boundaries.
483///
484/// # Caveats
485/// Unless copied, the match results must live at least as long as `string`. This is because they are
486/// slices into `string` under the hood, for efficiency.
487///
488/// # Examples
489/// ```
490/// # use tre_regex::Result;
491/// # fn main() -> Result<()> {
492/// use tre_regex::{RegcompFlags, RegexecFlags, RegApproxParams, Regex, regaexec};
493///
494/// let regcomp_flags = RegcompFlags::new()
495///     .add(RegcompFlags::EXTENDED)
496///     .add(RegcompFlags::ICASE);
497/// let regaexec_flags = RegexecFlags::new().add(RegexecFlags::NONE);
498/// let regaexec_params = RegApproxParams::new()
499///     .cost_ins(1)
500///     .cost_del(1)
501///     .cost_subst(1)
502///     .max_cost(2)
503///     .max_del(2)
504///     .max_ins(2)
505///     .max_subst(2)
506///     .max_err(2);
507///
508/// let compiled_reg = Regex::new("^(hello).*(world)$", regcomp_flags)?;
509/// let result = regaexec(
510///     &compiled_reg,      // Compiled regex
511///     "hello world",      // String to match against
512///     &regaexec_params,   // Matching parameters
513///     3,                  // Number of matches we want
514///     regaexec_flags      // Flags
515/// )?;
516///
517/// for (i, matched) in result.get_matches().into_iter().enumerate() {
518///     match matched {
519///         Some(substr) => println!("Match {i}: {substr}"),
520///         None => println!("Match {i}: <None>"),
521///     }
522/// }
523/// # Ok(())
524/// # }
525/// ```
526#[inline]
527pub fn regaexec<'a>(
528    compiled_reg: &Regex,
529    string: &'a str,
530    params: &RegApproxParams,
531    nmatches: usize,
532    flags: RegexecFlags,
533) -> Result<RegApproxMatchStr<'a>> {
534    compiled_reg.regaexec(string, params, nmatches, flags)
535}
536
537/// Performs an approximate regex search on the passed bytes, returning `nmatches` results.
538///
539/// This is a thin wrapper around [`Regex::regaexec_bytes`].
540///
541/// This function should only be used if you need to match raw bytes, or bytes which may not be
542/// UTF-8 compliant. Otherwise, [`regaexec`] is recommended instead.
543///
544/// # Arguments
545/// * `compiled_reg`: the compiled [`Regex`] object.
546/// * `data`: [`u8`] slice to match against `compiled_reg`
547/// * `params`: see [`RegApproxParams`]
548/// * `nmatches`: number of matches to return
549/// * `flags`: [`RegexecFlags`] to pass to [`tre_regaexecb`](tre_regex_sys::tre_regaexecb).
550///
551/// # Returns
552/// If no error was found, a [`Vec`] of [`Option`]s will be returned.
553///
554/// If a given match index is empty, The `Option` will be `None`. Otherwise, [`u8`] slices will be
555/// returned.
556///
557/// # Errors
558/// If an error is encountered during matching, it returns a [`RegexError`].
559///
560/// # Caveats
561/// Unless copied, the match results must live at least as long as `data`. This is because they are
562/// slices into `data` under the hood, for efficiency. TRE's approximate byte matcher is
563/// NUL-terminated, so an embedded NUL marks the end of the searchable data.
564///
565/// # Examples
566/// ```
567/// # use tre_regex::Result;
568/// # fn main() -> Result<()> {
569/// use tre_regex::{RegcompFlags, RegexecFlags, RegApproxParams, Regex, regaexec_bytes};
570///
571/// let regcomp_flags = RegcompFlags::new()
572///     .add(RegcompFlags::EXTENDED)
573///     .add(RegcompFlags::ICASE);
574/// let regaexec_flags = RegexecFlags::new().add(RegexecFlags::NONE);
575/// let regaexec_params = RegApproxParams::new()
576///     .cost_ins(1)
577///     .cost_del(1)
578///     .cost_subst(1)
579///     .max_cost(2)
580///     .max_del(2)
581///     .max_ins(2)
582///     .max_subst(2)
583///     .max_err(2);
584///
585/// let compiled_reg = Regex::new_bytes(b"^(hello).*(world)$", regcomp_flags)?;
586/// let result = regaexec_bytes(
587///     &compiled_reg,      // Compiled regex
588///     b"hello world",     // Bytes to match against
589///     &regaexec_params,   // Matching parameters
590///     3,                  // Number of matches we want
591///     regaexec_flags      // Flags
592/// )?;
593///
594/// for (i, matched) in result.get_matches().into_iter().enumerate() {
595///     match matched {
596///         Some(substr) => println!(
597///             "Match {i}: {}",
598///             std::str::from_utf8(substr).unwrap()
599///         ),
600///         None => println!("Match {i}: <None>"),
601///     }
602/// }
603/// # Ok(())
604/// # }
605/// ```
606#[inline]
607pub fn regaexec_bytes<'a>(
608    compiled_reg: &Regex,
609    data: &'a [u8],
610    params: &RegApproxParams,
611    nmatches: usize,
612    flags: RegexecFlags,
613) -> Result<RegApproxMatchBytes<'a>> {
614    compiled_reg.regaexec_bytes(data, params, nmatches, flags)
615}