tor_config/
misc.rs

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
6use std::borrow::Cow;
7use std::fmt::Debug;
8
9use serde::{Deserialize, Serialize};
10use 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#[derive(Serialize, Deserialize)]
18#[serde(try_from = "BoolOrAutoSerde", into = "BoolOrAutoSerde")]
19pub enum BoolOrAuto {
20    #[default]
21    /// Automatic
22    Auto,
23    /// Explicitly specified
24    Explicit(bool),
25}
26
27impl 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    pub fn as_bool(self) -> Option<bool> {
40        match self {
41            BoolOrAuto::Auto => None,
42            BoolOrAuto::Explicit(v) => Some(v),
43        }
44    }
45}
46
47/// How we (de) serialize a [`BoolOrAuto`]
48#[derive(Serialize, Deserialize)]
49#[serde(untagged)]
50enum BoolOrAutoSerde {
51    /// String (in snake case)
52    String(Cow<'static, str>),
53    /// bool
54    Bool(bool),
55}
56
57impl From<BoolOrAuto> for BoolOrAutoSerde {
58    fn from(boa: BoolOrAuto) -> BoolOrAutoSerde {
59        use BoolOrAutoSerde as BoAS;
60        boa.as_bool()
61            .map(BoAS::Bool)
62            .unwrap_or_else(|| BoAS::String("auto".into()))
63    }
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""#)]
70pub struct InvalidBoolOrAuto {}
71
72impl TryFrom<BoolOrAutoSerde> for BoolOrAuto {
73    type Error = InvalidBoolOrAuto;
74
75    fn try_from(pls: BoolOrAutoSerde) -> Result<BoolOrAuto, Self::Error> {
76        use BoolOrAuto as BoA;
77        use BoolOrAutoSerde as BoAS;
78        Ok(match pls {
79            BoAS::Bool(v) => BoA::Explicit(v),
80            BoAS::String(s) if s == "false" => BoA::Explicit(false),
81            BoAS::String(s) if s == "true" => BoA::Explicit(true),
82            BoAS::String(s) if s == "auto" => BoA::Auto,
83            _ => return Err(InvalidBoolOrAuto {}),
84        })
85    }
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]
116macro_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                fn [<auto_is_not_a_valid_value_for_ $ty>]() {
129                    let res = $crate::deps::serde_value::Value::String(
130                        "auto".into()
131                    ).deserialize_into::<$ty>();
132
133                    assert!(
134                        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                }
141            }
142        }
143    };
144}
145
146/// A serializable value, or auto.
147///
148/// Used for implementing configuration options that can be explicitly initialized
149/// with a placeholder for their "default" value using the
150/// [`Auto`](ExplicitOrAuto::Auto) variant.
151///
152/// Unlike `#[serde(default)] field: T` or `#[serde(default)] field: Option<T>`,
153/// fields of this type can be present in the serialized configuration
154/// without being assigned a concrete value.
155///
156/// **Important**: the underlying type must implement [`NotAutoValue`].
157/// This trait should be implemented using the [`impl_not_auto_value`],
158/// and only for types that do not serialize to the same value as the
159/// [`Auto`](ExplicitOrAuto::Auto) variant.
160///
161/// ## Example
162///
163/// In the following serialized TOML config
164///
165/// ```toml
166///  foo = "auto"
167/// ```
168///
169/// `foo` is set to [`Auto`](ExplicitOrAuto::Auto), which indicates the
170/// implementation should use a default (but not necessarily [`Default::default`])
171/// value for the `foo` option.
172///
173/// For example, f field `foo` defaults to `13` if feature `bar` is enabled,
174/// and `9000` otherwise, a configuration with `foo` set to `"auto"` will
175/// behave in the "default" way regardless of which features are enabled.
176///
177/// ```rust,ignore
178/// struct Foo(usize);
179///
180/// impl Default for Foo {
181///     fn default() -> Foo {
182///         if cfg!(feature = "bar") {
183///             Foo(13)
184///         } else {
185///             Foo(9000)
186///         }
187///     }
188/// }
189///
190/// impl Foo {
191///     fn from_explicit_or_auto(foo: ExplicitOrAuto<Foo>) -> Self {
192///         match foo {
193///             // If Auto, choose a sensible default for foo
194///             ExplicitOrAuto::Auto => Default::default(),
195///             ExplicitOrAuto::Foo(foo) => foo,
196///         }
197///     }
198/// }
199///
200/// struct Config {
201///    foo: ExplicitOrAuto<Foo>,
202/// }
203/// ```
204#[derive(Clone, Copy, Hash, Debug, Default, Ord, PartialOrd, Eq, PartialEq)]
205#[allow(clippy::exhaustive_enums)] // we will add variants very rarely if ever
206#[derive(Serialize, Deserialize)]
207pub 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
217impl<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    /// Maps an `ExplicitOrAuto<T>` to an `ExplicitOrAuto<U>` by applying a function to a contained
247    /// value.
248    pub fn map<U: NotAutoValue>(self, f: impl FnOnce(T) -> U) -> ExplicitOrAuto<U> {
249        match self {
250            Self::Auto => ExplicitOrAuto::Auto,
251            Self::Explicit(x) => ExplicitOrAuto::Explicit(f(x)),
252        }
253    }
254}
255
256impl<T> From<T> for ExplicitOrAuto<T>
257where
258    T: NotAutoValue,
259{
260    fn from(x: T) -> Self {
261        Self::Explicit(x)
262    }
263}
264
265/// A marker trait for types that do not serialize to the same value as [`ExplicitOrAuto::Auto`].
266///
267/// **Important**: you should not implement this trait manually.
268/// Use the [`impl_not_auto_value`] macro instead.
269///
270/// This trait should be implemented for types that can be stored in [`ExplicitOrAuto`].
271pub trait NotAutoValue {}
272
273/// A helper for calling [`impl_not_auto_value`] for a number of types.
274macro_rules! impl_not_auto_value_for_types {
275    ($($ty:ty)*) => {
276        $(impl_not_auto_value!($ty);)*
277    }
278}
279
280// Implement `NotAutoValue` for various primitive types.
281impl_not_auto_value_for_types!(
282    i8 i16 i32 i64 i128 isize
283    u8 u16 u32 u64 u128 usize
284    f32 f64
285    char
286    bool
287);
288
289use tor_basic_utils::ByteQty;
290impl_not_auto_value!(ByteQty);
291
292// TODO implement `NotAutoValue` for other types too
293
294/// Padding enablement - rough amount of padding requested
295///
296/// Padding is cover traffic, used to help mitigate traffic analysis,
297/// obscure traffic patterns, and impede router-level data collection.
298///
299/// This same enum is used to control padding at various levels of the Tor system.
300//
301// (TODO circpad: actually we don't negotiate circuit padding yet. But when we do, we should look at
302// this.)
303//
304// This slightly-odd interleaving of derives and attributes stops rustfmt doing a daft thing
305#[derive(Clone, Copy, Hash, Debug, Ord, PartialOrd, Eq, PartialEq)]
306#[allow(clippy::exhaustive_enums)] // we will add variants very rarely if ever
307#[derive(Serialize, Deserialize)]
308#[serde(try_from = "PaddingLevelSerde", into = "PaddingLevelSerde")]
309#[derive(Display, EnumString, IntoStaticStr)]
310#[strum(serialize_all = "snake_case")]
311#[derive(Default)]
312pub enum PaddingLevel {
313    /// Disable padding completely
314    None,
315    /// Reduced padding (eg for mobile)
316    Reduced,
317    /// Normal padding (the default)
318    #[default]
319    Normal,
320}
321
322/// How we (de) serialize a [`PaddingLevel`]
323#[derive(Serialize, Deserialize)]
324#[serde(untagged)]
325enum PaddingLevelSerde {
326    /// String (in snake case)
327    ///
328    /// We always serialize this way
329    String(Cow<'static, str>),
330    /// bool
331    Bool(bool),
332}
333
334impl From<PaddingLevel> for PaddingLevelSerde {
335    fn from(pl: PaddingLevel) -> PaddingLevelSerde {
336        PaddingLevelSerde::String(<&str>::from(&pl).into())
337    }
338}
339
340/// Padding level configuration is invalid
341#[derive(thiserror::Error, Debug, Clone)]
342#[non_exhaustive]
343#[error("Invalid padding level")]
344struct InvalidPaddingLevel {}
345
346impl TryFrom<PaddingLevelSerde> for PaddingLevel {
347    type Error = InvalidPaddingLevel;
348
349    fn try_from(pls: PaddingLevelSerde) -> Result<PaddingLevel, Self::Error> {
350        Ok(match pls {
351            PaddingLevelSerde::String(s) => {
352                s.as_ref().try_into().map_err(|_| InvalidPaddingLevel {})?
353            }
354            PaddingLevelSerde::Bool(false) => PaddingLevel::None,
355            PaddingLevelSerde::Bool(true) => PaddingLevel::Normal,
356        })
357    }
358}
359
360#[cfg(test)]
361mod test {
362    // @@ begin test lint list maintained by maint/add_warning @@
363    #![allow(clippy::bool_assert_comparison)]
364    #![allow(clippy::clone_on_copy)]
365    #![allow(clippy::dbg_macro)]
366    #![allow(clippy::mixed_attributes_style)]
367    #![allow(clippy::print_stderr)]
368    #![allow(clippy::print_stdout)]
369    #![allow(clippy::single_char_pattern)]
370    #![allow(clippy::unwrap_used)]
371    #![allow(clippy::unchecked_time_subtraction)]
372    #![allow(clippy::useless_vec)]
373    #![allow(clippy::needless_pass_by_value)]
374    //! <!-- @@ end test lint list maintained by maint/add_warning @@ -->
375    use super::*;
376
377    #[derive(Debug, Default, Deserialize, Serialize)]
378    struct TestConfigFile {
379        #[serde(default)]
380        something_enabled: BoolOrAuto,
381
382        #[serde(default)]
383        padding: PaddingLevel,
384
385        #[serde(default)]
386        auto_or_usize: ExplicitOrAuto<usize>,
387
388        #[serde(default)]
389        auto_or_bool: ExplicitOrAuto<bool>,
390    }
391
392    #[test]
393    fn bool_or_auto() {
394        use BoolOrAuto as BoA;
395
396        let chk = |pl, s| {
397            let tc: TestConfigFile = toml::from_str(s).expect(s);
398            assert_eq!(pl, tc.something_enabled, "{:?}", s);
399        };
400
401        chk(BoA::Auto, "");
402        chk(BoA::Auto, r#"something_enabled = "auto""#);
403        chk(BoA::Explicit(true), r#"something_enabled = true"#);
404        chk(BoA::Explicit(true), r#"something_enabled = "true""#);
405        chk(BoA::Explicit(false), r#"something_enabled = false"#);
406        chk(BoA::Explicit(false), r#"something_enabled = "false""#);
407
408        let chk_e = |s| {
409            let tc: Result<TestConfigFile, _> = toml::from_str(s);
410            let _ = tc.expect_err(s);
411        };
412
413        chk_e(r#"something_enabled = 1"#);
414        chk_e(r#"something_enabled = "unknown""#);
415        chk_e(r#"something_enabled = "True""#);
416    }
417
418    #[test]
419    fn padding_level() {
420        use PaddingLevel as PL;
421
422        let chk = |pl, s| {
423            let tc: TestConfigFile = toml::from_str(s).expect(s);
424            assert_eq!(pl, tc.padding, "{:?}", s);
425        };
426
427        chk(PL::None, r#"padding = "none""#);
428        chk(PL::None, r#"padding = false"#);
429        chk(PL::Reduced, r#"padding = "reduced""#);
430        chk(PL::Normal, r#"padding = "normal""#);
431        chk(PL::Normal, r#"padding = true"#);
432        chk(PL::Normal, "");
433
434        let chk_e = |s| {
435            let tc: Result<TestConfigFile, _> = toml::from_str(s);
436            let _ = tc.expect_err(s);
437        };
438
439        chk_e(r#"padding = 1"#);
440        chk_e(r#"padding = "unknown""#);
441        chk_e(r#"padding = "Normal""#);
442    }
443
444    #[test]
445    fn explicit_or_auto() {
446        use ExplicitOrAuto as EOA;
447
448        let chk = |eoa: EOA<usize>, s| {
449            let tc: TestConfigFile = toml::from_str(s).expect(s);
450            assert_eq!(
451                format!("{:?}", eoa),
452                format!("{:?}", tc.auto_or_usize),
453                "{:?}",
454                s
455            );
456        };
457
458        chk(EOA::Auto, r#"auto_or_usize = "auto""#);
459        chk(EOA::Explicit(20), r#"auto_or_usize = 20"#);
460
461        let chk_e = |s| {
462            let tc: Result<TestConfigFile, _> = toml::from_str(s);
463            let _ = tc.expect_err(s);
464        };
465
466        chk_e(r#"auto_or_usize = """#);
467        chk_e(r#"auto_or_usize = []"#);
468        chk_e(r#"auto_or_usize = {}"#);
469
470        let chk = |eoa: EOA<bool>, s| {
471            let tc: TestConfigFile = toml::from_str(s).expect(s);
472            assert_eq!(
473                format!("{:?}", eoa),
474                format!("{:?}", tc.auto_or_bool),
475                "{:?}",
476                s
477            );
478        };
479
480        // ExplicitOrAuto<bool> works just like BoolOrAuto
481        chk(EOA::Auto, r#"auto_or_bool = "auto""#);
482        chk(EOA::Explicit(false), r#"auto_or_bool = false"#);
483
484        chk_e(r#"auto_or_bool= "not bool or auto""#);
485
486        let mut config = TestConfigFile::default();
487        let toml = toml::to_string(&config).unwrap();
488        assert_eq!(
489            toml,
490            r#"something_enabled = "auto"
491padding = "normal"
492auto_or_usize = "auto"
493auto_or_bool = "auto"
494"#
495        );
496
497        config.auto_or_bool = ExplicitOrAuto::Explicit(true);
498        let toml = toml::to_string(&config).unwrap();
499        assert_eq!(
500            toml,
501            r#"something_enabled = "auto"
502padding = "normal"
503auto_or_usize = "auto"
504auto_or_bool = true
505"#
506        );
507    }
508}