fast_glob/lib.rs
1//! `fast-glob` is a high-performance glob matching crate for Rust, originally forked from [`devongovett/glob-match`](https://github.com/devongovett/glob-match).
2//! This crate provides efficient glob pattern matching with support for multi-pattern matching and brace expansion.
3//!
4//! ## Key Features
5//!
6//! - Up to 60% performance improvement.
7//! - Support for more complex and efficient brace expansion.
8//! - Fixed matching issues with wildcard and globstar [`glob-match/issues#9`](https://github.com/devongovett/glob-match/issues/9).
9//!
10//! ## Examples
11//!
12//! ```rust
13//! use fast_glob::glob_match;
14//!
15//! let glob = "some/**/n*d[k-m]e?txt";
16//! let path = "some/a/bigger/path/to/the/crazy/needle.txt";
17//!
18//! assert!(glob_match(glob, path));
19//! ```
20//!
21//! ## Validation
22//!
23//! [`glob_match`] does not report invalid patterns — an unclosed `{` or `[`,
24//! a trailing `\`, more than 10 brace groups, or brace expansions nested deeper
25//! than 10 levels have an unspecified result (typically no match). This is a deliberate performance
26//! trade-off: there is no compile step, and the pattern is interpreted lazily
27//! while matching, so reliably detecting a malformed pattern would require an
28//! extra scan on every call. Validation is instead a separate, one-time step —
29//! use [`validate`] to reject such patterns with a descriptive [`Error`]:
30//!
31//! ```rust
32//! use fast_glob::{validate, Error, ErrorKind};
33//!
34//! assert!(validate("some/**/n*d[k-m]e?txt").is_ok());
35//! assert_eq!(
36//! validate("src/**/*.{js,ts"),
37//! Err(Error { kind: ErrorKind::UnclosedBrace, index: 9 })
38//! );
39//! ```
40//!
41//! ## Syntax
42//!
43//! `fast-glob` supports the following glob pattern syntax:
44//!
45//! | Syntax | Meaning |
46//! | ------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
47//! | `?` | Matches any single character. |
48//! | `*` | Matches zero or more characters, except for path separators (e.g., `/`). |
49//! | `**` | Matches zero or more characters, including path separators. Must match a complete path segment (i.e., followed by a `/` or the end of the pattern). |
50//! | `[ab]` | Matches one of the characters contained in the brackets, except path separators. Character ranges, e.g., `[a-z]`, are also supported. Use `[!ab]` or `[^ab]` to match any character _except_ those contained in the brackets. |
51//! | `{a,b}` | Matches one of the patterns contained in the braces. Any of the wildcard characters can be used in the sub-patterns. Patterns may contain up to 10 brace groups, nested up to 10 levels deep. |
52//! | `!` | When at the start of the glob, this negates the result. Multiple `!` characters negate the glob multiple times. |
53//! | `\` | A backslash character may be used to escape any of the above special characters. |
54//!
55//! ---
56//!
57//! For detailed usage and API reference, refer to the specific function and struct documentation.
58//!
59//! For any issues or contributions, please visit the [GitHub repository](https://github.com/oxc-project/fast-glob).
60
61/**
62 * The following code was originally forked from
63 * https://github.com/devongovett/glob-match/blob/d5a6c67/src/lib.rs
64 *
65 * MIT Licensed
66 * Copyright (c) 2023 Devon Govett
67 * https://github.com/devongovett/glob-match/tree/main/LICENSE
68 */
69use std::fmt;
70use std::path::is_separator;
71
72use arrayvec::ArrayVec;
73
74const MAX_BRACE_NESTING: usize = 10;
75const MAX_BRACE_GROUPS: usize = 10;
76
77#[derive(Clone, Debug, Default)]
78struct State {
79 path_index: usize,
80 glob_index: usize,
81 brace_depth: usize,
82
83 wildcard: Wildcard,
84 globstar: Wildcard,
85}
86
87#[derive(Clone, Copy, Debug, Default)]
88struct Wildcard {
89 glob_index: u32,
90 path_index: u32,
91 brace_depth: u32,
92}
93
94type BraceStack = ArrayVec<(u32, u32), MAX_BRACE_GROUPS>;
95
96/// An error describing why a glob pattern is invalid, returned by [`validate`].
97#[derive(Debug, Clone, Copy, PartialEq, Eq)]
98pub struct Error {
99 /// The kind of invalid construct that was found.
100 pub kind: ErrorKind,
101 /// Byte offset in the pattern of the offending character.
102 pub index: usize,
103}
104
105/// The kind of invalid construct described by an [`Error`].
106#[derive(Debug, Clone, Copy, PartialEq, Eq)]
107#[non_exhaustive]
108pub enum ErrorKind {
109 /// A `{` is never closed by a matching `}`.
110 UnclosedBrace,
111 /// A `[` is never closed by a matching `]`.
112 UnclosedBracket,
113 /// A `\` at the end of the pattern has no character to escape.
114 TrailingBackslash,
115 /// Brace expansions nest deeper than the supported 10 levels.
116 BraceNestingTooDeep,
117 /// A pattern contains more than the supported 10 brace groups.
118 TooManyBraceGroups,
119}
120
121impl fmt::Display for Error {
122 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
123 let index = self.index;
124 match self.kind {
125 ErrorKind::UnclosedBrace => write!(
126 f,
127 "unclosed brace expansion at byte {index}; missing '}}' (to match a literal '{{', escape it as '\\{{' or '[{{]')"
128 ),
129 ErrorKind::UnclosedBracket => write!(
130 f,
131 "unclosed character class at byte {index}; missing ']' (to match a literal '[', escape it as '\\[' or '[[]')"
132 ),
133 ErrorKind::TrailingBackslash => write!(
134 f,
135 "trailing backslash at byte {index} has no character to escape (to match a literal '\\', use '\\\\')"
136 ),
137 ErrorKind::BraceNestingTooDeep => write!(
138 f,
139 "brace expansion at byte {index} nests deeper than the supported {MAX_BRACE_NESTING} levels"
140 ),
141 ErrorKind::TooManyBraceGroups => write!(
142 f,
143 "brace expansion at byte {index} exceeds the supported limit of {MAX_BRACE_GROUPS} groups"
144 ),
145 }
146 }
147}
148
149impl std::error::Error for Error {}
150
151/// Performs glob pattern matching for `glob` against `path`.
152///
153/// `glob` is expected to be a valid pattern. An invalid pattern — an unclosed
154/// `{` or `[`, a trailing `\`, more than 10 brace groups, or brace expansions
155/// nested deeper than 10 levels — cannot be reported here and its result is
156/// unspecified: typically it matches nothing, and it never matches through
157/// `!` negation, but the exact behavior may change between releases. Callers
158/// accepting user-written patterns should reject invalid ones up front with
159/// [`validate`].
160pub fn glob_match(glob: impl AsRef<[u8]>, path: impl AsRef<[u8]>) -> bool {
161 let (matched, invalid_pattern) = glob_match_internal(glob.as_ref(), path.as_ref());
162 matched && !invalid_pattern
163}
164
165/// Checks that `glob` is a valid pattern.
166///
167/// [`glob_match`] has no way to report an invalid pattern, and its result for
168/// one is unspecified. Call this once when a pattern is first accepted (e.g.
169/// at configuration load time) to reject invalid patterns with an actionable
170/// error instead of silently matching nothing.
171///
172/// A pattern accepted here is never treated as invalid by [`glob_match`], so
173/// its matching behavior is well-defined. For a rejected pattern the result
174/// of [`glob_match`] is unspecified — typically it matches nothing.
175///
176/// # Examples
177///
178/// ```rust
179/// use fast_glob::{validate, Error, ErrorKind};
180///
181/// assert!(validate("some/**/n*d[k-m]e?txt").is_ok());
182/// assert_eq!(
183/// validate("src/**/*.{js,ts"),
184/// Err(Error { kind: ErrorKind::UnclosedBrace, index: 9 })
185/// );
186/// ```
187pub fn validate(glob: impl AsRef<[u8]>) -> Result<(), Error> {
188 let glob = glob.as_ref();
189 let mut index = 0;
190
191 // Leading `!` characters negate the glob and are not part of the pattern.
192 while index < glob.len() && glob[index] == b'!' {
193 index += 1;
194 }
195
196 let mut open_braces = ArrayVec::<usize, MAX_BRACE_NESTING>::new();
197 let mut brace_groups = 0;
198
199 while index < glob.len() {
200 match glob[index] {
201 b'\\' => {
202 if index + 1 >= glob.len() {
203 return Err(Error { kind: ErrorKind::TrailingBackslash, index });
204 }
205 index += 2;
206 }
207 b'[' => match skip_class(glob, index) {
208 Some(next) => index = next,
209 None => return Err(Error { kind: ErrorKind::UnclosedBracket, index }),
210 },
211 b'{' => {
212 if open_braces.try_push(index).is_err() {
213 return Err(Error { kind: ErrorKind::BraceNestingTooDeep, index });
214 }
215 brace_groups += 1;
216 if brace_groups > MAX_BRACE_GROUPS {
217 return Err(Error { kind: ErrorKind::TooManyBraceGroups, index });
218 }
219 index += 1;
220 }
221 // A `}` without a matching `{` is an ordinary character.
222 b'}' => {
223 open_braces.pop();
224 index += 1;
225 }
226 _ => index += 1,
227 }
228 }
229
230 if let Some(&index) = open_braces.first() {
231 return Err(Error { kind: ErrorKind::UnclosedBrace, index });
232 }
233
234 Ok(())
235}
236
237/// Returns the match result (with negation applied) alongside whether the
238/// pattern was detected as invalid, so tests can check the latter against
239/// [`validate`].
240fn glob_match_internal(glob: &[u8], path: &[u8]) -> (bool, bool) {
241 let mut state = State::default();
242
243 let mut negated = false;
244 while state.glob_index < glob.len() && glob[state.glob_index] == b'!' {
245 negated = !negated;
246 state.glob_index += 1;
247 }
248
249 let mut brace_stack = BraceStack::new();
250 let mut invalid_pattern = false;
251 let match_start = state.glob_index;
252 let matched =
253 state.glob_match_from(glob, path, match_start, &mut brace_stack, &mut invalid_pattern);
254
255 // A negated glob matches every path its pattern does not — for an invalid
256 // pattern that would be every path, even when the matcher never reaches
257 // the invalid construct (e.g. after an early literal mismatch). Gate the
258 // negation flip on validity instead of relying on lazy detection.
259 if negated && !matched && !invalid_pattern && validate(glob).is_err() {
260 return (false, true);
261 }
262
263 (negated ^ matched, invalid_pattern)
264}
265
266/// Returns the index just past the `]` closing the character class opened by
267/// the `[` at `index`, or `None` if the class is unclosed. Mirrors the class
268/// parsing in `glob_match_from`: an optional `^`/`!` prefix, then the first
269/// character is a literal member (so a leading `]` does not close the class),
270/// and `\` escapes the next character.
271fn skip_class(glob: &[u8], index: usize) -> Option<usize> {
272 let mut index = index + 1;
273 if matches!(glob.get(index), Some(b'^' | b'!')) {
274 index += 1;
275 }
276
277 let mut first = true;
278 loop {
279 match glob.get(index)? {
280 b']' if !first => return Some(index + 1),
281 b'\\' => index += 1,
282 _ => {}
283 }
284 first = false;
285 index += 1;
286 }
287}
288
289#[inline(always)]
290fn unescape(c: &mut u8, glob: &[u8], state: &mut State, invalid_pattern: &mut bool) -> bool {
291 if *c == b'\\' {
292 state.glob_index += 1;
293 if state.glob_index >= glob.len() {
294 // A trailing backslash has nothing to escape.
295 *invalid_pattern = true;
296 return false;
297 }
298 *c = match glob[state.glob_index] {
299 b'a' => b'\x61',
300 b'b' => b'\x08',
301 b'n' => b'\n',
302 b'r' => b'\r',
303 b't' => b'\t',
304 c => c,
305 }
306 }
307 true
308}
309
310impl State {
311 #[inline(always)]
312 fn backtrack(&mut self) {
313 self.glob_index = self.wildcard.glob_index as usize;
314 self.path_index = self.wildcard.path_index as usize;
315 self.brace_depth = self.wildcard.brace_depth as usize;
316 }
317
318 #[inline(always)]
319 fn skip_globstars(&mut self, glob: &[u8]) {
320 let mut glob_index = self.glob_index + 2;
321
322 while glob_index + 4 <= glob.len() && &glob[glob_index..glob_index + 4] == b"/**/" {
323 glob_index += 3;
324 }
325
326 if &glob[glob_index..] == b"/**" {
327 glob_index += 3;
328 }
329
330 self.glob_index = glob_index - 2;
331 }
332
333 #[inline(always)]
334 fn skip_to_separator(&mut self, path: &[u8], is_end_invalid: bool) {
335 if self.path_index == path.len() {
336 self.wildcard.path_index += 1;
337 return;
338 }
339
340 let mut path_index = self.path_index;
341 while path_index < path.len() && !is_separator(path[path_index] as char) {
342 path_index += 1;
343 }
344
345 if is_end_invalid || path_index != path.len() {
346 path_index += 1;
347 }
348
349 self.wildcard.path_index = path_index as u32;
350 self.globstar = self.wildcard;
351 }
352
353 #[inline(always)]
354 fn skip_branch(&mut self, glob: &[u8]) {
355 let end_brace_depth = self.brace_depth - 1;
356 while self.glob_index < glob.len() {
357 match glob[self.glob_index] {
358 b'{' => self.brace_depth += 1,
359 b'}' => {
360 self.brace_depth -= 1;
361 if self.brace_depth == end_brace_depth {
362 self.glob_index += 1;
363 return;
364 }
365 }
366 b'[' => {
367 // An unclosed class swallows the rest of the glob.
368 self.glob_index = skip_class(glob, self.glob_index).unwrap_or(glob.len());
369 continue;
370 }
371 b'\\' => self.glob_index += 1,
372 _ => (),
373 }
374 self.glob_index += 1;
375 }
376 }
377
378 #[inline(always)]
379 fn skip_branch_ends(&mut self, glob: &[u8]) {
380 while self.brace_depth > 0 && matches!(glob.get(self.glob_index), Some(b',' | b'}')) {
381 self.skip_branch(glob);
382 }
383 }
384
385 fn match_brace_branch(
386 &self,
387 glob: &[u8],
388 path: &[u8],
389 open_brace_index: usize,
390 branch_index: usize,
391 brace_stack: &mut BraceStack,
392 invalid_pattern: &mut bool,
393 ) -> bool {
394 // Gracefully reject patterns with more groups than BraceStack capacity.
395 if brace_stack.try_push((open_brace_index as u32, branch_index as u32)).is_err() {
396 *invalid_pattern = true;
397 return false;
398 }
399
400 let mut branch_state = self.clone();
401 branch_state.glob_index = branch_index;
402 // The stack also contains choices for earlier sequential groups, so
403 // derive syntactic nesting from the parent state instead of its length.
404 branch_state.brace_depth = self.brace_depth + 1;
405
406 let matched =
407 branch_state.glob_match_from(glob, path, branch_index, brace_stack, invalid_pattern);
408
409 brace_stack.pop();
410
411 matched
412 }
413
414 fn match_brace(
415 &mut self,
416 glob: &[u8],
417 path: &[u8],
418 brace_stack: &mut BraceStack,
419 invalid_pattern: &mut bool,
420 ) -> bool {
421 let mut brace_depth = 0;
422 let mut has_closing_brace = false;
423 let mut matched = false;
424
425 let open_brace_index = self.glob_index;
426
427 let mut branch_index = 0;
428
429 while self.glob_index < glob.len() {
430 match glob[self.glob_index] {
431 b'{' => {
432 brace_depth += 1;
433 if brace_depth == 1 {
434 branch_index = self.glob_index + 1;
435 }
436 }
437 b'}' => {
438 brace_depth -= 1;
439 if brace_depth == 0 {
440 has_closing_brace = true;
441 if self.match_brace_branch(
442 glob,
443 path,
444 open_brace_index,
445 branch_index,
446 brace_stack,
447 invalid_pattern,
448 ) {
449 matched = true;
450 }
451 break;
452 }
453 }
454 b',' if brace_depth == 1 => {
455 if self.match_brace_branch(
456 glob,
457 path,
458 open_brace_index,
459 branch_index,
460 brace_stack,
461 invalid_pattern,
462 ) {
463 matched = true;
464 }
465 branch_index = self.glob_index + 1;
466 }
467 b'[' => {
468 // An unclosed class swallows the rest of the glob,
469 // leaving the brace unclosed as well.
470 self.glob_index = skip_class(glob, self.glob_index).unwrap_or(glob.len());
471 continue;
472 }
473 b'\\' => self.glob_index += 1,
474 _ => (),
475 }
476 self.glob_index += 1;
477 }
478
479 if !has_closing_brace {
480 *invalid_pattern = true;
481 return false;
482 }
483
484 matched
485 }
486
487 #[inline(always)]
488 fn glob_match_from(
489 &mut self,
490 glob: &[u8],
491 path: &[u8],
492 match_start: usize,
493 brace_stack: &mut BraceStack,
494 invalid_pattern: &mut bool,
495 ) -> bool {
496 while self.glob_index < glob.len() || self.path_index < path.len() {
497 if self.glob_index < glob.len() {
498 match glob[self.glob_index] {
499 b'*' => {
500 let is_globstar =
501 self.glob_index + 1 < glob.len() && glob[self.glob_index + 1] == b'*';
502 if is_globstar {
503 self.skip_globstars(glob);
504 }
505
506 self.wildcard.glob_index = self.glob_index as u32;
507 self.wildcard.path_index = self.path_index as u32 + 1;
508 self.wildcard.brace_depth = self.brace_depth as u32;
509
510 let mut in_globstar = false;
511 if is_globstar {
512 self.glob_index += 2;
513
514 // A selected brace branch ends at `,` or `}`, but
515 // brace expansion makes the suffix after the group
516 // logically adjacent to this branch. Resolve those
517 // boundaries before deciding whether `**` occupies
518 // a complete path segment.
519 let mut after_globstar = self.clone();
520 after_globstar.skip_branch_ends(glob);
521 let is_end_invalid = after_globstar.glob_index < glob.len();
522
523 if (self.glob_index.saturating_sub(match_start) < 3
524 || glob[self.glob_index - 3] == b'/')
525 && (after_globstar.glob_index >= glob.len()
526 || glob[after_globstar.glob_index] == b'/')
527 {
528 self.glob_index = after_globstar.glob_index;
529 self.brace_depth = after_globstar.brace_depth;
530 if is_end_invalid {
531 self.glob_index += 1;
532 }
533
534 self.skip_to_separator(path, is_end_invalid);
535 in_globstar = true;
536 }
537 } else {
538 self.glob_index += 1;
539 }
540
541 if !in_globstar
542 && self.path_index < path.len()
543 && is_separator(path[self.path_index] as char)
544 {
545 self.wildcard = self.globstar;
546 }
547
548 continue;
549 }
550 b'?' if self.path_index < path.len()
551 && !is_separator(path[self.path_index] as char) =>
552 {
553 self.glob_index += 1;
554 self.path_index += 1;
555 continue;
556 }
557 b'[' if self.path_index < path.len() => {
558 self.glob_index += 1;
559
560 let mut negated = false;
561 if self.glob_index < glob.len()
562 && matches!(glob[self.glob_index], b'^' | b'!')
563 {
564 negated = true;
565 self.glob_index += 1;
566 }
567
568 let mut first = true;
569 let mut is_match = false;
570 let c = path[self.path_index];
571 while self.glob_index < glob.len()
572 && (first || glob[self.glob_index] != b']')
573 {
574 let mut low = glob[self.glob_index];
575 if !unescape(&mut low, glob, self, invalid_pattern) {
576 return false;
577 }
578
579 self.glob_index += 1;
580
581 let high = if self.glob_index + 1 < glob.len()
582 && glob[self.glob_index] == b'-'
583 && glob[self.glob_index + 1] != b']'
584 {
585 self.glob_index += 1;
586
587 let mut high = glob[self.glob_index];
588 if !unescape(&mut high, glob, self, invalid_pattern) {
589 return false;
590 }
591
592 self.glob_index += 1;
593 high
594 } else {
595 low
596 };
597
598 if low <= c && c <= high {
599 is_match = true;
600 }
601
602 first = false;
603 }
604
605 if self.glob_index >= glob.len() {
606 *invalid_pattern = true;
607 return false;
608 }
609
610 self.glob_index += 1;
611 if is_match != negated && !is_separator(c as char) {
612 self.path_index += 1;
613 continue;
614 }
615 }
616 b'{' => {
617 if let Some((_, branch_index)) =
618 brace_stack.iter().find(|(open_brace_index, _)| {
619 *open_brace_index == self.glob_index as u32
620 })
621 {
622 self.glob_index = *branch_index as usize;
623 self.brace_depth += 1;
624 continue;
625 }
626 return self.match_brace(glob, path, brace_stack, invalid_pattern);
627 }
628 b',' | b'}' if self.brace_depth > 0 => {
629 self.skip_branch(glob);
630 continue;
631 }
632 mut c if self.path_index < path.len() => {
633 if !unescape(&mut c, glob, self, invalid_pattern) {
634 return false;
635 }
636
637 let is_match = if c == b'/' {
638 is_separator(path[self.path_index] as char)
639 } else {
640 path[self.path_index] == c
641 };
642
643 if is_match {
644 self.glob_index += 1;
645 self.path_index += 1;
646
647 if c == b'/' {
648 self.wildcard = self.globstar;
649 }
650
651 continue;
652 }
653 }
654 _ => {}
655 }
656 }
657
658 if self.wildcard.path_index > 0 && self.wildcard.path_index <= path.len() as u32 {
659 self.backtrack();
660 continue;
661 }
662
663 return false;
664 }
665
666 true
667 }
668}
669
670#[cfg(test)]
671mod tests {
672 use super::*;
673
674 const ALPHABET: &[u8] = b"a/*[]{}\\,!-";
675
676 fn for_each_pattern(len: usize, f: &mut impl FnMut(&[u8])) {
677 let mut pattern = vec![0u8; len];
678 for mut n in 0..ALPHABET.len().pow(len as u32) {
679 for slot in &mut pattern {
680 *slot = ALPHABET[n % ALPHABET.len()];
681 n /= ALPHABET.len();
682 }
683 f(&pattern);
684 }
685 }
686
687 /// `validate` and the matcher must agree on what is invalid:
688 /// a pattern `validate` accepts is never flagged invalid by the matcher,
689 /// and an invalid non-negated pattern never matches any path.
690 /// Checked exhaustively over all short patterns built from the special characters.
691 #[test]
692 fn validate_agrees_with_matcher() {
693 const PATHS: &[&str] = &["", "a", "aa", "a/a", "-", ",", "!"];
694
695 for len in 0..=6 {
696 for_each_pattern(len, &mut |pattern| {
697 let valid = validate(pattern).is_ok();
698 for path in PATHS {
699 let (matched, invalid) = glob_match_internal(pattern, path.as_bytes());
700 if valid {
701 assert!(
702 !invalid,
703 "matcher flagged {:?} as invalid on path {path:?} but validate accepted it",
704 String::from_utf8_lossy(pattern),
705 );
706 } else if pattern.first() == Some(&b'!') || !pattern.contains(&b'{') {
707 // A negated invalid pattern never matches (the negation flip is gated on validity),
708 // and neither does a brace-free one, since every part of its glob is processed directly.
709 // (A non-negated invalid construct in a non-taken brace branch may go unnoticed,
710 // that behavior is documented as unspecified.)
711 assert!(
712 !(matched && !invalid),
713 "invalid pattern {:?} matched path {path:?}",
714 String::from_utf8_lossy(pattern),
715 );
716 }
717 }
718 });
719 }
720 }
721}