zerodds_websocket_bridge/
uri.rs1use alloc::string::String;
12
13#[derive(Debug, Clone, PartialEq, Eq)]
15pub struct WebSocketUri {
16 pub secure: bool,
18 pub host: String,
20 pub port: u16,
22 pub resource_name: String,
24 pub query: Option<String>,
26}
27
28#[derive(Debug, Clone, Copy, PartialEq, Eq)]
30pub enum UriError {
31 InvalidScheme,
33 MissingHost,
35 InvalidPort,
37 FragmentNotAllowed,
39}
40
41impl core::fmt::Display for UriError {
42 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
43 match self {
44 Self::InvalidScheme => write!(f, "InvalidScheme"),
45 Self::MissingHost => write!(f, "MissingHost"),
46 Self::InvalidPort => write!(f, "InvalidPort"),
47 Self::FragmentNotAllowed => write!(f, "FragmentNotAllowed"),
48 }
49 }
50}
51
52#[cfg(feature = "std")]
53impl std::error::Error for UriError {}
54
55pub fn parse_websocket_uri(input: &str) -> Result<WebSocketUri, UriError> {
60 let (secure, rest) = if let Some(r) = input.strip_prefix("ws://") {
61 (false, r)
62 } else if let Some(r) = input.strip_prefix("wss://") {
63 (true, r)
64 } else {
65 return Err(UriError::InvalidScheme);
66 };
67
68 if rest.contains('#') {
69 return Err(UriError::FragmentNotAllowed);
70 }
71
72 let (authority, path_query) = match rest.find('/') {
74 Some(i) => (&rest[..i], &rest[i..]),
75 None => (rest, "/"),
76 };
77
78 if authority.is_empty() {
79 return Err(UriError::MissingHost);
80 }
81
82 let (host, port) = if let Some(colon) = authority.rfind(':') {
86 let host_part = &authority[..colon];
87 let port_str = &authority[colon + 1..];
88 let port_num: u16 = port_str.parse().map_err(|_| UriError::InvalidPort)?;
89 if host_part.is_empty() {
90 return Err(UriError::MissingHost);
91 }
92 (host_part.to_string(), port_num)
93 } else {
94 (authority.to_string(), if secure { 443 } else { 80 })
95 };
96
97 let (path, query) = match path_query.find('?') {
99 Some(q) => (
100 path_query[..q].to_string(),
101 Some(path_query[q + 1..].to_string()),
102 ),
103 None => (path_query.to_string(), None),
104 };
105
106 Ok(WebSocketUri {
107 secure,
108 host,
109 port,
110 resource_name: path,
111 query,
112 })
113}
114
115#[must_use]
117pub fn default_port(secure: bool) -> u16 {
118 if secure { 443 } else { 80 }
119}
120
121#[must_use]
123pub fn resource_name(uri: &WebSocketUri) -> String {
124 match &uri.query {
125 Some(q) => {
126 let mut s = uri.resource_name.clone();
127 s.push('?');
128 s.push_str(q);
129 s
130 }
131 None => uri.resource_name.clone(),
132 }
133}
134
135#[must_use]
138pub fn is_local_loopback(host: &str) -> bool {
139 matches!(host, "localhost" | "127.0.0.1" | "::1")
140}
141
142#[cfg(test)]
147#[allow(clippy::expect_used)]
148mod tests {
149 use super::*;
150
151 #[test]
152 fn parses_basic_ws_uri() {
153 let u = parse_websocket_uri("ws://example.com/chat").expect("ok");
154 assert!(!u.secure);
155 assert_eq!(u.host, "example.com");
156 assert_eq!(u.port, 80);
157 assert_eq!(u.resource_name, "/chat");
158 assert!(u.query.is_none());
159 }
160
161 #[test]
162 fn parses_basic_wss_uri() {
163 let u = parse_websocket_uri("wss://example.com/").expect("ok");
164 assert!(u.secure);
165 assert_eq!(u.port, 443);
166 }
167
168 #[test]
169 fn parses_explicit_port() {
170 let u = parse_websocket_uri("ws://example.com:8080/foo").expect("ok");
171 assert_eq!(u.port, 8080);
172 }
173
174 #[test]
175 fn parses_query_string() {
176 let u = parse_websocket_uri("wss://e.com:443/p?token=abc").expect("ok");
177 assert_eq!(u.query.as_deref(), Some("token=abc"));
178 assert_eq!(u.resource_name, "/p");
179 }
180
181 #[test]
182 fn parses_default_path_when_missing() {
183 let u = parse_websocket_uri("ws://e.com").expect("ok");
184 assert_eq!(u.resource_name, "/");
185 }
186
187 #[test]
188 fn rejects_unknown_scheme() {
189 assert_eq!(
190 parse_websocket_uri("http://e.com"),
191 Err(UriError::InvalidScheme)
192 );
193 }
194
195 #[test]
196 fn rejects_missing_host() {
197 assert_eq!(parse_websocket_uri("ws://"), Err(UriError::MissingHost));
198 }
199
200 #[test]
201 fn rejects_missing_host_before_port() {
202 assert_eq!(
203 parse_websocket_uri("ws://:8080/"),
204 Err(UriError::MissingHost)
205 );
206 }
207
208 #[test]
209 fn rejects_invalid_port() {
210 assert_eq!(
211 parse_websocket_uri("ws://e.com:abc/"),
212 Err(UriError::InvalidPort)
213 );
214 }
215
216 #[test]
217 fn rejects_fragment() {
218 assert_eq!(
219 parse_websocket_uri("ws://e.com/#anchor"),
220 Err(UriError::FragmentNotAllowed)
221 );
222 }
223
224 #[test]
225 fn default_port_returns_443_for_wss() {
226 assert_eq!(default_port(true), 443);
227 assert_eq!(default_port(false), 80);
228 }
229
230 #[test]
231 fn resource_name_combines_path_and_query() {
232 let u = WebSocketUri {
233 secure: false,
234 host: "e.com".into(),
235 port: 80,
236 resource_name: "/foo".into(),
237 query: Some("a=1".into()),
238 };
239 assert_eq!(resource_name(&u), "/foo?a=1");
240 }
241
242 #[test]
243 fn resource_name_without_query_is_path() {
244 let u = WebSocketUri {
245 secure: false,
246 host: "e.com".into(),
247 port: 80,
248 resource_name: "/foo".into(),
249 query: None,
250 };
251 assert_eq!(resource_name(&u), "/foo");
252 }
253
254 #[test]
255 fn is_local_loopback_recognizes_localhost() {
256 assert!(is_local_loopback("localhost"));
257 assert!(is_local_loopback("127.0.0.1"));
258 assert!(is_local_loopback("::1"));
259 assert!(!is_local_loopback("example.com"));
260 }
261}