use core::{
fmt,
net::{IpAddr, Ipv4Addr, Ipv6Addr},
};
use crate::std::{
self as std,
borrow::ToOwned,
string::{String, ToString},
vec::Vec,
};
use super::domain::{DomainLabelIter, DomainLabels, Label};
use super::{Domain, UninterpretedHost, UninterpretedHostRef, parse_utils};
use crate::address::ip::{
IPV4_BROADCAST, IPV4_LOCALHOST, IPV4_UNSPECIFIED, IPV6_LOCALHOST, IPV6_UNSPECIFIED,
};
use rama_core::error::BoxErrorExt as _;
use rama_core::error::{BoxError, ErrorContext};
#[derive(Debug, Clone)]
#[non_exhaustive]
pub enum Host {
Name(Domain),
Address(IpAddr),
Uninterpreted(UninterpretedHost),
}
impl Host {
pub fn try_as_domain(&self) -> Result<crate::std::borrow::Cow<'_, Domain>, BoxError> {
match self {
Self::Name(d) => Ok(crate::std::borrow::Cow::Borrowed(d)),
Self::Address(_) => Err(BoxError::from_static_str("Host::Address is not a Domain")),
Self::Uninterpreted(host) => Domain::try_from(host)
.map(crate::std::borrow::Cow::Owned)
.map_err(Into::into),
}
}
pub fn try_into_domain(self) -> Result<Domain, BoxError> {
match self {
Self::Name(d) => Ok(d),
Self::Address(_) => Err(BoxError::from_static_str("Host::Address is not a Domain")),
Self::Uninterpreted(ref host) => Domain::try_from(host).map_err(Into::into),
}
}
pub fn try_as_ip(&self) -> Result<IpAddr, BoxError> {
match self {
Self::Address(ip) => Ok(*ip),
Self::Name(_) => Err(BoxError::from_static_str("Host::Name is not an IpAddr")),
Self::Uninterpreted(host) => IpAddr::try_from(host).map_err(Into::into),
}
}
#[must_use]
#[inline]
pub fn view(&self) -> HostRef<'_> {
HostRef::from(self)
}
#[must_use]
pub fn to_str(&self) -> crate::std::borrow::Cow<'_, str> {
HostRef::from(self).to_str()
}
#[cfg(feature = "idna")]
#[cfg_attr(docsrs, doc(cfg(feature = "idna")))]
#[must_use]
pub fn as_unicode(&self) -> crate::std::borrow::Cow<'_, str> {
HostRef::from(self).as_unicode()
}
#[must_use]
pub fn is_loopback(&self) -> bool {
HostRef::from(self).is_loopback()
}
}
impl Host {
#[must_use]
pub const fn from_static(s: &'static str) -> Self {
Self::Name(Domain::from_static(s))
}
pub const LOCALHOST_IPV4: Self = Self::Address(IPV4_LOCALHOST);
pub const LOCALHOST_IPV6: Self = Self::Address(IPV6_LOCALHOST);
pub const LOCALHOST_NAME: Self = Self::Name(Domain::from_static("localhost"));
pub const DEFAULT_IPV4: Self = Self::Address(IPV4_UNSPECIFIED);
pub const DEFAULT_IPV6: Self = Self::Address(IPV6_UNSPECIFIED);
pub const BROADCAST_IPV4: Self = Self::Address(IPV4_BROADCAST);
pub const EXAMPLE_NAME: Self = Self::Name(Domain::from_static("example.com"));
}
#[derive(Debug, Clone, Copy)]
#[non_exhaustive]
pub enum HostRef<'a> {
Name(super::domain::DomainRef<'a>),
Address(IpAddr),
Uninterpreted(UninterpretedHostRef<'a>),
}
impl<'a> HostRef<'a> {
#[must_use]
pub fn to_str(self) -> crate::std::borrow::Cow<'a, str> {
match self {
Self::Name(d) => d.as_str().into(),
Self::Address(ip) => ip.to_string().into(),
Self::Uninterpreted(host) if host.is_bracketed() => host.to_string().into(),
Self::Uninterpreted(host) => host.as_str().into(),
}
}
#[cfg(feature = "idna")]
#[cfg_attr(docsrs, doc(cfg(feature = "idna")))]
#[must_use]
pub fn as_unicode(self) -> crate::std::borrow::Cow<'a, str> {
match self {
Self::Name(d) => d.as_unicode(),
Self::Address(ip) => ip.to_string().into(),
Self::Uninterpreted(host) if host.is_bracketed() => host.to_string().into(),
Self::Uninterpreted(host) => host.as_unicode(),
}
}
#[must_use]
pub fn is_loopback(self) -> bool {
match self {
Self::Address(ip) => ip.is_loopback(),
Self::Name(domain) => domain.is_loopback(),
Self::Uninterpreted(host) => {
let decoded = host.as_unicode();
decoded
.parse::<IpAddr>()
.map(|ip| ip.is_loopback())
.unwrap_or_else(|_| super::domain::is_loopback_name(&decoded))
}
}
}
#[must_use]
pub fn into_owned(self) -> Host {
match self {
Self::Name(d) => Host::Name(d.into_owned()),
Self::Address(ip) => Host::Address(ip),
Self::Uninterpreted(host) => Host::Uninterpreted(host.into_owned()),
}
}
pub fn try_as_domain(self) -> Result<Domain, BoxError> {
match self {
Self::Name(d) => Ok(d.into_owned()),
Self::Address(_) => Err(BoxError::from_static_str(
"HostRef::Address is not a Domain",
)),
Self::Uninterpreted(host) => Domain::try_from(host).map_err(Into::into),
}
}
pub fn try_as_ip(self) -> Result<IpAddr, BoxError> {
match self {
Self::Address(ip) => Ok(ip),
Self::Name(_) => Err(BoxError::from_static_str("HostRef::Name is not an IpAddr")),
Self::Uninterpreted(host) => IpAddr::try_from(host).map_err(Into::into),
}
}
}
impl<'a> From<&'a Host> for HostRef<'a> {
fn from(h: &'a Host) -> Self {
match h {
Host::Name(d) => Self::Name(d.into()),
Host::Address(ip) => Self::Address(*ip),
Host::Uninterpreted(host) => Self::Uninterpreted(host.into()),
}
}
}
impl PartialEq<str> for Host {
fn eq(&self, other: &str) -> bool {
match self {
Self::Name(domain) => domain == other,
Self::Address(ip) => other.parse::<IpAddr>().is_ok_and(|parsed| parsed == *ip),
Self::Uninterpreted(host) if host.is_bracketed() => {
let o = other.as_bytes();
o.len() >= 2
&& o[0] == b'['
&& o[o.len() - 1] == b']'
&& rama_utils::str::eq_ignore_ascii_case(&o[1..o.len() - 1], host.as_bytes())
}
Self::Uninterpreted(host) => {
let bytes = host.as_bytes();
if rama_utils::str::eq_ignore_ascii_case(bytes, other.as_bytes()) {
return true;
}
if bytes.contains(&b'%')
&& let s = host.as_unicode()
&& rama_utils::str::eq_ignore_ascii_case(
s.as_ref().as_bytes(),
other.as_bytes(),
)
{
return true;
}
false
}
}
}
}
impl PartialEq<Host> for str {
fn eq(&self, other: &Host) -> bool {
other == self
}
}
impl PartialEq<&str> for Host {
fn eq(&self, other: &&str) -> bool {
self == *other
}
}
impl PartialEq<Host> for &str {
#[inline(always)]
fn eq(&self, other: &Host) -> bool {
other == *self
}
}
impl PartialEq<String> for Host {
#[inline(always)]
fn eq(&self, other: &String) -> bool {
self == other.as_str()
}
}
impl PartialEq<Host> for String {
fn eq(&self, other: &Host) -> bool {
other == self.as_str()
}
}
impl PartialEq<Ipv4Addr> for Host {
fn eq(&self, other: &Ipv4Addr) -> bool {
match self {
Self::Name(_) => false,
Self::Address(ip) => match ip {
IpAddr::V4(ip) => ip == other,
IpAddr::V6(ip) => ip.to_ipv4().map(|ip| ip == *other).unwrap_or_default(),
},
Self::Uninterpreted(host) => match IpAddr::try_from(host) {
Ok(IpAddr::V4(ip)) => ip == *other,
Ok(IpAddr::V6(ip)) => ip.to_ipv4().map(|ip| ip == *other).unwrap_or_default(),
Err(_) => false,
},
}
}
}
impl PartialEq<Host> for Ipv4Addr {
fn eq(&self, other: &Host) -> bool {
other == self
}
}
impl PartialEq<Ipv6Addr> for Host {
fn eq(&self, other: &Ipv6Addr) -> bool {
match self {
Self::Name(_) => false,
Self::Address(ip) => match ip {
IpAddr::V4(ip) => ip.to_ipv6_mapped() == *other,
IpAddr::V6(ip) => ip == other,
},
Self::Uninterpreted(host) => match IpAddr::try_from(host) {
Ok(IpAddr::V4(ip)) => ip.to_ipv6_mapped() == *other,
Ok(IpAddr::V6(ip)) => ip == *other,
Err(_) => false,
},
}
}
}
impl PartialEq<Host> for Ipv6Addr {
fn eq(&self, other: &Host) -> bool {
other == self
}
}
impl PartialEq<IpAddr> for Host {
fn eq(&self, other: &IpAddr) -> bool {
match other {
IpAddr::V4(ip) => self == ip,
IpAddr::V6(ip) => self == ip,
}
}
}
impl PartialEq<Host> for IpAddr {
fn eq(&self, other: &Host) -> bool {
other == self
}
}
#[derive(Clone)]
pub enum HostLabelIter<'a> {
Domain(DomainLabelIter<'a>),
Empty,
}
impl<'a> Iterator for HostLabelIter<'a> {
type Item = &'a Label;
fn next(&mut self) -> Option<&'a Label> {
match self {
Self::Domain(it) => it.next(),
Self::Empty => None,
}
}
}
impl DoubleEndedIterator for HostLabelIter<'_> {
fn next_back(&mut self) -> Option<Self::Item> {
match self {
Self::Domain(it) => it.next_back(),
Self::Empty => None,
}
}
}
impl DomainLabels for Host {
type LabelIter<'a> = HostLabelIter<'a>;
fn labels(&self) -> Self::LabelIter<'_> {
match self {
Self::Name(d) => HostLabelIter::Domain(d.labels()),
Self::Address(_) | Self::Uninterpreted(_) => HostLabelIter::Empty,
}
}
}
impl From<Domain> for Host {
fn from(domain: Domain) -> Self {
Self::Name(domain)
}
}
impl From<IpAddr> for Host {
fn from(ip: IpAddr) -> Self {
Self::Address(ip)
}
}
impl From<Ipv4Addr> for Host {
fn from(ip: Ipv4Addr) -> Self {
Self::Address(IpAddr::V4(ip))
}
}
impl From<Ipv6Addr> for Host {
fn from(ip: Ipv6Addr) -> Self {
Self::Address(IpAddr::V6(ip))
}
}
impl fmt::Display for Host {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> core::fmt::Result {
match self {
Self::Name(domain) => domain.fmt(f),
Self::Address(ip) => ip.fmt(f),
Self::Uninterpreted(host) => host.fmt(f),
}
}
}
impl fmt::Display for HostRef<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Name(d) => f.write_str(d.as_str()),
Self::Address(ip) => ip.fmt(f),
Self::Uninterpreted(host) => host.fmt(f),
}
}
}
enum HostCanonical<'a> {
Domain(super::domain::Domain),
DomainRef(super::domain::DomainRef<'a>),
Address(IpAddr),
Opaque(UninterpretedHostRef<'a>),
}
impl<'a> HostCanonical<'a> {
fn from_ref(host: HostRef<'a>) -> Self {
match host {
HostRef::Name(d) => Self::DomainRef(d),
HostRef::Address(ip) => Self::Address(ip),
HostRef::Uninterpreted(u) => {
if u.is_bracketed() {
return Self::Opaque(u);
}
if let Ok(ip) = IpAddr::try_from(u) {
return Self::Address(ip);
}
if let Ok(d) = Domain::try_from(u) {
return Self::Domain(d);
}
Self::Opaque(u)
}
}
}
fn as_view(&self) -> HostCanonicalView<'_> {
match self {
Self::Domain(d) => HostCanonicalView::Domain(d.into()),
Self::DomainRef(d) => HostCanonicalView::Domain(*d),
Self::Address(ip) => HostCanonicalView::Address(*ip),
Self::Opaque(u) => HostCanonicalView::Opaque(*u),
}
}
}
#[derive(Clone, Copy)]
enum HostCanonicalView<'a> {
Domain(super::domain::DomainRef<'a>),
Address(IpAddr),
Opaque(UninterpretedHostRef<'a>),
}
impl HostCanonicalView<'_> {
fn tag(&self) -> u8 {
match self {
Self::Domain(_) => 0,
Self::Address(_) => 1,
Self::Opaque(_) => 2,
}
}
}
impl PartialEq for HostRef<'_> {
fn eq(&self, other: &Self) -> bool {
match (self, other) {
(Self::Name(a), Self::Name(b)) => return a == b,
(Self::Address(a), Self::Address(b)) => return a == b,
(Self::Uninterpreted(a), Self::Uninterpreted(b))
if uninterpreted_byte_compare_is_canonical(*a)
&& uninterpreted_byte_compare_is_canonical(*b) =>
{
return a == b;
}
_ => {}
}
let lhs = HostCanonical::from_ref(*self);
let rhs = HostCanonical::from_ref(*other);
match (lhs.as_view(), rhs.as_view()) {
(HostCanonicalView::Domain(a), HostCanonicalView::Domain(b)) => a == b,
(HostCanonicalView::Address(a), HostCanonicalView::Address(b)) => a == b,
(HostCanonicalView::Opaque(a), HostCanonicalView::Opaque(b)) => a == b,
_ => false,
}
}
}
#[inline]
fn uninterpreted_byte_compare_is_canonical(u: UninterpretedHostRef<'_>) -> bool {
u.as_bytes().iter().all(|&b| b < 0x80 && b != b'%')
}
impl Eq for HostRef<'_> {}
impl Ord for HostRef<'_> {
fn cmp(&self, other: &Self) -> core::cmp::Ordering {
let lhs = HostCanonical::from_ref(*self);
let rhs = HostCanonical::from_ref(*other);
let (la, ra) = (lhs.as_view(), rhs.as_view());
match la.tag().cmp(&ra.tag()) {
core::cmp::Ordering::Equal => match (la, ra) {
(HostCanonicalView::Domain(a), HostCanonicalView::Domain(b)) => a.cmp(&b),
(HostCanonicalView::Address(a), HostCanonicalView::Address(b)) => a.cmp(&b),
(HostCanonicalView::Opaque(a), HostCanonicalView::Opaque(b)) => a.cmp(&b),
_ => unsafe { core::hint::unreachable_unchecked() },
},
non_eq => non_eq,
}
}
}
impl PartialOrd for HostRef<'_> {
fn partial_cmp(&self, other: &Self) -> Option<core::cmp::Ordering> {
Some(self.cmp(other))
}
}
impl core::hash::Hash for HostRef<'_> {
fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
let canonical = HostCanonical::from_ref(*self);
let view = canonical.as_view();
state.write_u8(view.tag());
match view {
HostCanonicalView::Domain(d) => d.hash(state),
HostCanonicalView::Address(ip) => ip.hash(state),
HostCanonicalView::Opaque(u) => u.hash(state),
}
}
}
impl PartialEq for Host {
fn eq(&self, other: &Self) -> bool {
HostRef::from(self) == HostRef::from(other)
}
}
impl Eq for Host {}
impl Ord for Host {
fn cmp(&self, other: &Self) -> core::cmp::Ordering {
HostRef::from(self).cmp(&HostRef::from(other))
}
}
impl PartialOrd for Host {
fn partial_cmp(&self, other: &Self) -> Option<core::cmp::Ordering> {
Some(self.cmp(other))
}
}
impl core::hash::Hash for Host {
fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
HostRef::from(self).hash(state);
}
}
impl core::str::FromStr for Host {
type Err = BoxError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Self::try_from(s)
}
}
impl TryFrom<String> for Host {
type Error = BoxError;
fn try_from(name: String) -> Result<Self, Self::Error> {
try_from_host_str(name.as_str())
}
}
impl TryFrom<&str> for Host {
type Error = BoxError;
fn try_from(name: &str) -> Result<Self, Self::Error> {
try_from_host_str(name)
}
}
fn try_from_host_str(s: &str) -> Result<Host, BoxError> {
if s.is_empty() {
return Err(BoxError::from_static_str("empty host string"));
}
if s.starts_with('[') && s.ends_with(']') {
let inside = &s[1..s.len() - 1];
if inside.is_empty() {
return Err(BoxError::from_static_str("empty bracketed IP-literal"));
}
if matches!(inside.as_bytes().first(), Some(b'v' | b'V')) {
crate::uri::parser::authority::validate_ipvfuture(inside.as_bytes())
.map_err(BoxError::from)
.context("parse bracketed IPvFuture")?;
return Ok(Host::Uninterpreted(
UninterpretedHost::from_validated_bytes(
rama_core::bytes::Bytes::copy_from_slice(inside.as_bytes()),
true,
),
));
}
if super::parse_utils::ipv6_bracket_has_zone(inside.as_bytes()) {
return Err(BoxError::from_static_str(
"ipv6 zone identifiers (RFC 6874) are not supported",
));
}
let addr = inside
.parse::<core::net::Ipv6Addr>()
.context("parse bracketed ipv6 host")?;
return Ok(Host::Address(IpAddr::V6(addr)));
}
if let Some(ip) = parse_utils::try_to_parse_str_to_ip(s) {
return Ok(Host::Address(ip));
}
if let Ok(domain) = Domain::try_from(s.to_owned()) {
return Ok(Host::Name(domain));
}
let host = UninterpretedHost::try_from_reg_name_str(s).context("parse host as reg-name")?;
Ok(Host::Uninterpreted(host))
}
impl TryFrom<Vec<u8>> for Host {
type Error = BoxError;
fn try_from(name: Vec<u8>) -> Result<Self, Self::Error> {
Self::try_from(name.as_slice())
}
}
impl TryFrom<&[u8]> for Host {
type Error = BoxError;
fn try_from(name: &[u8]) -> Result<Self, Self::Error> {
if let Ok(s) = core::str::from_utf8(name)
&& let Ok(host) = try_from_host_str(s)
{
return Ok(host);
}
if let Ok(arr) = <&[u8; 4]>::try_from(name) {
return Ok(Self::Address(IpAddr::from(*arr)));
}
if let Ok(arr) = <&[u8; 16]>::try_from(name) {
return Ok(Self::Address(IpAddr::from(*arr)));
}
Err(BoxError::from_static_str("parse host from bytes failed"))
}
}
use rama_utils::macros::serde_str::impl_serde_str;
impl_serde_str!(display Host);
#[cfg(test)]
mod tests {
use super::*;
#[derive(Debug, Clone, Copy)]
enum Is {
Domain(&'static str),
Ip(&'static str),
}
fn assert_is(host: Host, expected: Is) {
match expected {
Is::Domain(domain) => match host {
Host::Address(address) => {
panic!("expected host address {address} to be the domain: {domain}",)
}
Host::Name(name) => assert_eq!(domain, name),
Host::Uninterpreted(host) => {
panic!("expected host {host} to be the domain: {domain}")
}
},
Is::Ip(ip) => match host {
Host::Address(address) => assert_eq!(ip, address.to_string()),
Host::Name(name) => panic!("expected host domain {name} to be the ip: {ip}"),
Host::Uninterpreted(host) => {
panic!("expected uninterpreted host {host} to be the ip: {ip}")
}
},
}
}
#[test]
fn test_parse_specials() {
for (str, expected) in [
("localhost", Is::Domain("localhost")),
("0.0.0.0", Is::Ip("0.0.0.0")),
("::1", Is::Ip("::1")),
("[::1]", Is::Ip("::1")),
("127.0.0.1", Is::Ip("127.0.0.1")),
("::", Is::Ip("::")),
("[::]", Is::Ip("::")),
] {
let msg = format!("parsing {str}");
assert_is(Host::try_from(str).expect(msg.as_str()), expected);
assert_is(
Host::try_from(str.to_owned()).expect(msg.as_str()),
expected,
);
assert_is(
Host::try_from(str.as_bytes()).expect(msg.as_str()),
expected,
);
assert_is(
Host::try_from(str.as_bytes().to_vec()).expect(msg.as_str()),
expected,
);
}
}
#[test]
fn test_parse_bytes_valid() {
for (bytes, expected) in [
("example.com".as_bytes(), Is::Domain("example.com")),
("aA1".as_bytes(), Is::Domain("aA1")),
(&[127, 0, 0, 1], Is::Ip("127.0.0.1")),
(&[19, 117, 63, 126], Is::Ip("19.117.63.126")),
(
&[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1],
Is::Ip("::1"),
),
(
&[
32, 1, 13, 184, 51, 51, 68, 68, 85, 85, 102, 102, 119, 119, 136, 136,
],
Is::Ip("2001:db8:3333:4444:5555:6666:7777:8888"),
),
] {
let msg = format!("parsing {bytes:?}");
assert_is(Host::try_from(bytes).expect(msg.as_str()), expected);
assert_is(
Host::try_from(bytes.to_vec()).expect(msg.as_str()),
expected,
);
}
}
#[test]
fn test_parse_valid() {
for (str, expected) in [
("example.com", Is::Domain("example.com")),
("www.example.com", Is::Domain("www.example.com")),
("a-b-c.com", Is::Domain("a-b-c.com")),
("a-b-c.example.com", Is::Domain("a-b-c.example.com")),
("a-b-c.example", Is::Domain("a-b-c.example")),
("aA1", Is::Domain("aA1")),
(".example.com", Is::Domain(".example.com")),
("example.com.", Is::Domain("example.com.")),
(".example.com.", Is::Domain(".example.com.")),
("127.0.0.1", Is::Ip("127.0.0.1")),
("127.00.1", Is::Domain("127.00.1")),
("::1", Is::Ip("::1")),
("[::1]", Is::Ip("::1")),
(
"2001:db8:3333:4444:5555:6666:7777:8888",
Is::Ip("2001:db8:3333:4444:5555:6666:7777:8888"),
),
(
"[2001:db8:3333:4444:5555:6666:7777:8888]",
Is::Ip("2001:db8:3333:4444:5555:6666:7777:8888"),
),
("::", Is::Ip("::")),
("[::]", Is::Ip("::")),
("19.117.63.126", Is::Ip("19.117.63.126")),
] {
let msg = format!("parsing {str}");
assert_is(Host::try_from(str).expect(msg.as_str()), expected);
assert_is(
Host::try_from(str.to_owned()).expect(msg.as_str()),
expected,
);
assert_is(
Host::try_from(str.as_bytes()).expect(msg.as_str()),
expected,
);
assert_is(
Host::try_from(str.as_bytes().to_vec()).expect(msg.as_str()),
expected,
);
}
}
#[test]
fn test_parse_str_invalid() {
for str in [
"", "[::", "::]", "@",
] {
assert!(Host::try_from(str).is_err(), "parsing {str}");
assert!(Host::try_from(str.to_owned()).is_err(), "parsing {str}");
}
}
#[test]
fn is_loopback_for_ip_addresses() {
for s in ["127.0.0.1", "127.0.0.2", "127.1.2.3", "::1", "[::1]"] {
assert!(
s.parse::<Host>().unwrap().is_loopback(),
"{s} should be loopback",
);
}
for s in ["0.0.0.0", "8.8.8.8", "::", "192.168.0.1"] {
assert!(
!s.parse::<Host>().unwrap().is_loopback(),
"{s} should not be loopback",
);
}
let mapped = Host::Address(IpAddr::V6("::ffff:127.0.0.1".parse().unwrap()));
assert!(!mapped.is_loopback());
}
#[test]
fn is_loopback_for_localhost_names() {
for s in [
"localhost",
"LOCALHOST",
"LocalHost",
"foo.localhost",
"a.b.localhost",
"localhost.",
".localhost",
] {
assert!(
s.parse::<Host>().unwrap().is_loopback(),
"{s} should be a loopback name",
);
}
for s in [
"example.com",
"localhost.example.com",
"mylocalhost",
"localhostx",
"localhost.com",
] {
assert!(
!s.parse::<Host>().unwrap().is_loopback(),
"{s} should not be a loopback name",
);
}
}
#[test]
fn is_loopback_bridges_uninterpreted_variant() {
assert!(reg_host(b"%6C%6F%63%61%6C%68%6F%73%74").is_loopback());
assert!(reg_host(b"127.0.0.1").is_loopback());
assert!(!reg_host(b"example.com").is_loopback());
assert!(!reg_host(b"8.8.8.8").is_loopback());
assert!(!bracketed_host(b"v1.fe80::a").is_loopback());
}
#[test]
fn compare_host_with_ipv4_bidirectional() {
let test_cases = [
(
true,
"127.0.0.1".parse::<Host>().unwrap(),
Ipv4Addr::LOCALHOST,
),
(
false,
"127.0.0.2".parse::<Host>().unwrap(),
Ipv4Addr::LOCALHOST,
),
(
false,
"127.0.0.1".parse::<Host>().unwrap(),
Ipv4Addr::new(127, 0, 0, 2),
),
];
for (expected, a, b) in test_cases {
assert_eq!(expected, a == b, "a[{a}] == b[{b}]");
assert_eq!(expected, b == a, "b[{b}] == a[{a}]");
}
}
#[test]
fn compare_host_with_ipv6_bidirectional() {
let test_cases = [
(true, "::1".parse::<Host>().unwrap(), Ipv6Addr::LOCALHOST),
(false, "::2".parse::<Host>().unwrap(), Ipv6Addr::LOCALHOST),
(
false,
"::1".parse::<Host>().unwrap(),
Ipv6Addr::new(0, 0, 0, 0, 0, 0, 0, 2),
),
];
for (expected, a, b) in test_cases {
assert_eq!(expected, a == b, "a[{a}] == b[{b}]");
assert_eq!(expected, b == a, "b[{b}] == a[{a}]");
}
}
#[test]
fn compare_host_with_ip_bidirectional() {
let test_cases = [
(true, "127.0.0.1".parse::<Host>().unwrap(), IPV4_LOCALHOST),
(false, "127.0.0.2".parse::<Host>().unwrap(), IPV4_LOCALHOST),
(
false,
"127.0.0.1".parse::<Host>().unwrap(),
IpAddr::V4(Ipv4Addr::new(127, 0, 0, 2)),
),
(false, "::2".parse::<Host>().unwrap(), IPV4_LOCALHOST),
];
for (expected, a, b) in test_cases {
assert_eq!(expected, a == b, "a[{a}] == b[{b}]");
assert_eq!(expected, b == a, "b[{b}] == a[{a}]");
}
}
#[test]
fn host_labels_delegates_to_domain() {
let h = Host::Name(Domain::from_static("www.example.com"));
let labels: Vec<&str> = h.labels().map(|l| l.as_str()).collect();
assert_eq!(labels, vec!["www", "example", "com"]);
assert_eq!(h.label_count(), 3);
let parent_h = Host::Name(Domain::from_static("example.com"));
assert!(h.is_subdomain_of(&parent_h));
let p = h.parent().expect("parent");
assert_eq!(p.as_str(), "example.com");
}
#[test]
fn host_labels_ip_is_empty_and_never_subdomain() {
let h = Host::Address("127.0.0.1".parse::<core::net::IpAddr>().unwrap());
assert_eq!(h.labels().count(), 0);
assert_eq!(h.label_count(), 0);
assert!(h.parent().is_none());
let parent = Host::Name(Domain::from_static("example.com"));
assert!(!h.is_subdomain_of(&parent));
let d_host = Host::Name(Domain::from_static("example.com"));
assert!(!d_host.is_subdomain_of(&h));
}
#[test]
fn regression_host_rejects_ipv6_zone_id() {
for input in [
"fe80::1%eth0",
"fe80::1%25eth0",
"[fe80::1%eth0]",
"[fe80::1%25eth0]",
"::1%0",
] {
assert!(
Host::try_from(input).is_err(),
"host should reject zone-id input {input:?}",
);
}
}
fn reg_host(bytes: &'static [u8]) -> Host {
Host::Uninterpreted(UninterpretedHost::from_validated_bytes(
rama_core::bytes::Bytes::from_static(bytes),
false,
))
}
fn bracketed_host(bytes: &'static [u8]) -> Host {
Host::Uninterpreted(UninterpretedHost::from_validated_bytes(
rama_core::bytes::Bytes::from_static(bytes),
true,
))
}
#[test]
fn eq_str_byte_compares_raw_form() {
let h = reg_host(b"example.com");
assert!(h == "example.com");
assert!(h != "other.com");
}
#[test]
fn eq_str_is_case_insensitive_for_named_host() {
let h = Host::Name(Domain::from_static("example.com"));
assert!(h == "EXAMPLE.com");
assert!(h == "Example.Com");
assert!(h == "example.com");
}
#[test]
fn eq_str_is_case_insensitive_for_uninterpreted_reg_name() {
let h = reg_host(b"tag,WITH,commas");
assert!(h == "tag,with,commas");
assert!(h == "TAG,WITH,COMMAS");
}
#[test]
fn eq_str_is_case_insensitive_through_pct_decode() {
let h = reg_host(b"exa%6Dple.com");
assert!(h == "EXAMPLE.com");
}
#[test]
fn eq_str_is_case_insensitive_for_bracketed_ipvfuture() {
let h = bracketed_host(b"v1.FE80::A");
assert!(h == "[v1.fe80::a]");
assert!(h == "[V1.fe80::a]");
}
#[test]
fn eq_str_byte_compares_pct_encoded_raw_first() {
let h = reg_host(b"exa%6Dple.com");
assert!(h == "exa%6Dple.com");
}
#[test]
fn eq_str_decodes_pct_when_byte_compare_fails() {
let h = reg_host(b"exa%6Dple.com");
assert!(h == "example.com");
}
#[test]
fn eq_str_no_decode_when_no_pct_in_bytes() {
let h = reg_host(b"example.com");
assert!(h != "something.completely.different");
}
#[test]
fn eq_str_address_ipv6_mixed_case() {
let h = Host::Address("fe80::1".parse().unwrap());
assert!(h == "fe80::1");
assert!(h == "FE80::1");
assert!(h == "Fe80::1");
assert!(h == "fe80:0:0:0:0:0:0:1");
assert!(h != "fe80::2");
}
#[test]
fn eq_str_address_ipv4_mapped_ipv6_kept_distinct() {
let h4 = Host::Address("127.0.0.1".parse::<core::net::IpAddr>().unwrap());
assert!(h4 == "127.0.0.1");
assert!(h4 != "::ffff:127.0.0.1");
let h6 = Host::Address("::ffff:127.0.0.1".parse().unwrap());
assert!(h6 == "::ffff:127.0.0.1");
assert!(h6 != "127.0.0.1");
}
#[test]
fn eq_str_brackets_compared_without_allocation() {
let h = bracketed_host(b"v1.fe80::a");
assert!(h == "[v1.fe80::a]");
assert!(h != "v1.fe80::a");
assert!(h != "[v2.fe80::a]");
assert!(h != "");
assert!(h != "[]");
}
#[test]
fn eq_ipv4_decodes_pct_encoded_dotted_quad() {
let h = reg_host(b"%31%32%37.0.0.1");
assert!(h == Ipv4Addr::new(127, 0, 0, 1));
assert!(Ipv4Addr::new(127, 0, 0, 1) == h);
assert!(h != Ipv4Addr::new(127, 0, 0, 2));
}
#[test]
fn eq_ipv4_matches_unencoded_dotted_quad_too() {
let h = reg_host(b"127.0.0.1");
assert!(h == Ipv4Addr::new(127, 0, 0, 1));
}
#[test]
fn eq_ipv6_decodes_pct_encoded_colon_form() {
let h = reg_host(b"2001%3Adb8%3A%3A1");
assert!(h == "2001:db8::1".parse::<Ipv6Addr>().unwrap());
}
#[test]
fn eq_ipv4_mapped_v6_via_pct_encoded_v4() {
let h = reg_host(b"%31%32%37.0.0.1");
let mapped = Ipv4Addr::new(127, 0, 0, 1).to_ipv6_mapped();
assert!(h == mapped);
}
#[test]
fn eq_ipvfuture_never_matches_any_ip() {
let h = bracketed_host(b"v1.fe80::a");
assert!(h != Ipv4Addr::new(127, 0, 0, 1));
assert!(h != "::1".parse::<Ipv6Addr>().unwrap());
let any: IpAddr = "127.0.0.1".parse::<core::net::IpAddr>().unwrap();
assert!(h != any);
}
#[test]
fn eq_reg_name_never_matches_ip_when_not_decodable() {
let h = reg_host(b"example.com");
assert!(h != Ipv4Addr::new(127, 0, 0, 1));
assert!(h != "::1".parse::<Ipv6Addr>().unwrap());
}
#[test]
fn host_ref_display_matches_owned_host() {
for owned in [
Host::Name(Domain::from_static("example.com")),
Host::Address("127.0.0.1".parse::<core::net::IpAddr>().unwrap()),
Host::Address("::1".parse().unwrap()),
reg_host(b"exa%6Dple.com"),
bracketed_host(b"v1.fe80::a"),
] {
let r: HostRef<'_> = (&owned).into();
assert_eq!(format!("{r}"), format!("{owned}"));
}
}
#[test]
fn cross_variant_name_eq_uninterpreted_via_pct_decode() {
let typed = Host::Name(Domain::from_static("example.com"));
let pct = reg_host(b"exa%6Dple.com");
assert_eq!(typed, pct);
assert_eq!(pct, typed);
}
#[test]
fn cross_variant_name_eq_uninterpreted_case_insensitive() {
let typed = Host::Name(Domain::from_static("example.com"));
let upper_pct = reg_host(b"EXa%6Dple.COM");
assert_eq!(typed, upper_pct);
}
#[test]
fn cross_variant_address_eq_uninterpreted_via_pct_decode() {
let typed = Host::Address("127.0.0.1".parse::<core::net::IpAddr>().unwrap());
let pct = reg_host(b"%31%32%37.0.0.1");
assert_eq!(typed, pct);
}
#[test]
fn cross_variant_bracketed_ipvfuture_never_eq_typed() {
let bracketed = bracketed_host(b"v1.fe80::a");
let domain = Host::Name(Domain::from_static("v1"));
assert_ne!(bracketed, domain);
let v6 = Host::Address("fe80::a".parse().unwrap());
assert_ne!(bracketed, v6);
}
#[test]
fn cross_variant_opaque_uninterpreted_never_eq_typed() {
let opaque = reg_host(b"tag,with,commas");
assert_ne!(opaque, Host::Name(Domain::from_static("tag")));
assert_ne!(
opaque,
Host::Address("127.0.0.1".parse::<core::net::IpAddr>().unwrap())
);
}
#[test]
fn cross_variant_hash_agrees_with_eq() {
use ahash::{HashMap, HashMapExt as _};
let mut m: HashMap<Host, &'static str> = HashMap::new();
m.insert(Host::Name(Domain::from_static("example.com")), "value");
assert_eq!(m.get(®_host(b"exa%6Dple.com")), Some(&"value"));
assert_eq!(m.get(®_host(b"EXa%6Dple.COM")), Some(&"value"));
}
#[test]
fn cross_variant_hash_address_via_uninterpreted() {
use ahash::{HashMap, HashMapExt as _};
let mut m: HashMap<Host, ()> = HashMap::new();
m.insert(
Host::Address("127.0.0.1".parse::<core::net::IpAddr>().unwrap()),
(),
);
assert!(m.contains_key(®_host(b"%31%32%37.0.0.1")));
}
#[test]
fn cross_variant_ord_promotion_groups_typed_together() {
let mut v = [
reg_host(b"tag,with,commas"), reg_host(b"exa%6Dple.com"), Host::Address("127.0.0.1".parse::<core::net::IpAddr>().unwrap()), Host::Name(Domain::from_static("aaaa.example")), ];
v.sort();
assert!(matches!(&v[0], Host::Name(d) if d.as_str() == "aaaa.example"));
assert!(matches!(&v[1], Host::Uninterpreted(_)));
assert!(matches!(&v[2], Host::Address(_)));
assert!(matches!(&v[3], Host::Uninterpreted(u) if u.as_str() == "tag,with,commas"));
}
#[cfg(feature = "idna")]
#[test]
fn eq_transitive_uts46_via_non_ascii_uninterpreted() {
let a = reg_host("ß.de".as_bytes()); let b = reg_host("ẞ.de".as_bytes()); let c = Host::Name(Domain::try_from("ß.de").unwrap());
assert_eq!(a, c);
assert_eq!(b, c);
assert_eq!(a, b, "Eq must be transitive — A==C ∧ B==C ⇒ A==B");
}
#[cfg(feature = "idna")]
#[test]
fn eq_transitive_uts46_via_pct_encoded_non_ascii() {
let a = reg_host(b"%E1%BA%9E.de"); let b = reg_host(b"%C3%9F.de"); let c = Host::Name(Domain::try_from("ß.de").unwrap());
assert_eq!(a, c);
assert_eq!(b, c);
assert_eq!(a, b);
}
#[test]
fn hash_determinism_bracketed_ipvfuture() {
use crate::test_hash::hash;
let h1 = bracketed_host(b"v1.fe80::a");
let h2 = bracketed_host(b"v1.fe80::a");
assert_eq!(hash(&h1), hash(&h2));
}
#[test]
fn ord_transitive_across_bridge() {
let a = Host::Name(Domain::from_static("aaaa.example"));
let b = reg_host(b"bbbb.example"); let c = Host::Name(Domain::from_static("cccc.example"));
assert!(a < b);
assert!(b < c);
assert!(a < c, "Ord must be transitive across the bridge");
}
#[test]
fn try_as_domain_name_returns_borrowed() {
let h = Host::Name(Domain::from_static("example.com"));
let cow = h.try_as_domain().unwrap();
assert!(
matches!(cow, crate::std::borrow::Cow::Borrowed(_)),
"expected Cow::Borrowed for the Name variant"
);
assert_eq!(cow.as_str(), "example.com");
}
#[test]
fn try_as_domain_address_errors() {
let h = Host::Address("127.0.0.1".parse::<core::net::IpAddr>().unwrap());
h.try_as_domain().unwrap_err();
let h6 = Host::Address("::1".parse().unwrap());
h6.try_as_domain().unwrap_err();
}
#[test]
fn try_as_domain_uninterpreted_pct_decodes() {
let h = reg_host(b"exa%6Dple.com");
let cow = h.try_as_domain().unwrap();
assert!(matches!(cow, crate::std::borrow::Cow::Owned(_)));
assert_eq!(cow.as_str(), "example.com");
}
#[cfg(feature = "idna")]
#[test]
fn try_as_domain_uninterpreted_idn_normalizes() {
let h = reg_host("ß.de".as_bytes());
let d = h.try_as_domain().unwrap();
assert_eq!(d.as_str(), "xn--zca.de");
}
#[test]
fn try_as_domain_uninterpreted_ipvfuture_errors() {
let h = bracketed_host(b"v1.fe80::a");
h.try_as_domain().unwrap_err();
}
#[test]
fn try_as_domain_uninterpreted_subdelim_errors() {
let h = reg_host(b"tag,with,commas");
h.try_as_domain().unwrap_err();
}
#[test]
fn try_into_domain_agrees_with_try_as_domain() {
for h in [
Host::Name(Domain::from_static("example.com")),
reg_host(b"exa%6Dple.com"),
] {
let by_ref = h.try_as_domain().map(|c| c.as_str().to_owned());
let by_val = h.clone().try_into_domain().map(|d| d.as_str().to_owned());
assert_eq!(by_ref.unwrap(), by_val.unwrap());
}
for h in [
Host::Address("127.0.0.1".parse::<core::net::IpAddr>().unwrap()),
reg_host(b"tag,with,commas"),
bracketed_host(b"v1.fe80::a"),
] {
h.try_as_domain().unwrap_err();
h.clone().try_into_domain().unwrap_err();
}
}
#[test]
fn try_as_ip_address_returns_value() {
let h = Host::Address("127.0.0.1".parse::<core::net::IpAddr>().unwrap());
assert_eq!(
h.try_as_ip().unwrap(),
"127.0.0.1".parse::<core::net::IpAddr>().unwrap()
);
}
#[test]
fn try_as_ip_name_errors() {
let h = Host::Name(Domain::from_static("example.com"));
h.try_as_ip().unwrap_err();
}
#[test]
fn try_as_ip_uninterpreted_pct_encoded_ipv4() {
let h = reg_host(b"%31%32%37.0.0.1");
assert_eq!(
h.try_as_ip().unwrap(),
"127.0.0.1".parse::<core::net::IpAddr>().unwrap()
);
}
#[test]
fn try_as_ip_uninterpreted_ipvfuture_errors() {
let h = bracketed_host(b"v1.fe80::a");
h.try_as_ip().unwrap_err();
}
#[test]
fn try_as_ip_uninterpreted_subdelim_errors() {
let h = reg_host(b"tag,with,commas");
h.try_as_ip().unwrap_err();
}
#[test]
fn host_ref_try_as_domain_name_returns_owned() {
let owned = Host::Name(Domain::from_static("example.com"));
let r = HostRef::from(&owned);
let d = r.try_as_domain().unwrap();
assert_eq!(d.as_str(), "example.com");
}
#[test]
fn host_ref_try_as_ip_bridges_pct_encoded_ipv4() {
let owned = reg_host(b"%31%32%37.0.0.1");
let r = HostRef::from(&owned);
assert_eq!(
r.try_as_ip().unwrap(),
"127.0.0.1".parse::<core::net::IpAddr>().unwrap()
);
}
#[test]
fn host_ref_try_as_domain_address_errors() {
let owned = Host::Address("127.0.0.1".parse::<core::net::IpAddr>().unwrap());
let r = HostRef::from(&owned);
r.try_as_domain().unwrap_err();
}
#[test]
fn host_try_from_recovers_uninterpreted_pct_encoded() {
let h: Host = "exa%6Dple.com".parse().unwrap();
assert!(matches!(h, Host::Uninterpreted(_)));
assert_eq!(h.to_string(), "exa%6Dple.com");
}
#[test]
fn host_try_from_recovers_uninterpreted_subdelim() {
let h: Host = "tag,with,commas".parse().unwrap();
assert!(matches!(h, Host::Uninterpreted(_)));
assert_eq!(h.to_string(), "tag,with,commas");
}
#[test]
fn host_try_from_recovers_bracketed_ipvfuture() {
let h: Host = "[v1.fe80::a]".parse().unwrap();
assert!(matches!(h, Host::Uninterpreted(_)));
assert_eq!(h.to_string(), "[v1.fe80::a]");
}
#[test]
fn host_try_from_recovers_bracketed_ipv6() {
let h: Host = "[::1]".parse().unwrap();
assert!(matches!(h, Host::Address(IpAddr::V6(_))));
}
#[test]
fn host_serde_roundtrip_through_uninterpreted() {
let original = "exa%6Dple.com".parse::<Host>().unwrap();
let json = serde_json::to_string(&original).unwrap();
let round: Host = serde_json::from_str(&json).unwrap();
assert_eq!(original, round);
}
}