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}