1use regex::bytes::{Regex, RegexBuilder};
2
3const DEFAULT_SIZE_LIMIT_BYTES: usize = 10 * 1024 * 1024;
4
5#[derive(Clone, Debug)]
6pub enum CompiledPattern {
7 Literal(LiteralSearch),
8 Regex {
9 compiled: Regex,
10 raw_pattern: String,
11 case_insensitive: bool,
12 },
13}
14
15#[derive(Clone, Debug, PartialEq, Eq)]
16pub struct LiteralSearch {
17 pub needle: Vec<u8>,
18 pub case_insensitive_ascii: bool,
19}
20
21#[derive(Clone, Debug)]
22pub struct CompileOpts {
23 pub literal: bool,
24 pub case_insensitive: bool,
25 pub multi_line: bool,
26 pub size_limit_bytes: usize,
27}
28
29impl Default for CompileOpts {
30 fn default() -> Self {
31 Self {
32 literal: false,
33 case_insensitive: false,
34 multi_line: true,
35 size_limit_bytes: DEFAULT_SIZE_LIMIT_BYTES,
36 }
37 }
38}
39
40#[derive(Clone, Debug, PartialEq, Eq)]
41pub enum CompileResult {
42 Ok(CompiledPattern),
43 InvalidPattern { message: String, pattern: String },
44 UnsupportedSyntax { feature: String, pattern: String },
45}
46
47impl PartialEq for CompiledPattern {
48 fn eq(&self, other: &Self) -> bool {
49 match (self, other) {
50 (CompiledPattern::Literal(left), CompiledPattern::Literal(right)) => left == right,
51 (
52 CompiledPattern::Regex {
53 raw_pattern: left_pattern,
54 case_insensitive: left_case,
55 ..
56 },
57 CompiledPattern::Regex {
58 raw_pattern: right_pattern,
59 case_insensitive: right_case,
60 ..
61 },
62 ) => left_pattern == right_pattern && left_case == right_case,
63 _ => false,
64 }
65 }
66}
67
68impl Eq for CompiledPattern {}
69
70impl CompiledPattern {
71 pub fn is_literal(&self) -> bool {
72 matches!(self, CompiledPattern::Literal(_))
73 }
74
75 pub fn case_insensitive(&self) -> bool {
76 match self {
77 CompiledPattern::Literal(literal) => literal.case_insensitive_ascii,
78 CompiledPattern::Regex {
79 case_insensitive, ..
80 } => *case_insensitive,
81 }
82 }
83
84 pub fn raw_pattern_for_trigrams(&self) -> String {
85 match self {
86 CompiledPattern::Literal(literal) => {
87 String::from_utf8_lossy(&literal.needle).into_owned()
88 }
89 CompiledPattern::Regex { raw_pattern, .. } => raw_pattern.clone(),
90 }
91 }
92
93 pub fn ripgrep_pattern(&self) -> String {
94 match self {
95 CompiledPattern::Literal(literal) => {
96 String::from_utf8_lossy(&literal.needle).into_owned()
97 }
98 CompiledPattern::Regex { raw_pattern, .. } => raw_pattern.clone(),
99 }
100 }
101}
102
103pub fn compile(pattern: &str, opts: CompileOpts) -> CompileResult {
104 if pattern.len() > opts.size_limit_bytes {
105 return CompileResult::InvalidPattern {
106 message: format!(
107 "invalid regex: pattern exceeds size limit of {} bytes",
108 opts.size_limit_bytes
109 ),
110 pattern: pattern.to_string(),
111 };
112 }
113
114 if !opts.literal {
115 if let Some(feature) = detect_unsupported_features(pattern) {
116 return CompileResult::UnsupportedSyntax {
117 feature,
118 pattern: pattern.to_string(),
119 };
120 }
121 }
122
123 let has_regex_meta = has_regex_metachar(pattern);
124 let ascii_safe_literal = opts.case_insensitive && pattern.is_ascii();
125 if opts.literal || (!has_regex_meta && (!opts.case_insensitive || ascii_safe_literal)) {
126 if !opts.case_insensitive || pattern.is_ascii() {
127 let needle = if opts.case_insensitive {
128 pattern
129 .as_bytes()
130 .iter()
131 .map(|byte| byte.to_ascii_lowercase())
132 .collect()
133 } else {
134 pattern.as_bytes().to_vec()
135 };
136 return CompileResult::Ok(CompiledPattern::Literal(LiteralSearch {
137 needle,
138 case_insensitive_ascii: opts.case_insensitive,
139 }));
140 }
141 }
142
143 let mut regex_pattern = if opts.literal || !has_regex_meta {
144 regex::escape(pattern)
145 } else {
146 pattern.to_string()
147 };
148 let mut builder_case_insensitive = opts.case_insensitive;
149 if opts.case_insensitive && !pattern.is_ascii() {
150 regex_pattern = format!("(?i){regex_pattern}");
151 builder_case_insensitive = false;
152 }
153
154 let mut builder = RegexBuilder::new(®ex_pattern);
155 builder.case_insensitive(builder_case_insensitive);
156 builder.multi_line(opts.multi_line);
157 builder.size_limit(opts.size_limit_bytes);
158
159 match builder.build() {
160 Ok(compiled) => CompileResult::Ok(CompiledPattern::Regex {
161 compiled,
162 raw_pattern: regex_pattern,
163 case_insensitive: opts.case_insensitive,
164 }),
165 Err(error) => CompileResult::InvalidPattern {
166 message: format!("invalid regex: {error}"),
167 pattern: pattern.to_string(),
168 },
169 }
170}
171
172pub fn detect_unsupported_features(pattern: &str) -> Option<String> {
173 if ["(?=", "(?!", "(?<=", "(?<!"]
174 .iter()
175 .any(|token| contains_syntax_token(pattern, token))
176 {
177 return Some("lookaround".to_string());
178 }
179 if contains_syntax_token(pattern, "(?P=") || contains_numeric_backreference(pattern) {
180 return Some("backreference".to_string());
181 }
182 if ["*+", "++", "?+"]
183 .iter()
184 .any(|token| contains_syntax_token(pattern, token))
185 {
186 return Some("possessive quantifier".to_string());
187 }
188 if contains_syntax_token(pattern, "(?>") {
189 return Some("atomic group".to_string());
190 }
191 None
192}
193
194fn contains_syntax_token(pattern: &str, token: &str) -> bool {
195 let bytes = pattern.as_bytes();
196 let token = token.as_bytes();
197 let mut index = 0;
198 let mut class_depth = 0usize;
199
200 while index < bytes.len() {
201 match bytes[index] {
202 b'\\' => index = index.saturating_add(2),
203 b'[' => {
204 class_depth = class_depth.saturating_add(1);
205 index += 1;
206 }
207 b']' if class_depth > 0 => {
208 class_depth -= 1;
209 index += 1;
210 }
211 _ if class_depth == 0 && bytes[index..].starts_with(token) => return true,
212 _ => index += 1,
213 }
214 }
215
216 false
217}
218
219fn has_regex_metachar(pattern: &str) -> bool {
220 pattern.chars().any(|c| {
221 matches!(
222 c,
223 '.' | '*' | '+' | '?' | '(' | ')' | '[' | ']' | '{' | '}' | '|' | '^' | '$' | '\\'
224 )
225 })
226}
227
228fn contains_numeric_backreference(pattern: &str) -> bool {
229 let mut escaped = false;
230 for ch in pattern.chars() {
231 if escaped {
232 if ('1'..='9').contains(&ch) {
233 return true;
234 }
235 escaped = false;
236 continue;
237 }
238 escaped = ch == '\\';
239 }
240 false
241}
242
243#[cfg(test)]
244mod tests {
245 use super::*;
246
247 fn assert_literal(pattern: &str, case_insensitive: bool, expected: &[u8]) {
248 let result = compile(
249 pattern,
250 CompileOpts {
251 case_insensitive,
252 ..CompileOpts::default()
253 },
254 );
255 match result {
256 CompileResult::Ok(CompiledPattern::Literal(literal)) => {
257 assert_eq!(literal.needle, expected);
258 assert_eq!(literal.case_insensitive_ascii, case_insensitive);
259 }
260 other => panic!("expected literal, got {other:?}"),
261 }
262 }
263
264 #[test]
265 fn literal_pattern_without_metachars_uses_fast_path() {
266 assert_literal("needle", false, b"needle");
267 }
268
269 #[test]
270 fn ascii_case_insensitive_literal_uses_lowercase_fast_path() {
271 assert_literal("Needle", true, b"needle");
272 }
273
274 #[test]
275 fn non_ascii_case_insensitive_literal_forces_regex_with_inline_flag() {
276 let result = compile(
277 "Äbc",
278 CompileOpts {
279 case_insensitive: true,
280 ..CompileOpts::default()
281 },
282 );
283 match result {
284 CompileResult::Ok(CompiledPattern::Regex {
285 raw_pattern,
286 case_insensitive,
287 ..
288 }) => {
289 assert!(raw_pattern.starts_with("(?i)"));
290 assert!(case_insensitive);
291 }
292 other => panic!("expected regex, got {other:?}"),
293 }
294 }
295
296 #[test]
297 fn regex_pattern_retains_raw_pattern_and_compiles_bytes_regex() {
298 let result = compile("foo.*bar", CompileOpts::default());
299 match result {
300 CompileResult::Ok(CompiledPattern::Regex {
301 compiled,
302 raw_pattern,
303 ..
304 }) => {
305 assert_eq!(raw_pattern, "foo.*bar");
306 assert!(compiled.is_match(b"foo middle bar"));
307 }
308 other => panic!("expected regex, got {other:?}"),
309 }
310 }
311
312 #[test]
313 fn invalid_pattern_surfaces_compile_error() {
314 let result = compile("[", CompileOpts::default());
315 assert!(matches!(result, CompileResult::InvalidPattern { .. }));
316 }
317
318 #[test]
319 fn pattern_exceeding_size_limit_is_invalid() {
320 let result = compile(
321 "abcd",
322 CompileOpts {
323 size_limit_bytes: 3,
324 ..CompileOpts::default()
325 },
326 );
327 assert!(matches!(result, CompileResult::InvalidPattern { .. }));
328 }
329
330 #[test]
331 fn unsupported_syntax_is_detected_before_compile() {
332 for pattern in [
333 "(?=foo)",
334 "(?!foo)",
335 "(?<=foo)",
336 "(?<!foo)",
337 "(?P=name)",
338 r"\1",
339 "foo*+",
340 "(?>foo)",
341 ] {
342 assert!(
343 matches!(
344 compile(pattern, CompileOpts::default()),
345 CompileResult::UnsupportedSyntax { .. }
346 ),
347 "{pattern}"
348 );
349 }
350 }
351
352 #[test]
353 fn valid_regex_tokens_are_not_misclassified_as_unsupported() {
354 for (pattern, haystack) in [
355 (r"\*+", b"***".as_slice()),
356 (r"\(?=", b"(=".as_slice()),
357 (r"\(?P=", b"(P=".as_slice()),
358 (r"\(?>", b"(>".as_slice()),
359 (r"[(?=]+", b"(?=".as_slice()),
360 (r"[*+]+", b"*+".as_slice()),
361 (r"[(?>]+", b"(?>".as_slice()),
362 (r"[(?P=]+", b"(?P=".as_slice()),
363 ] {
364 match compile(pattern, CompileOpts::default()) {
365 CompileResult::Ok(CompiledPattern::Regex { compiled, .. }) => {
366 assert!(compiled.is_match(haystack), "{pattern}");
367 }
368 other => panic!("expected valid regex for {pattern}, got {other:?}"),
369 }
370 }
371 }
372
373 #[test]
374 fn forced_literal_honors_regex_characters() {
375 let result = compile(
376 "foo.*bar",
377 CompileOpts {
378 literal: true,
379 ..CompileOpts::default()
380 },
381 );
382 match result {
383 CompileResult::Ok(CompiledPattern::Literal(literal)) => {
384 assert_eq!(literal.needle, b"foo.*bar");
385 }
386 other => panic!("expected literal, got {other:?}"),
387 }
388 }
389}