Skip to main content

javelin_pattern/
bytes.rs

1//! Regular expressions over byte slices. All match offsets are measured in bytes.
2
3use std::{
4    ops::{ControlFlow, Range},
5    sync::Arc,
6};
7
8use javelin_sys as sys;
9
10use crate::{Error, ffi::Code};
11
12/// A match borrowing its input. The end offset is exclusive.
13#[derive(Clone, Copy, Debug, Eq, PartialEq)]
14pub struct Match<'s> {
15    subject: &'s [u8],
16    start: usize,
17    end: usize,
18}
19
20impl<'s> Match<'s> {
21    pub(crate) fn new(subject: &'s [u8], start: usize, end: usize) -> Self {
22        debug_assert!(
23            start <= end && end <= subject.len(),
24            "invalid native match range"
25        );
26        Self {
27            subject,
28            start,
29            end,
30        }
31    }
32
33    /// Returns the starting byte offset.
34    pub fn start(&self) -> usize {
35        self.start
36    }
37
38    /// Returns the exclusive ending byte offset.
39    pub fn end(&self) -> usize {
40        self.end
41    }
42
43    /// Returns the matched byte range in the original input.
44    pub fn range(&self) -> Range<usize> {
45        self.start..self.end
46    }
47
48    /// Returns the matched bytes.
49    pub fn as_bytes(&self) -> &'s [u8] {
50        &self.subject[self.start..self.end]
51    }
52}
53
54/// Capture groups for one match. Group zero is the whole match.
55///
56/// Captures own their ranges and borrow only the input, so they may be retained
57/// after a scan callback returns. Groups are addressed by number.
58#[derive(Clone, Debug, Eq, PartialEq)]
59pub struct Captures<'s> {
60    pub(crate) subject: &'s [u8],
61    pub(crate) ranges: Vec<Option<(usize, usize)>>,
62}
63
64impl<'s> Captures<'s> {
65    /// Returns a participating group, or `None` for an unmatched or absent group.
66    pub fn get(&self, index: usize) -> Option<Match<'s>> {
67        let (start, end) = self.ranges.get(index).copied().flatten()?;
68        Some(Match::new(self.subject, start, end))
69    }
70
71    /// Returns the number of capture groups, including group zero.
72    pub fn len(&self) -> usize {
73        self.ranges.len()
74    }
75
76    /// Returns whether there are no groups. Successful matches include group zero.
77    pub fn is_empty(&self) -> bool {
78        self.ranges.is_empty()
79    }
80
81    /// Visits groups in numerical order, including nonparticipating groups.
82    pub fn iter(&self) -> impl ExactSizeIterator<Item = Option<Match<'s>>> + '_ {
83        (0..self.len()).map(|index| self.get(index))
84    }
85}
86
87/// Options for compiling a regex. By default, matching operates on bytes with
88/// ASCII character classes and case-sensitive matching.
89#[derive(Clone, Debug, Default)]
90pub struct RegexBuilder {
91    options: i32,
92}
93
94impl RegexBuilder {
95    /// Creates a builder with the default options.
96    pub fn new() -> Self {
97        Self::default()
98    }
99
100    /// Compiles a pattern. Literal NUL bytes must be written as `\x00` escapes.
101    pub fn build(&self, pattern: &str) -> Result<Regex, Error> {
102        Ok(Regex {
103            code: Arc::new(Code::new(pattern, self.options)?),
104            pattern: Arc::from(pattern),
105        })
106    }
107
108    fn flag(&mut self, flag: i32, enabled: bool) -> &mut Self {
109        if enabled {
110            self.options |= flag;
111        } else {
112            self.options &= !flag;
113        }
114        self
115    }
116
117    /// Enables case-insensitive matching.
118    pub fn caseless(&mut self, yes: bool) -> &mut Self {
119        self.flag(sys::JP_OPTION_IGNORE_CASE, yes)
120    }
121
122    /// Allows `^` and `$` to match line boundaries inside the input.
123    pub fn multi_line(&mut self, yes: bool) -> &mut Self {
124        self.flag(sys::JP_OPTION_MULTILINE, yes)
125    }
126
127    /// Allows `.` to match newlines.
128    pub fn dotall(&mut self, yes: bool) -> &mut Self {
129        self.flag(sys::JP_OPTION_DOTALL, yes)
130    }
131
132    /// Decodes UTF-8 instead of matching individual bytes. Offsets remain bytes.
133    pub fn utf(&mut self, yes: bool) -> &mut Self {
134        self.flag(sys::JP_OPTION_UTF8, yes)
135    }
136
137    /// Uses Unicode properties for `\d`, `\w`, `\s`, and word boundaries.
138    /// This is independent of UTF-8 decoding and case folding.
139    pub fn ucp(&mut self, yes: bool) -> &mut Self {
140        self.flag(sys::JP_OPTION_UCP, yes)
141    }
142
143    /// Enables Unicode case folding when case-insensitive matching is enabled.
144    pub fn unicode_case(&mut self, yes: bool) -> &mut Self {
145        self.flag(sys::JP_OPTION_UNICODE_CASE, yes)
146    }
147
148    /// Reverses the default greediness of quantifiers.
149    pub fn ungreedy(&mut self, yes: bool) -> &mut Self {
150        self.flag(sys::JP_OPTION_UNGREEDY, yes)
151    }
152
153    /// Requires matches to start at the beginning of the input.
154    pub fn anchored(&mut self, yes: bool) -> &mut Self {
155        self.flag(sys::JP_OPTION_ANCHORED, yes)
156    }
157
158    /// Parses the pattern using JavelinPattern's glob syntax.
159    pub fn glob(&mut self, yes: bool) -> &mut Self {
160        self.flag(sys::JP_OPTION_GLOB_SYNTAX, yes)
161    }
162
163    /// Prefers the backtracking processor over automatic processor selection.
164    pub fn prefer_backtracking(&mut self, yes: bool) -> &mut Self {
165        self.flag(sys::JP_OPTION_PREFER_BACK_TRACKING, yes)
166    }
167
168    /// Replaces unused captures with non-capturing groups. Captures needed by
169    /// backreferences and recursion are retained. This can change capture counts
170    /// and numbering; leave it disabled when extracting capture groups.
171    pub fn auto_cluster(&mut self, yes: bool) -> &mut Self {
172        // Pattern::AUTO_CLUSTER is not named in the native C header.
173        self.flag(0x200, yes)
174    }
175}
176
177/// A compiled regex. Clones share the native pattern and may match concurrently.
178#[derive(Clone, Debug)]
179pub struct Regex {
180    code: Arc<Code>,
181    pattern: Arc<str>,
182}
183
184impl Regex {
185    /// Compiles a pattern using the default options.
186    pub fn new(pattern: &str) -> Result<Self, Error> {
187        RegexBuilder::new().build(pattern)
188    }
189
190    /// Returns the original pattern.
191    pub fn as_str(&self) -> &str {
192        &self.pattern
193    }
194
195    /// Returns the number of capture groups, including group zero.
196    pub fn captures_len(&self) -> usize {
197        self.code.capture_count
198    }
199
200    /// Returns whether the regex matches anywhere in the input.
201    pub fn is_match(&self, subject: &[u8]) -> bool {
202        self.is_match_at(subject, 0)
203    }
204
205    /// Searches from a byte offset while preserving anchor and lookbehind context.
206    /// An offset beyond the input returns `false`.
207    pub fn is_match_at(&self, subject: &[u8], start: usize) -> bool {
208        self.code.is_match(subject, start)
209    }
210
211    /// Returns whether the regex matches the entire input.
212    pub fn is_full_match(&self, subject: &[u8]) -> bool {
213        self.code.is_full_match(subject)
214    }
215
216    /// Finds the first match in the input.
217    pub fn find<'s>(&self, subject: &'s [u8]) -> Option<Match<'s>> {
218        self.find_at(subject, 0)
219    }
220
221    /// Finds the first match at or after a byte offset, preserving input context.
222    /// An offset beyond the input returns `None`.
223    pub fn find_at<'s>(&self, subject: &'s [u8], start: usize) -> Option<Match<'s>> {
224        match self.scan_at(subject, start, ControlFlow::Break) {
225            ControlFlow::Break(matched) => Some(matched),
226            ControlFlow::Continue(()) => None,
227        }
228    }
229
230    /// Returns the capture groups of the first match.
231    pub fn captures<'s>(&self, subject: &'s [u8]) -> Option<Captures<'s>> {
232        self.captures_at(subject, 0)
233    }
234
235    /// Returns captures for the first match at or after a byte offset.
236    /// Input context is preserved; offsets beyond the input return `None`.
237    pub fn captures_at<'s>(&self, subject: &'s [u8], start: usize) -> Option<Captures<'s>> {
238        self.code.captures(subject, start)
239    }
240
241    /// Visits non-overlapping matches in leftmost-first order.
242    ///
243    /// Return `ControlFlow::Continue(())` to continue or `ControlFlow::Break(value)`
244    /// to stop and return that value. Empty matches are included: after one,
245    /// nonempty alternatives at the same position are tried before advancing.
246    /// Callbacks may perform nested scans. Callback panics propagate after the
247    /// native scan has stopped, without unwinding through C++.
248    ///
249    /// ```
250    /// use std::ops::ControlFlow;
251    /// use javelin_pattern::bytes::Regex;
252    /// let re = Regex::new(r"\d+")?;
253    /// let mut ranges = Vec::new();
254    /// let result = re.scan(b"12 and 34", |m| {
255    ///     ranges.push(m.range());
256    ///     ControlFlow::<()>::Continue(())
257    /// });
258    /// assert_eq!(result, ControlFlow::Continue(()));
259    /// assert_eq!(ranges, [0..2, 7..9]);
260    /// # Ok::<(), javelin_pattern::Error>(())
261    /// ```
262    pub fn scan<'s, F, B>(&self, subject: &'s [u8], callback: F) -> ControlFlow<B>
263    where
264        F: FnMut(Match<'s>) -> ControlFlow<B>,
265    {
266        self.scan_at(subject, 0, callback)
267    }
268
269    /// Like [`Regex::scan`], starting at a byte offset into the original input.
270    /// An offset beyond the input completes without invoking the callback.
271    pub fn scan_at<'s, F, B>(&self, subject: &'s [u8], start: usize, callback: F) -> ControlFlow<B>
272    where
273        F: FnMut(Match<'s>) -> ControlFlow<B>,
274    {
275        self.code.scan(subject, start, callback)
276    }
277
278    /// Visits capture groups with the iteration and stopping rules of [`Regex::scan`].
279    /// Capture ranges are copied into Rust-owned storage for each match.
280    pub fn scan_captures<'s, F, B>(&self, subject: &'s [u8], callback: F) -> ControlFlow<B>
281    where
282        F: FnMut(Captures<'s>) -> ControlFlow<B>,
283    {
284        self.scan_captures_at(subject, 0, callback)
285    }
286
287    /// Like [`Regex::scan_captures`], starting at a byte offset into the original input.
288    /// An offset beyond the input completes without invoking the callback.
289    pub fn scan_captures_at<'s, F, B>(
290        &self,
291        subject: &'s [u8],
292        start: usize,
293        callback: F,
294    ) -> ControlFlow<B>
295    where
296        F: FnMut(Captures<'s>) -> ControlFlow<B>,
297    {
298        self.code.scan_captures(subject, start, callback)
299    }
300}