diff --git a/Cargo.toml b/Cargo.toml index 6438e4ed..0221b0b5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,8 +16,9 @@ [workspace] members = [ - "crates/protocol", + "crates/jmap", + "crates/jmap-proto", "crates/store", - "crates/core", + "crates/utils", "tests", ] diff --git a/crates/protocol/Cargo.lock b/crates/jmap-proto/Cargo.lock similarity index 100% rename from crates/protocol/Cargo.lock rename to crates/jmap-proto/Cargo.lock diff --git a/crates/protocol/Cargo.toml b/crates/jmap-proto/Cargo.toml similarity index 86% rename from crates/protocol/Cargo.toml rename to crates/jmap-proto/Cargo.toml index 87065c46..43dc29d2 100644 --- a/crates/protocol/Cargo.toml +++ b/crates/jmap-proto/Cargo.toml @@ -1,11 +1,11 @@ [package] -name = "protocol" +name = "jmap_proto" version = "0.1.0" edition = "2021" [dependencies] store = { path = "../store" } -utils = { path = "/home/vagrant/code/utils" } +utils = { path = "../utils" } mail-parser = { git = "https://github.com/stalwartlabs/mail-parser", features = ["full_encoding", "serde_support", "ludicrous_mode"] } fast-float = "0.2.0" serde = { version = "1.0", features = ["derive"]} diff --git a/crates/protocol/src/error/method.rs b/crates/jmap-proto/src/error/method.rs similarity index 100% rename from crates/protocol/src/error/method.rs rename to crates/jmap-proto/src/error/method.rs diff --git a/crates/protocol/src/error/mod.rs b/crates/jmap-proto/src/error/mod.rs similarity index 100% rename from crates/protocol/src/error/mod.rs rename to crates/jmap-proto/src/error/mod.rs diff --git a/crates/protocol/src/error/request.rs b/crates/jmap-proto/src/error/request.rs similarity index 100% rename from crates/protocol/src/error/request.rs rename to crates/jmap-proto/src/error/request.rs diff --git a/crates/protocol/src/error/set.rs b/crates/jmap-proto/src/error/set.rs similarity index 100% rename from crates/protocol/src/error/set.rs rename to crates/jmap-proto/src/error/set.rs diff --git a/crates/protocol/src/lib.rs b/crates/jmap-proto/src/lib.rs similarity index 100% rename from crates/protocol/src/lib.rs rename to crates/jmap-proto/src/lib.rs diff --git a/crates/protocol/src/method/changes.rs b/crates/jmap-proto/src/method/changes.rs similarity index 100% rename from crates/protocol/src/method/changes.rs rename to crates/jmap-proto/src/method/changes.rs diff --git a/crates/protocol/src/method/copy.rs b/crates/jmap-proto/src/method/copy.rs similarity index 100% rename from crates/protocol/src/method/copy.rs rename to crates/jmap-proto/src/method/copy.rs diff --git a/crates/protocol/src/method/get.rs b/crates/jmap-proto/src/method/get.rs similarity index 100% rename from crates/protocol/src/method/get.rs rename to crates/jmap-proto/src/method/get.rs diff --git a/crates/protocol/src/method/import.rs b/crates/jmap-proto/src/method/import.rs similarity index 100% rename from crates/protocol/src/method/import.rs rename to crates/jmap-proto/src/method/import.rs diff --git a/crates/protocol/src/method/mod.rs b/crates/jmap-proto/src/method/mod.rs similarity index 100% rename from crates/protocol/src/method/mod.rs rename to crates/jmap-proto/src/method/mod.rs diff --git a/crates/protocol/src/method/parse.rs b/crates/jmap-proto/src/method/parse.rs similarity index 100% rename from crates/protocol/src/method/parse.rs rename to crates/jmap-proto/src/method/parse.rs diff --git a/crates/protocol/src/method/query.rs b/crates/jmap-proto/src/method/query.rs similarity index 100% rename from crates/protocol/src/method/query.rs rename to crates/jmap-proto/src/method/query.rs diff --git a/crates/protocol/src/method/query_changes.rs b/crates/jmap-proto/src/method/query_changes.rs similarity index 100% rename from crates/protocol/src/method/query_changes.rs rename to crates/jmap-proto/src/method/query_changes.rs diff --git a/crates/protocol/src/method/search_snippet.rs b/crates/jmap-proto/src/method/search_snippet.rs similarity index 100% rename from crates/protocol/src/method/search_snippet.rs rename to crates/jmap-proto/src/method/search_snippet.rs diff --git a/crates/protocol/src/method/set.rs b/crates/jmap-proto/src/method/set.rs similarity index 100% rename from crates/protocol/src/method/set.rs rename to crates/jmap-proto/src/method/set.rs diff --git a/crates/protocol/src/method/validate.rs b/crates/jmap-proto/src/method/validate.rs similarity index 100% rename from crates/protocol/src/method/validate.rs rename to crates/jmap-proto/src/method/validate.rs diff --git a/crates/protocol/src/object/email.rs b/crates/jmap-proto/src/object/email.rs similarity index 100% rename from crates/protocol/src/object/email.rs rename to crates/jmap-proto/src/object/email.rs diff --git a/crates/protocol/src/object/email_submission.rs b/crates/jmap-proto/src/object/email_submission.rs similarity index 100% rename from crates/protocol/src/object/email_submission.rs rename to crates/jmap-proto/src/object/email_submission.rs diff --git a/crates/protocol/src/object/mailbox.rs b/crates/jmap-proto/src/object/mailbox.rs similarity index 100% rename from crates/protocol/src/object/mailbox.rs rename to crates/jmap-proto/src/object/mailbox.rs diff --git a/crates/protocol/src/object/mod.rs b/crates/jmap-proto/src/object/mod.rs similarity index 100% rename from crates/protocol/src/object/mod.rs rename to crates/jmap-proto/src/object/mod.rs diff --git a/crates/protocol/src/object/sieve.rs b/crates/jmap-proto/src/object/sieve.rs similarity index 100% rename from crates/protocol/src/object/sieve.rs rename to crates/jmap-proto/src/object/sieve.rs diff --git a/crates/protocol/src/parser/base32.rs b/crates/jmap-proto/src/parser/base32.rs similarity index 100% rename from crates/protocol/src/parser/base32.rs rename to crates/jmap-proto/src/parser/base32.rs diff --git a/crates/protocol/src/parser/impls.rs b/crates/jmap-proto/src/parser/impls.rs similarity index 100% rename from crates/protocol/src/parser/impls.rs rename to crates/jmap-proto/src/parser/impls.rs diff --git a/crates/protocol/src/parser/json.rs b/crates/jmap-proto/src/parser/json.rs similarity index 100% rename from crates/protocol/src/parser/json.rs rename to crates/jmap-proto/src/parser/json.rs diff --git a/crates/protocol/src/parser/mod.rs b/crates/jmap-proto/src/parser/mod.rs similarity index 100% rename from crates/protocol/src/parser/mod.rs rename to crates/jmap-proto/src/parser/mod.rs diff --git a/crates/protocol/src/request/capability.rs b/crates/jmap-proto/src/request/capability.rs similarity index 100% rename from crates/protocol/src/request/capability.rs rename to crates/jmap-proto/src/request/capability.rs diff --git a/crates/protocol/src/request/echo.rs b/crates/jmap-proto/src/request/echo.rs similarity index 100% rename from crates/protocol/src/request/echo.rs rename to crates/jmap-proto/src/request/echo.rs diff --git a/crates/protocol/src/request/method.rs b/crates/jmap-proto/src/request/method.rs similarity index 100% rename from crates/protocol/src/request/method.rs rename to crates/jmap-proto/src/request/method.rs diff --git a/crates/protocol/src/request/mod.rs b/crates/jmap-proto/src/request/mod.rs similarity index 100% rename from crates/protocol/src/request/mod.rs rename to crates/jmap-proto/src/request/mod.rs diff --git a/crates/protocol/src/request/parser.rs b/crates/jmap-proto/src/request/parser.rs similarity index 100% rename from crates/protocol/src/request/parser.rs rename to crates/jmap-proto/src/request/parser.rs diff --git a/crates/protocol/src/request/reference.rs b/crates/jmap-proto/src/request/reference.rs similarity index 100% rename from crates/protocol/src/request/reference.rs rename to crates/jmap-proto/src/request/reference.rs diff --git a/crates/protocol/src/response/mod.rs b/crates/jmap-proto/src/response/mod.rs similarity index 100% rename from crates/protocol/src/response/mod.rs rename to crates/jmap-proto/src/response/mod.rs diff --git a/crates/protocol/src/response/references.rs b/crates/jmap-proto/src/response/references.rs similarity index 100% rename from crates/protocol/src/response/references.rs rename to crates/jmap-proto/src/response/references.rs diff --git a/crates/protocol/src/types/acl.rs b/crates/jmap-proto/src/types/acl.rs similarity index 100% rename from crates/protocol/src/types/acl.rs rename to crates/jmap-proto/src/types/acl.rs diff --git a/crates/protocol/src/types/blob.rs b/crates/jmap-proto/src/types/blob.rs similarity index 100% rename from crates/protocol/src/types/blob.rs rename to crates/jmap-proto/src/types/blob.rs diff --git a/crates/protocol/src/types/collection.rs b/crates/jmap-proto/src/types/collection.rs similarity index 100% rename from crates/protocol/src/types/collection.rs rename to crates/jmap-proto/src/types/collection.rs diff --git a/crates/protocol/src/types/date.rs b/crates/jmap-proto/src/types/date.rs similarity index 100% rename from crates/protocol/src/types/date.rs rename to crates/jmap-proto/src/types/date.rs diff --git a/crates/protocol/src/types/id.rs b/crates/jmap-proto/src/types/id.rs similarity index 100% rename from crates/protocol/src/types/id.rs rename to crates/jmap-proto/src/types/id.rs diff --git a/crates/protocol/src/types/keyword.rs b/crates/jmap-proto/src/types/keyword.rs similarity index 100% rename from crates/protocol/src/types/keyword.rs rename to crates/jmap-proto/src/types/keyword.rs diff --git a/crates/protocol/src/types/mod.rs b/crates/jmap-proto/src/types/mod.rs similarity index 100% rename from crates/protocol/src/types/mod.rs rename to crates/jmap-proto/src/types/mod.rs diff --git a/crates/protocol/src/types/pointer.rs b/crates/jmap-proto/src/types/pointer.rs similarity index 100% rename from crates/protocol/src/types/pointer.rs rename to crates/jmap-proto/src/types/pointer.rs diff --git a/crates/protocol/src/types/property.rs b/crates/jmap-proto/src/types/property.rs similarity index 100% rename from crates/protocol/src/types/property.rs rename to crates/jmap-proto/src/types/property.rs diff --git a/crates/protocol/src/types/state.rs b/crates/jmap-proto/src/types/state.rs similarity index 100% rename from crates/protocol/src/types/state.rs rename to crates/jmap-proto/src/types/state.rs diff --git a/crates/protocol/src/types/type_state.rs b/crates/jmap-proto/src/types/type_state.rs similarity index 100% rename from crates/protocol/src/types/type_state.rs rename to crates/jmap-proto/src/types/type_state.rs diff --git a/crates/protocol/src/types/value.rs b/crates/jmap-proto/src/types/value.rs similarity index 100% rename from crates/protocol/src/types/value.rs rename to crates/jmap-proto/src/types/value.rs diff --git a/crates/core/Cargo.toml b/crates/jmap/Cargo.toml similarity index 82% rename from crates/core/Cargo.toml rename to crates/jmap/Cargo.toml index fbd9fb85..d7d363c1 100644 --- a/crates/core/Cargo.toml +++ b/crates/jmap/Cargo.toml @@ -1,12 +1,12 @@ [package] -name = "core" +name = "jmap" version = "0.1.0" edition = "2021" [dependencies] store = { path = "../store" } -protocol = { path = "../protocol" } -utils = { path = "/home/vagrant/code/utils" } +jmap_proto = { path = "../jmap-proto" } +utils = { path = "../utils" } 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" } diff --git a/crates/core/src/api/mod.rs b/crates/jmap/src/api/mod.rs similarity index 100% rename from crates/core/src/api/mod.rs rename to crates/jmap/src/api/mod.rs diff --git a/crates/core/src/api/request.rs b/crates/jmap/src/api/request.rs similarity index 99% rename from crates/core/src/api/request.rs rename to crates/jmap/src/api/request.rs index dbe420b4..8e28d8fe 100644 --- a/crates/core/src/api/request.rs +++ b/crates/jmap/src/api/request.rs @@ -1,4 +1,4 @@ -use protocol::{ +use jmap_proto::{ error::request::RequestError, method::{get, query}, request::{Request, RequestMethod}, diff --git a/crates/core/src/blob/get.rs b/crates/jmap/src/blob/get.rs similarity index 97% rename from crates/core/src/blob/get.rs rename to crates/jmap/src/blob/get.rs index be05b74f..eb072839 100644 --- a/crates/core/src/blob/get.rs +++ b/crates/jmap/src/blob/get.rs @@ -1,8 +1,8 @@ +use jmap_proto::types::blob::BlobId; use mail_parser::{ decoders::{base64::base64_decode, quoted_printable::quoted_printable_decode}, Encoding, }; -use protocol::types::blob::BlobId; use crate::JMAP; diff --git a/crates/core/src/blob/mod.rs b/crates/jmap/src/blob/mod.rs similarity index 100% rename from crates/core/src/blob/mod.rs rename to crates/jmap/src/blob/mod.rs diff --git a/crates/core/src/email/body.rs b/crates/jmap/src/email/body.rs similarity index 99% rename from crates/core/src/email/body.rs rename to crates/jmap/src/email/body.rs index 05797ae7..e096d07e 100644 --- a/crates/core/src/email/body.rs +++ b/crates/jmap/src/email/body.rs @@ -1,8 +1,8 @@ -use mail_parser::{HeaderValue, MessagePart, MimeHeaders, PartType}; -use protocol::{ +use jmap_proto::{ object::Object, types::{blob::BlobId, property::Property, value::Value}, }; +use mail_parser::{HeaderValue, MessagePart, MimeHeaders, PartType}; use super::headers::HeaderToValue; diff --git a/crates/core/src/email/get.rs b/crates/jmap/src/email/get.rs similarity index 99% rename from crates/core/src/email/get.rs rename to crates/jmap/src/email/get.rs index 4e50eddf..ac192d62 100644 --- a/crates/core/src/email/get.rs +++ b/crates/jmap/src/email/get.rs @@ -1,10 +1,10 @@ -use mail_parser::Message; -use protocol::{ +use jmap_proto::{ error::method::MethodError, method::get::{GetRequest, GetResponse}, object::{email::GetArguments, Object}, types::{blob::BlobId, collection::Collection, property::Property, value::Value}, }; +use mail_parser::Message; use store::ValueKey; use crate::{email::headers::HeaderToValue, JMAP}; diff --git a/crates/core/src/email/headers.rs b/crates/jmap/src/email/headers.rs similarity index 99% rename from crates/core/src/email/headers.rs rename to crates/jmap/src/email/headers.rs index 19997680..03334d4f 100644 --- a/crates/core/src/email/headers.rs +++ b/crates/jmap/src/email/headers.rs @@ -1,11 +1,11 @@ -use mail_parser::{parsers::MessageStream, Addr, HeaderName, HeaderValue, MessagePart, RfcHeader}; -use protocol::{ +use jmap_proto::{ object::Object, types::{ property::{HeaderForm, Property}, value::Value, }, }; +use mail_parser::{parsers::MessageStream, Addr, HeaderName, HeaderValue, MessagePart, RfcHeader}; pub trait IntoForm { fn into_form(self, form: &HeaderForm) -> Value; diff --git a/crates/core/src/email/import.rs b/crates/jmap/src/email/import.rs similarity index 99% rename from crates/core/src/email/import.rs rename to crates/jmap/src/email/import.rs index 4076137e..2a6e1e34 100644 --- a/crates/core/src/email/import.rs +++ b/crates/jmap/src/email/import.rs @@ -1,4 +1,4 @@ -use protocol::{ +use jmap_proto::{ error::{ method::MethodError, set::{SetError, SetErrorType}, diff --git a/crates/core/src/email/index.rs b/crates/jmap/src/email/index.rs similarity index 99% rename from crates/core/src/email/index.rs rename to crates/jmap/src/email/index.rs index d8ccfa32..53e87687 100644 --- a/crates/core/src/email/index.rs +++ b/crates/jmap/src/email/index.rs @@ -1,11 +1,6 @@ use std::borrow::Cow; -use mail_parser::{ - decoders::html::html_to_text, - parsers::{fields::thread::thread_name, preview::preview_text}, - Addr, GetHeader, Group, HeaderName, HeaderValue, Message, MessagePart, PartType, RfcHeader, -}; -use protocol::{ +use jmap_proto::{ object::Object, types::{ blob::{BlobId, BlobSection}, @@ -15,6 +10,11 @@ use protocol::{ value::Value, }, }; +use mail_parser::{ + decoders::html::html_to_text, + parsers::{fields::thread::thread_name, preview::preview_text}, + Addr, GetHeader, Group, HeaderName, HeaderValue, Message, MessagePart, PartType, RfcHeader, +}; use store::{ fts::{builder::FtsIndexBuilder, Language}, write::{BatchBuilder, F_BITMAP, F_INDEX, F_VALUE}, diff --git a/crates/core/src/email/ingest.rs b/crates/jmap/src/email/ingest.rs similarity index 99% rename from crates/core/src/email/ingest.rs rename to crates/jmap/src/email/ingest.rs index c2f20ba7..0dec48a8 100644 --- a/crates/core/src/email/ingest.rs +++ b/crates/jmap/src/email/ingest.rs @@ -1,13 +1,13 @@ -use mail_parser::{ - parsers::fields::thread::thread_name, HeaderName, HeaderValue, Message, RfcHeader, -}; -use protocol::{ +use jmap_proto::{ object::Object, types::{ blob::BlobId, collection::Collection, id::Id, keyword::Keyword, property::Property, value::Value, }, }; +use mail_parser::{ + parsers::fields::thread::thread_name, HeaderName, HeaderValue, Message, RfcHeader, +}; use store::{ query::Filter, write::{log::ChangeLogBuilder, now, BatchBuilder, F_BITMAP, F_CLEAR, F_VALUE}, diff --git a/crates/core/src/email/mod.rs b/crates/jmap/src/email/mod.rs similarity index 100% rename from crates/core/src/email/mod.rs rename to crates/jmap/src/email/mod.rs diff --git a/crates/core/src/email/query.rs b/crates/jmap/src/email/query.rs similarity index 99% rename from crates/core/src/email/query.rs rename to crates/jmap/src/email/query.rs index 9524006a..4c0a879b 100644 --- a/crates/core/src/email/query.rs +++ b/crates/jmap/src/email/query.rs @@ -1,4 +1,4 @@ -use protocol::{ +use jmap_proto::{ error::method::MethodError, method::query::{Comparator, Filter, QueryRequest, QueryResponse, SortProperty}, object::email::QueryArguments, diff --git a/crates/core/src/lib.rs b/crates/jmap/src/lib.rs similarity index 96% rename from crates/core/src/lib.rs rename to crates/jmap/src/lib.rs index 47ee198a..b01e3b62 100644 --- a/crates/core/src/lib.rs +++ b/crates/jmap/src/lib.rs @@ -1,4 +1,4 @@ -use protocol::error::method::MethodError; +use jmap_proto::error::method::MethodError; use store::{fts::Language, Store}; pub mod api; diff --git a/crates/store/Cargo.toml b/crates/store/Cargo.toml index 4fc015b0..77c889b3 100644 --- a/crates/store/Cargo.toml +++ b/crates/store/Cargo.toml @@ -4,7 +4,7 @@ version = "0.1.0" edition = "2021" [dependencies] -utils = { path = "/home/vagrant/code/utils" } +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 } diff --git a/crates/utils/Cargo.toml b/crates/utils/Cargo.toml new file mode 100644 index 00000000..0518bddd --- /dev/null +++ b/crates/utils/Cargo.toml @@ -0,0 +1,7 @@ +[package] +name = "utils" +version = "0.1.0" +edition = "2021" + +[dependencies] +serde = { version = "1.0", features = ["derive"]} diff --git a/crates/utils/src/codec/base32_custom.rs b/crates/utils/src/codec/base32_custom.rs new file mode 100644 index 00000000..cbf4fa90 --- /dev/null +++ b/crates/utils/src/codec/base32_custom.rs @@ -0,0 +1,215 @@ +/* + * 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::slice::Iter; + +use super::leb128::{Leb128Iterator, Leb128Writer}; + +pub static BASE32_ALPHABET: &[u8] = b"abcdefghijklmnopqrstuvwxyz792013"; +pub static BASE32_INVERSE: [u8; 256] = [ + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 29, 30, 28, 31, 255, 255, 255, 26, 255, 27, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 10, 11, + 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 255, 255, 255, 255, 255, 255, 0, 1, 2, + 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, +]; + +pub struct Base32Writer { + last_byte: u8, + pos: usize, + result: String, +} + +impl Base32Writer { + pub fn with_capacity(capacity: usize) -> Self { + Base32Writer { + result: String::with_capacity((capacity + 3) / 4 * 5), + last_byte: 0, + pos: 0, + } + } + + pub fn push_char(&mut self, ch: char) { + self.result.push(ch); + } + + fn push_byte(&mut self, byte: u8, is_remainder: bool) { + let (ch1, ch2) = match self.pos % 5 { + 0 => ((byte & 0xF8) >> 3, u8::MAX), + 1 => ( + (((self.last_byte & 0x07) << 2) | ((byte & 0xC0) >> 6)), + ((byte & 0x3E) >> 1), + ), + 2 => ( + (((self.last_byte & 0x01) << 4) | ((byte & 0xF0) >> 4)), + u8::MAX, + ), + 3 => ( + (((self.last_byte & 0x0F) << 1) | (byte >> 7)), + ((byte & 0x7C) >> 2), + ), + 4 => ( + (((self.last_byte & 0x03) << 3) | ((byte & 0xE0) >> 5)), + (byte & 0x1F), + ), + _ => unreachable!(), + }; + + self.result.push(char::from(BASE32_ALPHABET[ch1 as usize])); + if !is_remainder { + if ch2 != u8::MAX { + self.result.push(char::from(BASE32_ALPHABET[ch2 as usize])); + } + self.last_byte = byte; + self.pos += 1; + } + } + + pub fn finalize(mut self) -> String { + if self.pos % 5 != 0 { + self.push_byte(0, true); + } + + self.result + } +} + +impl std::io::Write for Base32Writer { + fn write(&mut self, bytes: &[u8]) -> std::io::Result { + let start_pos = self.pos; + + for &byte in bytes { + self.push_byte(byte, false); + } + + Ok(self.pos - start_pos) + } + + fn flush(&mut self) -> std::io::Result<()> { + Ok(()) + } +} + +#[derive(Debug)] +pub struct Base32Reader<'x> { + bytes: Iter<'x, u8>, + last_byte: u8, + pos: usize, +} + +impl<'x> Base32Reader<'x> { + pub fn new(bytes: &'x [u8]) -> Self { + Base32Reader { + bytes: bytes.iter(), + pos: 0, + last_byte: 0, + } + } + + #[inline(always)] + fn map_byte(&mut self) -> Option { + match self.bytes.next() { + Some(&byte) => match BASE32_INVERSE[byte as usize] { + byte if byte != u8::MAX => { + self.last_byte = byte; + Some(byte) + } + _ => None, + }, + _ => None, + } + } +} + +impl Iterator for Base32Reader<'_> { + type Item = u8; + fn next(&mut self) -> Option { + let pos = self.pos % 5; + let last_byte = self.last_byte; + let byte = self.map_byte()?; + self.pos += 1; + + match pos { + 0 => ((byte << 3) | (self.map_byte().unwrap_or(0) >> 2)).into(), + 1 => ((last_byte << 6) | (byte << 1) | (self.map_byte().unwrap_or(0) >> 4)).into(), + 2 => ((last_byte << 4) | (byte >> 1)).into(), + 3 => ((last_byte << 7) | (byte << 2) | (self.map_byte().unwrap_or(0) >> 3)).into(), + 4 => ((last_byte << 5) | byte).into(), + _ => None, + } + } +} + +impl Leb128Iterator for Base32Reader<'_> {} +impl Leb128Writer for Base32Writer {} + +#[cfg(test)] +mod tests { + use std::io::Write; + + use crate::codec::base32_custom::{Base32Reader, Base32Writer}; + + #[test] + fn base32_roundtrip() { + let mut bytes = Vec::with_capacity(100); + for byte in 0..100 { + bytes.push((100 - byte) as u8); + let mut writer = Base32Writer::with_capacity(10); + writer.write_all(&bytes).unwrap(); + let result = writer.finalize(); + + let mut bytes_result = Vec::new(); + for byte in Base32Reader::new(result.as_bytes()) { + bytes_result.push(byte); + } + + assert_eq!(bytes, bytes_result); + } + + for bytes in [ + vec![0], + vec![32, 43, 55, 99, 43, 55], + vec![84, 4, 43, 77, 62, 55, 92], + vec![84, 4, 43, 77, 62, 55, 92], + ] { + let mut writer = Base32Writer::with_capacity(10); + writer.write_all(&bytes).unwrap(); + let result = writer.finalize(); + + let mut bytes_result = Vec::new(); + for byte in Base32Reader::new(result.as_bytes()) { + bytes_result.push(byte); + } + + assert_eq!(bytes, bytes_result); + } + } +} diff --git a/crates/utils/src/codec/leb128.rs b/crates/utils/src/codec/leb128.rs new file mode 100644 index 00000000..9d5b2604 --- /dev/null +++ b/crates/utils/src/codec/leb128.rs @@ -0,0 +1,180 @@ +/* + * 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. +*/ + +#![allow(dead_code)] + +use std::{borrow::Borrow, io::Write}; + +pub trait Leb128_ { + fn to_leb128_writer(self, out: &mut impl Write) -> std::io::Result; + fn to_leb128_bytes(self, out: &mut Vec); + fn from_leb128_bytes(slice: &[u8]) -> Option<(Self, usize)> + where + Self: std::marker::Sized; + fn from_leb128_it(it: T) -> Option + where + Self: std::marker::Sized, + T: Iterator, + I: Borrow; +} + +pub trait Leb128Vec { + fn push_leb128(&mut self, value: T); +} + +pub trait Leb128Writer: Write + Sized { + #[inline(always)] + fn write_leb128(&mut self, value: T) -> std::io::Result { + T::to_leb128_writer(value, self) + } +} + +impl Leb128Vec for Vec { + #[inline(always)] + fn push_leb128(&mut self, value: T) { + T::to_leb128_bytes(value, self); + } +} + +pub trait Leb128Iterator: Iterator +where + I: Borrow, +{ + #[inline(always)] + fn next_leb128(&mut self) -> Option { + T::from_leb128_it(self) + } + + #[inline(always)] + fn skip_leb128(&mut self) -> Option<()> { + for byte in self { + if (byte.borrow() & 0x80) == 0 { + return Some(()); + } + } + None + } +} + +pub trait Leb128Reader: AsRef<[u8]> { + #[inline(always)] + fn read_leb128(&self) -> Option<(T, usize)> { + T::from_leb128_bytes(self.as_ref()) + } + + #[inline(always)] + fn skip_leb128(&self) -> Option { + for (pos, byte) in self.as_ref().iter().enumerate() { + if (byte & 0x80) == 0 { + return (pos + 1).into(); + } + } + None + } +} + +impl Leb128Reader for &[u8] {} +impl Leb128Reader for Vec {} +impl Leb128Reader for Box<[u8]> {} +impl<'x> Leb128Iterator<&'x u8> for std::slice::Iter<'x, u8> {} + +// Based on leb128.rs from rustc +macro_rules! impl_unsigned_leb128 { + ($int_ty:ident, $shifts:expr) => { + impl Leb128_ for $int_ty { + #[inline(always)] + fn to_leb128_writer(self, out: &mut impl Write) -> std::io::Result { + let mut value = self; + let mut bytes_written = 0; + loop { + if value < 0x80 { + bytes_written += out.write(&[value as u8])?; + break; + } else { + bytes_written += out.write(&[((value & 0x7f) | 0x80) as u8])?; + value >>= 7; + } + } + Ok(bytes_written) + } + + #[inline(always)] + fn to_leb128_bytes(self, out: &mut Vec) { + let mut value = self; + loop { + if value < 0x80 { + out.push(value as u8); + break; + } else { + out.push(((value & 0x7f) | 0x80) as u8); + value >>= 7; + } + } + } + + #[inline(always)] + fn from_leb128_bytes(slice: &[u8]) -> Option<($int_ty, usize)> { + let mut result = 0; + + for (shift, (pos, &byte)) in $shifts.into_iter().zip(slice.iter().enumerate()) { + if (byte & 0x80) == 0 { + result |= (byte as $int_ty) << shift; + return Some((result, pos + 1)); + } else { + result |= ((byte & 0x7F) as $int_ty) << shift; + } + } + + None + } + + #[inline(always)] + fn from_leb128_it(it: T) -> Option<$int_ty> + where + T: Iterator, + I: Borrow, + { + let mut result = 0; + + for (shift, byte_) in $shifts.into_iter().zip(it) { + let byte = byte_.borrow(); + + if (byte & 0x80) == 0 { + result |= (*byte as $int_ty) << shift; + return Some(result); + } else { + result |= ((byte & 0x7F) as $int_ty) << shift; + } + } + + None + } + } + }; +} + +impl_unsigned_leb128!(u8, [0]); +impl_unsigned_leb128!(u16, [0, 7, 14]); +impl_unsigned_leb128!(u32, [0, 7, 14, 21, 28]); +impl_unsigned_leb128!(u64, [0, 7, 14, 21, 28, 35, 42, 49, 56, 63]); +impl_unsigned_leb128!(usize, [0, 7, 14, 21, 28, 35, 42, 49, 56, 63]); diff --git a/crates/utils/src/codec/mod.rs b/crates/utils/src/codec/mod.rs new file mode 100644 index 00000000..095dfa5d --- /dev/null +++ b/crates/utils/src/codec/mod.rs @@ -0,0 +1,25 @@ +/* + * Copyright (c) 2023 Stalwart Labs Ltd. + * + * This file is part of the Stalwart SMTP 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 base32_custom; +pub mod leb128; diff --git a/crates/utils/src/config/mod.rs b/crates/utils/src/config/mod.rs new file mode 100644 index 00000000..b4af37ba --- /dev/null +++ b/crates/utils/src/config/mod.rs @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2023 Stalwart Labs Ltd. + * + * This file is part of the Stalwart SMTP 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 parser; +pub mod utils; + +use std::collections::BTreeMap; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Config { + keys: BTreeMap, +} + +pub type Result = std::result::Result; diff --git a/crates/utils/src/config/parser.rs b/crates/utils/src/config/parser.rs new file mode 100644 index 00000000..cfc53bcb --- /dev/null +++ b/crates/utils/src/config/parser.rs @@ -0,0 +1,545 @@ +/* + * Copyright (c) 2023 Stalwart Labs Ltd. + * + * This file is part of the Stalwart SMTP 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::{ + collections::{btree_map::Entry, BTreeMap}, + iter::Peekable, + str::Chars, +}; + +use super::{Config, Result}; +use std::fmt::Write; + +const MAX_NEST_LEVEL: usize = 10; + +// Simple TOML parser for Stalwart Mail Server configuration files. + +impl Config { + pub fn parse(toml: &str) -> Result { + let mut parser = TomlParser::new(toml); + let mut table_name = String::new(); + let mut last_array_name = String::new(); + let mut last_array_pos = 0; + + while parser.seek_next_char() { + match parser.peek_char()? { + '[' => { + parser.next_char(true, false)?; + table_name.clear(); + let mut is_array = match parser.next_char(true, false)? { + '[' => true, + ch => { + table_name.push(ch); + false + } + }; + let mut in_quote = false; + let mut last_ch = char::from(0); + loop { + let ch = parser.next_char(!in_quote, false)?; + match ch { + '\"' if !in_quote || last_ch != '\\' => { + in_quote = !in_quote; + } + '\\' if in_quote => (), + ']' if !in_quote => { + if table_name.is_empty() { + return Err(format!( + "Empty table name at line {}.", + parser.line + )); + } + if is_array { + if table_name == last_array_name { + last_array_pos += 1; + } else { + last_array_pos = 0; + last_array_name = table_name.to_string(); + } + is_array = false; + write!(table_name, ".{last_array_pos}").ok(); + } else { + break; + } + } + _ => { + if !in_quote { + if ch.is_alphanumeric() || ['.', '-', '_'].contains(&ch) { + table_name.push(ch.to_ascii_lowercase()); + } else { + return Err(format!( + "Unexpected character {:?} at line {}.", + ch, parser.line + )); + } + } else { + table_name.push(ch); + } + } + } + last_ch = ch; + } + parser.skip_line(); + } + 'a'..='z' | 'A'..='Z' | '0'..='9' | '\"' => { + let key = parser.key(if !table_name.is_empty() { + format!("{table_name}.") + } else { + String::with_capacity(10) + })?; + parser.value(key, &['\n'], 0)?; + } + '#' => { + parser.skip_line(); + } + ch => { + let ch = *ch; + return Err(format!( + "Unexpected character {:?} at line {}.", + ch, parser.line + )); + } + } + } + + Ok(Self { keys: parser.keys }) + } +} + +struct TomlParser<'x> { + keys: BTreeMap, + iter: Peekable>, + line: usize, +} + +impl<'x> TomlParser<'x> { + fn new(toml: &'x str) -> Self { + Self { + keys: BTreeMap::new(), + iter: toml.chars().peekable(), + line: 1, + } + } + + fn seek_next_char(&mut self) -> bool { + while let Some(ch) = self.iter.peek() { + match ch { + '\n' => { + self.iter.next(); + self.line += 1; + } + '\r' | ' ' | '\t' => { + self.iter.next(); + } + '#' => { + self.skip_line(); + } + _ => { + return true; + } + } + } + + false + } + + fn peek_char(&mut self) -> Result<&char> { + self.iter.peek().ok_or_else(|| "".to_string()) + } + + fn next_char(&mut self, skip_wsp: bool, allow_lf: bool) -> Result { + for ch in &mut self.iter { + match ch { + '\r' => (), + ' ' | '\t' if skip_wsp => (), + '\n' => { + return if allow_lf { + self.line += 1; + Ok(ch) + } else { + Err(format!("Unexpected end of line at line: {}", self.line)) + }; + } + _ => { + return Ok(ch); + } + } + } + Err(format!("Unexpected EOF at line: {}", self.line)) + } + + fn skip_line(&mut self) { + for ch in &mut self.iter { + if ch == '\n' { + self.line += 1; + break; + } + } + } + + #[allow(clippy::while_let_on_iterator)] + fn key(&mut self, mut key: String) -> Result { + while let Some(ch) = self.iter.next() { + match ch { + '=' => { + if !key.is_empty() { + return Ok(key); + } else { + return Err(format!("Empty key at line: {}", self.line)); + } + } + 'a'..='z' | '.' | 'A'..='Z' | '0'..='9' | '_' | '-' => { + key.push(ch); + } + '\"' => { + let mut last_ch = char::from(0); + while let Some(ch) = self.iter.next() { + match ch { + '\\' => (), + '\"' if last_ch != '\\' => { + break; + } + '\n' => { + return Err(format!( + "Unexpected end of line at line: {}", + self.line + )); + } + _ => { + key.push(ch); + } + } + last_ch = ch; + } + } + ' ' | '\t' | '\r' => (), + '\n' => { + return Err(format!("Unexpected end of line at line: {}", self.line)); + } + _ => { + return Err(format!( + "Unexpected character {:?} found in key at line {}.", + ch, self.line + )); + } + } + } + Err(format!("Unexpected EOF at line: {}", self.line)) + } + + fn value(&mut self, key: String, stop_chars: &[char], nest_level: usize) -> Result { + if nest_level == MAX_NEST_LEVEL { + return Err(format!("Too many nested structures at line {}.", self.line)); + } + match self.next_char(true, false)? { + '[' => { + let mut array_pos = 0; + self.seek_next_char(); + loop { + match self.value(format!("{key}.{array_pos}"), &[',', ']'], nest_level + 1)? { + ',' => { + self.seek_next_char(); + array_pos += 1; + } + ']' => break, + ch => { + return Err(format!( + "Unexpected character {:?} found in array for property {:?} at line {}.", + ch, key, self.line + )); + } + } + } + } + '{' => loop { + let sub_key = self.key(format!("{key}."))?; + self.seek_next_char(); + + match self.value(sub_key, &[',', '}'], nest_level + 1)? { + ',' => { + self.seek_next_char(); + } + '}' => break, + ch => { + return Err(format!( + "Unexpected character {:?} found in inline table for property {:?} at line {}.", + ch, key, self.line + )); + } + } + }, + qch @ ('\'' | '\"') => { + let mut value = String::new(); + if matches!(self.iter.peek(), Some(ch) if ch == &qch) { + self.iter.next(); + if matches!(self.iter.peek(), Some(ch) if ch == &qch) { + self.iter.next(); + if matches!(self.iter.peek(), Some(ch) if ch == &'\n') { + self.iter.next(); + self.line += 1; + } + + let mut last_ch = char::from(0); + let mut prev_last_ch = char::from(0); + loop { + let ch = self.next_char(false, true)?; + if !(ch == qch && last_ch == qch && prev_last_ch == qch) { + value.push(ch); + prev_last_ch = last_ch; + last_ch = ch; + } else { + value.truncate(value.len() - 2); + break; + } + } + } + } else { + let mut last_ch = char::from(0); + + loop { + let ch = self.next_char(false, true)?; + match ch { + '\\' if last_ch != '\\' => (), + 't' if last_ch == '\\' => { + value.push('\t'); + } + 'r' if last_ch == '\\' => { + value.push('\r'); + } + 'n' if last_ch == '\\' => { + value.push('\n'); + } + ch => { + if ch != qch || last_ch == '\\' { + value.push(ch); + } else { + break; + } + } + } + last_ch = ch; + } + } + match self.keys.entry(key) { + Entry::Vacant(e) => { + value.shrink_to_fit(); + e.insert(value); + } + Entry::Occupied(e) => { + return Err(format!( + "Duplicate key {:?} at line {}.", + e.key(), + self.line + )); + } + } + } + ch if ch.is_alphanumeric() || ['.', '+', '-'].contains(&ch) => { + let mut value = String::with_capacity(4); + value.push(ch); + while let Some(ch) = self.iter.peek() { + if ch.is_alphanumeric() || ['.', '+', '-'].contains(ch) { + value.push(self.next_char(true, false)?); + } else { + break; + } + } + match self.keys.entry(key) { + Entry::Vacant(e) => { + value.shrink_to_fit(); + e.insert(value); + } + Entry::Occupied(e) => { + return Err(format!( + "Duplicate key {:?} at line {}.", + e.key(), + self.line + )); + } + } + } + ch => { + return if stop_chars.contains(&ch) { + Ok(ch) + } else { + Err(format!( + "Expected {:?} but found {:?} in value at line {}.", + stop_chars, ch, self.line + )) + } + } + } + + loop { + match self.next_char(true, true)? { + '#' => { + self.skip_line(); + if stop_chars.contains(&'\n') { + return Ok('\n'); + } + } + ch if stop_chars.contains(&ch) => { + return Ok(ch); + } + '\n' if !stop_chars.contains(&'\n') => (), + ch => { + return Err(format!( + "Expected {:?} but found {:?} in value at line {}.", + stop_chars, ch, self.line + )); + } + } + } + } +} + +#[cfg(test)] +mod tests { + use std::{collections::BTreeMap, fs, path::PathBuf}; + + use crate::config::Config; + + #[test] + fn toml_parse() { + let mut file = PathBuf::from(env!("CARGO_MANIFEST_DIR")); + file.push("resources"); + file.push("tests"); + file.push("config"); + file.push("toml-parser.toml"); + + let config = Config::parse(&fs::read_to_string(file).unwrap()).unwrap(); + assert_eq!( + config.keys, + BTreeMap::from_iter([ + ("arrays.colors.0".to_string(), "red".to_string()), + ("arrays.colors.1".to_string(), "yellow".to_string()), + ("arrays.colors.2".to_string(), "green".to_string()), + ( + "arrays.contributors.0".to_string(), + "Foo Bar ".to_string() + ), + ( + "arrays.contributors.1.email".to_string(), + "bazqux@example.com".to_string() + ), + ( + "arrays.contributors.1.name".to_string(), + "Baz Qux".to_string() + ), + ( + "arrays.contributors.1.url".to_string(), + "https://example.com/bazqux".to_string() + ), + ("arrays.integers.0".to_string(), "1".to_string()), + ("arrays.integers.1".to_string(), "2".to_string()), + ("arrays.integers.2".to_string(), "3".to_string()), + ("arrays.integers2.0".to_string(), "1".to_string()), + ("arrays.integers2.1".to_string(), "2".to_string()), + ("arrays.integers2.2".to_string(), "3".to_string()), + ("arrays.integers3.0".to_string(), "4".to_string()), + ("arrays.integers3.1".to_string(), "5".to_string()), + ( + "arrays.nested_arrays_of_ints.0.0".to_string(), + "1".to_string() + ), + ( + "arrays.nested_arrays_of_ints.0.1".to_string(), + "2".to_string() + ), + ( + "arrays.nested_arrays_of_ints.1.0".to_string(), + "3".to_string() + ), + ( + "arrays.nested_arrays_of_ints.1.1".to_string(), + "4".to_string() + ), + ( + "arrays.nested_arrays_of_ints.1.2".to_string(), + "5".to_string() + ), + ("arrays.nested_mixed_array.0.0".to_string(), "1".to_string()), + ("arrays.nested_mixed_array.0.1".to_string(), "2".to_string()), + ("arrays.nested_mixed_array.1.0".to_string(), "a".to_string()), + ("arrays.nested_mixed_array.1.1".to_string(), "b".to_string()), + ("arrays.nested_mixed_array.1.2".to_string(), "c".to_string()), + ("arrays.numbers.0".to_string(), "0.1".to_string()), + ("arrays.numbers.1".to_string(), "0.2".to_string()), + ("arrays.numbers.2".to_string(), "0.5".to_string()), + ("arrays.numbers.3".to_string(), "1".to_string()), + ("arrays.numbers.4".to_string(), "2".to_string()), + ("arrays.numbers.5".to_string(), "5".to_string()), + ("arrays.string_array.0".to_string(), "all".to_string()), + ("arrays.string_array.1".to_string(), "strings".to_string()), + ( + "arrays.string_array.2".to_string(), + "are the same".to_string() + ), + ("arrays.string_array.3".to_string(), "type".to_string()), + ("database.data.0.0".to_string(), "delta".to_string()), + ("database.data.0.1".to_string(), "phi".to_string()), + ("database.data.1.0".to_string(), "3.14".to_string()), + ("database.enabled".to_string(), "true".to_string()), + ("database.ports.0".to_string(), "8000".to_string()), + ("database.ports.1".to_string(), "8001".to_string()), + ("database.ports.2".to_string(), "8002".to_string()), + ("database.temp_targets.case".to_string(), "72.0".to_string()), + ("database.temp_targets.cpu".to_string(), "79.5".to_string()), + ("products.0.name".to_string(), "Hammer".to_string()), + ("products.0.sku".to_string(), "738594937".to_string()), + ("products.2.color".to_string(), "gray".to_string()), + ("products.2.name".to_string(), "Nail".to_string()), + ("products.2.sku".to_string(), "284758393".to_string()), + ("servers.127.0.0.1".to_string(), "value".to_string()), + ("servers.alpha.ip".to_string(), "10.0.0.1".to_string()), + ("servers.alpha.role".to_string(), "frontend".to_string()), + ("servers.beta.ip".to_string(), "10.0.0.2".to_string()), + ("servers.beta.role".to_string(), "backend".to_string()), + ( + "servers.character encoding".to_string(), + "value".to_string() + ), + ( + "strings.my \"string\" test.lines".to_string(), + concat!( + "The first newline is\ntrimmed in raw strings.\n", + "All other whitespace\nis preserved.\n" + ) + .to_string() + ), + ( + "strings.my \"string\" test.str1".to_string(), + "I'm a string.".to_string() + ), + ( + "strings.my \"string\" test.str2".to_string(), + "You can \"quote\" me.".to_string() + ), + ( + "strings.my \"string\" test.str3".to_string(), + "Name\tTabs\nNew Line.".to_string() + ), + ]) + ); + } +} diff --git a/crates/utils/src/config/utils.rs b/crates/utils/src/config/utils.rs new file mode 100644 index 00000000..4f662804 --- /dev/null +++ b/crates/utils/src/config/utils.rs @@ -0,0 +1,499 @@ +/* + * Copyright (c) 2023 Stalwart Labs Ltd. + * + * This file is part of the Stalwart SMTP 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::{net::IpAddr, time::Duration}; + +use super::Config; + +impl Config { + pub fn property(&self, key: impl AsKey) -> super::Result> { + let key = key.as_key(); + if let Some(value) = self.keys.get(&key) { + T::parse_value(key, value).map(Some) + } else { + Ok(None) + } + } + + pub fn property_or_default( + &self, + key: impl AsKey, + default: impl AsKey, + ) -> super::Result> { + match self.property(key) { + Ok(None) => self.property(default), + result => result, + } + } + + pub fn property_require(&self, key: impl AsKey) -> super::Result { + match self.property(key.clone()) { + Ok(Some(result)) => Ok(result), + Ok(None) => Err(format!("Missing property {:?}.", key.as_key())), + Err(err) => Err(err), + } + } + + pub fn sub_keys<'x, 'y: 'x>(&'y self, prefix: impl AsKey) -> impl Iterator + 'x { + let mut last_key = ""; + let prefix = prefix.as_prefix(); + + self.keys.keys().filter_map(move |key| { + let key = key.strip_prefix(&prefix)?; + let key = if let Some((key, _)) = key.split_once('.') { + key + } else { + key + }; + if last_key != key { + last_key = key; + Some(key) + } else { + None + } + }) + } + + pub fn properties( + &self, + prefix: impl AsKey, + ) -> impl Iterator> { + let full_prefix = prefix.as_key(); + let prefix = prefix.as_prefix(); + + self.keys.iter().filter_map(move |(key, value)| { + if key.starts_with(&prefix) || key == &full_prefix { + T::parse_value(key.as_str(), value) + .map(|value| (key.as_str(), value)) + .into() + } else { + None + } + }) + } + + pub fn value(&self, key: impl AsKey) -> Option<&str> { + self.keys.get(&key.as_key()).map(|s| s.as_str()) + } + + pub fn value_require(&self, key: impl AsKey) -> super::Result<&str> { + self.keys + .get(&key.as_key()) + .map(|s| s.as_str()) + .ok_or_else(|| format!("Missing property {:?}.", key.as_key())) + } + + pub fn value_or_default(&self, key: impl AsKey, default: impl AsKey) -> Option<&str> { + self.keys + .get(&key.as_key()) + .or_else(|| self.keys.get(&default.as_key())) + .map(|s| s.as_str()) + } + + pub fn values(&self, prefix: impl AsKey) -> impl Iterator { + let full_prefix = prefix.as_key(); + let prefix = prefix.as_prefix(); + + self.keys.iter().filter_map(move |(key, value)| { + if key.starts_with(&prefix) || key == &full_prefix { + (key.as_str(), value.as_str()).into() + } else { + None + } + }) + } + + pub fn values_or_default( + &self, + prefix: impl AsKey, + default: impl AsKey, + ) -> impl Iterator { + let mut prefix = prefix.as_prefix(); + + self.values(if self.keys.keys().any(|k| k.starts_with(&prefix)) { + prefix.truncate(prefix.len() - 1); + prefix + } else { + default.as_key() + }) + } + + pub fn take_value(&mut self, key: &str) -> Option { + self.keys.remove(key) + } + + pub fn file_contents(&self, key: impl AsKey) -> super::Result> { + let key = key.as_key(); + if let Some(value) = self.keys.get(&key) { + if let Some(value) = value.strip_prefix("file://") { + std::fs::read(value).map_err(|err| { + format!("Failed to read file {value:?} for property {key:?}: {err}") + }) + } else { + Ok(value.to_string().into_bytes()) + } + } else { + Err(format!("Property {key:?} not found in configuration file.")) + } + } +} + +pub trait ParseValues: Sized + Default { + fn parse_values(key: impl AsKey, values: &Config) -> super::Result; + fn is_multivalue() -> bool; +} + +pub trait ParseValue: Sized { + fn parse_value(key: impl AsKey, value: &str) -> super::Result; +} + +pub trait ParseKey { + fn parse_key(&self, key: impl AsKey) -> super::Result; +} + +impl ParseKey for &str { + fn parse_key(&self, key: impl AsKey) -> super::Result { + T::parse_value(key, self) + } +} + +impl ParseKey for String { + fn parse_key(&self, key: impl AsKey) -> super::Result { + T::parse_value(key, self.as_str()) + } +} + +impl ParseKey for &String { + fn parse_key(&self, key: impl AsKey) -> super::Result { + T::parse_value(key, self.as_str()) + } +} + +impl ParseValues for Vec { + fn is_multivalue() -> bool { + true + } + + fn parse_values(key: impl AsKey, values: &Config) -> super::Result { + let mut result = Vec::new(); + for (key, value) in values.values(key) { + result.push(T::parse_value(key, value)?); + } + Ok(result) + } +} + +impl ParseValues for T { + fn is_multivalue() -> bool { + false + } + + fn parse_values(key: impl AsKey, values: &Config) -> super::Result { + let mut iter = values.values(key); + if let Some((key, value)) = iter.next() { + let result = T::parse_value(key, value)?; + if iter.next().is_none() { + Ok(result) + } else { + Err(format!( + "Property {:?} cannot have multiple values.", + key.as_key() + )) + } + } else { + Ok(T::default()) + } + } +} + +impl ParseValue for Option { + fn parse_value(key: impl AsKey, value: &str) -> super::Result { + if !value.is_empty() + && !value.eq_ignore_ascii_case("false") + && !value.eq_ignore_ascii_case("disable") + && !value.eq_ignore_ascii_case("disabled") + && !value.eq_ignore_ascii_case("never") + && !value.eq("0") + { + T::parse_value(key, value).map(Some) + } else { + Ok(None) + } + } +} + +impl ParseValue for String { + fn parse_value(_key: impl AsKey, value: &str) -> super::Result { + Ok(value.to_string()) + } +} + +impl ParseValue for u64 { + fn parse_value(key: impl AsKey, value: &str) -> super::Result { + value.parse().map_err(|_| { + format!( + "Invalid integer value {:?} for property {:?}.", + value, + key.as_key() + ) + }) + } +} + +impl ParseValue for u16 { + fn parse_value(key: impl AsKey, value: &str) -> super::Result { + value.parse().map_err(|_| { + format!( + "Invalid integer value {:?} for property {:?}.", + value, + key.as_key() + ) + }) + } +} + +impl ParseValue for i16 { + fn parse_value(key: impl AsKey, value: &str) -> super::Result { + value.parse().map_err(|_| { + format!( + "Invalid integer value {:?} for property {:?}.", + value, + key.as_key() + ) + }) + } +} + +impl ParseValue for u32 { + fn parse_value(key: impl AsKey, value: &str) -> super::Result { + value.parse().map_err(|_| { + format!( + "Invalid integer value {:?} for property {:?}.", + value, + key.as_key() + ) + }) + } +} + +impl ParseValue for IpAddr { + fn parse_value(key: impl AsKey, value: &str) -> super::Result { + value.parse().map_err(|_| { + format!( + "Invalid IP address value {:?} for property {:?}.", + value, + key.as_key() + ) + }) + } +} + +impl ParseValue for usize { + fn parse_value(key: impl AsKey, value: &str) -> super::Result { + value.parse().map_err(|_| { + format!( + "Invalid integer value {:?} for property {:?}.", + value, + key.as_key() + ) + }) + } +} + +impl ParseValue for bool { + fn parse_value(key: impl AsKey, value: &str) -> super::Result { + value.parse().map_err(|_| { + format!( + "Invalid boolean value {:?} for property {:?}.", + value, + key.as_key() + ) + }) + } +} + +impl ParseValue for Duration { + fn parse_value(key: impl AsKey, value: &str) -> super::Result { + let duration = value.trim_end().to_ascii_lowercase(); + let (num, multiplier) = if let Some(num) = duration.strip_suffix('d') { + (num, 24 * 60 * 60 * 1000) + } else if let Some(num) = duration.strip_suffix('h') { + (num, 60 * 60 * 1000) + } else if let Some(num) = duration.strip_suffix('m') { + (num, 60 * 1000) + } else if let Some(num) = duration.strip_suffix("ms") { + (num, 1) + } else if let Some(num) = duration.strip_suffix('s') { + (num, 1000) + } else { + (duration.as_str(), 1) + }; + num.trim() + .parse::() + .ok() + .and_then(|num| { + if num > 0 { + Some(Duration::from_millis(num * multiplier)) + } else { + None + } + }) + .ok_or_else(|| { + format!( + "Invalid duration value {:?} for property {:?}.", + value, + key.as_key() + ) + }) + } +} + +pub trait AsKey: Clone { + fn as_key(&self) -> String; + fn as_prefix(&self) -> String; +} + +impl AsKey for &str { + fn as_key(&self) -> String { + self.to_string() + } + + fn as_prefix(&self) -> String { + format!("{self}.") + } +} + +impl AsKey for String { + fn as_key(&self) -> String { + self.to_string() + } + + fn as_prefix(&self) -> String { + format!("{self}.") + } +} + +impl AsKey for (&str, &str) { + fn as_key(&self) -> String { + format!("{}.{}", self.0, self.1) + } + + fn as_prefix(&self) -> String { + format!("{}.{}.", self.0, self.1) + } +} + +impl AsKey for (&String, &str) { + fn as_key(&self) -> String { + format!("{}.{}", self.0, self.1) + } + + fn as_prefix(&self) -> String { + format!("{}.{}.", self.0, self.1) + } +} + +impl AsKey for (&str, &str, &str) { + fn as_key(&self) -> String { + format!("{}.{}.{}", self.0, self.1, self.2) + } + + fn as_prefix(&self) -> String { + format!("{}.{}.{}.", self.0, self.1, self.2) + } +} + +impl AsKey for (&str, &str, &str, &str) { + fn as_key(&self) -> String { + format!("{}.{}.{}.{}", self.0, self.1, self.2, self.3) + } + + fn as_prefix(&self) -> String { + format!("{}.{}.{}.{}.", self.0, self.1, self.2, self.3) + } +} + +#[cfg(test)] +mod tests { + use std::net::IpAddr; + + use crate::config::Config; + + #[test] + fn toml_utils() { + let toml = r#" +[queues."z"] +retry = [0, 1, 15, 60, 90] +value = "hi" + +[queues."x"] +retry = [3, 60] +value = "hi 2" + +[queues.a] +retry = [1, 2, 3, 4] +value = "hi 3" + +[servers."my relay"] +hostname = "mx.example.org" + +[[servers."my relay".transaction.auth.limits]] +idle = 10 + +[[servers."my relay".transaction.auth.limits]] +idle = 20 + +[servers."submissions"] +hostname = "submit.example.org" +ip = "a:b::1:1" +"#; + let config = Config::parse(toml).unwrap(); + + assert_eq!( + config.sub_keys("queues").collect::>(), + ["a", "x", "z"] + ); + assert_eq!( + config.sub_keys("servers").collect::>(), + ["my relay", "submissions"] + ); + assert_eq!( + config.sub_keys("queues.z.retry").collect::>(), + ["0", "1", "2", "3", "4"] + ); + assert_eq!( + config + .property::("servers.my relay.transaction.auth.limits.1.idle") + .unwrap() + .unwrap(), + 20 + ); + assert_eq!( + config + .property::(("servers", "submissions", "ip")) + .unwrap() + .unwrap(), + "a:b::1:1".parse::().unwrap() + ); + } +} diff --git a/crates/utils/src/lib.rs b/crates/utils/src/lib.rs new file mode 100644 index 00000000..e56286fe --- /dev/null +++ b/crates/utils/src/lib.rs @@ -0,0 +1,26 @@ +/* + * Copyright (c) 2023 Stalwart Labs Ltd. + * + * This file is part of the Stalwart SMTP 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 codec; +pub mod config; +pub mod map; diff --git a/crates/utils/src/map/bitmap.rs b/crates/utils/src/map/bitmap.rs new file mode 100644 index 00000000..83a52ca7 --- /dev/null +++ b/crates/utils/src/map/bitmap.rs @@ -0,0 +1,205 @@ +/* + * 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; + +#[derive( + Debug, serde::Serialize, serde::Deserialize, Clone, PartialOrd, Ord, PartialEq, Eq, Hash, +)] +pub struct Bitmap { + pub bitmap: u64, + #[serde(skip)] + _state: std::marker::PhantomData, +} + +pub trait BitmapItem: From + Into + Sized + Copy { + fn max() -> u64; + fn is_valid(&self) -> bool; +} + +impl Bitmap { + pub fn new() -> Self { + Self::default() + } + + #[inline(always)] + pub fn all() -> Self { + Self { + bitmap: u64::MAX >> (64 - T::max()), + _state: std::marker::PhantomData, + } + } + + #[inline(always)] + pub fn union(&mut self, items: &Bitmap) { + self.bitmap |= items.bitmap; + } + + #[inline(always)] + pub fn intersection(&mut self, items: &Bitmap) { + self.bitmap &= items.bitmap; + } + + #[inline(always)] + pub fn insert(&mut self, item: T) { + debug_assert!(item.is_valid()); + self.bitmap |= 1 << item.into(); + } + + #[inline(always)] + pub fn remove(&mut self, item: T) { + debug_assert!(item.is_valid()); + self.bitmap ^= 1 << item.into(); + } + + #[inline(always)] + pub fn pop(&mut self) -> Option { + if self.bitmap != 0 { + let item = 63 - self.bitmap.leading_zeros(); + self.bitmap ^= 1 << item; + Some((item as u64).into()) + } else { + None + } + } + + #[inline(always)] + pub fn contains(&self, item: T) -> bool { + self.bitmap & (1 << item.into()) != 0 + } + + #[inline(always)] + pub fn is_empty(&self) -> bool { + self.bitmap == 0 + } + + #[inline(always)] + pub fn clear(&mut self) -> Self { + let bitmap = self.bitmap; + self.bitmap = 0; + Bitmap { + bitmap, + _state: std::marker::PhantomData, + } + } +} + +impl From for Bitmap { + fn from(value: u64) -> Self { + Self { + bitmap: value, + _state: std::marker::PhantomData, + } + } +} + +impl AsRef for Bitmap { + fn as_ref(&self) -> &u64 { + &self.bitmap + } +} + +impl Deref for Bitmap { + type Target = u64; + + fn deref(&self) -> &Self::Target { + &self.bitmap + } +} + +impl Iterator for Bitmap { + type Item = T; + + fn next(&mut self) -> Option { + if self.bitmap != 0 { + let item = 63 - self.bitmap.leading_zeros(); + self.bitmap ^= 1 << item; + Some((item as u64).into()) + } else { + None + } + } +} + +impl From> for Bitmap { + fn from(values: Vec) -> Self { + let mut bitmap = Bitmap::default(); + for value in values { + if value.is_valid() { + bitmap.insert(value); + } + } + bitmap + } +} + +impl FromIterator for Bitmap { + fn from_iter>(iter: U) -> Self { + let mut bitmap = Bitmap::new(); + for value in iter { + if value.is_valid() { + bitmap.insert(value); + } + } + bitmap + } +} + +impl From<&Vec> for Bitmap { + fn from(values: &Vec) -> Self { + let mut bitmap = Bitmap::default(); + for value in values { + if value.is_valid() { + bitmap.insert(*value); + } + } + bitmap + } +} + +impl From for Bitmap { + fn from(value: T) -> Self { + let mut bitmap = Bitmap::default(); + bitmap.insert(value); + bitmap + } +} + +impl From> for Vec { + fn from(values: Bitmap) -> Self { + let mut list = Vec::new(); + for item in values { + list.push(item); + } + list + } +} + +impl Default for Bitmap { + fn default() -> Self { + Bitmap { + bitmap: 0, + _state: std::marker::PhantomData, + } + } +} diff --git a/crates/utils/src/map/mod.rs b/crates/utils/src/map/mod.rs new file mode 100644 index 00000000..6cac8af9 --- /dev/null +++ b/crates/utils/src/map/mod.rs @@ -0,0 +1,25 @@ +/* + * Copyright (c) 2023 Stalwart Labs Ltd. + * + * This file is part of the Stalwart SMTP 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 bitmap; +pub mod vec_map; diff --git a/crates/utils/src/map/vec_map.rs b/crates/utils/src/map/vec_map.rs new file mode 100644 index 00000000..33871e19 --- /dev/null +++ b/crates/utils/src/map/vec_map.rs @@ -0,0 +1,272 @@ +/* + * 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::Borrow, fmt}; + +use serde::{de::DeserializeOwned, ser::SerializeMap, Deserialize, Serialize}; + +// A map implemented using vectors +// used for small datasets of less than 20 items +// and when deserializing from JSON + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct VecMap { + pub k: Vec, + pub v: Vec, +} + +impl Default for VecMap { + fn default() -> Self { + VecMap { + k: Vec::new(), + v: Vec::new(), + } + } +} + +impl VecMap { + pub fn new() -> Self { + Self { + k: Vec::new(), + v: Vec::new(), + } + } + + pub fn with_capacity(capacity: usize) -> Self { + Self { + k: Vec::with_capacity(capacity), + v: Vec::with_capacity(capacity), + } + } + + #[inline(always)] + pub fn set(&mut self, key: K, value: V) -> bool { + if let Some(pos) = self.k.iter().position(|k| *k == key) { + self.v[pos] = value; + false + } else { + self.k.push(key); + self.v.push(value); + true + } + } + + #[inline(always)] + pub fn append(&mut self, key: K, value: V) { + self.k.push(key); + self.v.push(value); + } + + #[inline(always)] + pub fn get(&self, key: &Q) -> Option<&V> + where + K: Borrow + PartialEq, + { + self.k.iter().position(|k| k == key).map(|pos| &self.v[pos]) + } + + #[inline(always)] + pub fn get_mut(&mut self, key: &K) -> Option<&mut V> { + self.k + .iter_mut() + .position(|k| k == key) + .map(|pos| &mut self.v[pos]) + } + + #[inline(always)] + pub fn contains_key(&self, key: &K) -> bool { + self.k.contains(key) + } + + #[inline(always)] + pub fn remove(&mut self, key: &K) -> Option { + self.k.iter().position(|k| k == key).map(|pos| { + self.k.swap_remove(pos); + self.v.swap_remove(pos) + }) + } + + #[inline(always)] + pub fn remove_entry(&mut self, key: &K) -> Option<(K, V)> { + self.k + .iter() + .position(|k| k == key) + .map(|pos| (self.k.swap_remove(pos), self.v.swap_remove(pos))) + } + + #[inline(always)] + pub fn swap_remove(&mut self, index: usize) -> V { + self.k.swap_remove(index); + self.v.swap_remove(index) + } + + #[inline(always)] + pub fn is_empty(&self) -> bool { + self.k.is_empty() + } + + #[inline(always)] + pub fn len(&self) -> usize { + self.k.len() + } + + #[inline(always)] + pub fn clear(&mut self) { + self.k.clear(); + self.v.clear(); + } + + #[inline(always)] + pub fn iter(&self) -> impl Iterator { + self.k.iter().zip(self.v.iter()) + } + + #[inline(always)] + pub fn iter_mut(&mut self) -> impl Iterator { + self.k.iter_mut().zip(self.v.iter_mut()) + } + + #[inline(always)] + pub fn keys(&self) -> impl Iterator { + self.k.iter() + } + + #[inline(always)] + pub fn values(&self) -> impl Iterator { + self.v.iter() + } + + #[inline(always)] + pub fn values_mut(&mut self) -> impl Iterator { + self.v.iter_mut() + } + + pub fn get_mut_or_insert_with(&mut self, key: K, fnc: impl FnOnce() -> V) -> &mut V { + if let Some(pos) = self.k.iter().position(|k| k == &key) { + &mut self.v[pos] + } else { + self.k.push(key); + self.v.push(fnc()); + self.v.last_mut().unwrap() + } + } +} + +impl VecMap { + pub fn get_mut_or_insert(&mut self, key: K) -> &mut V { + if let Some(pos) = self.k.iter().position(|k| k == &key) { + &mut self.v[pos] + } else { + self.k.push(key); + self.v.push(V::default()); + self.v.last_mut().unwrap() + } + } +} + +impl IntoIterator for VecMap { + type Item = (K, V); + + type IntoIter = std::iter::Zip, std::vec::IntoIter>; + + fn into_iter(self) -> Self::IntoIter { + self.k.into_iter().zip(self.v.into_iter()) + } +} + +impl<'x, K: Eq + PartialEq, V> IntoIterator for &'x VecMap { + type Item = (&'x K, &'x V); + + type IntoIter = std::iter::Zip, std::slice::Iter<'x, V>>; + + fn into_iter(self) -> Self::IntoIter { + self.k.iter().zip(self.v.iter()) + } +} + +impl FromIterator<(K, V)> for VecMap { + fn from_iter(iter: T) -> Self + where + T: IntoIterator, + { + let mut map = VecMap::new(); + for (k, v) in iter { + map.append(k, v); + } + map + } +} + +struct VecMapVisitor { + phantom: std::marker::PhantomData<(K, V)>, +} + +impl<'de, K: Eq + PartialEq + DeserializeOwned, V: DeserializeOwned> serde::de::Visitor<'de> + for VecMapVisitor +{ + type Value = VecMap; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("a valid map") + } + + fn visit_map(self, mut map: A) -> Result + where + A: serde::de::MapAccess<'de>, + { + // Duplicates are not checked during deserialization + let mut vec_map = VecMap::new(); + while let Some(key) = map.next_key::()? { + vec_map.append(key, map.next_value()?); + } + Ok(vec_map) + } +} + +impl<'de, K: Eq + PartialEq + DeserializeOwned, V: DeserializeOwned> Deserialize<'de> + for VecMap +{ + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + deserializer.deserialize_map(VecMapVisitor { + phantom: std::marker::PhantomData, + }) + } +} + +impl Serialize for VecMap { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + let mut map = serializer.serialize_map(self.len().into())?; + + for (key, value) in self { + map.serialize_entry(key, value)? + } + + map.end() + } +} diff --git a/tests/Cargo.toml b/tests/Cargo.toml index 6ccbb868..fab41e99 100644 --- a/tests/Cargo.toml +++ b/tests/Cargo.toml @@ -5,7 +5,7 @@ edition = "2021" [dependencies] store = { path = "../crates/store", features = ["test_mode"] } -utils = { path = "/home/vagrant/code/utils" } +utils = { path = "../crates/utils" } [dev-dependencies] tokio = { version = "1.23", features = ["full"] }