//! Structure-aware Command roundtrip: construct an arbitrary Command, //! hand-encode it using the same wire layout the library parses, and //! confirm `Command::from_bytes` recovers the original value exactly. //! //! The library itself is asymmetric (firmware only decodes commands), //! so this fuzz target carries a matching encoder. #![no_main] use arbitrary::{Arbitrary, Unstructured}; use donglora_protocol::{Bandwidth, Command, MAX_PAYLOAD, RADIO_CONFIG_SIZE, RadioConfig}; use heapless::Vec as HVec; use libfuzzer_sys::fuzz_target; #[derive(Debug, Arbitrary)] enum ArbCommand { Ping, GetConfig, SetConfig(ArbConfig), StartRx, StopRx, Transmit { config: Option, payload: Vec, }, DisplayOn, DisplayOff, GetMac, } #[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, } 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_command(a: &ArbCommand) -> Command { match a { ArbCommand::Ping => Command::Ping, ArbCommand::GetConfig => Command::GetConfig, ArbCommand::SetConfig(c) => Command::SetConfig(into_config(c)), ArbCommand::StartRx => Command::StartRx, ArbCommand::StopRx => Command::StopRx, ArbCommand::Transmit { config, payload } => { let cfg = config.as_ref().map(into_config); let mut p: HVec = HVec::new(); let take = payload.len().min(MAX_PAYLOAD); let _ = p.extend_from_slice(&payload[..take]); Command::Transmit { config: cfg, payload: p, } } ArbCommand::DisplayOn => Command::DisplayOn, ArbCommand::DisplayOff => Command::DisplayOff, ArbCommand::GetMac => Command::GetMac, } } fn encode(cmd: &Command, out: &mut [u8]) -> usize { match cmd { Command::Ping => { out[0] = 0; 1 } Command::GetConfig => { out[0] = 1; 1 } Command::SetConfig(cfg) => { out[0] = 2; cfg.write_to(&mut out[1..]); 1 + RADIO_CONFIG_SIZE } Command::StartRx => { out[0] = 3; 1 } Command::StopRx => { out[0] = 4; 1 } Command::Transmit { config, payload } => { out[0] = 5; let mut pos = 1; if let Some(cfg) = config { out[pos] = 1; pos += 1; cfg.write_to(&mut out[pos..]); pos += RADIO_CONFIG_SIZE; } else { out[pos] = 0; pos += 1; } out[pos..pos + 2].copy_from_slice(&(payload.len() as u16).to_le_bytes()); pos += 2; out[pos..pos + payload.len()].copy_from_slice(payload); pos + payload.len() } Command::DisplayOn => { out[0] = 6; 1 } Command::DisplayOff => { out[0] = 7; 1 } Command::GetMac => { out[0] = 8; 1 } } } fuzz_target!(|data: &[u8]| { let mut u = Unstructured::new(data); let Ok(arb) = ArbCommand::arbitrary(&mut u) else { return; }; let cmd = into_command(&arb); let mut buf = [0u8; 1024]; let n = encode(&cmd, &mut buf); let parsed = Command::from_bytes(&buf[..n]).expect("encoded command must parse"); assert_eq!(parsed, cmd); });