fuzzcheck/traits.rs
1use std::any::Any;
2use std::fmt::Display;
3use std::path::PathBuf;
4
5use fuzzcheck_common::FuzzerEvent;
6
7use crate::fuzzer::PoolStorageIndex;
8use crate::subvalue_provider::SubValueProvider;
9
10/**
11A [`Mutator`] is an object capable of generating/mutating a value for the purpose of
12fuzz-testing.
13
14For example, a mutator could change the value
15`v1 = [1, 4, 2, 1]` to `v1' = [1, 5, 2, 1]`.
16The idea is that if `v1` is an “interesting” value to test, then `v1'` also
17has a high chance of being “interesting” to test.
18
19Fuzzcheck itself provides a few mutators for `std` types as well as procedural macros
20to generate mutators. See the [`mutators`](crate::mutators) module.
21
22## Complexity
23
24A mutator is also responsible for keeping track of the
25[complexity](crate::Mutator::complexity) of a value. The complexity is,
26roughly speaking, how large the value is.
27
28For example, the complexity of a vector could be the sum of the complexities
29of its elements. So `vec![]` would have a complexity of `1.0` (what we chose as
30the base complexity of a vector) and `vec![76]` would have a complexity of
31`9.0`: `1.0` for the base complexity of the vector itself + `8.0` for the 8-bit
32integer “76”. There is no fixed rule for how to compute the complexity of a
33value. However, all mutators of a value of type MUST agree on what its
34complexity is within a fuzz-test. In other words, if we have the following
35mutator for the type `(u8, u8)`:
36```ignore
37struct MutatorTuple2<M1, M2> where M1: Mutator<u8>, M2: Mutator<u8> {
38 m1: M1, // responsible for mutating the first element
39 m2: M2 // responsible for mutating the second element
40}
41```
42then the submutators `M1` and `M2` must always give the same complexity
43for all values of type `u8`.
44
45## Global search space complexity
46
47The search space complexity is, roughly, the base-2 logarithm of the number of
48possible values that can be produced by the mutator. Note that this is distinct
49from the complexity of a value. If we have a mutator for `usize` that can only
50produce the values `89` and `65`, then the search space complexity of the
51mutator is `1.0` but the complexity of the produced values could be `64.0`. If a
52mutator has a search space complexity of `0.0`, then it is only able to
53produce a single value.
54
55## [`Cache`](Mutator::Cache)
56
57In order to mutate values efficiently, the mutator is able to make use of a
58per-value *cache*. The [`Cache`](Mutator::Cache) contains information associated
59with the value that will make it faster to compute its complexity or apply a
60mutation to it. For a vector, its cache is its total complexity, along with a
61vector of the caches of each of its element.
62
63## [`MutationStep`](Mutator::MutationStep)
64
65The same values will be passed to the mutator many times, so that it is
66mutated in many different ways. There are different strategies to choose
67what mutation to apply to a value. The first one is to create a list of
68mutation operations, and choose one to apply randomly from this list.
69
70However, one may want to have better control over which mutation operation
71is used. For example, if the value to be mutated is of type `Option<T>`,
72then you may want to first mutate it to `None`, and then always mutate it
73to another `Some(t)`. This is where [`MutationStep`](Mutator::MutationStep)
74comes in. The mutation step is a type you define to allow you to keep track
75of which mutation operation has already been tried. This allows you to
76deterministically apply mutations to a value such that better mutations are
77tried first, and duplicate mutations are avoided.
78
79It is not always possible to schedule mutations in order. For that reason,
80we have two methods: [`random_mutate`](crate::Mutator::random_mutate) executes
81a random mutation, and [`ordered_mutate`](crate::Mutator::ordered_mutate) uses
82the [`MutationStep`](Mutator::MutationStep) to schedule mutations in order.
83The fuzzing engine only ever uses [`ordered_mutate`](crate::Mutator::ordered_mutate)
84directly, but the former is sometimes necessary to compose mutators together.
85
86If you don't want to bother with ordered mutations, that is fine. In that
87case, only implement [`random_mutate`](crate::Mutator::random_mutate) and call it from
88the [`ordered_mutate`](crate::Mutator::ordered_mutate) method.
89```ignore
90fn random_mutate(&self, value: &mut Value, cache: &mut Self::Cache, max_cplx: f64) -> (Self::UnmutateToken, f64) {
91 // ...
92}
93fn ordered_mutate(&self, value: &mut Value, cache: &mut Self::Cache, step: &mut Self::MutationStep, _subvalue_provider: &dyn SubValueProvider, max_cplx: f64) -> Option<(Self::UnmutateToken, f64)> {
94 Some(self.random_mutate(value, cache, max_cplx))
95}
96```
97
98## Arbitrary
99
100A mutator must also be able to generate new values from nothing. This is what
101the [`random_arbitrary`](crate::Mutator::random_arbitrary) and
102[`ordered_arbitrary`](crate::Mutator::ordered_arbitrary) methods are for. The
103latter one is called by the fuzzer directly and uses an
104[`ArbitraryStep`](Mutator::ArbitraryStep) that can be used to smartly generate
105more interesting values first and avoid duplicates.
106
107## Unmutate
108
109It is important to note that values and caches are mutated
110*in-place*. The fuzzer does not clone them before handing them to the
111mutator. Therefore, the mutator also needs to know how to reverse each
112mutation it performed. To do so, each mutation needs to return a token
113describing how to reverse it. The [unmutate](crate::Mutator::unmutate)
114method will later be called with that token to get the original value
115and cache back.
116
117For example, if the value is `[[1, 3], [5], [9, 8]]`, the mutator may
118mutate it to `[[1, 3], [5], [9, 1, 8]]` and return the token:
119`Element(2, Remove(1))`, which means that in order to reverse the
120mutation, the element at index 2 has to be unmutated by removing
121its element at index 1. In pseudocode:
122
123```
124use fuzzcheck::Mutator;
125# use fuzzcheck::subvalue_provider::EmptySubValueProvider;
126# use fuzzcheck::DefaultMutator;
127# let m = bool::default_mutator();
128# let mut value = false;
129# let mut cache = m.validate_value(&value).unwrap();
130# let mut step = m.default_mutation_step(&value, &cache);
131# let max_cplx = 8.0;
132# fn test(x: &bool) {}
133// value = [[1, 3], [5], [9, 8]];
134// cache: c1 (ommitted from example)
135// step: s1 (ommitted from example)
136
137let (unmutate_token, _cplx) = m.ordered_mutate(&mut value, &mut cache, &mut step, &EmptySubValueProvider, max_cplx).unwrap();
138
139// value = [[1, 3], [5], [9, 1, 8]]
140// token = Element(2, Remove(1))
141// cache = c2
142// step = s2
143
144test(&value);
145
146m.unmutate(&mut value, &mut cache, unmutate_token);
147
148// value = [[1, 3], [5], [9, 8]]
149// cache = c1 (back to original cache)
150// step = s2 (step has not been reversed)
151```
152
153When a mutated value is deemed interesting by the fuzzing engine, the method
154[`validate_value`](crate::Mutator::validate_value) is called on it in order to
155get a new Cache and MutationStep for it. The same method is called when the
156fuzzer reads values from a corpus to verify that they conform to the
157mutator’s expectations. For example, a [`CharWithinRangeMutator`](crate::mutators::char::CharWithinRangeMutator)
158will check whether the character is within a certain range.
159
160Note that in most cases, it is completely fine to never mutate a value’s cache,
161since it is recomputed by [`validate_value`](crate::Mutator::validate_value) when
162needed.
163
164## SubValueProvider
165
166The method `ordered_mutate` takes a [`&dyn SubValueProvider`](crate::SubValueProvider)
167as argument. The purpose of a sub-value provider is to provide the mutator with
168subvalues taken from the fuzzing corpus. If you are familiar with fuzzing
169terminology, then think of the sub-value provider as the structure-aware replacement
170for the “crossover” mutation and the dictionary. Here is how it works:
171
172For each value in the fuzzing corpus, the mutator iterates over each subpart of the
173value by calling [`self.visit_subvalues(value, cache, visit_closure)`](Mutator::visit_subvalues).
174For example, for the value
175```
176struct S {
177 a: usize,
178 b: Option<bool>,
179 c: (Option<bool>, usize)
180}
181let x = S {
182 a: 887236,
183 b: None,
184 c: (Some(true), 10372)
185};
186```
187the `visit_subvalues` method will call the `visit` closure with each subvalue
188and its complexity. For the value `x` above, it will be called with the
189following arguments:
190```ignore
191(&x.a , 64.0) // 887236
192(&x.b , 1.0) // None
193(&x.c , 66.0) // (Some(true), 10372)
194(&x.c.0 , 2.0) // Some(true)
195(&x.c.1 , 64.0) // 10372
196(&x.c.0.unwrap(), 1.0) // true
197```
198
199The fuzzer builds a data structure keeping track of these subvalues and pass it
200to the mutator as a `&dyn SubValueProvider`. The mutator could then use it as
201follows:
202```ignore
203fn ordered_mutate(&self, value: &mut S, cache: &mut Self::Cache, step: &mut Self::Step, subvalue_provider: &dyn SubValueProvider, max_cplx: f64) -> Option<(Self::UnmutateToken, f64)>
204{
205 // let's say we want to replace the value x.c.1 with something taken from the subvalue provider
206 if let Some((new_xc1, new_xc1_cplx)) = subvalue_provider.get_subvalue(TypeId::of::<usize>(), &mut idx, max_xc1_cplx) {
207 let new_xc1 = new_xc1.downcast_ref::<usize>().unwrap().clone(); // guaranteed to succeed
208 value.x.c.1 = new_xc1;
209 // etc.
210 }
211}
212```
213**/
214pub trait Mutator<Value: Clone + 'static>: 'static {
215 /// Accompanies each value to help compute its complexity and mutate it efficiently.
216 type Cache: Clone;
217 /// Contains information about what mutations have already been tried.
218 type MutationStep: Clone;
219 /// Contains information about what arbitrary values have already been generated.
220 type ArbitraryStep: Clone;
221 /// Describes how to reverse a mutation
222 type UnmutateToken;
223
224 /// Must be called after creating a mutator, to initialise its internal state.
225 fn initialize(&self);
226
227 /// The first [`ArbitraryStep`](Mutator::ArbitraryStep) value to be passed to [`ordered_arbitrary`](crate::Mutator::ordered_arbitrary)
228 fn default_arbitrary_step(&self) -> Self::ArbitraryStep;
229
230 /// Quickly verifies that the value conforms to the mutator’s expectations
231 fn is_valid(&self, value: &Value) -> bool;
232
233 /// Verifies that the value conforms to the mutator’s expectations and, if it does,
234 /// returns the [`Cache`](Mutator::Cache) associated with that value.
235 fn validate_value(&self, value: &Value) -> Option<Self::Cache>;
236
237 /// Returns the first [`MutationStep`](Mutator::MutationStep) associated with the value
238 /// and cache.
239 fn default_mutation_step(&self, value: &Value, cache: &Self::Cache) -> Self::MutationStep;
240
241 /// The log2 of the number of values that can be produced by this mutator,
242 /// or an approximation of this number (e.g. the number of bits that are
243 /// needed to identify each possible value).
244 ///
245 /// If the mutator can only produce one value, then the return value should
246 /// be equal to 0.0
247 fn global_search_space_complexity(&self) -> f64;
248
249 /// The maximum complexity that a value can possibly have.
250 fn max_complexity(&self) -> f64;
251
252 /// The minimum complexity that a value can possibly have.
253 fn min_complexity(&self) -> f64;
254
255 /// Computes the complexity of the value.
256 ///
257 /// The returned value must be greater or equal than 0.
258 /// It is only allowed to return 0 if the mutator cannot produce
259 /// any other value than the one given as argument.
260 fn complexity(&self, value: &Value, cache: &Self::Cache) -> f64;
261
262 /// Generates an entirely new value based on the given [`ArbitraryStep`](Mutator::ArbitraryStep).
263 ///
264 /// The generated value should be smaller than the given `max_cplx`.
265 ///
266 /// The return value is `None` if no more new value can be generated or if
267 /// it is not possible to stay within the given complexity. Otherwise, it
268 /// is the value itself and its complexity, which should be equal to
269 /// [`self.complexity(value, cache)`](Mutator::complexity)
270 fn ordered_arbitrary(&self, step: &mut Self::ArbitraryStep, max_cplx: f64) -> Option<(Value, f64)>;
271
272 /// Generates an entirely new value.
273 ///
274 /// The generated value should be smaller than the given `max_cplx`.
275 /// However, if that is not possible, then it should return a value of
276 /// the lowest possible complexity.
277 ///
278 /// Returns the value itself and its complexity, which must be equal to
279 /// [`self.complexity(value, cache)`](Mutator::complexity)
280 fn random_arbitrary(&self, max_cplx: f64) -> (Value, f64);
281
282 /// Mutates a value (and optionally its cache) based on the given
283 /// [`MutationStep`](Mutator::MutationStep).
284 ///
285 /// The mutated value should be within the given
286 /// `max_cplx`.
287 ///
288 /// Returns `None` if it no longer possible to mutate
289 /// the value to a new state, or if it is not possible to keep it under
290 /// `max_cplx`. Otherwise, return the [`UnmutateToken`](Mutator::UnmutateToken)
291 /// that describes how to undo the mutation, as well as the new complexity of the value.
292 fn ordered_mutate(
293 &self,
294 value: &mut Value,
295 cache: &mut Self::Cache,
296 step: &mut Self::MutationStep,
297 subvalue_provider: &dyn SubValueProvider,
298 max_cplx: f64,
299 ) -> Option<(Self::UnmutateToken, f64)>;
300
301 /// Mutates a value (and optionally its cache).
302 ///
303 /// The mutated value should be within the given `max_cplx`. But if that
304 /// is not possible, then it should mutate the value so that it has a minimal complexity.
305 ///
306 /// Returns the [`UnmutateToken`](Mutator::UnmutateToken) that describes how to undo
307 /// the mutation as well as the new complexity of the value.
308 fn random_mutate(&self, value: &mut Value, cache: &mut Self::Cache, max_cplx: f64) -> (Self::UnmutateToken, f64);
309
310 /// Undoes a mutation performed on the given value and cache, described by
311 /// the given [`UnmutateToken`](Mutator::UnmutateToken).
312 fn unmutate(&self, value: &mut Value, cache: &mut Self::Cache, t: Self::UnmutateToken);
313
314 /// Call the given closure on all subvalues and their complexities.
315 fn visit_subvalues<'a>(&self, value: &'a Value, cache: &'a Self::Cache, visit: &mut dyn FnMut(&'a dyn Any, f64));
316}
317
318/// A [Serializer] is used to encode and decode test cases into bytes.
319///
320/// It is used to transfer test cases between the corpus on the file system and the fuzzer’s storage.
321pub trait Serializer {
322 /// The type of the value to be serialized
323 type Value;
324
325 /// The extension of the file containing the serialized value
326 fn extension(&self) -> &str;
327
328 #[allow(clippy::wrong_self_convention)]
329 /// Deserialize the bytes into the value.
330 ///
331 /// This method can fail by returning `None`
332 fn from_data(&self, data: &[u8]) -> Option<Self::Value>;
333
334 /// Serialize the value into bytes
335 ///
336 /// This method should never fail.
337 fn to_data(&self, value: &Self::Value) -> Vec<u8>;
338}
339
340/// A [CorpusDelta] describes how to reflect a change in the pool’s content to the corpus on the file system.
341///
342/// It is used as the return type to [`pool.process(..)`](CompatibleWithObservations::process) where a test case along
343/// with its associated sensor observations is given to the pool. Thus, it is always implicitly associated with
344/// a specific pool and test case.
345#[derive(Debug)]
346pub struct CorpusDelta {
347 /// The common path to the subfolder inside the main corpus where the test cases (added or removed) reside
348 pub path: PathBuf,
349 /// Whether the test case was added to the pool
350 pub add: bool,
351 /// A list of test cases that were removed
352 pub remove: Vec<PoolStorageIndex>,
353}
354
355impl CorpusDelta {
356 #[coverage(off)]
357 pub fn fuzzer_event(deltas: &[CorpusDelta]) -> FuzzerEvent {
358 let mut add = 0;
359 let mut remove = 0;
360 for delta in deltas {
361 if delta.add {
362 add += 1;
363 }
364 remove += delta.remove.len();
365 }
366
367 if add == 0 && remove == 0 {
368 FuzzerEvent::None
369 } else {
370 FuzzerEvent::Replace(add, remove)
371 }
372 }
373}
374
375/**
376A [Sensor] records information when running the test function, which the
377fuzzer can use to determine the importance of a test case.
378
379For example, the sensor can record the code coverage triggered by the test case,
380store the source location of a panic, measure the number of allocations made, etc.
381The observations made by a sensor are then assessed by a [Pool], which must be
382explicitly [compatible](CompatibleWithObservations) with the sensor’s observations.
383*/
384pub trait Sensor: SaveToStatsFolder + 'static {
385 type Observations;
386
387 /// Signal to the sensor that it should prepare to record observations
388 fn start_recording(&mut self);
389 /// Signal to the sensor that it should stop recording observations
390 fn stop_recording(&mut self);
391
392 /// Access the sensor's observations
393 fn get_observations(&mut self) -> Self::Observations;
394}
395
396/// A trait implemented by the [statistics of a pool](crate::Pool::Stats)
397///
398/// The types implementing `Stats` must be displayable in the terminal and must be
399/// [convertible to CSV fields](crate::ToCSV). However, note that at the moment some pools
400/// choose to produce empty CSV values for their statistics. Consequently, their statistics
401/// will not be available in the `fuzz/stats/<id>/events.csv` file written by fuzzcheck
402/// at the end of a fuzz test.
403///
404/// Some pools may choose not to display their statistics in the terminal.
405pub trait Stats: Display + ToCSV + 'static {}
406
407/// An object safe trait that combines the methods of the [`Sensor`], [`Pool`], and [`CompatibleWithObservations`] traits.
408///
409/// While it's often useful to work with the [`Sensor`] and [`Pool`] traits separately, the
410/// fuzzer doesn't actually need to know about the sensor and pool individually. By having
411/// this `SensorAndPool` trait, we can give the fuzzer a `Box<dyn SensorAndPool>` and get rid of
412/// two generic type parameters: `S: Sensor` and `P: Pool + CompatibleWithObservations<S::Observations>`.
413///
414/// This is better for compile times and simplifies the implementation of the fuzzer. Users of
415/// `fuzzcheck` should feel free to ignore this trait, as it is arguably more an implementation detail
416/// than a fundamental building block of the fuzzer.
417///
418/// Currently, there are two types implementing `SensorAndPool`:
419/// 1. `(S, P)` where `S: Sensor` and `P: Pool + CompatibleWithObservations<S::Observations>`
420/// 2. [`AndSensorAndPool`](crate::sensors_and_pools::AndSensorAndPool)
421pub trait SensorAndPool: SaveToStatsFolder {
422 fn stats(&self) -> Box<dyn Stats>;
423 fn start_recording(&mut self);
424 fn stop_recording(&mut self);
425 fn process(&mut self, input_id: PoolStorageIndex, cplx: f64) -> Vec<CorpusDelta>;
426 fn get_random_index(&mut self) -> Option<PoolStorageIndex>;
427}
428impl<A, B> SaveToStatsFolder for (A, B)
429where
430 A: SaveToStatsFolder,
431 B: SaveToStatsFolder,
432{
433 #[coverage(off)]
434 fn save_to_stats_folder(&self) -> Vec<(PathBuf, Vec<u8>)> {
435 let mut x = self.0.save_to_stats_folder();
436 x.extend(self.1.save_to_stats_folder());
437 x
438 }
439}
440impl<S, P> SensorAndPool for (S, P)
441where
442 S: Sensor,
443 P: CompatibleWithObservations<S::Observations>,
444 S: SaveToStatsFolder,
445 P: SaveToStatsFolder,
446{
447 #[coverage(off)]
448 fn stats(&self) -> Box<dyn Stats> {
449 Box::new(self.1.stats())
450 }
451 #[coverage(off)]
452 fn start_recording(&mut self) {
453 self.0.start_recording();
454 }
455 #[coverage(off)]
456 fn stop_recording(&mut self) {
457 self.0.stop_recording();
458 }
459 #[coverage(off)]
460 fn process(&mut self, input_id: PoolStorageIndex, complexity: f64) -> Vec<CorpusDelta> {
461 self.1.process(input_id, &self.0.get_observations(), complexity)
462 }
463 #[coverage(off)]
464 fn get_random_index(&mut self) -> Option<PoolStorageIndex> {
465 self.1.get_random_index()
466 }
467}
468
469pub enum CSVField {
470 Integer(isize),
471 Float(f64),
472 String(String),
473}
474impl CSVField {
475 #[coverage(off)]
476 pub fn to_bytes(fields: &[CSVField]) -> Vec<u8> {
477 let mut bytes = vec![];
478 for field in fields {
479 match field {
480 CSVField::Integer(n) => {
481 bytes.extend(format!("{}", n).as_bytes());
482 }
483 CSVField::Float(f) => {
484 bytes.extend(format!("{:.4}", f).as_bytes());
485 }
486 CSVField::String(s) => {
487 bytes.extend(format!("{:?}", s).as_bytes());
488 }
489 }
490 bytes.extend(b",");
491 }
492 bytes.extend(b"\n");
493 bytes
494 }
495}
496
497/**
498Describes how to save a list of this value as a CSV file.
499
500It is done via two methods:
5011. [self.csv_headers\()](ToCSV::csv_headers) gives the first row of the file, as a list of [CSVField].
502For example, it can be `time, score`.
5032. [self.to_csv_record\()](ToCSV::to_csv_record) serializes the value as a CSV row. For example, it
504can be `16:07:32, 34.0`.
505
506Note that each call to [self.to_csv_record\()](ToCSV::to_csv_record) must return a list of [CSVField]
507where the field at index `i` corresponds to the header at index `i` given by [self.csv_headers()](ToCSV::csv_headers).
508Otherwise, the CSV file will be invalid.
509*/
510pub trait ToCSV {
511 /// The headers of the CSV file
512 fn csv_headers(&self) -> Vec<CSVField>;
513 /// Serializes `self` as a list of [CSVField]. Each element in the vector must correspond to a header given
514 /// by [self.csv_headers\()](ToCSV::csv_headers)
515 fn to_csv_record(&self) -> Vec<CSVField>;
516}
517impl ToCSV for Box<dyn Stats> {
518 #[coverage(off)]
519 fn csv_headers(&self) -> Vec<CSVField> {
520 self.as_ref().csv_headers()
521 }
522
523 #[coverage(off)]
524 fn to_csv_record(&self) -> Vec<CSVField> {
525 self.as_ref().to_csv_record()
526 }
527}
528impl Stats for Box<dyn Stats> {}
529/**
530A [`Pool`] ranks test cases based on observations recorded by a sensor.
531
532The pool trait is divided into two parts:
5331. [`Pool`] contains general methods that are independent of the sensor used
5342. [`CompatibleWithObservations<O>`] is a subtrait of [`Pool`]. It describes how the pool handles
535observations made by the [`Sensor`].
536*/
537pub trait Pool: SaveToStatsFolder {
538 /// Statistics about the pool to be printed to the terminal as the fuzzer is running and
539 /// saved to a .csv file after the run
540 type Stats: Stats;
541
542 /// The pool’s statistics
543 fn stats(&self) -> Self::Stats;
544
545 /// Get the index of a random test case.
546 ///
547 /// Most [Pool] implementations will want to prioritise certain test cases
548 /// over others based on their associated observations.
549 fn get_random_index(&mut self) -> Option<PoolStorageIndex>;
550
551 /// Gives the relative importance of the pool. It must be a positive number.
552 ///
553 /// The weight of the pool is not used by the fuzzer directly, but can be used
554 /// by types such as [`AndPool`](crate::sensors_and_pools::AndPool).
555 ///
556 /// The value is 1.0 by default.
557 fn weight(&self) -> f64 {
558 1.0
559 }
560}
561
562/**
563A subtrait of [Pool] describing how the pool handles observations made by a sensor.
564
565This trait is separate from [Pool] because a single pool type may handle multiple different kinds of sensors.
566
567It is responsible for judging whether the observations are interesting, and then adding the test case to the pool
568if they are. It communicates to the rest of the fuzzer what test cases were added or removed from the pool via the
569[`CorpusDelta`] type. This ensures that the right message can be printed to the terminal and that the corpus on the
570file system, which reflects the content of the pool, can be properly updated.
571*/
572pub trait CompatibleWithObservations<O>: Pool {
573 fn process(&mut self, input_id: PoolStorageIndex, observations: &O, complexity: f64) -> Vec<CorpusDelta>;
574}
575
576/// A trait for types that want to save their content to the `stats` folder which is created after a fuzzing run.
577pub trait SaveToStatsFolder {
578 /// Save information about `self` to the stats folder
579 ///
580 /// Return a vector of tuples `(path_to_file, serialised_content)` representing a list of files to create under
581 /// the `stats_folder`. The first element of each tuple is the path of the new created file. If this path is relative,
582 /// it is relative to the `stats` folder path. The second element is the content of the file, as bytes.
583 fn save_to_stats_folder(&self) -> Vec<(PathBuf, Vec<u8>)>;
584}