use crate::request::parse_command;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct Lcg(u64);
impl Lcg {
#[must_use]
pub const fn new(seed: u64) -> Self {
Self(if seed == 0 { 0x9E37_79B9_7F4A_7C15 } else { seed })
}
#[must_use]
pub const fn state(self) -> u64 {
self.0
}
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, Hash)]
#[non_exhaustive]
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, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub struct FuzzResult {
pub strategy: Strategy,
pub seed: u64,
pub input_len: usize,
pub outcome: FuzzOutcome,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
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 }
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Default)]
#[non_exhaustive]
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
);
}
}
#[path = "fuzz_campaign.rs"]
mod campaign;
pub use campaign::run_n;
#[cfg(test)]
#[path = "fuzz_tests.rs"]
mod tests;