1use std::{borrow::Cow, fmt};
2
3use crate::error::{ErrorKind, InvalidUrl};
4use crate::quoting::{Component, unquote};
5use crate::{chars, host::validate_host};
6
7#[repr(transparent)]
9pub struct Authority(str);
10
11str_type!(Authority);
12str_eq!(Authority);
13
14impl Authority {
15 pub fn new(src: &str) -> Result<&Authority, InvalidUrl> {
29 validate_authority(src)?;
30 Ok(Authority::from_str_unchecked(src))
31 }
32
33 pub fn from_static(src: &'static str) -> &'static Authority {
39 match Authority::new(src) {
40 Ok(auth) => auth,
41 Err(e) => panic!("invalid static authority {src:?}: {e}"),
42 }
43 }
44
45 pub fn userinfo(&self) -> Option<&UserInfo> {
47 split(&self.0).0.map(UserInfo::from_str_unchecked)
48 }
49
50 pub fn host(&self) -> &str {
52 split(&self.0).1
53 }
54
55 pub fn port(&self) -> Option<Port<&str>> {
57 split(&self.0).2.and_then(Port::parse)
58 }
59
60 pub fn port_u16(&self) -> Option<u16> {
62 self.port().map(|p| p.as_u16())
63 }
64
65 pub fn host_port(&self) -> &str {
74 self.0.rsplit_once('@').map_or(&self.0, |(_, hp)| hp)
75 }
76}
77
78pub(crate) fn split(s: &str) -> (Option<&str>, &str, Option<&str>) {
80 let (userinfo, hp) = match s.rfind('@') {
81 Some(i) => (Some(&s[..i]), &s[i + 1..]),
82 None => (None, s),
83 };
84 if hp.starts_with('[') {
85 if let Some(end) = hp.find(']') {
86 let rest = &hp[end + 1..];
87 if rest.is_empty() {
88 return (userinfo, hp, None);
89 }
90 if let Some(port) = rest.strip_prefix(':') {
91 return (userinfo, &hp[..=end], Some(port));
92 }
93 }
94 return (userinfo, hp, None);
95 }
96 match hp.rfind(':') {
97 Some(i) => (userinfo, &hp[..i], Some(&hp[i + 1..])),
98 None => (userinfo, hp, None),
99 }
100}
101
102pub(crate) fn offset(outer: &str, inner: &str) -> usize {
104 inner.as_ptr() as usize - outer.as_ptr() as usize
105}
106
107pub(crate) fn validate_authority(s: &str) -> Result<(), InvalidUrl> {
108 let (userinfo, host, port) = split(s);
109 if let Some(userinfo) = userinfo {
110 chars::check(userinfo, &chars::USERINFO)?;
111 }
112 validate_host(host).map_err(|e| e.offset(offset(s, host)))?;
113 if let Some(port) = port {
114 parse_port(port).map_err(|e| e.offset(offset(s, port)))?;
115 }
116 Ok(())
117}
118
119pub(crate) fn parse_port(s: &str) -> Result<u16, InvalidUrl> {
121 if let Some(i) = s.bytes().position(|b| !b.is_ascii_digit()) {
122 return Err(InvalidUrl::at(ErrorKind::InvalidPort, i));
123 }
124 s.bytes()
125 .try_fold(0u16, |acc, b| {
126 acc.checked_mul(10)?.checked_add(u16::from(b - b'0'))
127 })
128 .ok_or(InvalidUrl::at(ErrorKind::PortOutOfRange, 0))
129}
130
131#[repr(transparent)]
133pub struct UserInfo(str);
134
135str_type!(UserInfo);
136str_eq!(UserInfo);
137
138impl UserInfo {
139 pub fn new(src: &str) -> Result<&UserInfo, InvalidUrl> {
141 chars::check(src, &chars::USERINFO)?;
142 Ok(UserInfo::from_str_unchecked(src))
143 }
144
145 pub fn username(&self) -> &str {
147 self.0.split_once(':').map_or(&self.0, |(user, _)| user)
148 }
149
150 pub fn password(&self) -> Option<&str> {
152 self.0.split_once(':').map(|(_, password)| password)
153 }
154
155 pub fn decoded_username(&self) -> Cow<'_, str> {
157 unquote(self.username(), Component::UserInfo)
158 }
159
160 pub fn decoded_password(&self) -> Option<Cow<'_, str>> {
162 self.password().map(|p| unquote(p, Component::UserInfo))
163 }
164}
165
166#[derive(Copy, Clone, Debug)]
170pub struct Port<T> {
171 port: u16,
172 repr: T,
173}
174
175impl<'a> Port<&'a str> {
176 pub(crate) fn parse(repr: &'a str) -> Option<Self> {
177 let port = parse_port(repr).ok().filter(|_| !repr.is_empty())?;
178 Some(Port { port, repr })
179 }
180}
181
182impl<T> Port<T> {
183 pub const fn as_u16(&self) -> u16 {
185 self.port
186 }
187}
188
189impl<T: AsRef<str>> Port<T> {
190 pub fn as_str(&self) -> &str {
192 self.repr.as_ref()
193 }
194}
195
196impl<T> PartialEq<u16> for Port<T> {
197 fn eq(&self, other: &u16) -> bool {
198 self.port == *other
199 }
200}
201
202impl<T, U> PartialEq<Port<U>> for Port<T> {
203 fn eq(&self, other: &Port<U>) -> bool {
204 self.port == other.port
205 }
206}
207
208impl<T: AsRef<str>> fmt::Display for Port<T> {
209 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
210 f.write_str(self.as_str())
211 }
212}
213
214#[cfg(test)]
215mod tests {
216 use super::*;
217
218 #[test]
219 fn split_authority() {
220 assert_eq!(split("a@b:1"), (Some("a"), "b", Some("1")));
221 assert_eq!(split("a:p@b"), (Some("a:p"), "b", None));
222 assert_eq!(split("[::1]:80"), (None, "[::1]", Some("80")));
223 assert_eq!(split("[::1]"), (None, "[::1]", None));
224 assert_eq!(split("[::1]x"), (None, "[::1]x", None));
225 assert_eq!(split("b:"), (None, "b", Some("")));
226 }
227
228 #[test]
229 fn strict() {
230 assert!(Authority::new("").is_ok());
231 assert!(Authority::new("u:[email protected]:").is_ok());
232 assert!(Authority::new("[::1]:65535").is_ok());
233 let err = Authority::new("u p@h").unwrap_err();
234 assert_eq!(err.kind(), ErrorKind::InvalidChar(' '));
235 assert_eq!(err.position(), Some(1));
236 let err = Authority::new("h:8x").unwrap_err();
237 assert_eq!(err.kind(), ErrorKind::InvalidPort);
238 assert_eq!(err.position(), Some(3));
239 let err = Authority::new("[::1").unwrap_err();
240 assert_eq!(err.kind(), ErrorKind::InvalidIpv6);
241 let err = Authority::new("1.2.3.256").unwrap_err();
242 assert_eq!(err.kind(), ErrorKind::InvalidIpv4);
243 }
244
245 #[test]
246 fn port() {
247 let p = Port::parse("0080").unwrap();
248 assert_eq!(p, 80);
249 assert_eq!(p.as_str(), "0080");
250 assert!(Port::parse("65536").is_none());
251 assert!(Port::parse("").is_none());
252 assert!(Port::parse("1a").is_none());
253 }
254}