use super::PROJECT;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Spot<'a> {
Project(&'a str),
Absolute(&'a str),
Anywhere(&'a str),
}
pub fn spot(pattern: &str) -> Spot<'_> {
if pattern == PROJECT {
return Spot::Project("");
}
if let Some(rest) = pattern
.strip_prefix(PROJECT)
.and_then(|r| r.strip_prefix('/'))
{
return Spot::Project(rest);
}
let bytes = pattern.as_bytes();
let drive = bytes.len() >= 2 && bytes[0].is_ascii_alphabetic() && bytes[1] == b':';
if pattern.starts_with('/') || drive {
Spot::Absolute(pattern)
} else {
Spot::Anywhere(pattern.trim_start_matches("**/"))
}
}
pub const ANY_GIT: &str = "**/.git/**";
pub const RUNS_LATER: &[&str] = &[
"**/.github/workflows/**",
"**/.gitlab-ci.yml",
"**/.circleci/**",
"**/azure-pipelines.yml",
"**/Jenkinsfile",
"**/.buildkite/**",
"**/.husky/**",
"**/.pre-commit-config.yaml",
"**/lefthook.yml",
"**/.lefthook.yml",
"**/.vscode/tasks.json",
];
pub fn runs_later_exclusion(pattern: &str) -> bool {
let rest = match spot(pattern) {
Spot::Project(rest) => rest,
Spot::Absolute(path) => path,
Spot::Anywhere(_) => return false,
};
RUNS_LATER
.iter()
.any(|file| rest == *file || rest.ends_with(&format!("/{file}")))
}
pub fn git_exclusion(pattern: &str) -> bool {
match spot(pattern) {
Spot::Project(rest) => rest == ".git/**" || rest == ANY_GIT,
Spot::Absolute(path) => path.ends_with("/.git/**"),
Spot::Anywhere(_) => false,
}
}
pub fn has_glob(pattern: &str) -> bool {
pattern.contains(['*', '?', '[', ']', '{', '}'])
}
pub fn literal_tree(pattern: &str) -> Option<&str> {
let base = pattern.strip_suffix("/**").unwrap_or(pattern);
(!base.is_empty() && !has_glob(base)).then_some(base)
}
pub fn is_below(entry: &str, node: &str) -> bool {
let prefix = if node.ends_with('/') {
node.to_owned()
} else {
format!("{node}/")
};
entry.len() > prefix.len() && entry.starts_with(&prefix)
}
pub fn split(patterns: &[String]) -> (Vec<&str>, Vec<&str>) {
let (excluded, positive): (Vec<&str>, Vec<&str>) = patterns
.iter()
.map(String::as_str)
.partition(|p| p.starts_with('!'));
let excluded = excluded.into_iter().map(|p| &p[1..]).collect();
(positive, excluded)
}
pub fn domain(pattern: &str) -> bool {
let name = pattern.strip_prefix("*.").unwrap_or(pattern);
let label = |l: &str| !l.is_empty() && l.chars().all(|c| c.is_ascii_alphanumeric() || c == '-');
name.split('.').all(label)
&& name
.rsplit('.')
.next()
.is_some_and(|tld| tld.chars().any(|c| c.is_ascii_alphabetic()))
}
pub fn push_unique(list: &mut Vec<String>, value: String) {
if !list.contains(&value) {
list.push(value);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn spots_and_shapes() {
assert_eq!(spot("/__moat_project__"), Spot::Project(""));
assert_eq!(spot("/__moat_project__/.git/**"), Spot::Project(".git/**"));
assert_eq!(
spot("/__moat_project__x"),
Spot::Absolute("/__moat_project__x")
);
assert_eq!(spot("C:/x"), Spot::Absolute("C:/x"));
assert_eq!(spot("**/.env"), Spot::Anywhere(".env"));
assert_eq!(literal_tree("/h/.ssh/**"), Some("/h/.ssh"));
assert_eq!(literal_tree("/h/**/.env"), None);
assert_eq!(split(&["a".into(), "!b".into()]), (vec!["a"], vec!["b"]));
assert!(is_below("/h/.ssh", "/h") && is_below("/h", "/") && is_below("C:/x", "C:/"));
assert!(!is_below("/hx", "/h") && !is_below("/h", "/h") && !is_below("/", "/"));
for ok in ["github.com", "*.crates.io", "localhost"] {
assert!(domain(ok), "{ok}");
}
for bad in [
"169.254.*",
"fe80:*",
"100.100.100.200",
"*",
"a*.b.com",
"::1",
] {
assert!(!domain(bad), "{bad}");
}
}
}