1use std::{borrow::Cow, fmt, net::Ipv4Addr, net::Ipv6Addr};
2
3use crate::chars::{self, NONE, REG_NAME, char_at, lowercase};
4use crate::error::{ErrorKind, InvalidUrl};
5use crate::{idna, quoting::unquote_with};
6
7#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
18pub enum Host<'a> {
19 Domain(&'a str),
21 Ipv4(Ipv4Addr),
23 Ipv6(Ipv6Addr),
25}
26
27impl<'a> Host<'a> {
28 pub fn parse(src: &'a str) -> Result<Host<'a>, InvalidUrl> {
30 validate_host(src)?;
31 Ok(Host::classify(src))
32 }
33
34 pub(crate) fn classify(src: &'a str) -> Host<'a> {
35 if let Some(inner) = src.strip_prefix('[')
36 && let Ok(addr) = ipv6(inner)
37 {
38 Host::Ipv6(addr)
39 } else if looks_like_ipv4(src)
40 && let Ok(addr) = src.parse()
41 {
42 Host::Ipv4(addr)
43 } else {
44 Host::Domain(src)
45 }
46 }
47
48 pub fn to_unicode(&self) -> Cow<'a, str> {
57 match self {
58 Host::Domain(s) => idna::domain_to_unicode(s),
59 host => Cow::Owned(host.to_string()),
60 }
61 }
62}
63
64impl fmt::Display for Host<'_> {
65 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
66 match self {
67 Host::Domain(s) => f.write_str(s),
68 Host::Ipv4(addr) => fmt::Display::fmt(addr, f),
69 Host::Ipv6(addr) => write!(f, "[{addr}]"),
70 }
71 }
72}
73
74fn looks_like_ipv4(s: &str) -> bool {
75 !s.is_empty()
76 && s.bytes().all(|b| b.is_ascii_digit() || b == b'.')
77 && s.bytes().filter(|b| *b == b'.').count() == 3
78}
79
80fn ipv6(inner: &str) -> Result<Ipv6Addr, InvalidUrl> {
82 inner
83 .strip_suffix(']')
84 .and_then(|inner| inner.parse().ok())
85 .ok_or(InvalidUrl::at(ErrorKind::InvalidIpv6, 0))
86}
87
88fn check_ipv4(s: &str) -> Result<(), InvalidUrl> {
90 if looks_like_ipv4(s) && s.parse::<Ipv4Addr>().is_err() {
91 Err(InvalidUrl::at(ErrorKind::InvalidIpv4, 0))
92 } else {
93 Ok(())
94 }
95}
96
97pub(crate) fn validate_host(s: &str) -> Result<(), InvalidUrl> {
98 if let Some(inner) = s.strip_prefix('[') {
99 return ipv6(inner).map(|_| ());
100 }
101 chars::check(s, ®_NAME)?;
102 check_ipv4(s)
103}
104
105pub(crate) fn normalize_host(s: &str) -> Result<Cow<'_, str>, InvalidUrl> {
108 if let Some(inner) = s.strip_prefix('[') {
109 let canonical = format!("[{}]", ipv6(inner)?);
110 return Ok(if canonical == s {
111 Cow::Borrowed(s)
112 } else {
113 Cow::Owned(canonical)
114 });
115 }
116
117 let decoded = unquote_with(s, false, &NONE);
118 let host = if !decoded.is_ascii() {
119 Cow::Owned(
120 idna::domain_to_ascii(&decoded).ok_or(InvalidUrl::at(ErrorKind::InvalidHost, 0))?,
121 )
122 } else {
123 lowercase(decoded)
124 };
125 if let Some(i) = host.bytes().position(|b| !REG_NAME.contains(b)) {
126 return Err(if s.len() == host.len() && s.is_ascii() {
128 InvalidUrl::at(ErrorKind::InvalidChar(char_at(&host, i)), i)
129 } else {
130 InvalidUrl::at(ErrorKind::InvalidHost, 0)
131 });
132 }
133 check_ipv4(&host)?;
134 Ok(host)
135}
136
137#[cfg(test)]
138mod tests {
139 use super::*;
140
141 #[test]
142 fn normalize() {
143 assert!(matches!(
144 normalize_host("a.com"),
145 Ok(Cow::Borrowed("a.com"))
146 ));
147 assert_eq!(normalize_host("A.Com").unwrap(), "a.com");
148 assert_eq!(normalize_host("%41.com").unwrap(), "a.com");
149 assert_eq!(normalize_host("[0:0::1]").unwrap(), "[::1]");
150 assert_eq!(
151 normalize_host("a b").unwrap_err().kind(),
152 ErrorKind::InvalidChar(' ')
153 );
154 assert_eq!(
155 normalize_host("a%20b").unwrap_err().kind(),
156 ErrorKind::InvalidHost
157 );
158 assert_eq!(normalize_host("München.de").unwrap(), "xn--mnchen-3ya.de");
159 assert_eq!(
160 normalize_host("m%C3%BCnchen.de").unwrap(),
161 "xn--mnchen-3ya.de"
162 );
163 assert_eq!(
164 normalize_host("mü nchen.de").unwrap_err().kind(),
165 ErrorKind::InvalidHost
166 );
167 assert_eq!(
168 normalize_host("1.2.3.999").unwrap_err().kind(),
169 ErrorKind::InvalidIpv4
170 );
171 assert_eq!(
172 normalize_host("[::g]").unwrap_err().kind(),
173 ErrorKind::InvalidIpv6
174 );
175 }
176
177 #[test]
178 fn classify() {
179 assert_eq!(Host::classify("127.0.0.1"), Host::Ipv4(Ipv4Addr::LOCALHOST));
180 assert_eq!(Host::classify("1.2.3"), Host::Domain("1.2.3"));
181 assert_eq!(Host::Ipv6(Ipv6Addr::LOCALHOST).to_string(), "[::1]");
182 }
183}