use anyhow::{bail, Result}; use atoi::FromRadix16; use tokio::io::{AsyncRead, AsyncReadExt as _}; #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum RequestKind { Signal, Command, } impl RequestKind { fn name(self) -> &'static str { match self { Self::Signal => "Signal", Self::Command => "Command", } } } pub struct Request<'a> { pub kind: RequestKind, buffer: &'a [u8], } impl std::fmt::Debug for Request<'_> { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { let mut ds = f.debug_tuple(self.kind.name()); match self.kind { RequestKind::Signal => ds.field(&self.buffer), RequestKind::Command => match std::str::from_utf8(self.buffer) { Ok(str) => ds.field(&str), Err(_) => ds.field(&self.buffer), }, }; ds.finish() } } impl<'a> Request<'a> { pub async fn read( reader: &mut R, buffer: &'a mut Vec, ) -> Result { buffer.clear(); let mut char = reader.read_u8().await?; while char == b'+' { // just ignore positive acks char = reader.read_u8().await?; } if char == b'-' { bail!("no support for negative acks"); } if char == 0x03 { // This is how the client "cancels an in-flight request" buffer.push(char); return Ok(Self { kind: RequestKind::Signal, buffer, }); } if char != b'$' { // Messages are supposed to start with a dollar sign bail!("malformed message"); } // now read the body let mut checksum = 0u8; char = reader.read_u8().await?; while char != b'#' { if char == b'}' { // escape character checksum = checksum.wrapping_add(char); char = reader.read_u8().await?; checksum = checksum.wrapping_add(char); buffer.push(char ^ 0x20); } else { checksum = checksum.wrapping_add(char); buffer.push(char); } char = reader.read_u8().await?; } let mut checksum_bytes = [b'0'; 2]; reader.read_exact(&mut checksum_bytes).await?; let (real_checksum, 2) = u8::from_radix_16(&checksum_bytes) else { bail!("invalid checksum"); }; if checksum != real_checksum { bail!("mismatched checksum"); } Ok(Self { kind: RequestKind::Command, buffer, }) } pub fn match_str(&mut self, prefix: &str) -> bool { if let Some(new_buffer) = self.buffer.strip_prefix(prefix.as_bytes()) { self.buffer = new_buffer; return true; } false } pub fn match_hex(&mut self) -> Option { match I::from_radix_16(self.buffer) { (_, 0) => None, (val, used) => { self.buffer = self.buffer.split_at(used).1; Some(val) } } } }