use std::collections::HashMap;
use std::path::Component;
use std::path::Path;
use super::ObservedPaths;
pub(super) const MAX_GLOB_SEGMENTS: usize = 256;
pub(super) const MAX_GLOB_SEGMENT_LEN: usize = 1024;
fn segments_oversize<S: AsRef<str>>(segments: &[S]) -> bool {
segments.len() > MAX_GLOB_SEGMENTS
|| segments
.iter()
.any(|s| s.as_ref().len() > MAX_GLOB_SEGMENT_LEN)
}
#[derive(Debug, Clone)]
struct CompiledGlob {
segments: Vec<String>,
oversize: bool,
}
#[derive(Debug, Clone, Default)]
pub(super) struct PathGlobIndex {
literal_first: HashMap<String, Vec<CompiledGlob>>,
wild_first: Vec<CompiledGlob>,
empty_pattern: Vec<CompiledGlob>,
source_instance_id: u64,
source_mutation_epoch: u64,
}
impl PathGlobIndex {
pub(super) fn build(patterns: &ObservedPaths) -> Self {
let mut index = PathGlobIndex {
source_instance_id: patterns.instance_id(),
source_mutation_epoch: patterns.mutation_epoch(),
..PathGlobIndex::default()
};
for pattern in patterns {
let directory_pattern = pattern.replace('\\', "/").ends_with('/');
let mut normalized_pattern = normalize_path(pattern);
if directory_pattern && !normalized_pattern.is_empty() {
normalized_pattern.push_str("/**");
}
let segments: Vec<String> = split_segments(&normalized_pattern)
.into_iter()
.map(str::to_string)
.collect();
let oversize = segments_oversize(&segments);
let glob = CompiledGlob { segments, oversize };
match glob.segments.first() {
None => index.empty_pattern.push(glob),
Some(first) if first == "**" || first.contains('*') => {
index.wild_first.push(glob);
}
Some(first) => {
index
.literal_first
.entry(first.clone())
.or_default()
.push(glob);
}
}
}
index
}
pub(super) fn matches_sources(&self, patterns: &ObservedPaths) -> bool {
self.source_instance_id == patterns.instance_id()
&& self.source_mutation_epoch == patterns.mutation_epoch()
}
pub(super) fn matches(&self, normalized_path: &str) -> bool {
let path_segments = split_segments(normalized_path);
if segments_oversize(&path_segments) {
tracing::warn!(
"skipping oversized allowlist path match ({} segments). Fix: shorten the path",
path_segments.len()
);
return false;
}
let test = |glob: &CompiledGlob| -> bool {
!glob.oversize && glob_match_segments(&glob.segments, &path_segments)
};
if path_segments.is_empty() {
return self.empty_pattern.iter().any(test) || self.wild_first.iter().any(test);
}
let first = path_segments[0];
if let Some(bucket) = self.literal_first.get(first) {
if bucket.iter().any(test) {
return true;
}
}
self.wild_first.iter().any(test)
}
}
pub(super) fn split_segments(path: &str) -> Vec<&str> {
if path.is_empty() {
Vec::new()
} else {
path.split(['/', '\\']).collect()
}
}
fn glob_match_segments<S: AsRef<str>>(pattern: &[S], path: &[&str]) -> bool {
let mut states = vec![false; path.len() + 1];
let mut next = vec![false; path.len() + 1];
states[0] = true;
for segment in pattern {
let segment = segment.as_ref();
next.iter_mut().for_each(|slot| *slot = false);
if segment == "**" {
let mut reachable = false;
for idx in 0..=path.len() {
reachable |= states[idx];
next[idx] = reachable;
}
} else {
for idx in 0..path.len() {
if states[idx] && segment_match(segment, path[idx]) {
next[idx + 1] = true;
}
}
}
std::mem::swap(&mut states, &mut next);
}
states[path.len()]
}
fn segment_match(pattern: &str, text: &str) -> bool {
if pattern.is_ascii() && text.is_ascii() {
return segment_match_units(pattern.as_bytes(), text.as_bytes(), b'*');
}
let pattern_chars: Vec<char> = pattern.chars().collect();
let text_chars: Vec<char> = text.chars().collect();
segment_match_units(&pattern_chars, &text_chars, '*')
}
#[allow(clippy::similar_names)] fn segment_match_units<T>(pattern: &[T], text: &[T], star_unit: T) -> bool
where
T: Copy + Eq,
{
let mut pi = 0usize;
let mut ti = 0usize;
let mut star_pi = None;
let mut star_ti = 0usize;
while ti < text.len() {
if pi < pattern.len() && pattern[pi] == star_unit {
star_pi = Some(pi);
star_ti = ti;
pi += 1;
continue;
}
if pi < pattern.len() && pattern[pi] == text[ti] {
pi += 1;
ti += 1;
continue;
}
if let Some(star) = star_pi {
star_ti += 1;
ti = star_ti;
pi = star + 1;
continue;
}
return false;
}
while pi < pattern.len() && pattern[pi] == star_unit {
pi += 1;
}
pi == pattern.len()
}
pub(super) fn normalize_path(path: &str) -> String {
let path = path.replace('\\', "/");
let mut parts = Vec::new();
for component in Path::new(&path).components() {
match component {
Component::CurDir => {}
Component::ParentDir => {
if !parts.is_empty() && parts.last().is_some_and(|part| part != "..") {
parts.pop();
} else {
parts.push("..".to_string());
}
}
Component::Normal(part) => parts.push(part.to_string_lossy().into_owned()),
Component::RootDir => parts.clear(),
Component::Prefix(prefix) => parts.push(prefix.as_os_str().to_string_lossy().into()),
}
}
parts.join("/")
}