pub fn encode_strict(pattern: &str) -> Result<String, String> {
let pattern = pattern.strip_suffix('.').unwrap_or(pattern);
let (host, wildcard) = match pattern.strip_prefix("*.") {
Some(host) => (host, true),
None => (pattern, false),
};
let empty_host = host.is_empty();
let name_too_long = host.len() > 253;
let extra_wildcard = host.contains('*');
let ip_literal = !wildcard && host.parse::<std::net::IpAddr>().is_ok();
let invalid_label = host.split('.').any(|label| {
let empty = label.is_empty();
let too_long = label.len() > 63;
let edge_hyphen = label.starts_with('-') || label.ends_with('-');
let non_hostname_character = !label
.bytes()
.all(|byte| byte.is_ascii_alphanumeric() || byte == b'-');
empty || too_long || edge_hyphen || non_hostname_character
});
if empty_host || name_too_long || extra_wildcard || ip_literal || invalid_label {
return Err(format!(
"invalid host pattern {pattern:?}: use an exact DNS hostname or *.domain"
));
}
Ok(if wildcard {
format!("*.{}", host.to_ascii_lowercase())
} else {
format!("={}", host.to_ascii_lowercase())
})
}
pub fn matches(host: &str, entry: &str) -> bool {
let host = host.trim_end_matches('.').to_ascii_lowercase();
let entry = entry.trim_end_matches('.').to_ascii_lowercase();
if let Some(exact) = entry.strip_prefix('=') {
return !exact.is_empty() && host == exact;
}
if let Some(suffix) = entry.strip_prefix("*.") {
return !suffix.is_empty()
&& host
.strip_suffix(suffix)
.is_some_and(|prefix| prefix.ends_with('.') && prefix.len() > 1);
}
!entry.is_empty()
&& (host == entry
|| host
.strip_suffix(&entry)
.is_some_and(|prefix| prefix.ends_with('.')))
}
pub fn static_resolution_host(entry: &str) -> Option<&str> {
if entry.starts_with("*.") {
None
} else {
Some(entry.strip_prefix('=').unwrap_or(entry))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn old_and_new_entries_have_distinct_semantics() {
assert!(matches("api.example.com", "example.com"));
assert!(matches("example.com", "example.com"));
let exact = encode_strict("Example.COM").unwrap();
assert_eq!(exact, "=example.com");
assert_eq!(encode_strict("Example.COM.").unwrap(), exact);
assert!(matches("EXAMPLE.COM.", &exact));
assert!(!matches("api.example.com", &exact));
let wildcard = encode_strict("*.example.com").unwrap();
assert!(matches("api.example.com", &wildcard));
assert!(matches("a.b.example.com", &wildcard));
assert!(!matches("example.com", &wildcard));
assert!(!matches("notexample.com", &wildcard));
assert!(!matches("example.com.evil.test", &wildcard));
}
#[test]
fn rejects_invalid_patterns() {
for pattern in [
"",
"*",
"*.",
"*.*.example.com",
"foo*.example.com",
"https://example.com",
"example.com:443",
"example.com..",
"1.2.3.4",
] {
assert!(encode_strict(pattern).is_err(), "{pattern}");
}
}
#[test]
fn enforces_dns_name_and_label_size_limits() {
let longest_name = [
"a".repeat(63),
"b".repeat(63),
"c".repeat(63),
"d".repeat(61),
]
.join(".");
assert_eq!(longest_name.len(), 253);
assert!(encode_strict(&longest_name).is_ok());
assert!(encode_strict(&format!("{longest_name}d")).is_err());
assert!(encode_strict(&"a".repeat(64)).is_err());
}
}