zerodds_websocket_bridge/
frame.rs1use alloc::vec::Vec;
7
8#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
21pub enum Opcode {
22 Continuation,
24 Text,
26 Binary,
28 Close,
30 Ping,
32 Pong,
34 Reserved(u8),
36}
37
38impl Opcode {
39 #[must_use]
41 pub const fn from_bits(v: u8) -> Self {
42 match v & 0x0F {
43 0x0 => Self::Continuation,
44 0x1 => Self::Text,
45 0x2 => Self::Binary,
46 0x8 => Self::Close,
47 0x9 => Self::Ping,
48 0xA => Self::Pong,
49 other => Self::Reserved(other),
50 }
51 }
52
53 #[must_use]
55 pub const fn to_bits(self) -> u8 {
56 match self {
57 Self::Continuation => 0x0,
58 Self::Text => 0x1,
59 Self::Binary => 0x2,
60 Self::Close => 0x8,
61 Self::Ping => 0x9,
62 Self::Pong => 0xA,
63 Self::Reserved(v) => v & 0x0F,
64 }
65 }
66
67 #[must_use]
71 pub const fn is_control(self) -> bool {
72 match self {
73 Self::Close | Self::Ping | Self::Pong => true,
74 Self::Reserved(v) => (v & 0x08) != 0,
75 _ => false,
76 }
77 }
78}
79
80#[derive(Debug, Clone, PartialEq, Eq)]
82pub struct Frame {
83 pub fin: bool,
85 pub rsv1: bool,
87 pub rsv2: bool,
89 pub rsv3: bool,
91 pub opcode: Opcode,
93 pub masking_key: Option<[u8; 4]>,
96 pub payload: Vec<u8>,
99}
100
101impl Frame {
102 #[must_use]
104 pub fn text(s: impl Into<alloc::string::String>) -> Self {
105 Self {
106 fin: true,
107 rsv1: false,
108 rsv2: false,
109 rsv3: false,
110 opcode: Opcode::Text,
111 masking_key: None,
112 payload: s.into().into_bytes(),
113 }
114 }
115
116 #[must_use]
118 pub const fn binary(payload: Vec<u8>) -> Self {
119 Self {
120 fin: true,
121 rsv1: false,
122 rsv2: false,
123 rsv3: false,
124 opcode: Opcode::Binary,
125 masking_key: None,
126 payload,
127 }
128 }
129
130 #[must_use]
133 pub const fn ping(payload: Vec<u8>) -> Self {
134 Self {
135 fin: true,
136 rsv1: false,
137 rsv2: false,
138 rsv3: false,
139 opcode: Opcode::Ping,
140 masking_key: None,
141 payload,
142 }
143 }
144
145 #[must_use]
148 pub const fn pong(payload: Vec<u8>) -> Self {
149 Self {
150 fin: true,
151 rsv1: false,
152 rsv2: false,
153 rsv3: false,
154 opcode: Opcode::Pong,
155 masking_key: None,
156 payload,
157 }
158 }
159
160 #[must_use]
163 pub fn close(status: u16, reason: &str) -> Self {
164 let mut payload = Vec::with_capacity(2 + reason.len());
165 payload.extend_from_slice(&status.to_be_bytes());
166 payload.extend_from_slice(reason.as_bytes());
167 Self {
168 fin: true,
169 rsv1: false,
170 rsv2: false,
171 rsv3: false,
172 opcode: Opcode::Close,
173 masking_key: None,
174 payload,
175 }
176 }
177
178 #[must_use]
181 pub const fn with_mask(mut self, key: [u8; 4]) -> Self {
182 self.masking_key = Some(key);
183 self
184 }
185}
186
187#[cfg(test)]
188mod tests {
189 use super::*;
190
191 #[test]
192 fn opcode_round_trip_via_bits() {
193 for v in 0..16u8 {
194 let op = Opcode::from_bits(v);
195 assert_eq!(op.to_bits(), v);
196 }
197 }
198
199 #[test]
200 fn opcode_well_known_values_match_spec() {
201 assert_eq!(Opcode::Continuation.to_bits(), 0x0);
203 assert_eq!(Opcode::Text.to_bits(), 0x1);
204 assert_eq!(Opcode::Binary.to_bits(), 0x2);
205 assert_eq!(Opcode::Close.to_bits(), 0x8);
206 assert_eq!(Opcode::Ping.to_bits(), 0x9);
207 assert_eq!(Opcode::Pong.to_bits(), 0xA);
208 }
209
210 #[test]
211 fn opcode_is_control_predicate() {
212 assert!(Opcode::Close.is_control());
214 assert!(Opcode::Ping.is_control());
215 assert!(Opcode::Pong.is_control());
216 assert!(!Opcode::Text.is_control());
217 assert!(!Opcode::Binary.is_control());
218 assert!(!Opcode::Continuation.is_control());
219 assert!(Opcode::Reserved(0xB).is_control());
221 assert!(!Opcode::Reserved(0x3).is_control());
223 }
224
225 #[test]
226 fn text_frame_constructor_sets_fin_and_opcode() {
227 let f = Frame::text("hello");
228 assert!(f.fin);
229 assert_eq!(f.opcode, Opcode::Text);
230 assert!(f.masking_key.is_none());
231 assert_eq!(f.payload, alloc::vec![b'h', b'e', b'l', b'l', b'o']);
232 }
233
234 #[test]
235 fn close_frame_includes_status_code_in_be_payload() {
236 let f = Frame::close(1000, "");
238 assert_eq!(&f.payload[..2], &1000u16.to_be_bytes());
239 }
240
241 #[test]
242 fn close_frame_with_reason_carries_utf8_bytes() {
243 let f = Frame::close(1001, "Going Away");
244 assert_eq!(&f.payload[..2], &1001u16.to_be_bytes());
245 assert_eq!(&f.payload[2..], b"Going Away");
246 }
247
248 #[test]
249 fn with_mask_sets_masking_key() {
250 let f = Frame::text("hi").with_mask([1, 2, 3, 4]);
252 assert_eq!(f.masking_key, Some([1, 2, 3, 4]));
253 }
254}