Skip to main content

dapol/
dapol_config.rs

1use derive_builder::Builder;
2use log::debug;
3use serde::Deserialize;
4use std::{ffi::OsString, fs::File, io::Read, path::PathBuf, str::FromStr};
5
6use crate::{
7    accumulators::AccumulatorType,
8    entity::{self, EntitiesParser},
9    utils::LogOnErr,
10    DapolTree, DapolTreeError, Height, MaxLiability, MaxThreadCount, Salt, Secret,
11};
12use crate::{salt, secret};
13
14/// Configuration needed to construct a [DapolTree].
15///
16/// The config is defined by a struct. A builder pattern is used to construct
17/// the config, but it can also be constructed by deserializing a file.
18/// Currently only toml files are supported, with the following format:
19///
20/// ```toml,ignore
21#[doc = include_str!("../examples/dapol_config_example.toml")]
22/// ```
23///
24/// Example of how to use the builder to construct a [DapolTree]:
25/// ```
26/// use std::{path::PathBuf, str::FromStr};
27/// use dapol::{
28///     AccumulatorType, DapolConfigBuilder, DapolTree, Entity, Height,
29///     MaxLiability, MaxThreadCount, Salt, Secret,
30/// };
31///
32/// let secrets_file_path =
33/// PathBuf::from("./examples/dapol_secrets_example.toml");
34/// let entities_file_path = PathBuf::from("./examples/entities_example.csv");
35/// let height = Height::expect_from(8);
36/// let salt_b = Salt::from_str("salt_b").unwrap();
37/// let salt_s = Salt::from_str("salt_s").unwrap();
38/// let max_liability = MaxLiability::from(10_000_000);
39/// let max_thread_count = MaxThreadCount::from(8);
40///
41/// // The builder requires at least the following to be given:
42/// // - accumulator_type
43/// // - entities
44/// // - secrets
45/// let dapol_config = DapolConfigBuilder::default()
46///     .accumulator_type(AccumulatorType::NdmSmt)
47///     .height(height.clone())
48///     .salt_b(salt_b.clone())
49///     .salt_s(salt_s.clone())
50///     .max_liability(max_liability.clone())
51///     .max_thread_count(max_thread_count.clone())
52///     .secrets_file_path(secrets_file_path.clone())
53///     .entities_file_path(entities_file_path.clone())
54///     .build()
55///     .unwrap();
56/// ```
57///
58/// Example of how to use a config file to construct a [DapolTree]:
59/// ```
60/// use std::{path::PathBuf, str::FromStr};
61/// use dapol::DapolConfig;
62///
63/// let config_file_path =
64/// PathBuf::from("./examples/dapol_config_example.toml");
65/// let dapol_config_from_file =
66/// DapolConfig::deserialize(config_file_path).unwrap();
67/// ```
68///
69/// Note that you can also construct a [DapolTree] by calling the
70/// constructor directly (see [DapolTree]).
71#[derive(Deserialize, Debug, Builder, PartialEq)]
72#[builder(build_fn(skip))]
73pub struct DapolConfig {
74    #[doc = include_str!("./shared_docs/accumulator_type.md")]
75    accumulator_type: AccumulatorType,
76
77    #[doc = include_str!("./shared_docs/salt_b.md")]
78    salt_b: Salt,
79
80    #[doc = include_str!("./shared_docs/salt_s.md")]
81    salt_s: Salt,
82
83    #[doc = include_str!("./shared_docs/max_liability.md")]
84    max_liability: MaxLiability,
85
86    #[doc = include_str!("./shared_docs/height.md")]
87    height: Height,
88
89    #[doc = include_str!("./shared_docs/max_thread_count.md")]
90    max_thread_count: MaxThreadCount,
91
92    #[builder(setter(custom))]
93    random_seed: Option<u64>,
94
95    #[builder(private)]
96    entities: EntityConfig,
97
98    #[builder(private)]
99    secrets: SecretsConfig,
100}
101
102use serde_with::{serde_as, DisplayFromStr};
103#[serde_as]
104#[derive(Deserialize, Debug, Clone, Default, PartialEq)]
105pub struct SecretsConfig {
106    file_path: Option<PathBuf>,
107    #[serde_as(as = "Option<DisplayFromStr>")]
108    master_secret: Option<Secret>,
109}
110
111#[derive(Deserialize, Debug, Clone, Default, PartialEq)]
112pub struct EntityConfig {
113    file_path: Option<PathBuf>,
114    num_random_entities: Option<u64>,
115}
116
117// -------------------------------------------------------------------------------------------------
118// Builder.
119
120impl DapolConfigBuilder {
121    /// Set the path for the file containing the entity data.
122    ///
123    /// Wrapped in an option to provide ease of use if the PathBuf is already
124    /// an option.
125    pub fn entities_file_path_opt(&mut self, path: Option<PathBuf>) -> &mut Self {
126        match &mut self.entities {
127            None => {
128                self.entities = Some(EntityConfig {
129                    file_path: path,
130                    num_random_entities: None,
131                })
132            }
133            Some(entities) => entities.file_path = path,
134        }
135        self
136    }
137
138    /// Set the path for the file containing the entity data.
139    pub fn entities_file_path(&mut self, path: PathBuf) -> &mut Self {
140        self.entities_file_path_opt(Some(path))
141    }
142
143    /// Set the number of entities that will be generated randomly.
144    ///
145    /// If a path is also given for the entities then that is used instead,
146    /// i.e. they are not combined.
147    ///
148    /// Wrapped in an option to provide ease of use if the PathBuf is already
149    /// an option.
150    pub fn num_random_entities_opt(&mut self, num_entities: Option<u64>) -> &mut Self {
151        match &mut self.entities {
152            None => {
153                self.entities = Some(EntityConfig {
154                    file_path: None,
155                    num_random_entities: num_entities,
156                })
157            }
158            Some(entities) => entities.num_random_entities = num_entities,
159        }
160        self
161    }
162
163    /// Set the number of entities that will be generated randomly.
164    ///
165    /// If a path is also given for the entities then that is used instead,
166    /// i.e. they are not combined.
167    pub fn num_random_entities(&mut self, num_entities: u64) -> &mut Self {
168        self.num_random_entities_opt(Some(num_entities))
169    }
170
171    /// Set the path for the file containing the secrets.
172    ///
173    /// Wrapped in an option to provide ease of use if the PathBuf is already
174    /// an option.
175    pub fn secrets_file_path_opt(&mut self, path: Option<PathBuf>) -> &mut Self {
176        match &mut self.secrets {
177            None => {
178                self.secrets = Some(SecretsConfig {
179                    file_path: path,
180                    master_secret: None,
181                })
182            }
183            Some(secrets) => secrets.file_path = path,
184        }
185        self
186    }
187
188    /// Set the path for the file containing the secrets.
189    pub fn secrets_file_path(&mut self, path: PathBuf) -> &mut Self {
190        self.secrets_file_path_opt(Some(path))
191    }
192
193    /// Set the master secret value directly.
194    #[doc = include_str!("./shared_docs/master_secret.md")]
195    pub fn master_secret(&mut self, master_secret: Secret) -> &mut Self {
196        match &mut self.secrets {
197            None => {
198                self.secrets = Some(SecretsConfig {
199                    file_path: None,
200                    master_secret: Some(master_secret),
201                })
202            }
203            Some(secrets) => secrets.master_secret = Some(master_secret),
204        }
205        self
206    }
207
208    #[doc = include_str!("./shared_docs/salt_b.md")]
209    ///
210    /// Wrapped in an option to provide ease of use if the value is already
211    /// an option.
212    pub fn salt_b_opt(&mut self, salt_b: Option<Salt>) -> &mut Self {
213        self.salt_b = salt_b;
214        self
215    }
216
217    #[doc = include_str!("./shared_docs/salt_s.md")]
218    ///
219    /// Wrapped in an option to provide ease of use if the value is already
220    /// an option.
221    pub fn salt_s_opt(&mut self, salt_s: Option<Salt>) -> &mut Self {
222        self.salt_s = salt_s;
223        self
224    }
225
226    /// For seeding any PRNG to have deterministic output.
227    ///
228    /// Note: This is **not** cryptographically secure and should only be used
229    /// for testing.
230    #[cfg(any(test, feature = "testing"))]
231    pub fn random_seed(&mut self, random_seed: u64) -> &mut Self {
232        self.random_seed = Some(Some(random_seed));
233        self
234    }
235
236    #[cfg(any(test, feature = "testing"))]
237    fn get_random_seed(&self) -> Option<u64> {
238        self.random_seed.unwrap_or(None)
239    }
240
241    #[cfg(not(any(test, feature = "testing")))]
242    fn get_random_seed(&self) -> Option<u64> {
243        None
244    }
245
246    /// Build the config struct.
247    pub fn build(&self) -> Result<DapolConfig, DapolConfigBuilderError> {
248        let accumulator_type =
249            self.accumulator_type
250                .clone()
251                .ok_or(DapolConfigBuilderError::UninitializedField(
252                    "accumulator_type",
253                ))?;
254
255        let entities = EntityConfig {
256            file_path: self.entities.clone().and_then(|e| e.file_path).or(None),
257            num_random_entities: self
258                .entities
259                .clone()
260                .and_then(|e| e.num_random_entities)
261                .or(None),
262        };
263
264        if entities.file_path.is_none() && entities.num_random_entities.is_none() {
265            return Err(DapolConfigBuilderError::UninitializedField("entities"));
266        }
267
268        let secrets = SecretsConfig {
269            file_path: self.secrets.clone().and_then(|e| e.file_path).or(None),
270            master_secret: self.secrets.clone().and_then(|e| e.master_secret).or(None),
271        };
272
273        if secrets.file_path.is_none() && secrets.master_secret.is_none() {
274            return Err(DapolConfigBuilderError::UninitializedField("secrets"));
275        }
276
277        let salt_b = self.salt_b.clone().unwrap_or_default();
278        let salt_s = self.salt_s.clone().unwrap_or_default();
279        let height = self.height.unwrap_or_default();
280        let max_thread_count = self.max_thread_count.unwrap_or_default();
281        let max_liability = self.max_liability.unwrap_or_default();
282        let random_seed = self.get_random_seed();
283
284        Ok(DapolConfig {
285            accumulator_type,
286            salt_b,
287            salt_s,
288            max_liability,
289            height,
290            max_thread_count,
291            entities,
292            secrets,
293            random_seed,
294        })
295    }
296}
297
298// -------------------------------------------------------------------------------------------------
299// Deserialization & parsing.
300
301impl DapolConfig {
302    /// Open the file, then try to create the [DapolConfig] struct.
303    ///
304    /// An error is returned if:
305    /// 1. The file cannot be opened.
306    /// 2. The file cannot be read.
307    /// 3. The file type is not supported.
308    ///
309    /// Config deserialization example:
310    /// ```
311    /// use std::path::PathBuf;
312    /// use dapol::DapolConfig;
313    ///
314    /// let file_path = PathBuf::from("./examples/dapol_config_example.toml");
315    /// let config = DapolConfig::deserialize(file_path).unwrap();
316    /// ```
317    pub fn deserialize(config_file_path: PathBuf) -> Result<Self, DapolConfigError> {
318        debug!(
319            "Attempting to deserialize {:?} as a file containing DAPOL config",
320            config_file_path.clone().into_os_string()
321        );
322
323        let ext = config_file_path
324            .extension()
325            .and_then(|s| s.to_str())
326            .ok_or(DapolConfigError::UnknownFileType(
327                config_file_path.clone().into_os_string(),
328            ))?;
329
330        let mut config = match FileType::from_str(ext)? {
331            FileType::Toml => {
332                let mut buf = String::new();
333                File::open(config_file_path.clone())?.read_to_string(&mut buf)?;
334                let config: DapolConfig = toml::from_str(&buf)?;
335                config
336            }
337        };
338
339        config.entities.file_path =
340            extend_path_if_relative(config_file_path.clone(), config.entities.file_path);
341        config.secrets.file_path =
342            extend_path_if_relative(config_file_path, config.secrets.file_path);
343
344        debug!("Successfully deserialized DAPOL config file");
345
346        Ok(config)
347    }
348
349    /// Try to construct a [DapolTree] from the config.
350    // STENT TODO rather call this create_tree
351    #[cfg(any(test, feature = "testing"))]
352    pub fn parse(self) -> Result<DapolTree, DapolConfigError> {
353        debug!("Parsing config to create a new DAPOL tree: {:?}", self);
354
355        let salt_b = self.salt_b;
356        let salt_s = self.salt_s;
357
358        let entities = EntitiesParser::new()
359            .with_path_opt(self.entities.file_path)
360            .with_num_entities_opt(self.entities.num_random_entities)
361            .parse_file_or_generate_random()?;
362
363        let master_secret = if let Some(path) = self.secrets.file_path {
364            Ok(DapolConfig::parse_secrets_file(path)?)
365        } else if let Some(master_secret) = self.secrets.master_secret {
366            Ok(master_secret)
367        } else {
368            Err(DapolConfigError::CannotFindMasterSecret)
369        }?;
370
371        let dapol_tree = if let Some(random_seed) = self.random_seed {
372            DapolTree::new_with_random_seed(
373                self.accumulator_type,
374                master_secret,
375                salt_b,
376                salt_s,
377                self.max_liability,
378                self.max_thread_count,
379                self.height,
380                entities,
381                random_seed,
382            )
383            .log_on_err()?
384        } else {
385            DapolTree::new(
386                self.accumulator_type,
387                master_secret,
388                salt_b,
389                salt_s,
390                self.max_liability,
391                self.max_thread_count,
392                self.height,
393                entities,
394            )
395            .log_on_err()?
396        };
397
398        Ok(dapol_tree)
399    }
400
401    /// Try to construct a [DapolTree] from the config.
402    // STENT TODO rather call this create_tree
403    #[cfg(not(any(test, feature = "testing")))]
404    pub fn parse(self) -> Result<DapolTree, DapolConfigError> {
405        debug!("Parsing config to create a new DAPOL tree: {:?}", self);
406
407        let salt_b = self.salt_b;
408        let salt_s = self.salt_s;
409
410        let entities = EntitiesParser::new()
411            .with_path_opt(self.entities.file_path)
412            .with_num_entities_opt(self.entities.num_random_entities)
413            .parse_file_or_generate_random()?;
414
415        let master_secret = if let Some(path) = self.secrets.file_path {
416            Ok(DapolConfig::parse_secrets_file(path)?)
417        } else if let Some(master_secret) = self.secrets.master_secret {
418            Ok(master_secret)
419        } else {
420            Err(DapolConfigError::CannotFindMasterSecret)
421        }?;
422
423        Ok(DapolTree::new(
424            self.accumulator_type,
425            master_secret,
426            salt_b,
427            salt_s,
428            self.max_liability,
429            self.max_thread_count,
430            self.height,
431            entities,
432        )
433        .log_on_err()?)
434    }
435
436    /// Open and parse the secrets file, returning a [Secret].
437    ///
438    /// An error is returned if:
439    /// 1. The path is None (i.e. was not set).
440    /// 2. The file cannot be opened.
441    /// 3. The file cannot be read.
442    /// 4. The file type is not supported.
443    fn parse_secrets_file(path: PathBuf) -> Result<Secret, SecretsParserError> {
444        debug!(
445            "Attempting to parse {:?} as a file containing secrets",
446            path
447        );
448
449        let ext = path.extension().and_then(|s| s.to_str()).ok_or(
450            SecretsParserError::UnknownFileType(path.clone().into_os_string()),
451        )?;
452
453        let master_secret = match FileType::from_str(ext)? {
454            FileType::Toml => {
455                let mut buf = String::new();
456                File::open(path)?.read_to_string(&mut buf)?;
457                let secrets: DapolSecrets = toml::from_str(&buf)?;
458                secrets.master_secret
459            }
460        };
461
462        debug!("Successfully parsed DAPOL secrets file",);
463
464        Ok(master_secret)
465    }
466}
467
468fn extend_path_if_relative(
469    leader_path: PathBuf,
470    possibly_relative_path: Option<PathBuf>,
471) -> Option<PathBuf> {
472    match possibly_relative_path {
473        Some(path) => Some(
474            path.strip_prefix("./")
475                .map(|p| p.to_path_buf())
476                .ok()
477                .and_then(|tail| leader_path.parent().map(|parent| parent.join(tail)))
478                .unwrap_or(path.clone()),
479        ),
480        None => None,
481    }
482}
483
484/// Supported file types for deserialization.
485enum FileType {
486    Toml,
487}
488
489impl FromStr for FileType {
490    type Err = SecretsParserError;
491
492    fn from_str(ext: &str) -> Result<FileType, Self::Err> {
493        match ext {
494            "toml" => Ok(FileType::Toml),
495            _ => Err(SecretsParserError::UnsupportedFileType { ext: ext.into() }),
496        }
497    }
498}
499
500#[derive(Deserialize, Debug)]
501struct DapolSecrets {
502    master_secret: Secret,
503}
504
505// -------------------------------------------------------------------------------------------------
506// Errors.
507
508/// Errors encountered when parsing [DapolConfig].
509#[derive(thiserror::Error, Debug)]
510pub enum DapolConfigError {
511    #[error("Entities parsing failed while trying to parse DAPOL config")]
512    EntitiesError(#[from] entity::EntitiesParserError),
513    #[error("Error parsing the master secret string")]
514    MasterSecretParseError(#[from] secret::SecretParserError),
515    #[error("Error parsing the master secret file")]
516    MasterSecretFileParseError(#[from] SecretsParserError),
517    #[error("Either master secret must be set directly, or a path to a file containing it must be given")]
518    CannotFindMasterSecret,
519    #[error("Error parsing the salt string")]
520    SaltParseError(#[from] salt::SaltParserError),
521    #[error("Tree construction failed after parsing DAPOL config")]
522    BuildError(#[from] DapolTreeError),
523    #[error("Unable to find file extension for path {0:?}")]
524    UnknownFileType(OsString),
525    #[error("The file type with extension {ext:?} is not supported")]
526    UnsupportedFileType { ext: String },
527    #[error("Error reading the file")]
528    FileReadError(#[from] std::io::Error),
529    #[error("Deserialization process failed")]
530    DeserializationError(#[from] toml::de::Error),
531}
532
533#[derive(thiserror::Error, Debug)]
534pub enum SecretsParserError {
535    #[error("Unable to find file extension for path {0:?}")]
536    UnknownFileType(OsString),
537    #[error("The file type with extension {ext:?} is not supported")]
538    UnsupportedFileType { ext: String },
539    #[error("Error reading the file")]
540    FileReadError(#[from] std::io::Error),
541    #[error("Deserialization process failed")]
542    DeserializationError(#[from] toml::de::Error),
543}
544
545// -------------------------------------------------------------------------------------------------
546// Unit tests
547
548#[cfg(test)]
549mod tests {
550    use crate::accumulators::Accumulator;
551    use crate::utils::test_utils::assert_err;
552
553    use super::*;
554    use std::fs::File;
555    use std::io::{BufRead, BufReader};
556    use std::path::Path;
557
558    // Matches the config found in the dapol_config_example.toml file.
559    fn dapol_config_builder_matching_example_file() -> DapolConfigBuilder {
560        let src_dir = env!("CARGO_MANIFEST_DIR");
561        let resources_dir = Path::new(&src_dir).join("examples");
562        let secrets_file_path = resources_dir.join("dapol_secrets_example.toml");
563        let entities_file_path = resources_dir.join("entities_example.csv");
564
565        let height = Height::expect_from(16u8);
566        let salt_b = Salt::from_str("salt_b").unwrap();
567        let salt_s = Salt::from_str("salt_s").unwrap();
568        let max_liability = MaxLiability::from(10_000_000u64);
569        let max_thread_count = MaxThreadCount::from(8u8);
570        let master_secret = Secret::from_str("master_secret").unwrap();
571        let num_entities = 100u64;
572
573        DapolConfigBuilder::default()
574            .accumulator_type(AccumulatorType::NdmSmt)
575            .height(height.clone())
576            .salt_b(salt_b.clone())
577            .salt_s(salt_s.clone())
578            .max_liability(max_liability.clone())
579            .max_thread_count(max_thread_count.clone())
580            .secrets_file_path(secrets_file_path.clone())
581            .master_secret(master_secret.clone())
582            .entities_file_path(entities_file_path.clone())
583            .num_random_entities(num_entities)
584            .clone()
585    }
586
587    mod creating_config {
588        use super::*;
589
590        #[test]
591        fn builder_with_all_default_values_gives_correct_config() {
592            // The builder requires at least the following to be given:
593            // - accumulator_type
594            // - entities
595            // - secrets
596            // The rest are left as default.
597
598            let src_dir = env!("CARGO_MANIFEST_DIR");
599            let resources_dir = Path::new(&src_dir).join("examples");
600            let secrets_file_path = resources_dir.join("dapol_secrets_example.toml");
601            let entities_file_path = resources_dir.join("entities_example.csv");
602
603            let dapol_config = DapolConfigBuilder::default()
604                .accumulator_type(AccumulatorType::NdmSmt)
605                .secrets_file_path(secrets_file_path.clone())
606                .entities_file_path(entities_file_path.clone())
607                .build()
608                .unwrap();
609
610            // Assert the values that were explicitly set:
611            assert_eq!(dapol_config.accumulator_type, AccumulatorType::NdmSmt);
612            assert_eq!(dapol_config.entities.file_path, Some(entities_file_path));
613            assert_eq!(dapol_config.secrets.file_path, Some(secrets_file_path));
614
615            // Assert the values that were not set:
616            assert_eq!(dapol_config.entities.num_random_entities, None);
617            assert_eq!(dapol_config.secrets.master_secret, None);
618            assert_eq!(dapol_config.max_thread_count, MaxThreadCount::default());
619            assert_eq!(dapol_config.height, Height::default());
620            assert_eq!(dapol_config.max_liability, MaxLiability::default());
621
622            // Salts should be random bytes. Check that at least one byte is non-zero.
623            assert!(dapol_config.salt_b.as_bytes().iter().any(|b| *b != 0u8));
624            assert!(dapol_config.salt_s.as_bytes().iter().any(|b| *b != 0u8));
625        }
626
627        #[test]
628        fn builder_with_no_default_values_gives_correct_config() {
629            let src_dir = env!("CARGO_MANIFEST_DIR");
630            let resources_dir = Path::new(&src_dir).join("examples");
631            let secrets_file_path = resources_dir.join("dapol_secrets_example.toml");
632            let entities_file_path = resources_dir.join("entities_example.csv");
633
634            let height = Height::expect_from(16u8);
635            let salt_b = Salt::from_str("salt_b").unwrap();
636            let salt_s = Salt::from_str("salt_s").unwrap();
637            let max_liability = MaxLiability::from(10_000_000u64);
638            let max_thread_count = MaxThreadCount::from(8u8);
639            let master_secret = Secret::from_str("master_secret").unwrap();
640            let num_entities = 100u64;
641
642            let dapol_config = dapol_config_builder_matching_example_file()
643                .build()
644                .unwrap();
645
646            assert_eq!(dapol_config.accumulator_type, AccumulatorType::NdmSmt);
647            assert_eq!(dapol_config.entities.file_path, Some(entities_file_path));
648            assert_eq!(dapol_config.secrets.file_path, Some(secrets_file_path));
649            assert_eq!(
650                dapol_config.entities.num_random_entities,
651                Some(num_entities)
652            );
653            assert_eq!(dapol_config.secrets.master_secret, Some(master_secret));
654            assert_eq!(dapol_config.max_thread_count, max_thread_count);
655            assert_eq!(dapol_config.max_liability, max_liability);
656            assert_eq!(dapol_config.height, height);
657            assert_eq!(dapol_config.salt_b, salt_b);
658            assert_eq!(dapol_config.salt_s, salt_s);
659        }
660
661        #[test]
662        fn config_file_gives_same_config_as_builder() {
663            let src_dir = env!("CARGO_MANIFEST_DIR");
664            let resources_dir = Path::new(&src_dir).join("examples");
665            let config_file_path = resources_dir.join("dapol_config_example.toml");
666
667            let dapol_config_from_file = DapolConfig::deserialize(config_file_path).unwrap();
668            let dapol_config_from_builder = dapol_config_builder_matching_example_file()
669                .build()
670                .unwrap();
671
672            assert_eq!(dapol_config_from_file, dapol_config_from_builder);
673        }
674
675        #[test]
676        fn builder_without_accumulator_type_fails() {
677            let master_secret = Secret::from_str("master_secret").unwrap();
678            let num_entities = 100u64;
679
680            let res = DapolConfigBuilder::default()
681                .master_secret(master_secret)
682                .num_random_entities(num_entities)
683                .build();
684
685            assert_err!(
686                res,
687                Err(DapolConfigBuilderError::UninitializedField(
688                    "accumulator_type"
689                ))
690            );
691        }
692
693        #[test]
694        fn builder_without_secrets_fails() {
695            let num_entities = 100u64;
696
697            let res = DapolConfigBuilder::default()
698                .accumulator_type(AccumulatorType::NdmSmt)
699                .num_random_entities(num_entities)
700                .build();
701
702            assert_err!(
703                res,
704                Err(DapolConfigBuilderError::UninitializedField("secrets"))
705            );
706        }
707
708        #[test]
709        fn builder_without_entities_fails() {
710            let master_secret = Secret::from_str("master_secret").unwrap();
711
712            let res = DapolConfigBuilder::default()
713                .accumulator_type(AccumulatorType::NdmSmt)
714                .master_secret(master_secret)
715                .build();
716
717            assert_err!(
718                res,
719                Err(DapolConfigBuilderError::UninitializedField("entities"))
720            );
721        }
722
723        #[test]
724        fn fail_when_unsupproted_secrets_file_type() {
725            let this_file = std::file!();
726            let unsupported_path = PathBuf::from(this_file);
727
728            let num_entities = 100u64;
729
730            let res = DapolConfigBuilder::default()
731                .accumulator_type(AccumulatorType::NdmSmt)
732                .num_random_entities(num_entities)
733                .secrets_file_path(unsupported_path)
734                .build()
735                .unwrap()
736                .parse();
737
738            assert_err!(
739                res,
740                Err(DapolConfigError::MasterSecretFileParseError(
741                    SecretsParserError::UnsupportedFileType { ext: _ }
742                ))
743            );
744        }
745
746        #[test]
747        fn fail_when_unknown_secrets_file_type() {
748            let no_file_ext = PathBuf::from("../LICENSE");
749
750            let num_entities = 100u64;
751
752            let res = DapolConfigBuilder::default()
753                .accumulator_type(AccumulatorType::NdmSmt)
754                .num_random_entities(num_entities)
755                .secrets_file_path(no_file_ext)
756                .build()
757                .unwrap()
758                .parse();
759
760            assert_err!(
761                res,
762                Err(DapolConfigError::MasterSecretFileParseError(
763                    SecretsParserError::UnknownFileType(_)
764                ))
765            );
766        }
767    }
768
769    // TODO these are actually integration tests, so move them to tests dir
770    mod config_to_tree {
771        use super::*;
772
773        #[test]
774        fn parsing_config_gives_correct_tree() {
775            let src_dir = env!("CARGO_MANIFEST_DIR");
776            let resources_dir = Path::new(&src_dir).join("examples");
777            let entities_file_path = resources_dir.join("entities_example.csv");
778
779            let entities_file = File::open(entities_file_path.clone()).unwrap();
780            // "-1" because we don't include the top line of the csv which defines
781            // the column headings.
782            let num_entities = BufReader::new(entities_file).lines().count() - 1;
783
784            let height = Height::expect_from(8u8);
785            let master_secret = Secret::from_str("master_secret").unwrap();
786            let salt_b = Salt::from_str("salt_b").unwrap();
787            let salt_s = Salt::from_str("salt_s").unwrap();
788
789            let dapol_tree = DapolConfigBuilder::default()
790                .accumulator_type(AccumulatorType::NdmSmt)
791                .height(height.clone())
792                .salt_b(salt_b.clone())
793                .salt_s(salt_s.clone())
794                .master_secret(master_secret.clone())
795                .entities_file_path(entities_file_path.clone())
796                .build()
797                .unwrap()
798                .parse()
799                .unwrap();
800
801            assert_eq!(
802                dapol_tree.entity_mapping().unwrap().len(),
803                num_entities as usize
804            );
805            assert_eq!(dapol_tree.accumulator_type(), AccumulatorType::NdmSmt);
806            assert_eq!(*dapol_tree.height(), height);
807            assert_eq!(*dapol_tree.master_secret(), master_secret);
808            assert_eq!(dapol_tree.max_liability(), &MaxLiability::default());
809            assert_eq!(*dapol_tree.salt_b(), salt_b);
810            assert_eq!(*dapol_tree.salt_s(), salt_s);
811        }
812
813        #[test]
814        fn config_with_random_entities_gives_correct_tree() {
815            let height = Height::expect_from(8);
816            let num_random_entities = 10;
817            let master_secret = Secret::from_str("master_secret").unwrap();
818
819            let dapol_tree = DapolConfigBuilder::default()
820                .accumulator_type(AccumulatorType::NdmSmt)
821                .height(height)
822                .master_secret(master_secret)
823                .num_random_entities(num_random_entities)
824                .build()
825                .unwrap()
826                .parse()
827                .unwrap();
828
829            assert_eq!(
830                dapol_tree.entity_mapping().unwrap().len(),
831                num_random_entities as usize
832            );
833        }
834
835        #[test]
836        fn secrets_file_gives_same_master_secret_as_setting_directly() {
837            let src_dir = env!("CARGO_MANIFEST_DIR");
838            let resources_dir = Path::new(&src_dir).join("examples");
839            let secrets_file_path = resources_dir.join("dapol_secrets_example.toml");
840            let entities_file_path = resources_dir.join("entities_example.csv");
841            let master_secret = Secret::from_str("master_secret").unwrap();
842            let height = Height::expect_from(8u8);
843
844            let tree_from_secrets_file = DapolConfigBuilder::default()
845                .accumulator_type(AccumulatorType::NdmSmt)
846                .height(height)
847                .secrets_file_path(secrets_file_path.clone())
848                .entities_file_path(entities_file_path.clone())
849                .build()
850                .unwrap()
851                .parse()
852                .unwrap();
853
854            let tree_from_direct_secret = DapolConfigBuilder::default()
855                .accumulator_type(AccumulatorType::NdmSmt)
856                .height(height)
857                .master_secret(master_secret.clone())
858                .entities_file_path(entities_file_path.clone())
859                .build()
860                .unwrap()
861                .parse()
862                .unwrap();
863
864            assert_eq!(
865                tree_from_direct_secret.master_secret(),
866                tree_from_secrets_file.master_secret()
867            );
868        }
869
870        #[test]
871        fn secrets_file_preferred_over_setting_directly() {
872            let src_dir = env!("CARGO_MANIFEST_DIR");
873            let resources_dir = Path::new(&src_dir).join("examples");
874            let secrets_file_path = resources_dir.join("dapol_secrets_example.toml");
875            let entities_file_path = resources_dir.join("entities_example.csv");
876            let master_secret = Secret::from_str("garbage").unwrap();
877            let height = Height::expect_from(8u8);
878
879            let dapol_tree = DapolConfigBuilder::default()
880                .accumulator_type(AccumulatorType::NdmSmt)
881                .height(height)
882                .secrets_file_path(secrets_file_path.clone())
883                .master_secret(master_secret)
884                .entities_file_path(entities_file_path.clone())
885                .build()
886                .unwrap()
887                .parse()
888                .unwrap();
889
890            assert_eq!(
891                dapol_tree.master_secret(),
892                &Secret::from_str("master_secret").unwrap()
893            );
894        }
895    }
896}