Skip to main content

ws_kit/
codec.rs

1//! Message codec.
2//!
3//! [`Codec`] provides `encode`/`decode` for WebSocket text frames. A blanket
4//! implementation via `serde_json` is provided for any `Serialize + DeserializeOwned`.
5//!
6//! ## The wire message model: [`Frame`]
7//!
8//! WebSocket (RFC 6455) carries two data frame kinds: **text** (must be valid
9//! UTF-8) and **binary** (opaque bytes). Since 0.4.0 ws-kit models both in
10//! [`Frame`]:
11//!
12//! - [`Frame::Text`] — UTF-8 payloads; the only kind [`Codec`] and
13//!   [`JsonCodec`] operate on.
14//! - [`Frame::Binary`] — opaque bytes, carried verbatim.
15//!
16//! # Security posture (binary is never text)
17//!
18//! A `Frame::Binary` payload is **never parsed as text or JSON** by any
19//! ws-kit code path. `Codec::decode`/`JsonCodec::decode` take `&str` — the
20//! type system makes it impossible to feed binary bytes into the JSON
21//! decoder without an explicit, reviewer-visible UTF-8 conversion. Rooms,
22//! the hub, and (0.4.0) the Redis adapter move binary payloads opaquely:
23//! no sniffing, no best-effort decoding, no shell-out to text heuristics.
24//! Conversely, text is *guaranteed* UTF-8 end to end — a `Frame::Text` can
25//! only be constructed from a `String`, so downstream JSON decode cannot
26//! hit invalid-UTF-8 surprises.
27//!
28//! Authentication is unaffected by frame kind: `TokenExtractor` reads the
29//! HTTP upgrade request (headers/query), never frame payloads.
30
31use bytes::Bytes;
32use serde::{de::DeserializeOwned, Serialize};
33
34use crate::error::WsError;
35
36/// A wire message: the union of WebSocket text and binary data frames.
37///
38/// Conversions are zero-copy where the transport already owns the data:
39/// `String`, `&str`, and `Bytes` all move into a `Frame` without
40/// re-encoding. `Vec<u8>` and borrowed `&[u8]` copy once.
41///
42/// `Frame` implements `Serialize`/`Deserialize`, so it can ride the generic
43/// [`crate::hub::BroadcastHub<Frame>`]; in JSON form binary payloads are
44/// represented as byte arrays and — per the security posture below — are
45/// only ever decoded back into `Frame::Binary`, never parsed as text.
46///
47/// # Example
48///
49/// ```
50/// use ws_kit::codec::Frame;
51///
52/// let text = Frame::from("hello");          // &str — one UTF-8 copy
53/// let owned = Frame::from(String::from("hi")); // String — zero-copy move
54/// let bin = Frame::from(vec![0u8, 1, 2]);   // Vec<u8> — becomes Binary
55/// assert!(matches!(text, Frame::Text(_)));
56/// assert!(matches!(bin, Frame::Binary(_)));
57/// ```
58#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
59pub enum Frame {
60    /// A UTF-8 text frame (WebSocket opcode 0x1).
61    Text(String),
62    /// An opaque binary frame (WebSocket opcode 0x2). Never parsed as
63    /// text/JSON by ws-kit — see the [security posture](self#security-posture-binary-is-never-text).
64    Binary(Bytes),
65}
66
67impl Frame {
68    /// Construct a `Binary` frame from anything `Bytes` converts from.
69    pub fn binary(data: impl Into<Bytes>) -> Self {
70        Frame::Binary(data.into())
71    }
72
73    /// Payload length in bytes (UTF-8 length for text).
74    pub fn len(&self) -> usize {
75        match self {
76            Frame::Text(s) => s.len(),
77            Frame::Binary(b) => b.len(),
78        }
79    }
80
81    /// True when the payload is empty.
82    pub fn is_empty(&self) -> bool {
83        self.len() == 0
84    }
85
86    /// True for [`Frame::Text`].
87    pub fn is_text(&self) -> bool {
88        matches!(self, Frame::Text(_))
89    }
90
91    /// True for [`Frame::Binary`].
92    pub fn is_binary(&self) -> bool {
93        matches!(self, Frame::Binary(_))
94    }
95
96    /// Payload as bytes (UTF-8 for text), without consuming the frame.
97    pub fn as_bytes(&self) -> &[u8] {
98        match self {
99            Frame::Text(s) => s.as_bytes(),
100            Frame::Binary(b) => b.as_ref(),
101        }
102    }
103
104    /// Take the payload as owned bytes, consuming the frame.
105    pub fn into_bytes(self) -> Bytes {
106        match self {
107            Frame::Text(s) => Bytes::from(s),
108            Frame::Binary(b) => b,
109        }
110    }
111
112    /// Interpret the payload as UTF-8 text (binary is rejected, not sniffed).
113    pub fn into_text(self) -> Result<String, WsError> {
114        match self {
115            Frame::Text(s) => Ok(s),
116            Frame::Binary(_) => Err(WsError::InvalidMessage),
117        }
118    }
119}
120
121impl From<String> for Frame {
122    fn from(s: String) -> Self {
123        Frame::Text(s)
124    }
125}
126
127impl From<&str> for Frame {
128    fn from(s: &str) -> Self {
129        Frame::Text(s.to_string())
130    }
131}
132
133impl From<Bytes> for Frame {
134    fn from(b: Bytes) -> Self {
135        Frame::Binary(b)
136    }
137}
138
139impl From<Vec<u8>> for Frame {
140    fn from(v: Vec<u8>) -> Self {
141        Frame::Binary(Bytes::from(v))
142    }
143}
144
145impl From<&[u8]> for Frame {
146    fn from(v: &[u8]) -> Self {
147        Frame::Binary(Bytes::copy_from_slice(v))
148    }
149}
150
151/// Trait for encoding/decoding WebSocket messages.
152///
153/// The default blanket implementation uses JSON via `serde_json`.
154///
155/// # Example
156///
157/// ```
158/// use ws_kit::codec::Codec;
159/// use serde::{Serialize, Deserialize};
160///
161/// #[derive(Serialize, Deserialize, PartialEq, Debug)]
162/// struct Msg { text: String }
163///
164/// let msg = Msg { text: "hello".into() };
165/// let s = msg.encode();
166/// let decoded = Msg::decode(&s).unwrap();
167/// assert_eq!(msg, decoded);
168/// ```
169pub trait Codec: Sized {
170    /// Encode `self` into a JSON string.
171    ///
172    /// This implementation serializes via `serde_json::to_string`.
173    /// Panics are avoided — if serialization fails the error is surfaced
174    /// through `WsError::InvalidMessage` in the `Result` variant. The
175    /// infallible `encode` variant below unwraps for convenience; use
176    /// [`Codec::encode_result`] when you need error handling.
177    fn encode(&self) -> String
178    where
179        Self: Serialize,
180    {
181        serde_json::to_string(self).unwrap_or_default()
182    }
183
184    /// Encode with error handling.
185    fn encode_result(&self) -> Result<String, WsError>
186    where
187        Self: Serialize,
188    {
189        serde_json::to_string(self).map_err(|_| WsError::InvalidMessage)
190    }
191
192    /// Decode `Self` from a JSON string.
193    fn decode(s: &str) -> Result<Self, WsError>
194    where
195        Self: DeserializeOwned,
196    {
197        serde_json::from_str(s).map_err(|_| WsError::InvalidMessage)
198    }
199}
200
201/// Blanket implementation for all serde types.
202impl<T> Codec for T where T: Serialize + DeserializeOwned {}
203
204/// JSON codec helper for types that don't want to implement [`Codec`] directly
205/// or where you need associated functions without trait dispatch.
206pub struct JsonCodec;
207
208impl JsonCodec {
209    /// Encode a value to JSON string.
210    pub fn encode<T: Serialize>(value: &T) -> Result<String, WsError> {
211        serde_json::to_string(value).map_err(|_| WsError::InvalidMessage)
212    }
213
214    /// Decode a value from JSON string.
215    pub fn decode<T: DeserializeOwned>(s: &str) -> Result<T, WsError> {
216        serde_json::from_str(s).map_err(|_| WsError::InvalidMessage)
217    }
218
219    /// Encode pretty-printed JSON (for debugging).
220    pub fn encode_pretty<T: Serialize>(value: &T) -> Result<String, WsError> {
221        serde_json::to_string_pretty(value).map_err(|_| WsError::InvalidMessage)
222    }
223}
224
225// Tests exercise failure paths and invariants directly; unwrap/expect,
226// slicing, and panicking asserts are acceptable here — violations
227// surface as test failures, not production panics.
228#[allow(
229    clippy::unwrap_used,
230    clippy::expect_used,
231    clippy::indexing_slicing,
232    clippy::panic
233)]
234#[cfg(test)]
235mod tests {
236    use super::*;
237    use serde::{Deserialize, Serialize};
238
239    #[derive(Serialize, Deserialize, Debug, PartialEq, Clone)]
240    struct Payload {
241        id: u32,
242        msg: String,
243    }
244
245    #[test]
246    fn codec_roundtrip_trait() {
247        let p = Payload {
248            id: 42,
249            msg: "hello".into(),
250        };
251        let s = p.encode();
252        let d = Payload::decode(&s).unwrap();
253        assert_eq!(p, d);
254    }
255
256    #[test]
257    fn codec_roundtrip_result() {
258        let p = Payload {
259            id: 1,
260            msg: "world".into(),
261        };
262        let s = p.encode_result().unwrap();
263        let d = Payload::decode(&s).unwrap();
264        assert_eq!(p, d);
265    }
266
267    #[test]
268    fn codec_invalid_decode() {
269        let err = Payload::decode("not json").unwrap_err();
270        assert_eq!(err, WsError::InvalidMessage);
271    }
272
273    #[test]
274    fn json_codec_helpers() {
275        let p = Payload {
276            id: 7,
277            msg: "test".into(),
278        };
279        let s = JsonCodec::encode(&p).unwrap();
280        let d: Payload = JsonCodec::decode(&s).unwrap();
281        assert_eq!(p, d);
282
283        let pretty = JsonCodec::encode_pretty(&p).unwrap();
284        assert!(pretty.contains('\n'));
285    }
286
287    #[test]
288    fn encode_vec() {
289        let v = vec![1, 2, 3];
290        let s = v.encode();
291        let d: Vec<i32> = Vec::decode(&s).unwrap();
292        assert_eq!(v, d);
293    }
294
295    // --- Frame ---
296
297    #[test]
298    fn frame_constructors_and_kinds() {
299        assert!(Frame::from("hello".to_string()).is_text());
300        assert!(Frame::from("hello").is_text());
301        assert!(Frame::from(Bytes::from_static(b"\x00\x01")).is_binary());
302        assert!(Frame::from(vec![1u8, 2]).is_binary());
303        assert!(Frame::from(&[1u8, 2][..]).is_binary());
304        assert!(Frame::binary(Vec::new()).is_binary());
305    }
306
307    #[test]
308    fn frame_len_and_bytes() {
309        let t = Frame::from("héllo"); // multibyte: len is bytes
310        assert_eq!(t.len(), 6);
311        assert_eq!(t.as_bytes(), "héllo".as_bytes());
312        assert!(!t.is_empty());
313
314        let b = Frame::from(vec![0u8; 4]);
315        assert_eq!(b.len(), 4);
316        assert!(Frame::from(String::new()).is_empty());
317    }
318
319    #[test]
320    fn frame_into_bytes_and_text() {
321        let t = Frame::from("abc".to_string());
322        assert_eq!(&t.into_bytes()[..], b"abc");
323
324        let b = Frame::from(vec![9u8]);
325        assert_eq!(b.into_bytes()[0], 9);
326
327        let t2 = Frame::from("text".to_string()).into_text();
328        assert_eq!(t2.unwrap(), "text");
329
330        // Binary is rejected as text — never sniffed/coerced.
331        let b2 = Frame::from(vec![0xffu8, 0xfe]).into_text();
332        assert_eq!(b2.unwrap_err(), WsError::InvalidMessage);
333    }
334
335    #[test]
336    fn frame_zero_copy_moves() {
337        let s = String::from("owned");
338        let f = Frame::from(s);
339        assert_eq!(f, Frame::Text("owned".to_string()));
340        let v = vec![1u8; 8];
341        let f2 = Frame::from(v);
342        assert_eq!(f2.len(), 8);
343    }
344
345    #[test]
346    fn frame_clone_eq() {
347        let f = Frame::from("dup".to_string());
348        assert_eq!(f.clone(), f);
349        assert_ne!(
350            Frame::from("a".to_string()),
351            Frame::from(Bytes::from_static(b"a"))
352        );
353    }
354}