RIP TOML
This commit is contained in:
@@ -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;
|
||||
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)]
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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(¯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(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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"]);
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user