diff --git a/.gitignore b/.gitignore index 4fffb2f8..5b70a898 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ /target /Cargo.lock +.vscode diff --git a/Cargo.toml b/Cargo.toml index 1563d8e5..b860902f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,46 +1,23 @@ [package] -name = "store" -version = "0.1.0" +name = "stalwart-jmap" +description = "Stalwart JMAP Server" +authors = [ "Stalwart Labs Ltd. "] +repository = "https://github.com/stalwartlabs/jmap-server" +homepage = "https://stalw.art/jmap" +keywords = ["jmap", "email", "mail", "server"] +categories = ["email"] +license = "AGPL-3.0-only" +version = "0.3.0" edition = "2021" +resolver = "2" +[lib] +path = "crates/core/src/lib.rs" -[dependencies] -utils = { path = "../utils" } -rocksdb = { version = "0.20.1", optional = true } -foundationdb = { version = "0.7.0", optional = true } -rusqlite = { version = "0.29.0", features = ["bundled"], optional = true } -tokio = { version = "1.23", features = ["sync"], optional = true } -r2d2 = { version = "0.8.10", optional = true } -futures = { version = "0.3", optional = true } -rand = "0.8.5" -roaring = "0.10.1" -rayon = { version = "1.5.1", optional = true } -serde = { version = "1.0", features = ["derive"]} -ahash = { version = "0.8.0", features = ["serde"] } -bitpacking = "0.8.4" -lazy_static = "1.4" -whatlang = "0.16" # Language detection -rust-stemmers = "1.2" # Stemmers -tinysegmenter = "0.1" # Japanese tokenizer -jieba-rs = "0.6" # Chinese stemmer -xxhash-rust = { version = "0.8.5", features = ["xxh3"] } -farmhash = "1.1.5" -siphasher = "0.3" -maybe-async = "0.2" -parking_lot = { version = "0.12.1", optional = true } -lru-cache = { version = "0.1.2", optional = true } -blake3 = "1.3.3" - -[features] -default = ["sqlite"] -rocks = ["rocksdb", "rayon", "is_sync"] -sqlite = ["rusqlite", "rayon", "r2d2", "tokio", "is_sync"] -foundation = ["foundationdb", "futures", "is_async"] -is_sync = ["maybe-async/is_sync", "parking_lot", "lru-cache"] -is_async = [] - -[dev-dependencies] -tokio = { version = "1.23", features = ["full"] } -csv = "1.1" -rayon = { version = "1.5.1" } -flate2 = { version = "1.0.17", features = ["zlib"], default-features = false } +[workspace] +members = [ + "crates/protocol", + "crates/store", + "crates/core", + "tests", +] diff --git a/crates/core/Cargo.toml b/crates/core/Cargo.toml new file mode 100644 index 00000000..2387d4f2 --- /dev/null +++ b/crates/core/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "core" +version = "0.1.0" +edition = "2021" + +[dependencies] +mail-parser = { git = "https://github.com/stalwartlabs/mail-parser", features = ["full_encoding", "serde_support", "ludicrous_mode"] } +mail-builder = { git = "https://github.com/stalwartlabs/mail-builder", features = ["ludicrous_mode"] } +mail-send = { git = "https://github.com/stalwartlabs/mail-send" } +serde = { version = "1.0", features = ["derive"]} +serde_json = "1.0" diff --git a/crates/core/src/lib.rs b/crates/core/src/lib.rs new file mode 100644 index 00000000..e69de29b diff --git a/crates/protocol/Cargo.lock b/crates/protocol/Cargo.lock new file mode 100644 index 00000000..4cfd8d9d --- /dev/null +++ b/crates/protocol/Cargo.lock @@ -0,0 +1,159 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "ahash" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c99f64d1e06488f620f932677e24bc6e2897582980441ae90a671415bd7ec2f" +dependencies = [ + "cfg-if", + "getrandom", + "once_cell", + "serde", + "version_check", +] + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "fast-float" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95765f67b4b18863968b4a1bd5bb576f732b29a4a28c7cd84c09fa3e2875f33c" + +[[package]] +name = "getrandom" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c05aeb6a22b8f62540c194aac980f2115af067bfe15a0734d7277a768d396b31" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "itoa" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "453ad9f582a441959e5f0d088b02ce04cfe8d51a8eaf077f12ac6d3e94164ca6" + +[[package]] +name = "jmap-parser" +version = "0.1.0" +dependencies = [ + "ahash", + "fast-float", + "serde", + "serde_json", + "utils", +] + +[[package]] +name = "libc" +version = "0.2.139" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "201de327520df007757c1f0adce6e827fe8562fbc28bfd9c15571c66ca1f5f79" + +[[package]] +name = "once_cell" +version = "1.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7e5500299e16ebb147ae15a00a942af264cf3688f47923b8fc2cd5858f23ad3" + +[[package]] +name = "proc-macro2" +version = "1.0.51" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d727cae5b39d21da60fa540906919ad737832fe0b1c165da3a34d6548c849d6" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8856d8364d252a14d474036ea1358d63c9e6965c8e5c1885c18f73d70bff9c7b" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "ryu" +version = "1.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f91339c0467de62360649f8d3e185ca8de4224ff281f66000de5eb2a77a79041" + +[[package]] +name = "serde" +version = "1.0.152" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb7d1f0d3021d347a83e556fc4683dea2ea09d87bccdf88ff5c12545d89d5efb" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.152" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af487d118eecd09402d70a5d72551860e788df87b464af30e5ea6a38c75c541e" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.94" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c533a59c9d8a93a09c6ab31f0fd5e5f4dd1b8fc9434804029839884765d04ea" +dependencies = [ + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "unicode-ident" +version = "1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5464a87b239f13a63a501f2701565754bae92d243d4bb7eb12f6d57d2269bf4" + +[[package]] +name = "utils" +version = "0.1.0" +dependencies = [ + "serde", +] + +[[package]] +name = "version_check" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" diff --git a/crates/protocol/Cargo.toml b/crates/protocol/Cargo.toml new file mode 100644 index 00000000..726c008e --- /dev/null +++ b/crates/protocol/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "procotol" +version = "0.1.0" +edition = "2021" + +[dependencies] +utils = { path = "/home/vagrant/code/utils" } +fast-float = "0.2.0" +serde = { version = "1.0", features = ["derive"]} +ahash = { version = "0.8.0", features = ["serde"] } +serde_json = { version = "1.0", features = ["raw_value"] } diff --git a/crates/protocol/src/error/method.rs b/crates/protocol/src/error/method.rs new file mode 100644 index 00000000..1de2abb6 --- /dev/null +++ b/crates/protocol/src/error/method.rs @@ -0,0 +1,166 @@ +/* + * Copyright (c) 2020-2022, Stalwart Labs Ltd. + * + * This file is part of the Stalwart JMAP Server. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * in the LICENSE file at the top-level directory of this distribution. + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + * You can be released from the requirements of the AGPLv3 license by + * purchasing a commercial license. Please contact licensing@stalw.art + * for more details. +*/ + +use std::fmt::Display; + +use serde::ser::SerializeMap; +use serde::Serialize; + +#[derive(Debug)] +pub enum MethodError { + InvalidArguments(String), + RequestTooLarge, + StateMismatch, + AnchorNotFound, + UnsupportedFilter(String), + UnsupportedSort(String), + ServerFail(String), + UnknownMethod(String), + ServerUnavailable, + ServerPartialFail, + InvalidResultReference(String), + Forbidden(String), + AccountNotFound, + AccountNotSupportedByMethod, + AccountReadOnly, + NotFound, +} + +impl Display for MethodError { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + match self { + MethodError::InvalidArguments(err) => write!(f, "Invalid arguments: {}", err), + MethodError::RequestTooLarge => write!(f, "Request too large"), + MethodError::StateMismatch => write!(f, "State mismatch"), + MethodError::AnchorNotFound => write!(f, "Anchor not found"), + MethodError::UnsupportedFilter(err) => write!(f, "Unsupported filter: {}", err), + MethodError::UnsupportedSort(err) => write!(f, "Unsupported sort: {}", err), + MethodError::ServerFail(err) => write!(f, "Server error: {}", err), + MethodError::UnknownMethod(err) => write!(f, "Unknown method: {}", err), + MethodError::ServerUnavailable => write!(f, "Server unavailable"), + MethodError::ServerPartialFail => write!(f, "Server partial fail"), + MethodError::InvalidResultReference(err) => { + write!(f, "Invalid result reference: {}", err) + } + MethodError::Forbidden(err) => write!(f, "Forbidden: {}", err), + MethodError::AccountNotFound => write!(f, "Account not found"), + MethodError::AccountNotSupportedByMethod => { + write!(f, "Account not supported by method") + } + MethodError::AccountReadOnly => write!(f, "Account read only"), + MethodError::NotFound => write!(f, "Not found"), + } + } +} + +impl Serialize for MethodError { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + let mut map = serializer.serialize_map(2.into())?; + + let (error_type, description) = match self { + MethodError::InvalidArguments(description) => { + ("invalidArguments", description.as_str()) + } + MethodError::RequestTooLarge => ( + "requestTooLarge", + concat!( + "The number of ids requested by the client exceeds the maximum number ", + "the server is willing to process in a single method call." + ), + ), + MethodError::StateMismatch => ( + "stateMismatch", + concat!( + "An \"ifInState\" argument was supplied, but ", + "it does not match the current state." + ), + ), + MethodError::AnchorNotFound => ( + "anchorNotFound", + concat!( + "An anchor argument was supplied, but it ", + "cannot be found in the results of the query." + ), + ), + MethodError::UnsupportedFilter(description) => { + ("unsupportedFilter", description.as_str()) + } + MethodError::UnsupportedSort(description) => ("unsupportedSort", description.as_str()), + MethodError::ServerFail(_) => ("serverFail", { + concat!( + "An unexpected error occurred while processing ", + "this call, please contact the system administrator." + ) + }), + MethodError::NotFound => ("serverPartialFail", { + concat!( + "One or more items are no longer available on the ", + "server, please try again." + ) + }), + MethodError::UnknownMethod(description) => ("unknownMethod", description.as_str()), + MethodError::ServerUnavailable => ( + "serverUnavailable", + concat!( + "This server is temporarily unavailable. ", + "Attempting this same operation later may succeed." + ), + ), + MethodError::ServerPartialFail => ( + "serverPartialFail", + concat!( + "Some, but not all, expected changes described by the method ", + "occurred. Please resynchronize to determine server state." + ), + ), + MethodError::InvalidResultReference(description) => { + ("invalidResultReference", description.as_str()) + } + MethodError::Forbidden(description) => ("forbidden", description.as_str()), + MethodError::AccountNotFound => ( + "accountNotFound", + "The accountId does not correspond to a valid account", + ), + MethodError::AccountNotSupportedByMethod => ( + "accountNotSupportedByMethod", + concat!( + "The accountId given corresponds to a valid account, ", + "but the account does not support this method or data type." + ), + ), + MethodError::AccountReadOnly => ( + "accountReadOnly", + "This method modifies state, but the account is read-only.", + ), + }; + + map.serialize_entry("type", error_type)?; + if !description.is_empty() { + map.serialize_entry("description", description)?; + } + map.end() + } +} diff --git a/crates/protocol/src/error/mod.rs b/crates/protocol/src/error/mod.rs new file mode 100644 index 00000000..a5bd9d23 --- /dev/null +++ b/crates/protocol/src/error/mod.rs @@ -0,0 +1,26 @@ +/* + * Copyright (c) 2020-2022, Stalwart Labs Ltd. + * + * This file is part of the Stalwart JMAP Server. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * in the LICENSE file at the top-level directory of this distribution. + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + * You can be released from the requirements of the AGPLv3 license by + * purchasing a commercial license. Please contact licensing@stalw.art + * for more details. +*/ + +pub mod method; +pub mod request; +pub mod set; diff --git a/crates/protocol/src/error/request.rs b/crates/protocol/src/error/request.rs new file mode 100644 index 00000000..102224f1 --- /dev/null +++ b/crates/protocol/src/error/request.rs @@ -0,0 +1,187 @@ +use std::{borrow::Cow, fmt::Display}; + +#[derive(Debug, Clone, Copy, serde::Serialize)] +pub enum RequestLimitError { + #[serde(rename(serialize = "maxSizeRequest"))] + Size, + #[serde(rename(serialize = "maxCallsInRequest"))] + CallsIn, + #[serde(rename(serialize = "maxConcurrentRequests"))] + Concurrent, +} + +#[derive(Debug, serde::Serialize)] +pub enum RequestErrorType { + #[serde(rename(serialize = "urn:ietf:params:jmap:error:unknownCapability"))] + UnknownCapability, + #[serde(rename(serialize = "urn:ietf:params:jmap:error:notJSON"))] + NotJSON, + #[serde(rename(serialize = "urn:ietf:params:jmap:error:notRequest"))] + NotRequest, + #[serde(rename(serialize = "urn:ietf:params:jmap:error:limit"))] + Limit, + #[serde(rename(serialize = "about:blank"))] + Other, +} + +#[derive(Debug, serde::Serialize)] +pub struct RequestError { + #[serde(rename(serialize = "type"))] + pub p_type: RequestErrorType, + pub status: u16, + #[serde(skip_serializing_if = "Option::is_none")] + pub title: Option>, + pub detail: Cow<'static, str>, + #[serde(skip_serializing_if = "Option::is_none")] + pub limit: Option, +} + +impl RequestError { + pub fn blank( + status: u16, + title: impl Into>, + detail: impl Into>, + ) -> Self { + RequestError { + p_type: RequestErrorType::Other, + status, + title: Some(title.into()), + detail: detail.into(), + limit: None, + } + } + + pub fn internal_server_error() -> Self { + RequestError::blank( + 500, + "Internal Server Error", + concat!( + "There was a problem while processing your request. ", + "Please contact the system administrator." + ), + ) + } + + pub fn unavailable() -> Self { + RequestError::blank( + 503, + "Temporarily Unavailable", + concat!( + "There was a temporary problem while processing your request. ", + "Please try again in a few moments." + ), + ) + } + + pub fn invalid_parameters() -> Self { + RequestError::blank( + 400, + "Invalid Parameters", + "One or multiple parameters could not be parsed.", + ) + } + + pub fn forbidden() -> Self { + RequestError::blank( + 403, + "Forbidden", + "You do not have enough permissions to access this resource.", + ) + } + + pub fn too_many_requests() -> Self { + RequestError::blank( + 429, + "Too Many Requests", + "Your request has been rate limited. Please try again in a few seconds.", + ) + } + + pub fn too_many_auth_attempts() -> Self { + RequestError::blank( + 429, + "Too Many Authentication Attempts", + "Your request has been rate limited. Please try again in a few minutes.", + ) + } + + pub fn limit(limit_type: RequestLimitError) -> Self { + RequestError { + p_type: RequestErrorType::Limit, + status: 400, + title: None, + detail: match limit_type { + RequestLimitError::Size => concat!( + "The request is larger than the server ", + "is willing to process." + ), + RequestLimitError::CallsIn => concat!( + "The request exceeds the maximum number ", + "of calls in a single request." + ), + RequestLimitError::Concurrent => concat!( + "The request exceeds the maximum number ", + "of concurrent requests." + ), + } + .into(), + limit: Some(limit_type), + } + } + + pub fn not_found() -> Self { + RequestError::blank( + 404, + "Not Found", + "The requested resource does not exist on this server.", + ) + } + + pub fn unauthorized() -> Self { + RequestError::blank(401, "Unauthorized", "You have to authenticate first.") + } + + pub fn unknown_capability(capability: &str) -> RequestError { + RequestError { + p_type: RequestErrorType::UnknownCapability, + limit: None, + title: None, + status: 400, + detail: format!( + concat!( + "The Request object used capability ", + "'{}', which is not supported", + "by this server." + ), + capability + ) + .into(), + } + } + + pub fn not_json(detail: &str) -> RequestError { + RequestError { + p_type: RequestErrorType::NotJSON, + limit: None, + title: None, + status: 400, + detail: format!("Failed to parse JSON: {detail}").into(), + } + } + + pub fn not_request(detail: impl Into>) -> RequestError { + RequestError { + p_type: RequestErrorType::NotRequest, + limit: None, + title: None, + status: 400, + detail: detail.into(), + } + } +} + +impl Display for RequestError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str(&self.detail) + } +} diff --git a/crates/protocol/src/error/set.rs b/crates/protocol/src/error/set.rs new file mode 100644 index 00000000..d7a9e267 --- /dev/null +++ b/crates/protocol/src/error/set.rs @@ -0,0 +1,173 @@ +/* + * Copyright (c) 2020-2022, Stalwart Labs Ltd. + * + * This file is part of the Stalwart JMAP Server. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * in the LICENSE file at the top-level directory of this distribution. + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + * You can be released from the requirements of the AGPLv3 license by + * purchasing a commercial license. Please contact licensing@stalw.art + * for more details. +*/ + +use std::borrow::Cow; + +use crate::types::{id::Id, property::Property}; + +#[derive(Debug, Clone, serde::Serialize)] +pub struct SetError { + #[serde(rename = "type")] + pub type_: SetErrorType, + + #[serde(skip_serializing_if = "Option::is_none")] + description: Option>, + + #[serde(skip_serializing_if = "Option::is_none")] + properties: Option>, + + #[serde(rename = "existingId")] + #[serde(skip_serializing_if = "Option::is_none")] + existing_id: Option, +} + +#[derive(Debug, Clone, serde::Serialize)] +pub enum SetErrorType { + #[serde(rename = "forbidden")] + Forbidden, + #[serde(rename = "overQuota")] + OverQuota, + #[serde(rename = "tooLarge")] + TooLarge, + #[serde(rename = "rateLimit")] + RateLimit, + #[serde(rename = "notFound")] + NotFound, + #[serde(rename = "invalidPatch")] + InvalidPatch, + #[serde(rename = "willDestroy")] + WillDestroy, + #[serde(rename = "invalidProperties")] + InvalidProperties, + #[serde(rename = "singleton")] + Singleton, + #[serde(rename = "mailboxHasChild")] + MailboxHasChild, + #[serde(rename = "mailboxHasEmail")] + MailboxHasEmail, + #[serde(rename = "blobNotFound")] + BlobNotFound, + #[serde(rename = "tooManyKeywords")] + TooManyKeywords, + #[serde(rename = "tooManyMailboxes")] + TooManyMailboxes, + #[serde(rename = "forbiddenFrom")] + ForbiddenFrom, + #[serde(rename = "invalidEmail")] + InvalidEmail, + #[serde(rename = "tooManyRecipients")] + TooManyRecipients, + #[serde(rename = "noRecipients")] + NoRecipients, + #[serde(rename = "invalidRecipients")] + InvalidRecipients, + #[serde(rename = "forbiddenMailFrom")] + ForbiddenMailFrom, + #[serde(rename = "forbiddenToSend")] + ForbiddenToSend, + #[serde(rename = "cannotUnsend")] + CannotUnsend, + #[serde(rename = "alreadyExists")] + AlreadyExists, + #[serde(rename = "invalidScript")] + InvalidScript, + #[serde(rename = "scriptIsActive")] + ScriptIsActive, +} + +impl SetErrorType { + pub fn as_str(&self) -> &'static str { + match self { + SetErrorType::Forbidden => "forbidden", + SetErrorType::OverQuota => "overQuota", + SetErrorType::TooLarge => "tooLarge", + SetErrorType::RateLimit => "rateLimit", + SetErrorType::NotFound => "notFound", + SetErrorType::InvalidPatch => "invalidPatch", + SetErrorType::WillDestroy => "willDestroy", + SetErrorType::InvalidProperties => "invalidProperties", + SetErrorType::Singleton => "singleton", + SetErrorType::BlobNotFound => "blobNotFound", + SetErrorType::MailboxHasChild => "mailboxHasChild", + SetErrorType::MailboxHasEmail => "mailboxHasEmail", + SetErrorType::TooManyKeywords => "tooManyKeywords", + SetErrorType::TooManyMailboxes => "tooManyMailboxes", + SetErrorType::ForbiddenFrom => "forbiddenFrom", + SetErrorType::InvalidEmail => "invalidEmail", + SetErrorType::TooManyRecipients => "tooManyRecipients", + SetErrorType::NoRecipients => "noRecipients", + SetErrorType::InvalidRecipients => "invalidRecipients", + SetErrorType::ForbiddenMailFrom => "forbiddenMailFrom", + SetErrorType::ForbiddenToSend => "forbiddenToSend", + SetErrorType::CannotUnsend => "cannotUnsend", + SetErrorType::AlreadyExists => "alreadyExists", + SetErrorType::InvalidScript => "invalidScript", + SetErrorType::ScriptIsActive => "scriptIsActive", + } + } +} + +impl SetError { + pub fn new(type_: SetErrorType) -> Self { + SetError { + type_, + description: None, + properties: None, + existing_id: None, + } + } + + pub fn with_description(mut self, description: impl Into>) -> Self { + self.description = description.into().into(); + self + } + + pub fn with_property(mut self, property: Property) -> Self { + self.properties = vec![property].into(); + self + } + + pub fn with_properties(mut self, properties: impl IntoIterator) -> Self { + self.properties = properties.into_iter().collect::>().into(); + self + } + + pub fn with_existing_id(mut self, id: Id) -> Self { + self.existing_id = id.into(); + self + } + + pub fn invalid_properties() -> Self { + Self::new(SetErrorType::InvalidProperties) + } + + pub fn forbidden() -> Self { + Self::new(SetErrorType::Forbidden) + } + + pub fn already_exists() -> Self { + Self::new(SetErrorType::AlreadyExists) + } +} + +pub type Result = std::result::Result; diff --git a/crates/protocol/src/lib.rs b/crates/protocol/src/lib.rs new file mode 100644 index 00000000..b7383c72 --- /dev/null +++ b/crates/protocol/src/lib.rs @@ -0,0 +1,109 @@ +pub mod error; +pub mod method; +pub mod object; +pub mod parser; +pub mod request; +pub mod types; + +#[cfg(test)] +mod tests { + use std::{collections::BTreeMap, sync::Arc}; + + #[test] + fn gen_hash() { + //let mut table = BTreeMap::new(); + for value in ["blobIds", "ifInState", "emails"] { + let mut hash = 0; + let mut shift = 0; + let lower_first = false; + + for (pos, &ch) in value.as_bytes().iter().take(16).enumerate() { + if pos == 0 && lower_first { + hash |= (ch.to_ascii_lowercase() as u128) << shift; + } else { + hash |= (ch as u128) << shift; + } + shift += 8; + } + + shift = 0; + let mut hash2 = 0; + for &ch in value.as_bytes().iter().skip(16).take(16) { + hash2 |= (ch as u128) << shift; + shift += 8; + } + + println!( + "0x{} => {{}} // {}", + format!("{hash:x}") + .as_bytes() + .chunks(4) + .into_iter() + .map(|s| std::str::from_utf8(s).unwrap()) + .collect::>() + .join("_"), + value + ); + /*println!( + "(0x{}, 0x{}) => Filter::{}(),", + format!("{hash:x}") + .as_bytes() + .chunks(4) + .into_iter() + .map(|s| std::str::from_utf8(s).unwrap()) + .collect::>() + .join("_"), + format!("{hash2:x}") + .as_bytes() + .chunks(4) + .into_iter() + .map(|s| std::str::from_utf8(s).unwrap()) + .collect::>() + .join("_"), + value + );*/ + + /*let mut hash = 0; + let mut shift = 0; + let mut first_ch = 0; + let mut name = Vec::new(); + + for (pos, &ch) in value.as_bytes().iter().take(16).enumerate() { + if pos == 0 { + first_ch = ch.to_ascii_lowercase(); + name.push(ch.to_ascii_uppercase()); + } else { + hash |= (ch as u128) << shift; + shift += 8; + name.push(ch); + } + } + + //println!("Property::{} => {{}}", std::str::from_utf8(&name).unwrap()); + + table + .entry(first_ch) + .or_insert_with(|| vec![]) + .push((hash, name));*/ + } + + /*for (k, v) in table { + println!("b'{}' => match hash {{", k as char); + for (hash, value) in v { + println!( + " 0x{} => Property::{},", + format!("{hash:x}") + .as_bytes() + .chunks(4) + .into_iter() + .map(|s| std::str::from_utf8(s).unwrap()) + .collect::>() + .join("_"), + std::str::from_utf8(&value).unwrap() + ); + } + println!(" _ => parser.invalid_property()?,"); + println!("}}"); + }*/ + } +} diff --git a/crates/protocol/src/method/changes.rs b/crates/protocol/src/method/changes.rs new file mode 100644 index 00000000..52ce3fbd --- /dev/null +++ b/crates/protocol/src/method/changes.rs @@ -0,0 +1,105 @@ +use crate::{ + error::method::MethodError, + parser::{json::Parser, Error, Ignore, JsonObjectParser, Token}, + request::{method::MethodObject, RequestProperty}, + types::{id::Id, property::Property, state::State}, +}; + +#[derive(Debug, Clone)] +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 { + #[serde(rename = "accountId")] + pub account_id: Id, + + #[serde(rename = "oldState")] + pub old_state: State, + + #[serde(rename = "newState")] + pub new_state: State, + + #[serde(rename = "hasMoreChanges")] + pub has_more_changes: bool, + + pub created: Vec, + + pub updated: Vec, + + pub destroyed: Vec, + + #[serde(rename = "updatedProperties")] + #[serde(skip_serializing_if = "Option::is_none")] + updated_properties: Option>, +} + +#[derive(Debug, Clone, serde::Serialize)] +pub enum RequestArguments { + Email, + Mailbox, + Thread, + Identity, + EmailSubmission, +} + +impl JsonObjectParser for ChangesRequest { + fn parse(parser: &mut Parser<'_>) -> crate::parser::Result + where + Self: Sized, + { + let mut request = ChangesRequest { + arguments: match &parser.ctx { + MethodObject::Email => RequestArguments::Email, + MethodObject::Mailbox => RequestArguments::Mailbox, + MethodObject::Thread => RequestArguments::Thread, + MethodObject::Identity => RequestArguments::Identity, + MethodObject::EmailSubmission => RequestArguments::EmailSubmission, + _ => { + return Err(Error::Method(MethodError::UnknownMethod(format!( + "{}/changes", + parser.ctx + )))) + } + }, + account_id: Id::default(), + since_state: State::Initial, + max_changes: None, + }; + + parser + .next_token::()? + .assert_jmap(Token::DictStart)?; + + while { + let property = parser.next_dict_key::()?; + match &property.hash[0] { + 0x6449_746e_756f_6363_61 => { + request.account_id = parser.next_token::()?.unwrap_string("accountId")?; + } + 0x6574_6174_5365_636e_6973 => { + request.since_state = parser + .next_token::()? + .unwrap_string("sinceQueryState")?; + } + 0x7365_676e_6168_4378_616d => { + request.max_changes = parser + .next_token::()? + .unwrap_usize_or_null("maxChanges")?; + } + + _ => { + parser.skip_token(parser.depth_array, parser.depth_dict)?; + } + } + + !parser.is_dict_end()? + } {} + + Ok(request) + } +} diff --git a/crates/protocol/src/method/copy.rs b/crates/protocol/src/method/copy.rs new file mode 100644 index 00000000..d103c8d3 --- /dev/null +++ b/crates/protocol/src/method/copy.rs @@ -0,0 +1,195 @@ +use serde::Serialize; +use utils::map::vec_map::VecMap; + +use crate::{ + error::{method::MethodError, set::SetError}, + object::Object, + parser::{json::Parser, Error, JsonObjectParser, Token}, + request::{method::MethodObject, reference::MaybeReference, RequestProperty}, + types::{ + blob::BlobId, + id::Id, + state::State, + value::{SetValue, Value}, + }, +}; + +#[derive(Debug, Clone)] +pub struct CopyRequest { + 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 on_success_destroy_original: Option, + pub destroy_from_if_in_state: Option, + pub arguments: RequestArguments, +} + +#[derive(Debug, Clone, serde::Serialize)] +pub struct CopyResponse { + #[serde(rename = "fromAccountId")] + pub from_account_id: Id, + + #[serde(rename = "accountId")] + pub account_id: Id, + + #[serde(rename = "oldState")] + pub old_state: State, + + #[serde(rename = "newState")] + pub new_state: State, + + #[serde(rename = "created")] + #[serde(skip_serializing_if = "VecMap::is_empty")] + pub created: VecMap>, + + #[serde(rename = "notCreated")] + #[serde(skip_serializing_if = "VecMap::is_empty")] + pub not_created: VecMap, +} + +#[derive(Debug, Clone)] +pub struct CopyBlobRequest { + pub from_account_id: Id, + pub account_id: Id, + pub blob_ids: Vec, +} + +#[derive(Debug, Clone, Serialize)] +pub struct CopyBlobResponse { + #[serde(rename = "fromAccountId")] + pub from_account_id: Id, + + #[serde(rename = "accountId")] + pub account_id: Id, + + #[serde(rename = "copied")] + #[serde(skip_serializing_if = "Option::is_none")] + pub copied: Option>, + + #[serde(rename = "notCopied")] + #[serde(skip_serializing_if = "Option::is_none")] + pub not_copied: Option>, +} + +#[derive(Debug, Clone)] +pub enum RequestArguments { + Email, +} + +impl JsonObjectParser for CopyRequest { + fn parse(parser: &mut Parser) -> crate::parser::Result + where + Self: Sized, + { + let mut request = CopyRequest { + arguments: match &parser.ctx { + MethodObject::Email => RequestArguments::Email, + _ => { + return Err(Error::Method(MethodError::UnknownMethod(format!( + "{}/copy", + parser.ctx + )))) + } + }, + account_id: Id::default(), + if_in_state: None, + from_account_id: Id::default(), + if_from_in_state: None, + create: VecMap::default(), + on_success_destroy_original: None, + destroy_from_if_in_state: None, + }; + + parser + .next_token::()? + .assert_jmap(Token::DictStart)?; + + while { + let property = parser.next_dict_key::()?; + match &property.hash[0] { + 0x6449_746e_756f_6363_61 => { + request.account_id = parser.next_token::()?.unwrap_string("accountId")?; + } + 0x6574_6165_7263 => { + request.create = + , Object>>::parse(parser)?; + } + 0x6449_746e_756f_6363_416d_6f72_66 => { + request.from_account_id = + parser.next_token::()?.unwrap_string("fromAccountId")?; + } + 0x6574_6174_536e_496d_6f72_4666_69 => { + request.if_from_in_state = parser + .next_token::()? + .unwrap_string_or_null("ifFromInState")?; + } + 0x796f_7274_7365_4473_7365_6363_7553_6e6f => { + request.on_success_destroy_original = parser + .next_token::()? + .unwrap_bool_or_null("onSuccessDestroyOriginal")?; + } + 0x536e_4966_496d_6f72_4679_6f72_7473_6564 => { + request.destroy_from_if_in_state = parser + .next_token::()? + .unwrap_string_or_null("destroyFromIfInState")?; + } + 0x6574_6174_536e_4966_69 => { + request.if_in_state = parser + .next_token::()? + .unwrap_string_or_null("ifInState")?; + } + _ => { + parser.skip_token(parser.depth_array, parser.depth_dict)?; + } + } + + !parser.is_dict_end()? + } {} + + Ok(request) + } +} + +impl JsonObjectParser for CopyBlobRequest { + fn parse(parser: &mut Parser) -> crate::parser::Result + where + Self: Sized, + { + let mut request = CopyBlobRequest { + account_id: Id::default(), + from_account_id: Id::default(), + blob_ids: Vec::new(), + }; + + parser + .next_token::()? + .assert_jmap(Token::DictStart)?; + + while { + let property = parser.next_dict_key::()?; + match &property.hash[0] { + 0x6449_746e_756f_6363_61 => { + request.account_id = parser.next_token::()?.unwrap_string("accountId")?; + } + 0x6449_746e_756f_6363_416d_6f72_66 => { + request.from_account_id = + parser.next_token::()?.unwrap_string("fromAccountId")?; + } + 0x7364_4962_6f6c_62 => { + request.blob_ids = parser + .next_token::>()? + .unwrap_string("blobIds")?; + } + _ => { + parser.skip_token(parser.depth_array, parser.depth_dict)?; + } + } + + !parser.is_dict_end()? + } {} + + Ok(request) + } +} diff --git a/crates/protocol/src/method/get.rs b/crates/protocol/src/method/get.rs new file mode 100644 index 00000000..dc1e5def --- /dev/null +++ b/crates/protocol/src/method/get.rs @@ -0,0 +1,126 @@ +use crate::{ + error::method::MethodError, + object::{email, Object}, + parser::{json::Parser, Error, JsonObjectParser, Token}, + request::{ + method::MethodObject, + reference::{MaybeReference, ResultReference}, + RequestProperty, RequestPropertyParser, + }, + types::{id::Id, property::Property, state::State, value::Value}, +}; + +#[derive(Debug, Clone)] +pub struct GetRequest { + pub account_id: Id, + pub ids: Option, ResultReference>>, + pub properties: Option, ResultReference>>, + pub arguments: RequestArguments, +} + +#[derive(Debug, Clone)] +pub enum RequestArguments { + Email(email::GetArguments), + Mailbox, + Thread, + Identity, + EmailSubmission, + PushSubscription, + SieveScript, + VacationResponse, + Principal, +} + +#[derive(Debug, Clone, serde::Serialize)] +pub struct GetResponse { + #[serde(rename = "accountId")] + #[serde(skip_serializing_if = "Option::is_none")] + pub account_id: Option, + + pub state: State, + + pub list: Vec>, + + #[serde(rename = "notFound")] + pub not_found: Vec, +} + +impl JsonObjectParser for GetRequest { + fn parse(parser: &mut Parser<'_>) -> crate::parser::Result + where + Self: Sized, + { + let mut request = GetRequest { + arguments: match &parser.ctx { + MethodObject::Email => RequestArguments::Email(Default::default()), + MethodObject::Mailbox => RequestArguments::Mailbox, + MethodObject::Thread => RequestArguments::Thread, + MethodObject::Identity => RequestArguments::Identity, + MethodObject::EmailSubmission => RequestArguments::EmailSubmission, + MethodObject::PushSubscription => RequestArguments::PushSubscription, + MethodObject::SieveScript => RequestArguments::SieveScript, + MethodObject::VacationResponse => RequestArguments::VacationResponse, + MethodObject::Principal => RequestArguments::Principal, + _ => { + return Err(Error::Method(MethodError::UnknownMethod(format!( + "{}/get", + parser.ctx + )))) + } + }, + account_id: Id::default(), + ids: None, + properties: None, + }; + + parser + .next_token::()? + .assert_jmap(Token::DictStart)?; + + while { + let property = parser.next_dict_key::()?; + match &property.hash[0] { + 0x6449_746e_756f_6363_61 if !property.is_ref => { + request.account_id = parser.next_token::()?.unwrap_string("accountId")?; + } + 0x7364_69 => { + request.ids = if !property.is_ref { + >>::parse(parser)?.map(MaybeReference::Value) + } else { + Some(MaybeReference::Reference(ResultReference::parse(parser)?)) + }; + } + 0x7365_6974_7265_706f_7270 => { + request.properties = if !property.is_ref { + >>::parse(parser)?.map(MaybeReference::Value) + } else { + Some(MaybeReference::Reference(ResultReference::parse(parser)?)) + }; + } + _ => { + if !request.arguments.parse(parser, property)? { + parser.skip_token(parser.depth_array, parser.depth_dict)?; + } + } + } + + !parser.is_dict_end()? + } {} + + Ok(request) + } +} + +impl RequestPropertyParser for RequestArguments { + fn parse( + &mut self, + parser: &mut Parser, + property: RequestProperty, + ) -> crate::parser::Result { + if let RequestArguments::Email(arguments) = self { + arguments.parse(parser, property) + } else { + Ok(false) + } + } +} diff --git a/crates/protocol/src/method/import.rs b/crates/protocol/src/method/import.rs new file mode 100644 index 00000000..308baead --- /dev/null +++ b/crates/protocol/src/method/import.rs @@ -0,0 +1,147 @@ +use utils::map::vec_map::VecMap; + +use crate::{ + error::set::SetError, + object::Object, + parser::{json::Parser, JsonObjectParser, Token}, + request::{ + reference::{MaybeReference, ResultReference}, + RequestProperty, + }, + types::{ + blob::BlobId, + date::UTCDate, + id::Id, + keyword::Keyword, + state::State, + value::{SetValueMap, Value}, + }, +}; + +#[derive(Debug, Clone)] +pub struct ImportEmailRequest { + pub account_id: Id, + pub if_in_state: Option, + pub emails: VecMap, +} + +#[derive(Debug, Clone)] +pub struct EmailImport { + pub blob_id: BlobId, + pub mailbox_ids: Option>, ResultReference>>, + pub keywords: Vec, + pub received_at: Option, +} + +#[derive(Debug, Clone, serde::Serialize)] +pub struct EmailImportResponse { + #[serde(rename = "accountId")] + pub account_id: Id, + + #[serde(rename = "oldState")] + #[serde(skip_serializing_if = "Option::is_none")] + pub old_state: Option, + + #[serde(rename = "newState")] + pub new_state: State, + + #[serde(rename = "created")] + #[serde(skip_serializing_if = "Option::is_none")] + pub created: Option>>, + + #[serde(rename = "notCreated")] + #[serde(skip_serializing_if = "Option::is_none")] + pub not_created: Option>, +} + +impl JsonObjectParser for ImportEmailRequest { + fn parse(parser: &mut Parser<'_>) -> crate::parser::Result + where + Self: Sized, + { + let mut request = ImportEmailRequest { + account_id: Id::default(), + if_in_state: None, + emails: VecMap::new(), + }; + + parser + .next_token::()? + .assert_jmap(Token::DictStart)?; + + while { + let property = parser.next_dict_key::()?; + match &property.hash[0] { + 0x6449_746e_756f_6363_61 if !property.is_ref => { + request.account_id = parser.next_token::()?.unwrap_string("accountId")?; + } + 0x6574_6174_536e_4966_69 if !property.is_ref => { + request.if_in_state = parser + .next_token::()? + .unwrap_string_or_null("ifInState")?; + } + 0x736c_6961_6d65 if !property.is_ref => { + request.emails = >::parse(parser)?; + } + _ => { + parser.skip_token(parser.depth_array, parser.depth_dict)?; + } + } + + !parser.is_dict_end()? + } {} + + Ok(request) + } +} + +impl JsonObjectParser for EmailImport { + fn parse(parser: &mut Parser<'_>) -> crate::parser::Result + where + Self: Sized, + { + let mut request = EmailImport { + blob_id: BlobId::default(), + mailbox_ids: None, + keywords: vec![], + received_at: None, + }; + + parser + .next_token::()? + .assert_jmap(Token::DictStart)?; + + while { + let property = parser.next_dict_key::()?; + match &property.hash[0] { + 0x6449_626f_6c62 if !property.is_ref => { + request.blob_id = parser.next_token::()?.unwrap_string("blobId")?; + } + 0x7364_4978_6f62_6c69_616d => { + request.mailbox_ids = if !property.is_ref { + Some(MaybeReference::Value( + >>::parse(parser)?.values, + )) + } else { + Some(MaybeReference::Reference(ResultReference::parse(parser)?)) + }; + } + 0x7364_726f_7779_656b if !property.is_ref => { + request.keywords = >::parse(parser)?.values; + } + 0x7441_6465_7669_6563_6572 if !property.is_ref => { + request.received_at = parser + .next_token::()? + .unwrap_string_or_null("receivedAt")?; + } + _ => { + parser.skip_token(parser.depth_array, parser.depth_dict)?; + } + } + + !parser.is_dict_end()? + } {} + + Ok(request) + } +} diff --git a/crates/protocol/src/method/mod.rs b/crates/protocol/src/method/mod.rs new file mode 100644 index 00000000..3c10e859 --- /dev/null +++ b/crates/protocol/src/method/mod.rs @@ -0,0 +1,17 @@ +use ahash::AHashMap; + +pub mod changes; +pub mod copy; +pub mod get; +pub mod import; +pub mod parse; +pub mod query; +pub mod query_changes; +pub mod search_snippet; +pub mod set; +pub mod validate; + +#[inline(always)] +pub fn ahash_is_empty(map: &AHashMap) -> bool { + map.is_empty() +} diff --git a/crates/protocol/src/method/parse.rs b/crates/protocol/src/method/parse.rs new file mode 100644 index 00000000..aa1959dd --- /dev/null +++ b/crates/protocol/src/method/parse.rs @@ -0,0 +1,105 @@ +use utils::map::vec_map::VecMap; + +use crate::{ + object::Object, + parser::{json::Parser, Ignore, JsonObjectParser, Token}, + request::RequestProperty, + types::{blob::BlobId, id::Id, property::Property, value::Value}, +}; + +#[derive(Debug, Clone)] +pub struct ParseEmailRequest { + pub account_id: Id, + blob_ids: Vec, + properties: Option>, + body_properties: Option>, + fetch_text_body_values: Option, + fetch_html_body_values: Option, + fetch_all_body_values: Option, + max_body_value_bytes: Option, +} + +#[derive(Debug, Clone, serde::Serialize)] +pub struct EmailParseResponse { + #[serde(rename = "accountId")] + account_id: Id, + + #[serde(rename = "parsed")] + #[serde(skip_serializing_if = "VecMap::is_empty")] + parsed: VecMap>, + + #[serde(rename = "notParsable")] + #[serde(skip_serializing_if = "Vec::is_empty")] + not_parsable: Vec, + + #[serde(rename = "notFound")] + #[serde(skip_serializing_if = "Vec::is_empty")] + not_found: Vec, +} + +impl JsonObjectParser for ParseEmailRequest { + fn parse(parser: &mut Parser<'_>) -> crate::parser::Result + where + Self: Sized, + { + let mut request = ParseEmailRequest { + account_id: Id::default(), + properties: None, + blob_ids: vec![], + body_properties: None, + fetch_text_body_values: None, + fetch_html_body_values: None, + fetch_all_body_values: None, + max_body_value_bytes: None, + }; + + parser + .next_token::()? + .assert_jmap(Token::DictStart)?; + + while { + let property = parser.next_dict_key::()?; + match (&property.hash[0], &property.hash[1]) { + (0x6449_746e_756f_6363_61, _) if !property.is_ref => { + request.account_id = parser.next_token::()?.unwrap_string("accountId")?; + } + (0x7364_4962_6f6c_62, _) => { + request.blob_ids = >::parse(parser)?; + } + (0x7365_6974_7265_706f_7270, _) => { + request.properties = >>::parse(parser)?; + } + (0x7365_6974_7265_706f_7250_7964_6f62, _) => { + request.body_properties = >>::parse(parser)?; + } + (0x6c61_5679_646f_4274_7865_5468_6374_6566, 0x7365_75) => { + request.fetch_text_body_values = parser + .next_token::()? + .unwrap_bool_or_null("fetchTextBodyValues")?; + } + (0x6c61_5679_646f_424c_4d54_4868_6374_6566, 0x7365_75) => { + request.fetch_html_body_values = parser + .next_token::()? + .unwrap_bool_or_null("fetchHTMLBodyValues")?; + } + (0x756c_6156_7964_6f42_6c6c_4168_6374_6566, 0x7365) => { + request.fetch_all_body_values = parser + .next_token::()? + .unwrap_bool_or_null("fetchAllBodyValues")?; + } + (0x6574_7942_6575_6c61_5679_646f_4278_616d, 0x73) => { + request.max_body_value_bytes = parser + .next_token::()? + .unwrap_usize_or_null("maxBodyValueBytes")?; + } + _ => { + parser.skip_token(parser.depth_array, parser.depth_dict)?; + } + } + + !parser.is_dict_end()? + } {} + + Ok(request) + } +} diff --git a/crates/protocol/src/method/query.rs b/crates/protocol/src/method/query.rs new file mode 100644 index 00000000..ba541a94 --- /dev/null +++ b/crates/protocol/src/method/query.rs @@ -0,0 +1,596 @@ +use std::fmt::Display; + +use crate::{ + error::method::MethodError, + object::{email, mailbox}, + parser::{json::Parser, Error, Ignore, JsonObjectParser, Token}, + request::{method::MethodObject, RequestProperty, RequestPropertyParser}, + types::{date::UTCDate, id::Id, keyword::Keyword, state::State}, +}; + +#[derive(Debug, Clone)] +pub struct QueryRequest { + pub account_id: Id, + 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: RequestArguments, +} + +#[derive(Debug, Clone, serde::Serialize)] +pub struct QueryResponse { + #[serde(rename = "accountId")] + pub account_id: Id, + + #[serde(rename = "queryState")] + pub query_state: State, + + #[serde(rename = "canCalculateChanges")] + pub can_calculate_changes: bool, + + #[serde(rename = "position")] + pub position: i32, + + #[serde(rename = "ids")] + pub ids: Vec, + + #[serde(rename = "total")] + #[serde(skip_serializing_if = "Option::is_none")] + pub total: Option, + + #[serde(rename = "limit")] + #[serde(skip_serializing_if = "Option::is_none")] + pub limit: Option, +} + +#[derive(Clone, Debug)] +pub enum Filter { + Email(String), + Name(String), + DomainName(String), + Text(String), + Type(String), + Timezone(String), + Members(Id), + QuotaLt(u64), + QuotaGt(u64), + IdentityIds(Vec), + EmailIds(Vec), + ThreadIds(Vec), + UndoStatus(String), + Before(UTCDate), + After(UTCDate), + InMailbox(Id), + InMailboxOtherThan(Vec), + MinSize(u64), + MaxSize(u64), + AllInThreadHaveKeyword(Keyword), + SomeInThreadHaveKeyword(Keyword), + NoneInThreadHaveKeyword(Keyword), + HasKeyword(Keyword), + NotKeyword(Keyword), + HasAttachment(bool), + From(String), + To(String), + Cc(String), + Bcc(String), + Subject(String), + Body(String), + Header(Vec), + Id(Vec), + SentBefore(UTCDate), + SentAfter(UTCDate), + InThread(Id), + ParentId(Option), + Role(Option), + HasAnyRole(bool), + IsSubscribed(bool), + IsActive(bool), + _T(String), + + And, + Or, + Not, + Close, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Comparator { + pub is_ascending: bool, + pub collation: Option, + pub property: SortProperty, + pub keyword: Option, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum SortProperty { + Type, + Name, + Email, + EmailId, + ThreadId, + SentAt, + ReceivedAt, + Size, + From, + To, + Subject, + Cc, + SortOrder, + ParentId, + IsActive, + HasKeyword, + AllInThreadHaveKeyword, + SomeInThreadHaveKeyword, + _T(String), +} + +#[derive(Debug, Clone)] +pub enum RequestArguments { + Email(email::QueryArguments), + Mailbox(mailbox::QueryArguments), + EmailSubmission, + SieveScript, + Principal, +} + +impl JsonObjectParser for QueryRequest { + fn parse(parser: &mut Parser<'_>) -> crate::parser::Result + where + Self: Sized, + { + let mut request = QueryRequest { + arguments: match &parser.ctx { + MethodObject::Email => RequestArguments::Email(Default::default()), + MethodObject::Mailbox => RequestArguments::Mailbox(Default::default()), + MethodObject::EmailSubmission => RequestArguments::EmailSubmission, + MethodObject::SieveScript => RequestArguments::SieveScript, + MethodObject::Principal => RequestArguments::Principal, + _ => { + return Err(Error::Method(MethodError::UnknownMethod(format!( + "{}/query", + parser.ctx + )))) + } + }, + filter: vec![], + sort: None, + position: None, + anchor: None, + anchor_offset: None, + limit: None, + calculate_total: None, + account_id: Id::default(), + }; + + parser + .next_token::()? + .assert_jmap(Token::DictStart)?; + + while { + let property = parser.next_dict_key::()?; + match &property.hash[0] { + 0x6449_746e_756f_6363_61 => { + request.account_id = parser.next_token::()?.unwrap_string("accountId")?; + } + 0x7265_746c_6966 => match parser.next_token::()? { + Token::DictStart => { + request.filter = parse_filter(parser)?; + } + Token::Null => (), + token => { + return Err(token.error("filter", "object or null")); + } + }, + 0x7472_6f73 => { + request.sort = >>::parse(parser)?; + } + 0x6e6f_6974_6973_6f70 => { + request.position = parser + .next_token::()? + .unwrap_int_or_null("position")?; + } + 0x726f_6863_6e61 => { + request.anchor = parser.next_token::()?.unwrap_string_or_null("anchor")?; + } + 0x7465_7366_664f_726f_6863_6e61 => { + request.anchor_offset = parser + .next_token::()? + .unwrap_int_or_null("anchorOffset")? + } + 0x7469_6d69_6c => { + request.limit = parser + .next_token::()? + .unwrap_usize_or_null("limit")?; + } + 0x6c61_746f_5465_7461_6c75_636c_6163 => { + request.calculate_total = parser + .next_token::()? + .unwrap_bool_or_null("calculateTotal")?; + } + + _ => { + if !request.arguments.parse(parser, property)? { + parser.skip_token(parser.depth_array, parser.depth_dict)?; + } + } + } + + !parser.is_dict_end()? + } {} + + Ok(request) + } +} + +pub fn parse_filter(parser: &mut Parser) -> crate::parser::Result> { + let mut filter = vec![Filter::Close]; + let mut pos_stack = vec![0]; + + loop { + match parser.next_token::()? { + Token::String(property) => { + parser.next_token::()?.assert(Token::Colon)?; + filter[*pos_stack.last().unwrap()] = match &property.hash[0] { + 0x726f_7461_7265_706f => { + match parser.next_token::()?.unwrap_string("operator")? { + 0x444e_41 => Filter::And, + 0x524f => Filter::Or, + 0x544f_4e => Filter::Not, + _ => return Err(parser.error_value()), + } + } + 0x736e_6f69_7469_646e_6f63 => { + parser.next_token::()?.assert(Token::ArrayStart)?; + continue; + } + _ => match (&property.hash[0], &property.hash[1]) { + (0x6c69_616d_65, _) => { + Filter::Email(parser.next_token::()?.unwrap_string("email")?) + } + (0x656d_616e, _) => { + Filter::Name(parser.next_token::()?.unwrap_string("name")?) + } + (0x656d_614e_6e69_616d_6f64, _) => Filter::DomainName( + parser.next_token::()?.unwrap_string("domainName")?, + ), + (0x7478_6574, _) => { + Filter::Text(parser.next_token::()?.unwrap_string("text")?) + } + (0x6570_7974, _) => { + Filter::Type(parser.next_token::()?.unwrap_string("type")?) + } + (0x656e_6f7a_656d_6974, _) => Filter::Timezone( + parser.next_token::()?.unwrap_string("timezone")?, + ), + (0x7372_6562_6d65_6d, _) => { + Filter::Members(parser.next_token::()?.unwrap_string("members")?) + } + (0x6e61_6854_7265_776f_4c61_746f_7571, _) => Filter::QuotaLt( + parser + .next_token::()? + .unwrap_uint_or_null("quotaLowerThan")? + .unwrap_or_default(), + ), + (0x6e61_6854_7265_7461_6572_4761_746f_7571, _) => Filter::QuotaGt( + parser + .next_token::()? + .unwrap_uint_or_null("quotaGreaterThan")? + .unwrap_or_default(), + ), + (0x7364_4979_7469_746e_6564_69, _) => { + Filter::IdentityIds(>::parse(parser)?) + } + (0x7364_496c_6961_6d65, _) => Filter::EmailIds(>::parse(parser)?), + (0x7364_4964_6165_7268_74, _) => { + Filter::ThreadIds(>::parse(parser)?) + } + (0x7375_7461_7453_6f64_6e75, _) => Filter::UndoStatus( + parser.next_token::()?.unwrap_string("undoStatus")?, + ), + (0x6572_6f66_6562, _) => { + Filter::Before(parser.next_token::()?.unwrap_string("before")?) + } + (0x7265_7466_61, _) => { + Filter::After(parser.next_token::()?.unwrap_string("after")?) + } + (0x786f_626c_6961_4d6e_69, _) => Filter::InMailbox( + parser.next_token::()?.unwrap_string("inMailbox")?, + ), + (0x6854_7265_6874_4f78_6f62_6c69_614d_6e69, 0x6e61) => { + Filter::InMailboxOtherThan(>::parse(parser)?) + } + (0x657a_6953_6e69_6d, _) => Filter::MinSize( + parser + .next_token::()? + .unwrap_uint_or_null("minSize")? + .unwrap_or_default(), + ), + (0x657a_6953_7861_6d, _) => Filter::MaxSize( + parser + .next_token::()? + .unwrap_uint_or_null("maxSize")? + .unwrap_or_default(), + ), + (0x4b65_7661_4864_6165_7268_546e_496c_6c61, 0x6472_6f77_7965) => { + Filter::AllInThreadHaveKeyword( + parser + .next_token::()? + .unwrap_string("allInThreadHaveKeyword")?, + ) + } + (0x6576_6148_6461_6572_6854_6e49_656d_6f73, 0x6472_6f77_7965_4b) => { + Filter::SomeInThreadHaveKeyword( + parser + .next_token::()? + .unwrap_string("someInThreadHaveKeyword")?, + ) + } + (0x6576_6148_6461_6572_6854_6e49_656e_6f6e, 0x6472_6f77_7965_4b) => { + Filter::NoneInThreadHaveKeyword( + parser + .next_token::()? + .unwrap_string("noneInThreadHaveKeyword")?, + ) + } + (0x6472_6f77_7965_4b73_6168, _) => Filter::HasKeyword( + parser + .next_token::()? + .unwrap_string("hasKeyword")?, + ), + (0x6472_6f77_7965_4b74_6f6e, _) => Filter::NotKeyword( + parser + .next_token::()? + .unwrap_string("notKeyword")?, + ), + (0x746e_656d_6863_6174_7441_7361_68, _) => Filter::HasAttachment( + parser + .next_token::()? + .unwrap_bool("hasAttachment")?, + ), + (0x6d6f_7266, _) => { + Filter::From(parser.next_token::()?.unwrap_string("from")?) + } + (0x6f74, _) => { + Filter::To(parser.next_token::()?.unwrap_string("to")?) + } + (0x6363, _) => { + Filter::Cc(parser.next_token::()?.unwrap_string("cc")?) + } + (0x6363_62, _) => { + Filter::Bcc(parser.next_token::()?.unwrap_string("bcc")?) + } + (0x7463_656a_6275_73, _) => Filter::Subject( + parser.next_token::()?.unwrap_string("subject")?, + ), + (0x7964_6f62, _) => { + Filter::Body(parser.next_token::()?.unwrap_string("body")?) + } + (0x7265_6461_6568, _) => Filter::Header(>::parse(parser)?), + (0x6469, _) => Filter::Id(>::parse(parser)?), + (0x6572_6f66_6542_746e_6573, _) => Filter::SentBefore( + parser + .next_token::()? + .unwrap_string("sentBefore")?, + ), + (0x7265_7466_4174_6e65_73, _) => Filter::SentAfter( + parser.next_token::()?.unwrap_string("sentAfter")?, + ), + (0x6461_6572_6854_6e69, _) => { + Filter::InThread(parser.next_token::()?.unwrap_string("inThread")?) + } + (0x6449_746e_6572_6170, _) => Filter::ParentId( + parser + .next_token::()? + .unwrap_string_or_null("parentId")?, + ), + (0x656c_6f72, _) => Filter::Role( + parser + .next_token::()? + .unwrap_string_or_null("role")?, + ), + (0x656c_6f52_796e_4173_6168, _) => Filter::HasAnyRole( + parser.next_token::()?.unwrap_bool("hasAnyRole")?, + ), + (0x6465_6269_7263_7362_7553_7369, _) => Filter::IsSubscribed( + parser.next_token::()?.unwrap_bool("isSubscribed")?, + ), + (0x6576_6974_6341_7369, _) => Filter::IsActive( + parser.next_token::()?.unwrap_bool("isActive")?, + ), + _ => { + if parser.is_eof || parser.skip_string() { + let filter = Filter::_T( + String::from_utf8_lossy( + parser.bytes[parser.pos_marker..parser.pos - 1].as_ref(), + ) + .into_owned(), + ); + parser.skip_token(parser.depth_array, parser.depth_dict)?; + filter + } else { + return Err(parser.error_unterminated()); + } + } + }, + }; + } + Token::DictStart => { + pos_stack.push(filter.len()); + filter.push(Filter::Close); + } + Token::DictEnd => { + if !matches!(filter[pos_stack.pop().unwrap()], Filter::Close) { + if pos_stack.is_empty() { + break; + } + } else { + return Err(Error::Method(MethodError::InvalidArguments( + "Malformed filter".to_string(), + ))); + } + } + Token::ArrayEnd => { + filter.push(Filter::Close); + } + Token::Comma => (), + token => { + return Err(token.error("filter", "object or array")); + } + } + } + + Ok(filter) +} + +impl JsonObjectParser for Comparator { + fn parse(parser: &mut Parser<'_>) -> crate::parser::Result + where + Self: Sized, + { + let mut comp = Comparator { + is_ascending: true, + collation: None, + property: SortProperty::Type, + keyword: None, + }; + + parser + .next_token::()? + .assert_jmap(Token::DictStart)?; + + while { + match parser.next_dict_key::()? { + 0x676e_6964_6e65_6373_4173_69 => { + comp.is_ascending = parser + .next_token::()? + .unwrap_bool_or_null("isAscending")? + .unwrap_or_default(); + } + 0x6e6f_6974_616c_6c6f_63 => { + comp.collation = parser + .next_token::()? + .unwrap_string_or_null("collation")?; + } + 0x7974_7265_706f_7270 => { + comp.property = parser + .next_token::()? + .unwrap_string("property")?; + } + 0x6472_6f77_7965_6b => { + comp.keyword = parser + .next_token::()? + .unwrap_string_or_null("keyword")?; + } + _ => { + parser.skip_token(parser.depth_array, parser.depth_dict)?; + } + } + + !parser.is_dict_end()? + } {} + + Ok(comp) + } +} + +impl JsonObjectParser for SortProperty { + fn parse(parser: &mut Parser<'_>) -> crate::parser::Result + where + Self: Sized, + { + let mut hash = 0; + let mut shift = 0; + + while let Some(ch) = parser.next_unescaped()? { + if ch.is_ascii_alphabetic() { + if shift < 128 { + hash |= (ch as u128) << shift; + shift += 8; + } else { + break; + } + } else { + hash = 0; + break; + } + } + + match hash { + 0x6570_7974 => Ok(SortProperty::Type), + 0x656d_616e => Ok(SortProperty::Name), + 0x6c69_616d_65 => Ok(SortProperty::Email), + 0x6449_6c69_616d_65 => Ok(SortProperty::EmailId), + 0x6449_6461_6572_6874 => Ok(SortProperty::ThreadId), + 0x7441_746e_6573 => Ok(SortProperty::SentAt), + 0x7441_6465_7669_6563_6572 => Ok(SortProperty::ReceivedAt), + 0x657a_6973 => Ok(SortProperty::Size), + 0x6d6f_7266 => Ok(SortProperty::From), + 0x6f74 => Ok(SortProperty::To), + 0x7463_656a_6275_73 => Ok(SortProperty::Subject), + 0x6363 => Ok(SortProperty::Cc), + 0x7265_6472_4f74_726f_73 => Ok(SortProperty::SortOrder), + 0x6449_746e_6572_6170 => Ok(SortProperty::ParentId), + 0x6576_6974_6341_7369 => Ok(SortProperty::IsActive), + 0x6472_6f77_7965_4b73_6168 => Ok(SortProperty::HasKeyword), + 0x4b65_7661_4864_6165_7268_546e_496c_6c61 => Ok(SortProperty::AllInThreadHaveKeyword), + 0x6576_6148_6461_6572_6854_6e49_656d_6f73 => Ok(SortProperty::SomeInThreadHaveKeyword), + _ => { + if parser.is_eof || parser.skip_string() { + Ok(SortProperty::_T( + String::from_utf8_lossy( + parser.bytes[parser.pos_marker..parser.pos - 1].as_ref(), + ) + .into_owned(), + )) + } else { + Err(parser.error_unterminated()) + } + } + } + } +} + +impl Display for SortProperty { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str(match self { + SortProperty::Type => "type", + SortProperty::Name => "name", + SortProperty::Email => "email", + SortProperty::EmailId => "emailId", + SortProperty::ThreadId => "threadId", + SortProperty::SentAt => "sentAt", + SortProperty::ReceivedAt => "receivedAt", + SortProperty::Size => "size", + SortProperty::From => "from", + SortProperty::To => "to", + SortProperty::Subject => "subject", + SortProperty::Cc => "cc", + SortProperty::SortOrder => "sortOrder", + SortProperty::ParentId => "parentId", + SortProperty::IsActive => "isActive", + SortProperty::HasKeyword => "hasKeyword", + SortProperty::AllInThreadHaveKeyword => "allInThreadHaveKeyword", + SortProperty::SomeInThreadHaveKeyword => "someInThreadHaveKeyword", + SortProperty::_T(s) => s, + }) + } +} + +impl RequestPropertyParser for RequestArguments { + fn parse( + &mut self, + parser: &mut Parser, + property: RequestProperty, + ) -> crate::parser::Result { + match self { + RequestArguments::Email(args) => args.parse(parser, property), + RequestArguments::Mailbox(args) => args.parse(parser, property), + _ => Ok(false), + } + } +} diff --git a/crates/protocol/src/method/query_changes.rs b/crates/protocol/src/method/query_changes.rs new file mode 100644 index 00000000..55149419 --- /dev/null +++ b/crates/protocol/src/method/query_changes.rs @@ -0,0 +1,136 @@ +use crate::{ + error::method::MethodError, + parser::{json::Parser, Error, Ignore, JsonObjectParser, Token}, + request::{method::MethodObject, RequestProperty, RequestPropertyParser}, + types::{id::Id, state::State}, +}; + +use super::query::{parse_filter, Comparator, Filter, RequestArguments}; + +#[derive(Debug, Clone)] +pub struct QueryChangesRequest { + pub account_id: Id, + 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, +} + +#[derive(Debug, Clone, serde::Serialize)] +pub struct QueryChangesResponse { + #[serde(rename = "accountId")] + pub account_id: Id, + + #[serde(rename = "oldQueryState")] + pub old_query_state: State, + + #[serde(rename = "newQueryState")] + pub new_query_state: State, + + #[serde(rename = "total")] + #[serde(skip_serializing_if = "Option::is_none")] + pub total: Option, + + #[serde(rename = "removed")] + pub removed: Vec, + + #[serde(rename = "added")] + pub added: Vec, +} + +#[derive(Debug, Clone, serde::Serialize)] +pub struct AddedItem { + id: Id, + index: usize, +} + +impl AddedItem { + pub fn new(id: Id, index: usize) -> Self { + Self { id, index } + } +} + +impl JsonObjectParser for QueryChangesRequest { + fn parse(parser: &mut Parser<'_>) -> crate::parser::Result + where + Self: Sized, + { + let mut request = QueryChangesRequest { + arguments: match &parser.ctx { + MethodObject::Email => RequestArguments::Email(Default::default()), + MethodObject::Mailbox => RequestArguments::Mailbox(Default::default()), + MethodObject::EmailSubmission => RequestArguments::EmailSubmission, + _ => { + return Err(Error::Method(MethodError::UnknownMethod(format!( + "{}/queryChanges", + parser.ctx + )))) + } + }, + filter: vec![], + sort: None, + calculate_total: None, + account_id: Id::default(), + since_query_state: State::Initial, + max_changes: None, + up_to_id: None, + }; + + parser + .next_token::()? + .assert_jmap(Token::DictStart)?; + + while { + let property = parser.next_dict_key::()?; + match &property.hash[0] { + 0x6449_746e_756f_6363_61 => { + request.account_id = parser.next_token::()?.unwrap_string("accountId")?; + } + 0x7265_746c_6966 => match parser.next_token::()? { + Token::DictStart => { + request.filter = parse_filter(parser)?; + } + Token::Null => (), + token => { + return Err(token.error("filter", "object or null")); + } + }, + 0x7472_6f73 => { + request.sort = >>::parse(parser)?; + } + 0x6574_6174_5379_7265_7551_6563_6e69_73 => { + request.since_query_state = parser + .next_token::()? + .unwrap_string("sinceQueryState")?; + } + 0x7365_676e_6168_4378_616d => { + request.max_changes = parser + .next_token::()? + .unwrap_usize_or_null("maxChanges")?; + } + 0x6449_6f54_7075 => { + request.up_to_id = + parser.next_token::()?.unwrap_string_or_null("upToId")?; + } + 0x6c61_746f_5465_7461_6c75_636c_6163 => { + request.calculate_total = parser + .next_token::()? + .unwrap_bool_or_null("calculateTotal")?; + } + + _ => { + if !request.arguments.parse(parser, property)? { + parser.skip_token(parser.depth_array, parser.depth_dict)?; + } + } + } + + !parser.is_dict_end()? + } {} + + Ok(request) + } +} diff --git a/crates/protocol/src/method/search_snippet.rs b/crates/protocol/src/method/search_snippet.rs new file mode 100644 index 00000000..91bba07c --- /dev/null +++ b/crates/protocol/src/method/search_snippet.rs @@ -0,0 +1,91 @@ +use crate::{ + parser::{json::Parser, Ignore, JsonObjectParser, Token}, + request::{ + reference::{MaybeReference, ResultReference}, + RequestProperty, + }, + types::id::Id, +}; + +use super::query::{parse_filter, Filter}; + +#[derive(Debug, Clone)] +pub struct GetSearchSnippetRequest { + pub account_id: Id, + pub filter: Vec, + pub email_ids: MaybeReference, ResultReference>, +} + +#[derive(Debug, Clone, serde::Serialize)] +pub struct GetSearchSnippetResponse { + #[serde(rename = "accountId")] + pub account_id: Id, + + #[serde(rename = "list")] + pub list: Vec, + + #[serde(rename = "notFound")] + #[serde(skip_serializing_if = "Option::is_none")] + pub not_found: Option>, +} + +#[derive(serde::Serialize, Clone, Debug)] +pub struct SearchSnippet { + #[serde(rename = "emailId")] + pub email_id: Id, + + #[serde(skip_serializing_if = "Option::is_none")] + pub subject: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + pub preview: Option, +} + +impl JsonObjectParser for GetSearchSnippetRequest { + fn parse(parser: &mut Parser<'_>) -> crate::parser::Result + where + Self: Sized, + { + let mut request = GetSearchSnippetRequest { + account_id: Id::default(), + filter: vec![], + email_ids: MaybeReference::Value(vec![]), + }; + + parser + .next_token::()? + .assert_jmap(Token::DictStart)?; + + while { + let property = parser.next_dict_key::()?; + match &property.hash[0] { + 0x6449_746e_756f_6363_61 if !property.is_ref => { + request.account_id = parser.next_token::()?.unwrap_string("accountId")?; + } + 0x7265_746c_6966 if !property.is_ref => match parser.next_token::()? { + Token::DictStart => { + request.filter = parse_filter(parser)?; + } + Token::Null => (), + token => { + return Err(token.error("filter", "object or null")); + } + }, + 0x7364_496c_6961_6d65 => { + request.email_ids = if !property.is_ref { + MaybeReference::Value(>::parse(parser)?) + } else { + MaybeReference::Reference(ResultReference::parse(parser)?) + }; + } + _ => { + parser.skip_token(parser.depth_array, parser.depth_dict)?; + } + } + + !parser.is_dict_end()? + } {} + + Ok(request) + } +} diff --git a/crates/protocol/src/method/set.rs b/crates/protocol/src/method/set.rs new file mode 100644 index 00000000..120a941d --- /dev/null +++ b/crates/protocol/src/method/set.rs @@ -0,0 +1,352 @@ +use ahash::AHashMap; +use utils::map::vec_map::VecMap; + +use crate::{ + error::{method::MethodError, set::SetError}, + object::{email_submission, mailbox, sieve, Object}, + parser::{json::Parser, Error, JsonObjectParser, Token}, + request::{ + method::MethodObject, + reference::{MaybeReference, ResultReference}, + RequestProperty, RequestPropertyParser, + }, + types::{ + acl::Acl, + blob::BlobId, + date::UTCDate, + id::Id, + keyword::Keyword, + property::{HeaderForm, ObjectProperty, Property, SetProperty}, + state::State, + type_state::TypeState, + value::{SetValue, SetValueMap, Value}, + }, +}; + +use super::ahash_is_empty; + +#[derive(Debug, Clone)] +pub struct SetRequest { + pub account_id: Id, + pub if_in_state: Option, + pub create: Option>>, + pub update: Option>>, + pub destroy: Option, ResultReference>>, + pub arguments: RequestArguments, +} + +#[derive(Debug, Clone)] +pub enum RequestArguments { + Email, + Mailbox(mailbox::SetArguments), + Identity, + EmailSubmission(email_submission::SetArguments), + PushSubscription, + SieveScript(sieve::SetArguments), + VacationResponse, + Principal, +} + +#[derive(Debug, Clone, Default, serde::Serialize)] +pub struct SetResponse { + #[serde(rename = "accountId")] + #[serde(skip_serializing_if = "Option::is_none")] + pub account_id: Option, + + #[serde(rename = "oldState")] + #[serde(skip_serializing_if = "Option::is_none")] + pub old_state: Option, + + #[serde(rename = "newState")] + #[serde(skip_serializing_if = "Option::is_none")] + pub new_state: Option, + + #[serde(rename = "created")] + #[serde(skip_serializing_if = "ahash_is_empty")] + pub created: AHashMap>, + + #[serde(rename = "updated")] + #[serde(skip_serializing_if = "VecMap::is_empty")] + pub updated: VecMap>>, + + #[serde(rename = "destroyed")] + #[serde(skip_serializing_if = "Vec::is_empty")] + pub destroyed: Vec, + + #[serde(rename = "notCreated")] + #[serde(skip_serializing_if = "VecMap::is_empty")] + pub not_created: VecMap, + + #[serde(rename = "notUpdated")] + #[serde(skip_serializing_if = "VecMap::is_empty")] + pub not_updated: VecMap, + + #[serde(rename = "notDestroyed")] + #[serde(skip_serializing_if = "VecMap::is_empty")] + pub not_destroyed: VecMap, +} + +impl JsonObjectParser for SetRequest { + fn parse(parser: &mut Parser) -> crate::parser::Result + where + Self: Sized, + { + let mut request = SetRequest { + arguments: match &parser.ctx { + MethodObject::Email => RequestArguments::Email, + MethodObject::Mailbox => RequestArguments::Mailbox(Default::default()), + MethodObject::Identity => RequestArguments::Identity, + MethodObject::EmailSubmission => { + RequestArguments::EmailSubmission(Default::default()) + } + MethodObject::PushSubscription => RequestArguments::PushSubscription, + MethodObject::VacationResponse => RequestArguments::VacationResponse, + MethodObject::SieveScript => RequestArguments::SieveScript(Default::default()), + MethodObject::Principal => RequestArguments::Principal, + _ => { + return Err(Error::Method(MethodError::UnknownMethod(format!( + "{}/set", + parser.ctx + )))) + } + }, + account_id: Id::default(), + if_in_state: None, + create: None, + update: None, + destroy: None, + }; + + parser + .next_token::()? + .assert_jmap(Token::DictStart)?; + + while { + let property = parser.next_dict_key::()?; + match &property.hash[0] { + 0x6449_746e_756f_6363_61 if !property.is_ref => { + request.account_id = parser.next_token::()?.unwrap_string("accountId")?; + } + 0x6574_6165_7263 if !property.is_ref => { + request.create = >>>::parse(parser)?; + } + 0x6574_6164_7075 if !property.is_ref => { + request.update = >>>::parse(parser)?; + } + 0x0079_6f72_7473_6564 => { + request.destroy = if !property.is_ref { + >>::parse(parser)?.map(MaybeReference::Value) + } else { + Some(MaybeReference::Reference(ResultReference::parse(parser)?)) + }; + } + 0x6574_6174_536e_4966_69 if !property.is_ref => { + request.if_in_state = parser + .next_token::()? + .unwrap_string_or_null("ifInState")?; + } + _ => { + if !request.arguments.parse(parser, property)? { + parser.skip_token(parser.depth_array, parser.depth_dict)?; + } + } + } + + !parser.is_dict_end()? + } {} + + Ok(request) + } +} + +impl JsonObjectParser for Object { + fn parse(parser: &mut Parser<'_>) -> crate::parser::Result + where + Self: Sized, + { + let mut obj = Object { + properties: VecMap::with_capacity(8), + }; + + parser + .next_token::()? + .assert_jmap(Token::DictStart)?; + + while { + let mut property = parser.next_dict_key::()?; + let value = if !property.is_ref { + match &property.property { + Property::Id | Property::ThreadId => parser + .next_token::()? + .unwrap_string_or_null("")? + .map(|id| SetValue::Value(Value::Id(id))) + .unwrap_or(SetValue::Value(Value::Null)), + Property::BlobId | Property::Picture => parser + .next_token::()? + .unwrap_string_or_null("")? + .map(|id| SetValue::Value(Value::BlobId(id))) + .unwrap_or(SetValue::Value(Value::Null)), + Property::SentAt + | Property::ReceivedAt + | Property::Expires + | Property::FromDate + | Property::ToDate => parser + .next_token::()? + .unwrap_string_or_null("")? + .map(|date| SetValue::Value(Value::Date(date))) + .unwrap_or(SetValue::Value(Value::Null)), + Property::Subject + | Property::Preview + | Property::Name + | Property::Description + | Property::Timezone + | Property::Email + | Property::Secret + | Property::DeviceClientId + | Property::Url + | Property::VerificationCode + | Property::HtmlSignature + | Property::TextSignature + | Property::Type + | Property::Charset + | Property::Disposition + | Property::Language + | Property::Location + | Property::Cid + | Property::Role + | Property::PartId => parser + .next_token::()? + .unwrap_string_or_null("")? + .map(|text| SetValue::Value(Value::Text(text))) + .unwrap_or(SetValue::Value(Value::Null)), + Property::TextBody | Property::HtmlBody => { + if let MethodObject::Email = &parser.ctx { + SetValue::Value(Value::parse::(parser)?) + } else { + parser + .next_token::()? + .unwrap_string_or_null("")? + .map(|text| SetValue::Value(Value::Text(text))) + .unwrap_or(SetValue::Value(Value::Null)) + } + } + Property::HasAttachment + | Property::IsSubscribed + | Property::IsEnabled + | Property::IsActive => parser + .next_token::()? + .unwrap_bool_or_null("")? + .map(|bool| SetValue::Value(Value::Bool(bool))) + .unwrap_or(SetValue::Value(Value::Null)), + Property::Size | Property::SortOrder | Property::Quota => parser + .next_token::()? + .unwrap_uint_or_null("")? + .map(|uint| SetValue::Value(Value::UnsignedInt(uint))) + .unwrap_or(SetValue::Value(Value::Null)), + Property::ParentId + | Property::EmailId + | Property::IdentityId + | Property::UndoStatus => parser + .next_token::>()? + .unwrap_string_or_null("")? + .map(|id| match id { + MaybeReference::Value(id) => SetValue::Value(Value::Id(id)), + MaybeReference::Reference(r) => SetValue::Value(Value::Text(r)), + }) + .unwrap_or(SetValue::Value(Value::Null)), + Property::MailboxIds => { + if property.patch.is_empty() { + SetValue::Value(Value::List( + >>::parse(parser)? + .values + .into_iter() + .map(|id| match id { + MaybeReference::Value(id) => Value::Id(id), + MaybeReference::Reference(r) => Value::Text(r), + }) + .collect(), + )) + } else { + property.patch.push(Value::Bool(bool::parse(parser)?)); + SetValue::Patch(property.patch) + } + } + Property::Keywords => { + if property.patch.is_empty() { + SetValue::Value(Value::List( + >::parse(parser)? + .values + .into_iter() + .map(Value::Keyword) + .collect(), + )) + } else { + property.patch.push(Value::Bool(bool::parse(parser)?)); + SetValue::Patch(property.patch) + } + } + + Property::Acl => SetValue::Value(Value::parse::(parser)?), + Property::Aliases + | Property::Attachments + | Property::Bcc + | Property::BodyStructure + | Property::BodyValues + | Property::Capabilities + | Property::Cc + | Property::Envelope + | Property::From + | Property::Headers + | Property::InReplyTo + | Property::Keys + | Property::MessageId + | Property::References + | Property::ReplyTo + | Property::Sender + | Property::SubParts + | Property::To => { + SetValue::Value(Value::parse::(parser)?) + } + Property::Members => { + SetValue::Value(Value::parse::(parser)?) + } + Property::Header(h) => SetValue::Value(if matches!(h.form, HeaderForm::Date) { + Value::parse::(parser) + } else { + Value::parse::(parser) + }?), + Property::Types => { + SetValue::Value(Value::parse::(parser)?) + } + _ => { + parser.skip_token(parser.depth_array, parser.depth_dict)?; + SetValue::Value(Value::Null) + } + } + } else { + SetValue::ResultReference(ResultReference::parse(parser)?) + }; + + obj.properties.append(property.property, value); + + !parser.is_dict_end()? + } {} + + Ok(obj) + } +} + +impl RequestPropertyParser for RequestArguments { + fn parse( + &mut self, + parser: &mut Parser, + property: RequestProperty, + ) -> crate::parser::Result { + match self { + RequestArguments::Mailbox(args) => args.parse(parser, property), + RequestArguments::EmailSubmission(args) => args.parse(parser, property), + RequestArguments::SieveScript(args) => args.parse(parser, property), + _ => Ok(false), + } + } +} diff --git a/crates/protocol/src/method/validate.rs b/crates/protocol/src/method/validate.rs new file mode 100644 index 00000000..880fb7a4 --- /dev/null +++ b/crates/protocol/src/method/validate.rs @@ -0,0 +1,56 @@ +use serde::Serialize; + +use crate::{ + error::set::SetError, + parser::{json::Parser, JsonObjectParser, Token}, + request::RequestProperty, + types::{blob::BlobId, id::Id}, +}; + +#[derive(Debug, Clone)] +pub struct ValidateSieveScriptRequest { + pub account_id: Id, + pub blob_id: BlobId, +} + +#[derive(Debug, Serialize)] +pub struct ValidateSieveScriptResponse { + #[serde(rename = "accountId")] + pub account_id: Id, + pub error: Option, +} + +impl JsonObjectParser for ValidateSieveScriptRequest { + fn parse(parser: &mut Parser<'_>) -> crate::parser::Result + where + Self: Sized, + { + let mut request = ValidateSieveScriptRequest { + account_id: Id::default(), + blob_id: BlobId::default(), + }; + + parser + .next_token::()? + .assert_jmap(Token::DictStart)?; + + while { + let property = parser.next_dict_key::()?; + match &property.hash[0] { + 0x6449_746e_756f_6363_61 if !property.is_ref => { + request.account_id = parser.next_token::()?.unwrap_string("accountId")?; + } + 0x6449_626f_6c62 if !property.is_ref => { + request.blob_id = parser.next_token::()?.unwrap_string("blobId")?; + } + _ => { + parser.skip_token(parser.depth_array, parser.depth_dict)?; + } + } + + !parser.is_dict_end()? + } {} + + Ok(request) + } +} diff --git a/crates/protocol/src/object/email.rs b/crates/protocol/src/object/email.rs new file mode 100644 index 00000000..777a5c3f --- /dev/null +++ b/crates/protocol/src/object/email.rs @@ -0,0 +1,73 @@ +use crate::{ + parser::{json::Parser, Ignore, JsonObjectParser}, + request::{RequestProperty, RequestPropertyParser}, + types::property::Property, +}; + +#[derive(Debug, Clone, Default)] +pub struct GetArguments { + pub body_properties: Option>, + pub fetch_text_body_values: Option, + pub fetch_html_body_values: Option, + pub fetch_all_body_values: Option, + pub max_body_value_bytes: Option, +} + +#[derive(Debug, Clone, Default)] +pub struct QueryArguments { + collapse_threads: Option, +} + +impl RequestPropertyParser for GetArguments { + fn parse( + &mut self, + parser: &mut Parser, + property: RequestProperty, + ) -> crate::parser::Result { + match (&property.hash[0], &property.hash[1]) { + (0x7365_6974_7265_706f_7250_7964_6f62, _) => { + self.body_properties = >>::parse(parser)?; + } + (0x6c61_5679_646f_4274_7865_5468_6374_6566, 0x7365_75) => { + self.fetch_text_body_values = parser + .next_token::()? + .unwrap_bool_or_null("fetchTextBodyValues")?; + } + (0x6c61_5679_646f_424c_4d54_4868_6374_6566, 0x7365_75) => { + self.fetch_html_body_values = parser + .next_token::()? + .unwrap_bool_or_null("fetchHTMLBodyValues")?; + } + (0x756c_6156_7964_6f42_6c6c_4168_6374_6566, 0x7365) => { + self.fetch_all_body_values = parser + .next_token::()? + .unwrap_bool_or_null("fetchAllBodyValues")?; + } + (0x6574_7942_6575_6c61_5679_646f_4278_616d, 0x73) => { + self.max_body_value_bytes = parser + .next_token::()? + .unwrap_usize_or_null("maxBodyValueBytes")?; + } + _ => return Ok(false), + } + + Ok(true) + } +} + +impl RequestPropertyParser for QueryArguments { + fn parse( + &mut self, + parser: &mut Parser, + property: RequestProperty, + ) -> crate::parser::Result { + if property.hash[0] == 0x7364_6165_7268_5465_7370_616c_6c6f_63 { + self.collapse_threads = parser + .next_token::()? + .unwrap_bool_or_null("collapseThreads")?; + Ok(true) + } else { + Ok(false) + } + } +} diff --git a/crates/protocol/src/object/email_submission.rs b/crates/protocol/src/object/email_submission.rs new file mode 100644 index 00000000..62ab0326 --- /dev/null +++ b/crates/protocol/src/object/email_submission.rs @@ -0,0 +1,39 @@ +use utils::map::vec_map::VecMap; + +use crate::{ + parser::{json::Parser, JsonObjectParser}, + request::{reference::MaybeReference, RequestProperty, RequestPropertyParser}, + types::{id::Id, value::SetValue}, +}; + +use super::Object; + +#[derive(Debug, Clone, Default)] +pub struct SetArguments { + pub on_success_update_email: Option, Object>>, + pub on_success_destroy_email: Option>>, +} + +impl RequestPropertyParser for SetArguments { + fn parse( + &mut self, + parser: &mut Parser, + property: RequestProperty, + ) -> crate::parser::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)?; + Ok(true) + } else if property.hash[0] == 0x796f_7274_7365_4473_7365_6363_7553_6e6f + && property.hash[1] == 0x6c69_616d_45 + { + self.on_success_destroy_email = + >>>::parse(parser)?; + Ok(true) + } else { + Ok(false) + } + } +} diff --git a/crates/protocol/src/object/mailbox.rs b/crates/protocol/src/object/mailbox.rs new file mode 100644 index 00000000..0141e983 --- /dev/null +++ b/crates/protocol/src/object/mailbox.rs @@ -0,0 +1,58 @@ +use crate::{ + parser::{json::Parser, Ignore}, + request::{RequestProperty, RequestPropertyParser}, +}; + +#[derive(Debug, Clone, Default)] +pub struct SetArguments { + pub on_destroy_remove_emails: Option, +} + +#[derive(Debug, Clone, Default)] +pub struct QueryArguments { + sort_as_tree: Option, + filter_as_tree: Option, +} + +impl RequestPropertyParser for SetArguments { + fn parse( + &mut self, + parser: &mut Parser, + property: RequestProperty, + ) -> crate::parser::Result { + if property.hash[0] == 0x4565_766f_6d65_5279_6f72_7473_6544_6e6f + && property.hash[1] == 0x736c_6961_6d + { + self.on_destroy_remove_emails = parser + .next_token::()? + .unwrap_bool_or_null("onDestroyRemoveEmails")?; + Ok(true) + } else { + Ok(false) + } + } +} + +impl RequestPropertyParser for QueryArguments { + fn parse( + &mut self, + parser: &mut Parser, + property: RequestProperty, + ) -> crate::parser::Result { + match &property.hash[0] { + 0x6565_7254_7341_7472_6f73 => { + self.sort_as_tree = parser + .next_token::()? + .unwrap_bool_or_null("sortAsTree")?; + } + 0x6565_7254_7341_7265_746c_6966 => { + self.filter_as_tree = parser + .next_token::()? + .unwrap_bool_or_null("filterAsTree")?; + } + _ => return Ok(false), + } + + Ok(true) + } +} diff --git a/crates/protocol/src/object/mod.rs b/crates/protocol/src/object/mod.rs new file mode 100644 index 00000000..c6326d76 --- /dev/null +++ b/crates/protocol/src/object/mod.rs @@ -0,0 +1,13 @@ +pub mod email; +pub mod email_submission; +pub mod mailbox; +pub mod sieve; + +use utils::map::vec_map::VecMap; + +use crate::types::property::Property; + +#[derive(Debug, Clone, Default, serde::Serialize)] +pub struct Object { + pub properties: VecMap, +} diff --git a/crates/protocol/src/object/sieve.rs b/crates/protocol/src/object/sieve.rs new file mode 100644 index 00000000..46970a8c --- /dev/null +++ b/crates/protocol/src/object/sieve.rs @@ -0,0 +1,37 @@ +use crate::{ + parser::json::Parser, + request::{reference::MaybeReference, RequestProperty, RequestPropertyParser}, + types::id::Id, +}; + +#[derive(Debug, Clone, Default)] +pub struct SetArguments { + 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, + ) -> crate::parser::Result { + if property.hash[0] == 0x7461_7669_7463_4173_7365_6363_7553_6e6f + && property.hash[1] == 0x7470_6972_6353_65 + { + self.on_success_activate_script = parser + .next_token::>()? + .unwrap_string_or_null("onSuccessActivateScript")?; + Ok(true) + } else if property.hash[0] == 0x7669_7463_6165_4473_7365_6363_7553_6e6f + && property.hash[1] == 0x7470_6972_6353_6574_61 + { + self.on_success_deactivate_script = parser + .next_token::()? + .unwrap_bool_or_null("onSuccessDeactivateScript")?; + Ok(true) + } else { + Ok(false) + } + } +} diff --git a/crates/protocol/src/parser/base32.rs b/crates/protocol/src/parser/base32.rs new file mode 100644 index 00000000..7d78f519 --- /dev/null +++ b/crates/protocol/src/parser/base32.rs @@ -0,0 +1,59 @@ +use utils::codec::{base32_custom::BASE32_INVERSE, leb128::Leb128Iterator}; + +use super::{json::Parser, Error}; + +#[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) -> Error { + self.bytes.error_value() + } +} + +impl<'x, 'y> Iterator for JsonBase32Reader<'x, 'y> { + 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<'x, 'y> Leb128Iterator for JsonBase32Reader<'x, 'y> {} diff --git a/crates/protocol/src/parser/impls.rs b/crates/protocol/src/parser/impls.rs new file mode 100644 index 00000000..8cb113d6 --- /dev/null +++ b/crates/protocol/src/parser/impls.rs @@ -0,0 +1,276 @@ +use std::fmt::Display; + +use utils::map::vec_map::VecMap; + +use super::{json::Parser, Ignore, JsonObjectParser, Token}; + +impl JsonObjectParser for u64 { + fn parse(parser: &mut Parser<'_>) -> super::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<'_>) -> super::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<'_>) -> super::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(Into::into) + .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<'_>) -> super::Result + where + Self: Sized, + { + let mut vec = Vec::new(); + + parser.next_token::()?.assert(Token::ArrayStart)?; + while { + vec.push(parser.next_token::()?.unwrap_string("")?); + + !parser.is_array_end()? + } {} + + Ok(vec) + } +} + +impl JsonObjectParser for Option> { + fn parse(parser: &mut Parser<'_>) -> super::Result + where + Self: Sized, + { + match parser.next_token::()? { + Token::ArrayStart => { + let mut vec = Vec::new(); + while { + vec.push(parser.next_token::()?.unwrap_string("")?); + + !parser.is_array_end()? + } {} + + Ok(Some(vec)) + } + Token::Null => Ok(None), + token => Err(token.error("", &token.to_string())), + } + } +} + +impl JsonObjectParser for VecMap { + fn parse(parser: &mut Parser<'_>) -> super::Result + where + Self: Sized, + { + let mut map = VecMap::new(); + + parser.next_token::()?.assert(Token::DictStart)?; + while { + map.append(parser.next_dict_key()?, V::parse(parser)?); + !parser.is_dict_end()? + } {} + + Ok(map) + } +} + +impl JsonObjectParser + for Option> +{ + fn parse(parser: &mut Parser<'_>) -> super::Result + where + Self: Sized, + { + match parser.next_token::()? { + Token::DictStart => { + let mut map = VecMap::new(); + + parser.next_token::()?.assert(Token::DictStart)?; + while { + map.append(parser.next_dict_key()?, V::parse(parser)?); + !parser.is_dict_end()? + } {} + + Ok(Some(map)) + } + Token::Null => Ok(None), + token => Err(token.error("", &token.to_string())), + } + } +} + +impl JsonObjectParser for bool { + fn parse(parser: &mut Parser<'_>) -> super::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<'_>) -> super::Result + where + Self: Sized, + { + if parser.skip_string() { + Ok(Ignore {}) + } else { + Err(parser.error_unterminated()) + } + } +} diff --git a/crates/protocol/src/parser/json.rs b/crates/protocol/src/parser/json.rs new file mode 100644 index 00000000..cd9ffa67 --- /dev/null +++ b/crates/protocol/src/parser/json.rs @@ -0,0 +1,383 @@ +use std::{fmt::Display, slice::Iter}; + +use crate::{error::method::MethodError, request::method::MethodObject}; + +use super::{Error, Ignore, JsonObjectParser, Token}; + +const MAX_NESTED_LEVELS: u32 = 16; + +#[derive(Debug)] +pub struct Parser<'x> { + pub bytes: &'x [u8], + pub iter: Iter<'x, u8>, + 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(), + 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) -> Error { + format!("{message} at position {}.", self.pos).into() + } + + pub fn error_unterminated(&self) -> Error { + format!("Unterminated string at position {pos}.", pos = self.pos).into() + } + + pub fn error_utf8(&self) -> Error { + format!("Invalid UTF-8 sequence at position {pos}.", pos = self.pos).into() + } + + pub fn error_value(&mut self) -> Error { + if self.is_eof || self.skip_string() { + Error::Method(MethodError::InvalidArguments(format!( + "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 next_char(&mut self) -> Option { + self.pos += 1; + self.iter.next().copied() + } + + #[inline(always)] + pub fn next_unescaped(&mut self) -> super::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) -> super::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) -> super::Result { + self.next_token::().and_then(|k| { + let k = k.unwrap_string("")?; + self.next_token::()?.assert(Token::Colon)?; + Ok(k) + }) + } + + pub fn is_dict_end(&mut self) -> super::Result { + match self.next_token::()? { + Token::Comma => Ok(false), + Token::DictEnd => Ok(true), + token => Err(self.error(&format!("Expected ',' or '}}', found {}", token))), + } + } + + pub fn is_array_end(&mut self) -> super::Result { + match self.next_token::()? { + Token::Comma => Ok(false), + Token::ArrayEnd => Ok(true), + token => Err(self.error(&format!("Expected ',' or ']', found {}", token))), + } + } + + pub fn skip_token( + &mut self, + start_depth_array: u32, + start_depth_dict: u32, + ) -> super::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/protocol/src/parser/mod.rs b/crates/protocol/src/parser/mod.rs new file mode 100644 index 00000000..8ff4f6d0 --- /dev/null +++ b/crates/protocol/src/parser/mod.rs @@ -0,0 +1,164 @@ +use std::fmt::Display; + +use crate::error::{method::MethodError, request::RequestError}; + +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<'_>) -> Result + where + Self: Sized; +} + +pub type Result = std::result::Result; + +#[derive(Debug)] +pub enum Error { + Request(RequestError), + Method(MethodError), +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct Ignore {} + +impl Token { + pub fn unwrap_string(self, property: &str) -> Result { + match self { + Token::String(s) => Ok(s), + token => Err(token.error(property, "string")), + } + } + + pub fn unwrap_string_or_null(self, property: &str) -> 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) -> Result { + match self { + Token::Boolean(v) => Ok(v), + token => Err(token.error(property, "boolean")), + } + } + + pub fn unwrap_bool_or_null(self, property: &str) -> 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) -> 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) -> 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) -> 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 assert(self, token: Token) -> Result<()> { + if self == token { + Ok(()) + } else { + Err(self.error("", &token.to_string())) + } + } + + pub fn assert_jmap(self, token: Token) -> Result<()> { + if self == token { + Ok(()) + } else { + Err(Error::Request(RequestError::not_request(format!( + "Invalid JMAP request: expected '{token}', got '{self}'." + )))) + } + } + + pub fn error(&self, property: &str, expected: &str) -> Error { + Error::Method(MethodError::InvalidArguments(if !property.is_empty() { + format!("Invalid argument for '{property:?}': expected '{expected}', got '{self}'.",) + } else { + format!("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 From for Error { + fn from(s: String) -> Self { + Error::Request(RequestError::not_json(&s)) + } +} + +impl From<&str> for Error { + fn from(s: &str) -> Self { + Error::Request(RequestError::not_json(s)) + } +} + +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/protocol/src/request/capability.rs b/crates/protocol/src/request/capability.rs new file mode 100644 index 00000000..054366c6 --- /dev/null +++ b/crates/protocol/src/request/capability.rs @@ -0,0 +1,69 @@ +use crate::{ + error::request::RequestError, + parser::{json::Parser, Error, JsonObjectParser}, +}; + +#[derive(Debug, Clone, Copy, serde::Serialize, Hash, PartialEq, Eq)] +pub enum Capability { + #[serde(rename(serialize = "urn:ietf:params:jmap:core"))] + Core = 1 << 0, + #[serde(rename(serialize = "urn:ietf:params:jmap:mail"))] + Mail = 1 << 1, + #[serde(rename(serialize = "urn:ietf:params:jmap:submission"))] + Submission = 1 << 2, + #[serde(rename(serialize = "urn:ietf:params:jmap:vacationresponse"))] + VacationResponse = 1 << 3, + #[serde(rename(serialize = "urn:ietf:params:jmap:contacts"))] + Contacts = 1 << 4, + #[serde(rename(serialize = "urn:ietf:params:jmap:calendars"))] + Calendars = 1 << 5, + #[serde(rename(serialize = "urn:ietf:params:jmap:websocket"))] + WebSocket = 1 << 6, + #[serde(rename(serialize = "urn:ietf:params:jmap:sieve"))] + Sieve = 1 << 7, +} + +impl JsonObjectParser for Capability { + fn parse(parser: &mut Parser<'_>) -> crate::parser::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), + 0x7372_6164_6e65_6c61_63 => Ok(Capability::Calendars), + 0x7465_6b63_6f73_6265_77 => Ok(Capability::WebSocket), + 0x6576_6569_73 => Ok(Capability::Sieve), + _ => Err(parser.error_capability()), + }, + Err(Error::Method(_)) => Err(parser.error_capability()), + Err(err @ Error::Request(_)) => Err(err), + } + } +} + +impl<'x> Parser<'x> { + fn error_capability(&mut self) -> Error { + if self.is_eof || self.skip_string() { + Error::Request(RequestError::unknown_capability(&String::from_utf8_lossy( + self.bytes[self.pos_marker..self.pos - 1].as_ref(), + ))) + } else { + self.error_unterminated() + } + } +} diff --git a/crates/protocol/src/request/echo.rs b/crates/protocol/src/request/echo.rs new file mode 100644 index 00000000..1310866f --- /dev/null +++ b/crates/protocol/src/request/echo.rs @@ -0,0 +1,32 @@ +use serde_json::value::RawValue; +use std::fmt::Write; + +use crate::parser::{json::Parser, JsonObjectParser, Token}; + +#[derive(Debug)] +pub struct Echo { + pub payload: Box, +} + +impl JsonObjectParser for Echo { + fn parse(parser: &mut Parser<'_>) -> crate::parser::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/protocol/src/request/method.rs b/crates/protocol/src/request/method.rs new file mode 100644 index 00000000..5b9f8523 --- /dev/null +++ b/crates/protocol/src/request/method.rs @@ -0,0 +1,196 @@ +use std::fmt::Display; + +use crate::parser::{json::Parser, JsonObjectParser}; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct MethodName { + pub obj: MethodObject, + pub fnc: MethodFunction, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum MethodObject { + Email, + Mailbox, + Core, + Blob, + PushSubscription, + Thread, + SearchSnippet, + Identity, + EmailSubmission, + VacationResponse, + SieveScript, + Principal, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum MethodFunction { + Get, + Set, + Changes, + Query, + QueryChanges, + Copy, + Import, + Parse, + Validate, + Echo, +} + +impl JsonObjectParser for MethodName { + fn parse(parser: &mut Parser<'_>) -> crate::parser::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 { + 0x6c69_616d_45 => MethodObject::Email, + 0x786f_626c_6961_4d => MethodObject::Mailbox, + 0x6461_6572_6854 => MethodObject::Thread, + 0x626f_6c42 => MethodObject::Blob, + 0x6e6f_6973_7369_6d62_7553_6c69_616d_45 => MethodObject::EmailSubmission, + 0x7465_7070_696e_5368_6372_6165_53 => 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, + 0x7470_6972_6353_6576_6569_53 => MethodObject::SieveScript, + 0x6c61_7069_636e_6972_50 => MethodObject::Principal, + 0x6572_6f43 => MethodObject::Core, + _ => return Err(parser.error_value()), + }, + fnc: match fnc_hash { + 0x7465_67 => MethodFunction::Get, + 0x7972_6575_71 => MethodFunction::Query, + 0x7465_73 => MethodFunction::Set, + 0x7365_676e_6168_63 => MethodFunction::Changes, + 0x7365_676e_6168_4379_7265_7571 => MethodFunction::QueryChanges, + 0x7970_6f63 => MethodFunction::Copy, + 0x7472_6f70_6d69 => MethodFunction::Import, + 0x6573_7261_70 => MethodFunction::Parse, + 0x6574_6164_696c_6176 => MethodFunction::Validate, + 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()) + } +} + +impl MethodName { + pub fn unknown_method() -> Self { + Self { + obj: MethodObject::Thread, + fnc: MethodFunction::Echo, + } + } + + pub fn as_str(&self) -> &'static str { + match (self.fnc, self.obj) { + (MethodFunction::Echo, MethodObject::Core) => "Core/echo", + (MethodFunction::Copy, MethodObject::Blob) => "Blob/copy", + (MethodFunction::Get, MethodObject::PushSubscription) => "PushSubscription/get", + (MethodFunction::Set, MethodObject::PushSubscription) => "PushSubscription/set", + (MethodFunction::Get, MethodObject::Mailbox) => "Mailbox/get", + (MethodFunction::Changes, MethodObject::Mailbox) => "Mailbox/changes", + (MethodFunction::Query, MethodObject::Mailbox) => "Mailbox/query", + (MethodFunction::QueryChanges, MethodObject::Mailbox) => "Mailbox/queryChanges", + (MethodFunction::Set, MethodObject::Mailbox) => "Mailbox/set", + (MethodFunction::Get, MethodObject::Thread) => "Thread/get", + (MethodFunction::Changes, MethodObject::Thread) => "Thread/changes", + (MethodFunction::Get, MethodObject::Email) => "Email/get", + (MethodFunction::Changes, MethodObject::Email) => "Email/changes", + (MethodFunction::Query, MethodObject::Email) => "Email/query", + (MethodFunction::QueryChanges, MethodObject::Email) => "Email/queryChanges", + (MethodFunction::Set, MethodObject::Email) => "Email/set", + (MethodFunction::Copy, MethodObject::Email) => "Email/copy", + (MethodFunction::Import, MethodObject::Email) => "Email/import", + (MethodFunction::Parse, MethodObject::Email) => "Email/parse", + (MethodFunction::Get, MethodObject::SearchSnippet) => "SearchSnippet/get", + (MethodFunction::Get, MethodObject::Identity) => "Identity/get", + (MethodFunction::Changes, MethodObject::Identity) => "Identity/changes", + (MethodFunction::Set, MethodObject::Identity) => "Identity/set", + (MethodFunction::Get, MethodObject::EmailSubmission) => "EmailSubmission/get", + (MethodFunction::Changes, MethodObject::EmailSubmission) => "EmailSubmission/changes", + (MethodFunction::Query, MethodObject::EmailSubmission) => "EmailSubmission/query", + (MethodFunction::QueryChanges, MethodObject::EmailSubmission) => { + "EmailSubmission/queryChanges" + } + (MethodFunction::Set, MethodObject::EmailSubmission) => "EmailSubmission/set", + (MethodFunction::Get, MethodObject::VacationResponse) => "VacationResponse/get", + (MethodFunction::Set, MethodObject::VacationResponse) => "VacationResponse/set", + (MethodFunction::Get, MethodObject::SieveScript) => "SieveScript/get", + (MethodFunction::Set, MethodObject::SieveScript) => "SieveScript/set", + (MethodFunction::Query, MethodObject::SieveScript) => "SieveScript/query", + (MethodFunction::Validate, MethodObject::SieveScript) => "SieveScript/validate", + (MethodFunction::Get, MethodObject::Principal) => "Principal/get", + (MethodFunction::Set, MethodObject::Principal) => "Principal/set", + (MethodFunction::Query, MethodObject::Principal) => "Principal/query", + _ => "error", + } + } +} + +impl Display for MethodObject { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str(match self { + MethodObject::Blob => "Blob", + MethodObject::EmailSubmission => "EmailSubmission", + MethodObject::SearchSnippet => "SearchSnippet", + MethodObject::Identity => "Identity", + MethodObject::VacationResponse => "VacationResponse", + MethodObject::PushSubscription => "PushSubscription", + MethodObject::SieveScript => "SieveScript", + MethodObject::Principal => "Principal", + MethodObject::Core => "Core", + MethodObject::Mailbox => "Mailbox", + MethodObject::Thread => "Thread", + MethodObject::Email => "Email", + }) + } +} + +// Method serialization +impl serde::Serialize for MethodName { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + serializer.serialize_str(self.as_str()) + } +} diff --git a/crates/protocol/src/request/mod.rs b/crates/protocol/src/request/mod.rs new file mode 100644 index 00000000..666c81f7 --- /dev/null +++ b/crates/protocol/src/request/mod.rs @@ -0,0 +1,109 @@ +pub mod capability; +pub mod echo; +pub mod method; +pub mod parser; +pub mod reference; + +use std::{ + collections::HashMap, + fmt::{Debug, Display}, +}; + +use crate::{ + error::method::MethodError, + method::{ + changes::ChangesRequest, + copy::{CopyBlobRequest, CopyRequest}, + get::GetRequest, + import::ImportEmailRequest, + parse::ParseEmailRequest, + query::QueryRequest, + query_changes::QueryChangesRequest, + search_snippet::GetSearchSnippetRequest, + set::SetRequest, + validate::ValidateSieveScriptRequest, + }, + parser::{json::Parser, JsonObjectParser}, + types::id::Id, +}; + +use self::echo::Echo; + +#[derive(Debug)] +pub struct Request { + pub using: u32, + pub method_calls: Vec>, + pub created_ids: Option>, +} + +#[derive(Debug)] +pub struct Call { + pub id: String, + 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), + Changes(ChangesRequest), + Copy(CopyRequest), + CopyBlob(CopyBlobRequest), + ImportEmail(ImportEmailRequest), + ParseEmail(ParseEmailRequest), + QueryChanges(QueryChangesRequest), + Query(QueryRequest), + SearchSnippet(GetSearchSnippetRequest), + ValidateScript(ValidateSieveScriptRequest), + Echo(Echo), + Error(MethodError), +} + +impl JsonObjectParser for RequestProperty { + fn parse(parser: &mut Parser<'_>) -> crate::parser::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 shift < 128 { + if ch != b'#' || parser.pos > parser.pos_marker + 1 { + *hash |= (ch as u128) << shift; + shift += 8; + } else { + is_ref = true; + } + } else { + shift = 0; + continue 'outer; + } + } + } + + 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, + ) -> crate::parser::Result; +} diff --git a/crates/protocol/src/request/parser.rs b/crates/protocol/src/request/parser.rs new file mode 100644 index 00000000..4e3cfac7 --- /dev/null +++ b/crates/protocol/src/request/parser.rs @@ -0,0 +1,220 @@ +use std::collections::HashMap; + +use crate::{ + error::{ + method::MethodError, + request::{RequestError, RequestLimitError}, + }, + method::{ + changes::ChangesRequest, + copy::{CopyBlobRequest, CopyRequest}, + get::GetRequest, + import::ImportEmailRequest, + parse::ParseEmailRequest, + query::QueryRequest, + query_changes::QueryChangesRequest, + set::SetRequest, + validate::ValidateSieveScriptRequest, + }, + parser::{json::Parser, Error, Ignore, JsonObjectParser, Token}, + types::id::Id, +}; + +use super::{ + capability::Capability, + echo::Echo, + method::{MethodFunction, MethodName, MethodObject}, + Call, Request, RequestMethod, +}; + +impl Request { + pub fn parse(json: &[u8], max_calls: usize, max_size: usize) -> Result { + if json.len() <= max_size { + let mut request = Request { + using: 0, + method_calls: Vec::new(), + created_ids: None, + }; + let mut found_valid_keys = false; + let mut parser = Parser::new(json); + parser.next_token::()?.assert(Token::DictStart)?; + while { + match parser.next_dict_key::()? { + 0x676e_6973_75 => { + found_valid_keys = true; + parser.next_token::()?.assert(Token::ArrayStart)?; + while { + request.using |= + parser.next_token::()?.unwrap_string("using")? as u32; + !parser.is_array_end()? + } {} + } + 0x736c_6c61_4364_6f68_7465_6d => { + found_valid_keys = true; + + parser + .next_token::()? + .assert_jmap(Token::ArrayStart)?; + while { + if request.method_calls.len() < max_calls { + parser + .next_token::()? + .assert_jmap(Token::ArrayStart)?; + let method = match parser.next_token::() { + Ok(Token::String(method)) => method, + Ok(_) => { + return Err(RequestError::not_request( + "Invalid JMAP request", + )); + } + Err(Error::Method(MethodError::InvalidArguments(_))) => { + MethodName::unknown_method() + } + Err(err) => { + return Err(err.into()); + } + }; + parser.next_token::()?.assert_jmap(Token::Comma)?; + parser.ctx = method.obj; + let start_depth_array = parser.depth_array; + let start_depth_dict = parser.depth_dict; + + let method = match (&method.fnc, &method.obj) { + (MethodFunction::Get, _) => { + GetRequest::parse(&mut parser).map(RequestMethod::Get) + } + (MethodFunction::Query, _) => { + QueryRequest::parse(&mut parser).map(RequestMethod::Query) + } + (MethodFunction::Set, _) => { + SetRequest::parse(&mut parser).map(RequestMethod::Set) + } + (MethodFunction::Changes, _) => { + ChangesRequest::parse(&mut parser) + .map(RequestMethod::Changes) + } + (MethodFunction::QueryChanges, _) => { + QueryChangesRequest::parse(&mut parser) + .map(RequestMethod::QueryChanges) + } + (MethodFunction::Copy, MethodObject::Email) => { + CopyRequest::parse(&mut parser).map(RequestMethod::Copy) + } + (MethodFunction::Copy, MethodObject::Blob) => { + CopyBlobRequest::parse(&mut parser) + .map(RequestMethod::CopyBlob) + } + (MethodFunction::Import, MethodObject::Email) => { + ImportEmailRequest::parse(&mut parser) + .map(RequestMethod::ImportEmail) + } + (MethodFunction::Parse, MethodObject::Email) => { + ParseEmailRequest::parse(&mut parser) + .map(RequestMethod::ParseEmail) + } + (MethodFunction::Validate, MethodObject::SieveScript) => { + ValidateSieveScriptRequest::parse(&mut parser) + .map(RequestMethod::ValidateScript) + } + (MethodFunction::Echo, MethodObject::Core) => { + Echo::parse(&mut parser).map(RequestMethod::Echo) + } + _ => Err(Error::Method(MethodError::UnknownMethod( + method.to_string(), + ))), + }; + + let method = match method { + Ok(method) => method, + Err(Error::Method(err)) => { + parser.skip_token(start_depth_array, start_depth_dict)?; + RequestMethod::Error(err) + } + Err(err) => { + return Err(err.into()); + } + }; + + parser.next_token::()?.assert_jmap(Token::Comma)?; + let id = parser.next_token::()?.unwrap_string("")?; + parser + .next_token::()? + .assert_jmap(Token::ArrayEnd)?; + request.method_calls.push(Call { id, method }); + } else { + return Err(RequestError::limit(RequestLimitError::CallsIn)); + } + !parser.is_array_end()? + } {} + } + 0x7364_4964_6574_6165_7263 => { + found_valid_keys = true; + let mut created_ids = HashMap::new(); + parser.next_token::()?.assert(Token::DictStart)?; + while { + created_ids.insert( + parser.next_dict_key::()?, + parser.next_token::()?.unwrap_string("createdIds")?, + ); + !parser.is_dict_end()? + } {} + request.created_ids = Some(created_ids); + } + _ => { + parser.skip_token(parser.depth_array, parser.depth_dict)?; + } + } + + !parser.is_dict_end()? + } {} + + if found_valid_keys { + Ok(request) + } else { + Err(RequestError::not_request("Invalid JMAP request")) + } + } else { + Err(RequestError::limit(RequestLimitError::Size)) + } + } +} + +impl From for RequestError { + fn from(value: Error) -> Self { + match value { + Error::Request(err) => err, + Error::Method(err) => RequestError::not_request(err.to_string()), + } + } +} + +#[cfg(test)] +mod tests { + use crate::request::Request; + + const TEST: &str = r#" + { + "using": [ "urn:ietf:params:jmap:core", "urn:ietf:params:jmap:mail" ], + "methodCalls": [ + [ "method1", { + "arg1": "arg1data", + "arg2": "arg2data" + }, "c1" ], + [ "Core/echo", { + "hello": true, + "high": 5 + }, "c2" ], + [ "method3", {"hello": [{"a": {"b": true}}]}, "c3" ] + ], + "createdIds": { + "c1": "m1", + "c2": "m2" + } + } + "#; + + #[test] + fn parse_request() { + println!("{:?}", Request::parse(TEST.as_bytes(), 10, 1024)); + } +} diff --git a/crates/protocol/src/request/reference.rs b/crates/protocol/src/request/reference.rs new file mode 100644 index 00000000..1010fcb6 --- /dev/null +++ b/crates/protocol/src/request/reference.rs @@ -0,0 +1,101 @@ +use std::fmt::Display; + +use crate::{ + error::method::MethodError, + parser::{json::Parser, Error, JsonObjectParser, Token}, + types::{id::Id, pointer::JSONPointer}, +}; + +use super::method::MethodName; + +#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize)] +pub struct ResultReference { + #[serde(rename = "resultOf")] + pub result_of: String, + pub name: MethodName, + pub path: JSONPointer, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum MaybeReference { + Value(V), + Reference(R), +} + +impl JsonObjectParser for ResultReference { + fn parse(parser: &mut Parser) -> crate::parser::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 { + match parser.next_dict_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)?; + } + } + + !parser.is_dict_end()? + } {} + + if let (Some(result_of), Some(name), Some(path)) = (result_of, name, path) { + Ok(Self { + result_of, + name, + path, + }) + } else { + Err(Error::Method(MethodError::InvalidResultReference( + "Missing required fields".into(), + ))) + } + } +} + +impl Display for ResultReference { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "{{ resultOf: {}, name: {}, path: {} }}", + self.result_of, self.name, self.path + ) + } +} + +impl Display for MaybeReference { + 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), + } + } +} + +// MaybeReference de/serialization +impl serde::Serialize for MaybeReference { + 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)), + } + } +} diff --git a/crates/protocol/src/types/acl.rs b/crates/protocol/src/types/acl.rs new file mode 100644 index 00000000..8fdbae01 --- /dev/null +++ b/crates/protocol/src/types/acl.rs @@ -0,0 +1,82 @@ +use std::fmt::{self, Display}; + +use crate::parser::{json::Parser, JsonObjectParser}; + +#[derive(Debug, Eq, PartialEq, PartialOrd, Ord, Hash, Clone, Copy)] +pub enum Acl { + Read, + Modify, + Delete, + ReadItems, + AddItems, + ModifyItems, + RemoveItems, + CreateChild, + Administer, + Submit, +} + +impl JsonObjectParser for Acl { + fn parse(parser: &mut Parser<'_>) -> crate::parser::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), + 0x736d_6574_4964_6165_72 => Ok(Acl::ReadItems), + 0x736d_6574_4964_6461 => Ok(Acl::AddItems), + 0x736d_6574_4979_6669_646f_6d => Ok(Acl::ModifyItems), + 0x736d_6574_4965_766f_6d65_72 => Ok(Acl::RemoveItems), + 0x646c_6968_4365_7461_6572_63 => Ok(Acl::CreateChild), + 0x7265_7473_696e_696d_6461 => Ok(Acl::Administer), + 0x7469_6d62_7573 => Ok(Acl::Submit), + _ => Err(parser.error_value()), + } + } +} + +impl Acl { + fn as_str(&self) -> &'static str { + match self { + Acl::Read => "read", + Acl::Modify => "modify", + Acl::Delete => "delete", + Acl::ReadItems => "readItems", + Acl::AddItems => "addItems", + Acl::ModifyItems => "modifyItems", + Acl::RemoveItems => "removeItems", + Acl::CreateChild => "createChild", + Acl::Administer => "administer", + Acl::Submit => "submit", + } + } +} + +impl Display for Acl { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{}", self.as_str()) + } +} + +impl serde::Serialize for Acl { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + serializer.serialize_str(self.as_str()) + } +} diff --git a/crates/protocol/src/types/blob.rs b/crates/protocol/src/types/blob.rs new file mode 100644 index 00000000..09659b79 --- /dev/null +++ b/crates/protocol/src/types/blob.rs @@ -0,0 +1,219 @@ +/* + * Copyright (c) 2020-2022, Stalwart Labs Ltd. + * + * This file is part of the Stalwart JMAP Server. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * in the LICENSE file at the top-level directory of this distribution. + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + * You can be released from the requirements of the AGPLv3 license by + * purchasing a commercial license. Please contact licensing@stalw.art + * for more details. +*/ + +use std::io::Write; + +use utils::codec::{ + base32_custom::Base32Writer, + leb128::{Leb128Iterator, Leb128Writer}, +}; + +use crate::parser::{base32::JsonBase32Reader, json::Parser, JsonObjectParser}; + +pub const BLOB_HASH_LEN: usize = 32; +pub const BLOB_LOCAL: u8 = 0; +pub const BLOB_EXTERNAL: u8 = 1; + +#[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)] +pub enum BlobHash { + Local { hash: [u8; BLOB_HASH_LEN] }, + External { hash: [u8; BLOB_HASH_LEN] }, +} + +#[derive(Clone, Debug, PartialEq, Eq, Hash)] +pub struct BlobId { + pub id: BlobHash, + pub section: Option, +} + +#[derive(Clone, Debug, PartialEq, Eq, Hash)] +pub struct BlobSection { + pub offset_start: usize, + pub size: usize, + pub encoding: u8, +} + +impl JsonObjectParser for BlobId { + fn parse(parser: &mut Parser<'_>) -> crate::parser::Result + where + Self: Sized, + { + let (is_local, encoding) = match parser + .next_unescaped()? + .ok_or_else(|| parser.error_value())? + { + b'b' => (false, None), + b'a' => (true, None), + b @ b'c'..=b'g' => (true, Some(b - b'c')), + b @ b'h'..=b'l' => (false, Some(b - b'h')), + _ => { + return Err(parser.error_value()); + } + }; + + let mut it = JsonBase32Reader::new(parser); + let mut hash = [0; BLOB_HASH_LEN]; + + for byte in hash.iter_mut().take(BLOB_HASH_LEN) { + *byte = it.next().ok_or_else(|| it.error())?; + } + + Ok(BlobId { + id: if is_local { + BlobHash::Local { hash } + } else { + BlobHash::External { hash } + }, + section: if let Some(encoding) = encoding { + BlobSection { + offset_start: it.next_leb128().ok_or_else(|| it.error())?, + size: it.next_leb128().ok_or_else(|| it.error())?, + encoding, + } + .into() + } else { + None + }, + }) + } +} + +impl BlobId { + pub fn new(id: BlobHash) -> Self { + BlobId { id, section: None } + } + + pub fn new_section(id: BlobHash, offset_start: usize, offset_end: usize, encoding: u8) -> Self { + BlobId { + id, + section: BlobSection { + offset_start, + size: offset_end - offset_start, + encoding, + } + .into(), + } + } + + pub fn start_offset(&self) -> usize { + if let Some(section) = &self.section { + section.offset_start + } else { + 0 + } + } +} + +impl From<&BlobHash> for BlobId { + fn from(id: &BlobHash) -> Self { + BlobId::new(id.clone()) + } +} + +impl From for BlobId { + fn from(id: BlobHash) -> Self { + BlobId::new(id) + } +} + +impl Default for BlobId { + fn default() -> Self { + Self { + id: BlobHash::Local { + hash: [0; BLOB_HASH_LEN], + }, + section: None, + } + } +} + +impl serde::Serialize for BlobId { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + serializer.serialize_str(self.to_string().as_str()) + } +} + +impl std::fmt::Display for BlobId { + #[allow(clippy::unused_io_amount)] + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let mut writer; + if let Some(section) = &self.section { + writer = + Base32Writer::with_capacity(BLOB_HASH_LEN + (std::mem::size_of::() * 2) + 1); + writer.push_char(char::from(if self.id.is_local() { + b'c' + section.encoding + } else { + b'h' + section.encoding + })); + writer.write(self.id.hash()).unwrap(); + writer.write_leb128(section.offset_start).unwrap(); + writer.write_leb128(section.size).unwrap(); + } else { + writer = Base32Writer::with_capacity(BLOB_HASH_LEN + 1); + writer.push_char(if self.id.is_local() { 'a' } else { 'b' }); + writer.write(self.id.hash()).unwrap(); + } + + f.write_str(&writer.finalize()) + } +} + +impl BlobHash { + /*pub fn new_local(bytes: &[u8]) -> Self { + // Create blob key + let mut hasher = Sha256::new(); + hasher.update(bytes); + + BlobId::Local { + hash: hasher.finalize().into(), + } + } + + pub fn new_external(bytes: &[u8]) -> Self { + // Create blob key + let mut hasher = Sha256::new(); + hasher.update(bytes); + + BlobId::External { + hash: hasher.finalize().into(), + } + }*/ + + pub fn is_local(&self) -> bool { + matches!(self, BlobHash::Local { .. }) + } + + pub fn is_external(&self) -> bool { + matches!(self, BlobHash::External { .. }) + } + + pub fn hash(&self) -> &[u8] { + match self { + BlobHash::Local { hash } => hash, + BlobHash::External { hash } => hash, + } + } +} diff --git a/crates/protocol/src/types/collection.rs b/crates/protocol/src/types/collection.rs new file mode 100644 index 00000000..6a80938d --- /dev/null +++ b/crates/protocol/src/types/collection.rs @@ -0,0 +1,12 @@ +#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)] +#[repr(u8)] +pub enum Collection { + Principal = 0, + PushSubscription = 1, + Mail = 2, + Mailbox = 3, + Thread = 4, + Identity = 5, + EmailSubmission = 6, + SieveScript = 7, +} diff --git a/crates/protocol/src/types/date.rs b/crates/protocol/src/types/date.rs new file mode 100644 index 00000000..ae5120e2 --- /dev/null +++ b/crates/protocol/src/types/date.rs @@ -0,0 +1,266 @@ +/* + * Copyright (c) 2020-2022, Stalwart Labs Ltd. + * + * This file is part of the Stalwart JMAP Server. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * in the LICENSE file at the top-level directory of this distribution. + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + * You can be released from the requirements of the AGPLv3 license by + * purchasing a commercial license. Please contact licensing@stalw.art + * for more details. +*/ + +use std::fmt::Display; + +use crate::parser::{json::Parser, JsonObjectParser}; + +#[derive(Debug, Default, Clone, PartialEq, Eq, Hash)] +pub struct UTCDate { + pub year: u16, + pub month: u8, + pub day: u8, + pub hour: u8, + pub minute: u8, + pub second: u8, + pub tz_before_gmt: bool, + pub tz_hour: u8, + pub tz_minute: u8, +} + +impl JsonObjectParser for UTCDate { + fn parse(parser: &mut Parser<'_>) -> crate::parser::Result + where + Self: Sized, + { + // 2004 - 06 - 28 T 23 : 43 : 45 . 000 Z + // 1969 - 02 - 13 T 23 : 32 : 00 - 03 : 30 + // 0 1 2 3 4 5 6 7 + + let mut pos = 0; + let mut parts = [0u32; 8]; + let mut parts_sizes = [ + 4u32, // Year (0) + 2u32, // Month (1) + 2u32, // Day (2) + 2u32, // Hour (3) + 2u32, // Minute (4) + 2u32, // Second (5) + 2u32, // TZ Hour (6) + 2u32, // TZ Minute (7) + ]; + let mut skip_digits = false; + let mut is_plus = true; + + while let Some(ch) = parser.next_unescaped()? { + match ch { + b'0'..=b'9' => { + if !skip_digits { + if parts_sizes[pos] > 0 { + parts_sizes[pos] -= 1; + parts[pos] += (ch - b'0') as u32 * u32::pow(10, parts_sizes[pos]); + } else { + break; + } + } + } + b'-' => { + if pos <= 1 { + pos += 1; + } else if pos == 5 { + pos += 1; + is_plus = false; + skip_digits = false; + } else { + break; + } + } + b'T' => { + if pos == 2 { + pos += 1; + } else { + break; + } + } + b':' => { + if [3, 4, 6].contains(&pos) { + pos += 1; + } else { + break; + } + } + b'+' => { + if pos == 5 { + pos += 1; + skip_digits = false; + } else { + break; + } + } + b'.' => { + if pos == 5 { + skip_digits = true; + } else { + break; + } + } + b'Z' | b'z' => (), + _ => { + break; + } + } + } + + if pos >= 5 { + Ok(UTCDate { + year: parts[0] as u16, + month: parts[1] as u8, + day: parts[2] as u8, + hour: parts[3] as u8, + minute: parts[4] as u8, + second: parts[5] as u8, + tz_hour: parts[6] as u8, + tz_minute: parts[7] as u8, + tz_before_gmt: !is_plus, + }) + } else { + Err(parser.error_value()) + } + } +} + +impl UTCDate { + pub fn from_timestamp(timestamp: i64) -> Self { + // Ported from http://howardhinnant.github.io/date_algorithms.html#civil_from_days + let (z, seconds) = ((timestamp / 86400) + 719468, timestamp % 86400); + let era: i64 = (if z >= 0 { z } else { z - 146096 }) / 146097; + let doe: u64 = (z - era * 146097) as u64; // [0, 146096] + let yoe: u64 = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365; // [0, 399] + let y: i64 = (yoe as i64) + era * 400; + let doy: u64 = doe - (365 * yoe + yoe / 4 - yoe / 100); // [0, 365] + let mp = (5 * doy + 2) / 153; // [0, 11] + let d: u64 = doy - (153 * mp + 2) / 5 + 1; // [1, 31] + let m: u64 = if mp < 10 { mp + 3 } else { mp - 9 }; // [1, 12] + let (h, mn, s) = (seconds / 3600, (seconds / 60) % 60, seconds % 60); + + UTCDate { + year: (y + i64::from(m <= 2)) as u16, + month: m as u8, + day: d as u8, + hour: h as u8, + minute: mn as u8, + second: s as u8, + tz_before_gmt: false, + tz_hour: 0, + tz_minute: 0, + } + } + + pub fn is_valid(&self) -> bool { + (0..=23).contains(&self.tz_hour) + && (1970..=3000).contains(&self.year) + && (0..=59).contains(&self.tz_minute) + && (1..=12).contains(&self.month) + && (1..=31).contains(&self.day) + && (0..=23).contains(&self.hour) + && (0..=59).contains(&self.minute) + && (0..=59).contains(&self.second) + } + + pub fn timestamp(&self) -> i64 { + // Ported from https://github.com/protocolbuffers/upb/blob/22182e6e/upb/json_decode.c#L982-L992 + let month = self.month as u32; + let year_base = 4800; /* Before min year, multiple of 400. */ + let m_adj = month.wrapping_sub(3); /* March-based month. */ + let carry = i64::from(m_adj > month); + let adjust = if carry > 0 { 12 } else { 0 }; + let y_adj = self.year as i64 + year_base - carry; + let month_days = ((m_adj.wrapping_add(adjust)) * 62719 + 769) / 2048; + let leap_days = y_adj / 4 - y_adj / 100 + y_adj / 400; + (y_adj * 365 + leap_days + month_days as i64 + (self.day as i64 - 1) - 2472632) * 86400 + + self.hour as i64 * 3600 + + self.minute as i64 * 60 + + self.second as i64 + + ((self.tz_hour as i64 * 3600 + self.tz_minute as i64 * 60) + * if self.tz_before_gmt { 1 } else { -1 }) + } +} + +impl Display for UTCDate { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + if self.tz_hour != 0 || self.tz_minute != 0 { + write!( + f, + "{:04}-{:02}-{:02}T{:02}:{:02}:{:02}{}{:02}:{:02}", + self.year, + self.month, + self.day, + self.hour, + self.minute, + self.second, + if self.tz_before_gmt && (self.tz_hour > 0 || self.tz_minute > 0) { + "-" + } else { + "+" + }, + self.tz_hour, + self.tz_minute, + ) + } else { + write!( + f, + "{:04}-{:02}-{:02}T{:02}:{:02}:{:02}Z", + self.year, self.month, self.day, self.hour, self.minute, self.second, + ) + } + } +} + +impl serde::Serialize for UTCDate { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + serializer.serialize_str(self.to_string().as_str()) + } +} + +#[cfg(test)] +mod tests { + use crate::{parser::json::Parser, types::date::UTCDate}; + + #[test] + fn parse_jmap_date() { + for (input, expected_result) in [ + ("1997-11-21T09:55:06-06:00", "1997-11-21T09:55:06-06:00"), + ("1997-11-21T09:55:06+00:00", "1997-11-21T09:55:06Z"), + ("2021-01-01T09:55:06+02:00", "2021-01-01T09:55:06+02:00"), + ("2004-06-28T23:43:45.000Z", "2004-06-28T23:43:45Z"), + ("1997-11-21T09:55:06.123+00:00", "1997-11-21T09:55:06Z"), + ( + "2021-01-01T09:55:06.4567+02:00", + "2021-01-01T09:55:06+02:00", + ), + ] { + let date = Parser::new(format!("\"{input}\"").as_bytes()) + .next_token::() + .unwrap() + .unwrap_string("") + .unwrap(); + assert_eq!(date.to_string(), expected_result); + + let timestamp = date.timestamp(); + assert_eq!(UTCDate::from_timestamp(timestamp).timestamp(), timestamp); + } + } +} diff --git a/crates/protocol/src/types/id.rs b/crates/protocol/src/types/id.rs new file mode 100644 index 00000000..3dbdb0d4 --- /dev/null +++ b/crates/protocol/src/types/id.rs @@ -0,0 +1,283 @@ +/* + * Copyright (c) 2020-2022, Stalwart Labs Ltd. + * + * This file is part of the Stalwart JMAP Server. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * in the LICENSE file at the top-level directory of this distribution. + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + * You can be released from the requirements of the AGPLv3 license by + * purchasing a commercial license. Please contact licensing@stalw.art + * for more details. +*/ + +use std::ops::Deref; + +use utils::codec::base32_custom::{BASE32_ALPHABET, BASE32_INVERSE}; + +use crate::{ + parser::{json::Parser, JsonObjectParser}, + request::reference::MaybeReference, +}; + +use super::DocumentId; + +#[derive(Debug, Clone, PartialEq, Eq, Hash, Copy)] +pub struct Id { + id: u64, +} + +impl Default for Id { + fn default() -> Self { + Id { id: u64::MAX } + } +} + +impl JsonObjectParser for Id { + fn parse(parser: &mut Parser<'_>) -> crate::parser::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 { id }) + } +} + +impl JsonObjectParser for MaybeReference { + fn parse(parser: &mut Parser<'_>) -> crate::parser::Result + where + Self: Sized, + { + let ch = parser + .next_unescaped()? + .ok_or_else(|| parser.error_value())?; + + if ch != b'#' { + let mut id = BASE32_INVERSE[ch as usize] as u64; + + if id != u8::MAX as u64 { + 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(MaybeReference::Value(Id { id })) + } else { + Err(parser.error_value()) + } + } else { + String::parse(parser).map(MaybeReference::Reference) + } + } +} + +impl Id { + pub fn new(id: u64) -> Self { + Self { id } + } + + pub fn singleton() -> Self { + Self::new(20080258862541) + } + + // From https://github.com/archer884/crockford by J/A + // License: MIT/Apache 2.0 + pub fn as_string(&self) -> String { + match self.id { + 0 => "a".to_string(), + mut n => { + // Used for the initial shift. + const QUAD_SHIFT: usize = 60; + const QUAD_RESET: usize = 4; + + // Used for all subsequent shifts. + const FIVE_SHIFT: usize = 59; + const FIVE_RESET: usize = 5; + + // After we clear the four most significant bits, the four least significant bits will be + // replaced with 0001. We can then know to stop once the four most significant bits are, + // likewise, 0001. + const STOP_BIT: u64 = 1 << QUAD_SHIFT; + + let mut buf = String::with_capacity(7); + + // Start by getting the most significant four bits. We get four here because these would be + // leftovers when starting from the least significant bits. In either case, tag the four least + // significant bits with our stop bit. + match (n >> QUAD_SHIFT) as usize { + // Eat leading zero-bits. This should not be done if the first four bits were non-zero. + // Additionally, we *must* do this in increments of five bits. + 0 => { + n <<= QUAD_RESET; + n |= 1; + n <<= n.leading_zeros() / 5 * 5; + } + + // Write value of first four bytes. + i => { + n <<= QUAD_RESET; + n |= 1; + buf.push(char::from(BASE32_ALPHABET[i])); + } + } + + // From now until we reach the stop bit, take the five most significant bits and then shift + // left by five bits. + while n != STOP_BIT { + buf.push(char::from(BASE32_ALPHABET[(n >> FIVE_SHIFT) as usize])); + n <<= FIVE_RESET; + } + + buf + } + } + } + + pub fn from_parts(prefix_id: DocumentId, doc_id: DocumentId) -> Id { + Id { + id: (prefix_id as u64) << 32 | doc_id as u64, + } + } + + pub fn get_document_id(&self) -> DocumentId { + (self.id & 0xFFFFFFFF) as DocumentId + } + + pub fn get_prefix_id(&self) -> DocumentId { + (self.id >> 32) as DocumentId + } + + pub fn is_singleton(&self) -> bool { + self.id == 20080258862541 + } +} + +impl From for Id { + fn from(id: u64) -> Self { + Id { id } + } +} + +impl From for Id { + fn from(id: u32) -> Self { + Id { id: id as u64 } + } +} + +impl From for u64 { + fn from(id: Id) -> Self { + id.id + } +} + +impl From<&Id> for u64 { + fn from(id: &Id) -> Self { + id.id + } +} + +impl From<(u32, u32)> for Id { + fn from(id: (u32, u32)) -> Self { + Id::from_parts(id.0, id.1) + } +} + +impl Deref for Id { + type Target = u64; + + fn deref(&self) -> &Self::Target { + &self.id + } +} + +impl AsRef for Id { + fn as_ref(&self) -> &u64 { + &self.id + } +} + +impl From for u32 { + fn from(id: Id) -> Self { + id.get_document_id() + } +} + +impl From for String { + fn from(id: Id) -> Self { + id.as_string() + } +} + +impl serde::Serialize for Id { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + serializer.serialize_str(self.as_string().as_str()) + } +} + +impl std::fmt::Display for Id { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str(&self.as_string()) + } +} + +#[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/protocol/src/types/keyword.rs b/crates/protocol/src/types/keyword.rs new file mode 100644 index 00000000..f71a2430 --- /dev/null +++ b/crates/protocol/src/types/keyword.rs @@ -0,0 +1,116 @@ +use std::fmt::Display; + +use crate::parser::{json::Parser, JsonObjectParser}; + +pub const SEEN: u8 = 0; +pub const DRAFT: u8 = 1; +pub const FLAGGED: u8 = 2; +pub const ANSWERED: u8 = 3; +pub const RECENT: u8 = 4; +pub const IMPORTANT: u8 = 5; +pub const PHISHING: u8 = 6; +pub const JUNK: u8 = 7; +pub const NOTJUNK: u8 = 8; +pub const DELETED: u8 = 9; +pub const FORWARDED: u8 = 10; +pub const MDN_SENT: u8 = 11; + +#[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize)] +#[serde(untagged)] +pub enum Keyword { + #[serde(rename(serialize = "$seen"))] + Seen, + #[serde(rename(serialize = "$draft"))] + Draft, + #[serde(rename(serialize = "$flagged"))] + Flagged, + #[serde(rename(serialize = "$answered"))] + Answered, + #[serde(rename(serialize = "$recent"))] + Recent, + #[serde(rename(serialize = "$important"))] + Important, + #[serde(rename(serialize = "$phishing"))] + Phishing, + #[serde(rename(serialize = "$junk"))] + Junk, + #[serde(rename(serialize = "$notjunk"))] + NotJunk, + #[serde(rename(serialize = "$deleted"))] + Deleted, + #[serde(rename(serialize = "$forwarded"))] + Forwarded, + #[serde(rename(serialize = "$mdnsent"))] + MdnSent, + Other(String), +} + +impl JsonObjectParser for Keyword { + fn parse(parser: &mut Parser<'_>) -> crate::parser::Result + where + Self: Sized, + { + 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), + 0x6465_6767_616c_66 => return Ok(Keyword::Flagged), + 0x6465_7265_7773_6e61 => return Ok(Keyword::Answered), + 0x746e_6563_6572 => return Ok(Keyword::Recent), + 0x746e_6174_726f_706d_69 => return Ok(Keyword::Important), + 0x676e_6968_7369_6870 => return Ok(Keyword::Phishing), + 0x6b6e_756a => return Ok(Keyword::Junk), + 0x6b6e_756a_746f_6e => return Ok(Keyword::NotJunk), + 0x0064_6574_656c_6564 => return Ok(Keyword::Deleted), + 0x6465_6472_6177_726f_66 => return Ok(Keyword::Forwarded), + 0x746e_6573_6e64_6d => return Ok(Keyword::MdnSent), + _ => (), + } + } + + if parser.is_eof || parser.skip_string() { + Ok(Keyword::Other( + String::from_utf8_lossy(parser.bytes[parser.pos_marker..parser.pos - 1].as_ref()) + .into_owned(), + )) + } else { + Err(parser.error_unterminated()) + } + } +} + +impl Display for Keyword { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Keyword::Seen => write!(f, "$seen"), + Keyword::Draft => write!(f, "$draft"), + Keyword::Flagged => write!(f, "$flagged"), + Keyword::Answered => write!(f, "$answered"), + Keyword::Recent => write!(f, "$recent"), + Keyword::Important => write!(f, "$important"), + Keyword::Phishing => write!(f, "$phishing"), + Keyword::Junk => write!(f, "$junk"), + Keyword::NotJunk => write!(f, "$notjunk"), + Keyword::Deleted => write!(f, "$deleted"), + Keyword::Forwarded => write!(f, "$forwarded"), + Keyword::MdnSent => write!(f, "$mdnsent"), + Keyword::Other(s) => write!(f, "{}", s), + } + } +} diff --git a/crates/protocol/src/types/mod.rs b/crates/protocol/src/types/mod.rs new file mode 100644 index 00000000..067eb2bc --- /dev/null +++ b/crates/protocol/src/types/mod.rs @@ -0,0 +1,14 @@ +pub mod acl; +pub mod blob; +pub mod collection; +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; + +pub type DocumentId = u32; +pub type ChangeId = u64; diff --git a/crates/protocol/src/types/pointer.rs b/crates/protocol/src/types/pointer.rs new file mode 100644 index 00000000..fda2c900 --- /dev/null +++ b/crates/protocol/src/types/pointer.rs @@ -0,0 +1,276 @@ +/* + * Copyright (c) 2020-2022, Stalwart Labs Ltd. + * + * This file is part of the Stalwart JMAP Server. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * in the LICENSE file at the top-level directory of this distribution. + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + * You can be released from the requirements of the AGPLv3 license by + * purchasing a commercial license. Please contact licensing@stalw.art + * for more details. +*/ + +use std::fmt::Display; + +use crate::parser::{json::Parser, JsonObjectParser}; + +#[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize)] +pub enum JSONPointer { + Root, + Wildcard, + String(String), + Number(u64), + Path(Vec), +} + +pub trait JSONPointerEval { + fn eval_json_pointer(&self, ptr: &JSONPointer) -> Option>; +} + +enum TokenType { + Unknown, + Number, + String, + Wildcard, + Escaped, +} + +impl JsonObjectParser for JSONPointer { + fn parse(parser: &mut Parser<'_>) -> crate::parser::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 is_item_query(&self, name: &str) -> bool { + match self { + JSONPointer::String(property) => property == name, + JSONPointer::Path(path) if path.len() == 2 => { + if let (Some(JSONPointer::String(property)), Some(JSONPointer::Wildcard)) = + (path.get(0), path.get(1)) + { + property == name + } else { + false + } + } + _ => false, + } + } +} + +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/protocol/src/types/property.rs b/crates/protocol/src/types/property.rs new file mode 100644 index 00000000..e0abf2cd --- /dev/null +++ b/crates/protocol/src/types/property.rs @@ -0,0 +1,807 @@ +use std::fmt::{Display, Formatter}; + +use serde::Serialize; + +use crate::parser::{json::Parser, Error, JsonObjectParser}; + +use super::{acl::Acl, id::Id, keyword::Keyword, value::Value}; + +#[derive(Debug, PartialEq, Eq, Hash, Clone, Serialize)] +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, + _T(String), +} + +#[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) -> crate::parser::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':' && first_char == b'h' && hash == 0x7265_6461_65 { + return parse_header_property(parser); + } else { + return parser.invalid_property(); + } + } + + parse_property(parser, first_char, hash) + } +} + +impl JsonObjectParser for SetProperty { + fn parse(parser: &mut Parser) -> crate::parser::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 == 0x7265_6461_65 && !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 = parse_property(parser, first_char, hash)?; + 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(Error::Method(_)) => { + property = parser.invalid_property()?; + } + Err(err) => { + return Err(err); + } + }, + Property::Keywords => match Keyword::parse(parser) { + Ok(keyword) => { + patch.push(Value::Keyword(keyword)); + } + Err(Error::Method(_)) => { + 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::Acl(acl)); + } + Err(Error::Method(_)) => { + 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( + parser: &mut Parser, + first_char: u8, + hash: u128, +) -> crate::parser::Result { + Ok(match first_char { + b'a' => match hash { + 0x6c63 => Property::Acl, + 0x7365_7361_696c => Property::Aliases, + 0x7374_6e65_6d68_6361_7474 => Property::Attachments, + _ => parser.invalid_property()?, + }, + b'b' => match hash { + 0x6363 => Property::Bcc, + 0x6449_626f_6c => Property::BlobId, + 0x6572_7574_6375_7274_5379_646f => Property::BodyStructure, + 0x7365_756c_6156_7964_6f => Property::BodyValues, + _ => parser.invalid_property()?, + }, + b'c' => match hash { + 0x7365_6974_696c_6962_6170_61 => Property::Capabilities, + 0x63 => Property::Cc, + 0x7465_7372_6168 => Property::Charset, + 0x6469 => Property::Cid, + _ => parser.invalid_property()?, + }, + b'd' => match hash { + 0x7375_7461_7453_7972_6576_696c_65 => Property::DeliveryStatus, + 0x6e6f_6974_7069_7263_7365 => Property::Description, + 0x6449_746e_6569_6c43_6563_6976_65 => Property::DeviceClientId, + 0x6e6f_6974_6973_6f70_7369 => Property::Disposition, + 0x7364_4962_6f6c_426e_73 => Property::DsnBlobIds, + _ => parser.invalid_property()?, + }, + b'e' => match hash { + 0x6c69_616d => Property::Email, + 0x6449_6c69_616d => Property::EmailId, + 0x7364_496c_6961_6d => Property::EmailIds, + 0x6570_6f6c_6576_6e => Property::Envelope, + 0x7365_7269_7078 => Property::Expires, + _ => parser.invalid_property()?, + }, + b'f' => match hash { + 0x6d6f_72 => Property::From, + 0x6574_6144_6d6f_72 => Property::FromDate, + _ => parser.invalid_property()?, + }, + b'h' => match hash { + 0x746e_656d_6863_6174_7441_7361 => Property::HasAttachment, + 0x7372_6564_6165 => Property::Headers, + 0x7964_6f42_6c6d_74 => Property::HtmlBody, + 0x6572_7574_616e_6769_536c_6d74 => Property::HtmlSignature, + _ => parser.invalid_property()?, + }, + b'i' => match hash { + 0x64 => Property::Id, + 0x0064_4979_7469_746e_6564 => Property::IdentityId, + 0x6f54_796c_7065_526e => Property::InReplyTo, + 0x6576_6974_6341_73 => Property::IsActive, + 0x6465_6c62_616e_4573 => Property::IsEnabled, + 0x6465_6269_7263_7362_7553_73 => Property::IsSubscribed, + _ => parser.invalid_property()?, + }, + b'k' => match hash { + 0x7379_65 => Property::Keys, + 0x7364_726f_7779_65 => Property::Keywords, + _ => parser.invalid_property()?, + }, + b'l' => match hash { + 0x6567_6175_676e_61 => Property::Language, + 0x6e6f_6974_6163_6f => Property::Location, + _ => parser.invalid_property()?, + }, + b'm' => match hash { + 0x7364_4978_6f62_6c69_61 => 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, + 0x7374_6867_6952_79 => Property::MyRights, + _ => parser.invalid_property()?, + }, + b'n' => match hash { + 0x656d_61 => Property::Name, + _ => parser.invalid_property()?, + }, + b'p' => match hash { + 0x6449_746e_6572_61 => Property::ParentId, + 0x6449_7472_61 => Property::PartId, + 0x6572_7574_6369 => Property::Picture, + 0x7765_6976_6572 => Property::Preview, + _ => parser.invalid_property()?, + }, + b'q' => match hash { + 0x6174_6f75 => Property::Quota, + _ => parser.invalid_property()?, + }, + b'r' => match hash { + 0x7441_6465_7669_6563_65 => Property::ReceivedAt, + 0x7365_636e_6572_6566_65 => Property::References, + 0x6f54_796c_7065 => Property::ReplyTo, + 0x656c_6f => Property::Role, + _ => parser.invalid_property()?, + }, + b's' => match hash { + 0x7465_7263_65 => Property::Secret, + 0x7441_646e_65 => Property::SendAt, + 0x7265_646e_65 => Property::Sender, + 0x7441_746e_65 => Property::SentAt, + 0x657a_69 => Property::Size, + 0x7265_6472_4f74_726f => Property::SortOrder, + 0x7463_656a_6275 => Property::Subject, + 0x7374_7261_5062_7573 => Property::SubParts, + _ => parser.invalid_property()?, + }, + b't' => match hash { + 0x7964_6f42_7478_65 => Property::TextBody, + 0x6572_7574_616e_6769_5374_7865 => Property::TextSignature, + 0x6449_6461_6572_68 => Property::ThreadId, + 0x656e_6f7a_656d_69 => Property::Timezone, + 0x6f => Property::To, + 0x6574_6144_6f => Property::ToDate, + 0x736c_6961_6d45_6c61_746f => Property::TotalEmails, + 0x7364_6165_7268_546c_6174_6f => Property::TotalThreads, + 0x6570_79 => Property::Type, + 0x7365_7079 => Property::Types, + _ => parser.invalid_property()?, + }, + b'u' => match hash { + 0x7375_7461_7453_6f64_6e => Property::UndoStatus, + 0x736c_6961_6d45_6461_6572_6e => Property::UnreadEmails, + 0x7364_6165_7268_5464_6165_726e => Property::UnreadThreads, + 0x6c72 => Property::Url, + _ => parser.invalid_property()?, + }, + b'v' => match hash { + 0x6564_6f43_6e6f_6974_6163_6966_6972_65 => Property::VerificationCode, + _ => parser.invalid_property()?, + }, + _ => parser.invalid_property()?, + }) +} + +fn parse_header_property(parser: &mut Parser) -> crate::parser::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, + 0x7365_7373_6572_6464_41 => 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, + 0x7761_52 => 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 })) +} + +impl JsonObjectParser for ObjectProperty { + fn parse(parser: &mut Parser) -> crate::parser::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 { + break; + } + } else { + first_char = ch; + } + } else if ch == b':' && first_char == b'h' && hash == 0x7265_6461_65 { + 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, + 0x6874_75 => Property::Auth, + _ => parser.invalid_property()?, + }, + b'b' => match hash { + 0x6449_626f_6c => 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, + _ => parser.invalid_property()?, + }, + b'i' => match hash { + 0x656c_626f_7250_676e_6964_6f63_6e45_73 => Property::IsEncodingProblem, + 0x6465_7461_636e_7572_5473 => Property::IsTruncated, + _ => parser.invalid_property()?, + }, + b'l' => match hash { + 0x6567_6175_676e_61 => Property::Language, + 0x6e6f_6974_6163_6f => Property::Location, + _ => parser.invalid_property()?, + }, + b'm' => match hash { + 0x6d6f_7246_6c69_61 => Property::MailFrom, + 0x736d_6574_4964_6165_5279_61 => Property::MayReadItems, + 0x736d_6574_4964_6441_7961 => Property::MayAddItems, + 0x736d_6574_4965_766f_6d65_5279_61 => Property::MayRemoveItems, + 0x6e65_6553_7465_5379_61 => Property::MaySetSeen, + 0x7364_726f_7779_654b_7465_5379_61 => Property::MaySetKeywords, + 0x646c_6968_4365_7461_6572_4379_61 => 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 { + 0x656d_61 => Property::Name, + _ => parser.invalid_property()?, + }, + b'p' => match hash { + 0x6449_7472_61 => Property::PartId, + 0x0068_6436_3532 => Property::P256dh, + 0x7372_6574_656d_6172_61 => Property::Parameters, + _ => parser.invalid_property()?, + }, + b'r' => match hash { + 0x6f54_7470_63 => Property::RcptTo, + _ => parser.invalid_property()?, + }, + b's' => match hash { + 0x657a_69 => Property::Size, + 0x7374_7261_5062_75 => Property::SubParts, + 0x796c_7065_5270_746d => Property::SmtpReply, + _ => parser.invalid_property()?, + }, + b't' => match hash { + 0x6570_79 => Property::Type, + _ => parser.invalid_property()?, + }, + b'v' => match hash { + 0x6575_6c61 => Property::Value, + _ => parser.invalid_property()?, + }, + _ => parser.invalid_property()?, + })) + } +} + +impl<'x> Parser<'x> { + fn invalid_property(&mut self) -> crate::parser::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 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::_T(s) => write!(f, "{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, Serialize)] +pub struct HeaderProperty { + pub form: HeaderForm, + pub header: String, + pub all: bool, +} + +#[derive(Debug, PartialEq, Eq, Hash, Clone, Copy, Serialize)] +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"), + } + } +} diff --git a/crates/protocol/src/types/state.rs b/crates/protocol/src/types/state.rs new file mode 100644 index 00000000..8c3b47df --- /dev/null +++ b/crates/protocol/src/types/state.rs @@ -0,0 +1,191 @@ +/* + * Copyright (c) 2020-2022, Stalwart Labs Ltd. + * + * This file is part of the Stalwart JMAP Server. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * in the LICENSE file at the top-level directory of this distribution. + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + * You can be released from the requirements of the AGPLv3 license by + * purchasing a commercial license. Please contact licensing@stalw.art + * for more details. +*/ + +use utils::codec::{ + base32_custom::Base32Writer, + leb128::{Leb128Iterator, Leb128Writer}, +}; + +use crate::parser::{base32::JsonBase32Reader, json::Parser, JsonObjectParser}; + +use super::ChangeId; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct JMAPIntermediateState { + pub from_id: ChangeId, + pub to_id: ChangeId, + pub items_sent: usize, +} + +#[derive(Debug, Clone, PartialEq, Eq, Default)] +pub enum State { + #[default] + Initial, + Exact(ChangeId), + Intermediate(JMAPIntermediateState), +} + +impl From for State { + fn from(change_id: ChangeId) -> Self { + State::Exact(change_id) + } +} + +impl JsonObjectParser for State { + fn parse(parser: &mut Parser<'_>) -> crate::parser::Result + where + Self: Sized, + { + match parser + .next_unescaped()? + .ok_or_else(|| parser.error_value())? + { + 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()) + } + b'r' => { + let mut it = JsonBase32Reader::new(parser); + + if let (Some(from_id), Some(to_id), Some(items_sent)) = ( + it.next_leb128::(), + it.next_leb128::(), + it.next_leb128::(), + ) { + if items_sent > 0 { + Ok(State::Intermediate(JMAPIntermediateState { + from_id, + to_id: from_id.saturating_add(to_id), + items_sent, + })) + } else { + Err(parser.error_value()) + } + } else { + Err(parser.error_value()) + } + } + _ => Err(parser.error_value()), + } + } +} + +impl State { + pub fn new_initial() -> Self { + State::Initial + } + + pub fn new_exact(id: ChangeId) -> Self { + State::Exact(id) + } + + pub fn new_intermediate(from_id: ChangeId, to_id: ChangeId, items_sent: usize) -> Self { + State::Intermediate(JMAPIntermediateState { + from_id, + to_id, + items_sent, + }) + } + + pub fn get_change_id(&self) -> ChangeId { + match self { + State::Exact(id) => *id, + State::Intermediate(intermediate) => intermediate.to_id, + State::Initial => ChangeId::MAX, + } + } +} + +impl serde::Serialize for State { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + serializer.serialize_str(self.to_string().as_str()) + } +} + +impl std::fmt::Display for State { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let mut writer = Base32Writer::with_capacity(10); + + match self { + State::Initial => { + writer.push_char('n'); + } + State::Exact(id) => { + writer.push_char('s'); + writer.write_leb128(*id).unwrap(); + } + State::Intermediate(intermediate) => { + writer.push_char('r'); + writer.write_leb128(intermediate.from_id).unwrap(); + writer + .write_leb128(intermediate.to_id - intermediate.from_id) + .unwrap(); + writer.write_leb128(intermediate.items_sent).unwrap(); + } + } + + f.write_str(&writer.finalize()) + } +} + +#[cfg(test)] +mod tests { + + use crate::{parser::json::Parser, types::ChangeId}; + + use super::State; + + #[test] + fn test_state_id() { + for id in [ + State::new_initial(), + State::new_exact(0), + State::new_exact(12345678), + State::new_exact(ChangeId::MAX), + State::new_intermediate(0, 0, 1), + State::new_intermediate(1024, 2048, 100), + State::new_intermediate(12345678, 87654321, 1), + State::new_intermediate(0, 0, 12345678), + State::new_intermediate(0, 87654321, 12345678), + State::new_intermediate(12345678, 87654321, 1), + 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 + ); + } + } +} diff --git a/crates/protocol/src/types/type_state.rs b/crates/protocol/src/types/type_state.rs new file mode 100644 index 00000000..a4dd4f99 --- /dev/null +++ b/crates/protocol/src/types/type_state.rs @@ -0,0 +1,69 @@ +use std::fmt::Display; + +use serde::Serialize; + +use crate::parser::{json::Parser, JsonObjectParser}; + +#[derive(Debug, Eq, PartialEq, Hash, Clone, Copy, Serialize)] +pub enum TypeState { + #[serde(rename = "Email")] + Email = 0, + #[serde(rename = "EmailDelivery")] + EmailDelivery = 1, + #[serde(rename = "EmailSubmission")] + EmailSubmission = 2, + #[serde(rename = "Mailbox")] + Mailbox = 3, + #[serde(rename = "Thread")] + Thread = 4, + #[serde(rename = "Identity")] + Identity = 5, +} + +impl JsonObjectParser for TypeState { + fn parse(parser: &mut Parser<'_>) -> crate::parser::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 { + 0x6c69_616d_45 => Ok(TypeState::Email), + 0x7972_6576_696c_6544_6c69_616d_45 => Ok(TypeState::EmailDelivery), + 0x6e6f_6973_7369_6d62_7553_6c69_616d_45 => Ok(TypeState::EmailSubmission), + 0x786f_626c_6961_4d => Ok(TypeState::Mailbox), + 0x6461_6572_6854 => Ok(TypeState::Thread), + 0x7974_6974_6e65_6449 => Ok(TypeState::Identity), + _ => Err(parser.error_value()), + } + } +} + +impl TypeState { + pub fn as_str(&self) -> &'static str { + match self { + TypeState::Email => "Email", + TypeState::EmailDelivery => "EmailDelivery", + TypeState::EmailSubmission => "EmailSubmission", + TypeState::Mailbox => "Mailbox", + TypeState::Thread => "Thread", + TypeState::Identity => "Identity", + } + } +} + +impl Display for TypeState { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.as_str()) + } +} diff --git a/crates/protocol/src/types/value.rs b/crates/protocol/src/types/value.rs new file mode 100644 index 00000000..40e47bbf --- /dev/null +++ b/crates/protocol/src/types/value.rs @@ -0,0 +1,206 @@ +use std::fmt::Display; + +use serde::Serialize; +use utils::map::vec_map::VecMap; + +use crate::{ + parser::{json::Parser, Ignore, JsonObjectParser, Token}, + request::reference::ResultReference, +}; + +use super::{ + acl::Acl, + blob::BlobId, + date::UTCDate, + id::Id, + keyword::Keyword, + property::{HeaderForm, IntoProperty, ObjectProperty, Property}, + type_state::TypeState, +}; + +#[derive(Debug, Clone, PartialEq, Eq, Serialize)] +pub enum Value { + Text(String), + UnsignedInt(u64), + Bool(bool), + Id(Id), + Date(UTCDate), + BlobId(BlobId), + Keyword(Keyword), + TypeState(TypeState), + Acl(Acl), + List(Vec), + Object(VecMap), + Null, +} + +#[derive(Debug, Clone)] +pub enum SetValue { + Value(Value), + Patch(Vec), + ResultReference(ResultReference), +} + +#[derive(Debug, Clone)] +pub struct SetValueMap { + pub values: Vec, +} + +pub trait IntoValue: Eq { + fn into_value(self) -> Value; +} + +impl Value { + pub fn parse( + parser: &mut Parser<'_>, + ) -> crate::parser::Result { + Ok(match parser.next_token::()? { + Token::String(v) => v.into_value(), + Token::DictStart => { + let mut properties = VecMap::with_capacity(4); + while { + let property = parser.next_dict_key::()?.into_property(); + let value = Value::from_property(parser, &property)?; + properties.append(property, value); + !parser.is_dict_end()? + } {} + Value::Object(properties) + } + Token::ArrayStart => { + let mut values = Vec::with_capacity(4); + while { + values.push(Value::parse::(parser)?); + !parser.is_array_end()? + } {} + 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, + ) -> crate::parser::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) + } else { + Value::parse::(parser) + } + } + + Property::Headers + | Property::Addresses + | Property::MailFrom + | Property::RcptTo + | Property::SubParts => Value::parse::(parser), + Property::Language | Property::Parameters => Value::parse::(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), + } + } +} + +impl JsonObjectParser for SetValueMap { + fn parse(parser: &mut Parser<'_>) -> crate::parser::Result + where + Self: Sized, + { + let mut values = Vec::new(); + match parser.next_token::()? { + Token::DictStart => { + parser.next_token::()?.assert(Token::DictStart)?; + while { + let value = parser.next_dict_key::()?; + if bool::parse(parser)? { + values.push(value); + } + !parser.is_dict_end()? + } {} + } + 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 IntoValue for Acl { + fn into_value(self) -> Value { + Value::Acl(self) + } +} + +impl IntoValue for TypeState { + fn into_value(self) -> Value { + Value::TypeState(self) + } +} diff --git a/crates/store/Cargo.lock b/crates/store/Cargo.lock new file mode 100644 index 00000000..421639c3 --- /dev/null +++ b/crates/store/Cargo.lock @@ -0,0 +1,1540 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "adler" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" + +[[package]] +name = "ahash" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcb51a0695d8f838b1ee009b3fbf66bda078cd64590202a864a8f3e8c4315c47" +dependencies = [ + "getrandom", + "once_cell", + "version_check", +] + +[[package]] +name = "ahash" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c99f64d1e06488f620f932677e24bc6e2897582980441ae90a671415bd7ec2f" +dependencies = [ + "cfg-if", + "getrandom", + "once_cell", + "serde", + "version_check", +] + +[[package]] +name = "aho-corasick" +version = "0.7.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc936419f96fa211c1b9166887b38e5e40b19958e5b895be7c1f93adec7071ac" +dependencies = [ + "memchr", +] + +[[package]] +name = "arrayref" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b4930d2cb77ce62f89ee5d5289b4ac049559b1c45539271f5ed4fdc7db34545" + +[[package]] +name = "arrayvec" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8da52d66c7071e2e3fa2a1e5c6d088fec47b593032b254f5e980de8ea54454d6" + +[[package]] +name = "async-recursion" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e97ce7de6cf12de5d7226c73f5ba9811622f4db3a5b91b55c53e987e5f91cba" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.3", +] + +[[package]] +name = "async-trait" +version = "0.1.67" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86ea188f25f0255d8f92797797c97ebf5631fa88178beb1a46fdf5622c9a00e4" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.3", +] + +[[package]] +name = "atty" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" +dependencies = [ + "hermit-abi 0.1.19", + "libc", + "winapi", +] + +[[package]] +name = "autocfg" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" + +[[package]] +name = "bindgen" +version = "0.60.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "062dddbc1ba4aca46de6338e2bf87771414c335f7b2f2036e8f3e9befebf88e6" +dependencies = [ + "bitflags 1.3.2", + "cexpr", + "clang-sys", + "clap", + "env_logger", + "lazy_static", + "lazycell", + "log", + "peeking_take_while", + "proc-macro2", + "quote", + "regex", + "rustc-hash", + "shlex", + "which", +] + +[[package]] +name = "bindgen" +version = "0.64.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4243e6031260db77ede97ad86c27e501d646a27ab57b59a574f725d98ab1fb4" +dependencies = [ + "bitflags 1.3.2", + "cexpr", + "clang-sys", + "lazy_static", + "lazycell", + "peeking_take_while", + "proc-macro2", + "quote", + "regex", + "rustc-hash", + "shlex", + "syn 1.0.109", +] + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "487f1e0fcbe47deb8b0574e646def1c903389d95241dd1bbcc6ce4a715dfc0c1" + +[[package]] +name = "bitpacking" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8c7d2ac73c167c06af4a5f37e6e59d84148d57ccbe4480b76f0273eefea82d7" +dependencies = [ + "crunchy", +] + +[[package]] +name = "blake3" +version = "1.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42ae2468a89544a466886840aa467a25b766499f4f04bf7d9fcd10ecee9fccef" +dependencies = [ + "arrayref", + "arrayvec", + "cc", + "cfg-if", + "constant_time_eq", + "digest", +] + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "bytemuck" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17febce684fd15d89027105661fec94afb475cb995fbc59d2865198446ba2eea" + +[[package]] +name = "byteorder" +version = "1.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" + +[[package]] +name = "bytes" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89b2fd2a0dcf38d7971e2194b6b6eebab45ae01067456a7fd93d5547a61b70be" + +[[package]] +name = "bzip2-sys" +version = "0.1.11+1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "736a955f3fa7875102d57c82b8cac37ec45224a07fd32d58f9f7a186b6cd4cdc" +dependencies = [ + "cc", + "libc", + "pkg-config", +] + +[[package]] +name = "cc" +version = "1.0.79" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50d30906286121d95be3d479533b458f87493b30a4b5f79a607db8f5d11aa91f" +dependencies = [ + "jobserver", +] + +[[package]] +name = "cedarwood" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d910bedd62c24733263d0bed247460853c9d22e8956bd4cd964302095e04e90" +dependencies = [ + "smallvec", +] + +[[package]] +name = "cexpr" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" +dependencies = [ + "nom", +] + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "clang-sys" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ed9a53e5d4d9c573ae844bfac6872b159cb1d1585a83b29e7a64b7eef7332a" +dependencies = [ + "glob", + "libc", + "libloading", +] + +[[package]] +name = "clap" +version = "3.2.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71655c45cb9845d3270c9d6df84ebe72b4dad3c2ba3f7023ad47c144e4e473a5" +dependencies = [ + "atty", + "bitflags 1.3.2", + "clap_lex", + "indexmap", + "strsim", + "termcolor", + "textwrap", +] + +[[package]] +name = "clap_lex" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2850f2f5a82cbf437dd5af4d49848fbdfc27c157c3d010345776f952765261c5" +dependencies = [ + "os_str_bytes", +] + +[[package]] +name = "constant_time_eq" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13418e745008f7349ec7e449155f419a61b92b58a99cc3616942b926825ec76b" + +[[package]] +name = "crc32fast" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crossbeam-channel" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf2b3e8478797446514c91ef04bafcb59faba183e621ad488df88983cc14128c" +dependencies = [ + "cfg-if", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce6fd6f855243022dcecf8702fef0c297d4338e226845fe067f6341ad9fa0cef" +dependencies = [ + "cfg-if", + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46bd5f3f85273295a9d14aedfb86f6aadbff6d8f5295c4a9edb08e819dcf5695" +dependencies = [ + "autocfg", + "cfg-if", + "crossbeam-utils", + "memoffset", + "scopeguard", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c063cd8cc95f5c377ed0d4b49a4b21f632396ff690e8470c29b3359b346984b" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crunchy" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7" + +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "csv" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b015497079b9a9d69c02ad25de6c0a6edef051ea6360a327d0bd05802ef64ad" +dependencies = [ + "csv-core", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "csv-core" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b2466559f260f48ad25fe6317b3c8dac77b5bdb5763ac7d9d6103530663bc90" +dependencies = [ + "memchr", +] + +[[package]] +name = "digest" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8168378f4e5023e7218c89c891c0fd8ecdb5e5e4f18cb78f38cf245dd021e76f" +dependencies = [ + "block-buffer", + "crypto-common", + "subtle", +] + +[[package]] +name = "either" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fcaabb2fef8c910e7f4c7ce9f67a1283a1715879a7c230ca9d6d1ae31f16d91" + +[[package]] +name = "env_logger" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a12e6657c4c97ebab115a42dcee77225f7f482cdd841cf7088c657a42e9e00e7" +dependencies = [ + "atty", + "humantime", + "log", + "regex", + "termcolor", +] + +[[package]] +name = "fallible-iterator" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4443176a9f2c162692bd3d352d745ef9413eec5782a80d8fd6f8a1ac692a07f7" + +[[package]] +name = "fallible-streaming-iterator" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" + +[[package]] +name = "farmhash" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f35ce9c8fb9891c75ceadbc330752951a4e369b50af10775955aeb9af3eee34b" + +[[package]] +name = "flate2" +version = "1.0.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8a2db397cb1c8772f31494cb8917e48cd1e64f0fa7efac59fbd741a0a8ce841" +dependencies = [ + "crc32fast", + "libz-sys", + "miniz_oxide", +] + +[[package]] +name = "foundationdb" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69adb701525370e5f8958454b46e8459b276d81ce6391edbf84eae32eeddff75" +dependencies = [ + "async-recursion", + "async-trait", + "foundationdb-gen", + "foundationdb-macros", + "foundationdb-sys", + "futures", + "memchr", + "rand", + "static_assertions", + "uuid", +] + +[[package]] +name = "foundationdb-gen" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "134e1c986a2bb78904f426d4924a55e8c14162ba764e229501eb6f95c8c37489" +dependencies = [ + "xml-rs", +] + +[[package]] +name = "foundationdb-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2163c7326208be8edc605e10303ec6ae45cf106c12540754a9970bcce0f80cae" +dependencies = [ + "quote", + "syn 1.0.109", +] + +[[package]] +name = "foundationdb-sys" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3eb26eee771096794dbee1a2a9defa455443a2c150a810386331aa0d6603d356" +dependencies = [ + "bindgen 0.60.1", +] + +[[package]] +name = "futures" +version = "0.3.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "531ac96c6ff5fd7c62263c5e3c67a603af4fcaee2e1a0ae5565ba3a11e69e549" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "164713a5a0dcc3e7b4b1ed7d3b433cabc18025386f9339346e8daf15963cf7ac" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86d7a0c1aa76363dac491de0ee99faf6941128376f1cf96f07db7603b7de69dd" + +[[package]] +name = "futures-executor" +version = "0.3.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1997dd9df74cdac935c76252744c1ed5794fac083242ea4fe77ef3ed60ba0f83" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89d422fa3cbe3b40dca574ab087abb5bc98258ea57eea3fd6f1fa7162c778b91" + +[[package]] +name = "futures-macro" +version = "0.3.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3eb14ed937631bd8b8b8977f2c198443447a8355b6e3ca599f38c975e5a963b6" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "futures-sink" +version = "0.3.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec93083a4aecafb2a80a885c9de1f0ccae9dbd32c2bb54b0c3a65690e0b8d2f2" + +[[package]] +name = "futures-task" +version = "0.3.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd65540d33b37b16542a0438c12e6aeead10d4ac5d05bd3f805b8f35ab592879" + +[[package]] +name = "futures-util" +version = "0.3.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ef6b17e481503ec85211fed8f39d1970f128935ca1f814cd32ac4a6842e84ab" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "pin-utils", + "slab", +] + +[[package]] +name = "fxhash" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c31b6d751ae2c7f11320402d34e41349dd1016f8d5d45e48c4312bc8625af50c" +dependencies = [ + "byteorder", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c05aeb6a22b8f62540c194aac980f2115af067bfe15a0734d7277a768d396b31" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "glob" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" + +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" +dependencies = [ + "ahash 0.7.6", +] + +[[package]] +name = "hashlink" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69fe1fcf8b4278d860ad0548329f892a3631fb63f82574df68275f34cdbe0ffa" +dependencies = [ + "hashbrown", +] + +[[package]] +name = "hermit-abi" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" +dependencies = [ + "libc", +] + +[[package]] +name = "hermit-abi" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee512640fe35acbfb4bb779db6f0d80704c2cacfa2e39b601ef3e3f47d1ae4c7" +dependencies = [ + "libc", +] + +[[package]] +name = "humantime" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" + +[[package]] +name = "indexmap" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg", + "hashbrown", +] + +[[package]] +name = "itoa" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "453ad9f582a441959e5f0d088b02ce04cfe8d51a8eaf077f12ac6d3e94164ca6" + +[[package]] +name = "jieba-rs" +version = "0.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37228e06c75842d1097432d94d02f37fe3ebfca9791c2e8fef6e9db17ed128c1" +dependencies = [ + "cedarwood", + "fxhash", + "hashbrown", + "lazy_static", + "phf", + "phf_codegen", + "regex", +] + +[[package]] +name = "jobserver" +version = "0.1.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "936cfd212a0155903bcbc060e316fb6cc7cbf2e1907329391ebadc1fe0ce77c2" +dependencies = [ + "libc", +] + +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" + +[[package]] +name = "lazycell" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" + +[[package]] +name = "libc" +version = "0.2.140" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99227334921fae1a979cf0bfdfcc6b3e5ce376ef57e16fb6fb3ea2ed6095f80c" + +[[package]] +name = "libloading" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b67380fd3b2fbe7527a606e18729d21c6f3951633d0500574c4dc22d2d638b9f" +dependencies = [ + "cfg-if", + "winapi", +] + +[[package]] +name = "librocksdb-sys" +version = "0.10.0+7.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fe4d5874f5ff2bc616e55e8c6086d478fcda13faf9495768a4aa1c22042d30b" +dependencies = [ + "bindgen 0.64.0", + "bzip2-sys", + "cc", + "glob", + "libc", + "libz-sys", + "lz4-sys", + "zstd-sys", +] + +[[package]] +name = "libsqlite3-sys" +version = "0.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "afc22eff61b133b115c6e8c74e818c628d6d5e7a502afea6f64dee076dd94326" +dependencies = [ + "cc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "libz-sys" +version = "1.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9702761c3935f8cc2f101793272e202c72b99da8f4224a19ddcf1279a6450bbf" +dependencies = [ + "cc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "linked-hash-map" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" + +[[package]] +name = "lock_api" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "435011366fe56583b16cf956f9df0095b405b82d76425bc8981c0e22e60ec4df" +dependencies = [ + "autocfg", + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "lru-cache" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31e24f1ad8321ca0e8a1e0ac13f23cb668e6f5466c2c57319f6a5cf1cc8e3b1c" +dependencies = [ + "linked-hash-map", +] + +[[package]] +name = "lz4-sys" +version = "1.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57d27b317e207b10f69f5e75494119e391a96f48861ae870d1da6edac98ca900" +dependencies = [ + "cc", + "libc", +] + +[[package]] +name = "maplit" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e2e65a1a2e43cfcb47a895c4c8b10d1f4a61097f9f254f183aee60cad9c651d" + +[[package]] +name = "maybe-async" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f1b8c13cb1f814b634a96b2c725449fe7ed464a7b8781de8688be5ffbd3f305" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "memchr" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" + +[[package]] +name = "memoffset" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d61c719bcfbcf5d62b3a09efa6088de8c54bc0bfcd3ea7ae39fcc186108b8de1" +dependencies = [ + "autocfg", +] + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] +name = "miniz_oxide" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b275950c28b37e794e8c55d88aeb5e139d0ce23fdbbeda68f8d7174abdf9e8fa" +dependencies = [ + "adler", +] + +[[package]] +name = "mio" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b9d9a46eff5b4ff64b45a9e316a6d1e0bc719ef429cbec4dc630684212bfdf9" +dependencies = [ + "libc", + "log", + "wasi", + "windows-sys", +] + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "num_cpus" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fac9e2da13b5eb447a6ce3d392f23a29d8694bff781bf03a16cd9ac8697593b" +dependencies = [ + "hermit-abi 0.2.6", + "libc", +] + +[[package]] +name = "once_cell" +version = "1.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7e5500299e16ebb147ae15a00a942af264cf3688f47923b8fc2cd5858f23ad3" + +[[package]] +name = "os_str_bytes" +version = "6.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ceedf44fb00f2d1984b0bc98102627ce622e083e49a5bacdb3e514fa4238e267" + +[[package]] +name = "parking_lot" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9069cbb9f99e3a5083476ccb29ceb1de18b9118cafa53e90c9551235de2b9521" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-sys", +] + +[[package]] +name = "peeking_take_while" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19b17cddbe7ec3f8bc800887bab5e717348c95ea2ca0b1bf0837fb964dc67099" + +[[package]] +name = "phf" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "928c6535de93548188ef63bb7c4036bd415cd8f36ad25af44b9789b2ee72a48c" +dependencies = [ + "phf_shared", +] + +[[package]] +name = "phf_codegen" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a56ac890c5e3ca598bbdeaa99964edb5b0258a583a9eb6ef4e89fc85d9224770" +dependencies = [ + "phf_generator", + "phf_shared", +] + +[[package]] +name = "phf_generator" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1181c94580fa345f50f19d738aaa39c0ed30a600d95cb2d3e23f94266f14fbf" +dependencies = [ + "phf_shared", + "rand", +] + +[[package]] +name = "phf_shared" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1fb5f6f826b772a8d4c0394209441e7d37cbbb967ae9c7e0e8134365c9ee676" +dependencies = [ + "siphasher", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0a7ae3ac2f1173085d398531c705756c94a4c56843785df85a60c1a0afac116" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "pkg-config" +version = "0.3.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ac9a59f73473f1b8d852421e59e64809f025994837ef743615c6d0c5b305160" + +[[package]] +name = "ppv-lite86" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" + +[[package]] +name = "proc-macro2" +version = "1.0.52" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d0e1ae9e836cc3beddd63db0df682593d7e2d3d891ae8c9083d2113e1744224" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4424af4bf778aae2051a77b60283332f386554255d722233d09fbfc7e30da2fc" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r2d2" +version = "0.8.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51de85fb3fb6524929c8a2eb85e6b6d363de4e8c48f9e2c2eac4944abc181c93" +dependencies = [ + "log", + "parking_lot", + "scheduled-thread-pool", +] + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom", +] + +[[package]] +name = "rayon" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d2df5196e37bcc87abebc0053e20787d73847bb33134a69841207dd0a47f03b" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b8f95bd6966f5c87776639160a66bd8ab9895d9d4ab01ddba9fc60661aebe8d" +dependencies = [ + "crossbeam-channel", + "crossbeam-deque", + "crossbeam-utils", + "num_cpus", +] + +[[package]] +name = "redox_syscall" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" +dependencies = [ + "bitflags 1.3.2", +] + +[[package]] +name = "regex" +version = "1.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48aaa5748ba571fb95cd2c85c09f629215d3a6ece942baa100950af03a34f733" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.6.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "456c603be3e8d448b072f410900c09faf164fbce2d480456f50eea6e25f9c848" + +[[package]] +name = "retain_mut" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c31b5c4033f8fdde8700e4657be2c497e7288f01515be52168c631e2e4d4086" + +[[package]] +name = "roaring" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef0fb5e826a8bde011ecae6a8539dd333884335c57ff0f003fbe27c25bbe8f71" +dependencies = [ + "bytemuck", + "byteorder", + "retain_mut", +] + +[[package]] +name = "rocksdb" +version = "0.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "015439787fce1e75d55f279078d33ff14b4af5d93d995e8838ee4631301c8a99" +dependencies = [ + "libc", + "librocksdb-sys", +] + +[[package]] +name = "rusqlite" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "549b9d036d571d42e6e85d1c1425e2ac83491075078ca9a15be021c56b1641f2" +dependencies = [ + "bitflags 2.0.2", + "fallible-iterator", + "fallible-streaming-iterator", + "hashlink", + "libsqlite3-sys", + "smallvec", +] + +[[package]] +name = "rust-stemmers" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e46a2036019fdb888131db7a4c847a1063a7493f971ed94ea82c67eada63ca54" +dependencies = [ + "serde", + "serde_derive", +] + +[[package]] +name = "rustc-hash" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" + +[[package]] +name = "ryu" +version = "1.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f91339c0467de62360649f8d3e185ca8de4224ff281f66000de5eb2a77a79041" + +[[package]] +name = "scheduled-thread-pool" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3cbc66816425a074528352f5789333ecff06ca41b36b0b0efdfbb29edc391a19" +dependencies = [ + "parking_lot", +] + +[[package]] +name = "scopeguard" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" + +[[package]] +name = "serde" +version = "1.0.158" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "771d4d9c4163ee138805e12c710dd365e4f44be8be0503cb1bb9eb989425d9c9" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.158" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e801c1712f48475582b7696ac71e0ca34ebb30e09338425384269d9717c62cad" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.3", +] + +[[package]] +name = "shlex" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43b2853a4d09f215c24cc5489c992ce46052d359b5109343cbafbf26bc62f8a3" + +[[package]] +name = "signal-hook-registry" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8229b473baa5980ac72ef434c4415e70c4b5e71b423043adb4ba059f89c99a1" +dependencies = [ + "libc", +] + +[[package]] +name = "siphasher" +version = "0.3.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7bd3e3206899af3f8b12af284fafc038cc1dc2b41d1b89dd17297221c5d225de" + +[[package]] +name = "slab" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6528351c9bc8ab22353f9d776db39a20288e8d6c37ef8cfe3317cf875eecfc2d" +dependencies = [ + "autocfg", +] + +[[package]] +name = "smallvec" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a507befe795404456341dfab10cef66ead4c041f62b8b11bbb92bffe5d0953e0" + +[[package]] +name = "socket2" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64a4a911eed85daf18834cfaa86a79b7d266ff93ff5ba14005426219480ed662" +dependencies = [ + "libc", + "winapi", +] + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[package]] +name = "store" +version = "0.1.0" +dependencies = [ + "ahash 0.8.3", + "bitpacking", + "blake3", + "csv", + "farmhash", + "flate2", + "foundationdb", + "futures", + "jieba-rs", + "lazy_static", + "lru-cache", + "maybe-async", + "parking_lot", + "r2d2", + "rand", + "rayon", + "roaring", + "rocksdb", + "rusqlite", + "rust-stemmers", + "serde", + "siphasher", + "tinysegmenter", + "tokio", + "utils", + "whatlang", + "xxhash-rust", +] + +[[package]] +name = "strsim" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" + +[[package]] +name = "subtle" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601" + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8234ae35e70582bfa0f1fedffa6daa248e41dd045310b19800c4a36382c8f60" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "termcolor" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be55cf8942feac5c765c2c993422806843c9a9a45d4d5c407ad6dd2ea95eb9b6" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "textwrap" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "222a222a5bfe1bba4a77b45ec488a741b3cb8872e5e499451fd7d0129c9c7c3d" + +[[package]] +name = "tinysegmenter" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1755695d17d470baf2d937a59ab4e86de3034b056fc8700e21411b0efca36497" +dependencies = [ + "lazy_static", + "maplit", +] + +[[package]] +name = "tokio" +version = "1.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03201d01c3c27a29c8a5cee5b55a93ddae1ccf6f08f65365c2c918f8c1b76f64" +dependencies = [ + "autocfg", + "bytes", + "libc", + "memchr", + "mio", + "num_cpus", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "windows-sys", +] + +[[package]] +name = "tokio-macros" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d266c00fde287f55d3f1c3e96c500c362a2b8c695076ec180f27918820bc6df8" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "typenum" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "497961ef93d974e23eb6f433eb5fe1b7930b659f06d12dec6fc44a8f554c0bba" + +[[package]] +name = "unicode-ident" +version = "1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5464a87b239f13a63a501f2701565754bae92d243d4bb7eb12f6d57d2269bf4" + +[[package]] +name = "utils" +version = "0.1.0" +dependencies = [ + "serde", +] + +[[package]] +name = "uuid" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1674845326ee10d37ca60470760d4288a6f80f304007d92e5c53bab78c9cfd79" + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "version_check" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "whatlang" +version = "0.16.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c531a2dc4c462b833788be2c07eef4e621d0e9edbd55bf280cc164c1c1aa043" +dependencies = [ + "hashbrown", + "once_cell", +] + +[[package]] +name = "which" +version = "4.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2441c784c52b289a054b7201fc93253e288f094e2f4be9058343127c4226a269" +dependencies = [ + "either", + "libc", + "once_cell", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" +dependencies = [ + "winapi", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-sys" +version = "0.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" + +[[package]] +name = "windows_i686_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" + +[[package]] +name = "windows_i686_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" + +[[package]] +name = "xml-rs" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2d7d3948613f75c98fd9328cfdcc45acc4d360655289d0a7d4ec931392200a3" + +[[package]] +name = "xxhash-rust" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "735a71d46c4d68d71d4b24d03fdc2b98e38cea81730595801db779c04fe80d70" + +[[package]] +name = "zstd-sys" +version = "2.0.7+zstd.1.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94509c3ba2fe55294d752b79842c530ccfab760192521df74a081a78d2b3c7f5" +dependencies = [ + "cc", + "libc", + "pkg-config", +] diff --git a/crates/store/Cargo.toml b/crates/store/Cargo.toml new file mode 100644 index 00000000..bd99e05b --- /dev/null +++ b/crates/store/Cargo.toml @@ -0,0 +1,40 @@ +[package] +name = "store" +version = "0.1.0" +edition = "2021" + +[dependencies] +utils = { path = "/home/vagrant/code/utils" } +rocksdb = { version = "0.20.1", optional = true } +foundationdb = { version = "0.7.0", optional = true } +rusqlite = { version = "0.29.0", features = ["bundled"], optional = true } +tokio = { version = "1.23", features = ["sync"], optional = true } +r2d2 = { version = "0.8.10", optional = true } +futures = { version = "0.3", optional = true } +rand = "0.8.5" +roaring = "0.10.1" +rayon = { version = "1.5.1", optional = true } +serde = { version = "1.0", features = ["derive"]} +ahash = { version = "0.8.0", features = ["serde"] } +bitpacking = "0.8.4" +lazy_static = "1.4" +whatlang = "0.16" # Language detection +rust-stemmers = "1.2" # Stemmers +tinysegmenter = "0.1" # Japanese tokenizer +jieba-rs = "0.6" # Chinese stemmer +xxhash-rust = { version = "0.8.5", features = ["xxh3"] } +farmhash = "1.1.5" +siphasher = "0.3" +maybe-async = "0.2" +parking_lot = { version = "0.12.1", optional = true } +lru-cache = { version = "0.1.2", optional = true } +blake3 = "1.3.3" + +[features] +default = ["sqlite"] +rocks = ["rocksdb", "rayon", "is_sync"] +sqlite = ["rusqlite", "rayon", "r2d2", "tokio", "is_sync"] +foundation = ["foundationdb", "futures", "is_async"] +is_sync = ["maybe-async/is_sync", "parking_lot", "lru-cache"] +is_async = [] + diff --git a/src/backend/foundationdb/bitmap.rs b/crates/store/src/backend/foundationdb/bitmap.rs similarity index 100% rename from src/backend/foundationdb/bitmap.rs rename to crates/store/src/backend/foundationdb/bitmap.rs diff --git a/src/backend/foundationdb/main.rs b/crates/store/src/backend/foundationdb/main.rs similarity index 100% rename from src/backend/foundationdb/main.rs rename to crates/store/src/backend/foundationdb/main.rs diff --git a/src/backend/foundationdb/mod.rs b/crates/store/src/backend/foundationdb/mod.rs similarity index 100% rename from src/backend/foundationdb/mod.rs rename to crates/store/src/backend/foundationdb/mod.rs diff --git a/src/backend/foundationdb/read.rs b/crates/store/src/backend/foundationdb/read.rs similarity index 100% rename from src/backend/foundationdb/read.rs rename to crates/store/src/backend/foundationdb/read.rs diff --git a/src/backend/foundationdb/write.rs b/crates/store/src/backend/foundationdb/write.rs similarity index 100% rename from src/backend/foundationdb/write.rs rename to crates/store/src/backend/foundationdb/write.rs diff --git a/src/backend/mod.rs b/crates/store/src/backend/mod.rs similarity index 100% rename from src/backend/mod.rs rename to crates/store/src/backend/mod.rs diff --git a/src/backend/rocksdb/bitmap.rs b/crates/store/src/backend/rocksdb/bitmap.rs similarity index 100% rename from src/backend/rocksdb/bitmap.rs rename to crates/store/src/backend/rocksdb/bitmap.rs diff --git a/src/backend/rocksdb/log.rs b/crates/store/src/backend/rocksdb/log.rs similarity index 100% rename from src/backend/rocksdb/log.rs rename to crates/store/src/backend/rocksdb/log.rs diff --git a/src/backend/rocksdb/main.rs b/crates/store/src/backend/rocksdb/main.rs similarity index 100% rename from src/backend/rocksdb/main.rs rename to crates/store/src/backend/rocksdb/main.rs diff --git a/src/backend/rocksdb/mod.rs b/crates/store/src/backend/rocksdb/mod.rs similarity index 100% rename from src/backend/rocksdb/mod.rs rename to crates/store/src/backend/rocksdb/mod.rs diff --git a/src/backend/rocksdb/read.rs b/crates/store/src/backend/rocksdb/read.rs similarity index 100% rename from src/backend/rocksdb/read.rs rename to crates/store/src/backend/rocksdb/read.rs diff --git a/src/backend/rocksdb/write.rs b/crates/store/src/backend/rocksdb/write.rs similarity index 100% rename from src/backend/rocksdb/write.rs rename to crates/store/src/backend/rocksdb/write.rs diff --git a/src/backend/sqlite/id_assign.rs b/crates/store/src/backend/sqlite/id_assign.rs similarity index 100% rename from src/backend/sqlite/id_assign.rs rename to crates/store/src/backend/sqlite/id_assign.rs diff --git a/src/backend/sqlite/main.rs b/crates/store/src/backend/sqlite/main.rs similarity index 100% rename from src/backend/sqlite/main.rs rename to crates/store/src/backend/sqlite/main.rs diff --git a/src/backend/sqlite/mod.rs b/crates/store/src/backend/sqlite/mod.rs similarity index 100% rename from src/backend/sqlite/mod.rs rename to crates/store/src/backend/sqlite/mod.rs diff --git a/src/backend/sqlite/pool.rs b/crates/store/src/backend/sqlite/pool.rs similarity index 100% rename from src/backend/sqlite/pool.rs rename to crates/store/src/backend/sqlite/pool.rs diff --git a/src/backend/sqlite/read.rs b/crates/store/src/backend/sqlite/read.rs similarity index 100% rename from src/backend/sqlite/read.rs rename to crates/store/src/backend/sqlite/read.rs diff --git a/src/backend/sqlite/write.rs b/crates/store/src/backend/sqlite/write.rs similarity index 99% rename from src/backend/sqlite/write.rs rename to crates/store/src/backend/sqlite/write.rs index f7b04d16..4fae42b0 100644 --- a/src/backend/sqlite/write.rs +++ b/crates/store/src/backend/sqlite/write.rs @@ -230,7 +230,6 @@ impl Store { .await } - #[cfg(test)] pub async fn destroy(&self) { use crate::{ SUBSPACE_ACLS, SUBSPACE_BITMAPS, SUBSPACE_BLOBS, SUBSPACE_INDEXES, SUBSPACE_LOGS, diff --git a/src/blob/mod.rs b/crates/store/src/blob/mod.rs similarity index 100% rename from src/blob/mod.rs rename to crates/store/src/blob/mod.rs diff --git a/src/blob/purge.rs b/crates/store/src/blob/purge.rs similarity index 100% rename from src/blob/purge.rs rename to crates/store/src/blob/purge.rs diff --git a/src/blob/read.rs b/crates/store/src/blob/read.rs similarity index 100% rename from src/blob/read.rs rename to crates/store/src/blob/read.rs diff --git a/src/blob/write.rs b/crates/store/src/blob/write.rs similarity index 100% rename from src/blob/write.rs rename to crates/store/src/blob/write.rs diff --git a/src/fts/bloom.rs b/crates/store/src/fts/bloom.rs similarity index 100% rename from src/fts/bloom.rs rename to crates/store/src/fts/bloom.rs diff --git a/src/fts/builder.rs b/crates/store/src/fts/builder.rs similarity index 100% rename from src/fts/builder.rs rename to crates/store/src/fts/builder.rs diff --git a/src/fts/lang.rs b/crates/store/src/fts/lang.rs similarity index 100% rename from src/fts/lang.rs rename to crates/store/src/fts/lang.rs diff --git a/src/fts/mod.rs b/crates/store/src/fts/mod.rs similarity index 100% rename from src/fts/mod.rs rename to crates/store/src/fts/mod.rs diff --git a/src/fts/ngram.rs b/crates/store/src/fts/ngram.rs similarity index 100% rename from src/fts/ngram.rs rename to crates/store/src/fts/ngram.rs diff --git a/src/fts/pdf.rs b/crates/store/src/fts/pdf.rs similarity index 100% rename from src/fts/pdf.rs rename to crates/store/src/fts/pdf.rs diff --git a/src/fts/query.rs b/crates/store/src/fts/query.rs similarity index 100% rename from src/fts/query.rs rename to crates/store/src/fts/query.rs diff --git a/src/fts/search_snippet.rs b/crates/store/src/fts/search_snippet.rs similarity index 100% rename from src/fts/search_snippet.rs rename to crates/store/src/fts/search_snippet.rs diff --git a/src/fts/stemmer.rs b/crates/store/src/fts/stemmer.rs similarity index 100% rename from src/fts/stemmer.rs rename to crates/store/src/fts/stemmer.rs diff --git a/src/fts/term_index.rs b/crates/store/src/fts/term_index.rs similarity index 100% rename from src/fts/term_index.rs rename to crates/store/src/fts/term_index.rs diff --git a/src/fts/tokenizers/chinese.rs b/crates/store/src/fts/tokenizers/chinese.rs similarity index 100% rename from src/fts/tokenizers/chinese.rs rename to crates/store/src/fts/tokenizers/chinese.rs diff --git a/src/fts/tokenizers/indo_european.rs b/crates/store/src/fts/tokenizers/indo_european.rs similarity index 100% rename from src/fts/tokenizers/indo_european.rs rename to crates/store/src/fts/tokenizers/indo_european.rs diff --git a/src/fts/tokenizers/japanese.rs b/crates/store/src/fts/tokenizers/japanese.rs similarity index 100% rename from src/fts/tokenizers/japanese.rs rename to crates/store/src/fts/tokenizers/japanese.rs diff --git a/src/fts/tokenizers/mod.rs b/crates/store/src/fts/tokenizers/mod.rs similarity index 100% rename from src/fts/tokenizers/mod.rs rename to crates/store/src/fts/tokenizers/mod.rs diff --git a/src/fts/tokenizers/word.rs b/crates/store/src/fts/tokenizers/word.rs similarity index 100% rename from src/fts/tokenizers/word.rs rename to crates/store/src/fts/tokenizers/word.rs diff --git a/src/lib.rs b/crates/store/src/lib.rs similarity index 99% rename from src/lib.rs rename to crates/store/src/lib.rs index 40d78cb0..78654623 100644 --- a/src/lib.rs +++ b/crates/store/src/lib.rs @@ -8,8 +8,7 @@ pub mod fts; pub mod query; pub mod write; -#[cfg(test)] -pub mod tests; +pub use ahash; #[cfg(feature = "rocks")] pub struct Store { diff --git a/src/query/filter.rs b/crates/store/src/query/filter.rs similarity index 100% rename from src/query/filter.rs rename to crates/store/src/query/filter.rs diff --git a/src/query/get.rs b/crates/store/src/query/get.rs similarity index 100% rename from src/query/get.rs rename to crates/store/src/query/get.rs diff --git a/src/query/log.rs b/crates/store/src/query/log.rs similarity index 100% rename from src/query/log.rs rename to crates/store/src/query/log.rs diff --git a/src/query/mod.rs b/crates/store/src/query/mod.rs similarity index 99% rename from src/query/mod.rs rename to crates/store/src/query/mod.rs index 4072f3f1..b6a7ef65 100644 --- a/src/query/mod.rs +++ b/crates/store/src/query/mod.rs @@ -165,7 +165,6 @@ impl Filter { } } - #[cfg(test)] pub fn match_english(field: impl Into, text: impl Into) -> Self { Self::match_text(field, text, Language::English) } diff --git a/src/query/sort.rs b/crates/store/src/query/sort.rs similarity index 100% rename from src/query/sort.rs rename to crates/store/src/query/sort.rs diff --git a/src/write/batch.rs b/crates/store/src/write/batch.rs similarity index 100% rename from src/write/batch.rs rename to crates/store/src/write/batch.rs diff --git a/src/write/key.rs b/crates/store/src/write/key.rs similarity index 100% rename from src/write/key.rs rename to crates/store/src/write/key.rs diff --git a/src/write/log.rs b/crates/store/src/write/log.rs similarity index 100% rename from src/write/log.rs rename to crates/store/src/write/log.rs diff --git a/src/write/mod.rs b/crates/store/src/write/mod.rs similarity index 100% rename from src/write/mod.rs rename to crates/store/src/write/mod.rs diff --git a/tests/Cargo.toml b/tests/Cargo.toml new file mode 100644 index 00000000..70f3b3bc --- /dev/null +++ b/tests/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "tests" +version = "0.1.0" +edition = "2021" + +[dependencies] +store = { path = "../crates/store" } +utils = { path = "/home/vagrant/code/utils" } + +[dev-dependencies] +tokio = { version = "1.23", features = ["full"] } +csv = "1.1" +rayon = { version = "1.5.1" } +flate2 = { version = "1.0.17", features = ["zlib"], default-features = false } + diff --git a/src/tests/resources/artwork_data.csv.gz b/tests/resources/artwork_data.csv.gz similarity index 100% rename from src/tests/resources/artwork_data.csv.gz rename to tests/resources/artwork_data.csv.gz diff --git a/tests/src/lib.rs b/tests/src/lib.rs new file mode 100644 index 00000000..5d6a9ee3 --- /dev/null +++ b/tests/src/lib.rs @@ -0,0 +1,2 @@ +#[cfg(test)] +pub mod store; diff --git a/src/tests/assign_id.rs b/tests/src/store/assign_id.rs similarity index 98% rename from src/tests/assign_id.rs rename to tests/src/store/assign_id.rs index e2d0df84..0031e540 100644 --- a/src/tests/assign_id.rs +++ b/tests/src/store/assign_id.rs @@ -1,8 +1,8 @@ use std::{collections::HashSet, sync::Arc, time::Duration}; -use ahash::AHashSet; +use store::ahash::AHashSet; -use crate::{write::BatchBuilder, Store}; +use store::{write::BatchBuilder, Store}; pub async fn test(db: Arc) { test_1(db.clone()).await; diff --git a/src/tests/blobs.rs b/tests/src/store/blobs.rs similarity index 57% rename from src/tests/blobs.rs rename to tests/src/store/blobs.rs index 262e5305..0f5c216d 100644 --- a/src/tests/blobs.rs +++ b/tests/src/store/blobs.rs @@ -1,8 +1,8 @@ use std::{sync::Arc, time::Duration}; -use ahash::AHashMap; +use store::ahash::AHashMap; -use crate::{ +use store::{ write::{BatchBuilder, F_CLEAR}, BlobId, BlobKey, Store, BLOB_HASH_LEN, }; @@ -45,11 +45,11 @@ pub async fn test(db: Arc) { // Count number of blobs let mut expected_count = AHashMap::from_iter([(blob_id_1, (0, 1)), (blob_id_2, (0, 2))]); - assert_eq!(expected_count, db.get_all_blobs().await); + assert_eq!(expected_count, get_all_blobs(&db).await); // Purgimg should not delete any blobs at this point db.purge_blobs(ttl).await.unwrap(); - assert_eq!(expected_count, db.get_all_blobs().await); + assert_eq!(expected_count, get_all_blobs(&db).await); // Link blob to an account db.write( @@ -65,14 +65,14 @@ pub async fn test(db: Arc) { // Check expected count expected_count.insert(blob_id_1, (1, 1)); - assert_eq!(expected_count, db.get_all_blobs().await); + assert_eq!(expected_count, get_all_blobs(&db).await); // Wait 1 second until the blob reaches its TTL tokio::time::sleep(Duration::from_millis(1100)).await; db.purge_blobs(ttl).await.unwrap(); expected_count.insert(blob_id_1, (1, 0)); expected_count.remove(&blob_id_2); - assert_eq!(expected_count, db.get_all_blobs().await); + assert_eq!(expected_count, get_all_blobs(&db).await); // Unlink blob, purge and make sure it is removed. db.write( @@ -87,7 +87,7 @@ pub async fn test(db: Arc) { .unwrap(); db.purge_blobs(ttl).await.unwrap(); expected_count.remove(&blob_id_1); - assert_eq!(expected_count, db.get_all_blobs().await); + assert_eq!(expected_count, get_all_blobs(&db).await); } struct BlobPurge { @@ -97,56 +97,54 @@ struct BlobPurge { id: [u8; BLOB_HASH_LEN], } -impl Store { - async fn get_all_blobs(&self) -> AHashMap { - let results = BlobPurge { - result: AHashMap::new(), - id: [0u8; BLOB_HASH_LEN], - link_count: u32::MAX, - ephemeral_count: u32::MAX, - }; +async fn get_all_blobs(store: &Store) -> AHashMap { + let results = BlobPurge { + result: AHashMap::new(), + id: [0u8; BLOB_HASH_LEN], + link_count: u32::MAX, + ephemeral_count: u32::MAX, + }; - let from_key = BlobKey { - account_id: 0, - collection: 0, - document_id: 0, - hash: [0; BLOB_HASH_LEN], - }; - let to_key = BlobKey { - account_id: u32::MAX, - collection: u8::MAX, - document_id: u32::MAX, - hash: [u8::MAX; BLOB_HASH_LEN], - }; + let from_key = BlobKey { + account_id: 0, + collection: 0, + document_id: 0, + hash: [0; BLOB_HASH_LEN], + }; + let to_key = BlobKey { + account_id: u32::MAX, + collection: u8::MAX, + document_id: u32::MAX, + hash: [u8::MAX; BLOB_HASH_LEN], + }; - let mut b = self - .iterate(results, from_key, to_key, false, true, move |b, k, v| { - if !k.starts_with(&b.id) { - if b.link_count != u32::MAX { - let id = BlobId { hash: b.id }; - b.result.insert(id, (b.link_count, b.ephemeral_count)); - } - b.link_count = 0; - b.ephemeral_count = 0; - b.id.copy_from_slice(&k[..BLOB_HASH_LEN]); + let mut b = store + .iterate(results, from_key, to_key, false, true, move |b, k, v| { + if !k.starts_with(&b.id) { + if b.link_count != u32::MAX { + let id = BlobId { hash: b.id }; + b.result.insert(id, (b.link_count, b.ephemeral_count)); } + b.link_count = 0; + b.ephemeral_count = 0; + b.id.copy_from_slice(&k[..BLOB_HASH_LEN]); + } - if v.is_empty() { - b.link_count += 1; - } else { - b.ephemeral_count += 1; - } + if v.is_empty() { + b.link_count += 1; + } else { + b.ephemeral_count += 1; + } - Ok(true) - }) - .await - .unwrap(); + Ok(true) + }) + .await + .unwrap(); - if b.link_count != u32::MAX { - let id = BlobId { hash: b.id }; - b.result.insert(id, (b.link_count, b.ephemeral_count)); - } - - b.result + if b.link_count != u32::MAX { + let id = BlobId { hash: b.id }; + b.result.insert(id, (b.link_count, b.ephemeral_count)); } + + b.result } diff --git a/src/tests/mod.rs b/tests/src/store/mod.rs similarity index 98% rename from src/tests/mod.rs rename to tests/src/store/mod.rs index d474e541..1c1481fa 100644 --- a/src/tests/mod.rs +++ b/tests/src/store/mod.rs @@ -4,10 +4,9 @@ pub mod query; use std::{io::Read, sync::Arc}; +use ::store::Store; use utils::config::Config; -use super::*; - struct TempDir { path: std::path::PathBuf, } diff --git a/src/tests/query.rs b/tests/src/store/query.rs similarity index 99% rename from src/tests/query.rs rename to tests/src/store/query.rs index 1283a697..e6840927 100644 --- a/src/tests/query.rs +++ b/tests/src/store/query.rs @@ -26,16 +26,17 @@ use std::{ time::Instant, }; -use ahash::AHashMap; +use store::ahash::AHashMap; -use crate::{ +use store::{ fts::{builder::FtsIndexBuilder, Language}, query::{Comparator, Filter}, - tests::deflate_artwork_data, write::{BatchBuilder, F_INDEX, F_TOKENIZE, F_VALUE}, Store, ValueKey, }; +use crate::store::deflate_artwork_data; + pub const FIELDS: [&str; 20] = [ "id", "accession_number",