const ACTION_SEND_BINARY: u8 = 0;
const ACTION_SEND_TEXT: u8 = 1;
const ACTION_EMIT_EVENT: u8 = 2;
const ACTION_DOWNLOAD_SNAPSHOT: u8 = 3;
#[derive(Debug)]
pub enum DecodedAction {
SendBinary(Vec<u8>),
SendText(String),
EmitEvent(Vec<u8>),
DownloadSnapshot(String),
}
pub fn decode_actions(data: &[u8]) -> Result<Vec<DecodedAction>, String> {
if data.len() < 2 {
return Err("Buffer too short for action count".into());
}
let num = u16::from_le_bytes([data[0], data[1]]) as usize;
let mut offset = 2;
let mut actions = Vec::with_capacity(num);
for _ in 0..num {
if offset >= data.len() {
return Err("Unexpected end of buffer".into());
}
let action_type = data[offset];
offset += 1;
if offset + 4 > data.len() {
return Err("Unexpected end of buffer reading payload length".into());
}
let payload_len = u32::from_le_bytes([
data[offset],
data[offset + 1],
data[offset + 2],
data[offset + 3],
]) as usize;
offset += 4;
if offset + payload_len > data.len() {
return Err("Payload exceeds buffer".into());
}
let payload = &data[offset..offset + payload_len];
offset += payload_len;
let action = match action_type {
ACTION_SEND_BINARY => DecodedAction::SendBinary(payload.to_vec()),
ACTION_SEND_TEXT => {
let text = String::from_utf8(payload.to_vec())
.map_err(|e| format!("Invalid UTF-8 in SendText: {e}"))?;
DecodedAction::SendText(text)
}
ACTION_EMIT_EVENT => DecodedAction::EmitEvent(payload.to_vec()),
ACTION_DOWNLOAD_SNAPSHOT => {
let id = String::from_utf8(payload.to_vec())
.map_err(|e| format!("Invalid UTF-8 in DownloadSnapshot: {e}"))?;
DecodedAction::DownloadSnapshot(id)
}
other => return Err(format!("Unknown action type: {other}")),
};
actions.push(action);
}
Ok(actions)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn decode_empty() {
let data = [0u8, 0]; let actions = decode_actions(&data).unwrap();
assert!(actions.is_empty());
}
#[test]
fn decode_send_binary() {
let mut data = Vec::new();
data.extend_from_slice(&1u16.to_le_bytes()); data.push(ACTION_SEND_BINARY);
data.extend_from_slice(&4u32.to_le_bytes()); data.extend_from_slice(&[1, 2, 3, 4]);
let actions = decode_actions(&data).unwrap();
assert_eq!(actions.len(), 1);
match &actions[0] {
DecodedAction::SendBinary(bytes) => assert_eq!(bytes, &[1, 2, 3, 4]),
other => panic!("Expected SendBinary, got {other:?}"),
}
}
#[test]
fn decode_mixed() {
let mut data = Vec::new();
data.extend_from_slice(&2u16.to_le_bytes());
data.push(ACTION_SEND_TEXT);
data.extend_from_slice(&2u32.to_le_bytes());
data.extend_from_slice(b"hi");
data.push(ACTION_DOWNLOAD_SNAPSHOT);
data.extend_from_slice(&4u32.to_le_bytes());
data.extend_from_slice(b"ws-1");
let actions = decode_actions(&data).unwrap();
assert_eq!(actions.len(), 2);
match &actions[0] {
DecodedAction::SendText(t) => assert_eq!(t, "hi"),
other => panic!("Expected SendText, got {other:?}"),
}
match &actions[1] {
DecodedAction::DownloadSnapshot(id) => assert_eq!(id, "ws-1"),
other => panic!("Expected DownloadSnapshot, got {other:?}"),
}
}
#[test]
fn decode_truncated() {
let data = [1u8, 0]; assert!(decode_actions(&data).is_err());
}
}