use url::Url;
pub fn extract_host(input: &str) -> Option<String> {
let url = if input.contains("://") {
Url::parse(input).ok()?
} else {
Url::parse(&format!("dns://{input}")).ok()?
};
let host = url.host_str()?;
let host = host
.strip_prefix('[')
.and_then(|rest| rest.strip_suffix(']'))
.unwrap_or(host);
(!host.is_empty()).then(|| host.to_owned())
}
pub fn reduce_to_origin(input: &str) -> Option<Url> {
let mut url = Url::parse(input).ok()?;
if !url.has_host() || url.port_or_known_default().is_none() {
return None;
}
url.set_path("/");
url.set_query(None);
url.set_fragment(None);
let _ = url.set_username("");
let _ = url.set_password(None);
Some(url)
}
pub fn origin_key(url: &Url) -> String {
format!(
"{}://{}:{}",
url.scheme(),
url.host_str().unwrap_or_default(),
url.port_or_known_default().unwrap_or_default(),
)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn prefetch_dns_takes_a_bare_host() {
assert_eq!(extract_host("example.com").as_deref(), Some("example.com"));
}
#[test]
fn prefetch_dns_ignores_the_parts_a_name_does_not_have() {
for input in [
"https://example.com",
"https://example.com:8443",
"https://example.com/some/path?q=1#frag",
"https://user:pass@example.com/",
"example.com:8443",
] {
assert_eq!(
extract_host(input).as_deref(),
Some("example.com"),
"{input:?} names example.com whatever else it carries"
);
}
}
#[test]
fn prefetch_dns_unwraps_an_ipv6_literal() {
assert_eq!(
extract_host("https://[2001:db8::1]:8443").as_deref(),
Some("2001:db8::1")
);
}
#[test]
fn prefetch_dns_rejects_a_string_with_no_host() {
for input in ["", " ", "/just/a/path", "https://"] {
assert!(
extract_host(input).is_none(),
"{input:?} names no host to resolve"
);
}
}
#[test]
fn preconnect_reduces_a_longer_url_to_its_origin() {
let url = reduce_to_origin("https://user:pass@example.com/some/path?q=1#frag")
.expect("a full URL reduces to its origin");
assert_eq!(url.as_str(), "https://example.com/");
assert_eq!(url.username(), "", "userinfo is stripped");
assert_eq!(url.password(), None);
assert_eq!(url.query(), None);
assert_eq!(url.fragment(), None);
}
#[test]
fn preconnect_defaults_the_port_by_scheme() {
for (bare, spelled) in [
("https://example.com", "https://example.com:443"),
("http://example.com", "http://example.com:80"),
] {
let bare = origin_key(&reduce_to_origin(bare).expect("parses"));
let spelled = origin_key(&reduce_to_origin(spelled).expect("parses"));
assert_eq!(
bare, spelled,
"the omitted port defaults to the spelled one"
);
}
}
#[test]
fn preconnect_keeps_distinct_origins_apart() {
let key = |input: &str| origin_key(&reduce_to_origin(input).expect("parses"));
assert_ne!(key("https://example.com"), key("https://example.com:8443"));
assert_ne!(key("https://example.com"), key("http://example.com"));
assert_ne!(key("https://example.com"), key("https://other.example"));
}
#[test]
fn preconnect_rejects_what_cannot_be_connected_to() {
for input in [
"not an origin",
"",
"/just/a/path",
"file:///etc/hosts",
"unknownscheme://example.com",
] {
assert!(
reduce_to_origin(input).is_none(),
"{input:?} is not a connectable origin"
);
}
}
}