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, pub data: Option, pub attachments: Vec>, pub expected_attachments: usize, } #[derive(Clone, Debug)] pub struct EventPayload { pub event: String, pub args: Vec, pub binary: Vec>, pub ack_id: Option, pub ack: Option, } impl Packet { pub fn connect(namespace: impl Into, data: Option) -> Self { Self::new(PacketType::Connect, namespace, None, data) } pub fn event( namespace: impl Into, event: &str, args: Vec, ) -> 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, id: u64, args: Vec, ) -> Self { Self::new( PacketType::Ack, namespace, Some(id), Some(Value::Array(args)), ) } pub fn connect_error( namespace: impl Into, message: impl Into, ) -> Self { Self::new( PacketType::ConnectError, namespace, None, Some(json!({ "message": message.into() })), ) } pub fn new( packet_type: PacketType, namespace: impl Into, id: Option, data: Option, ) -> Self { Self { packet_type, namespace: namespace.into(), id, data, attachments: Vec::new(), expected_attachments: 0, } } pub fn with_binary(mut self, attachments: Vec>) -> 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 { 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, ) -> Result { 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 { 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"); } }