#[derive(Debug, Clone)]
pub(crate) enum Seg {
DoubleStar,
Glob(Vec<Tok>),
}
#[derive(Debug, Clone)]
pub(crate) enum Tok {
Lit(char),
Star,
Question,
Class {
negated: bool,
items: Vec<ClassItem>,
},
}
#[derive(Debug, Clone)]
pub(crate) enum ClassItem {
Ch(char),
Range(char, char),
}
#[derive(Debug)]
pub(crate) struct PathSeg<'a> {
pub(crate) text: &'a str,
ascii: bool,
}
impl<'a> PathSeg<'a> {
pub(crate) fn new(text: &'a str) -> Self {
Self {
text,
ascii: text.is_ascii(),
}
}
}
impl Seg {
pub(crate) fn literal(&self) -> Option<String> {
match self {
Seg::DoubleStar => None,
Seg::Glob(toks) => toks
.iter()
.map(|t| match t {
Tok::Lit(c) => Some(*c),
Tok::Star | Tok::Question | Tok::Class { .. } => None,
})
.collect(),
}
}
}
pub(crate) fn parse_segment(s: &str) -> Option<Seg> {
if s.len() >= 2 && s.bytes().all(|b| b == b'*') {
return Some(Seg::DoubleStar);
}
let chars: Vec<char> = s.chars().collect();
let mut toks = Vec::new();
let mut i = 0;
while i < chars.len() {
match chars[i] {
'\\' => {
if i + 1 < chars.len() {
toks.push(Tok::Lit(chars[i + 1]));
i += 2;
} else {
toks.push(Tok::Lit('\\'));
i += 1;
}
}
'*' => {
while i < chars.len() && chars[i] == '*' {
i += 1;
}
toks.push(Tok::Star);
}
'?' => {
toks.push(Tok::Question);
i += 1;
}
'[' => {
let (class, next) = parse_class(&chars, i)?;
toks.push(class);
i = next;
}
c => {
toks.push(Tok::Lit(c));
i += 1;
}
}
}
Some(Seg::Glob(toks))
}
fn parse_class(chars: &[char], open: usize) -> Option<(Tok, usize)> {
let mut i = open + 1;
let mut negated = false;
if i < chars.len() && (chars[i] == '!' || chars[i] == '^') {
negated = true;
i += 1;
}
let first = i;
let mut items = Vec::new();
let mut prev: Option<char> = None;
loop {
let c = *chars.get(i)?;
if c == ']' && i > first {
return Some((Tok::Class { negated, items }, i + 1));
}
if c == '\\' {
let escaped = *chars.get(i + 1)?;
items.push(ClassItem::Ch(escaped));
prev = Some(escaped);
i += 2;
} else if c == '-'
&& let Some(low) = prev
&& let Some(&next) = chars.get(i + 1)
&& next != ']'
{
let (high, width) = if next == '\\' {
(*chars.get(i + 2)?, 3)
} else {
(next, 2)
};
items.push(ClassItem::Range(low, high));
prev = None;
i += width;
} else {
items.push(ClassItem::Ch(c));
prev = Some(c);
i += 1;
}
}
}
fn class_matches(negated: bool, items: &[ClassItem], c: char) -> bool {
let hit = items.iter().any(|it| match it {
ClassItem::Ch(x) => *x == c,
ClassItem::Range(a, b) => *a <= c && c <= *b,
});
hit != negated
}
fn char_at(s: &str, i: usize) -> (char, usize) {
let c = s[i..].chars().next().unwrap_or(char::REPLACEMENT_CHARACTER);
(c, c.len_utf8())
}
pub(crate) fn match_segments(pat: &[Seg], path: &[PathSeg<'_>]) -> bool {
match pat.split_first() {
None => path.is_empty(),
Some((Seg::DoubleStar, rest)) => {
if rest.iter().any(|s| matches!(s, Seg::DoubleStar)) {
match_reachable(pat, path)
} else if rest.is_empty() {
!path.is_empty()
} else {
rest.len() <= path.len() && match_segments(rest, &path[path.len() - rest.len()..])
}
}
Some((Seg::Glob(toks), rest)) => match path.split_first() {
Some((head, tail)) if glob_seg_match(toks, head) => match_segments(rest, tail),
_ => false,
},
}
}
fn match_reachable(pat: &[Seg], path: &[PathSeg<'_>]) -> bool {
let mut cur = vec![false; path.len() + 1];
let mut next = vec![false; path.len() + 1];
cur[0] = true;
for (i, seg) in pat.iter().enumerate() {
next.fill(false);
match seg {
Seg::DoubleStar => {
let at_least = usize::from(i + 1 == pat.len());
if let Some(earliest) = cur.iter().position(|&r| r) {
for r in next.iter_mut().skip(earliest + at_least) {
*r = true;
}
}
}
Seg::Glob(toks) => {
for (j, seg) in path.iter().enumerate() {
if cur[j] && glob_seg_match(toks, seg) {
next[j + 1] = true;
}
}
}
}
std::mem::swap(&mut cur, &mut next);
if !cur.contains(&true) {
return false;
}
}
cur[path.len()]
}
fn glob_seg_match(toks: &[Tok], seg: &PathSeg<'_>) -> bool {
#[cfg(test)]
tests::GLOB_SEG_MATCHES.with(|n| n.set(n.get() + 1));
let bytes = seg.text.as_bytes();
let mut ti = 0;
let mut si = 0;
let mut star: Option<(usize, usize)> = None;
while si < bytes.len() {
if ti < toks.len() {
let (c, width) = if seg.ascii {
(char::from(bytes[si]), 1)
} else {
char_at(seg.text, si)
};
let matched = match &toks[ti] {
Tok::Star => {
star = Some((ti + 1, si));
ti += 1;
continue;
}
Tok::Question => true,
Tok::Lit(lit) => *lit == c,
Tok::Class { negated, items } => class_matches(*negated, items, c),
};
if matched {
ti += 1;
si += width;
continue;
}
}
if let Some((sti, ssi)) = star {
ti = sti;
let width = if seg.ascii {
1
} else {
char_at(seg.text, ssi).1
};
si = ssi + width;
star = Some((sti, si));
} else {
return false;
}
}
while ti < toks.len() && matches!(toks[ti], Tok::Star) {
ti += 1;
}
ti == toks.len()
}
#[cfg(test)]
mod tests {
use super::{PathSeg, Seg, glob_seg_match, match_segments, parse_segment};
use std::cell::Cell;
thread_local! {
pub(super) static GLOB_SEG_MATCHES: Cell<usize> = const { Cell::new(0) };
}
fn count_matches(f: impl FnOnce() -> bool) -> (bool, usize) {
let before = GLOB_SEG_MATCHES.with(Cell::get);
let result = f();
(result, GLOB_SEG_MATCHES.with(Cell::get) - before)
}
fn match_segments_reference(pat: &[Seg], path: &[PathSeg<'_>]) -> bool {
match pat.split_first() {
None => path.is_empty(),
Some((Seg::DoubleStar, rest)) => {
if rest.iter().any(|s| matches!(s, Seg::DoubleStar)) {
(0..=path.len()).any(|i| match_segments_reference(rest, &path[i..]))
} else if rest.is_empty() {
!path.is_empty()
} else {
rest.len() <= path.len()
&& match_segments_reference(rest, &path[path.len() - rest.len()..])
}
}
Some((Seg::Glob(toks), rest)) => match path.split_first() {
Some((head, tail)) if glob_seg_match(toks, head) => {
match_segments_reference(rest, tail)
}
_ => false,
},
}
}
fn segs(pattern: &[&str]) -> Vec<Seg> {
pattern
.iter()
.map(|s| parse_segment(s).expect("a valid segment"))
.collect()
}
fn sequences<'a>(alphabet: &[&'a str], max: usize) -> Vec<Vec<&'a str>> {
let mut all = vec![Vec::new()];
let mut level = vec![Vec::new()];
for _ in 0..max {
level = level
.iter()
.flat_map(|prefix: &Vec<&str>| {
alphabet.iter().map(move |item| {
let mut seq = prefix.clone();
seq.push(*item);
seq
})
})
.collect();
all.extend(level.iter().cloned());
}
all
}
#[test]
fn reachability_agrees_with_the_recursive_reference_exhaustively() {
let patterns = sequences(&["**", "a", "b", "*"], 6);
let paths = sequences(&["a", "b"], 6);
let mut multi = 0;
for pattern in &patterns {
let pat = segs(pattern);
if pat.iter().filter(|s| matches!(s, Seg::DoubleStar)).count() >= 2 {
multi += 1;
}
for path in &paths {
let path: Vec<PathSeg<'_>> = path.iter().map(|s| PathSeg::new(s)).collect();
assert_eq!(
match_segments(&pat, &path),
match_segments_reference(&pat, &path),
"pattern {pattern:?} against {path:?}",
);
}
}
assert!(multi > 1000, "only {multi} multi-`**` patterns");
}
#[test]
fn many_double_stars_match_in_bounded_work() {
let mut pattern = ["**", "a"].repeat(6);
pattern.push("b");
let pat = segs(&pattern);
let path: Vec<PathSeg<'_>> = (0..24).map(|_| PathSeg::new("a")).collect();
let bound = pat.len() * path.len();
let (matched, work) = count_matches(|| match_segments(&pat, &path));
assert!(!matched);
assert!(work <= bound, "{work} glob matches, bound {bound}");
let (reference, reference_work) = count_matches(|| match_segments_reference(&pat, &path));
assert!(!reference);
assert!(
reference_work > 100 * bound,
"the reference ran only {reference_work} glob matches"
);
}
#[test]
fn many_double_stars_in_a_gitignore_stay_bounded_across_ancestors() {
let double_stars = 6;
let mut rule = "**/a/".repeat(double_stars);
rule.push_str("b/");
let rule_segments = 2 * double_stars + 1;
let mut stack = crate::IgnoreStack::new();
stack.push_gitignore("", &rule);
let depth = 24;
let path = format!("{}/x.ts", vec!["a"; depth].join("/"));
let (ignored, work) = count_matches(|| stack.is_ignored(&path, crate::PathKind::File));
assert!(!ignored);
let bound: usize = (1..=depth).map(|k| rule_segments * k).sum();
assert!(work <= bound, "{work} glob matches, bound {bound}");
}
struct Lcg(u64);
impl Lcg {
fn below(&mut self, n: usize) -> usize {
self.0 = self
.0
.wrapping_mul(6_364_136_223_846_793_005)
.wrapping_add(1_442_695_040_888_963_407);
usize::try_from(self.0 >> 33).unwrap_or(0) % n
}
}
#[test]
fn reachability_agrees_with_the_recursive_reference_on_random_cases() {
let pattern_alphabet = [
"**", "**", "***", "a", "b", "*", "?", "[ab]", "[!a]", "c*", "*b", "[a-c]", "a?",
"[]a]",
];
let path_alphabet = ["a", "b", "c", "ab", "ba", "cb", "]"];
let mut rng = Lcg(0x5eed);
for _ in 0..100_000 {
let pattern: Vec<&str> = (0..=rng.below(8))
.map(|_| pattern_alphabet[rng.below(pattern_alphabet.len())])
.collect();
let path: Vec<&str> = (0..rng.below(10))
.map(|_| path_alphabet[rng.below(path_alphabet.len())])
.collect();
let pat = segs(&pattern);
let path_segs: Vec<PathSeg<'_>> = path.iter().map(|s| PathSeg::new(s)).collect();
assert_eq!(
match_segments(&pat, &path_segs),
match_segments_reference(&pat, &path_segs),
"pattern {pattern:?} against {path:?}",
);
}
}
}