1
//! Miscellaneous types used in configuration
2
//!
3
//! This module contains types that need to be shared across various crates
4
//! and layers, but which don't depend on specific elements of the Tor system.
5

            
6
use std::borrow::Cow;
7
use std::fmt::Debug;
8

            
9
use serde::{Deserialize, Serialize};
10
use strum::{Display, EnumString, IntoStaticStr};
11

            
12
/// Boolean, but with additional `"auto"` option
13
//
14
// This slightly-odd interleaving of derives and attributes stops rustfmt doing a daft thing
15
#[derive(Clone, Copy, Hash, Debug, Default, Ord, PartialOrd, Eq, PartialEq)]
16
#[allow(clippy::exhaustive_enums)] // we will add variants very rarely if ever
17
46
#[derive(Serialize, Deserialize)]
18
#[serde(try_from = "BoolOrAutoSerde", into = "BoolOrAutoSerde")]
19
pub enum BoolOrAuto {
20
    #[default]
21
    /// Automatic
22
    Auto,
23
    /// Explicitly specified
24
    Explicit(bool),
25
}
26

            
27
impl BoolOrAuto {
28
    /// Returns the explicitly set boolean value, or `None`
29
    ///
30
    /// ```
31
    /// use tor_config::BoolOrAuto;
32
    ///
33
    /// fn calculate_default() -> bool { //...
34
    /// # false }
35
    /// let bool_or_auto: BoolOrAuto = // ...
36
    /// # Default::default();
37
    /// let _: bool = bool_or_auto.as_bool().unwrap_or_else(|| calculate_default());
38
    /// ```
39
5512
    pub fn as_bool(self) -> Option<bool> {
40
5512
        match self {
41
4288
            BoolOrAuto::Auto => None,
42
1224
            BoolOrAuto::Explicit(v) => Some(v),
43
        }
44
5512
    }
45
}
46

            
47
/// How we (de) serialize a [`BoolOrAuto`]
48
#[derive(Serialize, Deserialize)]
49
#[serde(untagged)]
50
enum BoolOrAutoSerde {
51
    /// String (in snake case)
52
    String(Cow<'static, str>),
53
    /// bool
54
    Bool(bool),
55
}
56

            
57
impl From<BoolOrAuto> for BoolOrAutoSerde {
58
4
    fn from(boa: BoolOrAuto) -> BoolOrAutoSerde {
59
        use BoolOrAutoSerde as BoAS;
60
4
        boa.as_bool()
61
4
            .map(BoAS::Bool)
62
6
            .unwrap_or_else(|| BoAS::String("auto".into()))
63
4
    }
64
}
65

            
66
/// Boolean or `"auto"` configuration is invalid
67
#[derive(thiserror::Error, Debug, Clone)]
68
#[non_exhaustive]
69
#[error(r#"Invalid value, expected boolean or "auto""#)]
70
pub struct InvalidBoolOrAuto {}
71

            
72
impl TryFrom<BoolOrAutoSerde> for BoolOrAuto {
73
    type Error = InvalidBoolOrAuto;
74

            
75
1102
    fn try_from(pls: BoolOrAutoSerde) -> Result<BoolOrAuto, Self::Error> {
76
        use BoolOrAuto as BoA;
77
        use BoolOrAutoSerde as BoAS;
78
278
        Ok(match pls {
79
820
            BoAS::Bool(v) => BoA::Explicit(v),
80
282
            BoAS::String(s) if s == "false" => BoA::Explicit(false),
81
280
            BoAS::String(s) if s == "true" => BoA::Explicit(true),
82
278
            BoAS::String(s) if s == "auto" => BoA::Auto,
83
4
            _ => return Err(InvalidBoolOrAuto {}),
84
        })
85
1102
    }
86
}
87

            
88
/// A macro that implements [`NotAutoValue`] for your type.
89
///
90
/// This macro generates:
91
///   * a [`NotAutoValue`] impl for `ty`
92
///   * a test module with a test that ensures "auto" cannot be deserialized as `ty`
93
///
94
/// ## Example
95
///
96
/// ```rust
97
/// # use tor_config::{impl_not_auto_value, ExplicitOrAuto};
98
/// # use serde::{Serialize, Deserialize};
99
//  #
100
/// #[derive(Serialize, Deserialize)]
101
/// struct Foo;
102
///
103
/// impl_not_auto_value!(Foo);
104
///
105
/// #[derive(Serialize, Deserialize)]
106
/// struct Bar;
107
///
108
/// fn main() {
109
///    let _foo: ExplicitOrAuto<Foo> = ExplicitOrAuto::Auto;
110
///
111
///    // Using a type that does not implement NotAutoValue is an error:
112
///    // let _bar: ExplicitOrAuto<Bar> = ExplicitOrAuto::Auto;
113
/// }
114
/// ```
115
#[macro_export]
116
macro_rules! impl_not_auto_value {
117
    ($ty:ty) => {
118
        $crate::deps::paste! {
119
            impl $crate::NotAutoValue for $ty {}
120

            
121
            #[cfg(test)]
122
            #[allow(non_snake_case)]
123
            mod [<test_not_auto_value_ $ty>] {
124
                #[allow(unused_imports)]
125
                use super::*;
126

            
127
                #[test]
128
36
                fn [<auto_is_not_a_valid_value_for_ $ty>]() {
129
36
                    let res = $crate::deps::serde_value::Value::String(
130
36
                        "auto".into()
131
36
                    ).deserialize_into::<$ty>();
132
36

            
133
36
                    assert!(
134
36
                        res.is_err(),
135
                        concat!(
136
                            stringify!($ty), " is not a valid NotAutoValue type: ",
137
                            "NotAutoValue types should not be deserializable from \"auto\""
138
                        ),
139
                    );
140
34
                }
141
2
            }
142
2
        }
143
2
    };
144
2
}
145
2

            
146
2
/// A serializable value, or auto.
147
2
///
148
2
/// Used for implementing configuration options that can be explicitly initialized
149
2
/// with a placeholder for their "default" value using the
150
2
/// [`Auto`](ExplicitOrAuto::Auto) variant.
151
2
///
152
2
/// Unlike `#[serde(default)] field: T` or `#[serde(default)] field: Option<T>`,
153
2
/// fields of this type can be present in the serialized configuration
154
2
/// without being assigned a concrete value.
155
2
///
156
2
/// **Important**: the underlying type must implement [`NotAutoValue`].
157
2
/// This trait should be implemented using the [`impl_not_auto_value`],
158
2
/// and only for types that do not serialize to the same value as the
159
2
/// [`Auto`](ExplicitOrAuto::Auto) variant.
160
2
///
161
2
/// ## Example
162
2
///
163
2
/// In the following serialized TOML config
164
2
///
165
2
/// ```toml
166
2
///  foo = "auto"
167
2
/// ```
168
2
///
169
2
/// `foo` is set to [`Auto`](ExplicitOrAuto::Auto), which indicates the
170
2
/// implementation should use a default (but not necessarily [`Default::default`])
171
2
/// value for the `foo` option.
172
2
///
173
2
/// For example, f field `foo` defaults to `13` if feature `bar` is enabled,
174
2
/// and `9000` otherwise, a configuration with `foo` set to `"auto"` will
175
2
/// behave in the "default" way regardless of which features are enabled.
176
2
///
177
2
/// ```rust,ignore
178
2
/// struct Foo(usize);
179
2
///
180
2
/// impl Default for Foo {
181
2
///     fn default() -> Foo {
182
2
///         if cfg!(feature = "bar") {
183
2
///             Foo(13)
184
2
///         } else {
185
2
///             Foo(9000)
186
2
///         }
187
2
///     }
188
2
/// }
189
2
///
190
2
/// impl Foo {
191
2
///     fn from_explicit_or_auto(foo: ExplicitOrAuto<Foo>) -> Self {
192
2
///         match foo {
193
2
///             // If Auto, choose a sensible default for foo
194
2
///             ExplicitOrAuto::Auto => Default::default(),
195
2
///             ExplicitOrAuto::Foo(foo) => foo,
196
2
///         }
197
2
///     }
198
2
/// }
199
2
///
200
2
/// struct Config {
201
2
///    foo: ExplicitOrAuto<Foo>,
202
2
/// }
203
2
/// ```
204
2
#[derive(Clone, Copy, Hash, Debug, Default, Ord, PartialOrd, Eq, PartialEq)]
205
2
#[allow(clippy::exhaustive_enums)] // we will add variants very rarely if ever
206
22
#[derive(Serialize, Deserialize)]
207
pub enum ExplicitOrAuto<T: NotAutoValue> {
208
    /// Automatic
209
    #[default]
210
    #[serde(rename = "auto")]
211
    Auto,
212
    /// Explicitly specified
213
    #[serde(untagged)]
214
    Explicit(T),
215
}
216

            
217
impl<T: NotAutoValue> ExplicitOrAuto<T> {
218
    /// Returns the explicitly set value, or `None`.
219
    ///
220
    /// ```
221
    /// use tor_config::ExplicitOrAuto;
222
    ///
223
    /// fn calculate_default() -> usize { //...
224
    /// # 2 }
225
    /// let explicit_or_auto: ExplicitOrAuto<usize> = // ...
226
    /// # Default::default();
227
    /// let _: usize = explicit_or_auto.into_value().unwrap_or_else(|| calculate_default());
228
    /// ```
229
    pub fn into_value(self) -> Option<T> {
230
        match self {
231
            ExplicitOrAuto::Auto => None,
232
            ExplicitOrAuto::Explicit(v) => Some(v),
233
        }
234
    }
235

            
236
    /// Returns a reference to the explicitly set value, or `None`.
237
    ///
238
    /// Like [`ExplicitOrAuto::into_value`], except it returns a reference to the inner type.
239
    pub fn as_value(&self) -> Option<&T> {
240
        match self {
241
            ExplicitOrAuto::Auto => None,
242
            ExplicitOrAuto::Explicit(v) => Some(v),
243
        }
244
    }
245
}
246

            
247
/// A marker trait for types that do not serialize to the same value as [`ExplicitOrAuto::Auto`].
248
///
249
/// **Important**: you should not implement this trait manually.
250
/// Use the [`impl_not_auto_value`] macro instead.
251
///
252
/// This trait should be implemented for types that can be stored in [`ExplicitOrAuto`].
253
pub trait NotAutoValue {}
254

            
255
/// A helper for calling [`impl_not_auto_value`] for a number of types.
256
macro_rules! impl_not_auto_value_for_types {
257
    ($($ty:ty)*) => {
258
        $(impl_not_auto_value!($ty);)*
259
    }
260
}
261

            
262
// Implement `NotAutoValue` for various primitive types.
263
impl_not_auto_value_for_types!(
264
    i8 i16 i32 i64 i128 isize
265
    u8 u16 u32 u64 u128 usize
266
    f32 f64
267
    char
268
    bool
269
);
270

            
271
// TODO implement `NotAutoValue` for other types too
272

            
273
/// Padding enablement - rough amount of padding requested
274
///
275
/// Padding is cover traffic, used to help mitigate traffic analysis,
276
/// obscure traffic patterns, and impede router-level data collection.
277
///
278
/// This same enum is used to control padding at various levels of the Tor system.
279
/// (TODO: actually we don't do circuit padding yet.)
280
//
281
// This slightly-odd interleaving of derives and attributes stops rustfmt doing a daft thing
282
#[derive(Clone, Copy, Hash, Debug, Ord, PartialOrd, Eq, PartialEq)]
283
#[allow(clippy::exhaustive_enums)] // we will add variants very rarely if ever
284
18
#[derive(Serialize, Deserialize)]
285
#[serde(try_from = "PaddingLevelSerde", into = "PaddingLevelSerde")]
286
146
#[derive(Display, EnumString, IntoStaticStr)]
287
#[strum(serialize_all = "snake_case")]
288
#[derive(Default)]
289
pub enum PaddingLevel {
290
    /// Disable padding completely
291
    None,
292
    /// Reduced padding (eg for mobile)
293
    Reduced,
294
    /// Normal padding (the default)
295
    #[default]
296
    Normal,
297
}
298

            
299
/// How we (de) serialize a [`PaddingLevel`]
300
#[derive(Serialize, Deserialize)]
301
#[serde(untagged)]
302
enum PaddingLevelSerde {
303
    /// String (in snake case)
304
    ///
305
    /// We always serialize this way
306
    String(Cow<'static, str>),
307
    /// bool
308
    Bool(bool),
309
}
310

            
311
impl From<PaddingLevel> for PaddingLevelSerde {
312
4
    fn from(pl: PaddingLevel) -> PaddingLevelSerde {
313
4
        PaddingLevelSerde::String(<&str>::from(&pl).into())
314
4
    }
315
}
316

            
317
/// Padding level configuration is invalid
318
#[derive(thiserror::Error, Debug, Clone)]
319
#[non_exhaustive]
320
#[error("Invalid padding level")]
321
struct InvalidPaddingLevel {}
322

            
323
impl TryFrom<PaddingLevelSerde> for PaddingLevel {
324
    type Error = InvalidPaddingLevel;
325

            
326
150
    fn try_from(pls: PaddingLevelSerde) -> Result<PaddingLevel, Self::Error> {
327
150
        Ok(match pls {
328
146
            PaddingLevelSerde::String(s) => {
329
148
                s.as_ref().try_into().map_err(|_| InvalidPaddingLevel {})?
330
            }
331
2
            PaddingLevelSerde::Bool(false) => PaddingLevel::None,
332
2
            PaddingLevelSerde::Bool(true) => PaddingLevel::Normal,
333
        })
334
150
    }
335
}
336

            
337
#[cfg(test)]
338
mod test {
339
    // @@ begin test lint list maintained by maint/add_warning @@
340
    #![allow(clippy::bool_assert_comparison)]
341
    #![allow(clippy::clone_on_copy)]
342
    #![allow(clippy::dbg_macro)]
343
    #![allow(clippy::mixed_attributes_style)]
344
    #![allow(clippy::print_stderr)]
345
    #![allow(clippy::print_stdout)]
346
    #![allow(clippy::single_char_pattern)]
347
    #![allow(clippy::unwrap_used)]
348
    #![allow(clippy::unchecked_duration_subtraction)]
349
    #![allow(clippy::useless_vec)]
350
    #![allow(clippy::needless_pass_by_value)]
351
    //! <!-- @@ end test lint list maintained by maint/add_warning @@ -->
352
    use super::*;
353

            
354
    #[derive(Debug, Default, Deserialize, Serialize)]
355
    struct TestConfigFile {
356
        #[serde(default)]
357
        something_enabled: BoolOrAuto,
358

            
359
        #[serde(default)]
360
        padding: PaddingLevel,
361

            
362
        #[serde(default)]
363
        auto_or_usize: ExplicitOrAuto<usize>,
364

            
365
        #[serde(default)]
366
        auto_or_bool: ExplicitOrAuto<bool>,
367
    }
368

            
369
    #[test]
370
    fn bool_or_auto() {
371
        use BoolOrAuto as BoA;
372

            
373
        let chk = |pl, s| {
374
            let tc: TestConfigFile = toml::from_str(s).expect(s);
375
            assert_eq!(pl, tc.something_enabled, "{:?}", s);
376
        };
377

            
378
        chk(BoA::Auto, "");
379
        chk(BoA::Auto, r#"something_enabled = "auto""#);
380
        chk(BoA::Explicit(true), r#"something_enabled = true"#);
381
        chk(BoA::Explicit(true), r#"something_enabled = "true""#);
382
        chk(BoA::Explicit(false), r#"something_enabled = false"#);
383
        chk(BoA::Explicit(false), r#"something_enabled = "false""#);
384

            
385
        let chk_e = |s| {
386
            let tc: Result<TestConfigFile, _> = toml::from_str(s);
387
            let _ = tc.expect_err(s);
388
        };
389

            
390
        chk_e(r#"something_enabled = 1"#);
391
        chk_e(r#"something_enabled = "unknown""#);
392
        chk_e(r#"something_enabled = "True""#);
393
    }
394

            
395
    #[test]
396
    fn padding_level() {
397
        use PaddingLevel as PL;
398

            
399
        let chk = |pl, s| {
400
            let tc: TestConfigFile = toml::from_str(s).expect(s);
401
            assert_eq!(pl, tc.padding, "{:?}", s);
402
        };
403

            
404
        chk(PL::None, r#"padding = "none""#);
405
        chk(PL::None, r#"padding = false"#);
406
        chk(PL::Reduced, r#"padding = "reduced""#);
407
        chk(PL::Normal, r#"padding = "normal""#);
408
        chk(PL::Normal, r#"padding = true"#);
409
        chk(PL::Normal, "");
410

            
411
        let chk_e = |s| {
412
            let tc: Result<TestConfigFile, _> = toml::from_str(s);
413
            let _ = tc.expect_err(s);
414
        };
415

            
416
        chk_e(r#"padding = 1"#);
417
        chk_e(r#"padding = "unknown""#);
418
        chk_e(r#"padding = "Normal""#);
419
    }
420

            
421
    #[test]
422
    fn explicit_or_auto() {
423
        use ExplicitOrAuto as EOA;
424

            
425
        let chk = |eoa: EOA<usize>, s| {
426
            let tc: TestConfigFile = toml::from_str(s).expect(s);
427
            assert_eq!(
428
                format!("{:?}", eoa),
429
                format!("{:?}", tc.auto_or_usize),
430
                "{:?}",
431
                s
432
            );
433
        };
434

            
435
        chk(EOA::Auto, r#"auto_or_usize = "auto""#);
436
        chk(EOA::Explicit(20), r#"auto_or_usize = 20"#);
437

            
438
        let chk_e = |s| {
439
            let tc: Result<TestConfigFile, _> = toml::from_str(s);
440
            let _ = tc.expect_err(s);
441
        };
442

            
443
        chk_e(r#"auto_or_usize = """#);
444
        chk_e(r#"auto_or_usize = []"#);
445
        chk_e(r#"auto_or_usize = {}"#);
446

            
447
        let chk = |eoa: EOA<bool>, s| {
448
            let tc: TestConfigFile = toml::from_str(s).expect(s);
449
            assert_eq!(
450
                format!("{:?}", eoa),
451
                format!("{:?}", tc.auto_or_bool),
452
                "{:?}",
453
                s
454
            );
455
        };
456

            
457
        // ExplicitOrAuto<bool> works just like BoolOrAuto
458
        chk(EOA::Auto, r#"auto_or_bool = "auto""#);
459
        chk(EOA::Explicit(false), r#"auto_or_bool = false"#);
460

            
461
        chk_e(r#"auto_or_bool= "not bool or auto""#);
462

            
463
        let mut config = TestConfigFile::default();
464
        let toml = toml::to_string(&config).unwrap();
465
        assert_eq!(
466
            toml,
467
            r#"something_enabled = "auto"
468
padding = "normal"
469
auto_or_usize = "auto"
470
auto_or_bool = "auto"
471
"#
472
        );
473

            
474
        config.auto_or_bool = ExplicitOrAuto::Explicit(true);
475
        let toml = toml::to_string(&config).unwrap();
476
        assert_eq!(
477
            toml,
478
            r#"something_enabled = "auto"
479
padding = "normal"
480
auto_or_usize = "auto"
481
auto_or_bool = true
482
"#
483
        );
484
    }
485
}