1use std::fmt;
2use std::net::{Ipv4Addr, Ipv6Addr};
3
4use crate::parse;
5
6#[derive(Debug, Clone, PartialEq, Eq, Hash)]
11#[non_exhaustive]
12pub enum Host {
13 IPv4(Ipv4Addr),
15 IPv6(Ipv6Addr),
17 Hostname(String),
19}
20
21impl Host {
22 pub(crate) fn parse_from_uri(s: &str) -> Result<(Self, usize), String> {
27 if s.is_empty() {
28 return Err("empty host".into());
29 }
30
31 if s.starts_with('[') {
32 let end = s
34 .find(']')
35 .ok_or_else(|| "unclosed IPv6 bracket".to_string())?;
36 let addr_str = &s[1..end];
37 let addr: Ipv6Addr = addr_str
38 .parse()
39 .map_err(|e| format!("invalid IPv6 address: {e}"))?;
40 Ok((Host::IPv6(addr), end + 1))
41 } else {
42 let end = find_host_end(s);
45 let host_str = &s[..end];
46
47 if host_str.is_empty() {
48 return Err("empty host".into());
49 }
50
51 let decoded = if host_str.contains('%') {
53 parse::percent_decode(host_str, parse::is_unreserved)
54 } else {
55 host_str.to_string()
56 };
57
58 if decoded
60 .bytes()
61 .all(|b| b.is_ascii_digit() || b == b'.')
62 {
63 if let Ok(addr) = decoded.parse::<Ipv4Addr>() {
64 return Ok((Host::IPv4(addr), end));
65 }
66 }
67
68 if !decoded
70 .bytes()
71 .all(|b| b.is_ascii_alphanumeric() || matches!(b, b'-' | b'.'))
72 {
73 return Err(format!("invalid hostname character in '{decoded}'"));
74 }
75
76 Ok((Host::Hostname(decoded.to_ascii_lowercase()), end))
77 }
78 }
79}
80
81fn find_host_end(s: &str) -> usize {
83 let bytes = s.as_bytes();
84 let mut i = 0;
85 while i < bytes.len() {
86 match bytes[i] {
87 b':' | b';' | b'?' | b'#' | b'>' => return i,
88 b'%' if i + 2 < bytes.len() => {
89 i += 3;
91 }
92 _ => i += 1,
93 }
94 }
95 i
96}
97
98impl Host {
99 pub fn fmt_uri(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
101 match self {
102 Host::IPv4(addr) => write!(f, "{addr}"),
103 Host::IPv6(addr) => write!(f, "[{addr}]"),
104 Host::Hostname(name) => write!(f, "{name}"),
105 }
106 }
107}
108
109impl fmt::Display for Host {
110 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
111 self.fmt_uri(f)
112 }
113}
114
115#[cfg(test)]
116mod tests {
117 use super::*;
118
119 #[test]
120 fn parse_ipv4() {
121 let (host, consumed) = Host::parse_from_uri("172.21.55.55:5060").unwrap();
122 assert_eq!(host, Host::IPv4(Ipv4Addr::new(172, 21, 55, 55)));
123 assert_eq!(consumed, 12);
124 }
125
126 #[test]
127 fn parse_ipv6() {
128 let (host, consumed) = Host::parse_from_uri("[::1]:56001").unwrap();
129 assert_eq!(host, Host::IPv6(Ipv6Addr::LOCALHOST));
130 assert_eq!(consumed, 5);
131 }
132
133 #[test]
134 fn parse_ipv6_full() {
135 let (host, consumed) = Host::parse_from_uri("[2001:db8::1]:5061").unwrap();
136 assert_eq!(
137 host,
138 Host::IPv6(
139 "2001:db8::1"
140 .parse::<Ipv6Addr>()
141 .unwrap()
142 )
143 );
144 assert_eq!(consumed, 13);
145 }
146
147 #[test]
148 fn parse_hostname() {
149 let (host, consumed) = Host::parse_from_uri("example.com;transport=tcp").unwrap();
150 assert_eq!(host, Host::Hostname("example.com".into()));
151 assert_eq!(consumed, 11);
152 }
153
154 #[test]
155 fn hostname_lowercased() {
156 let (host, _) = Host::parse_from_uri("MY.DOMAIN").unwrap();
157 assert_eq!(host, Host::Hostname("my.domain".into()));
158 }
159
160 #[test]
161 fn display_ipv6_has_brackets() {
162 let host = Host::IPv6(Ipv6Addr::LOCALHOST);
163 assert_eq!(host.to_string(), "[::1]");
164 }
165
166 #[test]
167 fn empty_host_fails() {
168 assert!(Host::parse_from_uri("").is_err());
169 assert!(Host::parse_from_uri(":5060").is_err());
170 }
171}