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 /// ®aexec_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 /// ®aexec_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/// ®aexec_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/// ®aexec_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}