use std::collections::HashMap;
use std::path::Path;
use crate::rules::{Rule, RuleMatch};
#[derive(Debug)]
pub(super) enum CompiledPattern {
Exact(String),
Glob(glob::Pattern),
Directory(String),
}
#[derive(Debug)]
pub(super) struct CompiledRule {
pub(super) pattern: CompiledPattern,
pub(super) case_insensitive: bool,
pub(super) handler: String,
pub(super) priority: i32,
pub(super) options: HashMap<String, String>,
}
pub(super) fn compile_rules(rules: &[Rule]) -> Vec<CompiledRule> {
rules
.iter()
.map(|rule| {
let raw_pattern = rule.pattern.clone();
let case_insensitive = rule.case_insensitive;
let normalized = if case_insensitive {
raw_pattern.to_lowercase()
} else {
raw_pattern
};
let pattern = if normalized.ends_with('/') {
let dir_name = normalized.trim_end_matches('/').to_string();
CompiledPattern::Directory(dir_name)
} else if normalized.contains('*')
|| normalized.contains('?')
|| normalized.contains('[')
{
match glob::Pattern::new(&normalized) {
Ok(p) => CompiledPattern::Glob(p),
Err(_) => CompiledPattern::Exact(normalized),
}
} else {
CompiledPattern::Exact(normalized)
};
CompiledRule {
pattern,
case_insensitive,
handler: rule.handler.clone(),
priority: rule.priority,
options: rule.options.clone(),
}
})
.collect()
}
pub(super) fn matches_entry(pattern: &CompiledPattern, filename: &str, is_dir: bool) -> bool {
match pattern {
CompiledPattern::Exact(name) => filename == name,
CompiledPattern::Glob(glob) => glob.matches(filename),
CompiledPattern::Directory(dir_name) => is_dir && filename == dir_name,
}
}
pub(super) fn match_file<'a>(
sorted: &'a [&'a CompiledRule],
has_ci_rules: bool,
filename: &str,
is_dir: bool,
rel_path: &Path,
abs_path: &Path,
pack: &str,
) -> Option<RuleMatch> {
let lowered = if has_ci_rules {
Some(filename.to_lowercase())
} else {
None
};
let pick = |rule: &CompiledRule| -> &str {
if rule.case_insensitive {
lowered.as_deref().unwrap_or(filename)
} else {
filename
}
};
for rule in sorted {
if matches_entry(&rule.pattern, pick(rule), is_dir) {
return Some(RuleMatch {
relative_path: rel_path.to_path_buf(),
absolute_path: abs_path.to_path_buf(),
pack: pack.to_string(),
handler: rule.handler.clone(),
is_dir,
options: rule.options.clone(),
preprocessor_source: None,
rendered_bytes: None,
});
}
}
None
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn exact_match() {
let compiled = compile_rules(&[Rule {
pattern: "install.sh".into(),
handler: "install".into(),
priority: 0,
case_insensitive: false,
options: HashMap::new(),
}]);
assert!(matches_entry(&compiled[0].pattern, "install.sh", false));
assert!(!matches_entry(&compiled[0].pattern, "other.sh", false));
}
#[test]
fn glob_match() {
let compiled = compile_rules(&[Rule {
pattern: "*.sh".into(),
handler: "shell".into(),
priority: 0,
case_insensitive: false,
options: HashMap::new(),
}]);
assert!(matches_entry(&compiled[0].pattern, "aliases.sh", false));
assert!(matches_entry(&compiled[0].pattern, "profile.sh", false));
assert!(!matches_entry(&compiled[0].pattern, "vimrc", false));
}
#[test]
fn directory_match() {
let compiled = compile_rules(&[Rule {
pattern: "bin/".into(),
handler: "path".into(),
priority: 0,
case_insensitive: false,
options: HashMap::new(),
}]);
assert!(matches_entry(&compiled[0].pattern, "bin", true));
assert!(!matches_entry(&compiled[0].pattern, "bin", false));
assert!(!matches_entry(&compiled[0].pattern, "lib", true));
}
#[test]
fn case_insensitive_pattern_lowercases_at_compile_time() {
let compiled = compile_rules(&[Rule {
pattern: "README.*".into(),
handler: "skip".into(),
priority: 50,
case_insensitive: true,
options: HashMap::new(),
}]);
assert!(compiled[0].case_insensitive);
assert!(matches_entry(&compiled[0].pattern, "readme.md", false));
}
#[test]
fn catchall_matches_everything() {
let compiled = compile_rules(&[Rule {
pattern: "*".into(),
handler: "symlink".into(),
priority: 0,
case_insensitive: false,
options: HashMap::new(),
}]);
assert!(matches_entry(&compiled[0].pattern, "anything", false));
assert!(matches_entry(&compiled[0].pattern, "vimrc", false));
}
}