Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 41 additions & 11 deletions crates/globset/src/glob.rs
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,8 @@ struct GlobStrategic {
strategy: MatchStrategy,
/// The pattern, as a compiled regex.
re: Regex,
/// Whether the pattern was compiled with case-insensitive matching.
case_insensitive: bool,
}

#[cfg(test)]
Expand All @@ -176,9 +178,19 @@ impl GlobStrategic {
let byte_path = &*candidate.path;

match self.strategy {
MatchStrategy::Literal(ref lit) => lit.as_bytes() == byte_path,
MatchStrategy::Literal(ref lit) => {
if self.case_insensitive {
byte_path.to_ascii_lowercase() == lit.as_bytes()
} else {
lit.as_bytes() == byte_path
}
}
MatchStrategy::BasenameLiteral(ref lit) => {
lit.as_bytes() == &*candidate.basename
if self.case_insensitive {
candidate.basename.to_ascii_lowercase() == lit.as_bytes()
} else {
lit.as_bytes() == &*candidate.basename
}
}
MatchStrategy::Extension(ref ext) => {
ext.as_bytes() == &*candidate.ext
Expand Down Expand Up @@ -301,7 +313,11 @@ impl Glob {
let strategy = MatchStrategy::new(self);
let re =
new_regex(&self.re).expect("regex compilation shouldn't fail");
GlobStrategic { strategy, re }
GlobStrategic {
strategy,
re,
case_insensitive: self.is_case_insensitive(),
}
}

/// Returns the original glob pattern used to build this pattern.
Expand All @@ -327,19 +343,25 @@ impl Glob {
&self.re
}

/// Returns whether this pattern was built with case-insensitive
/// matching.
pub(crate) fn is_case_insensitive(&self) -> bool {
self.opts.case_insensitive
}

/// Returns the pattern as a literal if and only if the pattern must match
/// an entire path exactly.
///
/// The basic format of these patterns is `{literal}`.
fn literal(&self) -> Option<String> {
if self.opts.case_insensitive {
return None;
}
let mut lit = String::new();
for t in &*self.tokens {
let Token::Literal(c) = *t else { return None };
lit.push(c);
}
if self.opts.case_insensitive {
lit = lit.to_ascii_lowercase();
}
if lit.is_empty() { None } else { Some(lit) }
}

Expand Down Expand Up @@ -508,9 +530,6 @@ impl Glob {
/// doesn't *always* match when a file path has a basename of `foo`. e.g.,
/// `foo` doesn't match `abc/foo`.
fn basename_tokens(&self) -> Option<&[Token]> {
if self.opts.case_insensitive {
return None;
}
let start = match *self.tokens.get(0)? {
Token::RecursivePrefix => 1,
_ => {
Expand Down Expand Up @@ -563,6 +582,9 @@ impl Glob {
let Token::Literal(c) = *t else { return None };
lit.push(c);
}
if self.opts.case_insensitive {
lit = lit.to_ascii_lowercase();
}
Some(lit)
}
}
Expand Down Expand Up @@ -1461,6 +1483,8 @@ mod tests {
matches!(matchcasei2, "aBcDeFg", "abcdefg", CASEI);
matches!(matchcasei3, "aBcDeFg", "ABCDEFG", CASEI);
matches!(matchcasei4, "aBcDeFg", "AbCdEfG", CASEI);
matches!(matchcasei_basename1, "**/FOO", "bar/foo", CASEI);
matches!(matchcasei_basename2, "**/FOO", "bar/FOO", CASEI);

matches!(matchalt1, "a,b", "a,b");
matches!(matchalt2, ",", ",");
Expand Down Expand Up @@ -1618,7 +1642,7 @@ mod tests {
}

literal!(extract_lit1, "foo", Some(s("foo")));
literal!(extract_lit2, "foo", None, CASEI);
literal!(extract_lit2, "foo", Some(s("foo")), CASEI);
literal!(extract_lit3, "/foo", Some(s("/foo")));
literal!(extract_lit4, "/foo/", Some(s("/foo/")));
literal!(extract_lit5, "/foo/bar", Some(s("/foo/bar")));
Expand All @@ -1631,7 +1655,12 @@ mod tests {
"**/foo",
Some(&*vec![Literal('f'), Literal('o'), Literal('o'),])
);
basetokens!(extract_basetoks2, "**/foo", None, CASEI);
basetokens!(
extract_basetoks2,
"**/foo",
Some(&*vec![Literal('f'), Literal('o'), Literal('o'),]),
CASEI
);
basetokens!(
extract_basetoks3,
"**/foo",
Expand Down Expand Up @@ -1683,4 +1712,5 @@ mod tests {
baseliteral!(extract_baselit2, "foo", None);
baseliteral!(extract_baselit3, "*foo", None);
baseliteral!(extract_baselit4, "*/foo", None);
baseliteral!(extract_baselit5, "**/FOO", Some(s("foo")), CASEI);
}
87 changes: 69 additions & 18 deletions crates/globset/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -469,8 +469,10 @@ impl GlobSet {
}

let mut len = 0;
let mut lits = LiteralStrategy::new();
let mut base_lits = BasenameLiteralStrategy::new();
let mut lits = LiteralStrategy::new(false);
let mut lits_ci = LiteralStrategy::new(true);
let mut base_lits = BasenameLiteralStrategy::new(false);
let mut base_lits_ci = BasenameLiteralStrategy::new(true);
let mut exts = ExtensionStrategy::new();
let mut prefixes = MultiStrategyBuilder::new();
let mut suffixes = MultiStrategyBuilder::new();
Expand All @@ -480,12 +482,21 @@ impl GlobSet {
len += 1;

let p = p.as_ref();
let case_insensitive = p.is_case_insensitive();
match MatchStrategy::new(p) {
MatchStrategy::Literal(lit) => {
lits.add(i, lit);
if case_insensitive {
lits_ci.add(i, lit);
Comment on lines +485 to +489
} else {
lits.add(i, lit);
}
}
MatchStrategy::BasenameLiteral(lit) => {
base_lits.add(i, lit);
if case_insensitive {
base_lits_ci.add(i, lit);
} else {
base_lits.add(i, lit);
}
}
MatchStrategy::Extension(ext) => {
exts.add(i, ext);
Expand All @@ -494,6 +505,9 @@ impl GlobSet {
prefixes.add(i, prefix);
}
MatchStrategy::Suffix { suffix, component } => {
// `suffix()` never returns `Some` for a
// case-insensitive pattern, so this is always the
// case-sensitive literal strategy.
if component {
lits.add(i, suffix[1..].to_string());
}
Expand All @@ -513,27 +527,36 @@ impl GlobSet {
}
}
debug!(
"built glob set; {} literals, {} basenames, {} extensions, \
"built glob set; {} literals, {} case-insensitive literals, \
{} basenames, {} case-insensitive basenames, {} extensions, \
{} prefixes, {} suffixes, {} required extensions, {} regexes",
lits.0.len(),
lits_ci.0.len(),
base_lits.0.len(),
base_lits_ci.0.len(),
exts.0.len(),
prefixes.literals.len(),
suffixes.literals.len(),
required_exts.0.len(),
regexes.literals.len()
);
let mut strats = Vec::with_capacity(7);
let mut strats = Vec::with_capacity(9);
// Only add strategies that are populated
if !exts.0.is_empty() {
strats.push(GlobSetMatchStrategy::Extension(exts));
}
if !base_lits.0.is_empty() {
strats.push(GlobSetMatchStrategy::BasenameLiteral(base_lits));
}
if !base_lits_ci.0.is_empty() {
strats.push(GlobSetMatchStrategy::BasenameLiteral(base_lits_ci));
}
if !lits.0.is_empty() {
strats.push(GlobSetMatchStrategy::Literal(lits));
}
if !lits_ci.0.is_empty() {
strats.push(GlobSetMatchStrategy::Literal(lits_ci));
}
if !suffixes.is_empty() {
strats.push(GlobSetMatchStrategy::Suffix(suffixes.suffix()));
}
Expand Down Expand Up @@ -707,19 +730,28 @@ impl GlobSetMatchStrategy {
}

#[derive(Clone, Debug)]
struct LiteralStrategy(fnv::HashMap<Vec<u8>, Vec<usize>>);
struct LiteralStrategy(fnv::HashMap<Vec<u8>, Vec<usize>>, bool);

impl LiteralStrategy {
fn new() -> LiteralStrategy {
LiteralStrategy(fnv::HashMap::default())
fn new(case_insensitive: bool) -> LiteralStrategy {
LiteralStrategy(fnv::HashMap::default(), case_insensitive)
}

fn add(&mut self, global_index: usize, lit: String) {
self.0.entry(lit.into_bytes()).or_insert(vec![]).push(global_index);
let lit = if self.1 {
lit.into_bytes().to_ascii_lowercase()
} else {
lit.into_bytes()
};
self.0.entry(lit).or_insert(vec![]).push(global_index);
}

fn is_match(&self, candidate: &Candidate<'_>) -> bool {
self.0.contains_key(candidate.path.as_bytes())
if self.1 {
self.0.contains_key(&candidate.path.to_ascii_lowercase())
} else {
self.0.contains_key(candidate.path.as_bytes())
Comment on lines 749 to +753
}
}

fn matches_all(&self, candidate: &Candidate<'_>) -> bool {
Expand All @@ -732,29 +764,43 @@ impl LiteralStrategy {
candidate: &Candidate<'_>,
matches: &mut Vec<usize>,
) {
if let Some(hits) = self.0.get(candidate.path.as_bytes()) {
let hits = if self.1 {
self.0.get(&candidate.path.to_ascii_lowercase())
} else {
self.0.get(candidate.path.as_bytes())
};
if let Some(hits) = hits {
matches.extend(hits);
}
}
}

#[derive(Clone, Debug)]
struct BasenameLiteralStrategy(fnv::HashMap<Vec<u8>, Vec<usize>>);
struct BasenameLiteralStrategy(fnv::HashMap<Vec<u8>, Vec<usize>>, bool);

impl BasenameLiteralStrategy {
fn new() -> BasenameLiteralStrategy {
BasenameLiteralStrategy(fnv::HashMap::default())
fn new(case_insensitive: bool) -> BasenameLiteralStrategy {
BasenameLiteralStrategy(fnv::HashMap::default(), case_insensitive)
}

fn add(&mut self, global_index: usize, lit: String) {
self.0.entry(lit.into_bytes()).or_insert(vec![]).push(global_index);
let lit = if self.1 {
lit.into_bytes().to_ascii_lowercase()
} else {
lit.into_bytes()
};
self.0.entry(lit).or_insert(vec![]).push(global_index);
}

fn is_match(&self, candidate: &Candidate<'_>) -> bool {
if candidate.basename.is_empty() {
return false;
}
self.0.contains_key(candidate.basename.as_bytes())
if self.1 {
Comment on lines 795 to +799
self.0.contains_key(&candidate.basename.to_ascii_lowercase())
} else {
self.0.contains_key(candidate.basename.as_bytes())
}
}

fn matches_all(&self, candidate: &Candidate<'_>) -> bool {
Expand All @@ -770,7 +816,12 @@ impl BasenameLiteralStrategy {
if candidate.basename.is_empty() {
return;
}
if let Some(hits) = self.0.get(candidate.basename.as_bytes()) {
let hits = if self.1 {
self.0.get(&candidate.basename.to_ascii_lowercase())
} else {
self.0.get(candidate.basename.as_bytes())
};
if let Some(hits) = hits {
matches.extend(hits);
}
}
Expand Down
Loading