use thiserror::Error;
pub const HEAD: [u8; 2] = [0x55, 0x55];
pub const TAIL: [u8; 2] = [0xAA, 0xAA];
pub const MAX_DATA_LEN: usize = u8::MAX as usize;
pub const FRAME_OVERHEAD: usize = 7;
pub const MAX_FRAME_LEN: usize = FRAME_OVERHEAD + MAX_DATA_LEN;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Packet {
pub cmd: u8,
pub data: Vec<u8>,
}
#[derive(Debug, Error)]
pub enum PacketError {
#[error("packet too short ({0} bytes)")]
TooShort(usize),
#[error("invalid head (expected 55 55)")]
BadHead,
#[error("invalid tail (expected AA AA)")]
BadTail,
#[error("length field {claimed} does not match buffer")]
BadLength { claimed: usize },
#[error("checksum mismatch: got {got:#04x}, expected {expected:#04x}")]
BadChecksum { got: u8, expected: u8 },
#[error("data is {len} bytes; the LEN field allows at most {MAX_DATA_LEN}")]
DataTooLong { len: usize },
}
impl Packet {
pub fn new(cmd: u8, data: impl Into<Vec<u8>>) -> Self {
Self {
cmd,
data: data.into(),
}
}
pub fn try_new(cmd: u8, data: impl Into<Vec<u8>>) -> Result<Self, PacketError> {
let data = data.into();
if data.len() > MAX_DATA_LEN {
return Err(PacketError::DataTooLong { len: data.len() });
}
Ok(Self { cmd, data })
}
pub fn checksum(cmd: u8, len: u8, data: &[u8]) -> u8 {
let mut c = cmd ^ len;
for b in data {
c ^= *b;
}
c
}
pub fn encode(&self) -> Result<Vec<u8>, PacketError> {
let len = u8::try_from(self.data.len()).map_err(|_| PacketError::DataTooLong {
len: self.data.len(),
})?;
let sum = Self::checksum(self.cmd, len, &self.data);
let mut out = Vec::with_capacity(7 + self.data.len());
out.extend_from_slice(&HEAD);
out.push(self.cmd);
out.push(len);
out.extend_from_slice(&self.data);
out.push(sum);
out.extend_from_slice(&TAIL);
Ok(out)
}
pub(crate) fn encode_into<'a>(
&self,
out: &'a mut [u8; MAX_FRAME_LEN],
) -> Result<&'a [u8], PacketError> {
let len = u8::try_from(self.data.len()).map_err(|_| PacketError::DataTooLong {
len: self.data.len(),
})?;
let data_end = 4 + self.data.len();
let frame_len = FRAME_OVERHEAD + self.data.len();
out[..2].copy_from_slice(&HEAD);
out[2] = self.cmd;
out[3] = len;
out[4..data_end].copy_from_slice(&self.data);
out[data_end] = Self::checksum(self.cmd, len, &self.data);
out[data_end + 1..frame_len].copy_from_slice(&TAIL);
Ok(&out[..frame_len])
}
pub fn decode(buf: &[u8]) -> Result<Self, PacketError> {
if buf.len() < 7 {
return Err(PacketError::TooShort(buf.len()));
}
if buf[0] != HEAD[0] || buf[1] != HEAD[1] {
return Err(PacketError::BadHead);
}
let cmd = buf[2];
let len = buf[3] as usize;
let total = 7 + len;
if buf.len() < total {
return Err(PacketError::BadLength { claimed: len });
}
if buf[total - 2] != TAIL[0] || buf[total - 1] != TAIL[1] {
return Err(PacketError::BadTail);
}
let data = &buf[4..4 + len];
let got = buf[4 + len];
let expected = Self::checksum(cmd, len as u8, data);
if got != expected {
return Err(PacketError::BadChecksum { got, expected });
}
Ok(Self {
cmd,
data: data.to_vec(),
})
}
pub fn drain_buffer(buf: &mut Vec<u8>) -> Vec<Self> {
PacketDecoder::drain(buf)
}
}
#[derive(Debug, Default)]
pub struct PacketDecoder {
buf: Vec<u8>,
}
impl PacketDecoder {
pub fn new() -> Self {
Self::default()
}
pub fn push(&mut self, bytes: &[u8]) -> Vec<Packet> {
let mut packets = Vec::new();
for chunk in bytes.chunks(MAX_FRAME_LEN) {
self.buf.extend_from_slice(chunk);
Self::drain_into(&mut self.buf, &mut packets);
}
packets
}
pub fn buffered_len(&self) -> usize {
self.buf.len()
}
fn drain(buf: &mut Vec<u8>) -> Vec<Packet> {
let mut packets = Vec::new();
Self::drain_into(buf, &mut packets);
packets
}
fn drain_into(buf: &mut Vec<u8>, packets: &mut Vec<Packet>) {
let mut consumed = 0;
loop {
let remaining = &buf[consumed..];
let Some(pos) = remaining.windows(2).position(|w| w == HEAD) else {
consumed = buf.len() - usize::from(remaining.last() == Some(&HEAD[0]));
break;
};
consumed += pos;
let remaining = &buf[consumed..];
if remaining.len() < FRAME_OVERHEAD {
break;
}
let total = FRAME_OVERHEAD + usize::from(remaining[3]);
if remaining.len() < total {
break;
}
match Packet::decode(&remaining[..total]) {
Ok(packet) => {
packets.push(packet);
consumed += total;
}
Err(_) => consumed += 1,
}
}
buf.drain(..consumed);
debug_assert!(buf.len() < MAX_FRAME_LEN);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn large_input_does_not_leave_a_large_receive_allocation() {
let packet = Packet::new(0x85, vec![0x42; MAX_DATA_LEN]);
let mut bytes = packet.encode().unwrap().repeat(4_096);
bytes.push(HEAD[0]);
let mut decoder = PacketDecoder::new();
let packets = decoder.push(&bytes);
assert_eq!(packets.len(), 4_096);
assert!(packets.iter().all(|p| p == &packet));
assert_eq!(decoder.buffered_len(), 1);
assert!(decoder.buf.capacity() <= MAX_FRAME_LEN * 4);
assert!(decoder.push(&[]).is_empty());
assert_eq!(decoder.buffered_len(), 1);
let frame = packet.encode().unwrap();
assert_eq!(decoder.push(&frame[1..]), vec![packet]);
assert_eq!(decoder.buffered_len(), 0);
}
#[test]
fn encode_simple_rfid() {
let p = Packet::new(0x1a, vec![0x01]);
assert_eq!(
p.encode().unwrap(),
hex::decode("55551a01011aaaaa").unwrap()
);
}
#[test]
fn roundtrip() {
let p = Packet::new(0x40, vec![0x0b]);
let enc = p.encode().unwrap();
let d = Packet::decode(&enc).unwrap();
assert_eq!(d, p);
}
#[test]
fn drain_fragmented() {
let a = Packet::new(0x1a, vec![0x01]).encode().unwrap();
let b = Packet::new(0x40, vec![0x0b]).encode().unwrap();
let mut buf = Vec::new();
buf.extend_from_slice(&a[..4]);
assert!(Packet::drain_buffer(&mut buf).is_empty());
buf.extend_from_slice(&a[4..]);
buf.extend_from_slice(&b);
let pkts = Packet::drain_buffer(&mut buf);
assert_eq!(pkts.len(), 2);
assert_eq!(pkts[0].cmd, 0x1a);
assert_eq!(pkts[1].cmd, 0x40);
assert!(buf.is_empty());
}
#[test]
fn decoder_preserves_header_split_between_reads() {
let packet = Packet::new(0x40, vec![0x0b]);
let encoded = packet.encode().unwrap();
let mut decoder = PacketDecoder::new();
assert!(decoder.push(&encoded[..1]).is_empty());
assert_eq!(decoder.buffered_len(), 1);
assert_eq!(decoder.push(&encoded[1..]), vec![packet]);
}
#[test]
fn decoder_accepts_every_single_split_point() {
let packet = Packet::new(0x40, vec![0x0b, 0x55, 0xaa]);
let encoded = packet.encode().unwrap();
for split in 0..=encoded.len() {
let mut decoder = PacketDecoder::new();
let mut got = decoder.push(&encoded[..split]);
got.extend(decoder.push(&encoded[split..]));
assert_eq!(got, vec![packet.clone()], "split at {split}");
}
}
#[test]
fn print_start_b1_seven_bytes() {
let data = [0x00u8, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00];
let p = Packet::new(0x01, data);
let enc = p.encode().unwrap();
assert_eq!(&enc[..2], &[0x55, 0x55]);
assert_eq!(enc[2], 0x01);
assert_eq!(enc[3], 7);
assert_eq!(&enc[enc.len() - 2..], &[0xAA, 0xAA]);
assert_eq!(Packet::decode(&enc).unwrap(), p);
}
#[test]
fn set_page_size_6b_checksum() {
let mut data = Vec::new();
data.extend_from_slice(&240u16.to_be_bytes());
data.extend_from_slice(&384u16.to_be_bytes());
data.extend_from_slice(&1u16.to_be_bytes());
let p = Packet::new(0x13, data);
let enc = p.encode().unwrap();
assert_eq!(enc[3], 6);
assert_eq!(Packet::decode(&enc).unwrap().data.len(), 6);
}
#[test]
fn oversized_payload_is_rejected_not_truncated() {
let p = Packet::new(0x85, vec![0xAB; 300]);
assert!(matches!(
p.encode(),
Err(PacketError::DataTooLong { len: 300 })
));
assert!(matches!(
Packet::try_new(0x85, vec![0xAB; 300]),
Err(PacketError::DataTooLong { len: 300 })
));
}
#[test]
fn max_length_payload_still_roundtrips() {
let p = Packet::try_new(0x85, vec![0xAB; MAX_DATA_LEN]).expect("255 bytes fits");
let enc = p.encode().unwrap();
assert_eq!(enc[3] as usize, MAX_DATA_LEN);
assert_eq!(Packet::decode(&enc).unwrap(), p);
}
#[test]
fn fixed_buffer_encoding_matches_allocating_encoding_at_every_length() {
let mut frame = [0xCC; MAX_FRAME_LEN];
for len in 0..=MAX_DATA_LEN {
let data: Vec<u8> = (0..len).map(|i| (i as u8).wrapping_mul(37)).collect();
let packet = Packet::new((len as u8).wrapping_mul(11), data);
let expected = packet.encode().unwrap();
let actual = packet.encode_into(&mut frame).unwrap();
assert_eq!(actual, expected, "payload length {len}");
}
}
#[test]
fn fixed_buffer_can_be_reused_and_rejects_oversized_payloads() {
let mut frame = [0xCC; MAX_FRAME_LEN];
let longest = Packet::new(0x85, vec![0xAB; MAX_DATA_LEN]);
assert_eq!(
longest.encode_into(&mut frame).unwrap(),
longest.encode().unwrap()
);
let shortest = Packet::new(0x1A, [0x01]);
assert_eq!(
shortest.encode_into(&mut frame).unwrap(),
hex::decode("55551a01011aaaaa").unwrap()
);
let before = frame;
let oversized = Packet::new(0x85, vec![0xAB; MAX_DATA_LEN + 1]);
assert!(matches!(
oversized.encode_into(&mut frame),
Err(PacketError::DataTooLong { len }) if len == MAX_DATA_LEN + 1
));
assert_eq!(frame, before, "failed encoding must not alter the buffer");
}
#[test]
fn drain_garbage_never_panics_and_clears() {
let mut buf = vec![0u8, 1, 2, 0xAA, 0x55, 0xFF];
let pkts = Packet::drain_buffer(&mut buf);
assert!(pkts.is_empty());
assert!(buf.is_empty());
}
#[test]
fn drain_recovers_valid_after_noise() {
let good = Packet::new(0x1a, vec![0x01]).encode().unwrap();
let mut buf = vec![0xDE, 0xAD, 0xBE, 0xEF];
buf.extend_from_slice(&good);
let pkts = Packet::drain_buffer(&mut buf);
assert_eq!(pkts.len(), 1);
assert_eq!(pkts[0].cmd, 0x1a);
assert!(buf.is_empty());
}
#[test]
fn drain_bad_checksum_skips_and_continues() {
let good = Packet::new(0x40, vec![0x0b]).encode().unwrap();
let mut bad = good.clone();
let cs = bad.len() - 3;
bad[cs] ^= 0xFF;
let mut buf = bad;
buf.extend_from_slice(&good);
let pkts = Packet::drain_buffer(&mut buf);
assert_eq!(pkts.len(), 1);
assert_eq!(pkts[0].cmd, 0x40);
}
#[test]
fn prop_drain_random_noise_no_panic() {
use proptest::prelude::*;
proptest::proptest!(|(noise in prop::collection::vec(any::<u8>(), 0..512))| {
let mut buf = noise;
let pkts = Packet::drain_buffer(&mut buf);
for p in pkts {
let enc = p.encode().unwrap();
let dec = Packet::decode(&enc).expect("roundtrip after drain");
prop_assert_eq!(dec, p);
}
if !buf.is_empty() {
prop_assert!(buf.len() < 7 || buf.starts_with(&HEAD));
}
});
}
#[test]
fn prop_valid_packet_survives_noise_wrap() {
use proptest::prelude::*;
let noise = prop::collection::vec(0u8..=0x54u8, 0..24);
proptest::proptest!(|(
cmd in any::<u8>(),
data in prop::collection::vec(any::<u8>(), 0..32),
prefix in noise.clone(),
suffix in noise,
)| {
let p = Packet::new(cmd, data);
let enc = p.encode().unwrap();
let mut buf = prefix;
buf.extend_from_slice(&enc);
buf.extend_from_slice(&suffix);
let pkts = Packet::drain_buffer(&mut buf);
prop_assert!(
pkts.iter().any(|x| x == &p),
"lost packet among cmds {:?}",
pkts.iter().map(|x| x.cmd).collect::<Vec<_>>()
);
});
}
}