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 match_start = state.glob_index;
239    let matched =
240        state.glob_match_from(glob, path, match_start, &mut brace_stack, &mut invalid_pattern);
241
242    // A negated glob matches every path its pattern does not — for an invalid
243    // pattern that would be every path, even when the matcher never reaches
244    // the invalid construct (e.g. after an early literal mismatch). Gate the
245    // negation flip on validity instead of relying on lazy detection.
246    if negated && !matched && !invalid_pattern && validate(glob).is_err() {
247        return (false, true);
248    }
249
250    (negated ^ matched, invalid_pattern)
251}
252
253/// Returns the index just past the `]` closing the character class opened by
254/// the `[` at `index`, or `None` if the class is unclosed. Mirrors the class
255/// parsing in `glob_match_from`: an optional `^`/`!` prefix, then the first
256/// character is a literal member (so a leading `]` does not close the class),
257/// and `\` escapes the next character.
258fn skip_class(glob: &[u8], index: usize) -> Option<usize> {
259    let mut index = index + 1;
260    if matches!(glob.get(index), Some(b'^' | b'!')) {
261        index += 1;
262    }
263
264    let mut first = true;
265    loop {
266        match glob.get(index)? {
267            b']' if !first => return Some(index + 1),
268            b'\\' => index += 1,
269            _ => {}
270        }
271        first = false;
272        index += 1;
273    }
274}
275
276#[inline(always)]
277fn unescape(c: &mut u8, glob: &[u8], state: &mut State, invalid_pattern: &mut bool) -> bool {
278    if *c == b'\\' {
279        state.glob_index += 1;
280        if state.glob_index >= glob.len() {
281            // A trailing backslash has nothing to escape.
282            *invalid_pattern = true;
283            return false;
284        }
285        *c = match glob[state.glob_index] {
286            b'a' => b'\x61',
287            b'b' => b'\x08',
288            b'n' => b'\n',
289            b'r' => b'\r',
290            b't' => b'\t',
291            c => c,
292        }
293    }
294    true
295}
296
297impl State {
298    #[inline(always)]
299    fn backtrack(&mut self) {
300        self.glob_index = self.wildcard.glob_index as usize;
301        self.path_index = self.wildcard.path_index as usize;
302        self.brace_depth = self.wildcard.brace_depth as usize;
303    }
304
305    #[inline(always)]
306    fn skip_globstars(&mut self, glob: &[u8]) {
307        let mut glob_index = self.glob_index + 2;
308
309        while glob_index + 4 <= glob.len() && &glob[glob_index..glob_index + 4] == b"/**/" {
310            glob_index += 3;
311        }
312
313        if &glob[glob_index..] == b"/**" {
314            glob_index += 3;
315        }
316
317        self.glob_index = glob_index - 2;
318    }
319
320    #[inline(always)]
321    fn skip_to_separator(&mut self, path: &[u8], is_end_invalid: bool) {
322        if self.path_index == path.len() {
323            self.wildcard.path_index += 1;
324            return;
325        }
326
327        let mut path_index = self.path_index;
328        while path_index < path.len() && !is_separator(path[path_index] as char) {
329            path_index += 1;
330        }
331
332        if is_end_invalid || path_index != path.len() {
333            path_index += 1;
334        }
335
336        self.wildcard.path_index = path_index as u32;
337        self.globstar = self.wildcard;
338    }
339
340    #[inline(always)]
341    fn skip_branch(&mut self, glob: &[u8]) {
342        let end_brace_depth = self.brace_depth - 1;
343        while self.glob_index < glob.len() {
344            match glob[self.glob_index] {
345                b'{' => self.brace_depth += 1,
346                b'}' => {
347                    self.brace_depth -= 1;
348                    if self.brace_depth == end_brace_depth {
349                        self.glob_index += 1;
350                        return;
351                    }
352                }
353                b'[' => {
354                    // An unclosed class swallows the rest of the glob.
355                    self.glob_index = skip_class(glob, self.glob_index).unwrap_or(glob.len());
356                    continue;
357                }
358                b'\\' => self.glob_index += 1,
359                _ => (),
360            }
361            self.glob_index += 1;
362        }
363    }
364
365    fn match_brace_branch(
366        &self,
367        glob: &[u8],
368        path: &[u8],
369        open_brace_index: usize,
370        branch_index: usize,
371        brace_stack: &mut BraceStack,
372        invalid_pattern: &mut bool,
373    ) -> bool {
374        // Gracefully reject brace expansions deeper than BraceStack capacity.
375        if brace_stack.try_push((open_brace_index as u32, branch_index as u32)).is_err() {
376            *invalid_pattern = true;
377            return false;
378        }
379
380        let mut branch_state = self.clone();
381        branch_state.glob_index = branch_index;
382        branch_state.brace_depth = brace_stack.len();
383
384        let matched =
385            branch_state.glob_match_from(glob, path, branch_index, brace_stack, invalid_pattern);
386
387        brace_stack.pop();
388
389        matched
390    }
391
392    fn match_brace(
393        &mut self,
394        glob: &[u8],
395        path: &[u8],
396        brace_stack: &mut BraceStack,
397        invalid_pattern: &mut bool,
398    ) -> bool {
399        let mut brace_depth = 0;
400        let mut has_closing_brace = false;
401        let mut matched = false;
402
403        let open_brace_index = self.glob_index;
404
405        let mut branch_index = 0;
406
407        while self.glob_index < glob.len() {
408            match glob[self.glob_index] {
409                b'{' => {
410                    brace_depth += 1;
411                    if brace_depth == 1 {
412                        branch_index = self.glob_index + 1;
413                    }
414                }
415                b'}' => {
416                    brace_depth -= 1;
417                    if brace_depth == 0 {
418                        has_closing_brace = true;
419                        if self.match_brace_branch(
420                            glob,
421                            path,
422                            open_brace_index,
423                            branch_index,
424                            brace_stack,
425                            invalid_pattern,
426                        ) {
427                            matched = true;
428                        }
429                        break;
430                    }
431                }
432                b',' if brace_depth == 1 => {
433                    if self.match_brace_branch(
434                        glob,
435                        path,
436                        open_brace_index,
437                        branch_index,
438                        brace_stack,
439                        invalid_pattern,
440                    ) {
441                        matched = true;
442                    }
443                    branch_index = self.glob_index + 1;
444                }
445                b'[' => {
446                    // An unclosed class swallows the rest of the glob,
447                    // leaving the brace unclosed as well.
448                    self.glob_index = skip_class(glob, self.glob_index).unwrap_or(glob.len());
449                    continue;
450                }
451                b'\\' => self.glob_index += 1,
452                _ => (),
453            }
454            self.glob_index += 1;
455        }
456
457        if !has_closing_brace {
458            *invalid_pattern = true;
459            return false;
460        }
461
462        matched
463    }
464
465    #[inline(always)]
466    fn glob_match_from(
467        &mut self,
468        glob: &[u8],
469        path: &[u8],
470        match_start: usize,
471        brace_stack: &mut BraceStack,
472        invalid_pattern: &mut bool,
473    ) -> bool {
474        while self.glob_index < glob.len() || self.path_index < path.len() {
475            if self.glob_index < glob.len() {
476                match glob[self.glob_index] {
477                    b'*' => {
478                        let is_globstar =
479                            self.glob_index + 1 < glob.len() && glob[self.glob_index + 1] == b'*';
480                        if is_globstar {
481                            self.skip_globstars(glob);
482                        }
483
484                        self.wildcard.glob_index = self.glob_index as u32;
485                        self.wildcard.path_index = self.path_index as u32 + 1;
486                        self.wildcard.brace_depth = self.brace_depth as u32;
487
488                        let mut in_globstar = false;
489                        if is_globstar {
490                            self.glob_index += 2;
491
492                            let is_end_invalid = self.glob_index != glob.len();
493
494                            if (self.glob_index.saturating_sub(match_start) < 3
495                                || glob[self.glob_index - 3] == b'/')
496                                && (!is_end_invalid || glob[self.glob_index] == b'/')
497                            {
498                                if is_end_invalid {
499                                    self.glob_index += 1;
500                                }
501
502                                self.skip_to_separator(path, is_end_invalid);
503                                in_globstar = true;
504                            }
505                        } else {
506                            self.glob_index += 1;
507                        }
508
509                        if !in_globstar
510                            && self.path_index < path.len()
511                            && is_separator(path[self.path_index] as char)
512                        {
513                            self.wildcard = self.globstar;
514                        }
515
516                        continue;
517                    }
518                    b'?' if self.path_index < path.len()
519                        && !is_separator(path[self.path_index] as char) =>
520                    {
521                        self.glob_index += 1;
522                        self.path_index += 1;
523                        continue;
524                    }
525                    b'[' if self.path_index < path.len() => {
526                        self.glob_index += 1;
527
528                        let mut negated = false;
529                        if self.glob_index < glob.len()
530                            && matches!(glob[self.glob_index], b'^' | b'!')
531                        {
532                            negated = true;
533                            self.glob_index += 1;
534                        }
535
536                        let mut first = true;
537                        let mut is_match = false;
538                        let c = path[self.path_index];
539                        while self.glob_index < glob.len()
540                            && (first || glob[self.glob_index] != b']')
541                        {
542                            let mut low = glob[self.glob_index];
543                            if !unescape(&mut low, glob, self, invalid_pattern) {
544                                return false;
545                            }
546
547                            self.glob_index += 1;
548
549                            let high = if self.glob_index + 1 < glob.len()
550                                && glob[self.glob_index] == b'-'
551                                && glob[self.glob_index + 1] != b']'
552                            {
553                                self.glob_index += 1;
554
555                                let mut high = glob[self.glob_index];
556                                if !unescape(&mut high, glob, self, invalid_pattern) {
557                                    return false;
558                                }
559
560                                self.glob_index += 1;
561                                high
562                            } else {
563                                low
564                            };
565
566                            if low <= c && c <= high {
567                                is_match = true;
568                            }
569
570                            first = false;
571                        }
572
573                        if self.glob_index >= glob.len() {
574                            *invalid_pattern = true;
575                            return false;
576                        }
577
578                        self.glob_index += 1;
579                        if is_match != negated {
580                            self.path_index += 1;
581                            continue;
582                        }
583                    }
584                    b'{' => {
585                        if let Some((_, branch_index)) =
586                            brace_stack.iter().find(|(open_brace_index, _)| {
587                                *open_brace_index == self.glob_index as u32
588                            })
589                        {
590                            self.glob_index = *branch_index as usize;
591                            self.brace_depth += 1;
592                            continue;
593                        }
594                        return self.match_brace(glob, path, brace_stack, invalid_pattern);
595                    }
596                    b',' | b'}' if self.brace_depth > 0 => {
597                        self.skip_branch(glob);
598                        continue;
599                    }
600                    mut c if self.path_index < path.len() => {
601                        if !unescape(&mut c, glob, self, invalid_pattern) {
602                            return false;
603                        }
604
605                        let is_match = if c == b'/' {
606                            is_separator(path[self.path_index] as char)
607                        } else {
608                            path[self.path_index] == c
609                        };
610
611                        if is_match {
612                            self.glob_index += 1;
613                            self.path_index += 1;
614
615                            if c == b'/' {
616                                self.wildcard = self.globstar;
617                            }
618
619                            continue;
620                        }
621                    }
622                    _ => {}
623                }
624            }
625
626            if self.wildcard.path_index > 0 && self.wildcard.path_index <= path.len() as u32 {
627                self.backtrack();
628                continue;
629            }
630
631            return false;
632        }
633
634        true
635    }
636}
637
638#[cfg(test)]
639mod tests {
640    use super::*;
641
642    const ALPHABET: &[u8] = b"a/*[]{}\\,!-";
643
644    fn for_each_pattern(len: usize, f: &mut impl FnMut(&[u8])) {
645        let mut pattern = vec![0u8; len];
646        for mut n in 0..ALPHABET.len().pow(len as u32) {
647            for slot in &mut pattern {
648                *slot = ALPHABET[n % ALPHABET.len()];
649                n /= ALPHABET.len();
650            }
651            f(&pattern);
652        }
653    }
654
655    /// `validate` and the matcher must agree on what is invalid:
656    /// a pattern `validate` accepts is never flagged invalid by the matcher,
657    /// and an invalid non-negated pattern never matches any path.
658    /// Checked exhaustively over all short patterns built from the special characters.
659    #[test]
660    fn validate_agrees_with_matcher() {
661        const PATHS: &[&str] = &["", "a", "aa", "a/a", "-", ",", "!"];
662
663        for len in 0..=6 {
664            for_each_pattern(len, &mut |pattern| {
665                let valid = validate(pattern).is_ok();
666                for path in PATHS {
667                    let (matched, invalid) = glob_match_internal(pattern, path.as_bytes());
668                    if valid {
669                        assert!(
670                            !invalid,
671                            "matcher flagged {:?} as invalid on path {path:?} but validate accepted it",
672                            String::from_utf8_lossy(pattern),
673                        );
674                    } else if pattern.first() == Some(&b'!') || !pattern.contains(&b'{') {
675                        // A negated invalid pattern never matches (the negation flip is gated on validity),
676                        // and neither does a brace-free one, since every part of its glob is processed directly.
677                        // (A non-negated invalid construct in a non-taken brace branch may go unnoticed,
678                        // that behavior is documented as unspecified.)
679                        assert!(
680                            !(matched && !invalid),
681                            "invalid pattern {:?} matched path {path:?}",
682                            String::from_utf8_lossy(pattern),
683                        );
684                    }
685                }
686            });
687        }
688    }
689}