pub(crate) fn glob_match(pattern: &str, path: &str) -> bool {
let parts: Vec<&str> = pattern.split('*').collect();
if parts.len() == 1 {
return path.contains(parts[0]);
}
let mut memo = std::collections::HashMap::new();
match_parts(&parts, path, 0, 0, false, &mut memo)
}
fn match_parts(
parts: &[&str],
path: &str,
parts_idx: usize,
path_offset: usize,
slash_free: bool,
memo: &mut std::collections::HashMap<(usize, usize, bool), bool>,
) -> bool {
let key = (parts_idx, path_offset, slash_free);
if let Some(&cached) = memo.get(&key) {
return cached;
}
let result = if parts_idx == parts.len() - 1 {
let last = parts[parts_idx];
if last.is_empty() {
true } else {
match path[path_offset..].strip_suffix(last) {
Some(pre) => !slash_free || !pre.contains('/'),
None => false,
}
}
} else {
let part = parts[parts_idx];
if part.is_empty() {
match_parts(parts, path, parts_idx + 1, path_offset, slash_free, memo)
} else {
let mut matched = false;
for (rel_off, _) in path[path_offset..].match_indices(part) {
let abs_off = path_offset + rel_off;
if slash_free && path[path_offset..abs_off].contains('/') {
break;
}
let after = abs_off + part.len();
if match_parts(parts, path, parts_idx + 1, after, true, memo) {
matched = true;
break;
}
}
matched
}
};
memo.insert(key, result);
result
}
#[cfg(test)]
mod glob_tests {
use super::glob_match;
#[test]
fn no_wildcard_substring_match() {
assert!(glob_match("node_modules", "a/node_modules/b"));
assert!(glob_match("node_modules", "node_modules"));
assert!(glob_match("foo", "x/foo/y"));
assert!(!glob_match("foo", "bar"));
assert!(glob_match("foo", "foobar"));
}
#[test]
fn leading_star_ts_single_level() {
assert!(glob_match("*.ts", "src/a/types.ts"));
assert!(glob_match("*.ts", "types.ts"));
assert!(glob_match("*.ts", "src/a/b/types.ts"));
}
#[test]
fn no_wildcard_negative() {
assert!(glob_match("node_modules", "notnode_modules_here/foo"));
assert!(!glob_match("node_modules", "vendor/lodash/index.js"));
}
#[test]
fn star_does_not_cross_slash_positive() {
assert!(glob_match("src/*/types.ts", "src/a/types.ts"));
assert!(glob_match("src/*/types.ts", "src/models/types.ts"));
}
#[test]
fn star_does_not_cross_slash_negative() {
assert!(!glob_match("src/*/types.ts", "src/a/b/types.ts"));
assert!(!glob_match("src/*/types.ts", "src/a/b/c/types.ts"));
}
#[test]
fn node_modules_substring_still_works() {
assert!(glob_match("node_modules", "pkg/node_modules/foo"));
assert!(glob_match(
"node_modules",
"very/deep/pkg/node_modules/lodash/index.js"
));
}
#[test]
fn star_spec_ts() {
assert!(glob_match("*.spec.ts", "foo.spec.ts"));
assert!(glob_match("*.spec.ts", "src/foo.spec.ts"));
assert!(glob_match("*.spec.ts", "deep/a/b/foo.spec.ts"));
assert!(!glob_match("*.spec.ts", "deep/foo.spec/bar.ts"));
}
#[test]
fn backtracking_second_occurrence() {
assert!(glob_match("a/*/b.ts", "a/x/a/y/b.ts")); assert!(!glob_match("a/*/b.ts", "a/x/y/b.ts"));
}
#[test]
fn spec_table_no_wildcard() {
assert!(glob_match("node_modules", "a/node_modules/b"));
assert!(glob_match("node_modules", "node_modules"));
assert!(glob_match("node_modules", "notnode_modules_here/foo")); assert!(!glob_match("node_modules", "vendor/lodash/index.js"));
}
#[test]
fn spec_table_leading_star_ts() {
assert!(glob_match("*.ts", "src/a/b/types.ts")); assert!(glob_match("*.ts", "types.ts"));
}
#[test]
fn spec_table_src_star_types() {
assert!(glob_match("src/*/types.ts", "src/a/types.ts")); assert!(!glob_match("src/*/types.ts", "src/a/b/types.ts"));
assert!(!glob_match("src/*/types.ts", "src/types.ts"));
}
#[test]
fn multibyte_path_no_panic() {
assert!(glob_match("*.ts", "src/café/索引.ts"));
}
}