use core::{
fmt,
net::{AddrParseError, IpAddr, Ipv4Addr, Ipv6Addr},
};
use crate::std::{borrow::Cow, string::String, vec::Vec};
use super::Domain;
use super::domain::DomainParseError;
use rama_core::bytes::Bytes;
use rama_core::error::{BoxError, ErrorContext};
#[derive(Debug, Clone)]
pub struct UninterpretedHost {
bracketed: bool,
bytes: Bytes,
}
impl UninterpretedHost {
#[inline]
pub(crate) fn from_validated_bytes(bytes: Bytes, bracketed: bool) -> Self {
Self { bracketed, bytes }
}
pub(crate) fn try_from_reg_name_str(s: &str) -> Result<Self, BoxError> {
crate::uri::parser::authority::validate_reg_name_graceful(s.as_bytes())
.map_err(BoxError::from)
.context("validate reg-name bytes")?;
Ok(Self::from_validated_bytes(
Bytes::copy_from_slice(s.as_bytes()),
false,
))
}
#[expect(dead_code, reason = "exposed for strict-mode address constructors")]
pub(crate) fn try_from_reg_name_str_strict(s: &str) -> Result<Self, BoxError> {
crate::uri::parser::authority::validate_reg_name_strict(s.as_bytes())
.map_err(BoxError::from)
.context("validate reg-name bytes")?;
Ok(Self::from_validated_bytes(
Bytes::copy_from_slice(s.as_bytes()),
false,
))
}
#[must_use]
#[inline]
pub fn view(&self) -> UninterpretedHostRef<'_> {
UninterpretedHostRef::from(self)
}
#[must_use]
pub fn as_str(&self) -> &str {
self.view().as_str()
}
#[must_use]
pub fn as_bytes(&self) -> &[u8] {
self.view().as_bytes()
}
#[must_use]
pub fn is_bracketed(&self) -> bool {
self.bracketed
}
#[must_use]
pub fn as_unicode(&self) -> Cow<'_, str> {
self.view().as_unicode()
}
}
impl fmt::Display for UninterpretedHost {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
UninterpretedHostRef::from(self).fmt(f)
}
}
#[derive(Debug, Clone, Copy)]
pub struct UninterpretedHostRef<'a> {
bracketed: bool,
bytes: &'a [u8],
}
impl<'a> UninterpretedHostRef<'a> {
#[must_use]
pub fn as_str(&self) -> &'a str {
unsafe { core::str::from_utf8_unchecked(self.bytes) }
}
#[must_use]
pub fn as_bytes(&self) -> &'a [u8] {
self.bytes
}
#[must_use]
pub fn is_bracketed(&self) -> bool {
self.bracketed
}
#[must_use]
pub fn as_unicode(&self) -> Cow<'a, str> {
if !self.bytes.contains(&b'%') {
return Cow::Borrowed(self.as_str());
}
let mut out = Vec::with_capacity(self.bytes.len());
let mut i = 0;
while i < self.bytes.len() {
let b = self.bytes[i];
if b == b'%'
&& i + 2 < self.bytes.len()
&& let Some(decoded) =
rama_utils::hex::decode_pair(self.bytes[i + 1], self.bytes[i + 2])
{
out.push(decoded);
i += 3;
} else {
out.push(b);
i += 1;
}
}
match String::from_utf8(out) {
Ok(s) => Cow::Owned(s),
Err(e) => {
Cow::Owned(String::from_utf8_lossy(&e.into_bytes()).into_owned())
}
}
}
#[must_use]
pub fn into_owned(self) -> UninterpretedHost {
UninterpretedHost::from_validated_bytes(Bytes::copy_from_slice(self.bytes), self.bracketed)
}
}
impl fmt::Display for UninterpretedHostRef<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if self.bracketed {
write!(f, "[{}]", self.as_str())
} else {
f.write_str(self.as_str())
}
}
}
impl<'a> From<&'a UninterpretedHost> for UninterpretedHostRef<'a> {
fn from(host: &'a UninterpretedHost) -> Self {
Self {
bracketed: host.bracketed,
bytes: &host.bytes,
}
}
}
struct LogicalBytes<'a> {
buf: &'a [u8],
pos: usize,
}
impl<'a> LogicalBytes<'a> {
#[inline]
fn new(buf: &'a [u8]) -> Self {
Self { buf, pos: 0 }
}
}
impl<'a> Iterator for LogicalBytes<'a> {
type Item = u8;
fn next(&mut self) -> Option<u8> {
if self.pos >= self.buf.len() {
return None;
}
if self.buf[self.pos] == b'%'
&& self.pos + 2 < self.buf.len()
&& let Some(d) =
rama_utils::hex::decode_pair(self.buf[self.pos + 1], self.buf[self.pos + 2])
{
self.pos += 3;
return Some(d);
}
let b = self.buf[self.pos];
self.pos += 1;
Some(b)
}
}
impl PartialEq for UninterpretedHostRef<'_> {
fn eq(&self, other: &Self) -> bool {
if self.bracketed != other.bracketed {
return false;
}
if !self.bytes.contains(&b'%') && !other.bytes.contains(&b'%') {
return rama_utils::str::eq_ignore_ascii_case(self.bytes, other.bytes);
}
let a = LogicalBytes::new(self.bytes).map(|b| b.to_ascii_lowercase());
let b = LogicalBytes::new(other.bytes).map(|b| b.to_ascii_lowercase());
a.eq(b)
}
}
impl Eq for UninterpretedHostRef<'_> {}
impl Ord for UninterpretedHostRef<'_> {
fn cmp(&self, other: &Self) -> core::cmp::Ordering {
self.bracketed.cmp(&other.bracketed).then_with(|| {
if !self.bytes.contains(&b'%') && !other.bytes.contains(&b'%') {
let a = self.bytes.iter().map(|b| b.to_ascii_lowercase());
let b = other.bytes.iter().map(|b| b.to_ascii_lowercase());
return a.cmp(b);
}
let a = LogicalBytes::new(self.bytes).map(|b| b.to_ascii_lowercase());
let b = LogicalBytes::new(other.bytes).map(|b| b.to_ascii_lowercase());
a.cmp(b)
})
}
}
impl PartialOrd for UninterpretedHostRef<'_> {
fn partial_cmp(&self, other: &Self) -> Option<core::cmp::Ordering> {
Some(self.cmp(other))
}
}
impl core::hash::Hash for UninterpretedHostRef<'_> {
fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
self.bracketed.hash(state);
if !self.bytes.contains(&b'%') {
for &b in self.bytes {
state.write_u8(b.to_ascii_lowercase());
}
state.write_usize(self.bytes.len());
return;
}
let mut count = 0usize;
for b in LogicalBytes::new(self.bytes) {
state.write_u8(b.to_ascii_lowercase());
count += 1;
}
state.write_usize(count);
}
}
impl PartialEq for UninterpretedHost {
fn eq(&self, other: &Self) -> bool {
UninterpretedHostRef::from(self) == UninterpretedHostRef::from(other)
}
}
impl Eq for UninterpretedHost {}
impl Ord for UninterpretedHost {
fn cmp(&self, other: &Self) -> core::cmp::Ordering {
UninterpretedHostRef::from(self).cmp(&UninterpretedHostRef::from(other))
}
}
impl PartialOrd for UninterpretedHost {
fn partial_cmp(&self, other: &Self) -> Option<core::cmp::Ordering> {
Some(self.cmp(other))
}
}
impl core::hash::Hash for UninterpretedHost {
fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
UninterpretedHostRef::from(self).hash(state);
}
}
impl<'a> TryFrom<UninterpretedHostRef<'a>> for Domain {
type Error = DomainParseError;
fn try_from(host: UninterpretedHostRef<'a>) -> Result<Self, Self::Error> {
if host.bracketed {
return Err(DomainParseError::bracketed_ip_literal());
}
match host.as_unicode() {
Cow::Borrowed(s) => Self::try_from(s),
Cow::Owned(s) => Self::try_from(s),
}
}
}
impl<'a> TryFrom<UninterpretedHostRef<'a>> for IpAddr {
type Error = AddrParseError;
fn try_from(host: UninterpretedHostRef<'a>) -> Result<Self, Self::Error> {
host.as_unicode().as_ref().parse()
}
}
impl<'a> TryFrom<UninterpretedHostRef<'a>> for Ipv4Addr {
type Error = AddrParseError;
fn try_from(host: UninterpretedHostRef<'a>) -> Result<Self, Self::Error> {
host.as_unicode().as_ref().parse()
}
}
impl<'a> TryFrom<UninterpretedHostRef<'a>> for Ipv6Addr {
type Error = AddrParseError;
fn try_from(host: UninterpretedHostRef<'a>) -> Result<Self, Self::Error> {
host.as_unicode().as_ref().parse()
}
}
impl TryFrom<&UninterpretedHost> for Domain {
type Error = DomainParseError;
#[inline]
fn try_from(host: &UninterpretedHost) -> Result<Self, Self::Error> {
UninterpretedHostRef::from(host).try_into()
}
}
impl TryFrom<&UninterpretedHost> for IpAddr {
type Error = AddrParseError;
#[inline]
fn try_from(host: &UninterpretedHost) -> Result<Self, Self::Error> {
UninterpretedHostRef::from(host).try_into()
}
}
impl TryFrom<&UninterpretedHost> for Ipv4Addr {
type Error = AddrParseError;
#[inline]
fn try_from(host: &UninterpretedHost) -> Result<Self, Self::Error> {
UninterpretedHostRef::from(host).try_into()
}
}
impl TryFrom<&UninterpretedHost> for Ipv6Addr {
type Error = AddrParseError;
#[inline]
fn try_from(host: &UninterpretedHost) -> Result<Self, Self::Error> {
UninterpretedHostRef::from(host).try_into()
}
}
impl TryFrom<UninterpretedHost> for Domain {
type Error = DomainParseError;
#[inline]
fn try_from(host: UninterpretedHost) -> Result<Self, Self::Error> {
Self::try_from(&host)
}
}
impl TryFrom<UninterpretedHost> for IpAddr {
type Error = AddrParseError;
#[inline]
fn try_from(host: UninterpretedHost) -> Result<Self, Self::Error> {
Self::try_from(&host)
}
}
impl TryFrom<UninterpretedHost> for Ipv4Addr {
type Error = AddrParseError;
#[inline]
fn try_from(host: UninterpretedHost) -> Result<Self, Self::Error> {
Self::try_from(&host)
}
}
impl TryFrom<UninterpretedHost> for Ipv6Addr {
type Error = AddrParseError;
#[inline]
fn try_from(host: UninterpretedHost) -> Result<Self, Self::Error> {
Self::try_from(&host)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn reg(bytes: &'static [u8]) -> UninterpretedHost {
UninterpretedHost::from_validated_bytes(Bytes::from_static(bytes), false)
}
fn bracketed(bytes: &'static [u8]) -> UninterpretedHost {
UninterpretedHost::from_validated_bytes(Bytes::from_static(bytes), true)
}
#[test]
fn as_unicode_borrows_when_no_pct() {
let h = reg(b"example.com");
assert!(matches!(h.as_unicode(), Cow::Borrowed(_)));
assert_eq!(&*h.as_unicode(), "example.com");
}
#[test]
fn as_unicode_decodes_pct_to_ascii() {
let h = reg(b"exa%6Dple.com");
assert!(matches!(h.as_unicode(), Cow::Owned(_)));
assert_eq!(&*h.as_unicode(), "example.com");
}
#[test]
fn as_unicode_decodes_pct_to_utf8() {
let h = reg(b"m%C3%BCller.de");
assert_eq!(&*h.as_unicode(), "müller.de");
}
#[test]
fn as_unicode_lossy_fallback_on_invalid_utf8() {
let h = reg(b"%C3%C3");
let decoded = h.as_unicode();
assert_ne!(&*decoded, "%C3%C3", "expected lossy decoded form, not raw");
assert!(
decoded.contains('\u{FFFD}'),
"expected U+FFFD in lossy fallback, got {decoded:?}"
);
}
#[test]
fn try_into_domain_decodes_pct_encoded_ascii() {
let h = reg(b"exa%6Dple.com");
let d: Domain = (&h).try_into().unwrap();
assert_eq!(d.as_str(), "example.com");
}
#[cfg(feature = "idna")]
#[test]
fn try_into_domain_applies_idna_on_decoded_utf8() {
let h = reg(b"m%C3%BCnchen.de");
let d: Domain = (&h).try_into().unwrap();
assert_eq!(d.as_str(), "xn--mnchen-3ya.de");
}
#[test]
fn try_into_domain_fails_on_sub_delim_chars() {
let h = reg(b"tag,with,commas");
Domain::try_from(&h).unwrap_err();
}
#[test]
fn try_into_domain_fails_on_bracketed_with_typed_error() {
let h = bracketed(b"v1.fe80::a");
let err = Domain::try_from(&h).unwrap_err();
assert!(
format!("{err}").contains("bracketed IP-literal"),
"got: {err}"
);
}
#[test]
fn try_into_ip_addr_decodes_pct_encoded_ipv4() {
let h = reg(b"%31%32%37.0.0.1");
let ip: IpAddr = (&h).try_into().unwrap();
assert_eq!(ip, "127.0.0.1".parse::<IpAddr>().unwrap());
}
#[test]
fn try_into_ipv4_addr_works_for_dotted_quad() {
let h = reg(b"192.0.2.1");
let ip: Ipv4Addr = (&h).try_into().unwrap();
assert_eq!(ip, Ipv4Addr::new(192, 0, 2, 1));
}
#[test]
fn try_into_ipv6_addr_works_for_colon_form() {
let h = reg(b"2001:db8::1");
let ip: Ipv6Addr = (&h).try_into().unwrap();
assert_eq!(ip, "2001:db8::1".parse::<Ipv6Addr>().unwrap());
}
#[test]
fn try_into_ip_addr_fails_for_ipvfuture() {
let h = bracketed(b"v1.fe80::a");
IpAddr::try_from(&h).unwrap_err();
Ipv4Addr::try_from(&h).unwrap_err();
Ipv6Addr::try_from(&h).unwrap_err();
}
#[test]
fn try_into_ip_addr_fails_for_pure_reg_name() {
let h = reg(b"example.com");
IpAddr::try_from(&h).unwrap_err();
}
#[test]
fn try_into_domain_owned_works() {
let h = reg(b"exa%6Dple.com");
let d: Domain = h.try_into().unwrap();
assert_eq!(d.as_str(), "example.com");
}
#[test]
fn try_into_ip_addr_owned_works() {
let h = reg(b"127.0.0.1");
let ip: IpAddr = h.try_into().unwrap();
assert_eq!(ip, "127.0.0.1".parse::<IpAddr>().unwrap());
}
#[test]
fn try_into_ipv4_owned_works() {
let h = reg(b"127.0.0.1");
let ip: Ipv4Addr = h.try_into().unwrap();
assert_eq!(ip, Ipv4Addr::new(127, 0, 0, 1));
}
#[test]
fn try_into_ipv6_owned_works() {
let h = reg(b"::1");
let ip: Ipv6Addr = h.try_into().unwrap();
assert_eq!(ip, "::1".parse::<Ipv6Addr>().unwrap());
}
#[test]
fn try_into_domain_owned_propagates_bracketed_error() {
let h = bracketed(b"v1.fe80::a");
let err: DomainParseError = Domain::try_from(h).unwrap_err();
assert!(format!("{err}").contains("bracketed IP-literal"));
}
#[test]
fn display_brackets_ip_literal() {
let h = bracketed(b"v1.fe80::a");
assert_eq!(h.to_string(), "[v1.fe80::a]");
}
#[test]
fn display_renders_reg_name_verbatim() {
let h = reg(b"exa%6Dple.com");
assert_eq!(h.to_string(), "exa%6Dple.com");
}
#[test]
fn eq_distinguishes_bracketed_flag() {
let a = reg(b"v1");
let b = bracketed(b"v1");
assert_ne!(a, b);
}
#[test]
fn ord_sorts_bracketed_after_reg_name() {
let mut v = [bracketed(b"v1"), reg(b"zzz"), reg(b"aaa")];
v.sort();
assert_eq!(v[0].as_str(), "aaa");
assert!(!v[0].is_bracketed());
assert_eq!(v[1].as_str(), "zzz");
assert!(!v[1].is_bracketed());
assert_eq!(v[2].as_str(), "v1");
assert!(v[2].is_bracketed());
}
#[test]
fn eq_ignores_ascii_case_in_reg_name_bytes() {
assert_eq!(reg(b"Example.COM"), reg(b"example.com"));
assert_eq!(reg(b"EXAMPLE.com"), reg(b"example.com"));
}
#[test]
fn eq_ignores_pct_hex_case() {
assert_eq!(reg(b"exa%6Dple.com"), reg(b"exa%6dple.com"));
}
#[test]
fn eq_treats_pct_encoded_as_decoded_form() {
assert_eq!(reg(b"exa%6Dple.com"), reg(b"example.com"));
}
#[test]
fn eq_folds_pct_encoded_letter_case() {
assert_eq!(reg(b"%44host.com"), reg(b"%64host.com"));
assert_eq!(reg(b"%44host.com"), reg(b"Dhost.com"));
assert_eq!(reg(b"%44host.com"), reg(b"dhost.com"));
}
#[test]
fn eq_distinguishes_bracketed_regardless_of_case() {
assert_ne!(reg(b"V1"), bracketed(b"v1"));
assert_ne!(reg(b"v1"), bracketed(b"V1"));
}
#[test]
fn hash_matches_eq_for_case_variants() {
use ahash::{HashMap, HashMapExt as _};
let mut m: HashMap<UninterpretedHost, &'static str> = HashMap::new();
m.insert(reg(b"Example.com"), "value");
assert_eq!(m.get(®(b"example.com")), Some(&"value"));
assert_eq!(m.get(®(b"EXAMPLE.COM")), Some(&"value"));
assert!(!m.contains_key(&bracketed(b"Example.com")));
}
#[test]
fn hash_matches_eq_for_pct_hex_case_variants() {
use ahash::{HashMap, HashMapExt as _};
let mut m: HashMap<UninterpretedHost, ()> = HashMap::new();
m.insert(reg(b"exa%6Dple.com"), ());
assert!(m.contains_key(®(b"exa%6dple.com")));
}
#[test]
fn hash_matches_eq_across_encoding_forms() {
use ahash::{HashMap, HashMapExt as _};
let mut m: HashMap<UninterpretedHost, &'static str> = HashMap::new();
m.insert(reg(b"exa%6Dple.com"), "value");
assert_eq!(m.get(®(b"example.com")), Some(&"value"));
assert_eq!(m.get(®(b"EXAMPLE.com")), Some(&"value"));
assert_eq!(m.get(®(b"exa%6dple.com")), Some(&"value"));
}
#[test]
fn ord_uses_case_folded_compare_within_shape() {
let mut v = [reg(b"B.com"), reg(b"a.com")];
v.sort();
assert_eq!(v[0].as_str(), "a.com");
assert_eq!(v[1].as_str(), "B.com");
}
#[test]
fn eq_ref_and_owned_agree() {
let a = reg(b"Example.com");
let b = reg(b"example.com");
let ar: UninterpretedHostRef<'_> = (&a).into();
let br: UninterpretedHostRef<'_> = (&b).into();
assert_eq!(ar, br);
assert_eq!(a, b);
}
#[test]
fn ref_from_owned_borrows_bytes() {
let h = reg(b"exa%6Dple.com");
let r: UninterpretedHostRef<'_> = (&h).into();
assert_eq!(r.as_bytes(), b"exa%6Dple.com");
assert!(!r.is_bracketed());
}
#[test]
fn ref_as_unicode_decodes_pct() {
let h = reg(b"exa%6Dple.com");
let r: UninterpretedHostRef<'_> = (&h).into();
assert!(matches!(r.as_unicode(), Cow::Owned(_)));
assert_eq!(&*r.as_unicode(), "example.com");
}
#[test]
fn ref_into_owned_roundtrip() {
let h = reg(b"exa%6Dple.com");
let r: UninterpretedHostRef<'_> = (&h).into();
let back: UninterpretedHost = r.into_owned();
assert_eq!(back, h);
}
#[test]
fn ref_display_brackets_ip_literal() {
let h = bracketed(b"v1.fe80::a");
let r: UninterpretedHostRef<'_> = (&h).into();
assert_eq!(r.to_string(), "[v1.fe80::a]");
}
#[test]
fn ref_try_into_domain_decodes_pct() {
let h = reg(b"exa%6Dple.com");
let r: UninterpretedHostRef<'_> = (&h).into();
let d: Domain = r.try_into().unwrap();
assert_eq!(d.as_str(), "example.com");
}
#[test]
fn ref_try_into_ipv4_decodes_pct() {
let h = reg(b"%31%32%37.0.0.1");
let r: UninterpretedHostRef<'_> = (&h).into();
let ip: Ipv4Addr = r.try_into().unwrap();
assert_eq!(ip, Ipv4Addr::new(127, 0, 0, 1));
}
}