zerodds_websocket_bridge/
negotiation.rs1use alloc::string::{String, ToString};
15use alloc::vec::Vec;
16
17#[derive(Debug, Clone, PartialEq, Eq)]
19pub struct ExtensionOffer {
20 pub name: String,
22 pub params: Vec<(String, Option<String>)>,
25}
26
27#[must_use]
34pub fn parse_extensions(header: &str) -> Vec<ExtensionOffer> {
35 let mut offers = Vec::new();
36 for raw in header.split(',') {
37 let trimmed = raw.trim();
38 if trimmed.is_empty() {
39 continue;
40 }
41 let mut parts = trimmed.split(';').map(str::trim);
42 let Some(name) = parts.next() else {
43 continue;
44 };
45 if name.is_empty() {
46 continue;
47 }
48 let mut params = Vec::new();
49 for p in parts {
50 if p.is_empty() {
51 continue;
52 }
53 if let Some(eq_pos) = p.find('=') {
54 let k = p[..eq_pos].trim().to_string();
55 let v = p[eq_pos + 1..].trim().trim_matches('"').to_string();
56 params.push((k, Some(v)));
57 } else {
58 params.push((p.to_string(), None));
59 }
60 }
61 offers.push(ExtensionOffer {
62 name: name.to_string(),
63 params,
64 });
65 }
66 offers
67}
68
69#[must_use]
72pub fn parse_subprotocols(header: &str) -> Vec<String> {
73 header
74 .split(',')
75 .map(|s| s.trim().to_string())
76 .filter(|s| !s.is_empty())
77 .collect()
78}
79
80#[must_use]
84pub fn select_subprotocol(client_offered: &[String], server_preferred: &[&str]) -> Option<String> {
85 for offer in client_offered {
86 for pref in server_preferred {
87 if offer.eq_ignore_ascii_case(pref) {
88 return Some(offer.clone());
89 }
90 }
91 }
92 None
93}
94
95pub const SUBPROTOCOL_HEADER: &str = "Sec-WebSocket-Protocol";
97
98pub const EXTENSIONS_HEADER: &str = "Sec-WebSocket-Extensions";
100
101#[cfg(test)]
102mod tests {
103 use super::*;
104
105 #[test]
106 fn parse_extensions_single() {
107 let offers = parse_extensions("permessage-deflate");
108 assert_eq!(offers.len(), 1);
109 assert_eq!(offers[0].name, "permessage-deflate");
110 assert!(offers[0].params.is_empty());
111 }
112
113 #[test]
114 fn parse_extensions_with_param() {
115 let offers = parse_extensions("permessage-deflate; client_max_window_bits");
116 assert_eq!(offers.len(), 1);
117 assert_eq!(offers[0].params.len(), 1);
118 assert_eq!(offers[0].params[0], ("client_max_window_bits".into(), None));
119 }
120
121 #[test]
122 fn parse_extensions_with_value_param() {
123 let offers = parse_extensions("foo; bar=baz");
124 assert_eq!(offers[0].params[0].0, "bar");
125 assert_eq!(offers[0].params[0].1.as_deref(), Some("baz"));
126 }
127
128 #[test]
129 fn parse_extensions_strips_quoted_value() {
130 let offers = parse_extensions("foo; bar=\"baz\"");
131 assert_eq!(offers[0].params[0].1.as_deref(), Some("baz"));
132 }
133
134 #[test]
135 fn parse_extensions_multiple_offers() {
136 let offers = parse_extensions("foo, bar; x=1, baz");
137 assert_eq!(offers.len(), 3);
138 assert_eq!(offers[0].name, "foo");
139 assert_eq!(offers[1].name, "bar");
140 assert_eq!(offers[2].name, "baz");
141 }
142
143 #[test]
144 fn parse_extensions_empty_returns_empty() {
145 assert!(parse_extensions("").is_empty());
146 assert!(parse_extensions(" , ").is_empty());
147 }
148
149 #[test]
150 fn parse_subprotocols_basic() {
151 assert_eq!(
152 parse_subprotocols("chat, soap, mqtt"),
153 vec!["chat".to_string(), "soap".into(), "mqtt".into()]
154 );
155 }
156
157 #[test]
158 fn parse_subprotocols_empty_returns_empty() {
159 assert!(parse_subprotocols("").is_empty());
160 }
161
162 #[test]
163 fn select_subprotocol_picks_first_match() {
164 let client = vec!["soap".to_string(), "chat".into()];
165 let server = ["mqtt", "chat", "soap"];
166 assert_eq!(
167 select_subprotocol(&client, &server).as_deref(),
168 Some("soap")
169 );
170 }
171
172 #[test]
173 fn select_subprotocol_returns_none_when_no_match() {
174 let client = vec!["xmpp".to_string()];
175 let server = ["chat", "mqtt"];
176 assert!(select_subprotocol(&client, &server).is_none());
177 }
178
179 #[test]
180 fn select_subprotocol_is_case_insensitive() {
181 let client = vec!["CHAT".to_string()];
182 let server = ["chat"];
183 assert_eq!(
184 select_subprotocol(&client, &server).as_deref(),
185 Some("CHAT")
186 );
187 }
188
189 #[test]
190 fn header_constants_match_spec() {
191 assert_eq!(SUBPROTOCOL_HEADER, "Sec-WebSocket-Protocol");
192 assert_eq!(EXTENSIONS_HEADER, "Sec-WebSocket-Extensions");
193 }
194}