1const MAX_MATCH_CALLS: usize = 100_000;
15
16pub fn contains_glob(s: &str) -> bool {
43 s.contains('*') || s.contains('?') || s.contains('[')
44}
45
46pub fn glob_match(pattern: &str, input: &str) -> bool {
47 use std::cell::Cell;
48
49 let expanded = expand_braces(pattern);
51 let calls = Cell::new(0usize);
52 for pat in expanded {
53 let pat_chars: Vec<char> = pat.chars().collect();
54 let input_chars: Vec<char> = input.chars().collect();
55 if match_bounded(&pat_chars, 0, &input_chars, 0, &calls) {
56 return true;
57 }
58 }
59 false
60}
61
62pub fn expand_braces(pattern: &str) -> Vec<String> {
75 let chars: Vec<char> = pattern.chars().collect();
76
77 let mut depth = 0;
79 let mut brace_start = None;
80 let mut brace_end = None;
81
82 for (i, &c) in chars.iter().enumerate() {
83 match c {
84 '{' => {
85 if depth == 0 {
86 brace_start = Some(i);
87 }
88 depth += 1;
89 }
90 '}' => {
91 depth -= 1;
92 if depth == 0 && brace_start.is_some() {
93 brace_end = Some(i);
94 break;
95 }
96 }
97 _ => {}
98 }
99 }
100
101 let (start, end) = match (brace_start, brace_end) {
103 (Some(s), Some(e)) => (s, e),
104 _ => return vec![pattern.to_string()],
105 };
106
107 let prefix: String = chars[..start].iter().collect();
109 let suffix: String = chars[end + 1..].iter().collect();
110 let brace_content: String = chars[start + 1..end].iter().collect();
111
112 let alternatives = split_brace_alternatives(&brace_content);
114
115 let mut results = Vec::new();
117 for alt in alternatives {
118 let combined = format!("{}{}{}", prefix, alt, suffix);
119 results.extend(expand_braces(&combined));
121 }
122
123 results
124}
125
126fn split_brace_alternatives(content: &str) -> Vec<String> {
128 let mut alternatives = Vec::new();
129 let mut current = String::new();
130 let mut depth = 0;
131
132 for c in content.chars() {
133 match c {
134 '{' => {
135 depth += 1;
136 current.push(c);
137 }
138 '}' => {
139 depth -= 1;
140 current.push(c);
141 }
142 ',' if depth == 0 => {
143 alternatives.push(current);
144 current = String::new();
145 }
146 _ => current.push(c),
147 }
148 }
149
150 alternatives.push(current);
152
153 alternatives
154}
155
156fn match_bounded(
161 pattern: &[char],
162 pi: usize,
163 input: &[char],
164 ii: usize,
165 calls: &std::cell::Cell<usize>,
166) -> bool {
167 let count = calls.get() + 1;
168 calls.set(count);
169 if count > MAX_MATCH_CALLS {
170 return false;
171 }
172
173 if pi >= pattern.len() && ii >= input.len() {
175 return true;
176 }
177
178 if pi >= pattern.len() {
180 return false;
181 }
182
183 match pattern[pi] {
184 '*' => {
185 let mut next_pi = pi;
187 while next_pi < pattern.len() && pattern[next_pi] == '*' {
188 next_pi += 1;
189 }
190
191 if next_pi >= pattern.len() {
193 return true;
194 }
195
196 for skip in 0..=(input.len() - ii) {
198 if match_bounded(pattern, next_pi, input, ii + skip, calls) {
199 return true;
200 }
201 }
202 false
203 }
204
205 '?' => {
206 if ii >= input.len() {
208 return false;
209 }
210 match_bounded(pattern, pi + 1, input, ii + 1, calls)
211 }
212
213 '[' => {
214 if ii >= input.len() {
216 return false;
217 }
218
219 let (matches, end_idx) = parse_char_class(&pattern[pi..], input[ii]);
221 if matches {
222 match_bounded(pattern, pi + end_idx, input, ii + 1, calls)
223 } else {
224 false
225 }
226 }
227
228 '\\' if pi + 1 < pattern.len() => {
230 if ii >= input.len() {
231 return false;
232 }
233 if pattern[pi + 1] == input[ii] {
234 match_bounded(pattern, pi + 2, input, ii + 1, calls)
235 } else {
236 false
237 }
238 }
239
240 c => {
241 if ii >= input.len() {
243 return false;
244 }
245 if c == input[ii] {
246 match_bounded(pattern, pi + 1, input, ii + 1, calls)
247 } else {
248 false
249 }
250 }
251 }
252}
253
254fn parse_char_class(pattern: &[char], ch: char) -> (bool, usize) {
258 if pattern.is_empty() || pattern[0] != '[' {
259 return (false, 0);
260 }
261
262 let mut idx = 1;
263 let mut negate = false;
264
265 if idx < pattern.len() && (pattern[idx] == '!' || pattern[idx] == '^') {
267 negate = true;
268 idx += 1;
269 }
270
271 let first_char = idx;
273 let mut matched = false;
274
275 while idx < pattern.len() {
276 let c = pattern[idx];
277
278 if c == ']' && idx > first_char {
280 idx += 1;
281 break;
282 }
283
284 if idx + 2 < pattern.len() && pattern[idx + 1] == '-' && pattern[idx + 2] != ']' {
286 let start = c;
287 let end = pattern[idx + 2];
288 if ch >= start && ch <= end {
289 matched = true;
290 }
291 idx += 3;
292 continue;
293 }
294
295 if c == ch {
297 matched = true;
298 }
299 idx += 1;
300 }
301
302 if idx >= pattern.len() && (pattern.len() < 2 || pattern[pattern.len() - 1] != ']') {
304 return (pattern[0] == ch, 1);
305 }
306
307 (if negate { !matched } else { matched }, idx)
308}
309
310#[cfg(test)]
311mod tests {
312 use super::*;
313
314 #[test]
315 fn literal_matches() {
316 assert!(glob_match("hello", "hello"));
317 assert!(glob_match("", ""));
318 assert!(!glob_match("hello", "world"));
319 assert!(!glob_match("hello", "hell"));
320 assert!(!glob_match("hello", "helloo"));
321 }
322
323 #[test]
324 fn star_wildcard() {
325 assert!(glob_match("*", ""));
326 assert!(glob_match("*", "anything"));
327 assert!(glob_match("*.rs", "main.rs"));
328 assert!(glob_match("*.rs", ".rs"));
329 assert!(glob_match("test*", "test"));
330 assert!(glob_match("test*", "testing"));
331 assert!(glob_match("*test*", "mytestfile"));
332 assert!(glob_match("a*b*c", "abc"));
333 assert!(glob_match("a*b*c", "aXXXbYYYc"));
334 assert!(!glob_match("*.rs", "main.txt"));
335 assert!(!glob_match("test*", "mytest"));
336 }
337
338 #[test]
339 fn question_wildcard() {
340 assert!(glob_match("?", "a"));
341 assert!(glob_match("???", "abc"));
342 assert!(glob_match("test?", "test1"));
343 assert!(glob_match("?est", "test"));
344 assert!(!glob_match("?", ""));
345 assert!(!glob_match("?", "ab"));
346 assert!(!glob_match("???", "ab"));
347 }
348
349 #[test]
350 fn char_class_simple() {
351 assert!(glob_match("[abc]", "a"));
352 assert!(glob_match("[abc]", "b"));
353 assert!(glob_match("[abc]", "c"));
354 assert!(!glob_match("[abc]", "d"));
355 assert!(!glob_match("[abc]", ""));
356 }
357
358 #[test]
359 fn char_class_range() {
360 assert!(glob_match("[a-z]", "m"));
361 assert!(glob_match("[a-z]", "a"));
362 assert!(glob_match("[a-z]", "z"));
363 assert!(!glob_match("[a-z]", "A"));
364 assert!(!glob_match("[a-z]", "0"));
365 assert!(glob_match("[0-9]", "5"));
366 assert!(glob_match("[a-zA-Z]", "M"));
367 }
368
369 #[test]
370 fn char_class_negated() {
371 assert!(glob_match("[!abc]", "d"));
372 assert!(glob_match("[^abc]", "d"));
373 assert!(!glob_match("[!abc]", "a"));
374 assert!(!glob_match("[^abc]", "b"));
375 }
376
377 #[test]
378 fn escape_sequence() {
379 assert!(glob_match("\\*", "*"));
380 assert!(glob_match("\\?", "?"));
381 assert!(glob_match("test\\*", "test*"));
382 assert!(!glob_match("\\*", "a"));
383 }
384
385 #[test]
386 fn combined_patterns() {
387 assert!(glob_match("*.tar.gz", "archive.tar.gz"));
388 assert!(glob_match("file[0-9].txt", "file5.txt"));
389 assert!(glob_match("test_?_*.rs", "test_a_foo.rs"));
390 assert!(!glob_match("file[0-9].txt", "filea.txt"));
391 }
392
393 #[test]
394 fn case_statement_patterns() {
395 assert!(glob_match("*.rs", "main.rs"));
396 assert!(glob_match("*.py", "script.py"));
397 assert!(glob_match("y", "y"));
398 assert!(glob_match("yes", "yes"));
399 assert!(glob_match("[Yy]*", "Yes"));
400 assert!(glob_match("[Yy]*", "yes"));
401 assert!(glob_match("[Yy]*", "y"));
402 assert!(glob_match("[Nn]*", "no"));
403 assert!(glob_match("*", "anything"));
404 }
405
406 #[test]
407 fn consecutive_stars() {
408 assert!(glob_match("**", "anything"));
409 assert!(glob_match("a**b", "ab"));
410 assert!(glob_match("a**b", "aXXXb"));
411 }
412
413 #[test]
414 fn edge_cases() {
415 assert!(glob_match("", ""));
416 assert!(!glob_match("", "a"));
417 assert!(glob_match("a*", "a"));
418 assert!(glob_match("*a", "a"));
419 assert!(glob_match("*/*", "foo/bar"));
420 assert!(!glob_match("*/*", "foobar"));
421 }
422
423 #[test]
424 fn char_class_literal_dash() {
425 assert!(glob_match("[-abc]", "-"));
426 assert!(glob_match("[-abc]", "a"));
427 assert!(glob_match("[abc-]", "-"));
428 assert!(glob_match("[abc-]", "c"));
429 assert!(glob_match("[a-c]", "b"));
430 assert!(!glob_match("[a-c]", "-"));
431 }
432
433 #[test]
434 fn char_class_literal_bracket() {
435 assert!(glob_match("[]abc]", "]"));
436 assert!(glob_match("[]abc]", "a"));
437 assert!(glob_match("[!]abc]", "x"));
438 assert!(!glob_match("[!]abc]", "]"));
439 }
440
441 #[test]
442 fn char_class_multiple_ranges() {
443 assert!(glob_match("[a-zA-Z0-9]", "m"));
444 assert!(glob_match("[a-zA-Z0-9]", "M"));
445 assert!(glob_match("[a-zA-Z0-9]", "5"));
446 assert!(!glob_match("[a-zA-Z0-9]", "_"));
447 assert!(!glob_match("[a-zA-Z0-9]", " "));
448 }
449
450 #[test]
451 fn char_class_with_wildcards() {
452 assert!(glob_match("[abc]*", "aXXX"));
453 assert!(glob_match("[abc]*", "a"));
454 assert!(!glob_match("[abc]*", "dXXX"));
455 assert!(glob_match("*[0-9]", "test5"));
456 assert!(glob_match("*[0-9]", "5"));
457 assert!(!glob_match("*[0-9]", "test"));
458 assert!(glob_match("[abc]?", "a1"));
459 assert!(!glob_match("[abc]?", "a"));
460 assert!(!glob_match("[abc]?", "a12"));
461 }
462
463 #[test]
464 fn multiple_char_classes() {
465 assert!(glob_match("[abc][123]", "a1"));
466 assert!(glob_match("[abc][123]", "c3"));
467 assert!(!glob_match("[abc][123]", "a4"));
468 assert!(!glob_match("[abc][123]", "d1"));
469 assert!(glob_match("[a-z][A-Z][0-9]", "xY9"));
470 }
471
472 #[test]
473 fn backtracking_stress() {
474 assert!(glob_match("a*a*a*a*a*a*a*a", "aaaaaaaaaaaaaaaa"));
475 assert!(!glob_match("a*a*a*a*a*a*a*ab", "aaaaaaaaaaaaaaaa"));
476 assert!(glob_match("*a*b*c", "XXXaYYYbZZZc"));
477 assert!(glob_match("*a*b*c", "abc"));
478 assert!(!glob_match("*a*b*c", "XXXaYYYcZZZb"));
479 assert!(glob_match("*.*.txt", "file.backup.txt"));
480 assert!(!glob_match("*.*.txt", "file.txt"));
481 }
482
483 #[test]
484 fn real_world_file_patterns() {
485 assert!(glob_match("*.rs", "main.rs"));
486 assert!(glob_match("*.rs", "lib.rs"));
487 assert!(glob_match("*_test.rs", "parser_test.rs"));
488 assert!(!glob_match("*_test.rs", "parser.rs"));
489 assert!(glob_match(".*", ".gitignore"));
490 assert!(glob_match(".*", ".env"));
491 assert!(!glob_match(".*", "visible"));
492 assert!(glob_match("*.tar.gz", "archive.tar.gz"));
493 assert!(glob_match("*.tar.gz", "backup.tar.gz"));
494 assert!(!glob_match("*.tar.gz", "archive.tar"));
495 assert!(!glob_match("*.tar.gz", "archive.gz"));
496 assert!(glob_match("app.log.[0-9]", "app.log.1"));
497 assert!(glob_match("app.log.[0-9]", "app.log.9"));
498 assert!(!glob_match("app.log.[0-9]", "app.log.10"));
499 assert!(glob_match("*.{json,yaml,toml}", "config.json"));
500 assert!(glob_match("*.{json,yaml,toml}", "config.yaml"));
501 assert!(glob_match("*.{json,yaml,toml}", "config.toml"));
502 assert!(!glob_match("*.{json,yaml,toml}", "config.xml"));
503 }
504
505 #[test]
506 fn special_characters_in_input() {
507 assert!(glob_match("test", "test"));
508 assert!(!glob_match("test", "te*t"));
509 assert!(!glob_match("test", "te?t"));
510 assert!(glob_match("file\\[1\\]", "file[1]"));
511 assert!(glob_match("test\\?", "test?"));
512 }
513
514 #[test]
515 fn whitespace_handling() {
516 assert!(glob_match("hello world", "hello world"));
517 assert!(glob_match("hello*world", "hello world"));
518 assert!(glob_match("* *", "hello world"));
519 assert!(glob_match("*\t*", "hello\tworld"));
520 }
521
522 #[test]
523 fn case_sensitivity() {
524 assert!(glob_match("Hello", "Hello"));
525 assert!(!glob_match("Hello", "hello"));
526 assert!(!glob_match("hello", "Hello"));
527 assert!(glob_match("[Hh]ello", "Hello"));
528 assert!(glob_match("[Hh]ello", "hello"));
529 }
530
531 #[test]
532 fn long_strings() {
533 let long_str = "a".repeat(1000);
534 assert!(glob_match("*", &long_str));
535 assert!(glob_match("a*", &long_str));
536 assert!(glob_match("*a", &long_str));
537 let mixed = format!("{}X{}", "a".repeat(500), "a".repeat(500));
538 assert!(glob_match("*X*", &mixed));
539 assert!(!glob_match("*Y*", &mixed));
540 }
541
542 #[test]
543 fn unicode_basic() {
544 assert!(glob_match("héllo", "héllo"));
545 assert!(glob_match("*ñ*", "español"));
546 assert!(glob_match("?", "ü"));
547 assert!(glob_match("[αβγ]", "β"));
548 }
549
550 #[test]
551 fn negated_char_class_edge_cases() {
552 assert!(glob_match("[!a-z]", "A"));
553 assert!(glob_match("[!a-z]", "5"));
554 assert!(!glob_match("[!a-z]", "m"));
555 assert!(glob_match("[!a-zA-Z]", "5"));
556 assert!(!glob_match("[!a-zA-Z]", "x"));
557 assert!(!glob_match("[!a-zA-Z]", "X"));
558 }
559
560 #[test]
561 fn path_like_patterns() {
562 assert!(glob_match("src/*.rs", "src/main.rs"));
563 assert!(!glob_match("src/*.rs", "test/main.rs"));
564 assert!(glob_match("*/*/*.rs", "src/foo/bar.rs"));
565 assert!(!glob_match("*/*/*.rs", "src/bar.rs"));
566 assert!(glob_match("v?.0", "v1.0"));
567 assert!(glob_match("v?.0", "v2.0"));
568 assert!(!glob_match("v?.0", "v10.0"));
569 }
570
571 #[test]
572 fn brace_expansion_basic() {
573 assert!(glob_match("{foo,bar}", "foo"));
574 assert!(glob_match("{foo,bar}", "bar"));
575 assert!(!glob_match("{foo,bar}", "baz"));
576 assert!(glob_match("test_{a,b,c}", "test_a"));
577 assert!(glob_match("test_{a,b,c}", "test_b"));
578 assert!(glob_match("test_{a,b,c}", "test_c"));
579 assert!(!glob_match("test_{a,b,c}", "test_d"));
580 assert!(glob_match("{debug,release}.exe", "debug.exe"));
581 assert!(glob_match("{debug,release}.exe", "release.exe"));
582 assert!(glob_match("lib{foo,bar}.so", "libfoo.so"));
583 assert!(glob_match("lib{foo,bar}.so", "libbar.so"));
584 }
585
586 #[test]
587 fn brace_expansion_with_wildcards() {
588 assert!(glob_match("*.{rs,go,py}", "main.rs"));
589 assert!(glob_match("*.{rs,go,py}", "server.go"));
590 assert!(glob_match("*.{rs,go,py}", "script.py"));
591 assert!(!glob_match("*.{rs,go,py}", "style.css"));
592 assert!(glob_match("file{1,2,3}.txt", "file1.txt"));
593 assert!(glob_match("test?.{log,txt}", "test1.log"));
594 assert!(glob_match("test?.{log,txt}", "testA.txt"));
595 assert!(glob_match("[abc].{x,y}", "a.x"));
596 assert!(glob_match("[abc].{x,y}", "b.y"));
597 }
598
599 #[test]
600 fn brace_expansion_multiple_braces() {
601 assert!(glob_match("{a,b}{1,2}", "a1"));
602 assert!(glob_match("{a,b}{1,2}", "a2"));
603 assert!(glob_match("{a,b}{1,2}", "b1"));
604 assert!(glob_match("{a,b}{1,2}", "b2"));
605 assert!(!glob_match("{a,b}{1,2}", "c1"));
606 assert!(glob_match("{a,b}{1,2}{x,y}", "a1x"));
607 assert!(glob_match("{a,b}{1,2}{x,y}", "b2y"));
608 }
609
610 #[test]
611 fn brace_expansion_nested() {
612 assert!(glob_match("{a,{b,c}}", "a"));
613 assert!(glob_match("{a,{b,c}}", "b"));
614 assert!(glob_match("{a,{b,c}}", "c"));
615 assert!(glob_match("{{a,b},{c,d}}", "a"));
616 assert!(glob_match("{{a,b},{c,d}}", "b"));
617 assert!(glob_match("{{a,b},{c,d}}", "c"));
618 assert!(glob_match("{{a,b},{c,d}}", "d"));
619 }
620
621 #[test]
622 fn brace_expansion_empty_alternatives() {
623 assert!(glob_match("{,un}do", "do"));
624 assert!(glob_match("{,un}do", "undo"));
625 assert!(glob_match("test{,s}", "test"));
626 assert!(glob_match("test{,s}", "tests"));
627 }
628
629 #[test]
630 fn brace_expansion_single_item() {
631 assert!(glob_match("{foo}", "foo"));
632 assert!(!glob_match("{foo}", "bar"));
633 assert!(glob_match("test_{only}.rs", "test_only.rs"));
634 }
635
636 #[test]
637 fn brace_expansion_real_world() {
638 assert!(glob_match("src/**/*.{ts,tsx,js,jsx}", "src/**/*.ts"));
639 assert!(glob_match("{M,m}akefile", "Makefile"));
640 assert!(glob_match("{M,m}akefile", "makefile"));
641 assert!(glob_match("README{,.md,.txt}", "README"));
642 assert!(glob_match("README{,.md,.txt}", "README.md"));
643 assert!(glob_match("README{,.md,.txt}", "README.txt"));
644 assert!(glob_match("{LICENSE,LICENCE}{,.md,.txt}", "LICENSE"));
645 assert!(glob_match("{LICENSE,LICENCE}{,.md,.txt}", "LICENCE.md"));
646 assert!(glob_match("{,.}config{,.json,.yaml}", "config"));
647 assert!(glob_match("{,.}config{,.json,.yaml}", ".config"));
648 assert!(glob_match("{,.}config{,.json,.yaml}", "config.json"));
649 assert!(glob_match("{,.}config{,.json,.yaml}", ".config.yaml"));
650 assert!(glob_match("{D,d}ocker{file,-compose.yml}", "Dockerfile"));
651 assert!(glob_match("{D,d}ocker{file,-compose.yml}", "dockerfile"));
652 assert!(glob_match("{D,d}ocker{file,-compose.yml}", "Docker-compose.yml"));
653 }
654
655 #[test]
656 fn brace_expansion_no_braces() {
657 assert!(glob_match("simple", "simple"));
658 assert!(glob_match("*.rs", "main.rs"));
659 assert!(glob_match("[abc]", "b"));
660 }
661
662 #[test]
663 fn brace_expansion_unclosed() {
664 assert!(glob_match("{abc", "{abc"));
665 assert!(glob_match("test{", "test{"));
666 assert!(glob_match("abc}", "abc}"));
667 }
668
669 #[test]
670 fn expand_braces_unit() {
671 assert_eq!(expand_braces("simple"), vec!["simple"]);
672 assert_eq!(expand_braces("{a,b}"), vec!["a", "b"]);
673 assert_eq!(expand_braces("x{a,b}y"), vec!["xay", "xby"]);
674 let mut result = expand_braces("{a,b}{1,2}");
675 result.sort();
676 assert_eq!(result, vec!["a1", "a2", "b1", "b2"]);
677 }
678
679 #[test]
680 fn redos_protection() {
681 let pattern = format!("{}b", "*a".repeat(50));
684 let input = "a".repeat(100);
685 let _result = glob_match(&pattern, &input);
687 }
688}