#![allow(missing_docs)]
use crate::request::parse_command;
#[derive(Debug, Clone, Copy)]
pub struct Lcg(pub u64);
impl Lcg {
#[must_use]
pub const fn new(seed: u64) -> Self {
Self(if seed == 0 { 0x9E37_79B9_7F4A_7C15 } else { seed })
}
pub fn next_u64(&mut self) -> u64 {
self.0 = self
.0
.wrapping_mul(6_364_136_223_846_793_005)
.wrapping_add(1_442_695_040_888_963_407);
self.0
}
pub fn next_u8(&mut self) -> u8 {
(self.next_u64() >> 24) as u8
}
pub fn bound(&mut self, bound: usize) -> usize {
(self.next_u64() as usize) % bound.max(1)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Strategy {
Uniform,
StructuredJunk,
MutatedValid,
OversizedClaim,
NegativeLengths,
}
impl Strategy {
pub const ALL: [Self; 5] = [
Self::Uniform,
Self::StructuredJunk,
Self::MutatedValid,
Self::OversizedClaim,
Self::NegativeLengths,
];
pub fn pick(rng: &mut Lcg) -> Self {
Self::ALL[rng.bound(Self::ALL.len())]
}
}
#[must_use]
pub fn generate(strategy: Strategy, seed: u64) -> Vec<u8> {
let mut rng = Lcg::new(seed);
match strategy {
Strategy::Uniform => {
let len = rng.bound(2048);
(0..len).map(|_| rng.next_u8()).collect()
}
Strategy::StructuredJunk => {
let marker = b"*$+-:_;>"[rng.bound(8)];
let mut out = vec![marker];
let len = rng.bound(512);
out.extend((0..len).map(|_| rng.next_u8()));
out
}
Strategy::MutatedValid => {
let mut out = b"*3\r\n$3\r\nSET\r\n$3\r\nfoo\r\n$3\r\nbar\r\n".to_vec();
let idx = rng.bound(out.len());
out[idx] = rng.next_u8();
out
}
Strategy::OversizedClaim => {
let claim = format!("*{}\r\n", rng.next_u64() % 1_000_000_000);
let mut out = claim.into_bytes();
let tail_len = rng.bound(64);
out.extend((0..tail_len).map(|_| rng.next_u8()));
out
}
Strategy::NegativeLengths => {
let marker = if rng.bound(2) == 0 { '$' } else { '*' };
let n: i64 = -(rng.bound(99) as i64);
format!("{marker}{n}\r\nignored body").into_bytes()
}
}
}
#[derive(Debug)]
pub struct FuzzResult {
pub strategy: Strategy,
pub seed: u64,
pub input_len: usize,
pub outcome: FuzzOutcome,
}
#[derive(Debug)]
pub enum FuzzOutcome {
Parsed { consumed: usize },
Incomplete,
ParseError,
Timeout { elapsed_micros: u128 },
}
pub const PER_CALL_TIMEOUT_MICROS: u128 = 10_000;
#[must_use]
pub fn run_one(strategy: Strategy, seed: u64) -> FuzzResult {
let input = generate(strategy, seed);
let start = std::time::Instant::now();
let result = parse_command(&input);
let elapsed = start.elapsed().as_micros();
let outcome = if elapsed > PER_CALL_TIMEOUT_MICROS {
FuzzOutcome::Timeout { elapsed_micros: elapsed }
} else {
match result {
Ok(Some((_, consumed))) => FuzzOutcome::Parsed { consumed },
Ok(None) => FuzzOutcome::Incomplete,
Err(_) => FuzzOutcome::ParseError,
}
};
FuzzResult { strategy, seed, input_len: input.len(), outcome }
}
#[must_use]
pub fn run_n(n: u64, base_seed: u64) -> Summary {
let mut summary = Summary::default();
for i in 0..n {
let seed = base_seed.wrapping_add(i);
let strategy = Strategy::pick(&mut Lcg::new(seed.wrapping_mul(0xDEAD_BEEF_CAFE_F00D)));
let r = run_one(strategy, seed);
summary.total += 1;
match r.outcome {
FuzzOutcome::Parsed { .. } => summary.parsed += 1,
FuzzOutcome::Incomplete => summary.incomplete += 1,
FuzzOutcome::ParseError => summary.errored += 1,
FuzzOutcome::Timeout { elapsed_micros } => {
summary.timed_out.push((strategy, seed, elapsed_micros));
}
}
}
summary
}
#[derive(Debug, Default)]
pub struct Summary {
pub total: u64,
pub parsed: u64,
pub incomplete: u64,
pub errored: u64,
pub timed_out: Vec<(Strategy, u64, u128)>,
}
impl Summary {
pub fn assert_clean(&self, expected_total: u64) {
assert_eq!(self.total, expected_total, "fuzz campaign skipped seeds");
assert!(
self.timed_out.is_empty(),
"fuzz campaign hit {} timeouts: {:?}",
self.timed_out.len(),
self.timed_out
);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn fuzz_1k_all_strategies_clean() {
let summary = run_n(1000, 0xC0DE);
summary.assert_clean(1000);
}
#[test]
fn lcg_is_deterministic() {
let mut a = Lcg::new(42);
let mut b = Lcg::new(42);
for _ in 0..100 {
assert_eq!(a.next_u64(), b.next_u64());
}
}
#[test]
fn known_valid_input_parses() {
let r = run_one(Strategy::MutatedValid, 0);
matches!(
r.outcome,
FuzzOutcome::Parsed { .. } | FuzzOutcome::Incomplete | FuzzOutcome::ParseError
);
}
}