355 lines
9.8 KiB
Rust
355 lines
9.8 KiB
Rust
use serde_json::{Value, json};
|
|
|
|
use crate::error::{Result, SocketIoError};
|
|
|
|
#[derive(
|
|
Clone, Copy, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize,
|
|
)]
|
|
pub enum PacketType {
|
|
Connect,
|
|
Disconnect,
|
|
Event,
|
|
Ack,
|
|
ConnectError,
|
|
BinaryEvent,
|
|
BinaryAck,
|
|
}
|
|
|
|
#[derive(Clone, Debug, PartialEq)]
|
|
pub struct Packet {
|
|
pub packet_type: PacketType,
|
|
pub namespace: String,
|
|
pub id: Option<u64>,
|
|
pub data: Option<Value>,
|
|
pub attachments: Vec<Vec<u8>>,
|
|
pub expected_attachments: usize,
|
|
}
|
|
|
|
#[derive(Clone, Debug)]
|
|
pub struct EventPayload {
|
|
pub event: String,
|
|
pub args: Vec<Value>,
|
|
pub binary: Vec<Vec<u8>>,
|
|
pub ack_id: Option<u64>,
|
|
pub ack: Option<crate::socket::AckSender>,
|
|
}
|
|
|
|
impl Packet {
|
|
pub fn connect(namespace: impl Into<String>, data: Option<Value>) -> Self {
|
|
Self::new(PacketType::Connect, namespace, None, data)
|
|
}
|
|
|
|
pub fn event(
|
|
namespace: impl Into<String>,
|
|
event: &str,
|
|
args: Vec<Value>,
|
|
) -> Self {
|
|
let mut data = Vec::with_capacity(args.len() + 1);
|
|
data.push(Value::String(event.to_owned()));
|
|
data.extend(args);
|
|
Self::new(PacketType::Event, namespace, None, Some(Value::Array(data)))
|
|
}
|
|
|
|
pub fn ack(
|
|
namespace: impl Into<String>,
|
|
id: u64,
|
|
args: Vec<Value>,
|
|
) -> Self {
|
|
Self::new(
|
|
PacketType::Ack,
|
|
namespace,
|
|
Some(id),
|
|
Some(Value::Array(args)),
|
|
)
|
|
}
|
|
|
|
pub fn connect_error(
|
|
namespace: impl Into<String>,
|
|
message: impl Into<String>,
|
|
) -> Self {
|
|
Self::new(
|
|
PacketType::ConnectError,
|
|
namespace,
|
|
None,
|
|
Some(json!({ "message": message.into() })),
|
|
)
|
|
}
|
|
|
|
pub fn new(
|
|
packet_type: PacketType,
|
|
namespace: impl Into<String>,
|
|
id: Option<u64>,
|
|
data: Option<Value>,
|
|
) -> Self {
|
|
Self {
|
|
packet_type,
|
|
namespace: namespace.into(),
|
|
id,
|
|
data,
|
|
attachments: Vec::new(),
|
|
expected_attachments: 0,
|
|
}
|
|
}
|
|
|
|
pub fn with_binary(mut self, attachments: Vec<Vec<u8>>) -> Self {
|
|
self.expected_attachments = attachments.len();
|
|
if self.expected_attachments > 0
|
|
&& let Some(Value::Array(values)) = &mut self.data
|
|
{
|
|
for num in 0..self.expected_attachments {
|
|
values.push(json!({ "_placeholder": true, "num": num }));
|
|
}
|
|
}
|
|
self.attachments = attachments;
|
|
self.packet_type = match self.packet_type {
|
|
PacketType::Ack => PacketType::BinaryAck,
|
|
PacketType::Event => PacketType::BinaryEvent,
|
|
other => other,
|
|
};
|
|
self
|
|
}
|
|
|
|
pub fn encode(&self) -> String {
|
|
let mut out = String::new();
|
|
out.push(packet_type_digit(self.packet_type));
|
|
|
|
if matches!(
|
|
self.packet_type,
|
|
PacketType::BinaryEvent | PacketType::BinaryAck
|
|
) {
|
|
out.push_str(&self.expected_attachments.to_string());
|
|
out.push('-');
|
|
}
|
|
|
|
if self.namespace != "/" {
|
|
out.push_str(&self.namespace);
|
|
out.push(',');
|
|
}
|
|
|
|
if let Some(id) = self.id {
|
|
out.push_str(&id.to_string());
|
|
}
|
|
|
|
if let Some(data) = &self.data {
|
|
out.push_str(&data.to_string());
|
|
}
|
|
|
|
out
|
|
}
|
|
|
|
pub fn decode(input: &str) -> Result<Self> {
|
|
let mut chars = input.char_indices();
|
|
let (_, first) = chars.next().ok_or_else(|| {
|
|
SocketIoError::InvalidPacket("empty packet".to_owned())
|
|
})?;
|
|
let packet_type = packet_type_from_digit(first)?;
|
|
let mut index = first.len_utf8();
|
|
let mut expected_attachments = 0;
|
|
|
|
if matches!(
|
|
packet_type,
|
|
PacketType::BinaryEvent | PacketType::BinaryAck
|
|
) {
|
|
let rest = &input[index..];
|
|
let dash = rest.find('-').ok_or_else(|| {
|
|
SocketIoError::InvalidPacket(
|
|
"binary packet missing attachment count".to_owned(),
|
|
)
|
|
})?;
|
|
expected_attachments = rest[..dash].parse().map_err(|_| {
|
|
SocketIoError::InvalidPacket(
|
|
"invalid attachment count".to_owned(),
|
|
)
|
|
})?;
|
|
index += dash + 1;
|
|
}
|
|
|
|
let namespace = if input[index..].starts_with('/') {
|
|
let rest = &input[index..];
|
|
if let Some(comma) = rest.find(',') {
|
|
index += comma + 1;
|
|
rest[..comma].to_owned()
|
|
} else {
|
|
index = input.len();
|
|
rest.to_owned()
|
|
}
|
|
} else {
|
|
"/".to_owned()
|
|
};
|
|
|
|
let id_start = index;
|
|
while let Some(ch) = input[index..].chars().next() {
|
|
if ch.is_ascii_digit() {
|
|
index += ch.len_utf8();
|
|
} else {
|
|
break;
|
|
}
|
|
}
|
|
let id = if index > id_start {
|
|
Some(input[id_start..index].parse().map_err(|_| {
|
|
SocketIoError::InvalidPacket(
|
|
"invalid acknowledgment id".to_owned(),
|
|
)
|
|
})?)
|
|
} else {
|
|
None
|
|
};
|
|
|
|
let data = if index < input.len() {
|
|
Some(serde_json::from_str(&input[index..])?)
|
|
} else {
|
|
None
|
|
};
|
|
|
|
Ok(Self {
|
|
packet_type,
|
|
namespace,
|
|
id,
|
|
data,
|
|
attachments: Vec::new(),
|
|
expected_attachments,
|
|
})
|
|
}
|
|
|
|
pub fn into_event_payload(
|
|
self,
|
|
ack: Option<crate::socket::AckSender>,
|
|
) -> Result<EventPayload> {
|
|
let values = match self.data {
|
|
Some(Value::Array(values)) if !values.is_empty() => values,
|
|
_ => {
|
|
return Err(SocketIoError::InvalidPacket(
|
|
"event payload must be a non-empty array".to_owned(),
|
|
));
|
|
}
|
|
};
|
|
let mut values = values.into_iter();
|
|
let event = values
|
|
.next()
|
|
.and_then(|value| value.as_str().map(ToOwned::to_owned))
|
|
.ok_or_else(|| {
|
|
SocketIoError::InvalidPacket(
|
|
"event name must be a string".to_owned(),
|
|
)
|
|
})?;
|
|
|
|
let args = values.filter(|value| !is_placeholder(value)).collect();
|
|
|
|
Ok(EventPayload {
|
|
event,
|
|
args,
|
|
binary: self.attachments,
|
|
ack_id: self.id,
|
|
ack,
|
|
})
|
|
}
|
|
}
|
|
|
|
fn is_placeholder(value: &Value) -> bool {
|
|
value
|
|
.as_object()
|
|
.and_then(|object| object.get("_placeholder"))
|
|
.and_then(Value::as_bool)
|
|
.unwrap_or(false)
|
|
}
|
|
|
|
fn packet_type_digit(packet_type: PacketType) -> char {
|
|
match packet_type {
|
|
PacketType::Connect => '0',
|
|
PacketType::Disconnect => '1',
|
|
PacketType::Event => '2',
|
|
PacketType::Ack => '3',
|
|
PacketType::ConnectError => '4',
|
|
PacketType::BinaryEvent => '5',
|
|
PacketType::BinaryAck => '6',
|
|
}
|
|
}
|
|
|
|
fn packet_type_from_digit(value: char) -> Result<PacketType> {
|
|
match value {
|
|
'0' => Ok(PacketType::Connect),
|
|
'1' => Ok(PacketType::Disconnect),
|
|
'2' => Ok(PacketType::Event),
|
|
'3' => Ok(PacketType::Ack),
|
|
'4' => Ok(PacketType::ConnectError),
|
|
'5' => Ok(PacketType::BinaryEvent),
|
|
'6' => Ok(PacketType::BinaryAck),
|
|
_ => Err(SocketIoError::InvalidPacket(format!(
|
|
"unknown socket packet type {value}"
|
|
))),
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn event_packet_round_trips() {
|
|
let packet =
|
|
Packet::event("/", "message", vec![json!({ "body": "hello" })]);
|
|
assert_eq!(packet.encode(), "2[\"message\",{\"body\":\"hello\"}]");
|
|
|
|
let decoded = Packet::decode(&packet.encode()).unwrap();
|
|
assert_eq!(decoded.packet_type, PacketType::Event);
|
|
assert_eq!(decoded.namespace, "/");
|
|
assert_eq!(decoded.data, packet.data);
|
|
}
|
|
|
|
#[test]
|
|
fn event_packet_accepts_namespace_and_ack_id() {
|
|
let decoded =
|
|
Packet::decode("2/admin,17[\"save\",{\"ok\":true}]").unwrap();
|
|
assert_eq!(decoded.packet_type, PacketType::Event);
|
|
assert_eq!(decoded.namespace, "/admin");
|
|
assert_eq!(decoded.id, Some(17));
|
|
}
|
|
|
|
#[test]
|
|
fn binary_packet_accepts_attachment_count() {
|
|
let decoded = Packet::decode(
|
|
"51-/admin,13[\"file\",{\"_placeholder\":true,\"num\":0}]",
|
|
)
|
|
.unwrap();
|
|
assert_eq!(decoded.packet_type, PacketType::BinaryEvent);
|
|
assert_eq!(decoded.expected_attachments, 1);
|
|
assert_eq!(decoded.namespace, "/admin");
|
|
assert_eq!(decoded.id, Some(13));
|
|
}
|
|
|
|
#[test]
|
|
fn binary_emit_adds_placeholders() {
|
|
let packet = Packet::event("/", "file", vec![json!("meta")])
|
|
.with_binary(vec![vec![1, 2]]);
|
|
assert_eq!(
|
|
packet.encode(),
|
|
"51-[\"file\",\"meta\",{\"_placeholder\":true,\"num\":0}]"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn decode_empty_packet_returns_error() {
|
|
assert!(Packet::decode("").is_err());
|
|
}
|
|
|
|
#[test]
|
|
fn decode_unknown_type_returns_error() {
|
|
assert!(Packet::decode("9[]").is_err());
|
|
}
|
|
|
|
#[test]
|
|
fn default_namespace_omits_prefix_in_encode() {
|
|
let packet = Packet::event("/", "ping", vec![]);
|
|
assert_eq!(packet.encode(), "2[\"ping\"]");
|
|
}
|
|
|
|
#[test]
|
|
fn custom_namespace_includes_trailing_comma() {
|
|
let packet = Packet::event("/chat", "msg", vec![json!("hi")]);
|
|
let encoded = packet.encode();
|
|
assert!(encoded.starts_with("2/chat,"));
|
|
let decoded = Packet::decode(&encoded).unwrap();
|
|
assert_eq!(decoded.namespace, "/chat");
|
|
}
|
|
}
|