Skip to main content

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}