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        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
164/// A compiled regex. Clones share the native pattern and may match concurrently.
165#[derive(Clone, Debug)]
166pub struct Regex {
167    code: Arc<Code>,
168    pattern: Arc<str>,
169}
170
171impl Regex {
172    /// Compiles a pattern using the default options.
173    pub fn new(pattern: &str) -> Result<Self, Error> {
174        RegexBuilder::new().build(pattern)
175    }
176
177    /// Returns the original pattern.
178    pub fn as_str(&self) -> &str {
179        &self.pattern
180    }
181
182    /// Returns the number of capture groups, including group zero.
183    pub fn captures_len(&self) -> usize {
184        self.code.capture_count
185    }
186
187    /// Returns whether the regex matches anywhere in the input.
188    pub fn is_match(&self, subject: &[u8]) -> bool {
189        self.is_match_at(subject, 0)
190    }
191
192    /// Searches from a byte offset while preserving anchor and lookbehind context.
193    /// An offset beyond the input returns `false`.
194    pub fn is_match_at(&self, subject: &[u8], start: usize) -> bool {
195        self.code.is_match(subject, start)
196    }
197
198    /// Returns whether the regex matches the entire input.
199    pub fn is_full_match(&self, subject: &[u8]) -> bool {
200        self.code.is_full_match(subject)
201    }
202
203    /// Finds the first match in the input.
204    pub fn find<'s>(&self, subject: &'s [u8]) -> Option<Match<'s>> {
205        self.find_at(subject, 0)
206    }
207
208    /// Finds the first match at or after a byte offset, preserving input context.
209    /// An offset beyond the input returns `None`.
210    pub fn find_at<'s>(&self, subject: &'s [u8], start: usize) -> Option<Match<'s>> {
211        match self.scan_at(subject, start, ControlFlow::Break) {
212            ControlFlow::Break(matched) => Some(matched),
213            ControlFlow::Continue(()) => None,
214        }
215    }
216
217    /// Returns the capture groups of the first match.
218    pub fn captures<'s>(&self, subject: &'s [u8]) -> Option<Captures<'s>> {
219        self.captures_at(subject, 0)
220    }
221
222    /// Returns captures for the first match at or after a byte offset.
223    /// Input context is preserved; offsets beyond the input return `None`.
224    pub fn captures_at<'s>(&self, subject: &'s [u8], start: usize) -> Option<Captures<'s>> {
225        self.code.captures(subject, start)
226    }
227
228    /// Visits non-overlapping matches in leftmost-first order.
229    ///
230    /// Return `ControlFlow::Continue(())` to continue or `ControlFlow::Break(value)`
231    /// to stop and return that value. Empty matches are included: after one,
232    /// nonempty alternatives at the same position are tried before advancing.
233    /// Callbacks may perform nested scans. Callback panics propagate after the
234    /// native scan has stopped, without unwinding through C++.
235    ///
236    /// ```
237    /// use std::ops::ControlFlow;
238    /// use javelin_pattern::bytes::Regex;
239    /// let re = Regex::new(r"\d+")?;
240    /// let mut ranges = Vec::new();
241    /// let result = re.scan(b"12 and 34", |m| {
242    ///     ranges.push(m.range());
243    ///     ControlFlow::<()>::Continue(())
244    /// });
245    /// assert_eq!(result, ControlFlow::Continue(()));
246    /// assert_eq!(ranges, [0..2, 7..9]);
247    /// # Ok::<(), javelin_pattern::Error>(())
248    /// ```
249    pub fn scan<'s, F, B>(&self, subject: &'s [u8], callback: F) -> ControlFlow<B>
250    where
251        F: FnMut(Match<'s>) -> ControlFlow<B>,
252    {
253        self.scan_at(subject, 0, callback)
254    }
255
256    /// Like [`Regex::scan`], starting at a byte offset into the original input.
257    /// An offset beyond the input completes without invoking the callback.
258    pub fn scan_at<'s, F, B>(&self, subject: &'s [u8], start: usize, callback: F) -> ControlFlow<B>
259    where
260        F: FnMut(Match<'s>) -> ControlFlow<B>,
261    {
262        self.code.scan(subject, start, callback)
263    }
264
265    /// Visits capture groups with the iteration and stopping rules of [`Regex::scan`].
266    /// Capture ranges are copied into Rust-owned storage for each match.
267    pub fn scan_captures<'s, F, B>(&self, subject: &'s [u8], callback: F) -> ControlFlow<B>
268    where
269        F: FnMut(Captures<'s>) -> ControlFlow<B>,
270    {
271        self.scan_captures_at(subject, 0, callback)
272    }
273
274    /// Like [`Regex::scan_captures`], starting at a byte offset into the original input.
275    /// An offset beyond the input completes without invoking the callback.
276    pub fn scan_captures_at<'s, F, B>(
277        &self,
278        subject: &'s [u8],
279        start: usize,
280        callback: F,
281    ) -> ControlFlow<B>
282    where
283        F: FnMut(Captures<'s>) -> ControlFlow<B>,
284    {
285        self.code.scan_captures(subject, start, callback)
286    }
287}