use core::fmt;
use core::str::FromStr;
use super::{DomainName, Hostname, IpAddr};
#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
#[cfg(feature = "arbitrary")]
use arbitrary::Arbitrary;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum HostError {
InvalidInput,
}
impl fmt::Display for HostError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::InvalidInput => write!(f, "invalid host"),
}
}
}
#[cfg(feature = "std")]
impl std::error::Error for HostError {}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub enum Host {
IpAddr(IpAddr),
DomainName(DomainName),
Hostname(Hostname),
}
#[cfg(feature = "arbitrary")]
impl<'a> Arbitrary<'a> for Host {
fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result<Self> {
let choice = u8::arbitrary(u)? % 3;
match choice {
0 => Ok(Self::IpAddr(IpAddr::arbitrary(u)?)),
1 => Ok(Self::DomainName(DomainName::arbitrary(u)?)),
_ => Ok(Self::Hostname(Hostname::arbitrary(u)?)),
}
}
}
impl Host {
#[must_use]
#[inline]
pub const fn from_ipaddr(ipaddr: IpAddr) -> Self {
Self::IpAddr(ipaddr)
}
#[must_use]
#[inline]
pub const fn from_domainname(domain: DomainName) -> Self {
Self::DomainName(domain)
}
#[must_use]
#[inline]
pub const fn from_hostname(hostname: Hostname) -> Self {
Self::Hostname(hostname)
}
#[must_use]
#[inline]
pub const fn is_ipaddr(&self) -> bool {
matches!(self, Self::IpAddr(_))
}
#[must_use]
#[inline]
pub const fn is_domainname(&self) -> bool {
matches!(self, Self::DomainName(_))
}
#[must_use]
#[inline]
pub const fn is_hostname(&self) -> bool {
matches!(self, Self::Hostname(_))
}
#[must_use]
#[inline]
pub const fn as_ipaddr(&self) -> Option<&IpAddr> {
match self {
Self::IpAddr(ipaddr) => Some(ipaddr),
_ => None,
}
}
#[must_use]
#[inline]
pub const fn as_domainname(&self) -> Option<&DomainName> {
match self {
Self::DomainName(domain) => Some(domain),
_ => None,
}
}
#[must_use]
#[inline]
pub const fn as_hostname(&self) -> Option<&Hostname> {
match self {
Self::Hostname(hostname) => Some(hostname),
_ => None,
}
}
#[inline]
#[must_use]
pub fn is_localhost(&self) -> bool {
match self {
Self::IpAddr(ip) => ip.is_loopback(),
Self::DomainName(domain) => domain.as_str() == "localhost",
Self::Hostname(hostname) => hostname.is_localhost(),
}
}
pub fn parse_str(s: &str) -> Result<Self, HostError> {
if s.is_empty() {
return Err(HostError::InvalidInput);
}
if let Ok(ipaddr) = s.parse::<IpAddr>() {
return Ok(Self::IpAddr(ipaddr));
}
if let Ok(domain) = DomainName::new(s) {
return Ok(Self::DomainName(domain));
}
if let Ok(hostname) = Hostname::new(s) {
return Ok(Self::Hostname(hostname));
}
Err(HostError::InvalidInput)
}
}
impl FromStr for Host {
type Err = HostError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Self::parse_str(s)
}
}
impl fmt::Display for Host {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::IpAddr(ipaddr) => write!(f, "{ipaddr}"),
Self::DomainName(domain) => write!(f, "{domain}"),
Self::Hostname(hostname) => write!(f, "{hostname}"),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_from_ipaddr() {
let ipaddr = "192.168.1.1".parse::<IpAddr>().unwrap();
let host = Host::from_ipaddr(ipaddr);
assert!(host.is_ipaddr());
assert!(!host.is_domainname());
assert!(!host.is_hostname());
}
#[test]
fn test_from_domainname() {
let domain = DomainName::new("example.com").unwrap();
let host = Host::from_domainname(domain);
assert!(!host.is_ipaddr());
assert!(host.is_domainname());
assert!(!host.is_hostname());
}
#[test]
fn test_from_hostname() {
let hostname = Hostname::new("example.com").unwrap();
let host = Host::from_hostname(hostname);
assert!(!host.is_ipaddr());
assert!(!host.is_domainname());
assert!(host.is_hostname());
}
#[test]
fn test_parse_ipv4() {
let host: Host = "192.168.1.1".parse().unwrap();
assert!(host.is_ipaddr());
assert_eq!(format!("{host}"), "192.168.1.1");
}
#[test]
fn test_parse_ipv6() {
let host: Host = "::1".parse().unwrap();
assert!(host.is_ipaddr());
assert_eq!(format!("{host}"), "::1");
}
#[test]
fn test_parse_domainname_digit_start() {
let host: Host = "123.example.com".parse().unwrap();
assert!(host.is_domainname());
assert_eq!(format!("{host}"), "123.example.com");
}
#[test]
fn test_parse_hostname_letter_start() {
let host: Host = "www.example.com".parse().unwrap();
assert!(host.is_domainname());
assert_eq!(format!("{host}"), "www.example.com");
}
#[test]
fn test_parse_priority_ipaddr_over_domainname() {
let host: Host = "127.0.0.1".parse().unwrap();
assert!(host.is_ipaddr());
}
#[test]
fn test_parse_priority_domainname_over_hostname() {
let host: Host = "123.example.com".parse().unwrap();
assert!(host.is_domainname());
assert!(!host.is_hostname());
}
#[test]
fn test_parse_str_empty() {
assert!(Host::parse_str("").is_err());
}
#[test]
fn test_parse_str_invalid() {
assert!(Host::parse_str("-invalid").is_err());
assert!(Host::parse_str("example..com").is_err());
}
#[test]
fn test_as_ipaddr() {
let host: Host = "192.168.1.1".parse().unwrap();
assert!(host.as_ipaddr().is_some());
assert!(host.as_domainname().is_none());
assert!(host.as_hostname().is_none());
}
#[test]
fn test_as_domainname() {
let host: Host = "123.example.com".parse().unwrap();
assert!(host.as_ipaddr().is_none());
assert!(host.as_domainname().is_some());
assert!(host.as_hostname().is_none());
}
#[test]
fn test_as_hostname() {
let host: Host = "www.example.com".parse().unwrap();
assert!(host.as_ipaddr().is_none());
assert!(host.as_domainname().is_some());
assert!(host.as_hostname().is_none());
}
#[test]
fn test_equality_ipaddr() {
let host1: Host = "192.168.1.1".parse().unwrap();
let host2: Host = "192.168.1.1".parse().unwrap();
let host3: Host = "192.168.1.2".parse().unwrap();
assert_eq!(host1, host2);
assert_ne!(host1, host3);
}
#[test]
fn test_equality_domainname() {
let host1: Host = "123.example.com".parse().unwrap();
let host2: Host = "123.example.com".parse().unwrap();
let host3: Host = "456.example.com".parse().unwrap();
assert_eq!(host1, host2);
assert_ne!(host1, host3);
}
#[test]
fn test_equality_hostname() {
let host1: Host = "www.example.com".parse().unwrap();
let host2: Host = "www.example.com".parse().unwrap();
let host3: Host = "api.example.com".parse().unwrap();
assert_eq!(host1, host2);
assert_ne!(host1, host3);
}
#[test]
fn test_equality_different_types() {
let host1: Host = "192.168.1.1".parse().unwrap();
let host2: Host = "www.example.com".parse().unwrap();
assert_ne!(host1, host2);
}
#[test]
fn test_clone() {
let host: Host = "www.example.com".parse().unwrap();
let host2 = host.clone();
assert_eq!(host, host2);
}
#[test]
fn test_display_ipaddr() {
let host: Host = "192.168.1.1".parse().unwrap();
assert_eq!(format!("{host}"), "192.168.1.1");
}
#[test]
fn test_display_domainname() {
let host: Host = "123.example.com".parse().unwrap();
assert_eq!(format!("{host}"), "123.example.com");
}
#[test]
fn test_display_hostname() {
let host: Host = "www.example.com".parse().unwrap();
assert_eq!(format!("{host}"), "www.example.com");
}
#[test]
fn test_debug() {
let host: Host = "www.example.com".parse().unwrap();
let debug = format!("{:?}", host);
assert!(debug.contains("DomainName"));
}
#[test]
fn test_hash() {
use core::hash::Hash;
use core::hash::Hasher;
#[derive(Default)]
struct SimpleHasher(u64);
impl Hasher for SimpleHasher {
fn finish(&self) -> u64 {
self.0
}
fn write(&mut self, bytes: &[u8]) {
for byte in bytes {
self.0 = self.0.wrapping_mul(31).wrapping_add(*byte as u64);
}
}
}
let host1: Host = "www.example.com".parse().unwrap();
let host2: Host = "www.example.com".parse().unwrap();
let host3: Host = "api.example.com".parse().unwrap();
let mut hasher1 = SimpleHasher::default();
let mut hasher2 = SimpleHasher::default();
let mut hasher3 = SimpleHasher::default();
host1.hash(&mut hasher1);
host2.hash(&mut hasher2);
host3.hash(&mut hasher3);
assert_eq!(hasher1.finish(), hasher2.finish());
assert_ne!(hasher1.finish(), hasher3.finish());
}
#[test]
fn test_parse_ipv4_private() {
let host: Host = "10.0.0.1".parse().unwrap();
assert!(host.is_ipaddr());
}
#[test]
fn test_parse_ipv6_loopback() {
let host: Host = "::1".parse().unwrap();
assert!(host.is_ipaddr());
}
#[test]
fn test_parse_ipv6_full() {
let host: Host = "2001:0db8:85a3:0000:0000:8a2e:0370:7334".parse().unwrap();
assert!(host.is_ipaddr());
}
#[test]
fn test_parse_domainname_numeric_label() {
let host: Host = "123.456.789".parse().unwrap();
assert!(host.is_domainname());
}
#[test]
fn test_parse_hostname_multi_label() {
let host: Host = "api.v1.example.com".parse().unwrap();
assert!(host.is_domainname());
}
#[test]
fn test_error_display() {
let err = HostError::InvalidInput;
assert_eq!(format!("{err}"), "invalid host");
}
#[test]
fn test_parse_str_method() {
let host = Host::parse_str("192.168.1.1").unwrap();
assert!(host.is_ipaddr());
let host = Host::parse_str("www.example.com").unwrap();
assert!(host.is_domainname());
}
#[test]
fn test_case_insensitive_hostname() {
let host1: Host = "WWW.EXAMPLE.COM".parse().unwrap();
let host2: Host = "www.example.com".parse().unwrap();
assert_eq!(host1, host2);
}
#[test]
fn test_case_insensitive_domainname() {
let host1: Host = "123.EXAMPLE.COM".parse().unwrap();
let host2: Host = "123.example.com".parse().unwrap();
assert_eq!(host1, host2);
}
#[test]
fn test_localhost_hostname() {
let host: Host = "localhost".parse().unwrap();
assert!(host.is_domainname());
}
#[test]
fn test_is_localhost() {
let host: Host = "127.0.0.1".parse().unwrap();
assert!(host.is_localhost());
let host: Host = "::1".parse().unwrap();
assert!(host.is_localhost());
let host: Host = "localhost".parse().unwrap();
assert!(host.is_localhost());
let host: Host = "example.com".parse().unwrap();
assert!(!host.is_localhost());
let host: Host = "192.168.1.1".parse().unwrap();
assert!(!host.is_localhost());
}
#[test]
fn test_numeric_only_domainname() {
let host: Host = "123".parse().unwrap();
assert!(host.is_domainname());
}
#[test]
fn test_mixed_alphanumeric_hostname() {
let host: Host = "api-v1.example.com".parse().unwrap();
assert!(host.is_domainname());
}
#[test]
fn test_from_hostname_variant() {
let hostname = Hostname::new("example.com").unwrap();
let host = Host::from_hostname(hostname);
assert!(host.is_hostname());
assert!(
host.as_hostname()
.map(|h: &Hostname| h.is_localhost())
.unwrap_or(false)
== false
);
}
}