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}