rama_http/layer/har/spec/
websocket.rs1use base64::{Engine as _, engine::general_purpose::STANDARD};
2use rama_utils::str::arcstr::ArcStr;
3use serde::{Deserialize, Serialize};
4
5#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
15pub struct WebSocketMessage {
16 pub r#type: WebSocketMessageType,
18 pub time: f64,
22 pub opcode: WebSocketMessageOpcode,
24 pub data: ArcStr,
30}
31
32impl WebSocketMessage {
33 #[must_use]
35 pub fn new(
36 r#type: WebSocketMessageType,
37 time: f64,
38 opcode: WebSocketMessageOpcode,
39 data: impl Into<ArcStr>,
40 ) -> Self {
41 Self {
42 r#type,
43 time,
44 opcode,
45 data: data.into(),
46 }
47 }
48
49 #[must_use]
51 pub fn text(r#type: WebSocketMessageType, time: f64, data: impl Into<ArcStr>) -> Self {
52 Self::new(r#type, time, WebSocketMessageOpcode::TEXT, data)
53 }
54
55 #[must_use]
57 pub fn binary(r#type: WebSocketMessageType, time: f64, data: impl AsRef<[u8]>) -> Self {
58 Self::binary_with_opcode(r#type, time, WebSocketMessageOpcode::BINARY, data)
59 }
60
61 #[must_use]
63 pub fn binary_with_opcode(
64 r#type: WebSocketMessageType,
65 time: f64,
66 opcode: WebSocketMessageOpcode,
67 data: impl AsRef<[u8]>,
68 ) -> Self {
69 Self::new(r#type, time, opcode, STANDARD.encode(data))
70 }
71
72 #[must_use]
74 pub fn error(time: f64, message: impl Into<ArcStr>) -> Self {
75 Self::new(
76 WebSocketMessageType::Error,
77 time,
78 WebSocketMessageOpcode::ERROR,
79 message,
80 )
81 }
82
83 pub fn binary_data(&self) -> Option<Result<Vec<u8>, base64::DecodeError>> {
88 (self.opcode == WebSocketMessageOpcode::BINARY)
89 .then(|| STANDARD.decode(self.data.as_bytes()))
90 }
91}
92
93#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
95#[serde(rename_all = "lowercase")]
96pub enum WebSocketMessageType {
97 Send,
99 Receive,
101 Error,
103}
104
105#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
113#[serde(transparent)]
114pub struct WebSocketMessageOpcode(i32);
115
116impl WebSocketMessageOpcode {
117 pub const ERROR: Self = Self(-1);
118 pub const CONTINUATION: Self = Self(0);
119 pub const TEXT: Self = Self(1);
120 pub const BINARY: Self = Self(2);
121 pub const CLOSE: Self = Self(8);
122 pub const PING: Self = Self(9);
123 pub const PONG: Self = Self(10);
124
125 #[must_use]
126 pub const fn new(value: i32) -> Self {
127 Self(value)
128 }
129
130 #[must_use]
131 pub const fn as_i32(self) -> i32 {
132 self.0
133 }
134}
135
136impl From<i32> for WebSocketMessageOpcode {
137 fn from(value: i32) -> Self {
138 Self::new(value)
139 }
140}
141
142impl From<WebSocketMessageOpcode> for i32 {
143 fn from(value: WebSocketMessageOpcode) -> Self {
144 value.as_i32()
145 }
146}
147
148#[cfg(test)]
149mod tests {
150 use super::*;
151 use serde_json::json;
152
153 const TIME: f64 = 1_558_730_482.507_147_3;
154
155 #[test]
156 fn serializes_in_chromium_shape() {
157 let message =
158 WebSocketMessage::text(WebSocketMessageType::Send, TIME, "Hello, WebSockets!");
159
160 assert_eq!(
161 serde_json::to_value(message).expect("serialize text message"),
162 json!({
163 "type": "send",
164 "time": TIME,
165 "opcode": 1,
166 "data": "Hello, WebSockets!",
167 })
168 );
169 }
170
171 #[test]
172 fn binary_constructor_base64_encodes_and_decodes_payload() {
173 let bytes = [0x00, 0x01, 0x02, 0xaa];
174 let message = WebSocketMessage::binary(WebSocketMessageType::Receive, TIME, bytes);
175
176 assert_eq!(message.opcode, WebSocketMessageOpcode::BINARY);
177 assert_eq!(message.data.as_str(), "AAECqg==");
178 assert_eq!(
179 message
180 .binary_data()
181 .expect("binary opcode")
182 .expect("valid base64"),
183 bytes
184 );
185 }
186
187 #[test]
188 fn binary_data_rejects_non_binary_messages_and_invalid_base64() {
189 let text = WebSocketMessage::text(WebSocketMessageType::Send, TIME, "hello");
190 assert!(text.binary_data().is_none());
191
192 let malformed = WebSocketMessage::new(
193 WebSocketMessageType::Receive,
194 TIME,
195 WebSocketMessageOpcode::BINARY,
196 "not base64!",
197 );
198 malformed
199 .binary_data()
200 .expect("binary opcode")
201 .expect_err("malformed base64 must fail");
202 }
203
204 #[test]
205 fn error_shape_uses_signed_opcode_sentinel() {
206 let message = WebSocketMessage::error(TIME, "Invalid frame header");
207
208 assert_eq!(
209 serde_json::to_value(&message).expect("serialize error message"),
210 json!({
211 "type": "error",
212 "time": TIME,
213 "opcode": -1,
214 "data": "Invalid frame header",
215 })
216 );
217 assert_eq!(message.opcode.as_i32(), -1);
218 }
219
220 #[test]
221 fn message_round_trip_preserves_all_types_and_unknown_opcodes() {
222 let messages = vec![
223 WebSocketMessage::text(WebSocketMessageType::Send, TIME, "sent"),
224 WebSocketMessage::binary(WebSocketMessageType::Receive, TIME + 1.0, [1, 2, 3]),
225 WebSocketMessage::error(TIME + 2.0, "failed"),
226 WebSocketMessage::new(
227 WebSocketMessageType::Receive,
228 TIME + 3.0,
229 WebSocketMessageOpcode::new(42),
230 "future opcode",
231 ),
232 ];
233
234 let encoded = serde_json::to_vec(&messages).expect("serialize messages");
235 let decoded: Vec<WebSocketMessage> =
236 serde_json::from_slice(&encoded).expect("deserialize messages");
237 assert_eq!(decoded, messages);
238 assert_eq!(decoded[3].opcode.as_i32(), 42);
239 }
240
241 #[test]
242 fn rfc_opcode_constants_have_expected_wire_values() {
243 assert_eq!(WebSocketMessageOpcode::CONTINUATION.as_i32(), 0);
244 assert_eq!(WebSocketMessageOpcode::TEXT.as_i32(), 1);
245 assert_eq!(WebSocketMessageOpcode::BINARY.as_i32(), 2);
246 assert_eq!(WebSocketMessageOpcode::CLOSE.as_i32(), 8);
247 assert_eq!(WebSocketMessageOpcode::PING.as_i32(), 9);
248 assert_eq!(WebSocketMessageOpcode::PONG.as_i32(), 10);
249 }
250
251 #[test]
252 fn opcode_integer_conversions_preserve_value() {
253 let opcode = WebSocketMessageOpcode::from(42);
254 assert_eq!(opcode, WebSocketMessageOpcode::new(42));
255 assert_eq!(i32::from(opcode), 42);
256 }
257}