- command_roundtrip: structure-aware (via arbitrary crate) Command encode → parse → equal. Symmetric to cobs_roundtrip. - cobs_decoder_multi_frame: chunked feeding with arbitrary split sizes; asserts bounded payload and that command count never exceeds sentinel count. - cobs_chunk_invariant: same stream through two decoders (chunked vs all-at-once) must produce the same command sequence. Adds test-fuzz-long recipe (600s per target, ~1h 10m total) for CI cron / pre-release. All seven targets smoke-tested at 100k runs each with zero findings.
158 lines
4.1 KiB
Rust
158 lines
4.1 KiB
Rust
//! 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<ArbConfig>,
|
|
payload: Vec<u8>,
|
|
},
|
|
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<u8, MAX_PAYLOAD> = 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);
|
|
});
|