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}