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,23 @@
|
||||
[package]
|
||||
name = "registry"
|
||||
version = "0.16.22"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
utils = { path = "../utils" }
|
||||
trc = { path = "../trc" }
|
||||
types = { path = "../types" }
|
||||
serde = { version = "1.0", features = ["derive"]}
|
||||
hashify = "0.2.9"
|
||||
ahash = { version = "0.8" }
|
||||
jmap-tools = { version = "0.1" }
|
||||
mail-auth = { version = "0.13" }
|
||||
tokio = { version = "1.53", features = ["fs"] }
|
||||
lz4_flex = { version = "0.14", features = ["alloc"], default-features = false }
|
||||
|
||||
[features]
|
||||
test_mode = []
|
||||
enterprise = []
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
@@ -0,0 +1,71 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use crate::{
|
||||
schema::prelude::Property,
|
||||
types::{error::PatchError, string::StringValidator},
|
||||
};
|
||||
use jmap_tools::{JsonPointer, Value};
|
||||
use std::fmt::Debug;
|
||||
use types::{blob::BlobId, id::Id};
|
||||
use utils::map::vec_map::VecMap;
|
||||
|
||||
pub mod patch;
|
||||
pub mod properties;
|
||||
pub mod ser;
|
||||
|
||||
pub type JmapValue<'x> = Value<'x, Property, RegistryValue>;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub enum RegistryValue {
|
||||
Id(Id),
|
||||
BlobId(BlobId),
|
||||
IdReference(String),
|
||||
}
|
||||
|
||||
pub type PatchResult<'x> = Result<MaybeUnpatched<'x>, PatchError>;
|
||||
|
||||
pub enum MaybeUnpatched<'x> {
|
||||
Unpatched {
|
||||
property: Property,
|
||||
value: JmapValue<'x>,
|
||||
},
|
||||
UnpatchedMany {
|
||||
properties: VecMap<Property, JmapValue<'x>>,
|
||||
},
|
||||
Patched,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct JsonPointerPatch<'x> {
|
||||
ptr: &'x JsonPointer<Property>,
|
||||
pos: usize,
|
||||
validators: &'x [StringValidator],
|
||||
is_create: bool,
|
||||
can_set_tenant: bool,
|
||||
can_set_account: bool,
|
||||
}
|
||||
|
||||
pub trait RegistryJsonPatch: Debug + Default {
|
||||
fn patch<'x>(&mut self, pointer: JsonPointerPatch<'_>, value: JmapValue<'x>)
|
||||
-> PatchResult<'x>;
|
||||
}
|
||||
pub trait RegistryJsonPropertyPatch: Debug + Default {
|
||||
fn patch_property<'x>(
|
||||
&mut self,
|
||||
pointer: JsonPointerPatch<'_>,
|
||||
value: JmapValue<'x>,
|
||||
) -> PatchResult<'x>;
|
||||
}
|
||||
|
||||
pub trait RegistryJsonEnumPatch: Debug {
|
||||
fn patch<'x>(&mut self, pointer: JsonPointerPatch<'_>, value: JmapValue<'x>)
|
||||
-> PatchResult<'x>;
|
||||
}
|
||||
|
||||
pub trait IntoValue {
|
||||
fn into_value(self) -> JmapValue<'static>;
|
||||
}
|
||||
@@ -0,0 +1,435 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use crate::{
|
||||
jmap::{
|
||||
JmapValue, JsonPointerPatch, MaybeUnpatched, PatchResult, RegistryJsonEnumPatch,
|
||||
RegistryJsonPatch, RegistryJsonPropertyPatch, RegistryValue,
|
||||
},
|
||||
schema::prelude::Property,
|
||||
types::{
|
||||
EnumImpl,
|
||||
error::PatchError,
|
||||
map::MapItem,
|
||||
string::{StringValidator, StringValidatorResult},
|
||||
},
|
||||
};
|
||||
use jmap_tools::{JsonPointer, JsonPointerItem, Key, Value};
|
||||
use utils::map::vec_map::VecMap;
|
||||
|
||||
impl<'x> JsonPointerPatch<'x> {
|
||||
pub fn new(ptr: &'x JsonPointer<Property>) -> Self {
|
||||
Self {
|
||||
ptr,
|
||||
pos: 0,
|
||||
validators: &[],
|
||||
is_create: false,
|
||||
can_set_tenant: false,
|
||||
can_set_account: false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn cloned_with_ptr(&self, ptr: &'x JsonPointer<Property>) -> Self {
|
||||
Self {
|
||||
ptr,
|
||||
pos: 0,
|
||||
validators: &[],
|
||||
is_create: self.is_create,
|
||||
can_set_tenant: self.can_set_tenant,
|
||||
can_set_account: self.can_set_account,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn cloned(&self) -> Self {
|
||||
Self {
|
||||
ptr: self.ptr,
|
||||
pos: 0,
|
||||
validators: &[],
|
||||
is_create: self.is_create,
|
||||
can_set_tenant: self.can_set_tenant,
|
||||
can_set_account: self.can_set_account,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_create(mut self, is_create: bool) -> Self {
|
||||
self.is_create = is_create;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_can_set_tenant(mut self, can_set_tenant: bool) -> Self {
|
||||
self.can_set_tenant = can_set_tenant;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_can_set_account(mut self, can_set_account: bool) -> Self {
|
||||
self.can_set_account = can_set_account;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_validators(mut self, validators: &'x [StringValidator]) -> Self {
|
||||
self.validators = validators;
|
||||
self
|
||||
}
|
||||
|
||||
#[allow(clippy::should_implement_trait)]
|
||||
pub fn next(&mut self) -> Option<&JsonPointerItem<Property>> {
|
||||
self.ptr.as_slice().get(self.pos).inspect(|_| self.pos += 1)
|
||||
}
|
||||
|
||||
pub fn next_property(&mut self) -> Option<Property> {
|
||||
self.next().and_then(|item| {
|
||||
if let JsonPointerItem::Key(Key::Property(prop)) = item {
|
||||
Some(*prop)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub fn peek(&self) -> Option<&JsonPointerItem<Property>> {
|
||||
self.ptr.as_slice().get(self.pos)
|
||||
}
|
||||
|
||||
pub fn path(&self) -> String {
|
||||
self.ptr.to_string()
|
||||
}
|
||||
|
||||
pub fn has_next(&self) -> bool {
|
||||
self.ptr.as_slice().len() > self.pos
|
||||
}
|
||||
|
||||
pub fn assert_eof(&self) -> PatchResult<'static> {
|
||||
if self.has_next() {
|
||||
Err(PatchError::new(self.cloned(), "Invalid JSON Pointer path"))
|
||||
} else {
|
||||
Ok(MaybeUnpatched::Patched)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn assert_read_only(self) -> Result<Self, PatchError> {
|
||||
if self.is_create {
|
||||
Ok(self)
|
||||
} else {
|
||||
Err(PatchError::new(
|
||||
self.cloned(),
|
||||
"Cannot modify read-only property",
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn assert_server_set(self) -> PatchResult<'static> {
|
||||
Err(PatchError::new(
|
||||
self.cloned(),
|
||||
"Cannot modify server set property",
|
||||
))
|
||||
}
|
||||
|
||||
pub fn assert_can_set_tenant(self) -> Result<Self, PatchError> {
|
||||
if self.can_set_tenant {
|
||||
Ok(self)
|
||||
} else {
|
||||
Err(PatchError::new(
|
||||
self.cloned(),
|
||||
"Cannot modify memberTenantId property",
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn assert_can_set_account(self) -> Result<Self, PatchError> {
|
||||
if self.can_set_account {
|
||||
Ok(self)
|
||||
} else {
|
||||
Err(PatchError::new(
|
||||
self.cloned(),
|
||||
"Cannot modify accountId property",
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: RegistryJsonPatch> RegistryJsonPatch for Option<T> {
|
||||
fn patch<'x>(
|
||||
&mut self,
|
||||
pointer: JsonPointerPatch<'_>,
|
||||
value: JmapValue<'x>,
|
||||
) -> PatchResult<'x> {
|
||||
if let Value::Null = value {
|
||||
*self = None;
|
||||
pointer.assert_eof()
|
||||
} else if let Some(inner) = self {
|
||||
inner.patch(pointer, value)
|
||||
} else {
|
||||
let mut inner = T::default();
|
||||
inner.patch(pointer, value).inspect(|_| *self = Some(inner))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: RegistryJsonEnumPatch + Default> RegistryJsonEnumPatch for Option<T> {
|
||||
fn patch<'x>(
|
||||
&mut self,
|
||||
pointer: JsonPointerPatch<'_>,
|
||||
value: JmapValue<'x>,
|
||||
) -> PatchResult<'x> {
|
||||
if let Value::Null = value {
|
||||
*self = None;
|
||||
pointer.assert_eof()
|
||||
} else if let Some(inner) = self {
|
||||
inner.patch(pointer, value)
|
||||
} else {
|
||||
let mut inner = T::default();
|
||||
inner.patch(pointer, value).inspect(|_| *self = Some(inner))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl RegistryJsonPatch for String {
|
||||
fn patch<'x>(
|
||||
&mut self,
|
||||
pointer: JsonPointerPatch<'_>,
|
||||
value: JmapValue<'x>,
|
||||
) -> PatchResult<'x> {
|
||||
if let Some(value) = value.into_string().filter(|v| !v.is_empty()) {
|
||||
let mut value = value.into_owned();
|
||||
|
||||
for validator in pointer.validators {
|
||||
match validator.validate(&value) {
|
||||
StringValidatorResult::Valid => {}
|
||||
StringValidatorResult::Replace(new_value) => value = new_value,
|
||||
StringValidatorResult::Invalid(err) => {
|
||||
return Err(PatchError::new(pointer, err));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
*self = value;
|
||||
pointer.assert_eof()
|
||||
} else {
|
||||
Err(PatchError::new(pointer, "Invalid value for property."))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl RegistryJsonPatch for bool {
|
||||
fn patch<'x>(
|
||||
&mut self,
|
||||
pointer: JsonPointerPatch<'_>,
|
||||
value: JmapValue<'x>,
|
||||
) -> PatchResult<'x> {
|
||||
if let Some(new_value) = value.as_bool() {
|
||||
*self = new_value;
|
||||
pointer.assert_eof()
|
||||
} else {
|
||||
Err(PatchError::new(
|
||||
pointer,
|
||||
"Invalid value for boolean property (expected true or false)",
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl RegistryJsonPatch for u64 {
|
||||
fn patch<'x>(
|
||||
&mut self,
|
||||
pointer: JsonPointerPatch<'_>,
|
||||
value: JmapValue<'x>,
|
||||
) -> PatchResult<'x> {
|
||||
if let Some(new_value) = value.as_u64() {
|
||||
*self = new_value;
|
||||
pointer.assert_eof()
|
||||
} else {
|
||||
Err(PatchError::new(
|
||||
pointer,
|
||||
"Invalid value for unsigned integer property (expected non-negative integer)",
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl RegistryJsonPatch for i64 {
|
||||
fn patch<'x>(
|
||||
&mut self,
|
||||
pointer: JsonPointerPatch<'_>,
|
||||
value: JmapValue<'x>,
|
||||
) -> PatchResult<'x> {
|
||||
if let Some(new_value) = value.as_i64() {
|
||||
*self = new_value;
|
||||
pointer.assert_eof()
|
||||
} else {
|
||||
Err(PatchError::new(
|
||||
pointer,
|
||||
"Invalid value for signed integer property (expected integer)",
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl RegistryJsonPatch for trc::Key {
|
||||
fn patch<'x>(
|
||||
&mut self,
|
||||
pointer: JsonPointerPatch<'_>,
|
||||
value: super::JmapValue<'x>,
|
||||
) -> PatchResult<'x> {
|
||||
if let Some(new_value) = value.as_str().and_then(|v| trc::Key::parse(v.as_ref())) {
|
||||
*self = new_value;
|
||||
pointer.assert_eof()
|
||||
} else {
|
||||
Err(PatchError::new(
|
||||
pointer,
|
||||
format!("Invalid value {:?} for enum type {:?}.", value, self),
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: EnumImpl> RegistryJsonEnumPatch for T {
|
||||
fn patch<'x>(
|
||||
&mut self,
|
||||
pointer: JsonPointerPatch<'_>,
|
||||
value: JmapValue<'x>,
|
||||
) -> PatchResult<'x> {
|
||||
if let Some(new_value) = value.as_str().and_then(|v| T::parse(v.as_ref())) {
|
||||
*self = new_value;
|
||||
pointer.assert_eof()
|
||||
} else {
|
||||
Err(PatchError::new(
|
||||
pointer,
|
||||
format!("Invalid value {:?} for enum type {:?}.", value, self),
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<K: MapItem, V: RegistryJsonPatch> RegistryJsonPatch for VecMap<K, V> {
|
||||
fn patch<'x>(
|
||||
&mut self,
|
||||
mut pointer: JsonPointerPatch<'_>,
|
||||
value: JmapValue<'x>,
|
||||
) -> PatchResult<'x> {
|
||||
match (pointer.next(), value) {
|
||||
(Some(JsonPointerItem::Number(idx)), value) => {
|
||||
if let Some(key) = K::try_from_integer(*idx) {
|
||||
return if matches!(value, Value::Null) && !pointer.has_next() {
|
||||
self.remove(&key);
|
||||
Ok(MaybeUnpatched::Patched)
|
||||
} else {
|
||||
self.get_mut_or_insert(key).patch(pointer, value)
|
||||
};
|
||||
}
|
||||
}
|
||||
(Some(JsonPointerItem::Key(key)), value) => {
|
||||
if let Some(key) = K::try_from_string(key.to_string().as_ref()) {
|
||||
return if matches!(value, Value::Null) && !pointer.has_next() {
|
||||
self.remove(&key);
|
||||
Ok(MaybeUnpatched::Patched)
|
||||
} else {
|
||||
self.get_mut_or_insert(key).patch(pointer, value)
|
||||
};
|
||||
}
|
||||
}
|
||||
(None, Value::Object(items)) => {
|
||||
self.clear();
|
||||
for (key, value) in items.into_vec() {
|
||||
if let Some(key) = K::try_from_string(key.to_string().as_ref()) {
|
||||
let mut inner = V::default();
|
||||
inner.patch(pointer.clone(), value)?;
|
||||
self.set(key, inner);
|
||||
} else {
|
||||
return Err(PatchError::new(
|
||||
pointer.clone(),
|
||||
"Invalid key for object property",
|
||||
));
|
||||
}
|
||||
}
|
||||
return Ok(MaybeUnpatched::Patched);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
Err(PatchError::new(
|
||||
pointer,
|
||||
"Invalid value for object property",
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: RegistryJsonPropertyPatch> RegistryJsonPatch for T {
|
||||
fn patch<'x>(
|
||||
&mut self,
|
||||
pointer: JsonPointerPatch<'_>,
|
||||
value: JmapValue<'x>,
|
||||
) -> PatchResult<'x> {
|
||||
if pointer.has_next() {
|
||||
self.patch_property(pointer, value)
|
||||
} else if let Some(object) = value.into_object() {
|
||||
let mut ptr = JsonPointer::new(vec![JsonPointerItem::Root]);
|
||||
let mut unpatched = VecMap::new();
|
||||
for (key, value) in object.into_vec() {
|
||||
if let Some(property) = key.as_property() {
|
||||
if *property != Property::Type {
|
||||
ptr.as_mut_slice()[0] = JsonPointerItem::Key(Key::Property(*property));
|
||||
match self.patch_property(pointer.cloned_with_ptr(&ptr), value) {
|
||||
Ok(MaybeUnpatched::Patched) => {}
|
||||
Ok(MaybeUnpatched::Unpatched { property, value }) => {
|
||||
unpatched.append(property, value);
|
||||
}
|
||||
Ok(MaybeUnpatched::UnpatchedMany { properties }) => {
|
||||
unpatched.extend(properties);
|
||||
}
|
||||
Err(mut e) => {
|
||||
if !e.path.is_empty() {
|
||||
if !pointer.ptr.as_slice().is_empty() {
|
||||
e.path = format!("{}/{}", pointer.path(), e.path);
|
||||
}
|
||||
} else {
|
||||
e.path = JsonPointer::new(
|
||||
pointer
|
||||
.ptr
|
||||
.as_slice()
|
||||
.iter()
|
||||
.cloned()
|
||||
.chain([JsonPointerItem::Key(Key::Property(*property))])
|
||||
.collect(),
|
||||
)
|
||||
.to_string();
|
||||
}
|
||||
return Err(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return Err(PatchError::new(pointer.clone(), "Invalid key for object"));
|
||||
}
|
||||
}
|
||||
if unpatched.is_empty() {
|
||||
Ok(MaybeUnpatched::Patched)
|
||||
} else {
|
||||
Ok(MaybeUnpatched::UnpatchedMany {
|
||||
properties: unpatched,
|
||||
})
|
||||
}
|
||||
} else {
|
||||
Err(PatchError::new(pointer, "Invalid value type for object"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn object_type<T: EnumImpl>(
|
||||
pointer: &JsonPointerPatch<'_>,
|
||||
value: &Value<'_, Property, RegistryValue>,
|
||||
) -> Result<T, PatchError> {
|
||||
value
|
||||
.as_object()
|
||||
.and_then(|obj| obj.get(&jmap_tools::Key::Property(Property::Type)))
|
||||
.and_then(|v| v.as_str())
|
||||
.and_then(|v| T::parse(v.as_ref()))
|
||||
.ok_or_else(|| {
|
||||
PatchError::new(
|
||||
pointer.clone(),
|
||||
"Missing or invalid '@type' property in object",
|
||||
)
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use crate::{jmap::RegistryValue, schema::prelude::Property, types::EnumImpl};
|
||||
use jmap_tools::Key;
|
||||
use std::{borrow::Cow, str::FromStr};
|
||||
use types::{blob::BlobId, id::Id};
|
||||
|
||||
impl jmap_tools::Property for Property {
|
||||
fn try_parse(_: Option<&Key<'_, Self>>, value: &str) -> Option<Self> {
|
||||
Property::parse(value)
|
||||
}
|
||||
|
||||
fn to_cow(&self) -> Cow<'static, str> {
|
||||
self.as_str().into()
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for Property {
|
||||
type Err = ();
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
Property::parse(s).ok_or(())
|
||||
}
|
||||
}
|
||||
|
||||
impl jmap_tools::Element for RegistryValue {
|
||||
type Property = Property;
|
||||
|
||||
fn try_parse<P>(key: &Key<'_, Self::Property>, value: &str) -> Option<Self> {
|
||||
if let Key::Property(prop) = key {
|
||||
match prop {
|
||||
Property::Id
|
||||
| Property::MemberGroupIds
|
||||
| Property::MemberTenantId
|
||||
| Property::RoleIds
|
||||
| Property::DnsServerId
|
||||
| Property::DirectoryId
|
||||
| Property::DomainId
|
||||
| Property::AccountId
|
||||
| Property::DefaultDomainId
|
||||
| Property::DefaultCertificateId
|
||||
| Property::DefaultUserRoleIds
|
||||
| Property::DefaultGroupRoleIds
|
||||
| Property::DefaultTenantRoleIds
|
||||
| Property::DefaultAdminRoleIds
|
||||
| Property::ListenerIds
|
||||
| Property::PublicKey
|
||||
| Property::QueueId
|
||||
| Property::ModelId
|
||||
| Property::AcmeProviderId => {
|
||||
if let Some(reference) = value.strip_prefix('#') {
|
||||
Some(RegistryValue::IdReference(reference.to_string()))
|
||||
} else {
|
||||
Id::from_str(value).map(RegistryValue::Id).ok()
|
||||
}
|
||||
}
|
||||
Property::BlobId => {
|
||||
if let Some(reference) = value.strip_prefix('#') {
|
||||
Some(RegistryValue::IdReference(reference.to_string()))
|
||||
} else {
|
||||
BlobId::from_str(value).map(RegistryValue::BlobId).ok()
|
||||
}
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn to_cow(&self) -> Cow<'static, str> {
|
||||
match self {
|
||||
RegistryValue::Id(id) => id.to_string().into(),
|
||||
RegistryValue::BlobId(blob_id) => blob_id.to_string().into(),
|
||||
RegistryValue::IdReference(r) => format!("#{r}").into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Id> for RegistryValue {
|
||||
fn from(id: Id) -> Self {
|
||||
RegistryValue::Id(id)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<BlobId> for RegistryValue {
|
||||
fn from(id: BlobId) -> Self {
|
||||
RegistryValue::BlobId(id)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use crate::{
|
||||
jmap::{IntoValue, JmapValue},
|
||||
schema::prelude::Property,
|
||||
types::EnumImpl,
|
||||
};
|
||||
use jmap_tools::Key;
|
||||
use std::fmt::Debug;
|
||||
use utils::map::vec_map::VecMap;
|
||||
|
||||
impl<T: IntoValue> IntoValue for Option<T> {
|
||||
fn into_value(self) -> JmapValue<'static> {
|
||||
match self {
|
||||
Some(value) => value.into_value(),
|
||||
None => JmapValue::Null,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoValue for String {
|
||||
fn into_value(self) -> JmapValue<'static> {
|
||||
JmapValue::Str(self.into())
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoValue for bool {
|
||||
fn into_value(self) -> JmapValue<'static> {
|
||||
JmapValue::Bool(self)
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoValue for u64 {
|
||||
fn into_value(self) -> JmapValue<'static> {
|
||||
JmapValue::Number(self.into())
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoValue for i64 {
|
||||
fn into_value(self) -> JmapValue<'static> {
|
||||
JmapValue::Number(self.into())
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: EnumImpl> IntoValue for T {
|
||||
fn into_value(self) -> JmapValue<'static> {
|
||||
JmapValue::Str(self.as_str().into())
|
||||
}
|
||||
}
|
||||
|
||||
trait MapKey: Sized + PartialEq + Eq + Debug {
|
||||
fn to_key(self) -> Key<'static, Property>;
|
||||
}
|
||||
|
||||
impl MapKey for String {
|
||||
fn to_key(self) -> Key<'static, Property> {
|
||||
Key::Owned(self)
|
||||
}
|
||||
}
|
||||
|
||||
impl MapKey for u32 {
|
||||
fn to_key(self) -> Key<'static, Property> {
|
||||
Key::Owned(self.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: EnumImpl> MapKey for T {
|
||||
fn to_key(self) -> Key<'static, Property> {
|
||||
Key::Borrowed(self.as_str())
|
||||
}
|
||||
}
|
||||
|
||||
impl<K: MapKey, V: IntoValue> IntoValue for VecMap<K, V> {
|
||||
fn into_value(self) -> JmapValue<'static> {
|
||||
let mut map = jmap_tools::Map::with_capacity(self.len());
|
||||
for (k, v) in self {
|
||||
map.insert_unchecked(k.to_key(), v.into_value());
|
||||
}
|
||||
JmapValue::Object(map)
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoValue for trc::Key {
|
||||
fn into_value(self) -> JmapValue<'static> {
|
||||
JmapValue::Str(self.as_str().into())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
#![warn(clippy::large_futures)]
|
||||
|
||||
pub mod jmap;
|
||||
pub mod pickle;
|
||||
pub mod schema;
|
||||
pub mod types;
|
||||
pub mod utils;
|
||||
@@ -0,0 +1,275 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use crate::types::EnumImpl;
|
||||
use std::{borrow::Cow, collections::HashMap};
|
||||
use utils::{
|
||||
codec::leb128::{Leb128_, Leb128Reader, Leb128Writer},
|
||||
map::vec_map::VecMap,
|
||||
};
|
||||
|
||||
const COMPRESS_MARKER: u8 = 1 << 7;
|
||||
const COMPRESS_WATERMARK: usize = 8192;
|
||||
|
||||
pub trait Pickle: Sized {
|
||||
fn pickle(&self, out: &mut Vec<u8>);
|
||||
fn unpickle(stream: &mut PickledStream<'_>) -> Option<Self>;
|
||||
}
|
||||
|
||||
pub struct PickledStream<'x> {
|
||||
data: Cow<'x, [u8]>,
|
||||
pos: usize,
|
||||
version: u8,
|
||||
}
|
||||
|
||||
pub(crate) fn maybe_compress_pickle(input: Vec<u8>) -> Vec<u8> {
|
||||
let input_len = input.len() - 1; // Exclude the version byte
|
||||
if input_len > COMPRESS_WATERMARK {
|
||||
let (version, input) = input.split_first().unwrap();
|
||||
let mut bytes: Vec<u8> = vec![
|
||||
version | COMPRESS_MARKER;
|
||||
lz4_flex::block::get_maximum_output_size(input_len)
|
||||
+ 1
|
||||
+ std::mem::size_of::<u32>()
|
||||
];
|
||||
|
||||
// Compress the data
|
||||
let compressed_len =
|
||||
lz4_flex::compress_into(input, &mut bytes[std::mem::size_of::<u32>() + 1..]).unwrap();
|
||||
if compressed_len < input_len {
|
||||
// Prepend the length of the uncompressed data
|
||||
bytes[1..(std::mem::size_of::<u32>() + 1)]
|
||||
.copy_from_slice(&(input_len as u32).to_le_bytes());
|
||||
|
||||
// Truncate to the actual size
|
||||
bytes.truncate(compressed_len + std::mem::size_of::<u32>() + 1);
|
||||
return bytes;
|
||||
}
|
||||
}
|
||||
input
|
||||
}
|
||||
|
||||
impl<'x> PickledStream<'x> {
|
||||
pub fn new(data: &'x [u8]) -> Option<Self> {
|
||||
let (marker, data) = data.split_first()?;
|
||||
let version = marker & !COMPRESS_MARKER;
|
||||
if marker & COMPRESS_MARKER != 0 {
|
||||
lz4_flex::block::decompress_size_prepended(data)
|
||||
.ok()
|
||||
.map(|data| PickledStream {
|
||||
data: Cow::Owned(data),
|
||||
pos: 0,
|
||||
version,
|
||||
})
|
||||
} else {
|
||||
PickledStream {
|
||||
data: Cow::Borrowed(data),
|
||||
pos: 0,
|
||||
version,
|
||||
}
|
||||
.into()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn read(&mut self) -> Option<u8> {
|
||||
self.data.get(self.pos).copied().inspect(|_| self.pos += 1)
|
||||
}
|
||||
|
||||
pub fn read_leb128<T: Leb128_>(&mut self) -> Option<T> {
|
||||
self.data
|
||||
.get(self.pos..)
|
||||
.and_then(|bytes| bytes.read_leb128())
|
||||
.map(|(value, read_bytes)| {
|
||||
self.pos += read_bytes;
|
||||
value
|
||||
})
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn read_bytes(&mut self, len: usize) -> Option<&'_ [u8]> {
|
||||
self.data.get(self.pos..self.pos + len).inspect(|_| {
|
||||
self.pos += len;
|
||||
})
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn eof(&self) -> bool {
|
||||
self.pos >= self.data.len()
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn bytes(&self) -> &'_ [u8] {
|
||||
self.data.as_ref()
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn version(&self) -> u8 {
|
||||
self.version
|
||||
}
|
||||
}
|
||||
|
||||
impl Pickle for u16 {
|
||||
fn pickle(&self, out: &mut Vec<u8>) {
|
||||
let _ = out.write_leb128(*self);
|
||||
}
|
||||
|
||||
fn unpickle(stream: &mut PickledStream<'_>) -> Option<Self> {
|
||||
stream.read_leb128()
|
||||
}
|
||||
}
|
||||
|
||||
impl Pickle for u64 {
|
||||
fn pickle(&self, out: &mut Vec<u8>) {
|
||||
let _ = out.write_leb128(*self);
|
||||
}
|
||||
|
||||
fn unpickle(stream: &mut PickledStream<'_>) -> Option<Self> {
|
||||
stream.read_leb128()
|
||||
}
|
||||
}
|
||||
|
||||
impl Pickle for u32 {
|
||||
fn pickle(&self, out: &mut Vec<u8>) {
|
||||
let _ = out.write_leb128(*self);
|
||||
}
|
||||
|
||||
fn unpickle(stream: &mut PickledStream<'_>) -> Option<Self> {
|
||||
stream.read_leb128()
|
||||
}
|
||||
}
|
||||
|
||||
impl Pickle for i64 {
|
||||
fn pickle(&self, out: &mut Vec<u8>) {
|
||||
let _ = out.write_leb128(*self as u64);
|
||||
}
|
||||
|
||||
fn unpickle(stream: &mut PickledStream<'_>) -> Option<Self> {
|
||||
stream.read_leb128::<u64>().map(|v| v as i64)
|
||||
}
|
||||
}
|
||||
|
||||
impl Pickle for bool {
|
||||
fn pickle(&self, out: &mut Vec<u8>) {
|
||||
out.push(if *self { 1 } else { 0 });
|
||||
}
|
||||
|
||||
fn unpickle(stream: &mut PickledStream<'_>) -> Option<Self> {
|
||||
match stream.read()? {
|
||||
0 => Some(false),
|
||||
1 => Some(true),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Pickle for String {
|
||||
fn pickle(&self, out: &mut Vec<u8>) {
|
||||
(self.len() as u32).pickle(out);
|
||||
out.extend_from_slice(self.as_bytes());
|
||||
}
|
||||
|
||||
fn unpickle(stream: &mut PickledStream<'_>) -> Option<Self> {
|
||||
u32::unpickle(stream)
|
||||
.and_then(|len| stream.read_bytes(len as usize))
|
||||
.and_then(|bytes| String::from_utf8(bytes.to_vec()).ok())
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: EnumImpl> Pickle for T {
|
||||
fn pickle(&self, out: &mut Vec<u8>) {
|
||||
self.to_id().pickle(out);
|
||||
}
|
||||
|
||||
fn unpickle(stream: &mut PickledStream<'_>) -> Option<Self> {
|
||||
u16::unpickle(stream).and_then(Self::from_id)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Pickle for Option<T>
|
||||
where
|
||||
T: Pickle,
|
||||
{
|
||||
fn pickle(&self, out: &mut Vec<u8>) {
|
||||
match self {
|
||||
Some(value) => {
|
||||
out.push(1);
|
||||
value.pickle(out);
|
||||
}
|
||||
None => {
|
||||
out.push(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn unpickle(stream: &mut PickledStream<'_>) -> Option<Self> {
|
||||
match stream.read()? {
|
||||
0 => Some(None),
|
||||
1 => T::unpickle(stream).map(Some),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<K, V, S> Pickle for HashMap<K, V, S>
|
||||
where
|
||||
K: Pickle + std::hash::Hash + Eq,
|
||||
V: Pickle,
|
||||
S: std::hash::BuildHasher + Default,
|
||||
{
|
||||
fn pickle(&self, out: &mut Vec<u8>) {
|
||||
(self.len() as u32).pickle(out);
|
||||
for (key, value) in self {
|
||||
key.pickle(out);
|
||||
value.pickle(out);
|
||||
}
|
||||
}
|
||||
|
||||
fn unpickle(stream: &mut PickledStream<'_>) -> Option<Self> {
|
||||
let len = u32::unpickle(stream)? as usize;
|
||||
let mut map = HashMap::with_capacity_and_hasher(len, S::default());
|
||||
for _ in 0..len {
|
||||
let key = K::unpickle(stream)?;
|
||||
let value = V::unpickle(stream)?;
|
||||
map.insert(key, value);
|
||||
}
|
||||
Some(map)
|
||||
}
|
||||
}
|
||||
|
||||
impl<K, V> Pickle for VecMap<K, V>
|
||||
where
|
||||
K: Pickle + std::hash::Hash + Eq,
|
||||
V: Pickle,
|
||||
{
|
||||
fn pickle(&self, out: &mut Vec<u8>) {
|
||||
(self.len() as u32).pickle(out);
|
||||
for (key, value) in self {
|
||||
key.pickle(out);
|
||||
value.pickle(out);
|
||||
}
|
||||
}
|
||||
|
||||
fn unpickle(stream: &mut PickledStream<'_>) -> Option<Self> {
|
||||
let len = u32::unpickle(stream)? as usize;
|
||||
let mut map = VecMap::with_capacity(len);
|
||||
for _ in 0..len {
|
||||
let key = K::unpickle(stream)?;
|
||||
let value = V::unpickle(stream)?;
|
||||
map.append(key, value);
|
||||
}
|
||||
Some(map)
|
||||
}
|
||||
}
|
||||
|
||||
impl Pickle for trc::Key {
|
||||
fn pickle(&self, out: &mut Vec<u8>) {
|
||||
self.to_id().pickle(out);
|
||||
}
|
||||
|
||||
fn unpickle(stream: &mut PickledStream<'_>) -> Option<Self> {
|
||||
u16::unpickle(stream).and_then(Self::from_id)
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,139 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use crate::{
|
||||
schema::{
|
||||
enums::{TracingLevel, TracingLevelOpt},
|
||||
prelude::{Object, ObjectInner, Property},
|
||||
},
|
||||
types::EnumImpl,
|
||||
};
|
||||
use std::{cmp::Ordering, fmt::Display};
|
||||
use trc::TOTAL_EVENT_COUNT;
|
||||
|
||||
#[allow(clippy::derivable_impls)]
|
||||
pub mod enums;
|
||||
pub mod enums_impl;
|
||||
pub mod prelude;
|
||||
#[allow(clippy::large_enum_variant)]
|
||||
pub mod properties;
|
||||
pub mod properties_impl;
|
||||
#[allow(clippy::large_enum_variant)]
|
||||
pub mod structs;
|
||||
#[allow(clippy::needless_borrows_for_generic_args)]
|
||||
#[allow(clippy::len_zero)]
|
||||
#[allow(clippy::collapsible_if)]
|
||||
#[allow(clippy::derivable_impls)]
|
||||
#[allow(clippy::field_reassign_with_default)]
|
||||
pub mod structs_impl;
|
||||
|
||||
impl Display for Property {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{}", self.as_str())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<TracingLevelOpt> for trc::Level {
|
||||
fn from(level: TracingLevelOpt) -> Self {
|
||||
match level {
|
||||
TracingLevelOpt::Error => trc::Level::Error,
|
||||
TracingLevelOpt::Warn => trc::Level::Warn,
|
||||
TracingLevelOpt::Info => trc::Level::Info,
|
||||
TracingLevelOpt::Debug => trc::Level::Debug,
|
||||
TracingLevelOpt::Trace => trc::Level::Trace,
|
||||
TracingLevelOpt::Disable => trc::Level::Disable,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<TracingLevel> for trc::Level {
|
||||
fn from(level: TracingLevel) -> Self {
|
||||
match level {
|
||||
TracingLevel::Error => trc::Level::Error,
|
||||
TracingLevel::Warn => trc::Level::Warn,
|
||||
TracingLevel::Info => trc::Level::Info,
|
||||
TracingLevel::Debug => trc::Level::Debug,
|
||||
TracingLevel::Trace => trc::Level::Trace,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl EnumImpl for trc::EventType {
|
||||
const COUNT: usize = TOTAL_EVENT_COUNT;
|
||||
|
||||
fn parse(s: &str) -> Option<Self> {
|
||||
trc::EventType::parse(s)
|
||||
}
|
||||
|
||||
fn as_str(&self) -> &'static str {
|
||||
trc::EventType::as_str(self)
|
||||
}
|
||||
|
||||
fn from_id(id: u16) -> Option<Self> {
|
||||
trc::EventType::from_id(id)
|
||||
}
|
||||
|
||||
fn to_id(&self) -> u16 {
|
||||
trc::EventType::to_id(self)
|
||||
}
|
||||
}
|
||||
|
||||
impl EnumImpl for trc::MetricType {
|
||||
const COUNT: usize = TOTAL_EVENT_COUNT;
|
||||
|
||||
fn parse(s: &str) -> Option<Self> {
|
||||
trc::MetricType::parse(s)
|
||||
}
|
||||
|
||||
fn as_str(&self) -> &'static str {
|
||||
trc::MetricType::as_str(self)
|
||||
}
|
||||
|
||||
fn from_id(id: u16) -> Option<Self> {
|
||||
trc::MetricType::from_id(id)
|
||||
}
|
||||
|
||||
fn to_id(&self) -> u16 {
|
||||
trc::MetricType::to_id(self)
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialOrd for Property {
|
||||
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
|
||||
Some(self.cmp(other))
|
||||
}
|
||||
}
|
||||
|
||||
impl Ord for Property {
|
||||
fn cmp(&self, other: &Self) -> Ordering {
|
||||
self.to_id().cmp(&other.to_id())
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Into<ObjectInner>> From<T> for Object {
|
||||
fn from(value: T) -> Self {
|
||||
Object {
|
||||
inner: value.into(),
|
||||
revision: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Object {
|
||||
pub fn new(inner: ObjectInner) -> Self {
|
||||
Object { inner, revision: 0 }
|
||||
}
|
||||
|
||||
pub fn with_revision(inner: ObjectInner, revision: u64) -> Self {
|
||||
Object { inner, revision }
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Property> for String {
|
||||
fn from(value: Property) -> Self {
|
||||
value.as_str().to_string()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
pub use crate::jmap::IntoValue;
|
||||
pub use crate::jmap::JmapValue;
|
||||
pub use crate::jmap::MaybeUnpatched;
|
||||
pub use crate::jmap::PatchResult;
|
||||
pub use crate::jmap::RegistryJsonEnumPatch;
|
||||
pub use crate::jmap::{
|
||||
JsonPointerPatch, RegistryJsonPatch, RegistryJsonPropertyPatch, patch::object_type,
|
||||
};
|
||||
pub use crate::pickle::Pickle;
|
||||
pub use crate::schema::enums::*;
|
||||
pub use crate::schema::properties::*;
|
||||
pub use crate::schema::structs::*;
|
||||
pub use crate::types::EnumImpl;
|
||||
pub use crate::types::ObjectImpl;
|
||||
pub use crate::types::datetime::UTCDateTime;
|
||||
pub use crate::types::duration::Duration;
|
||||
pub use crate::types::error::*;
|
||||
pub use crate::types::float::Float;
|
||||
pub use crate::types::index::{IndexBuilder, IndexSchema, IndexSchemaType, IndexSchemaValueType};
|
||||
pub use crate::types::ipaddr::IpAddr;
|
||||
pub use crate::types::ipmask::IpAddrOrMask;
|
||||
pub use crate::types::list::List;
|
||||
pub use crate::types::map::Map;
|
||||
pub use crate::types::socketaddr::SocketAddr;
|
||||
pub use crate::types::string::StringValidator;
|
||||
pub use serde::{Deserialize, Serialize};
|
||||
pub use std::borrow::Cow;
|
||||
pub use std::str::FromStr;
|
||||
pub use types::blob::BlobId;
|
||||
pub use types::id::Id;
|
||||
pub use utils::map::vec_map::VecMap;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Object {
|
||||
pub inner: ObjectInner,
|
||||
pub revision: u64,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct ExpressionContext<'x> {
|
||||
pub expr: &'x Expression,
|
||||
pub default: Option<Expression>,
|
||||
pub property: Property,
|
||||
pub allowed_variables: &'static [ExpressionVariable],
|
||||
pub allowed_constants: &'static [ExpressionConstant],
|
||||
}
|
||||
|
||||
pub const OBJ_SINGLETON: u64 = 1;
|
||||
pub const OBJ_SEQ_ID: u64 = 1 << 1;
|
||||
pub const OBJ_FILTER_ACCOUNT: u64 = 1 << 2;
|
||||
pub const OBJ_FILTER_TENANT: u64 = 1 << 3;
|
||||
|
||||
pub const MASKED_PASSWORD: &str = "****";
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,322 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use crate::{
|
||||
jmap::{
|
||||
IntoValue, JmapValue, JsonPointerPatch, MaybeUnpatched, PatchResult, RegistryJsonPatch,
|
||||
},
|
||||
pickle::{Pickle, PickledStream},
|
||||
types::error::PatchError,
|
||||
};
|
||||
use std::{fmt::Display, str::FromStr, time::SystemTime};
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
|
||||
#[repr(transparent)]
|
||||
pub struct UTCDateTime(i64);
|
||||
|
||||
struct DateTime {
|
||||
pub year: u16,
|
||||
pub month: u8,
|
||||
pub day: u8,
|
||||
pub hour: u8,
|
||||
pub minute: u8,
|
||||
pub second: u8,
|
||||
pub tz_before_gmt: bool,
|
||||
pub tz_hour: u8,
|
||||
pub tz_minute: u8,
|
||||
}
|
||||
|
||||
impl FromStr for UTCDateTime {
|
||||
type Err = ();
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
// 2004 - 06 - 28 T 23 : 43 : 45 . 000 Z
|
||||
// 1969 - 02 - 13 T 23 : 32 : 00 - 03 : 30
|
||||
// 0 1 2 3 4 5 6 7
|
||||
|
||||
let mut pos = 0;
|
||||
let mut parts = [0u32; 8];
|
||||
let mut parts_sizes = [
|
||||
4u32, // Year (0)
|
||||
2u32, // Month (1)
|
||||
2u32, // Day (2)
|
||||
2u32, // Hour (3)
|
||||
2u32, // Minute (4)
|
||||
2u32, // Second (5)
|
||||
2u32, // TZ Hour (6)
|
||||
2u32, // TZ Minute (7)
|
||||
];
|
||||
let mut skip_digits = false;
|
||||
let mut is_plus = true;
|
||||
|
||||
for ch in s.as_bytes() {
|
||||
match ch {
|
||||
b'0'..=b'9' => {
|
||||
if !skip_digits {
|
||||
if parts_sizes[pos] > 0 {
|
||||
parts_sizes[pos] -= 1;
|
||||
parts[pos] += (ch - b'0') as u32 * u32::pow(10, parts_sizes[pos]);
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
b'-' => {
|
||||
if pos <= 1 {
|
||||
pos += 1;
|
||||
} else if pos == 5 {
|
||||
pos += 1;
|
||||
is_plus = false;
|
||||
skip_digits = false;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
b'T' if pos == 2 => {
|
||||
pos += 1;
|
||||
}
|
||||
b':' if [3, 4, 6].contains(&pos) => {
|
||||
pos += 1;
|
||||
}
|
||||
b'+' if pos == 5 => {
|
||||
pos += 1;
|
||||
skip_digits = false;
|
||||
}
|
||||
b'.' if pos == 5 => {
|
||||
skip_digits = true;
|
||||
}
|
||||
b'Z' | b'z' => (),
|
||||
_ => {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let dt = DateTime {
|
||||
year: parts[0] as u16,
|
||||
month: parts[1] as u8,
|
||||
day: parts[2] as u8,
|
||||
hour: parts[3] as u8,
|
||||
minute: parts[4] as u8,
|
||||
second: parts[5] as u8,
|
||||
tz_hour: parts[6] as u8,
|
||||
tz_minute: parts[7] as u8,
|
||||
tz_before_gmt: !is_plus,
|
||||
};
|
||||
|
||||
if pos >= 5 && dt.is_valid() {
|
||||
Ok(UTCDateTime(dt.timestamp()))
|
||||
} else {
|
||||
Err(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl UTCDateTime {
|
||||
pub fn now() -> Self {
|
||||
UTCDateTime(
|
||||
SystemTime::now()
|
||||
.duration_since(SystemTime::UNIX_EPOCH)
|
||||
.map_or(0, |d| d.as_secs()) as i64,
|
||||
)
|
||||
}
|
||||
|
||||
pub fn from_timestamp(timestamp: i64) -> Self {
|
||||
UTCDateTime(timestamp)
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn timestamp(&self) -> i64 {
|
||||
self.0
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn is_valid(&self) -> bool {
|
||||
self.0 != i64::MAX
|
||||
}
|
||||
|
||||
pub fn add_seconds(&mut self, seconds: i64) {
|
||||
if self.is_valid() {
|
||||
self.0 += seconds;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl DateTime {
|
||||
pub fn from_timestamp(timestamp: i64) -> Self {
|
||||
// Ported from http://howardhinnant.github.io/date_algorithms.html#civil_from_days
|
||||
let (z, seconds) = ((timestamp / 86400) + 719468, timestamp % 86400);
|
||||
let era: i64 = (if z >= 0 { z } else { z - 146096 }) / 146097;
|
||||
let doe: u64 = (z - era * 146097) as u64; // [0, 146096]
|
||||
let yoe: u64 = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365; // [0, 399]
|
||||
let y: i64 = (yoe as i64) + era * 400;
|
||||
let doy: u64 = doe - (365 * yoe + yoe / 4 - yoe / 100); // [0, 365]
|
||||
let mp = (5 * doy + 2) / 153; // [0, 11]
|
||||
let d: u64 = doy - (153 * mp + 2) / 5 + 1; // [1, 31]
|
||||
let m: u64 = if mp < 10 { mp + 3 } else { mp - 9 }; // [1, 12]
|
||||
let (h, mn, s) = (seconds / 3600, (seconds / 60) % 60, seconds % 60);
|
||||
|
||||
DateTime {
|
||||
year: (y + i64::from(m <= 2)) as u16,
|
||||
month: m as u8,
|
||||
day: d as u8,
|
||||
hour: h as u8,
|
||||
minute: mn as u8,
|
||||
second: s as u8,
|
||||
tz_before_gmt: false,
|
||||
tz_hour: 0,
|
||||
tz_minute: 0,
|
||||
}
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn is_valid(&self) -> bool {
|
||||
(0..=23).contains(&self.tz_hour)
|
||||
&& (1970..=3000).contains(&self.year)
|
||||
&& (0..=59).contains(&self.tz_minute)
|
||||
&& (1..=12).contains(&self.month)
|
||||
&& (1..=31).contains(&self.day)
|
||||
&& (0..=23).contains(&self.hour)
|
||||
&& (0..=59).contains(&self.minute)
|
||||
&& (0..=59).contains(&self.second)
|
||||
}
|
||||
|
||||
pub fn timestamp(&self) -> i64 {
|
||||
// Ported from https://github.com/protocolbuffers/upb/blob/22182e6e/upb/json_decode.c#L982-L992
|
||||
let month = self.month as u32;
|
||||
let year_base = 4800; /* Before min year, multiple of 400. */
|
||||
let m_adj = month.wrapping_sub(3); /* March-based month. */
|
||||
let carry = i64::from(m_adj > month);
|
||||
let adjust = if carry > 0 { 12 } else { 0 };
|
||||
let y_adj = self.year as i64 + year_base - carry;
|
||||
let month_days = ((m_adj.wrapping_add(adjust)) * 62719 + 769) / 2048;
|
||||
let leap_days = y_adj / 4 - y_adj / 100 + y_adj / 400;
|
||||
(y_adj * 365 + leap_days + month_days as i64 + (self.day as i64 - 1) - 2472632) * 86400
|
||||
+ self.hour as i64 * 3600
|
||||
+ self.minute as i64 * 60
|
||||
+ self.second as i64
|
||||
+ ((self.tz_hour as i64 * 3600 + self.tz_minute as i64 * 60)
|
||||
* if self.tz_before_gmt { 1 } else { -1 })
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for UTCDateTime {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
let dt = DateTime::from_timestamp(self.0);
|
||||
|
||||
write!(
|
||||
f,
|
||||
"{:04}-{:02}-{:02}T{:02}:{:02}:{:02}Z",
|
||||
dt.year, dt.month, dt.day, dt.hour, dt.minute, dt.second,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for UTCDateTime {
|
||||
fn default() -> Self {
|
||||
UTCDateTime::now()
|
||||
}
|
||||
}
|
||||
|
||||
impl serde::Serialize for UTCDateTime {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: serde::Serializer,
|
||||
{
|
||||
serializer.serialize_str(self.to_string().as_str())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> serde::Deserialize<'de> for UTCDateTime {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
UTCDateTime::from_str(<&str>::deserialize(deserializer)?)
|
||||
.map_err(|_| serde::de::Error::custom("invalid DateTime"))
|
||||
}
|
||||
}
|
||||
|
||||
impl Pickle for UTCDateTime {
|
||||
fn pickle(&self, out: &mut Vec<u8>) {
|
||||
out.extend_from_slice(&self.0.to_be_bytes());
|
||||
}
|
||||
|
||||
fn unpickle(data: &mut PickledStream<'_>) -> Option<Self> {
|
||||
let mut arr = [0u8; 8];
|
||||
arr.copy_from_slice(data.read_bytes(8)?);
|
||||
Some(UTCDateTime(i64::from_be_bytes(arr)))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<u64> for UTCDateTime {
|
||||
fn from(value: u64) -> Self {
|
||||
UTCDateTime(value as i64)
|
||||
}
|
||||
}
|
||||
|
||||
impl RegistryJsonPatch for UTCDateTime {
|
||||
fn patch<'x>(
|
||||
&mut self,
|
||||
mut pointer: JsonPointerPatch<'_>,
|
||||
value: JmapValue<'x>,
|
||||
) -> PatchResult<'x> {
|
||||
match (value, pointer.next()) {
|
||||
(jmap_tools::Value::Str(value), None) => {
|
||||
if let Ok(new_value) = UTCDateTime::from_str(value.as_ref()) {
|
||||
*self = new_value;
|
||||
Ok(MaybeUnpatched::Patched)
|
||||
} else {
|
||||
Err(PatchError::new(
|
||||
pointer,
|
||||
"Failed to parse UTCDateTime from string",
|
||||
))
|
||||
}
|
||||
}
|
||||
_ => Err(PatchError::new(
|
||||
pointer,
|
||||
"Invalid path for UTCDateTime, expected a string value",
|
||||
)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoValue for UTCDateTime {
|
||||
fn into_value(self) -> JmapValue<'static> {
|
||||
JmapValue::Str(self.to_string().into())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::str::FromStr;
|
||||
|
||||
use crate::types::datetime::UTCDateTime;
|
||||
|
||||
#[test]
|
||||
fn parse_jmap_date() {
|
||||
for (input, _) in [
|
||||
("1997-11-21T09:55:06-06:00", "1997-11-21T09:55:06-06:00"),
|
||||
("1997-11-21T09:55:06+00:00", "1997-11-21T09:55:06Z"),
|
||||
("2021-01-01T09:55:06+02:00", "2021-01-01T09:55:06+02:00"),
|
||||
("2004-06-28T23:43:45.000Z", "2004-06-28T23:43:45Z"),
|
||||
("1997-11-21T09:55:06.123+00:00", "1997-11-21T09:55:06Z"),
|
||||
(
|
||||
"2021-01-01T09:55:06.4567+02:00",
|
||||
"2021-01-01T09:55:06+02:00",
|
||||
),
|
||||
] {
|
||||
let date = UTCDateTime::from_str(input).unwrap();
|
||||
//assert_eq!(date.to_string(), expected_result);
|
||||
|
||||
let timestamp = date.timestamp();
|
||||
assert_eq!(
|
||||
UTCDateTime::from_timestamp(timestamp).timestamp(),
|
||||
timestamp
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,179 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use crate::{
|
||||
jmap::{
|
||||
IntoValue, JmapValue, JsonPointerPatch, MaybeUnpatched, PatchResult, RegistryJsonPatch,
|
||||
},
|
||||
pickle::{Pickle, PickledStream},
|
||||
types::error::PatchError,
|
||||
};
|
||||
use std::{fmt::Display, str::FromStr};
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
#[repr(transparent)]
|
||||
pub struct Duration(pub std::time::Duration);
|
||||
|
||||
impl Duration {
|
||||
pub fn from_millis(millis: u64) -> Self {
|
||||
Duration(std::time::Duration::from_millis(millis))
|
||||
}
|
||||
|
||||
pub fn into_inner(self) -> std::time::Duration {
|
||||
self.0
|
||||
}
|
||||
|
||||
pub fn is_valid(&self) -> bool {
|
||||
self.0.as_millis() > 0
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn as_secs(&self) -> u64 {
|
||||
self.0.as_secs()
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn as_millis(&self) -> u64 {
|
||||
self.0.as_millis() as u64
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Duration {
|
||||
fn default() -> Self {
|
||||
Duration(std::time::Duration::from_millis(0))
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for Duration {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{}", self.0.as_millis())
|
||||
}
|
||||
}
|
||||
|
||||
impl serde::Serialize for Duration {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: serde::Serializer,
|
||||
{
|
||||
serializer.serialize_u64(self.0.as_millis() as u64)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> serde::Deserialize<'de> for Duration {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
<u64>::deserialize(deserializer)
|
||||
.map(std::time::Duration::from_millis)
|
||||
.map(Duration)
|
||||
.map_err(|_| serde::de::Error::custom("invalid Duration"))
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<std::time::Duration> for Duration {
|
||||
fn as_ref(&self) -> &std::time::Duration {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialOrd for Duration {
|
||||
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
|
||||
Some(self.cmp(other))
|
||||
}
|
||||
}
|
||||
|
||||
impl Ord for Duration {
|
||||
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
|
||||
self.0.cmp(&other.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for Duration {
|
||||
type Err = String;
|
||||
|
||||
fn from_str(value: &str) -> Result<Self, Self::Err> {
|
||||
let mut digits = String::new();
|
||||
let mut multiplier = String::new();
|
||||
|
||||
for ch in value.chars() {
|
||||
if ch.is_ascii_digit() {
|
||||
if !multiplier.is_empty() {
|
||||
return Err(format!("Invalid duration value {:?}.", value));
|
||||
}
|
||||
|
||||
digits.push(ch);
|
||||
} else if !ch.is_ascii_whitespace() {
|
||||
multiplier.push(ch.to_ascii_lowercase());
|
||||
}
|
||||
}
|
||||
|
||||
let multiplier = match multiplier.as_str() {
|
||||
"d" => 24 * 60 * 60 * 1000,
|
||||
"h" => 60 * 60 * 1000,
|
||||
"m" => 60 * 1000,
|
||||
"s" => 1000,
|
||||
"ms" | "" => 1,
|
||||
_ => return Err(format!("Invalid duration value {:?}.", value)),
|
||||
};
|
||||
|
||||
digits
|
||||
.parse::<u64>()
|
||||
.ok()
|
||||
.and_then(|num| num.checked_mul(multiplier))
|
||||
.map(std::time::Duration::from_millis)
|
||||
.map(Duration)
|
||||
.ok_or_else(|| format!("Invalid duration value {:?}.", value))
|
||||
}
|
||||
}
|
||||
|
||||
impl Pickle for Duration {
|
||||
fn pickle(&self, out: &mut Vec<u8>) {
|
||||
(self.0.as_millis() as u64).pickle(out);
|
||||
}
|
||||
|
||||
fn unpickle(data: &mut PickledStream<'_>) -> Option<Self> {
|
||||
u64::unpickle(data).map(|timestamp| Duration(std::time::Duration::from_millis(timestamp)))
|
||||
}
|
||||
}
|
||||
|
||||
impl RegistryJsonPatch for Duration {
|
||||
fn patch<'x>(
|
||||
&mut self,
|
||||
mut pointer: JsonPointerPatch<'_>,
|
||||
value: JmapValue<'x>,
|
||||
) -> PatchResult<'x> {
|
||||
match (value, pointer.next()) {
|
||||
(jmap_tools::Value::Number(value), None) => {
|
||||
if let Some(new_value) = value.as_u64().filter(|v| *v > 0) {
|
||||
*self = Duration::from_millis(new_value);
|
||||
Ok(MaybeUnpatched::Patched)
|
||||
} else {
|
||||
Err(PatchError::new(pointer, "Invalid duration value"))
|
||||
}
|
||||
}
|
||||
_ => Err(PatchError::new(pointer, "Invalid path for Duration")),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoValue for Duration {
|
||||
fn into_value(self) -> JmapValue<'static> {
|
||||
JmapValue::Number((self.0.as_millis() as u64).into())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<std::time::Duration> for Duration {
|
||||
fn from(value: std::time::Duration) -> Self {
|
||||
Duration(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<u64> for Duration {
|
||||
fn from(value: u64) -> Self {
|
||||
Duration(std::time::Duration::from_millis(value))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,212 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use crate::{
|
||||
jmap::JsonPointerPatch,
|
||||
schema::prelude::Property,
|
||||
types::{EnumImpl, id::ObjectId},
|
||||
};
|
||||
use std::{borrow::Cow, fmt::Display};
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
|
||||
#[serde(tag = "type")]
|
||||
pub enum ValidationError {
|
||||
Invalid { property: Property, value: String },
|
||||
Required { property: Property },
|
||||
MaxLength { property: Property, required: usize },
|
||||
MinLength { property: Property, required: usize },
|
||||
MaxValue { property: Property, required: i64 },
|
||||
MinValue { property: Property, required: i64 },
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum Error {
|
||||
Validation {
|
||||
object_id: ObjectId,
|
||||
errors: Vec<ValidationError>,
|
||||
},
|
||||
Build {
|
||||
object_id: ObjectId,
|
||||
message: String,
|
||||
},
|
||||
Internal {
|
||||
object_id: Option<ObjectId>,
|
||||
error: trc::Error,
|
||||
},
|
||||
NotFound {
|
||||
object_id: ObjectId,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct PatchError {
|
||||
pub path: String,
|
||||
pub message: Cow<'static, str>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct Warning {
|
||||
pub object_id: ObjectId,
|
||||
pub property: Option<Property>,
|
||||
pub message: String,
|
||||
}
|
||||
|
||||
impl ValidationError {
|
||||
pub fn required(property: Property) -> Self {
|
||||
Self::Required { property }
|
||||
}
|
||||
|
||||
pub fn invalid(property: Property, value: impl Display) -> Self {
|
||||
Self::Invalid {
|
||||
property,
|
||||
value: value.to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn min_items(property: Property, required: usize) -> Self {
|
||||
Self::MinLength { property, required }
|
||||
}
|
||||
|
||||
pub fn max_items(property: Property, required: usize) -> Self {
|
||||
Self::MaxLength { property, required }
|
||||
}
|
||||
|
||||
pub fn max_length(property: Property, required: usize) -> Self {
|
||||
Self::MaxLength { property, required }
|
||||
}
|
||||
|
||||
pub fn min_length(property: Property, required: usize) -> Self {
|
||||
Self::MinLength { property, required }
|
||||
}
|
||||
|
||||
pub fn max_value(property: Property, required: i64) -> Self {
|
||||
Self::MaxValue { property, required }
|
||||
}
|
||||
|
||||
pub fn min_value(property: Property, required: i64) -> Self {
|
||||
Self::MinValue { property, required }
|
||||
}
|
||||
}
|
||||
|
||||
impl Warning {
|
||||
pub fn new(object_id: ObjectId, message: impl Display) -> Self {
|
||||
Self {
|
||||
object_id,
|
||||
property: None,
|
||||
message: message.to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn for_property(object_id: ObjectId, property: Property, message: impl Display) -> Self {
|
||||
Self {
|
||||
object_id,
|
||||
property: Some(property),
|
||||
message: message.to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn log(&self) {
|
||||
trc::event!(
|
||||
Registry(trc::RegistryEvent::BuildWarning),
|
||||
Source = self.object_id.object().as_str(),
|
||||
Id = self.object_id.id().id(),
|
||||
Key = self.property.map(|key| key.as_str()),
|
||||
Reason = self.message.clone(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
impl Error {
|
||||
pub fn log(&self) {
|
||||
match self {
|
||||
Error::Validation { object_id, errors } => {
|
||||
trc::event!(
|
||||
Registry(trc::RegistryEvent::ValidationError),
|
||||
Source = object_id.object().as_str(),
|
||||
Id = object_id.id().id(),
|
||||
Reason = errors
|
||||
.iter()
|
||||
.map(|err| trc::Value::from(err.to_string()))
|
||||
.collect::<Vec<_>>(),
|
||||
);
|
||||
}
|
||||
Error::Build { object_id, message } => {
|
||||
trc::event!(
|
||||
Registry(trc::RegistryEvent::BuildError),
|
||||
Source = object_id.object().as_str(),
|
||||
Id = object_id.id().id(),
|
||||
Reason = message.clone(),
|
||||
);
|
||||
}
|
||||
Error::Internal { object_id, error } => {
|
||||
trc::event!(
|
||||
Registry(trc::RegistryEvent::ReadError),
|
||||
Source = object_id.as_ref().map(|id| id.object().as_str()),
|
||||
Id = object_id.as_ref().map(|id| id.id().id()),
|
||||
CausedBy = error.clone(),
|
||||
);
|
||||
}
|
||||
Error::NotFound { object_id } => {
|
||||
trc::event!(
|
||||
Registry(trc::RegistryEvent::BuildError),
|
||||
Source = object_id.object().as_str(),
|
||||
Id = object_id.id().id(),
|
||||
Reason = "Object not found",
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for ValidationError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
ValidationError::Invalid { property, value } => {
|
||||
write!(f, "Invalid value '{}' for property '{}'", value, property)
|
||||
}
|
||||
ValidationError::Required { property } => {
|
||||
write!(f, "Property '{}' is required", property)
|
||||
}
|
||||
ValidationError::MaxLength { property, required } => {
|
||||
write!(
|
||||
f,
|
||||
"Property '{}' exceeds maximum length of {}",
|
||||
property, required
|
||||
)
|
||||
}
|
||||
ValidationError::MinLength { property, required } => {
|
||||
write!(
|
||||
f,
|
||||
"Property '{}' is below minimum length of {}",
|
||||
property, required
|
||||
)
|
||||
}
|
||||
ValidationError::MaxValue { property, required } => {
|
||||
write!(
|
||||
f,
|
||||
"Property '{}' exceeds maximum value of {}",
|
||||
property, required
|
||||
)
|
||||
}
|
||||
ValidationError::MinValue { property, required } => {
|
||||
write!(
|
||||
f,
|
||||
"Property '{}' is below minimum value of {}",
|
||||
property, required
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PatchError {
|
||||
pub fn new(path: JsonPointerPatch<'_>, message: impl Into<Cow<'static, str>>) -> Self {
|
||||
Self {
|
||||
path: path.path(),
|
||||
message: message.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,138 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use crate::{
|
||||
jmap::{IntoValue, JmapValue, JsonPointerPatch, PatchResult, RegistryJsonPatch},
|
||||
pickle::{Pickle, PickledStream},
|
||||
types::error::PatchError,
|
||||
};
|
||||
use std::{fmt::Display, str::FromStr};
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
#[repr(transparent)]
|
||||
pub struct Float(f64);
|
||||
|
||||
impl Eq for Float {}
|
||||
|
||||
impl PartialOrd for Float {
|
||||
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
|
||||
Some(self.cmp(other))
|
||||
}
|
||||
}
|
||||
|
||||
impl Ord for Float {
|
||||
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
|
||||
self.0.partial_cmp(&other.0).unwrap_or_else(|| {
|
||||
if self.0.is_nan() && other.0.is_nan() {
|
||||
std::cmp::Ordering::Equal
|
||||
} else if self.0.is_nan() {
|
||||
std::cmp::Ordering::Greater
|
||||
} else {
|
||||
std::cmp::Ordering::Less
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Float {
|
||||
pub fn new(value: f64) -> Self {
|
||||
Float(value)
|
||||
}
|
||||
|
||||
pub fn into_inner(self) -> f64 {
|
||||
self.0
|
||||
}
|
||||
|
||||
pub fn is_valid(&self) -> bool {
|
||||
!self.0.is_nan() && self.0.is_finite()
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for Float {
|
||||
type Err = String;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
s.parse::<f64>().map(Float).map_err(|err| err.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for Float {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{}", self.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl serde::Serialize for Float {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: serde::Serializer,
|
||||
{
|
||||
serializer.serialize_f64(self.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> serde::Deserialize<'de> for Float {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
f64::deserialize(deserializer)
|
||||
.map(Float::new)
|
||||
.map_err(|_| serde::de::Error::custom("invalid Float"))
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<f64> for Float {
|
||||
fn as_ref(&self) -> &f64 {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Float {
|
||||
fn default() -> Self {
|
||||
Float(f64::NAN)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<f64> for Float {
|
||||
fn from(value: f64) -> Self {
|
||||
Float(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl Pickle for Float {
|
||||
fn pickle(&self, out: &mut Vec<u8>) {
|
||||
self.0.to_bits().pickle(out);
|
||||
}
|
||||
|
||||
fn unpickle(data: &mut PickledStream<'_>) -> Option<Self> {
|
||||
u64::unpickle(data).map(|bits| Float(f64::from_bits(bits)))
|
||||
}
|
||||
}
|
||||
|
||||
impl RegistryJsonPatch for Float {
|
||||
fn patch<'x>(
|
||||
&mut self,
|
||||
pointer: JsonPointerPatch<'_>,
|
||||
value: JmapValue<'x>,
|
||||
) -> PatchResult<'x> {
|
||||
if let Some(new_value) = value.as_f64().filter(|v| v.is_finite() && !v.is_nan()) {
|
||||
*self = Float(new_value);
|
||||
pointer.assert_eof()
|
||||
} else {
|
||||
Err(PatchError::new(
|
||||
pointer,
|
||||
"Invalid value for float property (expected finite number)",
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoValue for Float {
|
||||
fn into_value(self) -> JmapValue<'static> {
|
||||
JmapValue::Number(self.0.into())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,179 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use crate::{
|
||||
jmap::{
|
||||
IntoValue, JmapValue, JsonPointerPatch, MaybeUnpatched, PatchResult, RegistryJsonPatch,
|
||||
RegistryValue,
|
||||
},
|
||||
pickle::{Pickle, PickledStream},
|
||||
schema::prelude::ObjectType,
|
||||
types::{EnumImpl, error::PatchError},
|
||||
};
|
||||
use std::{fmt::Display, str::FromStr};
|
||||
use types::{
|
||||
blob::{BlobClass, BlobId},
|
||||
blob_hash::{BLOB_HASH_LEN, BlobHash},
|
||||
id::Id,
|
||||
};
|
||||
|
||||
#[derive(Debug, PartialEq, Clone, Copy, Eq, Hash, serde::Serialize, serde::Deserialize)]
|
||||
pub struct ObjectId {
|
||||
object: ObjectType,
|
||||
id: Id,
|
||||
}
|
||||
|
||||
impl ObjectId {
|
||||
pub fn new(object: ObjectType, id: Id) -> Self {
|
||||
Self { object, id }
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn id(&self) -> Id {
|
||||
self.id
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn object(&self) -> ObjectType {
|
||||
self.object
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn is_valid(&self) -> bool {
|
||||
self.id.is_valid()
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for ObjectId {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{} with id {}", self.object.as_str(), self.id)
|
||||
}
|
||||
}
|
||||
|
||||
impl ObjectType {
|
||||
pub fn id(&self, id: Id) -> ObjectId {
|
||||
ObjectId::new(*self, id)
|
||||
}
|
||||
|
||||
pub fn singleton(&self) -> ObjectId {
|
||||
ObjectId::new(*self, Id::singleton())
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for ObjectId {
|
||||
fn default() -> Self {
|
||||
ObjectId::new(ObjectType::Account, Id::default())
|
||||
}
|
||||
}
|
||||
|
||||
impl Pickle for Id {
|
||||
fn pickle(&self, out: &mut Vec<u8>) {
|
||||
self.id().pickle(out);
|
||||
}
|
||||
|
||||
fn unpickle(data: &mut PickledStream<'_>) -> Option<Self> {
|
||||
u64::unpickle(data).map(Id::new)
|
||||
}
|
||||
}
|
||||
|
||||
impl Pickle for BlobId {
|
||||
fn pickle(&self, out: &mut Vec<u8>) {
|
||||
out.extend_from_slice(self.hash.as_slice());
|
||||
}
|
||||
|
||||
fn unpickle(stream: &mut PickledStream<'_>) -> Option<Self> {
|
||||
stream.read_bytes(BLOB_HASH_LEN).map(|bytes| {
|
||||
BlobId::new(
|
||||
BlobHash::try_from_hash_slice(bytes).unwrap(),
|
||||
BlobClass::default(),
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl RegistryJsonPatch for Id {
|
||||
fn patch<'x>(
|
||||
&mut self,
|
||||
mut pointer: JsonPointerPatch<'_>,
|
||||
value: JmapValue<'x>,
|
||||
) -> PatchResult<'x> {
|
||||
match (value, pointer.next()) {
|
||||
(jmap_tools::Value::Element(RegistryValue::Id(value)), None) => {
|
||||
*self = value;
|
||||
Ok(MaybeUnpatched::Patched)
|
||||
}
|
||||
(jmap_tools::Value::Str(value), None) => {
|
||||
if let Ok(new_value) = Id::from_str(value.as_ref()) {
|
||||
*self = new_value;
|
||||
Ok(MaybeUnpatched::Patched)
|
||||
} else {
|
||||
Err(PatchError::new(pointer, "Failed to parse Id from string"))
|
||||
}
|
||||
}
|
||||
_ => Err(PatchError::new(
|
||||
pointer,
|
||||
"Invalid path for Id, expected a string value",
|
||||
)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl RegistryJsonPatch for BlobId {
|
||||
fn patch<'x>(
|
||||
&mut self,
|
||||
mut pointer: JsonPointerPatch<'_>,
|
||||
value: JmapValue<'x>,
|
||||
) -> PatchResult<'x> {
|
||||
match (value, pointer.next()) {
|
||||
(jmap_tools::Value::Element(RegistryValue::BlobId(value)), None) => {
|
||||
*self = value;
|
||||
Ok(MaybeUnpatched::Patched)
|
||||
}
|
||||
(jmap_tools::Value::Str(value), None) => {
|
||||
if let Ok(new_value) = BlobId::from_str(value.as_ref()) {
|
||||
*self = new_value;
|
||||
Ok(MaybeUnpatched::Patched)
|
||||
} else {
|
||||
Err(PatchError::new(
|
||||
pointer,
|
||||
"Failed to parse BlobId from string",
|
||||
))
|
||||
}
|
||||
}
|
||||
_ => Err(PatchError::new(
|
||||
pointer,
|
||||
"Invalid path for BlobId, expected a string value",
|
||||
)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoValue for Id {
|
||||
fn into_value(self) -> JmapValue<'static> {
|
||||
JmapValue::Element(RegistryValue::Id(self))
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoValue for BlobId {
|
||||
fn into_value(self) -> JmapValue<'static> {
|
||||
JmapValue::Element(RegistryValue::BlobId(self))
|
||||
}
|
||||
}
|
||||
|
||||
impl Ord for ObjectId {
|
||||
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
|
||||
match self.object.to_id().cmp(&other.object.to_id()) {
|
||||
std::cmp::Ordering::Equal => self.id.cmp(&other.id),
|
||||
ord => ord,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialOrd for ObjectId {
|
||||
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
|
||||
Some(self.cmp(other))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,234 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use crate::{
|
||||
schema::prelude::{ObjectType, Property},
|
||||
types::{id::ObjectId, ipmask::IpAddrOrMask},
|
||||
};
|
||||
use ahash::AHashSet;
|
||||
use std::borrow::Cow;
|
||||
use types::id::Id;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
|
||||
pub enum IndexKey<'x> {
|
||||
Unique {
|
||||
property: Property,
|
||||
value_1: IndexValue<'x>,
|
||||
value_2: IndexValue<'x>,
|
||||
global: bool,
|
||||
},
|
||||
Search {
|
||||
property: Property,
|
||||
value: IndexValue<'x>,
|
||||
},
|
||||
ForeignKey {
|
||||
object_id: ObjectId,
|
||||
type_filter: IndexValue<'x>,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||
pub struct ObjectFilter<'x> {
|
||||
pub property: Property,
|
||||
pub value: IndexValue<'x>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
|
||||
pub enum IndexValue<'x> {
|
||||
Text(Cow<'x, str>),
|
||||
Bytes(Vec<u8>),
|
||||
U64(u64),
|
||||
I64(i64),
|
||||
U16(u16),
|
||||
None,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
pub struct IndexSchema {
|
||||
pub prop: Property,
|
||||
pub typ: IndexSchemaType,
|
||||
pub value: IndexSchemaValueType,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
#[repr(u8)]
|
||||
pub enum IndexSchemaType {
|
||||
Unique,
|
||||
Search,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
#[repr(u8)]
|
||||
pub enum IndexSchemaValueType {
|
||||
Keyword,
|
||||
Text,
|
||||
Number,
|
||||
Enum,
|
||||
Boolean,
|
||||
Id,
|
||||
IpMask,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
|
||||
pub struct IndexBuilder<'x> {
|
||||
pub keys: AHashSet<IndexKey<'x>>,
|
||||
}
|
||||
|
||||
impl<'x> IndexBuilder<'x> {
|
||||
pub fn typ(&mut self, typ: u16) {
|
||||
self.keys.insert(IndexKey::Search {
|
||||
property: Property::Type,
|
||||
value: IndexValue::U16(typ),
|
||||
});
|
||||
}
|
||||
|
||||
pub fn unique(&mut self, property: Property, value: impl Into<IndexValue<'x>>) {
|
||||
self.keys.insert(IndexKey::Unique {
|
||||
property,
|
||||
value_1: value.into(),
|
||||
value_2: IndexValue::None,
|
||||
global: false,
|
||||
});
|
||||
}
|
||||
|
||||
pub fn search(&mut self, property: Property, value: impl Into<IndexValue<'x>>) {
|
||||
let value = value.into();
|
||||
if value != IndexValue::None {
|
||||
self.keys.insert(IndexKey::Search { property, value });
|
||||
}
|
||||
}
|
||||
|
||||
pub fn text(&mut self, property: Property, value: &'x str) {
|
||||
for word in value
|
||||
.split(|c: char| !c.is_alphanumeric())
|
||||
.filter(|s| s.len() > 1)
|
||||
{
|
||||
if word
|
||||
.chars()
|
||||
.all(|ch| ch.is_lowercase() || !ch.is_alphabetic())
|
||||
{
|
||||
self.keys.insert(IndexKey::Search {
|
||||
property,
|
||||
value: IndexValue::Text(Cow::Borrowed(word)),
|
||||
});
|
||||
} else {
|
||||
self.keys.insert(IndexKey::Search {
|
||||
property,
|
||||
value: IndexValue::Text(Cow::Owned(word.to_lowercase())),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn unique_global(&mut self, property: Property, value: impl Into<IndexValue<'x>>) {
|
||||
self.keys.insert(IndexKey::Unique {
|
||||
property,
|
||||
value_1: value.into(),
|
||||
value_2: IndexValue::None,
|
||||
global: true,
|
||||
});
|
||||
}
|
||||
|
||||
pub fn unique_global_composite(
|
||||
&mut self,
|
||||
property: Property,
|
||||
value: impl Into<IndexValue<'x>>,
|
||||
composite: impl Into<IndexValue<'x>>,
|
||||
) {
|
||||
self.keys.insert(IndexKey::Unique {
|
||||
property,
|
||||
value_1: value.into(),
|
||||
value_2: composite.into(),
|
||||
global: true,
|
||||
});
|
||||
}
|
||||
|
||||
pub fn foreign_key(&mut self, object: ObjectType, id: Option<Id>, type_filter: Option<u16>) {
|
||||
if let Some(id) = id {
|
||||
self.keys.insert(IndexKey::ForeignKey {
|
||||
object_id: ObjectId::new(object, id),
|
||||
type_filter: type_filter.map(IndexValue::U16).unwrap_or(IndexValue::None),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.keys.is_empty()
|
||||
}
|
||||
}
|
||||
|
||||
impl IndexSchema {
|
||||
pub const fn new(prop: Property, typ: IndexSchemaType, value: IndexSchemaValueType) -> Self {
|
||||
Self { prop, typ, value }
|
||||
}
|
||||
}
|
||||
|
||||
impl From<u64> for IndexValue<'_> {
|
||||
fn from(value: u64) -> Self {
|
||||
IndexValue::U64(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&u64> for IndexValue<'_> {
|
||||
fn from(value: &u64) -> Self {
|
||||
IndexValue::U64(*value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<i64> for IndexValue<'_> {
|
||||
fn from(value: i64) -> Self {
|
||||
IndexValue::I64(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&i64> for IndexValue<'_> {
|
||||
fn from(value: &i64) -> Self {
|
||||
IndexValue::I64(*value)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'x> From<&'x IpAddrOrMask> for IndexValue<'x> {
|
||||
fn from(value: &'x IpAddrOrMask) -> Self {
|
||||
IndexValue::Bytes(value.to_index_key())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'x> From<&'x trc::EventType> for IndexValue<'x> {
|
||||
fn from(value: &'x trc::EventType) -> Self {
|
||||
IndexValue::U16(value.to_id())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'x> From<&'x str> for IndexValue<'x> {
|
||||
fn from(value: &'x str) -> Self {
|
||||
IndexValue::Text(value.into())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'x> From<&'x String> for IndexValue<'x> {
|
||||
fn from(value: &'x String) -> Self {
|
||||
IndexValue::Text(Cow::Borrowed(value.as_str()))
|
||||
}
|
||||
}
|
||||
|
||||
impl<'x> From<&'x Id> for IndexValue<'x> {
|
||||
fn from(value: &'x Id) -> Self {
|
||||
IndexValue::U64(value.id())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'x, T> From<&'x Option<T>> for IndexValue<'x>
|
||||
where
|
||||
IndexValue<'x>: std::convert::From<&'x T>,
|
||||
{
|
||||
fn from(value: &'x Option<T>) -> Self {
|
||||
match value {
|
||||
Some(id) => id.into(),
|
||||
None => IndexValue::None,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,152 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use crate::{
|
||||
jmap::{
|
||||
IntoValue, JmapValue, JsonPointerPatch, MaybeUnpatched, PatchResult, RegistryJsonPatch,
|
||||
},
|
||||
pickle::{Pickle, PickledStream},
|
||||
types::error::PatchError,
|
||||
};
|
||||
use std::{fmt::Display, net::Ipv4Addr, str::FromStr};
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
#[repr(transparent)]
|
||||
pub struct IpAddr(pub std::net::IpAddr);
|
||||
|
||||
impl IpAddr {
|
||||
pub fn into_inner(self) -> std::net::IpAddr {
|
||||
self.0
|
||||
}
|
||||
|
||||
pub fn is_valid(&self) -> bool {
|
||||
!matches!(
|
||||
self.0,
|
||||
std::net::IpAddr::V4(addr) if addr == Ipv4Addr::UNSPECIFIED
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for IpAddr {
|
||||
type Err = String;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
s.parse::<std::net::IpAddr>()
|
||||
.map(IpAddr)
|
||||
.map_err(|err| err.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for IpAddr {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{}", self.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl serde::Serialize for IpAddr {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: serde::Serializer,
|
||||
{
|
||||
serializer.serialize_str(self.to_string().as_str())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> serde::Deserialize<'de> for IpAddr {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
IpAddr::from_str(<&str>::deserialize(deserializer)?)
|
||||
.map_err(|_| serde::de::Error::custom("invalid IpAddr"))
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<std::net::IpAddr> for IpAddr {
|
||||
fn as_ref(&self) -> &std::net::IpAddr {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for IpAddr {
|
||||
fn default() -> Self {
|
||||
IpAddr(std::net::IpAddr::V4(Ipv4Addr::UNSPECIFIED))
|
||||
}
|
||||
}
|
||||
|
||||
impl Pickle for std::net::IpAddr {
|
||||
fn pickle(&self, out: &mut Vec<u8>) {
|
||||
match self {
|
||||
std::net::IpAddr::V4(addr) => {
|
||||
out.push(4);
|
||||
out.extend_from_slice(&addr.octets());
|
||||
}
|
||||
std::net::IpAddr::V6(addr) => {
|
||||
out.push(6);
|
||||
out.extend_from_slice(&addr.octets());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn unpickle(data: &mut PickledStream<'_>) -> Option<Self> {
|
||||
let kind = data.read()?;
|
||||
match kind {
|
||||
4 => {
|
||||
let mut arr = [0u8; 4];
|
||||
arr.copy_from_slice(data.read_bytes(4)?);
|
||||
Some(std::net::IpAddr::V4(Ipv4Addr::from(arr)))
|
||||
}
|
||||
6 => {
|
||||
let mut arr = [0u8; 16];
|
||||
arr.copy_from_slice(data.read_bytes(16)?);
|
||||
Some(std::net::IpAddr::V6(std::net::Ipv6Addr::from(arr)))
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Pickle for IpAddr {
|
||||
fn pickle(&self, out: &mut Vec<u8>) {
|
||||
self.0.pickle(out);
|
||||
}
|
||||
|
||||
fn unpickle(data: &mut PickledStream<'_>) -> Option<Self> {
|
||||
std::net::IpAddr::unpickle(data).map(IpAddr)
|
||||
}
|
||||
}
|
||||
|
||||
impl RegistryJsonPatch for IpAddr {
|
||||
fn patch<'x>(
|
||||
&mut self,
|
||||
mut pointer: JsonPointerPatch<'_>,
|
||||
value: JmapValue<'x>,
|
||||
) -> PatchResult<'x> {
|
||||
match (value, pointer.next()) {
|
||||
(jmap_tools::Value::Str(value), None) => {
|
||||
if let Ok(new_value) = IpAddr::from_str(value.as_ref()) {
|
||||
*self = new_value;
|
||||
Ok(MaybeUnpatched::Patched)
|
||||
} else {
|
||||
Err(PatchError::new(
|
||||
pointer,
|
||||
"Failed to parse IpAddr from string",
|
||||
))
|
||||
}
|
||||
}
|
||||
_ => Err(PatchError::new(
|
||||
pointer,
|
||||
"Invalid path for IpAddr, expected a string value",
|
||||
)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoValue for IpAddr {
|
||||
fn into_value(self) -> JmapValue<'static> {
|
||||
JmapValue::Str(self.to_string().into())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,326 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use crate::{
|
||||
jmap::{
|
||||
IntoValue, JmapValue, JsonPointerPatch, MaybeUnpatched, PatchResult, RegistryJsonPatch,
|
||||
},
|
||||
pickle::{Pickle, PickledStream},
|
||||
types::error::PatchError,
|
||||
};
|
||||
use std::{
|
||||
fmt::{Display, Formatter},
|
||||
net::{IpAddr, Ipv4Addr, Ipv6Addr},
|
||||
str::FromStr,
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||
pub enum IpAddrOrMask {
|
||||
V4 { addr: Ipv4Addr, mask: u32 },
|
||||
V6 { addr: Ipv6Addr, mask: u128 },
|
||||
}
|
||||
|
||||
impl IpAddrOrMask {
|
||||
pub fn from_ip(ip: IpAddr) -> Self {
|
||||
match ip {
|
||||
IpAddr::V4(addr) => IpAddrOrMask::V4 {
|
||||
addr,
|
||||
mask: u32::MAX,
|
||||
},
|
||||
IpAddr::V6(addr) => IpAddrOrMask::V6 {
|
||||
addr,
|
||||
mask: u128::MAX,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_valid(&self) -> bool {
|
||||
!matches!(
|
||||
self,
|
||||
IpAddrOrMask::V4 { addr, mask: _ } if addr == &Ipv4Addr::UNSPECIFIED
|
||||
)
|
||||
}
|
||||
|
||||
pub fn try_to_ip(&self) -> Option<IpAddr> {
|
||||
match self {
|
||||
IpAddrOrMask::V4 { addr, mask } if *mask == u32::MAX => Some(IpAddr::V4(*addr)),
|
||||
IpAddrOrMask::V6 { addr, mask } if *mask == u128::MAX => Some(IpAddr::V6(*addr)),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn into_inner(self) -> (IpAddr, u128) {
|
||||
match self {
|
||||
IpAddrOrMask::V4 { addr, mask } => (IpAddr::V4(addr), mask as u128),
|
||||
IpAddrOrMask::V6 { addr, mask } => (IpAddr::V6(addr), mask),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn matches(&self, remote: &IpAddr) -> bool {
|
||||
match self {
|
||||
IpAddrOrMask::V4 { addr, mask } => match *mask {
|
||||
u32::MAX => match remote {
|
||||
IpAddr::V4(remote) => addr == remote,
|
||||
IpAddr::V6(remote) => {
|
||||
if let Some(remote) = remote.to_ipv4_mapped() {
|
||||
addr == &remote
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
},
|
||||
0 => {
|
||||
matches!(remote, IpAddr::V4(_))
|
||||
}
|
||||
_ => {
|
||||
u32::from_be_bytes(match remote {
|
||||
IpAddr::V4(ip) => ip.octets(),
|
||||
IpAddr::V6(ip) => {
|
||||
if let Some(ip) = ip.to_ipv4() {
|
||||
ip.octets()
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}) & mask
|
||||
== u32::from_be_bytes(addr.octets()) & mask
|
||||
}
|
||||
},
|
||||
IpAddrOrMask::V6 { addr, mask } => match *mask {
|
||||
u128::MAX => match remote {
|
||||
IpAddr::V6(remote) => remote == addr,
|
||||
IpAddr::V4(remote) => &remote.to_ipv6_mapped() == addr,
|
||||
},
|
||||
0 => {
|
||||
matches!(remote, IpAddr::V6(_))
|
||||
}
|
||||
_ => {
|
||||
u128::from_be_bytes(match remote {
|
||||
IpAddr::V6(ip) => ip.octets(),
|
||||
IpAddr::V4(ip) => ip.to_ipv6_mapped().octets(),
|
||||
}) & mask
|
||||
== u128::from_be_bytes(addr.octets()) & mask
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub fn to_index_key(&self) -> Vec<u8> {
|
||||
match self {
|
||||
IpAddrOrMask::V4 { addr, mask } => {
|
||||
let mut bytes = Vec::with_capacity(8);
|
||||
bytes.extend_from_slice(&addr.octets());
|
||||
bytes.extend_from_slice(&mask.to_be_bytes());
|
||||
bytes
|
||||
}
|
||||
IpAddrOrMask::V6 { addr, mask } => {
|
||||
let mut bytes = Vec::with_capacity(24);
|
||||
bytes.extend_from_slice(&addr.octets());
|
||||
bytes.extend_from_slice(&mask.to_be_bytes());
|
||||
bytes
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for IpAddrOrMask {
|
||||
type Err = String;
|
||||
|
||||
fn from_str(value: &str) -> Result<Self, Self::Err> {
|
||||
if let Some((addr, mask)) = value.rsplit_once('/') {
|
||||
if let (Ok(addr), Ok(mask)) =
|
||||
(addr.trim().parse::<IpAddr>(), mask.trim().parse::<u32>())
|
||||
{
|
||||
match addr {
|
||||
IpAddr::V4(addr) if (8..=32).contains(&mask) => {
|
||||
return Ok(IpAddrOrMask::V4 {
|
||||
addr,
|
||||
mask: u32::MAX << (32 - mask),
|
||||
});
|
||||
}
|
||||
IpAddr::V6(addr) if (8..=128).contains(&mask) => {
|
||||
return Ok(IpAddrOrMask::V6 {
|
||||
addr,
|
||||
mask: u128::MAX << (128 - mask),
|
||||
});
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
} else {
|
||||
match value.trim().parse::<IpAddr>() {
|
||||
Ok(IpAddr::V4(addr)) => {
|
||||
return Ok(IpAddrOrMask::V4 {
|
||||
addr,
|
||||
mask: u32::MAX,
|
||||
});
|
||||
}
|
||||
Ok(IpAddr::V6(addr)) => {
|
||||
return Ok(IpAddrOrMask::V6 {
|
||||
addr,
|
||||
mask: u128::MAX,
|
||||
});
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
|
||||
Err(format!("Invalid IP address {:?}", value,))
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for IpAddrOrMask {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
IpAddrOrMask::V4 { addr, mask } => {
|
||||
if (*mask) == u32::MAX {
|
||||
write!(f, "{}", addr)
|
||||
} else {
|
||||
let prefix = mask.count_ones();
|
||||
write!(f, "{}/{}", addr, prefix)
|
||||
}
|
||||
}
|
||||
IpAddrOrMask::V6 { addr, mask } => {
|
||||
if (*mask) == u128::MAX {
|
||||
write!(f, "{}", addr)
|
||||
} else {
|
||||
let prefix = mask.count_ones();
|
||||
write!(f, "{}/{}", addr, prefix)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl serde::Serialize for IpAddrOrMask {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: serde::Serializer,
|
||||
{
|
||||
serializer.serialize_str(self.to_string().as_str())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> serde::Deserialize<'de> for IpAddrOrMask {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
IpAddrOrMask::from_str(<&str>::deserialize(deserializer)?)
|
||||
.map_err(|_| serde::de::Error::custom("invalid IpAddrOrMask"))
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for IpAddrOrMask {
|
||||
fn default() -> Self {
|
||||
IpAddrOrMask::V4 {
|
||||
addr: Ipv4Addr::UNSPECIFIED,
|
||||
mask: u32::MAX,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Pickle for IpAddrOrMask {
|
||||
fn pickle(&self, out: &mut Vec<u8>) {
|
||||
match self {
|
||||
IpAddrOrMask::V4 { addr, mask } => {
|
||||
out.push(4);
|
||||
out.extend_from_slice(&addr.octets());
|
||||
out.extend_from_slice(&mask.to_be_bytes());
|
||||
}
|
||||
IpAddrOrMask::V6 { addr, mask } => {
|
||||
out.push(6);
|
||||
out.extend_from_slice(&addr.octets());
|
||||
out.extend_from_slice(&mask.to_be_bytes());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn unpickle(data: &mut PickledStream<'_>) -> Option<Self> {
|
||||
match data.read()? {
|
||||
4 => {
|
||||
let mut addr_arr = [0u8; 4];
|
||||
addr_arr.copy_from_slice(data.read_bytes(4)?);
|
||||
let mut mask_arr = [0u8; 4];
|
||||
mask_arr.copy_from_slice(data.read_bytes(4)?);
|
||||
Some(IpAddrOrMask::V4 {
|
||||
addr: Ipv4Addr::from(addr_arr),
|
||||
mask: u32::from_be_bytes(mask_arr),
|
||||
})
|
||||
}
|
||||
6 => {
|
||||
let mut addr_arr = [0u8; 16];
|
||||
addr_arr.copy_from_slice(data.read_bytes(16)?);
|
||||
let mut mask_arr = [0u8; 16];
|
||||
mask_arr.copy_from_slice(data.read_bytes(16)?);
|
||||
Some(IpAddrOrMask::V6 {
|
||||
addr: Ipv6Addr::from(addr_arr),
|
||||
mask: u128::from_be_bytes(mask_arr),
|
||||
})
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl RegistryJsonPatch for IpAddrOrMask {
|
||||
fn patch<'x>(
|
||||
&mut self,
|
||||
mut pointer: JsonPointerPatch<'_>,
|
||||
value: JmapValue<'x>,
|
||||
) -> PatchResult<'x> {
|
||||
match (value, pointer.next()) {
|
||||
(jmap_tools::Value::Str(value), None) => {
|
||||
if let Ok(new_value) = IpAddrOrMask::from_str(value.as_ref()) {
|
||||
*self = new_value;
|
||||
Ok(MaybeUnpatched::Patched)
|
||||
} else {
|
||||
Err(PatchError::new(
|
||||
pointer,
|
||||
"Failed to parse IpAddrOrMask from string",
|
||||
))
|
||||
}
|
||||
}
|
||||
_ => Err(PatchError::new(
|
||||
pointer,
|
||||
"Invalid path for IpAddrOrMask, expected a string value",
|
||||
)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoValue for IpAddrOrMask {
|
||||
fn into_value(self) -> JmapValue<'static> {
|
||||
JmapValue::Str(self.to_string().into())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_ipaddrmask() {
|
||||
for (mask, ip) in [
|
||||
("10.0.0.0/8", "10.30.20.11"),
|
||||
("10.0.0.0/8", "10.0.13.73"),
|
||||
("192.168.1.1", "192.168.1.1"),
|
||||
] {
|
||||
let mask = IpAddrOrMask::from_str(mask).unwrap();
|
||||
let ip = ip.parse::<IpAddr>().unwrap();
|
||||
assert!(mask.matches(&ip));
|
||||
}
|
||||
|
||||
for (mask, ip) in [
|
||||
("10.0.0.0/8", "11.30.20.11"),
|
||||
("192.168.1.1", "193.168.1.1"),
|
||||
] {
|
||||
let mask = IpAddrOrMask::from_str(mask).unwrap();
|
||||
let ip = ip.parse::<IpAddr>().unwrap();
|
||||
assert!(!mask.matches(&ip));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,235 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use crate::{
|
||||
jmap::{
|
||||
IntoValue, JmapValue, JsonPointerPatch, MaybeUnpatched, PatchResult, RegistryJsonPatch,
|
||||
},
|
||||
pickle::{Pickle, PickledStream},
|
||||
types::error::PatchError,
|
||||
};
|
||||
use jmap_tools::{JsonPointerItem, Key, Value};
|
||||
use serde::{
|
||||
Deserialize, Deserializer, Serialize, Serializer,
|
||||
de::{self, MapAccess, Visitor},
|
||||
ser::SerializeMap,
|
||||
};
|
||||
use std::{
|
||||
borrow::Cow,
|
||||
fmt::{self, Debug},
|
||||
marker::PhantomData,
|
||||
};
|
||||
use utils::map::vec_map::{KeyValue, VecMap};
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Default)]
|
||||
pub struct List<T>(pub VecMap<u32, T>);
|
||||
|
||||
impl<T> List<T> {
|
||||
pub fn with_capacity(capacity: usize) -> Self {
|
||||
Self(VecMap::with_capacity(capacity))
|
||||
}
|
||||
|
||||
pub fn len(&self) -> usize {
|
||||
self.0.len()
|
||||
}
|
||||
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.0.is_empty()
|
||||
}
|
||||
|
||||
pub fn iter(&self) -> impl Iterator<Item = &T> {
|
||||
self.0.values()
|
||||
}
|
||||
|
||||
pub fn values(&self) -> impl Iterator<Item = &T> {
|
||||
self.0.values()
|
||||
}
|
||||
|
||||
pub fn values_mut(&mut self) -> impl Iterator<Item = &mut T> {
|
||||
self.0.values_mut()
|
||||
}
|
||||
|
||||
pub fn push(&mut self, item: T) {
|
||||
let next_index = self.0.last().map(|(k, _)| *k + 1).unwrap_or(0);
|
||||
self.0.append(next_index, item);
|
||||
}
|
||||
|
||||
pub fn push_unchecked(&mut self, item: T) {
|
||||
let next_index = self.0.len() as u32;
|
||||
self.0.append(next_index, item);
|
||||
}
|
||||
|
||||
pub fn inner_mut(&mut self) -> &mut VecMap<u32, T> {
|
||||
&mut self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Pickle for List<T>
|
||||
where
|
||||
T: Pickle,
|
||||
{
|
||||
fn pickle(&self, out: &mut Vec<u8>) {
|
||||
(self.0.len() as u32).pickle(out);
|
||||
for item in self.0.values() {
|
||||
item.pickle(out);
|
||||
}
|
||||
}
|
||||
|
||||
fn unpickle(stream: &mut PickledStream<'_>) -> Option<Self> {
|
||||
let len = u32::unpickle(stream)? as usize;
|
||||
let mut vec = Self::with_capacity(len);
|
||||
for _ in 0..len {
|
||||
vec.push_unchecked(T::unpickle(stream)?);
|
||||
}
|
||||
Some(vec)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: RegistryJsonPatch + Default + Debug> RegistryJsonPatch for List<T> {
|
||||
fn patch<'x>(
|
||||
&mut self,
|
||||
mut pointer: JsonPointerPatch<'_>,
|
||||
value: JmapValue<'x>,
|
||||
) -> PatchResult<'x> {
|
||||
match (pointer.next(), value) {
|
||||
(Some(JsonPointerItem::Number(key)), value) => {
|
||||
let key = *key as u32;
|
||||
if matches!(value, Value::Null) && !pointer.has_next() {
|
||||
if self.0.remove(&key).is_some() {
|
||||
return Ok(MaybeUnpatched::Patched);
|
||||
}
|
||||
} else {
|
||||
let result = self.0.get_mut_or_insert(key).patch(pointer, value);
|
||||
self.0.sort_unstable_by_key();
|
||||
return result;
|
||||
}
|
||||
}
|
||||
(Some(JsonPointerItem::Key(key)), value) => {
|
||||
if let Ok(key) = key.to_string().parse::<u32>() {
|
||||
if matches!(value, Value::Null) && !pointer.has_next() {
|
||||
if self.0.remove(&key).is_some() {
|
||||
return Ok(MaybeUnpatched::Patched);
|
||||
}
|
||||
} else {
|
||||
let result = self.0.get_mut_or_insert(key).patch(pointer, value);
|
||||
self.0.sort_unstable_by_key();
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
(None, Value::Object(items)) => {
|
||||
self.0.clear();
|
||||
for (key, value) in items.into_vec() {
|
||||
if let Ok(key) = key.to_string().parse::<u32>() {
|
||||
let mut inner = T::default();
|
||||
inner.patch(pointer.clone(), value)?;
|
||||
self.0.set(key, inner);
|
||||
} else {
|
||||
return Err(PatchError::new(
|
||||
pointer.clone(),
|
||||
"Invalid key for object property",
|
||||
));
|
||||
}
|
||||
}
|
||||
self.0.sort_unstable_by_key();
|
||||
return Ok(MaybeUnpatched::Patched);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
Err(PatchError::new(
|
||||
pointer,
|
||||
"Invalid value for object property",
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
impl<V: IntoValue> IntoValue for List<V> {
|
||||
fn into_value(self) -> JmapValue<'static> {
|
||||
let mut map = jmap_tools::Map::with_capacity(self.0.len());
|
||||
for (idx, v) in self.0 {
|
||||
map.insert_unchecked(Key::Owned(idx.to_string()), v.into_value());
|
||||
}
|
||||
|
||||
JmapValue::Object(map)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Serialize> Serialize for List<T> {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
let mut map = serializer.serialize_map(Some(self.0.len()))?;
|
||||
for (key, value) in &self.0 {
|
||||
map.serialize_entry(&key.to_string(), value)?;
|
||||
}
|
||||
map.end()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de, T: Deserialize<'de>> Deserialize<'de> for List<T> {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
struct ListVisitor<T>(PhantomData<T>);
|
||||
|
||||
impl<'de, T: Deserialize<'de>> Visitor<'de> for ListVisitor<T> {
|
||||
type Value = List<T>;
|
||||
|
||||
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
|
||||
formatter.write_str("a map of string keys to values")
|
||||
}
|
||||
|
||||
fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
|
||||
where
|
||||
A: MapAccess<'de>,
|
||||
{
|
||||
let mut items = VecMap::with_capacity(map.size_hint().unwrap_or(0));
|
||||
|
||||
while let Some(key) = map.next_key::<Cow<str>>()? {
|
||||
let id: u32 = key
|
||||
.parse()
|
||||
.map_err(|_| de::Error::custom(format!("invalid integer key: {key}")))?;
|
||||
let value: T = map.next_value()?;
|
||||
items.set(id, value);
|
||||
}
|
||||
|
||||
items.sort_unstable_by_key();
|
||||
|
||||
Ok(List(items))
|
||||
}
|
||||
}
|
||||
|
||||
deserializer.deserialize_map(ListVisitor(PhantomData))
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> FromIterator<T> for List<T> {
|
||||
fn from_iter<I: IntoIterator<Item = T>>(iter: I) -> Self {
|
||||
Self(VecMap::from_iter(
|
||||
iter.into_iter().enumerate().map(|(i, v)| (i as u32, v)),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> From<Vec<T>> for List<T> {
|
||||
fn from(vec: Vec<T>) -> Self {
|
||||
Self(VecMap::from_iter(
|
||||
vec.into_iter().enumerate().map(|(i, v)| (i as u32, v)),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> IntoIterator for List<T> {
|
||||
type Item = T;
|
||||
type IntoIter = std::iter::Map<std::vec::IntoIter<KeyValue<u32, T>>, fn(KeyValue<u32, T>) -> T>;
|
||||
|
||||
fn into_iter(self) -> Self::IntoIter {
|
||||
self.0.inner.into_iter().map(|kv| kv.value)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,444 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use crate::{
|
||||
jmap::{
|
||||
IntoValue, JmapValue, JsonPointerPatch, MaybeUnpatched, PatchResult, RegistryJsonPatch,
|
||||
},
|
||||
pickle::{Pickle, PickledStream},
|
||||
schema::prelude::SocketAddr,
|
||||
types::{EnumImpl, error::PatchError, ipaddr::IpAddr, ipmask::IpAddrOrMask},
|
||||
};
|
||||
use jmap_tools::{JsonPointerItem, Key, Value};
|
||||
use serde::{
|
||||
Deserialize, Deserializer, Serialize, Serializer,
|
||||
de::{self, MapAccess, Visitor},
|
||||
ser::SerializeMap,
|
||||
};
|
||||
use std::{
|
||||
borrow::Cow,
|
||||
fmt::{self, Debug},
|
||||
marker::PhantomData,
|
||||
str::FromStr,
|
||||
};
|
||||
use types::id::Id;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Default)]
|
||||
pub struct Map<T: MapItem>(Vec<T>);
|
||||
|
||||
impl<T: MapItem> Map<T> {
|
||||
pub fn new(items: Vec<T>) -> Self {
|
||||
Self(items)
|
||||
}
|
||||
|
||||
pub fn with_capacity(capacity: usize) -> Self {
|
||||
Self(Vec::with_capacity(capacity))
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn contains(&self, item: &T) -> bool {
|
||||
self.0.contains(item)
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn len(&self) -> usize {
|
||||
self.0.len()
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.0.is_empty()
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn into_inner(self) -> Vec<T> {
|
||||
self.0
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn inner_mut(&mut self) -> &mut Vec<T> {
|
||||
&mut self.0
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn iter(&self) -> impl Iterator<Item = &T> {
|
||||
self.0.iter()
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn iter_mut(&mut self) -> impl Iterator<Item = &mut T> {
|
||||
self.0.iter_mut()
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn as_slice(&self) -> &[T] {
|
||||
&self.0
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn push(&mut self, item: T) {
|
||||
if !self.0.contains(&item) {
|
||||
self.0.push(item);
|
||||
}
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn push_unchecked(&mut self, item: T) {
|
||||
self.0.push(item);
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn clear(&mut self) {
|
||||
self.0.clear();
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Pickle for Map<T>
|
||||
where
|
||||
T: Pickle + MapItem,
|
||||
{
|
||||
fn pickle(&self, out: &mut Vec<u8>) {
|
||||
(self.0.len() as u32).pickle(out);
|
||||
for item in &self.0 {
|
||||
item.pickle(out);
|
||||
}
|
||||
}
|
||||
|
||||
fn unpickle(stream: &mut PickledStream<'_>) -> Option<Self> {
|
||||
let len = u32::unpickle(stream)? as usize;
|
||||
let mut vec = Vec::with_capacity(len);
|
||||
for _ in 0..len {
|
||||
vec.push(T::unpickle(stream)?);
|
||||
}
|
||||
Some(Self(vec))
|
||||
}
|
||||
}
|
||||
|
||||
impl<V: IntoValue + MapItem> IntoValue for Map<V> {
|
||||
fn into_value(self) -> JmapValue<'static> {
|
||||
let mut map = jmap_tools::Map::with_capacity(self.0.len());
|
||||
for v in self.0 {
|
||||
let key = match v.into_string() {
|
||||
Cow::Borrowed(s) => Key::Borrowed(s),
|
||||
Cow::Owned(s) => Key::Owned(s),
|
||||
};
|
||||
map.insert_unchecked(key, Value::Bool(true));
|
||||
}
|
||||
|
||||
JmapValue::Object(map)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: MapItem + Default> RegistryJsonPatch for Map<T> {
|
||||
fn patch<'x>(
|
||||
&mut self,
|
||||
mut pointer: JsonPointerPatch<'_>,
|
||||
value: JmapValue<'x>,
|
||||
) -> PatchResult<'x> {
|
||||
match (pointer.next(), value) {
|
||||
(Some(JsonPointerItem::Number(idx)), Value::Null | Value::Bool(false)) => {
|
||||
let key = T::try_from_integer(*idx);
|
||||
|
||||
if !pointer.has_next()
|
||||
&& let Some(key) = key
|
||||
{
|
||||
self.0.retain(|item| item != &key);
|
||||
return Ok(MaybeUnpatched::Patched);
|
||||
}
|
||||
}
|
||||
(Some(JsonPointerItem::Key(key)), Value::Null | Value::Bool(false)) => {
|
||||
let key = T::try_from_string(key.to_string().as_ref());
|
||||
|
||||
if !pointer.has_next()
|
||||
&& let Some(key) = key
|
||||
{
|
||||
self.0.retain(|item| item != &key);
|
||||
return Ok(MaybeUnpatched::Patched);
|
||||
}
|
||||
}
|
||||
(Some(JsonPointerItem::Key(key)), Value::Bool(true)) => {
|
||||
let key = T::try_from_string(key.to_string().as_ref());
|
||||
|
||||
if !pointer.has_next()
|
||||
&& let Some(key) = key
|
||||
{
|
||||
if !self.0.contains(&key) {
|
||||
self.0.push(key);
|
||||
}
|
||||
|
||||
return Ok(MaybeUnpatched::Patched);
|
||||
}
|
||||
}
|
||||
(Some(JsonPointerItem::Number(idx)), Value::Bool(true)) => {
|
||||
let key = T::try_from_integer(*idx);
|
||||
if !pointer.has_next()
|
||||
&& let Some(key) = key
|
||||
{
|
||||
if !self.0.contains(&key) {
|
||||
self.0.push(key);
|
||||
}
|
||||
|
||||
return Ok(MaybeUnpatched::Patched);
|
||||
}
|
||||
}
|
||||
(None, Value::Object(items)) => {
|
||||
self.0.clear();
|
||||
for (key, value) in items.into_vec() {
|
||||
if let (Some(key), Value::Bool(is_set)) =
|
||||
(T::try_from_string(key.to_string().as_ref()), value)
|
||||
{
|
||||
if is_set && !self.0.contains(&key) {
|
||||
self.0.push(key);
|
||||
}
|
||||
} else {
|
||||
return Err(PatchError::new(
|
||||
pointer.clone(),
|
||||
"Invalid key for object property",
|
||||
));
|
||||
}
|
||||
}
|
||||
return Ok(MaybeUnpatched::Patched);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
Err(PatchError::new(
|
||||
pointer,
|
||||
"Invalid value for object property",
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: MapItem> Serialize for Map<T> {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
let mut map = serializer.serialize_map(Some(self.0.len()))?;
|
||||
for item in &self.0 {
|
||||
map.serialize_entry(&item.as_string() as &str, &true)?;
|
||||
}
|
||||
map.end()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de, T: MapItem> Deserialize<'de> for Map<T> {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
struct MapVisitor<T>(PhantomData<T>);
|
||||
|
||||
impl<'de, T: MapItem> Visitor<'de> for MapVisitor<T> {
|
||||
type Value = Map<T>;
|
||||
|
||||
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
|
||||
formatter.write_str("a map of string keys to booleans or nulls")
|
||||
}
|
||||
|
||||
fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
|
||||
where
|
||||
A: MapAccess<'de>,
|
||||
{
|
||||
let mut items = Vec::with_capacity(map.size_hint().unwrap_or(0));
|
||||
|
||||
while let Some(key) = map.next_key::<Cow<'de, str>>()? {
|
||||
let value: Option<bool> = map.next_value()?;
|
||||
|
||||
if value == Some(true) {
|
||||
let item = T::try_from_string(&key)
|
||||
.ok_or_else(|| de::Error::custom(format!("invalid map key: {key}")))?;
|
||||
if !items.contains(&item) {
|
||||
items.push(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(Map(items))
|
||||
}
|
||||
}
|
||||
|
||||
deserializer.deserialize_map(MapVisitor(PhantomData))
|
||||
}
|
||||
}
|
||||
|
||||
pub trait MapItem: Sized + PartialEq + Eq + Debug {
|
||||
fn try_from_string(value: &str) -> Option<Self>;
|
||||
fn try_from_integer(value: u64) -> Option<Self>;
|
||||
fn into_string(self) -> Cow<'static, str>;
|
||||
fn as_string(&self) -> Cow<'_, str>;
|
||||
}
|
||||
|
||||
impl MapItem for String {
|
||||
fn try_from_string(value: &str) -> Option<Self> {
|
||||
let value = value.trim();
|
||||
if !value.is_empty() {
|
||||
Some(value.to_string())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn try_from_integer(value: u64) -> Option<Self> {
|
||||
Some(value.to_string())
|
||||
}
|
||||
|
||||
fn into_string(self) -> Cow<'static, str> {
|
||||
Cow::Owned(self)
|
||||
}
|
||||
|
||||
fn as_string(&self) -> Cow<'_, str> {
|
||||
Cow::Borrowed(self.as_str())
|
||||
}
|
||||
}
|
||||
|
||||
impl MapItem for Id {
|
||||
fn try_from_string(value: &str) -> Option<Self> {
|
||||
Id::from_str(value).ok()
|
||||
}
|
||||
|
||||
fn try_from_integer(value: u64) -> Option<Self> {
|
||||
Id::from_str(&value.to_string()).ok()
|
||||
}
|
||||
|
||||
fn into_string(self) -> Cow<'static, str> {
|
||||
Cow::Owned(self.as_string())
|
||||
}
|
||||
|
||||
fn as_string(&self) -> Cow<'_, str> {
|
||||
Cow::Owned(self.as_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: EnumImpl> MapItem for T {
|
||||
fn try_from_string(value: &str) -> Option<Self> {
|
||||
Self::parse(value)
|
||||
}
|
||||
|
||||
fn try_from_integer(_: u64) -> Option<Self> {
|
||||
None
|
||||
}
|
||||
|
||||
fn into_string(self) -> Cow<'static, str> {
|
||||
Cow::Borrowed(self.as_str())
|
||||
}
|
||||
|
||||
fn as_string(&self) -> Cow<'_, str> {
|
||||
Cow::Borrowed(self.as_str())
|
||||
}
|
||||
}
|
||||
|
||||
impl MapItem for IpAddr {
|
||||
fn try_from_string(value: &str) -> Option<Self> {
|
||||
Self::from_str(value).ok()
|
||||
}
|
||||
|
||||
fn try_from_integer(_: u64) -> Option<Self> {
|
||||
None
|
||||
}
|
||||
|
||||
fn into_string(self) -> Cow<'static, str> {
|
||||
Cow::Owned(self.to_string())
|
||||
}
|
||||
|
||||
fn as_string(&self) -> Cow<'_, str> {
|
||||
Cow::Owned(self.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl MapItem for IpAddrOrMask {
|
||||
fn try_from_string(value: &str) -> Option<Self> {
|
||||
Self::from_str(value).ok()
|
||||
}
|
||||
|
||||
fn try_from_integer(_: u64) -> Option<Self> {
|
||||
None
|
||||
}
|
||||
|
||||
fn into_string(self) -> Cow<'static, str> {
|
||||
Cow::Owned(self.to_string())
|
||||
}
|
||||
|
||||
fn as_string(&self) -> Cow<'_, str> {
|
||||
Cow::Owned(self.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl MapItem for SocketAddr {
|
||||
fn try_from_string(value: &str) -> Option<Self> {
|
||||
Self::from_str(value).ok()
|
||||
}
|
||||
|
||||
fn try_from_integer(_: u64) -> Option<Self> {
|
||||
None
|
||||
}
|
||||
|
||||
fn into_string(self) -> Cow<'static, str> {
|
||||
Cow::Owned(self.to_string())
|
||||
}
|
||||
|
||||
fn as_string(&self) -> Cow<'_, str> {
|
||||
Cow::Owned(self.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl MapItem for u64 {
|
||||
fn try_from_string(value: &str) -> Option<Self> {
|
||||
value.parse().ok()
|
||||
}
|
||||
|
||||
fn try_from_integer(value: u64) -> Option<Self> {
|
||||
Some(value)
|
||||
}
|
||||
|
||||
fn into_string(self) -> Cow<'static, str> {
|
||||
Cow::Owned(self.to_string())
|
||||
}
|
||||
|
||||
fn as_string(&self) -> Cow<'_, str> {
|
||||
Cow::Owned(self.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: MapItem> From<Vec<T>> for Map<T> {
|
||||
fn from(vec: Vec<T>) -> Self {
|
||||
Self(vec)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: MapItem> IntoIterator for Map<T> {
|
||||
type Item = T;
|
||||
type IntoIter = std::vec::IntoIter<T>;
|
||||
|
||||
fn into_iter(self) -> Self::IntoIter {
|
||||
self.0.into_iter()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::{jmap::JsonPointerPatch, schema::prelude::Property};
|
||||
use jmap_tools::JsonPointer;
|
||||
|
||||
fn patch_member(ptr_str: &str) -> Map<Id> {
|
||||
let mut map = Map::<Id>::default();
|
||||
let ptr = JsonPointer::<Property>::parse(ptr_str);
|
||||
let pointer = JsonPointerPatch::new(&ptr);
|
||||
map.patch(pointer, Value::Bool(true)).expect("patch failed");
|
||||
map
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn patch_map_id_digit_keys() {
|
||||
for id in [0u64, 28, 29, 70, 861, 954, 957, 30554] {
|
||||
let id = Id::new(id);
|
||||
let key = id.as_string();
|
||||
let map = patch_member(&key);
|
||||
assert_eq!(map.0, vec![id], "id {} via key '{key}'", id.id());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use crate::{
|
||||
pickle::{Pickle, maybe_compress_pickle},
|
||||
schema::prelude::ObjectType,
|
||||
types::{error::ValidationError, index::IndexBuilder},
|
||||
};
|
||||
use serde::{Serialize, de::DeserializeOwned};
|
||||
use std::fmt::Debug;
|
||||
|
||||
pub mod datetime;
|
||||
pub mod duration;
|
||||
pub mod error;
|
||||
pub mod float;
|
||||
pub mod id;
|
||||
pub mod index;
|
||||
pub mod ipaddr;
|
||||
pub mod ipmask;
|
||||
pub mod list;
|
||||
pub mod map;
|
||||
pub mod socketaddr;
|
||||
pub mod string;
|
||||
|
||||
pub trait EnumImpl: Sized + Debug + PartialEq + Eq {
|
||||
const COUNT: usize;
|
||||
|
||||
fn parse(s: &str) -> Option<Self>;
|
||||
fn as_str(&self) -> &'static str;
|
||||
fn from_id(id: u16) -> Option<Self>;
|
||||
fn to_id(&self) -> u16;
|
||||
}
|
||||
|
||||
pub trait ObjectImpl:
|
||||
Pickle + Serialize + DeserializeOwned + Default + Clone + Send + Sync
|
||||
{
|
||||
const FLAGS: u64;
|
||||
const OBJECT: ObjectType;
|
||||
const VERSION: u8;
|
||||
|
||||
fn validate(&self, errors: &mut Vec<ValidationError>) -> bool;
|
||||
fn index<'x>(&'x self, builder: &mut IndexBuilder<'x>);
|
||||
fn to_pickled_vec(&self) -> Vec<u8> {
|
||||
let mut out = Vec::with_capacity(256);
|
||||
out.push(Self::VERSION);
|
||||
self.pickle(&mut out);
|
||||
maybe_compress_pickle(out)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use crate::{
|
||||
jmap::{
|
||||
IntoValue, JmapValue, JsonPointerPatch, MaybeUnpatched, PatchResult, RegistryJsonPatch,
|
||||
},
|
||||
pickle::{Pickle, PickledStream},
|
||||
types::error::PatchError,
|
||||
};
|
||||
use std::{fmt::Display, str::FromStr};
|
||||
|
||||
const UNSET_SOCKET_ADDR: std::net::SocketAddr = std::net::SocketAddr::new(
|
||||
std::net::IpAddr::V4(std::net::Ipv4Addr::from_octets([255, 255, 255, 255])),
|
||||
u16::MAX,
|
||||
);
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct SocketAddr(pub std::net::SocketAddr);
|
||||
|
||||
impl SocketAddr {
|
||||
pub fn into_inner(self) -> std::net::SocketAddr {
|
||||
self.0
|
||||
}
|
||||
|
||||
pub fn is_valid(&self) -> bool {
|
||||
self.0 != UNSET_SOCKET_ADDR
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for SocketAddr {
|
||||
type Err = String;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
s.parse::<std::net::SocketAddr>()
|
||||
.map(SocketAddr)
|
||||
.map_err(|err| err.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for SocketAddr {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{}", self.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl serde::Serialize for SocketAddr {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: serde::Serializer,
|
||||
{
|
||||
serializer.serialize_str(self.to_string().as_str())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> serde::Deserialize<'de> for SocketAddr {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
SocketAddr::from_str(<&str>::deserialize(deserializer)?)
|
||||
.map_err(|_| serde::de::Error::custom("invalid SocketAddr"))
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for SocketAddr {
|
||||
fn default() -> Self {
|
||||
SocketAddr(UNSET_SOCKET_ADDR)
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<std::net::SocketAddr> for SocketAddr {
|
||||
fn as_ref(&self) -> &std::net::SocketAddr {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl Pickle for SocketAddr {
|
||||
fn pickle(&self, out: &mut Vec<u8>) {
|
||||
self.0.ip().pickle(out);
|
||||
self.0.port().pickle(out);
|
||||
}
|
||||
|
||||
fn unpickle(data: &mut PickledStream<'_>) -> Option<Self> {
|
||||
let ip = std::net::IpAddr::unpickle(data)?;
|
||||
let port = u16::unpickle(data)?;
|
||||
Some(SocketAddr(std::net::SocketAddr::new(ip, port)))
|
||||
}
|
||||
}
|
||||
|
||||
impl RegistryJsonPatch for SocketAddr {
|
||||
fn patch<'x>(
|
||||
&mut self,
|
||||
mut pointer: JsonPointerPatch<'_>,
|
||||
value: JmapValue<'x>,
|
||||
) -> PatchResult<'x> {
|
||||
match (value, pointer.next()) {
|
||||
(jmap_tools::Value::Str(value), None) => {
|
||||
if let Ok(new_value) = SocketAddr::from_str(value.as_ref()) {
|
||||
*self = new_value;
|
||||
Ok(MaybeUnpatched::Patched)
|
||||
} else {
|
||||
Err(PatchError::new(
|
||||
pointer,
|
||||
"Failed to parse SocketAddr from string",
|
||||
))
|
||||
}
|
||||
}
|
||||
_ => Err(PatchError::new(
|
||||
pointer,
|
||||
"Invalid path for SocketAddr, expected a string value",
|
||||
)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoValue for SocketAddr {
|
||||
fn into_value(self) -> JmapValue<'static> {
|
||||
JmapValue::Str(self.to_string().into())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use std::{net::IpAddr, str::FromStr};
|
||||
use utils::{sanitize_domain, sanitize_email, sanitize_email_local};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum StringValidator {
|
||||
Email,
|
||||
EmailLocalPart,
|
||||
Domain,
|
||||
Hostname,
|
||||
RemoveSpaces,
|
||||
Lowercase,
|
||||
Uppercase,
|
||||
Trim,
|
||||
}
|
||||
|
||||
pub enum StringValidatorResult {
|
||||
Valid,
|
||||
Replace(String),
|
||||
Invalid(&'static str),
|
||||
}
|
||||
|
||||
impl StringValidator {
|
||||
pub fn validate(&self, value: &str) -> StringValidatorResult {
|
||||
match self {
|
||||
Self::Email => sanitize_email(value)
|
||||
.map(StringValidatorResult::Replace)
|
||||
.unwrap_or(StringValidatorResult::Invalid("Invalid email address")),
|
||||
Self::EmailLocalPart => sanitize_email_local(value)
|
||||
.map(StringValidatorResult::Replace)
|
||||
.unwrap_or(StringValidatorResult::Invalid("Invalid email local part")),
|
||||
Self::Domain => sanitize_domain(value)
|
||||
.map(StringValidatorResult::Replace)
|
||||
.unwrap_or(StringValidatorResult::Invalid("Invalid domain name")),
|
||||
Self::Hostname => IpAddr::from_str(value)
|
||||
.ok()
|
||||
.map(|_| StringValidatorResult::Valid)
|
||||
.or_else(|| sanitize_domain(value).map(StringValidatorResult::Replace))
|
||||
.unwrap_or(StringValidatorResult::Invalid(
|
||||
"Invalid hostname or IP address",
|
||||
)),
|
||||
Self::RemoveSpaces => {
|
||||
if value.chars().any(|c| c.is_whitespace()) {
|
||||
StringValidatorResult::Replace(
|
||||
value.chars().filter(|c| !c.is_whitespace()).collect(),
|
||||
)
|
||||
} else {
|
||||
StringValidatorResult::Valid
|
||||
}
|
||||
}
|
||||
Self::Lowercase => StringValidatorResult::Replace(value.to_lowercase()),
|
||||
Self::Uppercase => StringValidatorResult::Replace(value.to_uppercase()),
|
||||
Self::Trim => {
|
||||
let trimmed = value.trim();
|
||||
if trimmed.len() != value.len() {
|
||||
if !trimmed.is_empty() {
|
||||
StringValidatorResult::Replace(trimmed.to_string())
|
||||
} else {
|
||||
StringValidatorResult::Invalid("String cannot be empty")
|
||||
}
|
||||
} else {
|
||||
StringValidatorResult::Valid
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,151 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use types::id::Id;
|
||||
|
||||
use crate::schema::prelude::{
|
||||
Account, Credential, GroupAccount, PasswordCredential, SecondaryCredential, UserAccount,
|
||||
};
|
||||
|
||||
impl Account {
|
||||
pub fn into_user(self) -> Option<UserAccount> {
|
||||
if let Account::User(user) = self {
|
||||
Some(user)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub fn into_group(self) -> Option<GroupAccount> {
|
||||
if let Account::Group(group) = self {
|
||||
Some(group)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl UserAccount {
|
||||
pub fn set_password(&mut self, password: String) {
|
||||
if let Some(credential) = self.credentials.0.values_mut().find_map(|credential| {
|
||||
if let Credential::Password(credential) = credential {
|
||||
Some(credential)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}) {
|
||||
credential.secret = password;
|
||||
} else {
|
||||
let credential_id = self.next_credential_id().into();
|
||||
self.credentials
|
||||
.push(Credential::Password(PasswordCredential {
|
||||
credential_id,
|
||||
secret: password,
|
||||
..Default::default()
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
pub fn password_credential(&self) -> Option<&PasswordCredential> {
|
||||
self.credentials.iter().find_map(|credential| {
|
||||
if let Credential::Password(credential) = credential {
|
||||
Some(credential)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub fn password_credential_mut(&mut self) -> Option<&mut PasswordCredential> {
|
||||
self.credentials.values_mut().find_map(|credential| {
|
||||
if let Credential::Password(credential) = credential {
|
||||
Some(credential)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub fn password(&self) -> Option<&str> {
|
||||
self.password_credential()
|
||||
.map(|credential| credential.secret.as_str())
|
||||
}
|
||||
|
||||
pub fn into_password_credential(self) -> Option<PasswordCredential> {
|
||||
self.credentials.into_iter().find_map(|credential| {
|
||||
if let Credential::Password(credential) = credential {
|
||||
Some(credential)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub fn into_password(self) -> Option<String> {
|
||||
self.into_password_credential()
|
||||
.map(|credential| credential.secret)
|
||||
}
|
||||
|
||||
pub fn next_credential_id(&self) -> u64 {
|
||||
self.credentials
|
||||
.0
|
||||
.values()
|
||||
.map(|credential| match credential {
|
||||
Credential::Password(credential) => credential.credential_id.id() + 1,
|
||||
Credential::AppPassword(credential_properties)
|
||||
| Credential::ApiKey(credential_properties) => {
|
||||
credential_properties.credential_id.id() + 1
|
||||
}
|
||||
})
|
||||
.max()
|
||||
.unwrap_or_default()
|
||||
}
|
||||
}
|
||||
|
||||
impl Credential {
|
||||
pub fn credential_id(&self) -> Id {
|
||||
match self {
|
||||
Credential::Password(credential) => credential.credential_id,
|
||||
Credential::AppPassword(credential_properties) => credential_properties.credential_id,
|
||||
Credential::ApiKey(credential_properties) => credential_properties.credential_id,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_credential_id(&mut self, credential_id: Id) {
|
||||
match self {
|
||||
Credential::Password(credential) => credential.credential_id = credential_id,
|
||||
Credential::AppPassword(credential_properties) => {
|
||||
credential_properties.credential_id = credential_id
|
||||
}
|
||||
Credential::ApiKey(credential_properties) => {
|
||||
credential_properties.credential_id = credential_id
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn into_secondary_credential(self) -> Option<SecondaryCredential> {
|
||||
match self {
|
||||
Credential::AppPassword(credential_properties) => Some(credential_properties),
|
||||
Credential::ApiKey(credential_properties) => Some(credential_properties),
|
||||
Credential::Password(_) => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn as_secondary_credential(&self) -> Option<&SecondaryCredential> {
|
||||
match self {
|
||||
Credential::AppPassword(credential_properties) => Some(credential_properties),
|
||||
Credential::ApiKey(credential_properties) => Some(credential_properties),
|
||||
Credential::Password(_) => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn as_main_credential(&self) -> Option<&PasswordCredential> {
|
||||
match self {
|
||||
Credential::Password(credential) => Some(credential),
|
||||
Credential::AppPassword(_) | Credential::ApiKey(_) => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use crate::schema::prelude::{ArchivedItem, UTCDateTime};
|
||||
use types::{blob::BlobId, id::Id};
|
||||
|
||||
impl ArchivedItem {
|
||||
pub fn account_id(&self) -> Id {
|
||||
match self {
|
||||
ArchivedItem::Email(i) => i.account_id,
|
||||
ArchivedItem::FileNode(i) => i.account_id,
|
||||
ArchivedItem::CalendarEvent(i) => i.account_id,
|
||||
ArchivedItem::ContactCard(i) => i.account_id,
|
||||
ArchivedItem::SieveScript(i) => i.account_id,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn blob_id(&self) -> &BlobId {
|
||||
match self {
|
||||
ArchivedItem::Email(i) => &i.blob_id,
|
||||
ArchivedItem::FileNode(i) => &i.blob_id,
|
||||
ArchivedItem::CalendarEvent(i) => &i.blob_id,
|
||||
ArchivedItem::ContactCard(i) => &i.blob_id,
|
||||
ArchivedItem::SieveScript(i) => &i.blob_id,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn archived_until(&self) -> UTCDateTime {
|
||||
match self {
|
||||
ArchivedItem::Email(i) => i.archived_until,
|
||||
ArchivedItem::FileNode(i) => i.archived_until,
|
||||
ArchivedItem::CalendarEvent(i) => i.archived_until,
|
||||
ArchivedItem::ContactCard(i) => i.archived_until,
|
||||
ArchivedItem::SieveScript(i) => i.archived_until,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn created_at(&self) -> UTCDateTime {
|
||||
match self {
|
||||
ArchivedItem::Email(i) => i.received_at,
|
||||
ArchivedItem::FileNode(i) => i.created_at,
|
||||
ArchivedItem::CalendarEvent(i) => i.created_at,
|
||||
ArchivedItem::ContactCard(i) => i.created_at,
|
||||
ArchivedItem::SieveScript(i) => i.created_at,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_account_id(&mut self, value: Id) {
|
||||
match self {
|
||||
ArchivedItem::Email(i) => i.account_id = value,
|
||||
ArchivedItem::FileNode(i) => i.account_id = value,
|
||||
ArchivedItem::CalendarEvent(i) => i.account_id = value,
|
||||
ArchivedItem::ContactCard(i) => i.account_id = value,
|
||||
ArchivedItem::SieveScript(i) => i.account_id = value,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_blob_id(&mut self, value: BlobId) {
|
||||
match self {
|
||||
ArchivedItem::Email(i) => i.blob_id = value,
|
||||
ArchivedItem::FileNode(i) => i.blob_id = value,
|
||||
ArchivedItem::CalendarEvent(i) => i.blob_id = value,
|
||||
ArchivedItem::ContactCard(i) => i.blob_id = value,
|
||||
ArchivedItem::SieveScript(i) => i.blob_id = value,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_archived_until(&mut self, value: UTCDateTime) {
|
||||
match self {
|
||||
ArchivedItem::Email(i) => i.archived_until = value,
|
||||
ArchivedItem::FileNode(i) => i.archived_until = value,
|
||||
ArchivedItem::CalendarEvent(i) => i.archived_until = value,
|
||||
ArchivedItem::ContactCard(i) => i.archived_until = value,
|
||||
ArchivedItem::SieveScript(i) => i.archived_until = value,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn account_id_mut(&mut self) -> &mut Id {
|
||||
match self {
|
||||
ArchivedItem::Email(i) => &mut i.account_id,
|
||||
ArchivedItem::FileNode(i) => &mut i.account_id,
|
||||
ArchivedItem::CalendarEvent(i) => &mut i.account_id,
|
||||
ArchivedItem::ContactCard(i) => &mut i.account_id,
|
||||
ArchivedItem::SieveScript(i) => &mut i.account_id,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn blob_id_mut(&mut self) -> &mut BlobId {
|
||||
match self {
|
||||
ArchivedItem::Email(i) => &mut i.blob_id,
|
||||
ArchivedItem::FileNode(i) => &mut i.blob_id,
|
||||
ArchivedItem::CalendarEvent(i) => &mut i.blob_id,
|
||||
ArchivedItem::ContactCard(i) => &mut i.blob_id,
|
||||
ArchivedItem::SieveScript(i) => &mut i.blob_id,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn archived_until_mut(&mut self) -> &mut UTCDateTime {
|
||||
match self {
|
||||
ArchivedItem::Email(i) => &mut i.archived_until,
|
||||
ArchivedItem::FileNode(i) => &mut i.archived_until,
|
||||
ArchivedItem::CalendarEvent(i) => &mut i.archived_until,
|
||||
ArchivedItem::ContactCard(i) => &mut i.archived_until,
|
||||
ArchivedItem::SieveScript(i) => &mut i.archived_until,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn into_blob_id(self) -> BlobId {
|
||||
match self {
|
||||
ArchivedItem::Email(i) => i.blob_id,
|
||||
ArchivedItem::FileNode(i) => i.blob_id,
|
||||
ArchivedItem::CalendarEvent(i) => i.blob_id,
|
||||
ArchivedItem::ContactCard(i) => i.blob_id,
|
||||
ArchivedItem::SieveScript(i) => i.blob_id,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use crate::schema::prelude::Cron;
|
||||
use utils::cron::SimpleCron;
|
||||
|
||||
impl From<Cron> for SimpleCron {
|
||||
fn from(value: Cron) -> Self {
|
||||
match value {
|
||||
Cron::Daily(cron) => SimpleCron::Day {
|
||||
hour: cron.hour as u32,
|
||||
minute: cron.minute as u32,
|
||||
},
|
||||
Cron::Weekly(cron) => SimpleCron::Week {
|
||||
day: cron.day as u32,
|
||||
hour: cron.hour as u32,
|
||||
minute: cron.minute as u32,
|
||||
},
|
||||
Cron::Hourly(cron) => SimpleCron::Hour {
|
||||
minute: cron.minute as u32,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use crate::schema::{
|
||||
enums::{DkimRotationStage, DkimSignatureType},
|
||||
prelude::{DkimSignature, UTCDateTime},
|
||||
};
|
||||
use types::id::Id;
|
||||
|
||||
impl DkimSignature {
|
||||
pub fn rotation_due(&self) -> Option<DkimRotationStage> {
|
||||
let (stage, next_transition) = match self {
|
||||
DkimSignature::Dkim1Ed25519Sha256(sign) => (sign.stage, sign.next_transition_at),
|
||||
DkimSignature::Dkim1RsaSha256(sign) => (sign.stage, sign.next_transition_at),
|
||||
DkimSignature::Dkim2Ed25519Sha256(sign) => (sign.stage, sign.next_transition_at),
|
||||
DkimSignature::Dkim2RsaSha256(sign) => (sign.stage, sign.next_transition_at),
|
||||
};
|
||||
next_transition.and_then(|next_transition| {
|
||||
if next_transition <= UTCDateTime::now() {
|
||||
Some(stage)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub fn next_transition(&self) -> Option<UTCDateTime> {
|
||||
match self {
|
||||
DkimSignature::Dkim1Ed25519Sha256(sign) => sign.next_transition_at,
|
||||
DkimSignature::Dkim1RsaSha256(sign) => sign.next_transition_at,
|
||||
DkimSignature::Dkim2Ed25519Sha256(sign) => sign.next_transition_at,
|
||||
DkimSignature::Dkim2RsaSha256(sign) => sign.next_transition_at,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_next_transition(&mut self, next_transition: UTCDateTime) {
|
||||
match self {
|
||||
DkimSignature::Dkim1Ed25519Sha256(sign) => {
|
||||
sign.next_transition_at = Some(next_transition)
|
||||
}
|
||||
DkimSignature::Dkim1RsaSha256(sign) => sign.next_transition_at = Some(next_transition),
|
||||
DkimSignature::Dkim2Ed25519Sha256(sign) => {
|
||||
sign.next_transition_at = Some(next_transition)
|
||||
}
|
||||
DkimSignature::Dkim2RsaSha256(sign) => sign.next_transition_at = Some(next_transition),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn stage(&self) -> DkimRotationStage {
|
||||
match self {
|
||||
DkimSignature::Dkim1Ed25519Sha256(sign) => sign.stage,
|
||||
DkimSignature::Dkim1RsaSha256(sign) => sign.stage,
|
||||
DkimSignature::Dkim2Ed25519Sha256(sign) => sign.stage,
|
||||
DkimSignature::Dkim2RsaSha256(sign) => sign.stage,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_stage(&mut self, stage: DkimRotationStage) {
|
||||
match self {
|
||||
DkimSignature::Dkim1Ed25519Sha256(sign) => sign.stage = stage,
|
||||
DkimSignature::Dkim1RsaSha256(sign) => sign.stage = stage,
|
||||
DkimSignature::Dkim2Ed25519Sha256(sign) => sign.stage = stage,
|
||||
DkimSignature::Dkim2RsaSha256(sign) => sign.stage = stage,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_active(&self) -> bool {
|
||||
match self {
|
||||
DkimSignature::Dkim1Ed25519Sha256(sign) => sign.stage == DkimRotationStage::Active,
|
||||
DkimSignature::Dkim1RsaSha256(sign) => sign.stage == DkimRotationStage::Active,
|
||||
DkimSignature::Dkim2Ed25519Sha256(sign) => sign.stage == DkimRotationStage::Active,
|
||||
DkimSignature::Dkim2RsaSha256(sign) => sign.stage == DkimRotationStage::Active,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_published(&self) -> bool {
|
||||
!matches!(self.stage(), DkimRotationStage::Retired)
|
||||
}
|
||||
|
||||
pub fn selector(&self) -> &str {
|
||||
match self {
|
||||
DkimSignature::Dkim1Ed25519Sha256(sign) => &sign.selector,
|
||||
DkimSignature::Dkim1RsaSha256(sign) => &sign.selector,
|
||||
DkimSignature::Dkim2Ed25519Sha256(sign) => &sign.selector,
|
||||
DkimSignature::Dkim2RsaSha256(sign) => &sign.selector,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn domain_id(&self) -> Id {
|
||||
match self {
|
||||
DkimSignature::Dkim1Ed25519Sha256(sign) => sign.domain_id,
|
||||
DkimSignature::Dkim1RsaSha256(sign) => sign.domain_id,
|
||||
DkimSignature::Dkim2Ed25519Sha256(sign) => sign.domain_id,
|
||||
DkimSignature::Dkim2RsaSha256(sign) => sign.domain_id,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl DkimSignatureType {
|
||||
pub const fn algorithm(self) -> &'static str {
|
||||
match self {
|
||||
Self::Dkim1Ed25519Sha256 | Self::Dkim2Ed25519Sha256 => "ed25519",
|
||||
Self::Dkim1RsaSha256 | Self::Dkim2RsaSha256 => "rsa",
|
||||
}
|
||||
}
|
||||
|
||||
pub const fn hash(self) -> &'static str {
|
||||
"sha256"
|
||||
}
|
||||
|
||||
pub const fn version(self) -> &'static str {
|
||||
match self {
|
||||
Self::Dkim1Ed25519Sha256 | Self::Dkim1RsaSha256 => "1",
|
||||
Self::Dkim2Ed25519Sha256 | Self::Dkim2RsaSha256 => "2",
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use crate::schema::prelude::{Duration, HttpAuth};
|
||||
use utils::{
|
||||
Client, HeaderMap,
|
||||
http::{build_http_client, build_http_headers},
|
||||
map::vec_map::VecMap,
|
||||
};
|
||||
|
||||
impl HttpAuth {
|
||||
pub async fn build_headers(
|
||||
&self,
|
||||
extra_headers: VecMap<String, String>,
|
||||
content_type: Option<&str>,
|
||||
) -> Result<HeaderMap, String> {
|
||||
match self {
|
||||
HttpAuth::Unauthenticated => {
|
||||
build_http_headers(extra_headers, None, None, None, content_type)
|
||||
}
|
||||
HttpAuth::Basic(auth) => build_http_headers(
|
||||
extra_headers,
|
||||
auth.username.as_str().into(),
|
||||
auth.secret.secret().await?.as_ref().into(),
|
||||
None,
|
||||
content_type,
|
||||
),
|
||||
HttpAuth::Bearer(auth) => build_http_headers(
|
||||
extra_headers,
|
||||
None,
|
||||
None,
|
||||
auth.bearer_token.secret().await?.as_ref().into(),
|
||||
content_type,
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn build_http_client(
|
||||
&self,
|
||||
extra_headers: VecMap<String, String>,
|
||||
content_type: Option<&str>,
|
||||
timeout: Duration,
|
||||
allow_invalid_certs: bool,
|
||||
) -> Result<Client, String> {
|
||||
match self {
|
||||
HttpAuth::Unauthenticated => build_http_client(
|
||||
extra_headers,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
content_type,
|
||||
timeout.into_inner(),
|
||||
allow_invalid_certs,
|
||||
),
|
||||
HttpAuth::Basic(auth) => build_http_client(
|
||||
extra_headers,
|
||||
auth.username.as_str().into(),
|
||||
auth.secret.secret().await?.as_ref().into(),
|
||||
None,
|
||||
content_type,
|
||||
timeout.into_inner(),
|
||||
allow_invalid_certs,
|
||||
),
|
||||
HttpAuth::Bearer(auth) => build_http_client(
|
||||
extra_headers,
|
||||
None,
|
||||
None,
|
||||
auth.bearer_token.secret().await?.as_ref().into(),
|
||||
content_type,
|
||||
timeout.into_inner(),
|
||||
allow_invalid_certs,
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use crate::schema::prelude::{DkimSignature, Roles, SecretText};
|
||||
use types::id::Id;
|
||||
|
||||
pub mod account;
|
||||
pub mod archived_item;
|
||||
pub mod cron;
|
||||
pub mod dkim;
|
||||
pub mod http;
|
||||
pub mod report;
|
||||
pub mod secret;
|
||||
pub mod task;
|
||||
|
||||
impl Roles {
|
||||
pub fn role_ids(&self) -> Option<&[Id]> {
|
||||
match self {
|
||||
Roles::Default => None,
|
||||
Roles::Custom(custom_roles) => Some(custom_roles.role_ids.as_slice()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl DkimSignature {
|
||||
pub fn private_key(&self) -> &SecretText {
|
||||
match self {
|
||||
DkimSignature::Dkim1Ed25519Sha256(signature) => &signature.private_key,
|
||||
DkimSignature::Dkim1RsaSha256(signature) => &signature.private_key,
|
||||
DkimSignature::Dkim2Ed25519Sha256(signature) => &signature.private_key,
|
||||
DkimSignature::Dkim2RsaSha256(signature) => &signature.private_key,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn private_key_mut(&mut self) -> &mut SecretText {
|
||||
match self {
|
||||
DkimSignature::Dkim1Ed25519Sha256(signature) => &mut signature.private_key,
|
||||
DkimSignature::Dkim1RsaSha256(signature) => &mut signature.private_key,
|
||||
DkimSignature::Dkim2Ed25519Sha256(signature) => &mut signature.private_key,
|
||||
DkimSignature::Dkim2RsaSha256(signature) => &mut signature.private_key,
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,152 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use crate::schema::prelude::{
|
||||
PublicStringOptional, PublicStringValue, PublicText, SecretKey, SecretKeyEnvironmentVariable,
|
||||
SecretKeyFile, SecretKeyOptional, SecretKeyValue, SecretText, SecretTextOptional,
|
||||
SecretTextValue,
|
||||
};
|
||||
use std::borrow::Cow;
|
||||
|
||||
impl SecretKey {
|
||||
pub async fn secret(&self) -> Result<Cow<'_, str>, String> {
|
||||
match self {
|
||||
SecretKey::Value(value) => Ok(Cow::Borrowed(value.secret())),
|
||||
SecretKey::File(file) => file.secret().await.map(Cow::Owned),
|
||||
SecretKey::EnvironmentVariable(env_var) => env_var.secret().map(Cow::Owned),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl SecretText {
|
||||
pub async fn secret(&self) -> Result<Cow<'_, str>, String> {
|
||||
match self {
|
||||
SecretText::Text(value) => Ok(Cow::Borrowed(value.secret())),
|
||||
SecretText::File(file) => file.secret().await.map(Cow::Owned),
|
||||
SecretText::EnvironmentVariable(env_var) => env_var.secret().map(Cow::Owned),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PublicText {
|
||||
pub async fn value(&self) -> Result<Cow<'_, str>, String> {
|
||||
match self {
|
||||
PublicText::Text(value) => Ok(Cow::Borrowed(value.value.as_str())),
|
||||
PublicText::File(file) => file.secret().await.map(Cow::Owned),
|
||||
PublicText::EnvironmentVariable(env_var) => env_var.secret().map(Cow::Owned),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl SecretKeyOptional {
|
||||
pub async fn secret(&self) -> Result<Option<Cow<'_, str>>, String> {
|
||||
match self {
|
||||
SecretKeyOptional::None => Ok(None),
|
||||
SecretKeyOptional::Value(secret_key_value) => {
|
||||
Ok(Some(Cow::Borrowed(secret_key_value.secret())))
|
||||
}
|
||||
SecretKeyOptional::EnvironmentVariable(secret_key_environment_variable) => {
|
||||
secret_key_environment_variable
|
||||
.secret()
|
||||
.map(|s| Some(Cow::Owned(s)))
|
||||
}
|
||||
SecretKeyOptional::File(secret_key_file) => {
|
||||
secret_key_file.secret().await.map(|s| Some(Cow::Owned(s)))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PublicStringOptional {
|
||||
pub async fn value(&self) -> Result<Option<Cow<'_, str>>, String> {
|
||||
match self {
|
||||
PublicStringOptional::None => Ok(None),
|
||||
PublicStringOptional::Value(public_string_value) => {
|
||||
Ok(Some(Cow::Borrowed(public_string_value.value())))
|
||||
}
|
||||
PublicStringOptional::EnvironmentVariable(secret_key_environment_variable) => {
|
||||
secret_key_environment_variable
|
||||
.secret()
|
||||
.map(|s| Some(Cow::Owned(s)))
|
||||
}
|
||||
PublicStringOptional::File(secret_key_file) => {
|
||||
secret_key_file.secret().await.map(|s| Some(Cow::Owned(s)))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PublicStringValue {
|
||||
pub fn value(&self) -> &str {
|
||||
self.value.as_str()
|
||||
}
|
||||
}
|
||||
|
||||
impl SecretTextOptional {
|
||||
pub async fn secret(&self) -> Result<Option<Cow<'_, str>>, String> {
|
||||
match self {
|
||||
SecretTextOptional::None => Ok(None),
|
||||
SecretTextOptional::Text(secret_text_value) => {
|
||||
Ok(Some(Cow::Borrowed(secret_text_value.secret())))
|
||||
}
|
||||
SecretTextOptional::EnvironmentVariable(secret_text_environment_variable) => {
|
||||
secret_text_environment_variable
|
||||
.secret()
|
||||
.map(|s| Some(Cow::Owned(s)))
|
||||
}
|
||||
SecretTextOptional::File(secret_text_file) => {
|
||||
secret_text_file.secret().await.map(|s| Some(Cow::Owned(s)))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl SecretKeyValue {
|
||||
pub fn secret(&self) -> &str {
|
||||
self.secret.as_str()
|
||||
}
|
||||
}
|
||||
|
||||
impl SecretTextValue {
|
||||
pub fn secret(&self) -> &str {
|
||||
self.secret.as_str()
|
||||
}
|
||||
}
|
||||
|
||||
impl SecretKeyFile {
|
||||
pub async fn secret(&self) -> Result<String, String> {
|
||||
let path = self.file_path.trim();
|
||||
if !path.is_empty() {
|
||||
tokio::fs::read_to_string(path)
|
||||
.await
|
||||
.map_err(|err| format!("Failed to read secret from file '{}': {}", path, err))
|
||||
.and_then(|content| {
|
||||
let secret = content.trim_end();
|
||||
if !secret.is_empty() {
|
||||
Ok(secret.to_string())
|
||||
} else {
|
||||
Err(format!("Secret in file '{}' is empty", path))
|
||||
}
|
||||
})
|
||||
} else {
|
||||
Err("File path cannot be empty".to_string())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl SecretKeyEnvironmentVariable {
|
||||
pub fn secret(&self) -> Result<String, String> {
|
||||
let var = self.variable_name.trim();
|
||||
if !var.is_empty() {
|
||||
std::env::var(var)
|
||||
.ok()
|
||||
.filter(|v| !v.is_empty())
|
||||
.ok_or_else(|| format!("Environment variable '{}' not found", var))
|
||||
} else {
|
||||
Err("Variable name cannot be empty".to_string())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use crate::schema::{
|
||||
enums::Permission,
|
||||
prelude::{Action, Task, TaskStatus, TaskStatusPending, UTCDateTime},
|
||||
};
|
||||
|
||||
impl Task {
|
||||
pub fn set_status(&mut self, status: TaskStatus) {
|
||||
match self {
|
||||
Task::IndexDocument(task) => task.status = status,
|
||||
Task::UnindexDocument(task) => task.status = status,
|
||||
Task::IndexTrace(task) => task.status = status,
|
||||
Task::CalendarAlarmEmail(task) => task.status = status,
|
||||
Task::CalendarAlarmNotification(task) => task.status = status,
|
||||
Task::CalendarItipMessage(task) => task.status = status,
|
||||
Task::MergeThreads(task) => task.status = status,
|
||||
Task::DmarcReport(task) => task.status = status,
|
||||
Task::TlsReport(task) => task.status = status,
|
||||
Task::RestoreArchivedItem(task) => task.status = status,
|
||||
Task::DestroyAccount(task) => task.status = status,
|
||||
Task::AccountMaintenance(task) => task.status = status,
|
||||
Task::StoreMaintenance(task) => task.status = status,
|
||||
Task::SpamFilterMaintenance(task) => task.status = status,
|
||||
Task::AcmeRenewal(task) => task.status = status,
|
||||
Task::DkimManagement(task) => task.status = status,
|
||||
Task::DnsManagement(task) => task.status = status,
|
||||
Task::TenantMaintenance(task) => task.status = status,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn status(&self) -> &TaskStatus {
|
||||
match self {
|
||||
Task::IndexDocument(task) => &task.status,
|
||||
Task::UnindexDocument(task) => &task.status,
|
||||
Task::IndexTrace(task) => &task.status,
|
||||
Task::CalendarAlarmEmail(task) => &task.status,
|
||||
Task::CalendarAlarmNotification(task) => &task.status,
|
||||
Task::CalendarItipMessage(task) => &task.status,
|
||||
Task::MergeThreads(task) => &task.status,
|
||||
Task::DmarcReport(task) => &task.status,
|
||||
Task::TlsReport(task) => &task.status,
|
||||
Task::RestoreArchivedItem(task) => &task.status,
|
||||
Task::DestroyAccount(task) => &task.status,
|
||||
Task::AccountMaintenance(task) => &task.status,
|
||||
Task::StoreMaintenance(task) => &task.status,
|
||||
Task::SpamFilterMaintenance(task) => &task.status,
|
||||
Task::AcmeRenewal(task) => &task.status,
|
||||
Task::DkimManagement(task) => &task.status,
|
||||
Task::DnsManagement(task) => &task.status,
|
||||
Task::TenantMaintenance(task) => &task.status,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn attempt_number(&self) -> u64 {
|
||||
match self.status() {
|
||||
TaskStatus::Pending(_) => 0,
|
||||
TaskStatus::Retry(status) => status.attempt_number,
|
||||
TaskStatus::Failed(status) => status.failed_attempt_number,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn due_timestamp(&self) -> u64 {
|
||||
match self.status() {
|
||||
TaskStatus::Pending(status) => status.due.timestamp() as u64,
|
||||
TaskStatus::Retry(status) => status.due.timestamp() as u64,
|
||||
TaskStatus::Failed(_) => u64::MAX,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn permission(&self) -> Permission {
|
||||
match self {
|
||||
Task::IndexDocument(_) => Permission::TaskIndexDocument,
|
||||
Task::UnindexDocument(_) => Permission::TaskUnindexDocument,
|
||||
Task::IndexTrace(_) => Permission::TaskIndexTrace,
|
||||
Task::CalendarAlarmEmail(_) => Permission::TaskCalendarAlarmEmail,
|
||||
Task::CalendarAlarmNotification(_) => Permission::TaskCalendarAlarmNotification,
|
||||
Task::CalendarItipMessage(_) => Permission::TaskCalendarItipMessage,
|
||||
Task::MergeThreads(_) => Permission::TaskMergeThreads,
|
||||
Task::DmarcReport(_) => Permission::TaskDmarcReport,
|
||||
Task::TlsReport(_) => Permission::TaskTlsReport,
|
||||
Task::RestoreArchivedItem(_) => Permission::TaskRestoreArchivedItem,
|
||||
Task::DestroyAccount(_) => Permission::TaskDestroyAccount,
|
||||
Task::AccountMaintenance(_) => Permission::TaskAccountMaintenance,
|
||||
Task::StoreMaintenance(_) => Permission::TaskStoreMaintenance,
|
||||
Task::SpamFilterMaintenance(_) => Permission::TaskSpamFilterMaintenance,
|
||||
Task::AcmeRenewal(_) => Permission::TaskAcmeRenewal,
|
||||
Task::DkimManagement(_) => Permission::TaskDkimManagement,
|
||||
Task::DnsManagement(_) => Permission::TaskDnsManagement,
|
||||
Task::TenantMaintenance(_) => Permission::TaskTenantMaintenance,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Action {
|
||||
pub fn permission(&self) -> Permission {
|
||||
match self {
|
||||
Action::ReloadSettings => Permission::ActionReloadSettings,
|
||||
Action::ReloadTlsCertificates => Permission::ActionReloadTlsCertificates,
|
||||
Action::ReloadLookupStores => Permission::ActionReloadLookupStores,
|
||||
Action::ReloadBlockedIps => Permission::ActionReloadBlockedIps,
|
||||
Action::TroubleshootDmarc(_) => Permission::ActionTroubleshootDmarc,
|
||||
Action::ClassifySpam(_) => Permission::ActionClassifySpam,
|
||||
Action::InvalidateCaches => Permission::ActionInvalidateCaches,
|
||||
Action::InvalidateNegativeCaches => Permission::ActionInvalidateNegativeCaches,
|
||||
Action::PauseMtaQueue => Permission::ActionPauseMtaQueue,
|
||||
Action::ResumeMtaQueue => Permission::ActionResumeMtaQueue,
|
||||
Action::UpdateApps => Permission::ActionUpdateApps,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TaskStatus {
|
||||
pub fn now() -> Self {
|
||||
let now = UTCDateTime::now();
|
||||
TaskStatus::Pending(TaskStatusPending {
|
||||
created_at: now,
|
||||
due: now,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn at(timestamp: i64) -> Self {
|
||||
TaskStatus::Pending(TaskStatusPending {
|
||||
due: UTCDateTime::from_timestamp(timestamp),
|
||||
created_at: UTCDateTime::now(),
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user