use crate::error::{MotorcortexError, Result};
const DEFAULT_REQ_PATH: &str = "/mcx_req";
const DEFAULT_SUB_PATH: &str = "/mcx_sub";
pub fn parse_url(s: &str) -> Result<(String, String)> {
let scheme_end = s
.find("://")
.ok_or_else(|| invalid("missing `scheme://` prefix"))?;
let scheme_prefix = &s[..scheme_end + 3]; let after = &s[scheme_end + 3..];
let (host, port_tail) = if let Some(rest) = after.strip_prefix('[') {
let close = rest
.find(']')
.ok_or_else(|| invalid("IPv6 host missing closing `]`"))?;
let host = &after[..=close + 1]; let tail = &rest[close + 1..]; (host, tail)
} else {
match after.find(':') {
Some(i) => (&after[..i], &after[i..]),
None => (after, ""),
}
};
if port_tail.is_empty() {
return Ok((
format!("{scheme_prefix}{host}{DEFAULT_REQ_PATH}"),
format!("{scheme_prefix}{host}{DEFAULT_SUB_PATH}"),
));
}
let ports = port_tail
.strip_prefix(':')
.ok_or_else(|| invalid("expected `:` before ports"))?;
let colon = ports
.find(':')
.ok_or_else(|| invalid("need two ports separated by `:`"))?;
let req_port = &ports[..colon];
let sub_port = &ports[colon + 1..];
req_port
.parse::<u16>()
.map_err(|_| invalid(&format!("req_port {req_port:?} is not a valid u16")))?;
sub_port
.parse::<u16>()
.map_err(|_| invalid(&format!("sub_port {sub_port:?} is not a valid u16")))?;
Ok((
format!("{scheme_prefix}{host}:{req_port}"),
format!("{scheme_prefix}{host}:{sub_port}"),
))
}
fn invalid(msg: &str) -> MotorcortexError {
MotorcortexError::Connection(format!("parse_url: {msg}"))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn hostname_with_both_ports() {
let (r, s) = parse_url("wss://host:5568:5567").unwrap();
assert_eq!(r, "wss://host:5568");
assert_eq!(s, "wss://host:5567");
}
#[test]
fn ipv4_with_both_ports() {
let (r, s) = parse_url("wss://192.168.1.10:5568:5567").unwrap();
assert_eq!(r, "wss://192.168.1.10:5568");
assert_eq!(s, "wss://192.168.1.10:5567");
}
#[test]
fn ipv6_loopback_with_both_ports() {
let (r, s) = parse_url("wss://[::1]:5568:5567").unwrap();
assert_eq!(r, "wss://[::1]:5568");
assert_eq!(s, "wss://[::1]:5567");
}
#[test]
fn ipv6_full_address() {
let (r, s) =
parse_url("wss://[2001:db8::8a2e:370:7334]:5568:5567").unwrap();
assert_eq!(r, "wss://[2001:db8::8a2e:370:7334]:5568");
assert_eq!(s, "wss://[2001:db8::8a2e:370:7334]:5567");
}
#[test]
fn tcp_scheme() {
let (r, s) = parse_url("tcp://host:5568:5567").unwrap();
assert_eq!(r, "tcp://host:5568");
assert_eq!(s, "tcp://host:5567");
}
#[test]
fn missing_scheme_errors() {
let err = parse_url("host:5568:5567").unwrap_err();
assert!(matches!(err, MotorcortexError::Connection(ref m) if m.contains("scheme")));
}
#[test]
fn hostname_no_ports_uses_default_paths() {
let (r, s) = parse_url("wss://host").unwrap();
assert_eq!(r, "wss://host/mcx_req");
assert_eq!(s, "wss://host/mcx_sub");
}
#[test]
fn ipv4_no_ports_uses_default_paths() {
let (r, s) = parse_url("wss://192.168.2.100").unwrap();
assert_eq!(r, "wss://192.168.2.100/mcx_req");
assert_eq!(s, "wss://192.168.2.100/mcx_sub");
}
#[test]
fn ipv6_no_ports_uses_default_paths() {
let (r, s) = parse_url("wss://[::1]").unwrap();
assert_eq!(r, "wss://[::1]/mcx_req");
assert_eq!(s, "wss://[::1]/mcx_sub");
}
#[test]
fn ipv6_link_local_no_ports_uses_default_paths() {
let (r, _) = parse_url("wss://[fe80::1]").unwrap();
assert_eq!(r, "wss://[fe80::1]/mcx_req");
}
#[test]
fn single_port_errors() {
let err = parse_url("wss://host:5568").unwrap_err();
assert!(matches!(err, MotorcortexError::Connection(ref m) if m.contains("two ports")));
}
#[test]
fn non_numeric_port_errors() {
let err = parse_url("wss://host:5568:abc").unwrap_err();
assert!(matches!(err, MotorcortexError::Connection(ref m) if m.contains("sub_port")));
}
#[test]
fn ipv6_unclosed_bracket_errors() {
let err = parse_url("wss://[::1:5568:5567").unwrap_err();
assert!(matches!(err, MotorcortexError::Connection(ref m) if m.contains("closing")));
}
#[test]
fn ipv6_missing_colon_after_bracket_errors() {
let err = parse_url("wss://[::1]5568:5567").unwrap_err();
assert!(matches!(err, MotorcortexError::Connection(ref m) if m.contains(":")));
}
#[test]
fn port_above_u16_max_errors() {
let err = parse_url("wss://host:70000:5567").unwrap_err();
assert!(matches!(err, MotorcortexError::Connection(ref m) if m.contains("req_port")));
}
}