gitdataai/lib/socketio/engine_packet.rs
2026-05-30 01:38:40 +08:00

122 lines
3.4 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(crate) enum EnginePacket {
Open(Value),
Close,
Ping(Option<String>),
Pong(Option<String>),
Message(SocketPayload),
Upgrade,
Noop,
}
#[derive(Clone, Debug, PartialEq)]
pub(crate) enum SocketPayload {
Text(String),
Binary(Vec<u8>),
}
pub(crate) 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(crate) 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(crate) 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(crate) 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);
}
}