use crate::WireError;
pub const MAX_FRAME_SIZE: usize = 512;
pub const fn max_rzcobs_encoding_length(n: usize) -> usize {
n + n.div_ceil(7) + 1
}
pub fn cobs_encode(data: &[u8], dst: &mut [u8]) -> Result<usize, WireError> {
let min_len = cobs::max_encoding_length(data.len()) + 1;
if dst.len() < min_len {
return Err(WireError::PayloadTooLarge);
}
let n = cobs::encode(data, dst);
dst[n] = 0x00;
Ok(n + 1)
}
pub fn cobs_decode(src: &[u8], dst: &mut [u8]) -> Result<usize, WireError> {
cobs::decode(src, dst)
.map(|report| report.frame_size())
.map_err(|_| WireError::FramingError)
}
struct SliceWriter<'a> {
dst: &'a mut [u8],
pos: usize,
}
impl rzcobs::Write for SliceWriter<'_> {
type Error = ();
fn write(&mut self, byte: u8) -> Result<(), ()> {
if self.pos >= self.dst.len() {
return Err(());
}
self.dst[self.pos] = byte;
self.pos += 1;
Ok(())
}
}
pub fn rzcobs_encode(data: &[u8], dst: &mut [u8]) -> Result<usize, WireError> {
let min_len = max_rzcobs_encoding_length(data.len()) + 1;
if dst.len() < min_len {
return Err(WireError::PayloadTooLarge);
}
let writer = SliceWriter { dst, pos: 0 };
let mut enc = rzcobs::Encoder::new(writer);
for &b in data {
enc.write(b).map_err(|_| WireError::PayloadTooLarge)?;
}
enc.end().map_err(|_| WireError::PayloadTooLarge)?;
let n = enc.writer().pos;
enc.writer().dst[n] = 0x00;
Ok(n + 1)
}
pub fn rzcobs_decode(src: &[u8], dst: &mut [u8]) -> Result<usize, WireError> {
let mut out_pos = 0usize;
let mut it = src.iter().rev().copied();
while let Some(x) = it.next() {
match x {
0x00 => return Err(WireError::FramingError),
0x01..=0x7F => {
for i in 0..7usize {
if out_pos >= dst.len() {
return Err(WireError::FramingError);
}
if x & (1 << (6 - i)) == 0 {
dst[out_pos] = it.next().ok_or(WireError::FramingError)?;
} else {
dst[out_pos] = 0x00;
}
out_pos += 1;
}
}
0x80..=0xFE => {
let n = usize::from(x & 0x7F) + 7;
if out_pos >= dst.len() {
return Err(WireError::FramingError);
}
dst[out_pos] = 0x00;
out_pos += 1;
for _ in 0..n {
if out_pos >= dst.len() {
return Err(WireError::FramingError);
}
dst[out_pos] = it.next().ok_or(WireError::FramingError)?;
out_pos += 1;
}
}
0xFF => {
for _ in 0..134usize {
if out_pos >= dst.len() {
return Err(WireError::FramingError);
}
dst[out_pos] = it.next().ok_or(WireError::FramingError)?;
out_pos += 1;
}
}
}
}
dst[..out_pos].reverse();
Ok(out_pos)
}
pub struct FrameAccumulator<const N: usize> {
buf: [u8; N],
len: usize,
overflow: bool,
}
impl<const N: usize> FrameAccumulator<N> {
pub const fn new() -> Self {
Self {
buf: [0u8; N],
len: 0,
overflow: false,
}
}
pub fn feed(&mut self, byte: u8) -> bool {
if byte == 0x00 {
return true;
}
if self.len >= N {
self.overflow = true;
self.len = 0;
return false;
}
self.buf[self.len] = byte;
self.len += 1;
false
}
pub fn frame(&self) -> Option<&[u8]> {
if self.overflow || self.len == 0 {
None
} else {
Some(&self.buf[..self.len])
}
}
pub fn reset(&mut self) {
self.len = 0;
self.overflow = false;
}
}
impl<const N: usize> Default for FrameAccumulator<N> {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn cobs_encode_decode_roundtrip() {
let data = b"hello telepath";
let mut encoded = [0u8; 64];
let n = cobs_encode(data, &mut encoded).unwrap();
assert_eq!(encoded[n - 1], 0x00);
for &b in &encoded[..n - 1] {
assert_ne!(b, 0x00);
}
let mut decoded = [0u8; 64];
let m = cobs_decode(&encoded[..n - 1], &mut decoded).unwrap();
assert_eq!(&decoded[..m], data);
}
#[test]
fn cobs_with_embedded_zeros() {
let data = [0x00u8, 0x42, 0x00, 0xFF, 0x00];
let mut encoded = [0u8; 32];
let n = cobs_encode(&data, &mut encoded).unwrap();
assert_eq!(encoded[n - 1], 0x00);
let mut decoded = [0u8; 32];
let m = cobs_decode(&encoded[..n - 1], &mut decoded).unwrap();
assert_eq!(&decoded[..m], &data);
}
#[test]
fn cobs_long_run_no_zeros() {
let data = [0x42u8; 300];
let mut encoded = [0u8; 512];
let n = cobs_encode(&data, &mut encoded).unwrap();
assert_eq!(encoded[n - 1], 0x00);
let mut decoded = [0u8; 512];
let m = cobs_decode(&encoded[..n - 1], &mut decoded).unwrap();
assert_eq!(&decoded[..m], &data[..]);
}
#[test]
fn cobs_max_payload_boundary() {
let data = [0xABu8; crate::MAX_PAYLOAD_SIZE];
let mut encoded = [0u8; MAX_FRAME_SIZE];
let n = cobs_encode(&data, &mut encoded).unwrap();
assert!(n <= MAX_FRAME_SIZE);
let mut decoded = [0u8; MAX_FRAME_SIZE];
let m = cobs_decode(&encoded[..n - 1], &mut decoded).unwrap();
assert_eq!(&decoded[..m], &data[..]);
}
#[test]
fn cobs_encode_overflow_returns_error() {
let data = b"hello";
let mut tiny = [0u8; 1];
assert!(matches!(
cobs_encode(data, &mut tiny),
Err(WireError::PayloadTooLarge)
));
}
#[test]
fn cobs_decode_malformed_returns_error() {
let bad = [0x00u8];
let mut dst = [0u8; 16];
assert!(matches!(
cobs_decode(&bad, &mut dst),
Err(WireError::FramingError)
));
}
#[test]
fn accumulator_basic() {
let mut acc: FrameAccumulator<64> = FrameAccumulator::new();
let data = b"ping";
let mut encoded = [0u8; 16];
let n = cobs_encode(data, &mut encoded).unwrap();
let mut complete = false;
for &b in &encoded[..n] {
complete = acc.feed(b);
}
assert!(complete);
let frame = acc.frame().unwrap();
let mut decoded = [0u8; 16];
let m = cobs_decode(frame, &mut decoded).unwrap();
assert_eq!(&decoded[..m], data);
}
#[test]
fn accumulator_reset_allows_second_frame() {
let mut acc: FrameAccumulator<64> = FrameAccumulator::new();
let data1 = b"first";
let data2 = b"second";
let mut enc = [0u8; 32];
let n = cobs_encode(data1, &mut enc).unwrap();
for &b in &enc[..n] {
acc.feed(b);
}
acc.reset();
let n = cobs_encode(data2, &mut enc).unwrap();
let mut complete = false;
for &b in &enc[..n] {
complete = acc.feed(b);
}
assert!(complete);
let frame = acc.frame().unwrap();
let mut decoded = [0u8; 32];
let m = cobs_decode(frame, &mut decoded).unwrap();
assert_eq!(&decoded[..m], data2);
}
#[test]
fn accumulator_overflow_returns_none() {
let mut acc: FrameAccumulator<4> = FrameAccumulator::new();
for _ in 0..5 {
acc.feed(0x42);
}
acc.feed(0x00);
assert!(acc.frame().is_none());
}
#[test]
fn max_frame_size_covers_max_payload() {
assert!(MAX_FRAME_SIZE >= crate::MAX_PAYLOAD_SIZE + 4);
}
#[test]
fn rzcobs_encode_decode_roundtrip() {
let data = b"hello telepath";
let mut encoded = [0u8; 64];
let n = rzcobs_encode(data, &mut encoded).unwrap();
assert_eq!(encoded[n - 1], 0x00);
let mut decoded = [0u8; 64];
let m = rzcobs_decode(&encoded[..n - 1], &mut decoded).unwrap();
assert_eq!(&decoded[..data.len()], data.as_slice());
assert!(decoded[data.len()..m].iter().all(|&b| b == 0x00));
}
#[test]
fn rzcobs_with_embedded_zeros() {
let data = [0x00u8, 0x42, 0x00, 0xFF, 0x00];
let mut encoded = [0u8; 32];
let n = rzcobs_encode(&data, &mut encoded).unwrap();
assert_eq!(encoded[n - 1], 0x00);
let mut decoded = [0u8; 32];
let m = rzcobs_decode(&encoded[..n - 1], &mut decoded).unwrap();
assert_eq!(&decoded[..data.len()], &data);
assert!(decoded[data.len()..m].iter().all(|&b| b == 0x00));
}
#[test]
fn rzcobs_long_run_no_zeros() {
let data = [0x42u8; 200];
let mut encoded = [0u8; 512];
let n = rzcobs_encode(&data, &mut encoded).unwrap();
assert_eq!(encoded[n - 1], 0x00);
let mut decoded = [0u8; 512];
let m = rzcobs_decode(&encoded[..n - 1], &mut decoded).unwrap();
assert_eq!(&decoded[..data.len()], &data[..]);
assert!(decoded[data.len()..m].iter().all(|&b| b == 0x00));
}
#[test]
fn rzcobs_max_payload_boundary() {
let data = [0xABu8; crate::MAX_PAYLOAD_SIZE];
let mut encoded = [0u8; MAX_FRAME_SIZE];
let n = rzcobs_encode(&data, &mut encoded).unwrap();
assert!(n <= MAX_FRAME_SIZE);
let mut decoded = [0u8; MAX_FRAME_SIZE];
let m = rzcobs_decode(&encoded[..n - 1], &mut decoded).unwrap();
assert_eq!(&decoded[..data.len()], &data[..]);
assert!(decoded[data.len()..m].iter().all(|&b| b == 0x00));
}
#[test]
fn rzcobs_encode_overflow_returns_error() {
let data = b"hello";
let mut tiny = [0u8; 1];
assert!(matches!(
rzcobs_encode(data, &mut tiny),
Err(WireError::PayloadTooLarge)
));
}
#[test]
fn rzcobs_decode_malformed_returns_error() {
let bad = [0x01u8];
let mut dst = [0u8; 16];
assert!(matches!(
rzcobs_decode(&bad, &mut dst),
Err(WireError::FramingError)
));
}
#[test]
fn rzcobs_max_frame_size_covers_max_payload() {
assert!(MAX_FRAME_SIZE >= max_rzcobs_encoding_length(crate::MAX_PAYLOAD_SIZE) + 1);
}
}