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