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}