//! Structure-aware COBS roundtrip: build an arbitrary `Response`, encode //! it through `cobs_encode_response`, decode the framed bytes via //! `ucobs::decode`, and confirm the decoded payload matches the raw //! `write_to` output. #![no_main] use arbitrary::{Arbitrary, Unstructured}; use donglora_protocol::framing::{MAX_FRAME, cobs_encode_response}; use donglora_protocol::{Bandwidth, ErrorCode, RadioConfig, Response}; use heapless::Vec as HVec; use libfuzzer_sys::fuzz_target; #[derive(Debug, Arbitrary)] enum ArbResponse { Pong, Config(ArbConfig), RxPacket { rssi: i16, snr: i16, payload: Vec, }, TxDone, Ok, Error(ArbError), MacAddress([u8; 6]), } #[derive(Debug, Arbitrary)] struct ArbConfig { freq_hz: u32, bw: u8, sf: u8, cr: u8, sync_word: u16, tx_power_dbm: i8, preamble_len: u16, cad: u8, } #[derive(Debug, Arbitrary, Clone, Copy)] enum ArbError { InvalidConfig, RadioBusy, TxTimeout, NotConfigured, NoDisplay, } fn into_error(a: ArbError) -> ErrorCode { match a { ArbError::InvalidConfig => ErrorCode::InvalidConfig, ArbError::RadioBusy => ErrorCode::RadioBusy, ArbError::TxTimeout => ErrorCode::TxTimeout, ArbError::NotConfigured => ErrorCode::NotConfigured, ArbError::NoDisplay => ErrorCode::NoDisplay, } } fn into_config(c: &ArbConfig) -> RadioConfig { let bw = match c.bw % 10 { 0 => Bandwidth::Khz7, 1 => Bandwidth::Khz10, 2 => Bandwidth::Khz15, 3 => Bandwidth::Khz20, 4 => Bandwidth::Khz31, 5 => Bandwidth::Khz41, 6 => Bandwidth::Khz62, 7 => Bandwidth::Khz125, 8 => Bandwidth::Khz250, _ => Bandwidth::Khz500, }; RadioConfig { freq_hz: c.freq_hz, bw, sf: c.sf, cr: c.cr, sync_word: c.sync_word, tx_power_dbm: c.tx_power_dbm, preamble_len: c.preamble_len, cad: c.cad, } } fn into_response(a: ArbResponse) -> Response { match a { ArbResponse::Pong => Response::Pong, ArbResponse::Config(c) => Response::Config(into_config(&c)), ArbResponse::RxPacket { rssi, snr, payload } => { let mut p = HVec::::new(); let take = payload.len().min(donglora_protocol::MAX_PAYLOAD); let _ = p.extend_from_slice(&payload[..take]); Response::RxPacket { rssi, snr, payload: p, } } ArbResponse::TxDone => Response::TxDone, ArbResponse::Ok => Response::Ok, ArbResponse::Error(e) => Response::Error(into_error(e)), ArbResponse::MacAddress(m) => Response::MacAddress(m), } } fuzz_target!(|data: &[u8]| { let mut u = Unstructured::new(data); let Ok(arb) = ArbResponse::arbitrary(&mut u) else { return; }; let resp = into_response(arb); let mut raw = [0u8; MAX_FRAME]; let raw_n = resp.clone().write_to(&mut raw); let mut write = [0u8; MAX_FRAME]; let mut encode = [0u8; MAX_FRAME]; let Some(framed) = cobs_encode_response(resp, &mut write, &mut encode) else { return; }; assert_eq!(*framed.last().unwrap(), 0x00); let mut decoded = [0u8; MAX_FRAME]; let n = ucobs::decode(&framed[..framed.len() - 1], &mut decoded).unwrap(); assert_eq!(&decoded[..n], &raw[..raw_n]); });