use std::time::Duration;
use crate::error::PacketError;
use bytes::Bytes;
use bytestring::ByteString;
use serde::Deserialize;
pub const PROBE: ByteString = ByteString::from_static("probe");
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Message {
Text(ByteString),
Binary(Bytes),
Close,
}
impl std::fmt::Display for Message {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Text(s) => f.debug_tuple("Text").field(&format_args!("{}", s)).finish(),
Self::Binary(b) => f.debug_struct("Binary").field("len", &b.len()).finish(),
Self::Close => f.write_str("Close"),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Frame {
Packet(Packet),
Binary(Bytes),
}
impl From<Packet> for Frame {
fn from(packet: Packet) -> Self {
Frame::Packet(packet)
}
}
impl From<Bytes> for Frame {
fn from(bytes: Bytes) -> Self {
Frame::Binary(bytes)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
pub struct Handshake {
pub sid: ByteString,
pub upgrades: Vec<ByteString>,
#[serde(rename = "pingInterval")]
pub ping_interval: u64,
#[serde(rename = "pingTimeout")]
pub ping_timeout: u64,
#[serde(rename = "maxPayload")]
pub max_payload: u64,
}
impl Handshake {
pub fn can_upgrade_to_websocket(&self) -> bool {
self.upgrades.iter().any(|u| u == "websocket")
}
pub fn ping_window(&self) -> Duration {
Duration::from_millis(self.ping_interval + self.ping_timeout)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Packet {
Open(Handshake),
Close,
Ping(ByteString),
Pong(ByteString),
Message(ByteString),
Upgrade,
Noop,
}
impl Packet {
pub fn write(&self, buffer: &mut String) {
match self {
Self::Open(..) => panic!("client should never encode an Open packet"),
Self::Close => buffer.push('1'),
Self::Ping(payload) => {
buffer.reserve(1 + payload.len());
buffer.push('2');
buffer.push_str(payload);
}
Self::Pong(payload) => {
buffer.reserve(1 + payload.len());
buffer.push('3');
buffer.push_str(payload);
}
Self::Message(payload) => {
buffer.reserve(1 + payload.len());
buffer.push('4');
buffer.push_str(payload);
}
Self::Upgrade => buffer.push('5'),
Self::Noop => panic!("client should never encode a Noop packet"),
}
}
pub fn encode(&self) -> String {
let mut buffer = String::new();
self.write(&mut buffer);
buffer
}
pub fn decode(bytes: ByteString) -> Result<Self, PacketError> {
let mut chars = bytes.chars();
let id = match chars.next() {
Some(c) => c,
None => return Err(PacketError::Empty),
};
let rest = chars.as_str();
match id {
'0' => Ok(Packet::Open(serde_json::from_str(rest)?)),
'1' => {
if rest.is_empty() {
Ok(Packet::Close)
} else {
Err(PacketError::Payload {
id: '1',
payload: bytes.slice_ref(rest),
})
}
}
'2' => Ok(Packet::Ping(bytes.slice_ref(rest))),
'3' => Ok(Packet::Pong(bytes.slice_ref(rest))),
'4' => Ok(Packet::Message(bytes.slice_ref(rest))),
'5' => {
if rest.is_empty() {
Ok(Packet::Upgrade)
} else {
Err(PacketError::Payload {
id: '5',
payload: bytes.slice_ref(rest),
})
}
}
'6' => {
if rest.is_empty() {
Ok(Packet::Noop)
} else {
Err(PacketError::Payload {
id: '6',
payload: bytes.slice_ref(rest),
})
}
}
id => Err(PacketError::InvalidId { id })?,
}
}
}
impl std::fmt::Display for Packet {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Open(h) => f
.debug_tuple("Open")
.field(&format_args!("{}", h.sid))
.finish(),
Self::Close => f.write_str("Close"),
Self::Ping(s) => f.debug_tuple("Ping").field(&format_args!("{}", s)).finish(),
Self::Pong(s) => f.debug_tuple("Pong").field(&format_args!("{}", s)).finish(),
Self::Message(s) => f
.debug_tuple("Message")
.field(&format_args!("{}", s))
.finish(),
Self::Upgrade => f.write_str("Upgrade"),
Self::Noop => f.write_str("Noop"),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn test_handshake() -> Handshake {
Handshake {
sid: "abc".into(),
upgrades: vec!["websocket".into()],
ping_interval: 25000,
ping_timeout: 5000,
max_payload: 1_000_000,
}
}
#[test]
fn handshake_missing_required_field_is_error() {
let json = serde_json::json!({
"sid": "test-456",
"upgrades": [],
"pingInterval": 25000,
"pingTimeout": 20000
});
let bytes = ByteString::from(format!("0{}", json));
assert!(Packet::decode(bytes).is_err());
}
#[test]
fn handshake_ping_window() {
let hs = test_handshake();
assert_eq!(
hs.ping_window(),
std::time::Duration::from_millis(25000 + 5000)
);
}
#[test]
fn decode_invalid_packet_id() {
assert!(matches!(
Packet::decode(ByteString::from_static("9")),
Err(PacketError::InvalidId { id: '9' })
));
}
#[test]
fn decode_empty_packet() {
assert!(matches!(
Packet::decode(ByteString::new()),
Err(PacketError::Empty)
));
}
#[test]
fn decode_open() {
let hs = test_handshake();
let json = r#"{"sid":"abc","upgrades":["websocket"],"pingInterval":25000,"pingTimeout":5000,"maxPayload":1000000}"#;
let bytes = ByteString::from(format!("0{json}"));
assert_eq!(Packet::decode(bytes).unwrap(), Packet::Open(hs));
}
#[test]
fn encode_decode_close_round_trip() {
let encoded = Packet::Close.encode();
assert_eq!(
Packet::decode(ByteString::from(encoded)).unwrap(),
Packet::Close
);
}
#[test]
fn encode_decode_ping_round_trip() {
let payload = ByteString::from_static("probe");
let encoded = Packet::Ping(payload.clone()).encode();
assert_eq!(
Packet::decode(ByteString::from(encoded)).unwrap(),
Packet::Ping(payload)
);
}
#[test]
fn encode_decode_ping_empty_payload() {
let encoded = Packet::Ping(ByteString::new()).encode();
assert_eq!(
Packet::decode(ByteString::from(encoded)).unwrap(),
Packet::Ping(ByteString::new())
);
}
#[test]
fn encode_decode_pong_round_trip() {
let payload = ByteString::from_static("probe");
let encoded = Packet::Pong(payload.clone()).encode();
assert_eq!(
Packet::decode(ByteString::from(encoded)).unwrap(),
Packet::Pong(payload)
);
}
#[test]
fn encode_decode_message_round_trip() {
let text = ByteString::from_static("hello world");
let encoded = Packet::Message(text.clone()).encode();
assert_eq!(
Packet::decode(ByteString::from(encoded)).unwrap(),
Packet::Message(text)
);
}
#[test]
fn encode_decode_upgrade_round_trip() {
let encoded = Packet::Upgrade.encode();
assert_eq!(
Packet::decode(ByteString::from(encoded)).unwrap(),
Packet::Upgrade
);
}
#[test]
fn decode_noop() {
assert_eq!(
Packet::decode(ByteString::from_static("6")).unwrap(),
Packet::Noop
);
}
}