kevy-resp 2.0.20

RESP2 + RESP3 wire-protocol codec. Pure Rust.
Documentation
//! Std-only RESP parser fuzz harness — v1.36 industrial-grade.
//!
//! Drives randomized byte streams through [`parse_command`] and
//! asserts that every call terminates in bounded time with one of
//! `Ok(Some)` / `Ok(None)` / `Err(_)`. Never panics, never hangs.
//!
//! 0-dep: uses a fixed-seed PCG-style LCG for determinism — no
//! `rand` crate, no `quickcheck`, no AFL. Each call records the seed
//! that produced it, so any failing input is bit-for-bit reproducible.
//!
//! Strategies:
//! - [`Strategy::Uniform`] — pure random bytes.
//! - [`Strategy::StructuredJunk`] — bytes that look like RESP type
//!   markers (`*`, `$`, `+`, `-`, `:`) followed by garbage.
//! - [`Strategy::MutatedValid`] — a valid SET frame with one random
//!   byte flipped.
//! - [`Strategy::OversizedClaim`] — `*<huge>\r\n` headers without
//!   matching body.
//! - [`Strategy::NegativeLengths`] — `$-99\r\n` / `*-99\r\n` etc.
//!
//! Run with [`run_one`] for one stream + [`run_n`] for a campaign.

#![allow(missing_docs)]

use crate::request::parse_command;

/// Std-only LCG PRNG (MMIX constants). Deterministic per seed.
#[derive(Debug, Clone, Copy)]
pub struct Lcg(pub u64);

impl Lcg {
    #[must_use]
    pub const fn new(seed: u64) -> Self {
        // Avoid the zero fixed-point.
        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
    }
    /// Returns a value in `0..bound` (uniform-ish, biased for small
    /// bound; fine for fuzz purposes).
    pub fn bound(&mut self, bound: usize) -> usize {
        (self.next_u64() as usize) % bound.max(1)
    }
}

/// Fuzz strategy. Each picks a different distribution of byte streams.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Strategy {
    /// Pure uniform random bytes.
    Uniform,
    /// First byte is a RESP type marker, then junk.
    StructuredJunk,
    /// A valid `SET key value` with one byte flipped.
    MutatedValid,
    /// `*<huge>\r\n` claim header, then short body.
    OversizedClaim,
    /// `$-99\r\n` or `*-99\r\n` — negative bulk/array lengths.
    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())]
    }
}

/// Generate one fuzz input under the given strategy + seed.
#[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 => {
            // Start from `*3\r\n$3\r\nSET\r\n$3\r\nfoo\r\n$3\r\nbar\r\n`.
            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 => {
            // Claim 10^9 args, but provide ~50 bytes of body.
            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()
        }
    }
}

/// Outcome of one fuzz call.
#[derive(Debug)]
pub struct FuzzResult {
    pub strategy: Strategy,
    pub seed: u64,
    pub input_len: usize,
    pub outcome: FuzzOutcome,
}

#[derive(Debug)]
pub enum FuzzOutcome {
    /// Parsed a complete frame; `consumed` ≤ input_len.
    Parsed { consumed: usize },
    /// Incomplete; needs more bytes.
    Incomplete,
    /// Parser returned an error (well-formed `ProtocolError`).
    ParseError,
    /// Parser took longer than the per-call timeout — indicates a
    /// runaway. Never observed in correct code; the harness records
    /// the offending seed for reproduction.
    Timeout { elapsed_micros: u128 },
}

/// Per-call wall-clock budget. RESP parsing of ≤ 2 KiB inputs should
/// finish in microseconds; 10 ms is a generous ceiling.
pub const PER_CALL_TIMEOUT_MICROS: u128 = 10_000;

/// Run one fuzz stream. Returns the outcome. Never panics on the
/// fuzz input — the whole point is that `parse_command` itself
/// doesn't panic. If the parser DID panic, `std::panic::catch_unwind`
/// catches it and returns a special record (see [`run_one_caught`]).
#[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 }
}

/// Run N campaigns across all strategies. Returns counts + any
/// timeouts found.
#[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 {
    /// Strict assertion helper: every call must have produced one of
    /// the three valid outcomes within the per-call timeout, and the
    /// total must match the campaign size.
    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);
        // Seed 0 may or may not flip in a way that breaks the frame.
        // Just assert the outcome is one of the valid variants
        // (not a timeout / panic).
        matches!(
            r.outcome,
            FuzzOutcome::Parsed { .. } | FuzzOutcome::Incomplete | FuzzOutcome::ParseError
        );
    }
}