This commit is contained in:
mdecimus
2026-02-03 12:29:49 +01:00
parent 7c8be27fcf
commit c6495f6fed
13 changed files with 10 additions and 2003 deletions

View File

@@ -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;

View File

@@ -15,7 +15,6 @@ use crate::tokenizers::{
word::WordTokenizer,
};
use std::borrow::Cow;
use utils::config::utils::ParseValue;
pub type LanguageTokenizer<'x> = Box<dyn Iterator<Item = Token<Cow<'x, str>>> + 'x + Sync + Send>;
@@ -207,9 +206,3 @@ impl Language {
}
}
}
impl ParseValue for Language {
fn parse_value(value: &str) -> utils::config::Result<Self> {
Language::from_iso_639(value).ok_or_else(|| format!("Invalid language code: {}", value))
}
}

View File

@@ -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)]

View File

@@ -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),

View File

@@ -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<Self> {
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 {

View File

@@ -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<Self, String> {
SpecialUse::parse(value).ok_or_else(|| format!("Unknown folder role {:?}", value))
}
}
impl<'x, P: Property, E: Element + From<SpecialUse>> From<SpecialUse> for Value<'x, P, E> {
fn from(id: SpecialUse) -> Self {
Value::Element(E::from(id))

View File

@@ -1,186 +0,0 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <hello@stalw.art>
*
* 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<Self> {
if let Some((addr, mask)) = value.rsplit_once('/') {
if let (Ok(addr), Ok(mask)) =
(addr.trim().parse::<IpAddr>(), mask.trim().parse::<u32>())
{
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::<IpAddr>() {
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<Self> {
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<Self> {
value
.parse()
.map_err(|_| format!("Invalid socket address {:?}.", value,))
}
}
impl ParseValue for SupportedCipherSuite {
fn parse_value(value: &str) -> super::Result<Self> {
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::<IpAddr>().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::<IpAddr>().unwrap();
assert!(!mask.matches(&ip));
}
}
}

View File

@@ -1,290 +0,0 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <hello@stalw.art>
*
* 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<String, String>,
pub warnings: AHashMap<String, ConfigWarning>,
pub errors: AHashMap<String, ConfigError>,
#[cfg(debug_assertions)]
#[serde(skip)]
pub keys_read: parking_lot::Mutex<ahash::AHashSet<String>>,
}
#[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<T> = std::result::Result<T, String>;
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(&macro_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(&macro_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(),
}
}
}

View File

@@ -1,588 +0,0 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <hello@stalw.art>
*
* 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<str>) -> Result<Self> {
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<String, String>,
iter: Peekable<Chars<'x>>,
line: usize,
}
impl<'x, 'y> TomlParser<'x, 'y> {
fn new(keys: &'y mut BTreeMap<String, String>, 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<char> {
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<char> {
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 <foo@example.com>"),
("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<_>>(),
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"]);
}
}

View File

@@ -1,844 +0,0 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <hello@stalw.art>
*
* 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<T: ParseValue>(&mut self, key: impl AsKey) -> Option<T> {
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<T: ParseValue>(
&mut self,
key: impl AsKey,
default: &str,
) -> Option<T> {
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<T: ParseValue>(
&mut self,
key: impl AsKey,
or_else: impl AsKey,
default: &str,
) -> Option<T> {
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<T: ParseValue>(&mut self, key: impl AsKey) -> Option<T> {
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<String> {
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<String> {
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<Item = &'x str> + '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<Item = &'x str> + '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<T: ParseValue>(&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<T: ParseValue>(&mut self, key: impl AsKey, value: &str) -> Option<T> {
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<Item = (&str, &str)> {
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<Item = (&str, &str)> {
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<Item = (&str, &str)> {
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<String>) {
self.errors.insert(
key.as_key(),
ConfigError::Parse {
error: details.into(),
},
);
}
pub fn new_build_error(&mut self, key: impl AsKey, details: impl Into<String>) {
self.errors.insert(
key.as_key(),
ConfigError::Build {
error: details.into(),
},
);
}
pub fn new_parse_warning(&mut self, key: impl AsKey, details: impl Into<String>) {
self.warnings.insert(
key.as_key(),
ConfigWarning::Parse {
error: details.into(),
},
);
}
pub fn new_build_warning(&mut self, key: impl AsKey, details: impl Into<String>) {
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<Self>;
}
impl<T: ParseValue> ParseValue for Option<T> {
fn parse_value(value: &str) -> super::Result<Self> {
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<Self> {
Ok(value.to_string())
}
}
impl ParseValue for u64 {
fn parse_value(value: &str) -> super::Result<Self> {
value
.parse()
.map_err(|_| format!("Invalid integer value {:?}.", value,))
}
}
impl ParseValue for f64 {
fn parse_value(value: &str) -> super::Result<Self> {
value
.parse()
.map_err(|_| format!("Invalid floating point value {:?}.", value))
}
}
impl ParseValue for u16 {
fn parse_value(value: &str) -> super::Result<Self> {
value
.parse()
.map_err(|_| format!("Invalid integer value {:?}.", value))
}
}
impl ParseValue for i16 {
fn parse_value(value: &str) -> super::Result<Self> {
value
.parse()
.map_err(|_| format!("Invalid integer value {:?}.", value))
}
}
impl ParseValue for u32 {
fn parse_value(value: &str) -> super::Result<Self> {
value
.parse()
.map_err(|_| format!("Invalid integer value {:?}.", value))
}
}
impl ParseValue for i32 {
fn parse_value(value: &str) -> super::Result<Self> {
value
.parse()
.map_err(|_| format!("Invalid integer value {:?}.", value))
}
}
impl ParseValue for f32 {
fn parse_value(value: &str) -> super::Result<Self> {
value
.parse()
.map_err(|_| format!("Invalid floating point value {:?}.", value))
}
}
impl ParseValue for IpAddr {
fn parse_value(value: &str) -> super::Result<Self> {
value
.parse()
.map_err(|_| format!("Invalid IP address value {:?}.", value))
}
}
impl ParseValue for usize {
fn parse_value(value: &str) -> super::Result<Self> {
value
.parse()
.map_err(|_| format!("Invalid integer value {:?}.", value))
}
}
impl ParseValue for bool {
fn parse_value(value: &str) -> super::Result<Self> {
value
.parse()
.map_err(|_| format!("Invalid boolean value {:?}.", value))
}
}
impl ParseValue for Ipv4Addr {
fn parse_value(value: &str) -> super::Result<Self> {
value
.parse()
.map_err(|_| format!("Invalid IPv4 value {:?}.", value))
}
}
impl ParseValue for Ipv6Addr {
fn parse_value(value: &str) -> super::Result<Self> {
value
.parse()
.map_err(|_| format!("Invalid IPv6 value {:?}.", value))
}
}
impl ParseValue for PathBuf {
fn parse_value(value: &str) -> super::Result<Self> {
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<Self> {
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<Self> {
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<Self> {
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<Self> {
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<Self> {
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<Self> {
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::<u64>()
.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<Self> {
if let Some((requests, period)) = value.split_once('/') {
Ok(Rate {
requests: requests
.trim()
.parse::<u64>()
.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<Self> {
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<Self> {
trc::EventType::parse(value).ok_or_else(|| format!("Unknown event type: {value}"))
}
}
impl ParseValue for () {
fn parse_value(_: &str) -> super::Result<Self> {
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<A, B> AsKey for (A, B)
where
A: AsRef<str> + Clone,
B: AsRef<str> + 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<A, B, C> AsKey for (A, B, C)
where
A: AsRef<str> + Clone,
B: AsRef<str> + Clone,
C: AsRef<str> + 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<A, B, C, D> AsKey for (A, B, C, D)
where
A: AsRef<str> + Clone,
B: AsRef<str> + Clone,
C: AsRef<str> + Clone,
D: AsRef<str> + 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<A, B, C, D, E> AsKey for (A, B, C, D, E)
where
A: AsRef<str> + Clone,
B: AsRef<str> + Clone,
C: AsRef<str> + Clone,
D: AsRef<str> + Clone,
E: AsRef<str> + 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::<u32>("servers.my relay.transaction.auth.limits.0001.idle")
.unwrap(),
20
);
assert_eq!(
config
.property::<IpAddr>(("servers", "submissions", "ip"))
.unwrap(),
"a:b::1:1".parse::<IpAddr>().unwrap()
);
}
}

View File

@@ -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<Self> {
impl FromStr for SimpleCron {
type Err = String;
fn from_str(value: &str) -> Result<Self, Self::Err> {
let mut hour = 0;
let mut minute = 0;

View File

@@ -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;