use alloc::string::{String, ToString};
use core::net::Ipv6Addr;
use core::str::FromStr;
use super::ConfigError;
pub fn parse_host_port(s: &str, default_port: u16) -> Result<(String, u16), ConfigError> {
if let Some(rest) = s.strip_prefix('[') {
let close = rest.find(']').ok_or_else(|| ConfigError::BadValue {
line: 0,
keyword: "host_port".to_string(),
msg: alloc::format!("missing `]` in bracketed host {s:?}"),
})?;
let host = &rest[..close];
let after = &rest[close + 1..];
if Ipv6Addr::from_str(host).is_err() {
return Err(ConfigError::BadValue {
line: 0,
keyword: "host_port".to_string(),
msg: alloc::format!(
"brackets are only valid around IPv6 literals; got {host:?} in {s:?}"
),
});
}
let port = if after.is_empty() {
default_port
} else {
let port_str = after
.strip_prefix(':')
.ok_or_else(|| ConfigError::BadValue {
line: 0,
keyword: "host_port".to_string(),
msg: alloc::format!("expected `:port` after `]` in {s:?}"),
})?;
port_str.parse::<u16>().map_err(|_| ConfigError::BadValue {
line: 0,
keyword: "host_port".to_string(),
msg: alloc::format!("bad port {port_str:?} in {s:?}"),
})?
};
return Ok((host.to_string(), port));
}
if Ipv6Addr::from_str(s).is_ok() {
return Ok((s.to_string(), default_port));
}
if let Some((host, port)) = s.rsplit_once(':') {
let port = port.parse::<u16>().map_err(|_| ConfigError::BadValue {
line: 0,
keyword: "host_port".to_string(),
msg: alloc::format!("bad port {port:?} in {s:?}"),
})?;
if host.is_empty() {
return Err(ConfigError::BadValue {
line: 0,
keyword: "host_port".to_string(),
msg: alloc::format!("empty host in {s:?}"),
});
}
return Ok((host.to_string(), port));
}
if s.is_empty() {
return Err(ConfigError::BadValue {
line: 0,
keyword: "host_port".to_string(),
msg: "empty host".to_string(),
});
}
Ok((s.to_string(), default_port))
}
pub fn parse_host_port_pattern(s: &str, default_port: u16) -> Result<(String, u16), ConfigError> {
if let Some(rest) = s.strip_prefix('[') {
let close = rest.find(']').ok_or_else(|| ConfigError::BadValue {
line: 0,
keyword: "host_port".to_string(),
msg: alloc::format!("missing `]` in bracketed host {s:?}"),
})?;
let host = &rest[..close];
let after = &rest[close + 1..];
let port = if after.is_empty() {
default_port
} else {
let port_str = after
.strip_prefix(':')
.ok_or_else(|| ConfigError::BadValue {
line: 0,
keyword: "host_port".to_string(),
msg: alloc::format!("expected `:port` after `]` in {s:?}"),
})?;
port_str.parse::<u16>().map_err(|_| ConfigError::BadValue {
line: 0,
keyword: "host_port".to_string(),
msg: alloc::format!("bad port {port_str:?} in {s:?}"),
})?
};
return Ok((host.to_string(), port));
}
Ok((s.to_string(), default_port))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_v4_with_port() {
let (h, p) = parse_host_port("192.0.2.1:22", 22).unwrap();
assert_eq!(h, "192.0.2.1");
assert_eq!(p, 22);
}
#[test]
fn parse_hostname_with_port() {
let (h, p) = parse_host_port("example.com:2222", 22).unwrap();
assert_eq!(h, "example.com");
assert_eq!(p, 2222);
}
#[test]
fn parse_hostname_no_port_uses_default() {
let (h, p) = parse_host_port("example.com", 22).unwrap();
assert_eq!(h, "example.com");
assert_eq!(p, 22);
let (h, p) = parse_host_port("example.com", 2200).unwrap();
assert_eq!(h, "example.com");
assert_eq!(p, 2200);
}
#[test]
fn parse_v4_no_port_uses_default() {
let (h, p) = parse_host_port("192.0.2.1", 22).unwrap();
assert_eq!(h, "192.0.2.1");
assert_eq!(p, 22);
}
#[test]
fn parse_v6_bracketed_with_port() {
let (h, p) = parse_host_port("[2001:db8::1]:2222", 22).unwrap();
assert_eq!(h, "2001:db8::1");
assert_eq!(p, 2222);
}
#[test]
fn parse_v6_bracketed_no_port() {
let (h, p) = parse_host_port("[2001:db8::1]", 22).unwrap();
assert_eq!(h, "2001:db8::1");
assert_eq!(p, 22);
}
#[test]
fn parse_v6_bare_uses_default() {
let (h, p) = parse_host_port("2001:db8::1", 22).unwrap();
assert_eq!(h, "2001:db8::1");
assert_eq!(p, 22);
}
#[test]
fn parse_v6_loopback_bare() {
let (h, p) = parse_host_port("::1", 22).unwrap();
assert_eq!(h, "::1");
assert_eq!(p, 22);
}
#[test]
fn parse_v6_full_form_bracketed() {
let (h, p) = parse_host_port("[2001:0db8:0000:0000:0000:0000:0000:0001]:2222", 22).unwrap();
assert_eq!(h, "2001:0db8:0000:0000:0000:0000:0000:0001");
assert_eq!(p, 2222);
}
#[test]
fn reject_missing_close_bracket() {
assert!(parse_host_port("[2001:db8::1", 22).is_err());
assert!(parse_host_port("[2001:db8::1:22", 22).is_err());
}
#[test]
fn reject_bracketed_v4() {
assert!(parse_host_port("[192.0.2.1]:22", 22).is_err());
assert!(parse_host_port("[192.0.2.1]", 22).is_err());
}
#[test]
fn reject_bracketed_hostname() {
assert!(parse_host_port("[example.com]:22", 22).is_err());
assert!(parse_host_port("[example.com]", 22).is_err());
}
#[test]
fn reject_bad_port() {
assert!(parse_host_port("host:not-a-port", 22).is_err());
assert!(parse_host_port("host:", 22).is_err());
assert!(parse_host_port("[2001:db8::1]:not-a-port", 22).is_err());
assert!(parse_host_port("[2001:db8::1]:", 22).is_err());
}
#[test]
fn reject_empty() {
assert!(parse_host_port("", 22).is_err());
}
#[test]
fn reject_garbage_after_close_bracket() {
assert!(parse_host_port("[2001:db8::1]garbage", 22).is_err());
}
#[test]
fn reject_empty_host_with_port() {
assert!(parse_host_port(":22", 22).is_err());
}
#[test]
fn pattern_plain_host_uses_default_port() {
let (h, p) = parse_host_port_pattern("example.com", 22).unwrap();
assert_eq!(h, "example.com");
assert_eq!(p, 22);
}
#[test]
fn pattern_bracketed_hostname_with_port() {
let (h, p) = parse_host_port_pattern("[example.com]:2222", 22).unwrap();
assert_eq!(h, "example.com");
assert_eq!(p, 2222);
}
#[test]
fn pattern_bracketed_v4_with_port() {
let (h, p) = parse_host_port_pattern("[192.0.2.1]:2222", 22).unwrap();
assert_eq!(h, "192.0.2.1");
assert_eq!(p, 2222);
}
#[test]
fn pattern_bracketed_v6_with_port() {
let (h, p) = parse_host_port_pattern("[2001:db8::1]:2222", 22).unwrap();
assert_eq!(h, "2001:db8::1");
assert_eq!(p, 2222);
}
#[test]
fn pattern_bracketed_no_port_defaults() {
let (h, p) = parse_host_port_pattern("[example.com]", 22).unwrap();
assert_eq!(h, "example.com");
assert_eq!(p, 22);
}
#[test]
fn pattern_bare_host_with_colon_taken_verbatim() {
let (h, p) = parse_host_port_pattern("example.com:2222", 22).unwrap();
assert_eq!(h, "example.com:2222");
assert_eq!(p, 22);
}
#[test]
fn pattern_bare_v6_uses_default_port() {
let (h, p) = parse_host_port_pattern("2001:db8::1", 22).unwrap();
assert_eq!(h, "2001:db8::1");
assert_eq!(p, 22);
}
#[test]
fn pattern_reject_missing_close_bracket() {
assert!(parse_host_port_pattern("[host", 22).is_err());
}
#[test]
fn pattern_reject_bad_port() {
assert!(parse_host_port_pattern("[host]:abc", 22).is_err());
assert!(parse_host_port_pattern("[host]:", 22).is_err());
}
#[test]
fn pattern_reject_garbage_after_close_bracket() {
assert!(parse_host_port_pattern("[host]xyz", 22).is_err());
}
}