use alloc::string::{String, ToString};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum EndpointError {
EmptyHost,
UnclosedBracket,
BadPort,
}
impl core::fmt::Display for EndpointError {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.write_str(match self {
Self::EmptyHost => "endpoint names no host",
Self::UnclosedBracket => "missing closing bracket for IPv6 host",
Self::BadPort => "port is not a number in 0..=65535",
})
}
}
pub fn split_host_port_opt(endpoint: &str) -> Result<(String, Option<u16>), EndpointError> {
if let Some(rest) = endpoint.strip_prefix('[') {
let Some((host, tail)) = rest.split_once(']') else {
return Err(EndpointError::UnclosedBracket);
};
if host.is_empty() {
return Err(EndpointError::EmptyHost);
}
let port = if tail.is_empty() {
None
} else {
Some(
tail.strip_prefix(':')
.ok_or(EndpointError::BadPort)?
.parse()
.map_err(|_| EndpointError::BadPort)?,
)
};
return Ok((host.to_string(), port));
}
match endpoint.rsplit_once(':') {
Some((head, _)) if head.contains(':') => Ok((endpoint.to_string(), None)),
Some(("", _)) => Err(EndpointError::EmptyHost),
Some((host, port)) => Ok((
host.to_string(),
Some(port.parse().map_err(|_| EndpointError::BadPort)?),
)),
None if endpoint.is_empty() => Err(EndpointError::EmptyHost),
None => Ok((endpoint.to_string(), None)),
}
}
pub fn split_host_port(endpoint: &str, default_port: u16) -> Result<(String, u16), EndpointError> {
let (host, port) = split_host_port_opt(endpoint)?;
Ok((host, port.unwrap_or(default_port)))
}
#[cfg(test)]
mod tests {
use super::*;
const DEFAULT: u16 = 7001;
fn split(endpoint: &str) -> Result<(String, u16), EndpointError> {
split_host_port(endpoint, DEFAULT)
}
#[test]
fn splits_host_and_port() {
assert_eq!(split("127.0.0.1:7002"), Ok(("127.0.0.1".into(), 7002)));
}
#[test]
fn a_bare_host_takes_the_default_port() {
assert_eq!(split("example.test"), Ok(("example.test".into(), DEFAULT)));
assert_eq!(
split_host_port_opt("example.test"),
Ok(("example.test".into(), None)),
"the caller can tell a defaulted port from a written one"
);
}
#[test]
fn an_unparsable_port_is_rejected() {
assert_eq!(split("host:not-a-port"), Err(EndpointError::BadPort));
assert_eq!(split("10.0.0.5:8080x"), Err(EndpointError::BadPort));
assert_eq!(split("host:99999"), Err(EndpointError::BadPort));
assert_eq!(split("host:"), Err(EndpointError::BadPort));
}
#[test]
fn brackets_are_stripped_from_an_ipv6_literal() {
assert_eq!(split("[::1]:7003"), Ok(("::1".into(), 7003)));
}
#[test]
fn a_bracketed_ipv6_host_without_a_port_is_not_mangled() {
assert_eq!(split("[::1]"), Ok(("::1".into(), DEFAULT)));
}
#[test]
fn an_unbracketed_ipv6_literal_keeps_all_its_colons() {
assert_eq!(split("::1"), Ok(("::1".into(), DEFAULT)));
assert_eq!(split("fe80::1"), Ok(("fe80::1".into(), DEFAULT)));
assert_eq!(
split("2001:db8::dead:beef"),
Ok(("2001:db8::dead:beef".into(), DEFAULT)),
"the trailing group is address, not a port"
);
}
#[test]
fn a_malformed_endpoint_is_rejected() {
assert_eq!(split(":99999"), Err(EndpointError::EmptyHost));
assert_eq!(split(""), Err(EndpointError::EmptyHost));
assert_eq!(split("[]:7001"), Err(EndpointError::EmptyHost));
assert_eq!(split("[::1"), Err(EndpointError::UnclosedBracket));
assert_eq!(
split("[::1]oops:7003"),
Err(EndpointError::BadPort),
"an explicit port behind junk is not silently honoured"
);
}
}