1use std::{fmt, str::FromStr};
18
19#[derive(Debug, Clone, PartialEq, Eq, Hash)]
24pub struct Authority {
25 host: String,
26 port: u16,
27}
28
29impl Authority {
30 pub fn new(host: impl Into<String>, port: u16) -> Result<Authority, InvalidAuthority> {
32 let host = host.into();
33 if port == 0 {
34 return Err(InvalidAuthority::InvalidPort);
35 }
36 if host.is_empty() {
37 return Err(InvalidAuthority::MissingHost);
38 }
39 if host.contains('@') {
40 return Err(InvalidAuthority::UserinfoNotAllowed);
41 }
42 let parsed = http::uri::Authority::from_str(&format!("{host}:{port}"))
43 .map_err(|_| InvalidAuthority::InvalidHost)?;
44 if parsed.host() != host {
45 return Err(InvalidAuthority::InvalidHost);
46 }
47 Ok(Authority {
48 host: host.to_ascii_lowercase(),
49 port,
50 })
51 }
52
53 #[must_use]
55 pub fn host(&self) -> &str {
56 &self.host
57 }
58
59 #[must_use]
61 pub fn port(&self) -> u16 {
62 self.port
63 }
64}
65
66impl FromStr for Authority {
67 type Err = InvalidAuthority;
68
69 fn from_str(s: &str) -> Result<Self, Self::Err> {
71 let (host, port) = match s.rsplit_once(':') {
72 Some(split) if !s.ends_with(']') => split,
73 _ => return Err(InvalidAuthority::MissingPort),
74 };
75 let port = port.parse().map_err(|_| InvalidAuthority::InvalidPort)?;
76 Authority::new(host, port)
77 }
78}
79
80impl TryFrom<&str> for Authority {
81 type Error = InvalidAuthority;
82
83 fn try_from(s: &str) -> Result<Self, Self::Error> {
84 s.parse()
85 }
86}
87
88impl fmt::Display for Authority {
89 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
90 write!(f, "{}:{}", self.host, self.port)
91 }
92}
93
94#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
96#[non_exhaustive]
97pub enum InvalidAuthority {
98 #[error("authority has no port")]
100 MissingPort,
101 #[error("port is not a number from 1 to 65535")]
103 InvalidPort,
104 #[error("authority has no host")]
106 MissingHost,
107 #[error("host is not a DNS name or an IP literal")]
109 InvalidHost,
110 #[error("authority must not contain userinfo")]
112 UserinfoNotAllowed,
113}
114
115#[cfg(test)]
116mod tests {
117 use super::*;
118
119 #[test]
120 fn parses_and_normalizes() {
121 let authority: Authority = "Example.org:8443".parse().unwrap();
122 assert_eq!(authority.host(), "example.org");
123 assert_eq!(authority.port(), 8443);
124 assert_eq!(authority.to_string(), "example.org:8443");
125
126 let v6: Authority = "[::1]:443".parse().unwrap();
127 assert_eq!(v6.host(), "[::1]");
128 assert_eq!(v6, Authority::new("[::1]", 443).unwrap());
129 }
130
131 #[test]
132 fn rejects_invalid_input() {
133 let cases = [
134 ("example.org", InvalidAuthority::MissingPort),
135 ("[::1]", InvalidAuthority::MissingPort),
136 ("example.org:0", InvalidAuthority::InvalidPort),
137 ("example.org:65536", InvalidAuthority::InvalidPort),
138 ("example.org:http", InvalidAuthority::InvalidPort),
139 (":443", InvalidAuthority::MissingHost),
140 ("user@example.org:443", InvalidAuthority::UserinfoNotAllowed),
141 ("a b:443", InvalidAuthority::InvalidHost),
142 ("a/b:443", InvalidAuthority::InvalidHost),
143 ];
144 for (input, expected) in cases {
145 assert_eq!(input.parse::<Authority>(), Err(expected), "{input}");
146 }
147 assert_eq!(
148 Authority::new("example.org", 0),
149 Err(InvalidAuthority::InvalidPort)
150 );
151 assert_eq!(
152 Authority::new("a:b", 443),
153 Err(InvalidAuthority::InvalidHost)
154 );
155 }
156}