Skip to main content

miden_debug_engine/
glob.rs

1//! This is a no-std-compatible implementation of shell glob matching against paths.
2//!
3//! This is a simplified version of the [globset](https://crates.io/crates/globset) crate that is
4//! defined as part of ripgrep, designed for single globs, with only the bare minimum features we
5//! need.
6use alloc::{
7    borrow::Cow,
8    string::{String, ToString},
9    vec::Vec,
10};
11use core::fmt::Write;
12#[cfg(feature = "std")]
13use std::path::is_separator;
14
15use miden_debug_types::Uri;
16
17#[cfg(not(feature = "std"))]
18fn is_separator(c: char) -> bool {
19    matches!(c, '/')
20}
21
22/// Represents an error that can occur when parsing a glob pattern.
23#[derive(Clone, Debug, Eq, PartialEq, thiserror::Error)]
24#[error("{}", self.format_error())]
25pub struct Error {
26    /// The original glob provided by the caller.
27    glob: Option<String>,
28    /// The kind of error.
29    kind: ErrorKind,
30}
31
32impl Error {
33    /// Return the glob that caused this error, if one exists.
34    pub fn glob(&self) -> Option<&str> {
35        self.glob.as_deref()
36    }
37
38    /// Return the kind of this error.
39    pub fn kind(&self) -> &ErrorKind {
40        &self.kind
41    }
42
43    fn format_error(&self) -> String {
44        if let Some(glob) = self.glob() {
45            format!("error parsing glob '{glob}': {}", self.kind)
46        } else {
47            format!("{}", self.kind)
48        }
49    }
50}
51
52/// The kind of error that can occur when parsing a glob pattern.
53#[derive(Clone, Debug, Eq, PartialEq, thiserror::Error)]
54#[non_exhaustive]
55pub enum ErrorKind {
56    /// Occurs when a character class (e.g., `[abc]`) is not closed.
57    #[error("unclosed character class; missing ']'")]
58    UnclosedClass,
59    /// Occurs when a range in a character (e.g., `[a-z]`) is invalid. For
60    /// example, if the range starts with a lexicographically larger character
61    /// than it ends with.
62    #[error("unclosed character range")]
63    InvalidRange(char, char),
64    /// Occurs when a `}` is found without a matching `{`.
65    #[error("unopened alternate group; missing '{{' (maybe escape '}}' with '[}}]'?)")]
66    UnopenedAlternates,
67    /// Occurs when a `{` is found without a matching `}`.
68    #[error("unclosed alternate group; missing '}}' (maybe escape '{{' with '[{{]'?)")]
69    UnclosedAlternates,
70    /// Occurs when an unescaped '\' is found at the end of a glob.
71    #[error("dangling '\\'")]
72    DanglingEscape,
73    /// An error associated with parsing or compiling a regex.
74    #[error("{0}")]
75    Regex(String),
76}
77
78/// Glob represents a successfully parsed shell glob pattern.
79///
80/// It cannot be used directly to match file paths, but it can be converted
81/// to a regular expression string or a matcher.
82#[derive(Clone, Eq)]
83pub struct Glob {
84    glob: String,
85    re: String,
86    opts: GlobOptions,
87    tokens: Tokens,
88}
89
90impl AsRef<Glob> for Glob {
91    fn as_ref(&self) -> &Glob {
92        self
93    }
94}
95
96impl PartialEq for Glob {
97    fn eq(&self, other: &Glob) -> bool {
98        self.glob == other.glob && self.opts == other.opts
99    }
100}
101
102#[cfg(feature = "std")]
103impl std::hash::Hash for Glob {
104    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
105        self.glob.hash(state);
106        self.opts.hash(state);
107    }
108}
109
110impl core::fmt::Debug for Glob {
111    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
112        if f.alternate() {
113            f.debug_struct("Glob")
114                .field("glob", &self.glob)
115                .field("re", &self.re)
116                .field("opts", &self.opts)
117                .field("tokens", &self.tokens)
118                .finish()
119        } else {
120            f.debug_tuple("Glob").field(&self.glob).finish()
121        }
122    }
123}
124
125impl core::fmt::Display for Glob {
126    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
127        self.glob.fmt(f)
128    }
129}
130
131impl core::str::FromStr for Glob {
132    type Err = Error;
133
134    fn from_str(glob: &str) -> Result<Self, Self::Err> {
135        Self::new(glob)
136    }
137}
138
139/// A matcher for a single pattern.
140#[derive(Clone, Debug)]
141pub struct GlobMatcher {
142    /// The underlying pattern.
143    pat: Glob,
144    /// The pattern, as a compiled regex.
145    re: regex::bytes::Regex,
146}
147
148impl Eq for GlobMatcher {}
149impl PartialEq for GlobMatcher {
150    fn eq(&self, other: &Self) -> bool {
151        self.pat == other.pat
152    }
153}
154
155impl GlobMatcher {
156    /// Tests whether the given path matches this pattern or not.
157    pub fn is_match(&self, path: &Uri) -> bool {
158        self.is_match_candidate(&Candidate::new(path))
159    }
160
161    /// Tests whether the given path matches this pattern or not.
162    pub fn is_match_candidate(&self, path: &Candidate<'_>) -> bool {
163        self.re.is_match(path.path.as_bytes())
164    }
165
166    /// Returns the `Glob` used to compile this matcher.
167    pub fn glob(&self) -> &Glob {
168        &self.pat
169    }
170}
171
172/// A candidate path for matching.
173///
174/// All glob matching in this crate operates on `Candidate` values.
175/// Constructing candidates has a very small cost associated with it, so
176/// callers may find it beneficial to amortize that cost when matching a single
177/// path against multiple globs or sets of globs.
178#[derive(Debug, Clone)]
179pub struct Candidate<'a> {
180    path: Cow<'a, str>,
181}
182
183impl<'a> Candidate<'a> {
184    /// Create a new candidate for matching from the given path.
185    pub fn new(uri: &'a Uri) -> Candidate<'a> {
186        let path = normalize_path(uri);
187        Candidate { path }
188    }
189}
190
191/// Normalizes a path to use `/` as a separator everywhere, even on platforms
192/// that recognize other characters as separators.
193#[cfg(unix)]
194pub(crate) fn normalize_path(uri: &Uri) -> Cow<'_, str> {
195    // UNIX only uses /, so we're good.
196    Cow::Borrowed(match uri.scheme() {
197        Some("file") => uri.as_str().strip_prefix("file://").unwrap(),
198        Some("stdin") => match uri.as_str().strip_prefix("stdin://").unwrap() {
199            "" => "stdin",
200            other => other,
201        },
202        // Try and match this other scheme anyway, which likely looks like a UNIX-style path
203        Some(_) => uri.as_str().split_once("://").unwrap().1,
204        None => uri.as_str(),
205    })
206}
207
208/// Normalizes a path to use `/` as a separator everywhere, even on platforms
209/// that recognize other characters as separators.
210#[cfg(not(unix))]
211pub(crate) fn normalize_path(uri: &Uri) -> Cow<'_, str> {
212    let path = match uri.scheme() {
213        Some("stdin") => {
214            return Cow::Borrowed(match uri.as_str().strip_prefix("stdin://").unwrap() {
215                "" => "stdin",
216                other => other,
217            });
218        }
219        Some(scheme) if scheme == "file" || scheme.chars().count() == 1 => {
220            uri.as_str().split_once("://").unwrap().1
221        }
222        Some(_) => return Cow::Borrowed(uri.as_str().split_once("://").unwrap().1),
223        None => uri.as_str(),
224    };
225    let mut output = String::with_capacity(path.len());
226    for c in path.chars() {
227        if matches!(c, '/') || !is_separator(c) {
228            output.push(c);
229            continue;
230        }
231        output.push('/');
232    }
233    Cow::Owned(output)
234}
235
236/// A builder for a pattern.
237///
238/// This builder enables configuring the match semantics of a pattern. For
239/// example, one can make matching case insensitive.
240///
241/// The lifetime `'a` refers to the lifetime of the pattern string.
242#[derive(Clone, Debug)]
243pub struct GlobBuilder<'a> {
244    /// The glob pattern to compile.
245    glob: &'a str,
246    /// Options for the pattern.
247    opts: GlobOptions,
248}
249
250#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
251struct GlobOptions {
252    /// Whether to match case insensitively.
253    case_insensitive: bool,
254    /// Whether to require a literal separator to match a separator in a file
255    /// path. e.g., when enabled, `*` won't match `/`.
256    literal_separator: bool,
257    /// Whether or not to use `\` to escape special characters.
258    /// e.g., when enabled, `\*` will match a literal `*`.
259    backslash_escape: bool,
260    /// Whether or not an empty case in an alternate will be removed.
261    /// e.g., when enabled, `{,a}` will match "" and "a".
262    empty_alternates: bool,
263    /// Whether or not an unclosed character class is allowed. When an unclosed
264    /// character class is found, the opening `[` is treated as a literal `[`.
265    /// When this isn't enabled, an opening `[` without a corresponding `]` is
266    /// treated as an error.
267    allow_unclosed_class: bool,
268}
269
270impl GlobOptions {
271    fn default() -> GlobOptions {
272        GlobOptions {
273            case_insensitive: false,
274            literal_separator: false,
275            backslash_escape: !is_separator('\\'),
276            empty_alternates: false,
277            allow_unclosed_class: false,
278        }
279    }
280}
281
282#[derive(Clone, Debug, Default, Eq, PartialEq)]
283struct Tokens(Vec<Token>);
284
285impl core::ops::Deref for Tokens {
286    type Target = Vec<Token>;
287
288    fn deref(&self) -> &Vec<Token> {
289        &self.0
290    }
291}
292
293impl core::ops::DerefMut for Tokens {
294    fn deref_mut(&mut self) -> &mut Vec<Token> {
295        &mut self.0
296    }
297}
298
299#[derive(Clone, Debug, Eq, PartialEq)]
300enum Token {
301    Literal(char),
302    Any,
303    ZeroOrMore,
304    RecursivePrefix,
305    RecursiveSuffix,
306    RecursiveZeroOrMore,
307    Class {
308        negated: bool,
309        ranges: Vec<(char, char)>,
310    },
311    Alternates(Vec<Tokens>),
312}
313
314impl Glob {
315    /// Builds a new pattern with default options.
316    pub fn new(glob: &str) -> Result<Glob, Error> {
317        GlobBuilder::new(glob).build()
318    }
319
320    /// Returns a matcher for this pattern.
321    pub fn compile_matcher(&self) -> GlobMatcher {
322        let mut re = regex::bytes::RegexBuilder::new(&self.re);
323        re.unicode(false).dot_matches_new_line(true);
324
325        let re = re.build().expect("regex compilation shouldn't fail");
326        GlobMatcher {
327            pat: self.clone(),
328            re,
329        }
330    }
331
332    /// Returns the original glob pattern used to build this pattern.
333    pub fn glob(&self) -> &str {
334        &self.glob
335    }
336
337    /// Returns the regular expression string for this glob.
338    ///
339    /// Note that regular expressions for globs are intended to be matched on
340    /// arbitrary bytes (`&[u8]`) instead of Unicode strings (`&str`). In
341    /// particular, globs are frequently used on file paths, where there is no
342    /// general guarantee that file paths are themselves valid UTF-8. As a
343    /// result, callers will need to ensure that they are using a regex API
344    /// that can match on arbitrary bytes. For example, the
345    /// [`regex`](https://crates.io/regex)
346    /// crate's
347    /// [`Regex`](https://docs.rs/regex/*/regex/struct.Regex.html)
348    /// API is not suitable for this since it matches on `&str`, but its
349    /// [`bytes::Regex`](https://docs.rs/regex/*/regex/bytes/struct.Regex.html)
350    /// API is suitable for this.
351    pub fn regex(&self) -> &str {
352        &self.re
353    }
354}
355
356impl<'a> GlobBuilder<'a> {
357    /// Create a new builder for the pattern given.
358    ///
359    /// The pattern is not compiled until `build` is called.
360    pub fn new(glob: &'a str) -> GlobBuilder<'a> {
361        GlobBuilder {
362            glob,
363            opts: GlobOptions::default(),
364        }
365    }
366
367    /// Parses and builds the pattern.
368    pub fn build(&self) -> Result<Glob, Error> {
369        let mut p = Parser {
370            glob: self.glob,
371            alternates_stack: Vec::new(),
372            branches: vec![Tokens::default()],
373            chars: self.glob.chars().peekable(),
374            prev: None,
375            cur: None,
376            found_unclosed_class: false,
377            opts: &self.opts,
378        };
379        p.parse()?;
380        if p.branches.is_empty() {
381            // OK because of how the the branches/alternate_stack are managed.
382            // If we end up here, then there *must* be a bug in the parser
383            // somewhere.
384            unreachable!()
385        } else if p.branches.len() > 1 {
386            Err(Error {
387                glob: Some(self.glob.to_string()),
388                kind: ErrorKind::UnclosedAlternates,
389            })
390        } else {
391            let tokens = p.branches.pop().unwrap();
392            Ok(Glob {
393                glob: self.glob.to_string(),
394                re: tokens.to_regex_with(&self.opts),
395                opts: self.opts,
396                tokens,
397            })
398        }
399    }
400
401    /// Toggle whether the pattern matches case insensitively or not.
402    ///
403    /// This is disabled by default.
404    pub fn case_insensitive(&mut self, yes: bool) -> &mut GlobBuilder<'a> {
405        self.opts.case_insensitive = yes;
406        self
407    }
408
409    /// Toggle whether a literal `/` is required to match a path separator.
410    ///
411    /// By default this is false: `*` and `?` will match `/`.
412    pub fn literal_separator(&mut self, yes: bool) -> &mut GlobBuilder<'a> {
413        self.opts.literal_separator = yes;
414        self
415    }
416
417    /// When enabled, a back slash (`\`) may be used to escape
418    /// special characters in a glob pattern. Additionally, this will
419    /// prevent `\` from being interpreted as a path separator on all
420    /// platforms.
421    ///
422    /// This is enabled by default on platforms where `\` is not a
423    /// path separator and disabled by default on platforms where `\`
424    /// is a path separator.
425    pub fn backslash_escape(&mut self, yes: bool) -> &mut GlobBuilder<'a> {
426        self.opts.backslash_escape = yes;
427        self
428    }
429
430    /// Toggle whether an empty pattern in a list of alternates is accepted.
431    ///
432    /// For example, if this is set then the glob `foo{,.txt}` will match both
433    /// `foo` and `foo.txt`.
434    ///
435    /// By default this is false.
436    pub fn empty_alternates(&mut self, yes: bool) -> &mut GlobBuilder<'a> {
437        self.opts.empty_alternates = yes;
438        self
439    }
440
441    /// Toggle whether unclosed character classes are allowed. When allowed,
442    /// a `[` without a matching `]` is treated literally instead of resulting
443    /// in a parse error.
444    ///
445    /// For example, if this is set then the glob `[abc` will be treated as the
446    /// literal string `[abc` instead of returning an error.
447    ///
448    /// By default, this is false. Generally speaking, enabling this leads to
449    /// worse failure modes since the glob parser becomes more permissive. You
450    /// might want to enable this when compatibility (e.g., with POSIX glob
451    /// implementations) is more important than good error messages.
452    pub fn allow_unclosed_class(&mut self, yes: bool) -> &mut GlobBuilder<'a> {
453        self.opts.allow_unclosed_class = yes;
454        self
455    }
456}
457
458impl Tokens {
459    /// Convert this pattern to a string that is guaranteed to be a valid
460    /// regular expression and will represent the matching semantics of this
461    /// glob pattern and the options given.
462    fn to_regex_with(&self, options: &GlobOptions) -> String {
463        let mut re = String::new();
464        re.push_str("(?-u)");
465        if options.case_insensitive {
466            re.push_str("(?i)");
467        }
468        re.push('^');
469        // Special case. If the entire glob is just `**`, then it should match
470        // everything.
471        if self.len() == 1 && self[0] == Token::RecursivePrefix {
472            re.push_str(".*");
473            re.push('$');
474            return re;
475        }
476        self.tokens_to_regex(options, self, &mut re);
477        re.push('$');
478        re
479    }
480
481    fn tokens_to_regex(&self, options: &GlobOptions, tokens: &[Token], re: &mut String) {
482        for tok in tokens.iter() {
483            match *tok {
484                Token::Literal(c) => {
485                    re.push_str(&char_to_escaped_literal(c));
486                }
487                Token::Any => {
488                    if options.literal_separator {
489                        re.push_str("[^/]");
490                    } else {
491                        re.push('.');
492                    }
493                }
494                Token::ZeroOrMore => {
495                    if options.literal_separator {
496                        re.push_str("[^/]*");
497                    } else {
498                        re.push_str(".*");
499                    }
500                }
501                Token::RecursivePrefix => {
502                    re.push_str("(?:/?|.*/)");
503                }
504                Token::RecursiveSuffix => {
505                    re.push_str("/.*");
506                }
507                Token::RecursiveZeroOrMore => {
508                    re.push_str("(?:/|/.*/)");
509                }
510                Token::Class {
511                    negated,
512                    ref ranges,
513                } => {
514                    re.push('[');
515                    if negated {
516                        re.push('^');
517                    }
518                    for r in ranges {
519                        if r.0 == r.1 {
520                            // Not strictly necessary, but nicer to look at.
521                            re.push_str(&char_to_escaped_literal(r.0));
522                        } else {
523                            re.push_str(&char_to_escaped_literal(r.0));
524                            re.push('-');
525                            re.push_str(&char_to_escaped_literal(r.1));
526                        }
527                    }
528                    re.push(']');
529                }
530                Token::Alternates(ref patterns) => {
531                    let mut parts = vec![];
532                    for pat in patterns {
533                        let mut altre = String::new();
534                        self.tokens_to_regex(options, pat, &mut altre);
535                        if !altre.is_empty() || options.empty_alternates {
536                            parts.push(altre);
537                        }
538                    }
539
540                    // It is possible to have an empty set in which case the
541                    // resulting alternation '()' would be an error.
542                    if !parts.is_empty() {
543                        re.push_str("(?:");
544                        re.push_str(&parts.join("|"));
545                        re.push(')');
546                    }
547                }
548            }
549        }
550    }
551}
552
553/// Convert a Unicode scalar value to an escaped string suitable for use as
554/// a literal in a non-Unicode regex.
555fn char_to_escaped_literal(c: char) -> String {
556    let mut buf = [0; 4];
557    let bytes = c.encode_utf8(&mut buf).as_bytes();
558    bytes_to_escaped_literal(bytes)
559}
560
561/// Converts an arbitrary sequence of bytes to a UTF-8 string. All non-ASCII
562/// code units are converted to their escaped form.
563fn bytes_to_escaped_literal(bs: &[u8]) -> String {
564    let mut s = String::with_capacity(bs.len());
565    for &b in bs {
566        if b <= 0x7f {
567            regex_syntax::escape_into(char::from(b).encode_utf8(&mut [0; 4]), &mut s);
568        } else {
569            write!(&mut s, "\\x{:02x}", b).unwrap();
570        }
571    }
572    s
573}
574
575struct Parser<'a> {
576    /// The glob to parse.
577    glob: &'a str,
578    /// Marks the index in `stack` where the alternation started.
579    alternates_stack: Vec<usize>,
580    /// The set of active alternation branches being parsed.
581    /// Tokens are added to the end of the last one.
582    branches: Vec<Tokens>,
583    /// A character iterator over the glob pattern to parse.
584    chars: core::iter::Peekable<core::str::Chars<'a>>,
585    /// The previous character seen.
586    prev: Option<char>,
587    /// The current character.
588    cur: Option<char>,
589    /// Whether we failed to find a closing `]` for a character
590    /// class. This can only be true when `GlobOptions::allow_unclosed_class`
591    /// is enabled. When enabled, it is impossible to ever parse another
592    /// character class with this glob. That's because classes cannot be
593    /// nested *and* the only way this happens is when there is never a `]`.
594    ///
595    /// We track this state so that we don't end up spending quadratic time
596    /// trying to parse something like `[[[[[[[[[[[[[[[[[[[[[[[...`.
597    found_unclosed_class: bool,
598    /// Glob options, which may influence parsing.
599    opts: &'a GlobOptions,
600}
601
602impl<'a> Parser<'a> {
603    fn error(&self, kind: ErrorKind) -> Error {
604        Error {
605            glob: Some(self.glob.to_string()),
606            kind,
607        }
608    }
609
610    fn parse(&mut self) -> Result<(), Error> {
611        while let Some(c) = self.bump() {
612            match c {
613                '?' => self.push_token(Token::Any)?,
614                '*' => self.parse_star()?,
615                '[' if !self.found_unclosed_class => self.parse_class()?,
616                '{' => self.push_alternate()?,
617                '}' => self.pop_alternate()?,
618                ',' => self.parse_comma()?,
619                '\\' => self.parse_backslash()?,
620                c => self.push_token(Token::Literal(c))?,
621            }
622        }
623        Ok(())
624    }
625
626    fn push_alternate(&mut self) -> Result<(), Error> {
627        self.alternates_stack.push(self.branches.len());
628        self.branches.push(Tokens::default());
629        Ok(())
630    }
631
632    fn pop_alternate(&mut self) -> Result<(), Error> {
633        let Some(start) = self.alternates_stack.pop() else {
634            return Err(self.error(ErrorKind::UnopenedAlternates));
635        };
636        assert!(start <= self.branches.len());
637        let alts = Token::Alternates(self.branches.drain(start..).collect());
638        self.push_token(alts)?;
639        Ok(())
640    }
641
642    fn push_token(&mut self, tok: Token) -> Result<(), Error> {
643        if let Some(ref mut pat) = self.branches.last_mut() {
644            pat.push(tok);
645            return Ok(());
646        }
647        Err(self.error(ErrorKind::UnopenedAlternates))
648    }
649
650    fn pop_token(&mut self) -> Result<Token, Error> {
651        if let Some(ref mut pat) = self.branches.last_mut() {
652            return Ok(pat.pop().unwrap());
653        }
654        Err(self.error(ErrorKind::UnopenedAlternates))
655    }
656
657    fn have_tokens(&self) -> Result<bool, Error> {
658        match self.branches.last() {
659            None => Err(self.error(ErrorKind::UnopenedAlternates)),
660            Some(pat) => Ok(!pat.is_empty()),
661        }
662    }
663
664    fn parse_comma(&mut self) -> Result<(), Error> {
665        // If we aren't inside a group alternation, then don't
666        // treat commas specially. Otherwise, we need to start
667        // a new alternate branch.
668        if self.alternates_stack.is_empty() {
669            self.push_token(Token::Literal(','))
670        } else {
671            self.branches.push(Tokens::default());
672            Ok(())
673        }
674    }
675
676    fn parse_backslash(&mut self) -> Result<(), Error> {
677        if self.opts.backslash_escape {
678            match self.bump() {
679                None => Err(self.error(ErrorKind::DanglingEscape)),
680                Some(c) => self.push_token(Token::Literal(c)),
681            }
682        } else if is_separator('\\') {
683            // Normalize all patterns to use / as a separator.
684            self.push_token(Token::Literal('/'))
685        } else {
686            self.push_token(Token::Literal('\\'))
687        }
688    }
689
690    fn parse_star(&mut self) -> Result<(), Error> {
691        let prev = self.prev;
692        if self.peek() != Some('*') {
693            self.push_token(Token::ZeroOrMore)?;
694            return Ok(());
695        }
696        assert!(self.bump() == Some('*'));
697        if !self.have_tokens()? {
698            if !self.peek().is_none_or(is_separator) {
699                self.push_token(Token::ZeroOrMore)?;
700                self.push_token(Token::ZeroOrMore)?;
701            } else {
702                self.push_token(Token::RecursivePrefix)?;
703                assert!(self.bump().is_none_or(is_separator));
704            }
705            return Ok(());
706        }
707
708        if !prev.map(is_separator).unwrap_or(false)
709            && (self.branches.len() <= 1 || (prev != Some(',') && prev != Some('{')))
710        {
711            self.push_token(Token::ZeroOrMore)?;
712            self.push_token(Token::ZeroOrMore)?;
713            return Ok(());
714        }
715        let is_suffix = match self.peek() {
716            None => {
717                assert!(self.bump().is_none());
718                true
719            }
720            Some(',') | Some('}') if self.branches.len() >= 2 => true,
721            Some(c) if is_separator(c) => {
722                assert!(self.bump().map(is_separator).unwrap_or(false));
723                false
724            }
725            _ => {
726                self.push_token(Token::ZeroOrMore)?;
727                self.push_token(Token::ZeroOrMore)?;
728                return Ok(());
729            }
730        };
731        match self.pop_token()? {
732            Token::RecursivePrefix => {
733                self.push_token(Token::RecursivePrefix)?;
734            }
735            Token::RecursiveSuffix => {
736                self.push_token(Token::RecursiveSuffix)?;
737            }
738            _ => {
739                if is_suffix {
740                    self.push_token(Token::RecursiveSuffix)?;
741                } else {
742                    self.push_token(Token::RecursiveZeroOrMore)?;
743                }
744            }
745        }
746        Ok(())
747    }
748
749    fn parse_class(&mut self) -> Result<(), Error> {
750        // Save parser state for potential rollback to literal '[' parsing.
751        let saved_chars = self.chars.clone();
752        let saved_prev = self.prev;
753        let saved_cur = self.cur;
754
755        fn add_to_last_range(glob: &str, r: &mut (char, char), add: char) -> Result<(), Error> {
756            r.1 = add;
757            if r.1 < r.0 {
758                Err(Error {
759                    glob: Some(glob.to_string()),
760                    kind: ErrorKind::InvalidRange(r.0, r.1),
761                })
762            } else {
763                Ok(())
764            }
765        }
766        let mut ranges = vec![];
767        let negated = match self.chars.peek() {
768            Some(&'!') | Some(&'^') => {
769                let bump = self.bump();
770                assert!(bump == Some('!') || bump == Some('^'));
771                true
772            }
773            _ => false,
774        };
775        let mut first = true;
776        let mut in_range = false;
777        loop {
778            let Some(c) = self.bump() else {
779                return if self.opts.allow_unclosed_class {
780                    self.chars = saved_chars;
781                    self.cur = saved_cur;
782                    self.prev = saved_prev;
783                    self.found_unclosed_class = true;
784
785                    self.push_token(Token::Literal('['))
786                } else {
787                    Err(self.error(ErrorKind::UnclosedClass))
788                };
789            };
790            match c {
791                ']' => {
792                    if first {
793                        ranges.push((']', ']'));
794                    } else {
795                        break;
796                    }
797                }
798                '-' => {
799                    if first {
800                        ranges.push(('-', '-'));
801                    } else if in_range {
802                        // invariant: in_range is only set when there is
803                        // already at least one character seen.
804                        let r = ranges.last_mut().unwrap();
805                        add_to_last_range(self.glob, r, '-')?;
806                        in_range = false;
807                    } else {
808                        assert!(!ranges.is_empty());
809                        in_range = true;
810                    }
811                }
812                c => {
813                    if in_range {
814                        // invariant: in_range is only set when there is
815                        // already at least one character seen.
816                        add_to_last_range(self.glob, ranges.last_mut().unwrap(), c)?;
817                    } else {
818                        ranges.push((c, c));
819                    }
820                    in_range = false;
821                }
822            }
823            first = false;
824        }
825        if in_range {
826            // Means that the last character in the class was a '-', so add
827            // it as a literal.
828            ranges.push(('-', '-'));
829        }
830        self.push_token(Token::Class { negated, ranges })
831    }
832
833    fn bump(&mut self) -> Option<char> {
834        self.prev = self.cur;
835        self.cur = self.chars.next();
836        self.cur
837    }
838
839    fn peek(&mut self) -> Option<char> {
840        self.chars.peek().copied()
841    }
842}
843
844#[cfg(test)]
845mod tests {
846    use miden_debug_types::Uri;
847
848    use super::{ErrorKind, Glob, GlobBuilder, Token, Token::*};
849
850    #[derive(Clone, Copy, Debug, Default)]
851    struct Options {
852        casei: Option<bool>,
853        litsep: Option<bool>,
854        bsesc: Option<bool>,
855        ealtre: Option<bool>,
856        unccls: Option<bool>,
857    }
858
859    macro_rules! syntax {
860        ($name:ident, $pat:expr, $tokens:expr) => {
861            #[test]
862            fn $name() {
863                let pat = Glob::new($pat).unwrap();
864                assert_eq!($tokens, pat.tokens.0);
865            }
866        };
867    }
868
869    macro_rules! syntaxerr {
870        ($name:ident, $pat:expr, $err:expr) => {
871            #[test]
872            fn $name() {
873                let err = Glob::new($pat).unwrap_err();
874                assert_eq!(&$err, err.kind());
875            }
876        };
877    }
878
879    macro_rules! toregex {
880        ($name:ident, $pat:expr, $re:expr) => {
881            toregex!($name, $pat, $re, Options::default());
882        };
883        ($name:ident, $pat:expr, $re:expr, $options:expr) => {
884            #[test]
885            fn $name() {
886                let mut builder = GlobBuilder::new($pat);
887                if let Some(casei) = $options.casei {
888                    builder.case_insensitive(casei);
889                }
890                if let Some(litsep) = $options.litsep {
891                    builder.literal_separator(litsep);
892                }
893                if let Some(bsesc) = $options.bsesc {
894                    builder.backslash_escape(bsesc);
895                }
896                if let Some(ealtre) = $options.ealtre {
897                    builder.empty_alternates(ealtre);
898                }
899                if let Some(unccls) = $options.unccls {
900                    builder.allow_unclosed_class(unccls);
901                }
902
903                let pat = builder.build().unwrap();
904                assert_eq!(format!("(?-u){}", $re), pat.regex());
905            }
906        };
907    }
908
909    macro_rules! matches {
910        ($name:ident, $pat:expr, $path:expr) => {
911            matches!($name, $pat, $path, Options::default());
912        };
913        ($name:ident, $pat:expr, $path:expr, $options:expr) => {
914            #[test]
915            fn $name() {
916                let mut builder = GlobBuilder::new($pat);
917                if let Some(casei) = $options.casei {
918                    builder.case_insensitive(casei);
919                }
920                if let Some(litsep) = $options.litsep {
921                    builder.literal_separator(litsep);
922                }
923                if let Some(bsesc) = $options.bsesc {
924                    builder.backslash_escape(bsesc);
925                }
926                if let Some(ealtre) = $options.ealtre {
927                    builder.empty_alternates(ealtre);
928                }
929                let pat = builder.build().unwrap();
930                let matcher = pat.compile_matcher();
931                let path = Uri::from($path);
932                assert!(matcher.is_match(&path));
933            }
934        };
935    }
936
937    macro_rules! nmatches {
938        ($name:ident, $pat:expr, $path:expr) => {
939            nmatches!($name, $pat, $path, Options::default());
940        };
941        ($name:ident, $pat:expr, $path:expr, $options:expr) => {
942            #[test]
943            fn $name() {
944                let mut builder = GlobBuilder::new($pat);
945                if let Some(casei) = $options.casei {
946                    builder.case_insensitive(casei);
947                }
948                if let Some(litsep) = $options.litsep {
949                    builder.literal_separator(litsep);
950                }
951                if let Some(bsesc) = $options.bsesc {
952                    builder.backslash_escape(bsesc);
953                }
954                if let Some(ealtre) = $options.ealtre {
955                    builder.empty_alternates(ealtre);
956                }
957                let pat = builder.build().unwrap();
958                let matcher = pat.compile_matcher();
959                let path = Uri::from($path);
960                assert!(!matcher.is_match(&path));
961            }
962        };
963    }
964
965    fn class(s: char, e: char) -> Token {
966        Class {
967            negated: false,
968            ranges: vec![(s, e)],
969        }
970    }
971
972    fn classn(s: char, e: char) -> Token {
973        Class {
974            negated: true,
975            ranges: vec![(s, e)],
976        }
977    }
978
979    fn rclass(ranges: &[(char, char)]) -> Token {
980        Class {
981            negated: false,
982            ranges: ranges.to_vec(),
983        }
984    }
985
986    fn rclassn(ranges: &[(char, char)]) -> Token {
987        Class {
988            negated: true,
989            ranges: ranges.to_vec(),
990        }
991    }
992
993    syntax!(literal1, "a", vec![Literal('a')]);
994    syntax!(literal2, "ab", vec![Literal('a'), Literal('b')]);
995    syntax!(any1, "?", vec![Any]);
996    syntax!(any2, "a?b", vec![Literal('a'), Any, Literal('b')]);
997    syntax!(seq1, "*", vec![ZeroOrMore]);
998    syntax!(seq2, "a*b", vec![Literal('a'), ZeroOrMore, Literal('b')]);
999    syntax!(
1000        seq3,
1001        "*a*b*",
1002        vec![ZeroOrMore, Literal('a'), ZeroOrMore, Literal('b'), ZeroOrMore,]
1003    );
1004    syntax!(rseq1, "**", vec![RecursivePrefix]);
1005    syntax!(rseq2, "**/", vec![RecursivePrefix]);
1006    syntax!(rseq3, "/**", vec![RecursiveSuffix]);
1007    syntax!(rseq4, "/**/", vec![RecursiveZeroOrMore]);
1008    syntax!(rseq5, "a/**/b", vec![Literal('a'), RecursiveZeroOrMore, Literal('b'),]);
1009    syntax!(cls1, "[a]", vec![class('a', 'a')]);
1010    syntax!(cls2, "[!a]", vec![classn('a', 'a')]);
1011    syntax!(cls3, "[a-z]", vec![class('a', 'z')]);
1012    syntax!(cls4, "[!a-z]", vec![classn('a', 'z')]);
1013    syntax!(cls5, "[-]", vec![class('-', '-')]);
1014    syntax!(cls6, "[]]", vec![class(']', ']')]);
1015    syntax!(cls7, "[*]", vec![class('*', '*')]);
1016    syntax!(cls8, "[!!]", vec![classn('!', '!')]);
1017    syntax!(cls9, "[a-]", vec![rclass(&[('a', 'a'), ('-', '-')])]);
1018    syntax!(cls10, "[-a-z]", vec![rclass(&[('-', '-'), ('a', 'z')])]);
1019    syntax!(cls11, "[a-z-]", vec![rclass(&[('a', 'z'), ('-', '-')])]);
1020    syntax!(cls12, "[-a-z-]", vec![rclass(&[('-', '-'), ('a', 'z'), ('-', '-')]),]);
1021    syntax!(cls13, "[]-z]", vec![class(']', 'z')]);
1022    syntax!(cls14, "[--z]", vec![class('-', 'z')]);
1023    syntax!(cls15, "[ --]", vec![class(' ', '-')]);
1024    syntax!(cls16, "[0-9a-z]", vec![rclass(&[('0', '9'), ('a', 'z')])]);
1025    syntax!(cls17, "[a-z0-9]", vec![rclass(&[('a', 'z'), ('0', '9')])]);
1026    syntax!(cls18, "[!0-9a-z]", vec![rclassn(&[('0', '9'), ('a', 'z')])]);
1027    syntax!(cls19, "[!a-z0-9]", vec![rclassn(&[('a', 'z'), ('0', '9')])]);
1028    syntax!(cls20, "[^a]", vec![classn('a', 'a')]);
1029    syntax!(cls21, "[^a-z]", vec![classn('a', 'z')]);
1030
1031    syntaxerr!(err_unclosed1, "[", ErrorKind::UnclosedClass);
1032    syntaxerr!(err_unclosed2, "[]", ErrorKind::UnclosedClass);
1033    syntaxerr!(err_unclosed3, "[!", ErrorKind::UnclosedClass);
1034    syntaxerr!(err_unclosed4, "[!]", ErrorKind::UnclosedClass);
1035    syntaxerr!(err_range1, "[z-a]", ErrorKind::InvalidRange('z', 'a'));
1036    syntaxerr!(err_range2, "[z--]", ErrorKind::InvalidRange('z', '-'));
1037    syntaxerr!(err_alt1, "{a,b", ErrorKind::UnclosedAlternates);
1038    syntaxerr!(err_alt2, "{a,{b,c}", ErrorKind::UnclosedAlternates);
1039    syntaxerr!(err_alt3, "a,b}", ErrorKind::UnopenedAlternates);
1040    syntaxerr!(err_alt4, "{a,b}}", ErrorKind::UnopenedAlternates);
1041
1042    const CASEI: Options = Options {
1043        casei: Some(true),
1044        litsep: None,
1045        bsesc: None,
1046        ealtre: None,
1047        unccls: None,
1048    };
1049    const SLASHLIT: Options = Options {
1050        casei: None,
1051        litsep: Some(true),
1052        bsesc: None,
1053        ealtre: None,
1054        unccls: None,
1055    };
1056    const NOBSESC: Options = Options {
1057        casei: None,
1058        litsep: None,
1059        bsesc: Some(false),
1060        ealtre: None,
1061        unccls: None,
1062    };
1063    const BSESC: Options = Options {
1064        casei: None,
1065        litsep: None,
1066        bsesc: Some(true),
1067        ealtre: None,
1068        unccls: None,
1069    };
1070    const EALTRE: Options = Options {
1071        casei: None,
1072        litsep: None,
1073        bsesc: Some(true),
1074        ealtre: Some(true),
1075        unccls: None,
1076    };
1077    const UNCCLS: Options = Options {
1078        casei: None,
1079        litsep: None,
1080        bsesc: None,
1081        ealtre: None,
1082        unccls: Some(true),
1083    };
1084
1085    toregex!(allow_unclosed_class_single, r"[", r"^\[$", &UNCCLS);
1086    toregex!(allow_unclosed_class_many, r"[abc", r"^\[abc$", &UNCCLS);
1087    toregex!(allow_unclosed_class_empty1, r"[]", r"^\[\]$", &UNCCLS);
1088    toregex!(allow_unclosed_class_empty2, r"[][", r"^\[\]\[$", &UNCCLS);
1089    toregex!(allow_unclosed_class_negated_unclosed, r"[!", r"^\[!$", &UNCCLS);
1090    toregex!(allow_unclosed_class_negated_empty, r"[!]", r"^\[!\]$", &UNCCLS);
1091    toregex!(allow_unclosed_class_brace1, r"{[abc,xyz}", r"^(?:\[abc|xyz)$", &UNCCLS);
1092    toregex!(allow_unclosed_class_brace2, r"{[abc,[xyz}", r"^(?:\[abc|\[xyz)$", &UNCCLS);
1093    toregex!(allow_unclosed_class_brace3, r"{[abc],[xyz}", r"^(?:[abc]|\[xyz)$", &UNCCLS);
1094
1095    toregex!(re_empty, "", "^$");
1096
1097    toregex!(re_casei, "a", "(?i)^a$", &CASEI);
1098
1099    toregex!(re_slash1, "?", r"^[^/]$", SLASHLIT);
1100    toregex!(re_slash2, "*", r"^[^/]*$", SLASHLIT);
1101
1102    toregex!(re1, "a", "^a$");
1103    toregex!(re2, "?", "^.$");
1104    toregex!(re3, "*", "^.*$");
1105    toregex!(re4, "a?", "^a.$");
1106    toregex!(re5, "?a", "^.a$");
1107    toregex!(re6, "a*", "^a.*$");
1108    toregex!(re7, "*a", "^.*a$");
1109    toregex!(re8, "[*]", r"^[\*]$");
1110    toregex!(re9, "[+]", r"^[\+]$");
1111    toregex!(re10, "+", r"^\+$");
1112    toregex!(re11, "☃", r"^\xe2\x98\x83$");
1113    toregex!(re12, "**", r"^.*$");
1114    toregex!(re13, "**/", r"^.*$");
1115    toregex!(re14, "**/*", r"^(?:/?|.*/).*$");
1116    toregex!(re15, "**/**", r"^.*$");
1117    toregex!(re16, "**/**/*", r"^(?:/?|.*/).*$");
1118    toregex!(re17, "**/**/**", r"^.*$");
1119    toregex!(re18, "**/**/**/*", r"^(?:/?|.*/).*$");
1120    toregex!(re19, "a/**", r"^a/.*$");
1121    toregex!(re20, "a/**/**", r"^a/.*$");
1122    toregex!(re21, "a/**/**/**", r"^a/.*$");
1123    toregex!(re22, "a/**/b", r"^a(?:/|/.*/)b$");
1124    toregex!(re23, "a/**/**/b", r"^a(?:/|/.*/)b$");
1125    toregex!(re24, "a/**/**/**/b", r"^a(?:/|/.*/)b$");
1126    toregex!(re25, "**/b", r"^(?:/?|.*/)b$");
1127    toregex!(re26, "**/**/b", r"^(?:/?|.*/)b$");
1128    toregex!(re27, "**/**/**/b", r"^(?:/?|.*/)b$");
1129    toregex!(re28, "a**", r"^a.*.*$");
1130    toregex!(re29, "**a", r"^.*.*a$");
1131    toregex!(re30, "a**b", r"^a.*.*b$");
1132    toregex!(re31, "***", r"^.*.*.*$");
1133    toregex!(re32, "/a**", r"^/a.*.*$");
1134    toregex!(re33, "/**a", r"^/.*.*a$");
1135    toregex!(re34, "/a**b", r"^/a.*.*b$");
1136    toregex!(re35, "{a,b}", r"^(?:a|b)$");
1137    toregex!(re36, "{a,{b,c}}", r"^(?:a|(?:b|c))$");
1138    toregex!(re37, "{{a,b},{c,d}}", r"^(?:(?:a|b)|(?:c|d))$");
1139
1140    matches!(match1, "a", "a");
1141    matches!(match2, "a*b", "a_b");
1142    matches!(match3, "a*b*c", "abc");
1143    matches!(match4, "a*b*c", "a_b_c");
1144    matches!(match5, "a*b*c", "a___b___c");
1145    matches!(match6, "abc*abc*abc", "abcabcabcabcabcabcabc");
1146    matches!(match7, "a*a*a*a*a*a*a*a*a", "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa");
1147    matches!(match8, "a*b[xyz]c*d", "abxcdbxcddd");
1148    matches!(match9, "*.rs", ".rs");
1149    matches!(match10, "☃", "☃");
1150
1151    matches!(matchrec1, "some/**/needle.txt", "some/needle.txt");
1152    matches!(matchrec2, "some/**/needle.txt", "some/one/needle.txt");
1153    matches!(matchrec3, "some/**/needle.txt", "some/one/two/needle.txt");
1154    matches!(matchrec4, "some/**/needle.txt", "some/other/needle.txt");
1155    matches!(matchrec5, "**", "abcde");
1156    matches!(matchrec6, "**", "");
1157    matches!(matchrec7, "**", ".asdf");
1158    matches!(matchrec8, "**", "/x/.asdf");
1159    matches!(matchrec9, "some/**/**/needle.txt", "some/needle.txt");
1160    matches!(matchrec10, "some/**/**/needle.txt", "some/one/needle.txt");
1161    matches!(matchrec11, "some/**/**/needle.txt", "some/one/two/needle.txt");
1162    matches!(matchrec12, "some/**/**/needle.txt", "some/other/needle.txt");
1163    matches!(matchrec13, "**/test", "one/two/test");
1164    matches!(matchrec14, "**/test", "one/test");
1165    matches!(matchrec15, "**/test", "test");
1166    matches!(matchrec16, "/**/test", "/one/two/test");
1167    matches!(matchrec17, "/**/test", "/one/test");
1168    matches!(matchrec18, "/**/test", "/test");
1169    matches!(matchrec19, "**/.*", ".abc");
1170    matches!(matchrec20, "**/.*", "abc/.abc");
1171    matches!(matchrec21, "**/foo/bar", "foo/bar");
1172    matches!(matchrec22, ".*/**", ".abc/abc");
1173    matches!(matchrec23, "test/**", "test/");
1174    matches!(matchrec24, "test/**", "test/one");
1175    matches!(matchrec25, "test/**", "test/one/two");
1176    matches!(matchrec26, "some/*/needle.txt", "some/one/needle.txt");
1177
1178    matches!(matchrange1, "a[0-9]b", "a0b");
1179    matches!(matchrange2, "a[0-9]b", "a9b");
1180    matches!(matchrange3, "a[!0-9]b", "a_b");
1181    matches!(matchrange4, "[a-z123]", "1");
1182    matches!(matchrange5, "[1a-z23]", "1");
1183    matches!(matchrange6, "[123a-z]", "1");
1184    matches!(matchrange7, "[abc-]", "-");
1185    matches!(matchrange8, "[-abc]", "-");
1186    matches!(matchrange9, "[-a-c]", "b");
1187    matches!(matchrange10, "[a-c-]", "b");
1188    matches!(matchrange11, "[-]", "-");
1189    matches!(matchrange12, "a[^0-9]b", "a_b");
1190
1191    matches!(matchpat1, "*hello.txt", "hello.txt");
1192    matches!(matchpat2, "*hello.txt", "gareth_says_hello.txt");
1193    matches!(matchpat3, "*hello.txt", "some/path/to/hello.txt");
1194    matches!(matchpat4, "*hello.txt", "some\\path\\to\\hello.txt");
1195    matches!(matchpat5, "*hello.txt", "/an/absolute/path/to/hello.txt");
1196    matches!(matchpat6, "*some/path/to/hello.txt", "some/path/to/hello.txt");
1197    matches!(matchpat7, "*some/path/to/hello.txt", "a/bigger/some/path/to/hello.txt");
1198
1199    matches!(matchescape, "_[[]_[]]_[?]_[*]_!_", "_[_]_?_*_!_");
1200
1201    matches!(matchcasei1, "aBcDeFg", "aBcDeFg", CASEI);
1202    matches!(matchcasei2, "aBcDeFg", "abcdefg", CASEI);
1203    matches!(matchcasei3, "aBcDeFg", "ABCDEFG", CASEI);
1204    matches!(matchcasei4, "aBcDeFg", "AbCdEfG", CASEI);
1205
1206    matches!(matchalt1, "a,b", "a,b");
1207    matches!(matchalt2, ",", ",");
1208    matches!(matchalt3, "{a,b}", "a");
1209    matches!(matchalt4, "{a,b}", "b");
1210    matches!(matchalt5, "{**/src/**,foo}", "abc/src/bar");
1211    matches!(matchalt6, "{**/src/**,foo}", "foo");
1212    matches!(matchalt7, "{[}],foo}", "}");
1213    matches!(matchalt8, "{foo}", "foo");
1214    matches!(matchalt9, "{}", "");
1215    matches!(matchalt10, "{,}", "");
1216    matches!(matchalt11, "{*.foo,*.bar,*.wat}", "test.foo");
1217    matches!(matchalt12, "{*.foo,*.bar,*.wat}", "test.bar");
1218    matches!(matchalt13, "{*.foo,*.bar,*.wat}", "test.wat");
1219    matches!(matchalt14, "foo{,.txt}", "foo.txt");
1220    nmatches!(matchalt15, "foo{,.txt}", "foo");
1221    matches!(matchalt16, "foo{,.txt}", "foo", EALTRE);
1222    matches!(matchalt17, "{a,b{c,d}}", "bc");
1223    matches!(matchalt18, "{a,b{c,d}}", "bd");
1224    matches!(matchalt19, "{a,b{c,d}}", "a");
1225
1226    matches!(matchslash1, "abc/def", "abc/def", SLASHLIT);
1227    #[cfg(unix)]
1228    nmatches!(matchslash2, "abc?def", "abc/def", SLASHLIT);
1229    #[cfg(not(unix))]
1230    nmatches!(matchslash2, "abc?def", "abc\\def", SLASHLIT);
1231    nmatches!(matchslash3, "abc*def", "abc/def", SLASHLIT);
1232    matches!(matchslash4, "abc[/]def", "abc/def", SLASHLIT); // differs
1233    #[cfg(unix)]
1234    nmatches!(matchslash5, "abc\\def", "abc/def", SLASHLIT);
1235    #[cfg(not(unix))]
1236    matches!(matchslash5, "abc\\def", "abc/def", SLASHLIT);
1237
1238    matches!(matchbackslash1, "\\[", "[", BSESC);
1239    matches!(matchbackslash2, "\\?", "?", BSESC);
1240    matches!(matchbackslash3, "\\*", "*", BSESC);
1241    matches!(matchbackslash4, "\\[a-z]", "\\a", NOBSESC);
1242    matches!(matchbackslash5, "\\?", "\\a", NOBSESC);
1243    matches!(matchbackslash6, "\\*", "\\\\", NOBSESC);
1244    #[cfg(unix)]
1245    matches!(matchbackslash7, "\\a", "a");
1246    #[cfg(not(unix))]
1247    matches!(matchbackslash8, "\\a", "/a");
1248
1249    nmatches!(matchnot1, "a*b*c", "abcd");
1250    nmatches!(matchnot2, "abc*abc*abc", "abcabcabcabcabcabcabca");
1251    nmatches!(matchnot3, "some/**/needle.txt", "some/other/notthis.txt");
1252    nmatches!(matchnot4, "some/**/**/needle.txt", "some/other/notthis.txt");
1253    nmatches!(matchnot5, "/**/test", "test");
1254    nmatches!(matchnot6, "/**/test", "/one/notthis");
1255    nmatches!(matchnot7, "/**/test", "/notthis");
1256    nmatches!(matchnot8, "**/.*", "ab.c");
1257    nmatches!(matchnot9, "**/.*", "abc/ab.c");
1258    nmatches!(matchnot10, ".*/**", "a.bc");
1259    nmatches!(matchnot11, ".*/**", "abc/a.bc");
1260    nmatches!(matchnot12, "a[0-9]b", "a_b");
1261    nmatches!(matchnot13, "a[!0-9]b", "a0b");
1262    nmatches!(matchnot14, "a[!0-9]b", "a9b");
1263    nmatches!(matchnot15, "[!-]", "-");
1264    nmatches!(matchnot16, "*hello.txt", "hello.txt-and-then-some");
1265    nmatches!(matchnot17, "*hello.txt", "goodbye.txt");
1266    nmatches!(matchnot18, "*some/path/to/hello.txt", "some/path/to/hello.txt-and-then-some");
1267    nmatches!(matchnot19, "*some/path/to/hello.txt", "some/other/path/to/hello.txt");
1268    nmatches!(matchnot20, "a", "foo/a");
1269    nmatches!(matchnot21, "./foo", "foo");
1270    nmatches!(matchnot22, "**/foo", "foofoo");
1271    nmatches!(matchnot23, "**/foo/bar", "foofoo/bar");
1272    nmatches!(matchnot24, "/*.c", "mozilla-sha1/sha1.c");
1273    nmatches!(matchnot25, "*.c", "mozilla-sha1/sha1.c", SLASHLIT);
1274    nmatches!(
1275        matchnot26,
1276        "**/m4/ltoptions.m4",
1277        "csharp/src/packages/repositories.config",
1278        SLASHLIT
1279    );
1280    nmatches!(matchnot27, "a[^0-9]b", "a0b");
1281    nmatches!(matchnot28, "a[^0-9]b", "a9b");
1282    nmatches!(matchnot29, "[^-]", "-");
1283    nmatches!(matchnot30, "some/*/needle.txt", "some/needle.txt");
1284    nmatches!(matchrec31, "some/*/needle.txt", "some/one/two/needle.txt", SLASHLIT);
1285    nmatches!(matchrec32, "some/*/needle.txt", "some/one/two/three/needle.txt", SLASHLIT);
1286    nmatches!(matchrec33, ".*/**", ".abc");
1287    nmatches!(matchrec34, "foo/**", "foo");
1288}