Import upstream v0.16.22, stripped
Upstream commit: 474dd0229cb20cf513036619781ed97bd8073c3f Enterprise-only files removed or emptied: 63 Enterprise-only snippets removed: 117 in 50 files Dangling module declarations removed: 5 Cargo edits turning enterprise off: 14 Verification: clean Enterprise feature gates left for rebuilt features: 19 in 18 files Produced by tools/fork/strip.py. The full report is in docs/fork/strip-reports/ on main.
This commit is contained in:
@@ -0,0 +1,103 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use std::{borrow::Borrow, hash::Hash, rc::Rc};
|
||||
|
||||
use ahash::AHashMap;
|
||||
|
||||
#[derive(Debug)]
|
||||
#[repr(transparent)]
|
||||
struct StringRef<T: IdBimapItem>(Rc<T>);
|
||||
|
||||
#[derive(Debug)]
|
||||
#[repr(transparent)]
|
||||
struct IdRef<T: IdBimapItem>(Rc<T>);
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct IdBimap<T: IdBimapItem> {
|
||||
id_to_name: AHashMap<IdRef<T>, Rc<T>>,
|
||||
name_to_id: AHashMap<StringRef<T>, Rc<T>>,
|
||||
}
|
||||
|
||||
impl<T: IdBimapItem> IdBimap<T> {
|
||||
pub fn with_capacity(capacity: usize) -> Self {
|
||||
Self {
|
||||
id_to_name: AHashMap::with_capacity(capacity),
|
||||
name_to_id: AHashMap::with_capacity(capacity),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn insert(&mut self, item: T) {
|
||||
let item = Rc::new(item);
|
||||
self.id_to_name.insert(IdRef(item.clone()), item.clone());
|
||||
self.name_to_id.insert(StringRef(item.clone()), item);
|
||||
}
|
||||
|
||||
pub fn by_name(&self, name: &str) -> Option<&T> {
|
||||
self.name_to_id.get(name).map(|v| v.as_ref())
|
||||
}
|
||||
|
||||
pub fn by_id(&self, id: u32) -> Option<&T> {
|
||||
self.id_to_name.get(&id).map(|v| v.as_ref())
|
||||
}
|
||||
|
||||
pub fn iter(&self) -> impl Iterator<Item = &T> {
|
||||
self.name_to_id.values().map(|v| v.as_ref())
|
||||
}
|
||||
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.name_to_id.is_empty()
|
||||
}
|
||||
}
|
||||
|
||||
// SAFETY: Safe because Rc<> are never returned from the struct
|
||||
unsafe impl<T: IdBimapItem> Send for IdBimap<T> {}
|
||||
unsafe impl<T: IdBimapItem> Sync for IdBimap<T> {}
|
||||
|
||||
pub trait IdBimapItem: std::fmt::Debug {
|
||||
fn id(&self) -> &u32;
|
||||
fn name(&self) -> &str;
|
||||
}
|
||||
|
||||
impl<T: IdBimapItem> Borrow<str> for StringRef<T> {
|
||||
fn borrow(&self) -> &str {
|
||||
self.0.name()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: IdBimapItem> Borrow<u32> for IdRef<T> {
|
||||
fn borrow(&self) -> &u32 {
|
||||
self.0.id()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: IdBimapItem> PartialEq for StringRef<T> {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.0.name() == other.0.name()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: IdBimapItem> Eq for StringRef<T> {}
|
||||
|
||||
impl<T: IdBimapItem> PartialEq for IdRef<T> {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.0.id() == other.0.id()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: IdBimapItem> Eq for IdRef<T> {}
|
||||
|
||||
impl<T: IdBimapItem> Hash for StringRef<T> {
|
||||
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
|
||||
self.0.name().hash(state)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: IdBimapItem> Hash for IdRef<T> {
|
||||
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
|
||||
self.0.id().hash(state)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,420 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use arcstr::ArcStr;
|
||||
use mail_auth::{DnssecStatus, MX, RecordSet, ResolverCache, Txt};
|
||||
use quick_cache::{
|
||||
Equivalent, Options, OptionsBuilder, Weighter,
|
||||
sync::{DefaultLifecycle, PlaceholderGuard},
|
||||
};
|
||||
use std::{
|
||||
borrow::Borrow,
|
||||
hash::Hash,
|
||||
net::{IpAddr, Ipv4Addr, Ipv6Addr},
|
||||
sync::Arc,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
pub struct Cache<K: Eq + Hash + CacheItemWeight, V: Clone + CacheItemWeight>(
|
||||
quick_cache::sync::Cache<K, V, CacheItemWeighter, ahash::RandomState>,
|
||||
);
|
||||
|
||||
pub struct CacheWithTtl<K: Eq + Hash + CacheItemWeight, V: Clone + CacheItemWeight>(
|
||||
quick_cache::sync::Cache<K, TtlEntry<V>, CacheItemWeighter, ahash::RandomState>,
|
||||
);
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct TtlEntry<V: Clone + CacheItemWeight> {
|
||||
value: V,
|
||||
expires: Instant,
|
||||
}
|
||||
|
||||
impl<K: Eq + Hash + CacheItemWeight, V: Clone + CacheItemWeight> Cache<K, V> {
|
||||
pub fn new(weight: u64, estimated_weight: u64) -> Self {
|
||||
Self::new_estimated(weight as usize / estimated_weight as usize, weight)
|
||||
}
|
||||
|
||||
pub fn new_estimated(estimated_items_capacity: usize, weight_capacity: u64) -> Self {
|
||||
Self(quick_cache::sync::Cache::with_options(
|
||||
cache_options(estimated_items_capacity, weight_capacity, None),
|
||||
CacheItemWeighter,
|
||||
ahash::RandomState::default(),
|
||||
DefaultLifecycle::default(),
|
||||
))
|
||||
}
|
||||
|
||||
pub fn new_single_shard(weight: u64, estimated_weight: u64) -> Self {
|
||||
Self(quick_cache::sync::Cache::with_options(
|
||||
cache_options(weight as usize / estimated_weight as usize, weight, Some(1)),
|
||||
CacheItemWeighter,
|
||||
ahash::RandomState::default(),
|
||||
DefaultLifecycle::default(),
|
||||
))
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn get<Q>(&self, key: &Q) -> Option<V>
|
||||
where
|
||||
Q: Hash + Equivalent<K> + ?Sized,
|
||||
{
|
||||
self.0.get(key)
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn peek<Q>(&self, key: &Q) -> Option<V>
|
||||
where
|
||||
Q: Hash + Equivalent<K> + ?Sized,
|
||||
{
|
||||
self.0.peek(key)
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub async fn get_value_or_guard_async<'a, Q>(
|
||||
&'a self,
|
||||
key: &Q,
|
||||
) -> Result<
|
||||
V,
|
||||
PlaceholderGuard<'a, K, V, CacheItemWeighter, ahash::RandomState, DefaultLifecycle<K, V>>,
|
||||
>
|
||||
where
|
||||
Q: Hash + Equivalent<K> + ToOwned<Owned = K> + ?Sized,
|
||||
{
|
||||
self.0.get_value_or_guard_async(key).await
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn insert(&self, key: K, value: V) {
|
||||
self.0.insert(key, value);
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn update(&self, key: K, value: V) {
|
||||
if let Err((key, value)) = self.0.replace(key, value, true) {
|
||||
self.0.insert(key, value);
|
||||
}
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn remove<Q>(&self, key: &Q) -> Option<V>
|
||||
where
|
||||
Q: Hash + Equivalent<K> + ?Sized,
|
||||
{
|
||||
self.0.remove(key).map(|(_, v)| v)
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn clear(&self) {
|
||||
self.0.clear();
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn inner(&self) -> &quick_cache::sync::Cache<K, V, CacheItemWeighter, ahash::RandomState> {
|
||||
&self.0
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn weight_capacity(&self) -> u64 {
|
||||
self.0.capacity()
|
||||
}
|
||||
}
|
||||
|
||||
impl<K: Eq + Hash + CacheItemWeight, V: Clone + CacheItemWeight> CacheWithTtl<K, V> {
|
||||
pub fn new(weight: u64, estimated_weight: u64) -> Self {
|
||||
Self::new_estimated(weight as usize / estimated_weight as usize, weight)
|
||||
}
|
||||
|
||||
pub fn new_estimated(estimated_items_capacity: usize, weight_capacity: u64) -> Self {
|
||||
Self(quick_cache::sync::Cache::with_options(
|
||||
cache_options(estimated_items_capacity, weight_capacity, None),
|
||||
CacheItemWeighter,
|
||||
ahash::RandomState::default(),
|
||||
DefaultLifecycle::default(),
|
||||
))
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn get<Q>(&self, key: &Q) -> Option<V>
|
||||
where
|
||||
Q: Hash + Equivalent<K> + ?Sized,
|
||||
{
|
||||
self.0.get(key).and_then(|v| {
|
||||
if v.expires > Instant::now() {
|
||||
Some(v.value)
|
||||
} else {
|
||||
self.0.remove(key);
|
||||
None
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub async fn get_value_or_guard_async<'a, Q>(
|
||||
&'a self,
|
||||
key: &Q,
|
||||
) -> Result<
|
||||
V,
|
||||
PlaceholderGuard<
|
||||
'a,
|
||||
K,
|
||||
TtlEntry<V>,
|
||||
CacheItemWeighter,
|
||||
ahash::RandomState,
|
||||
DefaultLifecycle<K, TtlEntry<V>>,
|
||||
>,
|
||||
>
|
||||
where
|
||||
Q: Hash + Equivalent<K> + ToOwned<Owned = K> + ?Sized,
|
||||
{
|
||||
match self.0.get_value_or_guard_async(key).await {
|
||||
Ok(value) => {
|
||||
if value.expires > Instant::now() {
|
||||
Ok(value.value)
|
||||
} else {
|
||||
self.0.remove(key);
|
||||
self.0.get_value_or_guard_async(key).await.map(|v| v.value)
|
||||
}
|
||||
}
|
||||
Err(err) => Err(err),
|
||||
}
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn insert(&self, key: K, value: V, expires: Duration) {
|
||||
self.0.insert(key, TtlEntry::new(value, expires));
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn insert_with_expiry(&self, key: K, value: V, expires: Instant) {
|
||||
self.0.insert(key, TtlEntry::with_expiry(value, expires));
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn remove<Q>(&self, key: &Q) -> Option<V>
|
||||
where
|
||||
Q: Hash + Equivalent<K> + ?Sized,
|
||||
{
|
||||
self.0.remove(key).map(|(_, v)| v.value)
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn retain(&self, f: impl Fn(&K) -> bool) {
|
||||
self.0.retain(|key, _| f(key));
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn clear(&self) {
|
||||
self.0.clear();
|
||||
}
|
||||
}
|
||||
|
||||
fn cache_options(
|
||||
estimated_items_capacity: usize,
|
||||
weight_capacity: u64,
|
||||
shards: Option<usize>,
|
||||
) -> Options {
|
||||
let mut builder = OptionsBuilder::new();
|
||||
builder
|
||||
.estimated_items_capacity(estimated_items_capacity.max(1))
|
||||
.weight_capacity(weight_capacity);
|
||||
if let Some(shards) = shards {
|
||||
builder.shards(shards.max(1));
|
||||
}
|
||||
builder.build().unwrap()
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct CacheItemWeighter;
|
||||
|
||||
impl<K: CacheItemWeight, V: CacheItemWeight> Weighter<K, V> for CacheItemWeighter {
|
||||
fn weight(&self, key: &K, val: &V) -> u64 {
|
||||
key.weight() + val.weight()
|
||||
}
|
||||
}
|
||||
|
||||
pub trait CacheItemWeight {
|
||||
fn weight(&self) -> u64;
|
||||
}
|
||||
|
||||
impl<T: Clone + CacheItemWeight> CacheItemWeight for TtlEntry<T> {
|
||||
fn weight(&self) -> u64 {
|
||||
self.value.weight() + std::mem::size_of::<Instant>() as u64
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Clone + CacheItemWeight> CacheItemWeight for Option<T> {
|
||||
fn weight(&self) -> u64 {
|
||||
match self {
|
||||
Some(v) => v.weight(),
|
||||
None => std::mem::size_of::<usize>() as u64,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: CacheItemWeight> CacheItemWeight for Arc<T> {
|
||||
fn weight(&self) -> u64 {
|
||||
self.as_ref().weight()
|
||||
}
|
||||
}
|
||||
|
||||
impl CacheItemWeight for u64 {
|
||||
fn weight(&self) -> u64 {
|
||||
std::mem::size_of::<u64>() as u64
|
||||
}
|
||||
}
|
||||
|
||||
impl CacheItemWeight for String {
|
||||
fn weight(&self) -> u64 {
|
||||
self.len() as u64 + std::mem::size_of::<String>() as u64
|
||||
}
|
||||
}
|
||||
|
||||
impl CacheItemWeight for Box<str> {
|
||||
fn weight(&self) -> u64 {
|
||||
self.len() as u64 + std::mem::size_of::<Box<str>>() as u64
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: CacheItemWeight> CacheItemWeight for Box<[T]> {
|
||||
fn weight(&self) -> u64 {
|
||||
std::mem::size_of::<Box<[T]>>() as u64 + self.iter().map(|item| item.weight()).sum::<u64>()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: CacheItemWeight> CacheItemWeight for Arc<[T]> {
|
||||
fn weight(&self) -> u64 {
|
||||
std::mem::size_of::<Arc<[T]>>() as u64 + self.iter().map(|item| item.weight()).sum::<u64>()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: CacheItemWeight> CacheItemWeight for RecordSet<T> {
|
||||
fn weight(&self) -> u64 {
|
||||
self.rrset.weight() + std::mem::size_of::<DnssecStatus>() as u64
|
||||
}
|
||||
}
|
||||
|
||||
impl CacheItemWeight for u32 {
|
||||
fn weight(&self) -> u64 {
|
||||
std::mem::size_of::<u32>() as u64
|
||||
}
|
||||
}
|
||||
|
||||
impl CacheItemWeight for IpAddr {
|
||||
fn weight(&self) -> u64 {
|
||||
std::mem::size_of::<IpAddr>() as u64
|
||||
}
|
||||
}
|
||||
|
||||
impl CacheItemWeight for Ipv4Addr {
|
||||
fn weight(&self) -> u64 {
|
||||
std::mem::size_of::<Ipv4Addr>() as u64
|
||||
}
|
||||
}
|
||||
|
||||
impl CacheItemWeight for Ipv6Addr {
|
||||
fn weight(&self) -> u64 {
|
||||
std::mem::size_of::<Ipv6Addr>() as u64
|
||||
}
|
||||
}
|
||||
|
||||
impl CacheItemWeight for MX {
|
||||
fn weight(&self) -> u64 {
|
||||
self.exchanges
|
||||
.iter()
|
||||
.map(|e| e.len() as u64 + std::mem::size_of::<Box<str>>() as u64)
|
||||
.sum::<u64>()
|
||||
+ std::mem::size_of::<MX>() as u64
|
||||
}
|
||||
}
|
||||
|
||||
impl CacheItemWeight for Txt {
|
||||
fn weight(&self) -> u64 {
|
||||
std::mem::size_of::<Txt>() as u64
|
||||
}
|
||||
}
|
||||
|
||||
impl CacheItemWeight for bool {
|
||||
fn weight(&self) -> u64 {
|
||||
std::mem::size_of::<bool>() as u64
|
||||
}
|
||||
}
|
||||
|
||||
impl CacheItemWeight for ArcStr {
|
||||
fn weight(&self) -> u64 {
|
||||
self.len() as u64 + std::mem::size_of::<ArcStr>() as u64
|
||||
}
|
||||
}
|
||||
|
||||
impl CacheItemWeight for () {
|
||||
fn weight(&self) -> u64 {
|
||||
0
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Clone + CacheItemWeight> TtlEntry<T> {
|
||||
pub fn new(value: T, expires: Duration) -> Self {
|
||||
Self {
|
||||
value,
|
||||
expires: Instant::now() + expires,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_expiry(value: T, expires: Instant) -> Self {
|
||||
Self { value, expires }
|
||||
}
|
||||
}
|
||||
|
||||
impl<K: Eq + Hash + CacheItemWeight, V: Clone + CacheItemWeight> ResolverCache<K, V>
|
||||
for CacheWithTtl<K, V>
|
||||
{
|
||||
fn get<Q>(&self, key: &Q) -> Option<V>
|
||||
where
|
||||
K: Borrow<Q>,
|
||||
Q: Hash + Eq + ?Sized,
|
||||
{
|
||||
CacheWithTtl::get(self, key)
|
||||
}
|
||||
|
||||
fn remove<Q>(&self, key: &Q) -> Option<V>
|
||||
where
|
||||
K: Borrow<Q>,
|
||||
Q: Hash + Eq + ?Sized,
|
||||
{
|
||||
CacheWithTtl::remove(self, key)
|
||||
}
|
||||
|
||||
fn insert(&self, key: K, value: V, expires: Instant) {
|
||||
self.0.insert(key, TtlEntry::with_expiry(value, expires));
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn single_shard_retains_large_entry() {
|
||||
let capacity = 10_000_000u64;
|
||||
let cache = Cache::<u32, String>::new_single_shard(capacity, 1000);
|
||||
assert_eq!(cache.inner().num_shards(), 1);
|
||||
|
||||
let value = "x".repeat(9_000_000);
|
||||
cache.insert(0, value.clone());
|
||||
assert_eq!(cache.get(&0), Some(value));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sharded_cache_drops_entry_larger_than_a_shard() {
|
||||
let capacity = 10_000_000u64;
|
||||
let cache = Cache::<u32, String>::new_estimated(10_000, capacity);
|
||||
|
||||
let value = "x".repeat((capacity / 2) as usize);
|
||||
cache.insert(0, value);
|
||||
|
||||
if cache.inner().num_shards() > 1 {
|
||||
assert_eq!(cache.get(&0), None);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,156 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use std::{borrow::Cow, ops::Range};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ChainedBytes<'x> {
|
||||
first: &'x [u8],
|
||||
last: &'x [u8],
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum SliceRange<'x> {
|
||||
Single(&'x [u8]),
|
||||
Split(&'x [u8], &'x [u8]),
|
||||
None,
|
||||
}
|
||||
|
||||
impl<'x> ChainedBytes<'x> {
|
||||
pub fn new(first: &'x [u8]) -> Self {
|
||||
Self { first, last: &[] }
|
||||
}
|
||||
|
||||
pub fn append(&mut self, bytes: &'x [u8]) {
|
||||
self.last = bytes;
|
||||
}
|
||||
|
||||
pub fn with_last(mut self, bytes: &'x [u8]) -> Self {
|
||||
self.last = bytes;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn get(&self, index: Range<usize>) -> Option<Cow<'x, [u8]>> {
|
||||
let start = index.start;
|
||||
let end = index.end;
|
||||
|
||||
if let Some(bytes) = self.first.get(start..end) {
|
||||
Some(Cow::Borrowed(bytes))
|
||||
} else if start >= self.first.len() {
|
||||
self.last
|
||||
.get(start - self.first.len()..end - self.first.len())
|
||||
.map(Cow::Borrowed)
|
||||
} else if let (Some(first), Some(last)) = (
|
||||
self.first.get(start..),
|
||||
self.last.get(..end - self.first.len()),
|
||||
) {
|
||||
let mut vec = vec![0u8; first.len() + last.len()];
|
||||
vec[..first.len()].copy_from_slice(first);
|
||||
vec[first.len()..].copy_from_slice(last);
|
||||
Some(Cow::Owned(vec))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_slice_range(&self, index: Range<usize>) -> SliceRange<'x> {
|
||||
let start = index.start;
|
||||
let end = index.end;
|
||||
|
||||
if let Some(bytes) = self.first.get(start..end) {
|
||||
SliceRange::Single(bytes)
|
||||
} else if start >= self.first.len() {
|
||||
self.last
|
||||
.get(start - self.first.len()..end - self.first.len())
|
||||
.map(SliceRange::Single)
|
||||
.unwrap_or(SliceRange::None)
|
||||
} else if let (Some(first), Some(last)) = (
|
||||
self.first.get(start..),
|
||||
self.last.get(..end - self.first.len()),
|
||||
) {
|
||||
SliceRange::Split(first, last)
|
||||
} else {
|
||||
SliceRange::None
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_full_range(&self) -> SliceRange<'x> {
|
||||
if self.last.is_empty() {
|
||||
SliceRange::Single(self.first)
|
||||
} else {
|
||||
SliceRange::Split(self.first, self.last)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn to_bytes(&self) -> Vec<u8> {
|
||||
let mut bytes = vec![0u8; self.first.len() + self.last.len()];
|
||||
bytes[..self.first.len()].copy_from_slice(self.first);
|
||||
bytes[self.first.len()..].copy_from_slice(self.last);
|
||||
bytes
|
||||
}
|
||||
|
||||
pub fn len(&self) -> usize {
|
||||
self.first.len() + self.last.len()
|
||||
}
|
||||
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.len() == 0
|
||||
}
|
||||
}
|
||||
|
||||
impl<'x> SliceRange<'x> {
|
||||
pub fn len(&self) -> usize {
|
||||
match self {
|
||||
SliceRange::Single(bytes) => bytes.len(),
|
||||
SliceRange::Split(first, last) => first.len() + last.len(),
|
||||
SliceRange::None => 0,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn try_into_bytes(self) -> Option<Cow<'x, [u8]>> {
|
||||
match self {
|
||||
SliceRange::Single(bytes) => Some(Cow::Borrowed(bytes)),
|
||||
SliceRange::Split(first, last) => {
|
||||
let mut vec = vec![0u8; first.len() + last.len()];
|
||||
vec[..first.len()].copy_from_slice(first);
|
||||
vec[first.len()..].copy_from_slice(last);
|
||||
Some(Cow::Owned(vec))
|
||||
}
|
||||
SliceRange::None => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.len() == 0
|
||||
}
|
||||
|
||||
fn into_pairs(self) -> (&'x [u8], &'x [u8]) {
|
||||
match self {
|
||||
SliceRange::Single(bytes) => (bytes, &[][..]),
|
||||
SliceRange::Split(first, last) => (first, last),
|
||||
SliceRange::None => (&[][..], &[][..]),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_none(&self) -> bool {
|
||||
matches!(self, SliceRange::None)
|
||||
}
|
||||
|
||||
pub fn is_some(&self) -> bool {
|
||||
!self.is_none()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'x> IntoIterator for SliceRange<'x> {
|
||||
type Item = &'x u8;
|
||||
type IntoIter = std::iter::Chain<std::slice::Iter<'x, u8>, std::slice::Iter<'x, u8>>;
|
||||
|
||||
fn into_iter(self) -> Self::IntoIter {
|
||||
let (first, last) = self.into_pairs();
|
||||
|
||||
first.iter().chain(last.iter())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,319 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use nohash_hasher::IsEnabled;
|
||||
use std::{
|
||||
collections::{BTreeMap, HashMap, HashSet},
|
||||
fmt::Debug,
|
||||
hash::Hash,
|
||||
str::FromStr,
|
||||
};
|
||||
|
||||
// A hash that can cheekily store small inputs directly without hashing them.
|
||||
#[derive(
|
||||
Copy, Clone, PartialEq, Eq, PartialOrd, Ord, rkyv::Serialize, rkyv::Deserialize, rkyv::Archive,
|
||||
)]
|
||||
#[repr(transparent)]
|
||||
pub struct CheekyHash([u8; HASH_SIZE]);
|
||||
|
||||
const HASH_SIZE: usize = std::mem::size_of::<u64>() * 2;
|
||||
const HASH_PAYLOAD: usize = HASH_SIZE - 1;
|
||||
|
||||
pub type CheekyHashSet = HashSet<CheekyHash, nohash_hasher::BuildNoHashHasher<CheekyHash>>;
|
||||
pub type CheekyHashMap<V> = HashMap<CheekyHash, V, nohash_hasher::BuildNoHashHasher<CheekyHash>>;
|
||||
pub type CheekyBTreeMap<V> = BTreeMap<CheekyHash, V>;
|
||||
|
||||
impl CheekyHash {
|
||||
pub const HASH_SIZE: usize = HASH_SIZE;
|
||||
pub const NULL: CheekyHash = CheekyHash([0u8; HASH_SIZE]);
|
||||
pub const FULL: CheekyHash = CheekyHash([u8::MAX; HASH_SIZE]);
|
||||
|
||||
pub fn new(bytes: impl AsRef<[u8]>) -> Self {
|
||||
let mut hash = [0u8; HASH_SIZE];
|
||||
let bytes = bytes.as_ref();
|
||||
|
||||
if bytes.len() <= HASH_PAYLOAD {
|
||||
hash[0] = bytes.len() as u8;
|
||||
hash[1..1 + bytes.len()].copy_from_slice(bytes);
|
||||
} else {
|
||||
let h1 = xxhash_rust::xxh3::xxh3_64(bytes).to_be_bytes();
|
||||
let h2 = farmhash::fingerprint64(bytes).to_be_bytes();
|
||||
hash[0] = bytes.len().min(u8::MAX as usize) as u8;
|
||||
hash[1..1 + std::mem::size_of::<u64>()].copy_from_slice(&h1);
|
||||
hash[1 + std::mem::size_of::<u64>()..]
|
||||
.copy_from_slice(&h2[..std::mem::size_of::<u64>() - 1]);
|
||||
}
|
||||
|
||||
CheekyHash(hash)
|
||||
}
|
||||
|
||||
pub fn deserialize(bytes: &[u8]) -> Option<Self> {
|
||||
let len = *bytes.first()?;
|
||||
let mut hash = [0u8; HASH_SIZE];
|
||||
let hash_len = 1 + (len as usize).min(HASH_PAYLOAD);
|
||||
|
||||
hash[0] = len;
|
||||
hash[1..hash_len].copy_from_slice(bytes.get(1..hash_len)?);
|
||||
Some(CheekyHash(hash))
|
||||
}
|
||||
|
||||
#[allow(clippy::len_without_is_empty)]
|
||||
#[inline(always)]
|
||||
pub fn len(&self) -> usize {
|
||||
(self.0[0] as usize).min(HASH_PAYLOAD) + 1
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn as_bytes(&self) -> &[u8] {
|
||||
&self.0[..self.len()]
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn as_raw_bytes(&self) -> &[u8; HASH_SIZE] {
|
||||
&self.0
|
||||
}
|
||||
|
||||
pub fn into_inner(self) -> [u8; HASH_SIZE] {
|
||||
self.0
|
||||
}
|
||||
|
||||
pub fn payload(&self) -> &[u8] {
|
||||
let len = self.0[0] as usize;
|
||||
if len <= HASH_PAYLOAD {
|
||||
&self.0[1..1 + len]
|
||||
} else {
|
||||
&self.0[1..]
|
||||
}
|
||||
}
|
||||
|
||||
pub fn payload_len(&self) -> u8 {
|
||||
self.0[0]
|
||||
}
|
||||
|
||||
fn as_u128(&self) -> u128 {
|
||||
u128::from_be_bytes(self.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<[u8]> for CheekyHash {
|
||||
fn as_ref(&self) -> &[u8] {
|
||||
self.as_bytes()
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for CheekyHash {
|
||||
type Err = std::num::ParseIntError;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
u128::from_str_radix(s, 16).map(|n| CheekyHash(n.to_be_bytes()))
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for CheekyHash {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{:032x}", self.as_u128())
|
||||
}
|
||||
}
|
||||
|
||||
impl Hash for CheekyHash {
|
||||
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
|
||||
let len = self.0[0] as usize;
|
||||
if len <= HASH_PAYLOAD {
|
||||
state.write_u64(xxhash_rust::xxh3::xxh3_64(&self.0[1..1 + len]));
|
||||
} else {
|
||||
state.write_u64(u64::from_be_bytes(
|
||||
self.0[1..1 + std::mem::size_of::<u64>()]
|
||||
.try_into()
|
||||
.unwrap(),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl IsEnabled for CheekyHash {}
|
||||
|
||||
impl ArchivedCheekyHash {
|
||||
#[inline(always)]
|
||||
pub fn as_raw_bytes(&self) -> &[u8; HASH_SIZE] {
|
||||
&self.0
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn as_bytes(&self) -> &[u8] {
|
||||
let len = self.0[0] as usize;
|
||||
&self.0[..1 + len.min(HASH_PAYLOAD)]
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn to_native(&self) -> CheekyHash {
|
||||
CheekyHash(self.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl Debug for CheekyHash {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
let len = self.payload_len();
|
||||
let payload = self.payload();
|
||||
let payload_str = if len <= HASH_PAYLOAD as u8 {
|
||||
std::str::from_utf8(payload).unwrap_or("<non-utf8>")
|
||||
} else {
|
||||
"<hashed data>"
|
||||
};
|
||||
|
||||
f.debug_struct("CheekyHash")
|
||||
.field("length", &len)
|
||||
.field("bytes", &payload_str)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_cheeky_hash_all() {
|
||||
// Test 1: Empty input
|
||||
let hash_empty = CheekyHash::new([]);
|
||||
assert_eq!(
|
||||
hash_empty.as_bytes()[0],
|
||||
0,
|
||||
"Empty input should have length 0"
|
||||
);
|
||||
assert_eq!(
|
||||
hash_empty.as_bytes().len(),
|
||||
1,
|
||||
"Empty input should only have length byte"
|
||||
);
|
||||
|
||||
// Test 2: Single byte input
|
||||
let hash_single = CheekyHash::new([42]);
|
||||
assert_eq!(
|
||||
hash_single.as_bytes()[0],
|
||||
1,
|
||||
"Single byte should have length 1"
|
||||
);
|
||||
assert_eq!(
|
||||
hash_single.as_bytes()[1],
|
||||
42,
|
||||
"Single byte value should be preserved"
|
||||
);
|
||||
assert_eq!(hash_single.as_bytes().len(), 2);
|
||||
|
||||
// Test 3: Small input (less than HASH_LEN)
|
||||
let small_data = b"hello";
|
||||
let hash_small = CheekyHash::new(small_data);
|
||||
assert_eq!(hash_small.as_bytes()[0], 5, "Length should be 5");
|
||||
assert_eq!(
|
||||
&hash_small.as_bytes()[1..6],
|
||||
small_data,
|
||||
"Small data should be stored directly"
|
||||
);
|
||||
assert_eq!(hash_small.as_bytes().len(), 6);
|
||||
|
||||
// Test 4: Input exactly at HASH_PAYLOAD boundary
|
||||
let boundary_data = vec![1u8; HASH_PAYLOAD - 1];
|
||||
let hash_boundary = CheekyHash::new(&boundary_data);
|
||||
assert_eq!(
|
||||
hash_boundary.as_bytes()[0],
|
||||
(HASH_PAYLOAD - 1) as u8,
|
||||
"Length should be HASH_LEN"
|
||||
);
|
||||
assert_eq!(
|
||||
&hash_boundary.as_bytes()[1..],
|
||||
&boundary_data[..],
|
||||
"Boundary data should be stored directly"
|
||||
);
|
||||
|
||||
// Test 5: Large input (greater than HASH_LEN) - uses hashing
|
||||
let large_data = vec![7u8; HASH_SIZE];
|
||||
let hash_large = CheekyHash::new(&large_data);
|
||||
assert_eq!(
|
||||
hash_large.as_bytes()[0],
|
||||
HASH_SIZE as u8,
|
||||
"Large data should have length byte set to HASH_LEN"
|
||||
);
|
||||
assert_eq!(
|
||||
hash_large.as_bytes().len(),
|
||||
HASH_SIZE,
|
||||
"Large data hash should be full length"
|
||||
);
|
||||
// Verify it's actually hashed (not raw data)
|
||||
assert_ne!(
|
||||
&hash_large.as_bytes()[1..],
|
||||
&large_data[..HASH_PAYLOAD],
|
||||
"Large data should be hashed, not stored directly"
|
||||
);
|
||||
|
||||
// Test 6: AsRef<[u8]> trait
|
||||
let hash = CheekyHash::new(b"test");
|
||||
let bytes_ref: &[u8] = hash.as_ref();
|
||||
assert_eq!(bytes_ref, hash.as_bytes(), "AsRef should match as_bytes");
|
||||
|
||||
// Test 7: Copy, Clone, PartialEq traits
|
||||
let hash1 = CheekyHash::new(b"identical");
|
||||
let hash2 = hash1; // Copy
|
||||
assert_eq!(hash1, hash2, "Copied hashes should be equal");
|
||||
|
||||
// Test 8: Different inputs produce different hashes
|
||||
let hash_a = CheekyHash::new(b"abc");
|
||||
let hash_b = CheekyHash::new(b"def");
|
||||
assert_ne!(
|
||||
hash_a, hash_b,
|
||||
"Different inputs should produce different hashes"
|
||||
);
|
||||
|
||||
// Test 9: Same input produces same hash (deterministic)
|
||||
let hash_x1 = CheekyHash::new(b"deterministic");
|
||||
let hash_x2 = CheekyHash::new(b"deterministic");
|
||||
assert_eq!(
|
||||
hash_x1, hash_x2,
|
||||
"Same input should produce identical hashes"
|
||||
);
|
||||
|
||||
// Test 10: Large inputs with different content produce different hashes
|
||||
let large1 = vec![1u8; 100];
|
||||
let large2 = vec![2u8; 100];
|
||||
let hash_large1 = CheekyHash::new(&large1);
|
||||
let hash_large2 = CheekyHash::new(&large2);
|
||||
assert_ne!(
|
||||
hash_large1, hash_large2,
|
||||
"Different large inputs should produce different hashes"
|
||||
);
|
||||
|
||||
// Test 11: Hash trait (can be used in HashMap/HashSet)
|
||||
use std::collections::HashMap;
|
||||
let mut map = HashMap::new();
|
||||
let key = CheekyHash::new(b"key");
|
||||
map.insert(key, "value");
|
||||
assert_eq!(
|
||||
map.get(&key),
|
||||
Some(&"value"),
|
||||
"CheekyHash should work as HashMap key"
|
||||
);
|
||||
|
||||
// Test 12: Debug trait
|
||||
let hash = CheekyHash::new(b"debug");
|
||||
let debug_str = format!("{:?}", hash);
|
||||
assert!(
|
||||
debug_str.contains("CheekyHash"),
|
||||
"Debug output should contain type name"
|
||||
);
|
||||
|
||||
// Test 13: CheekyHashSet and CheekyHashMap
|
||||
let mut cheeky_set: CheekyHashSet = CheekyHashSet::default();
|
||||
cheeky_set.insert(CheekyHash::new(b"set_item"));
|
||||
assert!(cheeky_set.contains(&CheekyHash::new(b"set_item")));
|
||||
let mut cheeky_map: CheekyHashMap<&str> = CheekyHashMap::default();
|
||||
cheeky_map.insert(CheekyHash::new(b"map_key"), "map_value");
|
||||
assert_eq!(
|
||||
cheeky_map.get(&CheekyHash::new(b"map_key")),
|
||||
Some(&"map_value")
|
||||
);
|
||||
|
||||
println!("All CheekyHash tests passed!");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,221 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use super::leb128::{Leb128Iterator, Leb128Writer};
|
||||
use std::{io::Write, slice::Iter};
|
||||
|
||||
pub static BASE32_ALPHABET: &[u8] = b"abcdefghijklmnopqrstuvwxyz792013";
|
||||
pub static BASE32_INVERSE: [u8; 256] = [
|
||||
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
|
||||
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
|
||||
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 29, 30, 28, 31, 255, 255, 255, 26, 255, 27,
|
||||
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 10, 11,
|
||||
12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 255, 255, 255, 255, 255, 255, 0, 1, 2,
|
||||
3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 255, 255,
|
||||
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
|
||||
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
|
||||
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
|
||||
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
|
||||
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
|
||||
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
|
||||
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
|
||||
];
|
||||
|
||||
pub struct Base32Writer {
|
||||
last_byte: u8,
|
||||
pos: usize,
|
||||
result: String,
|
||||
}
|
||||
|
||||
impl Base32Writer {
|
||||
pub fn from_bytes(bytes: impl AsRef<[u8]>) -> Self {
|
||||
let bytes = bytes.as_ref();
|
||||
let mut writer = Base32Writer::with_capacity(bytes.len());
|
||||
writer.write_all(bytes).unwrap();
|
||||
writer
|
||||
}
|
||||
|
||||
pub fn with_capacity(capacity: usize) -> Self {
|
||||
Self::with_raw_capacity(capacity.div_ceil(4) * 5)
|
||||
}
|
||||
|
||||
pub fn with_raw_capacity(capacity: usize) -> Self {
|
||||
Base32Writer {
|
||||
result: String::with_capacity(capacity),
|
||||
last_byte: 0,
|
||||
pos: 0,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn push_char(&mut self, ch: char) {
|
||||
self.result.push(ch);
|
||||
}
|
||||
|
||||
pub fn push_string(&mut self, string: &str) {
|
||||
self.result.push_str(string);
|
||||
}
|
||||
|
||||
fn push_byte(&mut self, byte: u8, is_remainder: bool) {
|
||||
let (ch1, ch2) = match self.pos % 5 {
|
||||
0 => ((byte & 0xF8) >> 3, u8::MAX),
|
||||
1 => (
|
||||
(((self.last_byte & 0x07) << 2) | ((byte & 0xC0) >> 6)),
|
||||
((byte & 0x3E) >> 1),
|
||||
),
|
||||
2 => (
|
||||
(((self.last_byte & 0x01) << 4) | ((byte & 0xF0) >> 4)),
|
||||
u8::MAX,
|
||||
),
|
||||
3 => (
|
||||
(((self.last_byte & 0x0F) << 1) | (byte >> 7)),
|
||||
((byte & 0x7C) >> 2),
|
||||
),
|
||||
4 => (
|
||||
(((self.last_byte & 0x03) << 3) | ((byte & 0xE0) >> 5)),
|
||||
(byte & 0x1F),
|
||||
),
|
||||
_ => unreachable!(),
|
||||
};
|
||||
|
||||
self.result.push(char::from(BASE32_ALPHABET[ch1 as usize]));
|
||||
if !is_remainder {
|
||||
if ch2 != u8::MAX {
|
||||
self.result.push(char::from(BASE32_ALPHABET[ch2 as usize]));
|
||||
}
|
||||
self.last_byte = byte;
|
||||
self.pos += 1;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn finalize(mut self) -> String {
|
||||
if !self.pos.is_multiple_of(5) {
|
||||
self.push_byte(0, true);
|
||||
}
|
||||
|
||||
self.result
|
||||
}
|
||||
}
|
||||
|
||||
impl std::io::Write for Base32Writer {
|
||||
fn write(&mut self, bytes: &[u8]) -> std::io::Result<usize> {
|
||||
let start_pos = self.pos;
|
||||
|
||||
for &byte in bytes {
|
||||
self.push_byte(byte, false);
|
||||
}
|
||||
|
||||
Ok(self.pos - start_pos)
|
||||
}
|
||||
|
||||
fn flush(&mut self) -> std::io::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Base32Reader<'x> {
|
||||
bytes: Iter<'x, u8>,
|
||||
last_byte: u8,
|
||||
pos: usize,
|
||||
}
|
||||
|
||||
impl<'x> Base32Reader<'x> {
|
||||
pub fn new(bytes: &'x [u8]) -> Self {
|
||||
Base32Reader {
|
||||
bytes: bytes.iter(),
|
||||
pos: 0,
|
||||
last_byte: 0,
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::should_implement_trait)]
|
||||
pub fn from_iter(bytes: Iter<'x, u8>) -> Self {
|
||||
Base32Reader {
|
||||
bytes,
|
||||
pos: 0,
|
||||
last_byte: 0,
|
||||
}
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
fn map_byte(&mut self) -> Option<u8> {
|
||||
match self.bytes.next() {
|
||||
Some(&byte) => match BASE32_INVERSE[byte as usize] {
|
||||
byte if byte != u8::MAX => {
|
||||
self.last_byte = byte;
|
||||
Some(byte)
|
||||
}
|
||||
_ => None,
|
||||
},
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Iterator for Base32Reader<'_> {
|
||||
type Item = u8;
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
let pos = self.pos % 5;
|
||||
let last_byte = self.last_byte;
|
||||
let byte = self.map_byte()?;
|
||||
self.pos += 1;
|
||||
|
||||
match pos {
|
||||
0 => ((byte << 3) | (self.map_byte().unwrap_or(0) >> 2)).into(),
|
||||
1 => ((last_byte << 6) | (byte << 1) | (self.map_byte().unwrap_or(0) >> 4)).into(),
|
||||
2 => ((last_byte << 4) | (byte >> 1)).into(),
|
||||
3 => ((last_byte << 7) | (byte << 2) | (self.map_byte().unwrap_or(0) >> 3)).into(),
|
||||
4 => ((last_byte << 5) | byte).into(),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Leb128Iterator<u8> for Base32Reader<'_> {}
|
||||
impl Leb128Writer for Base32Writer {}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::io::Write;
|
||||
|
||||
use crate::codec::base32_custom::{Base32Reader, Base32Writer};
|
||||
|
||||
#[test]
|
||||
fn base32_roundtrip() {
|
||||
let mut bytes = Vec::with_capacity(100);
|
||||
for byte in 0..100 {
|
||||
bytes.push((100 - byte) as u8);
|
||||
let mut writer = Base32Writer::with_capacity(10);
|
||||
writer.write_all(&bytes).unwrap();
|
||||
let result = writer.finalize();
|
||||
|
||||
let mut bytes_result = Vec::new();
|
||||
for byte in Base32Reader::new(result.as_bytes()) {
|
||||
bytes_result.push(byte);
|
||||
}
|
||||
|
||||
assert_eq!(bytes, bytes_result);
|
||||
}
|
||||
|
||||
for bytes in [
|
||||
vec![0],
|
||||
vec![32, 43, 55, 99, 43, 55],
|
||||
vec![84, 4, 43, 77, 62, 55, 92],
|
||||
vec![84, 4, 43, 77, 62, 55, 92],
|
||||
] {
|
||||
let mut writer = Base32Writer::with_capacity(10);
|
||||
writer.write_all(&bytes).unwrap();
|
||||
let result = writer.finalize();
|
||||
|
||||
let mut bytes_result = Vec::new();
|
||||
for byte in Base32Reader::new(result.as_bytes()) {
|
||||
bytes_result.push(byte);
|
||||
}
|
||||
|
||||
assert_eq!(bytes, bytes_result);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,189 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
#![allow(dead_code)]
|
||||
|
||||
use std::{borrow::Borrow, io::Write};
|
||||
|
||||
pub trait Leb128_ {
|
||||
fn to_leb128_writer(self, out: &mut impl Write) -> std::io::Result<usize>;
|
||||
fn to_leb128_bytes(self, out: &mut Vec<u8>);
|
||||
fn from_leb128_bytes_pos(slice: &[u8]) -> Option<(Self, usize)>
|
||||
where
|
||||
Self: std::marker::Sized;
|
||||
fn from_leb128_bytes(slice: &[u8]) -> Option<Self>
|
||||
where
|
||||
Self: std::marker::Sized;
|
||||
fn from_leb128_it<T, I>(it: T) -> Option<Self>
|
||||
where
|
||||
Self: std::marker::Sized,
|
||||
T: Iterator<Item = I>,
|
||||
I: Borrow<u8>;
|
||||
}
|
||||
|
||||
pub trait Leb128Vec<T: Leb128_> {
|
||||
fn push_leb128(&mut self, value: T);
|
||||
}
|
||||
|
||||
pub trait Leb128Writer: Write + Sized {
|
||||
#[inline(always)]
|
||||
fn write_leb128<T: Leb128_>(&mut self, value: T) -> std::io::Result<usize> {
|
||||
T::to_leb128_writer(value, self)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Leb128_> Leb128Vec<T> for Vec<u8> {
|
||||
#[inline(always)]
|
||||
fn push_leb128(&mut self, value: T) {
|
||||
T::to_leb128_bytes(value, self);
|
||||
}
|
||||
}
|
||||
|
||||
pub trait Leb128Iterator<I>: Iterator<Item = I>
|
||||
where
|
||||
I: Borrow<u8>,
|
||||
{
|
||||
#[inline(always)]
|
||||
fn next_leb128<T: Leb128_>(&mut self) -> Option<T> {
|
||||
T::from_leb128_it(self)
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
fn skip_leb128(&mut self) -> Option<()> {
|
||||
for byte in self {
|
||||
if (byte.borrow() & 0x80) == 0 {
|
||||
return Some(());
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub trait Leb128Reader: AsRef<[u8]> {
|
||||
#[inline(always)]
|
||||
fn read_leb128<T: Leb128_>(&self) -> Option<(T, usize)> {
|
||||
T::from_leb128_bytes_pos(self.as_ref())
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
fn skip_leb128(&self) -> Option<usize> {
|
||||
for (pos, byte) in self.as_ref().iter().enumerate() {
|
||||
if (byte & 0x80) == 0 {
|
||||
return (pos + 1).into();
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
impl Leb128Reader for &[u8] {}
|
||||
impl Leb128Reader for Vec<u8> {}
|
||||
impl Leb128Reader for Box<[u8]> {}
|
||||
impl<'x> Leb128Iterator<&'x u8> for std::slice::Iter<'x, u8> {}
|
||||
|
||||
// Based on leb128.rs from rustc
|
||||
macro_rules! impl_unsigned_leb128 {
|
||||
($int_ty:ident, $shifts:expr) => {
|
||||
impl Leb128_ for $int_ty {
|
||||
#[inline(always)]
|
||||
fn to_leb128_writer(self, out: &mut impl Write) -> std::io::Result<usize> {
|
||||
let mut value = self;
|
||||
let mut bytes_written = 0;
|
||||
loop {
|
||||
if value < 0x80 {
|
||||
bytes_written += out.write(&[value as u8])?;
|
||||
break;
|
||||
} else {
|
||||
bytes_written += out.write(&[((value & 0x7f) | 0x80) as u8])?;
|
||||
value >>= 7;
|
||||
}
|
||||
}
|
||||
Ok(bytes_written)
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
fn to_leb128_bytes(self, out: &mut Vec<u8>) {
|
||||
let mut value = self;
|
||||
loop {
|
||||
if value < 0x80 {
|
||||
out.push(value as u8);
|
||||
break;
|
||||
} else {
|
||||
out.push(((value & 0x7f) | 0x80) as u8);
|
||||
value >>= 7;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
fn from_leb128_bytes_pos(slice: &[u8]) -> Option<($int_ty, usize)> {
|
||||
let mut result = 0;
|
||||
|
||||
for (shift, (pos, &byte)) in $shifts.into_iter().zip(slice.iter().enumerate()) {
|
||||
if (byte & 0x80) == 0 {
|
||||
result |= (byte as $int_ty) << shift;
|
||||
return Some((result, pos + 1));
|
||||
} else {
|
||||
result |= ((byte & 0x7F) as $int_ty) << shift;
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
fn from_leb128_bytes(slice: &[u8]) -> Option<$int_ty> {
|
||||
let mut result = 0;
|
||||
|
||||
for (shift, &byte) in $shifts.into_iter().zip(slice.iter()) {
|
||||
if (byte & 0x80) == 0 {
|
||||
result |= (byte as $int_ty) << shift;
|
||||
return Some(result);
|
||||
} else {
|
||||
result |= ((byte & 0x7F) as $int_ty) << shift;
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
fn from_leb128_it<T, I>(it: T) -> Option<$int_ty>
|
||||
where
|
||||
T: Iterator<Item = I>,
|
||||
I: Borrow<u8>,
|
||||
{
|
||||
let mut result = 0;
|
||||
|
||||
for (shift, byte_) in $shifts.into_iter().zip(it) {
|
||||
let byte = byte_.borrow();
|
||||
|
||||
if (byte & 0x80) == 0 {
|
||||
result |= (*byte as $int_ty) << shift;
|
||||
return Some(result);
|
||||
} else {
|
||||
result |= ((byte & 0x7F) as $int_ty) << shift;
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
impl_unsigned_leb128!(u8, [0]);
|
||||
impl_unsigned_leb128!(u16, [0, 7, 14]);
|
||||
impl_unsigned_leb128!(u32, [0, 7, 14, 21, 28]);
|
||||
impl_unsigned_leb128!(u64, [0, 7, 14, 21, 28, 35, 42, 49, 56, 63]);
|
||||
impl_unsigned_leb128!(usize, [0, 7, 14, 21, 28, 35, 42, 49, 56, 63]);
|
||||
|
||||
impl Leb128Writer for Vec<u8> {
|
||||
#[inline(always)]
|
||||
fn write_leb128<T: Leb128_>(&mut self, value: T) -> std::io::Result<usize> {
|
||||
T::to_leb128_writer(value, self)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
pub mod base32_custom;
|
||||
pub mod leb128;
|
||||
@@ -0,0 +1,138 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use chrono::{Datelike, Local, TimeDelta, TimeZone, Timelike};
|
||||
use std::{str::FromStr, time::Duration};
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum SimpleCron {
|
||||
Day { hour: u32, minute: u32 },
|
||||
Week { day: u32, hour: u32, minute: u32 },
|
||||
Hour { minute: u32 },
|
||||
}
|
||||
|
||||
impl SimpleCron {
|
||||
pub fn time_to_next(&self) -> Duration {
|
||||
let now = Local::now();
|
||||
let next = match self {
|
||||
SimpleCron::Day { hour, minute } => {
|
||||
let next = Local
|
||||
.with_ymd_and_hms(now.year(), now.month(), now.day(), *hour, *minute, 0)
|
||||
.earliest()
|
||||
.unwrap_or_else(|| now - TimeDelta::try_seconds(1).unwrap_or_default());
|
||||
if next <= now {
|
||||
next + TimeDelta::try_days(1).unwrap_or_default()
|
||||
} else {
|
||||
next
|
||||
}
|
||||
}
|
||||
SimpleCron::Week { day, hour, minute } => {
|
||||
let next = Local
|
||||
.with_ymd_and_hms(now.year(), now.month(), now.day(), *hour, *minute, 0)
|
||||
.earliest()
|
||||
.unwrap_or_else(|| now - TimeDelta::try_seconds(1).unwrap_or_default());
|
||||
if next <= now {
|
||||
next + TimeDelta::try_days(
|
||||
(7 - now.weekday().number_from_monday() + *day).into(),
|
||||
)
|
||||
.unwrap_or_default()
|
||||
} else {
|
||||
next
|
||||
}
|
||||
}
|
||||
SimpleCron::Hour { minute } => {
|
||||
let next = Local
|
||||
.with_ymd_and_hms(now.year(), now.month(), now.day(), now.hour(), *minute, 0)
|
||||
.earliest()
|
||||
.unwrap_or_else(|| now - TimeDelta::try_seconds(1).unwrap_or_default());
|
||||
if next <= now {
|
||||
next + TimeDelta::try_hours(1).unwrap_or_default()
|
||||
} else {
|
||||
next
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
(next - now).to_std().unwrap_or_else(|_| self.as_duration())
|
||||
}
|
||||
|
||||
pub fn as_duration(&self) -> Duration {
|
||||
match self {
|
||||
SimpleCron::Day { .. } => Duration::from_secs(24 * 60 * 60),
|
||||
SimpleCron::Week { .. } => Duration::from_secs(7 * 24 * 60 * 60),
|
||||
SimpleCron::Hour { .. } => Duration::from_secs(60 * 60),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for SimpleCron {
|
||||
type Err = String;
|
||||
|
||||
fn from_str(value: &str) -> Result<Self, Self::Err> {
|
||||
let mut hour = 0;
|
||||
let mut minute = 0;
|
||||
|
||||
for (pos, value) in value.split(' ').enumerate() {
|
||||
if pos == 0 {
|
||||
minute = value
|
||||
.parse::<u32>()
|
||||
.map_err(|_| "Invalid cron key: failed to parse cron minute".to_string())?;
|
||||
if !(0..=59).contains(&minute) {
|
||||
return Err(format!(
|
||||
"Invalid cron key: failed to parse minute, invalid value: {minute}"
|
||||
));
|
||||
}
|
||||
} else if pos == 1 {
|
||||
if value
|
||||
.as_bytes()
|
||||
.first()
|
||||
.ok_or_else(|| "Invalid cron key: failed to parse cron hour".to_string())?
|
||||
== &b'*'
|
||||
{
|
||||
return Ok(SimpleCron::Hour { minute });
|
||||
} else {
|
||||
hour = value
|
||||
.parse::<u32>()
|
||||
.map_err(|_| "Invalid cron key: failed to parse cron hour".to_string())?;
|
||||
if !(0..=23).contains(&hour) {
|
||||
return Err(format!(
|
||||
"Invalid cron key: failed to parse hour, invalid value: {hour}"
|
||||
));
|
||||
}
|
||||
}
|
||||
} else if pos == 2 {
|
||||
if value
|
||||
.as_bytes()
|
||||
.first()
|
||||
.ok_or_else(|| "Invalid cron key: failed to parse cron weekday".to_string())?
|
||||
== &b'*'
|
||||
{
|
||||
return Ok(SimpleCron::Day { hour, minute });
|
||||
} else {
|
||||
let day = value.parse::<u32>().map_err(|_| {
|
||||
"Invalid cron key: failed to parse cron weekday".to_string()
|
||||
})?;
|
||||
if !(1..=7).contains(&hour) {
|
||||
return Err(format!(
|
||||
"Invalid cron key: failed to parse weekday, invalid value: {}, range is 1 (Monday) to 7 (Sunday).",
|
||||
hour,
|
||||
));
|
||||
}
|
||||
|
||||
return Ok(SimpleCron::Week { day, hour, minute });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Err("Invalid cron key: parse cron expression.".to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for SimpleCron {
|
||||
fn default() -> Self {
|
||||
SimpleCron::Hour { minute: 0 }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,264 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use ahash::{AHashMap, AHashSet};
|
||||
use serde::Deserialize;
|
||||
use std::borrow::Cow;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum MatchType {
|
||||
Equal(String),
|
||||
StartsWith(String),
|
||||
EndsWith(String),
|
||||
Matches(GlobPattern),
|
||||
All,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct GlobPattern {
|
||||
pattern: Vec<PatternChar>,
|
||||
to_lower: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum PatternChar {
|
||||
WildcardMany { num: usize, match_pos: usize },
|
||||
WildcardSingle { match_pos: usize },
|
||||
Char { char: char, match_pos: usize },
|
||||
}
|
||||
|
||||
impl GlobPattern {
|
||||
pub fn compile(pattern: &str, to_lower: bool) -> Self {
|
||||
let mut chars = Vec::new();
|
||||
let mut is_escaped = false;
|
||||
let mut str = pattern.chars().peekable();
|
||||
|
||||
while let Some(char) = str.next() {
|
||||
match char {
|
||||
'*' if !is_escaped => {
|
||||
let mut num = 1;
|
||||
while let Some('*') = str.peek() {
|
||||
num += 1;
|
||||
str.next();
|
||||
}
|
||||
chars.push(PatternChar::WildcardMany { num, match_pos: 0 });
|
||||
}
|
||||
'?' if !is_escaped => {
|
||||
chars.push(PatternChar::WildcardSingle { match_pos: 0 });
|
||||
}
|
||||
'\\' if !is_escaped => {
|
||||
is_escaped = true;
|
||||
continue;
|
||||
}
|
||||
_ => {
|
||||
if is_escaped {
|
||||
is_escaped = false;
|
||||
}
|
||||
if to_lower && char.is_uppercase() {
|
||||
for char in char.to_lowercase() {
|
||||
chars.push(PatternChar::Char { char, match_pos: 0 });
|
||||
}
|
||||
} else {
|
||||
chars.push(PatternChar::Char { char, match_pos: 0 });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
GlobPattern {
|
||||
pattern: chars,
|
||||
to_lower,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn try_compile(pattern: &str, to_lower: bool) -> Result<Self, String> {
|
||||
// Detect if the key is a glob pattern
|
||||
let mut last_ch = '\0';
|
||||
let mut has_escape = false;
|
||||
let mut is_glob = false;
|
||||
for ch in pattern.chars() {
|
||||
match ch {
|
||||
'\\' => {
|
||||
has_escape = true;
|
||||
}
|
||||
'*' | '?' if last_ch != '\\' => {
|
||||
is_glob = true;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
last_ch = ch;
|
||||
}
|
||||
|
||||
if is_glob {
|
||||
Ok(GlobPattern::compile(pattern, to_lower))
|
||||
} else {
|
||||
Err(if has_escape {
|
||||
pattern.replace('\\', "")
|
||||
} else {
|
||||
pattern.to_string()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Credits: Algorithm ported from https://research.swtch.com/glob
|
||||
pub fn matches(&self, value: &str) -> bool {
|
||||
let value = if self.to_lower {
|
||||
value.to_lowercase().chars().collect::<Vec<_>>()
|
||||
} else {
|
||||
value.chars().collect::<Vec<_>>()
|
||||
};
|
||||
|
||||
let mut px = 0;
|
||||
let mut nx = 0;
|
||||
let mut next_px = 0;
|
||||
let mut next_nx = 0;
|
||||
|
||||
while px < self.pattern.len() || nx < value.len() {
|
||||
match self.pattern.get(px) {
|
||||
Some(PatternChar::Char { char, .. }) => {
|
||||
if matches!(value.get(nx), Some(nc) if nc == char ) {
|
||||
px += 1;
|
||||
nx += 1;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
Some(PatternChar::WildcardSingle { .. }) if nx < value.len() => {
|
||||
px += 1;
|
||||
nx += 1;
|
||||
continue;
|
||||
}
|
||||
Some(PatternChar::WildcardMany { .. }) => {
|
||||
next_px = px;
|
||||
next_nx = nx + 1;
|
||||
px += 1;
|
||||
continue;
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
if 0 < next_nx && next_nx <= value.len() {
|
||||
px = next_px;
|
||||
nx = next_nx;
|
||||
continue;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct GlobSet {
|
||||
entries: AHashSet<String>,
|
||||
patterns: Vec<GlobPattern>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct GlobMap<V> {
|
||||
entries: AHashMap<String, V>,
|
||||
patterns: Vec<(GlobPattern, V)>,
|
||||
}
|
||||
|
||||
impl GlobSet {
|
||||
pub fn new() -> Self {
|
||||
GlobSet::default()
|
||||
}
|
||||
|
||||
pub fn insert_pattern(&mut self, pattern: &str) {
|
||||
match GlobPattern::try_compile(pattern, false) {
|
||||
Ok(glob) => {
|
||||
self.patterns.push(glob);
|
||||
}
|
||||
Err(entry) => {
|
||||
self.entries.insert(entry);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn insert_entry(&mut self, entry: String) {
|
||||
self.entries.insert(entry);
|
||||
}
|
||||
|
||||
pub fn contains(&self, key: &str) -> bool {
|
||||
self.entries.contains(key) || self.patterns.iter().any(|pattern| pattern.matches(key))
|
||||
}
|
||||
}
|
||||
|
||||
impl<V> GlobMap<V> {
|
||||
pub fn new() -> Self {
|
||||
GlobMap {
|
||||
entries: AHashMap::new(),
|
||||
patterns: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn insert_pattern(&mut self, pattern: &str, value: V) {
|
||||
match GlobPattern::try_compile(pattern, false) {
|
||||
Ok(glob) => {
|
||||
self.patterns.push((glob, value));
|
||||
}
|
||||
Err(entry) => {
|
||||
self.entries.insert(entry, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn insert_entry(&mut self, entry: String, value: V) {
|
||||
self.entries.insert(entry, value);
|
||||
}
|
||||
|
||||
pub fn get(&self, key: &str) -> Option<&V> {
|
||||
self.entries.get(key).or_else(|| {
|
||||
self.patterns
|
||||
.iter()
|
||||
.find_map(|(pattern, value)| pattern.matches(key).then_some(value))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl<V> Default for GlobMap<V> {
|
||||
fn default() -> Self {
|
||||
GlobMap::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl MatchType {
|
||||
pub fn parse(value: &str) -> Self {
|
||||
if value == "*" {
|
||||
MatchType::All
|
||||
} else if let Some(value) = value.strip_suffix('*') {
|
||||
MatchType::StartsWith(value.to_string())
|
||||
} else if let Some(value) = value.strip_prefix('*') {
|
||||
MatchType::EndsWith(value.to_string())
|
||||
} else if value.contains('*') {
|
||||
MatchType::Matches(GlobPattern::compile(value, false))
|
||||
} else {
|
||||
MatchType::Equal(value.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
pub fn matches(&self, value: &str) -> bool {
|
||||
match self {
|
||||
MatchType::Equal(pattern) => value == pattern,
|
||||
MatchType::StartsWith(pattern) => value.starts_with(pattern),
|
||||
MatchType::EndsWith(pattern) => value.ends_with(pattern),
|
||||
MatchType::Matches(pattern) => pattern.matches(value),
|
||||
MatchType::All => true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for GlobPattern {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
Ok(GlobPattern::compile(
|
||||
<Cow<&str>>::deserialize(deserializer)?.as_ref(),
|
||||
true,
|
||||
))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,203 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use base64::{Engine, engine::general_purpose};
|
||||
use reqwest::{
|
||||
Client, ClientBuilder,
|
||||
header::{AUTHORIZATION, CONTENT_TYPE, HeaderMap, HeaderName, HeaderValue, USER_AGENT},
|
||||
};
|
||||
use rustls::{
|
||||
ClientConfig, DigitallySignedStruct, Error as TlsError, SignatureScheme,
|
||||
client::danger::{HandshakeSignatureValid, ServerCertVerified, ServerCertVerifier},
|
||||
crypto::{CryptoProvider, aws_lc_rs},
|
||||
};
|
||||
use rustls_pki_types::{CertificateDer, ServerName, UnixTime};
|
||||
use std::{
|
||||
str::FromStr,
|
||||
sync::{Arc, LazyLock},
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
struct SharedTlsConfigs {
|
||||
strict: ClientConfig,
|
||||
strict_http1: ClientConfig,
|
||||
insecure: ClientConfig,
|
||||
insecure_http1: ClientConfig,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct NoCertificateVerification(Arc<CryptoProvider>);
|
||||
|
||||
impl ServerCertVerifier for NoCertificateVerification {
|
||||
fn verify_server_cert(
|
||||
&self,
|
||||
_end_entity: &CertificateDer<'_>,
|
||||
_intermediates: &[CertificateDer<'_>],
|
||||
_server_name: &ServerName<'_>,
|
||||
_ocsp_response: &[u8],
|
||||
_now: UnixTime,
|
||||
) -> Result<ServerCertVerified, TlsError> {
|
||||
Ok(ServerCertVerified::assertion())
|
||||
}
|
||||
|
||||
fn verify_tls12_signature(
|
||||
&self,
|
||||
_message: &[u8],
|
||||
_cert: &CertificateDer<'_>,
|
||||
_dss: &DigitallySignedStruct,
|
||||
) -> Result<HandshakeSignatureValid, TlsError> {
|
||||
Ok(HandshakeSignatureValid::assertion())
|
||||
}
|
||||
|
||||
fn verify_tls13_signature(
|
||||
&self,
|
||||
_message: &[u8],
|
||||
_cert: &CertificateDer<'_>,
|
||||
_dss: &DigitallySignedStruct,
|
||||
) -> Result<HandshakeSignatureValid, TlsError> {
|
||||
Ok(HandshakeSignatureValid::assertion())
|
||||
}
|
||||
|
||||
fn supported_verify_schemes(&self) -> Vec<SignatureScheme> {
|
||||
self.0.signature_verification_algorithms.supported_schemes()
|
||||
}
|
||||
}
|
||||
|
||||
static SHARED_TLS_CONFIGS: LazyLock<SharedTlsConfigs> = LazyLock::new(|| {
|
||||
let provider = Arc::new(aws_lc_rs::default_provider());
|
||||
|
||||
let verifier = rustls_platform_verifier::Verifier::new(provider.clone())
|
||||
.expect("Failed to load the platform certificate verifier");
|
||||
|
||||
let mut strict = ClientConfig::builder_with_provider(provider.clone())
|
||||
.with_safe_default_protocol_versions()
|
||||
.expect("Failed to build the TLS client configuration")
|
||||
.dangerous()
|
||||
.with_custom_certificate_verifier(Arc::new(verifier))
|
||||
.with_no_client_auth();
|
||||
strict.alpn_protocols = vec![b"h2".to_vec(), b"http/1.1".to_vec()];
|
||||
|
||||
let mut insecure = ClientConfig::builder_with_provider(provider.clone())
|
||||
.with_safe_default_protocol_versions()
|
||||
.expect("Failed to build the TLS client configuration")
|
||||
.dangerous()
|
||||
.with_custom_certificate_verifier(Arc::new(NoCertificateVerification(provider)))
|
||||
.with_no_client_auth();
|
||||
insecure.alpn_protocols = vec![b"h2".to_vec(), b"http/1.1".to_vec()];
|
||||
|
||||
let mut strict_http1 = strict.clone();
|
||||
strict_http1.alpn_protocols = vec![b"http/1.1".to_vec()];
|
||||
|
||||
let mut insecure_http1 = insecure.clone();
|
||||
insecure_http1.alpn_protocols = vec![b"http/1.1".to_vec()];
|
||||
|
||||
SharedTlsConfigs {
|
||||
strict,
|
||||
strict_http1,
|
||||
insecure,
|
||||
insecure_http1,
|
||||
}
|
||||
});
|
||||
|
||||
pub fn init_shared_tls_configs() {
|
||||
LazyLock::force(&SHARED_TLS_CONFIGS);
|
||||
}
|
||||
|
||||
pub fn shared_tls_config(allow_invalid_certs: bool) -> ClientConfig {
|
||||
if allow_invalid_certs {
|
||||
SHARED_TLS_CONFIGS.insecure.clone()
|
||||
} else {
|
||||
SHARED_TLS_CONFIGS.strict.clone()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn http_client_builder(allow_invalid_certs: bool) -> ClientBuilder {
|
||||
Client::builder().use_preconfigured_tls(shared_tls_config(allow_invalid_certs))
|
||||
}
|
||||
|
||||
pub fn http1_client_builder(allow_invalid_certs: bool) -> ClientBuilder {
|
||||
let tls = if allow_invalid_certs {
|
||||
SHARED_TLS_CONFIGS.insecure_http1.clone()
|
||||
} else {
|
||||
SHARED_TLS_CONFIGS.strict_http1.clone()
|
||||
};
|
||||
|
||||
Client::builder().http1_only().use_preconfigured_tls(tls)
|
||||
}
|
||||
|
||||
pub fn unpooled_http_client(allow_invalid_certs: bool) -> Client {
|
||||
http_client_builder(allow_invalid_certs)
|
||||
.pool_max_idle_per_host(0)
|
||||
.build()
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
pub fn build_http_client(
|
||||
raw_headers: impl IntoIterator<Item = (String, String)>,
|
||||
username: Option<&str>,
|
||||
password: Option<&str>,
|
||||
token: Option<&str>,
|
||||
content_type: Option<&str>,
|
||||
timeout: Duration,
|
||||
allow_invalid_certs: bool,
|
||||
) -> Result<Client, String> {
|
||||
let mut headers = build_http_headers(raw_headers, username, password, token, content_type)?;
|
||||
headers.insert(USER_AGENT, "Stalwart/1.0.0".parse().unwrap());
|
||||
|
||||
match http_client_builder(allow_invalid_certs)
|
||||
.connect_timeout(timeout)
|
||||
.default_headers(headers)
|
||||
.build()
|
||||
{
|
||||
Ok(client) => Ok(client),
|
||||
Err(err) => Err(format!("Failed to build HTTP client: {}", err)),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn build_http_headers(
|
||||
raw_headers: impl IntoIterator<Item = (String, String)>,
|
||||
username: Option<&str>,
|
||||
password: Option<&str>,
|
||||
token: Option<&str>,
|
||||
content_type: Option<&str>,
|
||||
) -> Result<HeaderMap, String> {
|
||||
let mut headers = HeaderMap::new();
|
||||
|
||||
if let Some(content_type) = content_type {
|
||||
headers.insert(CONTENT_TYPE, HeaderValue::from_str(content_type).unwrap());
|
||||
}
|
||||
|
||||
for (header, value) in raw_headers
|
||||
.into_iter()
|
||||
.map(|(k, v)| {
|
||||
Ok((
|
||||
HeaderName::from_str(k.trim())
|
||||
.map_err(|err| format!("Invalid header {k:?}: {err}",))?,
|
||||
HeaderValue::from_str(v.trim())
|
||||
.map_err(|err| format!("Invalid value {v:?}: {err}",))?,
|
||||
))
|
||||
})
|
||||
.collect::<Result<Vec<(HeaderName, HeaderValue)>, String>>()?
|
||||
{
|
||||
headers.insert(header, value);
|
||||
}
|
||||
|
||||
if let (Some(name), Some(secret)) = (username, password) {
|
||||
headers.insert(
|
||||
AUTHORIZATION,
|
||||
format!(
|
||||
"Basic {}",
|
||||
general_purpose::STANDARD.encode(format!("{}:{}", name, secret))
|
||||
)
|
||||
.parse()
|
||||
.unwrap(),
|
||||
);
|
||||
} else if let Some(token) = token {
|
||||
headers.insert(AUTHORIZATION, format!("Bearer {}", token).parse().unwrap());
|
||||
}
|
||||
|
||||
Ok(headers)
|
||||
}
|
||||
@@ -0,0 +1,536 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
#![warn(clippy::large_futures)]
|
||||
|
||||
pub mod bimap;
|
||||
pub mod cache;
|
||||
pub mod chained_bytes;
|
||||
pub mod cheeky_hash;
|
||||
pub mod codec;
|
||||
pub mod cron;
|
||||
pub mod glob;
|
||||
pub mod http;
|
||||
pub mod map;
|
||||
pub mod snowflake;
|
||||
pub mod template;
|
||||
pub mod tls;
|
||||
pub mod topological;
|
||||
pub mod url_params;
|
||||
|
||||
use compact_str::ToCompactString;
|
||||
use futures::StreamExt;
|
||||
pub use reqwest::Client;
|
||||
use reqwest::Response;
|
||||
pub use reqwest::header::HeaderMap;
|
||||
use std::borrow::Cow;
|
||||
use std::fmt::Write;
|
||||
|
||||
pub trait HttpLimitResponse: Sync + Send {
|
||||
fn bytes_with_limit(
|
||||
self,
|
||||
limit: usize,
|
||||
) -> impl std::future::Future<Output = reqwest::Result<Option<Vec<u8>>>> + Send;
|
||||
}
|
||||
|
||||
impl HttpLimitResponse for Response {
|
||||
async fn bytes_with_limit(self, limit: usize) -> reqwest::Result<Option<Vec<u8>>> {
|
||||
if self
|
||||
.content_length()
|
||||
.is_some_and(|len| len as usize > limit)
|
||||
{
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
let mut bytes = Vec::with_capacity(std::cmp::min(limit, 1024));
|
||||
let mut stream = self.bytes_stream();
|
||||
|
||||
while let Some(chunk) = stream.next().await {
|
||||
let chunk = chunk?;
|
||||
if bytes.len() + chunk.len() > limit {
|
||||
return Ok(None);
|
||||
}
|
||||
bytes.extend_from_slice(&chunk);
|
||||
}
|
||||
|
||||
Ok(Some(bytes))
|
||||
}
|
||||
}
|
||||
|
||||
pub trait UnwrapFailure<T> {
|
||||
fn failed(self, action: &str) -> T;
|
||||
}
|
||||
|
||||
impl<T> UnwrapFailure<T> for Option<T> {
|
||||
fn failed(self, message: &str) -> T {
|
||||
match self {
|
||||
Some(result) => result,
|
||||
None => {
|
||||
trc::event!(
|
||||
Server(trc::ServerEvent::StartupError),
|
||||
Details = message.to_compact_string()
|
||||
);
|
||||
eprintln!("{message}");
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, E: std::fmt::Display> UnwrapFailure<T> for Result<T, E> {
|
||||
fn failed(self, message: &str) -> T {
|
||||
match self {
|
||||
Ok(result) => result,
|
||||
Err(err) => {
|
||||
trc::event!(
|
||||
Server(trc::ServerEvent::StartupError),
|
||||
Details = message.to_compact_string(),
|
||||
Reason = err.to_compact_string()
|
||||
);
|
||||
|
||||
#[cfg(feature = "test_mode")]
|
||||
panic!("{message}: {err}");
|
||||
|
||||
#[cfg(not(feature = "test_mode"))]
|
||||
{
|
||||
eprintln!("{message}: {err}");
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn failed(message: &str) -> ! {
|
||||
trc::event!(
|
||||
Server(trc::ServerEvent::StartupError),
|
||||
Details = message.to_compact_string(),
|
||||
);
|
||||
eprintln!("{message}");
|
||||
std::process::exit(1);
|
||||
}
|
||||
|
||||
pub async fn wait_for_shutdown() {
|
||||
#[cfg(not(target_env = "msvc"))]
|
||||
let signal = {
|
||||
use tokio::signal::unix::{SignalKind, signal};
|
||||
|
||||
let mut h_term = signal(SignalKind::terminate()).failed("start signal handler");
|
||||
let mut h_int = signal(SignalKind::interrupt()).failed("start signal handler");
|
||||
|
||||
tokio::select! {
|
||||
_ = h_term.recv() => "SIGTERM",
|
||||
_ = h_int.recv() => "SIGINT",
|
||||
}
|
||||
};
|
||||
|
||||
#[cfg(target_env = "msvc")]
|
||||
let signal = {
|
||||
match tokio::signal::ctrl_c().await {
|
||||
Ok(()) => "SIGINT",
|
||||
Err(err) => {
|
||||
trc::event!(
|
||||
Server(trc::ServerEvent::ThreadError),
|
||||
Details = "Unable to listen for shutdown signal",
|
||||
Reason = err.to_string(),
|
||||
);
|
||||
"Error"
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
trc::event!(Server(trc::ServerEvent::Shutdown), CausedBy = signal);
|
||||
}
|
||||
|
||||
pub trait DomainPart {
|
||||
fn to_lowercase_address(&self, lower_local: bool) -> String;
|
||||
fn to_canonical_address(&self) -> Cow<'_, str>;
|
||||
fn domain_part(&self) -> &str;
|
||||
fn try_domain_part(&self) -> Option<&str>;
|
||||
fn try_local_part(&self) -> Option<&str>;
|
||||
fn to_ascii_domain(&self) -> Option<Cow<'_, str>>;
|
||||
}
|
||||
|
||||
impl<T: AsRef<str>> DomainPart for T {
|
||||
fn to_lowercase_address(&self, lower_local: bool) -> String {
|
||||
let address = self.as_ref();
|
||||
if let Some((local, domain)) = address.rsplit_once('@') {
|
||||
let mut address = String::with_capacity(address.len());
|
||||
if lower_local {
|
||||
for ch in local.chars() {
|
||||
for ch in ch.to_lowercase() {
|
||||
address.push(ch);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
address.push_str(local);
|
||||
}
|
||||
address.push('@');
|
||||
if domain.is_ascii() {
|
||||
for ch in domain.chars() {
|
||||
for ch in ch.to_lowercase() {
|
||||
address.push(ch);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let domain =
|
||||
idna::domain_to_ascii(domain).unwrap_or_else(|_| domain.to_lowercase());
|
||||
address.push_str(&domain);
|
||||
}
|
||||
address
|
||||
} else {
|
||||
address.to_lowercase()
|
||||
}
|
||||
}
|
||||
|
||||
fn to_canonical_address(&self) -> Cow<'_, str> {
|
||||
let address = self.as_ref();
|
||||
|
||||
if address
|
||||
.bytes()
|
||||
.any(|ch| !ch.is_ascii() || ch.is_ascii_uppercase())
|
||||
{
|
||||
Cow::Owned(address.to_lowercase_address(true))
|
||||
} else {
|
||||
Cow::Borrowed(address)
|
||||
}
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
fn try_domain_part(&self) -> Option<&str> {
|
||||
self.as_ref().rsplit_once('@').map(|(_, d)| d)
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
fn try_local_part(&self) -> Option<&str> {
|
||||
self.as_ref().rsplit_once('@').map(|(l, _)| l)
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
fn domain_part(&self) -> &str {
|
||||
self.as_ref()
|
||||
.rsplit_once('@')
|
||||
.map(|(_, d)| d)
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
fn to_ascii_domain(&self) -> Option<Cow<'_, str>> {
|
||||
let domain = self.as_ref();
|
||||
|
||||
if !domain.is_ascii() {
|
||||
idna::domain_to_ascii(domain).ok().map(Cow::Owned)
|
||||
} else if domain.bytes().any(|ch| ch.is_ascii_uppercase()) {
|
||||
Some(Cow::Owned(domain.to_ascii_lowercase()))
|
||||
} else {
|
||||
Some(Cow::Borrowed(domain))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub trait HexEncode {
|
||||
fn hex_encode(&self) -> String;
|
||||
}
|
||||
|
||||
impl<T: AsRef<[u8]>> HexEncode for T {
|
||||
fn hex_encode(&self) -> String {
|
||||
let bytes = self.as_ref();
|
||||
let mut s = String::with_capacity(bytes.len() * 2);
|
||||
for &b in bytes {
|
||||
let _ = write!(&mut s, "{b:02x}");
|
||||
}
|
||||
s
|
||||
}
|
||||
}
|
||||
|
||||
static NIL_CHAR: char = char::from_u32(0).unwrap();
|
||||
|
||||
// Basic email sanitizer
|
||||
pub fn sanitize_email(email: &str) -> Option<String> {
|
||||
let mut result = String::with_capacity(email.len());
|
||||
let mut last_ch = NIL_CHAR;
|
||||
let mut chars = email.chars();
|
||||
|
||||
for ch in chars.by_ref() {
|
||||
match ch {
|
||||
'.' => {
|
||||
if last_ch == NIL_CHAR || last_ch == '.' {
|
||||
return None;
|
||||
}
|
||||
result.push('.');
|
||||
}
|
||||
'!' | '#' | '$' | '%' | '&' | '\'' | '*' | '+' | '-' | '/' | '=' | '?' | '^' | '_'
|
||||
| '`' | '{' | '|' | '}' | '~' => {
|
||||
result.push(ch);
|
||||
}
|
||||
' ' | '\x09'..='\x0d' => continue,
|
||||
'@' => {
|
||||
if result.is_empty() || last_ch == '.' {
|
||||
return None;
|
||||
}
|
||||
last_ch = ch;
|
||||
result.push(ch);
|
||||
break;
|
||||
}
|
||||
_ => {
|
||||
if ch.is_uppercase() {
|
||||
for ch in ch.to_lowercase() {
|
||||
result.push(ch);
|
||||
}
|
||||
} else if ch.is_alphanumeric() {
|
||||
result.push(ch);
|
||||
} else {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
last_ch = ch;
|
||||
}
|
||||
|
||||
if last_ch != '@' {
|
||||
return None;
|
||||
}
|
||||
|
||||
last_ch = NIL_CHAR;
|
||||
let domain_start = result.len();
|
||||
let mut domain_is_ascii = true;
|
||||
|
||||
for ch in chars {
|
||||
match ch {
|
||||
'.' => {
|
||||
if !last_ch.is_alphanumeric() {
|
||||
return None;
|
||||
}
|
||||
result.push('.');
|
||||
}
|
||||
'-' | '_' => {
|
||||
if last_ch == NIL_CHAR || last_ch == '.' {
|
||||
return None;
|
||||
}
|
||||
result.push(ch);
|
||||
}
|
||||
' ' | '\x09'..='\x0d' => continue,
|
||||
_ => {
|
||||
if !ch.is_ascii() {
|
||||
domain_is_ascii = false;
|
||||
}
|
||||
if ch.is_uppercase() {
|
||||
for ch in ch.to_lowercase() {
|
||||
result.push(ch);
|
||||
}
|
||||
} else if ch.is_alphanumeric() {
|
||||
result.push(ch);
|
||||
} else {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
last_ch = ch;
|
||||
}
|
||||
|
||||
if !last_ch.is_alphanumeric() {
|
||||
return None;
|
||||
}
|
||||
|
||||
if domain_is_ascii {
|
||||
is_valid_domain(&result[domain_start..]).then_some(result)
|
||||
} else {
|
||||
let domain = idna::domain_to_ascii(&result[domain_start..]).ok()?;
|
||||
if !is_valid_domain(&domain) {
|
||||
return None;
|
||||
}
|
||||
result.truncate(domain_start);
|
||||
result.push_str(&domain);
|
||||
Some(result)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn sanitize_email_local(local: &str) -> Option<String> {
|
||||
let mut result = String::with_capacity(local.len());
|
||||
let mut last_ch = NIL_CHAR;
|
||||
|
||||
for ch in local.chars() {
|
||||
match ch {
|
||||
'.' => {
|
||||
if last_ch == NIL_CHAR || last_ch == '.' {
|
||||
return None;
|
||||
}
|
||||
result.push('.');
|
||||
}
|
||||
'!' | '#' | '$' | '%' | '&' | '\'' | '*' | '+' | '-' | '/' | '=' | '?' | '^' | '_'
|
||||
| '`' | '{' | '|' | '}' | '~' => {
|
||||
result.push(ch);
|
||||
}
|
||||
' ' | '\x09'..='\x0d' => continue,
|
||||
_ => {
|
||||
if ch.is_uppercase() {
|
||||
for ch in ch.to_lowercase() {
|
||||
result.push(ch);
|
||||
}
|
||||
} else if ch.is_alphanumeric() {
|
||||
result.push(ch);
|
||||
} else {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
last_ch = ch;
|
||||
}
|
||||
|
||||
if !result.is_empty() && last_ch != '.' {
|
||||
Some(result)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub fn sanitize_domain(domain: &str) -> Option<String> {
|
||||
let mut result = String::with_capacity(domain.len());
|
||||
let mut found_dot = false;
|
||||
let mut last_ch = char::from(0);
|
||||
let mut is_ascii = true;
|
||||
|
||||
for ch in domain.chars() {
|
||||
if !ch.is_whitespace() {
|
||||
if ch == '.' {
|
||||
found_dot = true;
|
||||
if !(last_ch.is_alphanumeric() || last_ch == '-' || last_ch == '_') {
|
||||
return None;
|
||||
}
|
||||
} else if !ch.is_ascii() {
|
||||
is_ascii = false;
|
||||
}
|
||||
last_ch = ch;
|
||||
for ch in ch.to_lowercase() {
|
||||
result.push(ch);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !(found_dot && last_ch != '.') {
|
||||
return None;
|
||||
}
|
||||
|
||||
if is_ascii {
|
||||
is_valid_domain(&result).then_some(result)
|
||||
} else {
|
||||
let domain = idna::domain_to_ascii(&result).ok()?;
|
||||
is_valid_domain(&domain).then_some(domain)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_valid_domain(domain: &str) -> bool {
|
||||
const RESERVED_TLDS: &[&str] = &[
|
||||
"test",
|
||||
"localhost",
|
||||
"local",
|
||||
"internal",
|
||||
"lan",
|
||||
"home",
|
||||
"corp",
|
||||
"intranet",
|
||||
"private",
|
||||
"localdomain",
|
||||
];
|
||||
(domain.contains('.') && psl::suffix(domain.as_bytes()).is_some_and(|s| s.typ().is_some()))
|
||||
|| RESERVED_TLDS.contains(&domain)
|
||||
|| domain
|
||||
.rsplit_once('.')
|
||||
.is_some_and(|(_, tld)| RESERVED_TLDS.contains(&tld))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::DomainPart;
|
||||
|
||||
use super::{sanitize_domain, sanitize_email};
|
||||
|
||||
#[test]
|
||||
fn idn_domains_canonicalize_to_a_label() {
|
||||
assert_eq!(
|
||||
sanitize_domain("straß6.de").as_deref(),
|
||||
Some("xn--stra6-oqa.de")
|
||||
);
|
||||
assert_eq!(
|
||||
sanitize_domain("STRASS.straß6.DE").as_deref(),
|
||||
Some("strass.xn--stra6-oqa.de")
|
||||
);
|
||||
assert_eq!(
|
||||
sanitize_domain("münchen.de").as_deref(),
|
||||
Some("xn--mnchen-3ya.de")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_label_and_ascii_domains_are_idempotent() {
|
||||
assert_eq!(
|
||||
sanitize_domain("xn--stra6-oqa.de").as_deref(),
|
||||
Some("xn--stra6-oqa.de")
|
||||
);
|
||||
assert_eq!(
|
||||
sanitize_domain(&sanitize_domain("straß6.de").unwrap()).as_deref(),
|
||||
Some("xn--stra6-oqa.de")
|
||||
);
|
||||
assert_eq!(
|
||||
sanitize_domain("Example.COM").as_deref(),
|
||||
Some("example.com")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn email_domain_part_canonicalizes_local_part_preserved() {
|
||||
assert_eq!(
|
||||
sanitize_email("cornelius_strauss@straß6.de").as_deref(),
|
||||
Some("[email protected]")
|
||||
);
|
||||
assert_eq!(
|
||||
sanitize_email("Foo.Bar@münchen.de").as_deref(),
|
||||
Some("[email protected]")
|
||||
);
|
||||
assert_eq!(
|
||||
sanitize_email("[email protected]").as_deref(),
|
||||
Some("[email protected]")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bare_public_suffix_domains_are_accepted() {
|
||||
assert_eq!(
|
||||
sanitize_email("[email protected]").as_deref(),
|
||||
Some("[email protected]")
|
||||
);
|
||||
assert_eq!(sanitize_email("[email protected]").as_deref(), Some("[email protected]"));
|
||||
assert_eq!(sanitize_email("user@com"), None);
|
||||
assert_eq!(sanitize_email("[email protected]"), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_label_email_domains_are_accepted_and_idempotent() {
|
||||
assert_eq!(
|
||||
sanitize_email("[email protected]").as_deref(),
|
||||
Some("[email protected]")
|
||||
);
|
||||
assert_eq!(
|
||||
sanitize_email("User@例子.com").as_deref(),
|
||||
sanitize_email("[email protected]").as_deref()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn to_ascii_domain_borrows_ascii_owns_idn() {
|
||||
assert!(matches!(
|
||||
"example.com".to_ascii_domain(),
|
||||
Some(std::borrow::Cow::Borrowed(_))
|
||||
));
|
||||
assert!(matches!(
|
||||
"straß6.de".to_ascii_domain(),
|
||||
Some(std::borrow::Cow::Owned(_))
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,316 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use std::ops::Deref;
|
||||
|
||||
#[derive(
|
||||
Debug,
|
||||
rkyv::Archive,
|
||||
rkyv::Deserialize,
|
||||
rkyv::Serialize,
|
||||
serde::Serialize,
|
||||
serde::Deserialize,
|
||||
Clone,
|
||||
Copy,
|
||||
PartialOrd,
|
||||
Ord,
|
||||
PartialEq,
|
||||
Eq,
|
||||
Hash,
|
||||
)]
|
||||
#[rkyv(compare(PartialEq), derive(Debug))]
|
||||
#[repr(transparent)]
|
||||
pub struct Bitmap<T: BitmapItem> {
|
||||
pub bitmap: u64,
|
||||
#[serde(skip)]
|
||||
#[rkyv(omit_bounds)]
|
||||
_state: std::marker::PhantomData<T>,
|
||||
}
|
||||
|
||||
pub trait BitmapItem: From<u64> + Into<u64> + Sized + Copy {
|
||||
fn max() -> u64;
|
||||
fn is_valid(&self) -> bool;
|
||||
}
|
||||
|
||||
pub trait BitPop {
|
||||
fn bit_push(&mut self, item: u8);
|
||||
fn bit_pop(&mut self) -> Option<u8>;
|
||||
}
|
||||
|
||||
impl<T: BitmapItem> Bitmap<T> {
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn all() -> Self {
|
||||
Self {
|
||||
bitmap: u64::MAX >> (64 - T::max()),
|
||||
_state: std::marker::PhantomData,
|
||||
}
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn union(&mut self, items: &Bitmap<T>) {
|
||||
self.bitmap |= items.bitmap;
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn union_raw(&mut self, items: impl Into<u64>) {
|
||||
self.bitmap |= items.into();
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn intersection(&mut self, items: &Bitmap<T>) {
|
||||
self.bitmap &= items.bitmap;
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn insert(&mut self, item: T) {
|
||||
debug_assert!(item.is_valid());
|
||||
self.bitmap |= 1 << item.into();
|
||||
}
|
||||
|
||||
pub fn insert_many(&mut self, items: impl IntoIterator<Item = T>) {
|
||||
for item in items.into_iter() {
|
||||
self.insert(item);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn remove_many(&mut self, items: impl IntoIterator<Item = T>) {
|
||||
for item in items.into_iter() {
|
||||
debug_assert!(item.is_valid());
|
||||
self.bitmap &= !(1 << item.into());
|
||||
}
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn with_item(mut self, item: T) -> Self {
|
||||
self.insert(item);
|
||||
self
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn remove(&mut self, item: T) {
|
||||
debug_assert!(item.is_valid());
|
||||
self.bitmap ^= 1 << item.into();
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn pop(&mut self) -> Option<T> {
|
||||
if self.bitmap != 0 {
|
||||
let item = 63 - self.bitmap.leading_zeros();
|
||||
self.bitmap ^= 1 << item;
|
||||
Some((item as u64).into())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn contains(&self, item: T) -> bool {
|
||||
self.bitmap & (1 << item.into()) != 0
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn contains_any(&self, items: impl Iterator<Item = T>) -> bool {
|
||||
for item in items {
|
||||
if self.bitmap & (1 << item.into()) != 0 {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn contains_all(&self, items: impl Iterator<Item = T>) -> bool {
|
||||
if !self.is_empty() {
|
||||
for item in items {
|
||||
if self.bitmap & (1 << item.into()) == 0 {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.bitmap == 0
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn clear(&mut self) -> Self {
|
||||
let bitmap = self.bitmap;
|
||||
self.bitmap = 0;
|
||||
Bitmap {
|
||||
bitmap,
|
||||
_state: std::marker::PhantomData,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn into_inner(self) -> u64 {
|
||||
self.bitmap
|
||||
}
|
||||
}
|
||||
|
||||
impl BitPop for u32 {
|
||||
fn bit_push(&mut self, item: u8) {
|
||||
*self |= 1 << item;
|
||||
}
|
||||
|
||||
fn bit_pop(&mut self) -> Option<u8> {
|
||||
if *self != 0 {
|
||||
let item = 31 - self.leading_zeros();
|
||||
*self ^= 1 << item;
|
||||
Some(item as u8)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl BitPop for u64 {
|
||||
fn bit_push(&mut self, item: u8) {
|
||||
*self |= 1 << item;
|
||||
}
|
||||
|
||||
fn bit_pop(&mut self) -> Option<u8> {
|
||||
if *self != 0 {
|
||||
let item = 63 - self.leading_zeros();
|
||||
*self ^= 1 << item;
|
||||
Some(item as u8)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: BitmapItem> From<ArchivedBitmap<T>> for Bitmap<T> {
|
||||
fn from(value: ArchivedBitmap<T>) -> Self {
|
||||
Self {
|
||||
bitmap: value.bitmap.into(),
|
||||
_state: std::marker::PhantomData,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: BitmapItem> From<&ArchivedBitmap<T>> for Bitmap<T> {
|
||||
fn from(value: &ArchivedBitmap<T>) -> Self {
|
||||
Self {
|
||||
bitmap: value.bitmap.into(),
|
||||
_state: std::marker::PhantomData,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: BitmapItem> From<u64> for Bitmap<T> {
|
||||
fn from(value: u64) -> Self {
|
||||
Self {
|
||||
bitmap: value,
|
||||
_state: std::marker::PhantomData,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: BitmapItem> AsRef<u64> for Bitmap<T> {
|
||||
fn as_ref(&self) -> &u64 {
|
||||
&self.bitmap
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: BitmapItem> Deref for Bitmap<T> {
|
||||
type Target = u64;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.bitmap
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: BitmapItem> From<Bitmap<T>> for u64 {
|
||||
fn from(value: Bitmap<T>) -> Self {
|
||||
value.bitmap
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: BitmapItem> Iterator for Bitmap<T> {
|
||||
type Item = T;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
if self.bitmap != 0 {
|
||||
let item = 63 - self.bitmap.leading_zeros();
|
||||
self.bitmap ^= 1 << item;
|
||||
Some((item as u64).into())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: BitmapItem> From<Vec<T>> for Bitmap<T> {
|
||||
fn from(values: Vec<T>) -> Self {
|
||||
let mut bitmap = Bitmap::default();
|
||||
for value in values {
|
||||
if value.is_valid() {
|
||||
bitmap.insert(value);
|
||||
}
|
||||
}
|
||||
bitmap
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: BitmapItem> FromIterator<T> for Bitmap<T> {
|
||||
fn from_iter<U: IntoIterator<Item = T>>(iter: U) -> Self {
|
||||
let mut bitmap = Bitmap::new();
|
||||
for value in iter {
|
||||
if value.is_valid() {
|
||||
bitmap.insert(value);
|
||||
}
|
||||
}
|
||||
bitmap
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: BitmapItem> From<&Vec<T>> for Bitmap<T> {
|
||||
fn from(values: &Vec<T>) -> Self {
|
||||
let mut bitmap = Bitmap::default();
|
||||
for value in values {
|
||||
if value.is_valid() {
|
||||
bitmap.insert(*value);
|
||||
}
|
||||
}
|
||||
bitmap
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: BitmapItem> From<T> for Bitmap<T> {
|
||||
fn from(value: T) -> Self {
|
||||
let mut bitmap = Bitmap::default();
|
||||
bitmap.insert(value);
|
||||
bitmap
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: BitmapItem> From<Bitmap<T>> for Vec<T> {
|
||||
fn from(values: Bitmap<T>) -> Self {
|
||||
let mut list = Vec::new();
|
||||
for item in values {
|
||||
list.push(item);
|
||||
}
|
||||
list
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: BitmapItem> Default for Bitmap<T> {
|
||||
fn default() -> Self {
|
||||
Bitmap {
|
||||
bitmap: 0,
|
||||
_state: std::marker::PhantomData,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
pub mod bitmap;
|
||||
pub mod mutex_map;
|
||||
pub mod vec_map;
|
||||
@@ -0,0 +1,71 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use core::hash::Hash;
|
||||
use std::hash::Hasher;
|
||||
|
||||
use ahash::AHasher;
|
||||
use tokio::sync::{Mutex, MutexGuard};
|
||||
|
||||
pub struct MutexMap<T: Default> {
|
||||
map: Box<[Mutex<T>]>,
|
||||
mask: u64,
|
||||
hasher: AHasher,
|
||||
}
|
||||
|
||||
pub struct MutexMapLockError;
|
||||
pub type Result<T> = std::result::Result<T, MutexMapLockError>;
|
||||
|
||||
#[allow(clippy::mutex_atomic)]
|
||||
impl<T: Default> MutexMap<T> {
|
||||
pub fn with_capacity(size: usize) -> MutexMap<T> {
|
||||
let size = size.next_power_of_two();
|
||||
MutexMap {
|
||||
map: (0..size)
|
||||
.map(|_| T::default().into())
|
||||
.collect::<Vec<Mutex<T>>>()
|
||||
.into_boxed_slice(),
|
||||
mask: (size - 1) as u64,
|
||||
hasher: AHasher::default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn lock<U>(&self, key: U) -> MutexGuard<'_, T>
|
||||
where
|
||||
U: Into<u64> + Copy,
|
||||
{
|
||||
let hash = key.into() & self.mask;
|
||||
self.map[hash as usize].lock().await
|
||||
}
|
||||
|
||||
/*pub async fn try_lock<U>(&self, key: U, timeout: Duration) -> Option<MutexGuard<'_, T>>
|
||||
where
|
||||
U: Into<u64> + Copy,
|
||||
{
|
||||
let hash = key.into() & self.mask;
|
||||
self.map[hash as usize].try_lock(timeout).await
|
||||
}*/
|
||||
|
||||
pub async fn lock_hash<U>(&self, key: U) -> MutexGuard<'_, T>
|
||||
where
|
||||
U: Hash,
|
||||
{
|
||||
let mut hasher = self.hasher.clone();
|
||||
key.hash(&mut hasher);
|
||||
let hash = hasher.finish() & self.mask;
|
||||
self.map[hash as usize].lock().await
|
||||
}
|
||||
|
||||
/*pub async fn try_lock_hash<U>(&self, key: U, timeout: Duration) -> Option<MutexGuard<'_, T>>
|
||||
where
|
||||
U: Hash,
|
||||
{
|
||||
let mut hasher = self.hasher.clone();
|
||||
key.hash(&mut hasher);
|
||||
let hash = hasher.finish() & self.mask;
|
||||
self.map[hash as usize].try_lock_for(timeout).await
|
||||
}*/
|
||||
}
|
||||
@@ -0,0 +1,396 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use rkyv::Archive;
|
||||
use serde::{Deserialize, Serialize, ser::SerializeMap};
|
||||
use std::{borrow::Borrow, cmp::Ordering, fmt, hash::Hash};
|
||||
|
||||
// A map implemented using vectors
|
||||
// used for small datasets of less than 20 items
|
||||
// and when deserializing from JSON
|
||||
|
||||
#[derive(rkyv::Archive, rkyv::Deserialize, rkyv::Serialize, Debug, Clone, PartialEq, Eq)]
|
||||
pub struct VecMap<K: Eq + PartialEq, V> {
|
||||
pub inner: Vec<KeyValue<K, V>>,
|
||||
}
|
||||
|
||||
#[derive(rkyv::Archive, rkyv::Deserialize, rkyv::Serialize, Debug, Clone, PartialEq, Eq, Hash)]
|
||||
pub struct KeyValue<K: Eq + PartialEq, V> {
|
||||
pub key: K,
|
||||
pub value: V,
|
||||
}
|
||||
|
||||
impl<K: Eq + PartialEq, V> Default for VecMap<K, V> {
|
||||
fn default() -> Self {
|
||||
VecMap { inner: Vec::new() }
|
||||
}
|
||||
}
|
||||
|
||||
impl<K: Eq + PartialEq, V> VecMap<K, V> {
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
pub fn with_capacity(capacity: usize) -> Self {
|
||||
Self {
|
||||
inner: Vec::with_capacity(capacity),
|
||||
}
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn set(&mut self, key: impl Into<K>, value: V) -> bool {
|
||||
let key = key.into();
|
||||
if let Some(kv) = self.inner.iter_mut().find(|kv| kv.key == key) {
|
||||
kv.value = value;
|
||||
false
|
||||
} else {
|
||||
self.inner.push(KeyValue { key, value });
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn append(&mut self, key: impl Into<K>, value: V) {
|
||||
self.inner.push(KeyValue {
|
||||
key: key.into(),
|
||||
value,
|
||||
});
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn with_append(mut self, key: impl Into<K>, value: V) -> Self {
|
||||
self.append(key, value);
|
||||
self
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn insert(&mut self, idx: usize, key: impl Into<K>, value: V) {
|
||||
self.inner.insert(
|
||||
idx,
|
||||
KeyValue {
|
||||
key: key.into(),
|
||||
value,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn get<Q: ?Sized>(&self, key: &Q) -> Option<&V>
|
||||
where
|
||||
K: Borrow<Q> + PartialEq<Q>,
|
||||
{
|
||||
self.inner.iter().find_map(|kv| {
|
||||
if &kv.key == key {
|
||||
Some(&kv.value)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn get_mut(&mut self, key: &K) -> Option<&mut V> {
|
||||
self.inner.iter_mut().find_map(|kv| {
|
||||
if &kv.key == key {
|
||||
Some(&mut kv.value)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn contains_key(&self, key: &K) -> bool {
|
||||
self.inner.iter().any(|kv| kv.key == *key)
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn remove<Q: ?Sized>(&mut self, key: &Q) -> Option<V>
|
||||
where
|
||||
K: Borrow<Q> + PartialEq<Q>,
|
||||
{
|
||||
self.inner
|
||||
.iter()
|
||||
.position(|kv| kv.key == *key)
|
||||
.map(|pos| self.inner.remove(pos).value)
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn remove_all(&mut self, key: &K) {
|
||||
self.inner.retain(|kv| kv.key != *key);
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn remove_entry(&mut self, key: &K) -> Option<(K, V)> {
|
||||
self.inner.iter().position(|k| &k.key == key).map(|pos| {
|
||||
let kv = self.inner.remove(pos);
|
||||
(kv.key, kv.value)
|
||||
})
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn swap_remove(&mut self, index: usize) -> V {
|
||||
self.inner.swap_remove(index).value
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.inner.is_empty()
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn len(&self) -> usize {
|
||||
self.inner.len()
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn clear(&mut self) {
|
||||
self.inner.clear();
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn iter(&self) -> impl Iterator<Item = (&K, &V)> {
|
||||
self.inner.iter().map(|kv| (&kv.key, &kv.value))
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn iter_by_key<'x, 'y: 'x>(&'x self, key: &'y K) -> impl Iterator<Item = &'x V> + 'x {
|
||||
self.inner.iter().filter_map(move |kv| {
|
||||
if &kv.key == key {
|
||||
Some(&kv.value)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn iter_mut(&mut self) -> impl Iterator<Item = (&mut K, &mut V)> {
|
||||
self.inner.iter_mut().map(|kv| (&mut kv.key, &mut kv.value))
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn iter_mut_by_key<'x, 'y: 'x>(
|
||||
&'x mut self,
|
||||
key: &'y K,
|
||||
) -> impl Iterator<Item = &'x mut V> + 'x {
|
||||
self.inner.iter_mut().filter_map(move |kv| {
|
||||
if &kv.key == key {
|
||||
Some(&mut kv.value)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn keys(&self) -> impl Iterator<Item = &K> {
|
||||
self.inner.iter().map(|kv| &kv.key)
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn values(&self) -> impl Iterator<Item = &V> {
|
||||
self.inner.iter().map(|kv| &kv.value)
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn last(&self) -> Option<(&K, &V)> {
|
||||
self.inner.last().map(|kv| (&kv.key, &kv.value))
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn values_mut(&mut self) -> impl Iterator<Item = &mut V> {
|
||||
self.inner.iter_mut().map(|kv| &mut kv.value)
|
||||
}
|
||||
|
||||
pub fn get_mut_or_insert_with(&mut self, key: K, fnc: impl FnOnce() -> V) -> &mut V {
|
||||
if let Some(pos) = self.inner.iter().position(|kv| kv.key == key) {
|
||||
&mut self.inner[pos].value
|
||||
} else {
|
||||
self.inner.push(KeyValue { key, value: fnc() });
|
||||
&mut self.inner.last_mut().unwrap().value
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_key_value(mut self, key: K, value: V) -> Self {
|
||||
self.append(key, value);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn sort_unstable(&mut self)
|
||||
where
|
||||
K: Ord,
|
||||
V: Ord,
|
||||
{
|
||||
self.inner.sort_unstable_by(|a, b| match a.key.cmp(&b.key) {
|
||||
Ordering::Equal => a.value.cmp(&b.value),
|
||||
cmp => cmp,
|
||||
});
|
||||
}
|
||||
|
||||
pub fn sort_unstable_by_key(&mut self)
|
||||
where
|
||||
K: Ord,
|
||||
{
|
||||
self.inner.sort_unstable_by(|a, b| a.key.cmp(&b.key));
|
||||
}
|
||||
|
||||
pub fn extend(&mut self, iter: impl IntoIterator<Item = (K, V)>) {
|
||||
for (k, v) in iter {
|
||||
self.append(k, v);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn drain(&mut self) -> impl Iterator<Item = (K, V)> + '_ {
|
||||
self.inner.drain(..).map(|kv| (kv.key, kv.value))
|
||||
}
|
||||
|
||||
pub fn into_values(self) -> impl Iterator<Item = V> {
|
||||
self.inner.into_iter().map(|kv| kv.value)
|
||||
}
|
||||
|
||||
pub fn into_keys(self) -> impl Iterator<Item = K> {
|
||||
self.inner.into_iter().map(|kv| kv.key)
|
||||
}
|
||||
}
|
||||
|
||||
impl<K: Eq + PartialEq, V: Default> VecMap<K, V> {
|
||||
pub fn get_mut_or_insert(&mut self, key: K) -> &mut V {
|
||||
if let Some(pos) = self.inner.iter().position(|kv| kv.key == key) {
|
||||
&mut self.inner[pos].value
|
||||
} else {
|
||||
self.inner.push(KeyValue {
|
||||
key,
|
||||
value: V::default(),
|
||||
});
|
||||
&mut self.inner.last_mut().unwrap().value
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<K: Archive + Eq + PartialEq, V: Archive> ArchivedVecMap<K, V> {
|
||||
pub fn len(&self) -> usize {
|
||||
self.inner.len()
|
||||
}
|
||||
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.inner.is_empty()
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn iter(
|
||||
&self,
|
||||
) -> impl Iterator<
|
||||
Item = (
|
||||
&<K as rkyv::Archive>::Archived,
|
||||
&<V as rkyv::Archive>::Archived,
|
||||
),
|
||||
> {
|
||||
self.inner.iter().map(|kv| (&kv.key, &kv.value))
|
||||
}
|
||||
}
|
||||
|
||||
impl<K: Eq + PartialEq, V> IntoIterator for VecMap<K, V> {
|
||||
type Item = (K, V);
|
||||
|
||||
type IntoIter =
|
||||
std::iter::Map<std::vec::IntoIter<KeyValue<K, V>>, fn(KeyValue<K, V>) -> (K, V)>;
|
||||
|
||||
fn into_iter(self) -> Self::IntoIter {
|
||||
self.inner.into_iter().map(|kv| (kv.key, kv.value))
|
||||
}
|
||||
}
|
||||
|
||||
impl<'x, K: Eq + PartialEq, V> IntoIterator for &'x VecMap<K, V> {
|
||||
type Item = (&'x K, &'x V);
|
||||
|
||||
type IntoIter = std::iter::Map<
|
||||
std::slice::Iter<'x, KeyValue<K, V>>,
|
||||
fn(&'x KeyValue<K, V>) -> (&'x K, &'x V),
|
||||
>;
|
||||
|
||||
fn into_iter(self) -> Self::IntoIter {
|
||||
self.inner.iter().map(|kv| (&kv.key, &kv.value))
|
||||
}
|
||||
}
|
||||
|
||||
impl<K, V> Hash for VecMap<K, V>
|
||||
where
|
||||
K: Eq + PartialEq + Hash,
|
||||
V: Hash,
|
||||
{
|
||||
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
|
||||
self.inner.hash(state);
|
||||
}
|
||||
}
|
||||
|
||||
impl<K: Eq + PartialEq, V> FromIterator<(K, V)> for VecMap<K, V> {
|
||||
fn from_iter<T>(iter: T) -> Self
|
||||
where
|
||||
T: IntoIterator<Item = (K, V)>,
|
||||
{
|
||||
let iter = iter.into_iter();
|
||||
let mut map = VecMap::with_capacity(iter.size_hint().0);
|
||||
for (k, v) in iter {
|
||||
map.append(k, v);
|
||||
}
|
||||
map
|
||||
}
|
||||
}
|
||||
|
||||
struct VecMapVisitor<K, V> {
|
||||
phantom: std::marker::PhantomData<(K, V)>,
|
||||
}
|
||||
|
||||
impl<'de, K: Eq + PartialEq + Deserialize<'de>, V: Deserialize<'de>> serde::de::Visitor<'de>
|
||||
for VecMapVisitor<K, V>
|
||||
{
|
||||
type Value = VecMap<K, V>;
|
||||
|
||||
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
|
||||
formatter.write_str("a valid map")
|
||||
}
|
||||
|
||||
fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
|
||||
where
|
||||
A: serde::de::MapAccess<'de>,
|
||||
{
|
||||
// Duplicates are not checked during deserialization
|
||||
let mut vec_map = VecMap::new();
|
||||
while let Some(key) = map.next_key::<K>()? {
|
||||
vec_map.append(key, map.next_value()?);
|
||||
}
|
||||
Ok(vec_map)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de, K: Eq + PartialEq + Deserialize<'de>, V: Deserialize<'de>> Deserialize<'de>
|
||||
for VecMap<K, V>
|
||||
{
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
deserializer.deserialize_map(VecMapVisitor {
|
||||
phantom: std::marker::PhantomData,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl<K: Eq + PartialEq + Serialize, V: Serialize> Serialize for VecMap<K, V> {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: serde::Serializer,
|
||||
{
|
||||
let mut map = serializer.serialize_map(self.len().into())?;
|
||||
|
||||
for (key, value) in self {
|
||||
map.serialize_entry(key, value)?
|
||||
}
|
||||
|
||||
map.end()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use std::{
|
||||
sync::atomic::{AtomicU64, Ordering},
|
||||
time::{Duration, SystemTime},
|
||||
};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct SnowflakeIdGenerator {
|
||||
epoch: SystemTime,
|
||||
sequence: AtomicU64,
|
||||
}
|
||||
|
||||
const SEQUENCE_LEN: u64 = 12;
|
||||
const NODE_ID_LEN: u64 = 9;
|
||||
|
||||
const SEQUENCE_MASK: u64 = (1 << SEQUENCE_LEN) - 1;
|
||||
const NODE_ID_MASK: u64 = (1 << NODE_ID_LEN) - 1;
|
||||
|
||||
pub const MAX_NODE_ID: u16 = NODE_ID_MASK as u16;
|
||||
|
||||
const DEFAULT_EPOCH: u64 = 1632280000; // 52 years after UNIX_EPOCH
|
||||
|
||||
static mut NODE_ID: u64 = 1;
|
||||
static SEQUENCE_ID: AtomicU64 = AtomicU64::new(0);
|
||||
|
||||
/*
|
||||
|
||||
ID characteristics:
|
||||
|
||||
- 43 bits for milliseconds since January 1st, 2022: 2^43 / (1000 * 60 * 60 * 24 * 365) = 278.92 years (from year 2022 until 2300)
|
||||
- 9 bits for a node id: 2^9 = 512 nodes
|
||||
- 12 bits for a sequence number: 2^12 = 4096 ids per millisecond
|
||||
|
||||
*/
|
||||
|
||||
#[inline(always)]
|
||||
fn node_id() -> u64 {
|
||||
unsafe { std::ptr::read_volatile(&raw const NODE_ID) }
|
||||
}
|
||||
|
||||
impl SnowflakeIdGenerator {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
epoch: SystemTime::UNIX_EPOCH + Duration::from_secs(DEFAULT_EPOCH), // 52 years after UNIX_EPOCH
|
||||
sequence: 0.into(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_node_id(set_node_id: u64) {
|
||||
let set_node_id = set_node_id & NODE_ID_MASK;
|
||||
|
||||
if set_node_id != node_id() {
|
||||
unsafe {
|
||||
NODE_ID = set_node_id;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_duration(period: Duration) -> Option<u64> {
|
||||
(SystemTime::UNIX_EPOCH + Duration::from_secs(DEFAULT_EPOCH))
|
||||
.elapsed()
|
||||
.ok()
|
||||
.map(|elapsed| {
|
||||
(elapsed.saturating_sub(period).as_millis() as u64) << (SEQUENCE_LEN + NODE_ID_LEN)
|
||||
})
|
||||
}
|
||||
|
||||
pub fn from_timestamp(timestamp: u64) -> Option<u64> {
|
||||
SystemTime::now()
|
||||
.duration_since(SystemTime::UNIX_EPOCH)
|
||||
.ok()
|
||||
.and_then(|now| now.as_secs().checked_sub(timestamp))
|
||||
.and_then(|diff| Self::from_duration(Duration::from_secs(diff)))
|
||||
}
|
||||
|
||||
pub fn global_id_from_timestamp(timestamp: u64) -> Option<u64> {
|
||||
let sequence = SEQUENCE_ID.fetch_add(1, Ordering::Relaxed) & SEQUENCE_MASK;
|
||||
Self::from_timestamp(timestamp).map(|id| id | (sequence << NODE_ID_LEN) | node_id())
|
||||
}
|
||||
|
||||
pub fn global_id() -> Option<u64> {
|
||||
let sequence = SEQUENCE_ID.fetch_add(1, Ordering::Relaxed) & SEQUENCE_MASK;
|
||||
|
||||
(SystemTime::UNIX_EPOCH + Duration::from_secs(DEFAULT_EPOCH))
|
||||
.elapsed()
|
||||
.ok()
|
||||
.map(|elapsed| {
|
||||
((elapsed.as_millis() as u64) << (SEQUENCE_LEN + NODE_ID_LEN))
|
||||
| (sequence << NODE_ID_LEN)
|
||||
| node_id()
|
||||
})
|
||||
}
|
||||
|
||||
pub fn to_timestamp(id: u64) -> u64 {
|
||||
(id >> (SEQUENCE_LEN + NODE_ID_LEN)) / 1000 + DEFAULT_EPOCH
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn past_id(&self, period: Duration) -> Option<u64> {
|
||||
self.epoch.elapsed().ok().map(|elapsed| {
|
||||
(elapsed.saturating_sub(period).as_millis() as u64) << (SEQUENCE_LEN + NODE_ID_LEN)
|
||||
})
|
||||
}
|
||||
|
||||
pub fn is_valid(&self) -> bool {
|
||||
self.epoch.elapsed().is_ok()
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn generate(&self) -> u64 {
|
||||
let elapsed = self
|
||||
.epoch
|
||||
.elapsed()
|
||||
.map(|e| e.as_millis())
|
||||
.unwrap_or_default() as u64;
|
||||
let sequence = self.sequence.fetch_add(1, Ordering::Relaxed) & SEQUENCE_MASK;
|
||||
|
||||
(elapsed << (SEQUENCE_LEN + NODE_ID_LEN)) | (sequence << NODE_ID_LEN) | node_id()
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for SnowflakeIdGenerator {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl Clone for SnowflakeIdGenerator {
|
||||
fn clone(&self) -> Self {
|
||||
Self {
|
||||
epoch: self.epoch,
|
||||
sequence: 0.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,618 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use ahash::AHashMap;
|
||||
use std::{hash::Hash, str::FromStr};
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Default)]
|
||||
pub struct Template<T> {
|
||||
pub items: Vec<TemplateItem<T>>,
|
||||
pub size: usize,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum TemplateItem<T> {
|
||||
Static(String),
|
||||
Variable { name: T, escape: bool },
|
||||
If { variable: T, block_end: usize },
|
||||
ForEach { variable: T, block_end: usize },
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum Variable<T: Eq + Hash, V: AsRef<str>> {
|
||||
Single(V),
|
||||
Block(Vec<AHashMap<T, V>>),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct Variables<T: Eq + Hash, V: AsRef<str>> {
|
||||
pub items: AHashMap<T, Variable<T, V>>,
|
||||
}
|
||||
|
||||
impl<T: FromStr + Eq + Hash + std::fmt::Debug> Template<T> {
|
||||
pub fn parse(mut template: &str) -> Result<Self, String> {
|
||||
let mut items = Vec::new();
|
||||
let mut block_stack = vec![];
|
||||
let mut size = 0;
|
||||
|
||||
loop {
|
||||
if let Some((start, end)) = template.split_once("{{") {
|
||||
if !start.is_empty() {
|
||||
items.push(TemplateItem::Static(start.to_string()));
|
||||
size += start.len();
|
||||
}
|
||||
let (var, rest) = end.split_once("}}").ok_or("Unmatched {{")?;
|
||||
template = rest;
|
||||
let var = var.trim();
|
||||
if let Some(var_name) = var.strip_prefix("#").map(|v| v.trim()) {
|
||||
let (is_each, var_name) = if let Some(each) = var_name.strip_prefix("each ") {
|
||||
(true, each)
|
||||
} else if let Some(if_cond) = var_name.strip_prefix("if ") {
|
||||
(false, if_cond)
|
||||
} else {
|
||||
return Err(format!("Invalid block start: {}", var_name));
|
||||
};
|
||||
let var = T::from_str(var_name)
|
||||
.map_err(|_| format!("Invalid variable: {}", var_name))?;
|
||||
|
||||
block_stack.push((var_name, items.len()));
|
||||
|
||||
if is_each {
|
||||
items.push(TemplateItem::ForEach {
|
||||
variable: var,
|
||||
block_end: 0,
|
||||
});
|
||||
} else {
|
||||
items.push(TemplateItem::If {
|
||||
variable: var,
|
||||
block_end: 0,
|
||||
});
|
||||
}
|
||||
} else if let Some(var_name) = var.strip_prefix("/").map(|v| v.trim()) {
|
||||
let (is_each, var_name) = if let Some(each) = var_name.strip_prefix("each ") {
|
||||
(true, each)
|
||||
} else if let Some(if_cond) = var_name.strip_prefix("if ") {
|
||||
(false, if_cond)
|
||||
} else {
|
||||
return Err(format!("Invalid block end: {}", var_name));
|
||||
};
|
||||
|
||||
if let Some((expected_name, if_pos)) = block_stack.pop() {
|
||||
if expected_name != var_name {
|
||||
return Err(format!(
|
||||
"Block end does not match start: expected {}, got {}",
|
||||
expected_name, var_name
|
||||
));
|
||||
}
|
||||
let block_end_idx = items.len();
|
||||
match &mut items[if_pos] {
|
||||
TemplateItem::If { block_end, .. } if !is_each => {
|
||||
*block_end = block_end_idx;
|
||||
}
|
||||
TemplateItem::ForEach { block_end, .. } if is_each => {
|
||||
*block_end = block_end_idx;
|
||||
}
|
||||
_ => {
|
||||
return Err(format!(
|
||||
"Block end does not match start type for {}",
|
||||
var_name
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let (name, escape) = var.strip_prefix("!").map_or((var, true), |v| (v, false));
|
||||
let name =
|
||||
T::from_str(name).map_err(|_| format!("Invalid variable: {}", name))?;
|
||||
items.push(TemplateItem::Variable { name, escape });
|
||||
}
|
||||
} else {
|
||||
if !template.is_empty() {
|
||||
items.push(TemplateItem::Static(template.to_string()));
|
||||
size += template.len();
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if block_stack.is_empty() {
|
||||
Ok(Template { items, size })
|
||||
} else {
|
||||
Err(format!("Unmatched {{: {}", block_stack.last().unwrap().0))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn eval<V>(&self, variables: &Variables<T, V>) -> String
|
||||
where
|
||||
V: AsRef<str>,
|
||||
{
|
||||
let mut result = String::with_capacity(self.size);
|
||||
let mut items = self.items.iter().enumerate();
|
||||
let mut base_offset = 0;
|
||||
|
||||
while let Some((idx, item)) = items.next() {
|
||||
let idx = idx + base_offset;
|
||||
match item {
|
||||
TemplateItem::Static(s) => result.push_str(s),
|
||||
TemplateItem::Variable { name, escape } => {
|
||||
if let Some(Variable::Single(variable)) = variables.items.get(name) {
|
||||
if *escape {
|
||||
html_escape(&mut result, variable.as_ref())
|
||||
} else {
|
||||
result.push_str(variable.as_ref());
|
||||
}
|
||||
}
|
||||
}
|
||||
TemplateItem::If {
|
||||
variable,
|
||||
block_end,
|
||||
} => {
|
||||
if !variables.items.contains_key(variable) {
|
||||
items = self.items[*block_end..].iter().enumerate();
|
||||
base_offset = *block_end;
|
||||
}
|
||||
}
|
||||
TemplateItem::ForEach {
|
||||
variable,
|
||||
block_end,
|
||||
} => {
|
||||
if let Some(Variable::Block(entries)) = variables.items.get(variable) {
|
||||
let slice = &self.items[idx + 1..*block_end];
|
||||
for entry in entries {
|
||||
let mut slice = slice.iter();
|
||||
while let Some(sub_item) = slice.next() {
|
||||
match sub_item {
|
||||
TemplateItem::Static(s) => result.push_str(s),
|
||||
TemplateItem::Variable { name, escape } => {
|
||||
if let Some(variable) = entry.get(name) {
|
||||
if *escape {
|
||||
html_escape(&mut result, variable.as_ref())
|
||||
} else {
|
||||
result.push_str(variable.as_ref());
|
||||
}
|
||||
}
|
||||
}
|
||||
TemplateItem::If {
|
||||
variable,
|
||||
block_end: start_pos,
|
||||
} if !entry.contains_key(variable) => {
|
||||
slice = self.items[*start_pos..*block_end].iter();
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
items = self.items[*block_end..].iter().enumerate();
|
||||
base_offset = *block_end;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
}
|
||||
|
||||
fn html_escape(result: &mut String, input: &str) {
|
||||
for c in input.chars() {
|
||||
match c {
|
||||
'&' => result.push_str("&"),
|
||||
'<' => result.push_str("<"),
|
||||
'>' => result.push_str(">"),
|
||||
'"' => result.push_str("""),
|
||||
'\'' => result.push_str("'"),
|
||||
_ => result.push(c),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Eq + Hash, V: AsRef<str>> Variables<T, V> {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
items: AHashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn insert_single(&mut self, key: T, value: V) {
|
||||
self.items.insert(key, Variable::Single(value));
|
||||
}
|
||||
|
||||
pub fn insert_block<V1, V2>(&mut self, key: T, value: V1)
|
||||
where
|
||||
V1: IntoIterator<Item = V2>,
|
||||
V2: IntoIterator<Item = (T, V)>,
|
||||
{
|
||||
self.items.insert(
|
||||
key,
|
||||
Variable::Block(value.into_iter().map(AHashMap::from_iter).collect()),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Eq + Hash, V: AsRef<str>> Default for Variables<T, V> {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_simple_variable_substitution() {
|
||||
let template = Template::parse("Hello {{name}}!").unwrap();
|
||||
let mut vars = Variables::<String, String>::new();
|
||||
vars.insert_single("name".to_string(), "World".to_string());
|
||||
|
||||
let result = template.eval(&vars);
|
||||
assert_eq!(result, "Hello World!");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_multiple_variables() {
|
||||
let template = Template::parse("{{greeting}} {{name}}, today is {{day}}").unwrap();
|
||||
let mut vars = Variables::<String, String>::new();
|
||||
vars.insert_single("greeting".to_string(), "Hello".to_string());
|
||||
vars.insert_single("name".to_string(), "Alice".to_string());
|
||||
vars.insert_single("day".to_string(), "Monday".to_string());
|
||||
|
||||
let result = template.eval(&vars);
|
||||
assert_eq!(result, "Hello Alice, today is Monday");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_missing_variable() {
|
||||
let template = Template::parse("Hello {{name}}!").unwrap();
|
||||
let vars = Variables::<String, String>::new();
|
||||
|
||||
let result = template.eval(&vars);
|
||||
assert_eq!(result, "Hello !");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_static_text_only() {
|
||||
let template = Template::parse("This is just static text").unwrap();
|
||||
let vars = Variables::<String, String>::new();
|
||||
|
||||
let result = template.eval(&vars);
|
||||
assert_eq!(result, "This is just static text");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_empty_template() {
|
||||
let template = Template::parse("").unwrap();
|
||||
let vars = Variables::<String, String>::new();
|
||||
|
||||
let result = template.eval(&vars);
|
||||
assert_eq!(result, "");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_if_block_with_existing_variable() {
|
||||
let template =
|
||||
Template::parse("{{#if show_message}}Hello World!{{/if show_message}}").unwrap();
|
||||
let mut vars = Variables::<String, String>::new();
|
||||
vars.insert_single("show_message".to_string(), "true".to_string());
|
||||
|
||||
let result = template.eval(&vars);
|
||||
assert_eq!(result, "Hello World!");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_if_block_with_missing_variable() {
|
||||
let template =
|
||||
Template::parse("{{#if show_message}}Hello World!{{/if show_message}}").unwrap();
|
||||
let vars = Variables::<String, String>::new();
|
||||
|
||||
let result = template.eval(&vars);
|
||||
assert_eq!(result, "");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_if_block_with_content_and_variables() {
|
||||
let template = Template::parse(
|
||||
"{{#if notifications}}You have notifications: {{count}}{{/if notifications}}",
|
||||
)
|
||||
.unwrap();
|
||||
let mut vars = Variables::<String, String>::new();
|
||||
vars.insert_single("notifications".to_string(), "true".to_string());
|
||||
vars.insert_single("count".to_string(), "5".to_string());
|
||||
|
||||
let result = template.eval(&vars);
|
||||
assert_eq!(result, "You have notifications: 5");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_foreach_block_basic() {
|
||||
let template = Template::parse("{{#each items}}{{name}} {{/each items}}").unwrap();
|
||||
let mut vars = Variables::<String, String>::new();
|
||||
|
||||
let items = vec![
|
||||
vec![("name".to_string(), "Item1".to_string())],
|
||||
vec![("name".to_string(), "Item2".to_string())],
|
||||
vec![("name".to_string(), "Item3".to_string())],
|
||||
];
|
||||
vars.insert_block("items".to_string(), items);
|
||||
|
||||
let result = template.eval(&vars);
|
||||
assert_eq!(result, "Item1 Item2 Item3 ");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_foreach_block_multiple_variables() {
|
||||
let template = Template::parse(
|
||||
"{{#each notifications}}* {{name}} at {{time}}\n{{/each notifications}}",
|
||||
)
|
||||
.unwrap();
|
||||
let mut vars = Variables::<String, String>::new();
|
||||
|
||||
let notifications = vec![
|
||||
vec![
|
||||
("name".to_string(), "Meeting".to_string()),
|
||||
("time".to_string(), "10:00".to_string()),
|
||||
],
|
||||
vec![
|
||||
("name".to_string(), "Call".to_string()),
|
||||
("time".to_string(), "14:30".to_string()),
|
||||
],
|
||||
];
|
||||
vars.insert_block("notifications".to_string(), notifications);
|
||||
|
||||
let result = template.eval(&vars);
|
||||
assert_eq!(result, "* Meeting at 10:00\n* Call at 14:30\n");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_foreach_block_empty() {
|
||||
let template = Template::parse("{{#each items}}{{name}}{{/each items}}").unwrap();
|
||||
let mut vars = Variables::<String, String>::new();
|
||||
vars.insert_block("items".to_string(), Vec::<Vec<(String, String)>>::new());
|
||||
|
||||
let result = template.eval(&vars);
|
||||
assert_eq!(result, "");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_foreach_block_missing_variable() {
|
||||
let template = Template::parse("{{#each items}}{{name}}{{/each items}}").unwrap();
|
||||
let vars = Variables::<String, String>::new();
|
||||
|
||||
let result = template.eval(&vars);
|
||||
assert_eq!(result, "");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_complex_template_example() {
|
||||
let template_str = r#"Hello {{name}},
|
||||
|
||||
{{#if notifications}}You have the following notifications:
|
||||
{{#each notifications}}* {{name}} at {{time}}
|
||||
{{/each notifications}}{{/if notifications}}
|
||||
Best regards"#;
|
||||
|
||||
let template = Template::parse(template_str).unwrap();
|
||||
let mut vars = Variables::<String, String>::new();
|
||||
vars.insert_single("name".to_string(), "Alice".to_string());
|
||||
vars.insert_single("notifications".to_string(), "true".to_string());
|
||||
|
||||
let notifications = vec![
|
||||
vec![
|
||||
("name".to_string(), "Team Meeting".to_string()),
|
||||
("time".to_string(), "09:00".to_string()),
|
||||
],
|
||||
vec![
|
||||
("name".to_string(), "Doctor Appointment".to_string()),
|
||||
("time".to_string(), "15:30".to_string()),
|
||||
],
|
||||
];
|
||||
vars.insert_block("notifications".to_string(), notifications);
|
||||
|
||||
let result = template.eval(&vars);
|
||||
let expected = r#"Hello Alice,
|
||||
|
||||
You have the following notifications:
|
||||
* Team Meeting at 09:00
|
||||
* Doctor Appointment at 15:30
|
||||
|
||||
Best regards"#;
|
||||
|
||||
assert_eq!(result, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_complex_template_no_notifications() {
|
||||
let template_str = r#"Hello {{name}},
|
||||
|
||||
{{#if notifications}}
|
||||
You have the following notifications:
|
||||
{{#each notifications}}
|
||||
* {{name}} at {{time}}
|
||||
{{/each notifications}}{{/if notifications}}
|
||||
Best regards"#;
|
||||
|
||||
let template = Template::parse(template_str).unwrap();
|
||||
let mut vars = Variables::<String, String>::new();
|
||||
vars.insert_single("name".to_string(), "Bob".to_string());
|
||||
|
||||
let result = template.eval(&vars);
|
||||
let expected = r#"Hello Bob,
|
||||
|
||||
|
||||
Best regards"#;
|
||||
|
||||
assert_eq!(result, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_whitespace_handling() {
|
||||
let template = Template::parse("{{ name }}").unwrap();
|
||||
let mut vars = Variables::<String, String>::new();
|
||||
vars.insert_single("name".to_string(), "Test".to_string());
|
||||
|
||||
let result = template.eval(&vars);
|
||||
assert_eq!(result, "Test");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_whitespace_in_blocks() {
|
||||
let template = Template::parse("{{# if condition }}Content{{/ if condition }}").unwrap();
|
||||
let mut vars = Variables::<String, String>::new();
|
||||
vars.insert_single("condition".to_string(), "true".to_string());
|
||||
|
||||
let result = template.eval(&vars);
|
||||
assert_eq!(result, "Content");
|
||||
}
|
||||
|
||||
// Error handling tests
|
||||
#[test]
|
||||
fn test_unmatched_opening_brace() {
|
||||
let result = Template::<String>::parse("Hello {{name");
|
||||
assert!(result.is_err());
|
||||
assert!(result.unwrap_err().contains("Unmatched {{"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_invalid_block_start() {
|
||||
let result = Template::<String>::parse("{{#invalid block}}{{/invalid block}}");
|
||||
assert!(result.is_err());
|
||||
assert!(result.unwrap_err().contains("Invalid block start"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_invalid_block_end() {
|
||||
let result = Template::<String>::parse("{{#if test}}{{\\/invalid block}}");
|
||||
assert!(result.is_err());
|
||||
assert!(result.unwrap_err().contains("Unmatched"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_mismatched_block_names() {
|
||||
let result = Template::<String>::parse("{{#if test}}{{/if different}}");
|
||||
assert!(result.is_err());
|
||||
assert!(
|
||||
result
|
||||
.unwrap_err()
|
||||
.contains("Block end does not match start")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_mismatched_block_types() {
|
||||
let result = Template::<String>::parse("{{#if test}}{{/each test}}");
|
||||
assert!(result.is_err());
|
||||
assert!(
|
||||
result
|
||||
.unwrap_err()
|
||||
.contains("Block end does not match start")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_consecutive_braces() {
|
||||
let template = Template::parse("{{}}").unwrap();
|
||||
let vars = Variables::<String, String>::new();
|
||||
|
||||
let result = template.eval(&vars);
|
||||
assert_eq!(result, "");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_foreach_with_missing_inner_variables() {
|
||||
let template =
|
||||
Template::parse("{{#each items}}{{name}}: {{missing}}{{/each items}}").unwrap();
|
||||
let mut vars = Variables::<String, String>::new();
|
||||
|
||||
let items = vec![
|
||||
vec![("name".to_string(), "Item1".to_string())],
|
||||
vec![("name".to_string(), "Item2".to_string())],
|
||||
];
|
||||
vars.insert_block("items".to_string(), items);
|
||||
|
||||
let result = template.eval(&vars);
|
||||
assert_eq!(result, "Item1: Item2: ");
|
||||
}
|
||||
|
||||
/*#[test]
|
||||
fn test_full() {
|
||||
// Load static html in memory from resources/email-templates/calendar-alarm.html
|
||||
let template_str = include_str!("../../../resources/email-templates/calendar-alarm.html");
|
||||
let template: Template<CalendarTemplateVariable> = Template::parse(template_str).unwrap();
|
||||
|
||||
let mut vars = Variables::<CalendarTemplateVariable, String>::new();
|
||||
vars.insert_single(
|
||||
CalendarTemplateVariable::PageTitle,
|
||||
"Test Event".to_string(),
|
||||
);
|
||||
vars.insert_single(CalendarTemplateVariable::Header, "Event Header".to_string());
|
||||
vars.insert_single(CalendarTemplateVariable::Footer, "Event Footer".to_string());
|
||||
vars.insert_single(
|
||||
CalendarTemplateVariable::EventTitle,
|
||||
"Meeting with Team".to_string(),
|
||||
);
|
||||
vars.insert_single(
|
||||
CalendarTemplateVariable::EventDescription,
|
||||
"Discuss project updates".to_string(),
|
||||
);
|
||||
vars.insert_single(
|
||||
CalendarTemplateVariable::EventDetails,
|
||||
"Details about the event".to_string(),
|
||||
);
|
||||
vars.insert_single(
|
||||
CalendarTemplateVariable::ActionUrl,
|
||||
"http://example.com/action".to_string(),
|
||||
);
|
||||
vars.insert_single(
|
||||
CalendarTemplateVariable::ActionName,
|
||||
"Join Meeting".to_string(),
|
||||
);
|
||||
vars.insert_single(
|
||||
CalendarTemplateVariable::AttendeesTitle,
|
||||
"Attendees".to_string(),
|
||||
);
|
||||
vars.insert_block(
|
||||
CalendarTemplateVariable::EventDetails,
|
||||
vec![
|
||||
vec![
|
||||
(CalendarTemplateVariable::Key, "Location".to_string()),
|
||||
(
|
||||
CalendarTemplateVariable::Value,
|
||||
"Conference Room A".to_string(),
|
||||
),
|
||||
],
|
||||
vec![
|
||||
(CalendarTemplateVariable::Key, "Time".to_string()),
|
||||
(
|
||||
CalendarTemplateVariable::Value,
|
||||
"10:00 AM - 11:00 AM".to_string(),
|
||||
),
|
||||
],
|
||||
],
|
||||
);
|
||||
vars.insert_block(
|
||||
CalendarTemplateVariable::Attendees,
|
||||
vec![
|
||||
vec![
|
||||
(CalendarTemplateVariable::Key, "Alice".to_string()),
|
||||
(
|
||||
CalendarTemplateVariable::Value,
|
||||
"[email protected]".to_string(),
|
||||
),
|
||||
],
|
||||
vec![
|
||||
(CalendarTemplateVariable::Key, "Bob".to_string()),
|
||||
(
|
||||
CalendarTemplateVariable::Value,
|
||||
"[email protected]".to_string(),
|
||||
),
|
||||
],
|
||||
],
|
||||
);
|
||||
let result = template.eval(&vars);
|
||||
// Write result to test.html
|
||||
std::fs::write("test.html", result).expect("Unable to write file");
|
||||
}*/
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use rustls::{
|
||||
ClientConfig, SignatureScheme,
|
||||
client::danger::{HandshakeSignatureValid, ServerCertVerified, ServerCertVerifier},
|
||||
};
|
||||
use rustls_platform_verifier::BuilderVerifierExt;
|
||||
use std::sync::Arc;
|
||||
use tokio_rustls::TlsConnector;
|
||||
|
||||
pub fn rustls_client_config(allow_invalid_certs: bool) -> Result<ClientConfig, String> {
|
||||
let config = ClientConfig::builder();
|
||||
|
||||
if !allow_invalid_certs {
|
||||
config
|
||||
.with_platform_verifier()
|
||||
.map(|config| config.with_no_client_auth())
|
||||
.map_err(|err| format!("Failed to build platform verifier: {err}"))
|
||||
} else {
|
||||
Ok(config
|
||||
.dangerous()
|
||||
.with_custom_certificate_verifier(Arc::new(DummyVerifier {}))
|
||||
.with_no_client_auth())
|
||||
}
|
||||
}
|
||||
|
||||
pub fn build_tls_connector(allow_invalid_certs: bool) -> Result<TlsConnector, String> {
|
||||
rustls_client_config(allow_invalid_certs)
|
||||
.map(Arc::new)
|
||||
.map(TlsConnector::from)
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct DummyVerifier;
|
||||
|
||||
impl ServerCertVerifier for DummyVerifier {
|
||||
fn verify_server_cert(
|
||||
&self,
|
||||
_end_entity: &rustls_pki_types::CertificateDer<'_>,
|
||||
_intermediates: &[rustls_pki_types::CertificateDer<'_>],
|
||||
_server_name: &rustls_pki_types::ServerName<'_>,
|
||||
_ocsp_response: &[u8],
|
||||
_now: rustls_pki_types::UnixTime,
|
||||
) -> Result<ServerCertVerified, rustls::Error> {
|
||||
Ok(ServerCertVerified::assertion())
|
||||
}
|
||||
|
||||
fn verify_tls12_signature(
|
||||
&self,
|
||||
_message: &[u8],
|
||||
_cert: &rustls_pki_types::CertificateDer<'_>,
|
||||
_dss: &rustls::DigitallySignedStruct,
|
||||
) -> Result<HandshakeSignatureValid, rustls::Error> {
|
||||
Ok(HandshakeSignatureValid::assertion())
|
||||
}
|
||||
|
||||
fn verify_tls13_signature(
|
||||
&self,
|
||||
_message: &[u8],
|
||||
_cert: &rustls_pki_types::CertificateDer<'_>,
|
||||
_dss: &rustls::DigitallySignedStruct,
|
||||
) -> Result<HandshakeSignatureValid, rustls::Error> {
|
||||
Ok(HandshakeSignatureValid::assertion())
|
||||
}
|
||||
|
||||
fn supported_verify_schemes(&self) -> Vec<SignatureScheme> {
|
||||
vec![
|
||||
SignatureScheme::RSA_PKCS1_SHA1,
|
||||
SignatureScheme::ECDSA_SHA1_Legacy,
|
||||
SignatureScheme::RSA_PKCS1_SHA256,
|
||||
SignatureScheme::ECDSA_NISTP256_SHA256,
|
||||
SignatureScheme::RSA_PKCS1_SHA384,
|
||||
SignatureScheme::ECDSA_NISTP384_SHA384,
|
||||
SignatureScheme::RSA_PKCS1_SHA512,
|
||||
SignatureScheme::ECDSA_NISTP521_SHA512,
|
||||
SignatureScheme::RSA_PSS_SHA256,
|
||||
SignatureScheme::RSA_PSS_SHA384,
|
||||
SignatureScheme::RSA_PSS_SHA512,
|
||||
SignatureScheme::ED25519,
|
||||
SignatureScheme::ED448,
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use ahash::AHashMap;
|
||||
use std::{collections::VecDeque, hash::Hash};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct TopologicalSort<T: Copy + Eq + Hash> {
|
||||
edges: AHashMap<T, Vec<T>>,
|
||||
count: AHashMap<T, usize>,
|
||||
}
|
||||
|
||||
impl<T: Copy + Eq + Hash + std::fmt::Debug> TopologicalSort<T> {
|
||||
pub fn with_capacity(capacity: usize) -> Self {
|
||||
Self {
|
||||
edges: AHashMap::with_capacity(capacity),
|
||||
count: AHashMap::with_capacity(capacity),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn insert(&mut self, from: T, to: T) {
|
||||
self.count.entry(from).or_insert(0);
|
||||
self.edges.entry(from).or_default().push(to);
|
||||
*self.count.entry(to).or_insert(0) += 1;
|
||||
}
|
||||
|
||||
pub fn into_iterator(mut self) -> TopologicalSortIterator<T> {
|
||||
let mut no_edges = VecDeque::with_capacity(self.count.len());
|
||||
self.count.retain(|node, count| {
|
||||
if *count == 0 {
|
||||
no_edges.push_back(*node);
|
||||
false
|
||||
} else {
|
||||
true
|
||||
}
|
||||
});
|
||||
|
||||
TopologicalSortIterator {
|
||||
edges: self.edges,
|
||||
count: self.count,
|
||||
no_edges,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct TopologicalSortIterator<T: Copy + Eq + Hash> {
|
||||
edges: AHashMap<T, Vec<T>>,
|
||||
count: AHashMap<T, usize>,
|
||||
no_edges: VecDeque<T>,
|
||||
}
|
||||
|
||||
impl<T: Copy + Eq + Hash> Iterator for TopologicalSortIterator<T> {
|
||||
type Item = T;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
let no_edge = self.no_edges.pop_back()?;
|
||||
|
||||
if let Some(edges) = self.edges.get(&no_edge) {
|
||||
for neighbor in edges {
|
||||
if let Some(count) = self.count.get_mut(neighbor) {
|
||||
*count -= 1;
|
||||
if *count == 0 {
|
||||
self.count.remove(neighbor);
|
||||
self.no_edges.push_front(*neighbor);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Some(no_edge)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Copy + Eq + Hash> TopologicalSortIterator<T> {
|
||||
pub fn is_valid(&self) -> bool {
|
||||
self.count.is_empty()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_topological_sort() {
|
||||
let mut sort = TopologicalSort::with_capacity(6);
|
||||
sort.insert(1, 2);
|
||||
sort.insert(1, 3);
|
||||
sort.insert(2, 4);
|
||||
sort.insert(3, 4);
|
||||
sort.insert(4, 5);
|
||||
sort.insert(5, 6);
|
||||
|
||||
let mut iter = sort.into_iterator();
|
||||
assert_eq!(iter.next(), Some(1));
|
||||
assert_eq!(iter.next(), Some(2));
|
||||
assert_eq!(iter.next(), Some(3));
|
||||
assert_eq!(iter.next(), Some(4));
|
||||
assert_eq!(iter.next(), Some(5));
|
||||
assert_eq!(iter.next(), Some(6));
|
||||
assert_eq!(iter.next(), None);
|
||||
assert!(iter.is_valid(), "{:?}", iter);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_topological_sort_cycle() {
|
||||
let mut sort = TopologicalSort::with_capacity(6);
|
||||
sort.insert(1, 2);
|
||||
sort.insert(2, 3);
|
||||
sort.insert(3, 1);
|
||||
|
||||
let mut iter = sort.into_iterator();
|
||||
assert_eq!(iter.next(), None);
|
||||
assert!(!iter.is_valid());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use std::{borrow::Cow, collections::HashMap};
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct UrlParams<'x> {
|
||||
params: HashMap<Cow<'x, str>, Cow<'x, str>>,
|
||||
}
|
||||
|
||||
impl<'x> UrlParams<'x> {
|
||||
pub fn new(query: Option<&'x str>) -> Self {
|
||||
if let Some(query) = query {
|
||||
Self {
|
||||
params: form_urlencoded::parse(query.as_bytes())
|
||||
.filter(|(_, value)| !value.is_empty())
|
||||
.collect(),
|
||||
}
|
||||
} else {
|
||||
Self::default()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get(&self, key: &str) -> Option<&str> {
|
||||
self.params.get(key).map(|v| v.as_ref())
|
||||
}
|
||||
|
||||
pub fn has_key(&self, key: &str) -> bool {
|
||||
self.params.contains_key(key)
|
||||
}
|
||||
|
||||
pub fn parse<T>(&self, key: &str) -> Option<T>
|
||||
where
|
||||
T: std::str::FromStr,
|
||||
{
|
||||
self.get(key).and_then(|v| v.parse().ok())
|
||||
}
|
||||
|
||||
pub fn into_inner(self) -> HashMap<Cow<'x, str>, Cow<'x, str>> {
|
||||
self.params
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user