use std::path::Path;
use globset::{GlobBuilder, GlobSet, GlobSetBuilder};
use regex::RegexSet;
pub(crate) fn extract_ruby_regexp(s: &str) -> Option<&str> {
let s = s.trim();
if !s.starts_with('/') || s.len() <= 1 {
return None;
}
let end = s[1..].rfind('/')?;
let after_close = &s[end + 2..];
if !after_close
.bytes()
.all(|b| matches!(b, b'i' | b'x' | b'm' | b'u' | b's' | b'n' | b'o' | b'e'))
{
return None;
}
Some(&s[1..end + 1])
}
fn expand_one_star_star(current: &str, expanded: &mut std::collections::HashSet<String>, pending: &mut Vec<String>) {
let mut start = 0;
while let Some(idx) = current[start..].find("**/") {
let idx = start + idx;
let mut variant = current.to_string();
variant.replace_range(idx..idx + 3, "");
if expanded.insert(variant.clone()) {
pending.push(variant);
}
start = idx + 1;
}
}
pub(crate) fn expand_zero_depth_globs(pattern: &str) -> Vec<String> {
let mut expanded = std::collections::HashSet::from([pattern.to_string()]);
let mut pending = vec![pattern.to_string()];
while let Some(current) = pending.pop() {
expand_one_star_star(¤t, &mut expanded, &mut pending);
}
let mut variants: Vec<_> = expanded.into_iter().collect();
variants.sort();
variants
}
fn try_add_glob(builder: &mut GlobSetBuilder, pattern: &str) -> bool {
match GlobBuilder::new(pattern).literal_separator(true).build() {
Ok(glob) => {
builder.add(glob);
true
}
Err(_) => false,
}
}
pub(crate) fn build_glob_set(patterns: &[&str]) -> Option<GlobSet> {
if patterns.is_empty() {
return None;
}
let mut builder = GlobSetBuilder::new();
let mut count = 0;
for pat in patterns {
if extract_ruby_regexp(pat).is_some() {
continue;
}
for expanded in expand_zero_depth_globs(pat) {
if try_add_glob(&mut builder, &expanded) {
count += 1;
}
}
}
if count == 0 {
return None;
}
builder.build().ok()
}
pub(crate) fn build_regex_set(patterns: &[&str]) -> Option<RegexSet> {
let regexes: Vec<&str> = patterns
.iter()
.filter_map(|p| extract_ruby_regexp(p))
.collect();
if regexes.is_empty() {
return None;
}
RegexSet::new(®exes).ok()
}
fn glob_expanded_matches(expanded: &str, path: &Path) -> bool {
let Ok(glob) = GlobBuilder::new(expanded).literal_separator(false).build() else {
return false;
};
let matcher = glob.compile_matcher();
if matcher.is_match(path) {
return true;
}
matcher.is_match(path.to_string_lossy().as_ref())
}
pub(crate) fn glob_matches(pattern: &str, path: &Path) -> bool {
if let Some(re_pattern) = extract_ruby_regexp(pattern) {
return regex::Regex::new(re_pattern)
.map(|re| re.is_match(&path.to_string_lossy()))
.unwrap_or(false);
}
expand_zero_depth_globs(pattern)
.iter()
.any(|expanded| glob_expanded_matches(expanded, path))
}