diff --git a/crates/tor-config/src/misc.rs b/crates/tor-config/src/misc.rs index 795a01ec0..b58abd9d2 100644 --- a/crates/tor-config/src/misc.rs +++ b/crates/tor-config/src/misc.rs @@ -14,6 +14,84 @@ use itertools::Itertools; use serde::{Deserialize, Serialize}; use strum::{Display, EnumString, IntoStaticStr}; +/// Boolean, but with additional `"auto"` option +// +// This slightly-odd interleaving of derives and attributes stops rustfmt doing a daft thing +#[derive(Clone, Copy, Hash, Debug, Ord, PartialOrd, Eq, PartialEq)] +#[allow(clippy::exhaustive_enums)] // we will add variants very rarely if ever +#[derive(Serialize, Deserialize)] +#[serde(try_from = "BoolOrAutoSerde", into = "BoolOrAutoSerde")] +#[derive(Educe)] +#[educe(Default)] +pub enum BoolOrAuto { + #[educe(Default)] + /// Automatic + Auto, + /// Explicitly specified + Explicit(bool), +} + +impl BoolOrAuto { + /// Returns the explicitly set boolean value, or `None` + /// + /// ``` + /// use tor_config::BoolOrAuto; + /// + /// fn calculate_default() -> bool { //... + /// # false } + /// let bool_or_auto: BoolOrAuto = // ... + /// # Default::default(); + /// let _: bool = bool_or_auto.as_bool().unwrap_or_else(|| calculate_default()); + /// ``` + pub fn as_bool(self) -> Option { + match self { + BoolOrAuto::Auto => None, + BoolOrAuto::Explicit(v) => Some(v), + } + } +} + +/// How we (de) serialize a [`BoolOrAuto`] +#[derive(Serialize, Deserialize)] +#[serde(untagged)] +enum BoolOrAutoSerde { + /// String (in snake case) + String(Cow<'static, str>), + /// bool + Bool(bool), +} + +impl From for BoolOrAutoSerde { + fn from(boa: BoolOrAuto) -> BoolOrAutoSerde { + use BoolOrAutoSerde as BoAS; + boa.as_bool() + .map(BoAS::Bool) + .unwrap_or_else(|| BoAS::String("auto".into())) + } +} + +/// Boolean or `"auto"` configuration is invalid +#[derive(thiserror::Error, Debug, Clone)] +#[non_exhaustive] +#[error(r#"Invalid value, respected boolean or "auto""#)] +pub struct InvalidBoolOrAuto {} + +impl TryFrom for BoolOrAuto { + type Error = InvalidBoolOrAuto; + + fn try_from(pls: BoolOrAutoSerde) -> Result { + use BoolOrAuto as BoA; + use BoolOrAutoSerde as BoAS; + Ok(match pls { + BoAS::Bool(v) => BoA::Explicit(v), + BoAS::String(s) if s == "false" => BoA::Explicit(false), + BoAS::String(s) if s == "true" => BoA::Explicit(true), + BoAS::String(s) if s == "auto" => BoA::Auto, + _ => return Err(InvalidBoolOrAuto {}), + }) + } +} + /// Padding enablement - rough amount of padding requested /// /// Padding is cover traffic, used to help mitigate traffic analysis, @@ -320,6 +398,9 @@ mod test { #[derive(Debug, Deserialize, Serialize)] struct TestConfigFile { + #[serde(default)] + something_enabled: BoolOrAuto, + #[serde(default)] padding: PaddingLevel, @@ -327,6 +408,32 @@ mod test { listen: Option, } + #[test] + fn bool_or_auto() { + use BoolOrAuto as BoA; + + let chk = |pl, s| { + let tc: TestConfigFile = toml::from_str(s).expect(s); + assert_eq!(pl, tc.something_enabled, "{:?}", s); + }; + + chk(BoA::Auto, ""); + chk(BoA::Auto, r#"something_enabled = "auto""#); + chk(BoA::Explicit(true), r#"something_enabled = true"#); + chk(BoA::Explicit(true), r#"something_enabled = "true""#); + chk(BoA::Explicit(false), r#"something_enabled = false"#); + chk(BoA::Explicit(false), r#"something_enabled = "false""#); + + let chk_e = |s| { + let tc: Result = toml::from_str(s); + let _ = tc.expect_err(s); + }; + + chk_e(r#"something_enabled = 1"#); + chk_e(r#"something_enabled = "unknown""#); + chk_e(r#"something_enabled = "True""#); + } + #[test] fn padding_level() { use PaddingLevel as PL;