hostport/
hostport.rs

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/// Represents a host and port combination.
11#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
12#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
13pub struct HostPort {
14    /// Hostname, network alias, or IP address.
15    host: String,
16
17    /// Port number.
18    port: u16,
19}
20
21impl HostPort {
22    /// Creates a new `HostPort` instance.
23    ///
24    /// # Examples
25    /// ```
26    /// use hostport::HostPort;
27    ///
28    /// let hostport = HostPort::new("localhost", 8080).unwrap();
29    /// assert_eq!(hostport.host(), "localhost");
30    /// assert_eq!(hostport.port(), 8080);
31    /// assert_eq!(hostport.to_string(), "localhost:8080");
32    /// ```
33    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    /// Returns the host part of the `HostPort`.
42    #[must_use]
43    pub fn host(&self) -> &str {
44        &self.host
45    }
46
47    /// Returns the port part of the `HostPort`.
48    #[must_use]
49    pub fn port(&self) -> u16 {
50        self.port
51    }
52}
53
54/// Implements the `From` trait for converting a `HostPort` to a string.
55///
56/// # Examples
57/// ```
58/// use hostport::HostPort;
59///
60/// let domain = HostPort::try_from("quake.se:28000").unwrap();
61/// assert_eq!(domain.host(), "quake.se");
62/// assert_eq!(domain.port(), 28000);
63///
64/// let ip = HostPort::try_from("10.10.10.10:28000").unwrap();
65/// assert_eq!(ip.host(), "10.10.10.10");
66/// assert_eq!(ip.port(), 28000);
67///
68/// let network_alias = HostPort::try_from("localhost:28000").unwrap();
69/// assert_eq!(network_alias.host(), "localhost");
70/// assert_eq!(network_alias.port(), 28000);
71/// ```
72impl 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/// Errors that can occur while parsing a [`HostPort`].
131///
132/// # Variants
133/// - `InvalidFormat`: The input string does not follow the `host:port` format.
134/// - `InvalidHost`: The host part of the input is invalid.
135/// - `InvalidPort`: The port part of the input is invalid.
136#[derive(Debug, Error, Eq, PartialEq)]
137pub enum ParseError {
138    /// The input string does not follow the `host:port` format.
139    #[error("Invalid format, expected host:port")]
140    InvalidFormat,
141
142    /// The host part of the input is invalid.
143    #[error("Invalid host: {0}")]
144    InvalidHost(String),
145
146    /// The port part of the input is invalid.
147    #[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            // Generate valid hostnames
163            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            // Generate invalid inputs for negative testing
166            invalid_str in r"[^:]*",
167            invalid_port in r"[a-zA-Z]{1,10}"
168        ) {
169            // Skip valid hosts
170            if !is_valid_host(&host) {
171                return Ok(());
172            }
173
174            // Valid case: host:port should parse correctly
175            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            // Invalid cases
184
185            // Case 1: No colon separator
186            let result = HostPort::try_from(invalid_str.as_str());
187            prop_assert_eq!(result.err(), Some(ParseError::InvalidFormat));
188
189            // Case 2: Invalid port (non-numeric)
190            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            // Case 3: Invalid host with valid port
198            let invalid_host = format!("{}$", host); // Add invalid character
199            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            // Case 4: Empty string before colon
205            let input = format!(":{}", port);
206            let result = HostPort::try_from(input.as_str());
207            prop_assert!(result.is_err());
208
209            // Case 5: Nothing after colon
210            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}