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;