1use crate::error::{MotorcortexError, Result};
15
16const DEFAULT_REQ_PATH: &str = "/mcx_req";
19const DEFAULT_SUB_PATH: &str = "/mcx_sub";
20
21pub fn parse_url(s: &str) -> Result<(String, String)> {
46 let scheme_end = s
47 .find("://")
48 .ok_or_else(|| invalid("missing `scheme://` prefix"))?;
49 let scheme_prefix = &s[..scheme_end + 3]; let after = &s[scheme_end + 3..];
51
52 let (host, port_tail) = if let Some(rest) = after.strip_prefix('[') {
56 let close = rest
57 .find(']')
58 .ok_or_else(|| invalid("IPv6 host missing closing `]`"))?;
59 let host = &after[..=close + 1]; let tail = &rest[close + 1..]; (host, tail)
62 } else {
63 match after.find(':') {
64 Some(i) => (&after[..i], &after[i..]),
65 None => (after, ""),
66 }
67 };
68
69 if port_tail.is_empty() {
70 return Ok((
71 format!("{scheme_prefix}{host}{DEFAULT_REQ_PATH}"),
72 format!("{scheme_prefix}{host}{DEFAULT_SUB_PATH}"),
73 ));
74 }
75
76 let ports = port_tail
77 .strip_prefix(':')
78 .ok_or_else(|| invalid("expected `:` before ports"))?;
79 let colon = ports
80 .find(':')
81 .ok_or_else(|| invalid("need two ports separated by `:`"))?;
82 let req_port = &ports[..colon];
83 let sub_port = &ports[colon + 1..];
84
85 req_port
86 .parse::<u16>()
87 .map_err(|_| invalid(&format!("req_port {req_port:?} is not a valid u16")))?;
88 sub_port
89 .parse::<u16>()
90 .map_err(|_| invalid(&format!("sub_port {sub_port:?} is not a valid u16")))?;
91
92 Ok((
93 format!("{scheme_prefix}{host}:{req_port}"),
94 format!("{scheme_prefix}{host}:{sub_port}"),
95 ))
96}
97
98fn invalid(msg: &str) -> MotorcortexError {
99 MotorcortexError::Connection(format!("parse_url: {msg}"))
100}
101
102#[cfg(test)]
103mod tests {
104 use super::*;
105
106 #[test]
107 fn hostname_with_both_ports() {
108 let (r, s) = parse_url("wss://host:5568:5567").unwrap();
109 assert_eq!(r, "wss://host:5568");
110 assert_eq!(s, "wss://host:5567");
111 }
112
113 #[test]
114 fn ipv4_with_both_ports() {
115 let (r, s) = parse_url("wss://192.168.1.10:5568:5567").unwrap();
116 assert_eq!(r, "wss://192.168.1.10:5568");
117 assert_eq!(s, "wss://192.168.1.10:5567");
118 }
119
120 #[test]
121 fn ipv6_loopback_with_both_ports() {
122 let (r, s) = parse_url("wss://[::1]:5568:5567").unwrap();
123 assert_eq!(r, "wss://[::1]:5568");
124 assert_eq!(s, "wss://[::1]:5567");
125 }
126
127 #[test]
128 fn ipv6_full_address() {
129 let (r, s) =
130 parse_url("wss://[2001:db8::8a2e:370:7334]:5568:5567").unwrap();
131 assert_eq!(r, "wss://[2001:db8::8a2e:370:7334]:5568");
132 assert_eq!(s, "wss://[2001:db8::8a2e:370:7334]:5567");
133 }
134
135 #[test]
136 fn tcp_scheme() {
137 let (r, s) = parse_url("tcp://host:5568:5567").unwrap();
138 assert_eq!(r, "tcp://host:5568");
139 assert_eq!(s, "tcp://host:5567");
140 }
141
142 #[test]
143 fn missing_scheme_errors() {
144 let err = parse_url("host:5568:5567").unwrap_err();
145 assert!(matches!(err, MotorcortexError::Connection(ref m) if m.contains("scheme")));
146 }
147
148 #[test]
149 fn hostname_no_ports_uses_default_paths() {
150 let (r, s) = parse_url("wss://host").unwrap();
151 assert_eq!(r, "wss://host/mcx_req");
152 assert_eq!(s, "wss://host/mcx_sub");
153 }
154
155 #[test]
156 fn ipv4_no_ports_uses_default_paths() {
157 let (r, s) = parse_url("wss://192.168.2.100").unwrap();
158 assert_eq!(r, "wss://192.168.2.100/mcx_req");
159 assert_eq!(s, "wss://192.168.2.100/mcx_sub");
160 }
161
162 #[test]
163 fn ipv6_no_ports_uses_default_paths() {
164 let (r, s) = parse_url("wss://[::1]").unwrap();
165 assert_eq!(r, "wss://[::1]/mcx_req");
166 assert_eq!(s, "wss://[::1]/mcx_sub");
167 }
168
169 #[test]
170 fn ipv6_link_local_no_ports_uses_default_paths() {
171 let (r, _) = parse_url("wss://[fe80::1]").unwrap();
172 assert_eq!(r, "wss://[fe80::1]/mcx_req");
173 }
174
175 #[test]
176 fn single_port_errors() {
177 let err = parse_url("wss://host:5568").unwrap_err();
179 assert!(matches!(err, MotorcortexError::Connection(ref m) if m.contains("two ports")));
180 }
181
182 #[test]
183 fn non_numeric_port_errors() {
184 let err = parse_url("wss://host:5568:abc").unwrap_err();
185 assert!(matches!(err, MotorcortexError::Connection(ref m) if m.contains("sub_port")));
186 }
187
188 #[test]
189 fn ipv6_unclosed_bracket_errors() {
190 let err = parse_url("wss://[::1:5568:5567").unwrap_err();
191 assert!(matches!(err, MotorcortexError::Connection(ref m) if m.contains("closing")));
192 }
193
194 #[test]
195 fn ipv6_missing_colon_after_bracket_errors() {
196 let err = parse_url("wss://[::1]5568:5567").unwrap_err();
198 assert!(matches!(err, MotorcortexError::Connection(ref m) if m.contains(":")));
199 }
200
201 #[test]
202 fn port_above_u16_max_errors() {
203 let err = parse_url("wss://host:70000:5567").unwrap_err();
204 assert!(matches!(err, MotorcortexError::Connection(ref m) if m.contains("req_port")));
205 }
206}