From 6c612f7e1cf041bb031a40a02815510788f8504e Mon Sep 17 00:00:00 2001 From: mdecimus Date: Tue, 23 Sep 2025 20:30:10 +0200 Subject: [PATCH] JMAP protocol layer refactoring (part 1) --- Cargo.lock | 34 +- crates/common/src/config/jmap/settings.rs | 78 +- crates/email/src/mailbox/mod.rs | 3 +- crates/imap-proto/src/lib.rs | 21 - crates/jmap-proto/Cargo.toml | 1 + crates/jmap-proto/src/error/method.rs | 3 +- crates/jmap-proto/src/error/set.rs | 26 +- crates/jmap-proto/src/lib.rs | 1 - crates/jmap-proto/src/method/changes.rs | 16 +- crates/jmap-proto/src/method/copy.rs | 37 +- crates/jmap-proto/src/method/get.rs | 36 +- crates/jmap-proto/src/method/import.rs | 21 +- crates/jmap-proto/src/method/lookup.rs | 17 +- crates/jmap-proto/src/method/parse.rs | 24 +- crates/jmap-proto/src/method/query.rs | 23 +- crates/jmap-proto/src/method/query_changes.rs | 14 +- .../jmap-proto/src/method/search_snippet.rs | 10 +- crates/jmap-proto/src/method/set.rs | 54 +- crates/jmap-proto/src/method/upload.rs | 8 +- crates/jmap-proto/src/method/validate.rs | 10 +- crates/jmap-proto/src/object/blob.rs | 169 ++- crates/jmap-proto/src/object/email.rs | 352 +++++- .../jmap-proto/src/object/email_submission.rs | 259 +++- crates/jmap-proto/src/object/identity.rs | 103 ++ crates/jmap-proto/src/object/mailbox.rs | 151 ++- crates/jmap-proto/src/object/mod.rs | 99 +- crates/jmap-proto/src/object/principal.rs | 113 ++ .../src/object/push_subscription.rs | 123 ++ crates/jmap-proto/src/object/quota.rs | 91 ++ .../jmap-proto/src/object/search_snippet.rs | 69 + crates/jmap-proto/src/object/sieve.rs | 86 +- crates/jmap-proto/src/object/thread.rs | 66 + .../src/object/vacation_response.rs | 88 ++ crates/jmap-proto/src/parser/base32.rs | 65 - crates/jmap-proto/src/parser/impls.rs | 308 ----- crates/jmap-proto/src/parser/json.rs | 394 ------ crates/jmap-proto/src/parser/mod.rs | 165 --- crates/jmap-proto/src/request/capability.rs | 66 +- crates/jmap-proto/src/request/echo.rs | 38 - crates/jmap-proto/src/request/method.rs | 151 +-- crates/jmap-proto/src/request/mod.rs | 109 +- crates/jmap-proto/src/request/parser.rs | 30 +- crates/jmap-proto/src/request/reference.rs | 134 +- crates/jmap-proto/src/request/websocket.rs | 15 +- crates/jmap-proto/src/response/mod.rs | 25 +- crates/jmap-proto/src/response/references.rs | 9 +- crates/jmap-proto/src/types/acl.rs | 41 - crates/jmap-proto/src/types/any_id.rs | 130 -- crates/jmap-proto/src/types/blob.rs | 18 - crates/jmap-proto/src/types/date.rs | 26 +- crates/jmap-proto/src/types/id.rs | 64 - crates/jmap-proto/src/types/keyword.rs | 58 - crates/jmap-proto/src/types/mod.rs | 43 - crates/jmap-proto/src/types/pointer.rs | 271 ---- crates/jmap-proto/src/types/property.rs | 1106 ----------------- crates/jmap-proto/src/types/state.rs | 50 +- crates/jmap-proto/src/types/type_state.rs | 44 - crates/jmap-proto/src/types/value.rs | 594 --------- crates/types/src/blob.rs | 13 +- crates/types/src/id.rs | 52 +- crates/types/src/keyword.rs | 191 ++- crates/types/src/lib.rs | 1 + crates/types/src/special_use.rs | 82 ++ crates/types/src/type_state.rs | 66 +- crates/utils/Cargo.toml | 2 - crates/utils/src/json/mod.rs | 32 - crates/utils/src/json/parser/base32.rs | 65 - crates/utils/src/json/parser/impls.rs | 308 ----- crates/utils/src/json/parser/json.rs | 390 ------ crates/utils/src/json/parser/mod.rs | 166 --- crates/utils/src/json/parser/pointer.rs | 222 ---- crates/utils/src/json/pointer.rs | 112 -- crates/utils/src/lib.rs | 4 - 73 files changed, 2222 insertions(+), 5644 deletions(-) create mode 100644 crates/jmap-proto/src/object/identity.rs create mode 100644 crates/jmap-proto/src/object/principal.rs create mode 100644 crates/jmap-proto/src/object/push_subscription.rs create mode 100644 crates/jmap-proto/src/object/quota.rs create mode 100644 crates/jmap-proto/src/object/search_snippet.rs create mode 100644 crates/jmap-proto/src/object/thread.rs create mode 100644 crates/jmap-proto/src/object/vacation_response.rs delete mode 100644 crates/jmap-proto/src/parser/base32.rs delete mode 100644 crates/jmap-proto/src/parser/impls.rs delete mode 100644 crates/jmap-proto/src/parser/json.rs delete mode 100644 crates/jmap-proto/src/parser/mod.rs delete mode 100644 crates/jmap-proto/src/request/echo.rs delete mode 100644 crates/jmap-proto/src/types/acl.rs delete mode 100644 crates/jmap-proto/src/types/any_id.rs delete mode 100644 crates/jmap-proto/src/types/blob.rs delete mode 100644 crates/jmap-proto/src/types/id.rs delete mode 100644 crates/jmap-proto/src/types/keyword.rs delete mode 100644 crates/jmap-proto/src/types/pointer.rs delete mode 100644 crates/jmap-proto/src/types/property.rs delete mode 100644 crates/jmap-proto/src/types/type_state.rs delete mode 100644 crates/jmap-proto/src/types/value.rs create mode 100644 crates/types/src/special_use.rs delete mode 100644 crates/utils/src/json/mod.rs delete mode 100644 crates/utils/src/json/parser/base32.rs delete mode 100644 crates/utils/src/json/parser/impls.rs delete mode 100644 crates/utils/src/json/parser/json.rs delete mode 100644 crates/utils/src/json/parser/mod.rs delete mode 100644 crates/utils/src/json/parser/pointer.rs delete mode 100644 crates/utils/src/json/pointer.rs diff --git a/Cargo.lock b/Cargo.lock index 11090f42..c3e4980c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2063,12 +2063,6 @@ version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10" -[[package]] -name = "downcast-rs" -version = "2.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "117240f60069e65410b3ae1bb213295bd828f707b5bec6596a1afc8793ce0cbc" - [[package]] name = "dsa" version = "0.6.3" @@ -2301,16 +2295,6 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" -[[package]] -name = "erased-serde" -version = "0.4.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "110ca254af04e46794fcc4be0991e72e13fdd8c78119e02c76a5473f6f74e049" -dependencies = [ - "serde_core", - "typeid", -] - [[package]] name = "errno" version = "0.3.14" @@ -3919,6 +3903,15 @@ dependencies = [ "tokio-tungstenite 0.21.0", ] +[[package]] +name = "jmap-tools" +version = "0.1.1" +dependencies = [ + "hashify", + "serde", + "serde_json", +] + [[package]] name = "jmap_proto" version = "0.13.3" @@ -3927,6 +3920,7 @@ dependencies = [ "compact_str", "fast-float", "hashify", + "jmap-tools", "mail-parser", "rkyv", "serde", @@ -8687,12 +8681,6 @@ version = "2.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ea3136b675547379c4bd395ca6b938e5ad3c3d20fad76e7fe85f9e0d011419c" -[[package]] -name = "typeid" -version = "1.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc7d623258602320d5c55d1bc22793b57daff0ec7efc270ea7d55ce1d5f5471c" - [[package]] name = "typenum" version = "1.18.0" @@ -8904,8 +8892,6 @@ dependencies = [ "blake3", "chrono", "compact_str", - "downcast-rs", - "erased-serde", "fast-float", "form_urlencoded", "futures", diff --git a/crates/common/src/config/jmap/settings.rs b/crates/common/src/config/jmap/settings.rs index d3a5e610..edb45b3c 100644 --- a/crates/common/src/config/jmap/settings.rs +++ b/crates/common/src/config/jmap/settings.rs @@ -7,6 +7,7 @@ use jmap_proto::request::capability::BaseCapabilities; use nlp::language::Language; use std::{str::FromStr, time::Duration}; +use types::special_use::SpecialUse; use utils::config::{Config, Rate, cron::SimpleCron, utils::ParseValue}; #[derive(Default, Clone)] @@ -83,22 +84,6 @@ pub struct DefaultFolder { pub create: bool, } -#[derive( - rkyv::Archive, rkyv::Deserialize, rkyv::Serialize, Clone, Copy, PartialEq, Eq, Hash, Debug, -)] -#[rkyv(derive(Debug))] -pub enum SpecialUse { - Inbox, - Trash, - Junk, - Drafts, - Archive, - Sent, - Shared, - Important, - None, -} - impl JmapConfig { pub fn parse(config: &mut Config) -> Self { // Parse HTTP headers @@ -364,65 +349,6 @@ impl JmapConfig { impl ParseValue for SpecialUse { fn parse_value(value: &str) -> Result { - hashify::tiny_map_ignore_case!(value.as_bytes(), - b"inbox" => SpecialUse::Inbox, - b"trash" => SpecialUse::Trash, - b"junk" => SpecialUse::Junk, - b"drafts" => SpecialUse::Drafts, - b"archive" => SpecialUse::Archive, - b"sent" => SpecialUse::Sent, - b"shared" => SpecialUse::Shared, - b"important" => SpecialUse::Important, - - ) - .ok_or_else(|| format!("Unknown folder role {:?}", value)) - } -} - -impl SpecialUse { - pub fn as_str(&self) -> Option<&'static str> { - match self { - SpecialUse::Inbox => Some("inbox"), - SpecialUse::Trash => Some("trash"), - SpecialUse::Junk => Some("junk"), - SpecialUse::Drafts => Some("drafts"), - SpecialUse::Archive => Some("archive"), - SpecialUse::Sent => Some("sent"), - SpecialUse::Shared => Some("shared"), - SpecialUse::Important => Some("important"), - SpecialUse::None => None, - } - } -} - -impl ArchivedSpecialUse { - pub fn as_str(&self) -> Option<&'static str> { - match self { - ArchivedSpecialUse::Inbox => Some("inbox"), - ArchivedSpecialUse::Trash => Some("trash"), - ArchivedSpecialUse::Junk => Some("junk"), - ArchivedSpecialUse::Drafts => Some("drafts"), - ArchivedSpecialUse::Archive => Some("archive"), - ArchivedSpecialUse::Sent => Some("sent"), - ArchivedSpecialUse::Shared => Some("shared"), - ArchivedSpecialUse::Important => Some("important"), - ArchivedSpecialUse::None => None, - } - } -} - -impl From<&ArchivedSpecialUse> for SpecialUse { - fn from(value: &ArchivedSpecialUse) -> Self { - match value { - ArchivedSpecialUse::Inbox => SpecialUse::Inbox, - ArchivedSpecialUse::Trash => SpecialUse::Trash, - ArchivedSpecialUse::Junk => SpecialUse::Junk, - ArchivedSpecialUse::Drafts => SpecialUse::Drafts, - ArchivedSpecialUse::Archive => SpecialUse::Archive, - ArchivedSpecialUse::Sent => SpecialUse::Sent, - ArchivedSpecialUse::Shared => SpecialUse::Shared, - ArchivedSpecialUse::Important => SpecialUse::Important, - ArchivedSpecialUse::None => SpecialUse::None, - } + SpecialUse::parse(value).ok_or_else(|| format!("Unknown folder role {:?}", value)) } } diff --git a/crates/email/src/mailbox/mod.rs b/crates/email/src/mailbox/mod.rs index 4510da3c..88cc9c24 100644 --- a/crates/email/src/mailbox/mod.rs +++ b/crates/email/src/mailbox/mod.rs @@ -4,8 +4,7 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use common::config::jmap::settings::SpecialUse; -use types::acl::AclGrant; +use types::{acl::AclGrant, special_use::SpecialUse}; pub mod destroy; pub mod index; diff --git a/crates/imap-proto/src/lib.rs b/crates/imap-proto/src/lib.rs index 693ae899..e4140ffe 100644 --- a/crates/imap-proto/src/lib.rs +++ b/crates/imap-proto/src/lib.rs @@ -243,24 +243,3 @@ impl StatusResponse { } } } - -/* -impl From for ResponseCode { - fn from(value: SetErrorType) -> Self { - match value { - SetErrorType::Forbidden => ResponseCode::NoPerm, - SetErrorType::OverQuota => ResponseCode::OverQuota, - SetErrorType::RateLimit | SetErrorType::TooLarge => ResponseCode::Limit, - SetErrorType::NotFound | SetErrorType::BlobNotFound => ResponseCode::NonExistent, - SetErrorType::MailboxHasChild | SetErrorType::MailboxHasEmail => { - ResponseCode::HasChildren - } - SetErrorType::ForbiddenFrom - | SetErrorType::ForbiddenMailFrom - | SetErrorType::ForbiddenToSend => ResponseCode::AuthorizationFailed, - SetErrorType::AlreadyExists => ResponseCode::AlreadyExists, - _ => ResponseCode::Cannot, - } - } -} -*/ diff --git a/crates/jmap-proto/Cargo.toml b/crates/jmap-proto/Cargo.toml index 1b9656da..302d1c93 100644 --- a/crates/jmap-proto/Cargo.toml +++ b/crates/jmap-proto/Cargo.toml @@ -9,6 +9,7 @@ store = { path = "../store" } utils = { path = "../utils" } types = { path = "../types" } trc = { path = "../trc" } +jmap-tools = { path = "/Users/me/code/jmap-tool" } mail-parser = { version = "0.11", features = ["full_encoding", "rkyv"] } fast-float = "0.2.0" serde = { version = "1.0", features = ["derive"]} diff --git a/crates/jmap-proto/src/error/method.rs b/crates/jmap-proto/src/error/method.rs index 62f33e95..87911650 100644 --- a/crates/jmap-proto/src/error/method.rs +++ b/crates/jmap-proto/src/error/method.rs @@ -4,10 +4,9 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use std::fmt::Display; - use serde::Serialize; use serde::ser::SerializeMap; +use std::fmt::Display; #[derive(Debug)] pub enum MethodError { diff --git a/crates/jmap-proto/src/error/set.rs b/crates/jmap-proto/src/error/set.rs index d5497d66..994a3582 100644 --- a/crates/jmap-proto/src/error/set.rs +++ b/crates/jmap-proto/src/error/set.rs @@ -4,14 +4,12 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ +use jmap_tools::{Key, Property}; use std::borrow::Cow; - use types::id::Id; -use crate::types::property::Property; - #[derive(Debug, Clone, serde::Serialize)] -pub struct SetError { +pub struct SetError { #[serde(rename = "type")] pub type_: SetErrorType, @@ -19,7 +17,7 @@ pub struct SetError { pub description: Option>, #[serde(skip_serializing_if = "Option::is_none")] - pub properties: Option>, + pub properties: Option>>, #[serde(rename = "existingId")] #[serde(skip_serializing_if = "Option::is_none")] @@ -27,9 +25,9 @@ pub struct SetError { } #[derive(Debug, Clone)] -pub enum InvalidProperty { - Property(Property), - Path(Vec), +pub enum InvalidProperty { + Property(Key<'static, T>), + Path(Vec>), } #[derive(Debug, Clone, PartialEq, Eq, serde::Serialize)] @@ -118,7 +116,7 @@ impl SetErrorType { } } -impl SetError { +impl SetError { pub fn new(type_: SetErrorType) -> Self { SetError { type_, @@ -188,19 +186,19 @@ impl SetError { } } -impl From for InvalidProperty { - fn from(property: Property) -> Self { +impl From for InvalidProperty { + fn from(property: T) -> Self { InvalidProperty::Property(property) } } -impl From<(Property, Property)> for InvalidProperty { - fn from((a, b): (Property, Property)) -> Self { +impl From<(T, T)> for InvalidProperty { + fn from((a, b): (T, T)) -> Self { InvalidProperty::Path(vec![a, b]) } } -impl serde::Serialize for InvalidProperty { +impl serde::Serialize for InvalidProperty { fn serialize(&self, serializer: S) -> Result where S: serde::Serializer, diff --git a/crates/jmap-proto/src/lib.rs b/crates/jmap-proto/src/lib.rs index aea9546e..d6f926bb 100644 --- a/crates/jmap-proto/src/lib.rs +++ b/crates/jmap-proto/src/lib.rs @@ -7,7 +7,6 @@ pub mod error; pub mod method; pub mod object; -pub mod parser; pub mod request; pub mod response; pub mod types; diff --git a/crates/jmap-proto/src/method/changes.rs b/crates/jmap-proto/src/method/changes.rs index ebf36bfe..75523426 100644 --- a/crates/jmap-proto/src/method/changes.rs +++ b/crates/jmap-proto/src/method/changes.rs @@ -4,12 +4,9 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use crate::{ - parser::{Ignore, JsonObjectParser, Token, json::Parser}, - request::{RequestProperty, method::MethodObject}, - types::{property::Property, state::State}, -}; +use crate::{object::JmapObject, request::method::MethodObject, types::state::State}; use compact_str::format_compact; +use jmap_tools::Property; use types::id::Id; #[derive(Debug, Clone)] @@ -17,11 +14,10 @@ pub struct ChangesRequest { pub account_id: Id, pub since_state: State, pub max_changes: Option, - pub arguments: RequestArguments, } #[derive(Debug, Clone, serde::Serialize)] -pub struct ChangesResponse { +pub struct ChangesResponse { #[serde(rename = "accountId")] pub account_id: Id, @@ -42,10 +38,10 @@ pub struct ChangesResponse { #[serde(rename = "updatedProperties")] #[serde(skip_serializing_if = "Option::is_none")] - pub updated_properties: Option>, + pub updated_properties: Option>, } -#[derive(Debug, Clone, serde::Serialize)] +/*#[derive(Debug, Clone, serde::Serialize)] pub enum RequestArguments { Email, Mailbox, @@ -53,7 +49,7 @@ pub enum RequestArguments { Identity, EmailSubmission, Quota, -} +}*/ impl JsonObjectParser for ChangesRequest { fn parse(parser: &mut Parser<'_>) -> trc::Result diff --git a/crates/jmap-proto/src/method/copy.rs b/crates/jmap-proto/src/method/copy.rs index e07caaae..76897639 100644 --- a/crates/jmap-proto/src/method/copy.rs +++ b/crates/jmap-proto/src/method/copy.rs @@ -6,32 +6,34 @@ use crate::{ error::set::SetError, - parser::{JsonObjectParser, Token, json::Parser}, - request::{RequestProperty, method::MethodObject, reference::MaybeReference}, - types::{ - state::State, - value::{Object, SetValue, Value}, - }, + object::{JmapObject, blob::BlobProperty}, + request::{MaybeInvalid, method::MethodObject, reference::MaybeIdReference}, + types::state::State, }; use compact_str::format_compact; +use jmap_tools::Value; use serde::Serialize; use types::{blob::BlobId, id::Id}; use utils::map::vec_map::VecMap; #[derive(Debug, Clone)] -pub struct CopyRequest { +pub struct CopyRequest<'x, T: JmapObject> { pub from_account_id: Id, pub if_from_in_state: Option, pub account_id: Id, pub if_in_state: Option, - pub create: VecMap, Object>, + pub create: VecMap, Value<'x, T::Property, T::Element>>, pub on_success_destroy_original: Option, pub destroy_from_if_in_state: Option, - pub arguments: T, } +/*#[derive(Debug, Clone)] +pub enum RequestArguments { + Email, +}*/ + #[derive(Debug, Clone, serde::Serialize)] -pub struct CopyResponse { +pub struct CopyResponse { #[serde(rename = "fromAccountId")] pub from_account_id: Id, @@ -46,18 +48,18 @@ pub struct CopyResponse { #[serde(rename = "created")] #[serde(skip_serializing_if = "VecMap::is_empty")] - pub created: VecMap>, + pub created: VecMap>, #[serde(rename = "notCreated")] #[serde(skip_serializing_if = "VecMap::is_empty")] - pub not_created: VecMap, + pub not_created: VecMap, SetError>, } #[derive(Debug, Clone)] pub struct CopyBlobRequest { pub from_account_id: Id, pub account_id: Id, - pub blob_ids: Vec, + pub blob_ids: Vec>, } #[derive(Debug, Clone, Serialize)] @@ -74,12 +76,7 @@ pub struct CopyBlobResponse { #[serde(rename = "notCopied")] #[serde(skip_serializing_if = "VecMap::is_empty")] - pub not_copied: VecMap, -} - -#[derive(Debug, Clone)] -pub enum RequestArguments { - Email, + pub not_copied: VecMap, SetError>, } impl JsonObjectParser for CopyRequest { @@ -116,7 +113,7 @@ impl JsonObjectParser for CopyRequest { } 0x6574_6165_7263 => { request.create = - , Object>>::parse(parser)?; + , Value<'x, P, E>>>::parse(parser)?; } 0x0064_4974_6e75_6f63_6341_6d6f_7266 => { request.from_account_id = diff --git a/crates/jmap-proto/src/method/get.rs b/crates/jmap-proto/src/method/get.rs index cfe62ac0..69ea229f 100644 --- a/crates/jmap-proto/src/method/get.rs +++ b/crates/jmap-proto/src/method/get.rs @@ -5,32 +5,28 @@ */ use crate::{ - object::{blob, email}, - parser::{JsonObjectParser, Token, json::Parser}, + object::JmapObject, request::{ - RequestProperty, RequestPropertyParser, + MaybeInvalid, method::MethodObject, - reference::{MaybeReference, ResultReference}, - }, - types::{ - any_id::AnyId, - property::Property, - state::State, - value::{Object, Value}, + reference::{MaybeIdReference, MaybeResultReference, ResultReference}, }, + types::state::State, }; use compact_str::format_compact; +use jmap_tools::{Property, Value}; use types::{blob::BlobId, id::Id}; -#[derive(Debug, Clone)] -pub struct GetRequest { +#[derive(Debug, Clone, serde::Deserialize)] +pub struct GetRequest { pub account_id: Id, - pub ids: Option>, ResultReference>>, - pub properties: Option, ResultReference>>, - pub arguments: T, + pub ids: Option>>>, + pub properties: Option>>, + #[serde(flatten)] + pub arguments: T::GetArguments, } -#[derive(Debug, Clone)] +/*#[derive(Debug, Clone)] pub enum RequestArguments { Email(email::GetArguments), Mailbox, @@ -43,10 +39,10 @@ pub enum RequestArguments { Principal, Quota, Blob(blob::GetArguments), -} +}*/ #[derive(Debug, Clone, serde::Serialize)] -pub struct GetResponse { +pub struct GetResponse { #[serde(rename = "accountId")] #[serde(skip_serializing_if = "Option::is_none")] pub account_id: Option, @@ -54,10 +50,10 @@ pub struct GetResponse { #[serde(skip_serializing_if = "Option::is_none")] pub state: Option, - pub list: Vec>, + pub list: Vec>, #[serde(rename = "notFound")] - pub not_found: Vec, + pub not_found: Vec>, } impl JsonObjectParser for GetRequest { diff --git a/crates/jmap-proto/src/method/import.rs b/crates/jmap-proto/src/method/import.rs index 4701c519..3896f1b4 100644 --- a/crates/jmap-proto/src/method/import.rs +++ b/crates/jmap-proto/src/method/import.rs @@ -6,19 +6,12 @@ use crate::{ error::set::SetError, - parser::{JsonObjectParser, Token, json::Parser}, - request::{ - RequestProperty, - reference::{MaybeReference, ResultReference}, - }, + object::email::{EmailProperty, EmailValue}, + request::reference::{MaybeIdReference, MaybeResultReference, ResultReference}, response::Response, - types::{ - date::UTCDate, - property::Property, - state::State, - value::{Object, SetValueMap, Value}, - }, + types::{date::UTCDate, state::State}, }; +use jmap_tools::Value; use types::{blob::BlobId, id::Id, keyword::Keyword}; use utils::map::vec_map::VecMap; @@ -32,7 +25,7 @@ pub struct ImportEmailRequest { #[derive(Debug, Clone)] pub struct ImportEmail { pub blob_id: BlobId, - pub mailbox_ids: MaybeReference>, ResultReference>, + pub mailbox_ids: MaybeResultReference>>, pub keywords: Vec, pub received_at: Option, } @@ -51,11 +44,11 @@ pub struct ImportEmailResponse { #[serde(rename = "created")] #[serde(skip_serializing_if = "VecMap::is_empty")] - pub created: VecMap>, + pub created: VecMap>, #[serde(rename = "notCreated")] #[serde(skip_serializing_if = "VecMap::is_empty")] - pub not_created: VecMap, + pub not_created: VecMap>, } impl JsonObjectParser for ImportEmailRequest { diff --git a/crates/jmap-proto/src/method/lookup.rs b/crates/jmap-proto/src/method/lookup.rs index 530c5983..326bf16f 100644 --- a/crates/jmap-proto/src/method/lookup.rs +++ b/crates/jmap-proto/src/method/lookup.rs @@ -4,19 +4,16 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use crate::{ - parser::{JsonObjectParser, Token, json::Parser}, - request::RequestProperty, - types::MaybeUnparsable, -}; use types::{blob::BlobId, id::Id, type_state::DataType}; use utils::map::vec_map::VecMap; +use crate::request::MaybeInvalid; + #[derive(Debug, Clone)] pub struct BlobLookupRequest { pub account_id: Id, - pub type_names: Vec>, - pub ids: Vec>, + pub type_names: Vec>, + pub ids: Vec>, } #[derive(Debug, Clone, Default, serde::Serialize)] @@ -28,7 +25,7 @@ pub struct BlobLookupResponse { pub list: Vec, #[serde(rename = "notFound")] - pub not_found: Vec>, + pub not_found: Vec>, } #[derive(Debug, Clone, Default, serde::Serialize)] @@ -59,10 +56,10 @@ impl JsonObjectParser for BlobLookupRequest { request.account_id = parser.next_token::()?.unwrap_string("accountId")?; } 0x0073_656d_614e_6570_7974 if !key.is_ref => { - request.type_names = >>::parse(parser)?; + request.type_names = >>::parse(parser)?; } 0x0073_6469 if !key.is_ref => { - request.ids = >>::parse(parser)?; + request.ids = >>::parse(parser)?; } _ => { parser.skip_token(parser.depth_array, parser.depth_dict)?; diff --git a/crates/jmap-proto/src/method/parse.rs b/crates/jmap-proto/src/method/parse.rs index afe9f01c..9804b0d3 100644 --- a/crates/jmap-proto/src/method/parse.rs +++ b/crates/jmap-proto/src/method/parse.rs @@ -4,23 +4,21 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use crate::{ - parser::{Ignore, JsonObjectParser, Token, json::Parser}, - request::RequestProperty, - types::{ - property::Property, - value::{Object, Value}, - }, -}; +use jmap_tools::Value; use types::{blob::BlobId, id::Id}; use utils::map::vec_map::VecMap; +use crate::{ + object::email::{EmailProperty, EmailValue}, + request::MaybeInvalid, +}; + #[derive(Debug, Clone)] pub struct ParseEmailRequest { pub account_id: Id, - pub blob_ids: Vec, - pub properties: Option>, - pub body_properties: Option>, + pub blob_ids: Vec>, + pub properties: Option>, + pub body_properties: Option>, pub fetch_text_body_values: Option, pub fetch_html_body_values: Option, pub fetch_all_body_values: Option, @@ -34,7 +32,7 @@ pub struct ParseEmailResponse { #[serde(rename = "parsed")] #[serde(skip_serializing_if = "VecMap::is_empty")] - pub parsed: VecMap>, + pub parsed: VecMap>, #[serde(rename = "notParsable")] #[serde(skip_serializing_if = "Vec::is_empty")] @@ -42,7 +40,7 @@ pub struct ParseEmailResponse { #[serde(rename = "notFound")] #[serde(skip_serializing_if = "Vec::is_empty")] - pub not_found: Vec, + pub not_found: Vec>, } impl JsonObjectParser for ParseEmailRequest { diff --git a/crates/jmap-proto/src/method/query.rs b/crates/jmap-proto/src/method/query.rs index 2a154347..69b410f7 100644 --- a/crates/jmap-proto/src/method/query.rs +++ b/crates/jmap-proto/src/method/query.rs @@ -5,9 +5,8 @@ */ use crate::{ - object::{email, mailbox}, - parser::{Ignore, JsonObjectParser, Token, json::Parser}, - request::{RequestProperty, RequestPropertyParser, method::MethodObject}, + object::{JmapObject, email, mailbox}, + request::method::MethodObject, types::{date::UTCDate, state::State}, }; use compact_str::format_compact; @@ -16,16 +15,16 @@ use store::fts::{FilterItem, FilterType, FtsFilter}; use types::{id::Id, keyword::Keyword}; #[derive(Debug, Clone)] -pub struct QueryRequest { +pub struct QueryRequest { pub account_id: Id, - pub filter: Vec, - pub sort: Option>, + pub filter: Vec, + pub sort: Option>, pub position: Option, pub anchor: Option, pub anchor_offset: Option, pub limit: Option, pub calculate_total: Option, - pub arguments: T, + pub arguments: T::QueryArguments, } #[derive(Debug, Clone, serde::Serialize)] @@ -664,16 +663,6 @@ impl Display for SortProperty { } } -impl RequestPropertyParser for RequestArguments { - fn parse(&mut self, parser: &mut Parser, property: RequestProperty) -> trc::Result { - match self { - RequestArguments::Email(args) => args.parse(parser, property), - RequestArguments::Mailbox(args) => args.parse(parser, property), - _ => Ok(false), - } - } -} - impl Filter { pub fn is_immutable(&self) -> bool { matches!( diff --git a/crates/jmap-proto/src/method/query_changes.rs b/crates/jmap-proto/src/method/query_changes.rs index 344a82b4..515a3636 100644 --- a/crates/jmap-proto/src/method/query_changes.rs +++ b/crates/jmap-proto/src/method/query_changes.rs @@ -5,24 +5,20 @@ */ use super::query::{Comparator, Filter, RequestArguments, parse_filter, parse_sort}; -use crate::{ - parser::{Ignore, JsonObjectParser, Token, json::Parser}, - request::{RequestProperty, RequestPropertyParser, method::MethodObject}, - types::state::State, -}; +use crate::{object::JmapObject, request::method::MethodObject, types::state::State}; use compact_str::format_compact; use types::id::Id; #[derive(Debug, Clone)] -pub struct QueryChangesRequest { +pub struct QueryChangesRequest { pub account_id: Id, - pub filter: Vec, - pub sort: Option>, + pub filter: Vec, + pub sort: Option>, pub since_query_state: State, pub max_changes: Option, pub up_to_id: Option, pub calculate_total: Option, - pub arguments: RequestArguments, + pub arguments: T::QueryArguments, } #[derive(Debug, Clone, serde::Serialize)] diff --git a/crates/jmap-proto/src/method/search_snippet.rs b/crates/jmap-proto/src/method/search_snippet.rs index da9bb693..37866b8d 100644 --- a/crates/jmap-proto/src/method/search_snippet.rs +++ b/crates/jmap-proto/src/method/search_snippet.rs @@ -5,20 +5,14 @@ */ use super::query::{Filter, parse_filter}; -use crate::{ - parser::{Ignore, JsonObjectParser, Token, json::Parser}, - request::{ - RequestProperty, - reference::{MaybeReference, ResultReference}, - }, -}; +use crate::request::reference::{MaybeResultReference, ResultReference}; use types::id::Id; #[derive(Debug, Clone)] pub struct GetSearchSnippetRequest { pub account_id: Id, pub filter: Vec, - pub email_ids: MaybeReference, ResultReference>, + pub email_ids: MaybeResultReference>, } #[derive(Debug, Clone, serde::Serialize)] diff --git a/crates/jmap-proto/src/method/set.rs b/crates/jmap-proto/src/method/set.rs index 7897e553..1551833a 100644 --- a/crates/jmap-proto/src/method/set.rs +++ b/crates/jmap-proto/src/method/set.rs @@ -7,38 +7,32 @@ use super::ahash_is_empty; use crate::{ error::set::{InvalidProperty, SetError}, - object::{email_submission, mailbox, sieve}, - parser::{JsonObjectParser, Token, json::Parser}, + object::JmapObject, request::{ - RequestProperty, RequestPropertyParser, + MaybeInvalid, method::MethodObject, - reference::{MaybeReference, ResultReference}, + reference::{MaybeResultReference, ResultReference}, }, response::Response, - types::{ - any_id::AnyId, - date::UTCDate, - property::{HeaderForm, ObjectProperty, Property, SetProperty}, - state::State, - value::{Object, SetValue, SetValueMap, Value}, - }, + types::{date::UTCDate, state::State}, }; use ahash::AHashMap; use compact_str::format_compact; +use jmap_tools::Value; use types::{acl::Acl, blob::BlobId, id::Id, keyword::Keyword}; use utils::map::{bitmap::Bitmap, vec_map::VecMap}; #[derive(Debug, Clone)] -pub struct SetRequest { +pub struct SetRequest<'x, T: JmapObject> { pub account_id: Id, pub if_in_state: Option, - pub create: Option>>, - pub update: Option>>, - pub destroy: Option, ResultReference>>, - pub arguments: T, + pub create: Option>>, + pub update: Option, Value<'x, T::Property, T::Element>>>, + pub destroy: Option>>>, + pub arguments: T::SetArguments, } -#[derive(Debug, Clone)] +/*#[derive(Debug, Clone)] pub enum RequestArguments { Email, Mailbox(mailbox::SetArguments), @@ -47,10 +41,10 @@ pub enum RequestArguments { PushSubscription, SieveScript(sieve::SetArguments), VacationResponse, -} +}*/ #[derive(Debug, Clone, Default, serde::Serialize)] -pub struct SetResponse { +pub struct SetResponse { #[serde(rename = "accountId")] #[serde(skip_serializing_if = "Option::is_none")] pub account_id: Option, @@ -65,11 +59,11 @@ pub struct SetResponse { #[serde(rename = "created")] #[serde(skip_serializing_if = "ahash_is_empty")] - pub created: AHashMap>, + pub created: AHashMap>, #[serde(rename = "updated")] #[serde(skip_serializing_if = "VecMap::is_empty")] - pub updated: VecMap>>, + pub updated: VecMap>>, #[serde(rename = "destroyed")] #[serde(skip_serializing_if = "Vec::is_empty")] @@ -77,15 +71,15 @@ pub struct SetResponse { #[serde(rename = "notCreated")] #[serde(skip_serializing_if = "VecMap::is_empty")] - pub not_created: VecMap, + pub not_created: VecMap>, #[serde(rename = "notUpdated")] #[serde(skip_serializing_if = "VecMap::is_empty")] - pub not_updated: VecMap, + pub not_updated: VecMap, SetError>, #[serde(rename = "notDestroyed")] #[serde(skip_serializing_if = "VecMap::is_empty")] - pub not_destroyed: VecMap, + pub not_destroyed: VecMap, SetError>, } impl JsonObjectParser for SetRequest { @@ -127,10 +121,10 @@ impl JsonObjectParser for SetRequest { request.account_id = parser.next_token::()?.unwrap_string("accountId")?; } 0x6574_6165_7263 if !key.is_ref => { - request.create = >>>::parse(parser)?; + request.create = >>>::parse(parser)?; } 0x6574_6164_7075 if !key.is_ref => { - request.update = >>>::parse(parser)?; + request.update = >>>::parse(parser)?; } 0x0079_6f72_7473_6564 => { request.destroy = if !key.is_ref { @@ -156,7 +150,7 @@ impl JsonObjectParser for SetRequest { } } -impl JsonObjectParser for Object { +impl JsonObjectParser for Value<'x, P, E> { fn parse(parser: &mut Parser<'_>) -> trc::Result where Self: Sized, @@ -409,11 +403,11 @@ impl SetRequest { self.create.as_ref().is_some_and(|objs| !objs.is_empty()) } - pub fn unwrap_create(&mut self) -> VecMap> { + pub fn unwrap_create(&mut self) -> VecMap> { self.create.take().unwrap_or_default() } - pub fn unwrap_update(&mut self) -> VecMap> { + pub fn unwrap_update(&mut self) -> VecMap> { self.update.take().unwrap_or_default() } @@ -513,7 +507,7 @@ impl SetResponse { } } - pub fn get_object_by_id(&mut self, id: Id) -> Option<&mut Object> { + pub fn get_object_by_id(&mut self, id: Id) -> Option<&mut Value<'x, P, E>> { if let Some(obj) = self.updated.get_mut(&id) { if let Some(obj) = obj { return Some(obj); diff --git a/crates/jmap-proto/src/method/upload.rs b/crates/jmap-proto/src/method/upload.rs index 781df334..f86f0715 100644 --- a/crates/jmap-proto/src/method/upload.rs +++ b/crates/jmap-proto/src/method/upload.rs @@ -6,9 +6,7 @@ use super::ahash_is_empty; use crate::{ - error::set::SetError, - parser::{Ignore, JsonObjectParser, Token, json::Parser}, - request::{RequestProperty, reference::MaybeReference}, + error::set::SetError, object::blob::BlobProperty, request::reference::MaybeIdReference, response::Response, }; use ahash::AHashMap; @@ -31,7 +29,7 @@ pub struct UploadObject { #[derive(Debug, Clone, PartialEq, Eq)] pub enum DataSourceObject { Id { - id: MaybeReference, + id: MaybeIdReference, length: Option, offset: Option, }, @@ -49,7 +47,7 @@ pub struct BlobUploadResponse { #[serde(rename = "notCreated")] #[serde(skip_serializing_if = "VecMap::is_empty")] - pub not_created: VecMap, + pub not_created: VecMap>, } #[derive(Debug, Clone, Default, serde::Serialize)] diff --git a/crates/jmap-proto/src/method/validate.rs b/crates/jmap-proto/src/method/validate.rs index 8db57fd1..ac909994 100644 --- a/crates/jmap-proto/src/method/validate.rs +++ b/crates/jmap-proto/src/method/validate.rs @@ -4,25 +4,21 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use crate::{ - error::set::SetError, - parser::{JsonObjectParser, Token, json::Parser}, - request::RequestProperty, -}; +use crate::{error::set::SetError, object::sieve::SieveProperty, request::MaybeInvalid}; use serde::Serialize; use types::{blob::BlobId, id::Id}; #[derive(Debug, Clone)] pub struct ValidateSieveScriptRequest { pub account_id: Id, - pub blob_id: BlobId, + pub blob_id: MaybeInvalid, } #[derive(Debug, Serialize)] pub struct ValidateSieveScriptResponse { #[serde(rename = "accountId")] pub account_id: Id, - pub error: Option, + pub error: Option>, } impl JsonObjectParser for ValidateSieveScriptRequest { diff --git a/crates/jmap-proto/src/object/blob.rs b/crates/jmap-proto/src/object/blob.rs index 128f6ec4..dacdfe81 100644 --- a/crates/jmap-proto/src/object/blob.rs +++ b/crates/jmap-proto/src/object/blob.rs @@ -4,10 +4,120 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use crate::{ - parser::{Ignore, json::Parser}, - request::{RequestProperty, RequestPropertyParser}, -}; +use crate::object::{MaybeReference, parse_ref}; +use jmap_tools::{Element, Key, Property}; +use std::borrow::Cow; +use types::blob::BlobId; + +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum BlobProperty { + Id, + BlobId, + Type, + Size, + Digest(DigestProperty), + Data(DataProperty), + IsEncodingProblem, + IsTruncated, +} + +#[derive(Debug, PartialEq, Eq, Hash, Clone)] +pub enum DigestProperty { + Sha, + Sha256, + Sha512, +} + +#[derive(Debug, PartialEq, Eq, Hash, Clone)] +pub enum DataProperty { + AsText, + AsBase64, + Default, +} + +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum BlobValue { + BlobId(BlobId), + IdReference(String), +} + +impl Property for BlobProperty { + fn try_parse(key: Option<&Key<'_, Self>>, value: &str) -> Option { + BlobProperty::parse(value) + } + + fn to_cow(&self) -> Cow<'static, str> { + match self { + BlobProperty::BlobId => "blobId", + BlobProperty::Id => "id", + BlobProperty::Size => "size", + BlobProperty::Type => "type", + BlobProperty::IsEncodingProblem => "isEncodingProblem", + BlobProperty::IsTruncated => "isTruncated", + BlobProperty::Data(data) => match data { + DataProperty::AsText => "data:asText", + DataProperty::AsBase64 => "data:asBase64", + DataProperty::Default => "data", + }, + BlobProperty::Digest(digest) => match digest { + DigestProperty::Sha => "digest:sha", + DigestProperty::Sha256 => "digest:sha-256", + DigestProperty::Sha512 => "digest:sha-512", + }, + } + .into() + } +} + +impl Element for BlobValue { + type Property = BlobProperty; + + fn try_parse

(key: &Key<'_, Self::Property>, value: &str) -> Option { + if let Key::Property(prop) = key { + match prop.patch_or_prop() { + BlobProperty::Id => match parse_ref(value) { + MaybeReference::Value(v) => Some(BlobValue::Id(v)), + MaybeReference::Reference(v) => Some(BlobValue::IdReference(v)), + MaybeReference::ParseError => None, + }, + BlobProperty::BlobId => match parse_ref(value) { + MaybeReference::Value(v) => Some(BlobValue::BlobId(v)), + MaybeReference::Reference(v) => Some(BlobValue::IdReference(v)), + MaybeReference::ParseError => None, + }, + _ => None, + } + } else { + None + } + } + + fn to_cow(&self) -> Cow<'static, str> { + match self { + BlobValue::BlobId(blob_id) => blob_id.to_string().into(), + BlobValue::IdReference(r) => format!("#{r}").into(), + } + } +} + +impl BlobProperty { + fn parse(value: &str) -> Option { + hashify::tiny_map!(value.as_bytes(), + b"blobId" => BlobProperty::BlobId, + b"id" => BlobProperty::Id, + b"size" => BlobProperty::Size, + b"type" => BlobProperty::Type, + b"isEncodingProblem" => BlobProperty::IsEncodingProblem, + b"isTruncated" => BlobProperty::IsTruncated, + b"data:asText" => BlobProperty::Data(DataProperty::AsText), + b"data:asBase64" => BlobProperty::Data(DataProperty::AsBase64), + b"data" => BlobProperty::Data(DataProperty::Default), + b"digest:sha" => BlobProperty::Digest(DigestProperty::Sha), + b"digest:sha-256" => BlobProperty::Digest(DigestProperty::Sha256), + b"digest:sha-512" => BlobProperty::Digest(DigestProperty::Sha512), + ) + } +} #[derive(Debug, Clone, Default)] pub struct GetArguments { @@ -15,6 +125,8 @@ pub struct GetArguments { pub length: Option, } +/* + impl RequestPropertyParser for GetArguments { fn parse(&mut self, parser: &mut Parser, property: RequestProperty) -> trc::Result { match &property.hash[0] { @@ -35,52 +147,5 @@ impl RequestPropertyParser for GetArguments { } } -#[cfg(test)] -mod tests { - #[test] - fn gen_ids() { - for label in ["sha-256", "sha-512"] { - let mut iter = label.chars(); - let mut hash = [0; 2]; - let mut shift = 0; - 'outer: for hash in hash.iter_mut() { - for ch in iter.by_ref() { - *hash |= (ch as u128) << shift; - shift += 8; - if shift == 128 { - shift = 0; - continue 'outer; - } - } - break; - } - - print!( - "0x{}", - format!("{:032x}", hash[0]) - .chars() - .collect::>() - .chunks_exact(4) - .map(|chunk| chunk.iter().collect::()) - .collect::>() - .join("_") - .replace("0000_", "") - ); - if hash[1] != 0 { - print!( - ", 0x{}", - format!("{:032x}", hash[1]) - .chars() - .collect::>() - .chunks_exact(4) - .map(|chunk| chunk.iter().collect::()) - .collect::>() - .join("_") - .replace("0000_", "") - ); - } - println!(" => Property::{},", label); - } - } -} +*/ diff --git a/crates/jmap-proto/src/object/email.rs b/crates/jmap-proto/src/object/email.rs index 0655708a..dc22d48d 100644 --- a/crates/jmap-proto/src/object/email.rs +++ b/crates/jmap-proto/src/object/email.rs @@ -5,14 +5,355 @@ */ use crate::{ - parser::{Ignore, JsonObjectParser, json::Parser}, - request::{RequestProperty, RequestPropertyParser}, - types::property::Property, + object::{MaybeReference, parse_ref}, + types::date::UTCDate, }; +use jmap_tools::{Element, JsonPointer, JsonPointerItem, Key, Property}; +use mail_parser::HeaderName; +use std::{borrow::Cow, fmt::Display}; +use types::{blob::BlobId, id::Id, keyword::Keyword}; + +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum EmailProperty { + // Metadata + Id, + BlobId, + ThreadId, + MailboxIds, + Keywords, + Size, + ReceivedAt, + + // Address + Name, + Email, + + // GroupedAddresses + Addresses, + + // Header Fields Properties + Value, + Header(HeaderProperty), + + // Convenience properties + MessageId, + InReplyTo, + References, + Sender, + From, + To, + Cc, + Bcc, + ReplyTo, + Subject, + SentAt, + + // Body Parts + TextBody, + HtmlBody, + Attachments, + PartId, + Headers, + Type, + Charset, + Disposition, + Cid, + Language, + Location, + SubParts, + BodyStructure, + BodyValues, + IsEncodingProblem, + IsTruncated, + HasAttachment, + Preview, + + // Other + Keyword(Keyword), + Pointer(JsonPointer), +} + +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct HeaderProperty { + pub form: HeaderForm, + pub header: String, + pub all: bool, +} + +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum HeaderForm { + Raw, + Text, + Addresses, + GroupedAddresses, + MessageIds, + Date, + URLs, +} + +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum EmailValue { + Id(Id), + Date(UTCDate), + BlobId(BlobId), + IdReference(String), +} + +impl Property for EmailProperty { + fn try_parse(key: Option<&Key<'_, Self>>, value: &str) -> Option { + let allow_patch = key.is_none(); + if let Some(Key::Property(key)) = key { + match key.patch_or_prop() { + EmailProperty::Keywords => EmailProperty::Keyword(Keyword::parse(value)).into(), + _ => EmailProperty::from_str(value, allow_patch), + } + } else { + EmailProperty::parse(value, allow_patch) + } + } + + fn to_cow(&self) -> Cow<'static, str> { + match self { + EmailProperty::Attachments => "attachments", + EmailProperty::Bcc => "bcc", + EmailProperty::BlobId => "blobId", + EmailProperty::BodyStructure => "bodyStructure", + EmailProperty::BodyValues => "bodyValues", + EmailProperty::Cc => "cc", + EmailProperty::Charset => "charset", + EmailProperty::Cid => "cid", + EmailProperty::Disposition => "disposition", + EmailProperty::Email => "email", + EmailProperty::From => "from", + EmailProperty::HasAttachment => "hasAttachment", + EmailProperty::Headers => "headers", + EmailProperty::HtmlBody => "htmlBody", + EmailProperty::Id => "id", + EmailProperty::InReplyTo => "inReplyTo", + EmailProperty::Keywords => "keywords", + EmailProperty::Language => "language", + EmailProperty::Location => "location", + EmailProperty::MailboxIds => "mailboxIds", + EmailProperty::MessageId => "messageId", + EmailProperty::Name => "name", + EmailProperty::PartId => "partId", + EmailProperty::Preview => "preview", + EmailProperty::ReceivedAt => "receivedAt", + EmailProperty::References => "references", + EmailProperty::ReplyTo => "replyTo", + EmailProperty::Sender => "sender", + EmailProperty::SentAt => "sentAt", + EmailProperty::Size => "size", + EmailProperty::Subject => "subject", + EmailProperty::SubParts => "subParts", + EmailProperty::TextBody => "textBody", + EmailProperty::ThreadId => "threadId", + EmailProperty::To => "to", + EmailProperty::Type => "type", + EmailProperty::Addresses => "addresses", + EmailProperty::Value => "value", + EmailProperty::IsEncodingProblem => "isEncodingProblem", + EmailProperty::IsTruncated => "isTruncated", + EmailProperty::Header(header) => return header.to_string().into(), + EmailProperty::Keyword(keyword) => return keyword.to_string().into(), + EmailProperty::Pointer(json_pointer) => return json_pointer.to_string().into(), + } + .into() + } +} + +impl Element for EmailValue { + type Property = EmailProperty; + + fn try_parse

(key: &Key<'_, Self::Property>, value: &str) -> Option { + if let Key::Property(prop) = key { + match prop.patch_or_prop() { + EmailProperty::Id | EmailProperty::ThreadId | EmailProperty::MailboxIds => { + match parse_ref(value) { + MaybeReference::Value(v) => Some(EmailValue::Id(v)), + MaybeReference::Reference(v) => Some(EmailValue::IdReference(v)), + MaybeReference::ParseError => None, + } + } + EmailProperty::BlobId => match parse_ref(value) { + MaybeReference::Value(v) => Some(EmailValue::BlobId(v)), + MaybeReference::Reference(v) => Some(EmailValue::IdReference(v)), + MaybeReference::ParseError => None, + }, + EmailProperty::Header(HeaderProperty { + form: HeaderForm::Date, + .. + }) + | EmailProperty::ReceivedAt + | EmailProperty::SentAt => UTCDate::from_str(value).ok().map(EmailValue::Date), + _ => None, + } + } else { + None + } + } + + fn to_cow(&self) -> Cow<'static, str> { + match self { + EmailValue::Id(id) => id.to_string().into(), + EmailValue::Date(utcdate) => utcdate.to_string().into(), + EmailValue::BlobId(blob_id) => blob_id.to_string().into(), + EmailValue::IdReference(r) => format!("#{r}").into(), + } + } +} + +impl EmailProperty { + fn parse(value: &str, allow_patch: bool) -> Option { + hashify::tiny_map!(value.as_bytes(), + "id" => EmailProperty::Id, + "blobId" => EmailProperty::BlobId, + "threadId" => EmailProperty::ThreadId, + "mailboxIds" => EmailProperty::MailboxIds, + "keywords" => EmailProperty::Keywords, + "size" => EmailProperty::Size, + "receivedAt" => EmailProperty::ReceivedAt, + "name" => EmailProperty::Name, + "email" => EmailProperty::Email, + "addresses" => EmailProperty::Addresses, + "value" => EmailProperty::Value, + "messageId" => EmailProperty::MessageId, + "inReplyTo" => EmailProperty::InReplyTo, + "references" => EmailProperty::References, + "sender" => EmailProperty::Sender, + "from" => EmailProperty::From, + "to" => EmailProperty::To, + "cc" => EmailProperty::Cc, + "bcc" => EmailProperty::Bcc, + "replyTo" => EmailProperty::ReplyTo, + "subject" => EmailProperty::Subject, + "sentAt" => EmailProperty::SentAt, + "textBody" => EmailProperty::TextBody, + "htmlBody" => EmailProperty::HtmlBody, + "attachments" => EmailProperty::Attachments, + "partId" => EmailProperty::PartId, + "headers" => EmailProperty::Headers, + "type" => EmailProperty::Type, + "charset" => EmailProperty::Charset, + "disposition" => EmailProperty::Disposition, + "cid" => EmailProperty::Cid, + "language" => EmailProperty::Language, + "location" => EmailProperty::Location, + "subParts" => EmailProperty::SubParts, + "bodyStructure" => EmailProperty::BodyStructure, + "bodyValues" => EmailProperty::BodyValues, + "isEncodingProblem" => EmailProperty::IsEncodingProblem, + "isTruncated" => EmailProperty::IsTruncated, + "hasAttachment" => EmailProperty::HasAttachment, + "preview" => EmailProperty::Preview + ) + .or_else(|| { + if let Some(header) = value.strip_prefix("header:") { + HeaderProperty::parse(header).map(EmailProperty::Header) + } else if allow_patch && value.contains('/') { + EmailProperty::Pointer(JsonPointer::parse(value)).into() + } else { + None + } + }) + } + + fn patch_or_prop(&self) -> &EmailProperty { + if let EmailProperty::Pointer(ptr) = self + && let Some(JsonPointerItem::Key(Key::Property(prop))) = ptr.last() + { + prop + } else { + self + } + } + + pub fn as_rfc_header(&self) -> HeaderName<'static> { + match self { + EmailProperty::MessageId => HeaderName::MessageId, + EmailProperty::InReplyTo => HeaderName::InReplyTo, + EmailProperty::References => HeaderName::References, + EmailProperty::Sender => HeaderName::Sender, + EmailProperty::From => HeaderName::From, + EmailProperty::To => HeaderName::To, + EmailProperty::Cc => HeaderName::Cc, + EmailProperty::Bcc => HeaderName::Bcc, + EmailProperty::ReplyTo => HeaderName::ReplyTo, + EmailProperty::Subject => HeaderName::Subject, + EmailProperty::SentAt => HeaderName::Date, + _ => unreachable!(), + } + } +} + +impl HeaderProperty { + fn parse(value: &str) -> Option { + let mut result = HeaderProperty { + form: HeaderForm::Raw, + header: String::new(), + all: false, + }; + + for (pos, value) in value.split(':').enumerate() { + match pos { + 0 => { + result.header = value.to_string(); + } + 1 => { + hashify::fnc_map!(value.as_bytes(), + b"asText" => { result.form = HeaderForm::Text;}, + b"asAddresses" => { result.form = HeaderForm::Addresses;}, + b"asGroupedAddresses" => { result.form = HeaderForm::GroupedAddresses;}, + b"asMessageIds" => { result.form = HeaderForm::MessageIds;}, + b"asDate" => { result.form = HeaderForm::Date;}, + b"asURLs" => { result.form = HeaderForm::URLs;}, + b"asRaw" => { result.form = HeaderForm::Raw; }, + b"all" => { result.all = true; }, + _ => { + return None; + } + ); + } + 2 if value == "all" && result.all == false => { + result.all = true; + } + _ => return None, + } + } + + if !result.header.is_empty() { + Some(result) + } else { + None + } + } +} + +impl Display for HeaderProperty { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + write!(f, "header:{}", self.header)?; + self.form.fmt(f)?; + if self.all { write!(f, ":all") } else { Ok(()) } + } +} + +impl Display for HeaderForm { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + match self { + HeaderForm::Raw => Ok(()), + HeaderForm::Text => write!(f, ":asText"), + HeaderForm::Addresses => write!(f, ":asAddresses"), + HeaderForm::GroupedAddresses => write!(f, ":asGroupedAddresses"), + HeaderForm::MessageIds => write!(f, ":asMessageIds"), + HeaderForm::Date => write!(f, ":asDate"), + HeaderForm::URLs => write!(f, ":asURLs"), + } + } +} #[derive(Debug, Clone, Default)] pub struct GetArguments { - pub body_properties: Option>, + pub body_properties: Option>, pub fetch_text_body_values: Option, pub fetch_html_body_values: Option, pub fetch_all_body_values: Option, @@ -24,7 +365,7 @@ pub struct QueryArguments { pub collapse_threads: Option, } -impl RequestPropertyParser for GetArguments { +/*impl RequestPropertyParser for GetArguments { fn parse(&mut self, parser: &mut Parser, property: RequestProperty) -> trc::Result { match (&property.hash[0], &property.hash[1]) { (0x7365_6974_7265_706f_7250_7964_6f62, _) => { @@ -69,3 +410,4 @@ impl RequestPropertyParser for QueryArguments { } } } +*/ diff --git a/crates/jmap-proto/src/object/email_submission.rs b/crates/jmap-proto/src/object/email_submission.rs index 3b6ea20f..1f8999f7 100644 --- a/crates/jmap-proto/src/object/email_submission.rs +++ b/crates/jmap-proto/src/object/email_submission.rs @@ -5,26 +5,264 @@ */ use crate::{ - parser::{JsonObjectParser, json::Parser}, - request::{RequestProperty, RequestPropertyParser, reference::MaybeReference}, - types::value::{Object, SetValue}, + object::{ + MaybeReference, + email::{EmailProperty, EmailValue}, + parse_ref, + }, + request::reference::MaybeIdReference, + types::date::UTCDate, }; -use types::id::Id; +use jmap_tools::{Element, JsonPointer, JsonPointerItem, Key, Property, Value}; +use std::borrow::Cow; +use types::{blob::BlobId, id::Id}; use utils::map::vec_map::VecMap; -#[derive(Debug, Clone, Default)] -pub struct SetArguments { - pub on_success_update_email: Option, Object>>, - pub on_success_destroy_email: Option>>, +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum EmailSubmissionProperty { + Id, + IdentityId, + ThreadId, + Envelope, + MailFrom, + RcptTo, + Email, + Parameters, + SendAt, + UndoStatus, + DeliveryStatus, + SmtpReply, + Delivered, + Displayed, + DsnBlobIds, + MdnBlobIds, + + Pointer(JsonPointer), } -impl RequestPropertyParser for SetArguments { +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum EmailSubmissionValue { + Id(Id), + Date(UTCDate), + BlobId(BlobId), + UndoStatus(UndoStatus), + Delivered(Delivered), + Displayed(Displayed), + IdReference(String), +} + +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum UndoStatus { + Pending, + Final, + Canceled, +} + +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum Delivered { + Queued, + Yes, + No, + Unknown, +} + +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum Displayed { + Yes, + Unknown, +} + +impl Property for EmailSubmissionProperty { + fn try_parse(key: Option<&Key<'_, Self>>, value: &str) -> Option { + EmailSubmissionProperty::from_str(value, key.is_none()) + } + + fn to_cow(&self) -> Cow<'static, str> { + match self { + EmailSubmissionProperty::DeliveryStatus => "deliveryStatus", + EmailSubmissionProperty::DsnBlobIds => "dsnBlobIds", + EmailSubmissionProperty::Email => "email", + EmailSubmissionProperty::Envelope => "envelope", + EmailSubmissionProperty::Id => "id", + EmailSubmissionProperty::IdentityId => "identityId", + EmailSubmissionProperty::MdnBlobIds => "mdnBlobIds", + EmailSubmissionProperty::SendAt => "sendAt", + EmailSubmissionProperty::ThreadId => "threadId", + EmailSubmissionProperty::UndoStatus => "undoStatus", + EmailSubmissionProperty::Parameters => "parameters", + EmailSubmissionProperty::SmtpReply => "smtpReply", + EmailSubmissionProperty::Delivered => "delivered", + EmailSubmissionProperty::Displayed => "displayed", + EmailSubmissionProperty::MailFrom => "mailFrom", + EmailSubmissionProperty::RcptTo => "rcptTo", + EmailSubmissionProperty::Pointer(json_pointer) => { + return json_pointer.to_string().into(); + } + } + .into() + } +} + +impl Element for EmailSubmissionValue { + type Property = EmailSubmissionProperty; + + fn try_parse

(key: &Key<'_, Self::Property>, value: &str) -> Option { + if let Key::Property(prop) = key { + match prop.patch_or_prop() { + EmailSubmissionProperty::Id | EmailSubmissionProperty::ThreadId => { + match parse_ref(value) { + MaybeReference::Value(v) => Some(EmailSubmissionValue::Id(v)), + MaybeReference::Reference(v) => Some(EmailSubmissionValue::IdReference(v)), + MaybeReference::ParseError => None, + } + } + EmailSubmissionProperty::MdnBlobIds | EmailSubmissionProperty::DsnBlobIds => { + match parse_ref(value) { + MaybeReference::Value(v) => Some(EmailSubmissionValue::BlobId(v)), + MaybeReference::Reference(v) => Some(EmailSubmissionValue::IdReference(v)), + MaybeReference::ParseError => None, + } + } + EmailSubmissionProperty::SendAt => UTCDate::from_str(value) + .ok() + .map(EmailSubmissionValue::Date), + EmailSubmissionProperty::UndoStatus => { + UndoStatus::parse(value).map(EmailSubmissionValue::UndoStatus) + } + EmailSubmissionProperty::Delivered => { + Delivered::parse(value).map(EmailSubmissionValue::Delivered) + } + EmailSubmissionProperty::Displayed => { + Displayed::parse(value).map(EmailSubmissionValue::Displayed) + } + _ => None, + } + } else { + None + } + } + + fn to_cow(&self) -> Cow<'static, str> { + match self { + EmailSubmissionValue::Id(id) => id.to_string().into(), + EmailSubmissionValue::Date(utcdate) => utcdate.to_string().into(), + EmailSubmissionValue::BlobId(blob_id) => blob_id.to_string().into(), + EmailSubmissionValue::IdReference(r) => format!("#{r}").into(), + EmailSubmissionValue::UndoStatus(undo_status) => undo_status.as_str().into(), + EmailSubmissionValue::Delivered(delivered) => delivered.as_str().into(), + EmailSubmissionValue::Displayed(displayed) => displayed.as_str().into(), + } + } +} + +impl EmailSubmissionProperty { + fn parse(value: &str, allow_patch: bool) -> Option { + hashify::tiny_map!(value.as_bytes(), + "id" => EmailSubmissionProperty::Id, + "identityId" => EmailSubmissionProperty::IdentityId, + "threadId" => EmailSubmissionProperty::ThreadId, + "envelope" => EmailSubmissionProperty::Envelope, + "mailFrom" => EmailSubmissionProperty::MailFrom, + "rcptTo" => EmailSubmissionProperty::RcptTo, + "email" => EmailSubmissionProperty::Email, + "parameters" => EmailSubmissionProperty::Parameters, + "sendAt" => EmailSubmissionProperty::SendAt, + "undoStatus" => EmailSubmissionProperty::UndoStatus, + "deliveryStatus" => EmailSubmissionProperty::DeliveryStatus, + "smtpReply" => EmailSubmissionProperty::SmtpReply, + "delivered" => EmailSubmissionProperty::Delivered, + "displayed" => EmailSubmissionProperty::Displayed, + "dsnBlobIds" => EmailSubmissionProperty::DsnBlobIds, + "mdnBlobIds" => EmailSubmissionProperty::MdnBlobIds, + ) + .or_else(|| { + if allow_patch && value.contains('/') { + EmailSubmissionProperty::Pointer(JsonPointer::parse(value)).into() + } else { + None + } + }) + } + + fn patch_or_prop(&self) -> &EmailSubmissionProperty { + if let EmailSubmissionProperty::Pointer(ptr) = self + && let Some(JsonPointerItem::Key(Key::Property(prop))) = ptr.last() + { + prop + } else { + self + } + } +} + +impl UndoStatus { + fn parse(value: &str) -> Option { + hashify::tiny_map!(value.as_bytes(), + b"pending" => UndoStatus::Pending, + b"final" => UndoStatus::Final, + b"canceled" => UndoStatus::Canceled, + ) + } + + fn as_str(&self) -> &'static str { + match self { + UndoStatus::Pending => "pending", + UndoStatus::Final => "final", + UndoStatus::Canceled => "canceled", + } + } +} + +impl Delivered { + fn parse(value: &str) -> Option { + hashify::tiny_map!(value.as_bytes(), + b"queued" => Delivered::Queued, + b"yes" => Delivered::Yes, + b"no" => Delivered::No, + b"unknown" => Delivered::Unknown, + ) + } + + fn as_str(&self) -> &'static str { + match self { + Delivered::Queued => "queued", + Delivered::Yes => "yes", + Delivered::No => "no", + Delivered::Unknown => "unknown", + } + } +} + +impl Displayed { + fn parse(value: &str) -> Option { + hashify::tiny_map!(value.as_bytes(), + b"yes" => Displayed::Yes, + b"unknown" => Displayed::Unknown, + ) + } + + fn as_str(&self) -> &'static str { + match self { + Displayed::Yes => "yes", + Displayed::Unknown => "unknown", + } + } +} + +#[derive(Debug, Clone, Default)] +pub struct SetArguments<'x> { + pub on_success_update_email: + Option, Value<'x, EmailProperty, EmailValue>>>, + pub on_success_destroy_email: Option>>, +} + +/*impl RequestPropertyParser for SetArguments { fn parse(&mut self, parser: &mut Parser, property: RequestProperty) -> trc::Result { if property.hash[0] == 0x4565_7461_6470_5573_7365_6363_7553_6e6f && property.hash[1] == 0x6c69_616d { self.on_success_update_email = - , Object>>>::parse(parser)?; + , Value<'x, P, E>>>>::parse(parser)?; Ok(true) } else if property.hash[0] == 0x796f_7274_7365_4473_7365_6363_7553_6e6f && property.hash[1] == 0x006c_6961_6d45 @@ -37,3 +275,4 @@ impl RequestPropertyParser for SetArguments { } } } +*/ diff --git a/crates/jmap-proto/src/object/identity.rs b/crates/jmap-proto/src/object/identity.rs new file mode 100644 index 00000000..a0f12d08 --- /dev/null +++ b/crates/jmap-proto/src/object/identity.rs @@ -0,0 +1,103 @@ +/* + * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL + */ + +use jmap_tools::{Element, JsonPointer, JsonPointerItem, Key, Property}; +use std::{borrow::Cow, str::FromStr}; +use types::id::Id; + +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum IdentityProperty { + Id, + Name, + Email, + ReplyTo, + Bcc, + TextSignature, + HtmlSignature, + MayDelete, + + // Other + Pointer(JsonPointer), +} + +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum IdentityValue { + Id(Id), +} + +impl Property for IdentityProperty { + fn try_parse(key: Option<&Key<'_, Self>>, value: &str) -> Option { + IdentityProperty::parse(value, key.is_none()) + } + + fn to_cow(&self) -> Cow<'static, str> { + match self { + IdentityProperty::Bcc => "bcc", + IdentityProperty::Email => "email", + IdentityProperty::HtmlSignature => "htmlSignature", + IdentityProperty::Id => "id", + IdentityProperty::MayDelete => "mayDelete", + IdentityProperty::Name => "name", + IdentityProperty::ReplyTo => "replyTo", + IdentityProperty::TextSignature => "textSignature", + IdentityProperty::Pointer(json_pointer) => return json_pointer.to_string().into(), + } + .into() + } +} + +impl Element for IdentityValue { + type Property = IdentityProperty; + + fn try_parse

(key: &Key<'_, Self::Property>, value: &str) -> Option { + if let Key::Property(prop) = key { + match prop.patch_or_prop() { + IdentityProperty::Id => Id::from_str(value).ok().map(IdentityValue::Id), + _ => None, + } + } else { + None + } + } + + fn to_cow(&self) -> Cow<'static, str> { + match self { + IdentityValue::Id(id) => id.to_string().into(), + } + } +} + +impl IdentityProperty { + fn parse(value: &str, allow_patch: bool) -> Option { + hashify::tiny_map!(value.as_bytes(), + b"id" => IdentityProperty::Id, + b"name" => IdentityProperty::Name, + b"email" => IdentityProperty::Email, + b"replyTo" => IdentityProperty::ReplyTo, + b"bcc" => IdentityProperty::Bcc, + b"textSignature" => IdentityProperty::TextSignature, + b"htmlSignature" => IdentityProperty::HtmlSignature, + b"mayDelete" => IdentityProperty::MayDelete, + ) + .or_else(|| { + if allow_patch && value.contains('/') { + IdentityProperty::Pointer(JsonPointer::parse(value)).into() + } else { + None + } + }) + } + + fn patch_or_prop(&self) -> &IdentityProperty { + if let IdentityProperty::Pointer(ptr) = self + && let Some(JsonPointerItem::Key(Key::Property(prop))) = ptr.last() + { + prop + } else { + self + } + } +} diff --git a/crates/jmap-proto/src/object/mailbox.rs b/crates/jmap-proto/src/object/mailbox.rs index b725d025..f7062ee5 100644 --- a/crates/jmap-proto/src/object/mailbox.rs +++ b/crates/jmap-proto/src/object/mailbox.rs @@ -4,10 +4,151 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use crate::{ - parser::{Ignore, json::Parser}, - request::{RequestProperty, RequestPropertyParser}, -}; +use std::borrow::Cow; + +use jmap_tools::{Element, JsonPointer, JsonPointerItem, Key, Property}; +use types::{id::Id, special_use::SpecialUse}; + +use crate::object::{MaybeReference, parse_ref}; + +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum MailboxProperty { + Id, + Name, + ParentId, + Role, + SortOrder, + TotalEmails, + UnreadEmails, + TotalThreads, + UnreadThreads, + ShareWith, + MyRights, + MayReadItems, + MayAddItems, + MayRemoveItems, + MaySetSeen, + MaySetKeywords, + MayCreateChild, + MayRename, + MaySubmit, + IsSubscribed, + + // Other + Pointer(JsonPointer), +} + +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum MailboxValue { + Id(Id), + IdReference(String), + Role(SpecialUse), +} + +impl Property for MailboxProperty { + fn try_parse(key: Option<&Key<'_, Self>>, value: &str) -> Option { + MailboxProperty::parse(value, key.is_none()) + } + + fn to_cow(&self) -> Cow<'static, str> { + match self { + MailboxProperty::Id => "id", + MailboxProperty::IsSubscribed => "isSubscribed", + MailboxProperty::MyRights => "myRights", + MailboxProperty::Name => "name", + MailboxProperty::ParentId => "parentId", + MailboxProperty::Role => "role", + MailboxProperty::SortOrder => "sortOrder", + MailboxProperty::TotalEmails => "totalEmails", + MailboxProperty::TotalThreads => "totalThreads", + MailboxProperty::UnreadEmails => "unreadEmails", + MailboxProperty::UnreadThreads => "unreadThreads", + MailboxProperty::MayReadItems => "mayReadItems", + MailboxProperty::MayAddItems => "mayAddItems", + MailboxProperty::MayRemoveItems => "mayRemoveItems", + MailboxProperty::MaySetSeen => "maySetSeen", + MailboxProperty::MaySetKeywords => "maySetKeywords", + MailboxProperty::MayCreateChild => "mayCreateChild", + MailboxProperty::MayRename => "mayRename", + MailboxProperty::MaySubmit => "maySubmit", + MailboxProperty::ShareWith => "shareWith", + MailboxProperty::Pointer(json_pointer) => return json_pointer.to_string().into(), + } + .into() + } +} + +impl Element for MailboxValue { + type Property = MailboxProperty; + + fn try_parse

(key: &Key<'_, Self::Property>, value: &str) -> Option { + if let Key::Property(prop) = key { + match prop.patch_or_prop() { + MailboxProperty::Id | MailboxProperty::ParentId => match parse_ref(value) { + MaybeReference::Value(v) => Some(MailboxValue::Id(v)), + MaybeReference::Reference(v) => Some(MailboxValue::IdReference(v)), + MaybeReference::ParseError => None, + }, + MailboxProperty::Role => SpecialUse::from_str(value).ok().map(MailboxValue::Role), + _ => None, + } + } else { + None + } + } + + fn to_cow(&self) -> Cow<'static, str> { + match self { + MailboxValue::Id(id) => id.to_string().into(), + MailboxValue::IdReference(r) => format!("#{r}").into(), + MailboxValue::Role(special_use) => special_use.as_str().unwrap_or_default().into(), + } + } +} + +impl MailboxProperty { + fn parse(value: &str, allow_patch: bool) -> Option { + hashify::tiny_map!(value.as_bytes(), + b"id" => MailboxProperty::Id, + b"name" => MailboxProperty::Name, + b"parentId" => MailboxProperty::ParentId, + b"role" => MailboxProperty::Role, + b"sortOrder" => MailboxProperty::SortOrder, + b"totalEmails" => MailboxProperty::TotalEmails, + b"unreadEmails" => MailboxProperty::UnreadEmails, + b"totalThreads" => MailboxProperty::TotalThreads, + b"unreadThreads" => MailboxProperty::UnreadThreads, + b"shareWith" => MailboxProperty::ShareWith, + b"myRights" => MailboxProperty::MyRights, + b"mayReadItems" => MailboxProperty::MayReadItems, + b"mayAddItems" => MailboxProperty::MayAddItems, + b"mayRemoveItems" => MailboxProperty::MayRemoveItems, + b"maySetSeen" => MailboxProperty::MaySetSeen, + b"maySetKeywords" => MailboxProperty::MaySetKeywords, + b"mayCreateChild" => MailboxProperty::MayCreateChild, + b"mayRename" => MailboxProperty::MayRename, + b"maySubmit" => MailboxProperty::MaySubmit, + b"isSubscribed" => MailboxProperty::IsSubscribed, + ) + .or_else(|| { + if allow_patch && value.contains('/') { + MailboxProperty::Pointer(JsonPointer::parse(value)).into() + } else { + None + } + }) + } + + fn patch_or_prop(&self) -> &MailboxProperty { + if let MailboxProperty::Pointer(ptr) = self + && let Some(JsonPointerItem::Key(Key::Property(prop))) = ptr.last() + { + prop + } else { + self + } + } +} #[derive(Debug, Clone, Default)] pub struct SetArguments { @@ -20,6 +161,7 @@ pub struct QueryArguments { pub filter_as_tree: Option, } +/* impl RequestPropertyParser for SetArguments { fn parse(&mut self, parser: &mut Parser, property: RequestProperty) -> trc::Result { if property.hash[0] == 0x4565_766f_6d65_5279_6f72_7473_6544_6e6f @@ -54,3 +196,4 @@ impl RequestPropertyParser for QueryArguments { Ok(true) } } +*/ diff --git a/crates/jmap-proto/src/object/mod.rs b/crates/jmap-proto/src/object/mod.rs index 9a3ffa7d..858e90a3 100644 --- a/crates/jmap-proto/src/object/mod.rs +++ b/crates/jmap-proto/src/object/mod.rs @@ -4,81 +4,50 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use crate::types::{ - property::Property, - value::{Object, Value}, -}; -use std::sync::Arc; -use types::id::Id; -use utils::{ - erased_serde, - json::{JsonPointerItem, JsonQueryable}, -}; +use std::str::FromStr; + +use jmap_tools::{Element, Property}; pub mod blob; pub mod email; pub mod email_submission; +pub mod identity; pub mod mailbox; +pub mod principal; +pub mod push_subscription; +pub mod quota; +pub mod search_snippet; pub mod sieve; +pub mod thread; +pub mod vacation_response; -pub trait JsonObjectTrait: JsonQueryable + erased_serde::Serialize { - fn id(&self) -> Option; +pub trait JmapObject { + type Property: Property; + type Element: Element; + type Id: FromStr; + + type Filter; + type Comparator; + + type GetArguments; + type SetArguments; + type QueryArguments; + type CopyArguments; } -#[derive(Clone, Debug)] -pub struct JsonObject(Arc); - -impl JsonObject { - pub fn new(value: T) -> Self { - Self(Arc::new(value)) - } - - #[inline] - pub fn id(&self) -> Option { - self.0.id() - } +#[derive(Debug, Clone, PartialEq, Eq)] +enum MaybeReference { + Value(T), + Reference(String), + ParseError, } -impl serde::Serialize for JsonObject { - fn serialize(&self, serializer: S) -> Result - where - S: serde::Serializer, - { - erased_serde::serialize(self.0.as_ref(), serializer) - } -} - -impl JsonObjectTrait for Object { - fn id(&self) -> Option { - self.get(&Property::Id).as_id().copied() - } -} - -impl JsonQueryable for Object { - fn eval_pointer<'x>( - &'x self, - mut pointer: std::slice::Iter, - results: &mut Vec<&'x dyn JsonQueryable>, - ) { - match pointer.next() { - Some(JsonPointerItem::String(n)) => { - if let Some(v) = self - .0 - .iter() - .find_map(|(k, v)| if k.as_str() == n { Some(v) } else { None }) - { - v.eval_pointer(pointer, results); - } - } - Some(JsonPointerItem::Wildcard) => { - for v in self.0.values() { - v.eval_pointer(pointer.clone(), results); - } - } - Some(JsonPointerItem::Root) | None => { - results.push(self); - } - _ => {} - } +fn parse_ref(value: &str) -> MaybeReference { + if let Some(reference) = value.strip_prefix('#') { + MaybeReference::Reference(reference.to_string()) + } else { + T::from_str(value) + .map(MaybeReference::Value) + .unwrap_or(MaybeReference::ParseError) } } diff --git a/crates/jmap-proto/src/object/principal.rs b/crates/jmap-proto/src/object/principal.rs new file mode 100644 index 00000000..98467174 --- /dev/null +++ b/crates/jmap-proto/src/object/principal.rs @@ -0,0 +1,113 @@ +/* + * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL + */ + +use jmap_tools::{Element, Key, Property}; +use std::{borrow::Cow, str::FromStr}; +use types::id::Id; + +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum PrincipalProperty { + Id, + Type, + Name, + Description, + Email, + Timezone, + Capabilities, +} + +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum PrincipalValue { + Id(Id), + Type(PrincipalType), +} + +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum PrincipalType { + Individual, + Group, + Resource, + Location, + Other, +} + +impl Property for PrincipalProperty { + fn try_parse(key: Option<&Key<'_, Self>>, value: &str) -> Option { + PrincipalProperty::parse(value) + } + + fn to_cow(&self) -> Cow<'static, str> { + match self { + PrincipalProperty::Capabilities => "capabilities", + PrincipalProperty::Description => "description", + PrincipalProperty::Email => "email", + PrincipalProperty::Id => "id", + PrincipalProperty::Name => "name", + PrincipalProperty::Timezone => "timezone", + PrincipalProperty::Type => "type", + } + .into() + } +} + +impl Element for PrincipalValue { + type Property = PrincipalProperty; + + fn try_parse

(key: &Key<'_, Self::Property>, value: &str) -> Option { + if let Key::Property(prop) = key { + match prop.patch_or_prop() { + PrincipalProperty::Id => Id::from_str(value).ok().map(PrincipalValue::Id), + PrincipalProperty::Type => PrincipalType::parse(value).map(PrincipalValue::Type), + _ => None, + } + } else { + None + } + } + + fn to_cow(&self) -> Cow<'static, str> { + match self { + PrincipalValue::Id(id) => id.to_string().into(), + PrincipalValue::Type(t) => t.as_str().into(), + } + } +} + +impl PrincipalProperty { + fn parse(value: &str) -> Option { + hashify::tiny_map!(value.as_bytes(), + b"id" => PrincipalProperty::Id, + b"type" => PrincipalProperty::Type, + b"name" => PrincipalProperty::Name, + b"description" => PrincipalProperty::Description, + b"email" => PrincipalProperty::Email, + b"timezone" => PrincipalProperty::Timezone, + b"capabilities" => PrincipalProperty::Capabilities, + ) + } +} + +impl PrincipalType { + pub fn parse(s: &str) -> Option { + hashify::tiny_map!(s.as_bytes(), + b"individual" => PrincipalType::Individual, + b"group" => PrincipalType::Group, + b"resource" => PrincipalType::Resource, + b"location" => PrincipalType::Location, + b"other" => PrincipalType::Other, + ) + } + + pub fn as_str(&self) -> &'static str { + match self { + PrincipalType::Individual => "individual", + PrincipalType::Group => "group", + PrincipalType::Resource => "resource", + PrincipalType::Location => "location", + PrincipalType::Other => "other", + } + } +} diff --git a/crates/jmap-proto/src/object/push_subscription.rs b/crates/jmap-proto/src/object/push_subscription.rs new file mode 100644 index 00000000..c16d5657 --- /dev/null +++ b/crates/jmap-proto/src/object/push_subscription.rs @@ -0,0 +1,123 @@ +/* + * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL + */ + +use crate::types::date::UTCDate; +use jmap_tools::{Element, JsonPointer, JsonPointerItem}; +use jmap_tools::{Key, Property}; +use std::borrow::Cow; +use types::{id::Id, type_state::DataType}; + +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum PushSubscriptionProperty { + Id, + DeviceClientId, + Url, + Keys, + P256dh, + Auth, + VerificationCode, + Expires, + Types, + + // Other + Pointer(JsonPointer), +} + +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum PushSubscriptionValue { + Id(Id), + Date(UTCDate), + Types(DataType), +} + +impl Property for PushSubscriptionProperty { + fn try_parse(key: Option<&Key<'_, Self>>, value: &str) -> Option { + PushSubscriptionProperty::parse(value, key.is_none()) + } + + fn to_cow(&self) -> Cow<'static, str> { + match self { + PushSubscriptionProperty::DeviceClientId => "deviceClientId", + PushSubscriptionProperty::Expires => "expires", + PushSubscriptionProperty::Id => "id", + PushSubscriptionProperty::Keys => "keys", + PushSubscriptionProperty::Types => "types", + PushSubscriptionProperty::Url => "url", + PushSubscriptionProperty::VerificationCode => "verificationCode", + PushSubscriptionProperty::P256dh => "p256dh", + PushSubscriptionProperty::Auth => "auth", + PushSubscriptionProperty::Pointer(json_pointer) => { + return json_pointer.to_string().into(); + } + } + .into() + } +} + +impl PushSubscriptionProperty { + fn parse(value: &str, allow_patch: bool) -> Option { + hashify::tiny_map!(value.as_bytes(), + b"id" => PushSubscriptionProperty::Id, + b"deviceClientId" => PushSubscriptionProperty::DeviceClientId, + b"url" => PushSubscriptionProperty::Url, + b"keys" => PushSubscriptionProperty::Keys, + b"p256dh" => PushSubscriptionProperty::P256dh, + b"auth" => PushSubscriptionProperty::Auth, + b"verificationCode" => PushSubscriptionProperty::VerificationCode, + b"expires" => PushSubscriptionProperty::Expires, + b"types" => PushSubscriptionProperty::Types, + ) + .or_else(|| { + if allow_patch && value.contains('/') { + PushSubscriptionProperty::Pointer(JsonPointer::parse(value)).into() + } else { + None + } + }) + } + + fn patch_or_prop(&self) -> &PushSubscriptionProperty { + if let PushSubscriptionProperty::Pointer(ptr) = self + && let Some(JsonPointerItem::Key(Key::Property(prop))) = ptr.last() + { + prop + } else { + self + } + } +} + +impl Element for PushSubscriptionValue { + type Property = PushSubscriptionProperty; + + fn try_parse

(key: &Key<'_, Self::Property>, value: &str) -> Option { + if let Key::Property(prop) = key { + match prop.patch_or_prop() { + PushSubscriptionProperty::Id => { + Id::from_str(value).ok().map(PushSubscriptionValue::Id) + } + PushSubscriptionProperty::Types => { + DataType::parse(value).map(PushSubscriptionValue::Types) + } + PushSubscriptionProperty::Expires => UTCDate::from_str(value) + .ok() + .map(PushSubscriptionValue::Date), + _ => None, + } + } else { + None + } + } + + fn to_cow(&self) -> Cow<'static, str> { + match self { + PushSubscriptionValue::Id(id) => id.to_string().into(), + PushSubscriptionValue::Date(utcdate) => utcdate.to_string().into(), + PushSubscriptionValue::BlobId(blob_id) => blob_id.to_string().into(), + PushSubscriptionValue::IdReference(r) => format!("#{r}").into(), + } + } +} diff --git a/crates/jmap-proto/src/object/quota.rs b/crates/jmap-proto/src/object/quota.rs new file mode 100644 index 00000000..5f52a115 --- /dev/null +++ b/crates/jmap-proto/src/object/quota.rs @@ -0,0 +1,91 @@ +/* + * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL + */ + +use jmap_tools::{Element, Key, Property}; +use std::{borrow::Cow, str::FromStr}; +use types::{id::Id, type_state::DataType}; + +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum QuotaProperty { + Id, + ResourceType, + Used, + Name, + Scope, + Types, + HardLimit, + WarnLimit, + SoftLimit, + Description, +} + +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum QuotaValue { + Id(Id), + Types(DataType), +} + +impl Property for QuotaProperty { + fn try_parse(key: Option<&Key<'_, Self>>, value: &str) -> Option { + QuotaProperty::parse(value) + } + + fn to_cow(&self) -> Cow<'static, str> { + match self { + QuotaProperty::Description => "description", + QuotaProperty::Id => "id", + QuotaProperty::Name => "name", + QuotaProperty::Types => "types", + QuotaProperty::ResourceType => "resourceType", + QuotaProperty::Used => "used", + QuotaProperty::HardLimit => "hardLimit", + QuotaProperty::Scope => "scope", + QuotaProperty::WarnLimit => "warnLimit", + QuotaProperty::SoftLimit => "softLimit", + } + .into() + } +} + +impl QuotaProperty { + fn parse(value: &str) -> Option { + hashify::tiny_map!(value.as_bytes(), + b"id" => QuotaProperty::Id, + b"resourceType" => QuotaProperty::ResourceType, + b"used" => QuotaProperty::Used, + b"name" => QuotaProperty::Name, + b"scope" => QuotaProperty::Scope, + b"types" => QuotaProperty::Types, + b"hardLimit" => QuotaProperty::HardLimit, + b"warnLimit" => QuotaProperty::WarnLimit, + b"softLimit" => QuotaProperty::SoftLimit, + b"description" => QuotaProperty::Description, + ) + } +} + +impl Element for QuotaValue { + type Property = QuotaProperty; + + fn try_parse

(key: &Key<'_, Self::Property>, value: &str) -> Option { + if let Key::Property(prop) = key { + match prop.patch_or_prop() { + QuotaProperty::Id => Id::from_str(value).ok().map(QuotaValue::Id), + QuotaProperty::Types => DataType::parse(value).map(QuotaValue::Types), + _ => None, + } + } else { + None + } + } + + fn to_cow(&self) -> Cow<'static, str> { + match self { + QuotaValue::Id(id) => id.to_string().into(), + QuotaValue::Types(data_type) => data_type.as_str().into(), + } + } +} diff --git a/crates/jmap-proto/src/object/search_snippet.rs b/crates/jmap-proto/src/object/search_snippet.rs new file mode 100644 index 00000000..be19b6e1 --- /dev/null +++ b/crates/jmap-proto/src/object/search_snippet.rs @@ -0,0 +1,69 @@ +/* + * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL + */ + +use jmap_tools::{Element, Key, Property}; +use std::{borrow::Cow, str::FromStr}; +use types::id::Id; + +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum SearchSnippetProperty { + EmailId, + Subject, + Preview, +} + +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum SearchSnippetValue { + Id(Id), +} + +impl Property for SearchSnippetProperty { + fn try_parse(key: Option<&Key<'_, Self>>, value: &str) -> Option { + SearchSnippetProperty::parse(value) + } + + fn to_cow(&self) -> Cow<'static, str> { + match self { + SearchSnippetProperty::Preview => "preview", + SearchSnippetProperty::Subject => "subject", + SearchSnippetProperty::EmailId => "emailId", + } + .into() + } +} + +impl Element for SearchSnippetValue { + type Property = SearchSnippetProperty; + + fn try_parse

(key: &Key<'_, Self::Property>, value: &str) -> Option { + if let Key::Property(prop) = key { + match prop.patch_or_prop() { + SearchSnippetProperty::EmailId => { + Id::from_str(value).ok().map(SearchSnippetValue::Id) + } + _ => None, + } + } else { + None + } + } + + fn to_cow(&self) -> Cow<'static, str> { + match self { + SearchSnippetValue::Id(id) => id.to_string().into(), + } + } +} + +impl SearchSnippetProperty { + fn parse(value: &str) -> Option { + hashify::tiny_map!(value.as_bytes(), + b"emailId" => SearchSnippetProperty::EmailId, + b"subject" => SearchSnippetProperty::Subject, + b"preview" => SearchSnippetProperty::Preview, + ) + } +} diff --git a/crates/jmap-proto/src/object/sieve.rs b/crates/jmap-proto/src/object/sieve.rs index ce4a29b2..7bd56b13 100644 --- a/crates/jmap-proto/src/object/sieve.rs +++ b/crates/jmap-proto/src/object/sieve.rs @@ -4,19 +4,96 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use types::id::Id; +use jmap_tools::{Element, Key, Property}; +use std::borrow::Cow; +use types::{blob::BlobId, id::Id}; use crate::{ - parser::json::Parser, - request::{RequestProperty, RequestPropertyParser, reference::MaybeReference}, + object::{MaybeReference, parse_ref}, + request::reference::MaybeIdReference, }; +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum SieveProperty { + Id, + Name, + BlobId, + IsActive, +} + +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum SieveValue { + Id(Id), + BlobId(BlobId), + IdReference(String), +} + +impl Property for SieveProperty { + fn try_parse(key: Option<&Key<'_, Self>>, value: &str) -> Option { + SieveProperty::parse(value) + } + + fn to_cow(&self) -> Cow<'static, str> { + match self { + SieveProperty::BlobId => "blobId", + SieveProperty::Id => "id", + SieveProperty::Name => "name", + SieveProperty::IsActive => "isActive", + } + .into() + } +} + +impl Element for SieveValue { + type Property = SieveProperty; + + fn try_parse

(key: &Key<'_, Self::Property>, value: &str) -> Option { + if let Key::Property(prop) = key { + match prop.patch_or_prop() { + SieveProperty::Id => match parse_ref(value) { + MaybeReference::Value(v) => Some(SieveValue::Id(v)), + MaybeReference::Reference(v) => Some(SieveValue::IdReference(v)), + MaybeReference::ParseError => None, + }, + SieveProperty::BlobId => match parse_ref(value) { + MaybeReference::Value(v) => Some(SieveValue::BlobId(v)), + MaybeReference::Reference(v) => Some(SieveValue::IdReference(v)), + MaybeReference::ParseError => None, + }, + _ => None, + } + } else { + None + } + } + + fn to_cow(&self) -> Cow<'static, str> { + match self { + SieveValue::Id(id) => id.to_string().into(), + SieveValue::BlobId(blob_id) => blob_id.to_string().into(), + SieveValue::IdReference(r) => format!("#{r}").into(), + } + } +} + +impl SieveProperty { + fn parse(value: &str) -> Option { + hashify::tiny_map!(value.as_bytes(), + b"id" => SieveProperty::Id, + b"name" => SieveProperty::Name, + b"blobId" => SieveProperty::BlobId, + b"isActive" => SieveProperty::IsActive, + ) + } +} + #[derive(Debug, Clone, Default)] pub struct SetArguments { - pub on_success_activate_script: Option>, + pub on_success_activate_script: Option>, pub on_success_deactivate_script: Option, } +/* impl RequestPropertyParser for SetArguments { fn parse(&mut self, parser: &mut Parser, property: RequestProperty) -> trc::Result { if property.hash[0] == 0x7461_7669_7463_4173_7365_6363_7553_6e6f @@ -38,3 +115,4 @@ impl RequestPropertyParser for SetArguments { } } } +*/ diff --git a/crates/jmap-proto/src/object/thread.rs b/crates/jmap-proto/src/object/thread.rs new file mode 100644 index 00000000..8bc3cda2 --- /dev/null +++ b/crates/jmap-proto/src/object/thread.rs @@ -0,0 +1,66 @@ +/* + * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL + */ + +use jmap_tools::{Element, Key, Property}; +use std::{borrow::Cow, str::FromStr}; +use types::id::Id; + +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum ThreadProperty { + Id, + EmailIds, +} + +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum ThreadValue { + Id(Id), +} + +impl Property for ThreadProperty { + fn try_parse(key: Option<&Key<'_, Self>>, value: &str) -> Option { + ThreadProperty::parse(value) + } + + fn to_cow(&self) -> Cow<'static, str> { + match self { + ThreadProperty::Id => "id", + ThreadProperty::EmailIds => "emailIds", + } + .into() + } +} + +impl Element for ThreadValue { + type Property = ThreadProperty; + + fn try_parse

(key: &Key<'_, Self::Property>, value: &str) -> Option { + if let Key::Property(prop) = key { + match prop.patch_or_prop() { + ThreadProperty::Id | ThreadProperty::EmailIds => { + Id::from_str(value).ok().map(ThreadValue::Id) + } + _ => None, + } + } else { + None + } + } + + fn to_cow(&self) -> Cow<'static, str> { + match self { + ThreadValue::Id(id) => id.to_string().into(), + } + } +} + +impl ThreadProperty { + fn parse(value: &str) -> Option { + hashify::tiny_map!(value.as_bytes(), + b"id" => ThreadProperty::Id, + b"emailIds" => ThreadProperty::EmailIds, + ) + } +} diff --git a/crates/jmap-proto/src/object/vacation_response.rs b/crates/jmap-proto/src/object/vacation_response.rs new file mode 100644 index 00000000..115446e9 --- /dev/null +++ b/crates/jmap-proto/src/object/vacation_response.rs @@ -0,0 +1,88 @@ +/* + * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL + */ + +use crate::types::date::UTCDate; +use jmap_tools::{Element, Key, Property}; +use std::{borrow::Cow, str::FromStr}; +use types::id::Id; + +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum VacationResponseProperty { + Id, + IsEnabled, + FromDate, + ToDate, + TextBody, + HtmlBody, +} + +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum VacationResponseValue { + Id(Id), + Date(UTCDate), +} + +impl Property for VacationResponseProperty { + fn try_parse(key: Option<&Key<'_, Self>>, value: &str) -> Option { + VacationResponseProperty::parse(value) + } + + fn to_cow(&self) -> Cow<'static, str> { + match self { + VacationResponseProperty::HtmlBody => "htmlBody", + VacationResponseProperty::Id => "id", + VacationResponseProperty::TextBody => "textBody", + VacationResponseProperty::FromDate => "fromDate", + VacationResponseProperty::IsEnabled => "isEnabled", + VacationResponseProperty::ToDate => "toDate", + } + .into() + } +} + +impl Element for VacationResponseValue { + type Property = VacationResponseProperty; + + fn try_parse

(key: &Key<'_, Self::Property>, value: &str) -> Option { + if let Key::Property(prop) = key { + match prop.patch_or_prop() { + VacationResponseProperty::Id => { + Id::from_str(value).ok().map(VacationResponseValue::Id) + } + VacationResponseProperty::FromDate | VacationResponseProperty::ToDate => { + UTCDate::from_str(value) + .ok() + .map(VacationResponseValue::Date) + } + _ => None, + } + } else { + None + } + } + + fn to_cow(&self) -> Cow<'static, str> { + match self { + VacationResponseValue::Id(id) => id.to_string().into(), + VacationResponseValue::Date(utcdate) => utcdate.to_string().into(), + VacationResponseValue::BlobId(blob_id) => blob_id.to_string().into(), + VacationResponseValue::IdReference(r) => format!("#{r}").into(), + } + } +} + +impl VacationResponseProperty { + fn parse(value: &str) -> Option { + hashify::tiny_map!(value.as_bytes(), + b"id" => VacationResponseProperty::Id, + b"isEnabled" => VacationResponseProperty::IsEnabled, + b"fromDate" => VacationResponseProperty::FromDate, + b"toDate" => VacationResponseProperty::ToDate, + b"textBody" => VacationResponseProperty::TextBody, + b"htmlBody" => VacationResponseProperty::HtmlBody, + ) + } +} diff --git a/crates/jmap-proto/src/parser/base32.rs b/crates/jmap-proto/src/parser/base32.rs deleted file mode 100644 index 52c5db09..00000000 --- a/crates/jmap-proto/src/parser/base32.rs +++ /dev/null @@ -1,65 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC - * - * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL - */ - -use utils::codec::{base32_custom::BASE32_INVERSE, leb128::Leb128Iterator}; - -use super::json::Parser; - -#[derive(Debug)] -pub struct JsonBase32Reader<'x, 'y> { - bytes: &'y mut Parser<'x>, - last_byte: u8, - pos: usize, -} - -impl<'x, 'y> JsonBase32Reader<'x, 'y> { - pub fn new(bytes: &'y mut Parser<'x>) -> Self { - JsonBase32Reader { - bytes, - pos: 0, - last_byte: 0, - } - } - - #[inline(always)] - fn map_byte(&mut self) -> Option { - match self.bytes.next_unescaped() { - Ok(Some(byte)) => match BASE32_INVERSE[byte as usize] { - decoded_byte if decoded_byte != u8::MAX => { - self.last_byte = decoded_byte; - Some(decoded_byte) - } - _ => None, - }, - _ => None, - } - } - - pub fn error(&mut self) -> trc::Error { - self.bytes.error_value() - } -} - -impl Iterator for JsonBase32Reader<'_, '_> { - type Item = u8; - fn next(&mut self) -> Option { - let pos = self.pos % 5; - let last_byte = self.last_byte; - let byte = self.map_byte()?; - self.pos += 1; - - match pos { - 0 => ((byte << 3) | (self.map_byte().unwrap_or(0) >> 2)).into(), - 1 => ((last_byte << 6) | (byte << 1) | (self.map_byte().unwrap_or(0) >> 4)).into(), - 2 => ((last_byte << 4) | (byte >> 1)).into(), - 3 => ((last_byte << 7) | (byte << 2) | (self.map_byte().unwrap_or(0) >> 3)).into(), - 4 => ((last_byte << 5) | byte).into(), - _ => None, - } - } -} - -impl Leb128Iterator for JsonBase32Reader<'_, '_> {} diff --git a/crates/jmap-proto/src/parser/impls.rs b/crates/jmap-proto/src/parser/impls.rs deleted file mode 100644 index dd5bc3cd..00000000 --- a/crates/jmap-proto/src/parser/impls.rs +++ /dev/null @@ -1,308 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC - * - * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL - */ - -use std::fmt::Display; - -use utils::map::{ - bitmap::{Bitmap, BitmapItem}, - vec_map::VecMap, -}; - -use super::{Ignore, JsonObjectParser, Token, json::Parser}; - -impl JsonObjectParser for u64 { - fn parse(parser: &mut Parser<'_>) -> trc::Result - where - Self: Sized, - { - let mut hash = 0; - let mut shift = 0; - - while let Some(ch) = parser.next_unescaped()? { - if shift < 64 { - hash |= (ch as u64) << shift; - shift += 8; - } else { - hash = 0; - break; - } - } - - Ok(hash) - } -} - -impl JsonObjectParser for u128 { - fn parse(parser: &mut Parser<'_>) -> trc::Result - where - Self: Sized, - { - let mut hash = 0; - let mut shift = 0; - - while let Some(ch) = parser.next_unescaped()? { - if shift < 128 { - hash |= (ch as u128) << shift; - shift += 8; - } else { - hash = 0; - break; - } - } - - Ok(hash) - } -} - -impl JsonObjectParser for String { - fn parse(parser: &mut Parser<'_>) -> trc::Result - where - Self: Sized, - { - let start_pos = parser.pos; - - while let Some(ch) = parser.next_char() { - match ch { - b'\\' => { - let mut is_escaped = true; - let mut buf = Vec::with_capacity((parser.pos - start_pos) + 16); - buf.extend_from_slice(&parser.bytes[start_pos..parser.pos - 1]); - - while let Some(ch) = parser.next_char() { - match ch { - b'\\' if !is_escaped => { - is_escaped = true; - } - b'"' if !is_escaped => { - parser.is_eof = true; - return String::from_utf8(buf).map_err(|_| parser.error_utf8()); - } - _ => { - if !is_escaped { - buf.push(ch); - } else { - match ch { - b'"' => { - buf.push(b'"'); - } - b'\\' => { - buf.push(b'\\'); - } - b'n' => { - buf.push(b'\n'); - } - b't' => { - buf.push(b'\t'); - } - b'r' => { - buf.push(b'\r'); - } - b'b' => { - buf.push(0x08); - } - b'f' => { - buf.push(0x0c); - } - b'/' => { - buf.push(b'/'); - } - b'u' => { - let mut code = [ - *parser.iter.next().ok_or_else(|| { - parser.error("Incomplete unicode sequence") - })?, - *parser.iter.next().ok_or_else(|| { - parser.error("Incomplete unicode sequence") - })?, - *parser.iter.next().ok_or_else(|| { - parser.error("Incomplete unicode sequence") - })?, - *parser.iter.next().ok_or_else(|| { - parser.error("Incomplete unicode sequence") - })?, - ]; - parser.pos += 4; - let code_str = std::str::from_utf8(&code) - .map_err(|_| parser.error_utf8())?; - let code_str = char::from_u32( - u32::from_str_radix(code_str, 16).map_err( - |_| { - parser.error(&format!( - "Invalid unicode sequence {code_str}" - )) - }, - )?, - ) - .ok_or_else(|| { - parser.error(&format!( - "Invalid unicode sequence {code_str}" - )) - })? - .encode_utf8(&mut code); - buf.extend_from_slice(code_str.as_bytes()); - } - _ => { - buf.push(ch); - } - } - is_escaped = false; - } - } - } - } - break; - } - b'"' => { - parser.is_eof = true; - return std::str::from_utf8( - parser - .bytes - .get(start_pos..parser.pos - 1) - .unwrap_or_default(), - ) - .map(Into::into) - .map_err(|_| parser.error_utf8()); - } - _ => (), - } - } - - Err(parser.error_unterminated()) - } -} - -impl JsonObjectParser for Vec { - fn parse(parser: &mut Parser<'_>) -> trc::Result - where - Self: Sized, - { - let mut vec = Vec::new(); - - parser.next_token::()?.assert(Token::ArrayStart)?; - loop { - match parser.next_token::()? { - Token::String(item) => vec.push(item), - Token::Comma => (), - Token::ArrayEnd => break, - token => return Err(token.error("", "[ or string")), - } - } - Ok(vec) - } -} - -impl JsonObjectParser for Option> { - fn parse(parser: &mut Parser<'_>) -> trc::Result - where - Self: Sized, - { - match parser.next_token::()? { - Token::ArrayStart => { - let mut vec = Vec::new(); - loop { - match parser.next_token::()? { - Token::String(item) => vec.push(item), - Token::Comma => (), - Token::ArrayEnd => break, - token => return Err(token.error("", "string")), - } - } - Ok(Some(vec)) - } - Token::Null => Ok(None), - token => Err(token.error("", "array or null")), - } - } -} - -impl JsonObjectParser for Bitmap { - fn parse(parser: &mut Parser<'_>) -> trc::Result - where - Self: Sized, - { - let mut bm = Bitmap::new(); - match parser.next_token::()? { - Token::ArrayStart => { - loop { - match parser.next_token::()? { - Token::String(item) => bm.insert(item), - Token::Comma => (), - Token::ArrayEnd => break, - token => return Err(token.error("", "string")), - } - } - Ok(bm) - } - Token::Null => Ok(bm), - token => Err(token.error("", "array or null")), - } - } -} - -impl JsonObjectParser for VecMap { - fn parse(parser: &mut Parser<'_>) -> trc::Result - where - Self: Sized, - { - let mut map = VecMap::new(); - - parser.next_token::()?.assert(Token::DictStart)?; - while let Some(key) = parser.next_dict_key()? { - map.append(key, V::parse(parser)?); - } - - Ok(map) - } -} - -impl JsonObjectParser - for Option> -{ - fn parse(parser: &mut Parser<'_>) -> trc::Result - where - Self: Sized, - { - match parser.next_token::()? { - Token::DictStart => { - let mut map = VecMap::new(); - - while let Some(key) = parser.next_dict_key()? { - map.append(key, V::parse(parser)?); - } - - Ok(Some(map)) - } - Token::Null => Ok(None), - token => Err(token.error("", &token.to_string())), - } - } -} - -impl JsonObjectParser for bool { - fn parse(parser: &mut Parser<'_>) -> trc::Result - where - Self: Sized, - { - match parser.next_token::()? { - Token::Boolean(value) => Ok(value), - Token::Null => Ok(false), - token => Err(token.error("", &token.to_string())), - } - } -} - -impl JsonObjectParser for Ignore { - fn parse(parser: &mut Parser<'_>) -> trc::Result - where - Self: Sized, - { - if parser.skip_string() { - Ok(Ignore {}) - } else { - Err(parser.error_unterminated()) - } - } -} diff --git a/crates/jmap-proto/src/parser/json.rs b/crates/jmap-proto/src/parser/json.rs deleted file mode 100644 index 874692ac..00000000 --- a/crates/jmap-proto/src/parser/json.rs +++ /dev/null @@ -1,394 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC - * - * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL - */ - -use std::{fmt::Display, iter::Peekable, slice::Iter}; - -use compact_str::format_compact; - -use crate::request::method::MethodObject; - -use super::{Ignore, JsonObjectParser, Token}; - -const MAX_NESTED_LEVELS: u32 = 16; - -#[derive(Debug)] -pub struct Parser<'x> { - pub bytes: &'x [u8], - pub iter: Peekable>, - pub next_ch: Option, - pub pos: usize, - pub pos_marker: usize, - pub depth_array: u32, - pub depth_dict: u32, - pub is_eof: bool, - pub ctx: MethodObject, -} - -impl<'x> Parser<'x> { - pub fn new(bytes: &'x [u8]) -> Self { - Self { - bytes, - iter: bytes.iter().peekable(), - next_ch: None, - pos: 0, - pos_marker: 0, - is_eof: false, - depth_array: 0, - depth_dict: 0, - ctx: MethodObject::Core, - } - } - - pub fn error(&self, message: &str) -> trc::Error { - trc::JmapEvent::NotJson - .into_err() - .details(format_compact!("{message} at position {}.", self.pos)) - } - - pub fn error_unterminated(&self) -> trc::Error { - trc::JmapEvent::NotJson.into_err().details(format_compact!( - "Unterminated string at position {pos}.", - pos = self.pos - )) - } - - pub fn error_utf8(&self) -> trc::Error { - trc::JmapEvent::NotJson.into_err().details(format_compact!( - "Invalid UTF-8 sequence at position {pos}.", - pos = self.pos - )) - } - - pub fn error_value(&mut self) -> trc::Error { - if self.is_eof || self.skip_string() { - trc::JmapEvent::InvalidArguments - .into_err() - .details(format_compact!( - "Invalid value {:?} at position {}.", - String::from_utf8_lossy(self.bytes[self.pos_marker..self.pos - 1].as_ref()), - self.pos - )) - } else { - self.error_unterminated() - } - } - - #[inline(always)] - pub fn peek_char(&mut self) -> Option { - self.iter.peek().map(|&&ch| ch) - } - - #[inline(always)] - pub fn next_char(&mut self) -> Option { - self.pos += 1; - self.iter.next().copied() - } - - #[inline(always)] - pub fn next_unescaped(&mut self) -> trc::Result> { - match self.next_char() { - Some(b'"') => { - self.is_eof = true; - Ok(None) - } - Some(b'\\') => self - .next_char() - .ok_or_else(|| self.error_unterminated()) - .map(Some), - Some(ch) => Ok(Some(ch)), - None => { - if self.is_eof { - Ok(None) - } else { - Err(self.error_unterminated()) - } - } - } - } - - pub fn skip_string(&mut self) -> bool { - let mut last_ch = 0; - - while let Some(ch) = self.next_char() { - if ch == b'"' && last_ch != b'\\' { - self.is_eof = true; - return true; - } else { - last_ch = ch; - } - } - - false - } - - pub fn next_token(&mut self) -> trc::Result> { - let mut next_ch = self.next_ch.take().or_else(|| self.next_char()); - - while let Some(mut ch) = next_ch { - match ch { - b'"' => { - self.pos_marker = self.pos; - self.is_eof = false; - let value = T::parse(self)?; - return if self.is_eof || self.skip_string() { - Ok(Token::String(value)) - } else { - Err(self.error_unterminated()) - }; - } - b',' => { - return Ok(Token::Comma); - } - b':' => { - return Ok(Token::Colon); - } - b'[' => { - if self.depth_array + self.depth_dict < MAX_NESTED_LEVELS { - self.depth_array += 1; - return Ok(Token::ArrayStart); - } else { - return Err(self.error("Too many nested objects")); - } - } - b']' => { - return if self.depth_array != 0 { - self.depth_array -= 1; - Ok(Token::ArrayEnd) - } else { - Err(self.error("Unexpected array end")) - }; - } - b'{' => { - if self.depth_array + self.depth_dict < MAX_NESTED_LEVELS { - self.depth_dict += 1; - return Ok(Token::DictStart); - } else { - return Err(self.error("Too many nested objects")); - } - } - b'}' => { - return if self.depth_dict != 0 { - self.depth_dict -= 1; - Ok(Token::DictEnd) - } else { - Err(self.error("Unexpected dictionary end")) - }; - } - b'0'..=b'9' | b'-' | b'+' => { - let mut num: i64 = 0; - let mut is_float = false; - let mut is_negative = false; - let num_start = self.pos - 1; - - loop { - match ch { - b'-' => { - is_negative = true; - } - b'0'..=b'9' => { - if !is_float { - num = num.saturating_mul(10).saturating_add((ch - b'0') as i64); - } - } - b',' | b']' | b'}' => { - self.next_ch = ch.into(); - break; - } - b'+' => (), - b'.' | b'e' | b'E' => { - is_float = true; - } - b' ' | b'\r' | b'\t' | b'\n' => { - break; - } - _ => { - return Err(self - .error(&format!("Unexpected character {:?}", char::from(ch)))); - } - } - - ch = self.next_char().ok_or_else(|| self.error_unterminated())?; - } - - return if !is_float { - Ok(Token::Integer(if !is_negative { num } else { -num })) - } else { - fast_float::parse( - self.bytes.get(num_start..self.pos - 1).unwrap_or_default(), - ) - .map(Token::Float) - .map_err(|_| { - self.error(&format!( - "Failed to parse number {:?}", - String::from_utf8_lossy( - self.bytes.get(num_start..self.pos - 1).unwrap_or_default() - ) - )) - }) - }; - } - b't' => { - return if let (Some(b'r'), Some(b'u'), Some(b'e')) = - (self.iter.next(), self.iter.next(), self.iter.next()) - { - self.pos += 3; - Ok(Token::Boolean(true)) - } else { - Err(self.error("Invalid JSON token")) - }; - } - b'f' => { - return if let (Some(b'a'), Some(b'l'), Some(b's'), Some(b'e')) = ( - self.iter.next(), - self.iter.next(), - self.iter.next(), - self.iter.next(), - ) { - self.pos += 4; - Ok(Token::Boolean(false)) - } else { - Err(self.error("Invalid JSON token")) - }; - } - b'n' => { - return if let (Some(b'u'), Some(b'l'), Some(b'l')) = - (self.iter.next(), self.iter.next(), self.iter.next()) - { - self.pos += 3; - Ok(Token::Null) - } else { - Err(self.error("Invalid JSON token")) - }; - } - b' ' | b'\t' | b'\r' | b'\n' => (), - _ => { - return Err(self.error(&format!("Unexpected character {:?}", char::from(ch)))); - } - } - - next_ch = self.next_char(); - } - - Err(self.error("Unexpected EOF")) - } - - pub fn next_dict_key(&mut self) -> trc::Result> { - loop { - match self.next_token::()? { - Token::String(k) => { - self.next_token::()?.assert(Token::Colon)?; - return Ok(Some(k)); - } - Token::Comma => (), - Token::DictEnd => return Ok(None), - token => { - return Err(self.error(&format!("Expected object property, found {}", token))); - } - } - } - } - - pub fn skip_token(&mut self, start_depth_array: u32, start_depth_dict: u32) -> trc::Result<()> { - while { - self.next_token::()?; - start_depth_array != self.depth_array || start_depth_dict != self.depth_dict - } {} - - Ok(()) - } -} - -#[cfg(test)] -mod tests { - - use crate::parser::Token; - - use super::Parser; - - #[test] - fn parse_json() { - for (input, expected_result) in [ - ( - &b"[true, false, 123, 456 , -123, 0.123, -0.456, 3.7e-5, 6.02e+23, null]"[..], - vec![ - Token::ArrayStart, - Token::Boolean(true), - Token::Comma, - Token::Boolean(false), - Token::Comma, - Token::Integer(123), - Token::Comma, - Token::Integer(456), - Token::Comma, - Token::Integer(-123), - Token::Comma, - Token::Float(0.123), - Token::Comma, - Token::Float(-0.456), - Token::Comma, - Token::Float(3.7e-5), - Token::Comma, - Token::Float(6.02e23), - Token::Comma, - Token::Null, - Token::ArrayEnd, - ], - ), - ( - &b"{\"\": true, \"\": false , \"\": {\"\": 123}, \"\": [ ]}"[..], - vec![ - Token::DictStart, - Token::String("".to_string()), - Token::Colon, - Token::Boolean(true), - Token::Comma, - Token::String("".to_string()), - Token::Colon, - Token::Boolean(false), - Token::Comma, - Token::String("".to_string()), - Token::Colon, - Token::DictStart, - Token::String("".to_string()), - Token::Colon, - Token::Integer(123), - Token::DictEnd, - Token::Comma, - Token::String("".to_string()), - Token::Colon, - Token::ArrayStart, - Token::ArrayEnd, - Token::DictEnd, - ], - ), - ] { - let mut p = Parser::new(input); - let mut result = Vec::new(); - while let Ok(token) = p.next_token() { - result.push(token); - } - - assert_eq!(result, expected_result); - } - - for (input, expected_result) in [ - ("hello\t\nworld", "hello\t\nworld"), - ("hello\t\n\\\"world\\\"\\n", "hello\t\n\"world\"\n"), - ("\\\"hello\\\tworld\\\"", "\"hello\tworld\""), - ("\\u0009\\u0020\\u263A", "\t ☺"), - ("", ""), - ] { - assert_eq!( - Parser::new(format!("\"{input}\"").as_bytes()) - .next_token::() - .unwrap() - .unwrap_string("") - .unwrap(), - expected_result - ); - } - } -} diff --git a/crates/jmap-proto/src/parser/mod.rs b/crates/jmap-proto/src/parser/mod.rs deleted file mode 100644 index ac3fadc4..00000000 --- a/crates/jmap-proto/src/parser/mod.rs +++ /dev/null @@ -1,165 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC - * - * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL - */ - -use std::fmt::Display; - -use compact_str::format_compact; - -use self::json::Parser; - -pub mod base32; -pub mod impls; -pub mod json; - -#[derive(Debug, PartialEq, Clone)] -pub enum Token { - Colon, - Comma, - DictStart, - DictEnd, - ArrayStart, - ArrayEnd, - Integer(i64), - Float(f64), - Boolean(bool), - String(T), - Null, -} - -impl Eq for Token {} - -pub trait JsonObjectParser { - fn parse(parser: &mut Parser<'_>) -> trc::Result - where - Self: Sized; -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub struct Ignore {} - -impl Token { - pub fn unwrap_string(self, property: &str) -> trc::Result { - match self { - Token::String(s) => Ok(s), - token => Err(token.error(property, "string")), - } - } - - pub fn unwrap_string_or_null(self, property: &str) -> trc::Result> { - match self { - Token::String(s) => Ok(Some(s)), - Token::Null => Ok(None), - token => Err(token.error(property, "string")), - } - } - - pub fn unwrap_bool(self, property: &str) -> trc::Result { - match self { - Token::Boolean(v) => Ok(v), - token => Err(token.error(property, "boolean")), - } - } - - pub fn unwrap_bool_or_null(self, property: &str) -> trc::Result> { - match self { - Token::Boolean(v) => Ok(Some(v)), - Token::Null => Ok(None), - token => Err(token.error(property, "boolean")), - } - } - - pub fn unwrap_usize_or_null(self, property: &str) -> trc::Result> { - match self { - Token::Integer(v) if v >= 0 => Ok(Some(v as usize)), - Token::Float(v) if v >= 0.0 => Ok(Some(v as usize)), - Token::Null => Ok(None), - token => Err(token.error(property, "unsigned integer")), - } - } - - pub fn unwrap_uint_or_null(self, property: &str) -> trc::Result> { - match self { - Token::Integer(v) if v >= 0 => Ok(Some(v as u64)), - Token::Float(v) if v >= 0.0 => Ok(Some(v as u64)), - Token::Null => Ok(None), - token => Err(token.error(property, "unsigned integer")), - } - } - - pub fn unwrap_int_or_null(self, property: &str) -> trc::Result> { - match self { - Token::Integer(v) => Ok(Some(v)), - Token::Float(v) => Ok(Some(v as i64)), - Token::Null => Ok(None), - token => Err(token.error(property, "unsigned integer")), - } - } - - pub fn unwrap_ints_or_null(self, property: &str) -> trc::Result> { - match self { - Token::Integer(v) => Ok(Some(v as i32)), - Token::Float(v) => Ok(Some(v as i32)), - Token::Null => Ok(None), - token => Err(token.error(property, "unsigned integer")), - } - } - - pub fn assert(self, token: Token) -> trc::Result<()> { - if self == token { - Ok(()) - } else { - Err(self.error("", &token.to_string())) - } - } - - pub fn assert_jmap(self, token: Token) -> trc::Result<()> { - if self == token { - Ok(()) - } else { - Err(trc::JmapEvent::NotRequest - .into_err() - .details(format_compact!( - "Invalid JMAP request: expected '{token}', got '{self}'." - ))) - } - } - - pub fn error(&self, property: &str, expected: &str) -> trc::Error { - trc::JmapEvent::InvalidArguments - .into_err() - .details(if !property.is_empty() { - format_compact!( - "Invalid argument for '{property:?}': expected '{expected}', got '{self}'.", - ) - } else { - format_compact!("Invalid argument: expected '{expected}', got '{self}'.") - }) - } -} - -impl Display for Ignore { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "string") - } -} - -impl Display for Token { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - Token::Colon => write!(f, ":"), - Token::Comma => write!(f, ","), - Token::DictStart => write!(f, "{{"), - Token::DictEnd => write!(f, "}}"), - Token::ArrayStart => write!(f, "["), - Token::ArrayEnd => write!(f, "]"), - Token::Integer(i) => write!(f, "{}", i), - Token::Float(v) => write!(f, "{}", v), - Token::Boolean(b) => write!(f, "{}", b), - Token::Null => write!(f, "null"), - Token::String(_) => write!(f, "string"), - } - } -} diff --git a/crates/jmap-proto/src/request/capability.rs b/crates/jmap-proto/src/request/capability.rs index bcd6f5ca..a6cc1180 100644 --- a/crates/jmap-proto/src/request/capability.rs +++ b/crates/jmap-proto/src/request/capability.rs @@ -4,11 +4,7 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use crate::{ - parser::{JsonObjectParser, json::Parser}, - response::serialize::serialize_hex, -}; -use compact_str::CompactString; +use crate::response::serialize::serialize_hex; use types::{id::Id, type_state::DataType}; use utils::map::vec_map::VecMap; @@ -316,51 +312,19 @@ impl WebSocketCapabilities { } } -impl JsonObjectParser for Capability { - fn parse(parser: &mut Parser<'_>) -> trc::Result - where - Self: Sized, - { - for ch in b"urn:ietf:params:jmap:" { - if parser - .next_unescaped()? - .ok_or_else(|| parser.error_capability())? - != *ch - { - return Err(parser.error_capability()); - } - } - - match u128::parse(parser) { - Ok(key) => match key { - 0x6572_6f63 => Ok(Capability::Core), - 0x6c69_616d => Ok(Capability::Mail), - 0x6e6f_6973_7369_6d62_7573 => Ok(Capability::Submission), - 0x6573_6e6f_7073_6572_6e6f_6974_6163_6176 => Ok(Capability::VacationResponse), - 0x7374_6361_746e_6f63 => Ok(Capability::Contacts), - 0x0073_7261_646e_656c_6163 => Ok(Capability::Calendars), - 0x0074_656b_636f_7362_6577 => Ok(Capability::WebSocket), - 0x0065_7665_6973 => Ok(Capability::Sieve), - 0x626f_6c62 => Ok(Capability::Blob), - 0x0061_746f_7571 => Ok(Capability::Quota), - _ => Err(parser.error_capability()), - }, - Err(err) if err.is_jmap_method_error() => Err(parser.error_capability()), - Err(err) => Err(err), - } - } -} - -impl Parser<'_> { - fn error_capability(&mut self) -> trc::Error { - if self.is_eof || self.skip_string() { - trc::JmapEvent::UnknownCapability - .into_err() - .details(CompactString::from_utf8_lossy( - self.bytes[self.pos_marker..self.pos - 1].as_ref(), - )) - } else { - self.error_unterminated() - } +impl Capability { + pub fn parse(s: &str) -> Option { + hashify::tiny_map!(s.as_bytes(), + "urn:ietf:params:jmap:core" => Capability::Core, + "urn:ietf:params:jmap:mail" => Capability::Mail, + "urn:ietf:params:jmap:submission" => Capability::Submission, + "urn:ietf:params:jmap:vacationresponse" => Capability::VacationResponse, + "urn:ietf:params:jmap:contacts" => Capability::Contacts, + "urn:ietf:params:jmap:calendars" => Capability::Calendars, + "urn:ietf:params:jmap:websocket" => Capability::WebSocket, + "urn:ietf:params:jmap:sieve" => Capability::Sieve, + "urn:ietf:params:jmap:blob" => Capability::Blob, + "urn:ietf:params:jmap:quota" => Capability::Quota, + ) } } diff --git a/crates/jmap-proto/src/request/echo.rs b/crates/jmap-proto/src/request/echo.rs deleted file mode 100644 index 1b486fd2..00000000 --- a/crates/jmap-proto/src/request/echo.rs +++ /dev/null @@ -1,38 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC - * - * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL - */ - -use serde_json::value::RawValue; -use std::fmt::Write; - -use crate::parser::{JsonObjectParser, Token, json::Parser}; - -#[derive(Debug, serde::Serialize)] -pub struct Echo { - pub payload: Box, -} - -impl JsonObjectParser for Echo { - fn parse(parser: &mut Parser<'_>) -> trc::Result - where - Self: Sized, - { - let start_depth_array = parser.depth_array; - let start_depth_dict = parser.depth_dict; - let mut value = String::new(); - - while { - let _ = match parser.next_token::()? { - Token::String(string) => write!(value, "{string:?}"), - token => write!(value, "{token}"), - }; - start_depth_array != parser.depth_array || start_depth_dict != parser.depth_dict - } {} - - Ok(Echo { - payload: RawValue::from_string(value).unwrap(), - }) - } -} diff --git a/crates/jmap-proto/src/request/method.rs b/crates/jmap-proto/src/request/method.rs index 02913b55..42e83e62 100644 --- a/crates/jmap-proto/src/request/method.rs +++ b/crates/jmap-proto/src/request/method.rs @@ -6,8 +6,6 @@ use std::fmt::Display; -use crate::parser::{JsonObjectParser, json::Parser}; - #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct MethodName { pub obj: MethodObject, @@ -47,77 +45,6 @@ pub enum MethodFunction { Echo, } -impl JsonObjectParser for MethodName { - fn parse(parser: &mut Parser<'_>) -> trc::Result - where - Self: Sized, - { - let mut shift = 0; - let mut obj_hash: u128 = 0; - let mut fnc_hash: u128 = 0; - - loop { - let ch = parser - .next_unescaped()? - .ok_or_else(|| parser.error_value())?; - if ch != b'/' { - if shift < 128 { - obj_hash |= (ch as u128) << shift; - shift += 8; - } else { - return Err(parser.error_value()); - } - } else { - break; - } - } - - shift = 0; - while let Some(ch) = parser.next_unescaped()? { - if shift < 128 { - fnc_hash |= (ch as u128) << shift; - shift += 8; - } else { - return Err(parser.error_value()); - } - } - - Ok(MethodName { - obj: match obj_hash { - 0x006c_6961_6d45 => MethodObject::Email, - 0x0078_6f62_6c69_614d => MethodObject::Mailbox, - 0x6461_6572_6854 => MethodObject::Thread, - 0x626f_6c42 => MethodObject::Blob, - 0x006e_6f69_7373_696d_6275_536c_6961_6d45 => MethodObject::EmailSubmission, - 0x0074_6570_7069_6e53_6863_7261_6553 => MethodObject::SearchSnippet, - 0x7974_6974_6e65_6449 => MethodObject::Identity, - 0x6573_6e6f_7073_6552_6e6f_6974_6163_6156 => MethodObject::VacationResponse, - 0x6e6f_6974_7069_7263_7362_7553_6873_7550 => MethodObject::PushSubscription, - 0x0074_7069_7263_5365_7665_6953 => MethodObject::SieveScript, - 0x006c_6170_6963_6e69_7250 => MethodObject::Principal, - 0x0061_746f_7551 => MethodObject::Quota, - 0x6572_6f43 => MethodObject::Core, - _ => return Err(parser.error_value()), - }, - fnc: match fnc_hash { - 0x0074_6567 => MethodFunction::Get, - 0x0079_7265_7571 => MethodFunction::Query, - 0x0074_6573 => MethodFunction::Set, - 0x0073_6567_6e61_6863 => MethodFunction::Changes, - 0x7365_676e_6168_4379_7265_7571 => MethodFunction::QueryChanges, - 0x7970_6f63 => MethodFunction::Copy, - 0x7472_6f70_6d69 => MethodFunction::Import, - 0x0065_7372_6170 => MethodFunction::Parse, - 0x6574_6164_696c_6176 => MethodFunction::Validate, - 0x7075_6b6f_6f6c => MethodFunction::Lookup, - 0x6461_6f6c_7075 => MethodFunction::Upload, - 0x6f68_6365 => MethodFunction::Echo, - _ => return Err(parser.error_value()), - }, - }) - } -} - impl Display for MethodName { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.write_str(self.as_str()) @@ -199,6 +126,69 @@ impl MethodName { _ => "error", } } + + pub fn parse(s: &str) -> Option { + hashify::tiny_map!(s.as_bytes(), + "PushSubscription/get" => (MethodObject::PushSubscription, MethodFunction::Get), + "PushSubscription/set" => (MethodObject::PushSubscription, MethodFunction::Set), + + "Mailbox/get" => (MethodObject::Mailbox, MethodFunction::Get), + "Mailbox/changes" => (MethodObject::Mailbox, MethodFunction::Changes), + "Mailbox/query" => (MethodObject::Mailbox, MethodFunction::Query), + "Mailbox/queryChanges" => (MethodObject::Mailbox, MethodFunction::QueryChanges), + "Mailbox/set" => (MethodObject::Mailbox, MethodFunction::Set), + + "Thread/get" => (MethodObject::Thread, MethodFunction::Get), + "Thread/changes" => (MethodObject::Thread, MethodFunction::Changes), + + "Email/get" => (MethodObject::Email, MethodFunction::Get), + "Email/changes" => (MethodObject::Email, MethodFunction::Changes), + "Email/query" => (MethodObject::Email, MethodFunction::Query), + "Email/queryChanges" => (MethodObject::Email, MethodFunction::QueryChanges), + "Email/set" => (MethodObject::Email, MethodFunction::Set), + "Email/copy" => (MethodObject::Email, MethodFunction::Copy), + "Email/import" => (MethodObject::Email, MethodFunction::Import), + "Email/parse" => (MethodObject::Email, MethodFunction::Parse), + + "SearchSnippet/get" => (MethodObject::SearchSnippet, MethodFunction::Get), + + "Identity/get" => (MethodObject::Identity, MethodFunction::Get), + "Identity/changes" => (MethodObject::Identity, MethodFunction::Changes), + "Identity/set" => (MethodObject::Identity, MethodFunction::Set), + + "EmailSubmission/get" => (MethodObject::EmailSubmission, MethodFunction::Get), + "EmailSubmission/changes" => (MethodObject::EmailSubmission, MethodFunction::Changes), + "EmailSubmission/query" => (MethodObject::EmailSubmission, MethodFunction::Query), + "EmailSubmission/queryChanges" => (MethodObject::EmailSubmission, MethodFunction::QueryChanges), + "EmailSubmission/set" => (MethodObject::EmailSubmission, MethodFunction::Set), + + "VacationResponse/get" => (MethodObject::VacationResponse, MethodFunction::Get), + "VacationResponse/set" => (MethodObject::VacationResponse, MethodFunction::Set), + + "SieveScript/get" => (MethodObject::SieveScript, MethodFunction::Get), + "SieveScript/set" => (MethodObject::SieveScript, MethodFunction::Set), + "SieveScript/query" => (MethodObject::SieveScript, MethodFunction::Query), + "SieveScript/validate" => (MethodObject::SieveScript, MethodFunction::Validate), + + "Principal/get" => (MethodObject::Principal, MethodFunction::Get), + "Principal/set" => (MethodObject::Principal, MethodFunction::Set), + "Principal/query" => (MethodObject::Principal, MethodFunction::Query), + + "Quota/get" => (MethodObject::Quota, MethodFunction::Get), + "Quota/changes" => (MethodObject::Quota, MethodFunction::Changes), + "Quota/query" => (MethodObject::Quota, MethodFunction::Query), + "Quota/queryChanges" => (MethodObject::Quota, MethodFunction::QueryChanges), + + "Blob/get" => (MethodObject::Blob, MethodFunction::Get), + "Blob/copy" => (MethodObject::Blob, MethodFunction::Copy), + "Blob/lookup" => (MethodObject::Blob, MethodFunction::Lookup), + "Blob/upload" => (MethodObject::Blob, MethodFunction::Upload), + + "Core/echo" => (MethodObject::Core, MethodFunction::Echo), + + ).map(|(obj, fnc)| MethodName { obj, fnc }) + } + } impl Display for MethodObject { @@ -221,7 +211,20 @@ impl Display for MethodObject { } } -// Method serialization + +impl<'de> serde::Deserialize<'de> for MethodName { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + let value = <&str>::deserialize(deserializer)?; + + MethodName::parse(value).ok_or_else(|| { + serde::de::Error::custom(format!("Invalid method name: {}", value)) + }) + } +} + impl serde::Serialize for MethodName { fn serialize(&self, serializer: S) -> Result where diff --git a/crates/jmap-proto/src/request/mod.rs b/crates/jmap-proto/src/request/mod.rs index 026f6e95..a173193a 100644 --- a/crates/jmap-proto/src/request/mod.rs +++ b/crates/jmap-proto/src/request/mod.rs @@ -5,43 +5,35 @@ */ pub mod capability; -pub mod echo; pub mod method; pub mod parser; pub mod reference; pub mod websocket; -use std::{ - collections::HashMap, - fmt::{Debug, Display}, -}; +use jmap_tools::{Null, Value}; -use crate::{ - method::{ - changes::ChangesRequest, - copy::{self, CopyBlobRequest, CopyRequest}, - get::{self, GetRequest}, - import::ImportEmailRequest, - lookup::BlobLookupRequest, - parse::ParseEmailRequest, - query::{self, QueryRequest}, - query_changes::QueryChangesRequest, - search_snippet::GetSearchSnippetRequest, - set::{self, SetRequest}, - upload::BlobUploadRequest, - validate::ValidateSieveScriptRequest, - }, - parser::{JsonObjectParser, json::Parser}, - types::any_id::AnyId, +use self::method::MethodName; +use crate::method::{ + changes::ChangesRequest, + copy::{self, CopyBlobRequest, CopyRequest}, + get::{self, GetRequest}, + import::ImportEmailRequest, + lookup::BlobLookupRequest, + parse::ParseEmailRequest, + query::{self, QueryRequest}, + query_changes::QueryChangesRequest, + search_snippet::GetSearchSnippetRequest, + set::{self, SetRequest}, + upload::BlobUploadRequest, + validate::ValidateSieveScriptRequest, }; - -use self::{echo::Echo, method::MethodName}; +use std::{collections::HashMap, fmt::Debug}; #[derive(Debug, Default)] -pub struct Request { +pub struct Request<'x> { pub using: u32, - pub method_calls: Vec>, - pub created_ids: Option>, + pub method_calls: Vec>>, + pub created_ids: Option>, } #[derive(Debug)] @@ -51,66 +43,27 @@ pub struct Call { pub method: T, } -#[derive(Debug, PartialEq, Eq)] -pub struct RequestProperty { - pub hash: [u128; 2], - pub is_ref: bool, -} - #[derive(Debug)] -pub enum RequestMethod { - Get(GetRequest), - Set(SetRequest), +pub enum RequestMethod<'x> { + //Get(GetRequest), + //Set(SetRequest), Changes(ChangesRequest), - Copy(CopyRequest), + //Copy(CopyRequest), CopyBlob(CopyBlobRequest), ImportEmail(ImportEmailRequest), ParseEmail(ParseEmailRequest), - QueryChanges(QueryChangesRequest), - Query(QueryRequest), + //QueryChanges(QueryChangesRequest), + //Query(QueryRequest), SearchSnippet(GetSearchSnippetRequest), ValidateScript(ValidateSieveScriptRequest), LookupBlob(BlobLookupRequest), UploadBlob(BlobUploadRequest), - Echo(Echo), - Error(trc::Error), + Echo(Value<'x, Null, Null>), + Error(String), } -impl JsonObjectParser for RequestProperty { - fn parse(parser: &mut Parser<'_>) -> trc::Result - where - Self: Sized, - { - let mut hash = [0; 2]; - let mut shift = 0; - let mut is_ref = false; - - 'outer: for hash in hash.iter_mut() { - while let Some(ch) = parser.next_unescaped()? { - if ch != b'#' || parser.pos > parser.pos_marker + 1 { - *hash |= (ch as u128) << shift; - shift += 8; - if shift == 128 { - shift = 0; - continue 'outer; - } - } else { - is_ref = true; - } - } - break; - } - - Ok(RequestProperty { hash, is_ref }) - } -} - -impl Display for RequestProperty { - fn fmt(&self, _f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - Ok(()) - } -} - -pub trait RequestPropertyParser { - fn parse(&mut self, parser: &mut Parser, property: RequestProperty) -> trc::Result; +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum MaybeInvalid { + Id(V), + Invalid(String), } diff --git a/crates/jmap-proto/src/request/parser.rs b/crates/jmap-proto/src/request/parser.rs index d02195df..e28fdc41 100644 --- a/crates/jmap-proto/src/request/parser.rs +++ b/crates/jmap-proto/src/request/parser.rs @@ -8,23 +8,19 @@ use std::collections::HashMap; use compact_str::ToCompactString; -use crate::{ - method::{ - changes::ChangesRequest, - copy::{CopyBlobRequest, CopyRequest}, - get::GetRequest, - import::ImportEmailRequest, - lookup::BlobLookupRequest, - parse::ParseEmailRequest, - query::QueryRequest, - query_changes::QueryChangesRequest, - search_snippet::GetSearchSnippetRequest, - set::SetRequest, - upload::BlobUploadRequest, - validate::ValidateSieveScriptRequest, - }, - parser::{Ignore, JsonObjectParser, Token, json::Parser}, - types::any_id::AnyId, +use crate::method::{ + changes::ChangesRequest, + copy::{CopyBlobRequest, CopyRequest}, + get::GetRequest, + import::ImportEmailRequest, + lookup::BlobLookupRequest, + parse::ParseEmailRequest, + query::QueryRequest, + query_changes::QueryChangesRequest, + search_snippet::GetSearchSnippetRequest, + set::SetRequest, + upload::BlobUploadRequest, + validate::ValidateSieveScriptRequest, }; use super::{ diff --git a/crates/jmap-proto/src/request/reference.rs b/crates/jmap-proto/src/request/reference.rs index 21cf9596..7f1b30ff 100644 --- a/crates/jmap-proto/src/request/reference.rs +++ b/crates/jmap-proto/src/request/reference.rs @@ -4,101 +4,30 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use std::fmt::Display; +use super::method::MethodName; +use jmap_tools::{JsonPointer, Null}; +use std::{fmt::Display, str::FromStr}; use types::id::Id; -use super::method::MethodName; -use crate::{ - parser::{JsonObjectParser, Token, json::Parser}, - types::pointer::JSONPointer, -}; - -#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize)] +#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)] pub struct ResultReference { #[serde(rename = "resultOf")] pub result_of: String, pub name: MethodName, - pub path: JSONPointer, + pub path: JsonPointer, } #[derive(Debug, Clone, PartialEq, Eq)] -pub enum MaybeReference { +pub enum MaybeIdReference { + Id(V), + Reference(String), + Invalid(String), +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum MaybeResultReference { Value(V), - Reference(R), -} - -impl MaybeReference { - pub fn unwrap(self) -> V { - match self { - MaybeReference::Value(v) => v, - MaybeReference::Reference(_) => panic!("unwrap() called on MaybeReference::Reference"), - } - } - - pub fn try_unwrap(self) -> Option { - match self { - MaybeReference::Value(v) => Some(v), - MaybeReference::Reference(_) => None, - } - } -} - -impl JsonObjectParser for ResultReference { - fn parse(parser: &mut Parser) -> trc::Result - where - Self: Sized, - { - let mut result_of = None; - let mut name = None; - let mut path = None; - - parser - .next_token::()? - .assert_jmap(Token::DictStart)?; - - while let Some(key) = parser.next_dict_key::()? { - match key { - 0x664f_746c_7573_6572 => { - result_of = Some(parser.next_token::()?.unwrap_string("resultOf")?); - } - 0x656d_616e => { - name = Some(parser.next_token::()?.unwrap_string("name")?); - } - 0x6874_6170 => { - path = Some(parser.next_token::()?.unwrap_string("path")?); - } - _ => { - parser.skip_token(parser.depth_array, parser.depth_dict)?; - } - } - } - - if let (Some(result_of), Some(name), Some(path)) = (result_of, name, path) { - Ok(Self { - result_of, - name, - path, - }) - } else { - Err(trc::JmapEvent::InvalidResultReference - .into_err() - .details("Missing required fields")) - } - } -} - -impl JsonObjectParser for MaybeReference { - fn parse(parser: &mut Parser<'_>) -> trc::Result - where - Self: Sized, - { - if let Some(b'#') = parser.peek_char() { - parser.next_unescaped()?; - String::parse(parser).map(MaybeReference::Reference) - } else { - T::parse(parser).map(MaybeReference::Value) - } - } + Reference(ResultReference), } impl Display for ResultReference { @@ -111,24 +40,45 @@ impl Display for ResultReference { } } -impl Display for MaybeReference { +impl Display for MaybeIdReference { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { - MaybeReference::Value(id) => write!(f, "{}", id), - MaybeReference::Reference(str) => write!(f, "#{}", str), + MaybeIdReference::Id(id) => write!(f, "{}", id), + MaybeIdReference::Reference(str) => write!(f, "#{}", str), + MaybeIdReference::Invalid(str) => write!(f, "{}", str), } } } -// MaybeReference de/serialization -impl serde::Serialize for MaybeReference { +impl<'de, V: FromStr> serde::Deserialize<'de> for MaybeIdReference { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + let value = <&str>::deserialize(deserializer)?; + + if let Some(reference) = value.strip_prefix('#') { + if reference.is_empty() { + return Ok(MaybeIdReference::Invalid(value.to_string())); + } + Ok(MaybeIdReference::Reference(reference.to_string())) + } else if let Ok(id) = V::from_str(value) { + Ok(MaybeIdReference::Id(id)) + } else { + Ok(MaybeIdReference::Invalid(value.to_string())) + } + } +} + +impl serde::Serialize for MaybeIdReference { fn serialize(&self, serializer: S) -> Result where S: serde::Serializer, { match self { - MaybeReference::Value(id) => id.serialize(serializer), - MaybeReference::Reference(str) => serializer.serialize_str(&format!("#{}", str)), + MaybeIdReference::Id(id) => id.serialize(serializer), + MaybeIdReference::Reference(str) => serializer.serialize_str(&format!("#{}", str)), + MaybeIdReference::Invalid(str) => serializer.serialize_str(str), } } } diff --git a/crates/jmap-proto/src/request/websocket.rs b/crates/jmap-proto/src/request/websocket.rs index dc7e720c..ebc0e3d3 100644 --- a/crates/jmap-proto/src/request/websocket.rs +++ b/crates/jmap-proto/src/request/websocket.rs @@ -4,22 +4,21 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use super::{Request, RequestProperty}; +use super::Request; use crate::{ error::request::{RequestError, RequestErrorType, RequestLimitError}, - parser::{JsonObjectParser, Token, json::Parser}, request::Call, response::{Response, ResponseMethod, serialize::serialize_hex}, - types::{any_id::AnyId, state::State}, + types::state::State, }; use std::{borrow::Cow, collections::HashMap}; use types::{id::Id, type_state::DataType}; use utils::map::vec_map::VecMap; #[derive(Debug)] -pub struct WebSocketRequest { +pub struct WebSocketRequest<'x> { pub id: Option, - pub request: Request, + pub request: Request<'x>, } #[derive(Debug, serde::Serialize)] @@ -36,7 +35,7 @@ pub struct WebSocketResponse { #[serde(rename(deserialize = "createdIds"))] #[serde(skip_serializing_if = "HashMap::is_empty")] - created_ids: HashMap, + created_ids: HashMap, #[serde(rename = "requestId")] #[serde(skip_serializing_if = "Option::is_none")] @@ -55,8 +54,8 @@ pub struct WebSocketPushEnable { } #[derive(Debug)] -pub enum WebSocketMessage { - Request(WebSocketRequest), +pub enum WebSocketMessage<'x> { + Request(WebSocketRequest<'x>), PushEnable(WebSocketPushEnable), PushDisable, } diff --git a/crates/jmap-proto/src/response/mod.rs b/crates/jmap-proto/src/response/mod.rs index c02babaa..ed9b7bf3 100644 --- a/crates/jmap-proto/src/response/mod.rs +++ b/crates/jmap-proto/src/response/mod.rs @@ -10,6 +10,8 @@ pub mod status; use std::collections::HashMap; +use jmap_tools::{Null, Value}; + use crate::{ error::method::MethodErrorWrapper, method::{ @@ -26,19 +28,18 @@ use crate::{ upload::BlobUploadResponse, validate::ValidateSieveScriptResponse, }, - request::{Call, echo::Echo, method::MethodName}, - types::any_id::AnyId, + request::{Call, method::MethodName}, }; use self::serialize::serialize_hex; #[derive(Debug, serde::Serialize)] #[serde(untagged)] -pub enum ResponseMethod { - Get(GetResponse), +pub enum ResponseMethod<'x> { + /*Get(GetResponse), Set(SetResponse), Changes(ChangesResponse), - Copy(CopyResponse), + Copy(CopyResponse),*/ CopyBlob(CopyBlobResponse), ImportEmail(ImportEmailResponse), ParseEmail(ParseEmailResponse), @@ -48,14 +49,14 @@ pub enum ResponseMethod { ValidateScript(ValidateSieveScriptResponse), LookupBlob(BlobLookupResponse), UploadBlob(BlobUploadResponse), - Echo(Echo), + Echo(Value<'x, Null, Null>), Error(MethodErrorWrapper), } #[derive(Debug, serde::Serialize)] -pub struct Response { +pub struct Response<'x> { #[serde(rename = "methodResponses")] - pub method_responses: Vec>, + pub method_responses: Vec>>, #[serde(rename = "sessionState")] #[serde(serialize_with = "serialize_hex")] @@ -63,11 +64,11 @@ pub struct Response { #[serde(rename = "createdIds")] #[serde(skip_serializing_if = "HashMap::is_empty")] - pub created_ids: HashMap, + pub created_ids: HashMap, } -impl Response { - pub fn new(session_state: u32, created_ids: HashMap, capacity: usize) -> Self { +impl Response<'_> { + pub fn new(session_state: u32, created_ids: HashMap, capacity: usize) -> Self { Response { session_state, created_ids, @@ -96,7 +97,7 @@ impl Response { }); } - pub fn push_created_id(&mut self, create_id: String, id: impl Into) { + pub fn push_created_id(&mut self, create_id: String, id: impl Into) { self.created_ids.insert(create_id, id.into()); } } diff --git a/crates/jmap-proto/src/response/references.rs b/crates/jmap-proto/src/response/references.rs index f5f44038..bb05e23c 100644 --- a/crates/jmap-proto/src/response/references.rs +++ b/crates/jmap-proto/src/response/references.rs @@ -12,11 +12,6 @@ use crate::{ RequestMethod, reference::{MaybeReference, ResultReference}, }, - types::{ - any_id::AnyId, - property::Property, - value::{MaybePatchValue, Object, SetValue, Value}, - }, }; use compact_str::format_compact; use std::collections::HashMap; @@ -259,7 +254,7 @@ impl Response { fn eval_object_references( &self, - obj: &mut Object, + obj: &mut Value<'x, P, E>, mut graph: Option<(&str, &mut HashMap>)>, ) -> trc::Result<()> { for set_value in obj.0.values_mut() { @@ -376,7 +371,7 @@ fn topological_sort( pub trait EvalObjectReferences { fn get_id(&self, id_ref: &str) -> Option; - fn eval_object_references(&self, set_value: SetValue) -> Result { + fn eval_object_references(&self, set_value: SetValue) -> Result> { match set_value { SetValue::Value(value) => Ok(MaybePatchValue::Value(value)), SetValue::Patch(patch) => Ok(MaybePatchValue::Patch(patch)), diff --git a/crates/jmap-proto/src/types/acl.rs b/crates/jmap-proto/src/types/acl.rs deleted file mode 100644 index 6b3a45fb..00000000 --- a/crates/jmap-proto/src/types/acl.rs +++ /dev/null @@ -1,41 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC - * - * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL - */ - -use crate::parser::{JsonObjectParser, json::Parser}; -use types::acl::Acl; - -impl JsonObjectParser for Acl { - fn parse(parser: &mut Parser<'_>) -> trc::Result - where - Self: Sized, - { - let mut hash = 0; - let mut shift = 0; - - while let Some(ch) = parser.next_unescaped()? { - if shift < 128 { - hash |= (ch as u128) << shift; - shift += 8; - } else { - return Err(parser.error_value()); - } - } - - match hash { - 0x6461_6572 => Ok(Acl::Read), - 0x7966_6964_6f6d => Ok(Acl::Modify), - 0x6574_656c_6564 => Ok(Acl::Delete), - 0x0073_6d65_7449_6461_6572 => Ok(Acl::ReadItems), - 0x736d_6574_4964_6461 => Ok(Acl::AddItems), - 0x0073_6d65_7449_7966_6964_6f6d => Ok(Acl::ModifyItems), - 0x0073_6d65_7449_6576_6f6d_6572 => Ok(Acl::RemoveItems), - 0x0064_6c69_6843_6574_6165_7263 => Ok(Acl::CreateChild), - 0x7265_7473_696e_696d_6461 => Ok(Acl::Administer), - 0x7469_6d62_7573 => Ok(Acl::Submit), - _ => Err(parser.error_value()), - } - } -} diff --git a/crates/jmap-proto/src/types/any_id.rs b/crates/jmap-proto/src/types/any_id.rs deleted file mode 100644 index f8fa9eb4..00000000 --- a/crates/jmap-proto/src/types/any_id.rs +++ /dev/null @@ -1,130 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC - * - * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL - */ - -use super::value::Value; -use crate::{ - parser::{JsonObjectParser, json::Parser}, - request::reference::MaybeReference, -}; -use types::{blob::BlobId, id::Id}; - -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum AnyId { - Id(Id), - Blob(BlobId), -} - -impl AnyId { - pub fn as_id(&self) -> Option<&Id> { - match self { - AnyId::Id(id) => Some(id), - _ => None, - } - } - - pub fn as_blob_id(&self) -> Option<&BlobId> { - match self { - AnyId::Blob(id) => Some(id), - _ => None, - } - } - - pub fn into_id(self) -> Option { - match self { - AnyId::Id(id) => Some(id), - _ => None, - } - } - - pub fn into_blob_id(self) -> Option { - match self { - AnyId::Blob(id) => Some(id), - _ => None, - } - } -} - -impl From for AnyId { - fn from(id: Id) -> Self { - Self::Id(id) - } -} - -impl From for AnyId { - fn from(id: BlobId) -> Self { - Self::Blob(id) - } -} - -impl From> for MaybeReference { - fn from(value: MaybeReference) -> Self { - match value { - MaybeReference::Value(value) => MaybeReference::Value(value.into()), - MaybeReference::Reference(reference) => MaybeReference::Reference(reference), - } - } -} - -impl From> for MaybeReference { - fn from(value: MaybeReference) -> Self { - match value { - MaybeReference::Value(value) => MaybeReference::Value(value.into()), - MaybeReference::Reference(reference) => MaybeReference::Reference(reference), - } - } -} - -impl From for Value { - fn from(value: AnyId) -> Self { - match value { - AnyId::Id(id) => Value::Id(id), - AnyId::Blob(blob_id) => Value::BlobId(blob_id), - } - } -} - -impl From<&AnyId> for Value { - fn from(value: &AnyId) -> Self { - match value { - AnyId::Id(id) => Value::Id(*id), - AnyId::Blob(blob_id) => Value::BlobId(blob_id.clone()), - } - } -} - -impl JsonObjectParser for AnyId { - fn parse(parser: &mut Parser<'_>) -> trc::Result - where - Self: Sized, - { - let mut id = Vec::with_capacity(16); - - while let Some(ch) = parser.next_unescaped()? { - id.push(ch); - } - - if id.is_empty() { - return Err(parser.error_value()); - } - - BlobId::from_base32(&id) - .map(AnyId::Blob) - .or_else(|| Id::from_bytes(&id).map(AnyId::Id)) - .ok_or_else(|| parser.error_value()) - } -} - -impl serde::Serialize for AnyId { - fn serialize(&self, serializer: S) -> Result - where - S: serde::Serializer, - { - match self { - AnyId::Id(id) => id.serialize(serializer), - AnyId::Blob(id) => id.serialize(serializer), - } - } -} diff --git a/crates/jmap-proto/src/types/blob.rs b/crates/jmap-proto/src/types/blob.rs deleted file mode 100644 index 23b839f3..00000000 --- a/crates/jmap-proto/src/types/blob.rs +++ /dev/null @@ -1,18 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC - * - * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL - */ - -use crate::parser::{JsonObjectParser, base32::JsonBase32Reader, json::Parser}; -use types::blob::BlobId; - -impl JsonObjectParser for BlobId { - fn parse(parser: &mut Parser<'_>) -> trc::Result - where - Self: Sized, - { - let mut it = JsonBase32Reader::new(parser); - BlobId::from_iter(&mut it).ok_or_else(|| it.error()) - } -} diff --git a/crates/jmap-proto/src/types/date.rs b/crates/jmap-proto/src/types/date.rs index 732b279f..9a2c9a51 100644 --- a/crates/jmap-proto/src/types/date.rs +++ b/crates/jmap-proto/src/types/date.rs @@ -4,12 +4,9 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use std::fmt::Display; - +use std::{fmt::Display, str::FromStr}; use store::SerializeInfallible; -use crate::parser::{JsonObjectParser, json::Parser}; - #[derive(Debug, Default, Clone, PartialEq, Eq, Hash)] pub struct UTCDate { pub year: u16, @@ -23,11 +20,10 @@ pub struct UTCDate { pub tz_minute: u8, } -impl JsonObjectParser for UTCDate { - fn parse(parser: &mut Parser<'_>) -> trc::Result - where - Self: Sized, - { +impl FromStr for UTCDate { + type Err = (); + + fn from_str(s: &str) -> Result { // 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 @@ -47,7 +43,7 @@ impl JsonObjectParser for UTCDate { let mut skip_digits = false; let mut is_plus = true; - while let Some(ch) = parser.next_unescaped()? { + for ch in s.as_bytes() { match ch { b'0'..=b'9' => { if !skip_digits { @@ -119,7 +115,7 @@ impl JsonObjectParser for UTCDate { tz_before_gmt: !is_plus, }) } else { - Err(parser.error_value()) + Err(()) } } } @@ -240,7 +236,7 @@ impl From for UTCDate { #[cfg(test)] mod tests { - use crate::{parser::json::Parser, types::date::UTCDate}; + use crate::types::date::UTCDate; #[test] fn parse_jmap_date() { @@ -255,11 +251,7 @@ mod tests { "2021-01-01T09:55:06+02:00", ), ] { - let date = Parser::new(format!("\"{input}\"").as_bytes()) - .next_token::() - .unwrap() - .unwrap_string("") - .unwrap(); + let date = UTCDate::from_str(input).unwrap(); assert_eq!(date.to_string(), expected_result); let timestamp = date.timestamp(); diff --git a/crates/jmap-proto/src/types/id.rs b/crates/jmap-proto/src/types/id.rs deleted file mode 100644 index b0f54795..00000000 --- a/crates/jmap-proto/src/types/id.rs +++ /dev/null @@ -1,64 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC - * - * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL - */ - -use crate::parser::{JsonObjectParser, json::Parser}; -use types::id::Id; -use utils::codec::base32_custom::BASE32_INVERSE; - -impl JsonObjectParser for Id { - fn parse(parser: &mut Parser<'_>) -> trc::Result - where - Self: Sized, - { - let mut id = 0; - - while let Some(ch) = parser.next_unescaped()? { - let i = BASE32_INVERSE[ch as usize]; - if i != u8::MAX { - id = (id << 5) | i as u64; - } else { - return Err(parser.error_value()); - } - } - - Ok(Id::new(id)) - } -} - -#[cfg(test)] -mod tests { - use crate::{parser::json::Parser, types::id::Id}; - - #[test] - fn parse_jmap_id() { - for number in [ - 0, - 1, - 10, - 1000, - Id::singleton().id(), - u64::MAX / 2, - u64::MAX - 1, - u64::MAX, - ] { - let id = Id::from(number); - assert_eq!( - Parser::new(format!("\"{id}\"").as_bytes()) - .next_token::() - .unwrap() - .unwrap_string("") - .unwrap(), - id - ); - } - - Parser::new(b"\"p333333333333p333333333333\"") - .next_token::() - .unwrap() - .unwrap_string("") - .unwrap(); - } -} diff --git a/crates/jmap-proto/src/types/keyword.rs b/crates/jmap-proto/src/types/keyword.rs deleted file mode 100644 index 4882b013..00000000 --- a/crates/jmap-proto/src/types/keyword.rs +++ /dev/null @@ -1,58 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC - * - * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL - */ - -use crate::parser::{JsonObjectParser, json::Parser}; -use types::keyword::Keyword; - -impl JsonObjectParser for Keyword { - fn parse(parser: &mut Parser<'_>) -> trc::Result - where - Self: Sized, - { - let pos = parser.pos; - if parser - .next_unescaped()? - .ok_or_else(|| parser.error_value())? - == b'$' - { - let mut hash = 0; - let mut shift = 0; - - while let Some(ch) = parser.next_unescaped()? { - if shift < 128 { - hash |= (ch as u128) << shift; - shift += 8; - } else { - break; - } - } - - match hash { - 0x6e65_6573 => return Ok(Keyword::Seen), - 0x0074_6661_7264 => return Ok(Keyword::Draft), - 0x0064_6567_6761_6c66 => return Ok(Keyword::Flagged), - 0x6465_7265_7773_6e61 => return Ok(Keyword::Answered), - 0x746e_6563_6572 => return Ok(Keyword::Recent), - 0x0074_6e61_7472_6f70_6d69 => return Ok(Keyword::Important), - 0x676e_6968_7369_6870 => return Ok(Keyword::Phishing), - 0x6b6e_756a => return Ok(Keyword::Junk), - 0x006b_6e75_6a74_6f6e => return Ok(Keyword::NotJunk), - 0x0064_6574_656c_6564 => return Ok(Keyword::Deleted), - 0x0064_6564_7261_7772_6f66 => return Ok(Keyword::Forwarded), - 0x0074_6e65_736e_646d => return Ok(Keyword::MdnSent), - _ => (), - } - } - - if parser.is_eof || parser.skip_string() { - Ok(Keyword::Other( - String::from_utf8_lossy(parser.bytes[pos..parser.pos - 1].as_ref()).into_owned(), - )) - } else { - Err(parser.error_unterminated()) - } - } -} diff --git a/crates/jmap-proto/src/types/mod.rs b/crates/jmap-proto/src/types/mod.rs index c78fe86c..41375741 100644 --- a/crates/jmap-proto/src/types/mod.rs +++ b/crates/jmap-proto/src/types/mod.rs @@ -4,48 +4,5 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use crate::parser::{JsonObjectParser, json::Parser}; - -pub mod acl; -pub mod any_id; -pub mod blob; pub mod date; -pub mod id; -pub mod keyword; -pub mod pointer; -pub mod property; pub mod state; -pub mod type_state; -pub mod value; - -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum MaybeUnparsable { - Value(V), - ParseError(String), -} - -impl JsonObjectParser for MaybeUnparsable { - fn parse(parser: &mut Parser) -> trc::Result { - match V::parse(parser) { - Ok(value) => Ok(MaybeUnparsable::Value(value)), - Err(_) if parser.is_eof || parser.skip_string() => Ok(MaybeUnparsable::ParseError( - String::from_utf8_lossy(parser.bytes[parser.pos_marker..parser.pos - 1].as_ref()) - .into_owned(), - )), - Err(err) => Err(err), - } - } -} - -// MaybeUnparsable de/serialization -impl serde::Serialize for MaybeUnparsable { - fn serialize(&self, serializer: S) -> Result - where - S: serde::Serializer, - { - match self { - MaybeUnparsable::Value(value) => value.serialize(serializer), - MaybeUnparsable::ParseError(str) => serializer.serialize_str(str), - } - } -} diff --git a/crates/jmap-proto/src/types/pointer.rs b/crates/jmap-proto/src/types/pointer.rs deleted file mode 100644 index 8837582f..00000000 --- a/crates/jmap-proto/src/types/pointer.rs +++ /dev/null @@ -1,271 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC - * - * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL - */ - -use std::fmt::Display; - -use crate::parser::{JsonObjectParser, json::Parser}; - -#[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize)] -pub enum JSONPointer { - Root, - Wildcard, - String(String), - Number(u64), - Path(Vec), -} - -enum TokenType { - Unknown, - Number, - String, - Wildcard, - Escaped, -} - -impl JsonObjectParser for JSONPointer { - fn parse(parser: &mut Parser<'_>) -> trc::Result - where - Self: Sized, - { - let mut path = Vec::new(); - let mut num = 0u64; - let mut buf = Vec::new(); - let mut token = TokenType::Unknown; - let mut start_pos = parser.pos; - - while let Some(ch) = parser.next_char() { - match (ch, &token) { - (b'0'..=b'9', TokenType::Unknown | TokenType::Number) => { - num = num.saturating_mul(10).saturating_add((ch - b'0') as u64); - token = TokenType::Number; - } - (b'*', TokenType::Unknown) => { - token = TokenType::Wildcard; - } - (b'0', TokenType::Escaped) => { - buf.push(b'~'); - token = TokenType::String; - } - (b'1', TokenType::Escaped) => { - buf.push(b'/'); - token = TokenType::String; - } - (b'/' | b'"', _) => { - match token { - TokenType::String => { - path.push(JSONPointer::String( - String::from_utf8(buf).map_err(|_| parser.error_utf8())?, - )); - buf = Vec::new(); - } - TokenType::Number => { - path.push(JSONPointer::Number(num)); - num = 0; - } - TokenType::Wildcard => { - path.push(JSONPointer::Wildcard); - } - TokenType::Unknown if parser.pos_marker != start_pos => { - path.push(JSONPointer::String(String::new())); - } - _ => (), - } - - if ch == b'/' { - token = TokenType::Unknown; - start_pos = parser.pos; - } else { - parser.is_eof = true; - return Ok(match path.len() { - 1 => path.pop().unwrap(), - 0 => JSONPointer::Root, - _ => JSONPointer::Path(path), - }); - } - } - (_, _) => { - if matches!(&token, TokenType::Number | TokenType::Wildcard) - && parser.pos - 1 > start_pos - { - buf.extend_from_slice( - parser - .bytes - .get(start_pos..parser.pos - 1) - .unwrap_or_default(), - ); - } - - token = match ch { - b'~' if !matches!(&token, TokenType::Escaped) => TokenType::Escaped, - b'\\' => { - buf.push(parser.next_char().unwrap_or(b'\\')); - TokenType::String - } - _ => { - buf.push(ch); - TokenType::String - } - }; - } - } - } - - Err(parser.error_unterminated()) - } -} - -impl JSONPointer { - pub fn to_string(&self) -> Option<&str> { - match self { - JSONPointer::String(s) => s.as_str().into(), - _ => None, - } - } - - pub fn unwrap_string(self) -> Option { - match self { - JSONPointer::String(s) => s.into(), - _ => None, - } - } - - pub fn item_query(&self) -> Option<&str> { - match self { - JSONPointer::String(property) => property.as_str().into(), - JSONPointer::Path(path) if path.len() == 2 => { - if let (Some(JSONPointer::String(property)), Some(JSONPointer::Wildcard)) = - (path.first(), path.get(1)) - { - property.as_str().into() - } else { - None - } - } - _ => None, - } - } - - pub fn item_subquery(&self) -> Option<(&str, &str)> { - match self { - JSONPointer::Path(path) if path.len() == 3 => { - match (path.first(), path.get(1), path.get(2)) { - ( - Some(JSONPointer::String(root)), - Some(JSONPointer::Wildcard), - Some(JSONPointer::String(property)), - ) => Some((root.as_str(), property.as_str())), - _ => None, - } - } - _ => None, - } - } -} - -impl Display for JSONPointer { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - JSONPointer::Root => write!(f, "/"), - JSONPointer::Wildcard => write!(f, "*"), - JSONPointer::String(s) => write!(f, "{}", s), - JSONPointer::Number(n) => write!(f, "{}", n), - JSONPointer::Path(path) => { - for (i, ptr) in path.iter().enumerate() { - if i > 0 { - write!(f, "/")?; - } - write!(f, "{}", ptr)?; - } - Ok(()) - } - } - } -} - -#[cfg(test)] -mod tests { - use crate::parser::json::Parser; - - use super::JSONPointer; - - #[test] - fn json_pointer_parse() { - for (input, output) in vec![ - ("hello", JSONPointer::String("hello".to_string())), - ("9a", JSONPointer::String("9a".to_string())), - ("a9", JSONPointer::String("a9".to_string())), - ("*a", JSONPointer::String("*a".to_string())), - ( - "/hello/world", - JSONPointer::Path(vec![ - JSONPointer::String("hello".to_string()), - JSONPointer::String("world".to_string()), - ]), - ), - ("*", JSONPointer::Wildcard), - ( - "/hello/*", - JSONPointer::Path(vec![ - JSONPointer::String("hello".to_string()), - JSONPointer::Wildcard, - ]), - ), - ("1234", JSONPointer::Number(1234)), - ( - "/hello/1234", - JSONPointer::Path(vec![ - JSONPointer::String("hello".to_string()), - JSONPointer::Number(1234), - ]), - ), - ("~0~1", JSONPointer::String("~/".to_string())), - ( - "/hello/~0~1", - JSONPointer::Path(vec![ - JSONPointer::String("hello".to_string()), - JSONPointer::String("~/".to_string()), - ]), - ), - ( - "/hello/1~0~1/*~1~0", - JSONPointer::Path(vec![ - JSONPointer::String("hello".to_string()), - JSONPointer::String("1~/".to_string()), - JSONPointer::String("*/~".to_string()), - ]), - ), - ( - "/hello/world/*/99", - JSONPointer::Path(vec![ - JSONPointer::String("hello".to_string()), - JSONPointer::String("world".to_string()), - JSONPointer::Wildcard, - JSONPointer::Number(99), - ]), - ), - ("/", JSONPointer::String("".to_string())), - ( - "///", - JSONPointer::Path(vec![ - JSONPointer::String("".to_string()), - JSONPointer::String("".to_string()), - JSONPointer::String("".to_string()), - ]), - ), - ("", JSONPointer::Root), - ] { - assert_eq!( - Parser::new(format!("\"{input}\"").as_bytes()) - .next_token::() - .unwrap() - .unwrap_string("") - .unwrap(), - output, - "{input}" - ); - } - } -} diff --git a/crates/jmap-proto/src/types/property.rs b/crates/jmap-proto/src/types/property.rs deleted file mode 100644 index 1426799c..00000000 --- a/crates/jmap-proto/src/types/property.rs +++ /dev/null @@ -1,1106 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC - * - * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL - */ - -use super::value::Value; -use crate::parser::{JsonObjectParser, json::Parser}; -use mail_parser::HeaderName; -use serde::Serialize; -use std::fmt::{Display, Formatter}; -use types::{acl::Acl, id::Id, keyword::Keyword}; - -#[derive(Debug, PartialEq, Eq, Hash, Clone)] -pub enum Property { - Acl, - Aliases, - Attachments, - Bcc, - BlobId, - BodyStructure, - BodyValues, - Capabilities, - Cc, - Charset, - Cid, - DeliveryStatus, - Description, - DeviceClientId, - Disposition, - DsnBlobIds, - Email, - EmailId, - EmailIds, - Envelope, - Expires, - From, - FromDate, - HasAttachment, - Header(HeaderProperty), - Headers, - HtmlBody, - HtmlSignature, - Id, - IdentityId, - InReplyTo, - IsActive, - IsEnabled, - IsSubscribed, - Keys, - Keywords, - Language, - Location, - MailboxIds, - MayDelete, - MdnBlobIds, - Members, - MessageId, - MyRights, - Name, - ParentId, - PartId, - Picture, - Preview, - Quota, - ReceivedAt, - References, - ReplyTo, - Role, - Secret, - SendAt, - Sender, - SentAt, - Size, - SortOrder, - Subject, - SubParts, - TextBody, - TextSignature, - ThreadId, - Timezone, - To, - ToDate, - TotalEmails, - TotalThreads, - Type, - Types, - UndoStatus, - UnreadEmails, - UnreadThreads, - Url, - VerificationCode, - Addresses, - P256dh, - Auth, - Value, - SmtpReply, - Delivered, - Displayed, - MailFrom, - RcptTo, - Parameters, - IsEncodingProblem, - IsTruncated, - MayReadItems, - MayAddItems, - MayRemoveItems, - MaySetSeen, - MaySetKeywords, - MayCreateChild, - MayRename, - MaySubmit, - ResourceType, - Used, - HardLimit, - WarnLimit, - SoftLimit, - Scope, - Digest(DigestProperty), - Data(DataProperty), - _T(String), -} - -#[derive(Debug, PartialEq, Eq, Hash, Clone)] -pub enum DigestProperty { - Sha, - Sha256, - Sha512, -} - -#[derive(Debug, PartialEq, Eq, Hash, Clone)] -pub enum DataProperty { - AsText, - AsBase64, - Default, -} - -#[derive(Debug, PartialEq, Eq, Clone)] -pub struct SetProperty { - pub property: Property, - pub patch: Vec, - pub is_ref: bool, -} - -#[derive(Debug, PartialEq, Eq, Clone)] -pub struct ObjectProperty(Property); - -pub trait IntoProperty: Eq + Display { - fn into_property(self) -> Property; -} - -impl JsonObjectParser for Property { - fn parse(parser: &mut Parser) -> trc::Result { - let mut first_char = 0; - let mut hash = 0; - let mut shift = 0; - - while let Some(ch) = parser.next_unescaped()? { - if ch.is_ascii_alphabetic() { - if first_char != 0 { - if shift < 128 { - hash |= (ch as u128) << shift; - shift += 8; - } else { - return parser.invalid_property(); - } - } else { - first_char = ch; - } - } else if ch == b':' { - return if first_char == b'h' && hash == 0x0072_6564_6165 { - parse_header_property(parser) - } else { - parse_sub_property(parser, first_char, hash) - }; - } else { - return parser.invalid_property(); - } - } - - if let Some(property) = parse_property(first_char, hash) { - Ok(property) - } else { - parser.invalid_property() - } - } -} - -impl JsonObjectParser for SetProperty { - fn parse(parser: &mut Parser) -> trc::Result { - let mut first_char = 0; - let mut hash = 0; - let mut shift = 0; - let mut is_ref = false; - let mut is_patch = false; - - while let Some(ch) = parser.next_unescaped()? { - if ch.is_ascii_alphabetic() { - if first_char != 0 { - if shift < 128 { - hash |= (ch as u128) << shift; - shift += 8; - } else { - return parser.invalid_property().map(|property| SetProperty { - property, - patch: vec![], - is_ref: false, - }); - } - } else { - first_char = ch; - } - } else { - match ch { - b'#' if first_char == 0 && !is_ref => is_ref = true, - b'/' if !is_ref => { - is_patch = true; - break; - } - b':' if first_char == b'h' && hash == 0x0072_6564_6165 && !is_ref => { - return parse_header_property(parser).map(|property| SetProperty { - property, - patch: vec![], - is_ref: false, - }); - } - _ => { - return parser.invalid_property().map(|property| SetProperty { - property, - patch: vec![], - is_ref: false, - }); - } - } - } - } - - let mut property = if let Some(property) = parse_property(first_char, hash) { - property - } else { - parser.invalid_property()? - }; - let mut patch = Vec::new(); - - if is_patch { - match &property { - Property::MailboxIds | Property::Members => match Id::parse(parser) { - Ok(id) => { - patch.push(Value::Id(id)); - } - Err(err) if err.is_jmap_method_error() => { - property = parser.invalid_property()?; - } - Err(err) => { - return Err(err); - } - }, - Property::Keywords => match Keyword::parse(parser) { - Ok(keyword) => { - patch.push(Value::Keyword(keyword)); - } - Err(err) if err.is_jmap_method_error() => { - property = parser.invalid_property()?; - } - Err(err) => { - return Err(err); - } - }, - Property::Acl => { - let mut has_acl = false; - let mut account = Vec::with_capacity(16); - - while let Some(ch) = parser.next_unescaped()? { - if ch != b'/' { - account.push(ch); - } else { - has_acl = true; - break; - } - } - - match String::from_utf8(account) { - Ok(account) if !account.is_empty() => { - patch.push(Value::Text(account)); - if has_acl { - match Acl::parse(parser) { - Ok(acl) => { - patch.push(Value::UnsignedInt(acl as u64)); - } - Err(err) if err.is_jmap_method_error() => { - property = parser.invalid_property()?; - } - Err(err) => { - return Err(err); - } - } - } - } - _ => { - property = parser.invalid_property()?; - } - } - } - Property::Aliases => match String::parse(parser) { - Ok(text) if !text.is_empty() => { - patch.push(Value::Text(text)); - } - Err(err) => { - return Err(err); - } - _ => { - property = parser.invalid_property()?; - } - }, - _ => { - property = parser.invalid_property()?; - } - } - } - - Ok(SetProperty { - property, - patch, - is_ref, - }) - } -} - -fn parse_property(first_char: u8, hash: u128) -> Option { - Some(match first_char { - b'a' => match hash { - 0x6c63 => Property::Acl, - 0x7365_7361_696c => Property::Aliases, - 0x7374_6e65_6d68_6361_7474 => Property::Attachments, - _ => return None, - }, - b'b' => match hash { - 0x6363 => Property::Bcc, - 0x0064_4962_6f6c => Property::BlobId, - 0x6572_7574_6375_7274_5379_646f => Property::BodyStructure, - 0x0073_6575_6c61_5679_646f => Property::BodyValues, - _ => return None, - }, - b'c' => match hash { - 0x0073_6569_7469_6c69_6261_7061 => Property::Capabilities, - 0x63 => Property::Cc, - 0x7465_7372_6168 => Property::Charset, - 0x6469 => Property::Cid, - _ => return None, - }, - b'd' => match hash { - 0x0073_7574_6174_5379_7265_7669_6c65 => Property::DeliveryStatus, - 0x6e6f_6974_7069_7263_7365 => Property::Description, - 0x0064_4974_6e65_696c_4365_6369_7665 => Property::DeviceClientId, - 0x6e6f_6974_6973_6f70_7369 => Property::Disposition, - 0x0073_6449_626f_6c42_6e73 => Property::DsnBlobIds, - 0x0061_7461 => Property::Data(DataProperty::Default), - _ => return None, - }, - b'e' => match hash { - 0x6c69_616d => Property::Email, - 0x6449_6c69_616d => Property::EmailId, - 0x0073_6449_6c69_616d => Property::EmailIds, - 0x0065_706f_6c65_766e => Property::Envelope, - 0x7365_7269_7078 => Property::Expires, - _ => return None, - }, - b'f' => match hash { - 0x006d_6f72 => Property::From, - 0x0065_7461_446d_6f72 => Property::FromDate, - _ => return None, - }, - b'h' => match hash { - 0x746e_656d_6863_6174_7441_7361 => Property::HasAttachment, - 0x7372_6564_6165 => Property::Headers, - 0x0079_646f_426c_6d74 => Property::HtmlBody, - 0x6572_7574_616e_6769_536c_6d74 => Property::HtmlSignature, - _ => return None, - }, - b'i' => match hash { - 0x64 => Property::Id, - 0x0064_4979_7469_746e_6564 => Property::IdentityId, - 0x6f54_796c_7065_526e => Property::InReplyTo, - 0x0065_7669_7463_4173 => Property::IsActive, - 0x6465_6c62_616e_4573 => Property::IsEnabled, - 0x0064_6562_6972_6373_6275_5373 => Property::IsSubscribed, - _ => return None, - }, - b'k' => match hash { - 0x0073_7965 => Property::Keys, - 0x0073_6472_6f77_7965 => Property::Keywords, - _ => return None, - }, - b'l' => match hash { - 0x0065_6761_7567_6e61 => Property::Language, - 0x006e_6f69_7461_636f => Property::Location, - _ => return None, - }, - b'm' => match hash { - 0x0073_6449_786f_626c_6961 => Property::MailboxIds, - 0x6574_656c_6544_7961 => Property::MayDelete, - 0x0073_6449_626f_6c42_6e64 => Property::MdnBlobIds, - 0x7372_6562_6d65 => Property::Members, - 0x6449_6567_6173_7365 => Property::MessageId, - 0x0073_7468_6769_5279 => Property::MyRights, - _ => return None, - }, - b'n' => match hash { - 0x0065_6d61 => Property::Name, - _ => return None, - }, - b'p' => match hash { - 0x0064_4974_6e65_7261 => Property::ParentId, - 0x0064_4974_7261 => Property::PartId, - 0x6572_7574_6369 => Property::Picture, - 0x7765_6976_6572 => Property::Preview, - _ => return None, - }, - b'q' => match hash { - 0x6174_6f75 => Property::Quota, - _ => return None, - }, - b'r' => match hash { - 0x0074_4164_6576_6965_6365 => Property::ReceivedAt, - 0x0073_6563_6e65_7265_6665 => Property::References, - 0x6f54_796c_7065 => Property::ReplyTo, - 0x0065_6c6f => Property::Role, - _ => return None, - }, - b's' => match hash { - 0x0074_6572_6365 => Property::Secret, - 0x0074_4164_6e65 => Property::SendAt, - 0x0072_6564_6e65 => Property::Sender, - 0x0074_4174_6e65 => Property::SentAt, - 0x0065_7a69 => Property::Size, - 0x7265_6472_4f74_726f => Property::SortOrder, - 0x7463_656a_6275 => Property::Subject, - 0x7374_7261_5062_7573 => Property::SubParts, - _ => return None, - }, - b't' => match hash { - 0x0079_646f_4274_7865 => Property::TextBody, - 0x6572_7574_616e_6769_5374_7865 => Property::TextSignature, - 0x0064_4964_6165_7268 => Property::ThreadId, - 0x0065_6e6f_7a65_6d69 => Property::Timezone, - 0x6f => Property::To, - 0x0065_7461_446f => Property::ToDate, - 0x736c_6961_6d45_6c61_746f => Property::TotalEmails, - 0x0073_6461_6572_6854_6c61_746f => Property::TotalThreads, - 0x0065_7079 => Property::Type, - 0x7365_7079 => Property::Types, - _ => return None, - }, - b'u' => match hash { - 0x0073_7574_6174_536f_646e => Property::UndoStatus, - 0x0073_6c69_616d_4564_6165_726e => Property::UnreadEmails, - 0x7364_6165_7268_5464_6165_726e => Property::UnreadThreads, - 0x6c72 => Property::Url, - _ => return None, - }, - b'v' => match hash { - 0x0065_646f_436e_6f69_7461_6369_6669_7265 => Property::VerificationCode, - _ => return None, - }, - _ => return None, - }) -} - -fn parse_header_property(parser: &mut Parser) -> trc::Result { - let hdr_start_pos = parser.pos; - let mut has_next = false; - - while let Some(ch) = parser.next_unescaped()? { - if ch == b':' { - has_next = true; - break; - } - } - - let mut all = false; - let mut form = HeaderForm::Raw; - let header = if parser.pos > hdr_start_pos + 1 { - String::from_utf8_lossy(&parser.bytes[hdr_start_pos..parser.pos - 1]).into_owned() - } else { - return parser.invalid_property(); - }; - - if has_next { - match (parser.next_unescaped()?, parser.next_unescaped()?) { - (Some(b'a'), Some(b's')) => { - let mut hash = 0; - let mut shift = 0; - has_next = false; - - while let Some(ch) = parser.next_unescaped()? { - if ch != b':' { - if shift < 128 { - hash |= (ch as u128) << shift; - shift += 8; - } else { - return parser.invalid_property(); - } - } else { - has_next = true; - break; - } - } - - form = match hash { - 0x7478_6554 => HeaderForm::Text, - 0x0073_6573_7365_7264_6441 => HeaderForm::Addresses, - 0x7365_7373_6572_6464_4164_6570_756f_7247 => HeaderForm::GroupedAddresses, - 0x7364_4965_6761_7373_654d => HeaderForm::MessageIds, - 0x6574_6144 => HeaderForm::Date, - 0x734c_5255 => HeaderForm::URLs, - 0x0077_6152 => HeaderForm::Raw, - _ => return parser.invalid_property(), - }; - - if has_next { - for ch in b"all" { - if Some(*ch) != parser.next_unescaped()? { - return parser.invalid_property(); - } - } - if parser.next_unescaped()?.is_none() { - all = true; - } else { - return parser.invalid_property(); - } - } - } - (Some(b'a'), Some(b'l')) => { - if let (Some(b'l'), None) = (parser.next_unescaped()?, parser.next_unescaped()?) { - all = true; - } else { - return parser.invalid_property(); - } - } - _ => { - return parser.invalid_property(); - } - } - } - - Ok(Property::Header(HeaderProperty { form, header, all })) -} - -fn parse_sub_property( - parser: &mut Parser, - first_char: u8, - parent_hash: u128, -) -> trc::Result { - let mut hash = 0; - let mut shift = 0; - - while let Some(ch) = parser.next_unescaped()? { - if ch.is_ascii_alphanumeric() || ch == b'-' { - if shift < 128 { - hash |= (ch as u128) << shift; - shift += 8; - } else { - return parser.invalid_property(); - } - } else { - return parser.invalid_property(); - } - } - - match (first_char, parent_hash, hash) { - (b'd', 0x0061_7461, 0x7478_6554_7361) => Ok(Property::Data(DataProperty::AsText)), - (b'd', 0x0061_7461, 0x3436_6573_6142_7361) => Ok(Property::Data(DataProperty::AsBase64)), - (b'd', 0x0074_7365_6769, 0x0061_6873) => Ok(Property::Digest(DigestProperty::Sha)), - (b'd', 0x0074_7365_6769, 0x0036_3532_2d61_6873) => { - Ok(Property::Digest(DigestProperty::Sha256)) - } - (b'd', 0x0074_7365_6769, 0x0032_3135_2d61_6873) => { - Ok(Property::Digest(DigestProperty::Sha512)) - } - _ => parser.invalid_property(), - } -} - -impl JsonObjectParser for ObjectProperty { - fn parse(parser: &mut Parser) -> trc::Result { - let mut first_char = 0; - let mut hash = 0; - let mut shift = 0; - - while let Some(ch) = parser.next_unescaped()? { - if ch.is_ascii_alphanumeric() { - if first_char != 0 { - if shift < 128 { - hash |= (ch as u128) << shift; - shift += 8; - } else { - break; - } - } else { - first_char = ch; - } - } else if ch == b':' && first_char == b'h' && hash == 0x0072_6564_6165 { - return parse_header_property(parser).map(ObjectProperty); - } else { - return parser.invalid_property().map(ObjectProperty); - } - } - - Ok(ObjectProperty(match first_char { - b'a' => match hash { - 0x7365_7373_6572_6464 => Property::Addresses, - 0x0068_7475 => Property::Auth, - _ => parser.invalid_property()?, - }, - b'b' => match hash { - 0x0064_4962_6f6c => Property::BlobId, - _ => parser.invalid_property()?, - }, - b'c' => match hash { - 0x7465_7372_6168 => Property::Charset, - 0x6469 => Property::Cid, - _ => parser.invalid_property()?, - }, - b'd' => match hash { - 0x6e6f_6974_6973_6f70_7369 => Property::Disposition, - 0x6465_7265_7669_6c65 => Property::Delivered, - 0x6465_7961_6c70_7369 => Property::Displayed, - _ => parser.invalid_property()?, - }, - b'e' => match hash { - 0x6c69_616d => Property::Email, - _ => parser.invalid_property()?, - }, - b'h' => match hash { - 0x7372_6564_6165 => Property::Headers, - 0x7469_6d69_4c64_7261 => Property::HardLimit, - _ => parser.invalid_property()?, - }, - b'i' => match hash { - 0x0065_6c62_6f72_5067_6e69_646f_636e_4573 => Property::IsEncodingProblem, - 0x6465_7461_636e_7572_5473 => Property::IsTruncated, - _ => parser.invalid_property()?, - }, - b'l' => match hash { - 0x0065_6761_7567_6e61 => Property::Language, - 0x006e_6f69_7461_636f => Property::Location, - _ => parser.invalid_property()?, - }, - b'm' => match hash { - 0x006d_6f72_466c_6961 => Property::MailFrom, - 0x0073_6d65_7449_6461_6552_7961 => Property::MayReadItems, - 0x736d_6574_4964_6441_7961 => Property::MayAddItems, - 0x0073_6d65_7449_6576_6f6d_6552_7961 => Property::MayRemoveItems, - 0x006e_6565_5374_6553_7961 => Property::MaySetSeen, - 0x0073_6472_6f77_7965_4b74_6553_7961 => Property::MaySetKeywords, - 0x0064_6c69_6843_6574_6165_7243_7961 => Property::MayCreateChild, - 0x656d_616e_6552_7961 => Property::MayRename, - 0x6574_656c_6544_7961 => Property::MayDelete, - 0x7469_6d62_7553_7961 => Property::MaySubmit, - _ => parser.invalid_property()?, - }, - b'n' => match hash { - 0x0065_6d61 => Property::Name, - _ => parser.invalid_property()?, - }, - b'p' => match hash { - 0x0064_4974_7261 => Property::PartId, - 0x0068_6436_3532 => Property::P256dh, - 0x0073_7265_7465_6d61_7261 => Property::Parameters, - _ => parser.invalid_property()?, - }, - b'r' => match hash { - 0x006f_5474_7063 => Property::RcptTo, - 0x0065_7079_5465_6372_756f_7365 => Property::ResourceType, - _ => parser.invalid_property()?, - }, - b's' => match hash { - 0x0065_7a69 => Property::Size, - 0x0073_7472_6150_6275 => Property::SubParts, - 0x796c_7065_5270_746d => Property::SmtpReply, - 0x7469_6d69_4c74_666f => Property::SoftLimit, - 0x6570_6f63 => Property::Scope, - _ => parser.invalid_property()?, - }, - b't' => match hash { - 0x0065_7079 => Property::Type, - _ => parser.invalid_property()?, - }, - b'u' => match hash { - 0x0064_6573 => Property::Used, - _ => parser.invalid_property()?, - }, - b'v' => match hash { - 0x6575_6c61 => Property::Value, - _ => parser.invalid_property()?, - }, - b'w' => match hash { - 0x7469_6d69_4c6e_7261 => Property::WarnLimit, - _ => parser.invalid_property()?, - }, - _ => parser.invalid_property()?, - })) - } -} - -impl Parser<'_> { - fn invalid_property(&mut self) -> trc::Result { - if self.is_eof || self.skip_string() { - Ok(Property::_T( - String::from_utf8_lossy(self.bytes[self.pos_marker..self.pos - 1].as_ref()) - .into_owned(), - )) - } else { - Err(self.error_unterminated()) - } - } -} - -impl Property { - pub fn parse(value: &str) -> Property { - let mut first_char = 0; - let mut hash = 0; - let mut shift = 0; - - for &ch in value.as_bytes() { - if ch.is_ascii_alphabetic() { - if first_char != 0 { - if shift < 128 { - hash |= (ch as u128) << shift; - shift += 8; - } else { - return Property::_T(value.to_string()); - } - } else { - first_char = ch; - } - } else { - return Property::_T(value.to_string()); - } - } - - if let Some(property) = parse_property(first_char, hash) { - property - } else { - Property::_T(value.to_string()) - } - } - - pub fn as_rfc_header(&self) -> HeaderName<'static> { - match self { - Property::MessageId => HeaderName::MessageId, - Property::InReplyTo => HeaderName::InReplyTo, - Property::References => HeaderName::References, - Property::Sender => HeaderName::Sender, - Property::From => HeaderName::From, - Property::To => HeaderName::To, - Property::Cc => HeaderName::Cc, - Property::Bcc => HeaderName::Bcc, - Property::ReplyTo => HeaderName::ReplyTo, - Property::Subject => HeaderName::Subject, - Property::SentAt => HeaderName::Date, - _ => unreachable!(), - } - } -} - -impl Display for Property { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - match self { - Property::Acl => write!(f, "acl"), - Property::Aliases => write!(f, "aliases"), - Property::Attachments => write!(f, "attachments"), - Property::Bcc => write!(f, "bcc"), - Property::BlobId => write!(f, "blobId"), - Property::BodyStructure => write!(f, "bodyStructure"), - Property::BodyValues => write!(f, "bodyValues"), - Property::Capabilities => write!(f, "capabilities"), - Property::Cc => write!(f, "cc"), - Property::Charset => write!(f, "charset"), - Property::Cid => write!(f, "cid"), - Property::DeliveryStatus => write!(f, "deliveryStatus"), - Property::Description => write!(f, "description"), - Property::DeviceClientId => write!(f, "deviceClientId"), - Property::Disposition => write!(f, "disposition"), - Property::DsnBlobIds => write!(f, "dsnBlobIds"), - Property::Email => write!(f, "email"), - Property::EmailId => write!(f, "emailId"), - Property::EmailIds => write!(f, "emailIds"), - Property::Envelope => write!(f, "envelope"), - Property::Expires => write!(f, "expires"), - Property::From => write!(f, "from"), - Property::FromDate => write!(f, "fromDate"), - Property::HasAttachment => write!(f, "hasAttachment"), - Property::Header(p) => write!(f, "{p}"), - Property::Headers => write!(f, "headers"), - Property::HtmlBody => write!(f, "htmlBody"), - Property::HtmlSignature => write!(f, "htmlSignature"), - Property::Id => write!(f, "id"), - Property::IdentityId => write!(f, "identityId"), - Property::InReplyTo => write!(f, "inReplyTo"), - Property::IsActive => write!(f, "isActive"), - Property::IsEnabled => write!(f, "isEnabled"), - Property::IsSubscribed => write!(f, "isSubscribed"), - Property::Keys => write!(f, "keys"), - Property::Keywords => write!(f, "keywords"), - Property::Language => write!(f, "language"), - Property::Location => write!(f, "location"), - Property::MailboxIds => write!(f, "mailboxIds"), - Property::MayDelete => write!(f, "mayDelete"), - Property::MdnBlobIds => write!(f, "mdnBlobIds"), - Property::Members => write!(f, "members"), - Property::MessageId => write!(f, "messageId"), - Property::MyRights => write!(f, "myRights"), - Property::Name => write!(f, "name"), - Property::ParentId => write!(f, "parentId"), - Property::PartId => write!(f, "partId"), - Property::Picture => write!(f, "picture"), - Property::Preview => write!(f, "preview"), - Property::Quota => write!(f, "quota"), - Property::ReceivedAt => write!(f, "receivedAt"), - Property::References => write!(f, "references"), - Property::ReplyTo => write!(f, "replyTo"), - Property::Role => write!(f, "role"), - Property::Secret => write!(f, "secret"), - Property::SendAt => write!(f, "sendAt"), - Property::Sender => write!(f, "sender"), - Property::SentAt => write!(f, "sentAt"), - Property::Size => write!(f, "size"), - Property::SortOrder => write!(f, "sortOrder"), - Property::Subject => write!(f, "subject"), - Property::SubParts => write!(f, "subParts"), - Property::TextBody => write!(f, "textBody"), - Property::TextSignature => write!(f, "textSignature"), - Property::ThreadId => write!(f, "threadId"), - Property::Timezone => write!(f, "timezone"), - Property::To => write!(f, "to"), - Property::ToDate => write!(f, "toDate"), - Property::TotalEmails => write!(f, "totalEmails"), - Property::TotalThreads => write!(f, "totalThreads"), - Property::Type => write!(f, "type"), - Property::Types => write!(f, "types"), - Property::UndoStatus => write!(f, "undoStatus"), - Property::UnreadEmails => write!(f, "unreadEmails"), - Property::UnreadThreads => write!(f, "unreadThreads"), - Property::Url => write!(f, "url"), - Property::VerificationCode => write!(f, "verificationCode"), - Property::Parameters => write!(f, "parameters"), - Property::Addresses => write!(f, "addresses"), - Property::P256dh => write!(f, "p256dh"), - Property::Auth => write!(f, "auth"), - Property::Value => write!(f, "value"), - Property::SmtpReply => write!(f, "smtpReply"), - Property::Delivered => write!(f, "delivered"), - Property::Displayed => write!(f, "displayed"), - Property::MailFrom => write!(f, "mailFrom"), - Property::RcptTo => write!(f, "rcptTo"), - Property::IsEncodingProblem => write!(f, "isEncodingProblem"), - Property::IsTruncated => write!(f, "isTruncated"), - Property::MayReadItems => write!(f, "mayReadItems"), - Property::MayAddItems => write!(f, "mayAddItems"), - Property::MayRemoveItems => write!(f, "mayRemoveItems"), - Property::MaySetSeen => write!(f, "maySetSeen"), - Property::MaySetKeywords => write!(f, "maySetKeywords"), - Property::MayCreateChild => write!(f, "mayCreateChild"), - Property::MayRename => write!(f, "mayRename"), - Property::MaySubmit => write!(f, "maySubmit"), - Property::Data(data) => f.write_str(match data { - DataProperty::AsText => "data:asText", - DataProperty::AsBase64 => "data:asBase64", - DataProperty::Default => "data", - }), - Property::Digest(digest) => f.write_str(match digest { - DigestProperty::Sha => "digest:sha", - DigestProperty::Sha256 => "digest:sha-256", - DigestProperty::Sha512 => "digest:sha-512", - }), - Property::ResourceType => write!(f, "resourceType"), - Property::Used => write!(f, "used"), - Property::HardLimit => write!(f, "hardLimit"), - Property::Scope => write!(f, "scope"), - Property::WarnLimit => write!(f, "warnLimit"), - Property::SoftLimit => write!(f, "softLimit"), - Property::_T(s) => write!(f, "{s}"), - } - } -} - -impl Property { - pub fn as_str(&self) -> &str { - match self { - Property::Acl => "acl", - Property::Aliases => "aliases", - Property::Attachments => "attachments", - Property::Bcc => "bcc", - Property::BlobId => "blobId", - Property::BodyStructure => "bodyStructure", - Property::BodyValues => "bodyValues", - Property::Capabilities => "capabilities", - Property::Cc => "cc", - Property::Charset => "charset", - Property::Cid => "cid", - Property::DeliveryStatus => "deliveryStatus", - Property::Description => "description", - Property::DeviceClientId => "deviceClientId", - Property::Disposition => "disposition", - Property::DsnBlobIds => "dsnBlobIds", - Property::Email => "email", - Property::EmailId => "emailId", - Property::EmailIds => "emailIds", - Property::Envelope => "envelope", - Property::Expires => "expires", - Property::From => "from", - Property::FromDate => "fromDate", - Property::HasAttachment => "hasAttachment", - Property::Header(_) => "header", - Property::Headers => "headers", - Property::HtmlBody => "htmlBody", - Property::HtmlSignature => "htmlSignature", - Property::Id => "id", - Property::IdentityId => "identityId", - Property::InReplyTo => "inReplyTo", - Property::IsActive => "isActive", - Property::IsEnabled => "isEnabled", - Property::IsSubscribed => "isSubscribed", - Property::Keys => "keys", - Property::Keywords => "keywords", - Property::Language => "language", - Property::Location => "location", - Property::MailboxIds => "mailboxIds", - Property::MayDelete => "mayDelete", - Property::MdnBlobIds => "mdnBlobIds", - Property::Members => "members", - Property::MessageId => "messageId", - Property::MyRights => "myRights", - Property::Name => "name", - Property::ParentId => "parentId", - Property::PartId => "partId", - Property::Picture => "picture", - Property::Preview => "preview", - Property::Quota => "quota", - Property::ReceivedAt => "receivedAt", - Property::References => "references", - Property::ReplyTo => "replyTo", - Property::Role => "role", - Property::Secret => "secret", - Property::SendAt => "sendAt", - Property::Sender => "sender", - Property::SentAt => "sentAt", - Property::Size => "size", - Property::SortOrder => "sortOrder", - Property::Subject => "subject", - Property::SubParts => "subParts", - Property::TextBody => "textBody", - Property::TextSignature => "textSignature", - Property::ThreadId => "threadId", - Property::Timezone => "timezone", - Property::To => "to", - Property::ToDate => "toDate", - Property::TotalEmails => "totalEmails", - Property::TotalThreads => "totalThreads", - Property::Type => "type", - Property::Types => "types", - Property::UndoStatus => "undoStatus", - Property::UnreadEmails => "unreadEmails", - Property::UnreadThreads => "unreadThreads", - Property::Url => "url", - Property::VerificationCode => "verificationCode", - Property::Parameters => "parameters", - Property::Addresses => "addresses", - Property::P256dh => "p256dh", - Property::Auth => "auth", - Property::Value => "value", - Property::SmtpReply => "smtpReply", - Property::Delivered => "delivered", - Property::Displayed => "displayed", - Property::MailFrom => "mailFrom", - Property::RcptTo => "rcptTo", - Property::IsEncodingProblem => "isEncodingProblem", - Property::IsTruncated => "isTruncated", - Property::MayReadItems => "mayReadItems", - Property::MayAddItems => "mayAddItems", - Property::MayRemoveItems => "mayRemoveItems", - Property::MaySetSeen => "maySetSeen", - Property::MaySetKeywords => "maySetKeywords", - Property::MayCreateChild => "mayCreateChild", - Property::MayRename => "mayRename", - Property::MaySubmit => "maySubmit", - Property::ResourceType => "resourceType", - Property::Used => "used", - Property::HardLimit => "hardLimit", - Property::WarnLimit => "warnLimit", - Property::SoftLimit => "softLimit", - Property::Scope => "scope", - Property::Data(data) => match data { - DataProperty::AsText => "data:asText", - DataProperty::AsBase64 => "data:asBase64", - DataProperty::Default => "data", - }, - Property::Digest(digest) => match digest { - DigestProperty::Sha => "digest:sha", - DigestProperty::Sha256 => "digest:sha-256", - DigestProperty::Sha512 => "digest:sha-512", - }, - Property::_T(s) => s, - } - } -} - -impl Display for SetProperty { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - self.property.fmt(f) - } -} - -impl Display for ObjectProperty { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - self.0.fmt(f) - } -} - -impl IntoProperty for ObjectProperty { - fn into_property(self) -> Property { - self.0 - } -} - -impl IntoProperty for String { - fn into_property(self) -> Property { - Property::_T(self) - } -} - -#[derive(Debug, PartialEq, Eq, Hash, Clone)] -pub struct HeaderProperty { - pub form: HeaderForm, - pub header: String, - pub all: bool, -} - -#[derive(Debug, PartialEq, Eq, Hash, Clone, Copy)] -pub enum HeaderForm { - Raw, - Text, - Addresses, - GroupedAddresses, - MessageIds, - Date, - URLs, -} - -impl Display for HeaderProperty { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "header:{}", self.header)?; - self.form.fmt(f)?; - if self.all { write!(f, ":all") } else { Ok(()) } - } -} - -impl Display for HeaderForm { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - HeaderForm::Raw => Ok(()), - HeaderForm::Text => write!(f, ":asText"), - HeaderForm::Addresses => write!(f, ":asAddresses"), - HeaderForm::GroupedAddresses => write!(f, ":asGroupedAddresses"), - HeaderForm::MessageIds => write!(f, ":asMessageIds"), - HeaderForm::Date => write!(f, ":asDate"), - HeaderForm::URLs => write!(f, ":asURLs"), - } - } -} - -impl Property { - pub fn from_header(header: &HeaderName) -> Self { - match header { - HeaderName::Subject => Property::Subject, - HeaderName::From => Property::From, - HeaderName::To => Property::To, - HeaderName::Cc => Property::Cc, - HeaderName::Date => Property::SentAt, - HeaderName::Bcc => Property::Bcc, - HeaderName::ReplyTo => Property::ReplyTo, - HeaderName::Sender => Property::Sender, - HeaderName::InReplyTo => Property::InReplyTo, - HeaderName::MessageId => Property::MessageId, - HeaderName::References => Property::References, - HeaderName::ResentMessageId => Property::EmailIds, - _ => unreachable!(), - } - } -} - -impl Serialize for Property { - fn serialize(&self, serializer: S) -> Result - where - S: serde::Serializer, - { - serializer.serialize_str(&self.to_string()) - } -} - -impl AsRef for Property { - fn as_ref(&self) -> &Property { - self - } -} diff --git a/crates/jmap-proto/src/types/state.rs b/crates/jmap-proto/src/types/state.rs index d515eced..c18a1d9a 100644 --- a/crates/jmap-proto/src/types/state.rs +++ b/crates/jmap-proto/src/types/state.rs @@ -4,10 +4,9 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use crate::parser::{JsonObjectParser, base32::JsonBase32Reader, json::Parser}; use types::ChangeId; use utils::codec::{ - base32_custom::Base32Writer, + base32_custom::{Base32Reader, Base32Writer}, leb128::{Leb128Iterator, Leb128Writer}, }; @@ -41,25 +40,18 @@ impl From> for State { } } -impl JsonObjectParser for State { - fn parse(parser: &mut Parser<'_>) -> trc::Result - where - Self: Sized, - { - match parser - .next_unescaped()? - .ok_or_else(|| parser.error_value())? - { +impl State { + pub fn parse(value: &str) -> Option { + let mut it = value.as_bytes().iter(); + + match it.next()? { b'n' => Ok(State::Initial), b's' => { - let mut reader = JsonBase32Reader::new(parser); - reader - .next_leb128::() - .map(State::Exact) - .ok_or_else(|| parser.error_value()) + let mut reader = Base32Reader::new(it); + reader.next_leb128::().map(State::Exact) } b'r' => { - let mut it = JsonBase32Reader::new(parser); + let mut it = Base32Reader::new(it); if let (Some(from_id), Some(to_id), Some(items_sent)) = ( it.next_leb128::(), @@ -73,18 +65,16 @@ impl JsonObjectParser for State { items_sent, })) } else { - Err(parser.error_value()) + None } } else { - Err(parser.error_value()) + None } } - _ => Err(parser.error_value()), + _ => None, } } -} -impl State { pub fn new_initial() -> Self { State::Initial } @@ -124,10 +114,8 @@ impl<'de> serde::Deserialize<'de> for State { where D: serde::Deserializer<'de>, { - // This is inefficient, but serde deserialize on State is only used in test mode - let value = format!("{}\"", <&str>::deserialize(deserializer)?); - let mut parser = Parser::new(value.as_bytes()); - State::parse(&mut parser).map_err(|_| serde::de::Error::custom("invalid JMAP State")) + State::parse(<&str>::deserialize(deserializer)?) + .map_err(|_| serde::de::Error::custom("invalid JMAP State")) } } @@ -160,7 +148,6 @@ impl std::fmt::Display for State { #[cfg(test)] mod tests { use super::State; - use crate::parser::json::Parser; use types::ChangeId; #[test] @@ -179,14 +166,7 @@ mod tests { State::new_intermediate(12345678, 87654321, 12345678), State::new_intermediate(ChangeId::MAX, ChangeId::MAX, ChangeId::MAX as usize), ] { - assert_eq!( - Parser::new(format!("\"{id}\"").as_bytes()) - .next_token::() - .unwrap() - .unwrap_string("") - .unwrap(), - id - ); + assert_eq!(State::parse(&id.to_string()).unwrap(), id); } } } diff --git a/crates/jmap-proto/src/types/type_state.rs b/crates/jmap-proto/src/types/type_state.rs deleted file mode 100644 index 077f4629..00000000 --- a/crates/jmap-proto/src/types/type_state.rs +++ /dev/null @@ -1,44 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC - * - * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL - */ - -use crate::parser::{JsonObjectParser, json::Parser}; -use types::type_state::DataType; - -impl JsonObjectParser for DataType { - fn parse(parser: &mut Parser<'_>) -> trc::Result - where - Self: Sized, - { - let mut hash = 0; - let mut shift = 0; - - while let Some(ch) = parser.next_unescaped()? { - if shift < 128 { - hash |= (ch as u128) << shift; - shift += 8; - } else { - return Err(parser.error_value()); - } - } - - match hash { - 0x006c_6961_6d45 => Ok(DataType::Email), - 0x0079_7265_7669_6c65_446c_6961_6d45 => Ok(DataType::EmailDelivery), - 0x006e_6f69_7373_696d_6275_536c_6961_6d45 => Ok(DataType::EmailSubmission), - 0x0078_6f62_6c69_614d => Ok(DataType::Mailbox), - 0x6461_6572_6854 => Ok(DataType::Thread), - 0x7974_6974_6e65_6449 => Ok(DataType::Identity), - 0x6572_6f43 => Ok(DataType::Core), - 0x6e6f_6974_7069_7263_7362_7553_6873_7550 => Ok(DataType::PushSubscription), - 0x0074_6570_7069_6e53_6863_7261_6553 => Ok(DataType::SearchSnippet), - 0x6573_6e6f_7073_6552_6e6f_6974_6163_6156 => Ok(DataType::VacationResponse), - 0x004e_444d => Ok(DataType::Mdn), - 0x0061_746f_7551 => Ok(DataType::Quota), - 0x0074_7069_7263_5365_7665_6953 => Ok(DataType::SieveScript), - _ => Err(parser.error_value()), - } - } -} diff --git a/crates/jmap-proto/src/types/value.rs b/crates/jmap-proto/src/types/value.rs deleted file mode 100644 index ed1dcb16..00000000 --- a/crates/jmap-proto/src/types/value.rs +++ /dev/null @@ -1,594 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC - * - * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL - */ - -use super::{ - any_id::AnyId, - date::UTCDate, - property::{HeaderForm, IntoProperty, ObjectProperty, Property}, -}; -use crate::{ - parser::{Ignore, JsonObjectParser, Token, json::Parser}, - request::reference::{MaybeReference, ResultReference}, -}; -use mail_parser::{Addr, DateTime, Group}; -use rkyv::{option::ArchivedOption, string::ArchivedString}; -use serde::Serialize; -use std::{borrow::Cow, fmt::Display}; -use types::{acl::AclGrant, blob::BlobId, id::Id, keyword::Keyword}; -use utils::{ - json::{JsonPointerItem, JsonQueryable}, - map::vec_map::VecMap, -}; - -#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize)] -#[serde(untagged)] -pub enum Value { - Text(String), - UnsignedInt(u64), - Bool(bool), - Id(Id), - Date(UTCDate), - BlobId(BlobId), - Keyword(Keyword), - List(Vec), - Object(Object), - Acl(Vec), - Blob(Vec), - #[default] - Null, -} - -#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize)] -pub struct Object(pub VecMap); - -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum SetValue { - Value(Value), - Patch(Vec), - IdReference(MaybeReference), - IdReferences(Vec>), - ResultReference(ResultReference), -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum MaybePatchValue { - Value(Value), - Patch(Vec), -} - -#[derive(Debug, Clone)] -pub struct SetValueMap { - pub values: Vec, -} - -pub trait IntoValue: Eq { - fn into_value(self) -> Value; -} - -impl Value { - pub fn parse( - token: Token, - parser: &mut Parser<'_>, - ) -> trc::Result { - Ok(match token { - Token::String(v) => v.into_value(), - Token::DictStart => { - let mut properties = VecMap::with_capacity(4); - while let Some(key) = parser.next_dict_key::()? { - let property = key.into_property(); - let value = Value::from_property(parser, &property)?; - properties.append(property, value); - } - Value::Object(Object(properties)) - } - Token::ArrayStart => { - let mut values = Vec::with_capacity(4); - loop { - match parser.next_token::()? { - Token::Comma => (), - Token::ArrayEnd => break, - token => { - values.push(Value::parse::(token, parser)?); - } - } - } - Value::List(values) - } - Token::Integer(v) => Value::UnsignedInt(std::cmp::max(v, 0) as u64), - Token::Float(v) => Value::UnsignedInt(if v > 0.0 { v as u64 } else { 0 }), - Token::Boolean(v) => Value::Bool(v), - Token::Null => Value::Null, - token => return Err(token.error("", "value")), - }) - } - - pub fn from_property(parser: &mut Parser<'_>, property: &Property) -> trc::Result { - match &property { - Property::BlobId => Ok(parser - .next_token::()? - .unwrap_string_or_null("")? - .map(Value::BlobId) - .unwrap_or(Value::Null)), - Property::Size => Ok(parser - .next_token::()? - .unwrap_uint_or_null("")? - .map(Value::UnsignedInt) - .unwrap_or(Value::Null)), - Property::PartId - | Property::Name - | Property::Email - | Property::Type - | Property::Charset - | Property::Cid - | Property::Disposition - | Property::Location - | Property::Value - | Property::SmtpReply - | Property::P256dh - | Property::Delivered - | Property::Displayed - | Property::Auth => Ok(parser - .next_token::()? - .unwrap_string_or_null("")? - .map(Value::Text) - .unwrap_or(Value::Null)), - - Property::Header(h) => { - if matches!(h.form, HeaderForm::Date) { - Value::parse::(parser.next_token()?, parser) - } else { - Value::parse::(parser.next_token()?, parser) - } - } - - Property::Headers - | Property::Addresses - | Property::MailFrom - | Property::RcptTo - | Property::SubParts => { - Value::parse::(parser.next_token()?, parser) - } - Property::Language | Property::Parameters => { - Value::parse::(parser.next_token()?, parser) - } - - Property::IsEncodingProblem - | Property::IsTruncated - | Property::MayReadItems - | Property::MayAddItems - | Property::MayRemoveItems - | Property::MaySetSeen - | Property::MaySetKeywords - | Property::MayCreateChild - | Property::MayRename - | Property::MayDelete - | Property::MaySubmit => Ok(parser - .next_token::()? - .unwrap_bool_or_null("")? - .map(Value::Bool) - .unwrap_or(Value::Null)), - _ => Value::parse::(parser.next_token()?, parser), - } - } - - pub fn try_unwrap_id(self) -> Option { - match self { - Value::Id(id) => id.into(), - _ => None, - } - } - - pub fn try_unwrap_bool(self) -> Option { - match self { - Value::Bool(b) => b.into(), - _ => None, - } - } - - pub fn try_unwrap_keyword(self) -> Option { - match self { - Value::Keyword(k) => k.into(), - _ => None, - } - } - - pub fn try_unwrap_string(self) -> Option { - match self { - Value::Text(s) => Some(s), - _ => None, - } - } - - pub fn try_unwrap_object(self) -> Option> { - match self { - Value::Object(o) => Some(o), - _ => None, - } - } - - pub fn try_unwrap_list(self) -> Option> { - match self { - Value::List(l) => Some(l), - _ => None, - } - } - - pub fn try_unwrap_date(self) -> Option { - match self { - Value::Date(d) => Some(d), - _ => None, - } - } - - pub fn try_unwrap_blob_id(self) -> Option { - match self { - Value::BlobId(b) => Some(b), - _ => None, - } - } - - pub fn try_unwrap_uint(self) -> Option { - match self { - Value::UnsignedInt(u) => Some(u), - _ => None, - } - } - - pub fn as_string(&self) -> Option<&str> { - match self { - Value::Text(s) => Some(s), - _ => None, - } - } - - pub fn as_id(&self) -> Option<&Id> { - match self { - Value::Id(id) => Some(id), - _ => None, - } - } - - pub fn as_blob_id(&self) -> Option<&BlobId> { - match self { - Value::BlobId(id) => Some(id), - _ => None, - } - } - - pub fn as_list(&self) -> Option<&Vec> { - match self { - Value::List(l) => Some(l), - _ => None, - } - } - - pub fn as_acl(&self) -> Option<&Vec> { - match self { - Value::Acl(l) => Some(l), - _ => None, - } - } - - pub fn as_uint(&self) -> Option { - match self { - Value::UnsignedInt(u) => Some(*u), - Value::Id(id) => Some(*id.as_ref()), - _ => None, - } - } - - pub fn as_bool(&self) -> Option { - match self { - Value::Bool(b) => Some(*b), - _ => None, - } - } - - pub fn as_date(&self) -> Option<&UTCDate> { - match self { - Value::Date(d) => Some(d), - _ => None, - } - } - - pub fn as_obj(&self) -> Option<&Object> { - match self { - Value::Object(o) => Some(o), - _ => None, - } - } - - pub fn as_obj_mut(&mut self) -> Option<&mut Object> { - match self { - Value::Object(o) => Some(o), - _ => None, - } - } - - pub fn try_cast_uint(&self) -> Option { - match self { - Value::UnsignedInt(u) => Some(*u), - Value::Id(id) => Some(id.id()), - Value::Bool(b) => Some(*b as u64), - _ => None, - } - } -} - -impl JsonObjectParser for SetValueMap { - fn parse(parser: &mut Parser<'_>) -> trc::Result - where - Self: Sized, - { - let mut values = Vec::new(); - match parser.next_token::()? { - Token::DictStart => { - while let Some(value) = parser.next_dict_key()? { - if bool::parse(parser)? { - values.push(value); - } - } - } - Token::Null => (), - token => return Err(token.error("", &token.to_string())), - } - Ok(SetValueMap { values }) - } -} - -impl IntoValue for String { - fn into_value(self) -> Value { - Value::Text(self) - } -} - -impl IntoValue for Id { - fn into_value(self) -> Value { - Value::Id(self) - } -} - -impl IntoValue for UTCDate { - fn into_value(self) -> Value { - Value::Date(self) - } -} - -impl From for Value { - fn from(value: usize) -> Self { - Value::UnsignedInt(value as u64) - } -} - -impl From for Value { - fn from(value: u64) -> Self { - Value::UnsignedInt(value) - } -} - -impl From for Value { - fn from(value: u32) -> Self { - Value::UnsignedInt(value as u64) - } -} - -impl From for Value { - fn from(value: String) -> Self { - Value::Text(value) - } -} - -impl From<&str> for Value { - fn from(value: &str) -> Self { - Value::Text(value.to_string()) - } -} - -impl From for Value { - fn from(value: bool) -> Self { - Value::Bool(value) - } -} - -impl From for Value { - fn from(value: Keyword) -> Self { - Value::Keyword(value) - } -} - -impl From> for Value { - fn from(value: Object) -> Self { - Value::Object(value) - } -} - -impl From for Value { - fn from(value: BlobId) -> Self { - Value::BlobId(value) - } -} - -impl From for Value { - fn from(value: Id) -> Self { - Value::Id(value) - } -} - -impl From for Value { - fn from(date: DateTime) -> Self { - Value::Date(UTCDate { - year: date.year, - month: date.month, - day: date.day, - hour: date.hour, - minute: date.minute, - second: date.second, - tz_before_gmt: date.tz_before_gmt, - tz_hour: date.tz_hour, - tz_minute: date.tz_minute, - }) - } -} - -impl From for Value { - fn from(date: UTCDate) -> Self { - Value::Date(date) - } -} - -impl From> for Value { - fn from(value: Cow<'_, str>) -> Self { - Value::Text(value.into_owned()) - } -} - -impl From<&ArchivedString> for Value { - fn from(value: &ArchivedString) -> Self { - Value::Text(value.to_string()) - } -} - -impl> From> for Value { - fn from(value: Vec) -> Self { - Value::List(value.into_iter().map(|v| v.into()).collect()) - } -} - -impl> From> for Value { - fn from(value: Option) -> Self { - match value { - Some(value) => value.into(), - None => Value::Null, - } - } -} - -impl From<&ArchivedOption> for Value { - fn from(value: &ArchivedOption) -> Self { - match value { - ArchivedOption::Some(value) => Value::Text(value.to_string()), - ArchivedOption::None => Value::Null, - } - } -} - -impl From<&ArchivedOption> for Value { - fn from(value: &ArchivedOption) -> Self { - match value { - ArchivedOption::Some(value) => Value::UnsignedInt(u32::from(value) as u64), - ArchivedOption::None => Value::Null, - } - } -} - -impl From<&rkyv::rend::u32_le> for Value { - fn from(value: &rkyv::rend::u32_le) -> Self { - Value::UnsignedInt(u32::from(value) as u64) - } -} - -impl From> for Value { - fn from(value: Addr<'_>) -> Self { - Value::Object(Object( - VecMap::with_capacity(2) - .with_append(Property::Name, Value::from(value.name)) - .with_append( - Property::Email, - Value::from(value.address.unwrap_or_default()), - ), - )) - } -} - -impl From> for Value { - fn from(group: Group<'_>) -> Self { - Value::Object(Object( - VecMap::with_capacity(2) - .with_append(Property::Name, Value::from(group.name)) - .with_append( - Property::Addresses, - Value::List( - group - .addresses - .into_iter() - .map(Value::from) - .collect::>(), - ), - ), - )) - } -} - -impl Object { - pub fn with_capacity(capacity: usize) -> Self { - Self(VecMap::with_capacity(capacity)) - } - - pub fn set(&mut self, property: Property, value: impl Into) -> bool { - self.0.set(property, value.into()) - } - - pub fn append(&mut self, property: Property, value: impl Into) { - self.0.append(property, value.into()); - } - - pub fn with_property(mut self, property: Property, value: impl Into) -> Self { - self.0.append(property, value.into()); - self - } - - pub fn remove(&mut self, property: &Property) -> Value { - self.0.remove(property).unwrap_or(Value::Null) - } - - pub fn get(&self, property: &Property) -> &Value { - self.0.get(property).unwrap_or(&Value::Null) - } -} - -impl JsonQueryable for Value { - fn eval_pointer<'x>( - &'x self, - mut pointer: std::slice::Iter, - results: &mut Vec<&'x dyn JsonQueryable>, - ) { - match pointer.next() { - Some(JsonPointerItem::String(n)) => { - if let Value::Object(map) = self - && let Some(v) = map - .0 - .iter() - .find_map(|(k, v)| if k.as_str() == n { Some(v) } else { None }) - { - v.eval_pointer(pointer, results); - } - } - Some(JsonPointerItem::Number(n)) => { - if let Value::List(values) = self - && let Some(v) = values.get(*n as usize) - { - v.eval_pointer(pointer, results); - } - } - Some(JsonPointerItem::Wildcard) => match self { - Value::List(values) => { - for v in values { - v.eval_pointer(pointer.clone(), results); - } - } - Value::Object(map) => { - for v in map.0.values() { - v.eval_pointer(pointer.clone(), results); - } - } - _ => {} - }, - Some(JsonPointerItem::Root) | None => { - results.push(self); - } - } - } -} diff --git a/crates/types/src/blob.rs b/crates/types/src/blob.rs index 04b8e34d..c7637b0e 100644 --- a/crates/types/src/blob.rs +++ b/crates/types/src/blob.rs @@ -4,7 +4,7 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use std::{borrow::Borrow, time::SystemTime}; +use std::{borrow::Borrow, str::FromStr, time::SystemTime}; use utils::codec::{ base32_custom::{Base32Reader, Base32Writer}, leb128::{Leb128Iterator, Leb128Writer}, @@ -79,6 +79,14 @@ pub struct BlobSection { pub encoding: u8, } +impl FromStr for BlobId { + type Err = (); + + fn from_str(s: &str) -> Result { + BlobId::from_base32(s).ok_or(()) + } +} + impl BlobId { pub fn new(hash: BlobHash, class: BlobClass) -> Self { BlobId { @@ -112,12 +120,13 @@ impl BlobId { self } + #[inline] pub fn from_base32(value: impl AsRef<[u8]>) -> Option { BlobId::from_iter(&mut Base32Reader::new(value.as_ref())) } #[allow(clippy::should_implement_trait)] - pub fn from_iter(it: &mut T) -> Option + fn from_iter(it: &mut T) -> Option where T: Iterator + Leb128Iterator, U: Borrow, diff --git a/crates/types/src/id.rs b/crates/types/src/id.rs index 3b4d9bf6..79c21431 100644 --- a/crates/types/src/id.rs +++ b/crates/types/src/id.rs @@ -5,7 +5,7 @@ */ use crate::DocumentId; -use std::ops::Deref; +use std::{ops::Deref, str::FromStr}; use utils::codec::base32_custom::{BASE32_ALPHABET, BASE32_INVERSE}; #[derive(Debug, Clone, PartialEq, Eq, Hash, Copy)] @@ -18,24 +18,28 @@ impl Default for Id { } } -impl Id { - pub fn new(id: u64) -> Self { - Self(id) - } +impl FromStr for Id { + type Err = (); - pub fn from_bytes(bytes: &[u8]) -> Option { + fn from_str(s: &str) -> Result { let mut id = 0; - for &ch in bytes { + for &ch in s.as_bytes() { let i = BASE32_INVERSE[ch as usize]; if i != u8::MAX { id = (id << 5) | i as u64; } else { - return None; + return Err(()); } } - Id(id).into() + Ok(Id(id)) + } +} + +impl Id { + pub fn new(id: u64) -> Self { + Self(id) } pub fn singleton() -> Self { @@ -190,8 +194,8 @@ impl<'de> serde::Deserialize<'de> for Id { where D: serde::Deserializer<'de>, { - Id::from_bytes(<&str>::deserialize(deserializer)?.as_bytes()) - .ok_or_else(|| serde::de::Error::custom("invalid JMAP ID")) + Id::from_str(<&str>::deserialize(deserializer)?) + .map_err(|_| serde::de::Error::custom("invalid JMAP ID")) } } @@ -200,3 +204,29 @@ impl std::fmt::Display for Id { f.write_str(&self.as_string()) } } + +#[cfg(test)] +mod tests { + use std::str::FromStr; + + use crate::id::Id; + + #[test] + fn parse_jmap_id() { + for number in [ + 0, + 1, + 10, + 1000, + Id::singleton().id(), + u64::MAX / 2, + u64::MAX - 1, + u64::MAX, + ] { + let id = Id::from(number); + assert_eq!(Id::from_str(&id.to_string()).unwrap(), id); + } + + Id::from_str("p333333333333p333333333333").unwrap(); + } +} diff --git a/crates/types/src/keyword.rs b/crates/types/src/keyword.rs index a7702f6a..9a3542e4 100644 --- a/crates/types/src/keyword.rs +++ b/crates/types/src/keyword.rs @@ -63,44 +63,82 @@ pub enum Keyword { Other(String), } -impl> From for Keyword { - fn from(value: T) -> Self { - let value = value.as_ref(); - if value - .as_bytes() - .first() - .is_some_and(|&ch| [b'$', b'\\'].contains(&ch)) - { - let mut hash = 0; - let mut shift = 0; +impl Keyword { + pub fn parse(value: &str) -> Self { + value + .split_at_checked(1) + .filter(|(prefix, _)| matches!(*prefix, "$" | "\\")) + .and_then(|(_, rest)| { + hashify::tiny_map_ignore_case!(rest.as_bytes(), + "seen" => Keyword::Seen, + "draft" => Keyword::Draft, + "flagged" => Keyword::Flagged, + "answered" => Keyword::Answered, + "recent" => Keyword::Recent, + "important" => Keyword::Important, + "phishing" => Keyword::Phishing, + "junk" => Keyword::Junk, + "notjunk" => Keyword::NotJunk, + "deleted" => Keyword::Deleted, + "forwarded" => Keyword::Forwarded, + "mdnsent" => Keyword::MdnSent + ) + }) + .unwrap_or_else(|| Keyword::Other(value.to_string())) + } - for &ch in value.as_bytes().iter().skip(1) { - if shift < 128 { - hash |= (ch.to_ascii_lowercase() as u128) << shift; - shift += 8; - } else { - break; - } - } - - match hash { - 0x6e65_6573 => return Keyword::Seen, - 0x0074_6661_7264 => return Keyword::Draft, - 0x0064_6567_6761_6c66 => return Keyword::Flagged, - 0x6465_7265_7773_6e61 => return Keyword::Answered, - 0x746e_6563_6572 => return Keyword::Recent, - 0x0074_6e61_7472_6f70_6d69 => return Keyword::Important, - 0x676e_6968_7369_6870 => return Keyword::Phishing, - 0x6b6e_756a => return Keyword::Junk, - 0x006b_6e75_6a74_6f6e => return Keyword::NotJunk, - 0x0064_6574_656c_6564 => return Keyword::Deleted, - 0x0064_6564_7261_7772_6f66 => return Keyword::Forwarded, - 0x0074_6e65_736e_646d => return Keyword::MdnSent, - _ => (), - } + pub fn id(&self) -> Result { + match self { + Keyword::Seen => Ok(SEEN as u32), + Keyword::Draft => Ok(DRAFT as u32), + Keyword::Flagged => Ok(FLAGGED as u32), + Keyword::Answered => Ok(ANSWERED as u32), + Keyword::Recent => Ok(RECENT as u32), + Keyword::Important => Ok(IMPORTANT as u32), + Keyword::Phishing => Ok(PHISHING as u32), + Keyword::Junk => Ok(JUNK as u32), + Keyword::NotJunk => Ok(NOTJUNK as u32), + Keyword::Deleted => Ok(DELETED as u32), + Keyword::Forwarded => Ok(FORWARDED as u32), + Keyword::MdnSent => Ok(MDN_SENT as u32), + Keyword::Other(string) => Err(string.as_str()), } + } - Keyword::Other(String::from(value)) + pub fn into_id(self) -> Result { + match self { + Keyword::Seen => Ok(SEEN as u32), + Keyword::Draft => Ok(DRAFT as u32), + Keyword::Flagged => Ok(FLAGGED as u32), + Keyword::Answered => Ok(ANSWERED as u32), + Keyword::Recent => Ok(RECENT as u32), + Keyword::Important => Ok(IMPORTANT as u32), + Keyword::Phishing => Ok(PHISHING as u32), + Keyword::Junk => Ok(JUNK as u32), + Keyword::NotJunk => Ok(NOTJUNK as u32), + Keyword::Deleted => Ok(DELETED as u32), + Keyword::Forwarded => Ok(FORWARDED as u32), + Keyword::MdnSent => Ok(MDN_SENT as u32), + Keyword::Other(string) => Err(string), + } + } + + pub fn try_from_id(id: usize) -> Result { + match id { + SEEN => Ok(Keyword::Seen), + DRAFT => Ok(Keyword::Draft), + FLAGGED => Ok(Keyword::Flagged), + ANSWERED => Ok(Keyword::Answered), + RECENT => Ok(Keyword::Recent), + IMPORTANT => Ok(Keyword::Important), + PHISHING => Ok(Keyword::Phishing), + JUNK => Ok(Keyword::Junk), + NOTJUNK => Ok(Keyword::NotJunk), + DELETED => Ok(Keyword::Deleted), + FORWARDED => Ok(Keyword::Forwarded), + MDN_SENT => Ok(Keyword::MdnSent), + _ => Err(id), + } } } @@ -164,62 +202,6 @@ impl From for Vec { } } -impl Keyword { - pub fn id(&self) -> Result { - match self { - Keyword::Seen => Ok(SEEN as u32), - Keyword::Draft => Ok(DRAFT as u32), - Keyword::Flagged => Ok(FLAGGED as u32), - Keyword::Answered => Ok(ANSWERED as u32), - Keyword::Recent => Ok(RECENT as u32), - Keyword::Important => Ok(IMPORTANT as u32), - Keyword::Phishing => Ok(PHISHING as u32), - Keyword::Junk => Ok(JUNK as u32), - Keyword::NotJunk => Ok(NOTJUNK as u32), - Keyword::Deleted => Ok(DELETED as u32), - Keyword::Forwarded => Ok(FORWARDED as u32), - Keyword::MdnSent => Ok(MDN_SENT as u32), - Keyword::Other(string) => Err(string.as_str()), - } - } - - pub fn into_id(self) -> Result { - match self { - Keyword::Seen => Ok(SEEN as u32), - Keyword::Draft => Ok(DRAFT as u32), - Keyword::Flagged => Ok(FLAGGED as u32), - Keyword::Answered => Ok(ANSWERED as u32), - Keyword::Recent => Ok(RECENT as u32), - Keyword::Important => Ok(IMPORTANT as u32), - Keyword::Phishing => Ok(PHISHING as u32), - Keyword::Junk => Ok(JUNK as u32), - Keyword::NotJunk => Ok(NOTJUNK as u32), - Keyword::Deleted => Ok(DELETED as u32), - Keyword::Forwarded => Ok(FORWARDED as u32), - Keyword::MdnSent => Ok(MDN_SENT as u32), - Keyword::Other(string) => Err(string), - } - } - - pub fn try_from_id(id: usize) -> Result { - match id { - SEEN => Ok(Keyword::Seen), - DRAFT => Ok(Keyword::Draft), - FLAGGED => Ok(Keyword::Flagged), - ANSWERED => Ok(Keyword::Answered), - RECENT => Ok(Keyword::Recent), - IMPORTANT => Ok(Keyword::Important), - PHISHING => Ok(Keyword::Phishing), - JUNK => Ok(Keyword::Junk), - NOTJUNK => Ok(Keyword::NotJunk), - DELETED => Ok(Keyword::Deleted), - FORWARDED => Ok(Keyword::Forwarded), - MDN_SENT => Ok(Keyword::MdnSent), - _ => Err(id), - } - } -} - impl ArchivedKeyword { pub fn id(&self) -> Result { match self { @@ -240,33 +222,6 @@ impl ArchivedKeyword { } } -/*impl From for TagValue { - fn from(value: Keyword) -> Self { - match value.into_id() { - Ok(id) => TagValue::Id(id), - Err(string) => TagValue::Text(string.as_bytes().to_vec()), - } - } -} - -impl From<&Keyword> for TagValue { - fn from(value: &Keyword) -> Self { - match value.id() { - Ok(id) => TagValue::Id(id), - Err(string) => TagValue::Text(string.as_bytes().to_vec()), - } - } -} - -impl From<&ArchivedKeyword> for TagValue { - fn from(value: &ArchivedKeyword) -> Self { - match value.id() { - Ok(id) => TagValue::Id(id), - Err(string) => TagValue::Text(string.as_bytes().to_vec()), - } - } -}*/ - impl From<&ArchivedKeyword> for Keyword { fn from(value: &ArchivedKeyword) -> Self { match value { diff --git a/crates/types/src/lib.rs b/crates/types/src/lib.rs index 5d5a9b2f..161feca6 100644 --- a/crates/types/src/lib.rs +++ b/crates/types/src/lib.rs @@ -12,6 +12,7 @@ pub mod field; pub mod id; pub mod keyword; pub mod semver; +pub mod special_use; pub mod type_state; pub type DocumentId = u32; diff --git a/crates/types/src/special_use.rs b/crates/types/src/special_use.rs new file mode 100644 index 00000000..75bd09a9 --- /dev/null +++ b/crates/types/src/special_use.rs @@ -0,0 +1,82 @@ +/* + * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL + */ + +#[derive( + rkyv::Archive, rkyv::Deserialize, rkyv::Serialize, Clone, Copy, PartialEq, Eq, Hash, Debug, +)] +#[rkyv(derive(Debug))] +pub enum SpecialUse { + Inbox, + Trash, + Junk, + Drafts, + Archive, + Sent, + Shared, + Important, + None, +} + +impl SpecialUse { + pub fn parse(s: &str) -> Option { + hashify::tiny_map_ignore_case!(s.as_bytes(), + b"inbox" => SpecialUse::Inbox, + b"trash" => SpecialUse::Trash, + b"junk" => SpecialUse::Junk, + b"drafts" => SpecialUse::Drafts, + b"archive" => SpecialUse::Archive, + b"sent" => SpecialUse::Sent, + b"shared" => SpecialUse::Shared, + b"important" => SpecialUse::Important, + ) + } + + pub fn as_str(&self) -> Option<&'static str> { + match self { + SpecialUse::Inbox => Some("inbox"), + SpecialUse::Trash => Some("trash"), + SpecialUse::Junk => Some("junk"), + SpecialUse::Drafts => Some("drafts"), + SpecialUse::Archive => Some("archive"), + SpecialUse::Sent => Some("sent"), + SpecialUse::Shared => Some("shared"), + SpecialUse::Important => Some("important"), + SpecialUse::None => None, + } + } +} + +impl ArchivedSpecialUse { + pub fn as_str(&self) -> Option<&'static str> { + match self { + ArchivedSpecialUse::Inbox => Some("inbox"), + ArchivedSpecialUse::Trash => Some("trash"), + ArchivedSpecialUse::Junk => Some("junk"), + ArchivedSpecialUse::Drafts => Some("drafts"), + ArchivedSpecialUse::Archive => Some("archive"), + ArchivedSpecialUse::Sent => Some("sent"), + ArchivedSpecialUse::Shared => Some("shared"), + ArchivedSpecialUse::Important => Some("important"), + ArchivedSpecialUse::None => None, + } + } +} + +impl From<&ArchivedSpecialUse> for SpecialUse { + fn from(value: &ArchivedSpecialUse) -> Self { + match value { + ArchivedSpecialUse::Inbox => SpecialUse::Inbox, + ArchivedSpecialUse::Trash => SpecialUse::Trash, + ArchivedSpecialUse::Junk => SpecialUse::Junk, + ArchivedSpecialUse::Drafts => SpecialUse::Drafts, + ArchivedSpecialUse::Archive => SpecialUse::Archive, + ArchivedSpecialUse::Sent => SpecialUse::Sent, + ArchivedSpecialUse::Shared => SpecialUse::Shared, + ArchivedSpecialUse::Important => SpecialUse::Important, + ArchivedSpecialUse::None => SpecialUse::None, + } + } +} diff --git a/crates/types/src/type_state.rs b/crates/types/src/type_state.rs index a1efb38e..8896b907 100644 --- a/crates/types/src/type_state.rs +++ b/crates/types/src/type_state.rs @@ -4,12 +4,11 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ +use crate::collection::SyncCollection; use serde::Serialize; use std::fmt::Display; use utils::map::bitmap::{Bitmap, BitmapItem}; -use crate::collection::SyncCollection; - #[derive(Debug, Eq, PartialEq, Hash, Clone, Copy, Serialize)] #[repr(u8)] pub enum DataType { @@ -130,41 +129,6 @@ impl From for u64 { } } -impl TryFrom<&str> for DataType { - type Error = (); - - fn try_from(value: &str) -> Result { - let mut hash = 0; - let mut shift = 0; - - for &ch in value.as_bytes() { - if shift < 128 { - hash |= (ch as u128) << shift; - shift += 8; - } else { - return Err(()); - } - } - - match hash { - 0x006c_6961_6d45 => Ok(DataType::Email), - 0x0079_7265_7669_6c65_446c_6961_6d45 => Ok(DataType::EmailDelivery), - 0x006e_6f69_7373_696d_6275_536c_6961_6d45 => Ok(DataType::EmailSubmission), - 0x0078_6f62_6c69_614d => Ok(DataType::Mailbox), - 0x6461_6572_6854 => Ok(DataType::Thread), - 0x7974_6974_6e65_6449 => Ok(DataType::Identity), - 0x6572_6f43 => Ok(DataType::Core), - 0x6e6f_6974_7069_7263_7362_7553_6873_7550 => Ok(DataType::PushSubscription), - 0x0074_6570_7069_6e53_6863_7261_6553 => Ok(DataType::SearchSnippet), - 0x6573_6e6f_7073_6552_6e6f_6974_6163_6156 => Ok(DataType::VacationResponse), - 0x004e_444d => Ok(DataType::Mdn), - 0x0061_746f_7551 => Ok(DataType::Quota), - 0x0074_7069_7263_5365_7665_6953 => Ok(DataType::SieveScript), - _ => Err(()), - } - } -} - impl DataType { pub fn try_from_sync(value: SyncCollection, is_container: bool) -> Option { match (value, is_container) { @@ -185,6 +149,30 @@ impl DataType { } impl DataType { + pub fn parse(value: &str) -> Option { + hashify::tiny_map!(value.as_bytes(), + b"Email" => DataType::Email, + b"EmailDelivery" => DataType::EmailDelivery, + b"EmailSubmission" => DataType::EmailSubmission, + b"Mailbox" => DataType::Mailbox, + b"Thread" => DataType::Thread, + b"Identity" => DataType::Identity, + b"Core" => DataType::Core, + b"PushSubscription" => DataType::PushSubscription, + b"SearchSnippet" => DataType::SearchSnippet, + b"VacationResponse" => DataType::VacationResponse, + b"MDN" => DataType::Mdn, + b"Quota" => DataType::Quota, + b"SieveScript" => DataType::SieveScript, + b"Calendar" => DataType::Calendar, + b"CalendarEvent" => DataType::CalendarEvent, + b"CalendarEventNotification" => DataType::CalendarEventNotification, + b"AddressBook" => DataType::AddressBook, + b"ContactCard" => DataType::ContactCard, + b"FileNode" => DataType::FileNode, + ) + } + pub fn as_str(&self) -> &'static str { match self { DataType::Email => "Email", @@ -222,7 +210,7 @@ impl<'de> serde::Deserialize<'de> for DataType { where D: serde::Deserializer<'de>, { - DataType::try_from(<&str>::deserialize(deserializer)?) - .map_err(|_| serde::de::Error::custom("invalid JMAP data type")) + DataType::parse(<&str>::deserialize(deserializer)?) + .ok_or_else(|| serde::de::Error::custom("invalid JMAP data type")) } } diff --git a/crates/utils/Cargo.toml b/crates/utils/Cargo.toml index 01cffb08..fb73abf0 100644 --- a/crates/utils/Cargo.toml +++ b/crates/utils/Cargo.toml @@ -34,9 +34,7 @@ http-body-util = "0.1.0" form_urlencoded = "1.1.0" psl = "2" quick_cache = "0.6.9" -downcast-rs = "2.0.1" fast-float = "0.2.0" -erased-serde = "0.4.5" rkyv = { version = "0.8.10", features = ["little_endian"] } compact_str = "0.9.0" diff --git a/crates/utils/src/json/mod.rs b/crates/utils/src/json/mod.rs deleted file mode 100644 index b4bd740a..00000000 --- a/crates/utils/src/json/mod.rs +++ /dev/null @@ -1,32 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC - * - * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL - */ - -pub mod parser; -pub mod pointer; - -use downcast_rs::{Downcast, impl_downcast}; -use std::{fmt::Debug, slice::Iter}; - -pub trait JsonQueryable: Downcast + Debug + 'static { - fn eval_pointer<'x>( - &'x self, - pointer: Iter, - results: &mut Vec<&'x dyn JsonQueryable>, - ); -} - -impl_downcast!(JsonQueryable); - -#[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize)] -pub struct JsonPointer(pub Vec); - -#[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize)] -pub enum JsonPointerItem { - Root, - Wildcard, - String(String), - Number(u64), -} diff --git a/crates/utils/src/json/parser/base32.rs b/crates/utils/src/json/parser/base32.rs deleted file mode 100644 index cfc56bf0..00000000 --- a/crates/utils/src/json/parser/base32.rs +++ /dev/null @@ -1,65 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC - * - * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL - */ - -use crate::codec::{base32_custom::BASE32_INVERSE, leb128::Leb128Iterator}; - -use super::json::Parser; - -#[derive(Debug)] -pub struct JsonBase32Reader<'x, 'y> { - bytes: &'y mut Parser<'x>, - last_byte: u8, - pos: usize, -} - -impl<'x, 'y> JsonBase32Reader<'x, 'y> { - pub fn new(bytes: &'y mut Parser<'x>) -> Self { - JsonBase32Reader { - bytes, - pos: 0, - last_byte: 0, - } - } - - #[inline(always)] - fn map_byte(&mut self) -> Option { - match self.bytes.next_unescaped() { - Ok(Some(byte)) => match BASE32_INVERSE[byte as usize] { - decoded_byte if decoded_byte != u8::MAX => { - self.last_byte = decoded_byte; - Some(decoded_byte) - } - _ => None, - }, - _ => None, - } - } - - pub fn error(&mut self) -> trc::Error { - self.bytes.error_value() - } -} - -impl Iterator for JsonBase32Reader<'_, '_> { - type Item = u8; - fn next(&mut self) -> Option { - let pos = self.pos % 5; - let last_byte = self.last_byte; - let byte = self.map_byte()?; - self.pos += 1; - - match pos { - 0 => ((byte << 3) | (self.map_byte().unwrap_or(0) >> 2)).into(), - 1 => ((last_byte << 6) | (byte << 1) | (self.map_byte().unwrap_or(0) >> 4)).into(), - 2 => ((last_byte << 4) | (byte >> 1)).into(), - 3 => ((last_byte << 7) | (byte << 2) | (self.map_byte().unwrap_or(0) >> 3)).into(), - 4 => ((last_byte << 5) | byte).into(), - _ => None, - } - } -} - -impl Leb128Iterator for JsonBase32Reader<'_, '_> {} diff --git a/crates/utils/src/json/parser/impls.rs b/crates/utils/src/json/parser/impls.rs deleted file mode 100644 index f334956e..00000000 --- a/crates/utils/src/json/parser/impls.rs +++ /dev/null @@ -1,308 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC - * - * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL - */ - -use std::fmt::Display; - -use crate::map::{ - bitmap::{Bitmap, BitmapItem}, - vec_map::VecMap, -}; - -use super::{Ignore, JsonObjectParser, Token, json::Parser}; - -impl JsonObjectParser for u64 { - fn parse(parser: &mut Parser<'_>) -> trc::Result - where - Self: Sized, - { - let mut hash = 0; - let mut shift = 0; - - while let Some(ch) = parser.next_unescaped()? { - if shift < 64 { - hash |= (ch as u64) << shift; - shift += 8; - } else { - hash = 0; - break; - } - } - - Ok(hash) - } -} - -impl JsonObjectParser for u128 { - fn parse(parser: &mut Parser<'_>) -> trc::Result - where - Self: Sized, - { - let mut hash = 0; - let mut shift = 0; - - while let Some(ch) = parser.next_unescaped()? { - if shift < 128 { - hash |= (ch as u128) << shift; - shift += 8; - } else { - hash = 0; - break; - } - } - - Ok(hash) - } -} - -impl JsonObjectParser for String { - fn parse(parser: &mut Parser<'_>) -> trc::Result - where - Self: Sized, - { - let start_pos = parser.pos; - - while let Some(ch) = parser.next_char() { - match ch { - b'\\' => { - let mut is_escaped = true; - let mut buf = Vec::with_capacity((parser.pos - start_pos) + 16); - buf.extend_from_slice(&parser.bytes[start_pos..parser.pos - 1]); - - while let Some(ch) = parser.next_char() { - match ch { - b'\\' if !is_escaped => { - is_escaped = true; - } - b'"' if !is_escaped => { - parser.is_eof = true; - return String::from_utf8(buf).map_err(|_| parser.error_utf8()); - } - _ => { - if !is_escaped { - buf.push(ch); - } else { - match ch { - b'"' => { - buf.push(b'"'); - } - b'\\' => { - buf.push(b'\\'); - } - b'n' => { - buf.push(b'\n'); - } - b't' => { - buf.push(b'\t'); - } - b'r' => { - buf.push(b'\r'); - } - b'b' => { - buf.push(0x08); - } - b'f' => { - buf.push(0x0c); - } - b'/' => { - buf.push(b'/'); - } - b'u' => { - let mut code = [ - *parser.iter.next().ok_or_else(|| { - parser.error("Incomplete unicode sequence") - })?, - *parser.iter.next().ok_or_else(|| { - parser.error("Incomplete unicode sequence") - })?, - *parser.iter.next().ok_or_else(|| { - parser.error("Incomplete unicode sequence") - })?, - *parser.iter.next().ok_or_else(|| { - parser.error("Incomplete unicode sequence") - })?, - ]; - parser.pos += 4; - let code_str = std::str::from_utf8(&code) - .map_err(|_| parser.error_utf8())?; - let code_str = char::from_u32( - u32::from_str_radix(code_str, 16).map_err( - |_| { - parser.error(&format!( - "Invalid unicode sequence {code_str}" - )) - }, - )?, - ) - .ok_or_else(|| { - parser.error(&format!( - "Invalid unicode sequence {code_str}" - )) - })? - .encode_utf8(&mut code); - buf.extend_from_slice(code_str.as_bytes()); - } - _ => { - buf.push(ch); - } - } - is_escaped = false; - } - } - } - } - break; - } - b'"' => { - parser.is_eof = true; - return std::str::from_utf8( - parser - .bytes - .get(start_pos..parser.pos - 1) - .unwrap_or_default(), - ) - .map(Into::into) - .map_err(|_| parser.error_utf8()); - } - _ => (), - } - } - - Err(parser.error_unterminated()) - } -} - -impl JsonObjectParser for Vec { - fn parse(parser: &mut Parser<'_>) -> trc::Result - where - Self: Sized, - { - let mut vec = Vec::new(); - - parser.next_token::()?.assert(Token::ArrayStart)?; - loop { - match parser.next_token::()? { - Token::String(item) => vec.push(item), - Token::Comma => (), - Token::ArrayEnd => break, - token => return Err(token.error("", "[ or string")), - } - } - Ok(vec) - } -} - -impl JsonObjectParser for Option> { - fn parse(parser: &mut Parser<'_>) -> trc::Result - where - Self: Sized, - { - match parser.next_token::()? { - Token::ArrayStart => { - let mut vec = Vec::new(); - loop { - match parser.next_token::()? { - Token::String(item) => vec.push(item), - Token::Comma => (), - Token::ArrayEnd => break, - token => return Err(token.error("", "string")), - } - } - Ok(Some(vec)) - } - Token::Null => Ok(None), - token => Err(token.error("", "array or null")), - } - } -} - -impl JsonObjectParser for Bitmap { - fn parse(parser: &mut Parser<'_>) -> trc::Result - where - Self: Sized, - { - let mut bm = Bitmap::new(); - match parser.next_token::()? { - Token::ArrayStart => { - loop { - match parser.next_token::()? { - Token::String(item) => bm.insert(item), - Token::Comma => (), - Token::ArrayEnd => break, - token => return Err(token.error("", "string")), - } - } - Ok(bm) - } - Token::Null => Ok(bm), - token => Err(token.error("", "array or null")), - } - } -} - -impl JsonObjectParser for VecMap { - fn parse(parser: &mut Parser<'_>) -> trc::Result - where - Self: Sized, - { - let mut map = VecMap::new(); - - parser.next_token::()?.assert(Token::DictStart)?; - while let Some(key) = parser.next_dict_key()? { - map.append(key, V::parse(parser)?); - } - - Ok(map) - } -} - -impl JsonObjectParser - for Option> -{ - fn parse(parser: &mut Parser<'_>) -> trc::Result - where - Self: Sized, - { - match parser.next_token::()? { - Token::DictStart => { - let mut map = VecMap::new(); - - while let Some(key) = parser.next_dict_key()? { - map.append(key, V::parse(parser)?); - } - - Ok(Some(map)) - } - Token::Null => Ok(None), - token => Err(token.error("", &token.to_string())), - } - } -} - -impl JsonObjectParser for bool { - fn parse(parser: &mut Parser<'_>) -> trc::Result - where - Self: Sized, - { - match parser.next_token::()? { - Token::Boolean(value) => Ok(value), - Token::Null => Ok(false), - token => Err(token.error("", &token.to_string())), - } - } -} - -impl JsonObjectParser for Ignore { - fn parse(parser: &mut Parser<'_>) -> trc::Result - where - Self: Sized, - { - if parser.skip_string() { - Ok(Ignore {}) - } else { - Err(parser.error_unterminated()) - } - } -} diff --git a/crates/utils/src/json/parser/json.rs b/crates/utils/src/json/parser/json.rs deleted file mode 100644 index 3f4da289..00000000 --- a/crates/utils/src/json/parser/json.rs +++ /dev/null @@ -1,390 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC - * - * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL - */ - -use std::{fmt::Display, iter::Peekable, slice::Iter}; - -use compact_str::format_compact; - -use super::{Ignore, JsonObjectParser, Token}; - -const MAX_NESTED_LEVELS: u32 = 16; - -#[derive(Debug)] -pub struct Parser<'x> { - pub bytes: &'x [u8], - pub iter: Peekable>, - pub next_ch: Option, - pub pos: usize, - pub pos_marker: usize, - pub depth_array: u32, - pub depth_dict: u32, - pub is_eof: bool, -} - -impl<'x> Parser<'x> { - pub fn new(bytes: &'x [u8]) -> Self { - Self { - bytes, - iter: bytes.iter().peekable(), - next_ch: None, - pos: 0, - pos_marker: 0, - is_eof: false, - depth_array: 0, - depth_dict: 0, - } - } - - pub fn error(&self, message: &str) -> trc::Error { - trc::JmapEvent::NotJson - .into_err() - .details(format_compact!("{message} at position {}.", self.pos)) - } - - pub fn error_unterminated(&self) -> trc::Error { - trc::JmapEvent::NotJson.into_err().details(format_compact!( - "Unterminated string at position {pos}.", - pos = self.pos - )) - } - - pub fn error_utf8(&self) -> trc::Error { - trc::JmapEvent::NotJson.into_err().details(format_compact!( - "Invalid UTF-8 sequence at position {pos}.", - pos = self.pos - )) - } - - pub fn error_value(&mut self) -> trc::Error { - if self.is_eof || self.skip_string() { - trc::JmapEvent::InvalidArguments - .into_err() - .details(format_compact!( - "Invalid value {:?} at position {}.", - String::from_utf8_lossy(self.bytes[self.pos_marker..self.pos - 1].as_ref()), - self.pos - )) - } else { - self.error_unterminated() - } - } - - #[inline(always)] - pub fn peek_char(&mut self) -> Option { - self.iter.peek().map(|&&ch| ch) - } - - #[inline(always)] - pub fn next_char(&mut self) -> Option { - self.pos += 1; - self.iter.next().copied() - } - - #[inline(always)] - pub fn next_unescaped(&mut self) -> trc::Result> { - match self.next_char() { - Some(b'"') => { - self.is_eof = true; - Ok(None) - } - Some(b'\\') => self - .next_char() - .ok_or_else(|| self.error_unterminated()) - .map(Some), - Some(ch) => Ok(Some(ch)), - None => { - if self.is_eof { - Ok(None) - } else { - Err(self.error_unterminated()) - } - } - } - } - - pub fn skip_string(&mut self) -> bool { - let mut last_ch = 0; - - while let Some(ch) = self.next_char() { - if ch == b'"' && last_ch != b'\\' { - self.is_eof = true; - return true; - } else { - last_ch = ch; - } - } - - false - } - - pub fn next_token(&mut self) -> trc::Result> { - let mut next_ch = self.next_ch.take().or_else(|| self.next_char()); - - while let Some(mut ch) = next_ch { - match ch { - b'"' => { - self.pos_marker = self.pos; - self.is_eof = false; - let value = T::parse(self)?; - return if self.is_eof || self.skip_string() { - Ok(Token::String(value)) - } else { - Err(self.error_unterminated()) - }; - } - b',' => { - return Ok(Token::Comma); - } - b':' => { - return Ok(Token::Colon); - } - b'[' => { - if self.depth_array + self.depth_dict < MAX_NESTED_LEVELS { - self.depth_array += 1; - return Ok(Token::ArrayStart); - } else { - return Err(self.error("Too many nested objects")); - } - } - b']' => { - return if self.depth_array != 0 { - self.depth_array -= 1; - Ok(Token::ArrayEnd) - } else { - Err(self.error("Unexpected array end")) - }; - } - b'{' => { - if self.depth_array + self.depth_dict < MAX_NESTED_LEVELS { - self.depth_dict += 1; - return Ok(Token::DictStart); - } else { - return Err(self.error("Too many nested objects")); - } - } - b'}' => { - return if self.depth_dict != 0 { - self.depth_dict -= 1; - Ok(Token::DictEnd) - } else { - Err(self.error("Unexpected dictionary end")) - }; - } - b'0'..=b'9' | b'-' | b'+' => { - let mut num: i64 = 0; - let mut is_float = false; - let mut is_negative = false; - let num_start = self.pos - 1; - - loop { - match ch { - b'-' => { - is_negative = true; - } - b'0'..=b'9' => { - if !is_float { - num = num.saturating_mul(10).saturating_add((ch - b'0') as i64); - } - } - b',' | b']' | b'}' => { - self.next_ch = ch.into(); - break; - } - b'+' => (), - b'.' | b'e' | b'E' => { - is_float = true; - } - b' ' | b'\r' | b'\t' | b'\n' => { - break; - } - _ => { - return Err(self - .error(&format!("Unexpected character {:?}", char::from(ch)))); - } - } - - ch = self.next_char().ok_or_else(|| self.error_unterminated())?; - } - - return if !is_float { - Ok(Token::Integer(if !is_negative { num } else { -num })) - } else { - fast_float::parse( - self.bytes.get(num_start..self.pos - 1).unwrap_or_default(), - ) - .map(Token::Float) - .map_err(|_| { - self.error(&format!( - "Failed to parse number {:?}", - String::from_utf8_lossy( - self.bytes.get(num_start..self.pos - 1).unwrap_or_default() - ) - )) - }) - }; - } - b't' => { - return if let (Some(b'r'), Some(b'u'), Some(b'e')) = - (self.iter.next(), self.iter.next(), self.iter.next()) - { - self.pos += 3; - Ok(Token::Boolean(true)) - } else { - Err(self.error("Invalid JSON token")) - }; - } - b'f' => { - return if let (Some(b'a'), Some(b'l'), Some(b's'), Some(b'e')) = ( - self.iter.next(), - self.iter.next(), - self.iter.next(), - self.iter.next(), - ) { - self.pos += 4; - Ok(Token::Boolean(false)) - } else { - Err(self.error("Invalid JSON token")) - }; - } - b'n' => { - return if let (Some(b'u'), Some(b'l'), Some(b'l')) = - (self.iter.next(), self.iter.next(), self.iter.next()) - { - self.pos += 3; - Ok(Token::Null) - } else { - Err(self.error("Invalid JSON token")) - }; - } - b' ' | b'\t' | b'\r' | b'\n' => (), - _ => { - return Err(self.error(&format!("Unexpected character {:?}", char::from(ch)))); - } - } - - next_ch = self.next_char(); - } - - Err(self.error("Unexpected EOF")) - } - - pub fn next_dict_key(&mut self) -> trc::Result> { - loop { - match self.next_token::()? { - Token::String(k) => { - self.next_token::()?.assert(Token::Colon)?; - return Ok(Some(k)); - } - Token::Comma => (), - Token::DictEnd => return Ok(None), - token => { - return Err(self.error(&format!("Expected object property, found {}", token))); - } - } - } - } - - pub fn skip_token(&mut self, start_depth_array: u32, start_depth_dict: u32) -> trc::Result<()> { - while { - self.next_token::()?; - start_depth_array != self.depth_array || start_depth_dict != self.depth_dict - } {} - - Ok(()) - } -} - -#[cfg(test)] -mod tests { - - use crate::json::parser::Token; - - use super::Parser; - - #[test] - fn parse_json() { - for (input, expected_result) in [ - ( - &b"[true, false, 123, 456 , -123, 0.123, -0.456, 3.7e-5, 6.02e+23, null]"[..], - vec![ - Token::ArrayStart, - Token::Boolean(true), - Token::Comma, - Token::Boolean(false), - Token::Comma, - Token::Integer(123), - Token::Comma, - Token::Integer(456), - Token::Comma, - Token::Integer(-123), - Token::Comma, - Token::Float(0.123), - Token::Comma, - Token::Float(-0.456), - Token::Comma, - Token::Float(3.7e-5), - Token::Comma, - Token::Float(6.02e23), - Token::Comma, - Token::Null, - Token::ArrayEnd, - ], - ), - ( - &b"{\"\": true, \"\": false , \"\": {\"\": 123}, \"\": [ ]}"[..], - vec![ - Token::DictStart, - Token::String("".to_string()), - Token::Colon, - Token::Boolean(true), - Token::Comma, - Token::String("".to_string()), - Token::Colon, - Token::Boolean(false), - Token::Comma, - Token::String("".to_string()), - Token::Colon, - Token::DictStart, - Token::String("".to_string()), - Token::Colon, - Token::Integer(123), - Token::DictEnd, - Token::Comma, - Token::String("".to_string()), - Token::Colon, - Token::ArrayStart, - Token::ArrayEnd, - Token::DictEnd, - ], - ), - ] { - let mut p = Parser::new(input); - let mut result = Vec::new(); - while let Ok(token) = p.next_token() { - result.push(token); - } - - assert_eq!(result, expected_result); - } - - for (input, expected_result) in [ - ("hello\t\nworld", "hello\t\nworld"), - ("hello\t\n\\\"world\\\"\\n", "hello\t\n\"world\"\n"), - ("\\\"hello\\\tworld\\\"", "\"hello\tworld\""), - ("\\u0009\\u0020\\u263A", "\t ☺"), - ("", ""), - ] { - assert_eq!( - Parser::new(format!("\"{input}\"").as_bytes()) - .next_token::() - .unwrap() - .unwrap_string("") - .unwrap(), - expected_result - ); - } - } -} diff --git a/crates/utils/src/json/parser/mod.rs b/crates/utils/src/json/parser/mod.rs deleted file mode 100644 index 106b115a..00000000 --- a/crates/utils/src/json/parser/mod.rs +++ /dev/null @@ -1,166 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC - * - * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL - */ - -use std::fmt::Display; - -use compact_str::format_compact; - -use self::json::Parser; - -pub mod base32; -pub mod impls; -pub mod json; -pub mod pointer; - -#[derive(Debug, PartialEq, Clone)] -pub enum Token { - Colon, - Comma, - DictStart, - DictEnd, - ArrayStart, - ArrayEnd, - Integer(i64), - Float(f64), - Boolean(bool), - String(T), - Null, -} - -impl Eq for Token {} - -pub trait JsonObjectParser { - fn parse(parser: &mut Parser<'_>) -> trc::Result - where - Self: Sized; -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub struct Ignore {} - -impl Token { - pub fn unwrap_string(self, property: &str) -> trc::Result { - match self { - Token::String(s) => Ok(s), - token => Err(token.error(property, "string")), - } - } - - pub fn unwrap_string_or_null(self, property: &str) -> trc::Result> { - match self { - Token::String(s) => Ok(Some(s)), - Token::Null => Ok(None), - token => Err(token.error(property, "string")), - } - } - - pub fn unwrap_bool(self, property: &str) -> trc::Result { - match self { - Token::Boolean(v) => Ok(v), - token => Err(token.error(property, "boolean")), - } - } - - pub fn unwrap_bool_or_null(self, property: &str) -> trc::Result> { - match self { - Token::Boolean(v) => Ok(Some(v)), - Token::Null => Ok(None), - token => Err(token.error(property, "boolean")), - } - } - - pub fn unwrap_usize_or_null(self, property: &str) -> trc::Result> { - match self { - Token::Integer(v) if v >= 0 => Ok(Some(v as usize)), - Token::Float(v) if v >= 0.0 => Ok(Some(v as usize)), - Token::Null => Ok(None), - token => Err(token.error(property, "unsigned integer")), - } - } - - pub fn unwrap_uint_or_null(self, property: &str) -> trc::Result> { - match self { - Token::Integer(v) if v >= 0 => Ok(Some(v as u64)), - Token::Float(v) if v >= 0.0 => Ok(Some(v as u64)), - Token::Null => Ok(None), - token => Err(token.error(property, "unsigned integer")), - } - } - - pub fn unwrap_int_or_null(self, property: &str) -> trc::Result> { - match self { - Token::Integer(v) => Ok(Some(v)), - Token::Float(v) => Ok(Some(v as i64)), - Token::Null => Ok(None), - token => Err(token.error(property, "unsigned integer")), - } - } - - pub fn unwrap_ints_or_null(self, property: &str) -> trc::Result> { - match self { - Token::Integer(v) => Ok(Some(v as i32)), - Token::Float(v) => Ok(Some(v as i32)), - Token::Null => Ok(None), - token => Err(token.error(property, "unsigned integer")), - } - } - - pub fn assert(self, token: Token) -> trc::Result<()> { - if self == token { - Ok(()) - } else { - Err(self.error("", &token.to_string())) - } - } - - pub fn assert_jmap(self, token: Token) -> trc::Result<()> { - if self == token { - Ok(()) - } else { - Err(trc::JmapEvent::NotRequest - .into_err() - .details(format_compact!( - "Invalid JMAP request: expected '{token}', got '{self}'." - ))) - } - } - - pub fn error(&self, property: &str, expected: &str) -> trc::Error { - trc::JmapEvent::InvalidArguments - .into_err() - .details(if !property.is_empty() { - format_compact!( - "Invalid argument for '{property:?}': expected '{expected}', got '{self}'.", - ) - } else { - format_compact!("Invalid argument: expected '{expected}', got '{self}'.") - }) - } -} - -impl Display for Ignore { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "string") - } -} - -impl Display for Token { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - Token::Colon => write!(f, ":"), - Token::Comma => write!(f, ","), - Token::DictStart => write!(f, "{{"), - Token::DictEnd => write!(f, "}}"), - Token::ArrayStart => write!(f, "["), - Token::ArrayEnd => write!(f, "]"), - Token::Integer(i) => write!(f, "{}", i), - Token::Float(v) => write!(f, "{}", v), - Token::Boolean(b) => write!(f, "{}", b), - Token::Null => write!(f, "null"), - Token::String(_) => write!(f, "string"), - } - } -} diff --git a/crates/utils/src/json/parser/pointer.rs b/crates/utils/src/json/parser/pointer.rs deleted file mode 100644 index 1ca44c92..00000000 --- a/crates/utils/src/json/parser/pointer.rs +++ /dev/null @@ -1,222 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC - * - * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL - */ - -use std::fmt::Display; - -use crate::json::{JsonPointer, JsonPointerItem}; - -use super::{JsonObjectParser, json::Parser}; - -enum TokenType { - Unknown, - Number, - String, - Wildcard, - Escaped, -} - -impl JsonObjectParser for JsonPointer { - fn parse(parser: &mut Parser<'_>) -> trc::Result - where - Self: Sized, - { - let mut path = Vec::new(); - let mut num = 0u64; - let mut buf = Vec::new(); - let mut token = TokenType::Unknown; - let mut start_pos = parser.pos; - - while let Some(ch) = parser.next_char() { - match (ch, &token) { - (b'0'..=b'9', TokenType::Unknown | TokenType::Number) => { - num = num.saturating_mul(10).saturating_add((ch - b'0') as u64); - token = TokenType::Number; - } - (b'*', TokenType::Unknown) => { - token = TokenType::Wildcard; - } - (b'0', TokenType::Escaped) => { - buf.push(b'~'); - token = TokenType::String; - } - (b'1', TokenType::Escaped) => { - buf.push(b'/'); - token = TokenType::String; - } - (b'/' | b'"', _) => { - match token { - TokenType::String => { - path.push(JsonPointerItem::String( - String::from_utf8(buf).map_err(|_| parser.error_utf8())?, - )); - buf = Vec::new(); - } - TokenType::Number => { - path.push(JsonPointerItem::Number(num)); - num = 0; - } - TokenType::Wildcard => { - path.push(JsonPointerItem::Wildcard); - } - TokenType::Unknown if parser.pos_marker != start_pos => { - path.push(JsonPointerItem::String(String::new())); - } - _ => (), - } - - if ch == b'/' { - token = TokenType::Unknown; - start_pos = parser.pos; - } else { - parser.is_eof = true; - - if path.is_empty() { - path.push(JsonPointerItem::Root); - } - - return Ok(JsonPointer(path)); - } - } - (_, _) => { - if matches!(&token, TokenType::Number | TokenType::Wildcard) - && parser.pos - 1 > start_pos - { - buf.extend_from_slice( - parser - .bytes - .get(start_pos..parser.pos - 1) - .unwrap_or_default(), - ); - } - - token = match ch { - b'~' if !matches!(&token, TokenType::Escaped) => TokenType::Escaped, - b'\\' => { - buf.push(parser.next_char().unwrap_or(b'\\')); - TokenType::String - } - _ => { - buf.push(ch); - TokenType::String - } - }; - } - } - } - - Err(parser.error_unterminated()) - } -} - -impl Display for JsonPointer { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - for (i, ptr) in self.0.iter().enumerate() { - if i > 0 { - write!(f, "/")?; - } - write!(f, "{}", ptr)?; - } - Ok(()) - } -} - -impl Display for JsonPointerItem { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - JsonPointerItem::Root => write!(f, "/"), - JsonPointerItem::Wildcard => write!(f, "*"), - JsonPointerItem::String(s) => write!(f, "{}", s), - JsonPointerItem::Number(n) => write!(f, "{}", n), - } - } -} - -#[cfg(test)] -mod tests { - - use crate::json::parser::json::Parser; - - use super::{JsonPointer, JsonPointerItem}; - - #[test] - fn json_pointer_parse() { - for (input, output) in vec![ - ("hello", vec![JsonPointerItem::String("hello".to_string())]), - ("9a", vec![JsonPointerItem::String("9a".to_string())]), - ("a9", vec![JsonPointerItem::String("a9".to_string())]), - ("*a", vec![JsonPointerItem::String("*a".to_string())]), - ( - "/hello/world", - vec![ - JsonPointerItem::String("hello".to_string()), - JsonPointerItem::String("world".to_string()), - ], - ), - ("*", vec![JsonPointerItem::Wildcard]), - ( - "/hello/*", - vec![ - JsonPointerItem::String("hello".to_string()), - JsonPointerItem::Wildcard, - ], - ), - ("1234", vec![JsonPointerItem::Number(1234)]), - ( - "/hello/1234", - vec![ - JsonPointerItem::String("hello".to_string()), - JsonPointerItem::Number(1234), - ], - ), - ("~0~1", vec![JsonPointerItem::String("~/".to_string())]), - ( - "/hello/~0~1", - vec![ - JsonPointerItem::String("hello".to_string()), - JsonPointerItem::String("~/".to_string()), - ], - ), - ( - "/hello/1~0~1/*~1~0", - vec![ - JsonPointerItem::String("hello".to_string()), - JsonPointerItem::String("1~/".to_string()), - JsonPointerItem::String("*/~".to_string()), - ], - ), - ( - "/hello/world/*/99", - vec![ - JsonPointerItem::String("hello".to_string()), - JsonPointerItem::String("world".to_string()), - JsonPointerItem::Wildcard, - JsonPointerItem::Number(99), - ], - ), - ("/", vec![JsonPointerItem::String("".to_string())]), - ( - "///", - vec![ - JsonPointerItem::String("".to_string()), - JsonPointerItem::String("".to_string()), - JsonPointerItem::String("".to_string()), - ], - ), - ("", vec![JsonPointerItem::Root]), - ] { - assert_eq!( - Parser::new(format!("\"{input}\"").as_bytes()) - .next_token::() - .unwrap() - .unwrap_string("") - .unwrap() - .0, - output, - "{input}" - ); - } - } -} diff --git a/crates/utils/src/json/pointer.rs b/crates/utils/src/json/pointer.rs deleted file mode 100644 index 884905a6..00000000 --- a/crates/utils/src/json/pointer.rs +++ /dev/null @@ -1,112 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC - * - * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL - */ - -use super::{JsonPointerItem, JsonQueryable}; -use std::hash::BuildHasher; -use std::{collections::HashMap, slice::Iter}; - -impl JsonQueryable for Vec { - fn eval_pointer<'x>( - &'x self, - mut pointer: Iter, - results: &mut Vec<&'x dyn JsonQueryable>, - ) { - match pointer.next() { - Some(JsonPointerItem::Number(n)) => { - if let Some(v) = self.get(*n as usize) { - v.eval_pointer(pointer, results); - } - } - Some(JsonPointerItem::Wildcard) => { - for v in self { - v.eval_pointer(pointer.clone(), results); - } - } - Some(JsonPointerItem::Root) | None => { - results.push(self); - } - _ => {} - } - } -} - -impl JsonQueryable for HashMap { - fn eval_pointer<'x>( - &'x self, - mut pointer: Iter, - results: &mut Vec<&'x dyn JsonQueryable>, - ) { - match pointer.next() { - Some(JsonPointerItem::String(n)) => { - if let Some(v) = self.get(n) { - v.eval_pointer(pointer, results); - } - } - Some(JsonPointerItem::Number(n)) => { - let n = n.to_string(); - if let Some(v) = self.get(&n) { - v.eval_pointer(pointer, results); - } - } - Some(JsonPointerItem::Wildcard) => { - for v in self.values() { - v.eval_pointer(pointer.clone(), results); - } - } - Some(JsonPointerItem::Root) | None => { - results.push(self); - } - } - } -} - -impl JsonQueryable for serde_json::Value { - fn eval_pointer<'x>( - &'x self, - mut pointer: Iter, - results: &mut Vec<&'x dyn JsonQueryable>, - ) { - match pointer.next() { - Some(JsonPointerItem::String(n)) => { - if let serde_json::Value::Object(map) = self - && let Some(v) = map.get(n) - { - v.eval_pointer(pointer, results); - } - } - Some(JsonPointerItem::Number(n)) => match self { - serde_json::Value::Array(values) => { - if let Some(v) = values.get(*n as usize) { - v.eval_pointer(pointer, results); - } - } - serde_json::Value::Object(map) => { - let n = n.to_string(); - if let Some(v) = map.get(&n) { - v.eval_pointer(pointer, results); - } - } - _ => {} - }, - Some(JsonPointerItem::Wildcard) => match self { - serde_json::Value::Array(values) => { - for v in values { - v.eval_pointer(pointer.clone(), results); - } - } - serde_json::Value::Object(map) => { - for v in map.values() { - v.eval_pointer(pointer.clone(), results); - } - } - _ => {} - }, - Some(JsonPointerItem::Root) | None => { - results.push(self); - } - } - } -} diff --git a/crates/utils/src/lib.rs b/crates/utils/src/lib.rs index 4fa7981d..2b7b9a4d 100644 --- a/crates/utils/src/lib.rs +++ b/crates/utils/src/lib.rs @@ -9,7 +9,6 @@ pub mod cache; pub mod codec; pub mod config; pub mod glob; -pub mod json; pub mod map; pub mod snowflake; pub mod template; @@ -26,9 +25,6 @@ use rustls::{ use rustls_pki_types::TrustAnchor; use std::sync::Arc; -pub use downcast_rs; -pub use erased_serde; - pub trait HttpLimitResponse: Sync + Send { fn bytes_with_limit( self,