Skip to main content

zerodds_websocket_bridge/
frame.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2026 ZeroDDS Contributors
3
4//! WebSocket Frame Modell — RFC 6455 §5.2.
5
6use alloc::vec::Vec;
7
8/// `Opcode` (RFC 6455 §5.2, S. 28-29) — 4-bit, definiert Interpretation
9/// der Payload.
10///
11/// Spec-Werte:
12/// * 0x0 — Continuation
13/// * 0x1 — Text Frame (UTF-8)
14/// * 0x2 — Binary Frame
15/// * 0x3-0x7 — Reserved Non-Control
16/// * 0x8 — Connection Close
17/// * 0x9 — Ping
18/// * 0xA — Pong
19/// * 0xB-0xF — Reserved Control
20#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
21pub enum Opcode {
22    /// `0x0` — Continuation frame.
23    Continuation,
24    /// `0x1` — Text frame (UTF-8).
25    Text,
26    /// `0x2` — Binary frame.
27    Binary,
28    /// `0x8` — Connection-Close frame.
29    Close,
30    /// `0x9` — Ping frame.
31    Ping,
32    /// `0xA` — Pong frame.
33    Pong,
34    /// `0x3-0x7` reserved non-control / `0xB-0xF` reserved control.
35    Reserved(u8),
36}
37
38impl Opcode {
39    /// Konvertiert vom Wire-Wert (4-bit).
40    #[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    /// Wire-Wert (4-bit).
54    #[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    /// Spec §5.5 — Control Frames sind opcodes 0x8-0xF. Sie haben
68    /// folgende Eigenschaften: payload <= 125 bytes (Spec §5.5),
69    /// duerfen nicht fragmentiert werden (FIN=1).
70    #[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/// WebSocket-Frame — RFC 6455 §5.2.
81#[derive(Debug, Clone, PartialEq, Eq)]
82pub struct Frame {
83    /// Spec §5.2 — `FIN` bit. Final fragment indicator.
84    pub fin: bool,
85    /// Spec §5.2 — `RSV1` bit. MUST be 0 unless extension negotiated.
86    pub rsv1: bool,
87    /// Spec §5.2 — `RSV2` bit.
88    pub rsv2: bool,
89    /// Spec §5.2 — `RSV3` bit.
90    pub rsv3: bool,
91    /// Spec §5.2 — Opcode.
92    pub opcode: Opcode,
93    /// Spec §5.2 — Masking-Key (`Some` wenn MASK=1; immer `Some` von
94    /// client→server).
95    pub masking_key: Option<[u8; 4]>,
96    /// Spec §5.2 — Payload (already unmasked beim Decode; wird beim
97    /// Encode automatisch maskiert wenn `masking_key` gesetzt).
98    pub payload: Vec<u8>,
99}
100
101impl Frame {
102    /// Konstruiert einen unmaskierten Text-Frame mit FIN=1.
103    #[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    /// Konstruiert einen unmaskierten Binary-Frame mit FIN=1.
117    #[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    /// Konstruiert einen Ping-Frame (FIN=1, max. 125 Bytes Payload —
131    /// Spec §5.5).
132    #[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    /// Konstruiert einen Pong-Frame (Spec §5.5.3 — als Reply-zu-Ping
146    /// MUST denselben Payload haben).
147    #[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    /// Konstruiert einen Close-Frame mit Status-Code + optionaler
161    /// Reason. Spec §5.5.1 + §7.4.
162    #[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    /// Aktiviert Client→Server-Masking. Spec §5.3 — "All frames sent
179    /// from client to server have this bit set to 1".
180    #[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        // RFC 6455 §5.2.
202        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        // Spec §5.5 — control opcodes 0x8-0xF.
213        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        // Reserved control range 0xB-0xF.
220        assert!(Opcode::Reserved(0xB).is_control());
221        // Reserved non-control 0x3-0x7.
222        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        // Spec §7.4 — Status-Code als 16-bit BE.
237        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        // Spec §5.3 — Client masks all frames.
251        let f = Frame::text("hi").with_mask([1, 2, 3, 4]);
252        assert_eq!(f.masking_key, Some([1, 2, 3, 4]));
253    }
254}