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}