use serde_json::Value; const MAX_ATTACHMENTS: usize = 10; #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub(crate) enum Kind { Connect, Disconnect, Event, Ack, ConnectError, BinaryEvent, BinaryAck, } impl Kind { fn from_char(value: char) -> Option { match value { '0' => Some(Self::Connect), '1' => Some(Self::Disconnect), '2' => Some(Self::Event), '3' => Some(Self::Ack), '4' => Some(Self::ConnectError), '5' => Some(Self::BinaryEvent), '6' => Some(Self::BinaryAck), _ => None, } } fn code(self) -> char { match self { Self::Connect => '0', Self::Disconnect => '1', Self::Event => '2', Self::Ack => '3', Self::ConnectError => '4', Self::BinaryEvent => '5', Self::BinaryAck => '6', } } fn is_binary(self) -> bool { matches!(self, Self::BinaryEvent | Self::BinaryAck) } } #[derive(Debug, Clone)] pub(crate) struct Packet { pub kind: Kind, pub namespace: String, pub id: Option, pub data: Value, pub attachments: usize, } pub(crate) fn decode(text: &str) -> Option { let mut chars = text.chars(); let kind = Kind::from_char(chars.next()?)?; let mut rest = chars.as_str(); let mut attachments = 0; if kind.is_binary() { let (count, tail) = rest.split_once('-')?; attachments = count.parse().ok()?; if !(1..=MAX_ATTACHMENTS).contains(&attachments) { return None; } rest = tail; } let mut namespace = "/".to_string(); if let Some(tail) = rest.strip_prefix('/') { let (name, tail) = tail.split_once(',').unwrap_or((tail, "")); namespace = format!("/{name}"); rest = tail; } let digits = rest .bytes() .take_while(|byte| byte.is_ascii_digit()) .count(); let (id, payload) = rest.split_at(digits); let id = if id.is_empty() { None } else { Some(id.parse().ok()?) }; let data = if payload.is_empty() { Value::Null } else { serde_json::from_str(payload).ok()? }; let valid = match kind { Kind::Connect => data.is_null() || data.is_object(), Kind::Disconnect => data.is_null(), Kind::Event | Kind::Ack | Kind::BinaryEvent | Kind::BinaryAck => data.is_array(), Kind::ConnectError => data.is_object(), }; valid.then_some(Packet { kind, namespace, id, data, attachments, }) } pub(crate) fn encode_connect(namespace: &str, sid: &str, device_id: &str) -> String { format!( "{}{}", prefix(Kind::Connect, namespace, None, 0), serde_json::json!({ "sid": sid, "device_id": device_id }) ) } pub(crate) fn encode_connect_error(namespace: &str, message: &str) -> String { format!( "{}{}", prefix(Kind::ConnectError, namespace, None, 0), serde_json::json!({ "message": message }) ) } pub(crate) fn encode_event(namespace: &str, id: Option, data: &[Value]) -> String { format!( "{}{}", prefix(Kind::Event, namespace, id, 0), Value::Array(data.to_vec()) ) } pub(crate) fn encode_ack(namespace: &str, id: u64, data: &[Value]) -> String { format!( "{}{}", prefix(Kind::Ack, namespace, Some(id), 0), Value::Array(data.to_vec()) ) } pub(crate) fn encode_binary_event( namespace: &str, id: Option, data: &[Value], attachments: usize, ) -> String { format!( "{}{}", prefix(Kind::BinaryEvent, namespace, id, attachments), Value::Array(data.to_vec()) ) } pub(crate) fn encode_binary_ack( namespace: &str, id: u64, data: &[Value], attachments: usize, ) -> String { format!( "{}{}", prefix(Kind::BinaryAck, namespace, Some(id), attachments), Value::Array(data.to_vec()) ) } fn prefix(kind: Kind, namespace: &str, id: Option, attachments: usize) -> String { let mut text = String::new(); text.push(kind.code()); if kind.is_binary() { text.push_str(&attachments.to_string()); text.push('-'); } if namespace != "/" { text.push_str(namespace); text.push(','); } if let Some(id) = id { text.push_str(&id.to_string()); } text }