From c6495f6fed4ac6b182c32f7630e889be02b01f89 Mon Sep 17 00:00:00 2001 From: mdecimus <11444311+mdecimus@users.noreply.github.com> Date: Tue, 3 Feb 2026 12:29:49 +0100 Subject: [PATCH] RIP TOML --- crates/common/src/enterprise/config.rs | 2 +- crates/nlp/src/language/mod.rs | 7 - crates/registry/src/schema/mod.rs | 6 +- crates/store/src/lib.rs | 15 - crates/store/src/search/mod.rs | 54 -- crates/types/src/special_use.rs | 7 - crates/utils/src/config/ipmask.rs | 186 ------ crates/utils/src/config/mod.rs | 290 --------- crates/utils/src/config/parser.rs | 588 ----------------- crates/utils/src/config/utils.rs | 844 ------------------------- crates/utils/src/{config => }/cron.rs | 11 +- crates/utils/src/{config => }/http.rs | 0 crates/utils/src/lib.rs | 3 +- 13 files changed, 10 insertions(+), 2003 deletions(-) delete mode 100644 crates/utils/src/config/ipmask.rs delete mode 100644 crates/utils/src/config/mod.rs delete mode 100644 crates/utils/src/config/parser.rs delete mode 100644 crates/utils/src/config/utils.rs rename crates/utils/src/{config => }/cron.rs (97%) rename crates/utils/src/{config => }/http.rs (100%) diff --git a/crates/common/src/enterprise/config.rs b/crates/common/src/enterprise/config.rs index 295e44f6..6547bca2 100644 --- a/crates/common/src/enterprise/config.rs +++ b/crates/common/src/enterprise/config.rs @@ -26,7 +26,7 @@ use registry::{ types::id::Id, }; use std::sync::Arc; -use store::Store; +use store::{Store, registry::bootstrap::Bootstrap}; use trc::MetricType; use utils::template::Template; diff --git a/crates/nlp/src/language/mod.rs b/crates/nlp/src/language/mod.rs index 47aad013..50348ac7 100644 --- a/crates/nlp/src/language/mod.rs +++ b/crates/nlp/src/language/mod.rs @@ -15,7 +15,6 @@ use crate::tokenizers::{ word::WordTokenizer, }; use std::borrow::Cow; -use utils::config::utils::ParseValue; pub type LanguageTokenizer<'x> = Box>> + 'x + Sync + Send>; @@ -207,9 +206,3 @@ impl Language { } } } - -impl ParseValue for Language { - fn parse_value(value: &str) -> utils::config::Result { - Language::from_iso_639(value).ok_or_else(|| format!("Invalid language code: {}", value)) - } -} diff --git a/crates/registry/src/schema/mod.rs b/crates/registry/src/schema/mod.rs index 5c163bac..39cdd08f 100644 --- a/crates/registry/src/schema/mod.rs +++ b/crates/registry/src/schema/mod.rs @@ -14,10 +14,8 @@ use crate::{ use std::{collections::HashMap, fmt::Display}; use utils::{ Client, HeaderMap, - config::{ - cron::SimpleCron, - http::{build_http_client, build_http_headers}, - }, + cron::SimpleCron, + http::{build_http_client, build_http_headers}, }; #[allow(clippy::derivable_impls)] diff --git a/crates/store/src/lib.rs b/crates/store/src/lib.rs index 0d44eb9f..8dabed42 100644 --- a/crates/store/src/lib.rs +++ b/crates/store/src/lib.rs @@ -24,7 +24,6 @@ pub use xxhash_rust; use ahash::AHashMap; use backend::{fs::FsStore, http::HttpStore, memory::StaticMemoryStore}; use std::{borrow::Cow, path::PathBuf, sync::Arc}; -use utils::config::cron::SimpleCron; use write::ValueClass; use crate::backend::{elastic::ElasticSearchStore, meili::MeiliSearchStore}; @@ -274,20 +273,6 @@ impl Default for SearchStore { } } -#[derive(Clone)] -pub enum PurgeStore { - Data(Store), - Blobs { store: Store, blob_store: BlobStore }, - Lookup(InMemoryStore), -} - -#[derive(Clone)] -pub struct PurgeSchedule { - pub cron: SimpleCron, - pub store_id: String, - pub store: PurgeStore, -} - #[derive(Clone, Debug, PartialEq)] pub enum Value<'x> { Integer(i64), diff --git a/crates/store/src/search/mod.rs b/crates/store/src/search/mod.rs index ef6cc461..f23fe6cc 100644 --- a/crates/store/src/search/mod.rs +++ b/crates/store/src/search/mod.rs @@ -22,7 +22,6 @@ use std::cmp::Ordering; use std::collections::hash_map::Entry; use std::fmt::Display; use std::ops::{BitAndAssign, BitOrAssign, BitXorAssign}; -use utils::config::utils::ParseValue; use utils::map::vec_map::VecMap; #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -270,59 +269,6 @@ pub trait SearchableField: Sized { fn is_text(&self) -> bool; } -impl ParseValue for SearchField { - fn parse_value(value: &str) -> utils::config::Result { - Ok(match value { - // Email - "email-from" => Self::Email(EmailSearchField::From), - "email-to" => Self::Email(EmailSearchField::To), - "email-cc" => Self::Email(EmailSearchField::Cc), - "email-bcc" => Self::Email(EmailSearchField::Bcc), - "email-subject" => Self::Email(EmailSearchField::Subject), - "email-body" => Self::Email(EmailSearchField::Body), - "email-attachment" => Self::Email(EmailSearchField::Attachment), - "email-received-at" => Self::Email(EmailSearchField::ReceivedAt), - "email-sent-at" => Self::Email(EmailSearchField::SentAt), - "email-size" => Self::Email(EmailSearchField::Size), - "email-has-attachment" => Self::Email(EmailSearchField::HasAttachment), - "email-headers" => Self::Email(EmailSearchField::Headers), - - // Calendar - "cal-title" => Self::Calendar(CalendarSearchField::Title), - "cal-desc" => Self::Calendar(CalendarSearchField::Description), - "cal-location" => Self::Calendar(CalendarSearchField::Location), - "cal-owner" => Self::Calendar(CalendarSearchField::Owner), - "cal-attendee" => Self::Calendar(CalendarSearchField::Attendee), - "cal-start" => Self::Calendar(CalendarSearchField::Start), - "cal-uid" => Self::Calendar(CalendarSearchField::Uid), - - // Contact - "contact-member" => Self::Contact(ContactSearchField::Member), - "contact-kind" => Self::Contact(ContactSearchField::Kind), - "contact-name" => Self::Contact(ContactSearchField::Name), - "contact-nickname" => Self::Contact(ContactSearchField::Nickname), - "contact-org" => Self::Contact(ContactSearchField::Organization), - "contact-email" => Self::Contact(ContactSearchField::Email), - "contact-phone" => Self::Contact(ContactSearchField::Phone), - "contact-online-service" => Self::Contact(ContactSearchField::OnlineService), - "contact-address" => Self::Contact(ContactSearchField::Address), - "contact-note" => Self::Contact(ContactSearchField::Note), - "contact-uid" => Self::Contact(ContactSearchField::Uid), - - // File - "file-name" => Self::File(FileSearchField::Name), - "file-content" => Self::File(FileSearchField::Content), - - // Tracing - "trace-event-type" => Self::Tracing(TracingSearchField::EventType), - "trace-queue-id" => Self::Tracing(TracingSearchField::QueueId), - "trace-keywords" => Self::Tracing(TracingSearchField::Keywords), - - _ => return Err(format!("Unknown search field: {value}")), - }) - } -} - impl Eq for SearchFilter {} impl SearchIndex { diff --git a/crates/types/src/special_use.rs b/crates/types/src/special_use.rs index 8c8da275..915f8d94 100644 --- a/crates/types/src/special_use.rs +++ b/crates/types/src/special_use.rs @@ -5,7 +5,6 @@ */ use jmap_tools::{Element, Property, Value}; -use utils::config::utils::ParseValue; #[derive( rkyv::Archive, @@ -109,12 +108,6 @@ impl From<&ArchivedSpecialUse> for SpecialUse { } } -impl ParseValue for SpecialUse { - fn parse_value(value: &str) -> Result { - SpecialUse::parse(value).ok_or_else(|| format!("Unknown folder role {:?}", value)) - } -} - impl<'x, P: Property, E: Element + From> From for Value<'x, P, E> { fn from(id: SpecialUse) -> Self { Value::Element(E::from(id)) diff --git a/crates/utils/src/config/ipmask.rs b/crates/utils/src/config/ipmask.rs deleted file mode 100644 index 84add087..00000000 --- a/crates/utils/src/config/ipmask.rs +++ /dev/null @@ -1,186 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC - * - * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL - */ - -use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr}; - -use rustls::{SupportedCipherSuite, crypto::ring::cipher_suite::*}; - -use super::utils::ParseValue; - -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum IpAddrMask { - V4 { addr: Ipv4Addr, mask: u32 }, - V6 { addr: Ipv6Addr, mask: u128 }, -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum IpAddrOrMask { - Ip(IpAddr), - Mask(IpAddrMask), -} - -impl IpAddrMask { - pub fn matches(&self, remote: &IpAddr) -> bool { - match self { - IpAddrMask::V4 { addr, mask } => match *mask { - u32::MAX => match remote { - IpAddr::V4(remote) => addr == remote, - IpAddr::V6(remote) => { - if let Some(remote) = remote.to_ipv4_mapped() { - addr == &remote - } else { - false - } - } - }, - 0 => { - matches!(remote, IpAddr::V4(_)) - } - _ => { - u32::from_be_bytes(match remote { - IpAddr::V4(ip) => ip.octets(), - IpAddr::V6(ip) => { - if let Some(ip) = ip.to_ipv4() { - ip.octets() - } else { - return false; - } - } - }) & mask - == u32::from_be_bytes(addr.octets()) & mask - } - }, - IpAddrMask::V6 { addr, mask } => match *mask { - u128::MAX => match remote { - IpAddr::V6(remote) => remote == addr, - IpAddr::V4(remote) => &remote.to_ipv6_mapped() == addr, - }, - 0 => { - matches!(remote, IpAddr::V6(_)) - } - _ => { - u128::from_be_bytes(match remote { - IpAddr::V6(ip) => ip.octets(), - IpAddr::V4(ip) => ip.to_ipv6_mapped().octets(), - }) & mask - == u128::from_be_bytes(addr.octets()) & mask - } - }, - } - } -} - -impl ParseValue for IpAddrMask { - fn parse_value(value: &str) -> super::Result { - if let Some((addr, mask)) = value.rsplit_once('/') { - if let (Ok(addr), Ok(mask)) = - (addr.trim().parse::(), mask.trim().parse::()) - { - match addr { - IpAddr::V4(addr) if (8..=32).contains(&mask) => { - return Ok(IpAddrMask::V4 { - addr, - mask: u32::MAX << (32 - mask), - }); - } - IpAddr::V6(addr) if (8..=128).contains(&mask) => { - return Ok(IpAddrMask::V6 { - addr, - mask: u128::MAX << (128 - mask), - }); - } - _ => (), - } - } - } else { - match value.trim().parse::() { - Ok(IpAddr::V4(addr)) => { - return Ok(IpAddrMask::V4 { - addr, - mask: u32::MAX, - }); - } - Ok(IpAddr::V6(addr)) => { - return Ok(IpAddrMask::V6 { - addr, - mask: u128::MAX, - }); - } - _ => (), - } - } - - Err(format!("Invalid IP address {:?}", value,)) - } -} - -impl ParseValue for IpAddrOrMask { - fn parse_value(ip: &str) -> super::Result { - if ip.contains('/') { - IpAddrMask::parse_value(ip).map(IpAddrOrMask::Mask) - } else { - IpAddr::parse_value(ip).map(IpAddrOrMask::Ip) - } - } -} - -impl ParseValue for SocketAddr { - fn parse_value(value: &str) -> super::Result { - value - .parse() - .map_err(|_| format!("Invalid socket address {:?}.", value,)) - } -} - -impl ParseValue for SupportedCipherSuite { - fn parse_value(value: &str) -> super::Result { - Ok(match value { - // TLS1.3 suites - "TLS13_AES_256_GCM_SHA384" => TLS13_AES_256_GCM_SHA384, - "TLS13_AES_128_GCM_SHA256" => TLS13_AES_128_GCM_SHA256, - "TLS13_CHACHA20_POLY1305_SHA256" => TLS13_CHACHA20_POLY1305_SHA256, - // TLS1.2 suites - "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384" => TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, - "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256" => TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, - "TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256" => { - TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256 - } - "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384" => TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, - "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256" => TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, - "TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256" => { - TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256 - } - cipher => return Err(format!("Unsupported TLS cipher suite {:?}", cipher,)), - }) - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_ipaddrmask() { - for (mask, ip) in [ - ("10.0.0.0/8", "10.30.20.11"), - ("10.0.0.0/8", "10.0.13.73"), - ("192.168.1.1", "192.168.1.1"), - ] { - let mask = IpAddrMask::parse_value(mask).unwrap(); - let ip = ip.parse::().unwrap(); - assert!(mask.matches(&ip)); - } - - for (mask, ip) in [ - ("10.0.0.0/8", "11.30.20.11"), - ("192.168.1.1", "193.168.1.1"), - ] { - let mask = IpAddrMask::parse_value(mask).unwrap(); - let ip = ip.parse::().unwrap(); - assert!(!mask.matches(&ip)); - } - } -} diff --git a/crates/utils/src/config/mod.rs b/crates/utils/src/config/mod.rs deleted file mode 100644 index f4e997e8..00000000 --- a/crates/utils/src/config/mod.rs +++ /dev/null @@ -1,290 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC - * - * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL - */ - -pub mod cron; -pub mod http; -pub mod ipmask; -pub mod parser; -pub mod utils; - -use ahash::AHashMap; -use compact_str::CompactString; -use serde::Serialize; -use std::{collections::BTreeMap, time::Duration}; - -#[derive(Debug, Default, Serialize)] -pub struct Config { - #[serde(skip)] - pub keys: BTreeMap, - pub warnings: AHashMap, - pub errors: AHashMap, - #[cfg(debug_assertions)] - #[serde(skip)] - pub keys_read: parking_lot::Mutex>, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize)] -#[serde(tag = "type")] -#[serde(rename_all = "camelCase")] -pub enum ConfigWarning { - Missing, - AppliedDefault { default: String }, - Unread { value: String }, - Build { error: String }, - Parse { error: String }, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize)] -#[serde(tag = "type")] -#[serde(rename_all = "camelCase")] -pub enum ConfigError { - Parse { error: String }, - Build { error: String }, - Macro { error: String }, -} - -#[derive(Debug, Default, PartialEq, Eq)] -pub struct ConfigKey { - pub key: String, - pub value: String, -} - -#[derive(Debug, Default, PartialEq, Eq, Clone)] -pub struct Rate { - pub requests: u64, - pub period: Duration, -} - -pub type Result = std::result::Result; - -impl Config { - pub async fn resolve_macros(&mut self, classes: &[&str]) { - for macro_class in classes { - self.resolve_macro_type(macro_class).await; - } - } - - pub async fn resolve_all_macros(&mut self) { - self.resolve_macros(&["env", "file", "cfg"]).await; - } - - async fn resolve_macro_type(&mut self, class: &str) { - let macro_start = format!("%{{{class}:"); - let mut replacements = AHashMap::new(); - 'outer: for (key, value) in &self.keys { - if value.contains(¯o_start) && value.contains("}%") { - let mut result = String::with_capacity(value.len()); - let mut snippet: &str = value.as_str(); - - loop { - if let Some((suffix, macro_name)) = snippet.split_once(¯o_start) { - if !suffix.is_empty() { - result.push_str(suffix); - } - if let Some((location, rest)) = macro_name.split_once("}%") { - match class { - "cfg" => { - if let Some(value) = replacements - .get(location) - .or_else(|| self.keys.get(location)) - { - result.push_str(value); - } else { - self.errors.insert( - key.clone(), - ConfigError::Macro { - error: format!("Unknown key {location:?}"), - }, - ); - } - } - "env" => match std::env::var(location) { - Ok(value) => { - result.push_str(&value); - } - Err(_) => { - self.errors.insert( - key.clone(), - ConfigError::Macro { error : format!( - "Failed to obtain environment variable {location:?}" - )}, - ); - } - }, - "file" => { - let file_name = location.strip_prefix("//").unwrap_or(location); - match tokio::fs::read(file_name).await { - Ok(value) => match String::from_utf8(value) { - Ok(value) => { - result.push_str(&value); - } - Err(err) => { - self.errors.insert( - key.clone(), - ConfigError::Macro { - error: format!( - "Failed to read file {file_name:?}: {err}" - ), - }, - ); - continue 'outer; - } - }, - Err(err) => { - self.errors.insert( - key.clone(), - ConfigError::Macro { - error: format!( - "Failed to read file {file_name:?}: {err}" - ), - }, - ); - continue 'outer; - } - } - } - _ => { - unreachable!() - } - }; - - snippet = rest; - } - } else { - result.push_str(snippet); - break; - } - } - - replacements.insert(key.clone(), result); - } - } - - if !replacements.is_empty() { - for (key, value) in replacements { - self.keys.insert(key, value); - } - } - } - - pub fn update(&mut self, settings: Vec<(String, String)>) { - self.keys.extend(settings); - } - - pub fn log_errors(&self) { - for (key, err) in &self.errors { - let (cause, message) = match err { - ConfigError::Parse { error } => ( - trc::ConfigEvent::ParseError, - format!("Failed to parse setting {key:?}: {error}"), - ), - ConfigError::Build { error } => ( - trc::ConfigEvent::BuildError, - format!("Build error for key {key:?}: {error}"), - ), - ConfigError::Macro { error } => ( - trc::ConfigEvent::MacroError, - format!("Macro expansion error for setting {key:?}: {error}"), - ), - }; - - trc::error!( - trc::EventType::Config(cause) - .into_err() - .details(CompactString::from(message)) - ); - } - } - - pub fn log_warnings(&mut self) { - #[cfg(debug_assertions)] - self.warn_unread_keys(); - - for (key, warn) in &self.warnings { - let (cause, message) = match warn { - ConfigWarning::AppliedDefault { default } => ( - trc::ConfigEvent::DefaultApplied, - format!("WARNING: Missing setting {key:?}, applied default {default:?}"), - ), - ConfigWarning::Missing => ( - trc::ConfigEvent::MissingSetting, - format!("WARNING: Missing setting {key:?}"), - ), - ConfigWarning::Unread { value } => ( - trc::ConfigEvent::UnusedSetting, - format!("WARNING: Unused setting {key:?} with value {value:?}"), - ), - ConfigWarning::Parse { error } => ( - trc::ConfigEvent::ParseWarning, - format!("WARNING: Failed to parse {key:?}: {error}"), - ), - ConfigWarning::Build { error } => ( - trc::ConfigEvent::BuildWarning, - format!("WARNING for {key:?}: {error}"), - ), - }; - - trc::error!( - trc::EventType::Config(cause) - .into_err() - .details(CompactString::from(message)) - ); - } - } -} - -impl Clone for Config { - fn clone(&self) -> Self { - Self { - keys: self.keys.clone(), - warnings: self.warnings.clone(), - errors: self.errors.clone(), - #[cfg(debug_assertions)] - keys_read: Default::default(), - } - } -} - -impl PartialEq for Config { - fn eq(&self, other: &Self) -> bool { - self.keys == other.keys && self.warnings == other.warnings && self.errors == other.errors - } -} - -impl Eq for Config {} - -impl From<(String, String)> for ConfigKey { - fn from((key, value): (String, String)) -> Self { - Self { key, value } - } -} - -impl From<(&str, &str)> for ConfigKey { - fn from((key, value): (&str, &str)) -> Self { - Self { - key: key.to_string(), - value: value.to_string(), - } - } -} - -impl From<(&str, String)> for ConfigKey { - fn from((key, value): (&str, String)) -> Self { - Self { - key: key.to_string(), - value, - } - } -} - -impl From<(String, &str)> for ConfigKey { - fn from((key, value): (String, &str)) -> Self { - Self { - key, - value: value.to_string(), - } - } -} diff --git a/crates/utils/src/config/parser.rs b/crates/utils/src/config/parser.rs deleted file mode 100644 index 071779a4..00000000 --- a/crates/utils/src/config/parser.rs +++ /dev/null @@ -1,588 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC - * - * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL - */ - -use std::{ - collections::{BTreeMap, btree_map::Entry}, - iter::Peekable, - str::Chars, -}; - -use super::{Config, Result}; -use std::fmt::Write; - -const MAX_NEST_LEVEL: usize = 10; - -// Simple TOML parser for Stalwart Server configuration files. -impl Config { - pub fn new(toml: impl AsRef) -> Result { - let mut config = Config::default(); - config.parse(toml.as_ref())?; - Ok(config) - } - - pub fn parse(&mut self, toml: &str) -> Result<()> { - let mut parser = TomlParser::new(&mut self.keys, toml); - let mut table_name = String::new(); - let mut last_array_name = String::new(); - let mut last_array_pos = 0; - - while parser.seek_next_char() { - match parser.peek_char()? { - '[' => { - parser.next_char(true, false)?; - table_name.clear(); - let mut is_array = match parser.next_char(true, false)? { - '[' => true, - ch => { - table_name.push(ch); - false - } - }; - let mut in_quote = false; - let mut last_ch = char::from(0); - loop { - let ch = parser.next_char(!in_quote, false)?; - match ch { - '\"' if !in_quote || last_ch != '\\' => { - in_quote = !in_quote; - } - '\\' if in_quote => (), - ']' if !in_quote => { - if table_name.is_empty() { - return Err(format!( - "Empty table name at line {}.", - parser.line - )); - } - if is_array { - if table_name == last_array_name { - last_array_pos += 1; - } else { - last_array_pos = 0; - last_array_name = table_name.to_string(); - } - is_array = false; - write!(table_name, ".{last_array_pos:04}").ok(); - } else { - break; - } - } - _ => { - if !in_quote { - if ch.is_alphanumeric() || ['.', '-', '_'].contains(&ch) { - table_name.push(ch.to_ascii_lowercase()); - } else { - return Err(format!( - "Unexpected character {:?} at line {}.", - ch, parser.line - )); - } - } else { - table_name.push(ch); - } - } - } - last_ch = ch; - } - parser.skip_line(); - } - 'a'..='z' | 'A'..='Z' | '0'..='9' | '\"' => { - let (key, _) = parser.key( - if !table_name.is_empty() { - format!("{table_name}.") - } else { - String::with_capacity(10) - }, - false, - )?; - parser.value(key, &['\n'], 0)?; - } - '#' => { - parser.skip_line(); - } - ch => { - let ch = *ch; - return Err(format!( - "Unexpected character {:?} at line {}.", - ch, parser.line - )); - } - } - } - - Ok(()) - } -} - -struct TomlParser<'x, 'y> { - keys: &'y mut BTreeMap, - iter: Peekable>, - line: usize, -} - -impl<'x, 'y> TomlParser<'x, 'y> { - fn new(keys: &'y mut BTreeMap, toml: &'x str) -> Self { - Self { - keys, - iter: toml.chars().peekable(), - line: 1, - } - } - - fn seek_next_char(&mut self) -> bool { - while let Some(ch) = self.iter.peek() { - match ch { - '\n' => { - self.iter.next(); - self.line += 1; - } - '\r' | ' ' | '\t' => { - self.iter.next(); - } - '#' => { - self.skip_line(); - } - _ => { - return true; - } - } - } - - false - } - - fn peek_char(&mut self) -> Result<&char> { - self.iter.peek().ok_or_else(|| "".to_string()) - } - - fn next_char(&mut self, skip_wsp: bool, allow_lf: bool) -> Result { - for ch in &mut self.iter { - match ch { - '\r' => (), - ' ' | '\t' if skip_wsp => (), - '\n' => { - return if allow_lf { - self.line += 1; - Ok(ch) - } else { - Err(format!("Unexpected end of line at line: {}", self.line)) - }; - } - _ => { - return Ok(ch); - } - } - } - Err(format!("Unexpected EOF at line: {}", self.line)) - } - - fn skip_line(&mut self) { - for ch in &mut self.iter { - if ch == '\n' { - self.line += 1; - break; - } - } - } - - #[allow(clippy::while_let_on_iterator)] - fn key(&mut self, mut key: String, in_curly: bool) -> Result<(String, char)> { - let start_key_len = key.len(); - while let Some(ch) = self.iter.next() { - match ch { - '=' => { - if start_key_len != key.len() { - return Ok((key, ch)); - } else { - return Err(format!("Empty key at line: {}", self.line)); - } - } - ',' | '}' if in_curly => { - if start_key_len != key.len() { - return Ok((key, ch)); - } else { - return Err(format!("Empty key at line: {}", self.line)); - } - } - /*'a'..='z' | '.' | 'A'..='Z' | '0'..='9' | '_' | '-' => { - key.push(ch); - }*/ - '\"' => { - let mut last_ch = char::from(0); - while let Some(ch) = self.iter.next() { - match ch { - '\\' => (), - '\"' if last_ch != '\\' => { - break; - } - '\n' => { - return Err(format!( - "Unexpected end of line while parsing quoted key at line: {}", - self.line - )); - } - _ => { - key.push(ch); - } - } - last_ch = ch; - } - } - ' ' | '\t' | '\r' => (), - '\n' => { - if start_key_len == key.len() { - self.line += 1; - } else { - return Err(format!( - "Unexpected end of line while parsing key {:?} at line: {}", - key, self.line - )); - } - } - _ => { - key.push(ch); - } - } - } - Err(format!("Unexpected EOF at line: {}", self.line)) - } - - fn value(&mut self, key: String, stop_chars: &[char], nest_level: usize) -> Result { - if nest_level == MAX_NEST_LEVEL { - return Err(format!("Too many nested structures at line {}.", self.line)); - } - match self.next_char(true, false)? { - '[' => { - let mut array_pos = 0; - self.seek_next_char(); - loop { - match self.value( - format!("{key}.{array_pos:04}"), - &[',', ']'], - nest_level + 1, - )? { - ',' => { - self.seek_next_char(); - array_pos += 1; - } - ']' => break, - ch => { - return Err(format!( - "Unexpected character {:?} found in array for property {:?} at line {}.", - ch, key, self.line - )); - } - } - } - } - '{' => { - let base_key = format!("{key}."); - let base_key_len = base_key.len(); - - loop { - let (sub_key, stop_char) = self.key(base_key.clone(), true)?; - match stop_char { - '=' => { - // Key value - self.seek_next_char(); - - match self.value(sub_key, &[',', '}'], nest_level + 1)? { - ',' => { - self.seek_next_char(); - } - '}' => break, - ch => { - return Err(format!( - "Unexpected character {:?} found in inline table for property {:?} at line {}.", - ch, key, self.line - )); - } - } - } - ',' => { - // Set - if sub_key.len() > base_key_len { - self.insert_key(sub_key, String::new())?; - } - } - '}' => { - // Set - if sub_key.len() > base_key_len { - self.insert_key(sub_key, String::new())?; - } - break; - } - _ => unreachable!(), - } - } - } - qch @ ('\'' | '\"') => { - let mut value = String::new(); - if matches!(self.iter.peek(), Some(ch) if ch == &qch) { - self.iter.next(); - if matches!(self.iter.peek(), Some(ch) if ch == &qch) { - self.iter.next(); - if matches!(self.iter.peek(), Some(ch) if ch == &'\n') { - self.iter.next(); - self.line += 1; - } - - let mut last_ch = char::from(0); - let mut prev_last_ch = char::from(0); - loop { - let ch = self.next_char(false, true)?; - if !(ch == qch && last_ch == qch && prev_last_ch == qch) { - value.push(ch); - prev_last_ch = last_ch; - last_ch = ch; - } else { - value.truncate(value.len() - 2); - break; - } - } - } - } else { - let mut last_ch = char::from(0); - - loop { - let ch = self.next_char(false, true)?; - match ch { - '\\' if last_ch != '\\' => (), - 't' if last_ch == '\\' => { - value.push('\t'); - } - 'r' if last_ch == '\\' => { - value.push('\r'); - } - 'n' if last_ch == '\\' => { - value.push('\n'); - } - ch => { - if ch != qch || last_ch == '\\' { - value.push(ch); - } else { - break; - } - } - } - last_ch = ch; - } - } - - self.insert_key(key, value)?; - } - ch if ch.is_alphanumeric() || ['.', '+', '-'].contains(&ch) => { - let mut value = String::with_capacity(4); - value.push(ch); - while let Some(ch) = self.iter.peek() { - if ch.is_alphanumeric() || ['.', '+', '-'].contains(ch) { - value.push(self.next_char(true, false)?); - } else { - break; - } - } - self.insert_key(key, value)?; - } - ch => { - return if stop_chars.contains(&ch) { - Ok(ch) - } else { - Err(format!( - "Expected {:?} but found {:?} in value at line {}.", - stop_chars, ch, self.line - )) - }; - } - } - - loop { - match self.next_char(true, true)? { - '#' => { - self.skip_line(); - if stop_chars.contains(&'\n') { - return Ok('\n'); - } - } - ch if stop_chars.contains(&ch) => { - return Ok(ch); - } - '\n' if !stop_chars.contains(&'\n') => (), - ch => { - return Err(format!( - "Expected {:?} but found {:?} in value at line {}.", - stop_chars, ch, self.line - )); - } - } - } - } - - fn insert_key(&mut self, key: String, mut value: String) -> Result<()> { - match self.keys.entry(key) { - Entry::Vacant(e) => { - value.shrink_to_fit(); - e.insert(value); - Ok(()) - } - Entry::Occupied(e) => Err(format!( - "Duplicate key {:?} at line {}.", - e.key(), - self.line - )), - } - } -} - -#[cfg(test)] -mod tests { - use std::{collections::BTreeMap, fs, path::PathBuf}; - - use crate::config::Config; - - #[test] - fn toml_parse() { - let file = PathBuf::from(env!("CARGO_MANIFEST_DIR")) - .parent() - .unwrap() - .parent() - .unwrap() - .to_path_buf() - .join("tests") - .join("resources") - .join("smtp") - .join("config") - .join("toml-parser.toml"); - - let mut config = Config::default(); - config.parse(&fs::read_to_string(file).unwrap()).unwrap(); - let expected = BTreeMap::from_iter( - [ - ("arrays.colors.0000", "red"), - ("arrays.colors.0001", "yellow"), - ("arrays.colors.0002", "green"), - ("arrays.contributors.0000", "Foo Bar "), - ("arrays.contributors.0001.email", "bazqux@example.com"), - ("arrays.contributors.0001.name", "Baz Qux"), - ("arrays.contributors.0001.url", "https://example.com/bazqux"), - ("arrays.integers.0000", "1"), - ("arrays.integers.0001", "2"), - ("arrays.integers.0002", "3"), - ("arrays.integers2.0000", "1"), - ("arrays.integers2.0001", "2"), - ("arrays.integers2.0002", "3"), - ("arrays.integers3.0000", "4"), - ("arrays.integers3.0001", "5"), - ("arrays.nested_arrays_of_ints.0000.0000", "1"), - ("arrays.nested_arrays_of_ints.0000.0001", "2"), - ("arrays.nested_arrays_of_ints.0001.0000", "3"), - ("arrays.nested_arrays_of_ints.0001.0001", "4"), - ("arrays.nested_arrays_of_ints.0001.0002", "5"), - ("arrays.nested_mixed_array.0000.0000", "1"), - ("arrays.nested_mixed_array.0000.0001", "2"), - ("arrays.nested_mixed_array.0001.0000", "a"), - ("arrays.nested_mixed_array.0001.0001", "b"), - ("arrays.nested_mixed_array.0001.0002", "c"), - ("arrays.numbers.0000", "0.1"), - ("arrays.numbers.0001", "0.2"), - ("arrays.numbers.0002", "0.5"), - ("arrays.numbers.0003", "1"), - ("arrays.numbers.0004", "2"), - ("arrays.numbers.0005", "5"), - ("arrays.string_array.0000", "all"), - ("arrays.string_array.0001", "strings"), - ("arrays.string_array.0002", "are the same"), - ("arrays.string_array.0003", "type"), - ("database.data.0000.0000", "delta"), - ("database.data.0000.0001", "phi"), - ("database.data.0001.0000", "3.14"), - ("database.enabled", "true"), - ("database.ports.0000", "8000"), - ("database.ports.0001", "8001"), - ("database.ports.0002", "8002"), - ("database.temp_targets.case", "72.0"), - ("database.temp_targets.cpu", "79.5"), - ("products.0000.name", "Hammer"), - ("products.0000.sku", "738594937"), - ("products.0002.color", "gray"), - ("products.0002.name", "Nail"), - ("products.0002.sku", "284758393"), - ("servers.127.0.0.1", "value"), - ("servers.alpha.ip", "10.0.0.1"), - ("servers.alpha.role", "frontend"), - ("servers.beta.ip", "10.0.0.2"), - ("servers.beta.role", "backend"), - ("servers.character encoding", "value"), - ( - "strings.my \"string\" test.lines", - concat!( - "The first newline is\ntrimmed in raw strings.\n", - "All other whitespace\nis preserved.\n" - ), - ), - ("strings.my \"string\" test.str1", "I'm a string."), - ("strings.my \"string\" test.str2", "You can \"quote\" me."), - ("strings.my \"string\" test.str3", "Name\tTabs\nNew Line."), - ("env.var1", "utils"), - ("env.var2", "utils"), - ("sets.integer.1", ""), - ("sets.integers.1", ""), - ("sets.integers.2", ""), - ("sets.integers.3", ""), - ("sets.string.red", ""), - ("sets.strings.red", ""), - ("sets.strings.yellow", ""), - ("sets.strings.green", ""), - ] - .map(|(k, v)| (k.to_string(), v.to_string())), - ); - - if config.keys != expected { - for (key, value) in &config.keys { - if let Some(expected_value) = expected.get(key) { - if value != expected_value { - panic!( - "Expected value {:?} for key {:?} but found {:?}.", - expected_value, key, value - ); - } - } else { - panic!( - "Unexpected key {:?} found in config with value {:?}.", - key, value - ); - } - } - - for (key, value) in &expected { - if let Some(config_value) = config.keys.get(key) { - if value != config_value { - panic!( - "Expected value {:?} for key {:?} but found {:?}.", - value, key, config_value - ); - } - } else { - panic!( - "Expected key {:?} not found in config with value {:?}.", - key, value - ); - } - } - } - - assert_eq!( - config.set_values("sets.strings").collect::>(), - vec!["green", "red", "yellow"] - ); - - assert_eq!( - config.sub_keys("sets.strings", ""), - vec!["green", "red", "yellow"] - ); - - assert_eq!(config.sub_keys("sets", ".red"), vec!["string", "strings"]); - } -} diff --git a/crates/utils/src/config/utils.rs b/crates/utils/src/config/utils.rs deleted file mode 100644 index 4616ddc6..00000000 --- a/crates/utils/src/config/utils.rs +++ /dev/null @@ -1,844 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC - * - * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL - */ - -use std::{ - net::{IpAddr, Ipv4Addr, Ipv6Addr}, - path::PathBuf, - str::FromStr, - time::Duration, -}; - -use mail_auth::{ - IpLookupStrategy, - common::crypto::{Algorithm, HashAlgorithm}, - dkim::Canonicalization, -}; -use smtp_proto::MtPriority; - -use super::{Config, ConfigError, ConfigWarning, Rate}; - -impl Config { - pub fn property(&mut self, key: impl AsKey) -> Option { - let key = key.as_key(); - - #[cfg(debug_assertions)] - self.keys_read.lock().insert(key.clone()); - - if let Some(value) = self.keys.get(&key) { - match T::parse_value(value) { - Ok(value) => Some(value), - Err(err) => { - self.new_parse_error(key, err); - None - } - } - } else { - None - } - } - - pub fn property_or_default( - &mut self, - key: impl AsKey, - default: &str, - ) -> Option { - let key = key.as_key(); - - #[cfg(debug_assertions)] - self.keys_read.lock().insert(key.clone()); - - let value = match self.keys.get(&key) { - Some(value) => value.as_str(), - None => default, - }; - match T::parse_value(value) { - Ok(value) => Some(value), - Err(err) => { - self.new_parse_error(key, err); - None - } - } - } - - pub fn property_or_else( - &mut self, - key: impl AsKey, - or_else: impl AsKey, - default: &str, - ) -> Option { - let key = key.as_key(); - let value = match self.value_or_else(key.as_str(), or_else.clone()) { - Some(value) => value, - None => default, - }; - - match T::parse_value(value) { - Ok(value) => Some(value), - Err(err) => { - self.new_parse_error(key, err); - None - } - } - } - - pub fn property_require(&mut self, key: impl AsKey) -> Option { - let key = key.as_key(); - - #[cfg(debug_assertions)] - self.keys_read.lock().insert(key.clone()); - - if let Some(value) = self.keys.get(&key) { - match T::parse_value(value) { - Ok(value) => Some(value), - Err(err) => { - self.new_parse_error(key, err); - None - } - } - } else { - self.new_parse_error(key, "Missing property"); - None - } - } - - pub fn sub_keys(&self, prefix: impl AsKey, suffix: &str) -> Vec { - let mut last_key = ""; - let prefix = prefix.as_prefix(); - - self.keys - .keys() - .filter_map(move |key| { - let key = key.strip_prefix(&prefix)?; - let key = if !suffix.is_empty() { - key.strip_suffix(suffix)? - } else if let Some((key, _)) = key.split_once('.') { - key - } else { - key - }; - if last_key != key { - last_key = key; - Some(key.to_string()) - } else { - None - } - }) - .collect() - } - - pub fn sub_keys_with_suffixes(&self, prefix: impl AsKey, suffixes: &[&str]) -> Vec { - let mut last_key = ""; - let prefix = prefix.as_prefix(); - - self.keys - .keys() - .filter_map(move |key| { - let key = key.strip_prefix(&prefix)?; - let key = suffixes - .iter() - .filter_map(|suffix| key.strip_suffix(suffix)) - .next()?; - if last_key != key { - last_key = key; - Some(key.to_string()) - } else { - None - } - }) - .collect() - } - - pub fn prefix<'x, 'y: 'x>(&'y self, prefix: impl AsKey) -> impl Iterator + 'x { - let prefix = prefix.as_prefix(); - self.keys - .keys() - .filter_map(move |key| key.strip_prefix(&prefix)) - } - - pub fn set_values<'x, 'y: 'x>( - &'y self, - prefix: impl AsKey, - ) -> impl Iterator + 'x { - let prefix = prefix.as_prefix(); - - #[cfg(debug_assertions)] - self.keys_read.lock().insert(prefix.clone()); - - self.keys - .keys() - .filter_map(move |key| key.strip_prefix(&prefix)) - } - - pub fn properties(&mut self, prefix: impl AsKey) -> Vec<(String, T)> { - let full_prefix = prefix.as_key(); - let prefix = prefix.as_prefix(); - let mut results = Vec::new(); - - #[cfg(debug_assertions)] - self.keys_read.lock().insert(prefix.clone()); - - for (key, value) in &self.keys { - if key.starts_with(&prefix) || key == &full_prefix { - match T::parse_value(value) { - Ok(value) => { - results.push((key.to_string(), value)); - } - Err(error) => { - self.errors - .insert(key.to_string(), ConfigError::Parse { error }); - } - } - } - } - - results - } - - pub fn value(&self, key: impl AsKey) -> Option<&str> { - let key = key.as_key(); - - #[cfg(debug_assertions)] - self.keys_read.lock().insert(key.clone()); - - self.keys.get(&key).map(|s| s.as_str()) - } - - pub fn contains_key(&self, key: impl AsKey) -> bool { - self.keys.contains_key(&key.as_key()) - } - - pub fn value_require(&mut self, key: impl AsKey) -> Option<&str> { - let key = key.as_key(); - - #[cfg(debug_assertions)] - self.keys_read.lock().insert(key.clone()); - - if let Some(value) = self.keys.get(&key) { - Some(value.as_str()) - } else { - self.errors.insert( - key, - ConfigError::Parse { - error: "Missing property".to_string(), - }, - ); - None - } - } - - pub fn value_require_non_empty(&mut self, key: impl AsKey) -> Option<&str> { - let key = key.as_key(); - - #[cfg(debug_assertions)] - self.keys_read.lock().insert(key.clone()); - - if let Some(value) = self.keys.get(&key).and_then(|v| { - let v = v.trim(); - if !v.is_empty() { Some(v) } else { None } - }) { - Some(value) - } else { - self.errors.insert( - key, - ConfigError::Parse { - error: "Missing property".to_string(), - }, - ); - None - } - } - - pub fn try_parse_value(&mut self, key: impl AsKey, value: &str) -> Option { - match T::parse_value(value) { - Ok(value) => Some(value), - Err(error) => { - self.errors - .insert(key.as_key(), ConfigError::Parse { error }); - None - } - } - } - - pub fn value_or_else(&self, key: impl AsKey, or_else: impl AsKey) -> Option<&str> { - let key = key.as_key(); - - #[cfg(debug_assertions)] - { - self.keys_read.lock().insert(key.clone()); - self.keys_read.lock().insert(or_else.clone().as_key()); - } - - self.keys - .get(&key) - .or_else(|| self.keys.get(&or_else.as_key())) - .map(|s| s.as_str()) - } - - pub fn values(&self, prefix: impl AsKey) -> impl Iterator { - let full_prefix = prefix.as_key(); - let prefix = prefix.as_prefix(); - - #[cfg(debug_assertions)] - self.keys_read.lock().insert(prefix.clone()); - - self.keys.iter().filter_map(move |(key, value)| { - if key.starts_with(&prefix) || key == &full_prefix { - (key.as_str(), value.as_str()).into() - } else { - None - } - }) - } - - pub fn iterate_prefix(&self, prefix: impl AsKey) -> impl Iterator { - let prefix = prefix.as_prefix(); - - #[cfg(debug_assertions)] - self.keys_read.lock().insert(prefix.clone()); - - self.keys - .iter() - .filter_map(move |(key, value)| Some((key.strip_prefix(&prefix)?, value.as_str()))) - } - - pub fn values_or_else( - &self, - prefix: impl AsKey, - or_else: impl AsKey, - ) -> impl Iterator { - let mut prefix = prefix.as_prefix(); - - #[cfg(debug_assertions)] - { - self.keys_read.lock().insert(prefix.clone()); - self.keys_read.lock().insert(or_else.clone().as_prefix()); - } - - self.values(if self.keys.keys().any(|k| k.starts_with(&prefix)) { - prefix.truncate(prefix.len() - 1); - prefix - } else { - or_else.as_key() - }) - } - - pub fn has_prefix(&self, prefix: impl AsKey) -> bool { - let prefix = prefix.as_prefix(); - self.keys.keys().any(|k| k.starts_with(&prefix)) - } - - pub fn new_parse_error(&mut self, key: impl AsKey, details: impl Into) { - self.errors.insert( - key.as_key(), - ConfigError::Parse { - error: details.into(), - }, - ); - } - - pub fn new_build_error(&mut self, key: impl AsKey, details: impl Into) { - self.errors.insert( - key.as_key(), - ConfigError::Build { - error: details.into(), - }, - ); - } - - pub fn new_parse_warning(&mut self, key: impl AsKey, details: impl Into) { - self.warnings.insert( - key.as_key(), - ConfigWarning::Parse { - error: details.into(), - }, - ); - } - - pub fn new_build_warning(&mut self, key: impl AsKey, details: impl Into) { - self.warnings.insert( - key.as_key(), - ConfigWarning::Build { - error: details.into(), - }, - ); - } - - pub fn new_missing_property(&mut self, key: impl AsKey) { - self.warnings.insert(key.as_key(), ConfigWarning::Missing); - } - - #[cfg(debug_assertions)] - pub fn warn_unread_keys(&mut self) { - let mut keys = self.keys.clone(); - - for key in self.keys_read.lock().iter() { - if let Some(base_key) = key.strip_suffix('.') { - keys.remove(base_key); - keys.retain(|k, _| !k.starts_with(key)); - } else { - keys.remove(key); - } - } - - for (key, value) in keys { - self.warnings.insert(key, ConfigWarning::Unread { value }); - } - } -} - -pub trait ParseValue: Sized { - fn parse_value(value: &str) -> super::Result; -} - -impl ParseValue for Option { - fn parse_value(value: &str) -> super::Result { - if !value.is_empty() - && !value.eq_ignore_ascii_case("false") - && !value.eq_ignore_ascii_case("disable") - && !value.eq_ignore_ascii_case("disabled") - && !value.eq_ignore_ascii_case("never") - && !value.eq("0") - { - T::parse_value(value).map(Some) - } else { - Ok(None) - } - } -} - -impl ParseValue for String { - fn parse_value(value: &str) -> super::Result { - Ok(value.to_string()) - } -} - -impl ParseValue for u64 { - fn parse_value(value: &str) -> super::Result { - value - .parse() - .map_err(|_| format!("Invalid integer value {:?}.", value,)) - } -} - -impl ParseValue for f64 { - fn parse_value(value: &str) -> super::Result { - value - .parse() - .map_err(|_| format!("Invalid floating point value {:?}.", value)) - } -} - -impl ParseValue for u16 { - fn parse_value(value: &str) -> super::Result { - value - .parse() - .map_err(|_| format!("Invalid integer value {:?}.", value)) - } -} - -impl ParseValue for i16 { - fn parse_value(value: &str) -> super::Result { - value - .parse() - .map_err(|_| format!("Invalid integer value {:?}.", value)) - } -} - -impl ParseValue for u32 { - fn parse_value(value: &str) -> super::Result { - value - .parse() - .map_err(|_| format!("Invalid integer value {:?}.", value)) - } -} - -impl ParseValue for i32 { - fn parse_value(value: &str) -> super::Result { - value - .parse() - .map_err(|_| format!("Invalid integer value {:?}.", value)) - } -} - -impl ParseValue for f32 { - fn parse_value(value: &str) -> super::Result { - value - .parse() - .map_err(|_| format!("Invalid floating point value {:?}.", value)) - } -} - -impl ParseValue for IpAddr { - fn parse_value(value: &str) -> super::Result { - value - .parse() - .map_err(|_| format!("Invalid IP address value {:?}.", value)) - } -} - -impl ParseValue for usize { - fn parse_value(value: &str) -> super::Result { - value - .parse() - .map_err(|_| format!("Invalid integer value {:?}.", value)) - } -} - -impl ParseValue for bool { - fn parse_value(value: &str) -> super::Result { - value - .parse() - .map_err(|_| format!("Invalid boolean value {:?}.", value)) - } -} - -impl ParseValue for Ipv4Addr { - fn parse_value(value: &str) -> super::Result { - value - .parse() - .map_err(|_| format!("Invalid IPv4 value {:?}.", value)) - } -} - -impl ParseValue for Ipv6Addr { - fn parse_value(value: &str) -> super::Result { - value - .parse() - .map_err(|_| format!("Invalid IPv6 value {:?}.", value)) - } -} - -impl ParseValue for PathBuf { - fn parse_value(value: &str) -> super::Result { - let path = PathBuf::from(value); - - if path.exists() { - Ok(path) - } else { - Err(format!("Directory {} does not exist.", path.display())) - } - } -} - -impl ParseValue for MtPriority { - fn parse_value(value: &str) -> super::Result { - match value.to_ascii_lowercase().as_str() { - "mixer" => Ok(MtPriority::Mixer), - "stanag4406" => Ok(MtPriority::Stanag4406), - "nsep" => Ok(MtPriority::Nsep), - _ => Err(format!("Invalid priority value {:?}.", value)), - } - } -} - -impl ParseValue for Canonicalization { - fn parse_value(value: &str) -> super::Result { - match value { - "relaxed" => Ok(Canonicalization::Relaxed), - "simple" => Ok(Canonicalization::Simple), - _ => Err(format!("Invalid canonicalization value {:?}.", value)), - } - } -} - -impl ParseValue for IpLookupStrategy { - fn parse_value(value: &str) -> super::Result { - Ok(match value.to_lowercase().as_str() { - "ipv4_only" => IpLookupStrategy::Ipv4Only, - "ipv6_only" => IpLookupStrategy::Ipv6Only, - //"ipv4_and_ipv6" => IpLookupStrategy::Ipv4AndIpv6, - "ipv6_then_ipv4" => IpLookupStrategy::Ipv6thenIpv4, - "ipv4_then_ipv6" => IpLookupStrategy::Ipv4thenIpv6, - _ => return Err(format!("Invalid IP lookup strategy {:?}.", value)), - }) - } -} - -impl ParseValue for Algorithm { - fn parse_value(value: &str) -> super::Result { - match value { - "ed25519-sha256" | "ed25519-sha-256" => Ok(Algorithm::Ed25519Sha256), - "rsa-sha-256" | "rsa-sha256" => Ok(Algorithm::RsaSha256), - "rsa-sha-1" | "rsa-sha1" => Ok(Algorithm::RsaSha1), - _ => Err(format!("Invalid algorithm {:?}.", value)), - } - } -} - -impl ParseValue for HashAlgorithm { - fn parse_value(value: &str) -> super::Result { - match value { - "sha256" | "sha-256" => Ok(HashAlgorithm::Sha256), - "sha-1" | "sha1" => Ok(HashAlgorithm::Sha1), - _ => Err(format!("Invalid hash algorithm {:?}.", value)), - } - } -} - -impl ParseValue for Duration { - fn parse_value(value: &str) -> super::Result { - let mut digits = String::new(); - let mut multiplier = String::new(); - - for ch in value.chars() { - if ch.is_ascii_digit() { - digits.push(ch); - } else if !ch.is_ascii_whitespace() { - multiplier.push(ch.to_ascii_lowercase()); - } - } - - let multiplier = match multiplier.as_str() { - "d" => 24 * 60 * 60 * 1000, - "h" => 60 * 60 * 1000, - "m" => 60 * 1000, - "s" => 1000, - "ms" | "" => 1, - _ => return Err(format!("Invalid duration value {:?}.", value)), - }; - - digits - .parse::() - .ok() - .and_then(|num| { - if num > 0 { - Some(Duration::from_millis(num * multiplier)) - } else { - None - } - }) - .ok_or_else(|| format!("Invalid duration value {:?}.", value)) - } -} - -impl ParseValue for Rate { - fn parse_value(value: &str) -> super::Result { - if let Some((requests, period)) = value.split_once('/') { - Ok(Rate { - requests: requests - .trim() - .parse::() - .ok() - .and_then(|r| if r > 0 { Some(r) } else { None }) - .ok_or_else(|| format!("Invalid rate value {:?}.", value))?, - period: std::cmp::max(Duration::parse_value(period)?, Duration::from_secs(1)), - }) - } else if ["false", "none", "unlimited"].contains(&value) { - Ok(Rate::default()) - } else { - Err(format!("Invalid rate value {:?}.", value)) - } - } -} - -impl ParseValue for trc::Level { - fn parse_value(value: &str) -> super::Result { - trc::Level::from_str(value).map_err(|err| format!("Invalid log level: {err}")) - } -} - -impl ParseValue for trc::EventType { - fn parse_value(value: &str) -> super::Result { - trc::EventType::parse(value).ok_or_else(|| format!("Unknown event type: {value}")) - } -} - -impl ParseValue for () { - fn parse_value(_: &str) -> super::Result { - Ok(()) - } -} - -pub trait AsKey: Clone { - fn as_key(&self) -> String; - fn as_prefix(&self) -> String; -} - -impl AsKey for String { - fn as_key(&self) -> String { - self.to_string() - } - - fn as_prefix(&self) -> String { - format!("{self}.") - } -} - -impl AsKey for &String { - fn as_key(&self) -> String { - self.to_string() - } - - fn as_prefix(&self) -> String { - format!("{self}.") - } -} - -impl AsKey for &str { - fn as_key(&self) -> String { - self.to_string() - } - - fn as_prefix(&self) -> String { - format!("{self}.") - } -} - -impl AsKey for (A, B) -where - A: AsRef + Clone, - B: AsRef + Clone, -{ - fn as_key(&self) -> String { - format!("{}.{}", self.0.as_ref(), self.1.as_ref(),) - } - - fn as_prefix(&self) -> String { - format!("{}.{}.", self.0.as_ref(), self.1.as_ref(),) - } -} - -impl AsKey for (A, B, C) -where - A: AsRef + Clone, - B: AsRef + Clone, - C: AsRef + Clone, -{ - fn as_key(&self) -> String { - format!( - "{}.{}.{}", - self.0.as_ref(), - self.1.as_ref(), - self.2.as_ref() - ) - } - - fn as_prefix(&self) -> String { - format!( - "{}.{}.{}.", - self.0.as_ref(), - self.1.as_ref(), - self.2.as_ref() - ) - } -} - -impl AsKey for (A, B, C, D) -where - A: AsRef + Clone, - B: AsRef + Clone, - C: AsRef + Clone, - D: AsRef + Clone, -{ - fn as_key(&self) -> String { - format!( - "{}.{}.{}.{}", - self.0.as_ref(), - self.1.as_ref(), - self.2.as_ref(), - self.3.as_ref() - ) - } - - fn as_prefix(&self) -> String { - format!( - "{}.{}.{}.{}.", - self.0.as_ref(), - self.1.as_ref(), - self.2.as_ref(), - self.3.as_ref() - ) - } -} - -impl AsKey for (A, B, C, D, E) -where - A: AsRef + Clone, - B: AsRef + Clone, - C: AsRef + Clone, - D: AsRef + Clone, - E: AsRef + Clone, -{ - fn as_key(&self) -> String { - format!( - "{}.{}.{}.{}.{}", - self.0.as_ref(), - self.1.as_ref(), - self.2.as_ref(), - self.3.as_ref(), - self.4.as_ref() - ) - } - - fn as_prefix(&self) -> String { - format!( - "{}.{}.{}.{}.{}.", - self.0.as_ref(), - self.1.as_ref(), - self.2.as_ref(), - self.3.as_ref(), - self.4.as_ref() - ) - } -} - -#[cfg(test)] -mod tests { - use std::net::IpAddr; - - use crate::config::Config; - - #[test] - fn toml_utils() { - let toml = r#" -[queues."z"] -retry = [0, 1, 15, 60, 90] -value = "hi" - -[queues."x"] -retry = [3, 60] -value = "hi 2" - -[queues.a] -retry = [1, 2, 3, 4] -value = "hi 3" - -[servers."my relay"] -hostname = "mx.example.org" - -[[servers."my relay".transaction.auth.limits]] -idle = 10 - -[[servers."my relay".transaction.auth.limits]] -idle = 20 - -[servers."submissions"] -hostname = "submit.example.org" -ip = "a:b::1:1" -"#; - let mut config = Config::default(); - config.parse(toml).unwrap(); - - assert_eq!(config.sub_keys("queues", ""), ["a", "x", "z"]); - assert_eq!(config.sub_keys("servers", ""), ["my relay", "submissions"]); - assert_eq!( - config.sub_keys("queues.z.retry", ""), - ["0000", "0001", "0002", "0003", "0004"] - ); - assert_eq!( - config - .property::("servers.my relay.transaction.auth.limits.0001.idle") - .unwrap(), - 20 - ); - assert_eq!( - config - .property::(("servers", "submissions", "ip")) - .unwrap(), - "a:b::1:1".parse::().unwrap() - ); - } -} diff --git a/crates/utils/src/config/cron.rs b/crates/utils/src/cron.rs similarity index 97% rename from crates/utils/src/config/cron.rs rename to crates/utils/src/cron.rs index 09fa3ae4..b9fc94e4 100644 --- a/crates/utils/src/config/cron.rs +++ b/crates/utils/src/cron.rs @@ -4,11 +4,8 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use std::time::Duration; - use chrono::{Datelike, Local, TimeDelta, TimeZone, Timelike}; - -use super::utils::ParseValue; +use std::{str::FromStr, time::Duration}; #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum SimpleCron { @@ -71,8 +68,10 @@ impl SimpleCron { } } -impl ParseValue for SimpleCron { - fn parse_value(value: &str) -> super::Result { +impl FromStr for SimpleCron { + type Err = String; + + fn from_str(value: &str) -> Result { let mut hour = 0; let mut minute = 0; diff --git a/crates/utils/src/config/http.rs b/crates/utils/src/http.rs similarity index 100% rename from crates/utils/src/config/http.rs rename to crates/utils/src/http.rs diff --git a/crates/utils/src/lib.rs b/crates/utils/src/lib.rs index c0ef26d5..1297ef26 100644 --- a/crates/utils/src/lib.rs +++ b/crates/utils/src/lib.rs @@ -9,8 +9,9 @@ pub mod cache; pub mod chained_bytes; pub mod cheeky_hash; pub mod codec; -pub mod config; +pub mod cron; pub mod glob; +pub mod http; pub mod map; pub mod snowflake; pub mod template;