use base64::{Engine, engine::general_purpose::STANDARD}; use serde_json::Value; use crate::error::{Result, SocketIoError}; const RECORD_SEPARATOR: char = '\x1e'; #[derive(Clone, Debug, PartialEq)] pub enum EnginePacket { Open(Value), Close, Ping(Option), Pong(Option), Message(SocketPayload), Upgrade, Noop, } #[derive(Clone, Debug, PartialEq)] pub enum SocketPayload { Text(String), Binary(Vec), } pub fn encode_engine_payload( packets: &[EnginePacket], polling: bool, ) -> String { packets .iter() .map(|packet| encode_engine_packet(packet, polling)) .collect::>() .join(&RECORD_SEPARATOR.to_string()) } pub fn decode_engine_payload(payload: &str) -> Result> { payload .split(RECORD_SEPARATOR) .filter(|item| !item.is_empty()) .map(decode_engine_text_packet) .collect() } pub fn encode_engine_packet(packet: &EnginePacket, _polling: bool) -> String { match packet { EnginePacket::Open(data) => format!("0{data}"), EnginePacket::Close => "1".to_owned(), EnginePacket::Ping(data) => { format!("2{}", data.as_deref().unwrap_or_default()) } EnginePacket::Pong(data) => { format!("3{}", data.as_deref().unwrap_or_default()) } EnginePacket::Message(SocketPayload::Text(text)) => format!("4{text}"), EnginePacket::Message(SocketPayload::Binary(bytes)) => { format!("b{}", STANDARD.encode(bytes)) } EnginePacket::Upgrade => "5".to_owned(), EnginePacket::Noop => "6".to_owned(), } } pub fn decode_engine_text_packet(input: &str) -> Result { if let Some(encoded) = input.strip_prefix('b') { return Ok(EnginePacket::Message(SocketPayload::Binary( STANDARD.decode(encoded).map_err(|_| { SocketIoError::InvalidPacket( "invalid base64 payload".to_owned(), ) })?, ))); } let mut chars = input.chars(); let packet_type = chars.next().ok_or_else(|| { SocketIoError::InvalidPacket("empty engine packet".to_owned()) })?; let rest = chars.as_str(); match packet_type { '0' => Ok(EnginePacket::Open(serde_json::from_str(rest)?)), '1' => Ok(EnginePacket::Close), '2' => Ok(EnginePacket::Ping(non_empty(rest))), '3' => Ok(EnginePacket::Pong(non_empty(rest))), '4' => Ok(EnginePacket::Message(SocketPayload::Text(rest.to_owned()))), '5' => Ok(EnginePacket::Upgrade), '6' => Ok(EnginePacket::Noop), _ => Err(SocketIoError::InvalidPacket(format!( "unknown engine packet type {packet_type}" ))), } } fn non_empty(value: &str) -> Option { if value.is_empty() { None } else { Some(value.to_owned()) } } #[cfg(test)] mod tests { use super::*; #[test] fn polling_payload_uses_record_separator() { let packets = vec![ EnginePacket::Message(SocketPayload::Text("40".to_owned())), EnginePacket::Message(SocketPayload::Text( "42[\"ready\",null]".to_owned(), )), ]; let encoded = encode_engine_payload(&packets, true); assert_eq!(decode_engine_payload(&encoded).unwrap(), packets); } }