1use alloc::sync::Arc;
2use alloc::vec::Vec;
3use portable_atomic::{AtomicU64, Ordering};
4
5pub use crate::pipe::capabilities::BytePayload;
6use crate::pipe::when::When;
7use bytes::Bytes;
8use serde::{Deserialize, Serialize};
9
10use crate::pipe::request::{Request, Response};
11
12#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
15#[serde(tag = "kind", rename_all = "snake_case")]
16pub enum Mutation {
17 BitFlip {
20 bits: u32,
22 },
23 Truncate,
26 Duplicate,
29}
30
31impl Mutation {
32 #[must_use]
36 pub fn mutate(&self, seed: u64, call_index: u64, input: &[u8]) -> Bytes {
37 if input.is_empty() {
38 return Bytes::copy_from_slice(input);
39 }
40 let mut rng = fastrand::Rng::with_seed(seed.wrapping_add(call_index));
41 match self {
42 Mutation::BitFlip { bits } => bit_flip(&mut rng, input, *bits),
43 Mutation::Truncate => truncate(&mut rng, input),
44 Mutation::Duplicate => duplicate(&mut rng, input),
45 }
46 }
47}
48
49fn bit_flip(rng: &mut fastrand::Rng, input: &[u8], bits: u32) -> Bytes {
50 let mut buffer = input.to_vec();
51 let total_bits = (buffer.len() as u64).saturating_mul(8);
52 for _ in 0..bits {
53 let position = rng.u64(0..total_bits);
54 let byte_index = (position / 8) as usize;
55 let bit = (position % 8) as u8;
56 buffer[byte_index] ^= 1 << bit;
57 }
58 Bytes::from(buffer)
59}
60
61fn truncate(rng: &mut fastrand::Rng, input: &[u8]) -> Bytes {
62 let new_len = rng.usize(0..input.len());
63 Bytes::copy_from_slice(&input[..new_len])
64}
65
66fn duplicate(rng: &mut fastrand::Rng, input: &[u8]) -> Bytes {
67 let start = rng.usize(0..input.len());
68 let end = rng.usize(start + 1..=input.len());
69 let mut buffer = Vec::with_capacity(input.len() + (end - start));
70 buffer.extend_from_slice(&input[..end]);
71 buffer.extend_from_slice(&input[start..end]);
72 buffer.extend_from_slice(&input[end..]);
73 Bytes::from(buffer)
74}
75
76#[derive(Debug, Clone, Serialize, Deserialize)]
79pub struct MutateOp {
80 #[serde(flatten)]
81 mutation: Mutation,
82 seed: u64,
83 #[serde(default, skip_serializing_if = "Option::is_none")]
84 when: Option<When>,
85 #[serde(skip)]
86 calls: Arc<AtomicU64>,
87}
88
89impl PartialEq for MutateOp {
90 fn eq(&self, other: &Self) -> bool {
91 self.mutation == other.mutation && self.seed == other.seed && self.when == other.when
92 }
93}
94
95impl MutateOp {
96 #[must_use]
98 pub fn new(mutation: Mutation, seed: u64) -> Self {
99 Self {
100 mutation,
101 seed,
102 when: None,
103 calls: Arc::new(AtomicU64::new(0)),
104 }
105 }
106
107 #[must_use]
109 pub fn with_when(mut self, when: When) -> Self {
110 self.when = Some(when);
111 self
112 }
113
114 #[must_use]
115 pub fn mutation(&self) -> &Mutation {
116 &self.mutation
117 }
118
119 #[must_use]
120 pub fn seed(&self) -> u64 {
121 self.seed
122 }
123
124 pub fn apply_to<Payload: BytePayload>(&self, mut payload: Payload) -> Payload {
126 let index = self.calls.fetch_add(1, Ordering::Relaxed);
127 if let Some(gate) = self.when
128 && !gate.fires(index)
129 {
130 return payload;
131 }
132 let mutated = self.mutation.mutate(self.seed, index, payload.bytes());
133 payload.set_bytes(mutated);
134 payload
135 }
136}
137
138impl BytePayload for Request<Bytes> {
141 fn set_bytes(&mut self, bytes: Bytes) {
142 self.payload = bytes;
143 }
144
145 fn bytes(&self) -> &[u8] {
146 &self.payload
147 }
148}
149
150impl BytePayload for Response<Bytes> {
151 fn set_bytes(&mut self, bytes: Bytes) {
152 self.payload = bytes;
153 }
154
155 fn bytes(&self) -> &[u8] {
156 &self.payload
157 }
158}
159
160#[cfg(test)]
161#[allow(clippy::unwrap_used, clippy::expect_used)]
162mod tests {
163 use super::*;
164
165 const SAMPLE: &[u8] = b"the quick brown fox jumps over the lazy dog";
166
167 #[test]
168 fn same_seed_and_index_yields_identical_bytes() {
169 let mutation = Mutation::BitFlip { bits: 3 };
170 let first = mutation.mutate(0xABCD, 7, SAMPLE);
171 let second = mutation.mutate(0xABCD, 7, SAMPLE);
172 assert_eq!(
173 first, second,
174 "same (seed, index) reproduces the same mutation"
175 );
176 }
177
178 #[test]
179 fn bit_flip_changes_only_a_bounded_number_of_bits() {
180 let mutated = Mutation::BitFlip { bits: 2 }.mutate(1, 0, SAMPLE);
181 assert_eq!(mutated.len(), SAMPLE.len(), "bit flip preserves length");
182 let differing_bits: u32 = SAMPLE
183 .iter()
184 .zip(mutated.iter())
185 .map(|(original, changed)| (original ^ changed).count_ones())
186 .sum();
187 assert!(differing_bits >= 1, "at least one bit flips");
188 assert!(differing_bits <= 2, "no more than `bits` flips");
189 }
190
191 #[test]
192 fn truncate_produces_a_strictly_shorter_payload() {
193 let mutated = Mutation::Truncate.mutate(5, 0, SAMPLE);
194 assert!(
195 mutated.len() < SAMPLE.len(),
196 "truncate shortens the payload"
197 );
198 assert_eq!(
199 &mutated[..],
200 &SAMPLE[..mutated.len()],
201 "truncate keeps a prefix"
202 );
203 }
204
205 #[test]
206 fn duplicate_grows_the_payload() {
207 let mutated = Mutation::Duplicate.mutate(9, 0, SAMPLE);
208 assert!(mutated.len() > SAMPLE.len(), "duplicate grows the payload");
209 }
210
211 #[test]
212 fn empty_payload_is_returned_unchanged_for_every_kind() {
213 for mutation in [
214 Mutation::BitFlip { bits: 4 },
215 Mutation::Truncate,
216 Mutation::Duplicate,
217 ] {
218 let mutated = mutation.mutate(3, 0, b"");
219 assert!(
220 mutated.is_empty(),
221 "empty input stays empty under {mutation:?}"
222 );
223 }
224 }
225
226 #[test]
227 fn distinct_indices_diverge() {
228 let mutation = Mutation::BitFlip { bits: 4 };
229 let early = mutation.mutate(42, 0, SAMPLE);
230 let late = mutation.mutate(42, 1, SAMPLE);
231 assert_ne!(
232 early, late,
233 "different call indices walk different mutations"
234 );
235 }
236
237 #[test]
238 fn config_builder_round_trip_parity() {
239 let op = MutateOp::new(Mutation::BitFlip { bits: 5 }, 0xC0FFEE);
240 let json = serde_json::to_value(&op).expect("serialize");
241 let parsed: MutateOp = serde_json::from_value(json.clone()).expect("deserialize");
242 assert_eq!(
243 parsed, op,
244 "serde round-trip is lossless (ignoring live counter)"
245 );
246 assert_eq!(
247 json,
248 serde_json::json!({"kind": "bit_flip", "bits": 5, "seed": 0xC0FFEE_u64}),
249 "mutation flattens into a single tagged op object"
250 );
251 }
252
253 #[test]
254 fn truncate_and_duplicate_round_trip_through_config() {
255 for op in [
256 MutateOp::new(Mutation::Truncate, 11),
257 MutateOp::new(Mutation::Duplicate, 12),
258 ] {
259 let json = serde_json::to_value(&op).expect("serialize");
260 let parsed: MutateOp = serde_json::from_value(json).expect("deserialize");
261 assert_eq!(parsed, op, "every mutation kind round-trips through config");
262 }
263 }
264
265 #[test]
266 fn http_request_body_is_mutated_in_place() {
267 let request = Request::builder()
268 .method("POST")
269 .path("/")
270 .body(Bytes::from_static(SAMPLE))
271 .build()
272 .expect("builder");
273 let mutated = MutateOp::new(Mutation::Truncate, 7).apply_to(request);
274 assert!(
275 mutated.payload.len() < SAMPLE.len(),
276 "request body was truncated"
277 );
278 }
279
280 #[test]
281 fn http_response_body_is_mutated_in_place() {
282 let response = Response::ok(Bytes::from_static(SAMPLE));
283 let mutated = MutateOp::new(Mutation::BitFlip { bits: 3 }, 7).apply_to(response);
284 assert_eq!(
285 mutated.payload.len(),
286 SAMPLE.len(),
287 "bit flip preserves response length"
288 );
289 assert_ne!(&mutated.payload[..], SAMPLE, "response body was corrupted");
290 }
291
292 #[test]
293 fn applying_an_op_twice_advances_the_sequence() {
294 let op = MutateOp::new(Mutation::BitFlip { bits: 2 }, 99);
295 let once = op.apply_to(Response::ok(Bytes::from_static(SAMPLE)));
296 let twice = op.apply_to(Response::ok(once.payload.clone()));
297
298 let direct_first = Mutation::BitFlip { bits: 2 }.mutate(99, 0, SAMPLE);
299 let direct_second = Mutation::BitFlip { bits: 2 }.mutate(99, 1, &direct_first);
300 assert_eq!(
301 &once.payload[..],
302 &direct_first[..],
303 "first apply uses index 0"
304 );
305 assert_eq!(
306 &twice.payload[..],
307 &direct_second[..],
308 "second apply uses index 1"
309 );
310 }
311
312 #[test]
313 fn gated_op_mutates_only_on_a_fire() {
314 let always =
315 MutateOp::new(Mutation::BitFlip { bits: 3 }, 7).with_when(When::prob(1.0).seed(1));
316 let never =
317 MutateOp::new(Mutation::BitFlip { bits: 3 }, 7).with_when(When::prob(0.0).seed(1));
318
319 let corrupted = always.apply_to(Response::ok(Bytes::from_static(SAMPLE)));
320 assert_ne!(
321 &corrupted.payload[..],
322 SAMPLE,
323 "a fired gate corrupts the body"
324 );
325
326 let untouched = never.apply_to(Response::ok(Bytes::from_static(SAMPLE)));
327 assert_eq!(
328 &untouched.payload[..],
329 SAMPLE,
330 "a never-firing gate leaves the body untouched"
331 );
332 }
333
334 #[test]
335 fn gated_op_advances_the_counter_even_on_a_miss() {
336 let op = MutateOp::new(Mutation::BitFlip { bits: 3 }, 7).with_when(When::prob(1.0).seed(1));
337 let first = op.apply_to(Response::ok(Bytes::from_static(SAMPLE)));
338 let second = op.apply_to(Response::ok(Bytes::from_static(SAMPLE)));
339 let at_zero = Mutation::BitFlip { bits: 3 }.mutate(7, 0, SAMPLE);
340 let at_one = Mutation::BitFlip { bits: 3 }.mutate(7, 1, SAMPLE);
341 assert_eq!(
342 &first.payload[..],
343 &at_zero[..],
344 "first apply walks index 0"
345 );
346 assert_eq!(
347 &second.payload[..],
348 &at_one[..],
349 "second apply walks index 1"
350 );
351 }
352
353 #[derive(Clone, PartialEq, Debug)]
354 struct Blob(Vec<u8>);
355
356 impl BytePayload for Blob {
357 fn set_bytes(&mut self, bytes: Bytes) {
358 self.0 = bytes.to_vec();
359 }
360
361 fn bytes(&self) -> &[u8] {
362 &self.0
363 }
364 }
365
366 #[test]
367 fn mutation_is_generic_over_a_non_http_byte_payload() {
368 let blob = MutateOp::new(Mutation::Duplicate, 0x5EED).apply_to(Blob(SAMPLE.to_vec()));
369 assert!(
370 blob.0.len() > SAMPLE.len(),
371 "non-http blob grows under Duplicate"
372 );
373
374 let again = MutateOp::new(Mutation::Duplicate, 0x5EED).apply_to(Blob(SAMPLE.to_vec()));
375 assert_eq!(blob, again, "same seed reproduces the same mutated blob");
376 }
377}