use core::net::{Ipv4Addr, Ipv6Addr};
use crate::parser;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum DomainScope {
Global,
Local,
Literal(LiteralScope),
}
impl DomainScope {
pub fn is_global(&self) -> bool {
match self {
Self::Global => true,
Self::Local => false,
Self::Literal(literal) => literal.is_global(),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum LiteralScope {
Ipv4(IpScope),
Ipv6(IpScope),
General,
}
impl LiteralScope {
pub fn is_global(&self) -> bool {
matches!(
self,
Self::Ipv4(IpScope::Global) | Self::Ipv6(IpScope::Global)
)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum IpScope {
Global,
Local,
}
const RESERVED: &[&str] = &[
"localhost",
"test",
"invalid",
"example",
"local",
"home.arpa",
"internal",
"onion",
"alt",
];
pub(crate) fn classify(domain: &str) -> DomainScope {
if let Some(content) = domain.strip_prefix('[').and_then(|r| r.strip_suffix(']')) {
return DomainScope::Literal(literal_scope(content));
}
if !domain.contains('.') || is_reserved(domain) || !is_globally_named(domain) {
return DomainScope::Local;
}
DomainScope::Global
}
fn is_reserved(domain: &str) -> bool {
RESERVED.iter().any(|name| is_within(domain, name))
}
fn is_within(domain: &str, name: &str) -> bool {
match domain.strip_suffix(name) {
Some("") => true,
Some(prefix) => prefix.ends_with('.'),
None => false,
}
}
#[cfg(feature = "psl")]
fn is_globally_named(domain: &str) -> bool {
structured_public_domains::is_known_suffix(domain)
}
#[cfg(not(feature = "psl"))]
fn is_globally_named(domain: &str) -> bool {
crate::validate::has_tld_like_suffix(domain)
}
fn literal_scope(content: &str) -> LiteralScope {
if let Some(addr) = parser::ipv6_address_literal(content)
.or_else(|| parser::ipv6_address_literal_with_padded_tail(content))
{
return LiteralScope::Ipv6(ipv6_scope(addr));
}
if let Some(addr) = parser::ipv4_address_literal(content) {
return LiteralScope::Ipv4(ipv4_scope(addr));
}
LiteralScope::General
}
fn ipv4_scope(addr: Ipv4Addr) -> IpScope {
let [a, b, c, d] = addr.octets();
let local = match (a, b, c, d) {
(10, ..) | (192, 168, ..) => true,
(172, 16..=31, ..) => true,
(100, 64..=127, ..) => true,
(169, 254, ..) => true,
(127 | 0, ..) => true,
(198, 18..=19, ..) => true,
(255, 255, 255, 255) => true,
(224, 0, 0, _) | (239, ..) => true,
_ => false,
};
if local {
IpScope::Local
} else {
IpScope::Global
}
}
fn ipv6_scope(addr: Ipv6Addr) -> IpScope {
if addr.is_loopback() || addr.is_unspecified() {
return IpScope::Local;
}
if let Some(embedded) = addr.to_ipv4() {
return ipv4_scope(embedded);
}
let leading = addr.segments()[0];
let bounded_multicast = leading & 0xff00 == 0xff00 && leading & 0x000f != 0x000e;
let local = leading & 0xfe00 == 0xfc00 || leading & 0xffc0 == 0xfe80 || leading & 0xffc0 == 0xfec0
|| bounded_multicast;
if local {
IpScope::Local
} else {
IpScope::Global
}
}
#[cfg(test)]
mod tests;