Skip to main content

urly/
authority.rs

1use std::{borrow::Cow, fmt};
2
3use crate::error::{ErrorKind, InvalidUrl};
4use crate::quoting::{Component, unquote};
5use crate::{chars, host::validate_host};
6
7/// URL authority: `[userinfo "@"] host [":" port]`.
8#[repr(transparent)]
9pub struct Authority(str);
10
11str_type!(Authority);
12str_eq!(Authority);
13
14impl Authority {
15    /// Strictly validates an authority.
16    ///
17    /// ```
18    /// use urly::{Authority, ErrorKind};
19    ///
20    /// let auth = Authority::new("user:[email protected]:8080").unwrap();
21    /// assert_eq!(auth.host(), "example.com");
22    /// assert_eq!(auth.port_u16(), Some(8080));
23    /// assert_eq!(auth.userinfo().unwrap().password(), Some("pw"));
24    ///
25    /// let err = Authority::new("example.com:99999").unwrap_err();
26    /// assert_eq!(err.kind(), ErrorKind::PortOutOfRange);
27    /// ```
28    pub fn new(src: &str) -> Result<&Authority, InvalidUrl> {
29        validate_authority(src)?;
30        Ok(Authority::from_str_unchecked(src))
31    }
32
33    /// Converts a static string to an authority.
34    ///
35    /// # Panics
36    ///
37    /// Panics if the authority is not valid.
38    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    /// Returns the userinfo, if present.
46    pub fn userinfo(&self) -> Option<&UserInfo> {
47        split(&self.0).0.map(UserInfo::from_str_unchecked)
48    }
49
50    /// Returns the host. IPv6 addresses include the brackets.
51    pub fn host(&self) -> &str {
52        split(&self.0).1
53    }
54
55    /// Returns the port, if present.
56    pub fn port(&self) -> Option<Port<&str>> {
57        split(&self.0).2.and_then(Port::parse)
58    }
59
60    /// Returns the port as a number, if present.
61    pub fn port_u16(&self) -> Option<u16> {
62        self.port().map(|p| p.as_u16())
63    }
64
65    /// Returns the authority without the userinfo.
66    ///
67    /// ```
68    /// use urly::Authority;
69    ///
70    /// let auth = Authority::new("user:[email protected]:8080").unwrap();
71    /// assert_eq!(auth.host_port(), "example.com:8080");
72    /// ```
73    pub fn host_port(&self) -> &str {
74        self.0.rsplit_once('@').map_or(&self.0, |(_, hp)| hp)
75    }
76}
77
78/// Splits an authority into userinfo, host and port.
79pub(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
102/// Byte offset of `inner` within `outer`.
103pub(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
119/// Parses a decimal port.
120pub(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/// URL userinfo: `user [":" password]`, percent-encoded.
132#[repr(transparent)]
133pub struct UserInfo(str);
134
135str_type!(UserInfo);
136str_eq!(UserInfo);
137
138impl UserInfo {
139    /// Strictly validates a userinfo.
140    pub fn new(src: &str) -> Result<&UserInfo, InvalidUrl> {
141        chars::check(src, &chars::USERINFO)?;
142        Ok(UserInfo::from_str_unchecked(src))
143    }
144
145    /// Returns the percent-encoded user name.
146    pub fn username(&self) -> &str {
147        self.0.split_once(':').map_or(&self.0, |(user, _)| user)
148    }
149
150    /// Returns the percent-encoded password, if present.
151    pub fn password(&self) -> Option<&str> {
152        self.0.split_once(':').map(|(_, password)| password)
153    }
154
155    /// Returns the decoded user name.
156    pub fn decoded_username(&self) -> Cow<'_, str> {
157        unquote(self.username(), Component::UserInfo)
158    }
159
160    /// Returns the decoded password, if present.
161    pub fn decoded_password(&self) -> Option<Cow<'_, str>> {
162        self.password().map(|p| unquote(p, Component::UserInfo))
163    }
164}
165
166/// URL port.
167///
168/// `T` is the textual representation the port was parsed from.
169#[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    /// Returns the port number.
184    pub const fn as_u16(&self) -> u16 {
185        self.port
186    }
187}
188
189impl<T: AsRef<str>> Port<T> {
190    /// Returns the port as written in the URL.
191    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}