aimdb_core/session/
endpoint.rs1use alloc::string::{String, ToString};
13
14#[derive(Debug, Clone, Copy, PartialEq, Eq)]
16pub enum EndpointError {
17 EmptyHost,
19 UnclosedBracket,
21 BadPort,
23}
24
25impl core::fmt::Display for EndpointError {
26 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
27 f.write_str(match self {
28 Self::EmptyHost => "endpoint names no host",
29 Self::UnclosedBracket => "missing closing bracket for IPv6 host",
30 Self::BadPort => "port is not a number in 0..=65535",
31 })
32 }
33}
34
35pub fn split_host_port_opt(endpoint: &str) -> Result<(String, Option<u16>), EndpointError> {
46 if let Some(rest) = endpoint.strip_prefix('[') {
47 let Some((host, tail)) = rest.split_once(']') else {
48 return Err(EndpointError::UnclosedBracket);
49 };
50 if host.is_empty() {
51 return Err(EndpointError::EmptyHost);
52 }
53 let port = if tail.is_empty() {
54 None
55 } else {
56 Some(
59 tail.strip_prefix(':')
60 .ok_or(EndpointError::BadPort)?
61 .parse()
62 .map_err(|_| EndpointError::BadPort)?,
63 )
64 };
65 return Ok((host.to_string(), port));
66 }
67 match endpoint.rsplit_once(':') {
68 Some((head, _)) if head.contains(':') => Ok((endpoint.to_string(), None)),
71 Some(("", _)) => Err(EndpointError::EmptyHost),
72 Some((host, port)) => Ok((
73 host.to_string(),
74 Some(port.parse().map_err(|_| EndpointError::BadPort)?),
75 )),
76 None if endpoint.is_empty() => Err(EndpointError::EmptyHost),
77 None => Ok((endpoint.to_string(), None)),
78 }
79}
80
81pub fn split_host_port(endpoint: &str, default_port: u16) -> Result<(String, u16), EndpointError> {
84 let (host, port) = split_host_port_opt(endpoint)?;
85 Ok((host, port.unwrap_or(default_port)))
86}
87
88#[cfg(test)]
89mod tests {
90 use super::*;
91
92 const DEFAULT: u16 = 7001;
93
94 fn split(endpoint: &str) -> Result<(String, u16), EndpointError> {
95 split_host_port(endpoint, DEFAULT)
96 }
97
98 #[test]
99 fn splits_host_and_port() {
100 assert_eq!(split("127.0.0.1:7002"), Ok(("127.0.0.1".into(), 7002)));
101 }
102
103 #[test]
104 fn a_bare_host_takes_the_default_port() {
105 assert_eq!(split("example.test"), Ok(("example.test".into(), DEFAULT)));
106 assert_eq!(
107 split_host_port_opt("example.test"),
108 Ok(("example.test".into(), None)),
109 "the caller can tell a defaulted port from a written one"
110 );
111 }
112
113 #[test]
117 fn an_unparsable_port_is_rejected() {
118 assert_eq!(split("host:not-a-port"), Err(EndpointError::BadPort));
119 assert_eq!(split("10.0.0.5:8080x"), Err(EndpointError::BadPort));
120 assert_eq!(split("host:99999"), Err(EndpointError::BadPort));
121 assert_eq!(split("host:"), Err(EndpointError::BadPort));
122 }
123
124 #[test]
127 fn brackets_are_stripped_from_an_ipv6_literal() {
128 assert_eq!(split("[::1]:7003"), Ok(("::1".into(), 7003)));
129 }
130
131 #[test]
132 fn a_bracketed_ipv6_host_without_a_port_is_not_mangled() {
133 assert_eq!(split("[::1]"), Ok(("::1".into(), DEFAULT)));
134 }
135
136 #[test]
139 fn an_unbracketed_ipv6_literal_keeps_all_its_colons() {
140 assert_eq!(split("::1"), Ok(("::1".into(), DEFAULT)));
141 assert_eq!(split("fe80::1"), Ok(("fe80::1".into(), DEFAULT)));
142 assert_eq!(
143 split("2001:db8::dead:beef"),
144 Ok(("2001:db8::dead:beef".into(), DEFAULT)),
145 "the trailing group is address, not a port"
146 );
147 }
148
149 #[test]
150 fn a_malformed_endpoint_is_rejected() {
151 assert_eq!(split(":99999"), Err(EndpointError::EmptyHost));
152 assert_eq!(split(""), Err(EndpointError::EmptyHost));
153 assert_eq!(split("[]:7001"), Err(EndpointError::EmptyHost));
154 assert_eq!(split("[::1"), Err(EndpointError::UnclosedBracket));
155 assert_eq!(
156 split("[::1]oops:7003"),
157 Err(EndpointError::BadPort),
158 "an explicit port behind junk is not silently honoured"
159 );
160 }
161}