1use crate::is_valid_host;
2use std::fmt::Display;
3use std::net::SocketAddrV4;
4use std::str::FromStr;
5use thiserror::Error;
6
7#[cfg(feature = "serde")]
8use serde::{Deserialize, Serialize};
9
10#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
12#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
13pub struct HostPort {
14 host: String,
16
17 port: u16,
19}
20
21impl HostPort {
22 pub fn new<S: Into<String>>(host: S, port: u16) -> Result<HostPort, ParseError> {
34 let host = host.into();
35 if !is_valid_host(&host) {
36 return Err(ParseError::InvalidHost(host));
37 }
38 Ok(Self { host, port })
39 }
40
41 #[must_use]
43 pub fn host(&self) -> &str {
44 &self.host
45 }
46
47 #[must_use]
49 pub fn port(&self) -> u16 {
50 self.port
51 }
52}
53
54impl TryFrom<&str> for HostPort {
73 type Error = ParseError;
74
75 fn try_from(value: &str) -> Result<Self, Self::Error> {
76 let (host, port_str) = value.split_once(':').ok_or(ParseError::InvalidFormat)?;
77
78 let port = port_str
79 .parse::<u16>()
80 .map_err(|_| ParseError::InvalidPort(port_str.to_string()))?;
81 HostPort::new(host, port)
82 }
83}
84
85impl Display for HostPort {
86 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
87 write!(f, "{}:{}", self.host, self.port)
88 }
89}
90
91impl From<&SocketAddrV4> for HostPort {
92 fn from(socket_addr: &SocketAddrV4) -> Self {
93 HostPort {
94 host: socket_addr.ip().to_string(),
95 port: socket_addr.port(),
96 }
97 }
98}
99
100impl From<SocketAddrV4> for HostPort {
101 fn from(socket_addr: SocketAddrV4) -> Self {
102 Self::from(&socket_addr)
103 }
104}
105
106impl FromStr for HostPort {
107 type Err = ParseError;
108
109 fn from_str(value: &str) -> Result<Self, Self::Err> {
110 HostPort::try_from(value)
111 }
112}
113
114impl PartialEq<&str> for HostPort {
115 fn eq(&self, other: &&str) -> bool {
116 HostPort::try_from(*other)
117 .map(|hp| self == &hp)
118 .unwrap_or(false)
119 }
120}
121
122impl PartialEq<HostPort> for &str {
123 fn eq(&self, other: &HostPort) -> bool {
124 HostPort::try_from(*self)
125 .map(|hp| &hp == other)
126 .unwrap_or(false)
127 }
128}
129
130#[derive(Debug, Error, Eq, PartialEq)]
137pub enum ParseError {
138 #[error("Invalid format, expected host:port")]
140 InvalidFormat,
141
142 #[error("Invalid host: {0}")]
144 InvalidHost(String),
145
146 #[error("Invalid port: {0}")]
148 InvalidPort(String),
149}
150
151#[cfg(test)]
152#[cfg_attr(coverage_nightly, coverage(off))]
153mod tests {
154 use super::*;
155 use anyhow::Result;
156 use pretty_assertions::assert_eq;
157 use proptest::prelude::*;
158
159 proptest! {
160 #[test]
161 fn hostport_try_from_proptest(
162 host in r"[a-zA-Z0-9]([-a-zA-Z0-9]*[a-zA-Z0-9])?((\.[a-zA-Z0-9]([-a-zA-Z0-9]*[a-zA-Z0-9])?)*)?",
164 port in any::<u16>(),
165 invalid_str in r"[^:]*",
167 invalid_port in r"[a-zA-Z]{1,10}"
168 ) {
169 if !is_valid_host(&host) {
171 return Ok(());
172 }
173
174 let input = format!("{}:{}", &host, &port);
176 let result = HostPort::try_from(input.as_str());
177
178 prop_assert!(result.is_ok());
179 let hostport = result.unwrap();
180 prop_assert_eq!(hostport.host(), host.clone());
181 prop_assert_eq!(hostport.port(), port);
182
183 let result = HostPort::try_from(invalid_str.as_str());
187 prop_assert_eq!(result.err(), Some(ParseError::InvalidFormat));
188
189 if !invalid_port.is_empty() {
191 let input = format!("{}:{}", host.clone(), invalid_port.clone());
192 let result = HostPort::try_from(input.as_str());
193 prop_assert!(result.is_err());
194 prop_assert!(matches!(result.err(), Some(ParseError::InvalidPort(_))));
195 }
196
197 let invalid_host = format!("{}$", host); let input = format!("{}:{}", invalid_host, port);
200 let result = HostPort::try_from(input.as_str());
201 prop_assert!(result.is_err());
202 prop_assert!(matches!(result.err(), Some(ParseError::InvalidHost(_))));
203
204 let input = format!(":{}", port);
206 let result = HostPort::try_from(input.as_str());
207 prop_assert!(result.is_err());
208
209 let input = format!("{}:", host);
211 let result = HostPort::try_from(input.as_str());
212 prop_assert!(result.is_err());
213 prop_assert!(matches!(result.err(), Some(ParseError::InvalidPort(_))));
214 }
215 }
216
217 #[test]
218 fn test_new() -> Result<()> {
219 {
220 assert_eq!(
221 HostPort::new("_", 50).unwrap_err(),
222 ParseError::InvalidHost("_".to_string())
223 );
224 }
225 {
226 let hostport = HostPort::new("quake.se", 28501)?;
227 assert_eq!(hostport.host(), "quake.se");
228 assert_eq!(hostport.port(), 28501);
229 }
230 Ok(())
231 }
232
233 #[test]
234 fn test_display() -> Result<()> {
235 let hostport = HostPort::new("quake.se", 28501)?;
236 assert_eq!(hostport.to_string(), "quake.se:28501");
237 Ok(())
238 }
239
240 #[test]
241 fn test_from_socket_addr() -> Result<()> {
242 let socket_addr = SocketAddrV4::from_str("10.10.10.10:28501")?;
243 let hostport: HostPort = HostPort::from(&socket_addr);
244 assert_eq!(hostport.host(), "10.10.10.10");
245 assert_eq!(hostport.port(), 28501);
246 assert_eq!(HostPort::from(&socket_addr), HostPort::from(socket_addr));
247 Ok(())
248 }
249
250 #[test]
251 fn test_from_str() -> Result<()> {
252 let hostport = HostPort::from_str("quake.se:28501")?;
253 assert_eq!(hostport.host(), "quake.se");
254 assert_eq!(hostport.port(), 28501);
255 Ok(())
256 }
257
258 #[test]
259 fn test_partial_eq() -> Result<()> {
260 assert_eq!(HostPort::new("quake.se", 28501)?, "quake.se:28501");
261 assert_ne!(HostPort::new("quake.se", 28501)?, "quake.se:28502");
262
263 assert_eq!("quake.se:28501", HostPort::new("quake.se", 28501)?);
264 assert_ne!("quake.se:28502", HostPort::new("quake.se", 28501)?);
265 Ok(())
266 }
267}