const REGISTRY_LABELS: &[&str] = &[
"ac", "co", "com", "edu", "gob", "go", "gov", "ltd", "mil", "ne", "net", "nic", "or", "org",
"plc", "sch",
];
fn host(text: &str) -> Option<String> {
let rest = text.split_once("://").map_or(text, |(_, rest)| rest);
let authority = rest.split(['/', '?', '#']).next()?;
let host_port = authority
.rsplit_once('@')
.map_or(authority, |(_, host)| host);
let host = match host_port.rsplit_once(':') {
Some((host, port)) if port.bytes().all(|byte| byte.is_ascii_digit()) => host,
_ => host_port,
};
let host = host.trim_end_matches('.').to_lowercase();
let labels: Vec<&str> = host.split('.').collect();
let valid = labels.len() >= 2
&& labels.iter().all(|label| {
!label.is_empty()
&& !label.starts_with('-')
&& label.chars().all(|c| c.is_alphanumeric() || c == '-')
});
let is_ipv4 = labels.len() == 4 && labels.iter().all(|label| label.parse::<u8>().is_ok());
let named = labels
.last()
.is_some_and(|tld| tld.chars().any(char::is_alphabetic));
(valid && (is_ipv4 || named)).then_some(host)
}
pub(super) fn query_host(filter: &str) -> Option<String> {
let filter = filter.trim();
if filter.is_empty() || filter.contains(char::is_whitespace) {
return None;
}
host(filter)
}
pub(super) fn uri_host(uri: &str) -> Option<String> {
let uri = uri.trim();
if let Some((scheme, _)) = uri.split_once("://") {
if !scheme.eq_ignore_ascii_case("http") && !scheme.eq_ignore_ascii_case("https") {
return None;
}
}
host(uri)
}
pub(super) fn base_domain(host: &str) -> String {
let labels: Vec<&str> = host.split('.').collect();
if labels.iter().all(|label| label.parse::<u8>().is_ok()) {
return host.to_string();
}
let n = labels.len();
let keep = if n >= 3 && labels[n - 1].len() == 2 && REGISTRY_LABELS.contains(&labels[n - 2]) {
3
} else {
2
};
labels[n.saturating_sub(keep)..].join(".")
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn query_host_reads_urls_and_domains() {
assert_eq!(
query_host("https://alice@Login.GitHub.com:8443/settings?tab=1#x").as_deref(),
Some("login.github.com")
);
assert_eq!(query_host("github.com").as_deref(), Some("github.com"));
assert_eq!(query_host("github.com/").as_deref(), Some("github.com"));
assert_eq!(query_host("10.0.0.1:8080").as_deref(), Some("10.0.0.1"));
for not_url in [
"github",
"two words.com",
"1.5",
"a..b",
"",
"-x.com",
"notes:",
] {
assert_eq!(query_host(not_url), None, "{not_url}");
}
}
#[test]
fn uri_host_skips_app_uris() {
assert_eq!(
uri_host("https://accounts.github.com/login").as_deref(),
Some("accounts.github.com")
);
assert_eq!(uri_host("github.com").as_deref(), Some("github.com"));
assert_eq!(uri_host("androidapp://com.github.android"), None);
assert_eq!(uri_host("iosapp://com.github"), None);
}
#[test]
fn base_domain_keeps_the_registrable_part() {
assert_eq!(base_domain("login.github.com"), "github.com");
assert_eq!(base_domain("github.com"), "github.com");
assert_eq!(base_domain("www.example.co.uk"), "example.co.uk");
assert_eq!(base_domain("shop.example.com.au"), "example.com.au");
assert_eq!(base_domain("mail.example.de"), "example.de");
assert_eq!(base_domain("192.168.1.1"), "192.168.1.1");
}
}