use alloc::string::{String, ToString};
use alloc::vec::Vec;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum HostPattern {
Any,
Positive(String),
Negative(String),
}
impl HostPattern {
pub fn parse(token: &str) -> Self {
if let Some(rest) = token.strip_prefix('!') {
HostPattern::Negative(rest.to_string())
} else if token == "*" {
HostPattern::Any
} else {
HostPattern::Positive(token.to_string())
}
}
pub fn parse_all(tokens: &[String]) -> Vec<HostPattern> {
tokens
.iter()
.map(|s| HostPattern::parse(s.as_str()))
.collect()
}
}
pub fn host_matches(patterns: &[HostPattern], host: &str) -> bool {
if patterns.is_empty() {
return false;
}
let mut any_positive = false;
let mut positive_hit = false;
for p in patterns {
match p {
HostPattern::Any => {
any_positive = true;
positive_hit = true;
}
HostPattern::Positive(g) => {
any_positive = true;
if !positive_hit && glob_match(g, host) {
positive_hit = true;
}
}
HostPattern::Negative(g) => {
if glob_match(g, host) {
return false;
}
}
}
}
any_positive && positive_hit
}
pub(crate) fn glob_match(pattern: &str, name: &str) -> bool {
let p = pattern.as_bytes();
let n = name.as_bytes();
let mut pi = 0usize;
let mut ni = 0usize;
let mut star_pi: Option<usize> = None;
let mut star_ni = 0usize;
while ni < n.len() {
if pi < p.len() && (p[pi] == b'?' || p[pi] == n[ni]) {
pi += 1;
ni += 1;
} else if pi < p.len() && p[pi] == b'*' {
star_pi = Some(pi);
star_ni = ni;
pi += 1;
} else if let Some(sp) = star_pi {
pi = sp + 1;
star_ni += 1;
ni = star_ni;
} else {
return false;
}
}
while pi < p.len() && p[pi] == b'*' {
pi += 1;
}
pi == p.len()
}
#[cfg(test)]
mod tests {
use super::*;
fn pat(tokens: &[&str]) -> Vec<HostPattern> {
let v: Vec<String> = tokens.iter().map(|s| s.to_string()).collect();
HostPattern::parse_all(&v)
}
#[test]
fn literal_match() {
assert!(host_matches(&pat(&["example.com"]), "example.com"));
assert!(!host_matches(&pat(&["example.com"]), "other.com"));
}
#[test]
fn star_matches_any() {
assert!(host_matches(&pat(&["*"]), "anything"));
assert!(host_matches(&pat(&["*"]), ""));
}
#[test]
fn star_partial() {
assert!(host_matches(&pat(&["*.example.com"]), "host.example.com"));
assert!(!host_matches(&pat(&["*.example.com"]), "example.com"));
}
#[test]
fn question_mark() {
assert!(host_matches(&pat(&["host?"]), "hosta"));
assert!(!host_matches(&pat(&["host?"]), "host"));
assert!(!host_matches(&pat(&["host?"]), "hostab"));
}
#[test]
fn negation_excludes() {
assert!(host_matches(
&pat(&["*.example.com", "!secret.example.com"]),
"ok.example.com"
));
assert!(!host_matches(
&pat(&["*.example.com", "!secret.example.com"]),
"secret.example.com"
));
}
#[test]
fn negation_alone_never_matches() {
assert!(!host_matches(&pat(&["!foo"]), "bar"));
assert!(!host_matches(&pat(&["!foo"]), "foo"));
}
#[test]
fn empty_never_matches() {
assert!(!host_matches(&[], "anything"));
}
}