use core::{
cmp::Ordering,
fmt::Write,
hash::{Hash, Hasher},
ops::Deref,
};
use pct_str::PctStr;
#[derive(static_automata::Validate, str_newtype::StrNewType)]
#[automaton(crate::uri::grammar::Host)]
#[newtype(
no_deref,
ord([u8], &[u8], str, &str, pct_str::PctStr, &pct_str::PctStr)
)]
#[cfg_attr(
feature = "std",
newtype(ord(Vec<u8>, String, pct_str::PctString), owned(HostBuf, derive(PartialEq, Eq, PartialOrd, Ord, Hash)))
)]
#[cfg_attr(feature = "serde", newtype(serde))]
pub struct Host(str);
impl Host {
#[inline]
pub fn as_pct_str(&self) -> &PctStr {
unsafe { PctStr::new_unchecked(self.as_str()) }
}
pub fn is_ip_literal(&self) -> bool {
self.as_bytes().first() == Some(&b'[')
}
pub fn is_ipv4(&self) -> bool {
let bytes = self.as_bytes();
let Some(i) = parse_dec_octet(bytes, 0) else {
return false;
};
if bytes.get(i) != Some(&b'.') {
return false;
}
let Some(j) = parse_dec_octet(bytes, i + 1) else {
return false;
};
if bytes.get(j) != Some(&b'.') {
return false;
}
let Some(k) = parse_dec_octet(bytes, j + 1) else {
return false;
};
if bytes.get(k) != Some(&b'.') {
return false;
}
let Some(l) = parse_dec_octet(bytes, k + 1) else {
return false;
};
l == bytes.len()
}
pub fn is_ipv6(&self) -> bool {
let bytes = self.as_bytes();
bytes.len() >= 4 && bytes[0] == b'[' && bytes[1] != b'v'
}
pub fn to_ipv4(&self) -> Option<u32> {
if !self.is_ipv4() {
return None;
}
Some(parse_ipv4(self.as_str()))
}
pub fn to_ipv6(&self) -> Option<u128> {
if !self.is_ipv6() {
return None;
}
let inner = &self.as_str()[1..self.as_str().len() - 1];
let (left, right) = match inner.split_once("::") {
Some((l, r)) => (l, Some(r)),
None => (inner, None),
};
let mut result: u128 = 0;
let mut count: u32 = 0;
if !left.is_empty() {
for g in left.split(':') {
if g.contains('.') {
let ipv4 = parse_ipv4(g);
result = result << 32 | ipv4 as u128;
count += 2;
} else {
result = result << 16 | u16::from_str_radix(g, 16).unwrap() as u128;
count += 1;
}
}
}
if let Some(right) = right {
let mut right_result: u128 = 0;
if !right.is_empty() {
for g in right.split(':') {
if g.contains('.') {
let ipv4 = parse_ipv4(g);
right_result = right_result << 32 | ipv4 as u128;
} else {
right_result =
right_result << 16 | u16::from_str_radix(g, 16).unwrap() as u128;
}
}
}
result = result.checked_shl((8 - count) * 16).unwrap_or(0) | right_result;
}
Some(result)
}
}
fn parse_dec_octet(bytes: &[u8], i: usize) -> Option<usize> {
let d0 = *bytes.get(i)?;
if !d0.is_ascii_digit() {
return None;
}
if d0 == b'0' {
return Some(i + 1);
}
let Some(&d1) = bytes.get(i + 1) else {
return Some(i + 1);
};
if !d1.is_ascii_digit() {
return Some(i + 1);
}
let Some(&d2) = bytes.get(i + 2) else {
return Some(i + 2);
};
if !d2.is_ascii_digit() {
return Some(i + 2);
}
if d0 == b'1' || (d0 == b'2' && (d1 <= b'4' || (d1 == b'5' && d2 <= b'5'))) {
Some(i + 3)
} else {
None
}
}
fn parse_ipv4(s: &str) -> u32 {
let bytes = s.as_bytes();
let mut result: u32 = 0;
let i = parse_dec_octet(bytes, 0).unwrap();
result |= (bytes[0..i]
.iter()
.fold(0u32, |a, &b| a * 10 + (b - b'0') as u32))
<< 24;
let j = parse_dec_octet(bytes, i + 1).unwrap();
result |= (bytes[i + 1..j]
.iter()
.fold(0u32, |a, &b| a * 10 + (b - b'0') as u32))
<< 16;
let k = parse_dec_octet(bytes, j + 1).unwrap();
result |= (bytes[j + 1..k]
.iter()
.fold(0u32, |a, &b| a * 10 + (b - b'0') as u32))
<< 8;
let l = parse_dec_octet(bytes, k + 1).unwrap();
result |= bytes[k + 1..l]
.iter()
.fold(0u32, |a, &b| a * 10 + (b - b'0') as u32);
result
}
impl Deref for Host {
type Target = PctStr;
fn deref(&self) -> &Self::Target {
self.as_pct_str()
}
}
impl PartialEq for Host {
#[inline]
fn eq(&self, other: &Host) -> bool {
self.as_pct_str() == other.as_pct_str()
}
}
impl Eq for Host {}
impl PartialOrd for Host {
#[inline]
fn partial_cmp(&self, other: &Host) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl Ord for Host {
#[inline]
fn cmp(&self, other: &Host) -> Ordering {
self.as_pct_str().cmp(other.as_pct_str())
}
}
impl Hash for Host {
#[inline]
fn hash<H: Hasher>(&self, hasher: &mut H) {
self.as_pct_str().hash(hasher)
}
}
#[cfg(feature = "std")]
impl HostBuf {
pub fn into_pct_string(self) -> pct_str::PctString {
unsafe { pct_str::PctString::new_unchecked(self.0) }
}
pub fn from_ipv4(addr: u32) -> Self {
let s = format!(
"{}.{}.{}.{}",
(addr >> 24) & 0xff,
(addr >> 16) & 0xff,
(addr >> 8) & 0xff,
addr & 0xff
);
unsafe { Self::new_unchecked(s) }
}
pub fn from_ipv6(addr: u128) -> Self {
let s = format!("[{}]", format_ipv6(addr));
unsafe { Self::new_unchecked(s) }
}
}
#[cfg(feature = "std")]
fn format_ipv6(addr: u128) -> String {
let groups: [u16; 8] = core::array::from_fn(|i| (addr >> (112 - i * 16)) as u16);
let mut best_start = 0;
let mut best_len = 0;
let mut cur_start = 0;
let mut cur_len = 0;
for (i, &g) in groups.iter().enumerate() {
if g == 0 {
if cur_len == 0 {
cur_start = i;
}
cur_len += 1;
if cur_len > best_len {
best_start = cur_start;
best_len = cur_len;
}
} else {
cur_len = 0;
}
}
let mut s = String::new();
let write_groups = |s: &mut String, groups: &[u16]| {
for (i, g) in groups.iter().enumerate() {
if i > 0 {
s.push(':');
}
write!(s, "{:x}", g).unwrap();
}
};
if best_len >= 2 {
write_groups(&mut s, &groups[..best_start]);
s.push_str("::");
write_groups(&mut s, &groups[best_start + best_len..]);
} else {
write_groups(&mut s, &groups);
}
s
}
#[macro_export]
macro_rules! host {
($value:literal) => {
match $crate::uri::Host::from_str($value) {
Ok(value) => value,
Err(_) => panic!("invalid URI authority host"),
}
};
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn is_ipv4_valid() {
for input in [
"0.0.0.0",
"255.255.255.255",
"127.0.0.1",
"1.2.3.4",
"249.249.249.249",
] {
assert!(
Host::new(input).unwrap().is_ipv4(),
"is_ipv4({input}) should be true"
);
}
}
#[test]
fn is_ipv4_invalid() {
for input in [
"example.org",
"[::1]",
"1.2.3",
"1.2.3.4.5",
"999.0.0.1",
"256.0.0.1",
] {
assert!(
!Host::new(input).unwrap().is_ipv4(),
"is_ipv4({input}) should be false"
);
}
}
#[test]
fn to_ipv4() {
let vectors: [(&str, Option<u32>); _] = [
("0.0.0.0", Some(0)),
("255.255.255.255", Some(0xFFFFFFFF)),
("127.0.0.1", Some(0x7F000001)),
("192.168.1.1", Some(0xC0A80101)),
("10.0.0.1", Some(0x0A000001)),
("1.2.3.4", Some(0x01020304)),
("[::1]", None),
("example.org", None),
];
for (input, expected) in vectors {
let host = Host::new(input).unwrap();
assert_eq!(host.to_ipv4(), expected, "to_ipv4({input})");
}
}
#[test]
fn to_ipv6() {
let vectors: [(&str, Option<u128>); _] = [
("[::]", Some(0)),
("[ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff]", Some(u128::MAX)),
("[::1]", Some(1)),
(
"[2001:0db8:85a3:0000:0000:8a2e:0370:7334]",
Some(0x20010db885a3000000008a2e03707334),
),
("[2001:db8::1]", Some(0x20010db8_00000000_00000000_00000001)),
("[1:2:3::]", Some(0x00010002000300000000000000000000)),
("[::1:2:3]", Some(0x00000000000000000000000100020003)),
("[::ffff]", Some(0xffff)),
("[::ffff:192.168.1.1]", Some(0x0000ffff_c0a80101)),
("[0:0:0:0:0:ffff:192.168.1.1]", Some(0x0000ffff_c0a80101)),
("[::127.0.0.1]", Some(0x7f000001)),
("127.0.0.1", None),
("example.org", None),
];
for (input, expected) in vectors {
let host = Host::new(input).unwrap();
assert_eq!(host.to_ipv6(), expected, "to_ipv6({input})");
}
}
#[test]
fn from_ipv4_round_trip() {
let vectors: [(u32, &str); _] = [
(0, "0.0.0.0"),
(0xFFFFFFFF, "255.255.255.255"),
(0x7F000001, "127.0.0.1"),
(0xC0A80101, "192.168.1.1"),
(0x0A000001, "10.0.0.1"),
];
for (addr, expected_str) in vectors {
let host = HostBuf::from_ipv4(addr);
assert_eq!(host.as_str(), expected_str, "from_ipv4(0x{addr:08x})");
assert!(host.is_ipv4());
assert_eq!(host.to_ipv4(), Some(addr));
}
}
#[test]
fn from_ipv6_round_trip() {
let vectors: [(u128, &str); _] = [
(0, "[::]"),
(1, "[::1]"),
(u128::MAX, "[ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff]"),
(0x20010db8_00000000_00000000_00000001, "[2001:db8::1]"),
(0x00010000000300040005000600070008, "[1:0:3:4:5:6:7:8]"),
(0x00010002000000000000000000070008, "[1:2::7:8]"),
(0x00010000000000040005000000000008, "[1::4:5:0:0:8]"),
];
for (addr, expected_str) in vectors {
let host = HostBuf::from_ipv6(addr);
assert_eq!(host.as_str(), expected_str, "from_ipv6(0x{addr:032x})");
assert!(host.is_ipv6());
assert_eq!(host.to_ipv6(), Some(addr));
}
}
}