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