1#![allow(missing_docs)]
24
25use crate::request::parse_command;
26
27#[derive(Debug, Clone, Copy)]
29pub struct Lcg(pub u64);
30
31impl Lcg {
32 #[must_use]
33 pub const fn new(seed: u64) -> Self {
34 Self(if seed == 0 { 0x9E37_79B9_7F4A_7C15 } else { seed })
36 }
37 pub fn next_u64(&mut self) -> u64 {
38 self.0 = self
39 .0
40 .wrapping_mul(6_364_136_223_846_793_005)
41 .wrapping_add(1_442_695_040_888_963_407);
42 self.0
43 }
44 pub fn next_u8(&mut self) -> u8 {
45 (self.next_u64() >> 24) as u8
46 }
47 pub fn bound(&mut self, bound: usize) -> usize {
50 (self.next_u64() as usize) % bound.max(1)
51 }
52}
53
54#[derive(Debug, Clone, Copy, PartialEq, Eq)]
56pub enum Strategy {
57 Uniform,
59 StructuredJunk,
61 MutatedValid,
63 OversizedClaim,
65 NegativeLengths,
67}
68
69impl Strategy {
70 pub const ALL: [Self; 5] = [
71 Self::Uniform,
72 Self::StructuredJunk,
73 Self::MutatedValid,
74 Self::OversizedClaim,
75 Self::NegativeLengths,
76 ];
77 pub fn pick(rng: &mut Lcg) -> Self {
78 Self::ALL[rng.bound(Self::ALL.len())]
79 }
80}
81
82#[must_use]
84pub fn generate(strategy: Strategy, seed: u64) -> Vec<u8> {
85 let mut rng = Lcg::new(seed);
86 match strategy {
87 Strategy::Uniform => {
88 let len = rng.bound(2048);
89 (0..len).map(|_| rng.next_u8()).collect()
90 }
91 Strategy::StructuredJunk => {
92 let marker = b"*$+-:_;>"[rng.bound(8)];
93 let mut out = vec![marker];
94 let len = rng.bound(512);
95 out.extend((0..len).map(|_| rng.next_u8()));
96 out
97 }
98 Strategy::MutatedValid => {
99 let mut out = b"*3\r\n$3\r\nSET\r\n$3\r\nfoo\r\n$3\r\nbar\r\n".to_vec();
101 let idx = rng.bound(out.len());
102 out[idx] = rng.next_u8();
103 out
104 }
105 Strategy::OversizedClaim => {
106 let claim = format!("*{}\r\n", rng.next_u64() % 1_000_000_000);
108 let mut out = claim.into_bytes();
109 let tail_len = rng.bound(64);
110 out.extend((0..tail_len).map(|_| rng.next_u8()));
111 out
112 }
113 Strategy::NegativeLengths => {
114 let marker = if rng.bound(2) == 0 { '$' } else { '*' };
115 let n: i64 = -(rng.bound(99) as i64);
116 format!("{marker}{n}\r\nignored body").into_bytes()
117 }
118 }
119}
120
121#[derive(Debug)]
123pub struct FuzzResult {
124 pub strategy: Strategy,
125 pub seed: u64,
126 pub input_len: usize,
127 pub outcome: FuzzOutcome,
128}
129
130#[derive(Debug)]
131pub enum FuzzOutcome {
132 Parsed { consumed: usize },
134 Incomplete,
136 ParseError,
138 Timeout { elapsed_micros: u128 },
142}
143
144pub const PER_CALL_TIMEOUT_MICROS: u128 = 10_000;
147
148#[must_use]
153pub fn run_one(strategy: Strategy, seed: u64) -> FuzzResult {
154 let input = generate(strategy, seed);
155 let start = std::time::Instant::now();
156 let result = parse_command(&input);
157 let elapsed = start.elapsed().as_micros();
158 let outcome = if elapsed > PER_CALL_TIMEOUT_MICROS {
159 FuzzOutcome::Timeout { elapsed_micros: elapsed }
160 } else {
161 match result {
162 Ok(Some((_, consumed))) => FuzzOutcome::Parsed { consumed },
163 Ok(None) => FuzzOutcome::Incomplete,
164 Err(_) => FuzzOutcome::ParseError,
165 }
166 };
167 FuzzResult { strategy, seed, input_len: input.len(), outcome }
168}
169
170#[must_use]
173pub fn run_n(n: u64, base_seed: u64) -> Summary {
174 let mut summary = Summary::default();
175 for i in 0..n {
176 let seed = base_seed.wrapping_add(i);
177 let strategy = Strategy::pick(&mut Lcg::new(seed.wrapping_mul(0xDEAD_BEEF_CAFE_F00D)));
178 let r = run_one(strategy, seed);
179 summary.total += 1;
180 match r.outcome {
181 FuzzOutcome::Parsed { .. } => summary.parsed += 1,
182 FuzzOutcome::Incomplete => summary.incomplete += 1,
183 FuzzOutcome::ParseError => summary.errored += 1,
184 FuzzOutcome::Timeout { elapsed_micros } => {
185 summary.timed_out.push((strategy, seed, elapsed_micros));
186 }
187 }
188 }
189 summary
190}
191
192#[derive(Debug, Default)]
193pub struct Summary {
194 pub total: u64,
195 pub parsed: u64,
196 pub incomplete: u64,
197 pub errored: u64,
198 pub timed_out: Vec<(Strategy, u64, u128)>,
199}
200
201impl Summary {
202 pub fn assert_clean(&self, expected_total: u64) {
211 assert_eq!(self.total, expected_total, "fuzz campaign skipped seeds");
212 assert!(
213 self.timed_out.is_empty(),
214 "fuzz campaign hit {} timeouts: {:?}",
215 self.timed_out.len(),
216 self.timed_out
217 );
218 }
219}
220
221#[cfg(test)]
222mod tests {
223 use super::*;
224
225 #[test]
226 fn fuzz_1k_all_strategies_clean() {
227 let summary = run_n(1000, 0xC0DE);
228 summary.assert_clean(1000);
229 }
230
231 #[test]
232 fn lcg_is_deterministic() {
233 let mut a = Lcg::new(42);
234 let mut b = Lcg::new(42);
235 for _ in 0..100 {
236 assert_eq!(a.next_u64(), b.next_u64());
237 }
238 }
239
240 #[test]
241 fn known_valid_input_parses() {
242 let r = run_one(Strategy::MutatedValid, 0);
243 matches!(
247 r.outcome,
248 FuzzOutcome::Parsed { .. } | FuzzOutcome::Incomplete | FuzzOutcome::ParseError
249 );
250 }
251}