pub const MAX_PACKET: usize = 64 * 1024;
pub const INTERRUPT: u8 = 0x03;
pub const ACK: u8 = b'+';
pub const NAK: u8 = b'-';
const HEX: &[u8; 16] = b"0123456789abcdef";
#[must_use]
pub fn checksum(data: &[u8]) -> u8 {
data.iter().fold(0u8, |acc, b| acc.wrapping_add(*b))
}
pub fn push_hex_u8(out: &mut Vec<u8>, value: u8) {
out.push(HEX[usize::from(value >> 4)]);
out.push(HEX[usize::from(value & 0x0f)]);
}
pub fn push_hex(out: &mut Vec<u8>, data: &[u8]) {
for byte in data {
push_hex_u8(out, *byte);
}
}
pub fn push_hex_u64(out: &mut Vec<u8>, value: u64) {
if value == 0 {
out.push(b'0');
return;
}
let mut started = false;
for shift in (0..16u32).rev() {
let nibble = (value >> (shift * 4)) & 0xf;
if nibble != 0 {
started = true;
}
if started {
out.push(HEX[nibble as usize]);
}
}
}
#[must_use]
pub fn hex_digit(c: u8) -> Option<u8> {
match c {
b'0'..=b'9' => Some(c - b'0'),
b'a'..=b'f' => Some(c - b'a' + 10),
b'A'..=b'F' => Some(c - b'A' + 10),
_ => None,
}
}
#[must_use]
pub fn hex_decode(text: &[u8]) -> Option<Vec<u8>> {
if !text.len().is_multiple_of(2) {
return None;
}
let (pairs, rest) = text.as_chunks::<2>();
if !rest.is_empty() {
return None;
}
let mut out = Vec::with_capacity(pairs.len());
for [hi, lo] in pairs {
let hi = hex_digit(*hi)?;
let lo = hex_digit(*lo)?;
out.push((hi << 4) | lo);
}
Some(out)
}
#[must_use]
pub fn parse_hex_u64(text: &[u8]) -> Option<u64> {
if text.is_empty() {
return None;
}
let mut value: u64 = 0;
for byte in text {
let digit = hex_digit(*byte)?;
value = value.checked_mul(16)?.checked_add(u64::from(digit))?;
}
Some(value)
}
#[must_use]
pub fn parse_hex_usize(text: &[u8]) -> Option<usize> {
usize::try_from(parse_hex_u64(text)?).ok()
}
const fn must_escape(byte: u8) -> bool {
matches!(byte, b'#' | b'$' | b'}' | b'*')
}
#[must_use]
pub fn encode_body(payload: &[u8]) -> Vec<u8> {
let mut out = Vec::with_capacity(payload.len());
let mut i = 0usize;
while let Some(&byte) = payload.get(i) {
if must_escape(byte) {
out.push(b'}');
out.push(byte ^ 0x20);
i += 1;
continue;
}
let mut run = 1usize;
while payload.get(i + run) == Some(&byte) {
run += 1;
}
out.push(byte);
let mut extra = run - 1;
while extra >= 3 {
let mut take = extra.min(97);
if take == 6 || take == 7 {
take = 5;
}
out.push(b'*');
out.push((take as u8).wrapping_add(29));
extra -= take;
}
for _ in 0..extra {
out.push(byte);
}
i += run;
}
out
}
#[must_use]
pub fn decode_body(body: &[u8]) -> Option<Vec<u8>> {
let mut out: Vec<u8> = Vec::with_capacity(body.len());
let mut i = 0usize;
while let Some(&byte) = body.get(i) {
match byte {
b'}' => {
let escaped = *body.get(i + 1)?;
if out.len() >= MAX_PACKET {
return None;
}
out.push(escaped ^ 0x20);
i += 2;
}
b'*' => {
let count = *body.get(i + 1)?;
let extra = usize::from(count.checked_sub(29)?);
let last = *out.last()?;
if out.len().checked_add(extra)? > MAX_PACKET {
return None;
}
out.extend(std::iter::repeat_n(last, extra));
i += 2;
}
other => {
if out.len() >= MAX_PACKET {
return None;
}
out.push(other);
i += 1;
}
}
}
Some(out)
}
pub fn frame(payload: &[u8], out: &mut Vec<u8>) {
let body = encode_body(payload);
out.push(b'$');
out.extend_from_slice(&body);
out.push(b'#');
push_hex_u8(out, checksum(&body));
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Event {
Packet(Vec<u8>),
Interrupt,
Ack,
Nak,
Corrupt,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Phase {
Idle,
Body,
Checksum,
}
#[derive(Debug)]
pub struct Framer {
phase: Phase,
body: Vec<u8>,
digits: [u8; 2],
have: usize,
overflow: bool,
}
impl Default for Framer {
fn default() -> Self {
Framer::new()
}
}
impl Framer {
#[must_use]
pub fn new() -> Framer {
Framer {
phase: Phase::Idle,
body: Vec::new(),
digits: [0; 2],
have: 0,
overflow: false,
}
}
pub fn reset(&mut self) {
self.phase = Phase::Idle;
self.body.clear();
self.have = 0;
self.overflow = false;
}
pub fn push(&mut self, byte: u8) -> Option<Event> {
match self.phase {
Phase::Idle => match byte {
b'$' => {
self.body.clear();
self.overflow = false;
self.phase = Phase::Body;
None
}
INTERRUPT => Some(Event::Interrupt),
ACK => Some(Event::Ack),
NAK => Some(Event::Nak),
_ => None,
},
Phase::Body => {
if byte == b'#' {
self.phase = Phase::Checksum;
self.have = 0;
return None;
}
if byte == b'$' {
self.body.clear();
self.overflow = false;
return None;
}
if self.body.len() >= MAX_PACKET {
self.overflow = true;
} else {
self.body.push(byte);
}
None
}
Phase::Checksum => {
if let Some(slot) = self.digits.get_mut(self.have) {
*slot = byte;
}
self.have += 1;
if self.have < 2 {
return None;
}
self.phase = Phase::Idle;
let sent = match (hex_digit(self.digits[0]), hex_digit(self.digits[1])) {
(Some(hi), Some(lo)) => (hi << 4) | lo,
_ => return Some(Event::Corrupt),
};
if self.overflow || sent != checksum(&self.body) {
return Some(Event::Corrupt);
}
match decode_body(&self.body) {
Some(payload) => Some(Event::Packet(payload)),
None => Some(Event::Corrupt),
}
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn feed(framer: &mut Framer, bytes: &[u8]) -> Vec<Event> {
bytes.iter().filter_map(|b| framer.push(*b)).collect()
}
#[test]
fn a_packet_round_trips_through_the_framer() {
let mut out = Vec::new();
frame(b"OK", &mut out);
assert_eq!(out, b"$OK#9a");
let mut framer = Framer::new();
assert_eq!(feed(&mut framer, &out), vec![Event::Packet(b"OK".to_vec())]);
}
#[test]
fn the_four_special_bytes_are_escaped_and_come_back() {
let payload = b"a#b$c}d*e";
let mut wire = Vec::new();
frame(payload, &mut wire);
let body = &wire[1..wire.len() - 3];
for special in *b"#$*" {
assert!(!body.contains(&special), "raw {special:?} in {body:?}");
}
let mut framer = Framer::new();
assert_eq!(
feed(&mut framer, &wire),
vec![Event::Packet(payload.to_vec())]
);
}
#[test]
fn long_runs_compress_and_expand_exactly() {
let payload = vec![0x41u8; 200];
let body = encode_body(&payload);
assert!(body.len() < 20, "200 bytes compressed to {}", body.len());
assert_eq!(decode_body(&body).as_deref(), Some(&payload[..]));
}
#[test]
fn every_run_length_round_trips() {
for len in 1..=300usize {
let payload = vec![0x30u8; len];
let body = encode_body(&payload);
assert!(
!body.contains(&b'#') && !body.contains(&b'$'),
"len {len} produced a packet terminator: {body:?}"
);
assert_eq!(
decode_body(&body).as_deref(),
Some(&payload[..]),
"len {len}"
);
}
}
#[test]
fn a_bad_checksum_is_reported_not_believed() {
let mut framer = Framer::new();
assert_eq!(feed(&mut framer, b"$OK#00"), vec![Event::Corrupt]);
assert_eq!(
feed(&mut framer, b"$OK#9a"),
vec![Event::Packet(b"OK".to_vec())]
);
}
#[test]
fn a_packet_split_across_reads_still_arrives() {
let mut framer = Framer::new();
let mut events = Vec::new();
for chunk in [&b"$m1"[..], b"0,4", b"#", b"2", b"e"] {
events.extend(feed(&mut framer, chunk));
}
assert_eq!(events, vec![Event::Packet(b"m10,4".to_vec())]);
}
#[test]
fn control_c_between_packets_is_an_interrupt() {
let mut framer = Framer::new();
assert_eq!(feed(&mut framer, &[INTERRUPT]), vec![Event::Interrupt]);
}
#[test]
fn acknowledgements_are_events_of_their_own() {
let mut framer = Framer::new();
assert_eq!(feed(&mut framer, b"+-"), vec![Event::Ack, Event::Nak]);
}
#[test]
fn malformed_bodies_are_rejected_rather_than_panicking() {
assert_eq!(decode_body(b"abc}"), None);
assert_eq!(decode_body(b"*!"), None);
assert_eq!(decode_body(b"a* "), Some(b"aaaa".to_vec()));
assert_eq!(decode_body(b"a*\x00"), None);
assert_eq!(decode_body(b"a*"), None);
}
#[test]
fn an_oversized_packet_is_refused_without_allocating_forever() {
let mut framer = Framer::new();
framer.push(b'$');
for _ in 0..(MAX_PACKET + 16) {
framer.push(b'z');
}
framer.push(b'#');
framer.push(b'0');
assert_eq!(framer.push(b'0'), Some(Event::Corrupt));
}
#[test]
fn hex_helpers_refuse_what_they_cannot_represent() {
assert_eq!(parse_hex_u64(b""), None);
assert_eq!(parse_hex_u64(b"12g4"), None);
assert_eq!(parse_hex_u64(b"ffffffffffffffff"), Some(u64::MAX));
assert_eq!(parse_hex_u64(b"1ffffffffffffffff"), None);
assert_eq!(hex_decode(b"abc"), None);
assert_eq!(hex_decode(b"00ff"), Some(vec![0x00, 0xff]));
let mut out = Vec::new();
push_hex_u64(&mut out, 0);
push_hex_u64(&mut out, 0xc0de);
assert_eq!(out, b"0c0de");
}
}