#[must_use]
pub fn domain_matches(pattern: &str, host: &str) -> bool {
if pattern.starts_with("*.") {
let suffix = &pattern[1..]; if let Some(remainder) = host.strip_suffix(suffix) {
!remainder.is_empty() && !remainder.contains('.')
} else {
false
}
} else {
pattern == host
}
}
pub fn validate_domain_patterns(patterns: &[String]) -> Result<(), String> {
for pattern in patterns {
if !pattern
.chars()
.all(|c| c.is_ascii_alphanumeric() || matches!(c, '.' | '*' | '-'))
{
return Err(format!(
"invalid domain pattern {pattern:?}: only alphanumeric characters, dots, \
hyphens, and a leading wildcard '*' are allowed"
));
}
if pattern.is_empty() {
return Err("empty domain pattern is not allowed".into());
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn exact_match() {
assert!(domain_matches("example.com", "example.com"));
assert!(!domain_matches("example.com", "other.com"));
assert!(!domain_matches("example.com", "sub.example.com"));
}
#[test]
fn wildcard_single_subdomain() {
assert!(domain_matches("*.example.com", "sub.example.com"));
assert!(!domain_matches("*.example.com", "example.com"));
assert!(!domain_matches("*.example.com", "a.b.example.com"));
}
#[test]
fn wildcard_does_not_match_empty_label() {
assert!(!domain_matches("*.example.com", ".example.com"));
}
#[test]
fn multi_wildcard_treated_as_exact() {
assert!(!domain_matches("*.*.example.com", "a.b.example.com"));
}
#[test]
fn validate_accepts_valid_patterns() {
let patterns = vec![
"example.com".to_owned(),
"*.pastebin.com".to_owned(),
"my-host.co.uk".to_owned(),
];
assert!(validate_domain_patterns(&patterns).is_ok());
}
#[test]
fn validate_rejects_spaces() {
let patterns = vec!["bad domain.com".to_owned()];
assert!(validate_domain_patterns(&patterns).is_err());
}
#[test]
fn validate_rejects_slashes() {
let patterns = vec!["example.com/path".to_owned()];
assert!(validate_domain_patterns(&patterns).is_err());
}
#[test]
fn validate_rejects_empty() {
let patterns = vec!["".to_owned()];
assert!(validate_domain_patterns(&patterns).is_err());
}
}