use std::hash::{Hash, Hasher};
use std::{fmt::Display, fmt::Formatter};
use url::Url;
use uv_redacted::DisplaySafeUrl;
use uv_small_str::SmallString;
#[derive(Debug, Clone)]
pub struct Realm {
scheme: SmallString,
host: Option<SmallString>,
port: Option<u16>,
}
impl From<&DisplaySafeUrl> for Realm {
fn from(url: &DisplaySafeUrl) -> Self {
Self::from(&**url)
}
}
impl From<&Url> for Realm {
fn from(url: &Url) -> Self {
Self {
scheme: SmallString::from(url.scheme()),
host: url.host_str().map(SmallString::from),
port: url.port(),
}
}
}
impl Display for Realm {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
if let Some(port) = self.port {
write!(
f,
"{}://{}:{port}",
self.scheme,
self.host.as_deref().unwrap_or_default()
)
} else {
write!(
f,
"{}://{}",
self.scheme,
self.host.as_deref().unwrap_or_default()
)
}
}
}
impl PartialEq for Realm {
fn eq(&self, other: &Self) -> bool {
RealmRef::from(self) == RealmRef::from(other)
}
}
impl Eq for Realm {}
impl Hash for Realm {
fn hash<H: Hasher>(&self, state: &mut H) {
RealmRef::from(self).hash(state);
}
}
#[derive(Debug, Copy, Clone)]
pub struct RealmRef<'a> {
scheme: &'a str,
host: Option<&'a str>,
port: Option<u16>,
}
impl RealmRef<'_> {
pub(crate) fn is_subdomain_of(&self, other: Self) -> bool {
other.scheme == self.scheme
&& other.port == self.port
&& other.host.is_some_and(|other_host| {
self.host.is_some_and(|self_host| {
self_host
.strip_suffix(other_host)
.is_some_and(|prefix| prefix.ends_with('.'))
})
})
}
}
impl<'a> From<&'a Url> for RealmRef<'a> {
fn from(url: &'a Url) -> Self {
Self {
scheme: url.scheme(),
host: url.host_str(),
port: url.port(),
}
}
}
impl PartialEq for RealmRef<'_> {
fn eq(&self, other: &Self) -> bool {
self.scheme == other.scheme && self.host == other.host && self.port == other.port
}
}
impl Eq for RealmRef<'_> {}
impl Hash for RealmRef<'_> {
fn hash<H: Hasher>(&self, state: &mut H) {
self.scheme.hash(state);
self.host.hash(state);
self.port.hash(state);
}
}
impl<'a> PartialEq<RealmRef<'a>> for Realm {
fn eq(&self, rhs: &RealmRef<'a>) -> bool {
RealmRef::from(self) == *rhs
}
}
impl PartialEq<Realm> for RealmRef<'_> {
fn eq(&self, rhs: &Realm) -> bool {
*self == RealmRef::from(rhs)
}
}
impl<'a> From<&'a Realm> for RealmRef<'a> {
fn from(realm: &'a Realm) -> Self {
Self {
scheme: &realm.scheme,
host: realm.host.as_deref(),
port: realm.port,
}
}
}
#[cfg(test)]
mod tests {
use url::{ParseError, Url};
use crate::Realm;
#[test]
fn test_should_retain_auth() -> Result<(), ParseError> {
assert_eq!(
Realm::from(&Url::parse("https://example.com")?),
Realm::from(&Url::parse("https://example.com")?)
);
assert_eq!(
Realm::from(&Url::parse("https://example.com:1234")?),
Realm::from(&Url::parse("https://example.com:1234")?)
);
assert_eq!(
Realm::from(&Url::parse("http://example.com")?),
Realm::from(&Url::parse("http://example.com")?)
);
assert_eq!(
Realm::from(&Url::parse("http://example.com/foo")?),
Realm::from(&Url::parse("http://example.com/bar")?)
);
assert_eq!(
Realm::from(&Url::parse("https://example.com:443")?),
Realm::from(&Url::parse("https://example.com")?)
);
assert_eq!(
Realm::from(&Url::parse("http://example.com:80")?),
Realm::from(&Url::parse("http://example.com")?)
);
assert_ne!(
Realm::from(&Url::parse("https://example.com")?),
Realm::from(&Url::parse("http://example.com")?)
);
assert_ne!(
Realm::from(&Url::parse("http://example.com")?),
Realm::from(&Url::parse("https://example.com")?)
);
assert_ne!(
Realm::from(&Url::parse("https://foo.com")?),
Realm::from(&Url::parse("https://bar.com")?)
);
assert_ne!(
Realm::from(&Url::parse("https://example.com:1234")?),
Realm::from(&Url::parse("https://example.com:5678")?)
);
assert_ne!(
Realm::from(&Url::parse("https://example.com:443")?),
Realm::from(&Url::parse("https://example.com:5678")?)
);
assert_ne!(
Realm::from(&Url::parse("https://example.com:1234")?),
Realm::from(&Url::parse("https://example.com:443")?)
);
assert_ne!(
Realm::from(&Url::parse("https://example.com:80")?),
Realm::from(&Url::parse("https://example.com")?)
);
Ok(())
}
#[test]
fn test_is_subdomain_of() -> Result<(), ParseError> {
use crate::realm::RealmRef;
let subdomain_url = Url::parse("https://sub.example.com")?;
let domain_url = Url::parse("https://example.com")?;
let subdomain = RealmRef::from(&subdomain_url);
let domain = RealmRef::from(&domain_url);
assert!(subdomain.is_subdomain_of(domain));
let deep_subdomain_url = Url::parse("https://foo.bar.example.com")?;
let deep_subdomain = RealmRef::from(&deep_subdomain_url);
assert!(deep_subdomain.is_subdomain_of(domain));
let parent_subdomain_url = Url::parse("https://bar.example.com")?;
let parent_subdomain = RealmRef::from(&parent_subdomain_url);
assert!(deep_subdomain.is_subdomain_of(parent_subdomain));
assert!(!domain.is_subdomain_of(subdomain));
assert!(!domain.is_subdomain_of(domain));
let different_tld_url = Url::parse("https://example.org")?;
let different_tld = RealmRef::from(&different_tld_url);
assert!(!different_tld.is_subdomain_of(domain));
let partial_match_url = Url::parse("https://notexample.com")?;
let partial_match = RealmRef::from(&partial_match_url);
assert!(!partial_match.is_subdomain_of(domain));
let http_subdomain_url = Url::parse("http://sub.example.com")?;
let https_domain_url = Url::parse("https://example.com")?;
let http_subdomain = RealmRef::from(&http_subdomain_url);
let https_domain = RealmRef::from(&https_domain_url);
assert!(!http_subdomain.is_subdomain_of(https_domain));
let subdomain_port_8080_url = Url::parse("https://sub.example.com:8080")?;
let domain_port_9090_url = Url::parse("https://example.com:9090")?;
let subdomain_port_8080 = RealmRef::from(&subdomain_port_8080_url);
let domain_port_9090 = RealmRef::from(&domain_port_9090_url);
assert!(!subdomain_port_8080.is_subdomain_of(domain_port_9090));
let subdomain_with_port_url = Url::parse("https://sub.example.com:8080")?;
let domain_with_port_url = Url::parse("https://example.com:8080")?;
let subdomain_with_port = RealmRef::from(&subdomain_with_port_url);
let domain_with_port = RealmRef::from(&domain_with_port_url);
assert!(subdomain_with_port.is_subdomain_of(domain_with_port));
let subdomain_default_url = Url::parse("https://sub.example.com")?;
let domain_explicit_443_url = Url::parse("https://example.com:443")?;
let subdomain_default = RealmRef::from(&subdomain_default_url);
let domain_explicit_443 = RealmRef::from(&domain_explicit_443_url);
assert!(subdomain_default.is_subdomain_of(domain_explicit_443));
let file_url = Url::parse("file:///path/to/file")?;
let https_url = Url::parse("https://example.com")?;
let file_realm = RealmRef::from(&file_url);
let https_realm = RealmRef::from(&https_url);
assert!(!file_realm.is_subdomain_of(https_realm));
assert!(!https_realm.is_subdomain_of(file_realm));
let subdomain_with_path_url = Url::parse("https://sub.example.com/path")?;
let domain_with_path_url = Url::parse("https://example.com/other")?;
let subdomain_with_path = RealmRef::from(&subdomain_with_path_url);
let domain_with_path = RealmRef::from(&domain_with_path_url);
assert!(subdomain_with_path.is_subdomain_of(domain_with_path));
Ok(())
}
}