117 lines
3.3 KiB
Rust
117 lines
3.3 KiB
Rust
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<String>),
|
|
Pong(Option<String>),
|
|
Message(SocketPayload),
|
|
Upgrade,
|
|
Noop,
|
|
}
|
|
|
|
#[derive(Clone, Debug, PartialEq)]
|
|
pub enum SocketPayload {
|
|
Text(String),
|
|
Binary(Vec<u8>),
|
|
}
|
|
|
|
pub fn encode_engine_payload(
|
|
packets: &[EnginePacket],
|
|
polling: bool,
|
|
) -> String {
|
|
packets
|
|
.iter()
|
|
.map(|packet| encode_engine_packet(packet, polling))
|
|
.collect::<Vec<_>>()
|
|
.join(&RECORD_SEPARATOR.to_string())
|
|
}
|
|
|
|
pub fn decode_engine_payload(payload: &str) -> Result<Vec<EnginePacket>> {
|
|
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<EnginePacket> {
|
|
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<String> {
|
|
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);
|
|
}
|
|
}
|