logicaffeine_system/
addr.rs1pub fn multiaddr_to_ws_url(addr: &str) -> Result<String, String> {
27 let trimmed = addr.trim();
28 if trimmed.is_empty() {
29 return Err("address is empty".to_string());
30 }
31
32 let lower = trimmed.to_ascii_lowercase();
33 if lower.starts_with("ws://") || lower.starts_with("wss://") {
34 return Ok(trimmed.to_string());
35 }
36
37 if !trimmed.starts_with('/') {
38 return Err(format!("not a ws:// URL or multiaddr: {trimmed}"));
39 }
40
41 let segs: Vec<&str> = trimmed.split('/').skip(1).collect();
42 let mut host: Option<(String, bool)> = None; let mut port: Option<String> = None;
44 let mut scheme = "ws";
45
46 let mut i = 0;
47 while i < segs.len() {
48 match segs[i] {
49 "ip4" | "dns" | "dns4" | "dns6" | "dnsaddr" => {
50 let v = segs.get(i + 1).filter(|s| !s.is_empty());
51 let v = v.ok_or_else(|| format!("multiaddr '{trimmed}' has no host after /{}/", segs[i]))?;
52 host = Some((v.to_string(), false));
53 i += 2;
54 }
55 "ip6" => {
56 let v = segs
57 .get(i + 1)
58 .filter(|s| !s.is_empty())
59 .ok_or_else(|| format!("multiaddr '{trimmed}' has no host after /ip6/"))?;
60 host = Some((v.to_string(), true));
61 i += 2;
62 }
63 "tcp" => {
64 let v = segs
65 .get(i + 1)
66 .filter(|s| !s.is_empty())
67 .ok_or_else(|| format!("multiaddr '{trimmed}' has no port after /tcp/"))?;
68 if v.parse::<u16>().is_err() {
69 return Err(format!("multiaddr '{trimmed}' has a non-numeric port '{v}'"));
70 }
71 port = Some(v.to_string());
72 i += 2;
73 }
74 "ws" => {
75 scheme = "ws";
76 i += 1;
77 }
78 "wss" | "tls" => {
79 scheme = "wss";
80 i += 1;
81 }
82 "p2p" | "p2p-circuit" => {
83 i += if segs[i] == "p2p" { 2 } else { 1 };
86 }
87 other => {
88 return Err(format!("multiaddr '{trimmed}' has unsupported protocol '/{other}'"));
89 }
90 }
91 }
92
93 let (host, is_v6) = host.ok_or_else(|| format!("multiaddr '{trimmed}' has no host component"))?;
94 let port = port.ok_or_else(|| format!("multiaddr '{trimmed}' has no /tcp/<port>"))?;
95 let host = if is_v6 { format!("[{host}]") } else { host };
96 Ok(format!("{scheme}://{host}:{port}"))
97}
98
99pub fn canonical_topic(addr: &str) -> String {
108 multiaddr_to_ws_url(addr).unwrap_or_else(|_| addr.trim().to_string())
109}
110
111#[cfg(test)]
112mod tests {
113 use super::*;
114
115 #[test]
116 fn canonical_topic_normalizes_multiaddr_and_url_to_same() {
117 assert_eq!(canonical_topic("/ip4/127.0.0.1/tcp/8000"), "ws://127.0.0.1:8000");
118 assert_eq!(canonical_topic("ws://127.0.0.1:8000"), "ws://127.0.0.1:8000");
119 assert_eq!(canonical_topic("/ip4/127.0.0.1/tcp/8000"), canonical_topic("ws://127.0.0.1:8000"));
121 }
122
123 #[test]
124 fn canonical_topic_passes_bare_names_through() {
125 assert_eq!(canonical_topic("alice"), "alice");
126 assert_eq!(canonical_topic(" bob "), "bob");
127 assert_eq!(canonical_topic("game-room-42"), "game-room-42");
128 }
129
130 #[test]
131 fn ip4_tcp_defaults_to_ws() {
132 assert_eq!(multiaddr_to_ws_url("/ip4/127.0.0.1/tcp/9944").unwrap(), "ws://127.0.0.1:9944");
133 }
134
135 #[test]
136 fn ip4_tcp_ws_suffix() {
137 assert_eq!(multiaddr_to_ws_url("/ip4/127.0.0.1/tcp/9944/ws").unwrap(), "ws://127.0.0.1:9944");
138 }
139
140 #[test]
141 fn ip4_tcp_wss_suffix_is_secure() {
142 assert_eq!(multiaddr_to_ws_url("/ip4/10.0.0.5/tcp/443/wss").unwrap(), "wss://10.0.0.5:443");
143 }
144
145 #[test]
146 fn dns4_hostname() {
147 assert_eq!(multiaddr_to_ws_url("/dns4/relay.example.com/tcp/443/wss").unwrap(), "wss://relay.example.com:443");
148 }
149
150 #[test]
151 fn dns_and_dns6_and_dnsaddr_are_hostnames() {
152 assert_eq!(multiaddr_to_ws_url("/dns/example.com/tcp/80/ws").unwrap(), "ws://example.com:80");
153 assert_eq!(multiaddr_to_ws_url("/dns6/example.com/tcp/80").unwrap(), "ws://example.com:80");
154 assert_eq!(multiaddr_to_ws_url("/dnsaddr/example.com/tcp/8000").unwrap(), "ws://example.com:8000");
155 }
156
157 #[test]
158 fn ip6_literal_is_bracketed() {
159 assert_eq!(multiaddr_to_ws_url("/ip6/::1/tcp/9944").unwrap(), "ws://[::1]:9944");
160 assert_eq!(
161 multiaddr_to_ws_url("/ip6/2001:db8::1/tcp/443/wss").unwrap(),
162 "wss://[2001:db8::1]:443"
163 );
164 }
165
166 #[test]
167 fn trailing_p2p_peer_id_is_ignored() {
168 assert_eq!(
169 multiaddr_to_ws_url("/ip4/127.0.0.1/tcp/9944/ws/p2p/12D3KooWABCDEF").unwrap(),
170 "ws://127.0.0.1:9944"
171 );
172 assert_eq!(
173 multiaddr_to_ws_url("/ip4/127.0.0.1/tcp/9944/p2p/12D3KooWABCDEF").unwrap(),
174 "ws://127.0.0.1:9944"
175 );
176 }
177
178 #[test]
179 fn ws_and_wss_urls_pass_through() {
180 assert_eq!(multiaddr_to_ws_url("ws://127.0.0.1:9944").unwrap(), "ws://127.0.0.1:9944");
181 assert_eq!(multiaddr_to_ws_url("wss://relay.example.com/path").unwrap(), "wss://relay.example.com/path");
182 assert_eq!(multiaddr_to_ws_url("WS://Host:1").unwrap(), "WS://Host:1");
184 }
185
186 #[test]
187 fn whitespace_is_trimmed() {
188 assert_eq!(multiaddr_to_ws_url(" /ip4/127.0.0.1/tcp/9944 ").unwrap(), "ws://127.0.0.1:9944");
189 }
190
191 #[test]
192 fn empty_is_error() {
193 assert!(multiaddr_to_ws_url("").is_err());
194 assert!(multiaddr_to_ws_url(" ").is_err());
195 }
196
197 #[test]
198 fn garbage_is_error() {
199 assert!(multiaddr_to_ws_url("not-an-address").is_err());
200 assert!(multiaddr_to_ws_url("/not/a/valid/addr").is_err());
201 }
202
203 #[test]
204 fn missing_port_is_error() {
205 assert!(multiaddr_to_ws_url("/ip4/127.0.0.1").is_err());
206 assert!(multiaddr_to_ws_url("/ip4/127.0.0.1/tcp").is_err());
207 }
208
209 #[test]
210 fn non_numeric_port_is_error() {
211 assert!(multiaddr_to_ws_url("/ip4/127.0.0.1/tcp/notaport").is_err());
212 }
213
214 #[test]
215 fn missing_host_is_error() {
216 assert!(multiaddr_to_ws_url("/tcp/9944").is_err());
217 }
218
219 #[test]
220 fn non_tcp_transport_is_error() {
221 assert!(multiaddr_to_ws_url("/ip4/127.0.0.1/udp/9944").is_err());
223 }
224}