Skip to main content

ferralk_glob/
lib.rs

1#![forbid(unsafe_code)]
2#![warn(missing_docs)]
3//! Compiled, byte-first glob patterns with explicit behavior-changing options.
4//!
5//! A [`Pattern`] is compiled once from any `AsRef<[u8]>` and matched many
6//! times against bytes, so filenames never pass through a lossy UTF-8
7//! conversion. Syntax that changes meaning is opt-in through
8//! [`PatternOptions`]: recursive `**`, braces, extglobs, hidden-name matching,
9//! and ASCII case folding are off in [`PatternOptions::default`] until asked
10//! for. [`PatternOptions::walker`] asks for the first three at once, which is
11//! the dialect the `ferralk` walker reads its patterns in.
12//!
13//! ```
14//! use ferralk_glob::{Pattern, PatternOptions};
15//!
16//! let pattern = Pattern::compile("src/**/*.{rs,toml}", PatternOptions::walker())?;
17//!
18//! assert!(pattern.is_match_glob_path("src/lib.rs"));
19//! assert!(pattern.is_match_glob_path("src/parser/Cargo.toml"));
20//! assert!(!pattern.is_match_glob_path("src/lib.rs.bak"));
21//! # Ok::<(), ferralk_glob::PatternError>(())
22//! ```
23//!
24//! Three entry points decide how far a wildcard may reach:
25//!
26//! - [`Pattern::is_match_glob_path`] keeps every ordinary wildcard inside one
27//!   path component, as a shell glob does. Use it for filesystem paths.
28//! - [`Pattern::is_match`] compares a whole byte sequence in which `/` is not
29//!   special.
30//! - [`Pattern::is_match_path`] and the [`Pattern::filter_paths`] family keep
31//!   zlob's list-filter rule for callers porting from it.
32//!
33//! The same pattern, `*.rs`, asked through each of them:
34//!
35//! | Candidate | [`is_match`](Pattern::is_match) | [`is_match_path`](Pattern::is_match_path) | [`is_match_glob_path`](Pattern::is_match_glob_path) |
36//! | --- | --- | --- | --- |
37//! | `lib.rs` | yes | yes | yes |
38//! | `src/lib.rs` | yes: `*` crosses `/` | yes: a wildcard in the first component crosses | no: `*` stays in one component |
39//! | `./lib.rs` | no: `./` is compared literally | yes: one leading `./` is ignored | yes: one leading `./` is ignored |
40//!
41//! The matcher covers literals, `*`, `?`, `**`, character
42//! classes, escapes, leading-period handling, ASCII case folding, nested brace
43//! expansion, and Bash-style extglobs. The
44//! [usage guide](https://github.com/sebastian-software/ferralk/blob/main/docs/usage.md)
45//! documents each option with its default.
46//!
47//! # Recipes
48//!
49//! Every recipe below is a tested example. Finding files on disk is the
50//! `ferralk` crate's job; its
51//! [recipes](https://docs.rs/ferralk/latest/ferralk/#recipes) cover
52//! `.gitignore`, include and exclude lists, early termination, errors, and
53//! async use.
54//!
55//! ## Match a path against several configured globs
56//!
57//! A [`PatternSet`] is the counterpart of `globset::GlobSet`: it compiles a
58//! list once and answers whether any glob matches, or which ones do, by
59//! their position in the list. It asks only the globs whose literal
60//! extension, component, prefix or suffix the path has, so a long list costs
61//! little more per path than a short one.
62//!
63//! ```
64//! use ferralk_glob::{PatternOptions, PatternSet, PatternSetError};
65//!
66//! let globs = ["src/**/*.rs", "tests/**/*.rs", "*.toml"];
67//! let set = PatternSet::new(globs, PatternOptions::walker())?;
68//!
69//! assert!(set.is_match_glob_path("src/parser/lexer.rs"));
70//! assert!(set.is_match_glob_path("Cargo.toml"));
71//! // `*` stays in its component, so `*.toml` names the top level only.
72//! assert!(!set.is_match_glob_path("crates/cli/Cargo.toml"));
73//!
74//! let mut which = Vec::new();
75//! set.matches_glob_path_into("tests/cli.rs", &mut which);
76//! assert_eq!(which, [1]);
77//! # Ok::<(), PatternSetError>(())
78//! ```
79//!
80//! [`PatternSetError::index`] names the glob that failed to compile. For
81//! globs with different options, collect compiled [`Pattern`]s into the set
82//! instead; [`PatternSet`] shows both.
83//!
84//! ## Port a fast-glob or globby list with `!` negations
85//!
86//! A leading `!` is not negation here: the matcher reads it as a literal `!`,
87//! so `!**/*.test.ts` compiles and matches nothing. Split such a list into
88//! includes and excludes first. fast-glob itself keeps `!(…)` as a negated
89//! extglob, and so does the split below.
90//!
91//! ```
92//! use ferralk_glob::{Pattern, PatternError, PatternOptions, PatternSet};
93//!
94//! fn compile_list(globs: &[&str]) -> Result<(PatternSet, PatternSet), PatternError> {
95//!     let options = PatternOptions::walker();
96//!     let (mut include, mut exclude) = (Vec::new(), Vec::new());
97//!     for glob in globs {
98//!         match glob.strip_prefix('!').filter(|rest| !rest.starts_with('(')) {
99//!             Some(negated) => exclude.push(Pattern::compile(negated, options)?),
100//!             None => include.push(Pattern::compile(glob, options)?),
101//!         }
102//!     }
103//!     Ok((include.into_iter().collect(), exclude.into_iter().collect()))
104//! }
105//!
106//! let (include, exclude) =
107//!     compile_list(&["src/**/*.ts", "!src/**/*.test.ts", "!**/generated/**"])?;
108//! let is_selected =
109//!     |path: &str| include.is_match_glob_path(path) && !exclude.is_match_glob_path(path);
110//! assert!(is_selected("src/app/main.ts"));
111//! assert!(!is_selected("src/app/main.test.ts"));
112//! assert!(!is_selected("src/generated/client.ts"));
113//! # Ok::<(), PatternError>(())
114//! ```
115//!
116//! fast-glob's `dot: true` is [`PatternOptions::match_hidden`]`(true)`, and
117//! its `caseSensitiveMatch: false` is
118//! [`PatternOptions::case_insensitive`]`(true)`. A walker takes the same
119//! list through `include` and `exclude`, and rejects a leading `!` instead of
120//! reading it literally.
121//!
122//! ## Match a `Path`
123//!
124//! Patterns take bytes, and [`path_bytes`] gives them for a
125//! [`Path`](std::path::Path) or an [`OsStr`] without a lossy
126//! conversion. Match the part of the path the pattern is written for: a
127//! pattern is relative, so strip the directory it is relative to first.
128//!
129//! ```
130//! use std::path::Path;
131//!
132//! use ferralk_glob::{Pattern, PatternOptions, path_bytes};
133//!
134//! let pattern = Pattern::compile("src/**/*.rs", PatternOptions::walker())?;
135//! let root = Path::new("/work/project");
136//! let path = root.join("src").join("bin").join("main.rs");
137//!
138//! let relative = path.strip_prefix(root).expect("path is below the root");
139//! assert!(pattern.is_match_glob_path(path_bytes(relative)));
140//! // The unstripped path starts with `/work`, which `src/**` does not.
141//! assert!(!pattern.is_match_glob_path(path_bytes(&path)));
142//! # Ok::<(), ferralk_glob::PatternError>(())
143//! ```
144//!
145//! A path that `ferralk` walked already carries its root-relative part as
146//! `WalkEntry::relative_path`, the spelling the walker's own patterns match.
147//!
148//! ## Put literal text into a pattern
149//!
150//! A directory name, a path, or user input can contain bytes that are syntax.
151//! [`escape`] and [`escape_str`] make them literal under every option
152//! combination with escaping enabled, so the text can be joined with glob
153//! syntax of the caller's own.
154//!
155//! ```
156//! use ferralk_glob::{Pattern, PatternOptions, escape_str};
157//!
158//! let directory = "reports {2024} [final]";
159//! let glob = format!("{}/**/*.csv", escape_str(directory));
160//! let pattern = Pattern::compile(&glob, PatternOptions::walker())?;
161//!
162//! assert!(pattern.is_match_glob_path("reports {2024} [final]/q1/sales.csv"));
163//! assert!(!pattern.is_match_glob_path("reports 2024 f/q1/sales.csv"));
164//!
165//! // Unescaped, the braces are an alternation and the brackets a class.
166//! let unescaped = Pattern::compile(format!("{directory}/**/*.csv"), PatternOptions::walker())?;
167//! assert!(unescaped.is_match_glob_path("reports 2024 f/q1/sales.csv"));
168//! assert!(!unescaped.is_match_glob_path("reports {2024} [final]/q1/sales.csv"));
169//! # Ok::<(), ferralk_glob::PatternError>(())
170//! ```
171//!
172//! ## Filter a list of paths
173//!
174//! [`Pattern::filter_paths`] keeps zlob's list rule, in which a wildcard in
175//! the first component crosses separators. For shell semantics, filter with
176//! [`Pattern::is_match_glob_path`] instead; both are shown on
177//! [`Pattern::filter_paths`].
178//!
179//! ## Report an invalid pattern
180//!
181//! [`PatternError::offset`] is a byte offset into the pattern, so a caller
182//! can point at the problem; see [`PatternError`].
183//!
184//! Provenance: semantics are ported and differentially checked against zlob
185//! v1.6.3, source commit 4bc4da2cbc823d3911b4a1436448687c398977dd, primarily
186//! `zig-src/fnmatch.zig`, `zig-src/pattern_context.zig`, and
187//! `test/test_fnmatch.zig`. Deliberate differences live in
188//! the checked-in corpus and compatibility matrix.
189
190use std::{
191    cell::RefCell,
192    collections::{BTreeMap, HashSet},
193    error::Error,
194    ffi::OsStr,
195    fmt,
196    hash::{Hash, Hasher},
197};
198
199use memchr::{memchr, memchr2, memchr3, memmem};
200
201mod set;
202#[cfg(all(target_arch = "aarch64", target_os = "macos"))]
203mod suffix_word;
204mod sweep;
205
206pub use set::{PatternSet, PatternSetError};
207use sweep::{SweepEngine, SweepState};
208
209/// A compiled glob pattern.
210#[derive(Debug, Clone)]
211pub struct Pattern {
212    alternatives: Vec<CompiledAlternative>,
213    alternative_fast_path: Option<Box<AlternativeFastPath>>,
214    /// The list-filter routes of [`Pattern::is_match_path`], or `None` when
215    /// every alternative answers it exactly like [`Pattern::is_match`].
216    path_filter: Option<Box<PathFilter>>,
217    walker_path_viability: WalkerPathViability,
218    walker_path_problem_offset: Option<usize>,
219    options: PatternOptions,
220}
221
222/// How the alternatives answer the list-filter entry point
223/// [`Pattern::is_match_path`].
224///
225/// Every alternative has exactly one route, chosen from its own syntax and
226/// never from a sibling's, so a brace arm's verdict does not depend on the
227/// arms beside it.
228#[derive(Debug, Clone)]
229struct PathFilter {
230    /// Second compiles for the component policy, with one leading `./`
231    /// removed and a fast path chosen for component-local wildcards. Once
232    /// one plain alternative needs such a copy every plain alternative gets
233    /// one, so they are all matched in one contiguous pass; the copy of an
234    /// alternative without a component-local position answers exactly as
235    /// the original does. An extglob alternative gets a copy only for its
236    /// leading `./`.
237    compiled: Vec<CompiledAlternative>,
238    /// Indices of extglob alternatives answered from their shared compile. A
239    /// program reads the component policy at match time, so a group directly
240    /// behind a separator is component-local exactly like a plain wildcard
241    /// there.
242    programs: Vec<usize>,
243    /// Indices of plain alternatives answered like [`Pattern::is_match`],
244    /// used only while no plain alternative needs a component copy.
245    whole: Vec<usize>,
246}
247
248impl PathFilter {
249    fn compile(
250        alternatives: &[CompiledAlternative],
251        options: PatternOptions,
252        budget: &mut IrBudget,
253    ) -> Result<Option<Self>, PatternError> {
254        let is_program = |alternative: &CompiledAlternative| {
255            options.extglob && alternative.extglob.is_some() && !alternative.raw.starts_with(b"./")
256        };
257        let needs_copy = |alternative: &CompiledAlternative| {
258            alternative.raw.starts_with(b"./")
259                || alternative.tokens.windows(2).any(|tokens| {
260                    matches!(
261                        tokens,
262                        [Token::Separator, Token::Any | Token::Star | Token::Class(_)]
263                    )
264                })
265        };
266        let plain_needs_copy = alternatives
267            .iter()
268            .any(|alternative| !is_program(alternative) && needs_copy(alternative));
269        let programs = alternatives.iter().filter(|item| is_program(item)).count();
270        let plain = alternatives.len() - programs;
271        // Sized up front: the filter lives as long as the pattern.
272        let (copies, whole) = if plain_needs_copy {
273            (plain, 0)
274        } else {
275            (0, plain)
276        };
277        let mut filter = Self {
278            compiled: Vec::with_capacity(copies),
279            programs: Vec::with_capacity(programs),
280            whole: Vec::with_capacity(whole),
281        };
282        for (index, alternative) in alternatives.iter().enumerate() {
283            if is_program(alternative) {
284                filter.programs.push(index);
285            } else if plain_needs_copy {
286                // A second compiled copy of the alternative, so it costs the
287                // budget a second time.
288                filter
289                    .compiled
290                    .push(Pattern::compile_path_filter_alternative(
291                        alternative,
292                        options,
293                        budget,
294                    )?);
295            } else {
296                filter.whole.push(index);
297            }
298        }
299        Ok((!filter.compiled.is_empty() || !filter.programs.is_empty()).then_some(filter))
300    }
301
302    /// Answers `path` with `options` on the component routes and
303    /// `whole_options` on the alternatives read like `is_match`.
304    fn is_match(
305        &self,
306        alternatives: &[CompiledAlternative],
307        options: PatternOptions,
308        whole_options: PatternOptions,
309        path: &[u8],
310    ) -> bool {
311        let shared = |indices: &[usize], options: PatternOptions| {
312            indices.iter().any(|&index| {
313                Pattern::match_alternatives(
314                    std::slice::from_ref(&alternatives[index]),
315                    options,
316                    path,
317                )
318            })
319        };
320        Pattern::match_alternatives(&self.compiled, options, path)
321            || shared(&self.programs, options)
322            || shared(&self.whole, whole_options)
323    }
324}
325
326/// Explicit switches that affect glob interpretation.
327///
328/// Two starting points have names. [`PatternOptions::default`] is the
329/// conservative reading: `**`, braces and extglobs are ordinary text until a
330/// switch enables them. [`PatternOptions::walker`] is the dialect the `ferralk`
331/// walker compiles its include and exclude patterns in. Either one can be
332/// adjusted further with the builder methods.
333///
334/// ```
335/// use ferralk_glob::{Pattern, PatternOptions};
336///
337/// // `walker()` is `default()` with three switches turned on.
338/// assert_eq!(
339///     PatternOptions::walker(),
340///     PatternOptions::default()
341///         .recursive_double_star(true)
342///         .braces(true)
343///         .extglob(true),
344/// );
345///
346/// // The same text is a different pattern under each of them.
347/// let walker = Pattern::compile("*.{rs,toml}", PatternOptions::walker())?;
348/// let conservative = Pattern::compile("*.{rs,toml}", PatternOptions::default())?;
349/// assert!(walker.is_match_glob_path("Cargo.toml"));
350/// assert!(!conservative.is_match_glob_path("Cargo.toml"));
351///
352/// // Further switches chain onto either one.
353/// let loose = PatternOptions::walker().match_hidden(true).case_insensitive(true);
354/// assert!(Pattern::compile("*.RS", loose)?.is_match_glob_path(".build.rs"));
355/// # Ok::<(), ferralk_glob::PatternError>(())
356/// ```
357#[derive(Debug, Clone, Copy, PartialEq, Eq)]
358pub struct PatternOptions {
359    braces: bool,
360    recursive_double_star: bool,
361    extglob: bool,
362    match_hidden: bool,
363    case_insensitive: bool,
364    escape: bool,
365    component_wildcards: bool,
366    root_component_wildcards: bool,
367    /// Whether the first wildcard of a compiled subpattern occupies a
368    /// component-local position in its enclosing pattern.
369    candidate_root_component_wildcard: bool,
370    /// Whether byte zero of this candidate starts a path component.
371    ///
372    /// Extglob alternatives borrow a range from their enclosing candidate.
373    /// The range can start in the middle of a component, where a leading dot
374    /// is an ordinary byte rather than a hidden-name marker.
375    candidate_starts_component: bool,
376}
377
378impl Default for PatternOptions {
379    fn default() -> Self {
380        Self::CONSERVATIVE
381    }
382}
383
384impl PatternOptions {
385    /// The value [`Default::default`] returns, spelled as a constant so that
386    /// [`Self::walker`] can build on it in a `const fn`.
387    const CONSERVATIVE: Self = Self {
388        braces: false,
389        recursive_double_star: false,
390        extglob: false,
391        match_hidden: false,
392        case_insensitive: false,
393        escape: true,
394        component_wildcards: false,
395        root_component_wildcards: false,
396        candidate_root_component_wildcard: false,
397        candidate_starts_component: true,
398    };
399
400    /// The dialect `ferralk::Walker` compiles its include and exclude patterns
401    /// in: [`default`](Self::default) with recursive `**`, braces and extglobs
402    /// enabled.
403    ///
404    /// Compile a walker pattern with these options to validate it up front or
405    /// to match it again with the walker's semantics, for instance inside a
406    /// `visit` predicate or over paths that never touched a filesystem. The
407    /// walker builds its own options from this constructor, so the two cannot
408    /// drift apart.
409    ///
410    /// Every other switch keeps its default. In particular a wildcard does not
411    /// cover a leading period, which matches the walker's default for
412    /// includes; chain [`match_hidden(true)`](Self::match_hidden) to mirror
413    /// `Walker::match_hidden(true)`, or to match an exclude, which the walker
414    /// always lets cover a leading period. Case folding and escaping are not
415    /// walker settings and stay at their defaults there too.
416    ///
417    /// Options select syntax, and the entry point selects how far a wildcard
418    /// reaches. For a root-relative path, [`Pattern::is_match_glob_path`]
419    /// answers as the walker does under its default
420    /// `WildcardMode::ComponentScoped`, and [`Pattern::is_match`] as it does
421    /// under `WildcardMode::SeparatorCrossing`. A few walker rules sit outside
422    /// the pattern dialect: the walker reads a trailing `/` as "directories
423    /// only", rewrites an absolute pattern against its root, and refuses a
424    /// pattern no walk can match, such as one with a `..` component, which
425    /// [`Pattern::compile`] accepts as matcher text.
426    ///
427    /// ```
428    /// use ferralk_glob::{Pattern, PatternOptions};
429    ///
430    /// let walker = PatternOptions::walker();
431    /// let conservative = PatternOptions::default();
432    ///
433    /// // `**` crosses directories only in the walker dialect; by default it
434    /// // is two ordinary stars that stay inside one component.
435    /// let recursive = Pattern::compile("src/**/*.rs", walker)?;
436    /// assert!(recursive.is_match_glob_path("src/lib.rs"));
437    /// assert!(recursive.is_match_glob_path("src/parser/token.rs"));
438    /// let plain = Pattern::compile("src/**/*.rs", conservative)?;
439    /// assert!(!plain.is_match_glob_path("src/lib.rs"));
440    ///
441    /// // Braces are alternatives in the walker dialect and literal text by
442    /// // default.
443    /// let either = Pattern::compile("*.{rs,toml}", walker)?;
444    /// assert!(either.is_match_glob_path("lib.rs"));
445    /// assert!(either.is_match_glob_path("Cargo.toml"));
446    /// let literal = Pattern::compile("*.{rs,toml}", conservative)?;
447    /// assert!(!literal.is_match_glob_path("lib.rs"));
448    /// assert!(literal.is_match_glob_path("odd.{rs,toml}"));
449    ///
450    /// // Like the walker, the preset leaves hidden names to an explicit opt-in.
451    /// assert!(!recursive.is_match_glob_path("src/.cache/lib.rs"));
452    /// let hidden = Pattern::compile("src/**/*.rs", walker.match_hidden(true))?;
453    /// assert!(hidden.is_match_glob_path("src/.cache/lib.rs"));
454    /// # Ok::<(), ferralk_glob::PatternError>(())
455    /// ```
456    #[must_use]
457    pub const fn walker() -> Self {
458        Self::CONSERVATIVE
459            .recursive_double_star(true)
460            .braces(true)
461            .extglob(true)
462    }
463
464    /// Enables nested brace alternatives.
465    #[must_use]
466    pub const fn braces(mut self, enabled: bool) -> Self {
467        self.braces = enabled;
468        self
469    }
470
471    /// Gives a `**` that forms a whole path component recursive,
472    /// separator-crossing semantics, as gitignore, Bash `globstar`, `globset`,
473    /// and `fast-glob` do.
474    ///
475    /// A star run is a whole component when an unescaped `/` or an end of the
476    /// pattern bounds it on each side: `**`, `**/x`, `x/**`, `x/**/y`. Braces
477    /// are expanded first, so every alternative is judged as the pattern it
478    /// expands to, and an extglob alternative takes its group's position. A
479    /// `**/` matches zero or more whole components and hands over only at a
480    /// component start, so `**/x` matches `x` and `a/x` but not `sx`, and
481    /// `a/**/b` matches `a/b` but not `a/xb`. A trailing `/**` also accepts the
482    /// path without it: `x/**` matches `x`. An extglob group reads like its
483    /// alternatives written in its place, so both rules hold around groups
484    /// too: `@(**)/y` matches `y`, and `@(x)/**` and `x/@(**)` match `x`.
485    ///
486    /// Any other run, such as `a**`, `**b`, or `**.ts`, stays ordinary stars:
487    /// component-local under [`Pattern::is_match_glob_path`], the position
488    /// rule under [`Pattern::is_match_path`], and separator-crossing under
489    /// [`Pattern::is_match`]. When disabled, every run reads that way.
490    ///
491    /// ```
492    /// use ferralk_glob::{Pattern, PatternOptions};
493    ///
494    /// let options = PatternOptions::default().recursive_double_star(true);
495    /// let whole = Pattern::compile("**/x", options)?;
496    /// assert!(whole.is_match_glob_path("x"));
497    /// assert!(whole.is_match_glob_path("a/b/x"));
498    /// assert!(!whole.is_match_glob_path("a/sx"));
499    ///
500    /// let attached = Pattern::compile("a**/x", options)?;
501    /// assert!(attached.is_match_glob_path("abc/x"));
502    /// assert!(!attached.is_match_glob_path("a/b/x"));
503    /// # Ok::<(), ferralk_glob::PatternError>(())
504    /// ```
505    #[must_use]
506    pub const fn recursive_double_star(mut self, enabled: bool) -> Self {
507        self.recursive_double_star = enabled;
508        self
509    }
510
511    /// Enables Bash-style extglobs.
512    #[must_use]
513    pub const fn extglob(mut self, enabled: bool) -> Self {
514        self.extglob = enabled;
515        self
516    }
517
518    /// Allows wildcard tokens to match a leading period in a path component.
519    #[must_use]
520    pub const fn match_hidden(mut self, enabled: bool) -> Self {
521        self.match_hidden = enabled;
522        self
523    }
524
525    /// Performs ASCII-only case-insensitive matching.
526    #[must_use]
527    pub const fn case_insensitive(mut self, enabled: bool) -> Self {
528        self.case_insensitive = enabled;
529        self
530    }
531
532    /// Enables backslash escapes in patterns.
533    #[must_use]
534    pub const fn escape(mut self, enabled: bool) -> Self {
535        self.escape = enabled;
536        self
537    }
538}
539
540/// An error returned when a pattern is not syntactically valid.
541///
542/// [`offset`](Self::offset) is a zero-based byte offset into the pattern, so
543/// a caller can point at the construct that failed. `Display` reads
544/// `<message> at byte <offset>`; the message is diagnostic text, while the
545/// offset is the stable part.
546///
547/// ```
548/// use ferralk_glob::{Pattern, PatternOptions};
549///
550/// let source = "src/[a-z/*.rs";
551/// let error = Pattern::compile(source, PatternOptions::walker()).unwrap_err();
552/// assert_eq!(error.offset(), 4); // the `[` that is never closed
553///
554/// // A byte offset is a column only for ASCII; count the characters before it
555/// // to place a marker under any pattern.
556/// let column = source[..error.offset()].chars().count();
557/// let report = format!("{error}\n{source}\n{:>width$}", "^", width = column + 1);
558/// assert!(report.ends_with("src/[a-z/*.rs\n    ^"));
559/// ```
560#[derive(Debug, Clone, PartialEq, Eq)]
561pub struct PatternError {
562    offset: usize,
563    message: &'static str,
564}
565
566impl PatternError {
567    /// Builds an error for a pattern a layer above the matcher rejected.
568    ///
569    /// Compilation produces these on its own; this exists for callers that
570    /// impose rules the pattern language cannot express, such as a walker
571    /// deciding that an absolute pattern cannot be rewritten for its root. Such
572    /// a caller already returns `PatternError`, and a second error type in the
573    /// same position would say nothing extra.
574    ///
575    /// `offset` is a byte offset into the pattern the caller was given, and
576    /// `message` describes the construct rather than the caller's own state, so
577    /// that [`Display`](fmt::Display) reads the same as a compilation error.
578    #[must_use]
579    pub const fn new(offset: usize, message: &'static str) -> Self {
580        Self { offset, message }
581    }
582
583    /// Zero-based byte offset of the invalid construct.
584    pub const fn offset(&self) -> usize {
585        self.offset
586    }
587
588    /// Description of the invalid construct.
589    ///
590    /// This text is intended for diagnostics and is not a stable interface;
591    /// use [`Self::offset`] for programmatic handling.
592    pub const fn message(&self) -> &'static str {
593        self.message
594    }
595}
596
597impl fmt::Display for PatternError {
598    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
599        write!(formatter, "{} at byte {}", self.message, self.offset)
600    }
601}
602
603impl Error for PatternError {}
604
605/// Whether a compiled pattern has an arm a walker can spell as a candidate.
606///
607/// This is compiler metadata for path-consuming embeddings. It is calculated
608/// after brace expansion from the compiled token and extglob branch objects,
609/// rather than by parsing the source expression again.
610#[doc(hidden)]
611#[derive(Debug, Clone, Copy, PartialEq, Eq)]
612#[non_exhaustive]
613pub enum WalkerPathViability {
614    /// At least one compiled arm avoids root-only and `.` components, and no
615    /// brace-expanded arm contains a literal `..` component.
616    Viable,
617    /// A compiled walker arm contains a literal `..` component.
618    ParentComponent,
619    /// Every compiled arm names only the walk root.
620    Root,
621    /// Every compiled arm has an empty leading or interior component: a
622    /// repeated separator, a leading `/`, or an empty alternative.
623    EmptyComponent,
624    /// Every compiled arm ends in a literal `.` component.
625    TrailingDot,
626    /// Every compiled arm contains a non-final literal `.` component.
627    DotComponent,
628}
629
630impl Pattern {
631    /// Reports whether the enabled syntax options make a pattern non-literal.
632    ///
633    /// This is a byte-first preflight helper. Like zlob's `hasWildcards`, it
634    /// deliberately reports syntax markers even when an escape option would
635    /// later make a particular marker literal.
636    #[must_use]
637    pub fn has_wildcards(pattern: impl AsRef<[u8]>, options: PatternOptions) -> bool {
638        let pattern = pattern.as_ref();
639        if memchr3(b'*', b'?', b'[', pattern).is_some() {
640            return true;
641        }
642        if options.braces && memchr(b'{', pattern).is_some() {
643            return true;
644        }
645        if !options.extglob {
646            return false;
647        }
648        let mut offset = 0;
649        while let Some(found) = memchr(b'(', &pattern[offset..]) {
650            let index = offset + found;
651            if index > 0 && matches!(pattern[index - 1], b'?' | b'*' | b'+' | b'@' | b'!') {
652                return true;
653            }
654            offset = index + 1;
655        }
656        false
657    }
658
659    /// Compiles a pattern once for repeated matching against raw path bytes.
660    ///
661    /// ```
662    /// use ferralk_glob::{Pattern, PatternOptions};
663    ///
664    /// let source_file = Pattern::compile("src/**/*.{rs,toml}", PatternOptions::walker())?;
665    ///
666    /// assert!(source_file.is_match_glob_path("src/lib.rs"));
667    /// assert!(!source_file.is_match_glob_path("src/generated/lib.rs.bak"));
668    /// # Ok::<(), ferralk_glob::PatternError>(())
669    /// ```
670    ///
671    /// # Errors
672    ///
673    /// Returns a [`PatternError`] when the pattern is not valid under
674    /// `options`: an unclosed class or extglob group, an invalid range, or a
675    /// pattern beyond the compile budget. [`PatternError::offset`] locates the
676    /// problem in the caller's pattern bytes.
677    pub fn compile(
678        pattern: impl AsRef<[u8]>,
679        options: PatternOptions,
680    ) -> Result<Self, PatternError> {
681        let pattern = pattern.as_ref();
682        // Direct source is an implicit identity range. Brace expansion only
683        // materializes the few source spans whose byte positions actually
684        // change; a literal never pays one `usize` per input byte.
685        let source_provenance = SourceProvenance::Contiguous { source_start: 0 };
686        let mut budget = IrBudget::new();
687        let mut provenance_budget = ProvenanceBudget::new();
688        let mut compiled = Self::compile_within(
689            pattern,
690            options,
691            &mut budget,
692            &mut provenance_budget,
693            Some(&source_provenance),
694            pattern.starts_with(b"./"),
695            ComponentBounds::WHOLE,
696        )?;
697        let (viability, offset) = walker_path_analysis(&compiled.alternatives, &mut budget)?;
698        compiled.walker_path_viability = viability;
699        compiled.walker_path_problem_offset = offset;
700        compiled.release_compile_state();
701        Ok(compiled)
702    }
703
704    /// Drops what only the compile reads.
705    ///
706    /// The walker path shapes and the extglob source provenance exist for
707    /// [`walker_path_analysis`], whose verdict is stored on the pattern.
708    /// Nothing a match reads changes.
709    fn release_compile_state(&mut self) {
710        let copies = self
711            .path_filter
712            .iter_mut()
713            .flat_map(|filter| filter.compiled.iter_mut());
714        for alternative in self.alternatives.iter_mut().chain(copies) {
715            alternative.release_compile_state();
716        }
717    }
718
719    /// Summarizes whether a walker can represent a match from this pattern.
720    ///
721    /// The summary is derived from the actual alternatives and extglob arms
722    /// [`Pattern::compile`] created. It intentionally exposes semantics rather
723    /// than source offsets, which can no longer identify one branch after
724    /// brace expansion.
725    #[doc(hidden)]
726    #[must_use]
727    pub const fn walker_path_viability(&self) -> WalkerPathViability {
728        self.walker_path_viability
729    }
730
731    /// Byte offset of the determinate component behind walker viability.
732    ///
733    /// The compiler retains original source provenance through brace expansion
734    /// whenever an offending byte survives in an expanded arm. It returns
735    /// `None` only when no one source location is available and an embedding
736    /// must use its conventional fallback location.
737    #[doc(hidden)]
738    #[must_use]
739    pub const fn walker_path_problem_offset(&self) -> Option<usize> {
740        self.walker_path_problem_offset
741    }
742
743    /// The compile every alternative shares, carrying the budget that bounds
744    /// their total.
745    ///
746    /// Brace expansion recurses through here once per alternative, and an
747    /// extglob group's alternatives recurse again, so one budget threaded down
748    /// is what makes the total observable at all: each call on its own looks
749    /// small.
750    fn compile_within(
751        pattern: &[u8],
752        options: PatternOptions,
753        budget: &mut IrBudget,
754        provenance_budget: &mut ProvenanceBudget,
755        walker_source_provenance: Option<&SourceProvenance>,
756        leading_dot_is_normalized: bool,
757        bounds: ComponentBounds,
758    ) -> Result<Self, PatternError> {
759        if options.braces {
760            let parse_options = PatternOptions {
761                braces: false,
762                ..options
763            };
764            ensure_source_brace_compiled_ir_lower_bound(
765                pattern,
766                parse_options,
767                budget,
768                provenance_budget,
769            )?;
770            let expanded = expand_brace_alternatives_with_provenance(
771                pattern,
772                walker_source_provenance,
773                options.escape,
774                provenance_budget,
775            )?;
776            ensure_brace_compiled_ir_lower_bound(&expanded, parse_options, budget)?;
777            // Every expanded alternative compiles to exactly one, so the
778            // pattern keeps no spare capacity.
779            let mut alternatives = Vec::with_capacity(expanded.len());
780            for alternative in expanded {
781                // The path entry points ignore one leading `./` on every
782                // expanded alternative, not only at the start of the source,
783                // so the walker analysis reads each alternative's prefix the
784                // same way (#395).
785                let leading_dot_is_normalized =
786                    leading_dot_is_normalized || alternative.bytes.starts_with(b"./");
787                let compiled = Self::compile_within(
788                    &alternative.bytes,
789                    parse_options,
790                    budget,
791                    provenance_budget,
792                    alternative.source_provenance.as_ref(),
793                    leading_dot_is_normalized,
794                    bounds,
795                )?;
796                alternatives.extend(compiled.alternatives);
797            }
798            return Self::from_alternatives(alternatives, options, budget);
799        }
800        let mut tokens = Vec::new();
801        let mut literals = Vec::new();
802        // Keep the walker-only path shape beside the token build. In
803        // particular an escaped dot and an ordinary dot both become a literal
804        // matcher token, but only the latter is a path operation a walker
805        // must reject.
806        let mut walker_path = WalkerPathShapeBuilder::new(leading_dot_is_normalized);
807        let mut index = 0;
808        // Charged as the tokens appear rather than per byte: a literal run is
809        // one token however long it is, so billing bytes would reject patterns
810        // that compile to almost nothing.
811        let mut charged = 0;
812        // The star run `index` is in, classified once at its first star: only
813        // a whole-component run may emit the recursive tokens (#419).
814        let mut star_run_end = 0;
815        let mut star_run_is_recursive = false;
816
817        while index < pattern.len() {
818            budget.charge(tokens.len() - charged, 0)?;
819            charged = tokens.len();
820            if pattern[index] == b'*' && index >= star_run_end {
821                star_run_end = index
822                    + pattern[index..]
823                        .iter()
824                        .take_while(|&&byte| byte == b'*')
825                        .count();
826                star_run_is_recursive = options.recursive_double_star
827                    && star_run_is_whole_component(
828                        pattern,
829                        index,
830                        star_run_end,
831                        options.escape,
832                        bounds,
833                    );
834            }
835            match pattern[index] {
836                b'/' => {
837                    flush_literals(&mut tokens, &mut literals);
838                    tokens.push(Token::Separator);
839                    walker_path.separator(
840                        walker_source_provenance.and_then(|provenance| provenance.offset_at(index)),
841                    );
842                    index += 1;
843                }
844                b'*' if star_run_is_recursive && pattern.get(index + 1) == Some(&b'*') => {
845                    flush_literals(&mut tokens, &mut literals);
846                    if pattern.get(index + 2) == Some(&b'/') {
847                        tokens.push(Token::RecursivePrefix);
848                        walker_path.wildcard();
849                        walker_path.separator(
850                            walker_source_provenance
851                                .and_then(|provenance| provenance.offset_at(index + 2)),
852                        );
853                        index += 3;
854                    } else {
855                        tokens.push(Token::RecursiveStar);
856                        walker_path.wildcard();
857                        index += 2;
858                    }
859                }
860                b'*' => {
861                    flush_literals(&mut tokens, &mut literals);
862                    tokens.push(Token::Star);
863                    walker_path.wildcard();
864                    index += 1;
865                }
866                b'?' => {
867                    flush_literals(&mut tokens, &mut literals);
868                    tokens.push(Token::Any);
869                    walker_path.wildcard();
870                    index += 1;
871                }
872                b'[' => {
873                    flush_literals(&mut tokens, &mut literals);
874                    let (class, next) =
875                        parse_class(pattern, index, options.escape).map_err(|mut error| {
876                            if let Some(source_offset) = walker_source_provenance
877                                .and_then(|provenance| provenance.offset_at(error.offset))
878                            {
879                                error.offset = source_offset;
880                            }
881                            error
882                        })?;
883                    // A class token owns its member list, so it costs more than
884                    // the one unit the loop charges for the token itself.
885                    budget.charge(class.members.len(), 0)?;
886                    tokens.push(Token::Class(class));
887                    walker_path.wildcard();
888                    index = next;
889                }
890                b'\\' if options.escape => {
891                    if let Some(&escaped) = pattern.get(index + 1) {
892                        literals.push(escaped);
893                        walker_path.escaped();
894                        index += 2;
895                    } else {
896                        // zlob's fnmatch core treats a trailing backslash as
897                        // a literal backslash instead of rejecting the pattern.
898                        literals.push(b'\\');
899                        walker_path.escaped();
900                        index += 1;
901                    }
902                }
903                byte => {
904                    literals.push(byte);
905                    walker_path.literal(
906                        byte,
907                        walker_source_provenance.and_then(|provenance| provenance.offset_at(index)),
908                    );
909                    index += 1;
910                }
911            }
912        }
913        flush_literals(&mut tokens, &mut literals);
914        budget.charge(tokens.len() - charged, 0)?;
915
916        let extglob = compile_extglob(
917            pattern,
918            options,
919            budget,
920            provenance_budget,
921            walker_source_provenance,
922            leading_dot_is_normalized,
923        )?;
924        let fast_path = FastPath::compile(&tokens, options);
925        let sweep = compile_sweep(
926            &tokens,
927            &fast_path,
928            extglob.as_deref(),
929            options,
930            budget,
931            None,
932        )?;
933        let walker_path_shape = walker_path.finish();
934        Self::from_alternatives(
935            vec![CompiledAlternative {
936                extglob,
937                raw: pattern.to_vec(),
938                fast_path,
939                prefilter: Prefilter::compile(&tokens),
940                sweep,
941                tokens,
942                walker_path_shape,
943            }],
944            options,
945            budget,
946        )
947    }
948
949    /// Reports whether a pattern is syntactically valid without retaining it.
950    ///
951    /// # Errors
952    ///
953    /// Returns the same [`PatternError`] that [`Pattern::compile`] would.
954    pub fn validate(
955        pattern: impl AsRef<[u8]>,
956        options: PatternOptions,
957    ) -> Result<(), PatternError> {
958        Self::compile(pattern, options).map(|_| ())
959    }
960
961    /// Matches the entire candidate byte sequence. Ordinary wildcards may
962    /// cross separators, and a leading `./` is compared literally.
963    ///
964    /// Use it for text in which `/` is not special. For a filesystem path,
965    /// [`Self::is_match_glob_path`] is usually the entry point you want; the
966    /// same three candidates are shown on each of the three.
967    ///
968    /// ```
969    /// use ferralk_glob::{Pattern, PatternOptions};
970    ///
971    /// let pattern = Pattern::compile("*.rs", PatternOptions::walker())?;
972    ///
973    /// assert!(pattern.is_match("lib.rs"));
974    /// // `*` crosses the separator.
975    /// assert!(pattern.is_match("src/lib.rs"));
976    /// // `./` is compared literally, and `*` does not cover its leading period.
977    /// assert!(!pattern.is_match("./lib.rs"));
978    /// # Ok::<(), ferralk_glob::PatternError>(())
979    /// ```
980    ///
981    /// Extglob patterns that fall back to the retained interpreter can still
982    /// spend quadratic time on one adversarially long component. For
983    /// untrusted path-shaped input, prefer a component-scoped entry point
984    /// such as [`Self::is_match_path`] where that matches the caller's
985    /// semantics.
986    #[must_use]
987    pub fn is_match(&self, path: impl AsRef<[u8]>) -> bool {
988        let path = path.as_ref();
989        if let [alternative] = self.alternatives.as_slice()
990            && (!self.options.extglob || alternative.extglob.is_none())
991            && let Some(fast_path) = &alternative.fast_path
992        {
993            return fast_path.is_match(path, self.options);
994        }
995        if let Some(fast_path) = &self.alternative_fast_path {
996            return fast_path.is_match(path, self.options, &self.alternatives);
997        }
998        Self::match_alternatives(&self.alternatives, self.options, path)
999    }
1000
1001    /// Reports whether every match engine agrees on `path`.
1002    ///
1003    /// Hidden from documentation: it exists for the differential fuzz
1004    /// harness, which cannot strip the per-alternative engines from outside
1005    /// the crate. Each entry point is asked as compiled, with the fast paths
1006    /// stripped so the sweep engine answers, and with the sweep stripped too
1007    /// so the memoized matcher answers.
1008    #[doc(hidden)]
1009    #[cfg(any(test, feature = "unstable-test-hooks"))]
1010    #[must_use]
1011    pub fn engines_agree(&self, path: impl AsRef<[u8]>) -> bool {
1012        let path = path.as_ref();
1013        let answers = |pattern: &Self| {
1014            (
1015                pattern.is_match(path),
1016                pattern.is_match_path(path),
1017                pattern.is_match_glob_path(path),
1018            )
1019        };
1020        let mut sweep_only = self.clone();
1021        sweep_only.strip_engines(true, false, false);
1022        let mut memoized = self.clone();
1023        memoized.strip_engines(true, true, true);
1024        let compiled = answers(self);
1025        compiled == answers(&sweep_only) && compiled == answers(&memoized)
1026    }
1027
1028    /// Removes accelerated engines so a differential run can pin one engine.
1029    ///
1030    /// `prefilters` neutralizes the fixed-ends prefilter as well, which turns
1031    /// the stripped pattern into the bare memoized engine — the oracle the
1032    /// prefilter and the sweep are both held against.
1033    #[cfg(any(test, feature = "unstable-test-hooks"))]
1034    fn strip_engines(&mut self, fast_paths: bool, sweeps: bool, prefilters: bool) {
1035        if fast_paths {
1036            self.alternative_fast_path = None;
1037        }
1038        let options = self.options;
1039        for alternative in self.compiled_alternatives_mut() {
1040            alternative.strip_engines(options, fast_paths, sweeps, prefilters);
1041        }
1042    }
1043
1044    /// Every compiled alternative, including the list filter's component
1045    /// copies, so a differential run can rewrite all of them.
1046    #[cfg(any(test, feature = "unstable-test-hooks"))]
1047    fn compiled_alternatives_mut(&mut self) -> impl Iterator<Item = &mut CompiledAlternative> {
1048        self.alternatives.iter_mut().chain(
1049            self.path_filter
1050                .iter_mut()
1051                .flat_map(|filter| filter.compiled.iter_mut()),
1052        )
1053    }
1054
1055    /// Matches `path` against any of `alternatives` under `options`.
1056    ///
1057    /// Free of `self` so a compiled extglob alternative can be run through the
1058    /// same routing without owning a `Pattern`.
1059    fn match_alternatives(
1060        alternatives: &[CompiledAlternative],
1061        options: PatternOptions,
1062        path: &[u8],
1063    ) -> bool {
1064        alternatives.iter().any(|alternative| {
1065            if options.extglob
1066                && let Some(program) = &alternative.extglob
1067            {
1068                match_extglob_program(program, path, options)
1069            } else if let Some(fast_path) = &alternative.fast_path
1070                && (!options.component_wildcards || fast_path.supports_component_wildcards())
1071            {
1072                fast_path.is_match(path, options)
1073            } else {
1074                // Neither engine consults the pattern's fixed ends before it
1075                // walks the candidate, so a candidate that cannot possibly
1076                // match is rejected here rather than after the walk: the sweep
1077                // pays per byte and the memoized engine per visited state.
1078                !alternative
1079                    .prefilter
1080                    .rejects(path, options.case_insensitive)
1081                    && if let Some(sweep) = &alternative.sweep {
1082                        sweep.is_match(path, options)
1083                    } else {
1084                        Self::matches_general(&alternative.tokens, path, options)
1085                    }
1086            }
1087        })
1088    }
1089
1090    /// Runs the general matcher on the thread's reusable scratch buffers.
1091    ///
1092    /// The visited matrix and the work list are sized by the candidate, so
1093    /// allocating them per call dominated a walker filter: `**/*.ts` leaves the
1094    /// inline matrix budget for any relative path longer than about 40 bytes.
1095    /// Borrowing them from a thread-local keeps repeated calls allocation-free
1096    /// without putting state on [`Pattern`], which stays `Send + Sync`.
1097    fn matches_general(tokens: &[Token], path: &[u8], options: PatternOptions) -> bool {
1098        SCRATCH.with(|cell| match cell.try_borrow_mut() {
1099            Ok(mut scratch) => {
1100                let matched = Self::matches_with_scratch(tokens, path, options, &mut scratch);
1101                // One oversized candidate must not pin a large matrix on a
1102                // worker thread for the rest of the process.
1103                if scratch.visited.len() > RETAINED_SCRATCH_WORDS {
1104                    scratch.visited.truncate(RETAINED_SCRATCH_WORDS);
1105                    scratch.visited.shrink_to_fit();
1106                }
1107                matched
1108            }
1109            // A nested match on this thread cannot share the borrowed buffers.
1110            Err(_) => Self::matches_with_scratch(tokens, path, options, &mut Scratch::default()),
1111        })
1112    }
1113
1114    fn matches_with_scratch(
1115        tokens: &[Token],
1116        path: &[u8],
1117        options: PatternOptions,
1118        scratch: &mut Scratch,
1119    ) -> bool {
1120        // Generation 0 marks a stale entry, so a live match never uses it.
1121        if scratch.generation == u64::MAX {
1122            scratch.scans.clear();
1123            scratch.generation = 0;
1124        }
1125        scratch.generation += 1;
1126        let Scratch {
1127            visited,
1128            deferred,
1129            scans,
1130            generation,
1131        } = scratch;
1132        deferred.clear();
1133        if scans.len() < tokens.len() {
1134            scans.resize(tokens.len(), StarScans::default());
1135        }
1136        let mut failed = FailedStates::new(tokens, path, visited);
1137        let (skip_token, skip_literal) =
1138            skipping_star(tokens, options).unwrap_or((usize::MAX, &[]));
1139        let mut work = StarWork {
1140            scans,
1141            generation: *generation,
1142            skip_token,
1143            skip_literal,
1144        };
1145        // Two instantiations: patterns that cannot skip keep a matcher loop
1146        // with none of the skipping machinery in it.
1147        if skip_token == usize::MAX {
1148            Self::matches_from::<false>(tokens, path, options, &mut failed, deferred, &mut work)
1149        } else {
1150            Self::matches_from::<true>(tokens, path, options, &mut failed, deferred, &mut work)
1151        }
1152    }
1153
1154    /// Returns the input paths accepted by this compiled pattern, in input
1155    /// order. Wildcards after an explicit separator stay within that path
1156    /// component; recursive `**` is the separator-crossing form. The returned
1157    /// references borrow the caller-owned path list.
1158    ///
1159    /// Each path is asked through [`Self::is_match_path`], zlob's list rule,
1160    /// under which a wildcard in the first component crosses separators. For
1161    /// shell semantics, filter with [`Self::is_match_glob_path`] yourself:
1162    ///
1163    /// ```
1164    /// use ferralk_glob::{Pattern, PatternOptions};
1165    ///
1166    /// let paths = ["lib.rs", "src/lib.rs", "src/bin/main.rs", "README.md"];
1167    ///
1168    /// // A wildcard directly behind `/` stays in its component...
1169    /// let direct = Pattern::compile("src/*.rs", PatternOptions::walker())?;
1170    /// assert_eq!(direct.filter_paths(&paths), [&"src/lib.rs"]);
1171    ///
1172    /// // ...while one in the first component crosses separators.
1173    /// let top = Pattern::compile("*.rs", PatternOptions::walker())?;
1174    /// assert_eq!(
1175    ///     top.filter_paths(&paths),
1176    ///     [&"lib.rs", &"src/lib.rs", &"src/bin/main.rs"],
1177    /// );
1178    ///
1179    /// // Shell semantics: `*.rs` names the top level only.
1180    /// let shell: Vec<_> = paths
1181    ///     .iter()
1182    ///     .filter(|path| top.is_match_glob_path(path))
1183    ///     .collect();
1184    /// assert_eq!(shell, [&"lib.rs"]);
1185    /// # Ok::<(), ferralk_glob::PatternError>(())
1186    /// ```
1187    #[must_use]
1188    pub fn filter_paths<'a, T>(&self, paths: impl IntoIterator<Item = &'a T>) -> Vec<&'a T>
1189    where
1190        T: AsRef<[u8]> + ?Sized + 'a,
1191    {
1192        paths
1193            .into_iter()
1194            .filter(|path| self.is_match_path(path.as_ref()))
1195            .collect()
1196    }
1197
1198    /// Matches one root-relative path with the zlob list-filter policy used by
1199    /// [`Pattern::filter_paths`]. A root wildcard may cross separators, while
1200    /// wildcards after an explicit separator are component-local. One leading
1201    /// `./` is ignored on both the pattern and candidate.
1202    ///
1203    /// ```
1204    /// use ferralk_glob::{Pattern, PatternOptions};
1205    ///
1206    /// let pattern = Pattern::compile("*.rs", PatternOptions::walker())?;
1207    ///
1208    /// assert!(pattern.is_match_path("lib.rs"));
1209    /// // A wildcard in the first component crosses the separator.
1210    /// assert!(pattern.is_match_path("src/lib.rs"));
1211    /// // One leading `./` is ignored.
1212    /// assert!(pattern.is_match_path("./lib.rs"));
1213    ///
1214    /// // A wildcard directly behind `/` stays in its component.
1215    /// let nested = Pattern::compile("src/*.rs", PatternOptions::walker())?;
1216    /// assert!(!nested.is_match_path("src/bin/main.rs"));
1217    /// # Ok::<(), ferralk_glob::PatternError>(())
1218    /// ```
1219    #[must_use]
1220    pub fn is_match_path(&self, path: impl AsRef<[u8]>) -> bool {
1221        self.matches_path_filter(path.as_ref())
1222    }
1223
1224    /// Matches one root-relative filesystem-glob path. Every ordinary
1225    /// wildcard stays within its path component; recursive `**` remains the
1226    /// separator-crossing form. This is stricter than [`Pattern::is_match_path`]
1227    /// at the root component and is suitable for traversal filters. One
1228    /// leading `./` is ignored on both the pattern and candidate.
1229    ///
1230    /// This is how a shell and the `ferralk` walker read a pattern, and the
1231    /// entry point to use for a path relative to where the pattern is
1232    /// anchored. Pass a [`Path`](std::path::Path) as [`path_bytes`]`(path)`.
1233    ///
1234    /// ```
1235    /// use ferralk_glob::{Pattern, PatternOptions};
1236    ///
1237    /// let pattern = Pattern::compile("*.rs", PatternOptions::walker())?;
1238    ///
1239    /// assert!(pattern.is_match_glob_path("lib.rs"));
1240    /// // `*` stays in one component; write `**/*.rs` for every depth.
1241    /// assert!(!pattern.is_match_glob_path("src/lib.rs"));
1242    /// // One leading `./` is ignored.
1243    /// assert!(pattern.is_match_glob_path("./lib.rs"));
1244    ///
1245    /// let recursive = Pattern::compile("**/*.rs", PatternOptions::walker())?;
1246    /// assert!(recursive.is_match_glob_path("lib.rs"));
1247    /// assert!(recursive.is_match_glob_path("src/lib.rs"));
1248    /// # Ok::<(), ferralk_glob::PatternError>(())
1249    /// ```
1250    #[must_use]
1251    pub fn is_match_glob_path(&self, path: impl AsRef<[u8]>) -> bool {
1252        let path = without_leading_dot_slash(path.as_ref());
1253        let options = PatternOptions {
1254            component_wildcards: true,
1255            root_component_wildcards: true,
1256            ..self.options
1257        };
1258        if self
1259            .alternatives
1260            .iter()
1261            .any(|alternative| alternative.raw.starts_with(b"./"))
1262            && let Some(filter) = &self.path_filter
1263        {
1264            // The `./` alternatives need their stripped copies; every other
1265            // route answers under this policy exactly as below.
1266            return filter.is_match(&self.alternatives, options, options, path);
1267        }
1268        if let [alternative] = self.alternatives.as_slice()
1269            && (!options.extglob || alternative.extglob.is_none())
1270            && let Some(fast_path) = &alternative.fast_path
1271            && fast_path.supports_component_wildcards()
1272        {
1273            return fast_path.is_match(path, options);
1274        }
1275        if let Some(fast_path) = &self.alternative_fast_path {
1276            return fast_path.is_match(path, options, &self.alternatives);
1277        }
1278        Self::match_alternatives(&self.alternatives, options, path)
1279    }
1280
1281    /// Matches one root-relative path with the separator-crossing wildcards of
1282    /// [`Pattern::is_match`], ignoring one leading `./` on the candidate and on
1283    /// every brace-expanded alternative exactly as [`Pattern::is_match_path`]
1284    /// and [`Pattern::is_match_glob_path`] do.
1285    ///
1286    /// Hidden: this is the separator-crossing walk's reading of an include or
1287    /// exclude, which needs the path entry points' `./` rule without their
1288    /// component-local wildcards.
1289    #[doc(hidden)]
1290    #[must_use]
1291    pub fn is_match_crossing_path(&self, path: impl AsRef<[u8]>) -> bool {
1292        let path = without_leading_dot_slash(path.as_ref());
1293        // The copies exist whenever an alternative has the prefix, so asking
1294        // for them first spares most patterns the scan over alternatives.
1295        if let Some(filter) = &self.path_filter
1296            && self
1297                .alternatives
1298                .iter()
1299                .any(|alternative| alternative.raw.starts_with(b"./"))
1300        {
1301            // The path-filter copies differ from the alternatives only by the
1302            // removed prefix. Their fast paths are the literal and
1303            // fixed-width shapes, which read the same under either wildcard
1304            // reach, and every other engine takes the reach from `options`.
1305            // Every `./` alternative has a copy; the others answer from the
1306            // shared compile.
1307            return filter.is_match(&self.alternatives, self.options, self.options, path);
1308        }
1309        self.is_match(path)
1310    }
1311
1312    /// Returns the input paths accepted relative to `base_path`, preserving
1313    /// the original full paths and caller order. Candidates outside the base
1314    /// directory are ignored.
1315    ///
1316    /// ```
1317    /// use ferralk_glob::{Pattern, PatternOptions};
1318    ///
1319    /// let pattern = Pattern::compile("src/*.rs", PatternOptions::walker())?;
1320    /// let paths = [
1321    ///     "crates/app/src/lib.rs",
1322    ///     "crates/app/README.md",
1323    ///     "crates/cli/src/main.rs",
1324    ///     "crates/application/src/lib.rs",
1325    /// ];
1326    ///
1327    /// // Each path is matched below the base and returned in full; a path
1328    /// // outside it, `crates/application` included, is left out.
1329    /// assert_eq!(
1330    ///     pattern.filter_paths_at("crates/app", &paths),
1331    ///     [&"crates/app/src/lib.rs"],
1332    /// );
1333    /// # Ok::<(), ferralk_glob::PatternError>(())
1334    /// ```
1335    #[must_use]
1336    pub fn filter_paths_at<'a, T>(
1337        &self,
1338        base_path: impl AsRef<[u8]>,
1339        paths: impl IntoIterator<Item = &'a T>,
1340    ) -> Vec<&'a T>
1341    where
1342        T: AsRef<[u8]> + ?Sized + 'a,
1343    {
1344        let base_path = base_path.as_ref();
1345        paths
1346            .into_iter()
1347            .filter(|path| {
1348                path_after_base(base_path, path.as_ref())
1349                    .is_some_and(|relative| self.is_match_path(relative))
1350            })
1351            .collect()
1352    }
1353
1354    /// Returns the indices of input paths accepted by this compiled pattern,
1355    /// in their original input order.
1356    ///
1357    /// Useful when the paths live beside other data, such as a list of
1358    /// records, and the caller wants positions rather than borrowed paths.
1359    ///
1360    /// ```
1361    /// use ferralk_glob::{Pattern, PatternOptions};
1362    ///
1363    /// let pattern = Pattern::compile("src/**/*.rs", PatternOptions::walker())?;
1364    /// let files = ["README.md", "src/lib.rs", "src/bin/main.rs"].map(String::from);
1365    ///
1366    /// assert_eq!(pattern.filter_path_indices(&files), [1, 2]);
1367    /// # Ok::<(), ferralk_glob::PatternError>(())
1368    /// ```
1369    #[must_use]
1370    pub fn filter_path_indices<'a, T>(&self, paths: impl IntoIterator<Item = &'a T>) -> Vec<usize>
1371    where
1372        T: AsRef<[u8]> + ?Sized + 'a,
1373    {
1374        paths
1375            .into_iter()
1376            .enumerate()
1377            .filter_map(|(index, path)| self.is_match_path(path.as_ref()).then_some(index))
1378            .collect()
1379    }
1380
1381    /// Returns the indices of full input paths accepted relative to
1382    /// `base_path`, in their original input order.
1383    ///
1384    /// ```
1385    /// use ferralk_glob::{Pattern, PatternOptions};
1386    ///
1387    /// let pattern = Pattern::compile("*.toml", PatternOptions::walker())?;
1388    /// let paths = ["crates/app/Cargo.toml", "Cargo.toml", "crates/app/src/x.toml"];
1389    ///
1390    /// // `Cargo.toml` is outside the base. `src/x.toml` is selected because a
1391    /// // wildcard in the first component crosses separators under the list
1392    /// // rule of `filter_paths`.
1393    /// assert_eq!(pattern.filter_path_indices_at("crates/app", &paths), [0, 2]);
1394    /// # Ok::<(), ferralk_glob::PatternError>(())
1395    /// ```
1396    #[must_use]
1397    pub fn filter_path_indices_at<'a, T>(
1398        &self,
1399        base_path: impl AsRef<[u8]>,
1400        paths: impl IntoIterator<Item = &'a T>,
1401    ) -> Vec<usize>
1402    where
1403        T: AsRef<[u8]> + ?Sized + 'a,
1404    {
1405        let base_path = base_path.as_ref();
1406        paths
1407            .into_iter()
1408            .enumerate()
1409            .filter_map(|(index, path)| {
1410                path_after_base(base_path, path.as_ref())
1411                    .is_some_and(|relative| self.is_match_path(relative))
1412                    .then_some(index)
1413            })
1414            .collect()
1415    }
1416
1417    fn matches_path_filter(&self, path: &[u8]) -> bool {
1418        let path = without_leading_dot_slash(path);
1419        let Some(filter) = &self.path_filter else {
1420            return self.is_match(path);
1421        };
1422        let component = PatternOptions {
1423            component_wildcards: true,
1424            ..self.options
1425        };
1426        filter.is_match(&self.alternatives, component, self.options, path)
1427    }
1428
1429    fn from_alternatives(
1430        alternatives: Vec<CompiledAlternative>,
1431        options: PatternOptions,
1432        budget: &mut IrBudget,
1433    ) -> Result<Self, PatternError> {
1434        let alternative_fast_path =
1435            AlternativeFastPath::compile(&alternatives, options, budget)?.map(Box::new);
1436        let path_filter = PathFilter::compile(&alternatives, options, budget)?.map(Box::new);
1437        Ok(Self {
1438            alternatives,
1439            alternative_fast_path,
1440            path_filter,
1441            walker_path_viability: WalkerPathViability::Viable,
1442            walker_path_problem_offset: None,
1443            options,
1444        })
1445    }
1446
1447    fn compile_path_filter_alternative(
1448        alternative: &CompiledAlternative,
1449        options: PatternOptions,
1450        budget: &mut IrBudget,
1451    ) -> Result<CompiledAlternative, PatternError> {
1452        let leading_dot_slash = alternative.raw.starts_with(b"./")
1453            && matches!(alternative.tokens.as_slice(), [Token::Literal(dot), Token::Separator, ..] if dot == b".");
1454        let raw = if leading_dot_slash {
1455            alternative.raw[2..].to_vec()
1456        } else {
1457            alternative.raw.clone()
1458        };
1459        let source_tokens = if leading_dot_slash {
1460            &alternative.tokens[2..]
1461        } else {
1462            &alternative.tokens
1463        };
1464        let tokens = source_tokens.to_vec();
1465        let fast_path = FastPath::compile(
1466            &tokens,
1467            PatternOptions {
1468                component_wildcards: true,
1469                ..options
1470            },
1471        );
1472        budget.charge(tokens.len(), 0)?;
1473        let mut provenance_budget = ProvenanceBudget::new();
1474        let extglob = compile_extglob(&raw, options, budget, &mut provenance_budget, None, false)?;
1475        // Without a removed `./` the copy has its original's tokens, and the
1476        // same tokens under the same options build the same engine.
1477        let built = alternative.sweep.as_ref().filter(|_| !leading_dot_slash);
1478        let sweep = compile_sweep(
1479            &tokens,
1480            &fast_path,
1481            extglob.as_deref(),
1482            options,
1483            budget,
1484            built,
1485        )?;
1486        Ok(CompiledAlternative {
1487            extglob,
1488            raw,
1489            fast_path,
1490            prefilter: Prefilter::compile(&tokens),
1491            sweep,
1492            tokens,
1493            walker_path_shape: alternative.walker_path_shape.clone(),
1494        })
1495    }
1496
1497    /// Explores the token/path state graph without native recursion.
1498    ///
1499    /// A star is the only token with two successors, and it is the one that
1500    /// used to recurse once per consumed path byte, so a long candidate
1501    /// overflowed the native stack. Its repetition branch is now deferred to an
1502    /// explicit work list while the "stop consuming here" branch continues in
1503    /// place. Depth-first order is unchanged, because a deferred entry is
1504    /// popped exactly when the branch that was taken instead is exhausted.
1505    ///
1506    /// The work list stays small: a star's repetition branch keeps its token
1507    /// index while its other successor advances it, so the deferred entries
1508    /// hold strictly increasing token indices and never exceed one per token.
1509    /// `FailedStates` still bounds the explored state space to
1510    /// `tokens × path` visits.
1511    fn matches_from<const SKIP: bool>(
1512        tokens: &[Token],
1513        path: &[u8],
1514        options: PatternOptions,
1515        failed: &mut FailedStates<'_>,
1516        deferred: &mut Vec<(usize, usize)>,
1517        work: &mut StarWork<'_>,
1518    ) -> bool {
1519        let mut state = (0_usize, 0_usize);
1520
1521        loop {
1522            let (token_index, path_index) = state;
1523            let advanced = if token_index == tokens.len() {
1524                if path_index == path.len() {
1525                    return true;
1526                }
1527                None
1528            } else if !failed.insert(token_index, path_index) {
1529                None
1530            } else {
1531                match &tokens[token_index] {
1532                    Token::Literal(literal) => {
1533                        Self::advance_literal(token_index, path_index, path, options, literal)
1534                    }
1535                    Token::Separator => {
1536                        if path.get(path_index).is_some_and(|byte| is_separator(*byte)) {
1537                            Some((token_index + 1, path_index + 1))
1538                        } else if path_index == path.len()
1539                            && token_index + 2 == tokens.len()
1540                            && matches!(tokens.get(token_index + 1), Some(Token::RecursiveStar))
1541                        {
1542                            return true;
1543                        } else {
1544                            None
1545                        }
1546                    }
1547                    Token::Any => {
1548                        Self::advance_one(tokens, token_index, path_index, path, options, |_| true)
1549                    }
1550                    Token::Class(class) => {
1551                        Self::advance_one(tokens, token_index, path_index, path, options, |byte| {
1552                            class.matches(byte, options.case_insensitive)
1553                        })
1554                    }
1555                    Token::Star => Self::advance_star::<SKIP>(
1556                        token_index,
1557                        path_index,
1558                        path,
1559                        options,
1560                        StarSemantics::ordinary(!Self::component_wildcard(
1561                            tokens,
1562                            token_index,
1563                            options,
1564                        )),
1565                        deferred,
1566                        work,
1567                    ),
1568                    Token::RecursiveStar => Self::advance_star::<SKIP>(
1569                        token_index,
1570                        path_index,
1571                        path,
1572                        options,
1573                        StarSemantics::ordinary(true),
1574                        deferred,
1575                        work,
1576                    ),
1577                    Token::RecursivePrefix => Self::advance_star::<SKIP>(
1578                        token_index,
1579                        path_index,
1580                        path,
1581                        options,
1582                        StarSemantics::RECURSIVE_PREFIX,
1583                        deferred,
1584                        work,
1585                    ),
1586                }
1587            };
1588
1589            debug_assert!(
1590                deferred.len() <= tokens.len(),
1591                "the work list holds at most one deferred repetition per token"
1592            );
1593            match advanced.or_else(|| deferred.pop()) {
1594                Some(next) => state = next,
1595                None => return false,
1596            }
1597        }
1598    }
1599
1600    fn component_wildcard(tokens: &[Token], token_index: usize, options: PatternOptions) -> bool {
1601        options.component_wildcards
1602            && (options.root_component_wildcards
1603                || token_index == 0 && options.candidate_root_component_wildcard
1604                || token_index > 0 && matches!(tokens[token_index - 1], Token::Separator))
1605    }
1606
1607    /// Returns the state after a literal token, or `None` if it does not match.
1608    fn advance_literal(
1609        token_index: usize,
1610        path_index: usize,
1611        path: &[u8],
1612        options: PatternOptions,
1613        literal: &[u8],
1614    ) -> Option<(usize, usize)> {
1615        let candidate = path.get(path_index..path_index + literal.len())?;
1616        literal
1617            .iter()
1618            .zip(candidate)
1619            .all(|(&expected, &actual)| bytes_equal(expected, actual, options.case_insensitive))
1620            .then_some((token_index + 1, path_index + literal.len()))
1621    }
1622
1623    /// Returns the state after a single-byte token, or `None` if the candidate
1624    /// byte is rejected by the token, the component policy, or the leading-dot
1625    /// policy.
1626    fn advance_one(
1627        tokens: &[Token],
1628        token_index: usize,
1629        path_index: usize,
1630        path: &[u8],
1631        options: PatternOptions,
1632        accepts: impl FnOnce(u8) -> bool,
1633    ) -> Option<(usize, usize)> {
1634        let &byte = path.get(path_index)?;
1635        (accepts(byte)
1636            && (!Self::component_wildcard(tokens, token_index, options) || !is_separator(byte))
1637            && (options.match_hidden
1638                || byte != b'.'
1639                || !at_component_start(path, path_index, options)))
1640        .then_some((token_index + 1, path_index + 1))
1641    }
1642
1643    /// Queues the star's repetition branch and returns the branch that stops
1644    /// consuming here, which the caller explores first. An ordinary star may
1645    /// not stop immediately before a component-leading period: doing so would
1646    /// let a following literal opt into a hidden name implicitly. The
1647    /// syntactic `**/` prefix is exempt because it explicitly advances to the
1648    /// next component without consuming one; in exchange it may stop only at
1649    /// a component start.
1650    fn advance_star<const SKIP: bool>(
1651        token_index: usize,
1652        path_index: usize,
1653        path: &[u8],
1654        options: PatternOptions,
1655        semantics: StarSemantics,
1656        deferred: &mut Vec<(usize, usize)>,
1657        work: &mut StarWork<'_>,
1658    ) -> Option<(usize, usize)> {
1659        if let Some(next) = Self::next_star_position::<SKIP>(
1660            token_index,
1661            path_index,
1662            path,
1663            options,
1664            semantics.recursive,
1665            work,
1666        ) {
1667            deferred.push((token_index, next));
1668        }
1669        let hidden_stop_allowed = !semantics.blocks_hidden_stop
1670            || options.match_hidden
1671            || path.get(path_index) != Some(&b'.')
1672            || !at_component_start(path, path_index, options);
1673        let component_stop_allowed =
1674            !semantics.stops_at_component_start || at_component_start(path, path_index, options);
1675        (hidden_stop_allowed && component_stop_allowed).then_some((token_index + 1, path_index))
1676    }
1677
1678    /// Where the star should resume consuming, or `None` when it cannot.
1679    ///
1680    /// A star followed by a literal only has to stop where that literal could
1681    /// begin, so the repetition jumps straight there instead of walking one
1682    /// byte at a time (ADR-0008). Positions in between can only fail: their
1683    /// sole other successor is the literal, which does not start there.
1684    ///
1685    /// The jump never crosses a byte the star may not consume, because it is
1686    /// clamped to [`star_barrier`]. Both the barrier and the literal search are
1687    /// monotone in `path_index` and cached as such, so a star re-entered at
1688    /// many positions still scans each byte a bounded number of times.
1689    ///
1690    /// Case-insensitive matching keeps the byte-wise walk: `memmem` has no
1691    /// folded form, and folding the candidate would mean allocating.
1692    #[inline]
1693    fn next_star_position<const SKIP: bool>(
1694        token_index: usize,
1695        path_index: usize,
1696        path: &[u8],
1697        options: PatternOptions,
1698        recursive: bool,
1699        work: &mut StarWork<'_>,
1700    ) -> Option<usize> {
1701        if SKIP && work.skip_token == token_index {
1702            return Self::skip_to_literal(token_index, path_index, path, options, recursive, work);
1703        }
1704
1705        let &byte = path.get(path_index)?;
1706        star_consumes_byte(path, path_index, byte, options, recursive).then_some(path_index + 1)
1707    }
1708
1709    /// The skipping half of [`Self::next_star_position`], kept out of line so
1710    /// the byte-wise walk stays a handful of instructions in the matcher loop.
1711    #[inline(never)]
1712    fn skip_to_literal(
1713        token_index: usize,
1714        path_index: usize,
1715        path: &[u8],
1716        options: PatternOptions,
1717        recursive: bool,
1718        work: &mut StarWork<'_>,
1719    ) -> Option<usize> {
1720        let literal = work.skip_literal;
1721        let scans = work.scans(token_index);
1722        if scans.stalled >= STALLED_SKIPS {
1723            let &byte = path.get(path_index)?;
1724            return star_consumes_byte(path, path_index, byte, options, recursive)
1725                .then_some(path_index + 1);
1726        }
1727        if let Some(cached) = scans.skip.get(path_index) {
1728            return cached;
1729        }
1730        let barrier = star_barrier(path, path_index, options, recursive, scans);
1731        let start = path_index + 1;
1732        if start > barrier {
1733            return scans.skip.record(path_index, path_index, None);
1734        }
1735        match next_literal_start(path, start, literal, &mut scans.literal) {
1736            // Nothing left to aim for, whatever the star consumes.
1737            None => scans.skip.record(path_index, usize::MAX, None),
1738            // The occurrence is out of reach, and stays out of reach for every
1739            // position up to the barrier.
1740            Some(found) if found > barrier => scans.skip.record(path_index, barrier, None),
1741            Some(found) => {
1742                scans.stalled = if found == start {
1743                    scans.stalled.saturating_add(1)
1744                } else {
1745                    0
1746                };
1747                scans.skip.record(path_index, found - 1, Some(found))
1748            }
1749        }
1750    }
1751}
1752
1753/// Visited token/path pairs for one matcher invocation.
1754///
1755/// The state space is dense by construction: token and path indices are both
1756/// bounded by the input slices. Small matrices live directly in a matcher
1757/// frame; larger inputs retain the flat heap matrix. Both avoid hashing while
1758/// keeping the original backtracking semantics.
1759///
1760/// A visited pair is never cleared. The recursive matcher used to clear one on
1761/// the way out of a successful frame, which was unobservable: a match returned
1762/// straight through every caller, and each invocation builds its own matrix.
1763enum FailedStates<'scratch> {
1764    Inline {
1765        width: usize,
1766        states: u128,
1767    },
1768    Heap {
1769        width: usize,
1770        states: &'scratch mut [u64],
1771    },
1772}
1773
1774impl<'scratch> FailedStates<'scratch> {
1775    fn new(tokens: &[Token], path: &[u8], scratch: &'scratch mut Vec<u64>) -> Self {
1776        let width = path.len() + 1;
1777        let state_count = tokens.len().saturating_mul(width);
1778        if state_count <= u128::BITS as usize {
1779            return Self::Inline { width, states: 0 };
1780        }
1781        let words = state_count.div_ceil(u64::BITS as usize);
1782        if scratch.len() < words {
1783            scratch.resize(words, 0);
1784        }
1785        let states = &mut scratch[..words];
1786        states.fill(0);
1787        Self::Heap { width, states }
1788    }
1789
1790    fn insert(&mut self, token_index: usize, path_index: usize) -> bool {
1791        match self {
1792            Self::Inline { width, states } => {
1793                let mask = 1_u128 << (token_index * *width + path_index);
1794                if *states & mask != 0 {
1795                    false
1796                } else {
1797                    *states |= mask;
1798                    true
1799                }
1800            }
1801            Self::Heap { width, states } => {
1802                let state = token_index * *width + path_index;
1803                let word = &mut states[state / u64::BITS as usize];
1804                let mask = 1_u64 << (state % u64::BITS as usize);
1805                if *word & mask != 0 {
1806                    false
1807                } else {
1808                    *word |= mask;
1809                    true
1810                }
1811            }
1812        }
1813    }
1814}
1815
1816/// Reusable matcher buffers for one thread.
1817///
1818/// Every field is sized by the candidate or the pattern, so building them per
1819/// call was the dominant cost of a walker filter. They are cleared, never
1820/// reallocated, once a thread has seen its first long candidate.
1821#[derive(Default)]
1822struct Scratch {
1823    visited: Vec<u64>,
1824    deferred: Vec<(usize, usize)>,
1825    scans: Vec<StarScans>,
1826    generation: u64,
1827}
1828
1829/// The literal-skipping bookkeeping for one match, borrowed out of [`Scratch`].
1830struct StarWork<'scratch> {
1831    /// Cached scans, one entry per token. Entries from an earlier match are
1832    /// recognized by their generation and reset when first used, so starting a
1833    /// match costs nothing per token.
1834    scans: &'scratch mut [StarScans],
1835    generation: u64,
1836    /// Token index of the one star that skips, or `usize::MAX` for none.
1837    /// Resolved once per match so the matcher loop only loads and compares a
1838    /// single word per star state. See [`skipping_star`].
1839    skip_token: usize,
1840    /// The literal that star aims at.
1841    skip_literal: &'scratch [u8],
1842}
1843
1844#[derive(Clone, Copy)]
1845struct StarSemantics {
1846    recursive: bool,
1847    blocks_hidden_stop: bool,
1848    /// The star may stop only where a path component starts. A `**/` prefix
1849    /// consumes whole components with their separators, so handing over in
1850    /// the middle of one would let `**/x` match `sx` (#419).
1851    stops_at_component_start: bool,
1852}
1853
1854impl StarSemantics {
1855    const RECURSIVE_PREFIX: Self = Self {
1856        recursive: true,
1857        blocks_hidden_stop: false,
1858        stops_at_component_start: true,
1859    };
1860
1861    const fn ordinary(recursive: bool) -> Self {
1862        Self {
1863            recursive,
1864            blocks_hidden_stop: true,
1865            stops_at_component_start: false,
1866        }
1867    }
1868}
1869
1870impl StarWork<'_> {
1871    fn scans(&mut self, token_index: usize) -> &mut StarScans {
1872        let scans = &mut self.scans[token_index];
1873        if scans.generation != self.generation {
1874            *scans = StarScans {
1875                generation: self.generation,
1876                ..StarScans::default()
1877            };
1878        }
1879        scans
1880    }
1881}
1882
1883/// Visited-matrix words a thread keeps between calls. 8 Ki words is 64 KiB and
1884/// holds 512 Ki token/path states — past any realistic path — while stopping
1885/// one huge candidate from pinning memory on a worker thread for good.
1886const RETAINED_SCRATCH_WORDS: usize = 8 * 1024;
1887
1888thread_local! {
1889    static SCRATCH: RefCell<Scratch> = const {
1890        RefCell::new(Scratch {
1891            visited: Vec::new(),
1892            deferred: Vec::new(),
1893            scans: Vec::new(),
1894            generation: 0,
1895        })
1896    };
1897}
1898
1899/// Capacities the calling thread's scratch currently holds.
1900///
1901/// Used by the steady-state test to show that repeated matches reuse the
1902/// buffers instead of reallocating them.
1903#[cfg(test)]
1904fn scratch_capacities() -> (usize, usize, usize) {
1905    SCRATCH.with(|cell| {
1906        let scratch = cell.borrow();
1907        (
1908            scratch.visited.capacity(),
1909            scratch.deferred.capacity(),
1910            scratch.scans.capacity(),
1911        )
1912    })
1913}
1914
1915/// The forward scans one star token needs, each cached on its own.
1916///
1917/// Every entry is private to a token, so the spans a token rescans are
1918/// disjoint and its total scanning stays linear in the candidate however often
1919/// an enclosing `**` re-enters it. `skip` caches the combined answer, which is
1920/// what an enclosing `**` hits on almost every position.
1921///
1922/// `generation` is the match that filled the entry; anything older is stale.
1923#[derive(Clone, Copy, Default)]
1924struct StarScans {
1925    generation: u64,
1926    skip: SkipDecision,
1927    separator: FirstAtOrAfter,
1928    component_dot: FirstAtOrAfter,
1929    literal: FirstAtOrAfter,
1930    /// Consecutive jumps that landed on the very next byte; see
1931    /// [`STALLED_SKIPS`].
1932    stalled: u8,
1933}
1934
1935/// How many single-byte jumps in a row turn skipping off for a token.
1936///
1937/// A candidate dense in the literal — `a*a*a*b` over a run of `a`s — has an
1938/// occurrence at every position, so the search finds only the next byte and the
1939/// scan is pure overhead on top of the step it replaces. Falling back to the
1940/// byte-wise walk there costs nothing in correctness: both answers name the
1941/// same state, and the walk is what the visited matrix shares anyway.
1942const STALLED_SKIPS: u8 = 4;
1943
1944/// Cached resume decision for one star token, valid for `from..=upto`.
1945///
1946/// Both inputs to the decision — the consumption barrier and the next literal
1947/// start — are monotone in the path index, so one answer covers the whole span
1948/// up to the position that changes it.
1949#[derive(Clone, Copy)]
1950struct SkipDecision {
1951    from: usize,
1952    upto: usize,
1953    /// Where the repetition resumes, or `usize::MAX` for "it cannot".
1954    jump: usize,
1955}
1956
1957impl SkipDecision {
1958    fn get(self, index: usize) -> Option<Option<usize>> {
1959        (self.from <= index && index <= self.upto)
1960            .then(|| (self.jump != usize::MAX).then_some(self.jump))
1961    }
1962
1963    fn record(&mut self, from: usize, upto: usize, jump: Option<usize>) -> Option<usize> {
1964        *self = Self {
1965            from,
1966            upto,
1967            jump: jump.unwrap_or(usize::MAX),
1968        };
1969        jump
1970    }
1971}
1972
1973impl Default for SkipDecision {
1974    fn default() -> Self {
1975        Self {
1976            from: usize::MAX,
1977            upto: 0,
1978            jump: usize::MAX,
1979        }
1980    }
1981}
1982
1983/// Cached answer of a monotone "first index at or after `from`" question.
1984///
1985/// The answer for `from` is also the answer for every index up to it, so one
1986/// entry serves the whole span it covers and a later miss starts beyond it.
1987/// Successive misses therefore scan disjoint parts of the candidate.
1988#[derive(Clone, Copy)]
1989struct FirstAtOrAfter {
1990    from: usize,
1991    answer: usize,
1992}
1993
1994impl FirstAtOrAfter {
1995    /// A cache that answers nothing.
1996    const EMPTY: Self = Self {
1997        from: usize::MAX,
1998        answer: 0,
1999    };
2000
2001    fn get(self, index: usize) -> Option<usize> {
2002        (self.from <= index && index <= self.answer).then_some(self.answer)
2003    }
2004
2005    fn record(&mut self, index: usize, answer: usize) -> usize {
2006        *self = Self {
2007            from: index,
2008            answer,
2009        };
2010        answer
2011    }
2012}
2013
2014impl Default for FirstAtOrAfter {
2015    fn default() -> Self {
2016        Self::EMPTY
2017    }
2018}
2019
2020/// The star whose repetition is worth skipping, and the literal it aims at.
2021///
2022/// A star is entered once per position its prefix can end at. Only the first
2023/// star has a fixed-width prefix; every later one is re-entered at as many
2024/// positions as the earlier stars can reach, and there the byte-wise steps are
2025/// already shared through the visited matrix — a scan per entry would cost more
2026/// than the state visits it removes. So skipping is applied to the first star
2027/// only, where one scan replaces a walk over the whole candidate.
2028///
2029/// Case folding keeps the byte-wise walk everywhere: `memmem` has no folded
2030/// form, and folding the candidate would mean allocating.
2031fn skipping_star(tokens: &[Token], options: PatternOptions) -> Option<(usize, &[u8])> {
2032    if options.case_insensitive {
2033        return None;
2034    }
2035    let index = tokens.iter().position(|token| {
2036        matches!(
2037            token,
2038            Token::Star | Token::RecursiveStar | Token::RecursivePrefix
2039        )
2040    })?;
2041    match tokens.get(index + 1) {
2042        Some(Token::Literal(literal)) if !literal.is_empty() => Some((index, literal)),
2043        _ => None,
2044    }
2045}
2046
2047/// Whether a star may consume the byte at `path_index`.
2048fn star_consumes_byte(
2049    path: &[u8],
2050    path_index: usize,
2051    byte: u8,
2052    options: PatternOptions,
2053    recursive: bool,
2054) -> bool {
2055    (recursive || !options.component_wildcards || !is_separator(byte))
2056        && (options.match_hidden || byte != b'.' || !at_component_start(path, path_index, options))
2057}
2058
2059/// First index at or after `path_index` that a star may not consume.
2060///
2061/// A component-local star stops at the next separator; unless hidden entries
2062/// match, every star stops at the leading dot of a component. This is the span
2063/// form of [`star_consumes_byte`], answered with `memchr`/`memmem` so a skipped
2064/// run costs one vectorised pass instead of one state visit per byte.
2065fn star_barrier(
2066    path: &[u8],
2067    path_index: usize,
2068    options: PatternOptions,
2069    recursive: bool,
2070    scans: &mut StarScans,
2071) -> usize {
2072    let mut barrier = path.len();
2073    if !recursive && options.component_wildcards {
2074        barrier = barrier.min(next_separator_from(path, path_index, &mut scans.separator));
2075    }
2076    if !options.match_hidden {
2077        barrier = barrier.min(next_component_dot(
2078            path,
2079            path_index,
2080            options.candidate_starts_component,
2081            &mut scans.component_dot,
2082        ));
2083    }
2084    barrier
2085}
2086
2087/// First index at or after `index` where `literal` starts.
2088fn next_literal_start(
2089    path: &[u8],
2090    index: usize,
2091    literal: &[u8],
2092    cache: &mut FirstAtOrAfter,
2093) -> Option<usize> {
2094    let found = match cache.get(index) {
2095        Some(found) => found,
2096        None => {
2097            let found = path
2098                .get(index..)
2099                .and_then(|tail| find_literal(tail, literal))
2100                .map_or(usize::MAX, |offset| index + offset);
2101            cache.record(index, found)
2102        }
2103    };
2104    (found != usize::MAX).then_some(found)
2105}
2106
2107/// Offset of `needle` in `haystack`.
2108///
2109/// `memmem` builds a searcher per call, which costs more than the whole scan
2110/// for the short candidates a walker filter sees. Below the floor a plain
2111/// `memchr` on the first byte plus a tail comparison is setup-free; above it
2112/// the vectorised two-byte search wins.
2113fn find_literal(haystack: &[u8], needle: &[u8]) -> Option<usize> {
2114    const MEMMEM_HAYSTACK_FLOOR: usize = 256;
2115    const SCALAR_HAYSTACK_CEILING: usize = 32;
2116    let (&first, rest) = needle.split_first()?;
2117    if haystack.len() >= MEMMEM_HAYSTACK_FLOOR {
2118        return memmem::find(haystack, needle);
2119    }
2120    if haystack.len() <= SCALAR_HAYSTACK_CEILING {
2121        // A path component is shorter than one SIMD block, so even `memchr`'s
2122        // entry sequence outweighs the comparisons it saves.
2123        return (0..haystack.len())
2124            .find(|&start| haystack[start] == first && haystack[start + 1..].starts_with(rest));
2125    }
2126    let mut offset = 0;
2127    while let Some(hit) = memchr(first, &haystack[offset..]) {
2128        let start = offset + hit;
2129        if haystack[start + 1..].starts_with(rest) {
2130            return Some(start);
2131        }
2132        offset = start + 1;
2133    }
2134    None
2135}
2136
2137/// First separator at or after `index`, or the candidate's length.
2138fn next_separator_from(path: &[u8], index: usize, cache: &mut FirstAtOrAfter) -> usize {
2139    if let Some(found) = cache.get(index) {
2140        return found;
2141    }
2142    let found = path
2143        .get(index..)
2144        .and_then(next_separator)
2145        .map_or(path.len(), |offset| index + offset);
2146    cache.record(index, found)
2147}
2148
2149/// Offset of the first path separator in `bytes`, using the platform's set.
2150fn next_separator(bytes: &[u8]) -> Option<usize> {
2151    if cfg!(windows) {
2152        memchr2(b'/', b'\\', bytes)
2153    } else {
2154        memchr(b'/', bytes)
2155    }
2156}
2157
2158/// First index at or after `index` that starts a component with a dot, or the
2159/// candidate's length.
2160///
2161/// Past index 0 a component starts exactly after a separator, so the blocked
2162/// positions are the dots that directly follow one.
2163fn next_component_dot(
2164    path: &[u8],
2165    index: usize,
2166    candidate_starts_component: bool,
2167    cache: &mut FirstAtOrAfter,
2168) -> usize {
2169    if let Some(found) = cache.get(index) {
2170        return found;
2171    }
2172    if index == 0 && candidate_starts_component && path.first() == Some(&b'.') {
2173        return cache.record(0, 0);
2174    }
2175    let start = index.saturating_sub(1);
2176    let found = match path.get(start..) {
2177        Some(window) => {
2178            let mut offset = 0;
2179            loop {
2180                let Some(hit) = next_separator(&window[offset..]) else {
2181                    break path.len();
2182                };
2183                let at = offset + hit;
2184                if window.get(at + 1) == Some(&b'.') {
2185                    break start + at + 1;
2186                }
2187                offset = at + 1;
2188            }
2189        }
2190        None => path.len(),
2191    };
2192    cache.record(index, found)
2193}
2194
2195/// Crate version exposed for build and integration diagnostics.
2196pub const VERSION: &str = env!("CARGO_PKG_VERSION");
2197
2198#[derive(Debug, Clone, PartialEq, Eq)]
2199enum Token {
2200    Literal(Vec<u8>),
2201    Separator,
2202    Any,
2203    Star,
2204    /// A whole-component `**` at the end of its pattern (see
2205    /// [`star_run_is_whole_component`]); a star that also crosses separators.
2206    RecursiveStar,
2207    /// A whole-component `**/`: zero or more whole components, each with its
2208    /// separator. It may only hand over at a component start.
2209    RecursivePrefix,
2210    Class(Class),
2211}
2212
2213/// Whether the two ends of a compiled pattern sit on path-component
2214/// boundaries of the pattern it came from.
2215///
2216/// A top-level pattern — and each brace alternative, which is a whole pattern
2217/// once expanded — is bounded on both sides. An extglob alternative inherits
2218/// its group's position: its start is a component start only when the group
2219/// operator is, and its end a component end only when the group's closing
2220/// parenthesis is. That is what makes `@(**)/y` recursive and `x@(**)/y` an
2221/// ordinary star run, exactly as `**/y` and `x**/y` are.
2222#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2223struct ComponentBounds {
2224    starts: bool,
2225    ends: bool,
2226}
2227
2228impl ComponentBounds {
2229    const WHOLE: Self = Self {
2230        starts: true,
2231        ends: true,
2232    };
2233}
2234
2235/// Whether the star run `pattern[start..end]` forms a whole path component
2236/// (#419, ADR-0020).
2237///
2238/// Only such a run may be recursive: on each side it must meet an unescaped
2239/// `/` or an end of the pattern that `bounds` marks as a component boundary.
2240/// An escaped `/` is a literal byte, not a pattern separator, so it bounds
2241/// nothing. Every other run is an ordinary star run, read exactly as it is
2242/// with `recursive_double_star` disabled.
2243fn star_run_is_whole_component(
2244    pattern: &[u8],
2245    start: usize,
2246    end: usize,
2247    escapes: bool,
2248    bounds: ComponentBounds,
2249) -> bool {
2250    starts_component_at(pattern, start, escapes, bounds) && ends_component_at(pattern, end, bounds)
2251}
2252
2253/// Whether offset `start` of `pattern` begins a path component: it follows an
2254/// unescaped `/`, or it is the pattern's start and `bounds` says that is one.
2255fn starts_component_at(
2256    pattern: &[u8],
2257    start: usize,
2258    escapes: bool,
2259    bounds: ComponentBounds,
2260) -> bool {
2261    let Some(before) = start.checked_sub(1) else {
2262        return bounds.starts;
2263    };
2264    let escaped = escapes
2265        && pattern[..before]
2266            .iter()
2267            .rev()
2268            .take_while(|&&byte| byte == b'\\')
2269            .count()
2270            % 2
2271            == 1;
2272    pattern[before] == b'/' && !escaped
2273}
2274
2275/// Whether offset `end` of `pattern` ends a path component: a `/` follows, or
2276/// it is the pattern's end and `bounds` says that is one.
2277fn ends_component_at(pattern: &[u8], end: usize, bounds: ComponentBounds) -> bool {
2278    pattern.get(end).map_or(bounds.ends, |&byte| byte == b'/')
2279}
2280
2281#[derive(Debug, Clone, PartialEq, Eq)]
2282/// Immutable compiled representation of one syntax alternative.
2283///
2284/// It is constructed only during [`Pattern::compile`]. Matching borrows it;
2285/// per-call memoization remains outside this IR in [`FailedStates`].
2286struct CompiledAlternative {
2287    raw: Vec<u8>,
2288    fast_path: Option<FastPath>,
2289    tokens: Vec<Token>,
2290    /// The compiled extglob program, present exactly when this alternative
2291    /// carries extglob syntax and the option is on. Matching borrows it; the
2292    /// interpreter used to re-derive all of it from `raw` on every call.
2293    extglob: Option<Box<CompiledExtglob>>,
2294    /// Necessary conditions on a candidate, read off `tokens` at compile time
2295    /// and checked before the general engine runs. See [`Prefilter`].
2296    prefilter: Prefilter,
2297    /// Walker-only path shape recorded while this alternative's tokens were
2298    /// compiled. It retains distinctions (such as escaped dots) that matcher
2299    /// tokens intentionally erase, and can be appended to an extglob's outer
2300    /// component without reparsing source syntax.
2301    walker_path_shape: WalkerPathShape,
2302    /// The bit-parallel engine for the general path, present when
2303    /// [`compile_sweep`] found the tokens suitable. It answers exactly like
2304    /// the memoized matcher and replaces it in the dispatch, never a fast
2305    /// path and never an extglob program.
2306    sweep: Option<SweepEngine>,
2307}
2308
2309impl CompiledAlternative {
2310    /// See [`Pattern::release_compile_state`]. Descends into extglob arms,
2311    /// whose shapes the analysis read as well.
2312    fn release_compile_state(&mut self) {
2313        self.walker_path_shape = WalkerPathShape::default();
2314        if let Some(program) = &mut self.extglob {
2315            program.walker_source_provenance = None;
2316            for group in &mut program.groups {
2317                for alternative in group
2318                    .alternatives
2319                    .iter_mut()
2320                    .chain(group.through_separator.iter_mut())
2321                {
2322                    for nested in &mut alternative.compiled {
2323                        nested.release_compile_state();
2324                    }
2325                }
2326            }
2327        }
2328    }
2329
2330    /// Removes accelerated engines, descending into extglob alternatives so a
2331    /// differential run pins one engine for the sub-matches too.
2332    #[cfg(any(test, feature = "unstable-test-hooks"))]
2333    fn strip_engines(
2334        &mut self,
2335        options: PatternOptions,
2336        fast_paths: bool,
2337        sweeps: bool,
2338        prefilters: bool,
2339    ) {
2340        if fast_paths {
2341            // With its fast path gone the engine the compile deferred is what
2342            // answers, as it did when every such alternative carried one.
2343            if !sweeps && sweep_is_deferred(self) {
2344                self.sweep = SweepEngine::build(&self.tokens, options).ok();
2345            }
2346            self.fast_path = None;
2347        }
2348        if sweeps {
2349            self.sweep = None;
2350        }
2351        if prefilters {
2352            self.prefilter = Prefilter::default();
2353        }
2354        if let Some(program) = &mut self.extglob {
2355            for group in &mut program.groups {
2356                for alternative in group
2357                    .alternatives
2358                    .iter_mut()
2359                    .chain(group.through_separator.iter_mut())
2360                {
2361                    for nested in &mut alternative.compiled {
2362                        nested.strip_engines(options, fast_paths, sweeps, prefilters);
2363                    }
2364                }
2365            }
2366        }
2367    }
2368}
2369
2370/// Compiles the Shift-And engine where the alternative would use it.
2371///
2372/// An extglob program keeps its own interpreter, and the literal and
2373/// deterministic fast paths win the dispatch under every option profile, so
2374/// building an engine behind either would spend budget on dead tables.
2375///
2376/// A starred fast path that also answers under the component policy wins the
2377/// dispatch of every entry point as well, so a top-level alternative never
2378/// runs the engine behind it either. The engine is charged but not built: an
2379/// extglob arm's prefix scan does run it, and
2380/// [`compile_extglob_alternative`] builds it there, and the differential
2381/// hooks build it on demand as their oracle. Every other shape may reach the
2382/// general path under some option profile and gets its engine here.
2383///
2384/// `built` is an engine already compiled from these very tokens under these
2385/// options. It is shared instead of building a second copy of its tables,
2386/// and charged as if it were built, so the budget does not depend on it.
2387fn compile_sweep(
2388    tokens: &[Token],
2389    fast_path: &Option<FastPath>,
2390    extglob: Option<&CompiledExtglob>,
2391    options: PatternOptions,
2392    budget: &mut IrBudget,
2393    built: Option<&SweepEngine>,
2394) -> Result<Option<SweepEngine>, PatternError> {
2395    if extglob.is_some()
2396        || matches!(
2397            fast_path,
2398            Some(FastPath::LiteralTokens(_) | FastPath::DeterministicTokens(_))
2399        )
2400    {
2401        return Ok(None);
2402    }
2403    if fast_path
2404        .as_ref()
2405        .is_some_and(FastPath::supports_component_wildcards)
2406    {
2407        SweepEngine::charge(tokens, budget)?;
2408        return Ok(None);
2409    }
2410    if let Some(built) = built {
2411        SweepEngine::charge(tokens, budget)?;
2412        return Ok(Some(built.clone()));
2413    }
2414    SweepEngine::compile(tokens, options, budget)
2415}
2416
2417/// Whether `alternative` carries an engine [`compile_sweep`] paid for but did
2418/// not build, because its fast path answers every top-level entry point.
2419fn sweep_is_deferred(alternative: &CompiledAlternative) -> bool {
2420    alternative.sweep.is_none()
2421        && alternative.extglob.is_none()
2422        && alternative.fast_path.as_ref().is_some_and(|fast_path| {
2423            fast_path.supports_component_wildcards()
2424                && !matches!(
2425                    fast_path,
2426                    FastPath::LiteralTokens(_) | FastPath::DeterministicTokens(_)
2427                )
2428        })
2429}
2430
2431/// Conditions every candidate the general engine accepts must already satisfy.
2432///
2433/// The engine is a memoized depth-first walk over (token, path position) pairs.
2434/// Nothing in it consults the pattern's trailing literal before exploring, so
2435/// `a*a*a*a*b` against a run of `a`s visits the whole state space and only then
2436/// fails on the final `b`. These three facts are cheap to check up front and
2437/// cannot turn a non-match into a match: each one is implied by a successful
2438/// match rather than being a second opinion about one.
2439///
2440/// Soundness rests on the engine accepting only when it has consumed the entire
2441/// candidate — `token_index == tokens.len() && path_index == path.len()`. The
2442/// leading run of `Literal`/`Separator` tokens therefore has to match at
2443/// position 0, the trailing run has to match at the end, and every token
2444/// consumes at least the byte count counted in `min_length`.
2445///
2446/// The one token whose contribution is not fixed is the `Separator` of a
2447/// trailing `.../**`: the engine lets it consume nothing and end the match
2448/// there, which is how `dir/**` accepts `dir`. It can only ever be the
2449/// second-to-last token, with a `RecursiveStar` behind it, so it can never sit
2450/// inside a *trailing* run — a run that reaches the end of the token list would
2451/// have to contain that `RecursiveStar`, which is neither a literal nor a
2452/// separator. It can sit at the end of a *leading* run, and `min_length`
2453/// counts it, so both of those subtract it explicitly.
2454///
2455/// A fourth fact covers the fixed runs in between: `**/node_modules/**` has no
2456/// fixed end, yet every match contains `node_modules`. See [`InteriorLiteral`].
2457#[derive(Debug, Clone, PartialEq, Eq, Default)]
2458struct Prefilter {
2459    /// Bytes every match starts with, one per byte of the leading
2460    /// `Literal`/`Separator` run.
2461    prefix: Vec<u8>,
2462    /// Bytes every match ends with, one per byte of the trailing run.
2463    suffix: Vec<u8>,
2464    /// Bytes the pattern consumes even when every star consumes nothing.
2465    min_length: usize,
2466    /// The longest fixed run strictly between the leading and trailing runs,
2467    /// which every match contains somewhere. Boxed because a searcher is much
2468    /// larger than the pointer, and most alternatives have none.
2469    interior: Option<Box<InteriorLiteral>>,
2470}
2471
2472/// A byte string every match contains, with its searcher built once.
2473///
2474/// Between two wildcards a run of `Literal`/`Separator` tokens is consumed as
2475/// a unit: a literal token only ever advances over exactly its own bytes and a
2476/// separator token over exactly one separator byte, so every accepted
2477/// candidate holds the run's bytes contiguously somewhere. Where it sits is up
2478/// to the wildcards around it, which is why this is a substring search rather
2479/// than an anchored comparison.
2480///
2481/// What the run may contain is narrower than what the fixed ends may:
2482///
2483/// - The separator before a terminal `**` may consume nothing, so it ends a
2484///   run instead of joining it. The `/` of a `**/` belongs to that wildcard
2485///   token, which is free to consume nothing, so it never joins either.
2486/// - On Windows a separator token also accepts `\`, which one needle byte
2487///   cannot express, so a separator ends a run there as well. An escaped `/`
2488///   inside a literal token is compared byte for byte and may stay.
2489/// - Case-insensitive matching compares folded bytes, which `memmem` cannot,
2490///   so [`Prefilter::rejects`] skips the search when folding is on. The needle
2491///   is derived without options, so the check has to be made per call.
2492///
2493/// Every other option changes what wildcards accept, never what a literal or
2494/// separator token consumes, and the prefilter is consulted on exactly the
2495/// candidate the engine then walks. That holds for every entry point: the
2496/// list-filter copies strip a leading `./` from their own tokens and from the
2497/// candidate before either reaches here, and an extglob sub-match asks about
2498/// the slice it is matching.
2499#[derive(Debug, Clone)]
2500struct InteriorLiteral(memmem::Finder<'static>);
2501
2502impl PartialEq for InteriorLiteral {
2503    fn eq(&self, other: &Self) -> bool {
2504        self.0.needle() == other.0.needle()
2505    }
2506}
2507
2508impl Eq for InteriorLiteral {}
2509
2510impl Prefilter {
2511    fn compile(tokens: &[Token]) -> Self {
2512        // `dir/**` matches `dir`, so the separator before a terminal `**` is
2513        // the one token that may consume nothing.
2514        let elidable_separator = tokens.len() >= 2
2515            && matches!(
2516                tokens[tokens.len() - 2..],
2517                [Token::Separator, Token::RecursiveStar]
2518            );
2519        let fixed = |token: &&Token| matches!(token, Token::Literal(_) | Token::Separator);
2520        let mut leading = tokens.iter().take_while(fixed).count();
2521        if elidable_separator && leading + 1 == tokens.len() {
2522            leading -= 1;
2523        }
2524        let trailing = tokens.iter().rev().take_while(fixed).count();
2525        let min_length =
2526            tokens.iter().map(token_min_length).sum::<usize>() - usize::from(elidable_separator);
2527        let prefilter = Self {
2528            prefix: run_bytes(&tokens[..leading]),
2529            suffix: run_bytes(&tokens[tokens.len() - trailing..]),
2530            min_length,
2531            interior: interior_literal(tokens, leading, trailing, elidable_separator)
2532                .map(|needle| Box::new(InteriorLiteral(memmem::Finder::new(&needle).into_owned()))),
2533        };
2534        debug_assert!(
2535            prefilter.min_length >= prefilter.prefix.len()
2536                && prefilter.min_length >= prefilter.suffix.len(),
2537            "the fixed runs are part of what the minimum length counts"
2538        );
2539        prefilter
2540    }
2541
2542    /// Whether `path` fails a condition every match satisfies. A `true` here is
2543    /// a rejection the engine would have reached the slow way.
2544    fn rejects(&self, path: &[u8], case_insensitive: bool) -> bool {
2545        if path.len() < self.min_length {
2546            return true;
2547        }
2548        let tail = path.len() - self.suffix.len();
2549        !run_matches(&self.prefix, &path[..self.prefix.len()], case_insensitive)
2550            || !run_matches(&self.suffix, &path[tail..], case_insensitive)
2551            || !case_insensitive
2552                && self
2553                    .interior
2554                    .as_ref()
2555                    .is_some_and(|interior| interior.0.find(path).is_none())
2556    }
2557}
2558
2559/// The longest run of bytes every match contains that the fixed ends do not
2560/// already check, if one holds a byte other than a separator.
2561///
2562/// `leading` and `trailing` are the token counts of the prefix and suffix
2563/// runs; their bytes are compared in place and would add nothing here. A lone
2564/// separator is left out because nearly every path contains one. Ties go to
2565/// the earlier run. See [`InteriorLiteral`] for what may join a run.
2566fn interior_literal(
2567    tokens: &[Token],
2568    leading: usize,
2569    trailing: usize,
2570    elidable_separator: bool,
2571) -> Option<Vec<u8>> {
2572    let interior_end = tokens.len() - trailing;
2573    let mut best: Option<Vec<u8>> = None;
2574    let mut run = Vec::new();
2575    for (index, token) in tokens.iter().enumerate() {
2576        let joins = index >= leading && index < interior_end;
2577        match token {
2578            Token::Literal(literal) if joins => run.extend_from_slice(literal),
2579            Token::Separator
2580                if joins
2581                    && !cfg!(windows)
2582                    && !(elidable_separator && index + 2 == tokens.len()) =>
2583            {
2584                run.push(b'/');
2585            }
2586            _ => {
2587                keep_longer_run(&mut best, &mut run);
2588            }
2589        }
2590    }
2591    keep_longer_run(&mut best, &mut run);
2592    best
2593}
2594
2595/// Moves `run` into `best` when it is strictly longer and not separators only,
2596/// and leaves `run` empty for the next one.
2597fn keep_longer_run(best: &mut Option<Vec<u8>>, run: &mut Vec<u8>) {
2598    if run.len() > best.as_ref().map_or(0, Vec::len) && run.iter().any(|&byte| !is_separator(byte))
2599    {
2600        *best = Some(std::mem::take(run));
2601    } else {
2602        run.clear();
2603    }
2604}
2605
2606/// Bytes a run of `Literal`/`Separator` tokens spells out, with a separator
2607/// written as `/`. Any other token in `run` would be a caller bug and is
2608/// counted as nothing, which keeps the run shorter and the filter weaker.
2609fn run_bytes(run: &[Token]) -> Vec<u8> {
2610    fn bytes(token: &Token) -> &[u8] {
2611        match token {
2612            Token::Literal(literal) => literal,
2613            _ => b"/",
2614        }
2615    }
2616    let mut collected = Vec::with_capacity(run.iter().map(|token| bytes(token).len()).sum());
2617    for token in run {
2618        collected.extend_from_slice(bytes(token));
2619    }
2620    collected
2621}
2622
2623fn token_min_length(token: &Token) -> usize {
2624    match token {
2625        Token::Literal(literal) => literal.len(),
2626        Token::Separator | Token::Any | Token::Class(_) => 1,
2627        Token::Star | Token::RecursiveStar | Token::RecursivePrefix => 0,
2628    }
2629}
2630
2631fn run_matches(expected: &[u8], actual: &[u8], case_insensitive: bool) -> bool {
2632    expected
2633        .iter()
2634        .zip(actual)
2635        .all(|(&expected, &actual)| run_byte_matches(expected, actual, case_insensitive))
2636}
2637
2638/// Whether one byte of a fixed run is satisfied by the candidate's byte.
2639///
2640/// A `Separator` token contributes `/` and accepts whatever the platform calls
2641/// a separator, which is what the engine's separator arm does. A literal `/` --
2642/// only reachable through an escape -- is then accepted a little more widely
2643/// than the engine accepts it, on Windows only. Being more permissive is the
2644/// safe direction for a filter that may only reject.
2645fn run_byte_matches(expected: u8, actual: u8, case_insensitive: bool) -> bool {
2646    if is_separator(expected) {
2647        is_separator(actual)
2648    } else {
2649        bytes_equal(expected, actual, case_insensitive)
2650    }
2651}
2652
2653/// The suffix representation selected once while compiling a fast path.
2654#[derive(Debug, Clone, PartialEq, Eq)]
2655enum LiteralSuffix {
2656    Plain(Vec<u8>),
2657    #[cfg(all(target_arch = "aarch64", target_os = "macos"))]
2658    Packed16(Box<suffix_word::PreparedSuffix16>),
2659}
2660
2661impl LiteralSuffix {
2662    fn new(suffix: Vec<u8>, case_insensitive: bool) -> Self {
2663        #[cfg(not(all(target_arch = "aarch64", target_os = "macos")))]
2664        let _ = case_insensitive;
2665
2666        #[cfg(all(target_arch = "aarch64", target_os = "macos"))]
2667        if !case_insensitive && let Some(prepared) = suffix_word::PreparedSuffix16::new(&suffix) {
2668            return Self::Packed16(Box::new(prepared));
2669        }
2670
2671        Self::Plain(suffix)
2672    }
2673
2674    fn strip_from<'a>(&self, path: &'a [u8], case_insensitive: bool) -> Option<&'a [u8]> {
2675        match self {
2676            Self::Plain(suffix) => strip_literal_suffix(path, suffix, case_insensitive),
2677            #[cfg(all(target_arch = "aarch64", target_os = "macos"))]
2678            Self::Packed16(suffix) => {
2679                if !case_insensitive && let Some(matches) = suffix.matches(path) {
2680                    return matches.then_some(&path[..path.len() - suffix.len()]);
2681                }
2682                let bytes = suffix.bytes();
2683                strip_literal_suffix(path, &bytes[16 - suffix.len()..], case_insensitive)
2684            }
2685        }
2686    }
2687}
2688
2689/// One matcher for a brace-expanded set that differs only in its literal
2690/// suffix. The trie reads a candidate from the end once, so the cost does not
2691/// depend on where a matching extension appeared in the source brace list.
2692#[derive(Debug, Clone, PartialEq, Eq)]
2693enum AlternativeFastPath {
2694    SuffixSet(SuffixSet),
2695    ScopedSuffixSet(ScopedSuffixSet),
2696}
2697
2698impl AlternativeFastPath {
2699    fn compile(
2700        alternatives: &[CompiledAlternative],
2701        options: PatternOptions,
2702        budget: &mut IrBudget,
2703    ) -> Result<Option<Self>, PatternError> {
2704        if alternatives.len() < 2 || alternatives.iter().any(|item| item.extglob.is_some()) {
2705            return Ok(None);
2706        }
2707        if let Some(set) = SuffixSet::compile(alternatives, options.case_insensitive, budget)? {
2708            return Ok(Some(Self::SuffixSet(set)));
2709        }
2710        Ok(
2711            ScopedSuffixSet::compile(alternatives, options.case_insensitive, budget)?
2712                .map(Self::ScopedSuffixSet),
2713        )
2714    }
2715
2716    fn is_match(
2717        &self,
2718        path: &[u8],
2719        options: PatternOptions,
2720        alternatives: &[CompiledAlternative],
2721    ) -> bool {
2722        match self {
2723            Self::SuffixSet(set) => set.is_match(path, options),
2724            Self::ScopedSuffixSet(set) => {
2725                alternatives
2726                    .first()
2727                    .and_then(|alternative| alternative.fast_path.as_ref())
2728                    .is_some_and(|fast_path| fast_path.is_match(path, options))
2729                    || set.is_match(path, options)
2730            }
2731        }
2732    }
2733}
2734
2735#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2736enum SuffixSetKind {
2737    Star,
2738    Recursive,
2739}
2740
2741#[derive(Debug, Clone, PartialEq, Eq)]
2742struct SuffixSet {
2743    kind: SuffixSetKind,
2744    trie: AffixTrie,
2745}
2746
2747/// Two independent tries are sound only for a complete prefix/suffix cross
2748/// product. Compilation proves that shape before this matcher is installed.
2749#[derive(Debug, Clone, PartialEq, Eq)]
2750struct ScopedSuffixSet {
2751    prefixes: AffixTrie,
2752    suffixes: AffixTrie,
2753}
2754
2755#[derive(Debug, Clone, PartialEq, Eq)]
2756struct AffixTrie {
2757    nodes: Vec<AffixNode>,
2758    edges: Vec<AffixEdge>,
2759}
2760
2761#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2762struct AffixNode {
2763    first_edge: usize,
2764    edge_count: usize,
2765    terminal: bool,
2766}
2767
2768#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2769struct AffixEdge {
2770    byte: u8,
2771    next: usize,
2772}
2773
2774#[derive(Default)]
2775struct BuildingAffixNode {
2776    edges: Vec<(u8, usize)>,
2777    terminal: bool,
2778}
2779
2780impl SuffixSet {
2781    fn compile(
2782        alternatives: &[CompiledAlternative],
2783        case_insensitive: bool,
2784        budget: &mut IrBudget,
2785    ) -> Result<Option<Self>, PatternError> {
2786        let mut suffixes = Vec::with_capacity(alternatives.len());
2787        let mut common_kind = None;
2788        for alternative in alternatives {
2789            let Some(fast_path) = alternative.fast_path.as_ref() else {
2790                return Ok(None);
2791            };
2792            let (kind, suffix) = match (fast_path, alternative.tokens.as_slice()) {
2793                (FastPath::StarSuffix { .. }, [Token::Star, Token::Literal(suffix)]) => {
2794                    (SuffixSetKind::Star, suffix)
2795                }
2796                (
2797                    FastPath::RecursiveSuffix { .. },
2798                    [Token::RecursivePrefix, Token::Star, Token::Literal(suffix)],
2799                ) => (SuffixSetKind::Recursive, suffix),
2800                _ => return Ok(None),
2801            };
2802            if common_kind.is_some_and(|common| common != kind) {
2803                return Ok(None);
2804            }
2805            suffixes.push(suffix.as_slice());
2806            common_kind = Some(kind);
2807        }
2808        Ok(Some(Self {
2809            kind: common_kind.expect("two suffix alternatives establish a kind"),
2810            trie: AffixTrie::new(&suffixes, true, case_insensitive, budget)?,
2811        }))
2812    }
2813
2814    fn is_match(&self, path: &[u8], options: PatternOptions) -> bool {
2815        let mut node = 0;
2816        for (offset, &byte) in path.iter().rev().enumerate() {
2817            let Some(next) = self
2818                .trie
2819                .next(node, fold_ascii(byte, options.case_insensitive))
2820            else {
2821                return false;
2822            };
2823            node = next;
2824            if !self.trie.nodes[node].terminal {
2825                continue;
2826            }
2827
2828            let variable_end = path.len() - offset - 1;
2829            if self.kind == SuffixSetKind::Star
2830                && options.component_wildcards
2831                && next_separator(&path[..variable_end]).is_some()
2832            {
2833                continue;
2834            }
2835            if star_stops_before_hidden_component(path, variable_end, options) {
2836                continue;
2837            }
2838            if options.match_hidden
2839                || !contains_hidden_component_in(
2840                    path,
2841                    0,
2842                    variable_end,
2843                    options.candidate_starts_component,
2844                )
2845            {
2846                return true;
2847            }
2848        }
2849        false
2850    }
2851}
2852
2853impl ScopedSuffixSet {
2854    fn compile(
2855        alternatives: &[CompiledAlternative],
2856        case_insensitive: bool,
2857        budget: &mut IrBudget,
2858    ) -> Result<Option<Self>, PatternError> {
2859        let mut prefixes = Vec::new();
2860        let mut suffixes = Vec::new();
2861        let mut pairs = Vec::with_capacity(alternatives.len());
2862        for alternative in alternatives {
2863            let Some(fast_path) = alternative.fast_path.as_ref() else {
2864                return Ok(None);
2865            };
2866            let (
2867                FastPath::RecursivePrefixSuffix { .. },
2868                [
2869                    Token::Literal(prefix),
2870                    Token::Separator,
2871                    Token::RecursivePrefix,
2872                    Token::Star,
2873                    Token::Literal(suffix),
2874                ],
2875            ) = (fast_path, alternative.tokens.as_slice())
2876            else {
2877                return Ok(None);
2878            };
2879            let prefix = literal_index(&mut prefixes, prefix, case_insensitive);
2880            let suffix = literal_index(&mut suffixes, suffix, case_insensitive);
2881            if !pairs.contains(&(prefix, suffix)) {
2882                pairs.push((prefix, suffix));
2883            }
2884        }
2885
2886        // Independent tries would otherwise accept a combination the source
2887        // never named, such as `lib/**/*.ts` from
2888        // `{src/**/*.ts,lib/**/*.js}`.
2889        if prefixes
2890            .len()
2891            .checked_mul(suffixes.len())
2892            .is_none_or(|product| pairs.len() != product)
2893        {
2894            return Ok(None);
2895        }
2896        Ok(Some(Self {
2897            prefixes: AffixTrie::new(&prefixes, false, case_insensitive, budget)?,
2898            suffixes: AffixTrie::new(&suffixes, true, case_insensitive, budget)?,
2899        }))
2900    }
2901
2902    fn is_match(&self, path: &[u8], options: PatternOptions) -> bool {
2903        let mut prefix_node = 0;
2904        let mut prefix_len = None;
2905        for (offset, &byte) in path.iter().enumerate() {
2906            let Some(next) = self
2907                .prefixes
2908                .next(prefix_node, fold_ascii(byte, options.case_insensitive))
2909            else {
2910                return false;
2911            };
2912            prefix_node = next;
2913            if self.prefixes.nodes[prefix_node].terminal
2914                && path.get(offset + 1).is_some_and(|byte| is_separator(*byte))
2915            {
2916                prefix_len = Some(offset + 1);
2917                break;
2918            }
2919        }
2920        let Some(prefix_len) = prefix_len else {
2921            return false;
2922        };
2923        let variable_start = prefix_len + 1;
2924
2925        let mut suffix_node = 0;
2926        for (offset, &byte) in path.iter().rev().enumerate() {
2927            let Some(next) = self
2928                .suffixes
2929                .next(suffix_node, fold_ascii(byte, options.case_insensitive))
2930            else {
2931                return false;
2932            };
2933            suffix_node = next;
2934            if !self.suffixes.nodes[suffix_node].terminal {
2935                continue;
2936            }
2937            let variable_end = path.len() - offset - 1;
2938            if variable_start > variable_end {
2939                continue;
2940            }
2941            if star_stops_before_hidden_component(path, variable_end, options) {
2942                continue;
2943            }
2944            if options.match_hidden
2945                || !contains_hidden_component_in(
2946                    path,
2947                    variable_start,
2948                    variable_end,
2949                    options.candidate_starts_component,
2950                )
2951            {
2952                return true;
2953            }
2954        }
2955        false
2956    }
2957}
2958
2959fn literal_index<'a>(
2960    literals: &mut Vec<&'a [u8]>,
2961    literal: &'a [u8],
2962    case_insensitive: bool,
2963) -> usize {
2964    if let Some(index) = literals.iter().position(|existing| {
2965        existing.len() == literal.len()
2966            && existing
2967                .iter()
2968                .zip(literal)
2969                .all(|(&left, &right)| bytes_equal(left, right, case_insensitive))
2970    }) {
2971        return index;
2972    }
2973    literals.push(literal);
2974    literals.len() - 1
2975}
2976
2977impl AffixTrie {
2978    fn new(
2979        words: &[&[u8]],
2980        reverse: bool,
2981        case_insensitive: bool,
2982        budget: &mut IrBudget,
2983    ) -> Result<Self, PatternError> {
2984        // The root exists in the temporary builder and the flattened trie at
2985        // the same time while the latter is assembled.
2986        budget.charge(2, 0)?;
2987        let mut building = vec![BuildingAffixNode::default()];
2988        for word in words {
2989            if reverse {
2990                Self::insert(
2991                    &mut building,
2992                    word.iter()
2993                        .rev()
2994                        .map(|&byte| fold_ascii(byte, case_insensitive)),
2995                    budget,
2996                )?;
2997            } else {
2998                Self::insert(
2999                    &mut building,
3000                    word.iter().map(|&byte| fold_ascii(byte, case_insensitive)),
3001                    budget,
3002                )?;
3003            }
3004        }
3005
3006        let edge_count = building.iter().map(|node| node.edges.len()).sum();
3007        let mut nodes = Vec::with_capacity(building.len());
3008        let mut edges = Vec::with_capacity(edge_count);
3009        for node in building {
3010            let first_edge = edges.len();
3011            edges.extend(
3012                node.edges
3013                    .into_iter()
3014                    .map(|(byte, next)| AffixEdge { byte, next }),
3015            );
3016            nodes.push(AffixNode {
3017                first_edge,
3018                edge_count: edges.len() - first_edge,
3019                terminal: node.terminal,
3020            });
3021        }
3022        Ok(Self { nodes, edges })
3023    }
3024
3025    fn insert(
3026        building: &mut Vec<BuildingAffixNode>,
3027        bytes: impl Iterator<Item = u8>,
3028        budget: &mut IrBudget,
3029    ) -> Result<(), PatternError> {
3030        let mut node = 0;
3031        for byte in bytes {
3032            let existing = building[node]
3033                .edges
3034                .iter()
3035                .find_map(|&(edge, next)| (edge == byte).then_some(next));
3036            node = if let Some(existing) = existing {
3037                existing
3038            } else {
3039                // Construction temporarily owns the builder node and edge as
3040                // well as their flattened counterparts. Three token-sized IR
3041                // units conservatively cover that peak, not merely the final
3042                // node/edge pair.
3043                budget.charge(3, 0)?;
3044                let next = building.len();
3045                building.push(BuildingAffixNode::default());
3046                building[node].edges.push((byte, next));
3047                next
3048            };
3049        }
3050        building[node].terminal = true;
3051        Ok(())
3052    }
3053
3054    fn next(&self, node: usize, byte: u8) -> Option<usize> {
3055        let node = self.nodes[node];
3056        self.edges[node.first_edge..node.first_edge + node.edge_count]
3057            .iter()
3058            .find_map(|edge| (edge.byte == byte).then_some(edge.next))
3059    }
3060}
3061
3062/// An allocation-free matcher for a common recursive-prefix/suffix shape.
3063///
3064/// It is deliberately narrower than the full token matcher. Keeping it as a
3065/// compiled variant makes the optimization opt-in by syntax and leaves every
3066/// other pattern on the corpus-tested general path.
3067#[derive(Debug, Clone, PartialEq, Eq)]
3068enum FastPath {
3069    LiteralTokens(Vec<Token>),
3070    DeterministicTokens(Vec<Token>),
3071    Star,
3072    PrefixStar {
3073        prefix: Vec<u8>,
3074    },
3075    StarSuffix {
3076        suffix: LiteralSuffix,
3077    },
3078    InfixStar {
3079        prefix: Vec<u8>,
3080        suffix: Vec<u8>,
3081    },
3082    StaticStar {
3083        prefix: Vec<Token>,
3084        suffix: Vec<Token>,
3085    },
3086    RecursiveTerminalPrefix {
3087        prefix: Vec<u8>,
3088    },
3089    RecursiveSuffix {
3090        suffix: LiteralSuffix,
3091        suffix_last: u8,
3092    },
3093    RecursivePrefixSuffix {
3094        prefix: Vec<u8>,
3095        suffix: LiteralSuffix,
3096        suffix_last: u8,
3097    },
3098}
3099
3100impl FastPath {
3101    fn compile(tokens: &[Token], options: PatternOptions) -> Option<Self> {
3102        if tokens
3103            .iter()
3104            .all(|token| matches!(token, Token::Literal(_) | Token::Separator))
3105        {
3106            return Some(Self::LiteralTokens(tokens.to_vec()));
3107        }
3108        if tokens.iter().all(|token| {
3109            matches!(
3110                token,
3111                Token::Literal(_) | Token::Separator | Token::Any | Token::Class(_)
3112            )
3113        }) {
3114            return Some(Self::DeterministicTokens(tokens.to_vec()));
3115        }
3116        if options.component_wildcards {
3117            return None;
3118        }
3119        match tokens {
3120            [Token::Star] => return Some(Self::Star),
3121            [Token::Literal(prefix), Token::Star] => {
3122                return Some(Self::PrefixStar {
3123                    prefix: prefix.clone(),
3124                });
3125            }
3126            [Token::Star, Token::Literal(suffix)] => {
3127                return Some(Self::StarSuffix {
3128                    suffix: LiteralSuffix::new(suffix.clone(), options.case_insensitive),
3129                });
3130            }
3131            [Token::Literal(prefix), Token::Star, Token::Literal(suffix)] => {
3132                return Some(Self::InfixStar {
3133                    prefix: prefix.clone(),
3134                    suffix: suffix.clone(),
3135                });
3136            }
3137            _ => {}
3138        }
3139        if let Some(star_index) = tokens.iter().position(|token| matches!(token, Token::Star))
3140            && tokens.len() > 1
3141            && tokens.iter().enumerate().all(|(index, token)| {
3142                index == star_index || matches!(token, Token::Literal(_) | Token::Separator)
3143            })
3144        {
3145            return Some(Self::StaticStar {
3146                prefix: tokens[..star_index].to_vec(),
3147                suffix: tokens[star_index + 1..].to_vec(),
3148            });
3149        }
3150        if let [
3151            Token::Literal(prefix),
3152            Token::Separator,
3153            Token::RecursiveStar,
3154        ] = tokens
3155        {
3156            return Some(Self::RecursiveTerminalPrefix {
3157                prefix: prefix.clone(),
3158            });
3159        }
3160        if let [Token::RecursivePrefix, Token::Star, Token::Literal(suffix)] = tokens {
3161            return Some(Self::RecursiveSuffix {
3162                suffix: LiteralSuffix::new(suffix.clone(), options.case_insensitive),
3163                suffix_last: *suffix.last().expect("literal token is non-empty"),
3164            });
3165        }
3166        let [
3167            Token::Literal(prefix),
3168            Token::Separator,
3169            Token::RecursivePrefix,
3170            Token::Star,
3171            Token::Literal(suffix),
3172        ] = tokens
3173        else {
3174            return None;
3175        };
3176        Some(Self::RecursivePrefixSuffix {
3177            prefix: prefix.clone(),
3178            suffix: LiteralSuffix::new(suffix.clone(), options.case_insensitive),
3179            suffix_last: *suffix.last().expect("literal token is non-empty"),
3180        })
3181    }
3182
3183    fn supports_component_wildcards(&self) -> bool {
3184        matches!(
3185            self,
3186            Self::LiteralTokens(_)
3187                | Self::DeterministicTokens(_)
3188                | Self::StarSuffix { .. }
3189                | Self::RecursiveSuffix { .. }
3190                | Self::RecursivePrefixSuffix { .. }
3191        )
3192    }
3193
3194    fn is_match(&self, path: &[u8], options: PatternOptions) -> bool {
3195        match self {
3196            Self::LiteralTokens(tokens) => {
3197                let mut path_index = 0;
3198                for token in tokens {
3199                    match token {
3200                        Token::Literal(literal) => {
3201                            let Some(candidate) = path.get(path_index..path_index + literal.len())
3202                            else {
3203                                return false;
3204                            };
3205                            if !literal.iter().zip(candidate).all(|(&expected, &actual)| {
3206                                bytes_equal(expected, actual, options.case_insensitive)
3207                            }) {
3208                                return false;
3209                            }
3210                            path_index += literal.len();
3211                        }
3212                        Token::Separator => {
3213                            if !path.get(path_index).is_some_and(|byte| is_separator(*byte)) {
3214                                return false;
3215                            }
3216                            path_index += 1;
3217                        }
3218                        _ => unreachable!("literal fast path only stores literal tokens"),
3219                    }
3220                }
3221                path_index == path.len()
3222            }
3223            Self::DeterministicTokens(tokens) => {
3224                let mut path_index = 0;
3225                for (token_index, token) in tokens.iter().enumerate() {
3226                    match token {
3227                        Token::Literal(literal) => {
3228                            let Some(candidate) = path.get(path_index..path_index + literal.len())
3229                            else {
3230                                return false;
3231                            };
3232                            if !literal.iter().zip(candidate).all(|(&expected, &actual)| {
3233                                bytes_equal(expected, actual, options.case_insensitive)
3234                            }) {
3235                                return false;
3236                            }
3237                            path_index += literal.len();
3238                        }
3239                        Token::Separator => {
3240                            if !path.get(path_index).is_some_and(|byte| is_separator(*byte)) {
3241                                return false;
3242                            }
3243                            path_index += 1;
3244                        }
3245                        Token::Any => {
3246                            let Some(&byte) = path.get(path_index) else {
3247                                return false;
3248                            };
3249                            if Pattern::component_wildcard(tokens, token_index, options)
3250                                && is_separator(byte)
3251                            {
3252                                return false;
3253                            }
3254                            if !options.match_hidden
3255                                && byte == b'.'
3256                                && at_component_start(path, path_index, options)
3257                            {
3258                                return false;
3259                            }
3260                            path_index += 1;
3261                        }
3262                        Token::Class(class) => {
3263                            let Some(&byte) = path.get(path_index) else {
3264                                return false;
3265                            };
3266                            if (Pattern::component_wildcard(tokens, token_index, options)
3267                                && is_separator(byte))
3268                                || !class.matches(byte, options.case_insensitive)
3269                                || (!options.match_hidden
3270                                    && byte == b'.'
3271                                    && at_component_start(path, path_index, options))
3272                            {
3273                                return false;
3274                            }
3275                            path_index += 1;
3276                        }
3277                        Token::Star | Token::RecursiveStar | Token::RecursivePrefix => {
3278                            return false;
3279                        }
3280                    }
3281                }
3282                path_index == path.len()
3283            }
3284            Self::Star => {
3285                options.match_hidden
3286                    || !contains_hidden_component_in(
3287                        path,
3288                        0,
3289                        path.len(),
3290                        options.candidate_starts_component,
3291                    )
3292            }
3293            Self::PrefixStar { prefix } => {
3294                let Some(variable) = strip_literal_prefix(path, prefix, options.case_insensitive)
3295                else {
3296                    return false;
3297                };
3298                options.match_hidden
3299                    || !contains_hidden_component_in(
3300                        path,
3301                        path.len() - variable.len(),
3302                        path.len(),
3303                        options.candidate_starts_component,
3304                    )
3305            }
3306            Self::StarSuffix { suffix } => {
3307                let Some(variable) = suffix.strip_from(path, options.case_insensitive) else {
3308                    return false;
3309                };
3310                // The leading star is component-local only in a
3311                // component-local root position, exactly as
3312                // `Pattern::component_wildcard` answers for token zero.
3313                if options.component_wildcards
3314                    && (options.root_component_wildcards
3315                        || options.candidate_root_component_wildcard)
3316                    && next_separator(variable).is_some()
3317                {
3318                    return false;
3319                }
3320                if star_stops_before_hidden_component(path, variable.len(), options) {
3321                    return false;
3322                }
3323                options.match_hidden
3324                    || !contains_hidden_component_in(
3325                        path,
3326                        0,
3327                        variable.len(),
3328                        options.candidate_starts_component,
3329                    )
3330            }
3331            Self::InfixStar { prefix, suffix } => {
3332                let Some(remainder) = strip_literal_prefix(path, prefix, options.case_insensitive)
3333                else {
3334                    return false;
3335                };
3336                let Some(variable) =
3337                    strip_literal_suffix(remainder, suffix, options.case_insensitive)
3338                else {
3339                    return false;
3340                };
3341                let variable_start = path.len() - remainder.len();
3342                let variable_end = variable_start + variable.len();
3343                if star_stops_before_hidden_component(path, variable_end, options) {
3344                    return false;
3345                }
3346                options.match_hidden
3347                    || !contains_hidden_component_in(
3348                        path,
3349                        variable_start,
3350                        variable_end,
3351                        options.candidate_starts_component,
3352                    )
3353            }
3354            Self::StaticStar { prefix, suffix } => {
3355                let Some(variable_start) = match_static_prefix(prefix, path, options) else {
3356                    return false;
3357                };
3358                let Some(variable_end) = match_static_suffix(suffix, path, options) else {
3359                    return false;
3360                };
3361                if variable_start > variable_end {
3362                    return false;
3363                }
3364                if star_stops_before_hidden_component(path, variable_end, options) {
3365                    return false;
3366                }
3367                options.match_hidden
3368                    || !contains_hidden_component_in(
3369                        path,
3370                        variable_start,
3371                        variable_end,
3372                        options.candidate_starts_component,
3373                    )
3374            }
3375            Self::RecursiveTerminalPrefix { prefix } => {
3376                let Some(remainder) = strip_literal_prefix(path, prefix, options.case_insensitive)
3377                else {
3378                    return false;
3379                };
3380                remainder.is_empty()
3381                    || remainder.first().is_some_and(|byte| is_separator(*byte))
3382                        && (options.match_hidden
3383                            || !contains_hidden_component_in(
3384                                path,
3385                                path.len() - remainder.len() + 1,
3386                                path.len(),
3387                                options.candidate_starts_component,
3388                            ))
3389            }
3390            Self::RecursiveSuffix {
3391                suffix,
3392                suffix_last,
3393            } => {
3394                let Some(&path_last) = path.last() else {
3395                    return false;
3396                };
3397                if !bytes_equal(*suffix_last, path_last, options.case_insensitive) {
3398                    return false;
3399                }
3400                let Some(variable) = suffix.strip_from(path, options.case_insensitive) else {
3401                    return false;
3402                };
3403                if star_stops_before_hidden_component(path, variable.len(), options) {
3404                    return false;
3405                }
3406                options.match_hidden
3407                    || !contains_hidden_component_in(
3408                        path,
3409                        0,
3410                        variable.len(),
3411                        options.candidate_starts_component,
3412                    )
3413            }
3414            Self::RecursivePrefixSuffix {
3415                prefix,
3416                suffix,
3417                suffix_last,
3418            } => {
3419                let Some(&path_last) = path.last() else {
3420                    return false;
3421                };
3422                if !bytes_equal(*suffix_last, path_last, options.case_insensitive) {
3423                    return false;
3424                }
3425                let Some(prefix_and_variable) = suffix.strip_from(path, options.case_insensitive)
3426                else {
3427                    return false;
3428                };
3429                let Some(remainder) =
3430                    strip_literal_prefix(prefix_and_variable, prefix, options.case_insensitive)
3431                else {
3432                    return false;
3433                };
3434                let Some((&separator, variable)) = remainder.split_first() else {
3435                    return false;
3436                };
3437                if !is_separator(separator) {
3438                    return false;
3439                }
3440                let variable_start = prefix.len() + 1;
3441                let variable_end = variable_start + variable.len();
3442                if star_stops_before_hidden_component(path, variable_end, options) {
3443                    return false;
3444                }
3445                options.match_hidden
3446                    || !contains_hidden_component_in(
3447                        path,
3448                        variable_start,
3449                        variable_end,
3450                        options.candidate_starts_component,
3451                    )
3452            }
3453        }
3454    }
3455}
3456
3457fn match_static_prefix(tokens: &[Token], path: &[u8], options: PatternOptions) -> Option<usize> {
3458    let mut index = 0;
3459    for token in tokens {
3460        match token {
3461            Token::Literal(literal) => {
3462                let candidate = path.get(index..index + literal.len())?;
3463                if !literal.iter().zip(candidate).all(|(&expected, &actual)| {
3464                    bytes_equal(expected, actual, options.case_insensitive)
3465                }) {
3466                    return None;
3467                }
3468                index += literal.len();
3469            }
3470            Token::Separator => {
3471                if !path.get(index).is_some_and(|byte| is_separator(*byte)) {
3472                    return None;
3473                }
3474                index += 1;
3475            }
3476            _ => return None,
3477        }
3478    }
3479    Some(index)
3480}
3481
3482fn match_static_suffix(tokens: &[Token], path: &[u8], options: PatternOptions) -> Option<usize> {
3483    let mut index = path.len();
3484    for token in tokens.iter().rev() {
3485        match token {
3486            Token::Literal(literal) => {
3487                let start = index.checked_sub(literal.len())?;
3488                if !literal
3489                    .iter()
3490                    .zip(&path[start..index])
3491                    .all(|(&expected, &actual)| {
3492                        bytes_equal(expected, actual, options.case_insensitive)
3493                    })
3494                {
3495                    return None;
3496                }
3497                index = start;
3498            }
3499            Token::Separator => {
3500                let start = index.checked_sub(1)?;
3501                if !is_separator(path[start]) {
3502                    return None;
3503                }
3504                index = start;
3505            }
3506            _ => return None,
3507        }
3508    }
3509    Some(index)
3510}
3511
3512fn strip_literal_prefix<'a>(
3513    path: &'a [u8],
3514    literal: &[u8],
3515    case_insensitive: bool,
3516) -> Option<&'a [u8]> {
3517    let candidate = path.get(..literal.len())?;
3518    literal
3519        .iter()
3520        .zip(candidate)
3521        .all(|(&expected, &actual)| bytes_equal(expected, actual, case_insensitive))
3522        .then_some(&path[literal.len()..])
3523}
3524
3525fn strip_literal_suffix<'a>(
3526    path: &'a [u8],
3527    literal: &[u8],
3528    case_insensitive: bool,
3529) -> Option<&'a [u8]> {
3530    let start = path.len().checked_sub(literal.len())?;
3531    literal
3532        .iter()
3533        .zip(&path[start..])
3534        .all(|(&expected, &actual)| bytes_equal(expected, actual, case_insensitive))
3535        .then_some(&path[..start])
3536}
3537
3538fn contains_hidden_component_in(
3539    path: &[u8],
3540    start: usize,
3541    end: usize,
3542    candidate_starts_component: bool,
3543) -> bool {
3544    let Some(segment) = path.get(start..end) else {
3545        return false;
3546    };
3547    let mut offset = start;
3548    while let Some(found) = memchr(b'.', &segment[offset - start..]) {
3549        let index = offset + found;
3550        if (index == 0 && candidate_starts_component)
3551            || (index > 0 && is_separator(path[index - 1]))
3552        {
3553            return true;
3554        }
3555        offset = index + 1;
3556    }
3557    false
3558}
3559
3560/// Whether an ordinary star would hand a component-leading period to the
3561/// literal suffix without consuming it.
3562fn star_stops_before_hidden_component(path: &[u8], index: usize, options: PatternOptions) -> bool {
3563    !options.match_hidden
3564        && path.get(index) == Some(&b'.')
3565        && at_component_start(path, index, options)
3566}
3567
3568/// Removes the one conventional current-directory prefix accepted by path APIs.
3569fn without_leading_dot_slash(path: &[u8]) -> &[u8] {
3570    path.strip_prefix(b"./").unwrap_or(path)
3571}
3572
3573fn path_after_base<'a>(base_path: &[u8], path: &'a [u8]) -> Option<&'a [u8]> {
3574    if base_path.is_empty() {
3575        return Some(path);
3576    }
3577    let base_path = base_path
3578        .iter()
3579        .rposition(|byte| !is_separator(*byte))
3580        .map_or(&base_path[..1], |last| &base_path[..=last]);
3581    if base_path.len() == 1 && is_separator(base_path[0]) {
3582        return path.strip_prefix(base_path);
3583    }
3584    let suffix = path.strip_prefix(base_path)?;
3585    suffix
3586        .strip_prefix(b"/")
3587        .or_else(|| cfg!(windows).then(|| suffix.strip_prefix(b"\\")).flatten())
3588}
3589
3590#[derive(Debug, Clone, PartialEq, Eq)]
3591struct Class {
3592    negated: bool,
3593    members: Vec<ClassMember>,
3594}
3595
3596impl Class {
3597    fn matches(&self, byte: u8, case_insensitive: bool) -> bool {
3598        let included = self.members.iter().any(|member| match member {
3599            ClassMember::Byte(expected) => bytes_equal(*expected, byte, case_insensitive),
3600            ClassMember::Range(start, end) => {
3601                let byte = fold_ascii(byte, case_insensitive);
3602                let start = fold_ascii(*start, case_insensitive);
3603                let end = fold_ascii(*end, case_insensitive);
3604                start <= byte && byte <= end
3605            }
3606            ClassMember::Posix(class) => class.matches(byte, case_insensitive),
3607        });
3608        included != self.negated
3609    }
3610}
3611
3612#[derive(Debug, Clone, PartialEq, Eq)]
3613enum ClassMember {
3614    Byte(u8),
3615    Range(u8, u8),
3616    Posix(PosixClass),
3617}
3618
3619/// One byte collected inside `[...]`, tagged with how it was written.
3620///
3621/// Bash and glibc `fnmatch` only treat an *unescaped* `-` as the range
3622/// operator, so the escape has to be remembered until ranges are grouped.
3623#[derive(Debug, Clone, Copy, PartialEq, Eq)]
3624struct ClassValue {
3625    byte: u8,
3626    escaped: bool,
3627}
3628
3629impl ClassValue {
3630    const fn literal(byte: u8) -> Self {
3631        Self {
3632            byte,
3633            escaped: false,
3634        }
3635    }
3636
3637    const fn escaped(byte: u8) -> Self {
3638        Self {
3639            byte,
3640            escaped: true,
3641        }
3642    }
3643
3644    const fn is_range_separator(self) -> bool {
3645        self.byte == b'-' && !self.escaped
3646    }
3647}
3648
3649#[derive(Debug, Clone, Copy, PartialEq, Eq)]
3650enum PosixClass {
3651    Alnum,
3652    Alpha,
3653    Ascii,
3654    Blank,
3655    Cntrl,
3656    Digit,
3657    Graph,
3658    Lower,
3659    Print,
3660    Punct,
3661    Space,
3662    Upper,
3663    Word,
3664    Xdigit,
3665}
3666
3667const MAX_POSIX_CLASS_NAME_LEN: usize = 6;
3668
3669impl PosixClass {
3670    fn matches(self, byte: u8, case_insensitive: bool) -> bool {
3671        match self {
3672            Self::Alnum => byte.is_ascii_alphanumeric(),
3673            Self::Alpha => byte.is_ascii_alphabetic(),
3674            Self::Ascii => byte.is_ascii(),
3675            Self::Blank => matches!(byte, b' ' | b'\t'),
3676            Self::Cntrl => byte.is_ascii_control(),
3677            Self::Digit => byte.is_ascii_digit(),
3678            Self::Graph => byte.is_ascii_graphic(),
3679            Self::Lower | Self::Upper if case_insensitive => byte.is_ascii_alphabetic(),
3680            Self::Lower => byte.is_ascii_lowercase(),
3681            Self::Print => byte.is_ascii_graphic() || byte == b' ',
3682            Self::Punct => byte.is_ascii_punctuation(),
3683            Self::Space => byte.is_ascii_whitespace(),
3684            Self::Upper => byte.is_ascii_uppercase(),
3685            Self::Word => byte.is_ascii_alphanumeric() || byte == b'_',
3686            Self::Xdigit => byte.is_ascii_hexdigit(),
3687        }
3688    }
3689}
3690
3691fn parse_class(
3692    pattern: &[u8],
3693    start: usize,
3694    escapes: bool,
3695) -> Result<(Class, usize), PatternError> {
3696    let mut index = start + 1;
3697    let mut negated = false;
3698    if pattern
3699        .get(index)
3700        .is_some_and(|byte| matches!(byte, b'!' | b'^'))
3701    {
3702        negated = true;
3703        index += 1;
3704    }
3705
3706    // Each entry keeps the byte together with whether it reached the class
3707    // through a backslash escape. Only an unescaped `-` separates a range, so
3708    // the flag has to survive until `class_members` groups the entries.
3709    let mut values: Vec<ClassValue> = Vec::new();
3710    let mut members = Vec::new();
3711    if pattern.get(index) == Some(&b']') {
3712        values.push(ClassValue::literal(b']'));
3713        index += 1;
3714    }
3715    while let Some(&byte) = pattern.get(index) {
3716        if byte == b']' {
3717            if values.is_empty() && members.is_empty() {
3718                return Err(PatternError {
3719                    offset: start,
3720                    message: "empty character class",
3721                });
3722            }
3723            return Ok((
3724                Class {
3725                    negated,
3726                    members: {
3727                        members.extend(class_members(values));
3728                        members
3729                    },
3730                },
3731                index + 1,
3732            ));
3733        }
3734        if byte == b'[' && pattern.get(index + 1) == Some(&b':') {
3735            let name_start = index + 2;
3736            // POSIX class names are a closed, short set. Bounding the search
3737            // keeps every malformed opener constant-time while preserving the
3738            // old rule that only the first `:]` can close this opener.
3739            let search_end = pattern.len().min(name_start + MAX_POSIX_CLASS_NAME_LEN + 2);
3740            if let Some(posix_end) = memmem::find(&pattern[name_start..search_end], b":]")
3741                && let Some(class) = parse_posix_class(&pattern[name_start..name_start + posix_end])
3742            {
3743                let name_end = name_start + posix_end;
3744                members.extend(class_members(std::mem::take(&mut values)));
3745                members.push(ClassMember::Posix(class));
3746                index = name_end + 2;
3747                continue;
3748            }
3749        }
3750        if byte == b'\\' && escapes {
3751            let Some(&escaped) = pattern.get(index + 1) else {
3752                return Err(PatternError {
3753                    offset: index,
3754                    message: "trailing escape in character class",
3755                });
3756            };
3757            values.push(ClassValue::escaped(escaped));
3758            index += 2;
3759        } else {
3760            values.push(ClassValue::literal(byte));
3761            index += 1;
3762        }
3763    }
3764
3765    Err(PatternError {
3766        offset: start,
3767        message: "unclosed character class",
3768    })
3769}
3770
3771/// One component's only path-relevant literal shapes.
3772#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash)]
3773enum WalkerComponentKind {
3774    #[default]
3775    Empty,
3776    Dot,
3777    Parent,
3778    Other,
3779}
3780
3781/// A source location kept for a diagnostic only.
3782///
3783/// It never distinguishes two walker states: the extglob analysis
3784/// deduplicates states by what they can still match, and letting a location
3785/// split them would multiply the state set, and the IR budget it is charged
3786/// against, by the number of separators the arms of a group end in. Of states
3787/// that differ only here, the first one reached keeps its location.
3788#[derive(Debug, Clone, Copy, Default)]
3789struct DiagnosticOffset(Option<usize>);
3790
3791impl PartialEq for DiagnosticOffset {
3792    fn eq(&self, _: &Self) -> bool {
3793        true
3794    }
3795}
3796
3797impl Eq for DiagnosticOffset {}
3798
3799impl Hash for DiagnosticOffset {
3800    fn hash<H: Hasher>(&self, _: &mut H) {}
3801}
3802
3803#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash)]
3804struct WalkerComponent {
3805    kind: WalkerComponentKind,
3806    /// The first literal dot in this component, when it still has a
3807    /// determinate location in the caller's unexpanded source bytes.
3808    offset: Option<usize>,
3809    /// Where an empty component is. It has no byte of its own, so it is
3810    /// located at a separator next to it: the one that closes it (the repeated
3811    /// separator in `src//x`), or for a trailing one the separator that opens
3812    /// it. Meaningful only while `kind` is `Empty`.
3813    separator: DiagnosticOffset,
3814}
3815
3816impl WalkerComponent {
3817    /// The component that follows a separator at `offset`.
3818    const fn opened_at(offset: Option<usize>) -> Self {
3819        Self {
3820            kind: WalkerComponentKind::Empty,
3821            offset: None,
3822            separator: DiagnosticOffset(offset),
3823        }
3824    }
3825
3826    /// Records the separator at `offset` that ends this component. Only an
3827    /// empty component takes its location from it.
3828    fn close_at(&mut self, offset: Option<usize>) {
3829        if self.kind == WalkerComponentKind::Empty && offset.is_some() {
3830            self.separator = DiagnosticOffset(offset);
3831        }
3832    }
3833
3834    /// Where this component is, if it is empty and that is known.
3835    const fn empty_location(self) -> Option<usize> {
3836        match self.kind {
3837            WalkerComponentKind::Empty => self.separator.0,
3838            _ => None,
3839        }
3840    }
3841
3842    fn push_literal(&mut self, byte: u8, offset: Option<usize>) {
3843        match (self.kind, byte) {
3844            (WalkerComponentKind::Empty, b'.') => {
3845                self.kind = WalkerComponentKind::Dot;
3846                self.offset = offset;
3847            }
3848            (WalkerComponentKind::Dot, b'.') => self.kind = WalkerComponentKind::Parent,
3849            (
3850                WalkerComponentKind::Empty | WalkerComponentKind::Dot | WalkerComponentKind::Parent,
3851                _,
3852            ) => {
3853                self.kind = WalkerComponentKind::Other;
3854                self.offset = None;
3855            }
3856            (WalkerComponentKind::Other, _) => {}
3857        }
3858    }
3859
3860    fn wildcard(&mut self) {
3861        self.kind = WalkerComponentKind::Other;
3862        self.offset = None;
3863    }
3864
3865    fn append(&mut self, other: Self) {
3866        match other.kind {
3867            WalkerComponentKind::Empty => {
3868                if self.kind == WalkerComponentKind::Empty && other.separator.0.is_some() {
3869                    self.separator = other.separator;
3870                }
3871            }
3872            WalkerComponentKind::Dot => self.push_literal(b'.', other.offset),
3873            WalkerComponentKind::Parent => {
3874                self.push_literal(b'.', other.offset);
3875                self.push_literal(b'.', None);
3876            }
3877            WalkerComponentKind::Other => self.wildcard(),
3878        }
3879    }
3880}
3881
3882/// Path-shape events emitted alongside the ordinary token compiler.
3883///
3884/// This is deliberately not a second parser: each method is called from the
3885/// same match arm that creates a token. It keeps only whether a component is a
3886/// real literal dot shape, a wildcard-bearing shape, or a separator.
3887#[derive(Default)]
3888struct WalkerPathShapeBuilder {
3889    leading_dot_is_normalized: bool,
3890    components: Vec<WalkerComponent>,
3891    current: WalkerComponent,
3892}
3893
3894impl WalkerPathShapeBuilder {
3895    fn new(leading_dot_is_normalized: bool) -> Self {
3896        Self {
3897            leading_dot_is_normalized,
3898            components: Vec::new(),
3899            current: WalkerComponent::default(),
3900        }
3901    }
3902    fn literal(&mut self, byte: u8, offset: Option<usize>) {
3903        self.current.push_literal(byte, offset);
3904    }
3905
3906    fn escaped(&mut self) {
3907        self.current.wildcard();
3908    }
3909
3910    fn wildcard(&mut self) {
3911        self.current.wildcard();
3912    }
3913
3914    fn separator(&mut self, offset: Option<usize>) {
3915        let mut component =
3916            std::mem::replace(&mut self.current, WalkerComponent::opened_at(offset));
3917        component.close_at(offset);
3918        self.components.push(component);
3919    }
3920
3921    fn finish(mut self) -> WalkerPathShape {
3922        self.components.push(self.current);
3923        WalkerPathShape {
3924            leading_dot_is_normalized: self.leading_dot_is_normalized,
3925            components: self.components,
3926        }
3927    }
3928}
3929
3930#[derive(Debug, Clone, PartialEq, Eq, Hash)]
3931struct WalkerPathState {
3932    leading_dot_is_normalized: bool,
3933    /// The number of components already closed by a separator, saturated once
3934    /// the first-component distinction no longer matters.
3935    completed_components: u8,
3936    all_empty_or_dot: bool,
3937    first_parent: Option<WalkerComponent>,
3938    first_dot: Option<WalkerComponent>,
3939    last_nonempty: Option<WalkerComponent>,
3940    /// The first empty component a separator closed. Such a leading or
3941    /// interior component is not a spelling the walker can select, even when
3942    /// later matcher text makes the final component nonempty.
3943    first_empty: Option<WalkerComponent>,
3944    current: WalkerComponent,
3945}
3946
3947impl WalkerPathState {
3948    fn new(leading_dot_is_normalized: bool) -> Self {
3949        Self {
3950            leading_dot_is_normalized,
3951            completed_components: 0,
3952            all_empty_or_dot: true,
3953            first_parent: None,
3954            first_dot: None,
3955            last_nonempty: None,
3956            first_empty: None,
3957            current: WalkerComponent::default(),
3958        }
3959    }
3960    fn literal(&mut self, byte: u8, offset: Option<usize>) {
3961        self.current.push_literal(byte, offset);
3962    }
3963
3964    fn escaped(&mut self) {
3965        // An escape is matcher text, including for `\.` and `\/`. It may
3966        // match a punctuation byte but does not request filesystem path
3967        // normalization.
3968        self.current.wildcard();
3969    }
3970
3971    fn wildcard(&mut self) {
3972        self.current.wildcard();
3973    }
3974
3975    fn separator(&mut self, offset: Option<usize>) {
3976        let mut component =
3977            std::mem::replace(&mut self.current, WalkerComponent::opened_at(offset));
3978        component.close_at(offset);
3979        if component.kind == WalkerComponentKind::Empty && self.first_empty.is_none() {
3980            self.first_empty = Some(component);
3981        }
3982        self.record_component(component);
3983    }
3984
3985    fn append_shape(&mut self, shape: &WalkerPathShape) {
3986        for (index, component) in shape.components.iter().copied().enumerate() {
3987            self.current.append(component);
3988            if index + 1 != shape.components.len() {
3989                // The shape's empty components already carry their separator
3990                // locations, which `append` has just carried over.
3991                self.separator(None);
3992            }
3993        }
3994    }
3995
3996    fn record_component(&mut self, component: WalkerComponent) {
3997        let first = self.completed_components == 0;
3998        if !matches!(
3999            component.kind,
4000            WalkerComponentKind::Empty | WalkerComponentKind::Dot
4001        ) {
4002            self.all_empty_or_dot = false;
4003        }
4004        if component.kind == WalkerComponentKind::Parent && self.first_parent.is_none() {
4005            self.first_parent = Some(component);
4006        }
4007        if component.kind == WalkerComponentKind::Dot
4008            && (!first || !self.leading_dot_is_normalized)
4009            && self.first_dot.is_none()
4010        {
4011            self.first_dot = Some(component);
4012        }
4013        if component.kind != WalkerComponentKind::Empty {
4014            self.last_nonempty = Some(component);
4015        }
4016        self.completed_components = self.completed_components.saturating_add(1).min(2);
4017    }
4018
4019    fn finish(mut self) -> WalkerPathEvaluation {
4020        let component = std::mem::take(&mut self.current);
4021        let empty_component = self
4022            .first_empty
4023            .or((component.kind == WalkerComponentKind::Empty).then_some(component));
4024        self.record_component(component);
4025        WalkerPathEvaluation {
4026            selects_candidate: empty_component.is_none(),
4027            empty_component_offset: empty_component.and_then(WalkerComponent::empty_location),
4028            problem: WalkerPathSummary {
4029                all_empty_or_dot: self.all_empty_or_dot,
4030                first_parent: self.first_parent,
4031                first_dot: self.first_dot,
4032                last_nonempty: self.last_nonempty,
4033            }
4034            .problem(),
4035        }
4036    }
4037}
4038
4039#[derive(Clone, Copy)]
4040struct WalkerPathEvaluation {
4041    /// A walker candidate ends in a nonempty component and contains no empty
4042    /// leading or interior component. Nullable groups must not use an empty
4043    /// arm as a viable escape from their real invalid arms.
4044    selects_candidate: bool,
4045    /// Where the empty component that makes this path unselectable is.
4046    empty_component_offset: Option<usize>,
4047    problem: Option<WalkerPathProblem>,
4048}
4049
4050impl WalkerPathEvaluation {
4051    fn complete_problem(self) -> Option<WalkerPathProblem> {
4052        self.problem.or_else(|| {
4053            (!self.selects_candidate).then_some(WalkerPathProblem {
4054                viability: WalkerPathViability::EmptyComponent,
4055                offset: self.empty_component_offset,
4056            })
4057        })
4058    }
4059}
4060
4061#[derive(Debug, Clone, Copy)]
4062struct WalkerPathSummary {
4063    all_empty_or_dot: bool,
4064    first_parent: Option<WalkerComponent>,
4065    first_dot: Option<WalkerComponent>,
4066    last_nonempty: Option<WalkerComponent>,
4067}
4068
4069impl WalkerPathSummary {
4070    fn problem(self) -> Option<WalkerPathProblem> {
4071        if self.all_empty_or_dot {
4072            return Some(WalkerPathProblem {
4073                viability: WalkerPathViability::Root,
4074                offset: self.last_nonempty.and_then(|component| component.offset),
4075            });
4076        }
4077        if let Some(component) = self.first_parent {
4078            return Some(WalkerPathProblem {
4079                viability: WalkerPathViability::ParentComponent,
4080                offset: component.offset,
4081            });
4082        }
4083        if self
4084            .last_nonempty
4085            .is_some_and(|component| component.kind == WalkerComponentKind::Dot)
4086        {
4087            return Some(WalkerPathProblem {
4088                viability: WalkerPathViability::TrailingDot,
4089                offset: self.last_nonempty.and_then(|component| component.offset),
4090            });
4091        }
4092        self.first_dot.map(|component| WalkerPathProblem {
4093            viability: WalkerPathViability::DotComponent,
4094            offset: component.offset,
4095        })
4096    }
4097}
4098
4099#[derive(Debug, Clone, PartialEq, Eq, Default)]
4100struct WalkerPathShape {
4101    leading_dot_is_normalized: bool,
4102    components: Vec<WalkerComponent>,
4103}
4104
4105impl WalkerPathShape {
4106    fn has_separator(&self) -> bool {
4107        self.components.len() > 1
4108    }
4109
4110    fn is_empty(&self) -> bool {
4111        matches!(
4112            self.components.as_slice(),
4113            [WalkerComponent {
4114                kind: WalkerComponentKind::Empty,
4115                ..
4116            }]
4117        )
4118    }
4119
4120    fn problem(&self) -> Option<WalkerPathProblem> {
4121        let mut state = WalkerPathState::new(self.leading_dot_is_normalized);
4122        state.append_shape(self);
4123        state.finish().complete_problem()
4124    }
4125}
4126
4127#[derive(Debug, Clone, Copy)]
4128struct WalkerPathProblem {
4129    viability: WalkerPathViability,
4130    offset: Option<usize>,
4131}
4132
4133/// The semantic summary of the alternatives the compiler actually built.
4134///
4135/// Brace expansion happens before this sees an alternative, while extglob
4136/// alternatives arrive through their compiled branch objects. The walker
4137/// policy therefore never needs a second source grammar.
4138fn walker_path_analysis(
4139    alternatives: &[CompiledAlternative],
4140    budget: &mut IrBudget,
4141) -> Result<(WalkerPathViability, Option<usize>), PatternError> {
4142    let mut first_problem = None;
4143    let mut saw_viable = false;
4144    for alternative in alternatives {
4145        match alternative.walker_path_problem(budget)? {
4146            None => saw_viable = true,
4147            Some(problem) if problem.viability == WalkerPathViability::ParentComponent => {
4148                return Ok((problem.viability, problem.offset));
4149            }
4150            Some(problem) => {
4151                first_problem.get_or_insert(problem);
4152            }
4153        };
4154    }
4155    if saw_viable {
4156        return Ok((WalkerPathViability::Viable, None));
4157    }
4158    match first_problem {
4159        Some(problem) => Ok((problem.viability, problem.offset)),
4160        None => Ok((WalkerPathViability::Viable, None)),
4161    }
4162}
4163
4164impl CompiledAlternative {
4165    fn walker_path_problem(
4166        &self,
4167        budget: &mut IrBudget,
4168    ) -> Result<Option<WalkerPathProblem>, PatternError> {
4169        match &self.extglob {
4170            Some(program) => program.walker_path_problem(budget),
4171            None => Ok(self.walker_path_shape.problem()),
4172        }
4173    }
4174}
4175
4176impl CompiledExtglob {
4177    fn walker_path_problem(
4178        &self,
4179        budget: &mut IrBudget,
4180    ) -> Result<Option<WalkerPathProblem>, PatternError> {
4181        let mut states = vec![WalkerPathState::new(self.leading_dot_is_normalized)];
4182        let mut index = 0;
4183        while let Some(step) = self.steps.get(index) {
4184            match step {
4185                ExtglobStep::Byte(b'/') => {
4186                    let offset = self
4187                        .walker_source_provenance
4188                        .as_ref()
4189                        .and_then(|provenance| provenance.offset_at(index));
4190                    for state in &mut states {
4191                        state.separator(offset);
4192                    }
4193                    index += 1;
4194                }
4195                ExtglobStep::Byte(byte) => {
4196                    for state in &mut states {
4197                        state.literal(
4198                            *byte,
4199                            self.walker_source_provenance
4200                                .as_ref()
4201                                .and_then(|provenance| provenance.offset_at(index)),
4202                        );
4203                    }
4204                    index += 1;
4205                }
4206                ExtglobStep::Escape { .. } => {
4207                    for state in &mut states {
4208                        state.escaped();
4209                    }
4210                    index += 2;
4211                }
4212                ExtglobStep::Group(group) => {
4213                    states = self.groups[*group].apply_to(states, budget)?;
4214                    index = self.groups[*group].rest;
4215                }
4216                ExtglobStep::Star { next, .. } | ExtglobStep::Class { next, .. } => {
4217                    for state in &mut states {
4218                        state.wildcard();
4219                    }
4220                    index = *next;
4221                }
4222                ExtglobStep::Any | ExtglobStep::UnclosedGroup { .. } => {
4223                    for state in &mut states {
4224                        state.wildcard();
4225                    }
4226                    index += 1;
4227                }
4228                // A group interior is skipped by its group's `rest` jump.
4229                // Outside a group, a no-match step still occupies matcher
4230                // text, so keep the compiled program's following top-level
4231                // components visible to the walker analysis.
4232                ExtglobStep::NoMatch => {
4233                    for state in &mut states {
4234                        state.wildcard();
4235                    }
4236                    index += 1;
4237                }
4238            }
4239        }
4240        let mut first_problem = None;
4241        let mut first_unselectable = None;
4242        for state in states {
4243            let evaluation = state.finish();
4244            if !evaluation.selects_candidate {
4245                first_unselectable.get_or_insert(evaluation.empty_component_offset);
4246                continue;
4247            }
4248            let Some(problem) = evaluation.problem else {
4249                return Ok(None);
4250            };
4251            first_problem.get_or_insert(problem);
4252        }
4253        Ok(first_problem.or_else(|| {
4254            first_unselectable.map(|offset| WalkerPathProblem {
4255                viability: WalkerPathViability::EmptyComponent,
4256                offset,
4257            })
4258        }))
4259    }
4260}
4261
4262impl ExtglobGroup {
4263    fn apply_to(
4264        &self,
4265        states: Vec<WalkerPathState>,
4266        budget: &mut IrBudget,
4267    ) -> Result<Vec<WalkerPathState>, PatternError> {
4268        match self.kind {
4269            ExtglobKind::Negated => {
4270                let mut states = states;
4271                for state in &mut states {
4272                    // A negated arm can generate arbitrary matcher text. A
4273                    // later outer `/.` or `/..` remains visible after this
4274                    // placeholder.
4275                    state.wildcard();
4276                }
4277                Ok(states)
4278            }
4279            ExtglobKind::Optional => {
4280                let mut output = states.clone();
4281                output.extend(self.exact_states(&states, budget)?);
4282                Ok(deduplicate_walker_states(output))
4283            }
4284            ExtglobKind::ZeroOrMore => self.fixed_point(states, false, budget),
4285            ExtglobKind::OneOrMore => self.fixed_point(states, true, budget),
4286            ExtglobKind::ExactlyOne => self.exact_states(&states, budget),
4287        }
4288    }
4289
4290    fn fixed_point(
4291        &self,
4292        states: Vec<WalkerPathState>,
4293        require_one: bool,
4294        budget: &mut IrBudget,
4295    ) -> Result<Vec<WalkerPathState>, PatternError> {
4296        let mut seen = HashSet::new();
4297        let mut all = Vec::new();
4298        let mut frontier = if require_one {
4299            self.exact_states(&states, budget)?
4300        } else {
4301            states
4302        };
4303        frontier.retain(|state| seen.insert(state.clone()));
4304        all.extend(frontier.iter().cloned());
4305        while !frontier.is_empty() {
4306            let next = self.exact_states(&frontier, budget)?;
4307            frontier = next
4308                .into_iter()
4309                .filter(|state| seen.insert(state.clone()))
4310                .collect();
4311            all.extend(frontier.iter().cloned());
4312        }
4313        Ok(all)
4314    }
4315
4316    fn exact_states(
4317        &self,
4318        states: &[WalkerPathState],
4319        budget: &mut IrBudget,
4320    ) -> Result<Vec<WalkerPathState>, PatternError> {
4321        let mut seen = HashSet::new();
4322        let mut output = Vec::new();
4323        for alternative in &self.alternatives {
4324            for arm in &alternative.compiled {
4325                for state in states {
4326                    // The state product is compiler-owned semantic IR. Charge
4327                    // every attempted transition before it can allocate, so
4328                    // an adversarial sequence of distinct groups is bounded
4329                    // by the same limit as the matcher IR.
4330                    budget.charge(1, self.start)?;
4331                    let mut state = state.clone();
4332                    if arm.walker_path_shape.has_separator() {
4333                        state.append_shape(&arm.walker_path_shape);
4334                    } else if arm.walker_path_shape.is_empty() {
4335                        // A syntactically empty arm (`?()`/`*()`) matches no
4336                        // matcher text and therefore leaves this canonical
4337                        // path state unchanged.
4338                    } else {
4339                        // A group that remains inside its containing component
4340                        // is matcher text (`@(..)`), not a path operation.
4341                        state.wildcard();
4342                    }
4343                    if seen.insert(state.clone()) {
4344                        output.push(state);
4345                    }
4346                }
4347            }
4348        }
4349        Ok(output)
4350    }
4351}
4352
4353fn deduplicate_walker_states(states: Vec<WalkerPathState>) -> Vec<WalkerPathState> {
4354    let mut seen = HashSet::new();
4355    states
4356        .into_iter()
4357        .filter(|state| seen.insert(state.clone()))
4358        .collect()
4359}
4360
4361fn parse_posix_class(name: &[u8]) -> Option<PosixClass> {
4362    match name {
4363        b"alnum" => Some(PosixClass::Alnum),
4364        b"alpha" => Some(PosixClass::Alpha),
4365        b"ascii" => Some(PosixClass::Ascii),
4366        b"blank" => Some(PosixClass::Blank),
4367        b"cntrl" => Some(PosixClass::Cntrl),
4368        b"digit" => Some(PosixClass::Digit),
4369        b"graph" => Some(PosixClass::Graph),
4370        b"lower" => Some(PosixClass::Lower),
4371        b"print" => Some(PosixClass::Print),
4372        b"punct" => Some(PosixClass::Punct),
4373        b"space" => Some(PosixClass::Space),
4374        b"upper" => Some(PosixClass::Upper),
4375        b"word" => Some(PosixClass::Word),
4376        b"xdigit" => Some(PosixClass::Xdigit),
4377        _ => None,
4378    }
4379}
4380
4381fn class_members(values: Vec<ClassValue>) -> Vec<ClassMember> {
4382    let mut members = Vec::new();
4383    let mut index = 0;
4384    while index < values.len() {
4385        // A range needs an unescaped `-` with an endpoint before the closing
4386        // bracket. `[a\-z]`, `[a-]` and `[-a]` therefore stay literal, while an
4387        // escaped byte may still bound a range as in `[\--0]` or `[a-\-]`.
4388        if index + 2 < values.len() && values[index + 1].is_range_separator() {
4389            members.push(ClassMember::Range(
4390                values[index].byte,
4391                values[index + 2].byte,
4392            ));
4393            index += 3;
4394        } else {
4395            members.push(ClassMember::Byte(values[index].byte));
4396            index += 1;
4397        }
4398    }
4399    members
4400}
4401
4402/// Ends the current literal run. The token gets an exact copy and the run's
4403/// buffer is kept for the next one, so a literal retains no spare capacity.
4404fn flush_literals(tokens: &mut Vec<Token>, literals: &mut Vec<u8>) {
4405    if !literals.is_empty() {
4406        tokens.push(Token::Literal(literals.clone()));
4407        literals.clear();
4408    }
4409}
4410
4411/// Alternatives one pattern may expand to before it is rejected.
4412///
4413/// Brace groups multiply, so expansion is exponential in the number of groups:
4414/// ten nine-way groups fit in 100 bytes and ask for 3.5 billion alternatives.
4415/// Neither zlob 1.6.3 nor glibc `GLOB_BRACE` bounds this, so a pattern that
4416/// small takes them minutes or kills them; ferralk rejects it instead, which is
4417/// a deliberate difference recorded in the compatibility guide.
4418///
4419/// The limit is the fuzz harness's long-standing cap. It is far past any real
4420/// pattern — a language's extension list is a handful of alternatives — and
4421/// every alternative is a token program [`Pattern::is_match`] tries in turn, so
4422/// a pattern needing more is a matching problem even where it fits in memory.
4423const MAX_BRACE_ALTERNATIVES: usize = 1 << 12;
4424
4425/// Bytes brace expansion may write before a pattern is rejected.
4426///
4427/// The alternative count alone does not bound the work. Expansion rewrites the
4428/// whole pattern once per group it resolves, so a pattern of many one-way
4429/// groups costs the square of its length while expanding to a single
4430/// alternative: `{a}` repeated 200,000 times is 600 KB and took 11.8 s. And a
4431/// pattern inside the alternative budget still materializes that budget times
4432/// its own length, so 4096 alternatives of a 100 KB pattern is 400 MB and a
4433/// second, however few groups produced them.
4434///
4435/// Counting the bytes written bounds both, because both are the same quantity:
4436/// what expansion copies. 64 MiB is roughly 10 ms of copying, and orders of
4437/// magnitude past any real pattern — a language's extension list against a path
4438/// glob is a few hundred bytes, and even the largest expansion the alternative
4439/// budget admits is under a megabyte at a realistic pattern length.
4440const MAX_BRACE_EXPANSION_BYTES: usize = 1 << 26;
4441
4442/// Compiled units one pattern may build before it is rejected.
4443///
4444/// The brace budgets bound the expanded pattern *text*: how many alternatives
4445/// and how many bytes they add up to. Neither sees what compiling that text
4446/// costs, and the compiled form is far larger than its source — a token per
4447/// wildcard byte, and for an extglob a program step per byte offset of the
4448/// alternative. Total compiled size is therefore
4449/// *(units per alternative) x (alternatives)*, a third dimension the byte
4450/// budget cannot express: 20 MB of expanded text became 1.9 GB of compiled
4451/// program, from a 5 KB pattern that sat inside both other budgets.
4452///
4453/// A unit is one token, one program step, one class member, or one attempted
4454/// compiler-derived walker-state transition — a class token owns its member
4455/// list, so it is charged for both. Affix tries charge their temporary builder
4456/// and flattened node/edge representation together at the construction peak.
4457/// The transition charge bounds the product of exact extglob arms without
4458/// adding a separate viability limit. [`Token`] is 32 bytes and
4459/// [`ExtglobStep`] is 40, pinned by a test so this arithmetic cannot go stale,
4460/// which puts the ceiling around 40 MB of compiled program and tens of
4461/// milliseconds of work. That is orders of magnitude past any real pattern:
4462/// a language's extension list against a path glob is a few hundred units.
4463const MAX_COMPILED_IR_UNITS: usize = 1 << 20;
4464
4465/// Tracks compiled units across every alternative of one [`Pattern::compile`].
4466///
4467/// Charged before each allocation rather than after, so a pattern that would
4468/// pass the ceiling stops there instead of building its way past it first.
4469/// The one message the compiled-size ceiling reports.
4470const TOO_MUCH_COMPILED_IR: &str = "pattern compiles to too much";
4471
4472struct IrBudget {
4473    remaining: usize,
4474}
4475
4476impl IrBudget {
4477    const fn new() -> Self {
4478        Self {
4479            remaining: MAX_COMPILED_IR_UNITS,
4480        }
4481    }
4482
4483    /// Charges `units`, reporting the pattern as too large if they run out.
4484    ///
4485    /// `offset` is where to point the error; every caller here compiles one
4486    /// alternative of a pattern the caller never wrote, so it points at the
4487    /// start of what they did write.
4488    fn charge(&mut self, units: usize, offset: usize) -> Result<(), PatternError> {
4489        match self.remaining.checked_sub(units) {
4490            Some(remaining) => {
4491                self.remaining = remaining;
4492                Ok(())
4493            }
4494            None => Err(PatternError {
4495                offset,
4496                message: TOO_MUCH_COMPILED_IR,
4497            }),
4498        }
4499    }
4500
4501    /// Rejects a proven lower bound without consuming it.
4502    ///
4503    /// The ordinary compiler still performs the exact charge as structures
4504    /// are built. This look-ahead only avoids starting work that cannot fit.
4505    fn ensure(&self, units: usize, offset: usize) -> Result<(), PatternError> {
4506        if units <= self.remaining {
4507            Ok(())
4508        } else {
4509            Err(PatternError {
4510                offset,
4511                message: TOO_MUCH_COMPILED_IR,
4512            })
4513        }
4514    }
4515}
4516
4517/// Bounds source-span metadata independently from matcher IR.
4518///
4519/// `SourceSpan` is three machine words. Capping the count at the established
4520/// brace-copy byte ceiling keeps provenance below that ceiling while charging
4521/// every materialized span before its vector allocation.
4522struct ProvenanceBudget {
4523    remaining: usize,
4524}
4525
4526impl ProvenanceBudget {
4527    const fn new() -> Self {
4528        Self {
4529            remaining: MAX_BRACE_EXPANSION_BYTES / std::mem::size_of::<SourceSpan>(),
4530        }
4531    }
4532
4533    fn charge(&mut self, spans: usize, offset: usize) -> Result<(), PatternError> {
4534        match self.remaining.checked_sub(spans) {
4535            Some(remaining) => {
4536                self.remaining = remaining;
4537                Ok(())
4538            }
4539            None => Err(PatternError {
4540                offset,
4541                message: "brace provenance is too large",
4542            }),
4543        }
4544    }
4545}
4546
4547/// The bytes a [`Pattern`] matches for a path: [`OsStr::as_encoded_bytes`] of
4548/// anything that is `AsRef<OsStr>`.
4549///
4550/// Every matching entry point takes `impl AsRef<[u8]>`, which a [`&str`](str)
4551/// already is and a [`Path`](std::path::Path) is not. This is the one
4552/// conversion for the rest, so `path.as_os_str().as_encoded_bytes()` need not
4553/// be spelled out at every call site. It borrows and never converts: raw
4554/// filesystem bytes on Unix and lossless WTF-8 on Windows (ADR-0005), where
4555/// the matcher reads both `/` and `\` as a separator. It works with
4556/// [`Pattern::is_match`], [`Pattern::is_match_path`],
4557/// [`Pattern::is_match_glob_path`], and, mapped over a list, with
4558/// [`Pattern::filter_path_indices`].
4559///
4560/// A pattern is relative to wherever it is anchored, so pass the part of the
4561/// path below that directory; a path that `ferralk` walked has it as
4562/// `WalkEntry::relative_path`.
4563///
4564/// ```
4565/// use std::path::{Path, PathBuf};
4566///
4567/// use ferralk_glob::{Pattern, PatternOptions, path_bytes};
4568///
4569/// let pattern = Pattern::compile("src/**/*.rs", PatternOptions::walker())?;
4570///
4571/// let path: PathBuf = ["src", "bin", "main.rs"].iter().collect();
4572/// assert!(pattern.is_match_glob_path(path_bytes(&path)));
4573///
4574/// let root = Path::new("/work/project");
4575/// let joined = root.join(&path);
4576/// let relative = joined.strip_prefix(root).expect("joined below the root");
4577/// assert!(pattern.is_match_glob_path(path_bytes(relative)));
4578/// // The joined path starts with `/work`, which `src/**` does not.
4579/// assert!(!pattern.is_match_glob_path(path_bytes(&joined)));
4580///
4581/// // A list of paths, answered by index into it.
4582/// let paths = [Path::new("src/lib.rs"), Path::new("README.md")];
4583/// let selected = pattern.filter_path_indices(paths.iter().map(path_bytes));
4584/// assert_eq!(selected, [0]);
4585/// # Ok::<(), ferralk_glob::PatternError>(())
4586/// ```
4587///
4588/// [`OsStr::as_encoded_bytes`]: std::ffi::OsStr::as_encoded_bytes
4589#[must_use]
4590pub fn path_bytes(path: &(impl AsRef<OsStr> + ?Sized)) -> &[u8] {
4591    path.as_ref().as_encoded_bytes()
4592}
4593
4594/// Whether `byte` is pattern syntax under some [`PatternOptions`]: a wildcard,
4595/// a class bracket, a brace or its separator, an extglob operator, parenthesis
4596/// or separator, or the escape character itself.
4597const fn is_syntax_byte(byte: u8) -> bool {
4598    matches!(
4599        byte,
4600        b'\\'
4601            | b'*'
4602            | b'?'
4603            | b'['
4604            | b']'
4605            | b'{'
4606            | b'}'
4607            | b'('
4608            | b')'
4609            | b','
4610            | b'|'
4611            | b'!'
4612            | b'@'
4613            | b'+'
4614    )
4615}
4616
4617/// Escapes `text` so that it compiles to a pattern matching exactly `text`.
4618///
4619/// This is the counterpart of `globset::escape`, for user input or a path that
4620/// has to become part of a pattern without its bytes turning into syntax. Every
4621/// byte that is syntax under any [`PatternOptions`] gets a `\` in front of it:
4622/// `\ * ? [ ] { } ( ) , | ! @ +`. The result is therefore literal whichever
4623/// options it is compiled with, as long as
4624/// [`escape`](PatternOptions::escape) stays enabled, which it is in both
4625/// [`PatternOptions::default`] and [`PatternOptions::walker`].
4626///
4627/// Only syntax is escaped, not path structure. `/` still separates
4628/// components, and a leading `./` is still the one the path entry points
4629/// ignore, so the escaped text matches the way the unescaped path would under
4630/// that entry point. Case folding, when enabled, applies to the escaped bytes
4631/// as to any literal. Because `,` and `|` are escaped too, the result stays
4632/// literal inside a brace or extglob alternative, and a leading `!` never
4633/// reads as a walker negation.
4634///
4635/// ```
4636/// use ferralk_glob::{Pattern, PatternOptions, escape};
4637///
4638/// let name = "notes [draft] {v2}.md";
4639/// let escaped = escape(name);
4640/// assert_eq!(escaped, br"notes \[draft\] \{v2\}.md");
4641///
4642/// let pattern = Pattern::compile(&escaped, PatternOptions::walker())?;
4643/// assert!(pattern.is_match_glob_path(name));
4644/// assert!(!pattern.is_match_glob_path("notes d {v2}.md"));
4645///
4646/// // Escaped text composes with pattern syntax around it.
4647/// let mut below = escape("/work/app[1]");
4648/// below.extend_from_slice(b"/**/*.{ts,tsx}");
4649/// let pattern = Pattern::compile(&below, PatternOptions::walker())?;
4650/// assert!(pattern.is_match_glob_path("/work/app[1]/src/main.tsx"));
4651/// assert!(!pattern.is_match_glob_path("/work/app1/src/main.tsx"));
4652/// # Ok::<(), ferralk_glob::PatternError>(())
4653/// ```
4654///
4655/// [`escape_str`] does the same for a `&str` and returns a `String`.
4656#[must_use]
4657pub fn escape(text: impl AsRef<[u8]>) -> Vec<u8> {
4658    let text = text.as_ref();
4659    let mut escaped = Vec::with_capacity(text.len());
4660    for &byte in text {
4661        if is_syntax_byte(byte) {
4662            escaped.push(b'\\');
4663        }
4664        escaped.push(byte);
4665    }
4666    escaped
4667}
4668
4669/// [`escape`] for UTF-8 text, returning a `String`.
4670///
4671/// Every escaped byte is ASCII, so the result is valid UTF-8 whenever the
4672/// input is, and it can be formatted into a larger pattern directly.
4673///
4674/// ```
4675/// use ferralk_glob::{Pattern, PatternOptions, escape_str};
4676///
4677/// let root = "/work/brace{project}";
4678/// let pattern = format!("{}/**/*.ts", escape_str(root));
4679/// assert_eq!(pattern, r"/work/brace\{project\}/**/*.ts");
4680///
4681/// let pattern = Pattern::compile(&pattern, PatternOptions::walker())?;
4682/// assert!(pattern.is_match_glob_path("/work/brace{project}/src/main.ts"));
4683/// assert!(!pattern.is_match_glob_path("/work/braceproject/src/main.ts"));
4684/// # Ok::<(), ferralk_glob::PatternError>(())
4685/// ```
4686#[must_use]
4687pub fn escape_str(text: &str) -> String {
4688    let mut escaped = String::with_capacity(text.len());
4689    for character in text.chars() {
4690        if u8::try_from(character).is_ok_and(is_syntax_byte) {
4691            escaped.push('\\');
4692        }
4693        escaped.push(character);
4694    }
4695    escaped
4696}
4697
4698/// Expands brace alternatives into the plain patterns a pattern stands for.
4699///
4700/// [`Pattern::compile`] expands braces before it compiles anything; this
4701/// exposes the same expansion so callers can reason about the alternatives
4702/// themselves. Deriving a prefilter is the motivating case: every alternative
4703/// of `**/*.{ts,tsx}` ends in a literal extension, so a caller can prefilter on
4704/// `ts` and `tsx` without giving up on brace patterns.
4705///
4706/// Without [`PatternOptions::braces`] a pattern stands for itself and the
4707/// result is the input unchanged. Alternatives come back in the order the
4708/// expansion produces them, and a pattern always expands to at least one.
4709///
4710/// ```
4711/// use ferralk_glob::{PatternOptions, expand_braces};
4712///
4713/// let options = PatternOptions::default().braces(true);
4714/// let alternatives = expand_braces("**/*.{ts,tsx}", options)?;
4715/// assert_eq!(alternatives, [b"**/*.ts".to_vec(), b"**/*.tsx".to_vec()]);
4716/// # Ok::<(), ferralk_glob::PatternError>(())
4717/// ```
4718///
4719/// # Errors
4720///
4721/// Reports a pattern that exceeds the documented brace-expansion limits, so a
4722/// caller never has to assume expansion succeeds. Glob syntax is not checked
4723/// here: an unclosed brace is ordinary text, the way [`Pattern::compile`]
4724/// treats it, and anything else malformed is reported when the alternative it
4725/// belongs to is compiled.
4726pub fn expand_braces(
4727    pattern: impl AsRef<[u8]>,
4728    options: PatternOptions,
4729) -> Result<Vec<Vec<u8>>, PatternError> {
4730    let pattern = pattern.as_ref();
4731    if !options.braces {
4732        return Ok(vec![pattern.to_vec()]);
4733    }
4734    let mut provenance_budget = ProvenanceBudget::new();
4735    Ok(expand_brace_alternatives_with_provenance(
4736        pattern,
4737        None,
4738        options.escape,
4739        &mut provenance_budget,
4740    )?
4741    .into_iter()
4742    .map(|alternative| alternative.bytes)
4743    .collect())
4744}
4745
4746/// One brace alternative and the source offset of its first byte.
4747type AlternativeWithFirstOffset = (Vec<u8>, Option<usize>);
4748
4749/// [`expand_braces`], with each alternative paired with the source offset of
4750/// its first byte, or `None` for an empty alternative.
4751///
4752/// This is provenance for path-consuming embeddings: a rule about how an
4753/// alternative begins can then be reported at the byte the caller wrote
4754/// instead of at an offset into an expansion the caller never saw.
4755#[doc(hidden)]
4756pub fn expand_braces_with_first_offsets(
4757    pattern: impl AsRef<[u8]>,
4758    options: PatternOptions,
4759) -> Result<Vec<AlternativeWithFirstOffset>, PatternError> {
4760    let pattern = pattern.as_ref();
4761    let first = (!pattern.is_empty()).then_some(0);
4762    if !options.braces {
4763        return Ok(vec![(pattern.to_vec(), first)]);
4764    }
4765    let source = SourceProvenance::Contiguous { source_start: 0 };
4766    let mut provenance_budget = ProvenanceBudget::new();
4767    Ok(expand_brace_alternatives_with_provenance(
4768        pattern,
4769        Some(&source),
4770        options.escape,
4771        &mut provenance_budget,
4772    )?
4773    .into_iter()
4774    .map(|alternative| {
4775        let first = if alternative.bytes.is_empty() {
4776            None
4777        } else {
4778            alternative
4779                .source_provenance
4780                .as_ref()
4781                .and_then(|provenance| provenance.offset_at(0))
4782        };
4783        (alternative.bytes, first)
4784    })
4785    .collect())
4786}
4787
4788/// A contiguous output range that came from a contiguous source range.
4789///
4790/// Brace expansion removes delimiters and alternatives but never manufactures
4791/// matcher bytes, so a handful of these spans preserves exact offsets without
4792/// an `usize` allocation per source byte.
4793#[derive(Clone, Debug, PartialEq, Eq)]
4794struct SourceSpan {
4795    output_start: usize,
4796    source_start: usize,
4797    len: usize,
4798}
4799
4800/// Original byte provenance for a compiled byte sequence.
4801#[derive(Clone, Debug, PartialEq, Eq)]
4802enum SourceProvenance {
4803    Contiguous { source_start: usize },
4804    Spans(Vec<SourceSpan>),
4805}
4806
4807impl SourceProvenance {
4808    fn offset_at(&self, index: usize) -> Option<usize> {
4809        match self {
4810            Self::Contiguous { source_start } => Some(source_start + index),
4811            Self::Spans(spans) => spans.iter().find_map(|span| {
4812                (index >= span.output_start && index - span.output_start < span.len)
4813                    .then_some(span.source_start + index - span.output_start)
4814            }),
4815        }
4816    }
4817
4818    fn span_count_in(&self, start: usize, end: usize) -> usize {
4819        match self {
4820            Self::Contiguous { .. } => usize::from(start < end),
4821            Self::Spans(spans) => spans
4822                .iter()
4823                .filter(|span| span.output_start < end && span.output_start + span.len > start)
4824                .count(),
4825        }
4826    }
4827
4828    fn append_range(
4829        &self,
4830        start: usize,
4831        end: usize,
4832        output_start: usize,
4833        target: &mut Vec<SourceSpan>,
4834    ) {
4835        if start == end {
4836            return;
4837        }
4838        match self {
4839            Self::Contiguous { source_start } => target.push(SourceSpan {
4840                output_start,
4841                source_start: source_start + start,
4842                len: end - start,
4843            }),
4844            Self::Spans(spans) => {
4845                for span in spans {
4846                    let span_end = span.output_start + span.len;
4847                    let overlap_start = span.output_start.max(start);
4848                    let overlap_end = span_end.min(end);
4849                    if overlap_start < overlap_end {
4850                        target.push(SourceSpan {
4851                            output_start: output_start + overlap_start - start,
4852                            source_start: span.source_start + overlap_start - span.output_start,
4853                            len: overlap_end - overlap_start,
4854                        });
4855                    }
4856                }
4857            }
4858        }
4859    }
4860
4861    fn slice(
4862        &self,
4863        start: usize,
4864        end: usize,
4865        provenance_budget: &mut ProvenanceBudget,
4866        error_offset: usize,
4867    ) -> Result<Self, PatternError> {
4868        if let Self::Contiguous { source_start } = self {
4869            return Ok(Self::Contiguous {
4870                source_start: source_start + start,
4871            });
4872        }
4873        let span_count = self.span_count_in(start, end);
4874        provenance_budget.charge(span_count, error_offset)?;
4875        let mut spans = Vec::with_capacity(span_count);
4876        self.append_range(start, end, 0, &mut spans);
4877        Ok(Self::Spans(spans))
4878    }
4879}
4880
4881/// One brace-expanded byte sequence and its compact source provenance.
4882struct BraceExpansion {
4883    bytes: Vec<u8>,
4884    source_provenance: Option<SourceProvenance>,
4885}
4886
4887#[derive(Clone, Copy)]
4888struct BraceExpansionSummary {
4889    source_length: usize,
4890    alternatives: usize,
4891    minimum_length: usize,
4892    final_length_sum: usize,
4893    nodes: usize,
4894    written: usize,
4895}
4896
4897impl BraceExpansionSummary {
4898    const fn literal(length: usize) -> Self {
4899        Self {
4900            source_length: length,
4901            alternatives: 1,
4902            minimum_length: length,
4903            final_length_sum: length,
4904            nodes: 1,
4905            written: length,
4906        }
4907    }
4908
4909    /// Symbolically expands `self` before `suffix`, matching the work-list's
4910    /// left-to-right order without materializing either expansion tree.
4911    fn concat(self, suffix: Self) -> Self {
4912        let suffix_descendants = suffix.nodes.saturating_sub(1);
4913        Self {
4914            source_length: self.source_length.saturating_add(suffix.source_length),
4915            alternatives: self.alternatives.saturating_mul(suffix.alternatives),
4916            minimum_length: self.minimum_length.saturating_add(suffix.minimum_length),
4917            final_length_sum: suffix
4918                .alternatives
4919                .saturating_mul(self.final_length_sum)
4920                .saturating_add(self.alternatives.saturating_mul(suffix.final_length_sum)),
4921            nodes: self
4922                .nodes
4923                .saturating_add(self.alternatives.saturating_mul(suffix_descendants)),
4924            written: self
4925                .written
4926                .saturating_add(self.nodes.saturating_mul(suffix.source_length))
4927                .saturating_add(suffix_descendants.saturating_mul(self.final_length_sum))
4928                .saturating_add(
4929                    self.alternatives
4930                        .saturating_mul(suffix.written.saturating_sub(suffix.source_length)),
4931                ),
4932        }
4933    }
4934}
4935
4936/// Refuses a source whose syntax proves that every brace expansion together
4937/// cannot fit the compiled-IR budget.
4938///
4939/// The summary computes only the number of final alternatives and the shortest
4940/// final byte length. Syntax outside every brace arm survives in every result,
4941/// so an unconditional extglob opener proves one step table of at least that
4942/// length per alternative, while unconditional question marks each prove one
4943/// ordinary token. Ambiguous, very deep, or over-the-brace-count-limit sources
4944/// simply defer to the existing expansion and exact compiler budgets.
4945fn ensure_source_brace_compiled_ir_lower_bound(
4946    pattern: &[u8],
4947    options: PatternOptions,
4948    budget: &IrBudget,
4949    provenance_budget: &ProvenanceBudget,
4950) -> Result<(), PatternError> {
4951    // Class parsing is the only fallible syntax pass after brace expansion.
4952    // Only look ahead when every class is valid and brace expansion cannot
4953    // split or rewrite one, preserving parser-error precedence and offsets.
4954    if !source_classes_are_stable_and_valid(pattern, options.escape) {
4955        return Ok(());
4956    }
4957    let Some(summary) = brace_expansion_summary(pattern, options.escape, 0) else {
4958        return Ok(());
4959    };
4960    if summary.alternatives > MAX_BRACE_ALTERNATIVES {
4961        return Ok(());
4962    }
4963    // Preserve the earlier expansion-byte error whenever both budgets would
4964    // fail. `written` models that exact work-list counter symbolically.
4965    if summary.written > MAX_BRACE_EXPANSION_BYTES {
4966        return Ok(());
4967    }
4968    // Selecting one brace arm can split at most two existing provenance
4969    // spans. Bound every generated node by the deepest possible source path;
4970    // if that cannot fit, preserve the provenance-budget error as well.
4971    let brace_openers = unescaped_brace_openers(pattern, options.escape);
4972    let spans_per_node = 1_usize.saturating_add(brace_openers.saturating_mul(2));
4973    let provenance_upper = summary
4974        .nodes
4975        .saturating_sub(1)
4976        .saturating_mul(spans_per_node);
4977    if provenance_upper > provenance_budget.remaining {
4978        return Ok(());
4979    }
4980    let (question_marks, has_extglob) = unconditional_compiled_units(pattern, options);
4981    let per_alternative = question_marks.saturating_add(if has_extglob {
4982        summary.minimum_length
4983    } else {
4984        0
4985    });
4986    budget.ensure(summary.alternatives.saturating_mul(per_alternative), 0)
4987}
4988
4989fn unescaped_brace_openers(pattern: &[u8], escapes: bool) -> usize {
4990    let mut openers = 0_usize;
4991    let mut index = 0;
4992    while index < pattern.len() {
4993        if escapes && pattern[index] == b'\\' {
4994            index += usize::from(index + 1 < pattern.len()) + 1;
4995        } else {
4996            openers += usize::from(pattern[index] == b'{');
4997            index += 1;
4998        }
4999    }
5000    openers
5001}
5002
5003fn source_classes_are_stable_and_valid(pattern: &[u8], escapes: bool) -> bool {
5004    let mut brace_depth = 0_usize;
5005    let mut index = 0;
5006    while index < pattern.len() {
5007        if escapes && pattern[index] == b'\\' {
5008            index += usize::from(index + 1 < pattern.len()) + 1;
5009            continue;
5010        }
5011        match pattern[index] {
5012            b'{' => brace_depth += 1,
5013            b'}' => brace_depth = brace_depth.saturating_sub(1),
5014            b'[' => {
5015                let Ok((_, next)) = parse_class(pattern, index, escapes) else {
5016                    return false;
5017                };
5018                let mut class_index = index + 1;
5019                while class_index < next {
5020                    if escapes && pattern[class_index] == b'\\' {
5021                        class_index += usize::from(class_index + 1 < next) + 1;
5022                        continue;
5023                    }
5024                    if matches!(pattern[class_index], b'{' | b'}')
5025                        || (brace_depth > 0 && pattern[class_index] == b',')
5026                    {
5027                        return false;
5028                    }
5029                    class_index += 1;
5030                }
5031                index = next;
5032                continue;
5033            }
5034            _ => {}
5035        }
5036        index += 1;
5037    }
5038    true
5039}
5040
5041fn brace_expansion_summary(
5042    pattern: &[u8],
5043    escapes: bool,
5044    depth: usize,
5045) -> Option<BraceExpansionSummary> {
5046    const MAX_SUMMARY_DEPTH: usize = 128;
5047    if depth > MAX_SUMMARY_DEPTH {
5048        return None;
5049    }
5050    let Some(open) = first_unescaped_brace(pattern, escapes) else {
5051        return Some(BraceExpansionSummary::literal(pattern.len()));
5052    };
5053    let Some(close) = matching_brace(pattern, open, escapes) else {
5054        return Some(BraceExpansionSummary::literal(pattern.len()));
5055    };
5056
5057    let content = &pattern[open + 1..close];
5058    let mut group_alternatives = 0_usize;
5059    let mut group_minimum = usize::MAX;
5060    let mut group_final_length_sum = 0_usize;
5061    let mut group_nodes = 1_usize;
5062    let mut group_written = close - open + 1;
5063    for range in split_brace_alternatives(content, escapes) {
5064        let arm = brace_expansion_summary(&content[range], escapes, depth + 1)?;
5065        group_alternatives = group_alternatives.saturating_add(arm.alternatives);
5066        group_minimum = group_minimum.min(arm.minimum_length);
5067        group_final_length_sum = group_final_length_sum.saturating_add(arm.final_length_sum);
5068        group_nodes = group_nodes.saturating_add(arm.nodes);
5069        group_written = group_written.saturating_add(arm.written);
5070    }
5071    let prefix = BraceExpansionSummary::literal(open);
5072    let group = BraceExpansionSummary {
5073        source_length: close - open + 1,
5074        alternatives: group_alternatives,
5075        minimum_length: group_minimum,
5076        final_length_sum: group_final_length_sum,
5077        nodes: group_nodes,
5078        written: group_written,
5079    };
5080    let suffix = brace_expansion_summary(&pattern[close + 1..], escapes, depth + 1)?;
5081    Some(prefix.concat(group).concat(suffix))
5082}
5083
5084/// Counts compiler units whose syntax is outside every matched brace group.
5085fn unconditional_compiled_units(pattern: &[u8], options: PatternOptions) -> (usize, bool) {
5086    let mut question_marks = 0_usize;
5087    let mut has_extglob = false;
5088    let mut index = 0;
5089    while index < pattern.len() {
5090        if options.escape && pattern[index] == b'\\' {
5091            index += usize::from(index + 1 < pattern.len()) + 1;
5092            continue;
5093        }
5094        if pattern[index] == b'{'
5095            && let Some(close) = matching_brace(pattern, index, options.escape)
5096        {
5097            index = close + 1;
5098            continue;
5099        }
5100        question_marks += usize::from(pattern[index] == b'?');
5101        has_extglob |= detect_extglob_at(pattern, index).is_some();
5102        index += 1;
5103    }
5104    (question_marks, options.extglob && has_extglob)
5105}
5106
5107/// Rejects brace expansions whose unavoidable compiler structures exceed the
5108/// remaining IR budget before building any alternative's matcher program.
5109///
5110/// This deliberately remains a lower bound. Every valid class contributes its
5111/// token and owned members, every ordinary `?` contributes a token, and an
5112/// extglob-enabled alternative with extglob syntax always allocates one step
5113/// slot per byte. Literal runs and the derived fast paths are left uncounted,
5114/// so passing this guard never promises that the exact compiler will fit.
5115/// Returning `None` for an invalid class preserves the parser error that the
5116/// ordinary alternative-by-alternative compiler reports.
5117fn ensure_brace_compiled_ir_lower_bound(
5118    alternatives: &[BraceExpansion],
5119    options: PatternOptions,
5120    budget: &IrBudget,
5121) -> Result<(), PatternError> {
5122    let mut total = 0_usize;
5123    for alternative in alternatives {
5124        let Some(units) = compiled_ir_lower_bound(&alternative.bytes, options) else {
5125            return Ok(());
5126        };
5127        total = total.saturating_add(units);
5128        if total > budget.remaining {
5129            return budget.ensure(total, 0);
5130        }
5131    }
5132    budget.ensure(total, 0)
5133}
5134
5135fn compiled_ir_lower_bound(pattern: &[u8], options: PatternOptions) -> Option<usize> {
5136    let mut units = if options.extglob && contains_extglob(pattern, options.escape) {
5137        pattern.len()
5138    } else {
5139        0
5140    };
5141    let mut index = 0;
5142    while index < pattern.len() {
5143        match pattern[index] {
5144            b'\\' if options.escape => index += usize::from(index + 1 < pattern.len()) + 1,
5145            b'[' => {
5146                let (class, next) = parse_class(pattern, index, options.escape).ok()?;
5147                units = units.saturating_add(1 + class.members.len());
5148                index = next;
5149            }
5150            b'?' => {
5151                units = units.saturating_add(1);
5152                index += 1;
5153            }
5154            _ => index += 1,
5155        }
5156    }
5157    Some(units)
5158}
5159
5160/// Expands brace groups into one pattern per combination.
5161///
5162/// The work list replaces recursion, which used to be one frame per brace group
5163/// and overflowed the stack on a pattern of many small groups. Popping from the
5164/// back with the alternatives pushed in reverse keeps the original order: every
5165/// combination of the first group is emitted before the second group's.
5166///
5167/// `expanded.len() + pending.len()` only ever grows and every pending entry
5168/// yields at least one alternative, so it is a running lower bound on the final
5169/// count. Checking it before each push therefore rejects exactly the patterns
5170/// whose expansion would exceed the documented alternative limit, and bounds
5171/// memory rather than only the result.
5172///
5173/// The bytes written are counted against the documented expansion-byte limit
5174/// the same way, and that is what bounds the time: rewriting the whole pattern
5175/// per group is quadratic in its length even where it expands to one
5176/// alternative. The running total only grows too, so stopping at the first
5177/// write that exceeds the limit rejects exactly the patterns whose finished
5178/// expansion would.
5179fn expand_brace_alternatives_with_provenance(
5180    pattern: &[u8],
5181    source_provenance: Option<&SourceProvenance>,
5182    escapes: bool,
5183    provenance_budget: &mut ProvenanceBudget,
5184) -> Result<Vec<BraceExpansion>, PatternError> {
5185    let Some(first_open) = first_unescaped_brace(pattern, escapes) else {
5186        return Ok(vec![BraceExpansion {
5187            bytes: pattern.to_vec(),
5188            source_provenance: source_provenance.cloned(),
5189        }]);
5190    };
5191
5192    let mut expanded = Vec::new();
5193    let mut pending = vec![BraceExpansion {
5194        bytes: pattern.to_vec(),
5195        source_provenance: source_provenance.cloned(),
5196    }];
5197    let mut written = pattern.len();
5198    while let Some(current) = pending.pop() {
5199        let Some(open) = first_unescaped_brace(&current.bytes, escapes) else {
5200            expanded.push(current);
5201            continue;
5202        };
5203        let Some(close) = matching_brace(&current.bytes, open, escapes) else {
5204            // zlob treats an unmatched brace as ordinary text.
5205            expanded.push(current);
5206            continue;
5207        };
5208
5209        let alternatives = split_brace_alternatives(&current.bytes[open + 1..close], escapes);
5210        for range in alternatives.iter().rev() {
5211            let alternative = &current.bytes[open + 1 + range.start..open + 1 + range.end];
5212            if expanded.len() + pending.len() >= MAX_BRACE_ALTERNATIVES {
5213                return Err(PatternError {
5214                    // Offsets into a partly expanded pattern would not point
5215                    // into the caller's, so report where its expansion starts.
5216                    offset: first_open,
5217                    message: "too many brace alternatives",
5218                });
5219            }
5220            let length = open + alternative.len() + current.bytes.len() - close - 1;
5221            written = written.saturating_add(length);
5222            if written > MAX_BRACE_EXPANSION_BYTES {
5223                return Err(PatternError {
5224                    offset: first_open,
5225                    message: "brace expansion is too large",
5226                });
5227            }
5228            let mut bytes = Vec::with_capacity(length);
5229            bytes.extend_from_slice(&current.bytes[..open]);
5230            bytes.extend_from_slice(alternative);
5231            bytes.extend_from_slice(&current.bytes[close + 1..]);
5232            let source_provenance = current
5233                .source_provenance
5234                .as_ref()
5235                .map(|provenance| {
5236                    let alternative_start = open + 1 + range.start;
5237                    let span_count = provenance.span_count_in(0, open)
5238                        + provenance.span_count_in(
5239                            alternative_start,
5240                            alternative_start + alternative.len(),
5241                        )
5242                        + provenance.span_count_in(close + 1, current.bytes.len());
5243                    // This charges compact provenance before it is materialized.
5244                    // Its bounded metadata budget prevents expansion from turning
5245                    // a small matcher into an unbounded offset side allocation.
5246                    provenance_budget.charge(span_count, first_open)?;
5247                    let mut spans = Vec::with_capacity(span_count);
5248                    provenance.append_range(0, open, 0, &mut spans);
5249                    provenance.append_range(
5250                        alternative_start,
5251                        alternative_start + alternative.len(),
5252                        open,
5253                        &mut spans,
5254                    );
5255                    provenance.append_range(
5256                        close + 1,
5257                        current.bytes.len(),
5258                        open + alternative.len(),
5259                        &mut spans,
5260                    );
5261                    Ok::<_, PatternError>(SourceProvenance::Spans(spans))
5262                })
5263                .transpose()?;
5264            pending.push(BraceExpansion {
5265                bytes,
5266                source_provenance,
5267            });
5268        }
5269    }
5270    Ok(expanded)
5271}
5272
5273#[cfg(test)]
5274fn expand_brace_alternatives(pattern: &[u8], escapes: bool) -> Result<Vec<Vec<u8>>, PatternError> {
5275    let mut provenance_budget = ProvenanceBudget::new();
5276    Ok(
5277        expand_brace_alternatives_with_provenance(pattern, None, escapes, &mut provenance_budget)?
5278            .into_iter()
5279            .map(|alternative| alternative.bytes)
5280            .collect(),
5281    )
5282}
5283
5284fn first_unescaped_brace(pattern: &[u8], escapes: bool) -> Option<usize> {
5285    if !escapes {
5286        return memchr(b'{', pattern);
5287    }
5288    let mut index = 0;
5289    while index < pattern.len() {
5290        if escapes && pattern[index] == b'\\' {
5291            index += 2;
5292        } else if pattern[index] == b'{' {
5293            return Some(index);
5294        } else {
5295            index += 1;
5296        }
5297    }
5298    None
5299}
5300
5301fn matching_brace(pattern: &[u8], open: usize, escapes: bool) -> Option<usize> {
5302    let mut depth = 0_usize;
5303    let mut index = open;
5304    while index < pattern.len() {
5305        if escapes && pattern[index] == b'\\' {
5306            index += 2;
5307            continue;
5308        }
5309        match pattern[index] {
5310            b'{' => depth += 1,
5311            b'}' => {
5312                depth -= 1;
5313                if depth == 0 {
5314                    return Some(index);
5315                }
5316            }
5317            _ => {}
5318        }
5319        index += 1;
5320    }
5321    None
5322}
5323
5324fn split_brace_alternatives(content: &[u8], escapes: bool) -> Vec<std::ops::Range<usize>> {
5325    let mut alternatives = Vec::new();
5326    let mut start = 0;
5327    let mut depth = 0_usize;
5328    let mut index = 0;
5329    while index < content.len() {
5330        if escapes && content[index] == b'\\' {
5331            index += 2;
5332            continue;
5333        }
5334        match content[index] {
5335            b'{' => depth += 1,
5336            b'}' => depth -= 1,
5337            b',' if depth == 0 => {
5338                alternatives.push(start..index);
5339                start = index + 1;
5340            }
5341            _ => {}
5342        }
5343        index += 1;
5344    }
5345    alternatives.push(start..content.len());
5346    alternatives
5347}
5348
5349fn is_separator(byte: u8) -> bool {
5350    byte == b'/' || (cfg!(windows) && byte == b'\\')
5351}
5352
5353fn at_component_start(path: &[u8], index: usize, options: PatternOptions) -> bool {
5354    (index == 0 && options.candidate_starts_component)
5355        || (index > 0 && path.get(index - 1).is_some_and(|byte| is_separator(*byte)))
5356}
5357
5358fn bytes_equal(expected: u8, actual: u8, case_insensitive: bool) -> bool {
5359    fold_ascii(expected, case_insensitive) == fold_ascii(actual, case_insensitive)
5360}
5361
5362fn fold_ascii(byte: u8, case_insensitive: bool) -> u8 {
5363    if case_insensitive {
5364        byte.to_ascii_lowercase()
5365    } else {
5366        byte
5367    }
5368}
5369
5370#[derive(Debug, Clone, Copy, PartialEq, Eq)]
5371enum ExtglobKind {
5372    Optional,
5373    ZeroOrMore,
5374    OneOrMore,
5375    ExactlyOne,
5376    Negated,
5377}
5378
5379/// A compiled extglob pattern.
5380///
5381/// One step per byte offset of the raw pattern. The interpreter this replaces
5382/// jumped by byte offset — into a group's rest, back to the last star, past a
5383/// class — so keeping offsets as the addressing scheme reproduces its control
5384/// flow exactly, while every parse it redid on each call happens once here.
5385///
5386/// Steps are filled only for offsets a walk can reach: a group jumps past its
5387/// own content, whose alternatives are compiled separately. An unreachable
5388/// offset keeps [`ExtglobStep::NoMatch`], so nothing depends on the interior
5389/// of a group being classifiable on its own.
5390#[derive(Debug, Clone, PartialEq, Eq)]
5391struct CompiledExtglob {
5392    /// The ordinary bytes the program starts with. Every engine consumes
5393    /// them one for one, so a candidate that does not start with them is
5394    /// rejected before either engine runs, as the plain prefilter does.
5395    literal_prefix: Vec<u8>,
5396    steps: Vec<ExtglobStep>,
5397    groups: Vec<ExtglobGroup>,
5398    /// Linear-space Thompson machine for eligible positive extglobs.
5399    /// Negation needs substring-complement semantics, while plain outer stars
5400    /// retain legacy backtracking quirks; those cases stay on the compatible
5401    /// explicit worklist below.
5402    positive_nfa: Option<PositiveExtglobNfa>,
5403    /// Dense memo state for every interpreter offset that participates in the
5404    /// recurrence: the entry point, each group start and continuation, plus
5405    /// the zero-directory continuation after a recursive `**/` prefix.
5406    /// The byte-indexed step table remains the interpreter's address space.
5407    memo_state_indices: Vec<usize>,
5408    memo_state_count: usize,
5409    /// The step offset of a separator that a candidate ending right in front
5410    /// of it may leave out, because only a trailing whole-component `**`
5411    /// follows it; see [`trailing_globstar_separator`].
5412    trailing_globstar_separator: Option<usize>,
5413    /// Original-source byte offsets for `steps`, retained after brace
5414    /// expansion so walker diagnostics preserve `PatternError` provenance.
5415    walker_source_provenance: Option<SourceProvenance>,
5416    leading_dot_is_normalized: bool,
5417}
5418
5419/// What the walk does at one byte offset.
5420#[derive(Debug, Clone, PartialEq, Eq)]
5421enum ExtglobStep {
5422    /// Nothing matches here; the walk falls back to its last star. Also the
5423    /// filler for an offset no walk reaches.
5424    NoMatch,
5425    /// A run of `*`, resuming at the offset after it.
5426    Star {
5427        next: usize,
5428        /// Ordinary stars cannot stop immediately before a leading period.
5429        /// A syntactic recursive `**/` prefix is the sole exemption.
5430        blocks_leading_period: bool,
5431    },
5432    /// `?`.
5433    Any,
5434    /// A bracket class, resuming at the offset after it.
5435    Class { class: Class, next: usize },
5436    /// A backslash with a byte to escape. Only the escaped byte matches, and
5437    /// the walk skips both offsets: the backslash is never read as a literal
5438    /// with the escaped byte as syntax, exactly as in the plain engines and
5439    /// in Bash.
5440    Escape { escaped: u8 },
5441    /// An ordinary byte.
5442    Byte(u8),
5443    /// An extglob group, indexing [`CompiledExtglob::groups`].
5444    Group(usize),
5445    /// A group opener whose parenthesis never closes. It still refuses a
5446    /// leading period, then reads `byte` as ordinary text.
5447    UnclosedGroup { byte: u8 },
5448}
5449
5450#[derive(Debug, Clone, PartialEq, Eq)]
5451struct PositiveExtglobNfa {
5452    states: Vec<PositiveExtglobState>,
5453    start: usize,
5454}
5455
5456#[derive(Debug, Clone, PartialEq, Eq)]
5457enum PositiveExtglobState {
5458    Epsilon {
5459        targets: Vec<usize>,
5460        blocks_leading_period: bool,
5461    },
5462    Consume {
5463        matcher: PositiveExtglobMatcher,
5464        next: usize,
5465    },
5466    Star {
5467        matcher: PositiveExtglobMatcher,
5468        next: usize,
5469        blocks_leading_period: bool,
5470        /// A `**/` prefix hands over only at a component start (#419).
5471        stops_at_component_start: bool,
5472    },
5473    Match,
5474}
5475
5476#[derive(Debug, Clone, PartialEq, Eq)]
5477enum PositiveExtglobMatcher {
5478    Literal(u8),
5479    GroupLiteral(u8),
5480    GroupSeparator,
5481    Wildcard(WildcardScope),
5482    Class(Class, WildcardScope),
5483}
5484
5485#[derive(Debug, Clone, Copy, PartialEq, Eq)]
5486enum WildcardScope {
5487    /// An outer-program wildcard and whether its source position follows an
5488    /// explicit separator.
5489    Extglob(bool),
5490    /// A group-alternative wildcard and whether its source position is the
5491    /// component-local root of that alternative or follows its own separator.
5492    Group(bool),
5493    /// An explicit recursive wildcard, which crosses separators under every
5494    /// entry point.
5495    Recursive,
5496}
5497
5498struct PositiveExtglobBuilder<'budget> {
5499    states: Vec<PositiveExtglobState>,
5500    budget: &'budget mut IrBudget,
5501}
5502
5503#[derive(Default)]
5504struct PositiveExtglobClosureScratch {
5505    seen: Vec<bool>,
5506    pending: Vec<usize>,
5507}
5508
5509#[derive(Default)]
5510struct PositiveExtglobMatchScratch {
5511    current: Vec<usize>,
5512    next: Vec<usize>,
5513    closure: PositiveExtglobClosureScratch,
5514}
5515
5516impl PositiveExtglobMatchScratch {
5517    fn prepare(&mut self, state_count: usize) {
5518        self.current.clear();
5519        self.next.clear();
5520        self.closure.seen.resize(state_count, false);
5521        self.closure.seen.fill(false);
5522        self.closure.pending.clear();
5523    }
5524
5525    fn release(&mut self) {
5526        self.current.clear();
5527        if self.current.capacity() > RETAINED_SCRATCH_WORDS {
5528            self.current.shrink_to(RETAINED_SCRATCH_WORDS);
5529        }
5530        self.next.clear();
5531        if self.next.capacity() > RETAINED_SCRATCH_WORDS {
5532            self.next.shrink_to(RETAINED_SCRATCH_WORDS);
5533        }
5534        self.closure.pending.clear();
5535        if self.closure.pending.capacity() > RETAINED_SCRATCH_WORDS {
5536            self.closure.pending.shrink_to(RETAINED_SCRATCH_WORDS);
5537        }
5538        let retained_seen_bits = RETAINED_SCRATCH_WORDS * u64::BITS as usize;
5539        if self.closure.seen.capacity() > retained_seen_bits {
5540            self.closure.seen.clear();
5541            self.closure.seen.shrink_to(retained_seen_bits);
5542        }
5543    }
5544}
5545
5546// A positive NFA touches the same four state buffers for every candidate, so
5547// keep their allocation per thread just like the general and fallback engines.
5548thread_local! {
5549    static POSITIVE_EXTGLOB_SCRATCH: RefCell<PositiveExtglobMatchScratch> =
5550        RefCell::new(PositiveExtglobMatchScratch::default());
5551}
5552
5553/// One alternative of a group, compiled as a whole-candidate pattern.
5554#[derive(Debug, Clone, PartialEq, Eq)]
5555struct ExtglobAlternative {
5556    compiled: Vec<CompiledAlternative>,
5557    /// Byte width when every token consumes a fixed count, which lets the scan
5558    /// over candidate end offsets visit one offset instead of the component.
5559    width: Option<usize>,
5560    /// Whether [`ExtglobGroup::through_separator`] holds this alternative
5561    /// with the separator behind the group, which the matchers use instead.
5562    /// The alternative itself stays for the compile-time analyses, which
5563    /// read the separator from the outer program.
5564    absorbed_by_separator: bool,
5565}
5566
5567/// One `?(…)`, `*(…)`, `+(…)`, `@(…)` or `!(…)`.
5568#[derive(Debug, Clone, PartialEq, Eq)]
5569struct ExtglobGroup {
5570    kind: ExtglobKind,
5571    alternatives: Vec<ExtglobAlternative>,
5572    /// Offset of this group's operator in the program.
5573    start: usize,
5574    /// Offset just past the closing parenthesis.
5575    rest: usize,
5576    /// Whether the group operator follows an explicit separator in the outer
5577    /// program. Its alternatives inherit that position at their root.
5578    component_local: bool,
5579    /// Whether some alternative spells a separator or a recursive star, so a
5580    /// match can span components even where every ordinary wildcard is
5581    /// component-local.
5582    spans_components: bool,
5583    /// Whether some alternative starts with an ordinary wildcard, the one
5584    /// token whose locality follows the group's position.
5585    root_wildcard: bool,
5586    /// Every alternative that ends in a whole-component `**` right before a
5587    /// separator of the outer program, compiled together with that separator
5588    /// and resuming behind it (#422). Written inline, the `**` and the
5589    /// separator are one `**/`, which may stand for no directory at all, so
5590    /// `@(**)/y` matches `y` as `**/y` does, and which leaves no trailing
5591    /// `/**` for the path to stop in front of, so `@(a/**)/**` refuses `a` as
5592    /// `a/**/**` does. Such an alternative can only be the last iteration of
5593    /// a repetition. Empty for a negated group, which consumes what its
5594    /// alternatives reject rather than their text.
5595    through_separator: Vec<ExtglobAlternative>,
5596}
5597
5598impl ExtglobGroup {
5599    /// Whether the first iteration of a repetition reads its root wildcard
5600    /// differently from a later one, which follows the previous iteration's
5601    /// text rather than the separator in front of the group.
5602    fn first_iteration_differs(&self) -> bool {
5603        self.component_local && self.root_wildcard
5604    }
5605
5606    /// The alternatives the matchers run with the continuation behind the
5607    /// group; the rest are matched through [`Self::through_separator`].
5608    fn matched_alternatives(&self) -> impl Iterator<Item = &ExtglobAlternative> {
5609        self.alternatives
5610            .iter()
5611            .filter(|alternative| !alternative.absorbed_by_separator)
5612    }
5613
5614    /// Whether the interpreter matches the first iteration on its own, which
5615    /// it does where that difference is observable: under the list-filter
5616    /// policy.
5617    fn first_iteration_apart(&self, options: PatternOptions) -> bool {
5618        self.first_iteration_differs()
5619            && options.component_wildcards
5620            && !options.root_component_wildcards
5621    }
5622
5623    /// The root position the interpreter gives an iteration after the first.
5624    fn later_iteration_root_local(&self, options: PatternOptions) -> bool {
5625        self.component_local && !self.first_iteration_apart(options)
5626    }
5627
5628    /// The last candidate offset a match of one alternative can end at.
5629    ///
5630    /// A negated group consumes the text its alternatives reject, a
5631    /// wildcard-like span that stays in the component under a
5632    /// component-local position. A positive alternative answers the position
5633    /// rule itself: its root wildcard is component-local exactly when the
5634    /// group is, and a wildcard behind it crosses again under the list-filter
5635    /// rule, as in `a/b*`. Bounding it by the component end is only a
5636    /// shortcut, taken when every ordinary wildcard is component-local and no
5637    /// alternative spells a separator or a recursive star.
5638    fn alternative_end_limit(
5639        &self,
5640        path: &[u8],
5641        path_index: usize,
5642        options: PatternOptions,
5643    ) -> usize {
5644        if self.kind == ExtglobKind::Negated {
5645            return extglob_component_end(path, path_index, options, self.component_local);
5646        }
5647        if options.component_wildcards && options.root_component_wildcards && !self.spans_components
5648        {
5649            return extglob_component_end(path, path_index, options, true);
5650        }
5651        path.len()
5652    }
5653}
5654
5655impl PositiveExtglobNfa {
5656    fn compile(
5657        steps: &[ExtglobStep],
5658        groups: &[ExtglobGroup],
5659        trailing_globstar_separator: Option<usize>,
5660        budget: &mut IrBudget,
5661    ) -> Result<Option<Self>, PatternError> {
5662        if groups
5663            .iter()
5664            .any(|group| group.kind == ExtglobKind::Negated)
5665            || steps.iter().any(|step| {
5666                matches!(
5667                    step,
5668                    ExtglobStep::Star { .. } | ExtglobStep::UnclosedGroup { byte: b'*' }
5669                )
5670            })
5671        {
5672            return Ok(None);
5673        }
5674        let mut builder = PositiveExtglobBuilder {
5675            states: Vec::new(),
5676            budget,
5677        };
5678        let accept = builder.push(PositiveExtglobState::Match, 0)?;
5679        let mut suffixes = vec![None; steps.len() + 1];
5680        suffixes[steps.len()] = Some(accept);
5681
5682        for index in (0..steps.len()).rev() {
5683            let start =
5684                match &steps[index] {
5685                    ExtglobStep::NoMatch => None,
5686                    ExtglobStep::Byte(byte) => builder
5687                        .consume(PositiveExtglobMatcher::Literal(*byte), suffixes[index + 1])?,
5688                    ExtglobStep::Any => builder.consume(
5689                        PositiveExtglobMatcher::Wildcard(WildcardScope::Extglob(
5690                            extglob_step_component_local(steps, index),
5691                        )),
5692                        suffixes[index + 1],
5693                    )?,
5694                    ExtglobStep::Class { class, next } => builder.consume(
5695                        PositiveExtglobMatcher::Class(
5696                            class.clone(),
5697                            WildcardScope::Extglob(extglob_step_component_local(steps, index)),
5698                        ),
5699                        suffixes[*next],
5700                    )?,
5701                    ExtglobStep::Star { .. } => {
5702                        unreachable!("outer stars keep the compatible interpreter")
5703                    }
5704                    ExtglobStep::Escape { escaped } => builder.consume(
5705                        PositiveExtglobMatcher::Literal(*escaped),
5706                        suffixes[index + 2],
5707                    )?,
5708                    ExtglobStep::Group(group) => {
5709                        let group = &groups[*group];
5710                        let behind_separator = if group.through_separator.is_empty() {
5711                            None
5712                        } else {
5713                            suffixes[group.rest + 1]
5714                        };
5715                        builder.group(group, suffixes[group.rest], behind_separator)?
5716                    }
5717                    ExtglobStep::UnclosedGroup { byte: b'*' } => {
5718                        unreachable!("unclosed outer stars keep the compatible interpreter")
5719                    }
5720                    ExtglobStep::UnclosedGroup { byte: b'?' } => builder.consume(
5721                        PositiveExtglobMatcher::Wildcard(WildcardScope::Extglob(
5722                            extglob_step_component_local(steps, index),
5723                        )),
5724                        suffixes[index + 1],
5725                    )?,
5726                    ExtglobStep::UnclosedGroup { byte } => builder
5727                        .consume(PositiveExtglobMatcher::Literal(*byte), suffixes[index + 1])?,
5728                };
5729            // A candidate may end in front of the separator of a trailing
5730            // `/**`, as `x/**` accepts `x` (#422).
5731            let start = if trailing_globstar_separator == Some(index) {
5732                builder.epsilon(start.into_iter().chain([accept]).collect(), false)?
5733            } else {
5734                start
5735            };
5736            suffixes[index] = start;
5737        }
5738        Ok(suffixes[0].map(|start| Self {
5739            states: builder.states,
5740            start,
5741        }))
5742    }
5743
5744    fn is_match(&self, path: &[u8], options: PatternOptions) -> bool {
5745        POSITIVE_EXTGLOB_SCRATCH.with(|cell| match cell.try_borrow_mut() {
5746            Ok(mut scratch) => {
5747                scratch.prepare(self.states.len());
5748                let matched = self.is_match_with_scratch(path, options, &mut scratch);
5749                scratch.release();
5750                matched
5751            }
5752            Err(_) => {
5753                let mut scratch = PositiveExtglobMatchScratch::default();
5754                scratch.prepare(self.states.len());
5755                self.is_match_with_scratch(path, options, &mut scratch)
5756            }
5757        })
5758    }
5759
5760    fn is_match_with_scratch(
5761        &self,
5762        path: &[u8],
5763        options: PatternOptions,
5764        scratch: &mut PositiveExtglobMatchScratch,
5765    ) -> bool {
5766        let PositiveExtglobMatchScratch {
5767            current,
5768            next,
5769            closure,
5770        } = scratch;
5771        self.add_closure(
5772            self.start,
5773            path.first().copied(),
5774            options.candidate_starts_component,
5775            options,
5776            current,
5777            closure,
5778        );
5779
5780        let mut at_component_start = options.candidate_starts_component;
5781        for (path_index, &byte) in path.iter().enumerate() {
5782            next.clear();
5783            closure.seen.fill(false);
5784            let next_byte = path.get(path_index + 1).copied();
5785            let next_starts_component = is_separator(byte);
5786            for &state in current.iter() {
5787                match &self.states[state] {
5788                    PositiveExtglobState::Consume {
5789                        matcher,
5790                        next: target,
5791                    } if matcher.matches(byte, at_component_start, options) => {
5792                        self.add_closure(
5793                            *target,
5794                            next_byte,
5795                            next_starts_component,
5796                            options,
5797                            next,
5798                            closure,
5799                        );
5800                    }
5801                    PositiveExtglobState::Star { matcher, .. }
5802                        if matcher.matches(byte, at_component_start, options) =>
5803                    {
5804                        self.add_closure(
5805                            state,
5806                            next_byte,
5807                            next_starts_component,
5808                            options,
5809                            next,
5810                            closure,
5811                        );
5812                    }
5813                    _ => {}
5814                }
5815            }
5816            if next.is_empty() {
5817                return false;
5818            }
5819            std::mem::swap(current, next);
5820            at_component_start = next_starts_component;
5821        }
5822        current
5823            .iter()
5824            .any(|&state| matches!(self.states[state], PositiveExtglobState::Match))
5825    }
5826
5827    fn add_closure(
5828        &self,
5829        start: usize,
5830        byte: Option<u8>,
5831        at_component_start: bool,
5832        options: PatternOptions,
5833        active: &mut Vec<usize>,
5834        scratch: &mut PositiveExtglobClosureScratch,
5835    ) {
5836        scratch.pending.clear();
5837        scratch.pending.push(start);
5838        while let Some(state) = scratch.pending.pop() {
5839            if std::mem::replace(&mut scratch.seen[state], true) {
5840                continue;
5841            }
5842            match &self.states[state] {
5843                PositiveExtglobState::Epsilon {
5844                    targets,
5845                    blocks_leading_period,
5846                } => {
5847                    if *blocks_leading_period
5848                        && !options.match_hidden
5849                        && at_component_start
5850                        && byte == Some(b'.')
5851                    {
5852                        continue;
5853                    }
5854                    scratch.pending.extend(targets.iter().copied());
5855                }
5856                PositiveExtglobState::Star {
5857                    next,
5858                    blocks_leading_period,
5859                    stops_at_component_start,
5860                    ..
5861                } => {
5862                    active.push(state);
5863                    if !(*blocks_leading_period
5864                        && !options.match_hidden
5865                        && at_component_start
5866                        && byte == Some(b'.'))
5867                        && (!*stops_at_component_start || at_component_start)
5868                    {
5869                        scratch.pending.push(*next);
5870                    }
5871                }
5872                PositiveExtglobState::Consume { .. } | PositiveExtglobState::Match => {
5873                    active.push(state);
5874                }
5875            }
5876        }
5877    }
5878}
5879
5880/// Scratch capacities after a positive-extglob NFA match, used to verify that
5881/// repeated matches reuse bounded thread-local allocations.
5882#[cfg(test)]
5883fn positive_extglob_scratch_capacities() -> (usize, usize, usize, usize) {
5884    POSITIVE_EXTGLOB_SCRATCH.with(|cell| {
5885        let scratch = cell.borrow();
5886        (
5887            scratch.current.capacity(),
5888            scratch.next.capacity(),
5889            scratch.closure.seen.capacity(),
5890            scratch.closure.pending.capacity(),
5891        )
5892    })
5893}
5894
5895impl PositiveExtglobMatcher {
5896    fn matches(&self, byte: u8, at_component_start: bool, options: PatternOptions) -> bool {
5897        match self {
5898            Self::Literal(expected) => bytes_equal(*expected, byte, options.case_insensitive),
5899            Self::GroupLiteral(expected) => bytes_equal(*expected, byte, options.case_insensitive),
5900            Self::GroupSeparator => is_separator(byte),
5901            Self::Wildcard(scope) => scope.accepts(byte, at_component_start, options),
5902            Self::Class(class, scope) => {
5903                scope.accepts(byte, at_component_start, options)
5904                    && class.matches(byte, options.case_insensitive)
5905            }
5906        }
5907    }
5908}
5909
5910impl WildcardScope {
5911    fn accepts(self, byte: u8, at_component_start: bool, options: PatternOptions) -> bool {
5912        let component_local = match self {
5913            Self::Extglob(position_local) | Self::Group(position_local) => {
5914                options.component_wildcards && (options.root_component_wildcards || position_local)
5915            }
5916            Self::Recursive => false,
5917        };
5918        (!is_separator(byte) || !component_local)
5919            && (options.match_hidden || byte != b'.' || !at_component_start)
5920    }
5921}
5922
5923fn token_position_component_local(
5924    tokens: &[Token],
5925    token_index: usize,
5926    root_component_local: bool,
5927) -> bool {
5928    token_index == 0 && root_component_local
5929        || token_index > 0 && matches!(tokens[token_index - 1], Token::Separator)
5930}
5931
5932fn extglob_step_component_local(steps: &[ExtglobStep], step_index: usize) -> bool {
5933    step_index > 0 && matches!(steps[step_index - 1], ExtglobStep::Byte(byte) if is_separator(byte))
5934}
5935
5936impl PositiveExtglobBuilder<'_> {
5937    fn push(
5938        &mut self,
5939        state: PositiveExtglobState,
5940        edge_count: usize,
5941    ) -> Result<usize, PatternError> {
5942        // A state contains an enum payload and every edge is one stored index.
5943        // Charging two IR units for the payload plus one per edge deliberately
5944        // overestimates the actual allocation before it is made.
5945        self.budget.charge(2 + edge_count, 0)?;
5946        let index = self.states.len();
5947        self.states.push(state);
5948        Ok(index)
5949    }
5950
5951    fn consume(
5952        &mut self,
5953        matcher: PositiveExtglobMatcher,
5954        next: Option<usize>,
5955    ) -> Result<Option<usize>, PatternError> {
5956        next.map(|next| self.push(PositiveExtglobState::Consume { matcher, next }, 1))
5957            .transpose()
5958    }
5959
5960    fn star(
5961        &mut self,
5962        matcher: PositiveExtglobMatcher,
5963        next: Option<usize>,
5964        semantics: StarSemantics,
5965    ) -> Result<Option<usize>, PatternError> {
5966        next.map(|next| {
5967            self.push(
5968                PositiveExtglobState::Star {
5969                    matcher,
5970                    next,
5971                    blocks_leading_period: semantics.blocks_hidden_stop,
5972                    stops_at_component_start: semantics.stops_at_component_start,
5973                },
5974                1,
5975            )
5976        })
5977        .transpose()
5978    }
5979
5980    fn epsilon(
5981        &mut self,
5982        mut targets: Vec<usize>,
5983        blocks_leading_period: bool,
5984    ) -> Result<Option<usize>, PatternError> {
5985        targets.sort_unstable();
5986        targets.dedup();
5987        if targets.is_empty() {
5988            return Ok(None);
5989        }
5990        if targets.len() == 1 && !blocks_leading_period {
5991            return Ok(targets.first().copied());
5992        }
5993        let edge_count = targets.len();
5994        self.push(
5995            PositiveExtglobState::Epsilon {
5996                targets,
5997                blocks_leading_period,
5998            },
5999            edge_count,
6000        )
6001        .map(Some)
6002    }
6003
6004    /// Compiles `group` in front of `rest`. `behind_separator` is the state
6005    /// behind the separator that follows the group, where the alternatives
6006    /// that take that separator along resume (#422).
6007    fn group(
6008        &mut self,
6009        group: &ExtglobGroup,
6010        rest: Option<usize>,
6011        behind_separator: Option<usize>,
6012    ) -> Result<Option<usize>, PatternError> {
6013        // The separator in front of `behind_separator` consumes one byte into
6014        // it, so it has no state exactly when `behind_separator` has none.
6015        let Some(rest) = rest else {
6016            return Ok(None);
6017        };
6018        let blocks_leading_period = !extglob_group_allows_literal_leading_period(group);
6019        match group.kind {
6020            ExtglobKind::ExactlyOne => {
6021                let mut alternatives = self.alternatives(group, rest, group.component_local)?;
6022                if let Some(behind_separator) = behind_separator {
6023                    alternatives.extend(self.through_separator(
6024                        group,
6025                        behind_separator,
6026                        group.component_local,
6027                    )?);
6028                }
6029                self.epsilon(alternatives, blocks_leading_period)
6030            }
6031            ExtglobKind::Optional => {
6032                let mut alternatives = self.alternatives(group, rest, group.component_local)?;
6033                alternatives.push(rest);
6034                if let Some(behind_separator) = behind_separator {
6035                    alternatives.extend(self.through_separator(
6036                        group,
6037                        behind_separator,
6038                        group.component_local,
6039                    )?);
6040                }
6041                self.epsilon(alternatives, blocks_leading_period)
6042            }
6043            ExtglobKind::ZeroOrMore | ExtglobKind::OneOrMore => {
6044                let hub = self.push(
6045                    PositiveExtglobState::Epsilon {
6046                        targets: Vec::new(),
6047                        blocks_leading_period: false,
6048                    },
6049                    0,
6050                )?;
6051                // Only the first iteration stands where the group does; a
6052                // later one follows the previous iteration's text, so its
6053                // root wildcard gets a copy that is not component-local.
6054                let later = self.alternatives(group, hub, false)?;
6055                let mut first = if group.first_iteration_differs() {
6056                    self.alternatives(group, hub, true)?
6057                } else {
6058                    later.clone()
6059                };
6060                let mut loop_targets = later;
6061                loop_targets.push(rest);
6062                // An alternative that takes the separator along can only be the
6063                // last iteration, so it leaves the loop behind the separator (#422).
6064                if let Some(behind_separator) = behind_separator {
6065                    first.extend(self.through_separator(
6066                        group,
6067                        behind_separator,
6068                        group.first_iteration_differs(),
6069                    )?);
6070                    loop_targets.extend(self.through_separator(group, behind_separator, false)?);
6071                }
6072                let edge_count = loop_targets.len();
6073                self.budget.charge(edge_count, 0)?;
6074                self.states[hub] = PositiveExtglobState::Epsilon {
6075                    targets: loop_targets,
6076                    blocks_leading_period: false,
6077                };
6078                if group.kind == ExtglobKind::ZeroOrMore {
6079                    let mut entry = first;
6080                    entry.push(rest);
6081                    self.epsilon(entry, blocks_leading_period)
6082                } else {
6083                    self.epsilon(first, blocks_leading_period)
6084                }
6085            }
6086            ExtglobKind::Negated => unreachable!("negated groups do not compile to this NFA"),
6087        }
6088    }
6089
6090    fn alternatives(
6091        &mut self,
6092        group: &ExtglobGroup,
6093        next: usize,
6094        root_component_local: bool,
6095    ) -> Result<Vec<usize>, PatternError> {
6096        self.alternative_list(group.matched_alternatives(), next, root_component_local)
6097    }
6098
6099    /// The alternatives that take the separator behind the group along,
6100    /// resuming at `behind_separator`.
6101    fn through_separator(
6102        &mut self,
6103        group: &ExtglobGroup,
6104        behind_separator: usize,
6105        root_component_local: bool,
6106    ) -> Result<Vec<usize>, PatternError> {
6107        self.alternative_list(
6108            &group.through_separator,
6109            behind_separator,
6110            root_component_local,
6111        )
6112    }
6113
6114    fn alternative_list<'a>(
6115        &mut self,
6116        alternatives: impl IntoIterator<Item = &'a ExtglobAlternative>,
6117        next: usize,
6118        root_component_local: bool,
6119    ) -> Result<Vec<usize>, PatternError> {
6120        let mut starts = Vec::new();
6121        for alternative in alternatives {
6122            for alternative in &alternative.compiled {
6123                if let Some(start) = self.tokens(&alternative.tokens, next, root_component_local)? {
6124                    starts.push(start);
6125                }
6126            }
6127        }
6128        Ok(starts)
6129    }
6130
6131    fn tokens(
6132        &mut self,
6133        tokens: &[Token],
6134        next: usize,
6135        root_component_local: bool,
6136    ) -> Result<Option<usize>, PatternError> {
6137        let mut start = Some(next);
6138        for (token_index, token) in tokens.iter().enumerate().rev() {
6139            start = match token {
6140                Token::Literal(literal) => {
6141                    let mut literal_start = start;
6142                    for &byte in literal.iter().rev() {
6143                        literal_start = self
6144                            .consume(PositiveExtglobMatcher::GroupLiteral(byte), literal_start)?;
6145                    }
6146                    literal_start
6147                }
6148                Token::Separator => self.consume(PositiveExtglobMatcher::GroupSeparator, start)?,
6149                Token::Any => self.consume(
6150                    PositiveExtglobMatcher::Wildcard(WildcardScope::Group(
6151                        token_position_component_local(tokens, token_index, root_component_local),
6152                    )),
6153                    start,
6154                )?,
6155                Token::Class(class) => self.consume(
6156                    PositiveExtglobMatcher::Class(
6157                        class.clone(),
6158                        WildcardScope::Group(token_position_component_local(
6159                            tokens,
6160                            token_index,
6161                            root_component_local,
6162                        )),
6163                    ),
6164                    start,
6165                )?,
6166                Token::Star => self.star(
6167                    PositiveExtglobMatcher::Wildcard(WildcardScope::Group(
6168                        token_position_component_local(tokens, token_index, root_component_local),
6169                    )),
6170                    start,
6171                    StarSemantics::ordinary(false),
6172                )?,
6173                Token::RecursiveStar => self.star(
6174                    PositiveExtglobMatcher::Wildcard(WildcardScope::Recursive),
6175                    start,
6176                    StarSemantics::ordinary(true),
6177                )?,
6178                Token::RecursivePrefix => self.star(
6179                    PositiveExtglobMatcher::Wildcard(WildcardScope::Recursive),
6180                    start,
6181                    StarSemantics::RECURSIVE_PREFIX,
6182                )?,
6183            };
6184            if token_index + 2 == tokens.len()
6185                && matches!(token, Token::Separator)
6186                && matches!(tokens.last(), Some(Token::RecursiveStar))
6187                && let Some(normal) = start
6188            {
6189                start = self.epsilon(vec![normal, next], false)?;
6190            }
6191        }
6192        Ok(start)
6193    }
6194}
6195
6196/// Compiles the extglob program for `pattern`, or `None` when it has no group.
6197///
6198/// This subsumes the scan `is_match` used to repeat on every call.
6199fn compile_extglob(
6200    pattern: &[u8],
6201    options: PatternOptions,
6202    budget: &mut IrBudget,
6203    provenance_budget: &mut ProvenanceBudget,
6204    walker_source_provenance: Option<&SourceProvenance>,
6205    leading_dot_is_normalized: bool,
6206) -> Result<Option<Box<CompiledExtglob>>, PatternError> {
6207    if !options.extglob || !contains_extglob(pattern, options.escape) {
6208        return Ok(None);
6209    }
6210    // The step table is one entry per byte offset whatever the walk reaches, so
6211    // it is charged in full before it is allocated.
6212    budget.charge(pattern.len(), 0)?;
6213    let mut steps = vec![ExtglobStep::NoMatch; pattern.len()];
6214    let mut compiled = vec![false; pattern.len()];
6215    let mut groups = Vec::new();
6216    let mut pending = vec![0_usize];
6217    while let Some(index) = pending.pop() {
6218        if index >= pattern.len() || compiled[index] {
6219            continue;
6220        }
6221        compiled[index] = true;
6222        let step = compile_extglob_step(
6223            pattern,
6224            index,
6225            options,
6226            &mut groups,
6227            budget,
6228            provenance_budget,
6229            walker_source_provenance,
6230        )?;
6231        match &step {
6232            ExtglobStep::Group(group) => pending.push(groups[*group].rest),
6233            ExtglobStep::Star { next, .. } | ExtglobStep::Class { next, .. } => pending.push(*next),
6234            // The escaped byte is consumed together with its backslash; the
6235            // offset between them is never where a walk stands.
6236            ExtglobStep::Escape { .. } => pending.push(index + 2),
6237            ExtglobStep::Any | ExtglobStep::Byte(_) | ExtglobStep::UnclosedGroup { .. } => {
6238                pending.push(index + 1);
6239            }
6240            // The matcher still stops at this state, but compiling the suffix
6241            // records the same outer path structure for embeddings that need
6242            // to classify every generated branch.
6243            ExtglobStep::NoMatch => pending.push(index + 1),
6244        }
6245        steps[index] = step;
6246    }
6247    let mut memo_state_indices = vec![usize::MAX; pattern.len() + 1];
6248    let mut memo_state_count = 0;
6249    let mut add_memo_state = |offset| {
6250        if memo_state_indices[offset] == usize::MAX {
6251            memo_state_indices[offset] = memo_state_count;
6252            memo_state_count += 1;
6253        }
6254    };
6255    add_memo_state(0);
6256    for group in &groups {
6257        add_memo_state(group.start);
6258        add_memo_state(group.rest);
6259        if !group.through_separator.is_empty() {
6260            // Alternatives that take the separator along resume behind it.
6261            add_memo_state(group.rest + 1);
6262        }
6263    }
6264    for step in &steps {
6265        if let ExtglobStep::Star {
6266            next,
6267            blocks_leading_period: false,
6268        } = step
6269            && steps
6270                .get(*next)
6271                .is_some_and(|step| matches!(step, ExtglobStep::Byte(byte) if is_separator(*byte)))
6272        {
6273            add_memo_state(*next + 1);
6274        }
6275    }
6276    let trailing_globstar_separator =
6277        trailing_globstar_separator(pattern, &steps, &groups, options);
6278    let positive_nfa =
6279        PositiveExtglobNfa::compile(&steps, &groups, trailing_globstar_separator, budget)?;
6280    // A candidate may stop in front of a trailing `/**`, so that separator
6281    // is not a byte every candidate starts with.
6282    let literal_prefix = steps[..trailing_globstar_separator.unwrap_or(steps.len())]
6283        .iter()
6284        .map_while(|step| match step {
6285            ExtglobStep::Byte(byte) => Some(*byte),
6286            _ => None,
6287        })
6288        .collect();
6289    Ok(Some(Box::new(CompiledExtglob {
6290        literal_prefix,
6291        steps,
6292        groups,
6293        positive_nfa,
6294        memo_state_indices,
6295        memo_state_count,
6296        trailing_globstar_separator,
6297        walker_source_provenance: walker_source_provenance.cloned(),
6298        leading_dot_is_normalized,
6299    })))
6300}
6301
6302/// Classifies one byte offset the way the interpreter classified it.
6303fn compile_extglob_step(
6304    pattern: &[u8],
6305    index: usize,
6306    options: PatternOptions,
6307    groups: &mut Vec<ExtglobGroup>,
6308    budget: &mut IrBudget,
6309    provenance_budget: &mut ProvenanceBudget,
6310    walker_source_provenance: Option<&SourceProvenance>,
6311) -> Result<ExtglobStep, PatternError> {
6312    if let Some(kind) = detect_extglob_at(pattern, index) {
6313        let open = index + 1;
6314        let Some(close) = closing_extglob_parenthesis(pattern, open, options.escape) else {
6315            return Ok(ExtglobStep::UnclosedGroup {
6316                byte: pattern[index],
6317            });
6318        };
6319        // An alternative stands where its group stands, so a `**` at its edge
6320        // is a whole component only when the group is (#419).
6321        let bounds = ComponentBounds {
6322            starts: starts_component_at(pattern, index, options.escape, ComponentBounds::WHOLE),
6323            ends: ends_component_at(pattern, close + 1, ComponentBounds::WHOLE),
6324        };
6325        // A separator right behind the group is where an alternative's
6326        // trailing `**` would read as `**/`, zero directories included (#422).
6327        let separator_follows = pattern.get(close + 1) == Some(&b'/');
6328        let mut alternatives = Vec::new();
6329        let mut through_separator = Vec::new();
6330        for range in split_extglob_alternatives(&pattern[open + 1..close], options.escape) {
6331            let start = open + 1 + range.start;
6332            let end = open + 1 + range.end;
6333            let alternative_provenance = match walker_source_provenance {
6334                Some(provenance) => Some(provenance.slice(start, end, provenance_budget, index)?),
6335                None => None,
6336            };
6337            let mut alternative = compile_extglob_alternative(
6338                &pattern[start..end],
6339                options,
6340                budget,
6341                provenance_budget,
6342                alternative_provenance.as_ref(),
6343                false,
6344                bounds,
6345            )?;
6346            if kind != ExtglobKind::Negated
6347                && separator_follows
6348                && alternative_ends_in_recursive_star(&alternative)
6349            {
6350                // Written inline, the trailing `**` and the separator are one
6351                // `**/`, so the alternative is compiled once more with that
6352                // separator, exactly as the inlined text tokenizes, and the
6353                // copy replaces it for matching.
6354                let mut with_separator = pattern[start..end].to_vec();
6355                with_separator.push(b'/');
6356                let absorbing = compile_extglob_alternative(
6357                    &with_separator,
6358                    options,
6359                    budget,
6360                    provenance_budget,
6361                    None,
6362                    false,
6363                    bounds,
6364                )?;
6365                debug_assert!(
6366                    absorbing.compiled.iter().all(|compiled| matches!(
6367                        compiled.tokens.last(),
6368                        Some(Token::RecursivePrefix)
6369                    )),
6370                    "the separator turns the trailing `**` into a `**/`"
6371                );
6372                through_separator.push(absorbing);
6373                alternative.absorbed_by_separator = true;
6374            }
6375            alternatives.push(alternative);
6376        }
6377        let spans_components = alternatives.iter().any(|alternative| {
6378            alternative.compiled.iter().any(|compiled| {
6379                compiled.tokens.iter().any(|token| match token {
6380                    Token::Separator | Token::RecursiveStar | Token::RecursivePrefix => true,
6381                    Token::Literal(literal) => literal.iter().any(|byte| is_separator(*byte)),
6382                    Token::Any | Token::Star | Token::Class(_) => false,
6383                })
6384            })
6385        });
6386        let root_wildcard = alternatives.iter().any(|alternative| {
6387            alternative.compiled.iter().any(|compiled| {
6388                matches!(
6389                    compiled.tokens.first(),
6390                    Some(Token::Any | Token::Class(_) | Token::Star)
6391                )
6392            })
6393        });
6394        groups.push(ExtglobGroup {
6395            kind,
6396            alternatives,
6397            start: index,
6398            rest: close + 1,
6399            component_local: index > 0 && is_separator(pattern[index - 1]),
6400            spans_components,
6401            root_wildcard,
6402            through_separator,
6403        });
6404        return Ok(ExtglobStep::Group(groups.len() - 1));
6405    }
6406    Ok(match pattern[index] {
6407        b'*' => {
6408            let mut next = index + 1;
6409            // Shell extglob grammar gives the final star in a run to a closed
6410            // `*(` group. Keep the preceding stars as one ordinary wildcard,
6411            // but leave that last offset for the group compiler instead of
6412            // greedily swallowing it into the run. An unclosed opener keeps
6413            // the compatible literal-suffix fallback and remains part of the
6414            // ordinary star run.
6415            while pattern.get(next) == Some(&b'*') {
6416                let closed_group_starts_here = detect_extglob_at(pattern, next).is_some()
6417                    && closing_extglob_parenthesis(pattern, next + 1, options.escape).is_some();
6418                if closed_group_starts_here {
6419                    break;
6420                }
6421                next += 1;
6422            }
6423            ExtglobStep::Star {
6424                next,
6425                blocks_leading_period: !(options.recursive_double_star
6426                    && next - index == 2
6427                    && pattern.get(next) == Some(&b'/')
6428                    && starts_component_at(pattern, index, options.escape, ComponentBounds::WHOLE)),
6429            }
6430        }
6431        b'?' => ExtglobStep::Any,
6432        // An unparseable class never matched and was never read as a literal
6433        // bracket either: its arm had no guard to fall through.
6434        b'[' => match parse_class(pattern, index, options.escape) {
6435            Ok((class, next)) => ExtglobStep::Class { class, next },
6436            Err(_) => ExtglobStep::NoMatch,
6437        },
6438        b'\\' if options.escape && index + 1 < pattern.len() => ExtglobStep::Escape {
6439            escaped: pattern[index + 1],
6440        },
6441        byte => ExtglobStep::Byte(byte),
6442    })
6443}
6444
6445/// Compiles one alternative as a whole-candidate pattern.
6446///
6447/// Braces and extglobs stay off, exactly as the per-match compile had them, so
6448/// a nested group inside an alternative stays ordinary text. The component
6449/// policy is left off and supplied by the caller, which is what lets one
6450/// compiled alternative serve `is_match` and `is_match_glob_path` alike.
6451fn compile_extglob_alternative(
6452    alternative: &[u8],
6453    options: PatternOptions,
6454    budget: &mut IrBudget,
6455    provenance_budget: &mut ProvenanceBudget,
6456    walker_source_provenance: Option<&SourceProvenance>,
6457    leading_dot_is_normalized: bool,
6458    bounds: ComponentBounds,
6459) -> Result<ExtglobAlternative, PatternError> {
6460    let options = PatternOptions {
6461        braces: false,
6462        extglob: false,
6463        component_wildcards: false,
6464        root_component_wildcards: false,
6465        ..options
6466    };
6467    let mut compiled = Pattern::compile_within(
6468        alternative,
6469        options,
6470        budget,
6471        provenance_budget,
6472        walker_source_provenance,
6473        leading_dot_is_normalized,
6474        bounds,
6475    )?
6476    .alternatives;
6477    // An arm's prefix scan runs the engine whatever fast path the arm has,
6478    // so the engine deferred for a top-level alternative is built here. The
6479    // compile already charged it.
6480    for compiled in &mut compiled {
6481        if sweep_is_deferred(compiled) {
6482            compiled.sweep = Some(SweepEngine::build(&compiled.tokens, options)?);
6483        }
6484    }
6485    let width = fixed_token_width(&compiled);
6486    Ok(ExtglobAlternative {
6487        compiled,
6488        width,
6489        absorbed_by_separator: false,
6490    })
6491}
6492
6493/// Whether the alternative ends in a whole-component `**` that is not the
6494/// start of a `**/` (see [`Token::RecursiveStar`]).
6495fn alternative_ends_in_recursive_star(alternative: &ExtglobAlternative) -> bool {
6496    alternative
6497        .compiled
6498        .iter()
6499        .any(|compiled| matches!(compiled.tokens.last(), Some(Token::RecursiveStar)))
6500}
6501
6502/// Whether the alternative is exactly one whole-component `**`, which at the
6503/// end of a pattern also accepts the path without the separator in front of
6504/// it, as `x/**` accepts `x`.
6505fn alternative_is_globstar(alternative: &ExtglobAlternative) -> bool {
6506    alternative
6507        .compiled
6508        .iter()
6509        .any(|compiled| matches!(compiled.tokens.as_slice(), [Token::RecursiveStar]))
6510}
6511
6512/// The step offset of the separator in front of a trailing whole-component
6513/// `**`, which a candidate that ends right before it does not need (#422).
6514///
6515/// The token engine lets a trailing `/**` accept the path without it, so
6516/// `x/**` matches `x`. The program keeps that rule where the `**` is an outer
6517/// star run behind a group, as in `@(x)/**`, and where it is the only
6518/// alternative of a trailing positive group, as in `x/@(**)`.
6519fn trailing_globstar_separator(
6520    pattern: &[u8],
6521    steps: &[ExtglobStep],
6522    groups: &[ExtglobGroup],
6523    options: PatternOptions,
6524) -> Option<usize> {
6525    if !options.recursive_double_star {
6526        return None;
6527    }
6528    // An outer run of exactly two stars that ends the program.
6529    let outer_star = steps.len().checked_sub(2).filter(
6530        |&star| matches!(steps[star], ExtglobStep::Star { next, .. } if next == steps.len()),
6531    );
6532    // Otherwise a trailing positive group with a `**` alternative. Its
6533    // interior is `NoMatch` filler, so it is found by where it ends.
6534    let globstar = match outer_star {
6535        Some(star) => star,
6536        None => {
6537            groups
6538                .iter()
6539                .find(|group| {
6540                    group.rest == steps.len()
6541                        && group.kind != ExtglobKind::Negated
6542                        && matches!(steps.get(group.start), Some(ExtglobStep::Group(_)))
6543                        && group.alternatives.iter().any(alternative_is_globstar)
6544                })?
6545                .start
6546        }
6547    };
6548    // Either one is a whole component only behind an unescaped separator.
6549    let separator = globstar.checked_sub(1)?;
6550    (matches!(steps[separator], ExtglobStep::Byte(b'/'))
6551        && starts_component_at(
6552            pattern,
6553            separator + 1,
6554            options.escape,
6555            ComponentBounds::WHOLE,
6556        ))
6557    .then_some(separator)
6558}
6559
6560/// Total bytes the alternative consumes, when that is the same for every
6561/// candidate it accepts.
6562///
6563/// Braces are off while compiling one, so there is exactly one token list to
6564/// measure; a star of any kind makes the width vary and gives up.
6565fn fixed_token_width(alternatives: &[CompiledAlternative]) -> Option<usize> {
6566    let [alternative] = alternatives else {
6567        return None;
6568    };
6569    let mut width = 0_usize;
6570    for token in &alternative.tokens {
6571        width += match token {
6572            Token::Literal(literal) => literal.len(),
6573            Token::Separator | Token::Any | Token::Class(_) => 1,
6574            Token::Star | Token::RecursiveStar | Token::RecursivePrefix => {
6575                return None;
6576            }
6577        };
6578    }
6579    Some(width)
6580}
6581
6582fn contains_extglob(pattern: &[u8], escapes: bool) -> bool {
6583    let mut index = 0;
6584    while index + 1 < pattern.len() {
6585        if escapes && pattern[index] == b'\\' {
6586            index += 2;
6587            continue;
6588        }
6589        if detect_extglob_at(pattern, index).is_some() {
6590            return true;
6591        }
6592        index += 1;
6593    }
6594    false
6595}
6596
6597fn detect_extglob_at(pattern: &[u8], index: usize) -> Option<ExtglobKind> {
6598    if pattern.get(index + 1) != Some(&b'(') {
6599        return None;
6600    }
6601    match pattern[index] {
6602        b'?' => Some(ExtglobKind::Optional),
6603        b'*' => Some(ExtglobKind::ZeroOrMore),
6604        b'+' => Some(ExtglobKind::OneOrMore),
6605        b'@' => Some(ExtglobKind::ExactlyOne),
6606        b'!' => Some(ExtglobKind::Negated),
6607        _ => None,
6608    }
6609}
6610
6611fn closing_extglob_parenthesis(pattern: &[u8], open: usize, escapes: bool) -> Option<usize> {
6612    if pattern.get(open) != Some(&b'(') {
6613        return None;
6614    }
6615    let mut depth = 1_usize;
6616    let mut index = open + 1;
6617    while index < pattern.len() {
6618        if escapes && pattern[index] == b'\\' {
6619            index += 2;
6620            continue;
6621        }
6622        match pattern[index] {
6623            b'(' => depth += 1,
6624            b')' => {
6625                depth -= 1;
6626                if depth == 0 {
6627                    return Some(index);
6628                }
6629            }
6630            _ => {}
6631        }
6632        index += 1;
6633    }
6634    None
6635}
6636
6637fn split_extglob_alternatives(content: &[u8], escapes: bool) -> Vec<std::ops::Range<usize>> {
6638    let mut alternatives = Vec::new();
6639    let mut start = 0;
6640    let mut depth = 0_usize;
6641    let mut index = 0;
6642    while index < content.len() {
6643        if escapes && content[index] == b'\\' {
6644            index += 2;
6645            continue;
6646        }
6647        match content[index] {
6648            b'(' => depth += 1,
6649            b')' if depth > 0 => depth -= 1,
6650            b'|' if depth == 0 => {
6651                alternatives.push(start..index);
6652                start = index + 1;
6653            }
6654            _ => {}
6655        }
6656        index += 1;
6657    }
6658    alternatives.push(start..content.len());
6659    alternatives
6660}
6661
6662/// Reusable state for one extglob match on a thread.
6663#[derive(Default)]
6664struct ExtglobScratch {
6665    /// One visited-position frame per repetition in flight.
6666    visited: Vec<u64>,
6667    /// Dense rows for extglob-recursive interpreter offsets. Each row lazily
6668    /// allocates fixed-size candidate pages as states are explored.
6669    failed: Vec<ExtglobFailedRow>,
6670    #[cfg(test)]
6671    /// The previous match's materialized failure pages, retained only as
6672    /// deterministic test instrumentation.
6673    failed_page_count: usize,
6674    #[cfg(test)]
6675    /// The previous match's distinct failed states, retained only as
6676    /// deterministic test instrumentation.
6677    failed_state_count: usize,
6678    #[cfg(test)]
6679    /// Largest number of deferred continuations live during the previous
6680    /// match. This proves duplicate states never inflate the worklist.
6681    pending_peak: usize,
6682    /// Deferred `(program, candidate)` continuations. Extglob groups append
6683    /// work here instead of recursing through the native stack.
6684    pending: Vec<(usize, usize)>,
6685    /// Reused match ends for ordinary and repeated extglob groups.
6686    ends: Vec<usize>,
6687    /// Temporary ends while one repetition alternative falls back to the
6688    /// general matcher.
6689    candidate_ends: Vec<usize>,
6690    /// Negated groups mark rejected offsets here before queuing the rest.
6691    excluded: Vec<bool>,
6692    /// One reusable Shift-And state per sweep-enabled repetition alternative.
6693    sweep_states: Vec<SweepState>,
6694    /// The two word rows used while a wide ordinary-group alternative reports
6695    /// all matching prefix ends. Narrow alternatives need no retained state.
6696    prefix_sweep_state: Option<SweepState>,
6697}
6698
6699/// Borrows the extglob scratch buffers for one match.
6700struct ExtglobMatchState<'scratch> {
6701    visited: &'scratch mut Vec<u64>,
6702    failed: ExtglobFailedStates<'scratch>,
6703    pending: &'scratch mut Vec<(usize, usize)>,
6704    #[cfg(test)]
6705    pending_peak: &'scratch mut usize,
6706    ends: &'scratch mut Vec<usize>,
6707    candidate_ends: &'scratch mut Vec<usize>,
6708    excluded: &'scratch mut Vec<bool>,
6709    sweep_states: &'scratch mut Vec<SweepState>,
6710    prefix_sweep_state: &'scratch mut Option<SweepState>,
6711}
6712
6713thread_local! {
6714    static EXTGLOB_SCRATCH: RefCell<ExtglobScratch> = RefCell::new(ExtglobScratch::default());
6715}
6716
6717/// Runs a compiled extglob program on the thread's reusable visited buffer.
6718///
6719/// That buffer is separate from the general matcher's scratch on purpose: an
6720/// alternative is matched through [`Pattern::match_alternatives`], which takes
6721/// the general scratch, so holding both at once would push every sub-match
6722/// onto the re-entrancy fallback and allocate there.
6723fn match_extglob_program(program: &CompiledExtglob, path: &[u8], options: PatternOptions) -> bool {
6724    let prefix = &program.literal_prefix;
6725    if path.len() < prefix.len()
6726        || !prefix
6727            .iter()
6728            .zip(path)
6729            .all(|(&expected, &actual)| bytes_equal(expected, actual, options.case_insensitive))
6730    {
6731        return false;
6732    }
6733    if let Some(nfa) = &program.positive_nfa {
6734        return nfa.is_match(path, options);
6735    }
6736    EXTGLOB_SCRATCH.with(|cell| match cell.try_borrow_mut() {
6737        Ok(mut scratch) => {
6738            let matched = {
6739                let ExtglobScratch {
6740                    visited,
6741                    failed,
6742                    #[cfg(test)]
6743                    failed_page_count,
6744                    #[cfg(test)]
6745                    failed_state_count,
6746                    #[cfg(test)]
6747                    pending_peak,
6748                    pending,
6749                    ends,
6750                    candidate_ends,
6751                    excluded,
6752                    sweep_states,
6753                    prefix_sweep_state,
6754                } = &mut *scratch;
6755                visited.clear();
6756                failed.clear();
6757                pending.clear();
6758                ends.clear();
6759                candidate_ends.clear();
6760                excluded.clear();
6761                sweep_states.clear();
6762                #[cfg(test)]
6763                {
6764                    *failed_page_count = 0;
6765                    *failed_state_count = 0;
6766                    *pending_peak = 0;
6767                }
6768                let mut state = ExtglobMatchState {
6769                    visited,
6770                    failed: ExtglobFailedStates::new(
6771                        program,
6772                        failed,
6773                        #[cfg(test)]
6774                        failed_page_count,
6775                        #[cfg(test)]
6776                        failed_state_count,
6777                    ),
6778                    pending,
6779                    #[cfg(test)]
6780                    pending_peak,
6781                    ends,
6782                    candidate_ends,
6783                    excluded,
6784                    sweep_states,
6785                    prefix_sweep_state,
6786                };
6787                match_extglob_from(program, path, 0, 0, options, &mut state)
6788            };
6789            scratch.visited.clear();
6790            if scratch.visited.capacity() > RETAINED_SCRATCH_WORDS {
6791                scratch.visited.shrink_to(RETAINED_SCRATCH_WORDS);
6792            }
6793            scratch.failed.clear();
6794            let retained_rows = RETAINED_SCRATCH_WORDS * std::mem::size_of::<u64>()
6795                / std::mem::size_of::<ExtglobFailedRow>();
6796            if scratch.failed.capacity() > retained_rows {
6797                scratch.failed.shrink_to(retained_rows);
6798            }
6799            scratch.pending.clear();
6800            if scratch.pending.capacity() > RETAINED_SCRATCH_WORDS {
6801                scratch.pending.shrink_to(RETAINED_SCRATCH_WORDS);
6802            }
6803            scratch.ends.clear();
6804            if scratch.ends.capacity() > RETAINED_SCRATCH_WORDS {
6805                scratch.ends.shrink_to(RETAINED_SCRATCH_WORDS);
6806            }
6807            scratch.candidate_ends.clear();
6808            if scratch.candidate_ends.capacity() > RETAINED_SCRATCH_WORDS {
6809                scratch.candidate_ends.shrink_to(RETAINED_SCRATCH_WORDS);
6810            }
6811            scratch.excluded.clear();
6812            if scratch.excluded.capacity() > RETAINED_SCRATCH_WORDS {
6813                scratch.excluded.shrink_to(RETAINED_SCRATCH_WORDS);
6814            }
6815            scratch.sweep_states.clear();
6816            if scratch.sweep_states.capacity() > RETAINED_SCRATCH_WORDS {
6817                scratch.sweep_states.shrink_to(RETAINED_SCRATCH_WORDS);
6818            }
6819            if scratch.prefix_sweep_state.as_ref().is_some_and(|state| {
6820                SweepEngine::state_exceeds_retained_words(state, RETAINED_SCRATCH_WORDS)
6821            }) {
6822                scratch.prefix_sweep_state = None;
6823            }
6824            matched
6825        }
6826        Err(_) => {
6827            let mut scratch = ExtglobScratch::default();
6828            let ExtglobScratch {
6829                visited,
6830                failed,
6831                #[cfg(test)]
6832                failed_page_count,
6833                #[cfg(test)]
6834                failed_state_count,
6835                #[cfg(test)]
6836                pending_peak,
6837                pending,
6838                ends,
6839                candidate_ends,
6840                excluded,
6841                sweep_states,
6842                prefix_sweep_state,
6843            } = &mut scratch;
6844            let mut state = ExtglobMatchState {
6845                visited,
6846                failed: ExtglobFailedStates::new(
6847                    program,
6848                    failed,
6849                    #[cfg(test)]
6850                    failed_page_count,
6851                    #[cfg(test)]
6852                    failed_state_count,
6853                ),
6854                pending,
6855                #[cfg(test)]
6856                pending_peak,
6857                ends,
6858                candidate_ends,
6859                excluded,
6860                sweep_states,
6861                prefix_sweep_state,
6862            };
6863            match_extglob_from(program, path, 0, 0, options, &mut state)
6864        }
6865    })
6866}
6867
6868/// Scratch capacities after an extglob match, used to keep the retained
6869/// thread-local allocation bounded without relying on process RSS.
6870#[cfg(test)]
6871fn extglob_scratch_capacities() -> (usize, usize, usize, usize, usize, usize, usize, usize) {
6872    EXTGLOB_SCRATCH.with(|cell| {
6873        let scratch = cell.borrow();
6874        (
6875            scratch.visited.capacity(),
6876            scratch.failed.capacity() * std::mem::size_of::<ExtglobFailedRow>()
6877                / std::mem::size_of::<u64>(),
6878            scratch.pending.capacity(),
6879            scratch.ends.capacity(),
6880            scratch.candidate_ends.capacity(),
6881            scratch.excluded.capacity(),
6882            scratch.sweep_states.capacity(),
6883            scratch
6884                .prefix_sweep_state
6885                .as_ref()
6886                .map_or(0, SweepEngine::retained_state_capacity),
6887        )
6888    })
6889}
6890
6891#[cfg(test)]
6892fn extglob_failed_len() -> usize {
6893    EXTGLOB_SCRATCH.with(|cell| cell.borrow().failed.len())
6894}
6895
6896#[cfg(test)]
6897fn extglob_failed_stats() -> (usize, usize) {
6898    EXTGLOB_SCRATCH.with(|cell| {
6899        let scratch = cell.borrow();
6900        (scratch.failed_page_count, scratch.failed_state_count)
6901    })
6902}
6903
6904#[cfg(test)]
6905fn extglob_pending_peak() -> usize {
6906    EXTGLOB_SCRATCH.with(|cell| cell.borrow().pending_peak)
6907}
6908
6909/// Failure memoization for a single dense interpreter row.
6910#[derive(Default)]
6911struct ExtglobFailedRow {
6912    pages: BTreeMap<usize, Box<[u64; EXTGLOB_MEMO_PAGE_WORDS]>>,
6913}
6914
6915/// A page holds 4,096 candidate offsets. This keeps a sparse failed-state
6916/// search compact without allocating the full state-by-candidate product.
6917const EXTGLOB_MEMO_PAGE_WORDS: usize = 64;
6918const EXTGLOB_MEMO_PAGE_BITS: usize = EXTGLOB_MEMO_PAGE_WORDS * u64::BITS as usize;
6919
6920/// A sparse paged bitset over dense extglob interpreter rows and candidate
6921/// offsets. Its boolean semantics are identical to a flat matrix, while only
6922/// recurrence states actually reached by the match materialize storage.
6923struct ExtglobFailedStates<'scratch> {
6924    rows: &'scratch mut [ExtglobFailedRow],
6925    #[cfg(test)]
6926    page_count: &'scratch mut usize,
6927    #[cfg(test)]
6928    state_count: &'scratch mut usize,
6929}
6930
6931impl<'scratch> ExtglobFailedStates<'scratch> {
6932    fn new(
6933        program: &CompiledExtglob,
6934        scratch: &'scratch mut Vec<ExtglobFailedRow>,
6935        #[cfg(test)] page_count: &'scratch mut usize,
6936        #[cfg(test)] state_count: &'scratch mut usize,
6937    ) -> Self {
6938        scratch.resize_with(program.memo_state_count, ExtglobFailedRow::default);
6939        Self {
6940            rows: scratch,
6941            #[cfg(test)]
6942            page_count,
6943            #[cfg(test)]
6944            state_count,
6945        }
6946    }
6947
6948    fn insert(&mut self, state_index: usize, path_index: usize) -> bool {
6949        let candidate_page = path_index / EXTGLOB_MEMO_PAGE_BITS;
6950        let page_bit = path_index % EXTGLOB_MEMO_PAGE_BITS;
6951        let row = &mut self.rows[state_index];
6952        let words = match row.pages.entry(candidate_page) {
6953            std::collections::btree_map::Entry::Occupied(entry) => entry.into_mut(),
6954            std::collections::btree_map::Entry::Vacant(entry) => {
6955                let words = entry.insert(Box::new([0; EXTGLOB_MEMO_PAGE_WORDS]));
6956                #[cfg(test)]
6957                {
6958                    *self.page_count += 1;
6959                }
6960                words
6961            }
6962        };
6963        let word = &mut words[page_bit / u64::BITS as usize];
6964        let mask = 1_u64 << (page_bit % u64::BITS as usize);
6965        if *word & mask != 0 {
6966            false
6967        } else {
6968            *word |= mask;
6969            #[cfg(test)]
6970            {
6971                *self.state_count += 1;
6972            }
6973            true
6974        }
6975    }
6976}
6977
6978/// Reserves a zeroed frame of `positions` bits and returns its first word.
6979fn push_visited(visited: &mut Vec<u64>, positions: usize) -> usize {
6980    let base = visited.len();
6981    let words = positions.div_ceil(u64::BITS as usize);
6982    visited.resize(base + words, 0);
6983    base
6984}
6985
6986/// Records `position` in the frame at `base`, reporting whether it is new.
6987fn visit(visited: &mut [u64], base: usize, position: usize) -> bool {
6988    let word = &mut visited[base + position / u64::BITS as usize];
6989    let mask = 1_u64 << (position % u64::BITS as usize);
6990    if *word & mask != 0 {
6991        false
6992    } else {
6993        *word |= mask;
6994        true
6995    }
6996}
6997
6998fn visited(visited: &[u64], base: usize, position: usize) -> bool {
6999    visited[base + position / u64::BITS as usize] & (1_u64 << (position % u64::BITS as usize)) != 0
7000}
7001
7002fn match_extglob_from(
7003    program: &CompiledExtglob,
7004    path: &[u8],
7005    start: usize,
7006    start_path_index: usize,
7007    options: PatternOptions,
7008    state: &mut ExtglobMatchState<'_>,
7009) -> bool {
7010    let pending_base = state.pending.len();
7011    queue_extglob_continuation(program, state, start, start_path_index);
7012    let mut matched = false;
7013    while state.pending.len() > pending_base {
7014        let (start, start_path_index) = state
7015            .pending
7016            .pop()
7017            .expect("the extglob continuation slice is non-empty");
7018        if match_extglob_task(program, path, start, start_path_index, options, state) {
7019            matched = true;
7020            break;
7021        }
7022    }
7023    state.pending.truncate(pending_base);
7024    matched
7025}
7026
7027/// Runs one deterministic outer-program branch. Groups append their possible
7028/// continuations to the shared work list, so sequential groups consume heap
7029/// work rather than native call frames. A preceding plain star keeps its local
7030/// two-pointer backtrack state and queues the group for each reachable split.
7031fn match_extglob_task(
7032    program: &CompiledExtglob,
7033    path: &[u8],
7034    start: usize,
7035    start_path_index: usize,
7036    options: PatternOptions,
7037    state: &mut ExtglobMatchState<'_>,
7038) -> bool {
7039    let steps = &program.steps;
7040    let mut pattern_index = start;
7041    let mut path_index = start_path_index;
7042    let mut star_pattern_index = 0_usize;
7043    let mut star_path_index = 0_usize;
7044    let mut has_star = false;
7045    let mut star_component_local = false;
7046    // The last separator-crossing star that a later component-local star
7047    // replaced as the backtrack point, as `(pattern resume, path consumed)`.
7048    // Only the latest star is normally kept, because it can absorb whatever
7049    // an earlier one would have: a component-local star cannot absorb a
7050    // separator, so when it reaches one, the crossing star before it has to
7051    // take the next byte instead. Without this, `**/*.@(x)` refused
7052    // `a/b/c.x` under the component policies although `**/*.x` accepts it.
7053    let mut crossing_star: Option<(usize, usize)> = None;
7054
7055    while path_index < path.len() || pattern_index < steps.len() {
7056        if pattern_index < steps.len() {
7057            let leading_period_is_forbidden = match &steps[pattern_index] {
7058                ExtglobStep::Group(group) => {
7059                    !extglob_group_allows_literal_leading_period(&program.groups[*group])
7060                }
7061                ExtglobStep::UnclosedGroup { .. } => true,
7062                _ => false,
7063            };
7064            if leading_period_is_forbidden
7065                && !options.match_hidden
7066                && path.get(path_index) == Some(&b'.')
7067                && at_component_start(path, path_index, options)
7068            {
7069                return false;
7070            }
7071            match &steps[pattern_index] {
7072                ExtglobStep::Group(group) => {
7073                    queue_extglob_group(
7074                        program,
7075                        &program.groups[*group],
7076                        path,
7077                        path_index,
7078                        options,
7079                        state,
7080                    );
7081                    // The group's continuations are queued; this branch
7082                    // then backtracks through the preceding star like any
7083                    // failed step, below.
7084                }
7085                ExtglobStep::Star {
7086                    next,
7087                    blocks_leading_period,
7088                } => {
7089                    let blocked = *blocks_leading_period
7090                        && !options.match_hidden
7091                        && path.get(path_index) == Some(&b'.')
7092                        && at_component_start(path, path_index, options);
7093                    if !blocked {
7094                        if !*blocks_leading_period
7095                            && steps
7096                                .get(*next)
7097                                .is_some_and(|step| matches!(step, ExtglobStep::Byte(byte) if is_separator(*byte)))
7098                        {
7099                            queue_extglob_continuation(
7100                                program,
7101                                state,
7102                                *next + 1,
7103                                path_index,
7104                            );
7105                        }
7106                        let component_local =
7107                            extglob_star_component_local(steps, pattern_index, options);
7108                        remember_crossing_star(
7109                            &mut crossing_star,
7110                            has_star.then_some((
7111                                star_pattern_index,
7112                                star_path_index,
7113                                star_component_local,
7114                            )),
7115                            component_local,
7116                        );
7117                        star_pattern_index = *next;
7118                        star_path_index = path_index;
7119                        star_component_local = component_local;
7120                        has_star = true;
7121                        pattern_index = *next;
7122                        continue;
7123                    }
7124                }
7125                ExtglobStep::UnclosedGroup { byte: b'*' } => {
7126                    let component_local =
7127                        extglob_wildcard_component_local(steps, pattern_index, options);
7128                    remember_crossing_star(
7129                        &mut crossing_star,
7130                        has_star.then_some((
7131                            star_pattern_index,
7132                            star_path_index,
7133                            star_component_local,
7134                        )),
7135                        component_local,
7136                    );
7137                    star_pattern_index = pattern_index + 1;
7138                    star_path_index = path_index;
7139                    star_component_local = component_local;
7140                    has_star = true;
7141                    pattern_index += 1;
7142                    continue;
7143                }
7144                ExtglobStep::Any | ExtglobStep::UnclosedGroup { byte: b'?' } => {
7145                    if path.get(path_index).is_some_and(|&byte| {
7146                        (!extglob_wildcard_component_local(steps, pattern_index, options)
7147                            || !is_separator(byte))
7148                            && (options.match_hidden
7149                                || byte != b'.'
7150                                || !at_component_start(path, path_index, options))
7151                    }) {
7152                        pattern_index += 1;
7153                        path_index += 1;
7154                        continue;
7155                    }
7156                    // The wildcard arm falling through leaves `?` readable as
7157                    // an ordinary byte, which only a literal `?` can match.
7158                    if path
7159                        .get(path_index)
7160                        .is_some_and(|&actual| bytes_equal(b'?', actual, options.case_insensitive))
7161                    {
7162                        pattern_index += 1;
7163                        path_index += 1;
7164                        continue;
7165                    }
7166                }
7167                ExtglobStep::Class { class, next } => {
7168                    if path.get(path_index).is_some_and(|&byte| {
7169                        (!extglob_wildcard_component_local(steps, pattern_index, options)
7170                            || !is_separator(byte))
7171                            && (options.match_hidden
7172                                || byte != b'.'
7173                                || !at_component_start(path, path_index, options))
7174                            && class.matches(byte, options.case_insensitive)
7175                    }) {
7176                        pattern_index = *next;
7177                        path_index += 1;
7178                        continue;
7179                    }
7180                }
7181                ExtglobStep::Escape { escaped } => {
7182                    if path
7183                        .get(path_index)
7184                        .is_some_and(|&byte| bytes_equal(*escaped, byte, options.case_insensitive))
7185                    {
7186                        pattern_index += 2;
7187                        path_index += 1;
7188                        continue;
7189                    }
7190                }
7191                ExtglobStep::Byte(expected) | ExtglobStep::UnclosedGroup { byte: expected } => {
7192                    if path.get(path_index).is_some_and(|&actual| {
7193                        bytes_equal(*expected, actual, options.case_insensitive)
7194                    }) {
7195                        pattern_index += 1;
7196                        path_index += 1;
7197                        continue;
7198                    }
7199                    // A candidate may end in front of the separator of a
7200                    // trailing `/**`, as `x/**` accepts `x` (#422).
7201                    if path_index == path.len()
7202                        && program.trailing_globstar_separator == Some(pattern_index)
7203                    {
7204                        return true;
7205                    }
7206                }
7207                ExtglobStep::NoMatch => {}
7208            }
7209        }
7210
7211        if has_star
7212            && star_path_index < path.len()
7213            && !extglob_star_can_consume(path, star_path_index, options, star_component_local)
7214            && let Some((resume, consumed)) = crossing_star.take()
7215        {
7216            star_pattern_index = resume;
7217            star_path_index = consumed;
7218            star_component_local = false;
7219        }
7220        if has_star && star_path_index < path.len() {
7221            if !extglob_star_can_consume(path, star_path_index, options, star_component_local) {
7222                pattern_index = star_pattern_index;
7223                path_index = star_path_index;
7224                has_star = false;
7225                continue;
7226            }
7227            pattern_index = star_pattern_index;
7228            star_path_index += 1;
7229            path_index = star_path_index;
7230            continue;
7231        }
7232        return false;
7233    }
7234    true
7235}
7236
7237/// Updates the crossing-star backtrack point when a new star replaces
7238/// `previous`, given as `(pattern resume, path consumed, component-local)`.
7239///
7240/// A new crossing star supersedes every earlier star. A new component-local
7241/// star keeps the latest crossing star as the fallback for the separator it
7242/// cannot consume.
7243fn remember_crossing_star(
7244    crossing_star: &mut Option<(usize, usize)>,
7245    previous: Option<(usize, usize, bool)>,
7246    component_local: bool,
7247) {
7248    if !component_local {
7249        *crossing_star = None;
7250    } else if let Some((resume, consumed, false)) = previous {
7251        *crossing_star = Some((resume, consumed));
7252    }
7253}
7254
7255fn extglob_wildcard_component_local(
7256    steps: &[ExtglobStep],
7257    step_index: usize,
7258    options: PatternOptions,
7259) -> bool {
7260    options.component_wildcards
7261        && (options.root_component_wildcards || extglob_step_component_local(steps, step_index))
7262}
7263
7264/// Whether the outer star run at `step_index` stays inside one component,
7265/// answered exactly as the token engine reads the same run.
7266///
7267/// A single star follows the position rule. A longer run that is a whole path
7268/// component starts with a recursive star under `recursive_double_star`,
7269/// which crosses under every policy (#419, ADR-0020). Any other longer run is
7270/// a sequence of ordinary stars: the second one stands behind the first
7271/// rather than behind a separator, so the run crosses unless every ordinary
7272/// wildcard is component-local.
7273fn extglob_star_component_local(
7274    steps: &[ExtglobStep],
7275    step_index: usize,
7276    options: PatternOptions,
7277) -> bool {
7278    match steps.get(step_index) {
7279        Some(ExtglobStep::Star { next, .. }) if *next - step_index >= 2 => {
7280            !(options.recursive_double_star
7281                && extglob_star_run_is_whole_component(steps, step_index, *next))
7282                && options.component_wildcards
7283                && options.root_component_wildcards
7284        }
7285        _ => extglob_wildcard_component_local(steps, step_index, options),
7286    }
7287}
7288
7289/// Whether the outer star run `start..end` is a whole path component, the
7290/// step-table reading of [`star_run_is_whole_component`]: it follows the
7291/// program start or a separator and ends at the program end or before one. An
7292/// escaped separator compiles to an escape, never to a separator byte, so it
7293/// bounds nothing, exactly as in the token engine.
7294fn extglob_star_run_is_whole_component(steps: &[ExtglobStep], start: usize, end: usize) -> bool {
7295    let is_separator_step =
7296        |index: usize| matches!(steps.get(index), Some(ExtglobStep::Byte(b'/')));
7297    (start == 0 || is_separator_step(start - 1)) && (end == steps.len() || is_separator_step(end))
7298}
7299
7300fn extglob_star_can_consume(
7301    path: &[u8],
7302    path_index: usize,
7303    options: PatternOptions,
7304    component_local: bool,
7305) -> bool {
7306    let byte = path[path_index];
7307    (!component_local || !is_separator(byte))
7308        && (options.match_hidden || byte != b'.' || !at_component_start(path, path_index, options))
7309}
7310
7311/// Whether a group can explicitly consume the leading period of a component.
7312///
7313/// Wildcard-led alternatives still observe `match_hidden`; only a literal dot
7314/// is an opt-in to matching a hidden path.
7315fn extglob_group_allows_literal_leading_period(group: &ExtglobGroup) -> bool {
7316    // Negation consumes whatever its alternatives reject, not a literal from
7317    // one of them, so it remains wildcard-like at a component boundary.
7318    if group.kind == ExtglobKind::Negated {
7319        return false;
7320    }
7321    group.alternatives.iter().any(|alternative| {
7322        alternative.compiled.iter().any(|alternative| {
7323            matches!(
7324                alternative.tokens.first(),
7325                Some(Token::Literal(literal)) if literal.first() == Some(&b'.')
7326            )
7327        })
7328    }) || group.through_separator.iter().any(|alternative| {
7329        // An alternative that begins with a `**/` may leave a leading period
7330        // to what follows, as `**/.h` matches `.h` (#422). The `**/` itself
7331        // consumes no hidden component, and every token after it keeps its
7332        // own rule.
7333        alternative
7334            .compiled
7335            .iter()
7336            .any(|alternative| matches!(alternative.tokens.first(), Some(Token::RecursivePrefix)))
7337    })
7338}
7339
7340fn queue_extglob_group(
7341    program: &CompiledExtglob,
7342    group: &ExtglobGroup,
7343    path: &[u8],
7344    path_index: usize,
7345    options: PatternOptions,
7346    state: &mut ExtglobMatchState<'_>,
7347) {
7348    match group.kind {
7349        ExtglobKind::ExactlyOne => {
7350            matching_extglob_group_ends(
7351                group,
7352                path,
7353                path_index,
7354                options,
7355                state.prefix_sweep_state,
7356                state.ends,
7357            );
7358            queue_extglob_continuations(program, state, group.rest);
7359            queue_extglob_through_separator(
7360                program, group, path, path_index, false, options, state,
7361            );
7362        }
7363        ExtglobKind::Optional => {
7364            queue_extglob_continuation(program, state, group.rest, path_index);
7365            matching_extglob_group_ends(
7366                group,
7367                path,
7368                path_index,
7369                options,
7370                state.prefix_sweep_state,
7371                state.ends,
7372            );
7373            queue_extglob_continuations(program, state, group.rest);
7374            queue_extglob_through_separator(
7375                program, group, path, path_index, false, options, state,
7376            );
7377        }
7378        ExtglobKind::ZeroOrMore => {
7379            queue_extglob_continuation(program, state, group.rest, path_index);
7380            matching_extglob_repetition_ends(group, path, path_index, options, state);
7381            queue_extglob_continuations(program, state, group.rest);
7382            queue_extglob_through_separator(program, group, path, path_index, true, options, state);
7383        }
7384        ExtglobKind::OneOrMore => {
7385            matching_extglob_repetition_ends(group, path, path_index, options, state);
7386            queue_extglob_continuations(program, state, group.rest);
7387            queue_extglob_through_separator(program, group, path, path_index, true, options, state);
7388        }
7389        ExtglobKind::Negated => {
7390            let stop_limit = negated_extglob_stop_limit(
7391                path,
7392                path_index,
7393                extglob_component_end(path, path_index, options, group.component_local),
7394                options,
7395            );
7396            state.excluded.resize(stop_limit - path_index, false);
7397            state.excluded.fill(false);
7398            matching_extglob_group_ends(
7399                group,
7400                path,
7401                path_index,
7402                options,
7403                state.prefix_sweep_state,
7404                state.ends,
7405            );
7406            // An alternative may match past the last permitted stop; the
7407            // negation cannot stop there either way, so nothing is excluded.
7408            for &end in state.ends.iter() {
7409                if let Some(excluded) = state.excluded.get_mut(end - path_index) {
7410                    *excluded = true;
7411                }
7412            }
7413            for offset in 0..state.excluded.len() {
7414                if !state.excluded[offset] {
7415                    queue_extglob_continuation(program, state, group.rest, path_index + offset);
7416                }
7417            }
7418        }
7419    }
7420}
7421
7422fn queue_extglob_continuations(
7423    program: &CompiledExtglob,
7424    state: &mut ExtglobMatchState<'_>,
7425    rest: usize,
7426) {
7427    for index in 0..state.ends.len() {
7428        let end = state.ends[index];
7429        queue_extglob_continuation(program, state, rest, end);
7430    }
7431}
7432
7433/// Queues the continuations of the alternatives in
7434/// [`ExtglobGroup::through_separator`], which resume behind the separator
7435/// after the group (#422).
7436///
7437/// For a repetition such an alternative can only be the last iteration. It
7438/// starts where the group starts or where an earlier iteration ended, which
7439/// `state.ends` holds once [`matching_extglob_repetition_ends`] ran. A later
7440/// iteration follows that iteration's text, so its root wildcard is read as
7441/// the repetition reads it.
7442fn queue_extglob_through_separator(
7443    program: &CompiledExtglob,
7444    group: &ExtglobGroup,
7445    path: &[u8],
7446    path_index: usize,
7447    repeated: bool,
7448    options: PatternOptions,
7449    state: &mut ExtglobMatchState<'_>,
7450) {
7451    if group.through_separator.is_empty() {
7452        return;
7453    }
7454    let resume = group.rest + 1;
7455    let later_root_local = group.later_iteration_root_local(options);
7456    let starts = if repeated { 1 + state.ends.len() } else { 1 };
7457    for start_index in 0..starts {
7458        let (start, root_component_local) = match start_index.checked_sub(1) {
7459            None => (path_index, group.component_local),
7460            Some(end_index) => (state.ends[end_index], later_root_local),
7461        };
7462        let span = AlternativeSpan {
7463            path,
7464            start,
7465            end_limit: group.alternative_end_limit(path, start, options),
7466            root_component_local,
7467        };
7468        state.candidate_ends.clear();
7469        for alternative in &group.through_separator {
7470            matching_extglob_alternative_ends(
7471                alternative,
7472                span,
7473                options,
7474                state.prefix_sweep_state,
7475                state.candidate_ends,
7476            );
7477        }
7478        for index in 0..state.candidate_ends.len() {
7479            let end = state.candidate_ends[index];
7480            queue_extglob_continuation(program, state, resume, end);
7481        }
7482    }
7483}
7484
7485/// Queues a continuation once. Marking at enqueue time bounds the live
7486/// worklist by the memo-state × candidate space instead of all duplicate
7487/// paths that happen to discover that state before it is popped.
7488fn queue_extglob_continuation(
7489    program: &CompiledExtglob,
7490    state: &mut ExtglobMatchState<'_>,
7491    start: usize,
7492    path_index: usize,
7493) {
7494    if state
7495        .failed
7496        .insert(program.memo_state_index(start), path_index)
7497    {
7498        state.pending.push((start, path_index));
7499        #[cfg(test)]
7500        {
7501            *state.pending_peak = (*state.pending_peak).max(state.pending.len());
7502        }
7503    }
7504}
7505
7506fn matching_extglob_group_ends(
7507    group: &ExtglobGroup,
7508    path: &[u8],
7509    path_index: usize,
7510    options: PatternOptions,
7511    prefix_sweep_state: &mut Option<SweepState>,
7512    output: &mut Vec<usize>,
7513) {
7514    output.clear();
7515    let span = AlternativeSpan {
7516        path,
7517        start: path_index,
7518        end_limit: group.alternative_end_limit(path, path_index, options),
7519        root_component_local: group.component_local,
7520    };
7521    for alternative in group.matched_alternatives() {
7522        matching_extglob_alternative_ends(alternative, span, options, prefix_sweep_state, output);
7523    }
7524    output.sort_unstable();
7525    output.dedup();
7526}
7527
7528/// All offsets reachable after one or more repetitions of `group`.
7529///
7530/// Variable-width alternatives run as streaming sweeps. Whenever a prior
7531/// match reaches the current offset, their initial boundary is injected into
7532/// the live state. This evaluates every possible partition in one candidate
7533/// pass instead of rematching every suffix from every reached offset.
7534fn matching_extglob_repetition_ends(
7535    group: &ExtglobGroup,
7536    path: &[u8],
7537    path_index: usize,
7538    options: PatternOptions,
7539    state: &mut ExtglobMatchState<'_>,
7540) {
7541    let component_end = group.alternative_end_limit(path, path_index, options);
7542    let position_count = component_end - path_index + 1;
7543    // Only the first iteration stands where the group does. A later one
7544    // follows the previous iteration's text, not the separator, so its first
7545    // wildcard is not component-local: `src/+(*.ts)` reads like
7546    // `src/*.ts*.ts`. The difference is only observable under the list-filter
7547    // policy; there the first iteration is matched on its own, because the
7548    // streaming sweeps share one policy across every live iteration.
7549    let first_iteration_apart = group.first_iteration_apart(options);
7550    let later_root_local = group.later_iteration_root_local(options);
7551    state.visited.clear();
7552    let reachable_base = push_visited(state.visited, position_count);
7553    let matched_base = push_visited(state.visited, position_count);
7554    visit(state.visited, reachable_base, 0);
7555    prepare_extglob_sweeps(group, state.sweep_states);
7556    for offset in 0..position_count {
7557        let absolute = path_index + offset;
7558        let root_component_local = if offset == 0 {
7559            group.component_local
7560        } else {
7561            later_root_local
7562        };
7563        if visited(state.visited, reachable_base, offset) {
7564            let mut sweep_index = 0;
7565            for alternative in group.matched_alternatives() {
7566                if let Some(width) = alternative.width {
7567                    let Some(end) = absolute.checked_add(width) else {
7568                        continue;
7569                    };
7570                    if end <= component_end
7571                        && match_extglob_alternative_exact(
7572                            alternative,
7573                            &path[absolute..end],
7574                            at_component_start(path, absolute, options),
7575                            options,
7576                            root_component_local,
7577                        )
7578                    {
7579                        let reached = end - path_index;
7580                        visit(state.visited, reachable_base, reached);
7581                        visit(state.visited, matched_base, reached);
7582                    }
7583                    continue;
7584                }
7585                let enumerate_apart = offset == 0 && first_iteration_apart;
7586                let mut has_fallback = enumerate_apart;
7587                for compiled in &alternative.compiled {
7588                    if let Some(sweep) = &compiled.sweep {
7589                        let sweep_state = &mut state.sweep_states[sweep_index];
7590                        sweep_index += 1;
7591                        if enumerate_apart {
7592                            continue;
7593                        }
7594                        sweep
7595                            .inject_start(sweep_state, at_component_start(path, absolute, options));
7596                        if sweep.accepts(sweep_state) {
7597                            visit(state.visited, matched_base, offset);
7598                        }
7599                    } else {
7600                        has_fallback = true;
7601                    }
7602                }
7603                if has_fallback {
7604                    state.candidate_ends.clear();
7605                    matching_extglob_alternative_ends(
7606                        alternative,
7607                        AlternativeSpan {
7608                            path,
7609                            start: absolute,
7610                            end_limit: component_end,
7611                            root_component_local,
7612                        },
7613                        options,
7614                        state.prefix_sweep_state,
7615                        state.candidate_ends,
7616                    );
7617                    for &end in state.candidate_ends.iter() {
7618                        let reached = end - path_index;
7619                        visit(state.visited, reachable_base, reached);
7620                        visit(state.visited, matched_base, reached);
7621                    }
7622                }
7623            }
7624            debug_assert_eq!(
7625                sweep_index,
7626                state.sweep_states.len(),
7627                "prepared repetition sweeps must align with injected variable-width alternatives"
7628            );
7629        }
7630
7631        if absolute == component_end {
7632            break;
7633        }
7634        let byte = path[absolute];
7635        let starts_component = at_component_start(path, absolute, options);
7636        let mut sweep_index = 0;
7637        for alternative in group.matched_alternatives() {
7638            for compiled in &alternative.compiled {
7639                let Some(sweep) = &compiled.sweep else {
7640                    continue;
7641                };
7642                let sweep_state = &mut state.sweep_states[sweep_index];
7643                sweep_index += 1;
7644                let alternative_options =
7645                    extglob_alternative_options(path, absolute, options, later_root_local);
7646                if sweep.advance(sweep_state, byte, starts_component, alternative_options)
7647                    && sweep.accepts(sweep_state)
7648                {
7649                    visit(state.visited, reachable_base, offset + 1);
7650                    visit(state.visited, matched_base, offset + 1);
7651                }
7652            }
7653        }
7654        debug_assert_eq!(
7655            sweep_index,
7656            state.sweep_states.len(),
7657            "prepared repetition sweep count must match the advance loop"
7658        );
7659    }
7660    state.ends.clear();
7661    state.ends.extend(
7662        (0..position_count)
7663            .filter(|&offset| visited(state.visited, matched_base, offset))
7664            .map(|offset| path_index + offset),
7665    );
7666}
7667
7668fn prepare_extglob_sweeps(group: &ExtglobGroup, states: &mut Vec<SweepState>) {
7669    let mut count = 0;
7670    for alternative in group.matched_alternatives() {
7671        for compiled in &alternative.compiled {
7672            let Some(sweep) = &compiled.sweep else {
7673                continue;
7674            };
7675            if let Some(state) = states.get_mut(count) {
7676                sweep.reset_state(state);
7677            } else {
7678                states.push(sweep.empty_state());
7679            }
7680            count += 1;
7681        }
7682    }
7683    states.truncate(count);
7684}
7685
7686/// Where one match of a group alternative starts and how far it may reach.
7687#[derive(Clone, Copy)]
7688struct AlternativeSpan<'a> {
7689    path: &'a [u8],
7690    start: usize,
7691    /// See [`ExtglobGroup::alternative_end_limit`].
7692    end_limit: usize,
7693    /// Whether the alternative's first wildcard is in a component-local
7694    /// position: the group's own position for its first iteration.
7695    root_component_local: bool,
7696}
7697
7698fn matching_extglob_alternative_ends(
7699    alternative: &ExtglobAlternative,
7700    span: AlternativeSpan<'_>,
7701    options: PatternOptions,
7702    prefix_sweep_state: &mut Option<SweepState>,
7703    output: &mut Vec<usize>,
7704) {
7705    let AlternativeSpan {
7706        path,
7707        start: path_index,
7708        end_limit: component_end,
7709        root_component_local,
7710    } = span;
7711    if let Some(width) = alternative.width {
7712        let Some(end) = path_index.checked_add(width) else {
7713            return;
7714        };
7715        if end <= component_end
7716            && match_extglob_alternative_exact(
7717                alternative,
7718                &path[path_index..end],
7719                at_component_start(path, path_index, options),
7720                options,
7721                root_component_local,
7722            )
7723        {
7724            output.push(end);
7725        }
7726        return;
7727    }
7728
7729    let alternative_options =
7730        extglob_alternative_options(path, path_index, options, root_component_local);
7731    let suffix = &path[path_index..component_end];
7732    for compiled in &alternative.compiled {
7733        if let Some(sweep) = &compiled.sweep {
7734            sweep.matching_prefix_ends(
7735                suffix,
7736                alternative_options,
7737                path_index,
7738                prefix_sweep_state,
7739                output,
7740            );
7741        } else {
7742            for end in path_index..=component_end {
7743                if Pattern::match_alternatives(
7744                    std::slice::from_ref(compiled),
7745                    alternative_options,
7746                    &path[path_index..end],
7747                ) {
7748                    output.push(end);
7749                }
7750            }
7751        }
7752    }
7753}
7754
7755fn extglob_alternative_options(
7756    path: &[u8],
7757    path_index: usize,
7758    options: PatternOptions,
7759    root_component_local: bool,
7760) -> PatternOptions {
7761    PatternOptions {
7762        braces: false,
7763        extglob: false,
7764        candidate_starts_component: at_component_start(path, path_index, options),
7765        candidate_root_component_wildcard: root_component_local,
7766        ..options
7767    }
7768}
7769
7770/// Matches one compiled alternative against the whole of `path`.
7771///
7772/// The component policy comes from the caller, reproducing the entry point the
7773/// per-match compile picked: `is_match_glob_path` once the root is
7774/// component-local, `is_match` otherwise.
7775fn match_extglob_alternative_exact(
7776    alternative: &ExtglobAlternative,
7777    path: &[u8],
7778    candidate_starts_component: bool,
7779    options: PatternOptions,
7780    root_component_local: bool,
7781) -> bool {
7782    if alternative.width.is_some_and(|width| width != path.len()) {
7783        return false;
7784    }
7785    let mut options = extglob_alternative_options(path, 0, options, root_component_local);
7786    options.candidate_starts_component = candidate_starts_component;
7787    Pattern::match_alternatives(&alternative.compiled, options, path)
7788}
7789
7790/// One past the last offset at which a negated group's span may stop, given
7791/// the `component_end` its locality allows.
7792///
7793/// A negation consumes matcher text like an ordinary `*`, so it observes the
7794/// same leading-period rule (ADR-0011): without `match_hidden` it neither
7795/// consumes the period that starts a component nor stops immediately before
7796/// it, which would let a following literal opt into the hidden name
7797/// implicitly. The group-start check already refuses a hidden component at
7798/// `path_index` itself, so this bounds a span that would otherwise cross a
7799/// separator into a hidden component.
7800fn negated_extglob_stop_limit(
7801    path: &[u8],
7802    path_index: usize,
7803    component_end: usize,
7804    options: PatternOptions,
7805) -> usize {
7806    (path_index..component_end)
7807        .find(|&index| star_stops_before_hidden_component(path, index, options))
7808        .unwrap_or(component_end + 1)
7809}
7810
7811fn extglob_component_end(
7812    path: &[u8],
7813    path_index: usize,
7814    options: PatternOptions,
7815    position_component_local: bool,
7816) -> usize {
7817    if options.component_wildcards && (options.root_component_wildcards || position_component_local)
7818    {
7819        path_index
7820            + path[path_index..]
7821                .iter()
7822                .position(|byte| is_separator(*byte))
7823                .unwrap_or(path.len() - path_index)
7824    } else {
7825        path.len()
7826    }
7827}
7828
7829impl CompiledExtglob {
7830    fn memo_state_index(&self, offset: usize) -> usize {
7831        let state_index = self.memo_state_indices[offset];
7832        debug_assert_ne!(
7833            state_index,
7834            usize::MAX,
7835            "only the entry point and group transitions participate in extglob recursion"
7836        );
7837        state_index
7838    }
7839}
7840
7841#[cfg(test)]
7842mod tests {
7843    #[cfg(all(target_arch = "aarch64", target_os = "macos"))]
7844    use super::LiteralSuffix;
7845    use super::{
7846        AlternativeFastPath, ComponentBounds, ExtglobStep, FailedStates, FastPath, IrBudget,
7847        Pattern, PatternOptions, Prefilter, ProvenanceBudget, SweepEngine, Token,
7848        WalkerPathViability, extglob_failed_len, extglob_failed_stats, extglob_pending_peak,
7849        extglob_scratch_capacities, positive_extglob_scratch_capacities, scratch_capacities,
7850        sweep_is_deferred,
7851    };
7852
7853    fn compile(pattern: &str) -> Pattern {
7854        Pattern::compile(pattern, PatternOptions::default()).unwrap()
7855    }
7856
7857    #[test]
7858    fn compiled_patterns_stay_shareable_across_threads() {
7859        fn assert_send_sync<T: Send + Sync>() {}
7860        assert_send_sync::<Pattern>();
7861        assert_send_sync::<PatternOptions>();
7862    }
7863
7864    #[cfg(all(target_arch = "aarch64", target_os = "macos"))]
7865    #[test]
7866    fn apple_silicon_only_packs_short_case_sensitive_suffixes() {
7867        assert!(matches!(
7868            LiteralSuffix::new(b".ts".to_vec(), false),
7869            LiteralSuffix::Packed16(_)
7870        ));
7871        assert!(matches!(
7872            LiteralSuffix::new(b".ts".to_vec(), true),
7873            LiteralSuffix::Plain(_)
7874        ));
7875        assert!(matches!(
7876            LiteralSuffix::new(vec![b'x'; 17], false),
7877            LiteralSuffix::Plain(_)
7878        ));
7879    }
7880
7881    #[test]
7882    fn repeated_general_matches_reuse_the_thread_local_scratch() {
7883        // libtest gives each test its own thread, so the scratch observed here
7884        // is private to this test.
7885        let mut pattern = Pattern::compile(
7886            "**/*.ts",
7887            PatternOptions::default().recursive_double_star(true),
7888        )
7889        .unwrap();
7890        // The scratch belongs to the memoized matcher; pin it, since the
7891        // sweep engine would otherwise answer for this shape without any
7892        // buffers at all.
7893        pattern.strip_engines(true, true, false);
7894        let matching = "src/deep/nested/module/component/widget/main.ts";
7895        let other = "src/deep/nested/module/component/widget/main.tsx";
7896
7897        for _ in 0..8 {
7898            assert!(pattern.is_match_glob_path(matching));
7899            assert!(!pattern.is_match_glob_path(other));
7900        }
7901        let warm = scratch_capacities();
7902        assert!(
7903            warm.0 > 0 && warm.1 > 0 && warm.2 > 0,
7904            "this shape must leave the inline budget and use every buffer: {warm:?}"
7905        );
7906
7907        for _ in 0..1_000 {
7908            assert!(pattern.is_match_glob_path(matching));
7909            assert!(!pattern.is_match_glob_path(other));
7910        }
7911        assert_eq!(
7912            scratch_capacities(),
7913            warm,
7914            "steady-state matching must not grow a scratch buffer"
7915        );
7916
7917        // One oversized candidate grows the matrix, but must not keep it.
7918        let long = "x".repeat(300_000);
7919        let mut oversized = Pattern::compile("*a*y", PatternOptions::default()).unwrap();
7920        oversized.strip_engines(false, true, true);
7921        assert!(!oversized.is_match(&long));
7922        assert!(
7923            scratch_capacities().0 <= super::RETAINED_SCRATCH_WORDS,
7924            "an oversized candidate must release the matrix it needed"
7925        );
7926    }
7927
7928    #[test]
7929    fn literal_skipping_honours_component_and_leading_dot_policies() {
7930        // A skip must never carry a star across a byte it may not consume.
7931        let component = Pattern::compile(
7932            "*b.ts",
7933            PatternOptions {
7934                component_wildcards: true,
7935                root_component_wildcards: true,
7936                ..PatternOptions::default()
7937            },
7938        )
7939        .unwrap();
7940        assert!(component.is_match("ab.ts"));
7941        assert!(!component.is_match("a/b.ts"));
7942
7943        // The dot that starts a component blocks the jump behind it.
7944        assert!(!compile("*b.ts").is_match("a/.b.ts"));
7945        assert!(
7946            Pattern::compile("*b.ts", PatternOptions::default().match_hidden(true))
7947                .unwrap()
7948                .is_match("a/.b.ts")
7949        );
7950        assert!(!compile("*.ts").is_match(".hidden.ts"));
7951
7952        // A jump target that only appears past the barrier is not reachable,
7953        // but an earlier one still is.
7954        assert!(compile("*b.ts").is_match("bb.ts"));
7955        assert!(!compile("*x.ts").is_match(".a/x.ts"));
7956    }
7957
7958    /// The walker preset is the conservative default with exactly the three
7959    /// syntax switches a filesystem glob needs; hidden names, case folding and
7960    /// escaping keep their defaults.
7961    #[test]
7962    fn walker_preset_enables_exactly_the_walker_syntax() {
7963        assert_eq!(
7964            PatternOptions::walker(),
7965            PatternOptions::default()
7966                .recursive_double_star(true)
7967                .braces(true)
7968                .extglob(true)
7969        );
7970    }
7971
7972    #[test]
7973    fn matches_literals_and_component_wildcards() {
7974        assert!(compile("src/*.rs").is_match("src/lib.rs"));
7975        assert!(compile("src/?.rs").is_match("src/a.rs"));
7976        assert!(compile("src/*.rs").is_match("src/bin/main.rs"));
7977        assert!(compile("*.rs").is_match("lib.rs"));
7978
7979        let star = compile("*");
7980        assert!(star.is_match("d/e/a.c"));
7981        assert!(star.is_match_path("d/e/a.c"));
7982        assert!(!star.is_match_glob_path("d/e/a.c"));
7983        assert!(compile("?").is_match("/"));
7984    }
7985
7986    #[test]
7987    fn leading_period_requires_an_explicit_option_or_literal() {
7988        assert!(!compile("*").is_match(".gitignore"));
7989        assert!(!compile("*.rs").is_match(".rs"));
7990        assert!(compile(".*").is_match(".gitignore"));
7991        assert!(
7992            Pattern::compile("*", PatternOptions::default().match_hidden(true))
7993                .unwrap()
7994                .is_match(".gitignore")
7995        );
7996        assert!(
7997            Pattern::compile("*.rs", PatternOptions::default().match_hidden(true))
7998                .unwrap()
7999                .is_match(".rs")
8000        );
8001    }
8002
8003    /// An escape has only its escaped reading in both extglob engines: a
8004    /// literal backslash followed by the escaped byte read as syntax is not
8005    /// a second branch. Bash 5.2 with `extglob` gives every verdict below.
8006    #[test]
8007    fn extglob_escapes_match_only_the_escaped_byte() {
8008        let options = PatternOptions::default().extglob(true);
8009        // `Some(true)` is the positive NFA, `Some(false)` the retained
8010        // interpreter, `None` a pattern whose only group opener is escaped
8011        // and which therefore stays with the plain engines.
8012        for (pattern, candidate, expected, positive_nfa) in [
8013            ("\\*@(a)", "*a", true, Some(true)),
8014            ("\\*@(a)", "\\xa", false, Some(true)),
8015            ("\\*@(a)", "xa", false, Some(true)),
8016            ("*(a)\\*", "aa*", true, Some(true)),
8017            ("*(a)\\*", "*", true, Some(true)),
8018            ("*(a)\\*", "\\x)\\", false, Some(true)),
8019            ("@(a)\\/b", "a/b", true, Some(true)),
8020            ("@(a)\\/b", "a\\/b", false, Some(true)),
8021            // An outer star or a negated group keeps the retained
8022            // interpreter.
8023            ("*\\*@(a)", "x*a", true, Some(false)),
8024            ("*\\*@(a)", "*a", true, Some(false)),
8025            ("*\\*@(a)", "x\\xa", false, Some(false)),
8026            ("!(b)\\*", "a*", true, Some(false)),
8027            ("!(b)\\*", "\\x", false, Some(false)),
8028            ("!(b)\\*", "a\\x", false, Some(false)),
8029            ("\\*(a)", "*(a)", true, None),
8030            ("\\*(a)", "\\a", false, None),
8031        ] {
8032            let compiled = Pattern::compile(pattern, options).expect("pattern compiles");
8033            assert_eq!(
8034                compiled.alternatives[0]
8035                    .extglob
8036                    .as_ref()
8037                    .map(|program| program.positive_nfa.is_some()),
8038                positive_nfa,
8039                "{pattern}: engine selection"
8040            );
8041            assert_eq!(
8042                compiled.is_match(candidate),
8043                expected,
8044                "{pattern} against {candidate}"
8045            );
8046            assert!(
8047                compiled.engines_agree(candidate),
8048                "{pattern} against {candidate}: engines disagree"
8049            );
8050        }
8051    }
8052
8053    /// zlob's list filter makes a wildcard component-local only when it
8054    /// stands directly behind an explicit separator. A later wildcard in the
8055    /// same component, the star of `?*`, a star behind a class or a literal,
8056    /// or the second star of a non-recursive `**`, crosses separators again;
8057    /// the zlob oracle lane replays these verdicts from the corpus. `is_match`
8058    /// crosses throughout and `is_match_glob_path` is component-local
8059    /// throughout, so the three entry points stay distinguishable.
8060    #[test]
8061    fn only_a_wildcard_directly_behind_a_separator_is_component_local() {
8062        let options = PatternOptions::default();
8063        let local = Pattern::compile("a/*/z", options).unwrap();
8064        assert!(local.is_match_path("a/b/z"));
8065        assert!(
8066            !local.is_match_path("a/b/c/z"),
8067            "a/*/z stays inside one component"
8068        );
8069        for pattern in ["a/**/z", "a/?*/z", "a/*[a-z]*/z", "a/b*/z"] {
8070            let compiled = Pattern::compile(pattern, options).unwrap();
8071            assert!(compiled.is_match("a/b/c/z"), "{pattern}: is_match crosses");
8072            assert!(compiled.is_match_path("a/b/z"), "{pattern}: one component");
8073            assert!(
8074                compiled.is_match_path("a/b/c/z"),
8075                "{pattern}: the wildcard behind the first one crosses under is_match_path, as in zlob's matchPaths"
8076            );
8077            assert!(
8078                !compiled.is_match_glob_path("a/b/c/z"),
8079                "{pattern}: the glob-path reading is component-local throughout"
8080            );
8081            for candidate in ["a/b/z", "a/b/c/z"] {
8082                assert!(
8083                    compiled.engines_agree(candidate),
8084                    "{pattern}: engines disagree on {candidate}"
8085                );
8086            }
8087        }
8088    }
8089
8090    /// A zero-width extglob branch still observes the leading-period rule;
8091    /// only a branch that spells the period opts a hidden component in.
8092    #[test]
8093    fn zero_width_extglob_branches_keep_the_leading_period_rule() {
8094        let walker_options = PatternOptions::default()
8095            .braces(true)
8096            .recursive_double_star(true)
8097            .extglob(true);
8098        let zero_width = Pattern::compile("**/?(visible).hidden/keep.txt", walker_options)
8099            .expect("pattern compiles");
8100        assert!(!zero_width.is_match("target/.hidden/keep.txt"));
8101        let explicitly_hidden = Pattern::compile("**/?(.visible).hidden/keep.txt", walker_options)
8102            .expect("pattern compiles");
8103        assert!(explicitly_hidden.is_match("target/.hidden/keep.txt"));
8104    }
8105
8106    #[test]
8107    fn recursive_double_star_is_explicit_and_component_aware() {
8108        let options = PatternOptions::default().recursive_double_star(true);
8109        let pattern = Pattern::compile("**/*.rs", options).unwrap();
8110        assert!(pattern.is_match("lib.rs"));
8111        assert!(pattern.is_match("src/bin/main.rs"));
8112        assert!(!pattern.is_match("src/.private.rs"));
8113        assert!(Pattern::compile("src/**", options).unwrap().is_match("src"));
8114        assert!(compile("**/*.rs").is_match("src/main.rs"));
8115        assert!(compile("**/*.rs").is_match("src/bin/main.rs"));
8116    }
8117
8118    #[test]
8119    fn long_candidates_do_not_overflow_the_native_stack() {
8120        // Star repetition used to recurse once per consumed path byte, so a
8121        // few thousand candidate bytes aborted the process. 256 KiB is well
8122        // below the 2 MiB a worker thread gets by default, which pins the
8123        // bound: the matcher must not use the native stack per path byte.
8124        std::thread::Builder::new()
8125            .stack_size(256 * 1024)
8126            .spawn(|| {
8127                let filler = "x".repeat(300_000);
8128                let pattern = compile("*a*y");
8129                assert!(!pattern.is_match(&filler));
8130
8131                let mut matching = String::with_capacity(filler.len() + 2);
8132                matching.push_str(&filler[..150_000]);
8133                matching.push('a');
8134                matching.push_str(&filler[150_000..]);
8135                matching.push('y');
8136                assert!(pattern.is_match(&matching));
8137
8138                // The walker routes every starred pattern through the general
8139                // matcher once wildcards are component-local.
8140                assert!(pattern.is_match_glob_path(&matching));
8141                assert!(!pattern.is_match_glob_path(&filler));
8142
8143                let recursive = Pattern::compile(
8144                    "**/*a*.rs",
8145                    PatternOptions::default().recursive_double_star(true),
8146                )
8147                .unwrap();
8148                let mut deep = "dir/".repeat(20_000);
8149                deep.push_str("zzz.rs");
8150                assert!(!recursive.is_match_glob_path(&deep));
8151                deep.truncate(deep.len() - "zzz.rs".len());
8152                deep.push_str("zaz.rs");
8153                assert!(recursive.is_match_glob_path(&deep));
8154            })
8155            .expect("spawn a small-stack matcher worker")
8156            .join()
8157            .expect("the matcher must not overflow a 256 KiB stack");
8158    }
8159
8160    #[test]
8161    fn character_classes_support_ranges_and_negation() {
8162        assert!(compile("file[0-9].rs").is_match("file7.rs"));
8163        assert!(compile("file[!0-9].rs").is_match("filex.rs"));
8164        assert!(compile("file[^0-9].rs").is_match("filex.rs"));
8165        assert!(!compile("file[!0-9].rs").is_match("file7.rs"));
8166    }
8167
8168    #[test]
8169    fn escaped_dash_in_a_class_is_a_literal_member() {
8170        let pattern = compile(r"[a\-z]");
8171        assert!(pattern.is_match("a"));
8172        assert!(pattern.is_match("-"));
8173        assert!(pattern.is_match("z"));
8174        assert!(!pattern.is_match("b"));
8175        assert!(!pattern.is_match("y"));
8176        assert!(!pattern.is_match("\\"));
8177    }
8178
8179    #[test]
8180    fn escaped_dash_stays_literal_without_escape_processing() {
8181        let options = PatternOptions::default().escape(false);
8182        let pattern = Pattern::compile(r"[a\-z]", options).unwrap();
8183        assert!(pattern.is_match("b"));
8184        assert!(pattern.is_match("\\"));
8185        assert!(!pattern.is_match("-"));
8186    }
8187
8188    #[test]
8189    fn escaped_class_members_still_bound_ranges() {
8190        let low = compile(r"[\--0]");
8191        assert!(low.is_match("-"));
8192        assert!(low.is_match("0"));
8193        assert!(!low.is_match("a"));
8194        assert!(!low.is_match("\\"));
8195
8196        let reversed = compile(r"[a-\-]");
8197        assert!(!reversed.is_match("a"));
8198        assert!(!reversed.is_match("-"));
8199        assert!(!reversed.is_match("\\"));
8200
8201        let escaped_start = compile(r"[\a-z]");
8202        assert!(escaped_start.is_match("a"));
8203        assert!(escaped_start.is_match("b"));
8204        assert!(escaped_start.is_match("z"));
8205        assert!(!escaped_start.is_match("-"));
8206    }
8207
8208    #[test]
8209    fn unescaped_edge_dashes_stay_literal() {
8210        let trailing = compile("[a-]");
8211        assert!(trailing.is_match("a"));
8212        assert!(trailing.is_match("-"));
8213        assert!(!trailing.is_match("b"));
8214
8215        let leading = compile("[-a]");
8216        assert!(leading.is_match("a"));
8217        assert!(leading.is_match("-"));
8218        assert!(!leading.is_match("b"));
8219
8220        let both = compile("[-a-c-]");
8221        assert!(both.is_match("-"));
8222        assert!(both.is_match("b"));
8223        assert!(!both.is_match("d"));
8224    }
8225
8226    #[test]
8227    fn negated_classes_honour_escaped_dashes() {
8228        let pattern = compile(r"[!a\-z]");
8229        assert!(!pattern.is_match("a"));
8230        assert!(!pattern.is_match("-"));
8231        assert!(!pattern.is_match("z"));
8232        assert!(pattern.is_match("b"));
8233    }
8234
8235    #[test]
8236    fn character_classes_support_posix_named_sets() {
8237        assert!(compile("[[:alpha:]]").is_match("a"));
8238        assert!(!compile("[[:alpha:]]").is_match("7"));
8239        assert!(compile("[[:digit:]]").is_match("7"));
8240        assert!(compile("[[:word:]]").is_match("_"));
8241        assert!(compile("[![:space:]]").is_match("x"));
8242    }
8243
8244    #[test]
8245    fn case_folding_makes_upper_and_lower_classes_ascii_alphabetic() {
8246        let folded = PatternOptions::default().case_insensitive(true);
8247        for class in ["lower", "upper"] {
8248            let pattern = Pattern::compile(format!("[[:{class}:]]"), folded).unwrap();
8249            assert!(pattern.is_match("a"));
8250            assert!(pattern.is_match("A"));
8251            assert!(!pattern.is_match("1"));
8252        }
8253
8254        let not_upper = Pattern::compile("[![:upper:]]", folded).unwrap();
8255        assert!(!not_upper.is_match("A"));
8256        assert!(not_upper.is_match("1"));
8257
8258        let path = Pattern::compile("src/[[:upper:]]*.rs", folded).unwrap();
8259        assert!(path.is_match("src/Main.rs"));
8260
8261        assert!(compile("[[:upper:]]").is_match("A"));
8262        assert!(!compile("[[:upper:]]").is_match("a"));
8263        assert!(compile("[[:lower:]]").is_match("a"));
8264        assert!(!compile("[[:lower:]]").is_match("A"));
8265    }
8266
8267    /// An empty component has no byte of its own; it is located at the
8268    /// separator that closes it, or for a trailing one the separator that opens
8269    /// it, through brace expansion and extglob arms alike (#400).
8270    #[test]
8271    fn walker_empty_component_is_located_at_its_separator() {
8272        let options = PatternOptions::default()
8273            .braces(true)
8274            .recursive_double_star(true)
8275            .extglob(true);
8276        for (source, offset) in [
8277            ("/bar", 0),
8278            ("src//*.ts", 4),
8279            ("a///b", 2),
8280            ("**//x", 3),
8281            ("src/", 3),
8282            ("src/{}/bar", 6),
8283            ("{a,b}//x", 6),
8284            ("src/{,./a.rs}/bar", 13),
8285            ("src/?()/bar", 7),
8286            ("src/@(a/)/bar", 9),
8287            ("src/@(a/|b/)", 7),
8288        ] {
8289            let compiled = Pattern::compile(source, options).expect("pattern compiles");
8290            assert_eq!(
8291                compiled.walker_path_viability(),
8292                WalkerPathViability::EmptyComponent,
8293                "{source}"
8294            );
8295            assert_eq!(
8296                compiled.walker_path_problem_offset(),
8297                Some(offset),
8298                "{source}"
8299            );
8300            assert_eq!(
8301                source.as_bytes()[offset],
8302                b'/',
8303                "{source} points at a separator"
8304            );
8305        }
8306
8307        // The location is diagnostic only. Arms that end in distinct
8308        // separators still leave one walker state, so a wide group is charged
8309        // as before instead of once per pair of arms and running out of IR.
8310        let arms = (0..1000)
8311            .map(|index| format!("a{index}/"))
8312            .collect::<Vec<_>>()
8313            .join("|");
8314        let empty_arms = (0..1000)
8315            .map(|index| format!("a{index}//"))
8316            .collect::<Vec<_>>()
8317            .join("|");
8318        for source in [
8319            format!("*({arms})x"),
8320            format!("@({arms})@({arms})x"),
8321            format!("*({empty_arms})x"),
8322        ] {
8323            assert_eq!(
8324                Pattern::compile(&source, options)
8325                    .expect("a wide group stays within the IR budget")
8326                    .walker_path_viability(),
8327                WalkerPathViability::Viable
8328            );
8329        }
8330    }
8331
8332    #[test]
8333    fn brace_alternatives_carry_the_source_offset_of_their_first_byte() {
8334        let braces = PatternOptions::default().braces(true);
8335        let expand = |source: &str, options| {
8336            super::expand_braces_with_first_offsets(source, options)
8337                .expect("review pattern expands")
8338                .into_iter()
8339                .map(|(bytes, first)| (String::from_utf8(bytes).unwrap(), first))
8340                .collect::<Vec<_>>()
8341        };
8342
8343        assert_eq!(expand("!src/**", braces), [("!src/**".into(), Some(0))]);
8344        assert_eq!(
8345            expand("{!a,b}/**", braces),
8346            [("!a/**".into(), Some(1)), ("b/**".into(), Some(4))]
8347        );
8348        assert_eq!(
8349            expand("{a,}!b", braces),
8350            [("a!b".into(), Some(1)), ("!b".into(), Some(4))]
8351        );
8352        assert_eq!(
8353            expand("x{{!a,b},c}", braces),
8354            [
8355                ("x!a".into(), Some(0)),
8356                ("xb".into(), Some(0)),
8357                ("xc".into(), Some(0))
8358            ]
8359        );
8360        assert_eq!(
8361            expand("{{!a,b},c}", braces),
8362            [
8363                ("!a".into(), Some(2)),
8364                ("b".into(), Some(5)),
8365                ("c".into(), Some(8))
8366            ]
8367        );
8368        assert_eq!(
8369            expand("{,}", braces),
8370            [(String::new(), None), (String::new(), None)]
8371        );
8372        assert_eq!(
8373            expand("{!a,b}", PatternOptions::default()),
8374            [("{!a,b}".into(), Some(0))]
8375        );
8376        assert_eq!(
8377            super::expand_braces_with_first_offsets("{!a,b}/**", braces)
8378                .unwrap()
8379                .into_iter()
8380                .map(|(bytes, _)| bytes)
8381                .collect::<Vec<_>>(),
8382            super::expand_braces("{!a,b}/**", braces).unwrap(),
8383            "the alternatives are the public expansion's"
8384        );
8385    }
8386
8387    #[test]
8388    fn walker_path_viability_follows_compiled_alternatives() {
8389        let options = PatternOptions::default().braces(true).extglob(true);
8390        fn viability(source: &str, options: PatternOptions) -> WalkerPathViability {
8391            Pattern::compile(source, options)
8392                .expect("review pattern compiles")
8393                .walker_path_viability()
8394        }
8395
8396        let posix = Pattern::compile("src/[[:alpha:]/../].rs", options).unwrap();
8397        assert!(posix.is_match("src/a.rs"));
8398        assert_eq!(
8399            viability("src/[[:alpha:]/../].rs", options),
8400            WalkerPathViability::Viable
8401        );
8402
8403        let leading_bracket = Pattern::compile("src/[]/../].rs", options).unwrap();
8404        assert!(leading_bracket.is_match("src/].rs"));
8405        assert_eq!(
8406            viability("src/[]/../].rs", options),
8407            WalkerPathViability::Viable
8408        );
8409
8410        // A literal parent component is invalid walker input even when a
8411        // sibling brace arm can name a candidate. Silently dropping that arm
8412        // makes a caller typo depend on expansion order.
8413        assert_eq!(
8414            viability("{dead/../branch,src/main.rs}", options),
8415            WalkerPathViability::ParentComponent
8416        );
8417        assert_eq!(
8418            viability("src/{a,..}", options),
8419            WalkerPathViability::ParentComponent
8420        );
8421
8422        // Extglob alternatives remain matcher branches rather than brace-
8423        // expanded path inputs.
8424        assert_eq!(
8425            viability("@(dead/../branch|src/main.rs)", options),
8426            WalkerPathViability::Viable
8427        );
8428        assert_eq!(
8429            viability("{dead/{nested/../branch},src/main.rs}", options),
8430            WalkerPathViability::ParentComponent
8431        );
8432        assert_eq!(
8433            viability("@(dead/@(nested/../branch)|src/main.rs)", options),
8434            WalkerPathViability::Viable
8435        );
8436        assert_eq!(
8437            viability(r"dead\/../branch", options),
8438            WalkerPathViability::Viable
8439        );
8440
8441        let brace_class = Pattern::compile("src/[{],a}/../].rs", options).unwrap();
8442        assert!(brace_class.is_match("src/a.rs"));
8443        assert!(brace_class.is_match("src/].rs"));
8444        assert_eq!(
8445            brace_class.walker_path_viability(),
8446            WalkerPathViability::Viable
8447        );
8448
8449        let brace_extglob =
8450            Pattern::compile("@(dead/{),x}/../branch|src/main.rs)", options).unwrap();
8451        assert!(brace_extglob.is_match("src/main.rs"));
8452        assert_eq!(
8453            brace_extglob.walker_path_viability(),
8454            WalkerPathViability::Viable
8455        );
8456
8457        // A brace or extglob delimiter is processed by its compiler phase
8458        // before the later class parse. These compile into an outer `..`.
8459        for source in [
8460            "{dead/[}]]/../x,src/main.rs}",
8461            "{dead/../branch}",
8462            "@(dead/../branch)",
8463            "@(dead|src)/../main.rs",
8464        ] {
8465            assert_eq!(
8466                viability(source, options),
8467                WalkerPathViability::ParentComponent,
8468                "all compiled arms remain unwalkable: {source}"
8469            );
8470        }
8471    }
8472
8473    /// #395: the path entry points ignore one leading `./` on each expanded
8474    /// alternative, so the walker summary and the separator-crossing walk
8475    /// reading must too.
8476    #[test]
8477    fn a_leading_dot_slash_is_read_per_brace_alternative() {
8478        let options = PatternOptions::default()
8479            .braces(true)
8480            .extglob(true)
8481            .recursive_double_star(true);
8482        let compile = |source: &str| Pattern::compile(source, options).expect("valid pattern");
8483
8484        for source in [
8485            "{./src/*.rs,./lib/*.rs}",
8486            "{./src/*.rs,lib/*.rs}",
8487            "{./a.rs}",
8488            "{x,}./src/*.rs",
8489            "{{./src,lib}/*.rs,x}",
8490            "./{src,lib}/*.rs",
8491        ] {
8492            assert_eq!(
8493                compile(source).walker_path_viability(),
8494                WalkerPathViability::Viable,
8495                "{source}"
8496            );
8497        }
8498        // Only one prefix per alternative, and only at its start.
8499        for (source, offset) in [
8500            ("{././a,././b}", 3),
8501            ("src/{./a,./b}", 5),
8502            ("{./src/./a,./lib/./b}", 7),
8503        ] {
8504            let pattern = compile(source);
8505            assert_eq!(
8506                pattern.walker_path_viability(),
8507                WalkerPathViability::DotComponent,
8508                "{source}"
8509            );
8510            assert_eq!(
8511                pattern.walker_path_problem_offset(),
8512                Some(offset),
8513                "{source}"
8514            );
8515        }
8516
8517        for (source, path, expected) in [
8518            ("{./src/*.rs,lib/*.rs}", "src/m.rs", true),
8519            ("{./src/*.rs,lib/*.rs}", "lib/n.rs", true),
8520            ("{./src/*.rs,./lib/*.rs}", "lib/n.rs", true),
8521            ("{./src/*.rs,lib/*.rs}", "src/a/m.rs", true),
8522            ("{./@(src|lib)/*.rs,x}", "lib/a/n.rs", true),
8523            ("{x,}./src/*", "src/m.rs", true),
8524            ("{./src,lib}", "src", true),
8525            ("./{./src/*.rs,lib/*.rs}", "src/m.rs", false),
8526            ("./{./src/*.rs,lib/*.rs}", "lib/n.rs", true),
8527            ("src/{./a,b}/*.rs", "src/a/m.rs", false),
8528            ("{./src/*.rs,lib/*.rs}", "docs/d.md", false),
8529        ] {
8530            let pattern = compile(source);
8531            assert_eq!(
8532                pattern.is_match_crossing_path(path),
8533                expected,
8534                "{source} vs {path}"
8535            );
8536            // The crossing reading differs from the component-local one only
8537            // in how far an ordinary wildcard reaches.
8538            if !path.contains("/a/") {
8539                assert_eq!(
8540                    pattern.is_match_glob_path(path),
8541                    expected,
8542                    "{source} vs {path}"
8543                );
8544            }
8545        }
8546        // An extglob branch is not a brace alternative: its `./` stays a real
8547        // `.` component under every path reading.
8548        let extglob = compile("@(./a.rs)");
8549        assert!(!extglob.is_match_glob_path("a.rs"));
8550        assert!(!extglob.is_match_crossing_path("a.rs"));
8551        // Without a `./` alternative the crossing reading is `is_match`.
8552        let plain = compile("src/*.rs");
8553        assert!(plain.is_match_crossing_path("src/a/m.rs"));
8554        assert!(!plain.is_match_glob_path("src/a/m.rs"));
8555    }
8556
8557    #[test]
8558    fn walker_path_viability_composes_extglob_quantifiers_without_state_products() {
8559        let options = PatternOptions::default().braces(true).extglob(true);
8560        let mandatory =
8561            Pattern::compile("+(dead/../branch)", options).expect("mandatory extglob compiles");
8562        assert!(mandatory.is_match("dead/../branch"));
8563        assert_eq!(
8564            mandatory.walker_path_viability(),
8565            WalkerPathViability::ParentComponent,
8566            "one required repetition preserves its real parent component"
8567        );
8568        assert_eq!(
8569            Pattern::compile("prefix+(../bar)", options)
8570                .expect("prefixed repetition compiles")
8571                .walker_path_viability(),
8572            WalkerPathViability::Viable,
8573            "the compiler composes an arm with its outer component"
8574        );
8575
8576        for source in [
8577            "?(dead/../branch)",
8578            "*(dead/../branch)",
8579            "+(dead/../branch)",
8580            "@(dead/../branch)",
8581        ] {
8582            assert_ne!(
8583                Pattern::compile(source, options)
8584                    .expect("quantified extglob compiles")
8585                    .walker_path_viability(),
8586                WalkerPathViability::Viable,
8587                "every matching repetition of {source} remains unwalkable"
8588            );
8589        }
8590        assert_eq!(
8591            Pattern::compile("!(dead/../branch)", options)
8592                .expect("negated extglob compiles")
8593                .walker_path_viability(),
8594            WalkerPathViability::Viable
8595        );
8596
8597        // Only a `./` that starts the source or a brace-expanded alternative
8598        // is normalized, as the path entry points normalize it (`{./a.rs}` is
8599        // viable, #395). An extglob-produced leading component is still a real
8600        // unmatchable dot component. Nor may a nullable arm use an empty
8601        // leading or interior component to escape that invalid positive arm.
8602        for source in [
8603            "@(./a.rs)",
8604            "src/?(./a.rs)",
8605            "src/*(./a.rs)",
8606            "src/?(./a.rs)/bar",
8607            "src/*(./a.rs)/bar",
8608        ] {
8609            assert_eq!(
8610                Pattern::compile(source, options)
8611                    .expect("edge-normalization regression compiles")
8612                    .walker_path_viability(),
8613                WalkerPathViability::DotComponent,
8614                "{source} has no selectable walker candidate"
8615            );
8616        }
8617        for source in ["src/?()/bar", "src/*()/bar", "?()/bar", "*()/bar"] {
8618            assert_eq!(
8619                Pattern::compile(source, options)
8620                    .expect("empty extglob arm compiles")
8621                    .walker_path_viability(),
8622                WalkerPathViability::EmptyComponent,
8623                "{source} has only an empty leading or interior component"
8624            );
8625        }
8626        for source in ["src//bar", "/bar", "src/{}/bar", "src/{,./a.rs}/bar"] {
8627            assert_eq!(
8628                Pattern::compile(source, options)
8629                    .expect("empty plain or brace arm compiles")
8630                    .walker_path_viability(),
8631                WalkerPathViability::EmptyComponent,
8632                "{source} has no selectable complete walker alternative"
8633            );
8634        }
8635        // Only a pattern made of nothing but empty and `.` components names
8636        // the root itself.
8637        for source in ["", ".", "./", "//"] {
8638            assert_eq!(
8639                Pattern::compile(source, options)
8640                    .expect("root spelling compiles")
8641                    .walker_path_viability(),
8642                WalkerPathViability::Root,
8643                "{source} names only the walk root"
8644            );
8645        }
8646        for source in ["src/?(x)/bar", "src/?()bar", "?(x)/bar"] {
8647            assert_eq!(
8648                Pattern::compile(source, options)
8649                    .expect("selectable nullable control compiles")
8650                    .walker_path_viability(),
8651                WalkerPathViability::Viable,
8652                "{source} retains a selectable compiler arm"
8653            );
8654        }
8655        assert_eq!(
8656            Pattern::compile("./a.rs", options)
8657                .expect("raw normalized spelling compiles")
8658                .walker_path_viability(),
8659            WalkerPathViability::Viable
8660        );
8661
8662        // Each group has two compiler arms, but their walker summaries are
8663        // equal. This used to construct 2^32 duplicate state vectors; the
8664        // canonical state set stays one state per group transition.
8665        let sequential = "@(a|b)".repeat(32);
8666        let compiled = Pattern::compile(&sequential, options)
8667            .expect("sequential equivalent extglobs stay within the IR budget");
8668        assert_eq!(
8669            compiled.walker_path_viability(),
8670            WalkerPathViability::Viable
8671        );
8672
8673        // Nesting must take the same bounded compiler path rather than falling
8674        // back to a second source-level grammar.
8675        let nested = "@(@(a|b)|@(c|d))".repeat(16);
8676        Pattern::compile(&nested, options)
8677            .expect("nested equivalent extglobs stay within the IR budget");
8678    }
8679
8680    #[test]
8681    fn braces_expand_nested_and_empty_alternatives() {
8682        let options = PatternOptions::default().braces(true);
8683        let pattern = Pattern::compile("{src,{lib,bin}}/*.{rs,toml}", options).unwrap();
8684        assert!(pattern.is_match("src/main.rs"));
8685        assert!(pattern.is_match("lib/Cargo.toml"));
8686        assert!(pattern.is_match("bin/main.rs"));
8687        assert!(!pattern.is_match("tests/main.rs"));
8688        assert!(
8689            Pattern::compile("test{,_suffix}.txt", options)
8690                .unwrap()
8691                .is_match("test_suffix.txt")
8692        );
8693    }
8694
8695    #[test]
8696    fn brace_expansion_keeps_its_order() {
8697        // The work list must emit combinations in the order the recursive
8698        // expansion did: every choice of the first group before the second's.
8699        assert_eq!(
8700            super::expand_brace_alternatives(b"{a,b}{c,d}", true).unwrap(),
8701            vec![
8702                b"ac".to_vec(),
8703                b"ad".to_vec(),
8704                b"bc".to_vec(),
8705                b"bd".to_vec()
8706            ]
8707        );
8708        assert_eq!(
8709            super::expand_brace_alternatives(b"{x,{y,z}}!", true).unwrap(),
8710            vec![b"x!".to_vec(), b"y!".to_vec(), b"z!".to_vec()]
8711        );
8712    }
8713
8714    #[test]
8715    fn brace_provenance_stays_span_bounded_for_large_and_branching_sources() {
8716        let mut provenance_budget = super::ProvenanceBudget::new();
8717        let literal = vec![b'x'; 1 << 20];
8718        let expanded = super::expand_brace_alternatives_with_provenance(
8719            &literal,
8720            Some(&super::SourceProvenance::Contiguous { source_start: 0 }),
8721            true,
8722            &mut provenance_budget,
8723        )
8724        .expect("large literal has no provenance allocation per byte");
8725        assert!(matches!(
8726            expanded[0].source_provenance,
8727            Some(super::SourceProvenance::Contiguous { source_start: 0 })
8728        ));
8729
8730        let branching = "{a,b}".repeat(12);
8731        let expanded = super::expand_brace_alternatives_with_provenance(
8732            branching.as_bytes(),
8733            Some(&super::SourceProvenance::Contiguous { source_start: 0 }),
8734            true,
8735            &mut provenance_budget,
8736        )
8737        .expect("bounded brace expansion compiles");
8738        assert_eq!(expanded.len(), 1 << 12);
8739        let span_count = expanded
8740            .iter()
8741            .map(|alternative| match &alternative.source_provenance {
8742                Some(super::SourceProvenance::Spans(spans)) => spans.len(),
8743                Some(super::SourceProvenance::Contiguous { .. }) | None => 0,
8744            })
8745            .sum::<usize>();
8746        assert!(
8747            span_count <= 12 * (1 << 12),
8748            "one compact source span per selected brace arm, never one usize per output byte"
8749        );
8750
8751        let mut capped = super::ProvenanceBudget::new();
8752        assert!(
8753            capped.charge(super::MAX_BRACE_EXPANSION_BYTES, 0).is_err(),
8754            "provenance rejects before a span vector can exceed its byte bound"
8755        );
8756    }
8757
8758    #[test]
8759    fn brace_expansion_stops_at_the_alternative_budget() {
8760        let options = PatternOptions::default().braces(true);
8761        // Two-way groups make the boundary exact: 2^12 is the budget.
8762        let within = "{a,b}".repeat(12);
8763        assert_eq!(
8764            super::expand_brace_alternatives(within.as_bytes(), true)
8765                .unwrap()
8766                .len(),
8767            super::MAX_BRACE_ALTERNATIVES
8768        );
8769        assert!(Pattern::compile(&within, options).is_ok());
8770
8771        let beyond = "{a,b}".repeat(13);
8772        let error = Pattern::compile(&beyond, options).unwrap_err();
8773        assert_eq!(error.message(), "too many brace alternatives");
8774        assert_eq!(error.offset(), 0);
8775
8776        // The reproductions from the issue: 9^5 and 9^10 alternatives.
8777        for groups in [5, 10] {
8778            let pattern = "{,,,,,,,,}".repeat(groups);
8779            let error = Pattern::compile(&pattern, options).unwrap_err();
8780            assert_eq!(error.message(), "too many brace alternatives");
8781        }
8782
8783        // The offset locates the expansion, not the start of the pattern.
8784        let error = Pattern::compile(format!("src/{beyond}"), options).unwrap_err();
8785        assert_eq!(error.offset(), 4);
8786
8787        // Without brace expansion the same pattern is ordinary text.
8788        assert!(Pattern::compile(&beyond, PatternOptions::default()).is_ok());
8789    }
8790
8791    #[test]
8792    fn brace_expansion_survives_many_small_groups() {
8793        // One alternative per group keeps the count at 1 however deep it goes,
8794        // so only the expansion's own recursion used to fail here. The work
8795        // budget now stops such a pattern long before the depth would matter,
8796        // so this stays inside it.
8797        let options = PatternOptions::default().braces(true);
8798        let pattern = "{a}".repeat(2_000);
8799        let compiled = Pattern::compile(&pattern, options).unwrap();
8800        assert!(compiled.is_match("a".repeat(2_000)));
8801    }
8802
8803    #[test]
8804    fn brace_expansion_summary_matches_the_materialization_tree() {
8805        for (pattern, expected) in [
8806            ("{a,b}", (5, 2, 1, 2, 3, 7)),
8807            ("{a,b}{c,d}", (10, 4, 2, 8, 7, 30)),
8808            ("x{a,b}y", (7, 2, 3, 6, 3, 13)),
8809            ("{a,{b,c}}", (9, 3, 1, 3, 5, 17)),
8810            ("x{a", (3, 1, 3, 3, 1, 3)),
8811        ] {
8812            let summary = super::brace_expansion_summary(pattern.as_bytes(), true, 0)
8813                .expect("small expansion has a summary");
8814            assert_eq!(
8815                (
8816                    summary.source_length,
8817                    summary.alternatives,
8818                    summary.minimum_length,
8819                    summary.final_length_sum,
8820                    summary.nodes,
8821                    summary.written,
8822                ),
8823                expected,
8824                "{pattern}"
8825            );
8826        }
8827    }
8828
8829    #[test]
8830    fn brace_expansion_stops_at_the_work_budget() {
8831        let options = PatternOptions::default().braces(true);
8832
8833        // One alternative per group: the count budget never fires, because the
8834        // expansion is a single alternative however many groups produced it.
8835        // Rewriting the whole pattern per group is what costs, and it is
8836        // quadratic in the length: `{a}` x 200,000 took 11.8 s.
8837        let degenerate = "{a}".repeat(200_000);
8838        let error = Pattern::compile(&degenerate, options).unwrap_err();
8839        assert_eq!(error.message(), "brace expansion is too large");
8840        assert_eq!(error.offset(), 0);
8841
8842        // Inside the alternative budget but not inside the work budget: 4096
8843        // alternatives of a 10 KB pattern is 80 MB of rewriting.
8844        let wide = format!("{}{}", "x".repeat(10_000), "{a,b}".repeat(12));
8845        let error = Pattern::compile(&wide, options).unwrap_err();
8846        assert_eq!(error.message(), "brace expansion is too large");
8847        assert_eq!(error.offset(), 10_000);
8848
8849        // The IR lower bound is also over budget, but expansion has always
8850        // been the first failing phase and keeps its established error.
8851        let overlapping = format!("{}{}", "?".repeat(10_000), "{a,b}".repeat(12));
8852        let error = Pattern::compile(&overlapping, options).unwrap_err();
8853        assert_eq!(error.message(), "brace expansion is too large");
8854        assert_eq!(error.offset(), 10_000);
8855
8856        // The same shape at a realistic length still compiles, and so does the
8857        // largest alternative count the other budget admits.
8858        let narrow = format!("{}{}", "x".repeat(1_000), "{a,b}".repeat(12));
8859        assert!(Pattern::compile(&narrow, options).is_ok());
8860        assert!(Pattern::compile("{a,b}".repeat(12), options).is_ok());
8861
8862        // Without brace expansion the same bytes are ordinary text.
8863        assert!(Pattern::compile(&degenerate, PatternOptions::default()).is_ok());
8864    }
8865
8866    #[test]
8867    fn compiled_size_is_budgeted_across_every_alternative() {
8868        let options = PatternOptions::default().braces(true).extglob(true);
8869
8870        // 4096 alternatives hundreds of bytes long: inside the alternative
8871        // and expansion-byte budgets, but millions of compiled units.
8872        let extglob = format!("@(a|b){}{}", "x".repeat(300), "{a,b}".repeat(12));
8873        let error = Pattern::compile(&extglob, options).unwrap_err();
8874        assert_eq!(error.message(), "pattern compiles to too much");
8875
8876        // The same dimension without extglob: one token per wildcard byte.
8877        let wildcards = format!("{}{}", "?".repeat(300), "{a,b}".repeat(12));
8878        let error =
8879            Pattern::compile(&wildcards, PatternOptions::default().braces(true)).unwrap_err();
8880        assert_eq!(error.message(), "pattern compiles to too much");
8881        assert!(
8882            extglob.len() < 384 && wildcards.len() < 384,
8883            "the budget rejection must remain reachable below the matcher fuzz ceiling"
8884        );
8885
8886        // A literal run is one token however long, so the same alternative
8887        // count over the same number of bytes compiles.
8888        let literals = format!("{}{}", "x".repeat(1_000), "{a,b}".repeat(12));
8889        assert!(Pattern::compile(&literals, PatternOptions::default().braces(true)).is_ok());
8890
8891        // The early lower bound must not hide a parser error that exact
8892        // alternative-by-alternative compilation would report first.
8893        let invalid_class = format!("{}[{}", "?".repeat(300), "{a,b}".repeat(12));
8894        assert_eq!(
8895            Pattern::compile(&invalid_class, PatternOptions::default().braces(true))
8896                .unwrap_err()
8897                .message(),
8898            "unclosed character class"
8899        );
8900        let stable_class = format!("{}[a-z]{}", "?".repeat(300), "{a,b}".repeat(12));
8901        assert_eq!(
8902            Pattern::compile(&stable_class, PatternOptions::default().braces(true))
8903                .unwrap_err()
8904                .message(),
8905            "pattern compiles to too much"
8906        );
8907
8908        // The budget is a compile-time ceiling, not a syntax rule.
8909        assert!(Pattern::compile(&wildcards, PatternOptions::default()).is_ok());
8910
8911        // A separator before a wildcard makes the pattern carry a second
8912        // compiled copy for the path-filter reading, which costs the budget a
8913        // second time. The same pattern without one compiles.
8914        let braces = PatternOptions::default().braces(true);
8915        let single = format!("{}{}", "{a,b}".repeat(9), "?".repeat(700));
8916        let doubled = format!("{}/{}", "{a,b}".repeat(9), "?".repeat(700));
8917        assert!(Pattern::compile(&single, braces).is_ok());
8918        assert_eq!(
8919            Pattern::compile(&doubled, braces).unwrap_err().message(),
8920            "pattern compiles to too much"
8921        );
8922    }
8923
8924    #[test]
8925    fn compiled_size_budget_leaves_realistic_patterns_alone() {
8926        let options = PatternOptions::default()
8927            .braces(true)
8928            .extglob(true)
8929            .recursive_double_star(true);
8930        for pattern in [
8931            "src/**/+(main|lib).{rs,toml}",
8932            "src/**/*.{js,jsx,ts,tsx,mjs,cjs}",
8933            "src/{a,b}/{c,d}/*.{e,f}",
8934            "**/@(foo|bar)/**/*.ts",
8935        ] {
8936            assert!(
8937                Pattern::compile(pattern, options).is_ok(),
8938                "{pattern} must stay inside the compiled-size ceiling"
8939            );
8940        }
8941    }
8942
8943    #[test]
8944    fn wide_sweep_patterns_at_one_mebibyte_are_bounded() {
8945        // Wide sweeps charge their bit rows to the same compiled-program
8946        // budget as the narrow engine. This is a resource boundary rather
8947        // than a syntax restriction: 0.8.x accepted this shape, but it could
8948        // construct an unboundedly large machine.
8949        let pattern = format!("{}*b", "?".repeat(1 << 20));
8950        let error = Pattern::compile(&pattern, PatternOptions::default()).unwrap_err();
8951        assert_eq!(error.message(), "pattern compiles to too much");
8952    }
8953
8954    #[test]
8955    fn the_three_compile_budgets_are_reported_apart() {
8956        // Each answers a different question — how many alternatives, how much
8957        // text they add up to, and how much program that text becomes — so a
8958        // caller can tell which ceiling it hit.
8959        let options = PatternOptions::default().braces(true);
8960        assert_eq!(
8961            Pattern::compile("{a,b}".repeat(13), options)
8962                .unwrap_err()
8963                .message(),
8964            "too many brace alternatives"
8965        );
8966        assert_eq!(
8967            Pattern::compile("{a}".repeat(200_000), options)
8968                .unwrap_err()
8969                .message(),
8970            "brace expansion is too large"
8971        );
8972        assert_eq!(
8973            Pattern::compile(
8974                format!("{}{}", "?".repeat(1_000), "{a,b}".repeat(12)),
8975                options
8976            )
8977            .unwrap_err()
8978            .message(),
8979            "pattern compiles to too much"
8980        );
8981    }
8982
8983    #[test]
8984    fn compiled_unit_sizes_match_what_the_budget_documents() {
8985        // The ceiling is expressed in units, and its documentation converts
8986        // that to memory. If a unit grows, the documented ceiling silently
8987        // becomes a larger one.
8988        assert_eq!(
8989            size_of::<Token>(),
8990            32,
8991            "Token grew; the budget doc is stale"
8992        );
8993        assert_eq!(
8994            size_of::<super::ExtglobStep>(),
8995            40,
8996            "ExtglobStep grew; the budget doc is stale"
8997        );
8998        assert!(
8999            size_of::<super::BuildingAffixNode>()
9000                + size_of::<(u8, usize)>()
9001                + size_of::<super::AffixNode>()
9002                + size_of::<super::AffixEdge>()
9003                <= 3 * size_of::<Token>(),
9004            "affix-trie construction outgrew its three-unit charge"
9005        );
9006    }
9007
9008    #[test]
9009    fn brace_expansion_budgets_are_reported_apart() {
9010        // The two limits answer different questions and must stay
9011        // distinguishable: how many alternatives, and how much copying.
9012        let options = PatternOptions::default().braces(true);
9013        let many = Pattern::compile("{a,b}".repeat(13), options).unwrap_err();
9014        assert_eq!(many.message(), "too many brace alternatives");
9015        let large = Pattern::compile("{a}".repeat(200_000), options).unwrap_err();
9016        assert_eq!(large.message(), "brace expansion is too large");
9017    }
9018
9019    #[test]
9020    fn escapes_and_case_folding_are_opt_in_and_byte_oriented() {
9021        assert!(compile("\\*.rs").is_match("*.rs"));
9022        assert!(!compile("\\*.rs").is_match("main.rs"));
9023        assert!(
9024            Pattern::compile(
9025                "README.md",
9026                PatternOptions::default().case_insensitive(true)
9027            )
9028            .unwrap()
9029            .is_match("readme.MD")
9030        );
9031    }
9032
9033    #[test]
9034    fn extglobs_support_alternation_repetition_and_negation() {
9035        let options = PatternOptions::default().extglob(true);
9036        assert!(
9037            Pattern::compile("@(foo|bar)", options)
9038                .unwrap()
9039                .is_match("foo")
9040        );
9041        assert!(
9042            Pattern::compile("file?(.bak).txt", options)
9043                .unwrap()
9044                .is_match("file.txt")
9045        );
9046        assert!(
9047            Pattern::compile("a*(X)b", options)
9048                .unwrap()
9049                .is_match("aXXXb")
9050        );
9051        assert!(
9052            Pattern::compile("+(a|b)", options)
9053                .unwrap()
9054                .is_match("abba")
9055        );
9056        assert!(
9057            Pattern::compile("*.!(js)", options)
9058                .unwrap()
9059                .is_match("file.txt")
9060        );
9061        assert!(
9062            !Pattern::compile("*.!(js)", options)
9063                .unwrap()
9064                .is_match("file.js")
9065        );
9066        assert!(
9067            !Pattern::compile("@(foo|bar)", PatternOptions::default())
9068                .unwrap()
9069                .is_match("foo")
9070        );
9071    }
9072
9073    #[test]
9074    fn sequential_extglob_repetitions_share_failed_suffixes() {
9075        let pattern = Pattern::compile("+(a)+(a)+(a)+(a)", PatternOptions::default().extglob(true))
9076            .expect("repeating extglobs compile");
9077
9078        // Every partition of this run reaches one of the same group/path
9079        // suffixes. The trailing byte makes all of them fail, exercising the
9080        // compiled NFA's shared frontier without a wall-clock assertion.
9081        let mut non_matching = "a".repeat(400);
9082        non_matching.push('x');
9083        assert!(!pattern.is_match(&non_matching));
9084
9085        // Adjacent `+()` groups remain independently non-empty.
9086        assert!(pattern.is_match("aaaa"));
9087        assert!(!pattern.is_match("aaa"));
9088
9089        // A long single repetition stays within one explicit work frame.
9090        let single = Pattern::compile("+(a)", PatternOptions::default().extglob(true))
9091            .expect("single repeating extglob compiles");
9092        assert!(single.is_match("a".repeat(5_000)));
9093    }
9094
9095    #[test]
9096    fn positive_extglob_nfa_reuses_thread_local_scratch() {
9097        let pattern = Pattern::compile(
9098            "@(src|tests)/lib.rs",
9099            PatternOptions::default().extglob(true),
9100        )
9101        .expect("positive extglob compiles");
9102        assert!(pattern.is_match("src/lib.rs"));
9103        assert!(!pattern.is_match("vendor/lib.rs"));
9104
9105        let warmed = positive_extglob_scratch_capacities();
9106        assert!(
9107            warmed.0 > 0 && warmed.1 > 0 && warmed.2 > 0 && warmed.3 > 0,
9108            "the NFA populated every reusable buffer: {warmed:?}"
9109        );
9110        for _ in 0..1_000 {
9111            assert!(pattern.is_match("tests/lib.rs"));
9112            assert!(!pattern.is_match("vendor/lib.rs"));
9113        }
9114        assert_eq!(
9115            positive_extglob_scratch_capacities(),
9116            warmed,
9117            "steady-state matches must not grow the reusable buffers"
9118        );
9119    }
9120
9121    #[test]
9122    fn positive_extglob_nfa_releases_oversized_scratch() {
9123        let mut scratch = super::PositiveExtglobMatchScratch::default();
9124        scratch.current.reserve(super::RETAINED_SCRATCH_WORDS + 1);
9125        scratch.next.reserve(super::RETAINED_SCRATCH_WORDS + 1);
9126        scratch
9127            .closure
9128            .pending
9129            .reserve(super::RETAINED_SCRATCH_WORDS + 1);
9130        let retained_seen_bits = super::RETAINED_SCRATCH_WORDS * u64::BITS as usize;
9131        scratch.closure.seen.reserve(retained_seen_bits + 1);
9132
9133        scratch.release();
9134
9135        assert!(scratch.current.capacity() <= super::RETAINED_SCRATCH_WORDS);
9136        assert!(scratch.next.capacity() <= super::RETAINED_SCRATCH_WORDS);
9137        assert!(scratch.closure.pending.capacity() <= super::RETAINED_SCRATCH_WORDS);
9138        assert!(scratch.closure.seen.capacity() <= retained_seen_bits);
9139    }
9140
9141    #[test]
9142    fn positive_extglob_nfa_has_a_reentrant_scratch_fallback() {
9143        let pattern = Pattern::compile("@(src|tests)", PatternOptions::default().extglob(true))
9144            .expect("positive extglob compiles");
9145
9146        super::POSITIVE_EXTGLOB_SCRATCH.with(|cell| {
9147            let _borrow = cell.borrow_mut();
9148            assert!(pattern.is_match("src"));
9149            assert!(!pattern.is_match("vendor"));
9150        });
9151    }
9152
9153    #[test]
9154    fn negated_extglob_interpreter_keeps_sparse_memo_and_releases_it() {
9155        let pattern = Pattern::compile(
9156            "!(z)",
9157            PatternOptions::default().extglob(true).match_hidden(true),
9158        )
9159        .expect("negated extglob compiles");
9160        let program = pattern.alternatives[0]
9161            .extglob
9162            .as_ref()
9163            .expect("the pattern carries an extglob program");
9164        assert!(program.positive_nfa.is_none());
9165        assert!(pattern.is_match("x"));
9166        let (pages, states) = extglob_failed_stats();
9167        assert!(
9168            pages > 0 && states > 0,
9169            "the fallback memo records reached states"
9170        );
9171
9172        let capacities = extglob_scratch_capacities();
9173        assert!(
9174            capacities.1 <= super::RETAINED_SCRATCH_WORDS,
9175            "the failed-state bitset must stay within its retained cap: {capacities:?}"
9176        );
9177        assert_eq!(
9178            extglob_failed_len(),
9179            0,
9180            "the retained memo is logically empty"
9181        );
9182    }
9183
9184    #[test]
9185    fn outer_star_deduplicates_negated_group_continuations_before_queueing() {
9186        let pattern = Pattern::compile(
9187            "*!(a)b",
9188            PatternOptions::default().extglob(true).match_hidden(true),
9189        )
9190        .expect("extglob compiles");
9191        assert!(
9192            pattern.alternatives[0]
9193                .extglob
9194                .as_ref()
9195                .is_some_and(|program| program.positive_nfa.is_none()),
9196            "the regression exercises the retained interpreter"
9197        );
9198        let candidate = vec![b'a'; 4_096];
9199        assert!(!pattern.is_match(&candidate));
9200        assert!(
9201            extglob_pending_peak() <= candidate.len() + 1,
9202            "duplicate retries must not make the worklist quadratic"
9203        );
9204    }
9205
9206    #[test]
9207    fn retained_extglob_interpreter_reuses_group_scratch() {
9208        let options = PatternOptions::default().extglob(true).match_hidden(true);
9209        let outer_star = Pattern::compile("*@(a)b", options).expect("extglob compiles");
9210        let negated = Pattern::compile("!(z)y", options).expect("extglob compiles");
9211        let repeated = Pattern::compile("*+(a*|b)c", options).expect("extglob compiles");
9212        let wide_literal = "a".repeat(70);
9213        let wide_negated = Pattern::compile(format!("*!({wide_literal}*)y"), options)
9214            .expect("wide extglob compiles");
9215
9216        for pattern in [&outer_star, &negated, &repeated, &wide_negated] {
9217            assert!(
9218                pattern.alternatives[0]
9219                    .extglob
9220                    .as_ref()
9221                    .is_some_and(|program| program.positive_nfa.is_none()),
9222                "the regression exercises the retained interpreter"
9223            );
9224        }
9225        assert!(outer_star.is_match("aab"));
9226        assert!(negated.is_match("xy"));
9227        assert!(repeated.is_match("aaabc"));
9228        let wide_candidate = format!("{}y", "x".repeat(128));
9229        assert!(wide_negated.is_match(&wide_candidate));
9230
9231        let warmed = extglob_scratch_capacities();
9232        assert!(
9233            warmed.3 > 0 && warmed.5 > 0 && warmed.6 > 0 && warmed.7 > 0,
9234            "group matching populated its retained scratch: {warmed:?}"
9235        );
9236        for _ in 0..1_000 {
9237            assert!(outer_star.is_match("aab"));
9238            assert!(negated.is_match("xy"));
9239            assert!(repeated.is_match("aaabc"));
9240            assert!(wide_negated.is_match(&wide_candidate));
9241        }
9242        assert_eq!(
9243            extglob_scratch_capacities(),
9244            warmed,
9245            "steady-state retained extglob matches must not grow their scratch"
9246        );
9247    }
9248
9249    #[test]
9250    fn extglob_alternatives_keep_the_enclosing_component_context() {
9251        let options = PatternOptions::default().extglob(true);
9252        let star = Pattern::compile("a@(*)", options).expect("extglob compiles");
9253        let question = Pattern::compile("a@(?b)", options).expect("extglob compiles");
9254        assert!(star.is_match("a.b"));
9255        assert!(question.is_match("a.b"));
9256
9257        let path_group = Pattern::compile("dir/a@(*)", options).expect("extglob compiles");
9258        assert!(path_group.is_match_path("dir/a.b"));
9259        assert!(path_group.is_match_glob_path("dir/a.b"));
9260
9261        let at_start = Pattern::compile("@(*)", options).expect("extglob compiles");
9262        assert!(!at_start.is_match(".b"));
9263        assert!(!at_start.is_match_path(".b"));
9264        assert!(!at_start.is_match_glob_path(".b"));
9265
9266        let after_separator = Pattern::compile("dir/@(*)", options).expect("extglob compiles");
9267        assert!(!after_separator.is_match_path("dir/.b"));
9268        assert!(!after_separator.is_match_glob_path("dir/.b"));
9269    }
9270
9271    #[test]
9272    fn extglob_zero_width_forms_honor_leading_period_policy() {
9273        let options = PatternOptions::default().extglob(true);
9274        let optional = Pattern::compile("?(a|b).c", options).expect("optional extglob compiles");
9275        assert!(!optional.is_match(".c"));
9276        assert!(optional.is_match("a.c"));
9277
9278        let repeating = Pattern::compile("*(ab).c", options).expect("repeating extglob compiles");
9279        assert!(!repeating.is_match(".c"));
9280        assert!(repeating.is_match("ab.c"));
9281
9282        let outer_star = Pattern::compile("*.c@(x)", options).expect("outer star compiles");
9283        assert!(!outer_star.is_match(".cx"));
9284
9285        let group_star = Pattern::compile("x@(*.c)", options).expect("group star compiles");
9286        assert!(!group_star.is_match("xvisible/.c"));
9287
9288        let hidden = Pattern::compile("?(a|b).c", options.match_hidden(true))
9289            .expect("period-enabled optional extglob compiles");
9290        assert!(hidden.is_match(".c"));
9291        assert!(
9292            Pattern::compile("*.c@(x)", options.match_hidden(true))
9293                .expect("period-enabled outer star compiles")
9294                .is_match(".cx")
9295        );
9296        assert!(
9297            Pattern::compile("x@(*.c)", options.match_hidden(true))
9298                .expect("period-enabled group star compiles")
9299                .is_match("xvisible/.c")
9300        );
9301    }
9302
9303    #[test]
9304    fn extglob_recursive_prefix_exemption_requires_exactly_two_stars() {
9305        let options = PatternOptions::default()
9306            .extglob(true)
9307            .recursive_double_star(true);
9308
9309        let recursive = Pattern::compile("**/@(foo)", options).expect("extglob compiles");
9310        assert!(matches!(
9311            recursive.alternatives[0]
9312                .extglob
9313                .as_ref()
9314                .expect("the pattern carries an extglob program")
9315                .steps[0],
9316            ExtglobStep::Star {
9317                next: 2,
9318                blocks_leading_period: false
9319            }
9320        ));
9321
9322        let ordinary = Pattern::compile("***/@(foo)", options).expect("extglob compiles");
9323        assert!(matches!(
9324            ordinary.alternatives[0]
9325                .extglob
9326                .as_ref()
9327                .expect("the pattern carries an extglob program")
9328                .steps[0],
9329            ExtglobStep::Star {
9330                next: 3,
9331                blocks_leading_period: true
9332            }
9333        ));
9334        assert!(!ordinary.is_match(".hidden/foo"));
9335    }
9336
9337    #[test]
9338    fn extglob_literal_dot_alternatives_honor_leading_period_policy() {
9339        let options = PatternOptions::default().extglob(true);
9340        let matcher =
9341            Pattern::compile("@(.gitignore|.npmignore)", options).expect("extglob compiles");
9342        assert!(matcher.is_match(".gitignore"));
9343        assert!(matcher.is_match(".npmignore"));
9344        assert!(!matcher.is_match(".env"));
9345
9346        // The exemption is for explicit dots only: a wildcard alternative
9347        // still cannot select a hidden path with the default options.
9348        let wildcard = Pattern::compile("@(*|visible)", options).expect("extglob compiles");
9349        assert!(!wildcard.is_match(".hidden"));
9350
9351        let mixed = Pattern::compile("@(.gitignore|*)", options).expect("extglob compiles");
9352        assert!(mixed.is_match(".gitignore"));
9353        assert!(!mixed.is_match(".hidden"));
9354
9355        // A negated group is itself wildcard-like: naming one hidden path as
9356        // the exception must not opt every other hidden path into matching.
9357        let negated = Pattern::compile("!(.gitignore)", options).expect("extglob compiles");
9358        assert!(!negated.is_match(".hidden"));
9359        let hidden = Pattern::compile("!(.gitignore)", options.match_hidden(true))
9360            .expect("period-enabled extglob compiles");
9361        assert!(hidden.is_match(".hidden"));
9362        assert!(!hidden.is_match(".gitignore"));
9363    }
9364
9365    #[test]
9366    fn negated_extglob_never_consumes_a_later_leading_period() {
9367        // Issue #394: a negation spans matcher text like `*`, so after
9368        // crossing a separator it must neither consume the period that starts
9369        // the next component nor stop right before it, under any entry point
9370        // or engine.
9371        let options = PatternOptions::default().extglob(true);
9372        let refused = [
9373            ("!(x)", "a/.env"),
9374            ("!(x)", ".env"),
9375            ("a!(x)", "a/.env"),
9376            ("src/!(x)", "src/a/.env"),
9377            ("!(.env)", "a/.git"),
9378            ("!(x)/b", "a/.c/b"),
9379            // Like `*.env`, the negation may not stop immediately before the
9380            // hidden component either, so the literal period cannot opt the
9381            // hidden name in implicitly.
9382            ("!(x).env", "a/.env"),
9383        ];
9384        for (pattern, path) in refused {
9385            let matcher = Pattern::compile(pattern, options).expect("extglob compiles");
9386            assert!(matcher.engines_agree(path), "{pattern} against {path}");
9387            assert!(!matcher.is_match(path), "{pattern} against {path}");
9388            assert!(!matcher.is_match_path(path), "{pattern} against {path}");
9389            assert!(
9390                !matcher.is_match_glob_path(path),
9391                "{pattern} against {path}"
9392            );
9393
9394            let hidden = Pattern::compile(pattern, options.match_hidden(true))
9395                .expect("period-enabled extglob compiles");
9396            assert!(hidden.engines_agree(path), "{pattern} against {path}");
9397            assert!(
9398                hidden.is_match(path),
9399                "{pattern} against {path} with match_hidden"
9400            );
9401        }
9402        let star = Pattern::compile("*.env", options).expect("star compiles");
9403        assert!(!star.is_match("a/.env"));
9404
9405        // A period the pattern writes at the start of its own component still
9406        // names the hidden component explicitly.
9407        let explicit = Pattern::compile("!(x)/.env", options).expect("extglob compiles");
9408        assert!(explicit.is_match("a/.env"));
9409        assert!(explicit.is_match_path("a/.env"));
9410        assert!(explicit.engines_agree("a/.env"));
9411        // A period inside a component is an ordinary byte.
9412        let inner = Pattern::compile("!(x)", options).expect("extglob compiles");
9413        assert!(inner.is_match("a/b.env"));
9414        assert!(inner.is_match_path("a/b.env"));
9415    }
9416
9417    #[test]
9418    fn extglob_programs_are_compiled_only_where_the_syntax_is_present() {
9419        let options = PatternOptions::default().extglob(true);
9420        let with_group = Pattern::compile("@(a|b)c", options).expect("group compiles");
9421        assert!(with_group.alternatives[0].extglob.is_some());
9422
9423        // The walker enables extglob for every traversal pattern, so a pattern
9424        // without the syntax must not carry a program — nor pay for a scan.
9425        let without_group = Pattern::compile("src/**/*.ts", options).expect("plain compiles");
9426        assert!(without_group.alternatives[0].extglob.is_none());
9427
9428        // The option gates it: the same bytes are ordinary text without it.
9429        let disabled =
9430            Pattern::compile("@(a|b)c", PatternOptions::default()).expect("plain compiles");
9431        assert!(disabled.alternatives[0].extglob.is_none());
9432        assert!(disabled.is_match("@(a|b)c"));
9433    }
9434
9435    #[test]
9436    fn extglob_alternatives_carry_a_fixed_width_only_when_they_have_one() {
9437        let options = PatternOptions::default().extglob(true);
9438        let pattern = Pattern::compile("@(foo|b?|c*)", options).expect("group compiles");
9439        let program = pattern.alternatives[0]
9440            .extglob
9441            .as_ref()
9442            .expect("the pattern carries a group");
9443        let widths: Vec<Option<usize>> = program.groups[0]
9444            .alternatives
9445            .iter()
9446            .map(|alternative| alternative.width)
9447            .collect();
9448        // `foo` is three bytes, `b?` is two, and a star makes `c*` vary.
9449        assert_eq!(widths, vec![Some(3), Some(2), None]);
9450    }
9451
9452    #[test]
9453    fn extglobs_avoid_native_stack_recursion() {
9454        let options = PatternOptions::default().extglob(true);
9455        let pattern = Pattern::compile("+(a*)b", options).expect("repetition compiles");
9456        let program = pattern.alternatives[0]
9457            .extglob
9458            .as_ref()
9459            .expect("the pattern carries an extglob program");
9460        assert!(program.positive_nfa.is_some());
9461
9462        std::thread::Builder::new()
9463            .stack_size(256 * 1024)
9464            .spawn(move || {
9465                assert!(!pattern.is_match("a".repeat(50_000)));
9466                let sequential = Pattern::compile("@(a)".repeat(2_000), options)
9467                    .expect("sequential groups compile");
9468                assert!(sequential.is_match("a".repeat(2_000)));
9469
9470                // An outer star deliberately retains the compatible
9471                // interpreter. Its continuation work must be heap-backed too.
9472                let compatible = Pattern::compile(["*", &"@(a)".repeat(800)].concat(), options)
9473                    .expect("compatible extglob compiles");
9474                assert!(compatible.is_match("a".repeat(800)));
9475            })
9476            .expect("spawn a small-stack matcher worker")
9477            .join()
9478            .expect("positive extglobs must not recurse through the native stack");
9479    }
9480
9481    #[test]
9482    fn positive_extglob_nfa_agrees_with_the_fallback_interpreter() {
9483        let candidates = byte_words(b"ab./", 5);
9484        for source in [
9485            "@(a|ab)b",
9486            "?(a|b)c",
9487            "*(a|bc)d",
9488            "+(a*|b?)c",
9489            "@(|a)b",
9490            "+(|a)b",
9491            "a@(b|[a-c])/?(.x|y)",
9492            r"x\@(a|b)@(c)",
9493            // A group's alternatives inherit its component position (#419); an
9494            // attached group keeps its stars ordinary.
9495            "x@(**)a",
9496        ] {
9497            for match_hidden in [false, true] {
9498                for case_insensitive in [false, true] {
9499                    let options = PatternOptions::default()
9500                        .extglob(true)
9501                        .recursive_double_star(true)
9502                        .match_hidden(match_hidden)
9503                        .case_insensitive(case_insensitive);
9504                    let compiled = Pattern::compile(source, options)
9505                        .expect("positive differential pattern compiles");
9506                    let mut interpreted = compiled.clone();
9507                    for alternative in interpreted.compiled_alternatives_mut() {
9508                        alternative
9509                            .extglob
9510                            .as_mut()
9511                            .expect("every differential pattern carries an extglob")
9512                            .positive_nfa = None;
9513                    }
9514                    for candidate in &candidates {
9515                        assert_eq!(
9516                            compiled.is_match(candidate),
9517                            interpreted.is_match(candidate),
9518                            "is_match diverges for {source:?} against {candidate:?} under {options:?}"
9519                        );
9520                        assert_eq!(
9521                            compiled.is_match_path(candidate),
9522                            interpreted.is_match_path(candidate),
9523                            "is_match_path diverges for {source:?} against {candidate:?} under {options:?}"
9524                        );
9525                        assert_eq!(
9526                            compiled.is_match_glob_path(candidate),
9527                            interpreted.is_match_glob_path(candidate),
9528                            "is_match_glob_path diverges for {source:?} against {candidate:?} under {options:?}"
9529                        );
9530                    }
9531                }
9532            }
9533        }
9534    }
9535
9536    /// The two extglob engines agree on recursive stars inside a group,
9537    /// including the component-start rule of a `**/` alternative and the
9538    /// ordinary reading of an attached group's `**` (#419).
9539    #[test]
9540    fn positive_extglob_nfa_agrees_on_recursive_group_alternatives() {
9541        let candidates = byte_words(b"ab./", 5);
9542        for source in [
9543            "@(**/a|b)",
9544            "a/@(**|b)/a",
9545            "@(**)/a",
9546            "+(**/a)",
9547            "@(a/**)",
9548            "x@(**/a)",
9549            "@(**)x",
9550        ] {
9551            for match_hidden in [false, true] {
9552                let options = PatternOptions::default()
9553                    .extglob(true)
9554                    .recursive_double_star(true)
9555                    .match_hidden(match_hidden);
9556                let compiled = Pattern::compile(source, options).expect("pattern compiles");
9557                assert!(
9558                    compiled.alternatives[0]
9559                        .extglob
9560                        .as_ref()
9561                        .is_some_and(|program| program.positive_nfa.is_some()),
9562                    "{source} compiles to the positive NFA"
9563                );
9564                let mut interpreted = compiled.clone();
9565                for alternative in interpreted.compiled_alternatives_mut() {
9566                    if let Some(program) = alternative.extglob.as_mut() {
9567                        program.positive_nfa = None;
9568                    }
9569                }
9570                for candidate in &candidates {
9571                    let answers = |pattern: &Pattern| {
9572                        (
9573                            pattern.is_match(candidate),
9574                            pattern.is_match_path(candidate),
9575                            pattern.is_match_glob_path(candidate),
9576                        )
9577                    };
9578                    assert_eq!(
9579                        answers(&compiled),
9580                        answers(&interpreted),
9581                        "the engines diverge for {source:?} against {:?} under {options:?}",
9582                        String::from_utf8_lossy(candidate)
9583                    );
9584                }
9585            }
9586        }
9587    }
9588
9589    #[test]
9590    fn outer_stars_honor_the_selected_path_component_policy() {
9591        let options = PatternOptions::default()
9592            .extglob(true)
9593            .recursive_double_star(true);
9594        let star_before_group = Pattern::compile("*@(a)b", options).expect("extglob compiles");
9595        assert!(
9596            star_before_group.alternatives[0]
9597                .extglob
9598                .as_ref()
9599                .is_some_and(|program| program.positive_nfa.is_none())
9600        );
9601        assert!(star_before_group.is_match("/ab"));
9602        assert!(star_before_group.is_match_path("/ab"));
9603        assert!(!star_before_group.is_match_glob_path("/ab"));
9604
9605        let retained = Pattern::compile("a@(b|[a-c])*/?(.x|y)", options)
9606            .expect("backtracking extglob compiles");
9607        assert!(retained.is_match_path("aa//"));
9608        assert!(!retained.is_match("aa/./"));
9609    }
9610
9611    #[test]
9612    fn last_star_in_a_run_opens_an_extglob_group() {
9613        let options = PatternOptions::default().extglob(true).match_hidden(true);
9614
9615        for pattern in ["**(a)", "***(a)", "a**(b)"] {
9616            let compiled = Pattern::compile(pattern, options).expect("extglob compiles");
9617            assert!(
9618                compiled.alternatives[0]
9619                    .extglob
9620                    .as_ref()
9621                    .is_some_and(|program| !program.groups.is_empty()),
9622                "{pattern:?} must retain its final `*(` as an extglob opener"
9623            );
9624        }
9625
9626        let leading = Pattern::compile("**(a)", options).expect("extglob compiles");
9627        for candidate in ["", "ab", "x", "(a)", "x(a)"] {
9628            assert!(
9629                leading.is_match(candidate),
9630                "the outer star may consume {candidate:?} before the zero-width group"
9631            );
9632        }
9633
9634        let embedded = Pattern::compile("a**(b)", options).expect("extglob compiles");
9635        for candidate in ["a", "ab", "ax"] {
9636            assert!(
9637                embedded.is_match(candidate),
9638                "expected {candidate:?} to match"
9639            );
9640        }
9641
9642        let disabled = Pattern::compile("**(a)", PatternOptions::default())
9643            .expect("literal parenthesized suffix compiles");
9644        assert!(disabled.is_match("x(a)"));
9645        assert!(!disabled.is_match("x"));
9646    }
9647
9648    #[test]
9649    fn unclosed_extglob_after_a_star_run_keeps_its_literal_suffix() {
9650        let options = PatternOptions::default().extglob(true).match_hidden(true);
9651
9652        let leading = Pattern::compile("**(a", options).expect("fallback pattern compiles");
9653        let leading_program = leading.alternatives[0]
9654            .extglob
9655            .as_ref()
9656            .expect("extglob interpreter is retained");
9657        assert!(matches!(
9658            leading_program.steps[0],
9659            ExtglobStep::Star { next: 2, .. }
9660        ));
9661        assert!(leading.is_match("x(a"));
9662        assert!(!leading.is_match("x"));
9663
9664        let embedded = Pattern::compile("a**(b", options).expect("fallback pattern compiles");
9665        assert!(embedded.is_match("ax(b"));
9666        assert!(!embedded.is_match("ax"));
9667    }
9668
9669    #[test]
9670    fn extglob_alternation_agrees_with_braces_over_exhaustive_byte_words() {
9671        // `@(x|y)` and `{x,y}` are two spellings of the same alternation, so
9672        // they must accept the same words. The extglob form is the one that
9673        // changed representation.
9674        let extglob = Pattern::compile(
9675            "@(a|bc)d",
9676            PatternOptions::default().extglob(true).match_hidden(true),
9677        )
9678        .expect("extglob alternation compiles");
9679        let braces = Pattern::compile(
9680            "{a,bc}d",
9681            PatternOptions::default().braces(true).match_hidden(true),
9682        )
9683        .expect("brace alternation compiles");
9684        for candidate in byte_words(b"abcd", 4) {
9685            assert_eq!(
9686                extglob.is_match(&candidate),
9687                braces.is_match(&candidate),
9688                "spellings disagree on {candidate:?}"
9689            );
9690        }
9691    }
9692
9693    #[test]
9694    fn extglob_reads_an_escaped_group_opener_as_text() {
9695        // A backslash before a group opener is the corner where the walk can
9696        // land on the escaped byte itself, which is a group start again.
9697        let options = PatternOptions::default().extglob(true);
9698        let pattern = Pattern::compile("x\\@(a|b)@(c)", options).expect("pattern compiles");
9699        assert!(pattern.is_match("x@(a|b)c"));
9700        assert!(!pattern.is_match("xac"));
9701
9702        let no_escapes = Pattern::compile("x\\@(a|b)@(c)", options.escape(false))
9703            .expect("pattern compiles without escapes");
9704        assert!(no_escapes.is_match("x\\ac"));
9705    }
9706
9707    #[test]
9708    fn literal_only_patterns_match_exactly_over_exhaustive_byte_words() {
9709        let words = byte_words(b"abc", 3);
9710        for pattern in &words {
9711            let matcher = Pattern::compile(pattern, PatternOptions::default())
9712                .expect("literal byte patterns compile");
9713            for candidate in &words {
9714                assert_eq!(
9715                    matcher.is_match(candidate),
9716                    pattern == candidate,
9717                    "literal pattern {pattern:?} against {candidate:?}"
9718                );
9719            }
9720        }
9721    }
9722
9723    #[test]
9724    fn wildcard_subset_invariants_hold_over_exhaustive_byte_words() {
9725        let options = PatternOptions::default().match_hidden(true);
9726        let any = Pattern::compile("?", options).expect("single wildcard compiles");
9727        let star = Pattern::compile("*", options).expect("star wildcard compiles");
9728        let prefixed = Pattern::compile("a*", options).expect("prefixed star compiles");
9729        let words = byte_words(b"ab.", 4);
9730        for candidate in &words {
9731            assert!(
9732                !any.is_match(candidate) || star.is_match(candidate),
9733                "question-mark matches a path the star rejects: {candidate:?}"
9734            );
9735            assert!(
9736                !prefixed.is_match(candidate) || star.is_match(candidate),
9737                "prefixed star matches a path the unrestricted star rejects: {candidate:?}"
9738            );
9739        }
9740    }
9741
9742    #[test]
9743    fn failed_state_memos_use_inline_and_heap_storage_without_changing_membership() {
9744        let mut words = Vec::new();
9745        let tokens = vec![Token::Star];
9746        let mut inline = FailedStates::new(&tokens, b"abc", &mut words);
9747        assert!(matches!(&inline, FailedStates::Inline { .. }));
9748        assert!(inline.insert(0, 1));
9749        assert!(!inline.insert(0, 1));
9750        assert!(inline.insert(0, 2));
9751        assert!(!inline.insert(0, 2));
9752        assert!(words.is_empty(), "an inline matrix must not touch the heap");
9753
9754        let tokens = vec![Token::Star; 2];
9755        let mut heap = FailedStates::new(&tokens, &[b'a'; 64], &mut words);
9756        assert!(matches!(&heap, FailedStates::Heap { .. }));
9757        assert!(heap.insert(1, 63));
9758        assert!(!heap.insert(1, 63));
9759        assert!(heap.insert(0, 63));
9760        assert!(!heap.insert(0, 63));
9761        // 2 tokens x 65 path positions is 130 bits, which is three words.
9762        assert_eq!(words.len(), 3);
9763
9764        // Reusing the buffer must present a cleared matrix, and the words the
9765        // previous call dirtied must not leak into the next one.
9766        let mut reused = FailedStates::new(&tokens, &[b'a'; 64], &mut words);
9767        assert!(reused.insert(1, 63));
9768        assert!(reused.insert(0, 63));
9769    }
9770
9771    #[test]
9772    fn recursive_prefix_suffix_fast_path_matches_the_general_matcher() {
9773        let options = PatternOptions::default()
9774            .recursive_double_star(true)
9775            .case_insensitive(true);
9776        let fast = Pattern::compile("Src/**/*.RS", options).expect("pattern compiles");
9777        assert!(matches!(
9778            fast.alternatives[0].fast_path,
9779            Some(FastPath::RecursivePrefixSuffix { .. })
9780        ));
9781        let mut general = fast.clone();
9782        general.alternatives[0].fast_path = None;
9783
9784        let mut candidates = vec![
9785            b"".to_vec(),
9786            b"src".to_vec(),
9787            b"src/".to_vec(),
9788            b"src/.r".to_vec(),
9789            b"src/rs".to_vec(),
9790            b"src/.rs".to_vec(),
9791            b"src/.hidden.rs".to_vec(),
9792            b"src/visible.rs".to_vec(),
9793            b"src/nested/.hidden.rs".to_vec(),
9794            b"src/nested/visible.rs".to_vec(),
9795            b"SRC/NESTED/VISIBLE.RS".to_vec(),
9796            b"SRC\\NESTED\\VISIBLE.RS".to_vec(),
9797            b"other/visible.rs".to_vec(),
9798        ];
9799        candidates.extend(
9800            byte_words(b"ab./rs", 4)
9801                .into_iter()
9802                .map(|suffix| [b"src/".as_slice(), suffix.as_slice()].concat()),
9803        );
9804        for candidate in candidates {
9805            assert_eq!(
9806                fast.is_match(&candidate),
9807                general.is_match(&candidate),
9808                "fast path differs for {candidate:?}"
9809            );
9810            assert_eq!(
9811                fast.is_match_glob_path(&candidate),
9812                general.is_match_glob_path(&candidate),
9813                "component fast path differs for {candidate:?}"
9814            );
9815        }
9816        #[cfg(windows)]
9817        assert!(fast.is_match_glob_path(br"SRC\NESTED\VISIBLE.RS"));
9818    }
9819
9820    #[test]
9821    fn recursive_suffix_fast_path_matches_the_general_matcher() {
9822        for options in [
9823            PatternOptions::default().recursive_double_star(true),
9824            PatternOptions::default()
9825                .recursive_double_star(true)
9826                .case_insensitive(true),
9827            PatternOptions::default()
9828                .recursive_double_star(true)
9829                .match_hidden(true),
9830        ] {
9831            let fast = Pattern::compile("**/*.ts", options).expect("pattern compiles");
9832            assert!(matches!(
9833                fast.alternatives[0].fast_path,
9834                Some(FastPath::RecursiveSuffix { .. })
9835            ));
9836            let mut general = fast.clone();
9837            general.alternatives[0].fast_path = None;
9838
9839            let mut candidates = vec![
9840                b"".to_vec(),
9841                b".ts".to_vec(),
9842                b"index.ts".to_vec(),
9843                b"INDEX.TS".to_vec(),
9844                b"index.tsx".to_vec(),
9845                b".hidden.ts".to_vec(),
9846                b"src/.ts".to_vec(),
9847                b"src/index.ts".to_vec(),
9848                b"src/.hidden.ts".to_vec(),
9849                b".hidden/index.ts".to_vec(),
9850                b"src/deep/index.ts".to_vec(),
9851            ];
9852            candidates.extend(byte_words(b"ab./tsTS", 4));
9853            for candidate in candidates {
9854                assert_eq!(
9855                    fast.is_match(&candidate),
9856                    general.is_match(&candidate),
9857                    "fast path differs for {options:?} against {candidate:?}"
9858                );
9859                assert_eq!(
9860                    fast.is_match_glob_path(&candidate),
9861                    general.is_match_glob_path(&candidate),
9862                    "component fast path differs for {options:?} against {candidate:?}"
9863                );
9864            }
9865        }
9866    }
9867
9868    #[test]
9869    fn suffix_set_fast_path_matches_linear_alternatives() {
9870        for options in [
9871            PatternOptions::default()
9872                .braces(true)
9873                .recursive_double_star(true),
9874            PatternOptions::default()
9875                .braces(true)
9876                .recursive_double_star(true)
9877                .case_insensitive(true),
9878            PatternOptions::default()
9879                .braces(true)
9880                .recursive_double_star(true)
9881                .match_hidden(true),
9882        ] {
9883            for source in ["*.{ts,tsx,js,jsx,mjs,cjs}", "**/*.{ts,tsx,js,jsx,mjs,cjs}"] {
9884                let fast = Pattern::compile(source, options).expect("suffix set compiles");
9885                assert!(matches!(
9886                    fast.alternative_fast_path.as_deref(),
9887                    Some(AlternativeFastPath::SuffixSet(_))
9888                ));
9889                let mut linear = fast.clone();
9890                linear.alternative_fast_path = None;
9891
9892                let mut candidates = vec![
9893                    b"".to_vec(),
9894                    b".ts".to_vec(),
9895                    b"index.ts".to_vec(),
9896                    b"INDEX.TSX".to_vec(),
9897                    b"index.cjs".to_vec(),
9898                    b"index.vue".to_vec(),
9899                    b"src/index.ts".to_vec(),
9900                    b"src/.hidden.ts".to_vec(),
9901                    b".hidden/index.jsx".to_vec(),
9902                    b"src/deep/index.mjs".to_vec(),
9903                ];
9904                candidates.extend(byte_words(b"ab./tTsSxjc", 4));
9905                for candidate in candidates {
9906                    assert_eq!(
9907                        fast.is_match(&candidate),
9908                        linear.is_match(&candidate),
9909                        "crossing match differs for {source:?}, {options:?}, {candidate:?}"
9910                    );
9911                    assert_eq!(
9912                        fast.is_match_glob_path(&candidate),
9913                        linear.is_match_glob_path(&candidate),
9914                        "component match differs for {source:?}, {options:?}, {candidate:?}"
9915                    );
9916                }
9917            }
9918        }
9919    }
9920
9921    #[test]
9922    fn scoped_suffix_set_fast_path_matches_only_complete_cross_products() {
9923        for options in [
9924            PatternOptions::default()
9925                .braces(true)
9926                .recursive_double_star(true),
9927            PatternOptions::default()
9928                .braces(true)
9929                .recursive_double_star(true)
9930                .case_insensitive(true),
9931            PatternOptions::default()
9932                .braces(true)
9933                .recursive_double_star(true)
9934                .match_hidden(true),
9935        ] {
9936            let fast = Pattern::compile("{src,packages}/**/*.{ts,tsx,js,jsx,mjs,cjs}", options)
9937                .expect("scoped suffix set compiles");
9938            assert!(matches!(
9939                fast.alternative_fast_path.as_deref(),
9940                Some(AlternativeFastPath::ScopedSuffixSet(_))
9941            ));
9942            let mut linear = fast.clone();
9943            linear.alternative_fast_path = None;
9944
9945            let mut candidates = vec![
9946                b"".to_vec(),
9947                b"src/.ts".to_vec(),
9948                b"src/index.ts".to_vec(),
9949                b"SRC/INDEX.TSX".to_vec(),
9950                b"packages/.js".to_vec(),
9951                b"packages/index.cjs".to_vec(),
9952                b"packages/deep/index.vue".to_vec(),
9953                b"vendor/index.ts".to_vec(),
9954                b"src/.hidden/index.ts".to_vec(),
9955                b"src/deep/.hidden.mjs".to_vec(),
9956                b"packages/deep/index.jsx".to_vec(),
9957            ];
9958            candidates.extend(
9959                byte_words(b"ab./tTsSxjc", 4)
9960                    .into_iter()
9961                    .map(|suffix| [b"src/".as_slice(), suffix.as_slice()].concat()),
9962            );
9963            // The recursive prefix may consume no component, but the ordinary
9964            // star cannot hand a leading period to its suffix unless hidden
9965            // matching is explicit. Use a non-first prefix/suffix pair so the
9966            // aggregate matcher cannot conceal a boundary error.
9967            assert_eq!(fast.is_match("packages/.js"), options.match_hidden);
9968            assert_eq!(
9969                fast.is_match_glob_path("packages/.js"),
9970                options.match_hidden
9971            );
9972            for candidate in candidates {
9973                assert_eq!(
9974                    fast.is_match(&candidate),
9975                    linear.is_match(&candidate),
9976                    "crossing match differs for {options:?}, {candidate:?}"
9977                );
9978                assert_eq!(
9979                    fast.is_match_glob_path(&candidate),
9980                    linear.is_match_glob_path(&candidate),
9981                    "component match differs for {options:?}, {candidate:?}"
9982                );
9983            }
9984        }
9985
9986        let coupled = Pattern::compile(
9987            "{src/**/*.ts,lib/**/*.js}",
9988            PatternOptions::default()
9989                .braces(true)
9990                .recursive_double_star(true),
9991        )
9992        .expect("coupled alternatives compile");
9993        assert!(coupled.alternative_fast_path.is_none());
9994        assert!(!coupled.is_match_glob_path("src/index.js"));
9995        assert!(!coupled.is_match_glob_path("lib/index.ts"));
9996    }
9997
9998    #[test]
9999    fn recursive_terminal_fast_path_matches_the_general_matcher() {
10000        let options = PatternOptions::default()
10001            .recursive_double_star(true)
10002            .case_insensitive(true);
10003        let fast = Pattern::compile("Src/**", options).expect("pattern compiles");
10004        assert!(matches!(
10005            fast.alternatives[0].fast_path,
10006            Some(FastPath::RecursiveTerminalPrefix { .. })
10007        ));
10008        let mut general = fast.clone();
10009        general.alternatives[0].fast_path = None;
10010
10011        let mut candidates = vec![
10012            b"src".to_vec(),
10013            b"SRC/".to_vec(),
10014            b"src/file.rs".to_vec(),
10015            b"src/nested/file.rs".to_vec(),
10016            b"src/.hidden".to_vec(),
10017            b"src/.hidden/file.rs".to_vec(),
10018            b"source/file.rs".to_vec(),
10019        ];
10020        candidates.extend(
10021            byte_words(b"src./", 5)
10022                .into_iter()
10023                .map(|suffix| [b"src".as_slice(), suffix.as_slice()].concat()),
10024        );
10025        for candidate in candidates {
10026            assert_eq!(
10027                fast.is_match(&candidate),
10028                general.is_match(&candidate),
10029                "fast path differs for {candidate:?}"
10030            );
10031        }
10032    }
10033
10034    #[test]
10035    fn literal_fast_path_matches_the_general_matcher() {
10036        let options = PatternOptions::default().case_insensitive(true);
10037        let fast = Pattern::compile("Src/Readme", options).expect("pattern compiles");
10038        assert!(matches!(
10039            fast.alternatives[0].fast_path,
10040            Some(FastPath::LiteralTokens(_))
10041        ));
10042        let mut general = fast.clone();
10043        general.alternatives[0].fast_path = None;
10044
10045        for candidate in [
10046            b"src/readme".as_slice(),
10047            b"SRC/README".as_slice(),
10048            b"src/readme.txt".as_slice(),
10049            b"src/readme/".as_slice(),
10050            b"other/readme".as_slice(),
10051        ] {
10052            assert_eq!(
10053                fast.is_match(candidate),
10054                general.is_match(candidate),
10055                "fast path differs for {candidate:?}"
10056            );
10057        }
10058    }
10059
10060    #[test]
10061    fn deterministic_fast_path_matches_the_general_matcher() {
10062        let options = PatternOptions::default().case_insensitive(true);
10063        let fast = Pattern::compile("src/[ab]?.[Rr][Ss]", options).expect("pattern compiles");
10064        assert!(matches!(
10065            fast.alternatives[0].fast_path,
10066            Some(FastPath::DeterministicTokens(_))
10067        ));
10068        let mut general = fast.clone();
10069        general.alternatives[0].fast_path = None;
10070
10071        let mut candidates = vec![
10072            b"src/ab.rs".to_vec(),
10073            b"src/aX.RS".to_vec(),
10074            b"src/.a.rs".to_vec(),
10075            b"src/ab.txt".to_vec(),
10076            b"lib/ab.rs".to_vec(),
10077        ];
10078        candidates.extend(
10079            byte_words(b"ab./rsRS", 4)
10080                .into_iter()
10081                .map(|suffix| [b"src/".as_slice(), suffix.as_slice()].concat()),
10082        );
10083        for candidate in candidates {
10084            assert_eq!(
10085                fast.is_match(&candidate),
10086                general.is_match(&candidate),
10087                "fast path differs for {candidate:?}"
10088            );
10089        }
10090    }
10091
10092    #[test]
10093    fn infix_star_fast_path_matches_the_general_matcher() {
10094        let options = PatternOptions::default().case_insensitive(true);
10095        let fast = Pattern::compile("Src*.rs", options).expect("pattern compiles");
10096        assert!(matches!(
10097            fast.alternatives[0].fast_path,
10098            Some(FastPath::InfixStar { .. })
10099        ));
10100        let mut general = fast.clone();
10101        general.alternatives[0].fast_path = None;
10102
10103        let mut candidates = vec![
10104            b"src.rs".to_vec(),
10105            b"srcMain.RS".to_vec(),
10106            b"src/nested/main.rs".to_vec(),
10107            b"src/.hidden.rs".to_vec(),
10108            b"other.rs".to_vec(),
10109            b"src.txt".to_vec(),
10110        ];
10111        candidates.extend(
10112            byte_words(b"src./RS", 5)
10113                .into_iter()
10114                .map(|middle| [b"src".as_slice(), middle.as_slice(), b".rs"].concat()),
10115        );
10116        for candidate in candidates {
10117            assert_eq!(
10118                fast.is_match(&candidate),
10119                general.is_match(&candidate),
10120                "fast path differs for {candidate:?}"
10121            );
10122        }
10123    }
10124
10125    #[test]
10126    fn static_star_fast_path_matches_the_general_matcher() {
10127        let options = PatternOptions::default().case_insensitive(true);
10128        let fast = Pattern::compile("Src/Lib/*.RS", options).expect("pattern compiles");
10129        assert!(matches!(
10130            fast.alternatives[0].fast_path,
10131            Some(FastPath::StaticStar { .. })
10132        ));
10133        let mut general = fast.clone();
10134        general.alternatives[0].fast_path = None;
10135
10136        let mut candidates = vec![
10137            b"src/lib/.rs".to_vec(),
10138            b"src/lib/main.rs".to_vec(),
10139            b"SRC/LIB/main.RS".to_vec(),
10140            b"src/lib/nested/main.rs".to_vec(),
10141            b"src/lib/.hidden.rs".to_vec(),
10142            b"src/other/main.rs".to_vec(),
10143            b"src/lib/main.txt".to_vec(),
10144        ];
10145        candidates.extend(
10146            byte_words(b"ab./rsRS", 4)
10147                .into_iter()
10148                .map(|middle| [b"src/lib/".as_slice(), middle.as_slice(), b".rs"].concat()),
10149        );
10150        for candidate in candidates {
10151            assert_eq!(
10152                fast.is_match(&candidate),
10153                general.is_match(&candidate),
10154                "fast path differs for {candidate:?}"
10155            );
10156        }
10157    }
10158
10159    #[test]
10160    fn static_star_edge_fast_paths_match_the_general_matcher() {
10161        let options = PatternOptions::default().case_insensitive(true);
10162        for (pattern, candidates) in [
10163            (
10164                "Src/Lib/*",
10165                vec![
10166                    b"src/lib/".as_slice(),
10167                    b"src/lib/main.rs".as_slice(),
10168                    b"SRC/LIB/MAIN.RS".as_slice(),
10169                    b"src/lib/.hidden".as_slice(),
10170                    b"src/lib/nested/main.rs".as_slice(),
10171                    b"src/other/main.rs".as_slice(),
10172                ],
10173            ),
10174            (
10175                "*/Main.RS",
10176                vec![
10177                    b"main.rs".as_slice(),
10178                    b"src/main.rs".as_slice(),
10179                    b"SRC/DEEP/MAIN.RS".as_slice(),
10180                    b".hidden/main.rs".as_slice(),
10181                    b"src/main.txt".as_slice(),
10182                ],
10183            ),
10184        ] {
10185            let fast = Pattern::compile(pattern, options).expect("pattern compiles");
10186            assert!(matches!(
10187                fast.alternatives[0].fast_path,
10188                Some(FastPath::StaticStar { .. })
10189            ));
10190            let mut general = fast.clone();
10191            general.alternatives[0].fast_path = None;
10192            for candidate in candidates {
10193                assert_eq!(
10194                    fast.is_match(candidate),
10195                    general.is_match(candidate),
10196                    "fast path differs for {pattern:?} against {candidate:?}"
10197                );
10198            }
10199        }
10200    }
10201
10202    #[test]
10203    fn single_star_fast_paths_match_the_general_matcher() {
10204        let mut candidates = byte_words(b"ab./rs", 4);
10205        candidates.extend([b"src..rs".to_vec(), b"src/.hidden".to_vec()]);
10206        for pattern in ["*", "src*", "*.rs"] {
10207            let fast =
10208                Pattern::compile(pattern, PatternOptions::default()).expect("pattern compiles");
10209            assert!(fast.alternatives[0].fast_path.is_some());
10210            let mut general = fast.clone();
10211            general.alternatives[0].fast_path = None;
10212            for candidate in &candidates {
10213                assert_eq!(
10214                    fast.is_match(candidate),
10215                    general.is_match(candidate),
10216                    "fast path differs for {pattern:?} against {candidate:?}"
10217                );
10218                assert_eq!(
10219                    fast.is_match_glob_path(candidate),
10220                    general.is_match_glob_path(candidate),
10221                    "component fast path differs for {pattern:?} against {candidate:?}"
10222                );
10223            }
10224        }
10225    }
10226
10227    #[test]
10228    fn component_suffix_fast_paths_preserve_separator_and_hidden_rules() {
10229        let star = Pattern::compile("*.ts", PatternOptions::default()).unwrap();
10230        assert!(star.is_match_glob_path("index.ts"));
10231        assert!(!star.is_match_glob_path(".ts"));
10232        assert!(!star.is_match_glob_path("src/index.ts"));
10233        assert!(!star.is_match_glob_path(".hidden.ts"));
10234
10235        let recursive = Pattern::compile(
10236            "**/*.ts",
10237            PatternOptions::default().recursive_double_star(true),
10238        )
10239        .unwrap();
10240        assert!(recursive.is_match_glob_path("index.ts"));
10241        assert!(recursive.is_match_glob_path("src/deep/index.ts"));
10242        assert!(!recursive.is_match_glob_path("src/.ts"));
10243        assert!(!recursive.is_match_glob_path("src/.hidden.ts"));
10244        assert!(!recursive.is_match_glob_path(".hidden/index.ts"));
10245
10246        let hidden = Pattern::compile(
10247            "**/*.ts",
10248            PatternOptions::default()
10249                .recursive_double_star(true)
10250                .match_hidden(true),
10251        )
10252        .unwrap();
10253        assert!(hidden.is_match_glob_path("src/.ts"));
10254        assert!(hidden.is_match_glob_path("src/.hidden.ts"));
10255        assert!(hidden.is_match_glob_path(".hidden/index.ts"));
10256    }
10257
10258    #[test]
10259    fn filter_paths_preserves_input_order_and_borrows_inputs() {
10260        let pattern = Pattern::compile("*.txt", PatternOptions::default()).unwrap();
10261        let paths = ["first.txt", "skip.rs", "second.txt"];
10262        assert_eq!(
10263            pattern.filter_paths(&paths),
10264            vec![&"first.txt", &"second.txt"]
10265        );
10266    }
10267
10268    #[test]
10269    fn filter_paths_keeps_wildcards_after_a_separator_in_one_component() {
10270        let pattern = Pattern::compile(
10271            "**/lua/*.lua",
10272            PatternOptions::default().recursive_double_star(true),
10273        )
10274        .unwrap();
10275        let paths = [
10276            "lua/init.lua",
10277            "nvim/lua/setup.lua",
10278            "nvim/lua/sub/nested.lua",
10279        ];
10280        assert_eq!(
10281            pattern.filter_paths(&paths),
10282            vec![&"lua/init.lua", &"nvim/lua/setup.lua"]
10283        );
10284        assert!(pattern.is_match("nvim/lua/sub/nested.lua"));
10285        assert!(pattern.is_match_path("nvim/lua/setup.lua"));
10286        assert!(!pattern.is_match_path("nvim/lua/sub/nested.lua"));
10287
10288        let root_pattern = Pattern::compile("*.rs", PatternOptions::default()).unwrap();
10289        assert!(root_pattern.is_match_path("src/nested.rs"));
10290        assert!(!root_pattern.is_match_glob_path("src/nested.rs"));
10291        assert!(root_pattern.is_match_glob_path("nested.rs"));
10292
10293        let extglob = Pattern::compile("@(a*)", PatternOptions::default().extglob(true)).unwrap();
10294        assert!(extglob.is_match_glob_path("aaa"));
10295        assert!(!extglob.is_match_glob_path("aaa/nested"));
10296        let nested_extglob =
10297            Pattern::compile("@(foo)/*/bar", PatternOptions::default().extglob(true)).unwrap();
10298        assert!(nested_extglob.is_match_glob_path("foo/a/bar"));
10299        assert!(!nested_extglob.is_match_glob_path("foo/a/deep/bar"));
10300    }
10301
10302    #[test]
10303    fn extglobs_share_each_entry_points_component_policy() {
10304        let options = PatternOptions::default().extglob(true);
10305        let mixed = Pattern::compile("*/x/b*@(y)", options).unwrap();
10306        assert!(mixed.is_match("a/b/x/bzy"));
10307        assert!(mixed.is_match_path("a/b/x/bzy"));
10308        assert!(!mixed.is_match_glob_path("a/b/x/bzy"));
10309
10310        let root = Pattern::compile("*@(y)", options).unwrap();
10311        assert!(root.is_match("a/b/zy"));
10312        assert!(root.is_match_path("a/b/zy"));
10313        assert!(!root.is_match_glob_path("a/b/zy"));
10314
10315        for pattern in [&mixed, &root] {
10316            assert!(pattern.engines_agree("a/b/x/bzy"));
10317            assert!(pattern.engines_agree("a/b/zy"));
10318        }
10319    }
10320
10321    #[test]
10322    fn recursive_prefix_before_extglob_matches_at_depth_zero() {
10323        let options = PatternOptions::default()
10324            .extglob(true)
10325            .recursive_double_star(true);
10326        let pattern = Pattern::compile("**/@(x)", options).unwrap();
10327        for candidate in ["x", "a/x"] {
10328            assert!(pattern.is_match(candidate), "is_match: {candidate}");
10329            assert!(
10330                pattern.is_match_path(candidate),
10331                "is_match_path: {candidate}"
10332            );
10333            assert!(
10334                pattern.is_match_glob_path(candidate),
10335                "is_match_glob_path: {candidate}"
10336            );
10337            assert!(pattern.engines_agree(candidate), "engines: {candidate}");
10338        }
10339
10340        let negated_suffix = Pattern::compile("**/*.!(js)", options).unwrap();
10341        assert!(negated_suffix.is_match_path("src/app.ts"));
10342        assert!(!negated_suffix.is_match_path("src/app.js"));
10343    }
10344
10345    #[test]
10346    fn filter_paths_precompiles_its_component_sensitive_ir() {
10347        let component_pattern = Pattern::compile("src/*.rs", PatternOptions::default())
10348            .expect("component pattern compiles");
10349        assert!(
10350            component_pattern
10351                .path_filter
10352                .as_ref()
10353                .is_some_and(|filter| {
10354                    filter.compiled.len() == 1
10355                        && filter.compiled[0].fast_path.is_none()
10356                        && filter.programs.is_empty()
10357                        && filter.whole.is_empty()
10358                })
10359        );
10360
10361        let root_pattern =
10362            Pattern::compile("*.rs", PatternOptions::default()).expect("root pattern compiles");
10363        assert!(root_pattern.path_filter.is_none());
10364    }
10365
10366    #[test]
10367    fn filter_paths_routes_each_brace_arm_on_its_own() {
10368        let options = PatternOptions::default().braces(true).extglob(true);
10369        // The extglob arm is answered from its shared program; the plain
10370        // arms share one pass over their component copies.
10371        let mixed = Pattern::compile("{zz,lib/*,src/@(*.ts)}", options).expect("pattern compiles");
10372        let filter = mixed
10373            .path_filter
10374            .as_ref()
10375            .expect("component arms need the list-filter routes");
10376        assert_eq!(filter.compiled.len(), 2);
10377        assert_eq!(filter.programs, [2]);
10378        assert!(filter.whole.is_empty());
10379
10380        // Without a plain arm that needs a copy, plain arms keep the
10381        // whole-sequence route and nothing is compiled twice.
10382        let lean = Pattern::compile("{zz,src/@(*.ts)}", options).expect("pattern compiles");
10383        let filter = lean
10384            .path_filter
10385            .as_ref()
10386            .expect("the extglob arm is routed");
10387        assert!(filter.compiled.is_empty());
10388        assert_eq!(filter.programs, [1]);
10389        assert_eq!(filter.whole, [0]);
10390
10391        // Issue #393: an extglob group directly behind a separator is
10392        // component-local, and a sibling arm cannot change that verdict.
10393        for pattern in [
10394            "src/@(*.ts)",
10395            "src/+(*.ts)",
10396            "src/?(*.ts)",
10397            "src/*(*.ts)",
10398            "src/!(x)",
10399            "src/@(x|*.ts)",
10400            "{src/@(*.ts),zz}",
10401            "{src/@(*.ts),lib/*}",
10402            "{zz,src/@(*.ts)}",
10403            "./src/@(*.ts)",
10404        ] {
10405            let pattern = Pattern::compile(pattern, options).expect("pattern compiles");
10406            assert!(pattern.is_match_path("src/b.ts"));
10407            assert!(!pattern.is_match_path("src/a/b.ts"));
10408            assert!(pattern.is_match("src/a/b.ts") || pattern.is_match("./src/a/b.ts"));
10409            assert!(!pattern.is_match_glob_path("src/a/b.ts"));
10410        }
10411
10412        // Only a repetition's first iteration stands behind the separator; a
10413        // later one follows the previous iteration's text, so `src/+(*.ts)`
10414        // reads like `src/*.ts*.ts` there. Both engines agree on it.
10415        let repeated_like = Pattern::compile("src/*.ts*.ts", options).expect("pattern compiles");
10416        for pattern in ["src/+(*.ts)", "src/*(*.ts)"] {
10417            let pattern = Pattern::compile(pattern, options).expect("pattern compiles");
10418            for candidate in ["src/a.tsb/c.ts", "src/a.ts/c.ts", "src/a/b.ts"] {
10419                assert_eq!(
10420                    pattern.is_match_path(candidate),
10421                    repeated_like.is_match_path(candidate),
10422                    "{candidate}"
10423                );
10424                assert!(pattern.engines_agree(candidate), "{candidate}");
10425            }
10426            let mut interpreted = pattern.clone();
10427            for alternative in interpreted.compiled_alternatives_mut() {
10428                if let Some(program) = alternative.extglob.as_mut() {
10429                    program.positive_nfa = None;
10430                }
10431            }
10432            for candidate in ["src/a.tsb/c.ts", "src/a/b.ts", "src/a.ts"] {
10433                assert_eq!(
10434                    pattern.is_match_path(candidate),
10435                    interpreted.is_match_path(candidate),
10436                    "{candidate}"
10437                );
10438            }
10439        }
10440    }
10441
10442    /// A one-alternative group answers both path entry points exactly like its
10443    /// alternative written inline: the group's position is the position of
10444    /// its first wildcard, a later wildcard in the alternative keeps its own
10445    /// position, and the outer program backtracks through the group as it does
10446    /// through plain syntax.
10447    #[test]
10448    fn extglob_groups_follow_the_plain_position_rule() {
10449        let pairs = [
10450            // Directly behind a separator: component-local (issue #393).
10451            ("src/@(*.ts)", "src/*.ts"),
10452            ("a/@(*)/z", "a/*/z"),
10453            ("x/@(*[z])*", "x/*[z]*"),
10454            // Behind a literal or at the root: crossing, as in `a/b*`.
10455            ("src/a@(*.ts)", "src/a*.ts"),
10456            ("@(*.ts)", "*.ts"),
10457            ("*/a@(*.ts)", "*/a*.ts"),
10458            ("a/@(b*)/z", "a/b*/z"),
10459            ("*/@(b*)/z", "*/b*/z"),
10460            ("a/@(?*)/z", "a/?*/z"),
10461            // A crossing star before a component-local one keeps its
10462            // backtrack point.
10463            ("*/*.@(ts)", "*/*.ts"),
10464            ("a*/*.@(ts)", "a*/*.ts"),
10465            ("**/*.@(ts)", "**/*.ts"),
10466            ("**/lua/@(*.lua)", "**/lua/*.lua"),
10467            // A spelled separator inside the group is ordinary syntax.
10468            ("*/@(a/b)", "*/a/b"),
10469            // A star run of two or more reads as the token engine reads it:
10470            // recursive under `recursive_double_star`, otherwise ordinary
10471            // stars of which only the first stands behind the separator.
10472            ("x@(y)/***/z", "xy/***/z"),
10473            ("src/@(*.ts)/***", "src/*.ts/***"),
10474            ("src/**/*.@(ts)", "src/**/*.ts"),
10475            // A trailing `/**` also accepts the path without it, and a
10476            // whole-component `**` in a group may stand for no directory at
10477            // all, exactly as written inline (#422).
10478            ("@(x)/**", "x/**"),
10479            ("x/@(**)", "x/**"),
10480            ("@(**)/z", "**/z"),
10481            ("a/@(**)/z", "a/**/z"),
10482            ("@(x/**)/z", "x/**/z"),
10483            ("@(**/**)/z", "**/**/z"),
10484            ("@(**)/*", "**/*"),
10485        ];
10486        let candidates = [
10487            "xy/b/c/z",
10488            "src/a.ts/b/c",
10489            "src/a/b/c.ts",
10490            "a/b/c/d.ts",
10491            "b.ts",
10492            "a/b.ts",
10493            "src/b.ts",
10494            "src/ab.ts",
10495            "src/a/b.ts",
10496            "src/ax/b.ts",
10497            "x/c.ts",
10498            "x/a/b/c.ts",
10499            "ax/b/c.ts",
10500            "q/a/b.ts",
10501            "q/ab.ts",
10502            "a/b/z",
10503            "a/bx/z",
10504            "a/b/c/z",
10505            "x/z",
10506            "x/zq",
10507            "x/a/z",
10508            "x/a/b",
10509            "lua/a.lua",
10510            "x/lua/a.lua",
10511            "x/lua/a/b.lua",
10512            "x/y/lua/a.lua",
10513            "x",
10514            "z",
10515            "a/z",
10516            ".z",
10517            "x/.z",
10518            "sz",
10519        ];
10520        for recursive in [false, true] {
10521            let options = PatternOptions::default()
10522                .extglob(true)
10523                .recursive_double_star(recursive);
10524            for (extglob, plain) in pairs {
10525                let extglob = Pattern::compile(extglob, options).expect("pattern compiles");
10526                let plain = Pattern::compile(plain, options).expect("pattern compiles");
10527                for candidate in candidates {
10528                    assert_eq!(
10529                        (
10530                            extglob.is_match(candidate),
10531                            extglob.is_match_path(candidate),
10532                            extglob.is_match_glob_path(candidate)
10533                        ),
10534                        (
10535                            plain.is_match(candidate),
10536                            plain.is_match_path(candidate),
10537                            plain.is_match_glob_path(candidate)
10538                        ),
10539                        "{extglob:?} against {candidate} (recursive {recursive})"
10540                    );
10541                    assert!(extglob.engines_agree(candidate), "{candidate}");
10542                }
10543            }
10544        }
10545
10546        // A recursive star inside a group crosses separators under both path
10547        // entry points; the group is not cut at the component end.
10548        let options = PatternOptions::default()
10549            .extglob(true)
10550            .recursive_double_star(true);
10551        for (pattern, candidate) in [("a/@(x|**)/*.ts", "a/b/c/d.ts"), ("*/@(**)/z", "a/b/c/z")] {
10552            let pattern = Pattern::compile(pattern, options).expect("pattern compiles");
10553            assert!(pattern.is_match_path(candidate), "{candidate}");
10554            assert!(pattern.is_match_glob_path(candidate), "{candidate}");
10555            assert!(pattern.engines_agree(candidate), "{candidate}");
10556        }
10557    }
10558
10559    /// A group next to a whole-component `**` answers every entry point like
10560    /// the union of its alternatives written inline (#422): before a trailing
10561    /// `/**` the path without that suffix matches, and a `**` that ends an
10562    /// alternative in front of a separator may stand for no directory. Both
10563    /// extglob engines agree on it.
10564    #[test]
10565    fn extglob_groups_around_a_globstar_read_like_their_inlined_alternatives() {
10566        let inlined: &[(&str, &[&str])] = &[
10567            ("@(a|b)/**", &["a/**", "b/**"]),
10568            ("?(a)/**", &["a/**", "/**"]),
10569            ("a/@(**)", &["a/**"]),
10570            ("a/?(**|b)", &["a/**", "a/b", "a/"]),
10571            ("@(**)/a", &["**/a"]),
10572            ("@(**|b)/a", &["**/a", "b/a"]),
10573            ("?(**)/a", &["**/a", "/a"]),
10574            ("@(**)/.a", &["**/.a"]),
10575            ("@(**)/*", &["**/*"]),
10576            ("@(**)/?", &["**/?"]),
10577            ("a/@(**)/b", &["a/**/b"]),
10578            ("@(a/**)/b", &["a/**/b"]),
10579            ("@(**/**)/a", &["**/**/a"]),
10580            ("@(a/**/**)/b", &["a/**/**/b"]),
10581            ("@(**/a|**)/b", &["**/a/b", "**/b"]),
10582            ("@(**)//a", &["**//a"]),
10583            ("/@(**)/", &["/**/"]),
10584            ("@(****)/a", &["****/a"]),
10585            ("@(***)/a", &["***/a"]),
10586        ];
10587        let candidates = byte_words(b"ab./", 4);
10588        for match_hidden in [false, true] {
10589            let options = PatternOptions::default()
10590                .extglob(true)
10591                .recursive_double_star(true)
10592                .match_hidden(match_hidden);
10593            for &(source, alternatives) in inlined {
10594                let group = Pattern::compile(source, options).expect("pattern compiles");
10595                let mut interpreted = group.clone();
10596                for alternative in interpreted.compiled_alternatives_mut() {
10597                    if let Some(program) = alternative.extglob.as_mut() {
10598                        program.positive_nfa = None;
10599                    }
10600                }
10601                let alternatives = alternatives
10602                    .iter()
10603                    .map(|alternative| Pattern::compile(alternative, options).unwrap())
10604                    .collect::<Vec<_>>();
10605                let answers = |pattern: &Pattern, candidate: &[u8]| {
10606                    [
10607                        pattern.is_match(candidate),
10608                        pattern.is_match_path(candidate),
10609                        pattern.is_match_glob_path(candidate),
10610                    ]
10611                };
10612                for candidate in &candidates {
10613                    // The path entry points drop one leading `./`, which an
10614                    // inlined alternative that starts with a separator would
10615                    // read differently.
10616                    if candidate.starts_with(b"./") {
10617                        continue;
10618                    }
10619                    let mut expected = alternatives.iter().fold([false; 3], |any, alternative| {
10620                        let answers = answers(alternative, candidate);
10621                        [0, 1, 2].map(|entry| any[entry] || answers[entry])
10622                    });
10623                    let mut actual = answers(&group, candidate);
10624                    let mut actual_interpreted = answers(&interpreted, candidate);
10625                    // Under the list filter the outer program reads a
10626                    // wildcard behind the separator of `**/` as behind a
10627                    // separator, where the token engine does not. That only
10628                    // shows where the wildcard would have to match the
10629                    // separator of an empty component, as `**/?@(x)` and
10630                    // `**/?x` already differ on `a//x`, so those candidates
10631                    // are left out of that entry point here.
10632                    if candidate.first() == Some(&b'/')
10633                        || candidate.windows(2).any(|pair| pair == b"//")
10634                    {
10635                        expected[1] = false;
10636                        actual[1] = false;
10637                        actual_interpreted[1] = false;
10638                    }
10639                    let lossy = String::from_utf8_lossy(candidate);
10640                    assert_eq!(
10641                        actual, expected,
10642                        "{source:?} against {lossy:?} (is_match, is_match_path, \
10643                         is_match_glob_path), match_hidden {match_hidden}"
10644                    );
10645                    assert_eq!(
10646                        actual_interpreted, expected,
10647                        "interpreter: {source:?} against {lossy:?}, match_hidden {match_hidden}"
10648                    );
10649                    assert!(
10650                        group.engines_agree(candidate),
10651                        "{source:?} against {lossy:?}"
10652                    );
10653                }
10654            }
10655        }
10656
10657        // Shapes without an inline spelling: the outer trailing `/**` still
10658        // accepts the path in front of it, and a repeated `**` alternative has
10659        // the same zero-directory reading as a single one.
10660        let options = PatternOptions::default()
10661            .extglob(true)
10662            .recursive_double_star(true);
10663        for (source, candidate, expected) in [
10664            ("!(y)/**", "x", true),
10665            ("!(y)/**", "y", false),
10666            ("!(y)/**", "x/a", true),
10667            ("*(a)/**", "a", true),
10668            ("*(a)/**", "aa", true),
10669            ("*(a)/**", "b", false),
10670            ("+(a)/**", "a", true),
10671            ("+(**)/a", "a", true),
10672            ("*(**)/a", "a", true),
10673            ("*(**)/a", "b/a", true),
10674            ("+(b|**)/a", "b/a", true),
10675            ("x/*(**)", "x", true),
10676            ("x/!(**)", "x", false),
10677            ("!(**)/a", "a", false),
10678        ] {
10679            let pattern = Pattern::compile(source, options).expect("pattern compiles");
10680            let mut interpreted = pattern.clone();
10681            for alternative in interpreted.compiled_alternatives_mut() {
10682                if let Some(program) = alternative.extglob.as_mut() {
10683                    program.positive_nfa = None;
10684                }
10685            }
10686            for matcher in [&pattern, &interpreted] {
10687                assert_eq!(
10688                    [
10689                        matcher.is_match(candidate),
10690                        matcher.is_match_path(candidate),
10691                        matcher.is_match_glob_path(candidate),
10692                    ],
10693                    [expected; 3],
10694                    "{source} against {candidate}"
10695                );
10696            }
10697            assert!(
10698                pattern.engines_agree(candidate),
10699                "{source} against {candidate}"
10700            );
10701        }
10702    }
10703
10704    #[test]
10705    fn component_deterministic_fast_path_matches_the_general_matcher() {
10706        let options = PatternOptions::default().case_insensitive(true);
10707        let fast = Pattern::compile("src/[ab]?.[Rr][Ss]", options).expect("pattern compiles");
10708        assert!(fast.path_filter.as_ref().is_some_and(|filter| {
10709            matches!(
10710                filter.compiled[0].fast_path,
10711                Some(FastPath::DeterministicTokens(_))
10712            )
10713        }));
10714        let mut general = fast.clone();
10715        for alternative in general.compiled_alternatives_mut() {
10716            alternative.fast_path = None;
10717        }
10718
10719        let mut candidates = vec![
10720            b"src/a1.rs".as_slice(),
10721            b"src/Bx.RS".as_slice(),
10722            b"src/c1.rs".as_slice(),
10723            b"src/a/.rs".as_slice(),
10724            b"src/.1.rs".as_slice(),
10725            b"src/a1.rs/extra".as_slice(),
10726        ]
10727        .into_iter()
10728        .map(ToOwned::to_owned)
10729        .collect::<Vec<_>>();
10730        candidates.extend(
10731            byte_words(b"ab./rRsS", 4)
10732                .into_iter()
10733                .map(|suffix| [b"src/".as_slice(), suffix.as_slice()].concat()),
10734        );
10735        for candidate in candidates {
10736            assert_eq!(
10737                fast.is_match_path(&candidate),
10738                general.is_match_path(&candidate),
10739                "path-list candidate: {}",
10740                String::from_utf8_lossy(&candidate)
10741            );
10742            assert_eq!(
10743                fast.is_match_glob_path(&candidate),
10744                general.is_match_glob_path(&candidate),
10745                "glob-path candidate: {}",
10746                String::from_utf8_lossy(&candidate)
10747            );
10748        }
10749    }
10750
10751    #[test]
10752    fn filter_paths_at_matches_relative_to_a_component_boundary() {
10753        let recursive = Pattern::compile(
10754            "**/*.c",
10755            PatternOptions::default().recursive_double_star(true),
10756        )
10757        .expect("recursive pattern compiles");
10758        let paths = [
10759            "/home/user/project/src/main.c",
10760            "/home/user/project/src/test/unit.c",
10761            "/home/user/project/lib/utils.c",
10762            "/home/user/project/docs/readme.md",
10763            "/home/user/project-other/ignored.c",
10764            "/short",
10765        ];
10766        let expected = vec![
10767            &"/home/user/project/src/main.c",
10768            &"/home/user/project/src/test/unit.c",
10769            &"/home/user/project/lib/utils.c",
10770        ];
10771        assert_eq!(
10772            recursive.filter_paths_at("/home/user/project/", &paths),
10773            expected
10774        );
10775        assert_eq!(
10776            recursive.filter_paths_at("/home/user/project", &paths),
10777            expected
10778        );
10779
10780        let literal = Pattern::compile("config.json", PatternOptions::default())
10781            .expect("literal pattern compiles");
10782        assert_eq!(
10783            literal.filter_paths_at(
10784                "/srv/data",
10785                &["/srv/data/config.json", "/srv/data/readme.md"],
10786            ),
10787            vec![&"/srv/data/config.json"]
10788        );
10789
10790        let dotted = Pattern::compile(
10791            "./**/*.c",
10792            PatternOptions::default().recursive_double_star(true),
10793        )
10794        .expect("dot-slash pattern compiles");
10795        assert_eq!(
10796            dotted.filter_paths_at(
10797                "/home/user/project",
10798                &[
10799                    "/home/user/project/src/main.c",
10800                    "/home/user/project/lib/utils.c"
10801                ],
10802            ),
10803            vec![
10804                &"/home/user/project/src/main.c",
10805                &"/home/user/project/lib/utils.c"
10806            ]
10807        );
10808    }
10809
10810    #[test]
10811    fn filter_path_indices_preserve_input_order_and_support_a_base() {
10812        let suffix =
10813            Pattern::compile("*.rs", PatternOptions::default()).expect("suffix pattern compiles");
10814        assert_eq!(
10815            suffix.filter_path_indices(&["foo.rs", "bar.txt", "baz.rs", "qux.md"]),
10816            vec![0, 2]
10817        );
10818        assert_eq!(
10819            suffix.filter_path_indices(&["z.rs", "a.rs", "m.rs"]),
10820            vec![0, 1, 2]
10821        );
10822
10823        let recursive = Pattern::compile(
10824            "src/**/*.rs",
10825            PatternOptions::default().recursive_double_star(true),
10826        )
10827        .expect("recursive pattern compiles");
10828        assert_eq!(
10829            recursive.filter_path_indices_at(
10830                "/home/me/proj",
10831                &[
10832                    "/home/me/proj/src/main.rs",
10833                    "/home/me/proj/src/lib/util.rs",
10834                    "/home/me/proj/tests/test.rs",
10835                    "/home/me/proj/Cargo.toml",
10836                ],
10837            ),
10838            vec![0, 1]
10839        );
10840    }
10841
10842    #[test]
10843    fn non_recursive_double_star_matches_one_star_per_entry_point() {
10844        let single = Pattern::compile("*/*.c", PatternOptions::default()).unwrap();
10845        let doubled = Pattern::compile("**/*.c", PatternOptions::default()).unwrap();
10846        for candidate in ["a.c", "d/a.c", "d/e/a.c", "d/e/a.rs"] {
10847            assert_eq!(
10848                doubled.is_match(candidate),
10849                single.is_match(candidate),
10850                "is_match differs for {candidate:?}"
10851            );
10852            assert_eq!(
10853                doubled.is_match_path(candidate),
10854                single.is_match_path(candidate),
10855                "is_match_path differs for {candidate:?}"
10856            );
10857            assert_eq!(
10858                doubled.is_match_glob_path(candidate),
10859                single.is_match_glob_path(candidate),
10860                "is_match_glob_path differs for {candidate:?}"
10861            );
10862        }
10863    }
10864
10865    #[test]
10866    fn filter_paths_normalizes_a_leading_dot_slash() {
10867        let options = PatternOptions::default().recursive_double_star(true);
10868        let bare = Pattern::compile("**/*.lua", options).unwrap();
10869        let dotted = Pattern::compile("./**/*.lua", options).unwrap();
10870        let paths = [
10871            "init.lua",
10872            "lua/setup.lua",
10873            "nested/deep/plugin.lua",
10874            "src/main.zig",
10875        ];
10876        assert_eq!(dotted.filter_paths(&paths), bare.filter_paths(&paths));
10877        assert!(!dotted.is_match("nested/deep/plugin.lua"));
10878
10879        for pattern in [&bare, &dotted] {
10880            assert!(pattern.is_match_path("lua/setup.lua"));
10881            assert!(pattern.is_match_path("./lua/setup.lua"));
10882            assert!(pattern.is_match_glob_path("lua/setup.lua"));
10883            assert!(pattern.is_match_glob_path("./lua/setup.lua"));
10884        }
10885
10886        let current_directory = compile("./");
10887        assert!(current_directory.is_match_path(""));
10888        assert!(current_directory.is_match_path("./"));
10889        assert!(current_directory.is_match_glob_path(""));
10890        assert!(current_directory.is_match_glob_path("./"));
10891        assert!(!current_directory.is_match(""));
10892    }
10893
10894    #[test]
10895    fn invalid_syntax_has_a_location() {
10896        let error = Pattern::compile("[abc", PatternOptions::default()).unwrap_err();
10897        assert_eq!(error.offset(), 0);
10898        assert_eq!(error.message(), "unclosed character class");
10899        assert!(compile("foo\\").is_match("foo\\"));
10900    }
10901
10902    #[test]
10903    fn invalid_classes_in_extglob_alternatives_are_compile_errors() {
10904        let options = PatternOptions::default().extglob(true);
10905        for (source, offset) in [
10906            ("@([)]", 2),
10907            ("@(a|[)]", 4),
10908            ("@(dead/[)]]/../x|src/main.rs)", 7),
10909        ] {
10910            let error = Pattern::compile(source, options)
10911                .expect_err("an invalid extglob alternative is rejected");
10912            assert_eq!(error.message(), "unclosed character class");
10913            assert_eq!(error.offset(), offset);
10914        }
10915    }
10916
10917    #[test]
10918    fn brace_expansion_errors_keep_source_offsets() {
10919        let options = PatternOptions::default().braces(true);
10920        for (pattern, offset) in [("abc{d,[}", 6), ("{a,b}[", 5), ("x{a,b}{c,[d}", 9)] {
10921            let error = Pattern::compile(pattern, options)
10922                .expect_err("the expanded alternative contains an unclosed class");
10923            assert_eq!(error.message(), "unclosed character class");
10924            assert_eq!(error.offset(), offset);
10925            assert_eq!(
10926                pattern.as_bytes().get(error.offset()),
10927                Some(&b'['),
10928                "{pattern} points into the caller's source"
10929            );
10930        }
10931    }
10932
10933    #[test]
10934    fn deeply_nested_unclosed_posix_openers_are_rejected() {
10935        let mut pattern = String::from("[");
10936        pattern.push_str("[:".repeat(32_768).as_str());
10937        let error = Pattern::compile(&pattern, PatternOptions::default()).unwrap_err();
10938        assert_eq!(error.offset(), 0);
10939        assert_eq!(error.message(), "unclosed character class");
10940    }
10941
10942    #[test]
10943    fn deeply_nested_posix_openers_with_a_final_bracket_compile_linearly() {
10944        let mut pattern = String::from("[");
10945        pattern.push_str("[:".repeat(32_768).as_str());
10946        pattern.push(']');
10947        assert!(Pattern::compile(&pattern, PatternOptions::default()).is_ok());
10948    }
10949
10950    #[test]
10951    fn invalid_posix_opener_does_not_hide_a_later_valid_class() {
10952        let pattern = Pattern::compile("[[:[:alpha:]]", PatternOptions::default())
10953            .expect("character class compiles");
10954        assert!(pattern.is_match("x"));
10955        assert!(!pattern.is_match("1"));
10956    }
10957
10958    #[test]
10959    fn has_wildcards_respects_brace_and_extglob_options() {
10960        let plain = PatternOptions::default();
10961        assert!(!Pattern::has_wildcards("literal", plain));
10962        assert!(Pattern::has_wildcards("file?.rs", plain));
10963        assert!(Pattern::has_wildcards("[[:alpha:]]", plain));
10964        assert!(!Pattern::has_wildcards("{src,lib}", plain));
10965        assert!(Pattern::has_wildcards(
10966            "{src,lib}",
10967            PatternOptions::default().braces(true)
10968        ));
10969        assert!(!Pattern::has_wildcards("@(src|lib)", plain));
10970        assert!(Pattern::has_wildcards(
10971            "@(src|lib)",
10972            PatternOptions::default().extglob(true)
10973        ));
10974        assert!(!Pattern::has_wildcards(
10975            "literal(",
10976            PatternOptions::default().extglob(true)
10977        ));
10978    }
10979
10980    /// Strips every route around the general engine, leaving the prefilter as
10981    /// the only thing between a candidate and the state walk.
10982    fn only_the_general_engine(pattern: &mut Pattern) {
10983        pattern.alternative_fast_path = None;
10984        for alternative in pattern.compiled_alternatives_mut() {
10985            alternative.fast_path = None;
10986        }
10987    }
10988
10989    /// The same pattern with the prefilter neutralized, so the engine decides
10990    /// on its own.
10991    fn without_the_prefilter(pattern: &Pattern) -> Pattern {
10992        let mut unfiltered = pattern.clone();
10993        for alternative in unfiltered.compiled_alternatives_mut() {
10994            alternative.prefilter = Prefilter::default();
10995        }
10996        unfiltered
10997    }
10998
10999    /// The prefilter may only reject what the general engine rejects.
11000    ///
11001    /// Every candidate is decided twice through the same engine — once with the
11002    /// compiled prefilter in front of it, once with the prefilter neutralized —
11003    /// across the three entry points that reach it, because each supplies its
11004    /// own `PatternOptions` and the prefilter is derived without them. The fast
11005    /// paths are removed from both copies so nothing routes around the engine
11006    /// and turns the comparison into a tautology.
11007    #[test]
11008    fn the_prefilter_rejects_only_what_the_general_engine_rejects() {
11009        let plain = PatternOptions::default();
11010        let recursive = PatternOptions::default().recursive_double_star(true);
11011        let folded = recursive.case_insensitive(true);
11012        let escaped = plain.escape(true);
11013        let cases: [(&str, PatternOptions); 24] = [
11014            // The bench case: nothing in the engine consults the trailing `b`.
11015            ("a*a*a*a*b", plain),
11016            ("*a*a*", plain),
11017            ("a*", plain),
11018            ("*a", plain),
11019            ("*", plain),
11020            ("?a*b?", plain),
11021            ("[ab]*b", plain),
11022            ("a/*/b", plain),
11023            ("a/**", recursive),
11024            ("**/a", recursive),
11025            ("**/*.b", recursive),
11026            ("**", recursive),
11027            ("a/**/b*b", recursive),
11028            ("A/**/*.B", folded),
11029            ("A*A*B", folded),
11030            ("./a/**/b", recursive),
11031            // Interior runs: fixed bytes with a wildcard on both sides.
11032            ("**/ab/**", recursive),
11033            ("**/a/*.b", recursive),
11034            ("*/ab/*", plain),
11035            ("*ab*", plain),
11036            ("*a\\/b*", escaped),
11037            ("./**/b./**", recursive),
11038            ("**/AB/**", folded),
11039            ("*A.B*", folded),
11040        ];
11041        let mut candidates = byte_words(b"ab./", 4);
11042        candidates.push(b"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa".to_vec());
11043        candidates.push(b"a/b/a/b/a/b.b".to_vec());
11044        candidates.push(b"A/B/A.B".to_vec());
11045        candidates.push(b"a/AB/b".to_vec());
11046        candidates.push(b"./a/b./b".to_vec());
11047
11048        for (source, options) in cases {
11049            let mut filtered = Pattern::compile(source, options).expect("case pattern compiles");
11050            only_the_general_engine(&mut filtered);
11051            let unfiltered = without_the_prefilter(&filtered);
11052            for candidate in &candidates {
11053                for (entry, filtered, unfiltered) in [
11054                    (
11055                        "is_match",
11056                        filtered.is_match(candidate),
11057                        unfiltered.is_match(candidate),
11058                    ),
11059                    (
11060                        "is_match_path",
11061                        filtered.is_match_path(candidate),
11062                        unfiltered.is_match_path(candidate),
11063                    ),
11064                    (
11065                        "is_match_glob_path",
11066                        filtered.is_match_glob_path(candidate),
11067                        unfiltered.is_match_glob_path(candidate),
11068                    ),
11069                ] {
11070                    assert_eq!(
11071                        filtered, unfiltered,
11072                        "{source} via {entry} disagrees with the unfiltered engine on {candidate:?}"
11073                    );
11074                }
11075            }
11076        }
11077    }
11078
11079    /// What the three facts are, on the shapes whose reading is not obvious.
11080    ///
11081    /// The separator before a terminal `**` is the one token that may consume
11082    /// nothing, so it belongs to neither the leading run nor the minimum
11083    /// length; `a/**` accepting `a` is what that buys.
11084    #[test]
11085    fn the_prefilter_leaves_out_the_separator_a_terminal_double_star_may_elide() {
11086        let recursive = PatternOptions::default().recursive_double_star(true);
11087        let terminal = Pattern::compile("ab/**", recursive).expect("terminal pattern compiles");
11088        let prefilter = &terminal.alternatives[0].prefilter;
11089        assert_eq!(
11090            prefilter.prefix, b"ab",
11091            "the elidable separator is left off"
11092        );
11093        assert_eq!(prefilter.suffix, b"", "a trailing run cannot hold a `**`");
11094        assert_eq!(prefilter.min_length, 2, "the separator is not counted");
11095        assert!(!prefilter.rejects(b"ab", false), "`ab/**` accepts `ab`");
11096
11097        // A separator anywhere else is an ordinary byte of the fixed run.
11098        let inner = Pattern::compile("ab/**/cd", recursive).expect("inner pattern compiles");
11099        let prefilter = &inner.alternatives[0].prefilter;
11100        assert_eq!(prefilter.prefix, b"ab/");
11101        assert_eq!(prefilter.suffix, b"cd");
11102        assert_eq!(prefilter.min_length, 5);
11103
11104        // Stars are worth nothing, every other token its own bytes.
11105        let mixed = Pattern::compile("a?[bc]*d", PatternOptions::default())
11106            .expect("mixed pattern compiles");
11107        let prefilter = &mixed.alternatives[0].prefilter;
11108        assert_eq!(prefilter.prefix, b"a");
11109        assert_eq!(prefilter.suffix, b"d");
11110        assert_eq!(prefilter.min_length, 4);
11111    }
11112
11113    /// The needle an interior run leaves, on the shapes that decide it.
11114    #[test]
11115    fn the_prefilter_searches_for_the_longest_interior_run() {
11116        let recursive = PatternOptions::default().recursive_double_star(true);
11117        let needle = |source: &str, options: PatternOptions| {
11118            let pattern = Pattern::compile(source, options).expect("needle pattern compiles");
11119            pattern.alternatives[0]
11120                .prefilter
11121                .interior
11122                .as_ref()
11123                .map(|interior| interior.0.needle().to_vec())
11124        };
11125        // Neither separator joins: `**/` may consume nothing at the start and
11126        // the terminal separator may consume nothing at the end.
11127        assert_eq!(
11128            needle("**/node_modules/**", recursive).as_deref(),
11129            Some(b"node_modules".as_slice())
11130        );
11131        // A separator between the run and a wildcard is consumed as a byte,
11132        // except on Windows, where it also accepts `\\`.
11133        let lib = if cfg!(windows) { "lib" } else { "lib/" };
11134        assert_eq!(
11135            needle("**/lib/*.js", recursive).as_deref(),
11136            Some(lib.as_bytes())
11137        );
11138        assert_eq!(
11139            needle("src/**/test/*.rs", recursive).as_deref(),
11140            Some(if cfg!(windows) {
11141                b"test".as_slice()
11142            } else {
11143                b"test/"
11144            })
11145        );
11146        // The longest run wins, the earlier one on a tie.
11147        assert_eq!(
11148            needle("*a*bcd*ef*", PatternOptions::default()).as_deref(),
11149            Some(b"bcd".as_slice())
11150        );
11151        assert_eq!(
11152            needle("*ab*cd*", PatternOptions::default()).as_deref(),
11153            Some(b"ab".as_slice())
11154        );
11155        // An escaped separator is a literal byte, compared exactly everywhere.
11156        assert_eq!(
11157            needle("*a\\/b*", PatternOptions::default().escape(true)).as_deref(),
11158            Some(b"a/b".as_slice())
11159        );
11160        // Fixed ends are compared in place, and a lone separator or the
11161        // elidable one carries nothing worth a search.
11162        assert_eq!(needle("ab/**", recursive), None);
11163        assert_eq!(needle("ab*cd", PatternOptions::default()), None);
11164        assert_eq!(needle("src/main.rs", PatternOptions::default()), None);
11165        assert_eq!(needle("*/*", PatternOptions::default()), None);
11166        assert_eq!(needle("**/*", recursive), None);
11167
11168        let excluded = Pattern::compile("**/node_modules/**", recursive).unwrap();
11169        let prefilter = &excluded.alternatives[0].prefilter;
11170        assert!(prefilter.rejects(b"src/lib/index.js", false));
11171        assert!(!prefilter.rejects(b"a/node_modules/b", false));
11172        assert!(!prefilter.rejects(b"node_modules", false));
11173        // Folding has no `memmem` form, so the search stands aside.
11174        assert!(!prefilter.rejects(b"a/NODE_MODULES/b", true));
11175    }
11176
11177    /// The prefilter may only reject what the engines behind it reject, over
11178    /// generated patterns and paths.
11179    ///
11180    /// The fragments lean on multi-byte literals next to every wildcard,
11181    /// separator and group form, so interior runs start and end in every
11182    /// position, and the path pieces reuse the same literals so a good share
11183    /// of candidates contain the needle and reach the engine. Both copies keep
11184    /// the sweep and every extglob program and only differ in their
11185    /// prefilters, nested extglob alternatives included; the fast paths are
11186    /// stripped from both so nothing answers before the prefilter is asked.
11187    #[test]
11188    fn the_prefilter_agrees_with_the_unfiltered_engines_over_randomized_patterns() {
11189        let fragments: &[&[u8]] = &[
11190            b"ab",
11191            b"nm",
11192            b"a",
11193            b"B",
11194            b".",
11195            b"/",
11196            b"./",
11197            b"*",
11198            b"?",
11199            b"**",
11200            b"**/",
11201            b"/**",
11202            b"[ab]",
11203            b"[!a]",
11204            b"{ab,B}",
11205            b"{a/b,nm}",
11206            b"\\/",
11207            b"\\*",
11208            b"@(ab|B)",
11209            b"!(nm)",
11210            b"+(a)",
11211            b"*(ab/)",
11212        ];
11213        let path_pieces: &[&[u8]] = &[
11214            b"a", b"b", b"B", b"ab", b"AB", b"nm", b"NM", b".", b"/", b"./", b"a/b",
11215        ];
11216        // The same reproducible generator as the sweep's randomized test.
11217        let mut seed = 0x9E37_79B9_7F4A_7C15_u64;
11218        let mut next = move |bound: usize| {
11219            seed = seed.wrapping_mul(0x2545_F491_4F6C_DD1D).wrapping_add(1);
11220            (usize::try_from(seed >> 33).expect("31 bits fit a usize")) % bound
11221        };
11222        let mut searched = 0_usize;
11223        for _ in 0..3_000 {
11224            let mut pattern = Vec::new();
11225            for _ in 0..1 + next(7) {
11226                pattern.extend_from_slice(fragments[next(fragments.len())]);
11227            }
11228            let options = PatternOptions::default()
11229                .braces(next(2) == 0)
11230                .recursive_double_star(next(4) != 0)
11231                .extglob(next(3) == 0)
11232                .match_hidden(next(2) == 0)
11233                .case_insensitive(next(4) == 0)
11234                .escape(next(2) == 0);
11235            let Ok(mut filtered) = Pattern::compile(&pattern, options) else {
11236                continue;
11237            };
11238            filtered.strip_engines(true, false, false);
11239            let mut unfiltered = filtered.clone();
11240            unfiltered.strip_engines(false, false, true);
11241            searched += usize::from(
11242                filtered
11243                    .alternatives
11244                    .iter()
11245                    .any(|alternative| alternative.prefilter.interior.is_some()),
11246            );
11247            for _ in 0..32 {
11248                let mut path = Vec::new();
11249                for _ in 0..next(9) {
11250                    path.extend_from_slice(path_pieces[next(path_pieces.len())]);
11251                }
11252                let shown = || {
11253                    format!(
11254                        "{:?} against {:?} under {options:?}",
11255                        String::from_utf8_lossy(&pattern),
11256                        String::from_utf8_lossy(&path)
11257                    )
11258                };
11259                assert_eq!(
11260                    filtered.is_match(&path),
11261                    unfiltered.is_match(&path),
11262                    "is_match: {}",
11263                    shown()
11264                );
11265                assert_eq!(
11266                    filtered.is_match_path(&path),
11267                    unfiltered.is_match_path(&path),
11268                    "is_match_path: {}",
11269                    shown()
11270                );
11271                assert_eq!(
11272                    filtered.is_match_glob_path(&path),
11273                    unfiltered.is_match_glob_path(&path),
11274                    "is_match_glob_path: {}",
11275                    shown()
11276                );
11277                // The walker's separator-crossing reading asks the `./`-free
11278                // copies under the pattern's own options.
11279                assert_eq!(
11280                    filtered.is_match_crossing_path(&path),
11281                    unfiltered.is_match_crossing_path(&path),
11282                    "is_match_crossing_path: {}",
11283                    shown()
11284                );
11285            }
11286        }
11287        assert!(
11288            searched > 500,
11289            "only {searched} generated patterns carry an interior search"
11290        );
11291    }
11292
11293    /// Asserts that the sweep engine and the memoized matcher agree on every
11294    /// candidate, under every entry point, and that the compiled dispatch
11295    /// answers the same.
11296    fn assert_sweep_agrees(pattern: &[u8], options: PatternOptions, candidates: &[Vec<u8>]) {
11297        let compiled = Pattern::compile(pattern, options).expect("differential pattern compiles");
11298        let mut sweep_only = compiled.clone();
11299        sweep_only.strip_engines(true, false, false);
11300        let mut memoized = sweep_only.clone();
11301        memoized.strip_engines(false, true, true);
11302        for candidate in candidates {
11303            let name = String::from_utf8_lossy(pattern);
11304            let shown = String::from_utf8_lossy(candidate);
11305            assert_eq!(
11306                sweep_only.is_match(candidate),
11307                memoized.is_match(candidate),
11308                "is_match diverges for {name:?} against {shown:?} under {options:?}"
11309            );
11310            assert_eq!(
11311                sweep_only.is_match_path(candidate),
11312                memoized.is_match_path(candidate),
11313                "is_match_path diverges for {name:?} against {shown:?} under {options:?}"
11314            );
11315            assert_eq!(
11316                sweep_only.is_match_glob_path(candidate),
11317                memoized.is_match_glob_path(candidate),
11318                "is_match_glob_path diverges for {name:?} against {shown:?} under {options:?}"
11319            );
11320            assert!(
11321                compiled.engines_agree(candidate),
11322                "an engine diverges for {name:?} against {shown:?} under {options:?}"
11323            );
11324        }
11325    }
11326
11327    #[test]
11328    fn sweep_engine_agrees_with_the_memoized_matcher_over_exhaustive_byte_words() {
11329        // Both letter cases, the hidden-file dot, and the separator: the four
11330        // byte roles the sweep tables distinguish.
11331        let words = byte_words(b"aB./", 4);
11332        let patterns: &[&[u8]] = &[
11333            b"*",
11334            b"?",
11335            b"a",
11336            b"*a",
11337            b"a*",
11338            b"*a*",
11339            b"a*B",
11340            b"*.*",
11341            b"a*a*a*B",
11342            b"?a?",
11343            b"[aB]",
11344            b"[!a]*",
11345            b"[.]a",
11346            b"[a-b]*",
11347            b"[[:alpha:]]?",
11348            b"*/",
11349            b"/*",
11350            b"a/*",
11351            b"*/a",
11352            b"a/?",
11353            b"a/??",
11354            b"**",
11355            b"**/",
11356            b"a/**",
11357            b"**/a",
11358            b"a/**/B",
11359            b"**/*.a",
11360            b"a/?*B",
11361            b"./a/*",
11362            b"\\*a",
11363            b"a\\/B",
11364            b"\\.a",
11365        ];
11366        for &pattern in patterns {
11367            for recursive in [false, true] {
11368                for match_hidden in [false, true] {
11369                    for case_insensitive in [false, true] {
11370                        let options = PatternOptions::default()
11371                            .recursive_double_star(recursive)
11372                            .match_hidden(match_hidden)
11373                            .case_insensitive(case_insensitive);
11374                        assert_sweep_agrees(pattern, options, &words);
11375                    }
11376                }
11377            }
11378        }
11379    }
11380
11381    #[test]
11382    fn sweep_engine_agrees_for_brace_alternatives_over_exhaustive_byte_words() {
11383        let words = byte_words(b"aB./", 4);
11384        for &pattern in [b"{a,B}*".as_slice(), b"{a/**,B?}", b"*.{a,B}"].iter() {
11385            for match_hidden in [false, true] {
11386                let options = PatternOptions::default()
11387                    .braces(true)
11388                    .recursive_double_star(true)
11389                    .match_hidden(match_hidden);
11390                assert_sweep_agrees(pattern, options, &words);
11391            }
11392        }
11393    }
11394
11395    #[test]
11396    fn sweep_engine_agrees_over_randomized_patterns_and_paths() {
11397        // A multiplicative generator keeps the search reproducible without a
11398        // random-number crate. The fragments cover every token kind the sweep
11399        // encodes, and the paths lean on the bytes with special roles.
11400        let fragments: &[&[u8]] = &[
11401            b"a", b"B", b".", b"/", b"*", b"?", b"**", b"[aB]", b"[!a]", b"[.-b]", b"{a,B}", b"\\*",
11402        ];
11403        let path_bytes = b"aB./";
11404        let mut seed = 0x0123_4567_89AB_CDEF_u64;
11405        let mut next = move |bound: usize| {
11406            seed = seed.wrapping_mul(0x2545_F491_4F6C_DD1D).wrapping_add(1);
11407            (usize::try_from(seed >> 33).expect("31 bits fit a usize")) % bound
11408        };
11409        for _ in 0..2_000 {
11410            let mut pattern = Vec::new();
11411            for _ in 0..next(8) {
11412                pattern.extend_from_slice(fragments[next(fragments.len())]);
11413            }
11414            let options = PatternOptions::default()
11415                .braces(next(2) == 0)
11416                .recursive_double_star(next(2) == 0)
11417                .match_hidden(next(2) == 0)
11418                .case_insensitive(next(2) == 0)
11419                .escape(next(2) == 0);
11420            let Ok(compiled) = Pattern::compile(&pattern, options) else {
11421                continue;
11422            };
11423            for _ in 0..24 {
11424                let mut path = Vec::new();
11425                for _ in 0..next(12) {
11426                    path.push(path_bytes[next(path_bytes.len())]);
11427                }
11428                assert!(
11429                    compiled.engines_agree(&path),
11430                    "engines diverge for {:?} against {:?} under {options:?}",
11431                    String::from_utf8_lossy(&pattern),
11432                    String::from_utf8_lossy(&path)
11433                );
11434            }
11435        }
11436    }
11437
11438    #[test]
11439    fn sweep_engine_is_present_exactly_where_the_general_path_needs_it() {
11440        let starred = Pattern::compile("a*a*B", PatternOptions::default()).unwrap();
11441        assert!(starred.alternatives[0].sweep.is_some());
11442
11443        // Literal and deterministic shapes never reach the general path.
11444        let literal = Pattern::compile("src/main.rs", PatternOptions::default()).unwrap();
11445        assert!(literal.alternatives[0].sweep.is_none());
11446        let deterministic = Pattern::compile("src/[ab]?.rs", PatternOptions::default()).unwrap();
11447        assert!(deterministic.alternatives[0].sweep.is_none());
11448
11449        // An extglob program keeps its own interpreter.
11450        let extglob = Pattern::compile("@(a|b)*", PatternOptions::default().extglob(true)).unwrap();
11451        assert!(extglob.alternatives[0].sweep.is_none());
11452
11453        // Past one machine word the wide sweep stays responsible, and the
11454        // retained memoized oracle still answers alike.
11455        let oversized_source = [b"a".repeat(70), b"*B".to_vec()].concat();
11456        let oversized = Pattern::compile(&oversized_source, PatternOptions::default()).unwrap();
11457        assert!(oversized.alternatives[0].sweep.is_some());
11458        let candidate = [b"a".repeat(70), b"xB".to_vec()].concat();
11459        assert!(oversized.is_match(&candidate));
11460        assert!(oversized.engines_agree(&candidate));
11461    }
11462
11463    #[test]
11464    fn sweeps_behind_a_complete_fast_path_are_built_only_where_they_run() {
11465        // A starred fast path that also answers under the component policy
11466        // wins every top-level dispatch, so its engine is not built there.
11467        let options = PatternOptions::walker();
11468        for source in ["*.rs", "**/*.rs", "src/**/*.rs"] {
11469            let pattern = Pattern::compile(source, options).unwrap();
11470            let alternative = &pattern.alternatives[0];
11471            assert!(
11472                alternative
11473                    .fast_path
11474                    .as_ref()
11475                    .is_some_and(FastPath::supports_component_wildcards),
11476                "{source} keeps its complete fast path"
11477            );
11478            assert!(alternative.sweep.is_none(), "{source} builds no engine");
11479            assert!(sweep_is_deferred(alternative));
11480            // The differential oracle still gets its engine on demand.
11481            let mut sweep_only = pattern.clone();
11482            sweep_only.strip_engines(true, false, false);
11483            assert!(sweep_only.alternatives[0].sweep.is_some());
11484            for path in ["lib.rs", "src/lib.rs", "src/.x.rs", "src/lib.ts"] {
11485                assert!(pattern.engines_agree(path), "{source} against {path}");
11486            }
11487        }
11488
11489        // An extglob arm's prefix scan runs the engine whatever its fast path
11490        // is, so the arm keeps one.
11491        let extglob = Pattern::compile("*(*.rs|x)", options).unwrap();
11492        let program = extglob.alternatives[0].extglob.as_ref().unwrap();
11493        let arm = &program.groups[0].alternatives[0].compiled[0];
11494        assert!(matches!(arm.fast_path, Some(FastPath::StarSuffix { .. })));
11495        assert!(arm.sweep.is_some());
11496
11497        // Budget use does not depend on whether the engine was built: the
11498        // same pattern charges as much as one whose engine is.
11499        let mut deferred = IrBudget::new();
11500        Pattern::compile_within(
11501            b"*.rs",
11502            options,
11503            &mut deferred,
11504            &mut ProvenanceBudget::new(),
11505            None,
11506            false,
11507            ComponentBounds::WHOLE,
11508        )
11509        .unwrap();
11510        let mut built = IrBudget::new();
11511        let tokens = [Token::Star, Token::Literal(b".rs".to_vec())];
11512        built.charge(tokens.len(), 0).unwrap();
11513        SweepEngine::compile(&tokens, options, &mut built).unwrap();
11514        assert_eq!(deferred.remaining, built.remaining);
11515    }
11516
11517    #[test]
11518    fn list_filter_copies_share_their_originals_engine() {
11519        // `docs/*.md` needs a component copy for `is_match_path`; with the
11520        // same tokens the copy reads its original's tables instead of
11521        // building its own.
11522        let pattern = Pattern::compile("docs/*.md", PatternOptions::walker()).unwrap();
11523        let filter = pattern.path_filter.as_ref().expect("a component copy");
11524        let (Some(original), Some(copy)) =
11525            (&pattern.alternatives[0].sweep, &filter.compiled[0].sweep)
11526        else {
11527            panic!("both run the sweep");
11528        };
11529        assert!(original.shares_tables_with(copy));
11530        assert!(pattern.is_match_path("docs/a.md"));
11531        assert!(!pattern.is_match_path("docs/a/b.md"));
11532
11533        // A removed `./` changes the tokens, so that copy builds its own.
11534        let dotted = Pattern::compile("./docs/*.md", PatternOptions::walker()).unwrap();
11535        let filter = dotted.path_filter.as_ref().expect("a stripped copy");
11536        assert!(filter.compiled[0].sweep.is_some());
11537        assert!(dotted.is_match_glob_path("docs/a.md"));
11538    }
11539
11540    #[test]
11541    fn wide_sweep_agrees_across_machine_word_boundaries() {
11542        let prefixes = [
11543            ["a".repeat(62), "***B".to_owned()].concat(),
11544            ["*a".repeat(40), "*B".to_owned()].concat(),
11545            ["a/".repeat(32), "**/B".to_owned()].concat(),
11546            ["[aB]?".repeat(33), "*".to_owned()].concat(),
11547        ];
11548        let mut candidates = Vec::new();
11549        for prefix in ["a".repeat(62), "a".repeat(80), "a/".repeat(32)] {
11550            for suffix in byte_words(b"aB./", 3) {
11551                candidates.push([prefix.as_bytes(), suffix.as_slice()].concat());
11552            }
11553        }
11554        for source in &prefixes {
11555            for match_hidden in [false, true] {
11556                let options = PatternOptions::default()
11557                    .recursive_double_star(true)
11558                    .match_hidden(match_hidden);
11559                assert_sweep_agrees(source.as_bytes(), options, &candidates);
11560            }
11561        }
11562    }
11563
11564    #[test]
11565    fn sweep_engine_keeps_the_terminal_recursive_star_end_of_path_case() {
11566        let options = PatternOptions::default().recursive_double_star(true);
11567        // `src/**` accepts `src` itself; `src/**/` does not, because the
11568        // recursive-prefix form still demands the separator.
11569        assert_sweep_agrees(
11570            b"src/**",
11571            options,
11572            &[
11573                b"src".to_vec(),
11574                b"src/".to_vec(),
11575                b"sr".to_vec(),
11576                b"src/a".to_vec(),
11577                b"src/.a".to_vec(),
11578            ],
11579        );
11580        assert_sweep_agrees(
11581            b"src/**/",
11582            options,
11583            &[b"src".to_vec(), b"src/".to_vec(), b"src/a/".to_vec()],
11584        );
11585        let terminal = Pattern::compile("src/**", options).unwrap();
11586        assert!(terminal.is_match("src"));
11587        let prefixed = Pattern::compile("src/**/", options).unwrap();
11588        assert!(!prefixed.is_match("src"));
11589    }
11590
11591    fn byte_words(alphabet: &[u8], max_length: usize) -> Vec<Vec<u8>> {
11592        let mut words = vec![Vec::new()];
11593        let mut current = vec![Vec::new()];
11594        for _ in 0..max_length {
11595            let mut next = Vec::new();
11596            for prefix in current {
11597                for &byte in alphabet {
11598                    let mut word = prefix.clone();
11599                    word.push(byte);
11600                    next.push(word);
11601                }
11602            }
11603            words.extend(next.iter().cloned());
11604            current = next;
11605        }
11606        words
11607    }
11608
11609    /// Which reach an ordinary wildcard has in [`reference_glob_match`].
11610    #[derive(Clone, Copy, Debug)]
11611    enum ReferenceReach {
11612        /// `is_match`: every ordinary wildcard crosses separators.
11613        Crossing,
11614        /// `is_match_path`: only a wildcard directly behind a `/` stays local.
11615        PathFilter,
11616        /// `is_match_glob_path`: every ordinary wildcard stays local.
11617        GlobPath,
11618    }
11619
11620    /// A deliberately naive specification of `**` as a whole path component
11621    /// (#419), written against the pattern bytes rather than the token IR.
11622    ///
11623    /// The generated alphabet has no braces, classes, escapes or extglobs. A
11624    /// run of exactly two stars bounded by `/` or a pattern end on both sides
11625    /// is a globstar: before a `/` it consumes zero or more whole components,
11626    /// each with its separator; at the end it consumes the rest of the path,
11627    /// and a `/**` suffix also accepts the path that stops before its `/`.
11628    /// Every other star is an ordinary one-byte-at-a-time wildcard. Without
11629    /// `match_hidden` nothing but a literal consumes the period that starts a
11630    /// component, and an ordinary star does not stop right before one.
11631    fn reference_glob_match(
11632        pattern: &[u8],
11633        path: &[u8],
11634        recursive: bool,
11635        reach: ReferenceReach,
11636        match_hidden: bool,
11637    ) -> bool {
11638        fn component_start(path: &[u8], index: usize) -> bool {
11639            index == 0 || path[index - 1] == b'/'
11640        }
11641        fn hidden_at(path: &[u8], index: usize, match_hidden: bool) -> bool {
11642            !match_hidden && path.get(index) == Some(&b'.') && component_start(path, index)
11643        }
11644        fn globstar_at(pattern: &[u8], index: usize) -> bool {
11645            pattern.get(index..index + 2) == Some(b"**")
11646                && pattern.get(index + 2) != Some(&b'*')
11647                && (index == 0 || pattern[index - 1] == b'/')
11648                && pattern.get(index + 2).is_none_or(|&byte| byte == b'/')
11649        }
11650        #[allow(clippy::too_many_arguments)]
11651        fn go(
11652            pattern: &[u8],
11653            path: &[u8],
11654            p: usize,
11655            s: usize,
11656            recursive: bool,
11657            reach: ReferenceReach,
11658            match_hidden: bool,
11659            memo: &mut std::collections::HashMap<(usize, usize), bool>,
11660        ) -> bool {
11661            if let Some(&known) = memo.get(&(p, s)) {
11662                return known;
11663            }
11664            let local = match reach {
11665                ReferenceReach::Crossing => false,
11666                ReferenceReach::GlobPath => true,
11667                // The `/` that ends a globstar prefix belongs to the prefix,
11668                // so the wildcard behind it is not behind an explicit
11669                // separator.
11670                ReferenceReach::PathFilter => {
11671                    p > 0
11672                        && pattern[p - 1] == b'/'
11673                        && !(recursive && p >= 3 && globstar_at(pattern, p - 3))
11674                }
11675            };
11676            let mut recurse = |p, s| go(pattern, path, p, s, recursive, reach, match_hidden, memo);
11677            let result = if p == pattern.len() {
11678                s == path.len()
11679            } else if recursive && globstar_at(pattern, p) {
11680                if p + 2 == pattern.len() {
11681                    // The rest of the path, through no hidden component start.
11682                    (s..path.len()).all(|index| !hidden_at(path, index, match_hidden))
11683                } else {
11684                    // Zero directories, or one whole visible component more.
11685                    recurse(p + 3, s) || {
11686                        let end = path[s..]
11687                            .iter()
11688                            .position(|&byte| byte == b'/')
11689                            .map(|offset| s + offset);
11690                        end.is_some_and(|end| {
11691                            !hidden_at(path, s, match_hidden) && recurse(p, end + 1)
11692                        })
11693                    }
11694                }
11695            } else {
11696                match pattern[p] {
11697                    b'*' => {
11698                        let stop = !hidden_at(path, s, match_hidden) && recurse(p + 1, s);
11699                        stop || (s < path.len()
11700                            && !(local && path[s] == b'/')
11701                            && !hidden_at(path, s, match_hidden)
11702                            && recurse(p, s + 1))
11703                    }
11704                    b'?' => {
11705                        s < path.len()
11706                            && !(local && path[s] == b'/')
11707                            && !hidden_at(path, s, match_hidden)
11708                            && recurse(p + 1, s + 1)
11709                    }
11710                    b'/' if recursive
11711                        && s == path.len()
11712                        && p + 3 == pattern.len()
11713                        && globstar_at(pattern, p + 1) =>
11714                    {
11715                        true
11716                    }
11717                    byte => path.get(s) == Some(&byte) && recurse(p + 1, s + 1),
11718                }
11719            };
11720            memo.insert((p, s), result);
11721            result
11722        }
11723        go(
11724            pattern,
11725            path,
11726            0,
11727            0,
11728            recursive,
11729            reach,
11730            match_hidden,
11731            &mut std::collections::HashMap::new(),
11732        )
11733    }
11734
11735    /// Every engine against the whole-component specification above, over
11736    /// every short pattern the alphabet spells (#419).
11737    #[test]
11738    fn every_engine_reads_double_star_as_a_whole_component_over_exhaustive_patterns() {
11739        // Four path bytes reach two nested components (`a/a/a`); the pattern
11740        // alphabet spells every globstar position up to five bytes, with `?`
11741        // up to four to keep the debug-build run short.
11742        let paths = byte_words(b"a./", 4);
11743        let mut patterns = byte_words(b"a*/.", 5);
11744        patterns.extend(byte_words(b"a*?/.", 4));
11745        patterns.sort_unstable();
11746        patterns.dedup();
11747        let mut compared = 0_usize;
11748        for pattern in patterns {
11749            // Longer runs keep their inherited token shape; the specification
11750            // above does not model them, and dedicated cases pin them.
11751            if pattern.windows(3).any(|window| window == b"***") {
11752                continue;
11753            }
11754            for recursive in [false, true] {
11755                for match_hidden in [false, true] {
11756                    let options = PatternOptions::default()
11757                        .recursive_double_star(recursive)
11758                        .match_hidden(match_hidden);
11759                    let compiled = Pattern::compile(&pattern, options).expect("pattern compiles");
11760                    let mut sweep_only = compiled.clone();
11761                    sweep_only.strip_engines(true, false, false);
11762                    let mut memoized = compiled.clone();
11763                    memoized.strip_engines(true, true, true);
11764                    for path in &paths {
11765                        let expected = [
11766                            ReferenceReach::Crossing,
11767                            ReferenceReach::PathFilter,
11768                            ReferenceReach::GlobPath,
11769                        ]
11770                        .map(|reach| {
11771                            reference_glob_match(&pattern, path, recursive, reach, match_hidden)
11772                        });
11773                        for (engine, matcher) in [
11774                            ("compiled", &compiled),
11775                            ("sweep", &sweep_only),
11776                            ("memoized", &memoized),
11777                        ] {
11778                            let actual = [
11779                                matcher.is_match(path),
11780                                // The path entry points ignore a leading `./`,
11781                                // which the specification does not model.
11782                                if path.starts_with(b"./") || pattern.starts_with(b"./") {
11783                                    expected[1]
11784                                } else {
11785                                    matcher.is_match_path(path)
11786                                },
11787                                if path.starts_with(b"./") || pattern.starts_with(b"./") {
11788                                    expected[2]
11789                                } else {
11790                                    matcher.is_match_glob_path(path)
11791                                },
11792                            ];
11793                            assert_eq!(
11794                                actual,
11795                                expected,
11796                                "{engine} engine: {:?} against {:?} (is_match, is_match_path, \
11797                                 is_match_glob_path) under {options:?}",
11798                                String::from_utf8_lossy(&pattern),
11799                                String::from_utf8_lossy(path),
11800                            );
11801                        }
11802                        compared += 1;
11803                    }
11804                }
11805            }
11806        }
11807        assert!(
11808            compared > 500_000,
11809            "the sweep compared only {compared} pairs"
11810        );
11811    }
11812
11813    #[test]
11814    fn only_a_whole_component_star_run_compiles_to_recursive_tokens() {
11815        let options = PatternOptions::default()
11816            .recursive_double_star(true)
11817            .braces(true);
11818        let tokens = |source: &str| {
11819            Pattern::compile(source, options)
11820                .expect("pattern compiles")
11821                .alternatives
11822                .into_iter()
11823                .map(|alternative| alternative.tokens)
11824                .collect::<Vec<_>>()
11825        };
11826        let literal = |bytes: &[u8]| Token::Literal(bytes.to_vec());
11827        assert_eq!(
11828            tokens("**/x"),
11829            [vec![Token::RecursivePrefix, literal(b"x")]]
11830        );
11831        assert_eq!(
11832            tokens("a/**"),
11833            [vec![literal(b"a"), Token::Separator, Token::RecursiveStar]]
11834        );
11835        assert_eq!(
11836            tokens("a**/x"),
11837            [vec![
11838                literal(b"a"),
11839                Token::Star,
11840                Token::Star,
11841                Token::Separator,
11842                literal(b"x"),
11843            ]]
11844        );
11845        assert_eq!(
11846            tokens("**x"),
11847            [vec![Token::Star, Token::Star, literal(b"x")]]
11848        );
11849        assert_eq!(
11850            tokens("x/**y"),
11851            [vec![
11852                literal(b"x"),
11853                Token::Separator,
11854                Token::Star,
11855                Token::Star,
11856                literal(b"y")
11857            ]]
11858        );
11859        // An escaped separator is a literal byte and bounds nothing.
11860        assert_eq!(
11861            tokens(r"a\/**"),
11862            [vec![literal(b"a/"), Token::Star, Token::Star]]
11863        );
11864        // Runs longer than two keep their inherited whole-component shape.
11865        assert_eq!(
11866            tokens("***/x"),
11867            [vec![
11868                Token::RecursiveStar,
11869                Token::Star,
11870                Token::Separator,
11871                literal(b"x"),
11872            ]]
11873        );
11874        // Each brace alternative is judged as the whole pattern it expands to.
11875        assert_eq!(
11876            tokens("{**,a**}/x"),
11877            [
11878                vec![Token::RecursivePrefix, literal(b"x")],
11879                vec![
11880                    literal(b"a"),
11881                    Token::Star,
11882                    Token::Star,
11883                    Token::Separator,
11884                    literal(b"x"),
11885                ],
11886            ]
11887        );
11888        // With the option off every run is ordinary, whatever its position.
11889        let ordinary = Pattern::compile("**/x", PatternOptions::default()).unwrap();
11890        assert_eq!(
11891            ordinary.alternatives[0].tokens,
11892            [Token::Star, Token::Star, Token::Separator, literal(b"x")]
11893        );
11894    }
11895
11896    #[test]
11897    fn extglob_alternatives_inherit_the_component_position_of_their_group() {
11898        let options = PatternOptions::default()
11899            .recursive_double_star(true)
11900            .extglob(true);
11901        let alternative_tokens = |source: &str| {
11902            let compiled = Pattern::compile(source, options).expect("pattern compiles");
11903            let program = compiled.alternatives[0]
11904                .extglob
11905                .as_ref()
11906                .expect("an extglob program")
11907                .clone();
11908            program.groups[0]
11909                .alternatives
11910                .iter()
11911                .map(|alternative| alternative.compiled[0].tokens.clone())
11912                .collect::<Vec<_>>()
11913        };
11914        assert_eq!(alternative_tokens("@(**)/y")[0], [Token::RecursiveStar]);
11915        assert_eq!(alternative_tokens("a/@(**)")[0], [Token::RecursiveStar]);
11916        assert_eq!(
11917            alternative_tokens("x@(**)/y")[0],
11918            [Token::Star, Token::Star]
11919        );
11920        assert_eq!(alternative_tokens("@(**)y")[0], [Token::Star, Token::Star]);
11921        assert_eq!(
11922            alternative_tokens("x@(**/y|z)")[0],
11923            [
11924                Token::Star,
11925                Token::Star,
11926                Token::Separator,
11927                Token::Literal(b"y".to_vec())
11928            ]
11929        );
11930        assert_eq!(
11931            alternative_tokens("@(y/**|z)x")[0],
11932            [
11933                Token::Literal(b"y".to_vec()),
11934                Token::Separator,
11935                Token::Star,
11936                Token::Star
11937            ]
11938        );
11939
11940        // The separator-crossing reading for groups whose alternatives cross,
11941        // and the filesystem-glob reading where an attached run must not.
11942        for (source, candidate, crossing, expected) in [
11943            ("@(**)/y", "a/b/y", true, true),
11944            ("@(**)/y", "a/b/y", false, true),
11945            ("@(**/x|z)", "a/b/x", false, true),
11946            ("x@(**)/y", "xa/b/y", false, false),
11947            ("x@(**)/y", "xa/y", false, true),
11948            ("@(**/x|z)", "sx", true, false),
11949            ("@(**/x|z)", "sx", false, false),
11950            ("@(**/x|z)", "a/b/x", true, true),
11951            ("+(**/x)", "a/x/b/x", true, true),
11952            ("+(**/x)", "a/sx", true, false),
11953            ("**/@(x)", "sx", true, false),
11954            ("**/@(x)", "a/x", false, true),
11955            ("a**/@(y)", "ay", true, false),
11956            ("a**/@(y)", "ab/y", false, true),
11957            ("a**/@(y)", "a/b/y", false, false),
11958        ] {
11959            let compiled = Pattern::compile(source, options).expect("pattern compiles");
11960            let actual = if crossing {
11961                compiled.is_match(candidate)
11962            } else {
11963                compiled.is_match_glob_path(candidate)
11964            };
11965            assert_eq!(actual, expected, "{source} against {candidate}");
11966            assert!(
11967                compiled.engines_agree(candidate),
11968                "{source} against {candidate}: engines disagree"
11969            );
11970        }
11971    }
11972}
11973
11974#[cfg(test)]
11975mod escape_tests {
11976    use super::{Pattern, PatternOptions, escape, escape_str};
11977
11978    /// Every byte that is syntax somewhere, next to ordinary bytes that are
11979    /// path structure (`.`, `/`), case-foldable, class-only syntax (`-`, `^`),
11980    /// or not UTF-8.
11981    const ALPHABET: &[u8] = b"\\*?[]{}(),|!@+-^./aAbZ: \x00\xc3\xa9\xff";
11982
11983    /// The reproducible generator the other randomized tests here use.
11984    fn generator(mut seed: u64) -> impl FnMut(usize) -> usize {
11985        move |bound: usize| {
11986            seed = seed.wrapping_mul(0x2545_F491_4F6C_DD1D).wrapping_add(1);
11987            (usize::try_from(seed >> 33).expect("31 bits fit a usize")) % bound
11988        }
11989    }
11990
11991    /// Every option combination with escaping on: 32 of them.
11992    fn escaping_options() -> Vec<PatternOptions> {
11993        (0..32_u8)
11994            .map(|bits| {
11995                PatternOptions::default()
11996                    .braces(bits & 1 != 0)
11997                    .recursive_double_star(bits & 2 != 0)
11998                    .extglob(bits & 4 != 0)
11999                    .match_hidden(bits & 8 != 0)
12000                    .case_insensitive(bits & 16 != 0)
12001            })
12002            .collect()
12003    }
12004
12005    fn random_text(next: &mut impl FnMut(usize) -> usize, max_len: usize) -> Vec<u8> {
12006        (0..next(max_len + 1))
12007            .map(|_| ALPHABET[next(ALPHABET.len())])
12008            .collect()
12009    }
12010
12011    /// The text itself and near misses: one byte inserted, removed, replaced,
12012    /// or case-swapped, a `./` added or removed, and unrelated text.
12013    fn candidates(text: &[u8], next: &mut impl FnMut(usize) -> usize) -> Vec<Vec<u8>> {
12014        let mut candidates = vec![text.to_vec(), [b"./", text].concat()];
12015        if let Some(rest) = text.strip_prefix(b"./") {
12016            candidates.push(rest.to_vec());
12017        }
12018        for _ in 0..12 {
12019            let mut candidate = text.to_vec();
12020            let at = next(candidate.len() + 1);
12021            match next(4) {
12022                0 => candidate.insert(at, ALPHABET[next(ALPHABET.len())]),
12023                1 if at < candidate.len() => {
12024                    candidate.remove(at);
12025                }
12026                2 if at < candidate.len() => candidate[at] = ALPHABET[next(ALPHABET.len())],
12027                3 if at < candidate.len() => candidate[at] ^= 0x20,
12028                _ => candidate = random_text(next, 6),
12029            }
12030            candidates.push(candidate);
12031        }
12032        candidates
12033    }
12034
12035    fn folded(bytes: &[u8], options: PatternOptions) -> Vec<u8> {
12036        if options.case_insensitive {
12037            bytes.to_ascii_lowercase()
12038        } else {
12039            bytes.to_vec()
12040        }
12041    }
12042
12043    /// The one leading `./` the path entry points ignore on both sides.
12044    fn without_dot_slash(bytes: &[u8]) -> &[u8] {
12045        bytes.strip_prefix(b"./").unwrap_or(bytes)
12046    }
12047
12048    /// `escape(text)` compiles under every option combination with escaping on
12049    /// and matches `text` and nothing else, where "the same" is what the entry
12050    /// point compares: whole bytes for `is_match`, bytes after one leading
12051    /// `./` for the path entry points, ASCII-folded under `case_insensitive`.
12052    ///
12053    /// On Windows a candidate path may use `\` as a separator, so a text with
12054    /// a backslash is not a Windows path and those cases are left to POSIX.
12055    #[test]
12056    fn escaped_text_matches_exactly_itself_under_every_option() {
12057        let options = escaping_options();
12058        let mut next = generator(0x0E5C_A9ED_7E57_0001);
12059        for _ in 0..1_500 {
12060            let text = random_text(&mut next, 10);
12061            let escaped = escape(&text);
12062            if let Ok(utf8) = std::str::from_utf8(&text) {
12063                assert_eq!(escape_str(utf8).as_bytes(), escaped, "{utf8:?}");
12064            }
12065            let candidates = candidates(&text, &mut next);
12066            for &options in &options {
12067                let shown = || format!("{:?} under {options:?}", String::from_utf8_lossy(&text));
12068                let pattern = Pattern::compile(&escaped, options)
12069                    .unwrap_or_else(|error| panic!("{error}: {}", shown()));
12070                for candidate in &candidates {
12071                    if cfg!(windows) && (text.contains(&b'\\') || candidate.contains(&b'\\')) {
12072                        continue;
12073                    }
12074                    let whole = folded(candidate, options) == folded(&text, options);
12075                    let path = folded(without_dot_slash(candidate), options)
12076                        == folded(without_dot_slash(&text), options);
12077                    let candidate_shown = String::from_utf8_lossy(candidate);
12078                    assert_eq!(
12079                        pattern.is_match(candidate),
12080                        whole,
12081                        "is_match({candidate_shown:?}): {}",
12082                        shown()
12083                    );
12084                    assert_eq!(
12085                        pattern.is_match_path(candidate),
12086                        path,
12087                        "is_match_path({candidate_shown:?}): {}",
12088                        shown()
12089                    );
12090                    assert_eq!(
12091                        pattern.is_match_glob_path(candidate),
12092                        path,
12093                        "is_match_glob_path({candidate_shown:?}): {}",
12094                        shown()
12095                    );
12096                }
12097            }
12098        }
12099    }
12100
12101    /// Escaped text stays literal as one alternative of a brace or extglob
12102    /// group, which is why `,` and `|` are escaped too.
12103    #[test]
12104    fn escaped_text_stays_literal_inside_a_group() {
12105        let mut next = generator(0x0E5C_A9ED_7E57_0002);
12106        for _ in 0..1_500 {
12107            // Group alternatives are compared within one component here.
12108            let left = random_text(&mut next, 6);
12109            let right = random_text(&mut next, 6);
12110            if [&left, &right]
12111                .iter()
12112                .any(|text| text.contains(&b'/') || (cfg!(windows) && text.contains(&b'\\')))
12113            {
12114                continue;
12115            }
12116            let options = PatternOptions::walker().match_hidden(next(2) == 0);
12117            let braces = [b"{", &escape(&left)[..], b",", &escape(&right), b"}"].concat();
12118            let extglob = [b"@(", &escape(&left)[..], b"|", &escape(&right), b")"].concat();
12119            for pattern in [braces, extglob] {
12120                let shown = || {
12121                    format!(
12122                        "{:?} for {:?} and {:?}",
12123                        String::from_utf8_lossy(&pattern),
12124                        String::from_utf8_lossy(&left),
12125                        String::from_utf8_lossy(&right)
12126                    )
12127                };
12128                let compiled = Pattern::compile(&pattern, options)
12129                    .unwrap_or_else(|error| panic!("{error}: {}", shown()));
12130                for candidate in candidates(&left, &mut next).iter().chain([&right]) {
12131                    let expected = *candidate == left || *candidate == right;
12132                    assert_eq!(
12133                        compiled.is_match(candidate),
12134                        expected,
12135                        "{:?}: {}",
12136                        String::from_utf8_lossy(candidate),
12137                        shown()
12138                    );
12139                }
12140            }
12141        }
12142    }
12143
12144    /// The fixed points worth reading: the bytes escaped, the ones left alone,
12145    /// and that escaping is not idempotent - it escapes its own `\`.
12146    #[test]
12147    fn escape_marks_every_syntax_byte_and_nothing_else() {
12148        assert_eq!(escape(br"\*?[]{}(),|!@+"), br"\\\*\?\[\]\{\}\(\)\,\|\!\@\+");
12149        assert_eq!(escape(b"./a-b^c/.d:e f\xff"), b"./a-b^c/.d:e f\xff");
12150        assert_eq!(escape(b""), b"");
12151        assert_eq!(escape(escape(b"*")), br"\\\*");
12152        assert_eq!(escape_str("caf\u{e9} [1]"), "caf\u{e9} \\[1\\]");
12153    }
12154
12155    /// With escaping switched off a backslash is an ordinary byte, so the
12156    /// escaped spelling is not literal there. This is the documented limit.
12157    #[test]
12158    fn escaped_text_needs_escaping_enabled() {
12159        let pattern = Pattern::compile(escape("a*"), PatternOptions::default().escape(false))
12160            .expect("valid pattern");
12161        assert!(!pattern.is_match("a*"));
12162        assert!(pattern.is_match(r"a\*"));
12163    }
12164}