Skip to main content

ferralk_glob/
set.rs

1//! Many compiled patterns asked as one.
2//!
3//! A [`PatternSet`] answers exactly what asking each of its [`Pattern`]s in
4//! turn would answer: [`PatternSet::is_match_glob_path`] is the OR of the
5//! members' [`Pattern::is_match_glob_path`], and
6//! [`PatternSet::matches_glob_path_into`] lists every member that says yes.
7//! What it saves is asking the members that cannot say yes.
8//!
9//! # How members are skipped
10//!
11//! Every alternative a pattern compiled to carries fixed bytes that any match
12//! must contain in a known place — the facts [`Prefilter`] already reads off
13//! the tokens. The set files each alternative under one such fact, chosen in
14//! this order:
15//!
16//! 1. **Extension.** The trailing fixed run ends in `.ext`, with no further
17//!    period after it (`**/*.rs`, `src/*.d.ts`). A matching path then ends in
18//!    exactly those bytes, so its own final `.`-suffix *is* the key: one hash
19//!    lookup per candidate covers every such alternative.
20//! 2. **Component.** A literal token stands alone between component
21//!    boundaries — a separator, a `**/` or an end of the pattern — on both
22//!    sides (`**/node_modules/**`, `dist/**`). A separator token consumes
23//!    exactly one separator byte and `**/` hands over only at a component
24//!    start, so a matching path has that literal as one whole component.
25//! 3. **Suffix.** The trailing fixed run's last component, looked up once per
26//!    distinct key length.
27//! 4. **Prefix.** The leading fixed run's first component, likewise per
28//!    length.
29//!
30//! An alternative with none of these, or one answered by an extglob program
31//! whose tokens are not its semantics, makes its pattern a candidate for
32//! every path. A pattern is a candidate when any of its alternatives' keys is
33//! found, and every candidate is then asked through its own entry point. A
34//! key is only ever a necessary condition, so the answer is the members'
35//! answer by construction; `set_answers_like_its_members` holds that against
36//! generated pattern lists on every entry point.
37//!
38//! The one normalization the path entry points apply before matching — one
39//! leading `./` on the candidate, and on every alternative that starts with
40//! it — is applied here too: keys are looked up in the candidate the members
41//! will read, and an alternative with a leading `./` is keyed by what follows
42//! it. Such an alternative never files a prefix key, because
43//! [`Pattern::is_match`] still compares its `./` literally.
44//!
45//! [`Prefilter`]: crate::Prefilter
46
47use std::{
48    collections::HashMap,
49    error::Error,
50    fmt,
51    hash::{BuildHasherDefault, Hasher},
52};
53
54use memchr::memrchr;
55
56use crate::{
57    CompiledAlternative, Pattern, PatternError, PatternOptions, Prefilter, Token, next_separator,
58    without_leading_dot_slash,
59};
60
61/// A list of compiled [`Pattern`]s matched as one, the counterpart of
62/// `globset::GlobSet`.
63///
64/// Every query answers exactly what asking the members one by one would: the
65/// `is_match*` methods are the OR of the members' verdicts, and the
66/// `matches*_into` methods list each member that matches, by its position in
67/// the list the set was built from. The three pairs mirror the three
68/// [`Pattern`] entry points, so a set reads its patterns the way a single
69/// pattern would be read.
70///
71/// The set is faster than a loop because it asks only the members a path can
72/// possibly satisfy. Literal extensions, whole literal components, and fixed
73/// prefixes and suffixes are indexed once at construction, so a list of
74/// hundreds of `**/*.ext`, `dir/**` and `**/name/**` globs costs a few hash
75/// lookups per path rather than hundreds of matches. Patterns without such a
76/// literal, such as `*` or an extglob, are asked for every path, as they
77/// would be in a loop.
78///
79/// ```
80/// use ferralk_glob::{PatternOptions, PatternSet};
81///
82/// let set = PatternSet::new(
83///     ["**/*.rs", "**/node_modules/**", "docs/*.md"],
84///     PatternOptions::walker(),
85/// )?;
86///
87/// assert!(set.is_match_glob_path("src/lib.rs"));
88/// assert!(set.is_match_glob_path("web/node_modules/left-pad/index.js"));
89/// assert!(!set.is_match_glob_path("docs/api/index.md"));
90///
91/// // Which members matched, by their index in the input list.
92/// let mut matched = Vec::new();
93/// set.matches_glob_path_into("node_modules/pkg/build.rs", &mut matched);
94/// assert_eq!(matched, [0, 1]);
95/// # Ok::<(), ferralk_glob::PatternSetError>(())
96/// ```
97///
98/// Members may carry different [`PatternOptions`]: compile each [`Pattern`]
99/// yourself and collect them.
100///
101/// ```
102/// use ferralk_glob::{Pattern, PatternOptions, PatternSet};
103///
104/// let options = PatternOptions::walker();
105/// let set: PatternSet = [
106///     Pattern::compile("**/*.rs", options)?,
107///     // Only this member folds ASCII case.
108///     Pattern::compile("**/README.md", options.case_insensitive(true))?,
109/// ]
110/// .into_iter()
111/// .collect();
112///
113/// assert!(set.is_match_glob_path("docs/readme.MD"));
114/// assert!(!set.is_match_glob_path("src/LIB.RS"));
115/// # Ok::<(), ferralk_glob::PatternError>(())
116/// ```
117#[derive(Debug, Clone, Default)]
118pub struct PatternSet {
119    patterns: Vec<Pattern>,
120    /// Keys compared byte for byte, from case-sensitive members.
121    exact: KeyIndex,
122    /// Keys stored ASCII-lowercased, from case-insensitive members, and
123    /// looked up in a lowercased copy of the candidate.
124    folded: KeyIndex,
125    /// Members with an alternative no key describes, in ascending order.
126    always: Vec<usize>,
127}
128
129/// The entry point a query stands for.
130#[derive(Clone, Copy)]
131enum Entry {
132    Whole,
133    Path,
134    GlobPath,
135}
136
137impl Entry {
138    fn matches(self, pattern: &Pattern, path: &[u8]) -> bool {
139        match self {
140            Self::Whole => pattern.is_match(path),
141            Self::Path => pattern.is_match_path(path),
142            Self::GlobPath => pattern.is_match_glob_path(path),
143        }
144    }
145
146    /// The bytes the members compare their fixed runs against. Only the path
147    /// entry points drop a leading `./` before matching.
148    fn view(self, path: &[u8]) -> &[u8] {
149        match self {
150            Self::Whole => path,
151            Self::Path | Self::GlobPath => without_leading_dot_slash(path),
152        }
153    }
154}
155
156impl PatternSet {
157    /// Compiles every glob with the same `options`, keeping their order.
158    ///
159    /// ```
160    /// use ferralk_glob::{PatternOptions, PatternSet};
161    ///
162    /// let excludes = PatternSet::new(["**/target/**", "**/*.tmp"], PatternOptions::walker())?;
163    /// assert!(excludes.is_match_glob_path("crates/cli/target/debug/cli"));
164    /// assert!(!excludes.is_match_glob_path("crates/cli/src/main.rs"));
165    /// # Ok::<(), ferralk_glob::PatternSetError>(())
166    /// ```
167    ///
168    /// # Errors
169    ///
170    /// Returns a [`PatternSetError`] for the first glob that does not compile.
171    /// [`PatternSetError::index`] is that glob's position in `patterns`, and
172    /// [`PatternSetError::pattern_error`] the [`PatternError`] that
173    /// [`Pattern::compile`] reported for it.
174    ///
175    /// ```
176    /// use ferralk_glob::{PatternOptions, PatternSet};
177    ///
178    /// let error = PatternSet::new(["src/**", "[a-", "*.md"], PatternOptions::walker())
179    ///     .expect_err("the class is never closed");
180    /// assert_eq!(error.index(), 1);
181    /// assert_eq!(error.pattern_error().offset(), 0);
182    /// ```
183    pub fn new<I>(patterns: I, options: PatternOptions) -> Result<Self, PatternSetError>
184    where
185        I: IntoIterator,
186        I::Item: AsRef<[u8]>,
187    {
188        let patterns = patterns
189            .into_iter()
190            .enumerate()
191            .map(|(index, pattern)| {
192                Pattern::compile(pattern, options).map_err(|error| PatternSetError { index, error })
193            })
194            .collect::<Result<Vec<_>, _>>()?;
195        Ok(Self::from_patterns(patterns))
196    }
197
198    /// Builds the index over already compiled members.
199    fn from_patterns(patterns: Vec<Pattern>) -> Self {
200        let mut set = Self {
201            patterns,
202            ..Self::default()
203        };
204        for (index, pattern) in set.patterns.iter().enumerate() {
205            let keys = pattern
206                .alternatives
207                .iter()
208                .map(Key::of)
209                .collect::<Option<Vec<_>>>();
210            let Some(keys) = keys else {
211                set.always.push(index);
212                continue;
213            };
214            let target = if pattern.options.case_insensitive {
215                &mut set.folded
216            } else {
217                &mut set.exact
218            };
219            for key in keys {
220                target.insert(key, index, pattern.options.case_insensitive);
221            }
222        }
223        set
224    }
225
226    /// The number of member patterns.
227    ///
228    /// ```
229    /// use ferralk_glob::{PatternOptions, PatternSet};
230    ///
231    /// let set = PatternSet::new(["*.rs", "*.toml"], PatternOptions::walker())?;
232    /// assert_eq!(set.len(), 2);
233    /// # Ok::<(), ferralk_glob::PatternSetError>(())
234    /// ```
235    #[must_use]
236    pub fn len(&self) -> usize {
237        self.patterns.len()
238    }
239
240    /// Whether the set has no members. An empty set matches nothing.
241    ///
242    /// ```
243    /// use ferralk_glob::PatternSet;
244    ///
245    /// let set = PatternSet::default();
246    /// assert!(set.is_empty());
247    /// assert!(!set.is_match_glob_path("anything"));
248    /// ```
249    #[must_use]
250    pub fn is_empty(&self) -> bool {
251        self.patterns.is_empty()
252    }
253
254    /// Whether any member's [`Pattern::is_match`] accepts `path`: the whole
255    /// byte sequence, with wildcards crossing `/`.
256    ///
257    /// ```
258    /// use ferralk_glob::{PatternOptions, PatternSet};
259    ///
260    /// let set = PatternSet::new(["*.rs", "*.toml"], PatternOptions::walker())?;
261    /// // `*` crosses the separator under this entry point.
262    /// assert!(set.is_match("src/lib.rs"));
263    /// assert!(!set.is_match("src/lib.c"));
264    /// # Ok::<(), ferralk_glob::PatternSetError>(())
265    /// ```
266    #[must_use]
267    pub fn is_match(&self, path: impl AsRef<[u8]>) -> bool {
268        self.any(path.as_ref(), Entry::Whole)
269    }
270
271    /// Whether any member's [`Pattern::is_match_path`] accepts `path`, under
272    /// zlob's list-filter rule.
273    ///
274    /// ```
275    /// use ferralk_glob::{PatternOptions, PatternSet};
276    ///
277    /// let set = PatternSet::new(["*.rs", "docs/*.md"], PatternOptions::walker())?;
278    /// // A wildcard in the first component crosses separators...
279    /// assert!(set.is_match_path("./src/lib.rs"));
280    /// // ...one directly behind `/` does not.
281    /// assert!(!set.is_match_path("docs/api/index.md"));
282    /// # Ok::<(), ferralk_glob::PatternSetError>(())
283    /// ```
284    #[must_use]
285    pub fn is_match_path(&self, path: impl AsRef<[u8]>) -> bool {
286        self.any(path.as_ref(), Entry::Path)
287    }
288
289    /// Whether any member's [`Pattern::is_match_glob_path`] accepts `path`,
290    /// with every ordinary wildcard inside one component as in a shell. This
291    /// is the entry point for root-relative filesystem paths.
292    ///
293    /// ```
294    /// use ferralk_glob::{PatternOptions, PatternSet};
295    ///
296    /// let set = PatternSet::new(["*.toml", "src/**/*.rs"], PatternOptions::walker())?;
297    /// assert!(set.is_match_glob_path("Cargo.toml"));
298    /// assert!(set.is_match_glob_path("./src/bin/main.rs"));
299    /// // `*` stays in its component.
300    /// assert!(!set.is_match_glob_path("crates/cli/Cargo.toml"));
301    /// # Ok::<(), ferralk_glob::PatternSetError>(())
302    /// ```
303    #[must_use]
304    pub fn is_match_glob_path(&self, path: impl AsRef<[u8]>) -> bool {
305        self.any(path.as_ref(), Entry::GlobPath)
306    }
307
308    /// Replaces the contents of `matches` with the index of every member
309    /// whose [`Pattern::is_match`] accepts `path`, in ascending order.
310    ///
311    /// The vector is cleared first and reused, so a caller that asks about
312    /// many paths allocates only while it grows.
313    ///
314    /// ```
315    /// use ferralk_glob::{PatternOptions, PatternSet};
316    ///
317    /// let set = PatternSet::new(["*.rs", "src/*", "*.md"], PatternOptions::walker())?;
318    /// let mut matches = Vec::new();
319    /// set.matches_into("src/lib.rs", &mut matches);
320    /// assert_eq!(matches, [0, 1]);
321    /// # Ok::<(), ferralk_glob::PatternSetError>(())
322    /// ```
323    pub fn matches_into(&self, path: impl AsRef<[u8]>, matches: &mut Vec<usize>) {
324        self.collect(path.as_ref(), Entry::Whole, matches);
325    }
326
327    /// Replaces the contents of `matches` with the index of every member
328    /// whose [`Pattern::is_match_path`] accepts `path`, in ascending order.
329    /// The vector is cleared first.
330    ///
331    /// ```
332    /// use ferralk_glob::{PatternOptions, PatternSet};
333    ///
334    /// let set = PatternSet::new(["*.rs", "src/*.rs", "*.md"], PatternOptions::walker())?;
335    /// let mut matches = Vec::new();
336    /// set.matches_path_into("./src/bin/main.rs", &mut matches);
337    /// // `src/*.rs` keeps its `*` inside `bin/`, which holds a separator.
338    /// assert_eq!(matches, [0]);
339    /// # Ok::<(), ferralk_glob::PatternSetError>(())
340    /// ```
341    pub fn matches_path_into(&self, path: impl AsRef<[u8]>, matches: &mut Vec<usize>) {
342        self.collect(path.as_ref(), Entry::Path, matches);
343    }
344
345    /// Replaces the contents of `matches` with the index of every member
346    /// whose [`Pattern::is_match_glob_path`] accepts `path`, in ascending
347    /// order. The vector is cleared first.
348    ///
349    /// ```
350    /// use ferralk_glob::{PatternOptions, PatternSet};
351    ///
352    /// let rules = ["**/*.ts", "**/*.test.ts", "src/generated/**"];
353    /// let set = PatternSet::new(rules, PatternOptions::walker())?;
354    /// let mut matches = Vec::new();
355    ///
356    /// set.matches_glob_path_into("src/generated/api.test.ts", &mut matches);
357    /// assert_eq!(matches, [0, 1, 2]);
358    ///
359    /// // The last matching rule wins, as in many configuration formats.
360    /// set.matches_glob_path_into("src/app.ts", &mut matches);
361    /// assert_eq!(matches.last().map(|&index| rules[index]), Some("**/*.ts"));
362    /// # Ok::<(), ferralk_glob::PatternSetError>(())
363    /// ```
364    pub fn matches_glob_path_into(&self, path: impl AsRef<[u8]>, matches: &mut Vec<usize>) {
365        self.collect(path.as_ref(), Entry::GlobPath, matches);
366    }
367
368    fn any(&self, path: &[u8], entry: Entry) -> bool {
369        let found = self.for_each_candidate(entry.view(path), |index| {
370            entry.matches(&self.patterns[index], path)
371        });
372        found
373            || self
374                .always
375                .iter()
376                .any(|&index| entry.matches(&self.patterns[index], path))
377    }
378
379    fn collect(&self, path: &[u8], entry: Entry, matches: &mut Vec<usize>) {
380        matches.clear();
381        self.for_each_candidate(entry.view(path), |index| {
382            matches.push(index);
383            false
384        });
385        // The keyed candidates come bucket by bucket, and one member may sit
386        // in several buckets; the always-asked members are merged in last so
387        // a set without keys skips the sort.
388        if !matches.is_empty() {
389            matches.extend_from_slice(&self.always);
390            matches.sort_unstable();
391            matches.dedup();
392        } else {
393            matches.extend_from_slice(&self.always);
394        }
395        matches.retain(|&index| entry.matches(&self.patterns[index], path));
396    }
397
398    /// Calls `visit` with every keyed member whose key `view` contains, until
399    /// `visit` returns `true`. A member may be visited more than once.
400    fn for_each_candidate(&self, view: &[u8], mut visit: impl FnMut(usize) -> bool) -> bool {
401        if self.exact.find(view, &mut visit) {
402            return true;
403        }
404        if self.folded.is_empty() {
405            return false;
406        }
407        if view.iter().any(u8::is_ascii_uppercase) {
408            // Most candidates fit the stack copy; a longer one is folded on
409            // the heap rather than refused.
410            let mut buffer = [0; 256];
411            if let Some(folded) = buffer.get_mut(..view.len()) {
412                folded.copy_from_slice(view);
413                folded.make_ascii_lowercase();
414                self.folded.find(folded, &mut visit)
415            } else {
416                self.folded.find(&view.to_ascii_lowercase(), &mut visit)
417            }
418        } else {
419            self.folded.find(view, &mut visit)
420        }
421    }
422}
423
424impl FromIterator<Pattern> for PatternSet {
425    /// Collects compiled patterns into a set, keeping their order and each
426    /// member's own [`PatternOptions`].
427    ///
428    /// ```
429    /// use ferralk_glob::{Pattern, PatternOptions, PatternSet};
430    ///
431    /// let set: PatternSet = ["*.rs", "*.toml"]
432    ///     .iter()
433    ///     .map(|glob| Pattern::compile(glob, PatternOptions::walker()))
434    ///     .collect::<Result<_, _>>()?;
435    /// assert!(set.is_match_glob_path("Cargo.toml"));
436    /// # Ok::<(), ferralk_glob::PatternError>(())
437    /// ```
438    fn from_iter<I: IntoIterator<Item = Pattern>>(patterns: I) -> Self {
439        Self::from_patterns(patterns.into_iter().collect())
440    }
441}
442
443/// The glob in a [`PatternSet::new`] list that failed to compile.
444///
445/// ```
446/// use ferralk_glob::{PatternOptions, PatternSet};
447///
448/// let globs = ["**/*.rs", "src/[a-z*"];
449/// let error = PatternSet::new(globs, PatternOptions::walker()).expect_err("the class is never closed");
450///
451/// assert_eq!(globs[error.index()], "src/[a-z*");
452/// // The byte offset points into that glob, as for `Pattern::compile`.
453/// assert_eq!(error.pattern_error().offset(), 4);
454/// ```
455#[derive(Debug, Clone, PartialEq, Eq)]
456pub struct PatternSetError {
457    index: usize,
458    error: PatternError,
459}
460
461impl PatternSetError {
462    /// The position of the failed glob in the list given to
463    /// [`PatternSet::new`].
464    #[must_use]
465    pub const fn index(&self) -> usize {
466        self.index
467    }
468
469    /// The compile error of that glob, with its byte offset into it.
470    #[must_use]
471    pub const fn pattern_error(&self) -> &PatternError {
472        &self.error
473    }
474}
475
476impl fmt::Display for PatternSetError {
477    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
478        write!(formatter, "pattern {}: {}", self.index, self.error)
479    }
480}
481
482impl Error for PatternSetError {
483    fn source(&self) -> Option<&(dyn Error + 'static)> {
484        Some(&self.error)
485    }
486}
487
488/// One necessary condition an alternative files itself under. The bytes never
489/// contain a separator of any platform.
490enum Key {
491    /// The candidate ends in these bytes, which start with its final `.`.
492    Extension(Vec<u8>),
493    /// One whole component of the candidate equals these bytes.
494    Component(Vec<u8>),
495    /// The candidate ends in these bytes.
496    Suffix(Vec<u8>),
497    /// The candidate starts with these bytes.
498    Prefix(Vec<u8>),
499}
500
501impl Key {
502    /// The most selective key every match of `alternative` satisfies, or
503    /// `None` when the alternative must be asked about every candidate.
504    fn of(alternative: &CompiledAlternative) -> Option<Self> {
505        // An extglob program answers from its own compile; its tokens spell
506        // the group syntax out as text.
507        if alternative.extglob.is_some() {
508            return None;
509        }
510        // The path entry points read an alternative with a leading `./` from
511        // its stripped copy, so only what follows it is fixed for all three.
512        let stripped = alternative.raw.starts_with(b"./")
513            && matches!(
514                alternative.tokens.as_slice(),
515                [Token::Literal(dot), Token::Separator, ..] if dot == b"."
516            );
517        let tokens = if stripped {
518            &alternative.tokens[2..]
519        } else {
520            &alternative.tokens[..]
521        };
522        let fixed = Prefilter::compile(tokens);
523        let suffix = last_component(&fixed.suffix);
524        // A final literal that is a whole component holds the extension and
525        // more (`**/Cargo.toml`), so it selects strictly fewer paths.
526        if let Some(name) = tokens
527            .len()
528            .checked_sub(1)
529            .and_then(|last| whole_component_at(tokens, last))
530        {
531            return Some(Self::Component(name.to_vec()));
532        }
533        if let Some(dot) = memrchr(b'.', suffix) {
534            return Some(Self::Extension(suffix[dot..].to_vec()));
535        }
536        if let Some(component) = (0..tokens.len())
537            .rev()
538            .filter_map(|index| whole_component_at(tokens, index))
539            .max_by_key(|literal| literal.len())
540        {
541            return Some(Self::Component(component.to_vec()));
542        }
543        if !suffix.is_empty() {
544            return Some(Self::Suffix(suffix.to_vec()));
545        }
546        // `Pattern::is_match` compares a leading `./` literally, so a stripped
547        // alternative's first bytes are not where that entry point reads them.
548        let prefix = first_component(&fixed.prefix);
549        (!stripped && !prefix.is_empty()).then(|| Self::Prefix(prefix.to_vec()))
550    }
551}
552
553/// The bytes after the last separator-like byte of a fixed run. A run spells
554/// a separator token as `/`, and an escaped `\` is excluded too so a key is
555/// separator-free on every platform.
556fn last_component(run: &[u8]) -> &[u8] {
557    run.iter()
558        .rposition(|&byte| matches!(byte, b'/' | b'\\'))
559        .map_or(run, |separator| &run[separator + 1..])
560}
561
562/// The bytes before the first separator-like byte of a fixed run.
563fn first_component(run: &[u8]) -> &[u8] {
564    run.iter()
565        .position(|&byte| matches!(byte, b'/' | b'\\'))
566        .map_or(run, |separator| &run[..separator])
567}
568
569/// The literal token at `index` when it must match one whole candidate
570/// component: it starts the tokens or follows a separator or `**/`, and it
571/// ends them or precedes a separator.
572fn whole_component_at(tokens: &[Token], index: usize) -> Option<&[u8]> {
573    let Token::Literal(literal) = &tokens[index] else {
574        return None;
575    };
576    let starts =
577        index == 0 || matches!(tokens[index - 1], Token::Separator | Token::RecursivePrefix);
578    let ends = matches!(tokens.get(index + 1), None | Some(Token::Separator));
579    (starts
580        && ends
581        && !literal.is_empty()
582        && !literal.iter().any(|&byte| matches!(byte, b'/' | b'\\')))
583    .then_some(literal.as_slice())
584}
585
586/// Keys of one case policy, each mapped to the members that filed it.
587#[derive(Debug, Clone, Default)]
588struct KeyIndex {
589    extensions: KeyMap,
590    components: KeyMap,
591    /// Suffix maps, one per key length.
592    suffixes: Vec<(usize, KeyMap)>,
593    /// Prefix maps, one per key length.
594    prefixes: Vec<(usize, KeyMap)>,
595}
596
597/// Members by key, with the key lengths present kept aside so a candidate
598/// slice of another length is turned away before it is hashed.
599#[derive(Debug, Clone, Default)]
600struct KeyMap {
601    members: HashMap<Box<[u8]>, Vec<usize>, BuildHasherDefault<KeyHasher>>,
602    /// Bit `n` is set when a key of length `n` exists; bit 63 stands for
603    /// every length from 63 up.
604    lengths: u64,
605}
606
607impl KeyMap {
608    fn length_bit(length: usize) -> u64 {
609        1 << length.min(63)
610    }
611
612    fn is_empty(&self) -> bool {
613        self.members.is_empty()
614    }
615
616    #[cfg(test)]
617    fn len(&self) -> usize {
618        self.members.len()
619    }
620
621    fn insert(&mut self, key: Vec<u8>, index: usize) {
622        self.lengths |= Self::length_bit(key.len());
623        let members = self.members.entry(key.into_boxed_slice()).or_default();
624        // Members are inserted in index order, so a repeat is the last entry.
625        if members.last() != Some(&index) {
626            members.push(index);
627        }
628    }
629
630    fn get(&self, key: &[u8]) -> Option<&[usize]> {
631        if self.lengths & Self::length_bit(key.len()) == 0 {
632            return None;
633        }
634        self.members.get(key).map(Vec::as_slice)
635    }
636}
637
638impl KeyIndex {
639    fn is_empty(&self) -> bool {
640        self.extensions.is_empty()
641            && self.components.is_empty()
642            && self.suffixes.is_empty()
643            && self.prefixes.is_empty()
644    }
645
646    fn insert(&mut self, key: Key, index: usize, fold: bool) {
647        let (map, mut bytes) = match key {
648            Key::Extension(bytes) => (&mut self.extensions, bytes),
649            Key::Component(bytes) => (&mut self.components, bytes),
650            Key::Suffix(bytes) => (by_length(&mut self.suffixes, bytes.len()), bytes),
651            Key::Prefix(bytes) => (by_length(&mut self.prefixes, bytes.len()), bytes),
652        };
653        if fold {
654            bytes.make_ascii_lowercase();
655        }
656        map.insert(bytes, index);
657    }
658
659    /// Visits the members of every key `view` satisfies, until `visit` returns
660    /// `true`.
661    fn find(&self, view: &[u8], visit: &mut impl FnMut(usize) -> bool) -> bool {
662        let mut hits = |map: &KeyMap, key: &[u8]| {
663            map.get(key)
664                .is_some_and(|members| members.iter().any(|&index| visit(index)))
665        };
666        if !self.extensions.is_empty()
667            && let Some(dot) = memrchr(b'.', view)
668            && hits(&self.extensions, &view[dot..])
669        {
670            return true;
671        }
672        if !self.components.is_empty() {
673            let mut rest = view;
674            loop {
675                let end = next_separator(rest).unwrap_or(rest.len());
676                if hits(&self.components, &rest[..end]) {
677                    return true;
678                }
679                if end == rest.len() {
680                    break;
681                }
682                rest = &rest[end + 1..];
683            }
684        }
685        for (length, map) in &self.suffixes {
686            if let Some(start) = view.len().checked_sub(*length)
687                && hits(map, &view[start..])
688            {
689                return true;
690            }
691        }
692        for (length, map) in &self.prefixes {
693            if let Some(prefix) = view.get(..*length)
694                && hits(map, prefix)
695            {
696                return true;
697            }
698        }
699        false
700    }
701}
702
703fn by_length(maps: &mut Vec<(usize, KeyMap)>, length: usize) -> &mut KeyMap {
704    let position = match maps.iter().position(|(existing, _)| *existing == length) {
705        Some(position) => position,
706        None => {
707            maps.push((length, KeyMap::default()));
708            maps.len() - 1
709        }
710    };
711    &mut maps[position].1
712}
713
714/// A multiply-rotate hash for short byte keys, in the manner of `FxHash`.
715///
716/// The keys are pattern literals and the lookups candidate bytes, so the
717/// default SipHash's resistance to chosen collisions buys nothing: only the
718/// pattern author chooses keys, and a pattern author already chooses the
719/// cost of every match. Hashing a key is most of a lookup's cost, which is
720/// why the set does not pay for SipHash.
721#[derive(Debug, Clone, Copy, Default)]
722struct KeyHasher(u64);
723
724impl KeyHasher {
725    const SEED: u64 = 0x51_7c_c1_b7_27_22_0a_95;
726
727    fn mix(&mut self, word: u64) {
728        self.0 = (self.0.rotate_left(5) ^ word).wrapping_mul(Self::SEED);
729    }
730}
731
732impl Hasher for KeyHasher {
733    fn write(&mut self, bytes: &[u8]) {
734        let (words, rest) = bytes.as_chunks::<8>();
735        for &word in words {
736            self.mix(u64::from_le_bytes(word));
737        }
738        if !rest.is_empty() {
739            let mut word = [0; 8];
740            word[..rest.len()].copy_from_slice(rest);
741            self.mix(u64::from_le_bytes(word));
742        }
743    }
744
745    fn write_usize(&mut self, value: usize) {
746        self.mix(value as u64);
747    }
748
749    fn finish(&self) -> u64 {
750        self.0
751    }
752}
753
754#[cfg(test)]
755mod tests {
756    use super::*;
757
758    /// Every member's own verdicts on `path`, one list per entry point.
759    fn member_verdicts(patterns: &[Pattern], path: &[u8]) -> [Vec<usize>; 3] {
760        let indices = |entry: Entry| {
761            patterns
762                .iter()
763                .enumerate()
764                .filter_map(|(index, pattern)| entry.matches(pattern, path).then_some(index))
765                .collect::<Vec<_>>()
766        };
767        [
768            indices(Entry::Whole),
769            indices(Entry::Path),
770            indices(Entry::GlobPath),
771        ]
772    }
773
774    fn assert_set_agrees(
775        set: &PatternSet,
776        patterns: &[Pattern],
777        path: &[u8],
778        shown: &dyn Fn() -> String,
779    ) {
780        let [whole, path_filter, glob_path] = member_verdicts(patterns, path);
781        let mut matches = vec![usize::MAX];
782        set.matches_into(path, &mut matches);
783        assert_eq!(matches, whole, "matches_into: {}", shown());
784        set.matches_path_into(path, &mut matches);
785        assert_eq!(matches, path_filter, "matches_path_into: {}", shown());
786        set.matches_glob_path_into(path, &mut matches);
787        assert_eq!(matches, glob_path, "matches_glob_path_into: {}", shown());
788        assert_eq!(
789            set.is_match(path),
790            !whole.is_empty(),
791            "is_match: {}",
792            shown()
793        );
794        assert_eq!(
795            set.is_match_path(path),
796            !path_filter.is_empty(),
797            "is_match_path: {}",
798            shown()
799        );
800        assert_eq!(
801            set.is_match_glob_path(path),
802            !glob_path.is_empty(),
803            "is_match_glob_path: {}",
804            shown()
805        );
806    }
807
808    /// The set answers exactly like asking its members in a loop, on every
809    /// entry point, over generated pattern lists with mixed per-member
810    /// options and generated candidates.
811    #[test]
812    fn set_answers_like_its_members() {
813        let fragments: &[&[u8]] = &[
814            b"a",
815            b"B",
816            b"ab",
817            b"node_modules",
818            b".rs",
819            b".RS",
820            b".d.ts",
821            b".",
822            b"/",
823            b"./",
824            b"*",
825            b"?",
826            b"**",
827            b"**/",
828            b"/**",
829            b"[ab]",
830            b"[!.]",
831            b"{a,b}",
832            b"{./a,.rs}",
833            b"{a/b,x.rs}",
834            b"\\/",
835            b"\\.",
836            b"\\\\",
837            b"\\*",
838            b"@(ab|B)",
839            b"!(nm)",
840            b"+(a)",
841        ];
842        let path_pieces: &[&[u8]] = &[
843            b"a",
844            b"b",
845            b"B",
846            b"ab",
847            b"AB",
848            b".",
849            b"/",
850            b"./",
851            b"a/b",
852            b".rs",
853            b".RS",
854            b"x.d.ts",
855            b"node_modules",
856            b"NODE_MODULES",
857            b"\\",
858            b"",
859        ];
860        // The generator of the other randomized tests in this crate.
861        let mut seed = 0x2545_F491_4F6C_DD1D_u64;
862        let mut next = move |bound: usize| {
863            seed = seed.wrapping_mul(0x2545_F491_4F6C_DD1D).wrapping_add(1);
864            (usize::try_from(seed >> 33).expect("31 bits fit a usize")) % bound
865        };
866        let mut keyed = 0_usize;
867        let mut always = 0_usize;
868        for _ in 0..1_500 {
869            let mut sources = Vec::new();
870            let mut patterns = Vec::new();
871            for _ in 0..1 + next(8) {
872                let mut source = Vec::new();
873                for _ in 0..1 + next(5) {
874                    source.extend_from_slice(fragments[next(fragments.len())]);
875                }
876                let options = PatternOptions::default()
877                    .braces(next(3) != 0)
878                    .recursive_double_star(next(4) != 0)
879                    .extglob(next(3) == 0)
880                    .match_hidden(next(2) == 0)
881                    .case_insensitive(next(3) == 0)
882                    .escape(next(4) != 0);
883                if let Ok(pattern) = Pattern::compile(&source, options) {
884                    sources.push((source, options));
885                    patterns.push(pattern);
886                }
887            }
888            let set: PatternSet = patterns.iter().cloned().collect();
889            keyed += set.len() - set.always.len();
890            always += set.always.len();
891            for _ in 0..24 {
892                let mut path = Vec::new();
893                for _ in 0..next(7) {
894                    path.extend_from_slice(path_pieces[next(path_pieces.len())]);
895                }
896                let shown = || {
897                    format!(
898                        "{:?} against {:?}",
899                        sources
900                            .iter()
901                            .map(|(source, options)| format!(
902                                "{:?} {options:?}",
903                                String::from_utf8_lossy(source)
904                            ))
905                            .collect::<Vec<_>>(),
906                        String::from_utf8_lossy(&path)
907                    )
908                };
909                assert_set_agrees(&set, &patterns, &path, &shown);
910            }
911        }
912        assert!(
913            keyed > 2 * always,
914            "only {keyed} of {} generated members were keyed",
915            keyed + always
916        );
917    }
918
919    /// Realistic list shapes, each bucket kind with many members, against
920    /// realistic paths.
921    #[test]
922    fn realistic_lists_answer_like_their_members() {
923        let globs = [
924            "**/*.rs",
925            "**/*.d.ts",
926            "src/**/*.{ts,tsx}",
927            "**/node_modules/**",
928            "target/**",
929            "./docs/**/*.md",
930            "**/Makefile",
931            "**/*rc",
932            "test_*",
933            "**/.git/**",
934            "*.lock",
935            "**/*.Snap",
936            "**/!(index).js",
937            "src/*/mod.rs",
938            "**",
939            "{./a,b}/**",
940        ];
941        let paths = [
942            "src/lib.rs",
943            "./src/lib.rs",
944            "src/app/view.tsx",
945            "types/index.d.ts",
946            "web/node_modules/pkg/index.js",
947            "target/debug/build",
948            "target",
949            "docs/guide/intro.md",
950            "./docs/intro.md",
951            "tools/Makefile",
952            "home/.bashrc",
953            "test_parser",
954            ".git/HEAD",
955            "Cargo.lock",
956            "snapshots/view.snap",
957            "lib/index.js",
958            "lib/main.js",
959            "src/parser/mod.rs",
960            "a/x",
961            "./a/x",
962            "b",
963            "",
964            ".",
965            "./",
966        ];
967        for options in [
968            PatternOptions::walker(),
969            PatternOptions::walker().match_hidden(true),
970            PatternOptions::walker().case_insensitive(true),
971            PatternOptions::default(),
972        ] {
973            let set = PatternSet::new(globs, options).expect("the globs are valid");
974            let patterns = globs
975                .iter()
976                .map(|glob| Pattern::compile(glob, options).expect("the glob is valid"))
977                .collect::<Vec<_>>();
978            for path in paths {
979                for path in [path.to_owned(), path.to_ascii_uppercase()] {
980                    let shown = || format!("{path:?} under {options:?}");
981                    assert_set_agrees(&set, &patterns, path.as_bytes(), &shown);
982                }
983            }
984        }
985    }
986
987    #[test]
988    fn common_shapes_are_keyed() {
989        let set = PatternSet::new(
990            [
991                "**/*.rs",
992                "**/node_modules/**",
993                "dist/**",
994                "**/Makefile",
995                "**/*rc",
996                "test_*",
997                "./src/**",
998                "*",
999                "**/*.@(js|ts)",
1000            ],
1001            PatternOptions::walker(),
1002        )
1003        .expect("the globs are valid");
1004        assert_eq!(set.exact.extensions.len(), 1);
1005        assert_eq!(set.exact.components.len(), 4);
1006        assert_eq!(set.exact.suffixes.len(), 1);
1007        assert_eq!(set.exact.prefixes.len(), 1);
1008        // `*` has no literal, and an extglob program is not read from tokens.
1009        assert_eq!(set.always, [7, 8]);
1010    }
1011
1012    #[test]
1013    fn compile_errors_name_the_failed_glob() {
1014        let error = PatternSet::new(["*.rs", "*.md", "[a-"], PatternOptions::walker())
1015            .expect_err("the class is never closed");
1016        assert_eq!(error.index(), 2);
1017        assert_eq!(
1018            error.pattern_error(),
1019            &Pattern::compile("[a-", PatternOptions::walker()).expect_err("same error")
1020        );
1021        assert!(error.to_string().starts_with("pattern 2: "));
1022        assert!(
1023            error
1024                .source()
1025                .is_some_and(|source| source.to_string() == error.pattern_error().to_string())
1026        );
1027    }
1028
1029    #[test]
1030    fn pattern_set_is_send_sync_clone_debug() {
1031        fn assert_traits<T: Send + Sync + Clone + fmt::Debug + Default>() {}
1032        assert_traits::<PatternSet>();
1033        fn assert_error<T: Send + Sync + Error + Clone + Eq + 'static>() {}
1034        assert_error::<PatternSetError>();
1035    }
1036}