Skip to main content

wscall_protocol/
lib.rs

1//! Shared protocol definitions for WSCALL.
2//!
3//! This crate contains the transport envelope, frame codec, encryption modes,
4//! and inline attachment model used by both the server and client crates.
5
6use std::borrow::Cow;
7use std::sync::Arc;
8
9use aes_gcm::{Aes256Gcm, KeyInit as AesKeyInit, Nonce as AesNonce, aead::Aead as AesAead};
10use base64::Engine;
11use base64::engine::general_purpose::STANDARD as BASE64;
12use chacha20poly1305::{ChaCha20Poly1305, Nonce};
13use getrandom::getrandom;
14use serde::de::Deserializer;
15use serde::ser::SerializeMap;
16use serde::{Deserialize, Serialize};
17use serde_json::{Value, json};
18use thiserror::Error;
19
20const AES256_NONCE_LEN: usize = 12;
21const CHACHA20_NONCE_LEN: usize = 12;
22const MAX_FRAME_BYTES: usize = 10 * 1024 * 1024;
23const MAX_PAYLOAD_BYTES: usize = MAX_FRAME_BYTES - 6;
24
25/// Distinguishes API messages from event messages inside a WSCALL frame.
26#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
27#[repr(u8)]
28pub enum MessageType {
29    /// API request or API response.
30    Api = 0x00,
31    /// Event emit or event acknowledgement.
32    Event = 0x01,
33}
34
35impl TryFrom<u8> for MessageType {
36    type Error = ProtocolError;
37
38    fn try_from(value: u8) -> Result<Self, Self::Error> {
39        match value {
40            0x00 => Ok(Self::Api),
41            0x01 => Ok(Self::Event),
42            _ => Err(ProtocolError::UnknownMessageType(value)),
43        }
44    }
45}
46
47/// Selects how the payload section of a frame is encoded.
48#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
49#[repr(u8)]
50pub enum EncryptionKind {
51    /// Unencrypted JSON payload.
52    None = 0x00,
53    /// ChaCha20-Poly1305 encrypted payload.
54    ChaCha20 = 0x01,
55    /// AES256-GCM encrypted payload.
56    Aes256 = 0x02,
57}
58
59impl TryFrom<u8> for EncryptionKind {
60    type Error = ProtocolError;
61
62    fn try_from(value: u8) -> Result<Self, Self::Error> {
63        match value {
64            0x00 => Ok(Self::None),
65            0x01 => Ok(Self::ChaCha20),
66            0x02 => Ok(Self::Aes256),
67            _ => Err(ProtocolError::UnknownEncryption(value)),
68        }
69    }
70}
71
72/// Inline attachment carried alongside JSON params or event data.
73#[derive(Debug, Clone, Serialize, Deserialize)]
74pub struct FileAttachment {
75    /// Attachment identifier referenced from JSON using `{ "$file": "..." }`.
76    pub id: String,
77    /// Original file name.
78    pub name: String,
79    /// MIME type supplied by the sender.
80    pub content_type: String,
81    /// Content transfer encoding. Current implementation uses Base64.
82    pub encoding: String,
83    /// Encoded attachment payload.
84    pub data: String,
85    /// Original byte length before encoding.
86    pub size: usize,
87}
88
89impl FileAttachment {
90    /// Builds an inline text attachment and encodes it as Base64.
91    pub fn inline_text(
92        id: impl Into<String>,
93        name: impl Into<String>,
94        content_type: impl Into<String>,
95        text: impl AsRef<str>,
96    ) -> Self {
97        Self::inline_bytes(id, name, content_type, text.as_ref().as_bytes().to_vec())
98    }
99
100    /// Builds an inline binary attachment and encodes it as Base64.
101    pub fn inline_bytes(
102        id: impl Into<String>,
103        name: impl Into<String>,
104        content_type: impl Into<String>,
105        bytes: Vec<u8>,
106    ) -> Self {
107        let size = bytes.len();
108        Self {
109            id: id.into(),
110            name: name.into(),
111            content_type: content_type.into(),
112            encoding: "base64".to_string(),
113            data: BASE64.encode(bytes),
114            size,
115        }
116    }
117
118    /// Decodes the attachment payload back into raw bytes.
119    pub fn decode_bytes(&self) -> Result<Vec<u8>, ProtocolError> {
120        BASE64
121            .decode(self.data.as_bytes())
122            .map_err(|source| ProtocolError::InvalidAttachmentEncoding(source.to_string()))
123    }
124
125    /// Returns a JSON reference object that points to an attachment by id.
126    pub fn param_ref(id: impl Into<String>) -> Value {
127        json!({ "$file": id.into() })
128    }
129}
130
131/// Standard error payload embedded in API responses and event acknowledgements.
132#[derive(Debug, Clone, Serialize, Deserialize)]
133pub struct ErrorPayload {
134    pub code: String,
135    pub message: String,
136    pub status: u16,
137    #[serde(skip_serializing_if = "Option::is_none")]
138    pub details: Option<Value>,
139}
140
141/// Compact numeric tag identifying each [`PacketBody`] variant in the JSON
142/// wire format. Using a single-byte numeric field (`"k": 0`) instead of a
143/// string tag (`"kind": "api_request"`) saves several bytes per frame.
144pub const K_API_REQUEST: u8 = 0;
145pub const K_EVENT_EMIT: u8 = 1;
146pub const K_API_RESPONSE: u8 = 2;
147pub const K_EVENT_ACK: u8 = 3;
148
149/// JSON-level message body transported inside a WSCALL frame.
150///
151/// The wire format uses short single-letter keys and a numeric `k` tag to
152/// minimize per-frame overhead:
153///
154/// | Variant | `k` | Fields |
155/// | --- | --- | --- |
156/// | `ApiRequest`  | 0 | `i` `r` `p` `a` `m` |
157/// | `EventEmit`   | 1 | `i` `n` `d` `a` `m` `e` [`si`] |
158/// | `ApiResponse` | 2 | `i` `o` `s` `d` `m` [`er`] |
159/// | `EventAck`    | 3 | `i` `o` `rc` [`er`] |
160#[derive(Debug, Clone)]
161pub enum PacketBody {
162    /// Client-to-server API request.
163    ApiRequest {
164        request_id: u64,
165        route: String,
166        params: Value,
167        attachments: Vec<FileAttachment>,
168        metadata: Value,
169    },
170    /// Server-to-client API response.
171    ApiResponse {
172        request_id: u64,
173        ok: bool,
174        status: u16,
175        data: Value,
176        error: Option<ErrorPayload>,
177        metadata: Value,
178    },
179    /// Event emission in either direction.
180    EventEmit {
181        event_id: u64,
182        name: String,
183        data: Value,
184        attachments: Vec<FileAttachment>,
185        metadata: Value,
186        expect_ack: bool,
187        /// Storage ID assigned by a database or other persistent store.
188        /// Serialized as `"si"` when present, omitted when `None`.
189        storage_id: Option<u64>,
190    },
191    /// Acknowledgement for an emitted event.
192    EventAck {
193        event_id: u64,
194        ok: bool,
195        receipt: Value,
196        error: Option<ErrorPayload>,
197    },
198}
199
200// --- Manual Serialize: short keys + numeric `k` tag -------------------------
201
202impl Serialize for PacketBody {
203    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
204    where
205        S: serde::Serializer,
206    {
207        match self {
208            Self::ApiRequest {
209                request_id,
210                route,
211                params,
212                attachments,
213                metadata,
214            } => {
215                let mut s = serializer.serialize_map(Some(6))?;
216                s.serialize_entry("k", &K_API_REQUEST)?;
217                s.serialize_entry("i", request_id)?;
218                s.serialize_entry("r", route)?;
219                s.serialize_entry("p", params)?;
220                s.serialize_entry("a", attachments)?;
221                s.serialize_entry("m", metadata)?;
222                s.end()
223            }
224            Self::ApiResponse {
225                request_id,
226                ok,
227                status,
228                data,
229                error,
230                metadata,
231            } => {
232                let field_count = 5 + usize::from(error.is_some()) + 1;
233                let mut s = serializer.serialize_map(Some(field_count))?;
234                s.serialize_entry("k", &K_API_RESPONSE)?;
235                s.serialize_entry("i", request_id)?;
236                s.serialize_entry("o", ok)?;
237                s.serialize_entry("s", status)?;
238                s.serialize_entry("d", data)?;
239                if let Some(err) = error {
240                    s.serialize_entry("er", err)?;
241                }
242                s.serialize_entry("m", metadata)?;
243                s.end()
244            }
245            Self::EventEmit {
246                event_id,
247                name,
248                data,
249                attachments,
250                metadata,
251                expect_ack,
252                storage_id,
253            } => {
254                let field_count = 7 + usize::from(storage_id.is_some());
255                let mut s = serializer.serialize_map(Some(field_count))?;
256                s.serialize_entry("k", &K_EVENT_EMIT)?;
257                s.serialize_entry("i", event_id)?;
258                s.serialize_entry("n", name)?;
259                s.serialize_entry("d", data)?;
260                s.serialize_entry("a", attachments)?;
261                s.serialize_entry("m", metadata)?;
262                s.serialize_entry("e", expect_ack)?;
263                if let Some(si) = storage_id {
264                    s.serialize_entry("si", si)?;
265                }
266                s.end()
267            }
268            Self::EventAck {
269                event_id,
270                ok,
271                receipt,
272                error,
273            } => {
274                let field_count = 3 + usize::from(error.is_some()) + 1;
275                let mut s = serializer.serialize_map(Some(field_count))?;
276                s.serialize_entry("k", &K_EVENT_ACK)?;
277                s.serialize_entry("i", event_id)?;
278                s.serialize_entry("o", ok)?;
279                s.serialize_entry("rc", receipt)?;
280                if let Some(err) = error {
281                    s.serialize_entry("er", err)?;
282                }
283                s.end()
284            }
285        }
286    }
287}
288
289// --- Manual Deserialize: read numeric `k`, dispatch to short-key fields -------
290
291/// Deserialization helper for [`PacketBody::ApiRequest`].
292#[derive(Deserialize)]
293struct ApiRequestFields {
294    #[serde(rename = "i")]
295    request_id: u64,
296    #[serde(rename = "r")]
297    route: String,
298    #[serde(rename = "p", default)]
299    params: Value,
300    #[serde(rename = "a", default)]
301    attachments: Vec<FileAttachment>,
302    #[serde(rename = "m", default)]
303    metadata: Value,
304}
305
306/// Deserialization helper for [`PacketBody::ApiResponse`].
307#[derive(Deserialize)]
308struct ApiResponseFields {
309    #[serde(rename = "i")]
310    request_id: u64,
311    #[serde(rename = "o", default)]
312    ok: bool,
313    #[serde(rename = "s", default)]
314    status: u16,
315    #[serde(rename = "d", default)]
316    data: Value,
317    #[serde(rename = "er", default)]
318    error: Option<ErrorPayload>,
319    #[serde(rename = "m", default)]
320    metadata: Value,
321}
322
323/// Deserialization helper for [`PacketBody::EventEmit`].
324#[derive(Deserialize)]
325struct EventEmitFields {
326    #[serde(rename = "i")]
327    event_id: u64,
328    #[serde(rename = "n")]
329    name: String,
330    #[serde(rename = "d", default)]
331    data: Value,
332    #[serde(rename = "a", default)]
333    attachments: Vec<FileAttachment>,
334    #[serde(rename = "m", default)]
335    metadata: Value,
336    #[serde(rename = "e", default)]
337    expect_ack: bool,
338    #[serde(rename = "si", default)]
339    storage_id: Option<u64>,
340}
341
342/// Deserialization helper for [`PacketBody::EventAck`].
343#[derive(Deserialize)]
344struct EventAckFields {
345    #[serde(rename = "i")]
346    event_id: u64,
347    #[serde(rename = "o", default)]
348    ok: bool,
349    #[serde(rename = "rc", default)]
350    receipt: Value,
351    #[serde(rename = "er", default)]
352    error: Option<ErrorPayload>,
353}
354
355impl<'de> Deserialize<'de> for PacketBody {
356    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
357    where
358        D: Deserializer<'de>,
359    {
360        // Parse into a generic JSON value first so we can inspect the numeric
361        // `k` discriminator, then dispatch to the appropriate short-key struct.
362        let value = Value::deserialize(deserializer)?;
363        let k = value.get("k").and_then(|v| v.as_u64()).ok_or_else(|| {
364            serde::de::Error::custom("missing or non-numeric 'k' field in packet body")
365        })?;
366        let k = u8::try_from(k).map_err(|_| {
367            serde::de::Error::custom(format!("'k' value {k} is outside the valid range"))
368        })?;
369
370        match k {
371            K_API_REQUEST => {
372                let f = serde_json::from_value::<ApiRequestFields>(value)
373                    .map_err(serde::de::Error::custom)?;
374                Ok(Self::ApiRequest {
375                    request_id: f.request_id,
376                    route: f.route,
377                    params: f.params,
378                    attachments: f.attachments,
379                    metadata: f.metadata,
380                })
381            }
382            K_EVENT_EMIT => {
383                let f = serde_json::from_value::<EventEmitFields>(value)
384                    .map_err(serde::de::Error::custom)?;
385                Ok(Self::EventEmit {
386                    event_id: f.event_id,
387                    name: f.name,
388                    data: f.data,
389                    attachments: f.attachments,
390                    metadata: f.metadata,
391                    expect_ack: f.expect_ack,
392                    storage_id: f.storage_id,
393                })
394            }
395            K_API_RESPONSE => {
396                let f = serde_json::from_value::<ApiResponseFields>(value)
397                    .map_err(serde::de::Error::custom)?;
398                Ok(Self::ApiResponse {
399                    request_id: f.request_id,
400                    ok: f.ok,
401                    status: f.status,
402                    data: f.data,
403                    error: f.error,
404                    metadata: f.metadata,
405                })
406            }
407            K_EVENT_ACK => {
408                let f = serde_json::from_value::<EventAckFields>(value)
409                    .map_err(serde::de::Error::custom)?;
410                Ok(Self::EventAck {
411                    event_id: f.event_id,
412                    ok: f.ok,
413                    receipt: f.receipt,
414                    error: f.error,
415                })
416            }
417            _ => Err(serde::de::Error::custom(format!("unknown 'k' value: {k}"))),
418        }
419    }
420}
421
422impl PacketBody {
423    pub fn message_type(&self) -> MessageType {
424        match self {
425            Self::ApiRequest { .. } | Self::ApiResponse { .. } => MessageType::Api,
426            Self::EventEmit { .. } | Self::EventAck { .. } => MessageType::Event,
427        }
428    }
429}
430
431/// Full transport envelope before frame encoding.
432#[derive(Debug, Clone, Serialize, Deserialize)]
433pub struct PacketEnvelope {
434    /// Message category declared in the frame header.
435    pub message_type: MessageType,
436    /// Encryption mode declared in the frame header.
437    pub encryption: EncryptionKind,
438    /// JSON body payload.
439    pub body: PacketBody,
440}
441
442impl PacketEnvelope {
443    /// Builds a plaintext envelope from a body.
444    pub fn new(body: PacketBody) -> Self {
445        Self {
446            message_type: body.message_type(),
447            encryption: EncryptionKind::None,
448            body,
449        }
450    }
451
452    /// Builds an envelope with an explicit encryption mode.
453    pub fn with_encryption(body: PacketBody, encryption: EncryptionKind) -> Self {
454        Self {
455            message_type: body.message_type(),
456            encryption,
457            body,
458        }
459    }
460}
461
462/// Encodes and decodes WSCALL binary frames.
463///
464/// The symmetric ciphers are constructed once when a key is configured and shared
465/// via [`Arc`] across every clone of the codec. This avoids redoing the key
466/// schedule (which for AES-256-GCM is especially expensive) on every frame.
467#[derive(Clone, Default)]
468pub struct FrameCodec {
469    aes256_cipher: Option<Arc<Aes256Gcm>>,
470    chacha20_cipher: Option<Arc<ChaCha20Poly1305>>,
471}
472
473impl std::fmt::Debug for FrameCodec {
474    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
475        f.debug_struct("FrameCodec")
476            .field("aes256", &self.aes256_cipher.is_some())
477            .field("chacha20", &self.chacha20_cipher.is_some())
478            .finish()
479    }
480}
481
482impl FrameCodec {
483    /// Builds a codec configured for plaintext transport.
484    pub fn plaintext() -> Self {
485        Self::default()
486    }
487
488    /// Configures a ChaCha20-Poly1305 key.
489    ///
490    /// The cipher is constructed eagerly so that subsequent frame encoding never
491    /// pays for the key schedule again.
492    pub fn with_chacha20_key(self, key: [u8; 32]) -> Self {
493        let cipher = ChaCha20Poly1305::new_from_slice(&key)
494            .expect("a 32-byte key is always valid for ChaCha20-Poly1305");
495        Self {
496            chacha20_cipher: Some(Arc::new(cipher)),
497            ..self
498        }
499    }
500
501    /// Configures an AES256-GCM key.
502    ///
503    /// The cipher is constructed eagerly so that subsequent frame encoding never
504    /// pays for the AES key expansion again.
505    pub fn with_aes256_key(self, key: [u8; 32]) -> Self {
506        let cipher =
507            Aes256Gcm::new_from_slice(&key).expect("a 32-byte key is always valid for AES-256-GCM");
508        Self {
509            aes256_cipher: Some(Arc::new(cipher)),
510            ..self
511        }
512    }
513
514    /// Encodes an envelope into a binary WSCALL frame.
515    pub fn encode(&self, packet: &PacketEnvelope) -> Result<Vec<u8>, ProtocolError> {
516        let payload = serde_json::to_vec(&packet.body)?;
517
518        // Pre-check the serialized JSON size before doing any crypto work so
519        // that oversized payloads are rejected without paying for encryption.
520        if payload.len() > MAX_PAYLOAD_BYTES {
521            return Err(ProtocolError::PayloadTooLarge {
522                actual: payload.len(),
523                max: MAX_PAYLOAD_BYTES,
524            });
525        }
526
527        let payload = match packet.encryption {
528            EncryptionKind::None => payload,
529            EncryptionKind::ChaCha20 => self.encrypt_chacha20(&payload)?,
530            EncryptionKind::Aes256 => self.encrypt_aes256(&payload)?,
531        };
532
533        // The encrypted form (nonce + ciphertext + tag) can still overflow the
534        // limit even when the plaintext fit, so guard once more.
535        if payload.len() > MAX_PAYLOAD_BYTES {
536            return Err(ProtocolError::PayloadTooLarge {
537                actual: payload.len(),
538                max: MAX_PAYLOAD_BYTES,
539            });
540        }
541
542        let frame_len = 2 + payload.len();
543        let mut frame = Vec::with_capacity(4 + frame_len);
544        frame.extend_from_slice(&(frame_len as u32).to_be_bytes());
545        frame.push(packet.message_type as u8);
546        frame.push(packet.encryption as u8);
547        frame.extend_from_slice(&payload);
548        Ok(frame)
549    }
550
551    /// Decodes a binary WSCALL frame back into an envelope.
552    pub fn decode(&self, frame: &[u8]) -> Result<PacketEnvelope, ProtocolError> {
553        if frame.len() < 6 {
554            return Err(ProtocolError::FrameTooShort);
555        }
556
557        let declared = u32::from_be_bytes([frame[0], frame[1], frame[2], frame[3]]) as usize;
558        let actual = frame.len() - 4;
559        if declared != actual {
560            return Err(ProtocolError::FrameLengthMismatch { declared, actual });
561        }
562
563        let payload_len = actual - 2;
564        if payload_len > MAX_PAYLOAD_BYTES {
565            return Err(ProtocolError::PayloadTooLarge {
566                actual: payload_len,
567                max: MAX_PAYLOAD_BYTES,
568            });
569        }
570
571        let message_type = MessageType::try_from(frame[4])?;
572        let encryption = EncryptionKind::try_from(frame[5])?;
573        // Borrow the plaintext slice directly instead of cloning it into a Vec.
574        let payload: Cow<'_, [u8]> = match encryption {
575            EncryptionKind::None => Cow::Borrowed(&frame[6..]),
576            EncryptionKind::ChaCha20 => Cow::Owned(self.decrypt_chacha20(&frame[6..])?),
577            EncryptionKind::Aes256 => Cow::Owned(self.decrypt_aes256(&frame[6..])?),
578        };
579
580        let body: PacketBody = serde_json::from_slice(&payload)?;
581        if body.message_type() != message_type {
582            return Err(ProtocolError::MessageTypeMismatch);
583        }
584
585        Ok(PacketEnvelope {
586            message_type,
587            encryption,
588            body,
589        })
590    }
591
592    fn encrypt_chacha20(&self, payload: &[u8]) -> Result<Vec<u8>, ProtocolError> {
593        let cipher = self
594            .chacha20_cipher
595            .as_ref()
596            .ok_or(ProtocolError::MissingEncryptionKey("chacha20"))?;
597        let mut nonce_bytes = [0_u8; CHACHA20_NONCE_LEN];
598        getrandom(&mut nonce_bytes).map_err(|source| ProtocolError::Random(source.to_string()))?;
599        let ciphertext = cipher
600            .encrypt(Nonce::from_slice(&nonce_bytes), payload)
601            .map_err(|_| ProtocolError::EncryptionFailed("chacha20"))?;
602
603        let mut encoded = Vec::with_capacity(CHACHA20_NONCE_LEN + ciphertext.len());
604        encoded.extend_from_slice(&nonce_bytes);
605        encoded.extend_from_slice(&ciphertext);
606        Ok(encoded)
607    }
608
609    fn decrypt_chacha20(&self, payload: &[u8]) -> Result<Vec<u8>, ProtocolError> {
610        if payload.len() < CHACHA20_NONCE_LEN {
611            return Err(ProtocolError::EncryptedPayloadTooShort {
612                algorithm: "chacha20",
613                expected_min: CHACHA20_NONCE_LEN,
614                actual: payload.len(),
615            });
616        }
617
618        let cipher = self
619            .chacha20_cipher
620            .as_ref()
621            .ok_or(ProtocolError::MissingEncryptionKey("chacha20"))?;
622        let (nonce_bytes, ciphertext) = payload.split_at(CHACHA20_NONCE_LEN);
623        cipher
624            .decrypt(Nonce::from_slice(nonce_bytes), ciphertext)
625            .map_err(|_| ProtocolError::DecryptionFailed("chacha20"))
626    }
627
628    fn encrypt_aes256(&self, payload: &[u8]) -> Result<Vec<u8>, ProtocolError> {
629        let cipher = self
630            .aes256_cipher
631            .as_ref()
632            .ok_or(ProtocolError::MissingEncryptionKey("aes256"))?;
633        let mut nonce_bytes = [0_u8; AES256_NONCE_LEN];
634        getrandom(&mut nonce_bytes).map_err(|source| ProtocolError::Random(source.to_string()))?;
635        let ciphertext = cipher
636            .encrypt(AesNonce::from_slice(&nonce_bytes), payload)
637            .map_err(|_| ProtocolError::EncryptionFailed("aes256"))?;
638
639        let mut encoded = Vec::with_capacity(AES256_NONCE_LEN + ciphertext.len());
640        encoded.extend_from_slice(&nonce_bytes);
641        encoded.extend_from_slice(&ciphertext);
642        Ok(encoded)
643    }
644
645    fn decrypt_aes256(&self, payload: &[u8]) -> Result<Vec<u8>, ProtocolError> {
646        if payload.len() < AES256_NONCE_LEN {
647            return Err(ProtocolError::EncryptedPayloadTooShort {
648                algorithm: "aes256",
649                expected_min: AES256_NONCE_LEN,
650                actual: payload.len(),
651            });
652        }
653
654        let cipher = self
655            .aes256_cipher
656            .as_ref()
657            .ok_or(ProtocolError::MissingEncryptionKey("aes256"))?;
658        let (nonce_bytes, ciphertext) = payload.split_at(AES256_NONCE_LEN);
659        cipher
660            .decrypt(AesNonce::from_slice(nonce_bytes), ciphertext)
661            .map_err(|_| ProtocolError::DecryptionFailed("aes256"))
662    }
663}
664
665/// Helper for encoding a plaintext frame without constructing a custom codec.
666pub fn encode_frame(packet: &PacketEnvelope) -> Result<Vec<u8>, ProtocolError> {
667    FrameCodec::plaintext().encode(packet)
668}
669
670/// Helper for decoding a plaintext frame without constructing a custom codec.
671pub fn decode_frame(frame: &[u8]) -> Result<PacketEnvelope, ProtocolError> {
672    FrameCodec::plaintext().decode(frame)
673}
674
675/// Errors returned while encoding or decoding WSCALL frames.
676#[derive(Debug, Error)]
677pub enum ProtocolError {
678    #[error("frame too short")]
679    FrameTooShort,
680    #[error("frame length mismatch: declared={declared}, actual={actual}")]
681    FrameLengthMismatch { declared: usize, actual: usize },
682    #[error("payload too large: actual={actual}, max={max}")]
683    PayloadTooLarge { actual: usize, max: usize },
684    #[error("unknown message type: {0:#x}")]
685    UnknownMessageType(u8),
686    #[error("unknown encryption kind: {0:#x}")]
687    UnknownEncryption(u8),
688    #[error("unsupported encryption kind: {0:#x}")]
689    UnsupportedEncryption(u8),
690    #[error("missing encryption key for {0}")]
691    MissingEncryptionKey(&'static str),
692    #[error("invalid encryption key for {0}")]
693    InvalidEncryptionKey(&'static str),
694    #[error("secure random generation failed: {0}")]
695    Random(String),
696    #[error(
697        "encrypted payload too short for {algorithm}: expected at least {expected_min}, actual={actual}"
698    )]
699    EncryptedPayloadTooShort {
700        algorithm: &'static str,
701        expected_min: usize,
702        actual: usize,
703    },
704    #[error("encryption failed for {0}")]
705    EncryptionFailed(&'static str),
706    #[error("decryption failed for {0}")]
707    DecryptionFailed(&'static str),
708    #[error("message type does not match packet body")]
709    MessageTypeMismatch,
710    #[error("invalid attachment encoding: {0}")]
711    InvalidAttachmentEncoding(String),
712    #[error("json error: {0}")]
713    Json(#[from] serde_json::Error),
714}
715
716#[cfg(test)]
717mod tests {
718    use super::{
719        EncryptionKind, FrameCodec, MAX_PAYLOAD_BYTES, MessageType, PacketBody, PacketEnvelope,
720        ProtocolError, decode_frame, encode_frame,
721    };
722    use serde_json::json;
723
724    const TEST_KEY: [u8; 32] = [0x11; 32];
725
726    #[test]
727    fn plaintext_helpers_still_work() {
728        let packet = PacketEnvelope::new(PacketBody::EventAck {
729            event_id: 1,
730            ok: true,
731            receipt: json!({ "ok": true }),
732            error: None,
733        });
734
735        let encoded = encode_frame(&packet).expect("encode plaintext");
736        let decoded = decode_frame(&encoded).expect("decode plaintext");
737        assert!(matches!(decoded.encryption, EncryptionKind::None));
738    }
739
740    #[test]
741    fn aes256_roundtrip_works() {
742        let codec = FrameCodec::plaintext().with_aes256_key(TEST_KEY);
743        let packet = PacketEnvelope::with_encryption(
744            PacketBody::ApiResponse {
745                request_id: 1,
746                ok: true,
747                status: 200,
748                data: json!({ "message": "encrypted" }),
749                error: None,
750                metadata: json!({}),
751            },
752            EncryptionKind::Aes256,
753        );
754
755        let encoded = codec.encode(&packet).expect("encode aes256");
756        let decoded = codec.decode(&encoded).expect("decode aes256");
757        assert!(matches!(decoded.encryption, EncryptionKind::Aes256));
758    }
759
760    #[test]
761    fn encode_rejects_payloads_over_limit() {
762        let codec = FrameCodec::plaintext();
763        let packet = PacketEnvelope::new(PacketBody::ApiResponse {
764            request_id: 999,
765            ok: true,
766            status: 200,
767            data: json!({ "blob": "a".repeat(10 * 1024 * 1024) }),
768            error: None,
769            metadata: json!({}),
770        });
771
772        let error = codec
773            .encode(&packet)
774            .expect_err("oversized payload should fail");
775        assert!(matches!(error, ProtocolError::PayloadTooLarge { .. }));
776    }
777
778    #[test]
779    fn decode_rejects_payloads_over_limit() {
780        let payload = vec![0_u8; MAX_PAYLOAD_BYTES + 1];
781        let frame_len = 2 + payload.len();
782        let mut frame = Vec::with_capacity(4 + frame_len);
783        frame.extend_from_slice(&(frame_len as u32).to_be_bytes());
784        frame.push(MessageType::Api as u8);
785        frame.push(EncryptionKind::None as u8);
786        frame.extend_from_slice(&payload);
787
788        let error = FrameCodec::plaintext()
789            .decode(&frame)
790            .expect_err("oversized payload should fail");
791        assert!(matches!(error, ProtocolError::PayloadTooLarge { .. }));
792    }
793}