1use std::fmt;
11use std::net::IpAddr;
12
13use serde::de::{self, Deserializer, Visitor};
14use serde::{Deserialize, Serialize};
15
16use super::error::ConfError;
17
18#[derive(Debug, Clone, Eq, PartialEq)]
32pub struct ConfListen {
33 pname: String,
34 name: String,
35 port: u16,
36 kind: EndpointKind,
37}
38
39#[derive(Debug, Clone, Copy, Eq, PartialEq)]
55pub enum EndpointKind {
56 V4,
58 V6,
60 Hostname,
62 UnixPath,
64}
65
66impl ConfListen {
67 pub fn parse(field: &'static str, raw: &str) -> Result<Self, ConfError> {
82 if raw.is_empty() {
83 return Err(ConfError::BadAddr {
84 field,
85 value: raw.to_string(),
86 reason: "empty value".to_string(),
87 });
88 }
89 if raw.starts_with('/') {
90 return Ok(Self {
91 pname: raw.to_string(),
92 name: raw.to_string(),
93 port: 0,
94 kind: EndpointKind::UnixPath,
95 });
96 }
97
98 let (host, port_str) = split_host_port(raw).ok_or_else(|| ConfError::BadAddr {
99 field,
100 value: raw.to_string(),
101 reason: "missing 'host:port' separator".to_string(),
102 })?;
103
104 let port: u16 = match port_str.parse::<u16>() {
105 Ok(p) if p > 0 => p,
106 Ok(_) | Err(_) => {
107 return Err(ConfError::BadAddr {
108 field,
109 value: raw.to_string(),
110 reason: "port must be a number in 1..=65535".to_string(),
111 });
112 }
113 };
114
115 let kind = classify_host(host).ok_or_else(|| ConfError::BadAddr {
116 field,
117 value: raw.to_string(),
118 reason: "host portion is empty or malformed".to_string(),
119 })?;
120
121 Ok(Self {
122 pname: raw.to_string(),
123 name: host.to_string(),
124 port,
125 kind,
126 })
127 }
128
129 pub fn pname(&self) -> &str {
139 &self.pname
140 }
141
142 pub fn name(&self) -> &str {
152 &self.name
153 }
154
155 pub fn port(&self) -> u16 {
165 self.port
166 }
167
168 pub fn kind(&self) -> EndpointKind {
178 self.kind
179 }
180
181 pub(crate) fn from_socket_addr(addr: std::net::SocketAddr) -> Self {
190 let (host, kind) = match addr {
191 std::net::SocketAddr::V4(v4) => (v4.ip().to_string(), EndpointKind::V4),
192 std::net::SocketAddr::V6(v6) => (v6.ip().to_string(), EndpointKind::V6),
193 };
194 let pname = match addr {
195 std::net::SocketAddr::V4(_) => format!("{host}:{}", addr.port()),
196 std::net::SocketAddr::V6(_) => format!("[{host}]:{}", addr.port()),
197 };
198 Self {
199 pname,
200 name: host,
201 port: addr.port(),
202 kind,
203 }
204 }
205}
206
207impl fmt::Display for ConfListen {
208 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
209 f.write_str(&self.pname)
210 }
211}
212
213impl Serialize for ConfListen {
214 fn serialize<S: serde::Serializer>(&self, ser: S) -> Result<S::Ok, S::Error> {
215 ser.serialize_str(&self.pname)
216 }
217}
218
219impl<'de> Deserialize<'de> for ConfListen {
220 fn deserialize<D: Deserializer<'de>>(de: D) -> Result<Self, D::Error> {
221 struct V;
222 impl Visitor<'_> for V {
223 type Value = ConfListen;
224 fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
225 f.write_str("a 'host:port' or '[ipv6]:port' endpoint string")
226 }
227 fn visit_str<E: de::Error>(self, v: &str) -> Result<Self::Value, E> {
228 ConfListen::parse("listen", v).map_err(|e| E::custom(e.to_string()))
229 }
230 }
231 de.deserialize_str(V)
232 }
233}
234
235fn split_host_port(raw: &str) -> Option<(&str, &str)> {
240 if let Some(rest) = raw.strip_prefix('[') {
241 let close = rest.find(']')?;
242 let host = &rest[..close];
243 let after = &rest[close + 1..];
244 let port = after.strip_prefix(':')?;
245 if host.is_empty() || port.is_empty() {
246 return None;
247 }
248 return Some((host, port));
249 }
250
251 let idx = raw.rfind(':')?;
252 let (host, port) = raw.split_at(idx);
253 let port = &port[1..];
254 if host.is_empty() || port.is_empty() {
255 return None;
256 }
257 Some((host, port))
258}
259
260fn classify_host(host: &str) -> Option<EndpointKind> {
261 if host.is_empty() {
262 return None;
263 }
264 if let Ok(ip) = host.parse::<IpAddr>() {
265 return Some(match ip {
266 IpAddr::V4(_) => EndpointKind::V4,
267 IpAddr::V6(_) => EndpointKind::V6,
268 });
269 }
270 if host
271 .bytes()
272 .all(|b| b.is_ascii_alphanumeric() || b == b'-' || b == b'.' || b == b'_')
273 {
274 Some(EndpointKind::Hostname)
275 } else {
276 None
277 }
278}
279
280#[cfg(test)]
281mod tests {
282 use super::*;
283
284 #[test]
285 fn ipv4_host_port() {
286 let l = ConfListen::parse("listen", "127.0.0.1:8102").unwrap();
287 assert_eq!(l.name(), "127.0.0.1");
288 assert_eq!(l.port(), 8102);
289 assert_eq!(l.kind(), EndpointKind::V4);
290 assert_eq!(l.to_string(), "127.0.0.1:8102");
291 }
292
293 #[test]
294 fn ipv6_bracketed() {
295 let l = ConfListen::parse("listen", "[::1]:8101").unwrap();
296 assert_eq!(l.name(), "::1");
297 assert_eq!(l.port(), 8101);
298 assert_eq!(l.kind(), EndpointKind::V6);
299 }
300
301 #[test]
302 fn hostname_accepted() {
303 let l = ConfListen::parse("listen", "node-1.example.com:22222").unwrap();
304 assert_eq!(l.name(), "node-1.example.com");
305 assert_eq!(l.port(), 22222);
306 assert_eq!(l.kind(), EndpointKind::Hostname);
307 }
308
309 #[test]
310 fn unix_path_accepted() {
311 let l = ConfListen::parse("listen", "/tmp/dynomite.sock").unwrap();
312 assert_eq!(l.kind(), EndpointKind::UnixPath);
313 assert_eq!(l.port(), 0);
314 }
315
316 #[test]
317 fn missing_port_rejected() {
318 assert!(ConfListen::parse("listen", "127.0.0.1").is_err());
319 assert!(ConfListen::parse("listen", "127.0.0.1:").is_err());
320 }
321
322 #[test]
323 fn out_of_range_port_rejected() {
324 assert!(ConfListen::parse("listen", "127.0.0.1:0").is_err());
325 assert!(ConfListen::parse("listen", "127.0.0.1:99999").is_err());
326 }
327
328 #[test]
329 fn malformed_ipv6_rejected() {
330 assert!(ConfListen::parse("listen", "[::1:8101").is_err());
331 assert!(ConfListen::parse("listen", "[]:8101").is_err());
332 }
333}