From c557691059726340039f642bbfb1f42abb3cf737 Mon Sep 17 00:00:00 2001 From: John Coffey Date: Thu, 29 Oct 2026 00:28:43 -0700 Subject: [PATCH 01/10] Sign-in: never offer or take a bearer token on a clear-text connection With plain-text sign-in allowed, IMAP, POP3 and ManageSieve offered OAUTHBEARER and XOAUTH2 on unencrypted connections and accepted them. SMTP submission did the same wherever its mechanism setting listed them, and the default setting listed them on submission ports before STARTTLS. ManageSieve also offered them on clear text with plain-text sign-in off, though it then refused them. A mail app would send its bearer token in the clear, where anyone on the network path could read it and use it as the account until it expired, in any protocol and over HTTP. Bearer token mechanisms now need TLS (implicit, STARTTLS, or a proxy that reports TLS) in all four protocols, whatever the plain-text or mechanism settings say. They are left out of the greeting, CAPABILITY, CAPA and EHLO lists on clear text, and a request for one is refused before the token is asked for: IMAP with NO [PRIVACYREQUIRED], ManageSieve with NO (ENCRYPT-NEEDED), POP3 with -ERR and SMTP with 538 5.7.11. Password sign-in keeps following the plain-text setting. ManageSieve offers an empty SASL list on clear text when plain-text sign-in is off, next to STARTTLS. Test: on clear-text IMAP, POP3 and ManageSieve with plain-text sign-in allowed, the greetings and capability lists offered OAUTHBEARER and XOAUTH2 and AUTHENTICATE answered "+" or tried the token on main; SMTP EHLO without TLS offered OAUTHBEARER where the setting allowed every mechanism. Now none is offered, each request is refused, PLAIN is still offered, and bearer sign-in over TLS still works. --- crates/imap-proto/src/protocol/capability.rs | 16 +- crates/imap/src/core/session.rs | 5 +- crates/imap/src/lib.rs | 23 ++- crates/imap/src/op/authenticate.rs | 11 ++ crates/imap/src/op/capability.rs | 1 + crates/managesieve/src/op/authenticate.rs | 12 +- crates/managesieve/src/op/capability.rs | 8 +- crates/pop3/src/client.rs | 16 +- crates/pop3/src/op/mod.rs | 5 +- crates/smtp/src/inbound/auth.rs | 27 +++- crates/smtp/src/inbound/ehlo.rs | 16 +- crates/smtp/src/inbound/session.rs | 25 +-- tests/src/imap/bearer_tls.rs | 152 +++++++++++++++++++ tests/src/imap/mod.rs | 11 ++ tests/src/smtp/inbound/auth.rs | 44 +++++- tests/src/system/oidc.rs | 107 ++++++++++++- 16 files changed, 427 insertions(+), 52 deletions(-) create mode 100644 tests/src/imap/bearer_tls.rs diff --git a/crates/imap-proto/src/protocol/capability.rs b/crates/imap-proto/src/protocol/capability.rs index 4fb7c577..f22dce7b 100644 --- a/crates/imap-proto/src/protocol/capability.rs +++ b/crates/imap-proto/src/protocol/capability.rs @@ -2,6 +2,8 @@ * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC * * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL + * + * Modified by Coffey Labs in 2026 for INBUXA. */ use super::{ImapResponse, authenticate::Mechanism}; @@ -144,6 +146,7 @@ impl Capability { is_authenticated: bool, offer_tls: bool, allow_auth: bool, + allow_bearer: bool, message_limit: u32, save_limit: u32, ) -> Vec { @@ -195,11 +198,14 @@ impl Capability { Capability::SaveLimit(save_limit), ]); } else if allow_auth { - capabilities.extend([ - Capability::Auth(Mechanism::Plain), - Capability::Auth(Mechanism::OAuthBearer), - Capability::Auth(Mechanism::XOauth2), - ]); + capabilities.push(Capability::Auth(Mechanism::Plain)); + // inbuxa: bearer tokens are only offered over TLS + if allow_bearer { + capabilities.extend([ + Capability::Auth(Mechanism::OAuthBearer), + Capability::Auth(Mechanism::XOauth2), + ]); + } } else { capabilities.push(Capability::LoginDisabled); } diff --git a/crates/imap/src/core/session.rs b/crates/imap/src/core/session.rs index e3ed12ae..1bfbd344 100644 --- a/crates/imap/src/core/session.rs +++ b/crates/imap/src/core/session.rs @@ -8,7 +8,7 @@ use super::{ImapSessionManager, Session, State}; use crate::{ - GREETING_WITH_TLS, GREETING_WITH_TLS_LOGIN_DISABLED, GREETING_WITHOUT_TLS, + GREETING_CLEAR_TEXT, GREETING_WITH_TLS, GREETING_WITH_TLS_LOGIN_DISABLED, GREETING_WITHOUT_TLS, GREETING_WITHOUT_TLS_LOGIN_DISABLED, }; use common::{ @@ -129,7 +129,8 @@ impl Session { let greeting = match (offer_tls, allow_auth) { (true, true) => &GREETING_WITH_TLS, (true, false) => &GREETING_WITH_TLS_LOGIN_DISABLED, - (false, true) => &GREETING_WITHOUT_TLS, + (false, true) if is_tls => &GREETING_WITHOUT_TLS, + (false, true) => &GREETING_CLEAR_TEXT, (false, false) => &GREETING_WITHOUT_TLS_LOGIN_DISABLED, }; diff --git a/crates/imap/src/lib.rs b/crates/imap/src/lib.rs index 010e5697..4d6aa835 100644 --- a/crates/imap/src/lib.rs +++ b/crates/imap/src/lib.rs @@ -21,21 +21,32 @@ pub mod op; static SERVER_GREETING: &str = concat!(types::brand!(), " IMAP4rev2 at your service."); pub(crate) static GREETING_WITH_TLS: LazyLock> = - LazyLock::new(|| build_greeting(true, true)); + LazyLock::new(|| build_greeting(true, true, false)); pub(crate) static GREETING_WITH_TLS_LOGIN_DISABLED: LazyLock> = - LazyLock::new(|| build_greeting(true, false)); + LazyLock::new(|| build_greeting(true, false, false)); pub(crate) static GREETING_WITHOUT_TLS: LazyLock> = - LazyLock::new(|| build_greeting(false, true)); + LazyLock::new(|| build_greeting(false, true, true)); + +// inbuxa: clear text with no STARTTLS to offer, so no bearer tokens either +pub(crate) static GREETING_CLEAR_TEXT: LazyLock> = + LazyLock::new(|| build_greeting(false, true, false)); pub(crate) static GREETING_WITHOUT_TLS_LOGIN_DISABLED: LazyLock> = - LazyLock::new(|| build_greeting(false, false)); + LazyLock::new(|| build_greeting(false, false, false)); -fn build_greeting(offer_tls: bool, allow_auth: bool) -> Vec { +fn build_greeting(offer_tls: bool, allow_auth: bool, allow_bearer: bool) -> Vec { StatusResponse::ok(SERVER_GREETING) .with_code(ResponseCode::Capability { - capabilities: Capability::all_capabilities(false, offer_tls, allow_auth, 0, 0), + capabilities: Capability::all_capabilities( + false, + offer_tls, + allow_auth, + allow_bearer, + 0, + 0, + ), }) .into_bytes() } diff --git a/crates/imap/src/op/authenticate.rs b/crates/imap/src/op/authenticate.rs index b540b266..f02c2a45 100644 --- a/crates/imap/src/op/authenticate.rs +++ b/crates/imap/src/op/authenticate.rs @@ -25,6 +25,16 @@ impl Session { pub async fn handle_authenticate(&mut self, request: Request) -> trc::Result<()> { let mut args = request.parse_authenticate()?; + // inbuxa: a bearer token is a credential on its own, so it is never + // asked for or taken on clear text, whatever the plain-text setting + if !self.is_tls && matches!(args.mechanism, Mechanism::OAuthBearer | Mechanism::XOauth2) { + return Err(trc::AuthEvent::Error + .into_err() + .details("Bearer tokens require TLS.") + .id(args.tag) + .code(ResponseCode::PrivacyRequired)); + } + match args.mechanism { Mechanism::Plain | Mechanism::OAuthBearer | Mechanism::XOauth2 => { if !args.params.is_empty() { @@ -138,6 +148,7 @@ impl Session { true, !self.is_tls && self.instance.acceptor.is_tls(), true, + self.is_tls, self.server.core.imap.max_messages_per_command, self.server.core.imap.max_messages_per_save, ), diff --git a/crates/imap/src/op/capability.rs b/crates/imap/src/op/capability.rs index f8a45707..2069698d 100644 --- a/crates/imap/src/op/capability.rs +++ b/crates/imap/src/op/capability.rs @@ -44,6 +44,7 @@ impl Session { self.state.is_authenticated(), !self.is_tls && self.instance.acceptor.is_tls(), self.is_tls || self.server.core.imap.allow_plain_auth, + self.is_tls, self.server.core.imap.max_messages_per_command, self.server.core.imap.max_messages_per_save, ), diff --git a/crates/managesieve/src/op/authenticate.rs b/crates/managesieve/src/op/authenticate.rs index 259778c1..a624b179 100644 --- a/crates/managesieve/src/op/authenticate.rs +++ b/crates/managesieve/src/op/authenticate.rs @@ -6,7 +6,7 @@ * Modified by Coffey Labs in 2026 for INBUXA. */ -use crate::core::{Command, Session, State, StatusResponse}; +use crate::core::{Command, ResponseCode, Session, State, StatusResponse}; use common::{ auth::AuthRequest, network::{SessionStream, legacy::LegacyProtocol, limiter::LimiterResult}, @@ -30,6 +30,16 @@ impl Session { let mut tokens = request.tokens.into_iter(); let mechanism = Mechanism::parse(&tokens.next().unwrap().unwrap_bytes()) .map_err(|err| trc::AuthEvent::Error.into_err().details(err))?; + + // inbuxa: a bearer token is never asked for or taken on clear text, + // whatever the plain-text setting + if !self.stream.is_tls() && matches!(mechanism, Mechanism::OAuthBearer | Mechanism::XOauth2) + { + return Err(trc::AuthEvent::Error + .into_err() + .code(ResponseCode::EncryptNeeded) + .details("Bearer tokens require TLS.")); + } let mut params: Vec = tokens .filter_map(|token| token.unwrap_string().ok()) .collect(); diff --git a/crates/managesieve/src/op/capability.rs b/crates/managesieve/src/op/capability.rs index fc4c2f63..8b24dfb2 100644 --- a/crates/managesieve/src/op/capability.rs +++ b/crates/managesieve/src/op/capability.rs @@ -28,10 +28,14 @@ impl Session { if !self.stream.is_tls() { response.extend_from_slice(b"\"STARTTLS\"\r\n"); } - if self.stream.is_tls() || self.server.core.imap.allow_plain_auth { + // inbuxa: bearer tokens are only offered over TLS, and nothing is + // offered on clear text with plain-text sign-in off (STARTTLS is) + if self.stream.is_tls() { response.extend_from_slice(b"\"SASL\" \"PLAIN OAUTHBEARER XOAUTH2\"\r\n"); + } else if self.server.core.imap.allow_plain_auth { + response.extend_from_slice(b"\"SASL\" \"PLAIN\"\r\n"); } else { - response.extend_from_slice(b"\"SASL\" \"OAUTHBEARER XOAUTH2\"\r\n"); + response.extend_from_slice(b"\"SASL\" \"\"\r\n"); }; if let Some(sieve) = self.server diff --git a/crates/pop3/src/client.rs b/crates/pop3/src/client.rs index 4f5bef50..72c20603 100644 --- a/crates/pop3/src/client.rs +++ b/crates/pop3/src/client.rs @@ -213,7 +213,21 @@ impl Session { | Command::Apop { .. } => { if let State::NotAuthenticated { username, .. } = &self.state { if self.stream.is_tls() || self.server.core.imap.allow_plain_auth { - if !matches!(command, Command::Pass { .. }) || username.is_some() { + if !self.stream.is_tls() + && matches!( + command, + Command::Auth { + mechanism: Mechanism::OAuthBearer | Mechanism::XOauth2, + .. + } + ) + { + // inbuxa: a bearer token is never taken on clear + // text, whatever the plain-text setting + Err(trc::Pop3Event::Error + .into_err() + .details("Bearer tokens require TLS.")) + } else if !matches!(command, Command::Pass { .. }) || username.is_some() { Ok(command) } else { Err(trc::Pop3Event::Error diff --git a/crates/pop3/src/op/mod.rs b/crates/pop3/src/op/mod.rs index 59d35576..8136252e 100644 --- a/crates/pop3/src/op/mod.rs +++ b/crates/pop3/src/op/mod.rs @@ -20,8 +20,11 @@ pub mod list; impl Session { pub async fn handle_capa(&mut self) -> trc::Result<()> { - let mechanisms = if self.stream.is_tls() || self.server.core.imap.allow_plain_auth { + let mechanisms = if self.stream.is_tls() { vec![Mechanism::Plain, Mechanism::OAuthBearer, Mechanism::XOauth2] + } else if self.server.core.imap.allow_plain_auth { + // inbuxa: bearer tokens are only offered over TLS + vec![Mechanism::Plain] } else { // inbuxa: no mechanism is offered where none would be accepted vec![] diff --git a/crates/smtp/src/inbound/auth.rs b/crates/smtp/src/inbound/auth.rs index d7ecd26f..4ca396d0 100644 --- a/crates/smtp/src/inbound/auth.rs +++ b/crates/smtp/src/inbound/auth.rs @@ -9,14 +9,18 @@ use crate::core::Session; use common::{ auth::AuthRequest, + config::smtp::session::Mechanism, network::{SessionStream, legacy::LegacyProtocol}, }; use directory::Credentials; use mail_parser::{HeaderName, HeaderValue, decoders::base64::base64_decode}; use registry::schema::enums::Permission; -use smtp_proto::{AUTH_LOGIN, AUTH_OAUTHBEARER, AUTH_PLAIN, AUTH_XOAUTH2, IntoString}; +use smtp_proto::{AUTH_LOGIN, AUTH_OAUTHBEARER, AUTH_PLAIN, AUTH_XOAUTH, AUTH_XOAUTH2, IntoString}; use trc::AuthEvent; +// inbuxa: mechanisms that send a bearer token, a credential on its own +pub const AUTH_BEARER: u64 = AUTH_OAUTHBEARER | AUTH_XOAUTH | AUTH_XOAUTH2; + pub struct SaslToken { mechanism: u64, credentials: Credentials, @@ -48,6 +52,27 @@ impl SaslToken { } impl Session { + /// The mechanisms this session may use: the configured ones, less the + /// bearer token ones on clear text whatever the configuration says. + pub async fn sasl_mechanisms(&self) -> u64 { + let mechanisms: u64 = self + .server + .eval_if::( + &self.server.core.smtp.session.auth.mechanisms, + self, + self.data.session_id, + ) + .await + .unwrap_or_default() + .into(); + + if self.stream.is_tls() { + mechanisms + } else { + mechanisms & !AUTH_BEARER + } + } + pub async fn handle_sasl_response( &mut self, token: &mut SaslToken, diff --git a/crates/smtp/src/inbound/ehlo.rs b/crates/smtp/src/inbound/ehlo.rs index d82d46b8..64831c85 100644 --- a/crates/smtp/src/inbound/ehlo.rs +++ b/crates/smtp/src/inbound/ehlo.rs @@ -2,13 +2,12 @@ * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC * * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL + * + * Modified by Coffey Labs in 2026 for INBUXA. */ use crate::{core::Session, scripts::ScriptResult}; -use common::{ - config::smtp::session::{Mechanism, Stage}, - network::SessionStream, -}; +use common::{config::smtp::session::Stage, network::SessionStream}; use mail_auth::{ SpfResult, spf::verify::{HasValidLabels, SpfParameters}, @@ -149,7 +148,6 @@ impl Session { response.capabilities |= EXT_START_TLS; } let ec = &self.server.core.smtp.session.extensions; - let ac = &self.server.core.smtp.session.auth; let dc = &self.server.core.smtp.session.data; // Pipelining @@ -214,12 +212,8 @@ impl Session { // Authentication if !self.is_authenticated() { - response.auth_mechanisms = self - .server - .eval_if::(&ac.mechanisms, self, self.data.session_id) - .await - .unwrap_or_default() - .into(); + // inbuxa: no bearer token mechanism on clear text + response.auth_mechanisms = self.sasl_mechanisms().await; if response.auth_mechanisms != 0 { response.capabilities |= EXT_AUTH; } diff --git a/crates/smtp/src/inbound/session.rs b/crates/smtp/src/inbound/session.rs index cae37532..11394997 100644 --- a/crates/smtp/src/inbound/session.rs +++ b/crates/smtp/src/inbound/session.rs @@ -7,7 +7,7 @@ */ use common::{ - config::{server::ServerProtocol, smtp::session::Mechanism}, + config::server::ServerProtocol, expr::{self, functions::ResolveVariable, *}, network::SessionStream, }; @@ -26,7 +26,7 @@ use trc::{NetworkEvent, SecurityEvent, SmtpEvent}; use crate::core::{Session, State}; -use super::auth::SaslToken; +use super::auth::{AUTH_BEARER, SaslToken}; impl Session { pub async fn ingest(&mut self, bytes: &[u8]) -> Result { @@ -102,16 +102,8 @@ impl Session { mechanism, initial_response, } => { - let auth: u64 = self - .server - .eval_if::( - &self.server.core.smtp.session.auth.mechanisms, - self, - self.data.session_id, - ) - .await - .unwrap_or_default() - .into(); + // inbuxa: no bearer token mechanism on clear text + let auth = self.sasl_mechanisms().await; if auth == 0 { trc::event!( Smtp(SmtpEvent::AuthNotAllowed), @@ -127,6 +119,15 @@ impl Session { ); self.write(b"503 5.5.1 Already authenticated.\r\n").await?; + } else if mechanism & AUTH_BEARER != 0 && !self.stream.is_tls() { + trc::event!( + Smtp(SmtpEvent::AuthMechanismNotSupported), + SpanId = self.data.session_id, + Details = "Bearer tokens require TLS", + ); + + self.write(b"538 5.7.11 Bearer tokens require TLS.\r\n") + .await?; } else if let Some(mut token) = SaslToken::from_mechanism(mechanism & auth) { diff --git a/tests/src/imap/bearer_tls.rs b/tests/src/imap/bearer_tls.rs new file mode 100644 index 00000000..b7d66ab4 --- /dev/null +++ b/tests/src/imap/bearer_tls.rs @@ -0,0 +1,152 @@ +/* + * SPDX-FileCopyrightText: 2026 Coffey Labs LLC + * + * SPDX-License-Identifier: AGPL-3.0-only + */ + +//! inbuxa: bearer tokens are never offered or accepted on a clear-text +//! connection, even with plain-text sign-in allowed. Password sign-in +//! there keeps following that setting. + +use base64::{Engine, engine::general_purpose}; +use tokio::{ + io::{AsyncBufReadExt, AsyncWriteExt, BufReader, Lines}, + net::{TcpStream, tcp::OwnedReadHalf}, +}; + +// Expects plain-text sign-in to be allowed, as the imap suite leaves it. +pub async fn test() { + println!("Running clear-text bearer token tests..."); + + let token = general_purpose::STANDARD + .encode("n,a=jdoe@example.com,\u{1}auth=Bearer not-a-real-token\u{1}\u{1}"); + let mut problems = Vec::new(); + + // IMAP, clear text with STARTTLS available + let (mut rx, mut tx) = connect("127.0.0.1:9991").await; + let greeting = rx.next_line().await.unwrap().unwrap(); + check_offer( + &mut problems, + "IMAP greeting", + &greeting, + "AUTH=PLAIN", + &["AUTH=OAUTHBEARER", "AUTH=XOAUTH2"], + ); + tx.write_all(b"a CAPABILITY\r\n").await.unwrap(); + let capability = read_until(&mut rx, "a ").await.join("\n"); + check_offer( + &mut problems, + "IMAP CAPABILITY", + &capability, + "AUTH=PLAIN", + &["AUTH=OAUTHBEARER", "AUTH=XOAUTH2"], + ); + tx.write_all(format!("b AUTHENTICATE OAUTHBEARER {token}\r\n").as_bytes()) + .await + .unwrap(); + let reply = read_until(&mut rx, "b ").await.join("\n"); + if !reply.contains("b NO [PRIVACYREQUIRED]") { + problems.push(format!( + "IMAP AUTHENTICATE OAUTHBEARER not refused: {reply}" + )); + } + tx.write_all(b"c AUTHENTICATE XOAUTH2\r\n").await.unwrap(); + let reply = rx.next_line().await.unwrap().unwrap(); + if !reply.starts_with("c NO [PRIVACYREQUIRED]") { + problems.push(format!("IMAP AUTHENTICATE XOAUTH2 not refused: {reply}")); + } + drop(tx); + + // POP3, clear text + let (mut rx, mut tx) = connect("127.0.0.1:4111").await; + assert!(rx.next_line().await.unwrap().unwrap().starts_with("+OK")); + tx.write_all(b"CAPA\r\n").await.unwrap(); + let capa = read_until(&mut rx, ".").await.join("\n"); + check_offer( + &mut problems, + "POP3 CAPA", + &capa, + "SASL PLAIN", + &["OAUTHBEARER", "XOAUTH2"], + ); + for (mechanism, param) in [("OAUTHBEARER", token.as_str()), ("XOAUTH2", "")] { + tx.write_all( + format!("AUTH {mechanism} {param}\r\n") + .trim_end() + .as_bytes(), + ) + .await + .unwrap(); + tx.write_all(b"\r\n").await.unwrap(); + let reply = rx.next_line().await.unwrap().unwrap(); + if !(reply.starts_with("-ERR") && reply.contains("TLS")) { + problems.push(format!("POP3 AUTH {mechanism} not refused: {reply}")); + } + } + drop(tx); + + // ManageSieve, clear text with STARTTLS available + let (mut rx, mut tx) = connect("127.0.0.1:4191").await; + let greeting = read_until(&mut rx, "OK").await.join("\n"); + check_offer( + &mut problems, + "ManageSieve greeting", + &greeting, + "PLAIN", + &["OAUTHBEARER", "XOAUTH2"], + ); + for (mechanism, param) in [ + ("OAUTHBEARER", format!(" \"{token}\"")), + ("XOAUTH2", "".into()), + ] { + tx.write_all(format!("AUTHENTICATE \"{mechanism}\"{param}\r\n").as_bytes()) + .await + .unwrap(); + let reply = rx.next_line().await.unwrap().unwrap(); + if !reply.starts_with("NO (ENCRYPT-NEEDED)") { + problems.push(format!( + "ManageSieve AUTHENTICATE {mechanism} not refused: {reply}" + )); + } + } + drop(tx); + + assert!( + problems.is_empty(), + "bearer tokens on clear text:\n{}", + problems.join("\n") + ); +} + +fn check_offer(problems: &mut Vec, what: &str, text: &str, keep: &str, drop: &[&str]) { + if !text.contains(keep) { + problems.push(format!("{what} lost {keep}: {text}")); + } + for mechanism in drop { + if text.contains(mechanism) { + problems.push(format!("{what} offered {mechanism}: {text}")); + } + } +} + +async fn connect( + addr: &str, +) -> ( + Lines>, + tokio::net::tcp::OwnedWriteHalf, +) { + let (rx, tx) = TcpStream::connect(addr).await.unwrap().into_split(); + (BufReader::new(rx).lines(), tx) +} + +async fn read_until(rx: &mut Lines>, prefix: &str) -> Vec { + let mut lines = Vec::new(); + loop { + let line = rx.next_line().await.unwrap().unwrap(); + let done = line.starts_with(prefix); + lines.push(line); + if done { + return lines; + } + } +} diff --git a/tests/src/imap/mod.rs b/tests/src/imap/mod.rs index 32065a8c..1c936820 100644 --- a/tests/src/imap/mod.rs +++ b/tests/src/imap/mod.rs @@ -10,6 +10,7 @@ pub mod acl; pub mod antispam; pub mod append; pub mod basic; +pub mod bearer_tls; pub mod body_structure; pub mod condstore; pub mod copy_move; @@ -55,6 +56,13 @@ pub async fn imap_tests() { .await .with_listener(NetworkListenerProtocol::Pop3, "pop3-plain", 4111, false) .await + .with_listener( + NetworkListenerProtocol::ManageSieve, + "sieve-plain", + 4191, + false, + ) + .await .build() .await; @@ -288,6 +296,9 @@ pub async fn imap_tests() { // Run POP3 tests pop::test(&test).await; + // Bearer tokens on clear-text connections + bearer_tls::test().await; + // Print elapsed time let elapsed = start_time.elapsed(); println!( diff --git a/tests/src/smtp/inbound/auth.rs b/tests/src/smtp/inbound/auth.rs index b8b4eebf..a86a554f 100644 --- a/tests/src/smtp/inbound/auth.rs +++ b/tests/src/smtp/inbound/auth.rs @@ -2,6 +2,8 @@ * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC * * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL + * + * Modified by Coffey Labs in 2026 for INBUXA. */ use crate::{ @@ -67,10 +69,17 @@ async fn auth() { else_: "false".into(), }, sasl_mechanisms: Expression { - match_: List::from_iter([ExpressionMatch { - if_: "remote_ip = '10.0.0.1' && is_tls".into(), - then: "[plain, login]".into(), - }]), + match_: List::from_iter([ + ExpressionMatch { + if_: "remote_ip = '10.0.0.1' && is_tls".into(), + then: "[plain, login]".into(), + }, + // inbuxa: every mechanism allowed with or without TLS + ExpressionMatch { + if_: "remote_ip = '10.0.0.3'".into(), + then: "[plain, login, oauthbearer, xoauth2]".into(), + }, + ]), else_: "0".into(), }, wait_on_fail: Expression { @@ -175,4 +184,31 @@ async fn auth() { session .auth_plain("john@example.org", "12345 + extra safety", "503 5.5.1") .await; + + // inbuxa: bearer tokens are neither offered nor accepted without TLS, + // even where the configuration allows every mechanism there + let mut session = test.new_mta_session(); + session.data.remote_ip_str = "10.0.0.3".into(); + session.eval_session_params().await; + session.stream.tls = false; + session + .ehlo("mx.foobar.org") + .await + .assert_contains(" PLAIN") + .assert_not_contains("OAUTHBEARER") + .assert_not_contains("XOAUTH2"); + session + .ingest(b"AUTH OAUTHBEARER bixhPWpvaG5AZXhhbXBsZS5vcmcsAWF1dGg9QmVhcmVyIHgBAQ==\r\n") + .await + .unwrap(); + session.response().assert_code("538 5.7.11"); + session.ingest(b"AUTH XOAUTH2\r\n").await.unwrap(); + session.response().assert_code("538 5.7.11"); + session.stream.tls = true; + session + .ehlo("mx.foobar.org") + .await + .assert_contains(" PLAIN") + .assert_contains("OAUTHBEARER") + .assert_contains("XOAUTH2"); } diff --git a/tests/src/system/oidc.rs b/tests/src/system/oidc.rs index a22795e8..38f3f0f8 100644 --- a/tests/src/system/oidc.rs +++ b/tests/src/system/oidc.rs @@ -8,7 +8,7 @@ use crate::utils::{ http::HttpRequest, - imap::{ImapConnection, Type}, + imap::{AssertResult, ImapConnection, Type}, pop3::Pop3Connection, server::TestServer, smtp::SmtpConnection, @@ -39,9 +39,15 @@ use registry::schema::{ prelude::{ObjectType, Property}, structs::{OAuthClient, OidcProvider, SecretText, SecretTextValue}, }; +use rustls_pki_types::ServerName; use serde::{Serialize, de::DeserializeOwned}; use std::time::{Duration, Instant}; use store::ahash::AHashMap; +use tokio::{ + io::{AsyncBufReadExt, AsyncRead, AsyncWriteExt, BufReader}, + net::TcpStream, +}; +use utils::tls::build_tls_connector; #[derive(Debug, serde::Deserialize)] pub struct OAuthMetadata { @@ -560,20 +566,31 @@ pub async fn test(test: &mut TestServer) { "n,a={},\u{1}auth=Bearer {}\u{1}\u{1}", "user@domain", token )); + // inbuxa: bearer tokens are refused on clear text, so the sign-ins that + // should work run over STARTTLS (SMTP) and implicit TLS (IMAP) let mut smtp = SmtpConnection::connect().await; - smtp.send(&format!("AUTH OAUTHBEARER {oauth_bearer_invalid_sasl}",)) - .await; - smtp.read(1, 4).await; smtp.send(&format!("AUTH OAUTHBEARER {oauth_bearer_sasl}",)) .await; - smtp.read(1, 2).await; + smtp.read(1, 5).await; + let replies = lmtp_starttls_auth(&[&oauth_bearer_invalid_sasl, &oauth_bearer_sasl]).await; + assert!( + replies[0].starts_with('4') && replies[1].starts_with("235"), + "SMTP OAUTHBEARER over TLS: {replies:?}" + ); // Try IMAP OAUTHBEARER auth let mut imap = ImapConnection::connect(b"_x ").await; imap.assert_read(Type::Untagged, ResponseType::Ok).await; imap.send(&format!("AUTHENTICATE OAUTHBEARER {oauth_bearer_sasl}")) .await; - imap.assert_read(Type::Tagged, ResponseType::Ok).await; + imap.assert_read(Type::Tagged, ResponseType::No) + .await + .assert_contains("PRIVACYREQUIRED"); + let reply = imap_tls_auth(&oauth_bearer_sasl).await; + assert!( + reply.starts_with("a OK"), + "IMAP OAUTHBEARER over TLS: {reply}" + ); // Try POP3 OAUTHBEARER auth let mut pop3 = Pop3Connection::connect().await; @@ -1260,3 +1277,81 @@ WiYrLO4z8/kmkqvA7wGElBok9IqhRANCAAQxZK68FnQtHC0eyh8CA05xRIvxhVHn 0ymka6XBh9aFtW4wfeoKhTkSKjHc/zjh9Rr2dr3kvmYe80fMGhW4ycGA -----END PRIVATE KEY----- "; + +// inbuxa: AUTH OAUTHBEARER with each SASL response on the LMTP port after +// STARTTLS, one attempt per response; returns the final reply lines. +async fn lmtp_starttls_auth(sasl: &[&str]) -> Vec { + let mut stream = BufReader::new(TcpStream::connect("127.0.0.1:11200").await.unwrap()); + read_smtp_reply(&mut stream).await; + stream + .get_mut() + .write_all(b"LHLO localhost\r\n") + .await + .unwrap(); + read_smtp_reply(&mut stream).await; + stream.get_mut().write_all(b"STARTTLS\r\n").await.unwrap(); + assert!(read_smtp_reply(&mut stream).await.starts_with("220")); + let tls = build_tls_connector(true) + .unwrap() + .connect( + ServerName::try_from("localhost").unwrap().to_owned(), + stream.into_inner(), + ) + .await + .unwrap(); + let mut stream = BufReader::new(tls); + stream + .get_mut() + .write_all(b"LHLO localhost\r\n") + .await + .unwrap(); + read_smtp_reply(&mut stream).await; + let mut replies = Vec::new(); + for response in sasl { + stream + .get_mut() + .write_all(format!("AUTH OAUTHBEARER {response}\r\n").as_bytes()) + .await + .unwrap(); + replies.push(read_smtp_reply(&mut stream).await); + } + replies +} + +async fn read_smtp_reply(stream: &mut BufReader) -> String { + loop { + let mut line = String::new(); + stream.read_line(&mut line).await.unwrap(); + if line.len() < 4 || line.as_bytes()[3] == b' ' { + return line; + } + } +} + +// inbuxa: AUTHENTICATE OAUTHBEARER on the implicit TLS IMAP port; returns the +// tagged reply. +async fn imap_tls_auth(sasl: &str) -> String { + let tls = build_tls_connector(true) + .unwrap() + .connect( + ServerName::try_from("localhost").unwrap().to_owned(), + TcpStream::connect("127.0.0.1:9992").await.unwrap(), + ) + .await + .unwrap(); + let mut stream = BufReader::new(tls); + let mut line = String::new(); + stream.read_line(&mut line).await.unwrap(); + stream + .get_mut() + .write_all(format!("a AUTHENTICATE OAUTHBEARER {sasl}\r\n").as_bytes()) + .await + .unwrap(); + loop { + line.clear(); + stream.read_line(&mut line).await.unwrap(); + if line.starts_with("a ") || line.is_empty() { + return line; + } + } +} -- 2.54.0 From 11960a6ac8fb8af5f845c0e5f06d2be107039488 Mon Sep 17 00:00:00 2001 From: John Coffey Date: Thu, 29 Oct 2026 00:40:00 -0700 Subject: [PATCH 02/10] IMAP: run at most four operations per session and keep writes in order Every IMAP command after sign-in started its own task with no limit, so a signed-in client could pipeline a burst of commands and have the server start as many tasks at once as the burst held, bounded only by the per-account request rate. The tasks also ran side by side whatever they did: an APPEND into a mailbox created by the CREATE just before it failed with TRYCREATE, a STATUS or FETCH sent after an APPEND or STORE could miss its result, and an EXPUNGE could run while an earlier COPY was still reading the messages. Each session now has a gate of four permits. A command that only reads (FETCH without marking messages seen, SEARCH, SORT, THREAD, STATUS, LIST, GETACL, MYRIGHTS, GETQUOTA) takes one; a command that changes the account or the session (APPEND, COPY, MOVE, STORE, CREATE, DELETE, RENAME, SUBSCRIBE, SETACL, a FETCH that sets \Seen, and every command run without a task of its own, such as SELECT, EXPUNGE and CLOSE) takes all four. So at most four reads run together, a write starts once everything before it has finished, and later commands wait for it (RFC 9051, section 5.5). The session waits for the permit before it starts the command and reads nothing more from the client meanwhile. Test: a new imap suite check pipelines CREATE, eight APPENDs and STATUS in one write; on main the APPENDs failed with TRYCREATE before the CREATE completed and STATUS saw no mailbox, now all complete in order and STATUS reports 8; it also pipelines STORE then FETCH. Unit tests of the gate check the bound and that a write never overlaps another operation. --- crates/imap/src/core/client.rs | 42 +++++++- crates/imap/src/core/gate.rs | 161 +++++++++++++++++++++++++++++ crates/imap/src/core/mod.rs | 3 + crates/imap/src/core/session.rs | 2 + crates/imap/src/op/acl.rs | 11 +- crates/imap/src/op/append.rs | 7 +- crates/imap/src/op/copy_move.rs | 7 +- crates/imap/src/op/create.rs | 5 +- crates/imap/src/op/delete.rs | 7 +- crates/imap/src/op/fetch.rs | 29 +++++- crates/imap/src/op/list.rs | 4 +- crates/imap/src/op/mod.rs | 6 +- crates/imap/src/op/quota.rs | 10 +- crates/imap/src/op/rename.rs | 7 +- crates/imap/src/op/search.rs | 5 +- crates/imap/src/op/status.rs | 7 +- crates/imap/src/op/store.rs | 5 +- crates/imap/src/op/subscribe.rs | 7 +- crates/imap/src/op/thread.rs | 7 +- tests/src/imap/mod.rs | 2 + tests/src/imap/pipelining.rs | 173 ++++++++++++++++++++++++++++++++ 21 files changed, 475 insertions(+), 32 deletions(-) create mode 100644 crates/imap/src/core/gate.rs create mode 100644 tests/src/imap/pipelining.rs diff --git a/crates/imap/src/core/client.rs b/crates/imap/src/core/client.rs index b0c105fe..435d4d61 100644 --- a/crates/imap/src/core/client.rs +++ b/crates/imap/src/core/client.rs @@ -18,7 +18,7 @@ use imap_proto::{ }; use trc::SecurityEvent; -use super::{SelectedMailbox, Session, SessionData, State}; +use super::{SelectedMailbox, Session, SessionData, State, gate::OpKind}; impl Session { pub async fn ingest(&mut self, bytes: &[u8]) -> SessionResult { @@ -113,6 +113,14 @@ impl Session { .collect::>(), _ => Vec::new(), }; + // inbuxa: a command handled here rather than in a spawned task + // waits for every operation in flight and holds the whole gate + // while it runs; the others take their permit in their handler + let inline_permit = if takes_own_permit(&request.command) { + None + } else { + Some(self.op_gate.acquire(OpKind::Write).await) + }; let dispatch = async { match request.command { Command::List | Command::Lsub => self @@ -285,6 +293,8 @@ impl Session { store::backend::scaleout::replica::replica_read(replica_accounts, dispatch).await }; + drop(inline_permit); + match result { Ok(SessionResult::Continue) => (), Ok(result) => return result, @@ -309,6 +319,36 @@ impl Session { } } +// inbuxa: commands whose handler takes a read or write permit itself and +// runs the operation in a spawned task +fn takes_own_permit(command: &Command) -> bool { + matches!( + command, + Command::List + | Command::Lsub + | Command::Create + | Command::Delete + | Command::Rename + | Command::Status + | Command::Append + | Command::Search(_) + | Command::Sort(_) + | Command::Fetch(_) + | Command::Store(_) + | Command::Copy(_) + | Command::Move(_) + | Command::Thread(_) + | Command::Subscribe + | Command::Unsubscribe + | Command::SetAcl + | Command::DeleteAcl + | Command::GetAcl + | Command::MyRights + | Command::GetQuota + | Command::GetQuotaRoot + ) +} + pub fn group_requests( requests: &mut Peekable>>, mut grouped_requests: Vec>, diff --git a/crates/imap/src/core/gate.rs b/crates/imap/src/core/gate.rs new file mode 100644 index 00000000..6a36e906 --- /dev/null +++ b/crates/imap/src/core/gate.rs @@ -0,0 +1,161 @@ +/* + * SPDX-FileCopyrightText: 2026 Coffey Labs LLC + * + * SPDX-License-Identifier: AGPL-3.0-only + */ + +//! inbuxa: bounds how many operations one IMAP session runs at once and +//! keeps commands that change state in order (RFC 9051, section 5.5). A +//! read takes one permit and a write takes all of them, so a write starts +//! once every earlier operation has finished and later ones wait for it. +//! The session loop waits for its permit before it starts the operation +//! and reads no more commands meanwhile. + +use std::sync::Arc; +use tokio::sync::{OwnedSemaphorePermit, Semaphore}; + +/// Operations one session may run at the same time. +pub const MAX_CONCURRENT_OPS: u32 = 4; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum OpKind { + /// Leaves the account and the session as they were + Read, + /// Changes the account or the session, so runs on its own + Write, +} + +pub type OpPermit = OwnedSemaphorePermit; + +#[derive(Clone)] +pub struct OpGate(Arc); + +impl OpGate { + pub fn new() -> Self { + OpGate(Arc::new(Semaphore::new(MAX_CONCURRENT_OPS as usize))) + } + + /// Waits until an operation of this kind may start. Permits are handed + /// out in the order they were asked for. + pub async fn acquire(&self, kind: OpKind) -> OpPermit { + let permits = match kind { + OpKind::Read => 1, + OpKind::Write => MAX_CONCURRENT_OPS, + }; + self.0 + .clone() + .acquire_many_owned(permits) + .await + .expect("the operation gate is never closed") + } +} + +impl Default for OpGate { + fn default() -> Self { + Self::new() + } +} + +#[cfg(test)] +mod tests { + use super::{MAX_CONCURRENT_OPS, OpGate, OpKind}; + use parking_lot::Mutex; + use std::{ + sync::{ + Arc, + atomic::{AtomicUsize, Ordering}, + }, + time::Duration, + }; + + // A permit that can't be had now: nothing else here frees one, so the + // timeout always expires + async fn is_blocked(gate: &OpGate, kind: OpKind) -> bool { + tokio::time::timeout(Duration::from_millis(50), gate.acquire(kind)) + .await + .is_err() + } + + #[tokio::test] + async fn reads_are_bounded() { + let gate = OpGate::new(); + let mut held = Vec::new(); + for _ in 0..MAX_CONCURRENT_OPS { + held.push(gate.acquire(OpKind::Read).await); + } + assert!(is_blocked(&gate, OpKind::Read).await); + held.pop(); + held.push(gate.acquire(OpKind::Read).await); + } + + #[tokio::test] + async fn write_waits_for_reads_and_reads_wait_for_write() { + let gate = OpGate::new(); + let read = gate.acquire(OpKind::Read).await; + assert!(is_blocked(&gate, OpKind::Write).await); + drop(read); + + let write = gate.acquire(OpKind::Write).await; + assert!(is_blocked(&gate, OpKind::Read).await); + assert!(is_blocked(&gate, OpKind::Write).await); + drop(write); + gate.acquire(OpKind::Read).await; + } + + // Drives the gate the way the session loop does: permits are taken in + // command order, then each operation runs in its own task + #[tokio::test(flavor = "multi_thread", worker_threads = 4)] + async fn pipelined_operations_keep_order() { + use OpKind::{Read, Write}; + let commands = [ + Read, Read, Read, Read, Read, Read, Write, Read, Read, Write, Write, Read, Read, Read, + Read, Read, Read, Read, Write, Read, + ]; + let gate = OpGate::new(); + let running = Arc::new(AtomicUsize::new(0)); + let most_running = Arc::new(AtomicUsize::new(0)); + let log = Arc::new(Mutex::new(Vec::new())); + let mut tasks = Vec::new(); + + for (id, kind) in commands.into_iter().enumerate() { + let permit = gate.acquire(kind).await; + let running = running.clone(); + let most_running = most_running.clone(); + let log = log.clone(); + tasks.push(tokio::spawn(async move { + let now = running.fetch_add(1, Ordering::SeqCst) + 1; + most_running.fetch_max(now, Ordering::SeqCst); + log.lock().push((id, true)); + tokio::time::sleep(Duration::from_millis(5)).await; + log.lock().push((id, false)); + running.fetch_sub(1, Ordering::SeqCst); + drop(permit); + })); + } + for task in tasks { + task.await.unwrap(); + } + + assert!(most_running.load(Ordering::SeqCst) <= MAX_CONCURRENT_OPS as usize); + let log = log.lock(); + let position = |id: usize, start: bool| log.iter().position(|e| *e == (id, start)).unwrap(); + for (write, kind) in commands.iter().enumerate() { + if *kind != Write { + continue; + } + for other in 0..commands.len() { + if other < write { + assert!( + position(other, false) < position(write, true), + "write {write} started before {other} finished" + ); + } else if other > write { + assert!( + position(write, false) < position(other, true), + "{other} started before write {write} finished" + ); + } + } + } + } +} diff --git a/crates/imap/src/core/mod.rs b/crates/imap/src/core/mod.rs index 7c41c02d..f2edcf73 100644 --- a/crates/imap/src/core/mod.rs +++ b/crates/imap/src/core/mod.rs @@ -29,6 +29,7 @@ use tokio::{ use trc::AddContext; pub mod client; +pub mod gate; pub mod mailbox; pub mod message; pub mod session; @@ -61,6 +62,8 @@ pub struct Session { pub in_flight: InFlight, pub remote_addr: IpAddr, pub session_id: u64, + // inbuxa: bounds and orders the operations this session runs + pub op_gate: gate::OpGate, } pub struct SessionData { diff --git a/crates/imap/src/core/session.rs b/crates/imap/src/core/session.rs index 1bfbd344..729e9295 100644 --- a/crates/imap/src/core/session.rs +++ b/crates/imap/src/core/session.rs @@ -170,6 +170,7 @@ impl Session { session_id: session.session_id, in_flight: session.in_flight, remote_addr: session.remote_ip, + op_gate: super::gate::OpGate::new(), stream_rx, stream_tx: Arc::new(tokio::sync::Mutex::new(stream_tx)), }) @@ -237,6 +238,7 @@ impl Session { session_id: self.session_id, in_flight: self.in_flight, remote_addr: self.remote_addr, + op_gate: self.op_gate, stream_rx, stream_tx, }) diff --git a/crates/imap/src/op/acl.rs b/crates/imap/src/op/acl.rs index 800a2f09..05e7cca4 100644 --- a/crates/imap/src/op/acl.rs +++ b/crates/imap/src/op/acl.rs @@ -7,7 +7,7 @@ */ use crate::{ - core::{MailboxId, Session, SessionData, State}, + core::{MailboxId, Session, SessionData, State, gate::OpKind}, op::ImapContext, spawn_op, }; @@ -40,13 +40,14 @@ impl Session { pub async fn handle_get_acl(&mut self, request: Request) -> trc::Result<()> { // Validate access self.assert_has_permission(Permission::ImapAclGet)?; + let permit = self.op_gate.acquire(OpKind::Read).await; let op_start = Instant::now(); let arguments = request.parse_acl(self.is_utf8)?; let is_utf8 = self.version.is_rev2() || self.is_utf8; let data = self.state.session_data(); - spawn_op!(data, { + spawn_op!(data, permit, { let (mailbox_id, mailbox_, _) = data .get_acl_mailbox(&arguments, true) .await @@ -169,13 +170,14 @@ impl Session { pub async fn handle_my_rights(&mut self, request: Request) -> trc::Result<()> { // Validate access self.assert_has_permission(Permission::ImapMyRights)?; + let permit = self.op_gate.acquire(OpKind::Read).await; let op_start = Instant::now(); let arguments = request.parse_acl(self.is_utf8)?; let data = self.state.session_data(); let is_utf8 = self.version.is_rev2() || self.is_utf8; - spawn_op!(data, { + spawn_op!(data, permit, { let (mailbox_id, mailbox_, access_token) = data .get_acl_mailbox(&arguments, false) .await @@ -262,13 +264,14 @@ impl Session { pub async fn handle_set_acl(&mut self, request: Request) -> trc::Result<()> { // Validate access self.assert_has_permission(Permission::ImapAclSet)?; + let permit = self.op_gate.acquire(OpKind::Write).await; let op_start = Instant::now(); let command = request.command; let arguments = request.parse_acl(self.is_utf8)?; let data = self.state.session_data(); - spawn_op!(data, { + spawn_op!(data, permit, { // Validate mailbox let (mailbox_id, current_mailbox, access_token) = data .get_acl_mailbox(&arguments, true) diff --git a/crates/imap/src/op/append.rs b/crates/imap/src/op/append.rs index f85645c3..a6cdf732 100644 --- a/crates/imap/src/op/append.rs +++ b/crates/imap/src/op/append.rs @@ -2,11 +2,13 @@ * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC * * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL + * + * Modified by Coffey Labs in 2026 for INBUXA. */ use super::{ImapContext, ToModSeq}; use crate::{ - core::{ImapUidToId, MailboxId, SelectedMailbox, Session, SessionData}, + core::{ImapUidToId, MailboxId, SelectedMailbox, Session, SessionData, gate::OpKind}, spawn_op, }; use common::{auth::BuildAccessToken, ipc::PushNotification, network::SessionStream}; @@ -29,6 +31,7 @@ impl Session { pub async fn handle_append(&mut self, request: Request) -> trc::Result<()> { // Validate access self.assert_has_permission(Permission::ImapAppend)?; + let permit = self.op_gate.acquire(OpKind::Write).await; let op_start = Instant::now(); let arguments = request.parse_append(self.is_utf8)?; @@ -67,7 +70,7 @@ impl Session { }; let is_qresync = self.is_qresync; - spawn_op!(data, { + spawn_op!(data, permit, { let response = data .append_messages(arguments, selected_mailbox, mailbox, is_qresync, op_start) .await? diff --git a/crates/imap/src/op/copy_move.rs b/crates/imap/src/op/copy_move.rs index f0eeabee..85cf92dc 100644 --- a/crates/imap/src/op/copy_move.rs +++ b/crates/imap/src/op/copy_move.rs @@ -2,11 +2,13 @@ * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC * * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL + * + * Modified by Coffey Labs in 2026 for INBUXA. */ use super::ImapContext; use crate::{ - core::{MailboxId, SelectedMailbox, Session, SessionData}, + core::{MailboxId, SelectedMailbox, Session, SessionData, gate::OpKind}, spawn_op, }; use ahash::AHashMap; @@ -58,6 +60,7 @@ impl Session { } else { Permission::ImapCopy })?; + let permit = self.op_gate.acquire(OpKind::Write).await; let op_start = Instant::now(); let arguments = request.parse_copy_move(self.is_utf8)?; @@ -70,7 +73,7 @@ impl Session { self.server.core.imap.max_messages_per_save }; - spawn_op!(data, { + spawn_op!(data, permit, { // Refresh mailboxes data.synchronize_mailboxes(false) .await diff --git a/crates/imap/src/op/create.rs b/crates/imap/src/op/create.rs index 8858168a..8f1d1f14 100644 --- a/crates/imap/src/op/create.rs +++ b/crates/imap/src/op/create.rs @@ -7,7 +7,7 @@ */ use crate::{ - core::{Session, SessionData}, + core::{Session, SessionData, gate::OpKind}, op::ImapContext, spawn_op, }; @@ -28,12 +28,13 @@ impl Session { pub async fn handle_create(&mut self, requests: Vec>) -> trc::Result<()> { // Validate access self.assert_has_permission(Permission::ImapCreate)?; + let permit = self.op_gate.acquire(OpKind::Write).await; let data = self.state.session_data(); let is_utf8 = self.is_utf8; let is_objectid = self.is_objectid; - spawn_op!(data, { + spawn_op!(data, permit, { for request in requests { match request.parse_create(is_utf8) { Ok(argument) => match data.create_folder(argument, is_objectid).await { diff --git a/crates/imap/src/op/delete.rs b/crates/imap/src/op/delete.rs index 60eb3567..3ec6230d 100644 --- a/crates/imap/src/op/delete.rs +++ b/crates/imap/src/op/delete.rs @@ -2,11 +2,13 @@ * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC * * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL + * + * Modified by Coffey Labs in 2026 for INBUXA. */ use super::ImapContext; use crate::{ - core::{Session, SessionData}, + core::{Session, SessionData, gate::OpKind}, spawn_op, }; use common::network::SessionStream; @@ -21,11 +23,12 @@ impl Session { pub async fn handle_delete(&mut self, requests: Vec>) -> trc::Result<()> { // Validate access self.assert_has_permission(Permission::ImapDelete)?; + let permit = self.op_gate.acquire(OpKind::Write).await; let data = self.state.session_data(); let is_utf8 = self.is_utf8; - spawn_op!(data, { + spawn_op!(data, permit, { for request in requests { match request.parse_delete(is_utf8) { Ok(argument) => match data.delete_folder(argument).await { diff --git a/crates/imap/src/op/fetch.rs b/crates/imap/src/op/fetch.rs index a34f3769..64475a58 100644 --- a/crates/imap/src/op/fetch.rs +++ b/crates/imap/src/op/fetch.rs @@ -8,7 +8,7 @@ use super::{FromModSeq, ImapContext}; use crate::{ - core::{SelectedMailbox, Session, SessionData}, + core::{SelectedMailbox, Session, SessionData, gate::OpKind}, spawn_op, }; use ahash::AHashMap; @@ -94,11 +94,23 @@ impl Session { } } + // inbuxa: a FETCH that marks messages \Seen changes the mailbox + let kind = if mailbox.is_select + && ops.iter().any(|op| { + op.as_ref() + .is_ok_and(|(_, _, arguments)| may_set_seen(&arguments.attributes)) + }) { + OpKind::Write + } else { + OpKind::Read + }; + let permit = self.op_gate.acquire(kind).await; + if activate_objectid && let Some(enabled) = self.activate_objectid() { self.write_bytes(enabled).await?; } - spawn_op!(data, { + spawn_op!(data, permit, { for op in ops { match op { Ok((is_uid, enabled_condstore, arguments)) => { @@ -131,6 +143,19 @@ impl Session { } } +// inbuxa: errs on the side of a write; a header-only section counts too +fn may_set_seen(attributes: &[Attribute]) -> bool { + attributes.iter().any(|attribute| { + matches!( + attribute, + Attribute::BodySection { peek: false, .. } + | Attribute::Binary { peek: false, .. } + | Attribute::Rfc822 + | Attribute::Rfc822Text + ) + }) +} + impl SessionData { #[allow(clippy::too_many_arguments)] pub async fn fetch( diff --git a/crates/imap/src/op/list.rs b/crates/imap/src/op/list.rs index 4c2274b8..8507f2ad 100644 --- a/crates/imap/src/op/list.rs +++ b/crates/imap/src/op/list.rs @@ -9,7 +9,7 @@ use std::time::Instant; use crate::{ - core::{Session, SessionData}, + core::{Session, SessionData, gate::OpKind}, spawn_op, }; use common::network::SessionStream; @@ -48,6 +48,7 @@ impl Session { request.parse_lsub(self.is_utf8) }?; + let permit = self.op_gate.acquire(OpKind::Read).await; // Activate OBJECTID+ when the OBJECTID return option or STATUS attribute is requested if let Arguments::Extended { return_options, .. } = &arguments && return_options.iter().any(|option| match option { @@ -67,6 +68,7 @@ impl Session { spawn_op!( data, + permit, data.list(arguments, is_lsub, version, is_utf8, op_start) .await ) diff --git a/crates/imap/src/op/mod.rs b/crates/imap/src/op/mod.rs index 99baf493..92b39325 100644 --- a/crates/imap/src/op/mod.rs +++ b/crates/imap/src/op/mod.rs @@ -62,10 +62,14 @@ impl ToModSeq for u64 { #[macro_export] macro_rules! spawn_op { - ($data:expr, $($code:tt)*) => { + ($data:expr, $permit:expr, $($code:tt)*) => { { + // inbuxa: the operation holds the permit the handler took from the + // session's gate until it finishes + let permit: $crate::core::gate::OpPermit = $permit; // inbuxa: ST-6: the operation keeps its read scope tokio::spawn(store::backend::scaleout::replica::carry(async move { + let _permit = permit; let data = &($data); if let Err(err) = (async { diff --git a/crates/imap/src/op/quota.rs b/crates/imap/src/op/quota.rs index 34ea9628..eca5c854 100644 --- a/crates/imap/src/op/quota.rs +++ b/crates/imap/src/op/quota.rs @@ -2,6 +2,8 @@ * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC * * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL + * + * Modified by Coffey Labs in 2026 for INBUXA. */ /* @@ -11,7 +13,7 @@ */ use crate::{ - core::{Session, SessionData}, + core::{Session, SessionData, gate::OpKind}, op::ImapContext, spawn_op, }; @@ -32,10 +34,11 @@ impl Session { pub async fn handle_get_quota(&mut self, request: Request) -> trc::Result<()> { // Validate access self.assert_has_permission(Permission::ImapStatus)?; + let permit = self.op_gate.acquire(OpKind::Read).await; let data = self.state.session_data(); - spawn_op!(data, { + spawn_op!(data, permit, { match request.parse_get_quota() { Ok(argument) => match data.get_quota(argument).await { Ok(response) => { @@ -55,11 +58,12 @@ impl Session { pub async fn handle_get_quota_root(&mut self, request: Request) -> trc::Result<()> { // Validate access self.assert_has_permission(Permission::ImapStatus)?; + let permit = self.op_gate.acquire(OpKind::Read).await; let data = self.state.session_data(); let is_utf8 = self.is_utf8; - spawn_op!(data, { + spawn_op!(data, permit, { match request.parse_get_quota_root(is_utf8) { Ok(argument) => match data.get_quota_root(argument).await { Ok(response) => { diff --git a/crates/imap/src/op/rename.rs b/crates/imap/src/op/rename.rs index 704f1000..b602f93b 100644 --- a/crates/imap/src/op/rename.rs +++ b/crates/imap/src/op/rename.rs @@ -2,10 +2,12 @@ * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC * * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL + * + * Modified by Coffey Labs in 2026 for INBUXA. */ use crate::{ - core::{Session, SessionData}, + core::{Session, SessionData, gate::OpKind}, spawn_op, }; use common::{network::SessionStream, sharing::EffectiveAcl, storage::index::ObjectIndexBuilder}; @@ -30,13 +32,14 @@ impl Session { pub async fn handle_rename(&mut self, request: Request) -> trc::Result<()> { // Validate access self.assert_has_permission(Permission::ImapRename)?; + let permit = self.op_gate.acquire(OpKind::Write).await; let op_start = Instant::now(); let arguments = request.parse_rename(self.is_utf8)?; let data = self.state.session_data(); let is_objectid = self.is_objectid; - spawn_op!(data, { + spawn_op!(data, permit, { let response = data.rename_folder(arguments, is_objectid, op_start).await?; data.write_bytes(response.into_bytes()).await }) diff --git a/crates/imap/src/op/search.rs b/crates/imap/src/op/search.rs index aa09b87e..1e58b290 100644 --- a/crates/imap/src/op/search.rs +++ b/crates/imap/src/op/search.rs @@ -8,7 +8,7 @@ use super::{FromModSeq, ToModSeq}; use crate::{ - core::{ImapId, SavedSearch, SelectedMailbox, Session, SessionData}, + core::{ImapId, SavedSearch, SelectedMailbox, Session, SessionData, gate::OpKind}, spawn_op, }; use common::network::SessionStream; @@ -58,6 +58,7 @@ impl Session { request.parse_sort() }?; + let permit = self.op_gate.acquire(OpKind::Read).await; // RFC 9586 forbids the sequence set criterion once UIDONLY is enabled if self.is_uidonly && arguments.filter.iter().any(|filter| { @@ -86,7 +87,7 @@ impl Session { (None, None) }; - spawn_op!(data, { + spawn_op!(data, permit, { let tag = std::mem::take(&mut arguments.tag); let bytes = match data .search( diff --git a/crates/imap/src/op/status.rs b/crates/imap/src/op/status.rs index a1a699fb..8a93462b 100644 --- a/crates/imap/src/op/status.rs +++ b/crates/imap/src/op/status.rs @@ -2,11 +2,13 @@ * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC * * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL + * + * Modified by Coffey Labs in 2026 for INBUXA. */ use super::ToModSeq; use crate::{ - core::{Mailbox, Session, SessionData}, + core::{Mailbox, Session, SessionData, gate::OpKind}, op::ImapContext, spawn_op, }; @@ -30,6 +32,7 @@ impl Session { pub async fn handle_status(&mut self, requests: Vec>) -> trc::Result<()> { // Validate access self.assert_has_permission(Permission::ImapStatus)?; + let permit = self.op_gate.acquire(OpKind::Read).await; let is_utf8 = self.is_utf8; @@ -53,7 +56,7 @@ impl Session { let data = self.state.session_data(); - spawn_op!(data, { + spawn_op!(data, permit, { let mut did_sync = false; for request in parsed { diff --git a/crates/imap/src/op/store.rs b/crates/imap/src/op/store.rs index 56078301..5f432d86 100644 --- a/crates/imap/src/op/store.rs +++ b/crates/imap/src/op/store.rs @@ -8,7 +8,7 @@ use super::{FromModSeq, ImapContext}; use crate::{ - core::{SelectedMailbox, Session, SessionData}, + core::{SelectedMailbox, Session, SessionData, gate::OpKind}, spawn_op, }; use ahash::AHashSet; @@ -51,6 +51,7 @@ impl Session { ) -> trc::Result<()> { // Validate access self.assert_has_permission(Permission::ImapStore)?; + let permit = self.op_gate.acquire(OpKind::Write).await; let op_start = Instant::now(); let arguments = request.parse_store()?; @@ -61,7 +62,7 @@ impl Session { let message_limit = self.server.core.imap.max_messages_per_command; if spawn { - spawn_op!(data, { + spawn_op!(data, permit, { let response = data .store( arguments, diff --git a/crates/imap/src/op/subscribe.rs b/crates/imap/src/op/subscribe.rs index 22e1df61..19ad7ca8 100644 --- a/crates/imap/src/op/subscribe.rs +++ b/crates/imap/src/op/subscribe.rs @@ -2,11 +2,13 @@ * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC * * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL + * + * Modified by Coffey Labs in 2026 for INBUXA. */ use super::ImapContext; use crate::{ - core::{Session, SessionData}, + core::{Session, SessionData, gate::OpKind}, spawn_op, }; use common::{network::SessionStream, storage::index::ObjectIndexBuilder}; @@ -27,12 +29,13 @@ impl Session { ) -> trc::Result<()> { // Validate access self.assert_has_permission(Permission::ImapSubscribe)?; + let permit = self.op_gate.acquire(OpKind::Write).await; let op_start = Instant::now(); let arguments = request.parse_subscribe(self.is_utf8)?; let data = self.state.session_data(); - spawn_op!(data, { + spawn_op!(data, permit, { let response = data .subscribe_folder( arguments.tag, diff --git a/crates/imap/src/op/thread.rs b/crates/imap/src/op/thread.rs index 94dc836d..c68a7730 100644 --- a/crates/imap/src/op/thread.rs +++ b/crates/imap/src/op/thread.rs @@ -2,10 +2,12 @@ * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC * * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL + * + * Modified by Coffey Labs in 2026 for INBUXA. */ use crate::{ - core::{SelectedMailbox, Session, SessionData}, + core::{SelectedMailbox, Session, SessionData, gate::OpKind}, spawn_op, }; use ahash::AHashMap; @@ -31,13 +33,14 @@ impl Session { ) -> trc::Result<()> { // Validate access self.assert_has_permission(Permission::ImapThread)?; + let permit = self.op_gate.acquire(OpKind::Read).await; let op_start = Instant::now(); let command = request.command; let mut arguments = request.parse_thread()?; let (data, mailbox) = self.state.mailbox_state(); - spawn_op!(data, { + spawn_op!(data, permit, { let tag = std::mem::take(&mut arguments.tag); match data.thread(arguments, mailbox, is_uid, op_start).await { diff --git a/tests/src/imap/mod.rs b/tests/src/imap/mod.rs index 1c936820..57c6405f 100644 --- a/tests/src/imap/mod.rs +++ b/tests/src/imap/mod.rs @@ -20,6 +20,7 @@ pub mod mailbox; pub mod managesieve; pub mod messagelimit; pub mod objectid; +pub mod pipelining; pub mod pop; pub mod search; pub mod store; @@ -274,6 +275,7 @@ pub async fn imap_tests() { acl::test(&mut imap, &mut imap_check, &test).await; uidbatches::test(&mut imap, &mut imap_check).await; messagelimit::test(&mut imap, &mut imap_check).await; + pipelining::test(&test).await; // UIDONLY cannot be disabled once enabled, so it uses its own connection uidonly::test(&test).await; diff --git a/tests/src/imap/pipelining.rs b/tests/src/imap/pipelining.rs new file mode 100644 index 00000000..b408286f --- /dev/null +++ b/tests/src/imap/pipelining.rs @@ -0,0 +1,173 @@ +/* + * SPDX-FileCopyrightText: 2026 Coffey Labs LLC + * + * SPDX-License-Identifier: AGPL-3.0-only + */ + +//! inbuxa: commands a client pipelines in one write run in order. A +//! command that changes state waits for the commands before it and the +//! commands after it wait for it, so they see its result. + +use crate::utils::server::TestServer; +use base64::{Engine, engine::general_purpose}; +use std::time::Duration; +use tokio::{ + io::{AsyncBufReadExt, AsyncWriteExt, BufReader, Lines}, + net::{TcpStream, tcp::OwnedReadHalf}, +}; + +const MESSAGES: usize = 8; + +pub async fn test(test: &TestServer) { + println!("Running pipelined command ordering tests..."); + + let account = test.account("jdoe@example.com"); + let (rx, mut tx) = TcpStream::connect("127.0.0.1:9991") + .await + .unwrap() + .into_split(); + let mut rx = BufReader::new(rx).lines(); + rx.next_line().await.unwrap().unwrap(); + let credentials = + general_purpose::STANDARD.encode(format!("\0{}\0{}", account.name(), account.secret())); + tx.write_all( + format!( + "a AUTHENTICATE PLAIN {{{}+}}\r\n{credentials}\r\n", + credentials.len() + ) + .as_bytes(), + ) + .await + .unwrap(); + let login = read_tags(&mut rx, &["a"]).await; + assert!( + login + .tagged + .first() + .is_some_and(|line| line.starts_with("a OK")), + "sign-in failed: {:?}", + login.lines + ); + + let mut problems = Vec::new(); + + // CREATE, then APPENDs into the new mailbox, then STATUS, in one write + let mut script = String::from("p00 CREATE \"Pipelined\"\r\n"); + let mut tags = vec!["p00".to_string()]; + for i in 1..=MESSAGES { + let message = + format!("From: pipe@example.com\r\nSubject: pipelined {i}\r\n\r\nbody {i}\r\n"); + script.push_str(&format!( + "p{i:02} APPEND \"Pipelined\" {{{}+}}\r\n{message}\r\n", + message.len() + )); + tags.push(format!("p{i:02}")); + } + let status_tag = format!("p{:02}", MESSAGES + 1); + script.push_str(&format!("{status_tag} STATUS \"Pipelined\" (MESSAGES)\r\n")); + tags.push(status_tag); + tx.write_all(script.as_bytes()).await.unwrap(); + let tags = tags.iter().map(String::as_str).collect::>(); + let result = read_tags(&mut rx, &tags).await; + result.check_in_order(&mut problems, "CREATE/APPEND/STATUS", &tags); + let expected = format!("MESSAGES {MESSAGES})"); + if !result + .lines + .iter() + .any(|line| line.starts_with("* STATUS") && line.contains(&expected)) + { + problems.push(format!( + "STATUS after the pipelined APPENDs did not report {expected}: {:?}", + result.untagged("* STATUS") + )); + } + + // STORE, then FETCH of the same messages, then DELETE, in one write. + // SELECT goes first on its own: the commands after it are checked + // against the session state as they are read. + tx.write_all(b"q1 SELECT \"Pipelined\"\r\n").await.unwrap(); + let result = read_tags(&mut rx, &["q1"]).await; + result.check_in_order(&mut problems, "SELECT", &["q1"]); + let tags = ["q2", "q3", "q4", "q5"]; + tx.write_all( + concat!( + "q2 STORE 1:* +FLAGS.SILENT (\\Flagged)\r\n", + "q3 FETCH 1:* (FLAGS)\r\n", + "q4 UNSELECT\r\n", + "q5 DELETE \"Pipelined\"\r\n", + ) + .as_bytes(), + ) + .await + .unwrap(); + let result = read_tags(&mut rx, &tags).await; + result.check_in_order(&mut problems, "STORE/FETCH/UNSELECT/DELETE", &tags); + let fetched = result.untagged("FETCH ("); + if fetched.len() != MESSAGES || fetched.iter().any(|line| !line.contains("\\Flagged")) { + problems.push(format!( + "FETCH after the pipelined STORE did not see \\Flagged on all {MESSAGES} messages: {fetched:?}" + )); + } + + tx.write_all(b"z LOGOUT\r\n").await.unwrap(); + + assert!(problems.is_empty(), "{}", problems.join("\n")); +} + +struct Responses { + lines: Vec, + tagged: Vec, +} + +impl Responses { + fn check_in_order(&self, problems: &mut Vec, what: &str, tags: &[&str]) { + let order = self + .tagged + .iter() + .map(|line| line.split_once(' ').map_or(line.as_str(), |(tag, _)| tag)) + .collect::>(); + if order != tags { + problems.push(format!( + "{what}: completions out of order: {:?}", + self.tagged + )); + } + for line in &self.tagged { + if !line.split(' ').nth(1).is_some_and(|status| status == "OK") { + problems.push(format!("{what}: command failed: {line:?}")); + } + } + } + + fn untagged(&self, needle: &str) -> Vec<&String> { + self.lines + .iter() + .filter(|line| line.starts_with("* ") && line.contains(needle)) + .collect() + } +} + +// Reads until every tag has completed, keeping the order they completed in +async fn read_tags(rx: &mut Lines>, tags: &[&str]) -> Responses { + let mut responses = Responses { + lines: Vec::new(), + tagged: Vec::new(), + }; + while responses.tagged.len() < tags.len() { + let line = match tokio::time::timeout(Duration::from_secs(15), rx.next_line()).await { + Ok(Ok(Some(line))) => line, + other => panic!( + "no response for all of {tags:?} ({other:?}); got {:?}", + responses.lines + ), + }; + if tags.iter().any(|tag| { + line.strip_prefix(tag) + .is_some_and(|rest| rest.starts_with(' ')) + }) { + responses.tagged.push(line.clone()); + } + responses.lines.push(line); + } + responses +} -- 2.54.0 From 207d999ef38ffbe05bb77cec3fb32d315ac7f06b Mon Sep 17 00:00:00 2001 From: John Coffey Date: Thu, 29 Oct 2026 01:03:08 -0700 Subject: [PATCH 03/10] Sieve: count the bytes tests scan against the CPU limit A Sieve script's CPU limit counted instructions only, and one header, address or body test can read a whole message once for every key it lists. A user's script with a few thousand keys in one body test, tested against a large message anyone can send, spent seconds of CPU on each delivery while staying far below the 5000-instruction limit; more such tests multiplied it. Tests now charge what they scan: each value compared times the number of keys, and the header block each time a header is looked up. Every 64 KiB scanned counts as one instruction. A test stops as soon as the run is over the limit, and the script fails with the same CPU limit error as for too many instructions (a deleteheader cut short deletes nothing). Under 64 KiB scanned costs nothing extra, so a usual script on a usual message runs as before; a user script's default of 5000 allows about 300 MiB scanned, a system script's 1,048,576 about 64 GiB. Test: on main one body test with 100 keys over a 4 MiB message ran in 3 instructions and a 100-pass loop over it in 503 (8,000 keys over 20 MiB: 14.2 s); both now stop with CPULimitReached (the 20 MiB case in 10 ms), and a script on a 16 KiB message fits the same limit as before. --- vendor/sieve-rs/VENDORED.md | 13 ++ vendor/sieve-rs/src/lib.rs | 1 + .../src/runtime/actions/action_editheader.rs | 12 ++ vendor/sieve-rs/src/runtime/context.rs | 150 +++++++++++++++++- .../src/runtime/tests/test_address.rs | 13 ++ .../sieve-rs/src/runtime/tests/test_body.rs | 8 + .../sieve-rs/src/runtime/tests/test_exists.rs | 8 +- .../sieve-rs/src/runtime/tests/test_header.rs | 25 ++- .../sieve-rs/src/runtime/tests/test_string.rs | 3 + 9 files changed, 229 insertions(+), 4 deletions(-) diff --git a/vendor/sieve-rs/VENDORED.md b/vendor/sieve-rs/VENDORED.md index 42c3fab5..7e5c7d60 100644 --- a/vendor/sieve-rs/VENDORED.md +++ b/vendor/sieve-rs/VENDORED.md @@ -29,6 +29,19 @@ Every changed file carries the AGPL 5(a) notice in its header. against. Every caller of `Comparator::regex` passes the cache. The `sieve_regexes_are_size_limited` and `sieve_variable_patterns_compile_once` tests in `crates/common` cover both. +- `src/runtime/context.rs`, `src/lib.rs`, `src/runtime/tests/test_header.rs`, + `test_address.rs`, `test_body.rs`, `test_string.rs`, `test_exists.rs`, + `src/runtime/actions/action_editheader.rs`: the bytes that tests scan count + against the cpu limit, one instruction for every 64 KiB + (`SCAN_BYTES_PER_INSTRUCTION`). Header, address, body and string tests and + `deleteheader` charge each value they compare times the number of keys, and + every search for a header charges the part's header block; the total is + `Context::scanned_bytes`. A test stops scanning once the run is over the + limit, and the run fails with `CPULimitReached` as it does for too many + instructions (`deleteheader` then deletes nothing). The `scan_limit_tests` + in `context.rs` cover it; they run from this directory with `cargo test + --offline --target-dir ../../target/sieve-rs-tests scan_limit` (the crate's + `test_suite` and `parse_rfc` need upstream's `tests/`, which isn't kept). - `Cargo.toml`: the example target is dropped, with the top-level `examples/` and `tests/`, which the server never builds. (`src/` keeps its own `tests` modules: the crate declares them.) diff --git a/vendor/sieve-rs/src/lib.rs b/vendor/sieve-rs/src/lib.rs index 0ad11608..97b91735 100644 --- a/vendor/sieve-rs/src/lib.rs +++ b/vendor/sieve-rs/src/lib.rs @@ -138,6 +138,7 @@ pub struct Context<'x> { pub(crate) num_instructions: usize, pub(crate) num_out_messages: usize, pub(crate) regex_cache: runtime::tests::comparator::RegexCache, + pub(crate) scanned_bytes: std::cell::Cell, } #[derive(Debug, Clone, Eq, PartialEq, Hash)] diff --git a/vendor/sieve-rs/src/runtime/actions/action_editheader.rs b/vendor/sieve-rs/src/runtime/actions/action_editheader.rs index a124cf6a..8640aff3 100644 --- a/vendor/sieve-rs/src/runtime/actions/action_editheader.rs +++ b/vendor/sieve-rs/src/runtime/actions/action_editheader.rs @@ -73,8 +73,16 @@ impl DeleteHeader { self.index, self.mime_anychild, |header, part_id, header_pos| { + if ctx.is_over_cpu_limit() { + return true; + } if !value_patterns.is_empty() { let did_match = ctx.find_header_values(header, &MimeOpts::None, |value| { + // inbuxa: charged like the header test; past the limit, + // nothing is deleted and the run stops + if ctx.charge_scan(value.len() * value_patterns.len()) { + return false; + } for (pattern_expr, pattern) in value_patterns.iter().zip(self.value_patterns.iter()) { @@ -125,6 +133,10 @@ impl DeleteHeader { }, ); + if ctx.is_over_cpu_limit() { + return; + } + if !deleted_headers.is_empty() { ctx.has_changes = true; for (part_id, header_pos) in deleted_headers.iter().rev() { diff --git a/vendor/sieve-rs/src/runtime/context.rs b/vendor/sieve-rs/src/runtime/context.rs index 64a6c4ff..56019ad0 100644 --- a/vendor/sieve-rs/src/runtime/context.rs +++ b/vendor/sieve-rs/src/runtime/context.rs @@ -23,6 +23,11 @@ use super::{ tests::{TestResult, test_envelope::parse_envelope_address}, }; +// inbuxa: tests are charged one instruction for every 64 KiB they scan (the +// bytes compared, times the keys compared against), so the cpu limit also bounds +// how much of a large message a script can read +pub(crate) const SCAN_BYTES_PER_INSTRUCTION: usize = 64 * 1024; + #[derive(Clone, Debug)] pub(crate) struct ScriptStack { pub(crate) script: Arc, @@ -73,6 +78,7 @@ impl<'x> Context<'x> { num_instructions: 0, num_out_messages: 0, regex_cache: Default::default(), + scanned_bytes: Default::default(), last_message_id: 0, main_message_id: 0, virus_status: VirusStatus::Unknown, @@ -127,7 +133,7 @@ impl<'x> Context<'x> { 'outer: loop { while let Some(instruction) = iter.next() { self.num_instructions += 1; - if self.num_instructions > self.runtime.cpu_limit { + if self.is_over_cpu_limit() { self.finish_loop(); return Some(Err(RuntimeError::CPULimitReached)); } @@ -157,6 +163,11 @@ impl<'x> Context<'x> { continue; } Instruction::Test(test) => match test.exec(self) { + // inbuxa: a test that scanned past the limit stops the run + _ if self.is_over_cpu_limit() => { + self.finish_loop(); + return Some(Err(RuntimeError::CPULimitReached)); + } TestResult::Bool(result) => { self.test_result = result; } @@ -304,7 +315,13 @@ impl<'x> Context<'x> { } } Instruction::AddHeader(add_header) => add_header.exec(self), - Instruction::DeleteHeader(delete_header) => delete_header.exec(self), + Instruction::DeleteHeader(delete_header) => { + delete_header.exec(self); + if self.is_over_cpu_limit() { + self.finish_loop(); + return Some(Err(RuntimeError::CPULimitReached)); + } + } Instruction::Set(set) => { set.exec(self); if let Some(event) = self.queued_events.next() { @@ -443,6 +460,22 @@ impl<'x> Context<'x> { } } + // inbuxa: adds `bytes` to what this run's tests have scanned; true once the + // run is over its cpu limit, so the caller can stop scanning + #[inline] + pub(crate) fn charge_scan(&self, bytes: usize) -> bool { + self.scanned_bytes + .set(self.scanned_bytes.get().saturating_add(bytes)); + self.is_over_cpu_limit() + } + + #[inline] + pub(crate) fn is_over_cpu_limit(&self) -> bool { + self.num_instructions + .saturating_add(self.scanned_bytes.get() / SCAN_BYTES_PER_INSTRUCTION) + > self.runtime.cpu_limit + } + pub(crate) fn finish_loop(&mut self) { self.script_stack.clear(); if let Some(event) = self.final_event.take() { @@ -684,6 +717,7 @@ impl<'x> Context<'x> { num_instructions: 0, num_out_messages: 0, regex_cache: Default::default(), + scanned_bytes: Default::default(), last_message_id: 0, main_message_id: 0, virus_status: VirusStatus::Unknown, @@ -691,3 +725,115 @@ impl<'x> Context<'x> { } } } + +// inbuxa: the bytes that tests scan count against the cpu limit +#[cfg(test)] +mod scan_limit_tests { + use mail_parser::MessageParser; + + use crate::{ + Compiler, Context, Input, Runtime, compiler::grammar::Capability, runtime::RuntimeError, + }; + + fn run(script: &str, raw_message: &[u8], cpu_limit: usize) -> Result { + let script = Compiler::new().compile(script.as_bytes()).unwrap(); + let runtime = Runtime::new() + .with_capability(Capability::While) + .with_capability(Capability::Expressions) + .with_cpu_limit(cpu_limit); + let mut ctx = Context::new(&runtime, MessageParser::new().parse(raw_message).unwrap()); + let mut input = Input::script("test", script); + while let Some(result) = ctx.run(input) { + result?; + input = Input::True; + } + Ok(ctx.num_instructions) + } + + fn assert_limited(result: Result) { + assert!( + matches!(result, Err(RuntimeError::CPULimitReached)), + "expected CPULimitReached, got {result:?}" + ); + } + + fn keys(n: usize) -> String { + (0..n) + .map(|i| format!("\"key-{i}\"")) + .collect::>() + .join(", ") + } + + fn large_body(size: usize) -> Vec { + let mut raw = b"From: a@example.org\r\nTo: b@example.org\r\nSubject: hi\r\n\r\n".to_vec(); + while raw.len() < size { + raw.extend_from_slice(b"lorem ipsum dolor sit amet consectetur adipiscing elit\r\n"); + } + raw + } + + #[test] + fn body_scans_count_against_cpu_limit() { + let raw = large_body(4 << 20); + + // one test, many keys: 4 MiB x 100 keys is 6400 instructions' worth + let script = format!( + "require \"body\";\nif body :raw :contains [{}] {{ stop; }}\n", + keys(100) + ); + assert_limited(run(&script, &raw, 5000)); + + // a loop over one key + let script = "require [\"body\", \"variables\", \"vnd.inbuxa.while\", \"vnd.inbuxa.expressions\"];\n\ + set \"i\" \"0\";\n\ + while \"i < 100\" {\n\ + if body :raw :contains \"not-there\" { stop; }\n\ + let \"i\" \"i + 1\";\n\ + }\n"; + assert_limited(run(script, &raw, 2000)); + } + + #[test] + fn header_scans_count_against_cpu_limit() { + let mut raw = b"From: a@example.org\r\nTo: b@example.org\r\n".to_vec(); + while raw.len() < 4 << 20 { + raw.extend_from_slice( + b"X-Filler: lorem ipsum dolor sit amet consectetur adipiscing elit\r\n", + ); + } + raw.extend_from_slice(b"Subject: hi\r\n\r\nbody\r\n"); + + let script = format!( + "if header :contains \"X-Filler\" [{}] {{ stop; }}\n", + keys(100) + ); + assert_limited(run(&script, &raw, 5000)); + + let script = format!( + "require \"variables\";\nif string :contains \"${{x}}\" [{}] {{ stop; }}\nif address :contains \"From\" [{}] {{ stop; }}\n", + keys(10), + keys(10) + ); + assert!(run(&script, &raw, 5000).is_ok()); + } + + #[test] + fn small_messages_cost_nothing_extra() { + let raw = large_body(16 << 10); + let script = "require [\"body\", \"variables\", \"editheader\"];\n\ + if header :contains [\"Subject\", \"To\"] [\"a\", \"b\", \"c\"] { set \"x\" \"1\"; }\n\ + if address :domain \"From\" \"example.org\" { set \"y\" \"1\"; }\n\ + if exists \"X-Spam\" { stop; }\n\ + if body :text :contains [\"one\", \"two\"] { stop; }\n\ + if string :is \"${x}\" \"2\" { stop; }\n\ + deleteheader :contains \"Subject\" \"zzz\";\n"; + + // the instructions alone fill the limit; the scans add nothing + let num_instructions = run(script, &raw, 5000).unwrap(); + assert_eq!( + run(script, &raw, num_instructions).ok(), + Some(num_instructions) + ); + assert_limited(run(script, &raw, num_instructions - 1)); + } +} diff --git a/vendor/sieve-rs/src/runtime/tests/test_address.rs b/vendor/sieve-rs/src/runtime/tests/test_address.rs index 13cbe48c..6396ad67 100644 --- a/vendor/sieve-rs/src/runtime/tests/test_address.rs +++ b/vendor/sieve-rs/src/runtime/tests/test_address.rs @@ -41,6 +41,9 @@ impl TestAddress { self.mime_anychild, |header, _, _| { ctx.find_addresses(header, &self.address_part, |value| { + if ctx.charge_scan(value.len() * key_list.len()) { + return true; + } for key in &key_list { if is_is { if self.comparator.is(&value, key) { @@ -62,6 +65,9 @@ impl TestAddress { self.mime_anychild, |header, _, _| { ctx.find_addresses(header, &self.address_part, |value| { + if ctx.charge_scan(value.len() * key_list.len()) { + return true; + } for key in &key_list { if self.comparator.relational(rel_match, &value, key) { return true; @@ -80,6 +86,9 @@ impl TestAddress { self.mime_anychild, |header, _, _| { ctx.find_addresses(header, &self.address_part, |value| { + if ctx.charge_scan(value.len() * key_list.len()) { + return true; + } for (pattern_expr, pattern) in key_list.iter().zip(self.key_list.iter()) { if is_matches { @@ -145,6 +154,10 @@ impl TestAddress { self.mime_anychild, |header, _, _| { ctx.find_addresses(header, &self.address_part, |value| { + // inbuxa: each value is compared with those kept so far + if ctx.charge_scan(value.len().saturating_mul(values.len() + 1)) { + return true; + } if !value.is_empty() && !values.iter().any(|v| v.eq(value)) { values.push(value.to_string()); } diff --git a/vendor/sieve-rs/src/runtime/tests/test_body.rs b/vendor/sieve-rs/src/runtime/tests/test_body.rs index 4ab03c6b..1a8b91b2 100644 --- a/vendor/sieve-rs/src/runtime/tests/test_body.rs +++ b/vendor/sieve-rs/src/runtime/tests/test_body.rs @@ -32,6 +32,10 @@ impl TestBody { ctx.message.header_raw("Subject").unwrap_or_default() }; + // inbuxa: charged like any other scan + if ctx.charge_scan(subject.len() * key_list.len()) { + return TestResult::Bool(false); + } for (key, pattern) in key_list.iter().zip(self.key_list.iter()) { let result = match &self.match_type { MatchType::Is => self.comparator.is(&subject, key), @@ -192,6 +196,10 @@ impl TestBody { return false; } }; + // inbuxa: extracting the text scans it once, and each key again + if ctx.charge_scan(text.len() * key_list.len().max(1)) { + return true; + } let mut result = false; for (key, pattern) in key_list.iter().zip(self.key_list.iter()) { diff --git a/vendor/sieve-rs/src/runtime/tests/test_exists.rs b/vendor/sieve-rs/src/runtime/tests/test_exists.rs index 2213e227..89294d54 100644 --- a/vendor/sieve-rs/src/runtime/tests/test_exists.rs +++ b/vendor/sieve-rs/src/runtime/tests/test_exists.rs @@ -2,11 +2,13 @@ * SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd * * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL + * + * Modified by Coffey Labs in 2026 for INBUXA. */ use crate::{Context, compiler::grammar::tests::test_exists::TestExists}; -use super::{TestResult, mime::SubpartIterator}; +use super::{TestResult, mime::SubpartIterator, test_header::header_bytes}; impl TestExists { pub(crate) fn exec(&self, ctx: &mut Context) -> TestResult { @@ -18,6 +20,10 @@ impl TestExists { while let Some((_, message_part)) = part_iter.next() { for (pos, header_name) in header_names.iter().enumerate() { + // inbuxa: looking for a header scans the part's headers + if !header_exists[pos] && ctx.charge_scan(header_bytes(message_part)) { + return TestResult::Bool(false); + } if !header_exists[pos] && message_part.headers.iter().any(|h| &h.name == header_name) { diff --git a/vendor/sieve-rs/src/runtime/tests/test_header.rs b/vendor/sieve-rs/src/runtime/tests/test_header.rs index 58ddd483..9febd081 100644 --- a/vendor/sieve-rs/src/runtime/tests/test_header.rs +++ b/vendor/sieve-rs/src/runtime/tests/test_header.rs @@ -6,7 +6,7 @@ * Modified by Coffey Labs in 2026 for INBUXA. */ -use mail_parser::{Header, HeaderName, HeaderValue, parsers::MessageStream}; +use mail_parser::{Header, HeaderName, HeaderValue, MessagePart, parsers::MessageStream}; use crate::{ Context, Event, @@ -40,6 +40,9 @@ impl TestHeader { self.mime_anychild, |header, _, _| { ctx.find_header_values(header, &mime_opts, |value| { + if ctx.charge_scan(value.len() * key_list.len()) { + return true; + } for key in &key_list { if is_is { if self.comparator.is(&value, key) { @@ -61,6 +64,9 @@ impl TestHeader { self.mime_anychild, |header, _, _| { ctx.find_header_values(header, &mime_opts, |value| { + if ctx.charge_scan(value.len() * key_list.len()) { + return true; + } for key in &key_list { if self.comparator.relational(rel_match, &value, key) { return true; @@ -79,6 +85,9 @@ impl TestHeader { self.mime_anychild, |header, _, _| { ctx.find_header_values(header, &mime_opts, |value| { + if ctx.charge_scan(value.len() * key_list.len()) { + return true; + } for (pattern_expr, pattern) in key_list.iter().zip(self.key_list.iter()) { if is_matches { @@ -163,6 +172,10 @@ impl TestHeader { self.mime_anychild, |header, _, _| { ctx.find_header_values(header, &mime_opts, |value| { + // inbuxa: each value is compared with those kept so far + if ctx.charge_scan(value.len().saturating_mul(values.len() + 1)) { + return true; + } if !value.is_empty() && !values.iter().any(|v| v.eq(value)) { values.push(value.to_string()); } @@ -228,6 +241,10 @@ impl Context<'_> { while let Some((part_id, message_part)) = part_iter.next() { 'outer: for header_name in header_names { + // inbuxa: looking for a header scans the part's headers + if self.charge_scan(header_bytes(message_part)) { + return true; + } match index { None => { for (pos, header) in message_part @@ -365,3 +382,9 @@ impl Context<'_> { } } } + +// inbuxa: the size of a part's header block, as it came in +pub(crate) fn header_bytes(part: &MessagePart) -> usize { + part.raw_body_offset() + .saturating_sub(part.raw_header_offset()) as usize +} diff --git a/vendor/sieve-rs/src/runtime/tests/test_string.rs b/vendor/sieve-rs/src/runtime/tests/test_string.rs index e918ed79..7bfa3af2 100644 --- a/vendor/sieve-rs/src/runtime/tests/test_string.rs +++ b/vendor/sieve-rs/src/runtime/tests/test_string.rs @@ -65,6 +65,9 @@ impl TestString { let key = ctx.eval_value(pattern); for source in &sources { if !empty_is_null || !source.is_empty() { + if ctx.charge_scan(source.len()) { + return TestResult::Bool(false); + } result = match &self.match_type { MatchType::Is => self.comparator.is(source, &key), MatchType::Contains => self.comparator.contains( -- 2.54.0 From 78da6d3526b764ea0aa2829172a2ae683a28dcd5 Mon Sep 17 00:00:00 2001 From: John Coffey Date: Thu, 29 Oct 2026 01:24:55 -0700 Subject: [PATCH 04/10] Calendars: build calcard from a vendored copy The server now builds calcard, its iCalendar and vCard library, from vendor/calcard instead of crates.io, patched in with [patch.crates-io] like sieve-rs, so it can carry fixes to it. The copy is the published 0.3.14 with the same checksum Cargo.lock had, under Apache-2.0 OR MIT; its 26 MB of test samples are left out. Nothing changes in how the server behaves: Cargo.lock only drops calcard's registry source. Test: the workspace builds, calcard's own tests pass from vendor/calcard (52; the six that read the left-out samples are skipped), and the webdav suite passes. --- Cargo.lock | 2 - Cargo.toml | 5 + vendor/calcard/.gitignore | 12 + vendor/calcard/CHANGELOG.md | 117 + vendor/calcard/Cargo.toml | 116 + vendor/calcard/LICENSES/Apache-2.0.txt | 201 ++ vendor/calcard/LICENSES/MIT.txt | 23 + vendor/calcard/README.md | 224 ++ vendor/calcard/VENDORED.md | 38 + vendor/calcard/src/common/iana.rs | 101 + vendor/calcard/src/common/mod.rs | 263 ++ vendor/calcard/src/common/parser.rs | 606 +++++ vendor/calcard/src/common/timezone.rs | 2046 ++++++++++++++ vendor/calcard/src/common/tokenizer.rs | 791 ++++++ vendor/calcard/src/common/types.rs | 451 ++++ vendor/calcard/src/common/writer.rs | 276 ++ vendor/calcard/src/datecalc/checks.rs | 20 + vendor/calcard/src/datecalc/counter_date.rs | 755 ++++++ vendor/calcard/src/datecalc/easter.rs | 52 + vendor/calcard/src/datecalc/error.rs | 176 ++ vendor/calcard/src/datecalc/filters.rs | 129 + vendor/calcard/src/datecalc/iterinfo.rs | 278 ++ vendor/calcard/src/datecalc/masks.rs | 280 ++ vendor/calcard/src/datecalc/mod.rs | 63 + vendor/calcard/src/datecalc/monthinfo.rs | 227 ++ vendor/calcard/src/datecalc/pos_list.rs | 66 + vendor/calcard/src/datecalc/rrule.rs | 267 ++ vendor/calcard/src/datecalc/rrule_iter.rs | 212 ++ vendor/calcard/src/datecalc/utils.rs | 194 ++ vendor/calcard/src/datecalc/validate.rs | 647 +++++ vendor/calcard/src/datecalc/yearinfo.rs | 202 ++ vendor/calcard/src/icalendar/builder.rs | 808 ++++++ vendor/calcard/src/icalendar/dates.rs | 644 +++++ vendor/calcard/src/icalendar/mod.rs | 902 +++++++ vendor/calcard/src/icalendar/parser.rs | 2165 +++++++++++++++ vendor/calcard/src/icalendar/rkyv_timezone.rs | 69 + vendor/calcard/src/icalendar/rkyv_types.rs | 871 ++++++ vendor/calcard/src/icalendar/rkyv_utils.rs | 433 +++ vendor/calcard/src/icalendar/rkyv_writer.rs | 519 ++++ vendor/calcard/src/icalendar/timezone.rs | 1221 +++++++++ vendor/calcard/src/icalendar/types.rs | 1458 ++++++++++ vendor/calcard/src/icalendar/utils.rs | 526 ++++ vendor/calcard/src/icalendar/writer.rs | 587 ++++ .../calcard/src/jscalendar/export/convert.rs | 2392 +++++++++++++++++ vendor/calcard/src/jscalendar/export/mod.rs | 41 + .../calcard/src/jscalendar/export/params.rs | 434 +++ vendor/calcard/src/jscalendar/export/props.rs | 414 +++ .../calcard/src/jscalendar/import/convert.rs | 2049 ++++++++++++++ vendor/calcard/src/jscalendar/import/mod.rs | 96 + .../calcard/src/jscalendar/import/params.rs | 423 +++ vendor/calcard/src/jscalendar/import/props.rs | 491 ++++ vendor/calcard/src/jscalendar/mod.rs | 799 ++++++ vendor/calcard/src/jscalendar/parser.rs | 226 ++ vendor/calcard/src/jscalendar/types.rs | 756 ++++++ .../calcard/src/jscontact/export/convert.rs | 1959 ++++++++++++++ vendor/calcard/src/jscontact/export/entry.rs | 144 + vendor/calcard/src/jscontact/export/mod.rs | 32 + vendor/calcard/src/jscontact/export/params.rs | 244 ++ vendor/calcard/src/jscontact/export/props.rs | 399 +++ .../calcard/src/jscontact/import/convert.rs | 1276 +++++++++ vendor/calcard/src/jscontact/import/entry.rs | 271 ++ vendor/calcard/src/jscontact/import/mod.rs | 170 ++ vendor/calcard/src/jscontact/import/params.rs | 280 ++ vendor/calcard/src/jscontact/import/props.rs | 655 +++++ vendor/calcard/src/jscontact/mod.rs | 707 +++++ vendor/calcard/src/jscontact/parser.rs | 150 ++ vendor/calcard/src/jscontact/types.rs | 561 ++++ vendor/calcard/src/lib.rs | 151 ++ vendor/calcard/src/vcard/builder.rs | 347 +++ vendor/calcard/src/vcard/media_type.rs | 406 +++ vendor/calcard/src/vcard/mod.rs | 452 ++++ vendor/calcard/src/vcard/parser.rs | 1341 +++++++++ vendor/calcard/src/vcard/rkyv_types.rs | 414 +++ vendor/calcard/src/vcard/rkyv_utils.rs | 216 ++ vendor/calcard/src/vcard/rkyv_writer.rs | 596 ++++ vendor/calcard/src/vcard/types.rs | 608 +++++ vendor/calcard/src/vcard/utils.rs | 424 +++ vendor/calcard/src/vcard/writer.rs | 651 +++++ 78 files changed, 39616 insertions(+), 2 deletions(-) create mode 100644 vendor/calcard/.gitignore create mode 100644 vendor/calcard/CHANGELOG.md create mode 100644 vendor/calcard/Cargo.toml create mode 100644 vendor/calcard/LICENSES/Apache-2.0.txt create mode 100644 vendor/calcard/LICENSES/MIT.txt create mode 100644 vendor/calcard/README.md create mode 100644 vendor/calcard/VENDORED.md create mode 100644 vendor/calcard/src/common/iana.rs create mode 100644 vendor/calcard/src/common/mod.rs create mode 100644 vendor/calcard/src/common/parser.rs create mode 100644 vendor/calcard/src/common/timezone.rs create mode 100644 vendor/calcard/src/common/tokenizer.rs create mode 100644 vendor/calcard/src/common/types.rs create mode 100644 vendor/calcard/src/common/writer.rs create mode 100644 vendor/calcard/src/datecalc/checks.rs create mode 100644 vendor/calcard/src/datecalc/counter_date.rs create mode 100644 vendor/calcard/src/datecalc/easter.rs create mode 100644 vendor/calcard/src/datecalc/error.rs create mode 100644 vendor/calcard/src/datecalc/filters.rs create mode 100644 vendor/calcard/src/datecalc/iterinfo.rs create mode 100644 vendor/calcard/src/datecalc/masks.rs create mode 100644 vendor/calcard/src/datecalc/mod.rs create mode 100644 vendor/calcard/src/datecalc/monthinfo.rs create mode 100644 vendor/calcard/src/datecalc/pos_list.rs create mode 100644 vendor/calcard/src/datecalc/rrule.rs create mode 100644 vendor/calcard/src/datecalc/rrule_iter.rs create mode 100644 vendor/calcard/src/datecalc/utils.rs create mode 100644 vendor/calcard/src/datecalc/validate.rs create mode 100644 vendor/calcard/src/datecalc/yearinfo.rs create mode 100644 vendor/calcard/src/icalendar/builder.rs create mode 100644 vendor/calcard/src/icalendar/dates.rs create mode 100644 vendor/calcard/src/icalendar/mod.rs create mode 100644 vendor/calcard/src/icalendar/parser.rs create mode 100644 vendor/calcard/src/icalendar/rkyv_timezone.rs create mode 100644 vendor/calcard/src/icalendar/rkyv_types.rs create mode 100644 vendor/calcard/src/icalendar/rkyv_utils.rs create mode 100644 vendor/calcard/src/icalendar/rkyv_writer.rs create mode 100644 vendor/calcard/src/icalendar/timezone.rs create mode 100644 vendor/calcard/src/icalendar/types.rs create mode 100644 vendor/calcard/src/icalendar/utils.rs create mode 100644 vendor/calcard/src/icalendar/writer.rs create mode 100644 vendor/calcard/src/jscalendar/export/convert.rs create mode 100644 vendor/calcard/src/jscalendar/export/mod.rs create mode 100644 vendor/calcard/src/jscalendar/export/params.rs create mode 100644 vendor/calcard/src/jscalendar/export/props.rs create mode 100644 vendor/calcard/src/jscalendar/import/convert.rs create mode 100644 vendor/calcard/src/jscalendar/import/mod.rs create mode 100644 vendor/calcard/src/jscalendar/import/params.rs create mode 100644 vendor/calcard/src/jscalendar/import/props.rs create mode 100644 vendor/calcard/src/jscalendar/mod.rs create mode 100644 vendor/calcard/src/jscalendar/parser.rs create mode 100644 vendor/calcard/src/jscalendar/types.rs create mode 100644 vendor/calcard/src/jscontact/export/convert.rs create mode 100644 vendor/calcard/src/jscontact/export/entry.rs create mode 100644 vendor/calcard/src/jscontact/export/mod.rs create mode 100644 vendor/calcard/src/jscontact/export/params.rs create mode 100644 vendor/calcard/src/jscontact/export/props.rs create mode 100644 vendor/calcard/src/jscontact/import/convert.rs create mode 100644 vendor/calcard/src/jscontact/import/entry.rs create mode 100644 vendor/calcard/src/jscontact/import/mod.rs create mode 100644 vendor/calcard/src/jscontact/import/params.rs create mode 100644 vendor/calcard/src/jscontact/import/props.rs create mode 100644 vendor/calcard/src/jscontact/mod.rs create mode 100644 vendor/calcard/src/jscontact/parser.rs create mode 100644 vendor/calcard/src/jscontact/types.rs create mode 100644 vendor/calcard/src/lib.rs create mode 100644 vendor/calcard/src/vcard/builder.rs create mode 100644 vendor/calcard/src/vcard/media_type.rs create mode 100644 vendor/calcard/src/vcard/mod.rs create mode 100644 vendor/calcard/src/vcard/parser.rs create mode 100644 vendor/calcard/src/vcard/rkyv_types.rs create mode 100644 vendor/calcard/src/vcard/rkyv_utils.rs create mode 100644 vendor/calcard/src/vcard/rkyv_writer.rs create mode 100644 vendor/calcard/src/vcard/types.rs create mode 100644 vendor/calcard/src/vcard/utils.rs create mode 100644 vendor/calcard/src/vcard/writer.rs diff --git a/Cargo.lock b/Cargo.lock index c7403b09..cd308dfc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1032,8 +1032,6 @@ dependencies = [ [[package]] name = "calcard" version = "0.3.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c601473ec15a875626bce73db1a1f0fc81e9c949ca25f1463b714947c0a70a1f" dependencies = [ "ahash", "chrono", diff --git a/Cargo.toml b/Cargo.toml index 99524cdd..5102a1a6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -86,12 +86,17 @@ rpath = false # vendor/sieve-rs is the published 0.7.3 with those renamed; see its # VENDORED.md. Re-vendor when the version in Cargo.lock moves. # +# inbuxa: vendor/calcard is the published 0.3.14, vendored so the server can +# carry fixes to it; see its VENDORED.md. Re-vendor when the version in +# Cargo.lock moves. +# # inbuxa: hickory and opentelemetry are upstream's patched branches, vendored # at the commits upstream pins so builds never fetch from upstream's # repositories; see vendor/hickory-dns/VENDORED.md and # vendor/opentelemetry-rust/VENDORED.md. [patch.crates-io] sieve-rs = { path = "vendor/sieve-rs" } +calcard = { path = "vendor/calcard" } hickory-proto = { path = "vendor/hickory-dns/hickory-proto" } hickory-net = { path = "vendor/hickory-dns/hickory-net" } hickory-resolver = { path = "vendor/hickory-dns/hickory-resolver" } diff --git a/vendor/calcard/.gitignore b/vendor/calcard/.gitignore new file mode 100644 index 00000000..3b747458 --- /dev/null +++ b/vendor/calcard/.gitignore @@ -0,0 +1,12 @@ +_ignore +Cargo.lock +target +web/dist +specs/ +.DS_Store +*.md +!CHANGELOG.md +!README.md + +# inbuxa: the vendoring notes are tracked +!VENDORED.md diff --git a/vendor/calcard/CHANGELOG.md b/vendor/calcard/CHANGELOG.md new file mode 100644 index 00000000..78f8e7b6 --- /dev/null +++ b/vendor/calcard/CHANGELOG.md @@ -0,0 +1,117 @@ +calcard 0.3.14 +================================ +- Updated JSCalendar conversion rules according to `draft-ietf-calext-jscalendar-icalendar-26` and `draft-ietf-calext-jscalendarbis-20`. +- Recurrence overrides converted from iCalendar no longer contain patches forbidden by `jscalendarbis` (`privacy`, `organizerCalendarAddress`, `sentBy`, `recurrenceRule`, nested `recurrenceOverrides`, ...), and such patches are ignored when converting to iCalendar; an override holding only forbidden patches is dropped, and a more restrictive `privacy` patch raises the privacy of the whole object. +- All recurrences of a calendar object now convert to a single `privacy` value, taken from the most restrictive `CLASS` among the main component, its recurrence overrides, or its recurrence instances (or among repeated `CLASS` properties in one component). +- Unknown `CLASS` values now convert to the `private` privacy, preserving the original property in `iCalendar/properties`. +- The `privacy` property now converts to the `CLASS` property of the main component and every recurrence override component, and a preserved `CLASS` with an unknown value is restored instead of `PRIVATE`, never emitting more than one `CLASS` per component. +- Recurrence override components exported from JSCalendar now keep the `ORGANIZER` property of the main component when the override's participants include the organizer. +- Bump `mail-builder` dependency to 1.0.0 and `uuid` to 1.26. + +calcard 0.3.13 +================================ +- Fix: serialized lines exceeded the 75 octet fold (#25). +- General performance improvements. + +calcard 0.3.12 +================================ +- Added `ICalendar::add_missing_timezones()`, which builds a `VTIMEZONE` component for every `TZID` parameter that has no matching definition, as required by RFC 5545, Section 3.6.5. +- Added `ICalendar::add_timezone()`, which builds a `VTIMEZONE` definition for a given time zone identifier over an explicit UTC range. + +calcard 0.3.11 +================================ +- Fix: whitespace immediately preceding a fold was discarded while unfolding, silently joining the words around it (RFC 5545, Section 3.1). +- Fix: `into_jscalendar()` panicked on `VJOURNAL`, `VFREEBUSY`, `VTIMEZONE` and unknown components, none of which have a JSCalendar type. +- Fix: `RRULE` values that could not be parsed were truncated to their first rule part instead of being preserved verbatim. +- Fix: unparseable `RRULE` values were serialized with `TEXT` escaping, turning the structural `;` and `,` separators of the `RECUR` value type into `\;` and `\,` (RFC 5545, Section 3.3.10). +- Fix: date-only vCard 3.0 `REV` values were serialized with a fabricated `T000000` time, although a `date` value is valid there (RFC 2426, Section 3.6.4). +- Fix: truncated iCalendar `DATE-TIME` and vCard `TIMESTAMP` values were not completed, so a value missing its seconds did not survive a round trip, and a value missing its minutes was silently downgraded to a `DATE`, discarding the time. +- Fix: control characters were retained in parameter values and emitted verbatim by URI parameters such as `ALTREP` and `DIR`, although `QSAFE-CHAR` does not permit them (RFC 5545, Section 3.1). +- Fix: vCard content lines lacking a value separator were parsed with no value but serialized with an empty one. +- Fix: serialized lines exceeded the 75 octet limit by one because the colon separating a property from its value was not counted. +- Fix: an empty value at a fold boundary emitted a continuation line holding nothing but the fold character. +- Fix: vCard 2.1 `BASE64` values continued on unindented lines and terminated by a blank line were truncated to their first line, discarding the rest of the encoded data. +- Fix: an `RRULE` holding an invalid rule part silently yielded a partial recurrence rule, since every part after the invalid one was skipped and the resulting rule was returned as if it had parsed cleanly. +- Expanded the iCalendar and vCard round-trip test corpus with samples collected from public test suites. + +calcard 0.3.10 +================================ +- Bump `mail-builder` dependency to 0.5. +- Use `rkyv::primitive::ArchivedU32` instead of the concrete `rkyv::rend::u32_le`. + +calcard 0.3.9 +================================ +- Updated JSCalendar conversion rules according to `draft-ietf-calext-jscalendar-icalendar-25`. +- Fix: `ORGANIZER` converted to a duplicate Participant object instead of merging with the `ATTENDEE` or `PARTICIPANT` sharing its calendar address (#24). +- Fix: the `owner` role was not set on the Participant object converted from `ORGANIZER`. +- Fix: `iCalendar` converted properties of Link and Participant objects were matched position rather than by object key when no `JSID` parameter was present. +- Fix: all-day events exported as a floating DATE-TIME plus `SHOW-WITHOUT-TIME` instead of the `DATE` value type. +- Fix: `VERSION` was missing from the generated `VCALENDAR` component, and the JSCalendar `version` property was exported as a `JSPROP` property. + +calcard 0.3.8 +================================ +- Fix: binary values without a media type serialized as `data:base64,`, which is not a valid + `data` URL (RFC 2397). +- Map the legacy vCard 3.0 `TYPE` parameter of `PHOTO`, `LOGO`, `SOUND` and `KEY` to the media + type of their binary value, so upgrading a v3.0 card to v4.0 yields `data:image/jpeg;base64,` + instead of a typeless `data` URL. +- Include `TYPE=` when exporting binary values to vCard v3.0 and below. +- Fix: archived iCalendar writer escaped `,` and `;` inside quoted URI parameter values such as + `DIR`. + +calcard 0.3.7 +================================ +- Fix: JSCalendar `rscale` not converted to the iCalendar `RSCALE` rule part. +- Fix: `mailto:` scheme not stripped from calendar addresses when uppercase or mixed case. + +calcard 0.3.6 +================================ +- Fix: `FN` property not generated for vCard when `N` property is present (#22). + +calcard 0.3.5 +================================ +- Include 'ENCODING=' when exporting vCard v3.0 and below. + +calcard 0.3.4 +================================ +- Include `CHARSET=UTF-8` when exporting vCard v3.0 and below. + +calcard 0.3.3 +================================ +- Support `STATUS:CANCELLED` mapping from `VTODO` to JSCalendar (#20). +- Fixed duration parsing for zero duration `PT0S`. + +calcard 0.3.2 +================================ +- Fixed vCard `CELL` to JSContact `mobile` mapping (#15). +- Updated JSCalendar conversion rules according to `draft-ietf-calext-jscalendar-icalendar-21`. + +calcard 0.3.1 +================================ +- Fixed jcal implementation to use lowercase property and component names. +- Updated conversion rules according to the latest JSCalendar-bis specification. + +calcard 0.3.0 +================================ +- JMAP for Calendars support. +- JMAP for Contacts support. + +calcard 0.2.0 +================================ +- JSCalendar parsing and conversion to iCalendar format. +- JSContact parsing and conversion to vCard format. +- Fix: `RRULE` with `UNTIL` dates not parsed correctly (#12). +- Fix: Support multiple periods in `FREEBUSY` component (#4). +- Fix: Incorrect `\ ` conversion (#3). + +calcard 0.1.3 +================================ +- Added some builder methods. + +calcard 0.1.1 +================================ +- Export vCard in legacy formats. + +calcard 0.1.0 +================================ +- Initial release. diff --git a/vendor/calcard/Cargo.toml b/vendor/calcard/Cargo.toml new file mode 100644 index 00000000..6b113e25 --- /dev/null +++ b/vendor/calcard/Cargo.toml @@ -0,0 +1,116 @@ +# THIS FILE IS AUTOMATICALLY GENERATED BY CARGO +# +# When uploading crates to the registry Cargo will automatically +# "normalize" Cargo.toml files for maximal compatibility +# with all versions of Cargo and also rewrite `path` dependencies +# to registry (e.g., crates.io) dependencies. +# +# If you are reading this file be aware that the original Cargo.toml +# will likely look very different (and much more reasonable). +# See Cargo.toml.orig for the original contents. + +[package] +edition = "2024" +name = "calcard" +version = "0.3.14" +authors = ["Stalwart Labs "] +build = false +autolib = false +autobins = false +autoexamples = false +autotests = false +autobenches = false +description = "iCalendar/JSCalendar and vCard/JSContact parsing, building and conversion library for Rust" +homepage = "https://github.com/stalwartlabs/calcard" +readme = "README.md" +keywords = [ + "icalendar", + "vcard", + "jscalendar", + "jscontact", + "parser", +] +categories = [ + "date-and-time", + "email", + "parser-implementations", +] +license = "Apache-2.0 OR MIT" +repository = "https://github.com/stalwartlabs/calcard" +resolver = "2" + +[features] +default = ["jmap"] +jmap = [ + "jmap-tools", + "uuid", + "serde", + "serde_json", +] +rkyv = ["dep:rkyv"] +serde = [ + "dep:serde", + "chrono/serde", +] +wasm = ["hashify/force-32bit"] + +[lib] +name = "calcard" +path = "src/lib.rs" +doctest = false + +[dependencies.ahash] +version = "0.8.11" + +[dependencies.chrono] +version = "0.4.40" + +[dependencies.chrono-tz] +version = "0.10.3" + +[dependencies.hashify] +version = "0.2.7" + +[dependencies.jmap-tools] +version = "0.1" +optional = true + +[dependencies.mail-builder] +version = "1.0.0" +default-features = false + +[dependencies.mail-parser] +version = "0.11" + +[dependencies.rkyv] +version = "0.8" +optional = true + +[dependencies.serde] +version = "1.0" +features = ["derive"] +optional = true + +[dependencies.serde_json] +version = "1.0" +optional = true + +[dependencies.uuid] +version = "1.26" +features = ["v5"] +optional = true +default-features = false + +[dev-dependencies.chrono] +version = "0.4" +features = ["serde"] + +[dev-dependencies.rkyv] +version = "0.8" + +[dev-dependencies.serde] +version = "1.0" +features = ["derive"] + +[dev-dependencies.serde_json] +version = "1.0" diff --git a/vendor/calcard/LICENSES/Apache-2.0.txt b/vendor/calcard/LICENSES/Apache-2.0.txt new file mode 100644 index 00000000..16fe87b0 --- /dev/null +++ b/vendor/calcard/LICENSES/Apache-2.0.txt @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + +Copyright [yyyy] [name of copyright owner] + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. diff --git a/vendor/calcard/LICENSES/MIT.txt b/vendor/calcard/LICENSES/MIT.txt new file mode 100644 index 00000000..31aa7938 --- /dev/null +++ b/vendor/calcard/LICENSES/MIT.txt @@ -0,0 +1,23 @@ +Permission is hereby granted, free of charge, to any +person obtaining a copy of this software and associated +documentation files (the "Software"), to deal in the +Software without restriction, including without +limitation the rights to use, copy, modify, merge, +publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software +is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice +shall be included in all copies or substantial portions +of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF +ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED +TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT +SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. diff --git a/vendor/calcard/README.md b/vendor/calcard/README.md new file mode 100644 index 00000000..275d6d0b --- /dev/null +++ b/vendor/calcard/README.md @@ -0,0 +1,224 @@ +# calcard + +[![crates.io](https://img.shields.io/crates/v/calcard)](https://crates.io/crates/calcard) +[![build](https://github.com/stalwartlabs/calcard/actions/workflows/rust.yml/badge.svg)](https://github.com/stalwartlabs/calcard/actions/workflows/rust.yml) +[![docs.rs](https://img.shields.io/docsrs/calcard)](https://docs.rs/calcard) +[![crates.io](https://img.shields.io/crates/l/calcard)](http://www.apache.org/licenses/LICENSE-2.0) + +`calcard` is a Rust crate for parsing, generating, and converting calendaring and contact data across multiple formats. It supports iCalendar and vCard, making it easy to work with `.ics` and `.vcf` files. It also fully handles JSCalendar and JSContact, the emerging standards commonly used in JMAP-based systems. In addition to parsing and serializing these formats, `calcard` provides seamless conversion between iCalendar and JSCalendar, as well as between vCard and JSContact, enabling smooth interoperability between traditional and modern calendaring and contact protocols. + +In general, this library abides by the Postel's law or [Robustness Principle](https://en.wikipedia.org/wiki/Robustness_principle) which states that an implementation must be conservative in its sending behavior and liberal in its receiving behavior. This means that `calcard` will make a best effort to parse non-conformant iCal/vCard/JSCalendar/JSContact objects as long as these do not deviate too much from the standard. + +## Features + +Comprehensive features for working with calendaring and contact data, including: + +- **iCalendar** support: Parsing and generating iCalendar data. +- **vCard** support: Parsing and generating vCard data. +- **JSCalendar** support: Parsing and generating JSCalendar data. +- **JSContact** support: Parsing and generating JSContact data. +- Seamless conversion between **iCalendar** and **JSCalendar**. +- Seamless conversion between **vCard** and **JSContact**. +- **Recurrence rules expansion**: Accurately computes and enumerates repeating events based on iCalendar and JSCalendar RRULEs. +- **IANA timezone detection**: Automatically resolves and handles custom and proprietary timezones. + +## Usage + +### Parsing an iCalendar and/or vCard stream + +You can parse vCard and iCalendar files using the stream-based parser: + +```rust +let input = "BEGIN:VCARD\nVERSION:3.0\nFN:John Doe\nEND:VCARD\n"; +let mut parser = Parser::new(&input); + +loop { + match parser.entry() { + Entry::VCard(vcard) => println!("Parsed VCard: {:?}", vcard), + Entry::ICalendar(ical) => println!("Parsed ICalendar: {:?}", ical), + Entry::InvalidLine(line) => eprintln!("Invalid line found: {}", line), + Entry::UnexpectedComponentEnd { expected, found } => { + eprintln!("Unexpected end: expected {:?}, found {:?}", expected, found); + }, + Entry::UnterminatedComponent(component) => { + eprintln!("Unterminated component: {:?}", component); + }, + Entry::Eof => { + break; + } + } +} +``` + +### Parsing iCalendar + +You can parse a single iCalendar using the `ICalendar::parse` method: + +```rust +let input = "BEGIN:VCALENDAR\nVERSION:2.0\nEND:VCALENDAR\n"; +let ical = ICalendar::parse(&input); + +println!("Parsed ICalendar: {:?}", ical); +``` + +### Parsing vCard + +You can parse a single vCard using the `VCard::parse` method: + +```rust +let input = "BEGIN:VCARD\nVERSION:3.0\nFN:John Doe\nEND:VCARD\n"; +let vcard = VCard::parse(&input); + +println!("Parsed VCard: {:?}", vcard); +``` +### Parsing JSCalendar + +You can parse a JSCalendar JSON string using the `JSCalendar::parse` method: + +```rust +let input = r#"{ + "@type": "Group", + "uid": "bf0ac22b-4989-4caf-9ebd-54301b4ee51a", + "updated": "2020-01-15T18:00:00Z", + "title": "A simple group", + "entries": [{ + "@type": "Event", + "uid": "a8df6573-0474-496d-8496-033ad45d7fea", + "updated": "2020-01-02T18:23:04Z", + "title": "Some event", + "start": "2020-01-15T13:00:00", + "timeZone": "America/New_York", + "duration": "PT1H" + }, + { + "@type": "Task", + "uid": "2a358cee-6489-4f14-a57f-c104db4dc2f2", + "updated": "2020-01-09T14:32:01Z", + "title": "Do something" + }] + }"#; + +let jscalendar = JSCalendar::::parse(input).unwrap(); +println!("Parsed JSCalendar: {}", jscalendar.to_string_pretty()); +``` + +### Parsing JSContact + +You can parse a JSContact JSON string using the `JSContact::parse` method: + +```rust +let input = r#"{ + "@type": "Card", + "version": "1.0", + "uid": "22B2C7DF-9120-4969-8460-05956FE6B065", + "kind": "individual", + "name": { + "components": [ + { "kind": "given", "value": "John" }, + { "kind": "surname", "value": "Doe" } + ], + "isOrdered": true + } + }"#; + +let jscontact = JSContact::::parse(input).unwrap(); +println!("Parsed JSContact: {}", jscontact.to_string_pretty()); +``` + +### Converting to/from iCalendar and JSCalendar + +To convert a JSCalendar to an iCalendar, use the `JSCalendar::into_icalendar` method: + +```rust +let ical = JSCalendar::::parse(input).unwrap().into_icalendar().unwrap(); +``` + +To convert an iCalendar to a JSCalendar, use the `ICalendar::into_jscalendar` method: + +```rust +let jscalendar = ICalendar::parse(&input).unwrap().into_jscalendar::().unwrap(); +``` + +### Converting to/from vCard and JSContact + +To convert a JSContact to a vCard, use the `JSContact::into_vcard` method: + +```rust +let vcard = JSContact::::parse(input).unwrap().into_vcard().unwrap(); +``` + +To convert a vCard to a JSContact, use the `VCard::into_jscontact` method: + +```rust +let jscontact = VCard::parse(&input).unwrap().into_jscontact::().unwrap(); +``` + +### Generating iCalendar and vCard + +To generate a vCard or iCalendar, simply call `.to_string()` on the parsed object: + +```rust +let vcard_string = vcard.to_string(); +println!("Generated vCard:\n{}", vcard_string); + +let ical_string = ical.to_string(); +println!("Generated ICalendar:\n{}", ical_string); +``` + +*Note: Documentation for creating VCard and ICalendar objects is coming soon.* + +### Generating JSCalendar and JSContact + +To generate a JSCalendar or JSContact, simply call `.to_string()` on the parsed object or `.to_string_pretty()` for a more human-readable format: + +```rust +let jscalendar_string = jscalendar.to_string(); +println!("Generated JSCalendar:\n{}", jscalendar_string); + +let jscontact_string = jscontact.to_string(); +println!("Generated JSContact:\n{}", jscontact_string); +``` + +*Note: Documentation for creating JSCalendar and JSContact objects is coming soon.* + + +## Testing and Fuzzing + +To run the testsuite: + +```bash + $ cargo test --all-features +``` + +or, to run the testsuite with MIRI: + +```bash + $ cargo +nightly miri test --all-features +``` + +To fuzz the library with `cargo-fuzz`: + +```bash + $ cargo +nightly fuzz run fuzz_all + $ cargo +nightly fuzz run fuzz_random + $ cargo +nightly fuzz run fuzz_structured +``` + +## License + +Licensed under either of + + * Apache License, Version 2.0 ([LICENSE-APACHE](LICENSE-APACHE) or http://www.apache.org/licenses/LICENSE-2.0) + * MIT license ([LICENSE-MIT](LICENSE-MIT) or http://opensource.org/licenses/MIT) + +at your option. + +## Funding + +Part of the development of this library was funded through the [NGI Zero Core](https://nlnet.nl/NGI0/), a fund established by [NLnet](https://nlnet.nl/) with financial support from the European Commission's programme, under the aegis of DG Communications Networks, Content and Technology under grant agreement No 101092990. + +If you find this library useful you can help by [becoming a sponsor](https://opencollective.com/stalwart). Thank you! + +## Copyright + +Copyright (C) 2020, Stalwart Labs LLC diff --git a/vendor/calcard/VENDORED.md b/vendor/calcard/VENDORED.md new file mode 100644 index 00000000..b2c80282 --- /dev/null +++ b/vendor/calcard/VENDORED.md @@ -0,0 +1,38 @@ +# calcard, vendored + +The published [calcard](https://crates.io/crates/calcard) **0.3.14**, taken +from crates.io (`.crate` sha256 +`c601473ec15a875626bce73db1a1f0fc81e9c949ca25f1463b714947c0a70a1f`, the +checksum the server's `Cargo.lock` had for it) under Apache-2.0 OR MIT. +INBUXA takes it under MIT. The root `Cargo.toml` patches it in with +`[patch.crates-io]`. + +Left out: the top-level `resources/` (26 MB of iCalendar, vCard, +JSCalendar and JSContact samples) and `.github/`, which the server never +builds, and `Cargo.lock`, `Cargo.toml.orig`, `.cargo_vcs_info.json` and +`.cargo-ok`. The crate's own tests that read `resources/` (`expand_rrule`, `parse_ical`, `parse_vcard`, +`convert_jscalendar`, `convert_jscontact` and `remove_component_ids`) can't +run here; the crate's other tests run from this directory with: + + cargo test --offline --target-dir ../../target/calcard-tests -- \ + --skip expand_rrule --skip parse_ical --skip parse_vcard \ + --skip convert_jscalendar --skip convert_jscontact \ + --skip remove_component_ids + +## Changes + +Every changed file carries a Coffey Labs notice in its header. + +- `.gitignore`: `VENDORED.md` is tracked (upstream ignores every `*.md` + but the changelog and README). + +## Updating + +When the server's `Cargo.lock` moves calcard to a new version, Cargo stops +using this copy and only warns that the patch went unused, so check +`Cargo.lock` has no `calcard` entry with a `source` line. Re-vendor the new +version from `~/.cargo/registry/src/*/calcard-*`, dropping the top-level +`resources/` and `.github/`, `Cargo.lock`, `Cargo.toml.orig`, +`.cargo_vcs_info.json` and `.cargo-ok` (`rsync --exclude /resources +--exclude /.github ...`), repeat the changes above, and update the version +here and in the root `Cargo.toml` comment. diff --git a/vendor/calcard/src/common/iana.rs b/vendor/calcard/src/common/iana.rs new file mode 100644 index 00000000..904030ec --- /dev/null +++ b/vendor/calcard/src/common/iana.rs @@ -0,0 +1,101 @@ +/* + * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC + * + * SPDX-License-Identifier: Apache-2.0 OR MIT + */ + +use std::borrow::Cow; + +use crate::common::{IanaString, IanaType}; + +impl IanaType { + pub fn iana(&self) -> Option<&I> { + match self { + IanaType::Iana(iana) => Some(iana), + _ => None, + } + } + + pub fn other(&self) -> Option<&O> { + match self { + IanaType::Other(s) => Some(s), + _ => None, + } + } + + pub fn into_iana(self) -> Option { + match self { + IanaType::Iana(iana) => Some(iana), + _ => None, + } + } + + pub fn is_iana(&self) -> bool { + matches!(self, IanaType::Iana(_)) + } + + pub fn is_iana_and(&self, f: impl FnOnce(&I) -> bool) -> bool { + match &self { + IanaType::Iana(x) => f(x), + _ => false, + } + } +} + +impl AsRef for IanaType +where + I: IanaString, + O: AsRef, +{ + fn as_ref(&self) -> &str { + match self { + IanaType::Iana(iana) => iana.as_str(), + IanaType::Other(s) => s.as_ref(), + } + } +} + +impl IanaType +where + I: IanaString, +{ + pub fn into_string(self) -> Cow<'static, str> { + match self { + IanaType::Iana(iana) => Cow::Borrowed(iana.as_str()), + IanaType::Other(s) => Cow::Owned(s), + } + } +} + +impl From for IanaType +where + I: IanaString, +{ + fn from(iana: I) -> Self { + IanaType::Iana(iana) + } +} + +impl From for IanaType { + fn from(s: String) -> Self { + IanaType::Other(s) + } +} + +impl Default for IanaType +where + I: IanaString + + std::fmt::Debug + + Clone + + PartialEq + + Eq + + PartialOrd + + Ord + + std::hash::Hash + + Default, + O: std::fmt::Debug + Clone + PartialEq + Eq + PartialOrd + Ord + std::hash::Hash, +{ + fn default() -> Self { + IanaType::Iana(I::default()) + } +} diff --git a/vendor/calcard/src/common/mod.rs b/vendor/calcard/src/common/mod.rs new file mode 100644 index 00000000..6e8a6656 --- /dev/null +++ b/vendor/calcard/src/common/mod.rs @@ -0,0 +1,263 @@ +/* + * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC + * + * SPDX-License-Identifier: Apache-2.0 OR MIT + */ + +use chrono::{FixedOffset, NaiveDate, NaiveDateTime}; +use mail_parser::DateTime; + +pub mod iana; +pub mod parser; +pub mod timezone; +pub mod tokenizer; +pub mod types; +pub mod writer; + +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum IanaType { + Iana(I), + Other(O), +} + +pub trait IanaString { + fn as_str(&self) -> &'static str; +} + +pub trait IanaParse: Sized { + fn parse(value: &[u8]) -> Option; +} + +#[derive(Debug, Clone, Default, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[cfg_attr( + any(test, feature = "serde"), + derive(serde::Serialize, serde::Deserialize) +)] +#[cfg_attr( + feature = "rkyv", + derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive) +)] +#[cfg_attr(feature = "rkyv", rkyv(compare(PartialEq), derive(Debug)))] +pub struct PartialDateTime { + pub year: Option, + pub month: Option, + pub day: Option, + pub hour: Option, + pub minute: Option, + pub second: Option, + pub tz_hour: Option, + pub tz_minute: Option, + pub tz_minus: bool, +} + +#[derive(Debug, Clone, Default, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[cfg_attr( + any(test, feature = "serde"), + derive(serde::Serialize, serde::Deserialize) +)] +#[cfg_attr(any(test, feature = "serde"), serde(tag = "type", content = "data"))] +#[cfg_attr( + feature = "rkyv", + derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive) +)] +#[cfg_attr(feature = "rkyv", rkyv(compare(PartialEq), derive(Debug)))] +pub enum CalendarScale { + #[default] + Gregorian, + Chinese, + IslamicCivil, + Hebrew, + Ethiopic, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) enum Encoding { + QuotedPrintable, + Base64, +} + +#[derive(Debug, Default, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[cfg_attr( + any(test, feature = "serde"), + derive(serde::Serialize, serde::Deserialize) +)] +#[cfg_attr( + feature = "rkyv", + derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive) +)] +#[cfg_attr(feature = "rkyv", rkyv(compare(PartialEq), derive(Debug)))] +pub struct Data { + pub content_type: Option, + pub data: Vec, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct DateTimeResult { + pub date_time: NaiveDateTime, + pub offset: Option, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[cfg_attr( + any(test, feature = "serde"), + derive(serde::Serialize, serde::Deserialize) +)] +#[cfg_attr(any(test, feature = "serde"), serde(tag = "type", content = "data"))] +#[cfg_attr( + feature = "rkyv", + derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive) +)] +#[cfg_attr(feature = "rkyv", rkyv(compare(PartialEq), derive(Debug)))] +pub enum LinkRelation { + About, + Acl, + Alternate, + Amphtml, + ApiCatalog, + Appendix, + AppleTouchIcon, + AppleTouchStartupImage, + Archives, + Author, + BlockedBy, + Bookmark, + C2paManifest, + Canonical, + Chapter, + CiteAs, + Collection, + CompressionDictionary, + Contents, + Convertedfrom, + Copyright, + CreateForm, + Current, + Deprecation, + Describedby, + Describes, + Disclosure, + DnsPrefetch, + Duplicate, + Edit, + EditForm, + EditMedia, + Enclosure, + External, + First, + Geofeed, + Glossary, + Help, + Hosts, + Hub, + IceServer, + Icon, + Index, + Intervalafter, + Intervalbefore, + Intervalcontains, + Intervaldisjoint, + Intervalduring, + Intervalequals, + Intervalfinishedby, + Intervalfinishes, + Intervalin, + Intervalmeets, + Intervalmetby, + Intervaloverlappedby, + Intervaloverlaps, + Intervalstartedby, + Intervalstarts, + Item, + Last, + LatestVersion, + License, + Linkset, + Lrdd, + Manifest, + MaskIcon, + Me, + MediaFeed, + Memento, + Micropub, + Modulepreload, + Monitor, + MonitorGroup, + Next, + NextArchive, + Nofollow, + Noopener, + Noreferrer, + Opener, + Openid2LocalId, + Openid2Provider, + Original, + P3pv1, + Payment, + Pingback, + Preconnect, + PredecessorVersion, + Prefetch, + Preload, + Prerender, + Prev, + Preview, + Previous, + PrevArchive, + PrivacyPolicy, + Profile, + Publication, + RdapActive, + RdapBottom, + RdapDown, + RdapTop, + RdapUp, + Related, + Restconf, + Replies, + Ruleinput, + Search, + Section, + Self_, + Service, + ServiceDesc, + ServiceDoc, + ServiceMeta, + SipTrunkingCapability, + Sponsored, + Start, + Status, + Stylesheet, + Subsection, + SuccessorVersion, + Sunset, + Tag, + TermsOfService, + Timegate, + Timemap, + Type, + Ugc, + Up, + VersionHistory, + Via, + Webmention, + WorkingCopy, + WorkingCopyOf, +} + +pub(crate) enum IdReference { + Value(T), + Reference(String), + Error, +} + +impl IdReference { + pub fn parse(s: &str) -> Self { + if let Some(reference) = s.strip_prefix('#') { + IdReference::Reference(reference.to_string()) + } else { + T::from_str(s) + .map(IdReference::Value) + .unwrap_or(IdReference::Error) + } + } +} diff --git a/vendor/calcard/src/common/parser.rs b/vendor/calcard/src/common/parser.rs new file mode 100644 index 00000000..9868ea79 --- /dev/null +++ b/vendor/calcard/src/common/parser.rs @@ -0,0 +1,606 @@ +/* + * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC + * + * SPDX-License-Identifier: Apache-2.0 OR MIT + */ + +use super::{Data, PartialDateTime, tokenizer::Token}; +use crate::{ + Parser, + common::{IanaParse, IanaString, IanaType}, + icalendar::Uri, +}; +use mail_parser::{ + DateTime, + decoders::{base64::base64_decode, hex::decode_hex}, +}; +use std::{borrow::Cow, iter::Peekable, slice::Iter, str::FromStr}; + +impl<'x> Parser<'x> { + pub(crate) fn raw_token(&mut self) -> Option> { + self.token_buf + .first() + .and_then(|first| { + self.input + .get(first.start..=self.token_buf.last().unwrap().end) + }) + .and_then(|v| std::str::from_utf8(v).ok()) + .map(unfold) + } + + pub(crate) fn buf_parse_many>>(&mut self) -> Vec { + self.token_buf.drain(..).map(T::from).collect() + } + + pub(crate) fn buf_parse_one>>(&mut self) -> Option { + let result = self.token_buf.pop().map(T::from); + self.token_buf.clear(); + result + } +} + +impl Token<'_> { + pub(crate) fn into_uri_bytes(self) -> std::result::Result { + match Data::try_parse(self.text.as_ref()) { + Some(data) => Ok(data), + None => Err(self.into_string()), + } + } + + pub(crate) fn into_timestamp( + self, + require_time: bool, + ) -> std::result::Result { + let mut dt = PartialDateTime::default(); + if dt.parse_timestamp(&mut self.text.iter().peekable(), require_time) { + Ok(dt) + } else { + Err(self.into_string()) + } + } + + pub(crate) fn into_offset(self) -> std::result::Result { + let mut dt = PartialDateTime::default(); + dt.parse_zone(&mut self.text.iter().peekable()); + if !dt.is_null() { + Ok(dt) + } else { + Err(self.into_string()) + } + } + + pub(crate) fn into_float(self) -> std::result::Result { + if let Ok(text) = std::str::from_utf8(self.text.as_ref()) + && let Ok(float) = text.parse::() + { + return Ok(float); + } + + Err(self.into_string()) + } + + pub(crate) fn into_integer(self) -> std::result::Result { + if let Ok(text) = std::str::from_utf8(self.text.as_ref()) + && let Ok(float) = text.parse::() + { + return Ok(float); + } + + Err(self.into_string()) + } + + pub(crate) fn into_boolean(self) -> bool { + self.text.as_ref().eq_ignore_ascii_case(b"true") + } +} + +impl Data { + pub fn try_parse(text: &[u8]) -> Option { + if text + .as_ref() + .get(0..5) + .unwrap_or_default() + .eq_ignore_ascii_case(b"data:") + { + let mut bin = Data::default(); + let text = text.as_ref().get(5..).unwrap_or_default(); + let mut offset_start = 0; + let mut is_base64 = false; + + for (idx, ch) in text.iter().enumerate() { + match ch { + b';' => { + if idx > offset_start && bin.content_type.is_none() { + bin.content_type = Some( + std::str::from_utf8(&text[offset_start..idx]) + .unwrap_or_default() + .to_string(), + ); + } + offset_start = idx + 1; + } + b',' => { + if idx != offset_start { + static B64_LEN: usize = "base64".len(); + + let text = text.get(offset_start..idx).unwrap_or_default(); + if text.len() == B64_LEN && text.eq_ignore_ascii_case(b"base64") + || text.len() == B64_LEN + 1 + && text[..B64_LEN].eq_ignore_ascii_case(b"base64") + { + is_base64 = true; + } else if bin.content_type.is_none() { + bin.content_type = + Some(std::str::from_utf8(text).unwrap_or_default().to_string()); + } + } + + offset_start = idx + 1; + break; + } + _ => {} + } + } + + let text = text.get(offset_start..).unwrap_or_default(); + if !text.is_empty() { + if is_base64 { + if let Some(bytes) = base64_decode(text) { + bin.data = bytes; + return Some(bin); + } + } else { + let (success, bytes) = decode_hex(text); + if success { + bin.data = bytes; + return Some(bin); + } + } + } + } + + None + } +} + +impl PartialDateTime { + pub fn parse_timestamp( + &mut self, + iter: &mut Peekable>, + require_time: bool, + ) -> bool { + let mut idx = 0; + for ch in iter { + match ch { + b'0'..=b'9' => { + let value = match idx { + 0..=3 => { + if let Some(value) = &mut self.year { + *value = + value.saturating_mul(10).saturating_add((ch - b'0') as u16); + } else { + self.year = Some((ch - b'0') as u16); + } + idx += 1; + continue; + } + 4..=5 => &mut self.month, + 6..=7 => &mut self.day, + 9..=10 => &mut self.hour, + 11..=12 => &mut self.minute, + 13..=14 => &mut self.second, + 16..=17 => &mut self.tz_hour, + 18..=19 => &mut self.tz_minute, + _ => return false, + }; + + if let Some(value) = value { + *value = value.saturating_mul(10).saturating_add(ch - b'0'); + } else { + *value = Some(ch - b'0'); + } + } + b'T' | b't' if idx == 8 => {} + b'+' if idx == 15 => {} + b'Z' | b'z' if idx == 15 => { + self.tz_hour = Some(0); + self.tz_minute = Some(0); + } + b'-' if idx == 15 => { + self.tz_minus = true; + } + b' ' | b'\t' | b'\r' | b'\n' => { + continue; + } + _ => break, + } + idx += 1; + } + + if self.hour.is_some() { + self.minute.get_or_insert(0); + self.second.get_or_insert(0); + } + + self.has_date() && (!require_time || self.has_time()) + } + + pub(crate) fn parse_zone(&mut self, iter: &mut Peekable>) -> bool { + self.tz_minus = match iter.peek() { + Some(b'-') => true, + Some(b'+') => false, + Some(b'Z') | Some(b'z') => { + self.tz_hour = Some(0); + self.tz_minute = Some(0); + iter.next(); + return true; + } + _ => return false, + }; + + iter.next(); + let mut idx = 0; + for ch in iter { + match ch { + b'0'..=b'9' => { + idx += 1; + let value = match idx { + 1 | 2 => &mut self.tz_hour, + 3 | 4 => &mut self.tz_minute, + _ => return false, + }; + + if let Some(value) = value { + *value = value.saturating_mul(10).saturating_add(ch - b'0'); + } else { + *value = Some(ch - b'0'); + } + } + _ => { + if !ch.is_ascii_whitespace() { + return false; + } + } + } + } + + self.tz_hour.is_some() + } + + pub fn is_null(&self) -> bool { + self.year.is_none() + && self.month.is_none() + && self.day.is_none() + && self.hour.is_none() + && self.minute.is_none() + && self.second.is_none() + && self.tz_hour.is_none() + && self.tz_minute.is_none() + } + + #[inline(always)] + pub fn has_date(&self) -> bool { + self.year.is_some() && self.month.is_some() && self.day.is_some() + } + + #[inline(always)] + pub fn has_time(&self) -> bool { + self.hour.is_some() && self.minute.is_some() + } + + #[inline(always)] + pub fn has_zone(&self) -> bool { + self.tz_hour.is_some() + } + + #[inline(always)] + pub fn has_date_and_time(&self) -> bool { + self.has_date() && self.has_time() + } + + fn to_datetime(&self) -> Option { + if self.has_date() && self.has_time() { + DateTime { + year: self.year.unwrap(), + month: self.month.unwrap(), + day: self.day.unwrap(), + hour: self.hour.unwrap(), + minute: self.minute.unwrap(), + second: self.second.unwrap_or_default(), + tz_before_gmt: self.tz_minus, + tz_hour: self.tz_hour.unwrap_or_default(), + tz_minute: self.tz_minute.unwrap_or_default(), + } + .into() + } else { + None + } + } + + pub fn to_rfc3339(&self) -> Option { + self.to_datetime().map(|dt| dt.to_rfc3339()) + } + + pub fn to_timestamp(&self) -> Option { + self.to_datetime().map(|dt| dt.to_timestamp()) + } +} + +pub(crate) fn parse_digits( + iter: &mut Peekable>, + target: &mut Option, + num: usize, + nullable: bool, +) -> bool { + let mut idx = 0; + while let Some(ch) = iter.peek() { + match ch { + b'0'..=b'9' => { + let ch = (*ch - b'0') as u16; + idx += 1; + iter.next(); + + if let Some(target) = target { + *target = target.saturating_mul(10).saturating_add(ch); + + if idx == num { + return true; + } + } else { + *target = Some(ch); + } + } + b'-' if nullable => { + idx += 1; + iter.next(); + if idx == num / 2 { + return true; + } + } + _ => { + if !ch.is_ascii_whitespace() { + return false; + } else { + iter.next(); + } + } + }; + } + + false +} + +pub(crate) fn parse_small_digits( + iter: &mut Peekable>, + target: &mut Option, + num: usize, + nullable: bool, +) -> bool { + let mut idx = 0; + while let Some(ch) = iter.peek() { + match ch { + b'0'..=b'9' => { + let ch = *ch - b'0'; + idx += 1; + iter.next(); + + if let Some(target) = target { + *target = target.saturating_mul(10).saturating_add(ch); + + if idx == num { + return true; + } + } else { + *target = Some(ch); + } + } + b'-' if nullable => { + idx += 1; + iter.next(); + if idx == num / 2 { + return true; + } + } + _ => { + if !ch.is_ascii_whitespace() { + return false; + } else { + iter.next(); + } + } + }; + } + + false +} + +pub(crate) fn unfold(text: &str) -> Cow<'_, str> { + if !text.as_bytes().contains(&b'\n') { + return Cow::Borrowed(text); + } + + let mut unfolded = String::with_capacity(text.len()); + + for (pos, chunk) in text.split('\n').enumerate() { + let chunk = if pos == 0 { + chunk + } else { + chunk.strip_prefix([' ', '\t']).unwrap_or(chunk) + }; + unfolded.push_str(chunk.strip_suffix('\r').unwrap_or(chunk)); + } + + Cow::Owned(unfolded) +} + +#[derive(Default)] +pub(crate) struct Timestamp(pub i64); + +impl FromStr for Timestamp { + type Err = (); + + fn from_str(s: &str) -> std::result::Result { + let mut dt = PartialDateTime::default(); + dt.parse_timestamp(&mut s.as_bytes().iter().peekable(), true); + dt.to_timestamp().map(Timestamp).ok_or(()) + } +} + +impl IanaParse for Timestamp { + fn parse(value: &[u8]) -> Option { + let mut dt = PartialDateTime::default(); + dt.parse_timestamp(&mut value.iter().peekable(), true); + dt.to_timestamp().map(Timestamp) + } +} + +pub(crate) struct Integer(pub i64); + +impl IanaParse for Integer { + fn parse(value: &[u8]) -> Option { + let mut num: i64 = 0; + let mut is_negative = false; + + for (pos, ch) in value.iter().enumerate() { + match ch { + b'-' if pos == 0 => { + is_negative = true; + } + b'+' if pos == 0 => {} + b'0'..=b'9' => { + num = num.saturating_mul(10).saturating_add((*ch - b'0') as i64); + } + _ => { + if !ch.is_ascii_whitespace() { + return None; + } + } + } + } + + Some(Integer(if is_negative { -num } else { num })) + } +} + +pub(crate) struct Boolean(pub bool); + +impl IanaParse for Boolean { + fn parse(value: &[u8]) -> Option { + if value.eq_ignore_ascii_case(b"true") { + Some(Boolean(true)) + } else if value.eq_ignore_ascii_case(b"false") { + Some(Boolean(false)) + } else { + None + } + } +} + +impl From> for IanaType +where + I: IanaParse, +{ + fn from(value: Token<'_>) -> Self { + I::parse(value.text.as_ref()) + .map(IanaType::Iana) + .unwrap_or_else(|| IanaType::Other(value.into_string())) + } +} + +impl From> for IanaType +where + I: IanaString + + IanaParse + + std::fmt::Debug + + Clone + + PartialEq + + Eq + + PartialOrd + + Ord + + std::hash::Hash, +{ + fn from(value: Token<'_>) -> Self { + I::parse(value.text.as_ref()) + .map(IanaType::Iana) + .unwrap_or_else(|| { + IanaType::Other( + value + .into_uri_bytes() + .map(Uri::Data) + .unwrap_or_else(Uri::Location), + ) + }) + } +} + +#[cfg(test)] +mod tests { + + use crate::common::tokenizer::StopChar; + + use super::*; + + #[test] + fn test_parse_uri() { + for (uri, expected) in [ + ( + concat!( + "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAsAAAALCAQAAAADpb+\n", + "tAAAAQklEQVQI122PQQ4AMAjCKv//Mzs4M0zmRYKkamEwWQVoRJogk4PuRoOoMC/EK8nYb+", + "l08WGvSxKlNHO5kxnp/WXrAzsSERN1N6q5AAAAAElFTkSuQmCC" + ), + Data { + content_type: Some("image/png".to_string()), + data: vec![ + 137, 80, 78, 71, 13, 10, 26, 10, 0, 0, 0, 13, 73, 72, 68, 82, 0, 0, 0, 11, + 0, 0, 0, 11, 8, 4, 0, 0, 0, 3, 165, 191, 173, 0, 0, 0, 66, 73, 68, 65, 84, + 8, 215, 109, 143, 65, 14, 0, 48, 8, 194, 42, 255, 255, 51, 59, 56, 51, 76, + 230, 69, 130, 164, 106, 97, 48, 89, 5, 104, 68, 154, 32, 147, 131, 238, 70, + 131, 168, 48, 47, 196, 43, 201, 216, 111, 233, 116, 241, 97, 175, 75, 18, + 165, 52, 115, 185, 147, 25, 233, 253, 101, 235, 3, 59, 18, 17, 19, 117, 55, + 170, 185, 0, 0, 0, 0, 73, 69, 78, 68, 174, 66, 96, 130, + ], + }, + ), + ( + concat!( + "data:text/html,%3Cbody%3E%3Cp%3EToller%20%3Cstron", + "g%20class%3D%22text-bold%22%3ETermin%3C%2Fstrong%3E%20f%C3%BCr%3C%2Fp", + "%3E%3Cblockquote%3E%3Cp%3Emal%20%3Cem%20class%3D%22text-italic%22%3Ez", + "u%22gucken%22%3C%2Fem%3E%3C%2Fp%3E%3C%2Fblockquote%3E%3Cp%3E%3Cu%20cl", + "ass%3D%22text-underline%22%3Eund%3C%2Fu%3E%20%3Cdel%3Eso%3C%2Fdel%3E%", + "3Cbr%2F%3E%3C%2Fp%3E%3C%2Fbody%3E" + ), + Data { + content_type: Some("text/html".to_string()), + data: vec![ + 60, 98, 111, 100, 121, 62, 60, 112, 62, 84, 111, 108, 108, 101, 114, 32, + 60, 115, 116, 114, 111, 110, 103, 32, 99, 108, 97, 115, 115, 61, 34, 116, + 101, 120, 116, 45, 98, 111, 108, 100, 34, 62, 84, 101, 114, 109, 105, 110, + 60, 47, 115, 116, 114, 111, 110, 103, 62, 32, 102, 195, 188, 114, 60, 47, + 112, 62, 60, 98, 108, 111, 99, 107, 113, 117, 111, 116, 101, 62, 60, 112, + 62, 109, 97, 108, 32, 60, 101, 109, 32, 99, 108, 97, 115, 115, 61, 34, 116, + 101, 120, 116, 45, 105, 116, 97, 108, 105, 99, 34, 62, 122, 117, 34, 103, + 117, 99, 107, 101, 110, 34, 60, 47, 101, 109, 62, 60, 47, 112, 62, 60, 47, + 98, 108, 111, 99, 107, 113, 117, 111, 116, 101, 62, 60, 112, 62, 60, 117, + 32, 99, 108, 97, 115, 115, 61, 34, 116, 101, 120, 116, 45, 117, 110, 100, + 101, 114, 108, 105, 110, 101, 34, 62, 117, 110, 100, 60, 47, 117, 62, 32, + 60, 100, 101, 108, 62, 115, 111, 60, 47, 100, 101, 108, 62, 60, 98, 114, + 47, 62, 60, 47, 112, 62, 60, 47, 98, 111, 100, 121, 62, + ], + }, + ), + ] { + assert_eq!( + Token { + text: uri.as_bytes().into(), + start: 0, + end: 0, + stop_char: StopChar::Lf + } + .into_uri_bytes(), + Ok(expected) + ); + } + } +} diff --git a/vendor/calcard/src/common/timezone.rs b/vendor/calcard/src/common/timezone.rs new file mode 100644 index 00000000..cf7bebca --- /dev/null +++ b/vendor/calcard/src/common/timezone.rs @@ -0,0 +1,2046 @@ +/* + * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC + * + * SPDX-License-Identifier: Apache-2.0 OR MIT + */ + +use super::DateTimeResult; +use crate::common::PartialDateTime; +use chrono::{DateTime, FixedOffset, NaiveDate, Offset, TimeZone, Utc}; +use hashify::tiny_map; +use std::{borrow::Cow, hash::Hash, str::FromStr}; + +#[derive(Clone, Copy, Default, Eq)] +pub enum Tz { + #[default] + Floating, + Fixed(FixedOffset), + Tz(chrono_tz::Tz), +} + +pub trait TzTimestamp { + fn to_naive_timestamp(&self) -> i64; +} + +impl PartialDateTime { + pub fn to_date_time_with_tz(&self, tz: Tz) -> Option> { + self.to_date_time() + .and_then(|dt| dt.to_date_time_with_tz(tz)) + } +} + +impl DateTimeResult { + pub fn tz(&self) -> Option { + self.offset.map(|offset| { + if offset.local_minus_utc() == 0 { + Tz::UTC + } else { + Tz::Fixed(offset) + } + }) + } + + pub fn to_date_time_with_tz(&self, tz: Tz) -> Option> { + if let Some(offset) = self.offset { + if offset.local_minus_utc() == 0 { + Tz::UTC.from_utc_datetime(&self.date_time).into() + } else { + Tz::Fixed(offset) + .from_local_datetime(&self.date_time) + .single() + } + } else { + tz.from_local_datetime(&self.date_time).single() + } + } +} + +impl Tz { + pub const UTC: Self = Self::Tz(chrono_tz::UTC); + + pub fn as_id(&self) -> u16 { + match self { + Tz::Tz(tz) => chrono_tz_to_id(tz), + Tz::Fixed(offset) => { + let offset = offset.local_minus_utc() / 60; + debug_assert!((-1440..=1440).contains(&offset) && offset != 0); + 0x8000 | ((offset + 1440) as u16) + } + Tz::Floating => 0x8000, + } + } + + pub fn from_id(id: u16) -> Option { + if id & 0x8000 == 0 { + id_to_chrono_tz(id).map(Self::Tz) + } else if id == 0x8000 { + Some(Self::Floating) + } else { + let offset = (id & 0x7FFF) as i16 - 1440; + debug_assert!((-1440..=1440).contains(&offset)); + FixedOffset::east_opt(offset as i32 * 60).map(Self::Fixed) + } + } + + pub fn name(&self) -> Option> { + match self { + Self::Tz(chrono_tz::Tz::UTC) => Some(Cow::Borrowed("Etc/UTC")), + Self::Tz(tz) => Some(Cow::Borrowed(tz.name())), + Self::Fixed(offset) => { + let hour = offset.local_minus_utc() / 3600; + Some(if hour != 0 { + Cow::Owned(format!( + "Etc/GMT{}{}", + if hour < 0 { "+" } else { "-" }, + hour.abs() + )) + } else { + Cow::Borrowed("Etc/UTC") + }) + } + Self::Floating => None, + } + } + + #[inline] + pub fn to_resolved(&self) -> Option { + match self { + Tz::Floating => None, + _ => Some(*self), + } + } + + #[inline] + pub fn is_floating(&self) -> bool { + matches!(self, Self::Floating) + } + + pub fn is_utc(&self) -> bool { + match self { + Self::Tz(chrono_tz::UTC | chrono_tz::Tz::Etc__GMT | chrono_tz::Tz::Etc__UTC) => true, + Self::Fixed(offset) => offset.local_minus_utc() == 0, + _ => false, + } + } + + pub fn offset_from_utc_date(&self, utc: &NaiveDate) -> i32 { + match self { + Tz::Floating => 0, + Tz::Fixed(offset) => offset.local_minus_utc(), + Tz::Tz(tz) => tz.offset_from_utc_date(utc).fix().local_minus_utc(), + } + } + + pub fn offset_parts(&self) -> PartialDateTime { + let mut seconds = self.offset_from_utc_date(&Utc::now().date_naive()); + + let tz_minus = if seconds < 0 { + seconds = -seconds; + true + } else { + false + }; + + PartialDateTime { + tz_hour: Some((seconds / 3600) as u8), + tz_minute: Some(((seconds % 3600) / 60) as u8), + tz_minus, + ..Default::default() + } + } + + pub fn from_ms_cdo_zone_id(id: &str) -> Option { + // Source https://learn.microsoft.com/en-us/previous-versions/office/developer/exchange-server-2007/aa563018(v=exchg.80) + + tiny_map!(id.as_bytes(), + "0" => chrono_tz::Tz::UTC, + "1" => chrono_tz::Tz::Europe__London, + "10" => chrono_tz::Tz::America__New_York, + "11" => chrono_tz::Tz::America__Chicago, + "12" => chrono_tz::Tz::America__Denver, + "13" => chrono_tz::Tz::America__Los_Angeles, + "14" => chrono_tz::Tz::America__Anchorage, + "15" => chrono_tz::Tz::Pacific__Honolulu, + "16" => chrono_tz::Tz::Pacific__Midway, + "17" => chrono_tz::Tz::Pacific__Auckland, + "18" => chrono_tz::Tz::Australia__Brisbane, + "19" => chrono_tz::Tz::Australia__Adelaide, + "2" => chrono_tz::Tz::Europe__Lisbon, + "20" => chrono_tz::Tz::Asia__Tokyo, + "21" => chrono_tz::Tz::Asia__Singapore, + "22" => chrono_tz::Tz::Asia__Bangkok, + "23" => chrono_tz::Tz::Asia__Calcutta, + "24" => chrono_tz::Tz::Asia__Muscat, + "25" => chrono_tz::Tz::Asia__Tehran, + "26" => chrono_tz::Tz::Asia__Baghdad, + "27" => chrono_tz::Tz::Asia__Jerusalem, + "28" => chrono_tz::Tz::America__St_Johns, + "29" => chrono_tz::Tz::Atlantic__Azores, + "3" => chrono_tz::Tz::Europe__Paris, + "30" => chrono_tz::Tz::America__Noronha, + "31" => chrono_tz::Tz::Africa__Casablanca, + "32" => chrono_tz::Tz::America__Argentina__Buenos_Aires, + "33" => chrono_tz::Tz::America__Caracas, + "34" => chrono_tz::Tz::America__Indiana__Indianapolis, + "35" => chrono_tz::Tz::America__Bogota, + "36" => chrono_tz::Tz::America__Edmonton, + "37" => chrono_tz::Tz::America__Mexico_City, + "38" => chrono_tz::Tz::America__Phoenix, + "39" => chrono_tz::Tz::Pacific__Kwajalein, + "4" => chrono_tz::Tz::Europe__Berlin, + "40" => chrono_tz::Tz::Pacific__Fiji, + "41" => chrono_tz::Tz::Asia__Magadan, + "42" => chrono_tz::Tz::Australia__Hobart, + "43" => chrono_tz::Tz::Pacific__Guam, + "44" => chrono_tz::Tz::Australia__Darwin, + "45" => chrono_tz::Tz::Asia__Shanghai, + "46" => chrono_tz::Tz::Asia__Almaty, + "47" => chrono_tz::Tz::Asia__Karachi, + "48" => chrono_tz::Tz::Asia__Kabul, + "49" => chrono_tz::Tz::Africa__Cairo, + "5" => chrono_tz::Tz::Europe__Bucharest, + "50" => chrono_tz::Tz::Africa__Harare, + "51" => chrono_tz::Tz::Europe__Moscow, + "53" => chrono_tz::Tz::Atlantic__Cape_Verde, + "54" => chrono_tz::Tz::Asia__Baku, + "55" => chrono_tz::Tz::America__Guatemala, + "56" => chrono_tz::Tz::Africa__Nairobi, + "58" => chrono_tz::Tz::Asia__Yekaterinburg, + "59" => chrono_tz::Tz::Europe__Helsinki, + "6" => chrono_tz::Tz::Europe__Prague, + "60" => chrono_tz::Tz::America__Godthab, + "61" => chrono_tz::Tz::Asia__Rangoon, + "62" => chrono_tz::Tz::Asia__Kathmandu, + "63" => chrono_tz::Tz::Asia__Irkutsk, + "64" => chrono_tz::Tz::Asia__Krasnoyarsk, + "65" => chrono_tz::Tz::America__Santiago, + "66" => chrono_tz::Tz::Asia__Colombo, + "67" => chrono_tz::Tz::Pacific__Tongatapu, + "68" => chrono_tz::Tz::Asia__Vladivostok, + "69" => chrono_tz::Tz::Africa__Luanda, + "7" => chrono_tz::Tz::Europe__Athens, + "70" => chrono_tz::Tz::Asia__Yakutsk, + "71" => chrono_tz::Tz::Asia__Dhaka, + "72" => chrono_tz::Tz::Asia__Seoul, + "73" => chrono_tz::Tz::Australia__Perth, + "74" => chrono_tz::Tz::Asia__Kuwait, + "75" => chrono_tz::Tz::Asia__Taipei, + "76" => chrono_tz::Tz::Australia__Sydney, + "8" => chrono_tz::Tz::America__Sao_Paulo, + "9" => chrono_tz::Tz::America__Halifax, + ) + .map(Self::Tz) + } +} + +impl TzTimestamp for DateTime { + fn to_naive_timestamp(&self) -> i64 { + self.naive_local().and_utc().timestamp() + } +} + +impl FromStr for Tz { + type Err = (); + + /* + Calconnect recommends ignoring VTIMEZONE and just "guess" the timezone + + See: https://standards.calconnect.org/cc/cc-r0602-2006.html + + */ + + fn from_str(s: &str) -> Result { + // First try with the chrono_tz::Tz + if let Ok(tz) = chrono_tz::Tz::from_str(s) { + return Ok(Self::Tz(tz)); + } + + // Strip common prefixes + let mut s = s.trim(); + let mut zone_offset = None; + let mut retry_chrono_tz = false; + if let Some(part) = s.strip_prefix('(') { + if let Some((zone, name)) = part.split_once(')') { + s = name.trim_start(); + zone_offset = Some(zone.trim()); + } + } else if let Some(mut name) = s.strip_prefix('/') { + /* + The presence of the SOLIDUS character as a prefix, indicates that + this "TZID" represents a unique ID in a globally defined time zone + registry (when such registry is defined). + */ + if name.as_bytes().iter().filter(|&&c| c == b'/').count() > 2 { + // Extract zones such as '/softwarestudio.org/Olson_20011030_5/America/Chicago' + if let Some(new_name) = name.splitn(3, '/').nth(2) { + name = new_name.strip_prefix("SystemV/").unwrap_or(new_name); + } + } + retry_chrono_tz = true; + s = name; + } + + // Try again with chrono_tz::Tz + if retry_chrono_tz && let Ok(tz) = chrono_tz::Tz::from_str(s) { + return Ok(Self::Tz(tz)); + } + + // Map proprietary timezones to chrono_tz::Tz + let result = hashify::map!(s.as_bytes(), chrono_tz::Tz, + "AUS Central Standard Time" => chrono_tz::Tz::Australia__Darwin, + "AUS Central" => chrono_tz::Tz::Australia__Darwin, + "AUS Eastern Standard Time" => chrono_tz::Tz::Australia__Sydney, + "AUS Eastern" => chrono_tz::Tz::Australia__Sydney, + "Abu Dhabi, Muscat" => chrono_tz::Tz::Asia__Muscat, + "Adelaide, Central Australia" => chrono_tz::Tz::Australia__Adelaide, + "Afghanistan Standard Time" => chrono_tz::Tz::Asia__Kabul, + "Afghanistan" => chrono_tz::Tz::Asia__Kabul, + "Alaska" => chrono_tz::Tz::America__Anchorage, + "Alaskan Standard Time" => chrono_tz::Tz::America__Anchorage, + "Alaskan" => chrono_tz::Tz::America__Anchorage, + "Aleutian Standard Time" => chrono_tz::Tz::America__Adak, + "Almaty, Novosibirsk, North Central Asia" => chrono_tz::Tz::Asia__Almaty, + "Altai Standard Time" => chrono_tz::Tz::Asia__Barnaul, + "Amsterdam, Berlin, Bern, Rom, Stockholm, Wien" => chrono_tz::Tz::Europe__Berlin, + "Amsterdam, Berlin, Bern, Rome, Stockholm, Vienna" => chrono_tz::Tz::Europe__Berlin, + "Arab Standard Time" => chrono_tz::Tz::Asia__Riyadh, + "Arab" => chrono_tz::Tz::Asia__Kuwait, + "Arab, Kuwait, Riyadh" => chrono_tz::Tz::Asia__Kuwait, + "Arabian Standard Time" => chrono_tz::Tz::Asia__Dubai, + "Arabian" => chrono_tz::Tz::Asia__Muscat, + "Arabic Standard Time" => chrono_tz::Tz::Asia__Baghdad, + "Arabic" => chrono_tz::Tz::Asia__Baghdad, + "Argentina Standard Time" => chrono_tz::Tz::America__Buenos_Aires, + "Argentina" => chrono_tz::Tz::America__Argentina__Buenos_Aires, + "Arizona" => chrono_tz::Tz::America__Phoenix, + "Armenian" => chrono_tz::Tz::Asia__Yerevan, + "Astana, Dhaka" => chrono_tz::Tz::Asia__Dhaka, + "Astrakhan Standard Time" => chrono_tz::Tz::Europe__Astrakhan, + "Athens, Istanbul, Minsk" => chrono_tz::Tz::Europe__Athens, + "Atlantic Standard Time" => chrono_tz::Tz::America__Halifax, + "Atlantic Time (Canada)" => chrono_tz::Tz::America__Halifax, + "Atlantic" => chrono_tz::Tz::America__Halifax, + "Auckland, Wellington" => chrono_tz::Tz::Pacific__Auckland, + "Aus Central W. Standard Time" => chrono_tz::Tz::Australia__Eucla, + "Azerbaijan Standard Time" => chrono_tz::Tz::Asia__Baku, + "Azerbijan" => chrono_tz::Tz::Asia__Baku, + "Azores Standard Time" => chrono_tz::Tz::Atlantic__Azores, + "Azores" => chrono_tz::Tz::Atlantic__Azores, + "Baghdad" => chrono_tz::Tz::Asia__Baghdad, + "Bahia Standard Time" => chrono_tz::Tz::America__Bahia, + "Baku, Tbilisi, Yerevan" => chrono_tz::Tz::Asia__Baku, + "Bangkok, Hanoi, Jakarta" => chrono_tz::Tz::Asia__Bangkok, + "Bangladesh Standard Time" => chrono_tz::Tz::Asia__Dhaka, + "Beijing, Chongqing, Hong Kong SAR, Urumqi" => chrono_tz::Tz::Asia__Shanghai, + "Belarus Standard Time" => chrono_tz::Tz::Europe__Minsk, + "Belgrade, Pozsony, Budapest, Ljubljana, Prague" => chrono_tz::Tz::Europe__Prague, + "Bogota, Lima, Quito" => chrono_tz::Tz::America__Bogota, + "Bougainville Standard Time" => chrono_tz::Tz::Pacific__Bougainville, + "Brasilia" => chrono_tz::Tz::America__Sao_Paulo, + "Brisbane, East Australia" => chrono_tz::Tz::Australia__Brisbane, + "Brussels, Copenhagen, Madrid, Paris" => chrono_tz::Tz::Europe__Paris, + "Bucharest" => chrono_tz::Tz::Europe__Bucharest, + "Buenos Aires" => chrono_tz::Tz::America__Argentina__Buenos_Aires, + "Cairo" => chrono_tz::Tz::Africa__Cairo, + "Canada Central Standard Time" => chrono_tz::Tz::America__Regina, + "Canada Central" => chrono_tz::Tz::America__Edmonton, + "Canberra, Melbourne, Sydney, Hobart" => chrono_tz::Tz::Australia__Sydney, + "Canberra, Melbourne, Sydney" => chrono_tz::Tz::Australia__Sydney, + "Cape Verde Is." => chrono_tz::Tz::Atlantic__Cape_Verde, + "Cape Verde Standard Time" => chrono_tz::Tz::Atlantic__Cape_Verde, + "Cape Verde" => chrono_tz::Tz::Atlantic__Cape_Verde, + "Caracas, La Paz" => chrono_tz::Tz::America__Caracas, + "Casablanca, Monrovia" => chrono_tz::Tz::Africa__Casablanca, + "Caucasus Standard Time" => chrono_tz::Tz::Asia__Yerevan, + "Caucasus" => chrono_tz::Tz::Asia__Yerevan, + "Cen. Australia Standard Time" => chrono_tz::Tz::Australia__Adelaide, + "Cen. Australia" => chrono_tz::Tz::Australia__Adelaide, + "Central America Standard Time" => chrono_tz::Tz::America__Guatemala, + "Central America" => chrono_tz::Tz::America__Guatemala, + "Central Asia Standard Time" => chrono_tz::Tz::Asia__Almaty, + "Central Asia" => chrono_tz::Tz::Asia__Dhaka, + "Central Brazilian Standard Time" => chrono_tz::Tz::America__Cuiaba, + "Central Brazilian" => chrono_tz::Tz::America__Manaus, + "Central Europe Standard Time" => chrono_tz::Tz::Europe__Budapest, + "Central Europe" => chrono_tz::Tz::Europe__Prague, + "Central European Standard Time" => chrono_tz::Tz::Europe__Warsaw, + "Central European" => chrono_tz::Tz::Europe__Sarajevo, + "Central Pacific Standard Time" => chrono_tz::Tz::Pacific__Guadalcanal, + "Central Pacific" => chrono_tz::Tz::Asia__Magadan, + "Central Standard Time (Mexico)" => chrono_tz::Tz::America__Mexico_City, + "Central Standard Time" => chrono_tz::Tz::America__Chicago, + "Central Time (US & Canada)" => chrono_tz::Tz::America__Chicago, + "Central Time (US and Canada)" => chrono_tz::Tz::America__Chicago, + "Central" => chrono_tz::Tz::America__Chicago, + "Chatham Islands Standard Time" => chrono_tz::Tz::Pacific__Chatham, + "China Standard Time" => chrono_tz::Tz::Asia__Shanghai, + "China" => chrono_tz::Tz::Asia__Shanghai, + "Cuba Standard Time" => chrono_tz::Tz::America__Havana, + "Darwin" => chrono_tz::Tz::Australia__Darwin, + "Dateline Standard Time" => chrono_tz::Tz::Etc__GMTPlus12, + "Dateline" => chrono_tz::Tz::Etc__GMTMinus12, + "E. Africa Standard Time" => chrono_tz::Tz::Africa__Nairobi, + "E. Africa" => chrono_tz::Tz::Africa__Nairobi, + "E. Australia Standard Time" => chrono_tz::Tz::Australia__Brisbane, + "E. Australia" => chrono_tz::Tz::Australia__Brisbane, + "E. Europe Standard Time" => chrono_tz::Tz::Europe__Chisinau, + "E. Europe" => chrono_tz::Tz::Europe__Minsk, + "E. South America Standard Time" => chrono_tz::Tz::America__Sao_Paulo, + "E. South America" => chrono_tz::Tz::America__Belem, + "East Africa, Nairobi" => chrono_tz::Tz::Africa__Nairobi, + "Easter Island Standard Time" => chrono_tz::Tz::Pacific__Easter, + "Eastern Standard Time (Mexico)" => chrono_tz::Tz::America__Cancun, + "Eastern Standard Time" => chrono_tz::Tz::America__New_York, + "Eastern Time (US & Canada)" => chrono_tz::Tz::America__New_York, + "Eastern Time (US and Canada)" => chrono_tz::Tz::America__New_York, + "Eastern" => chrono_tz::Tz::America__New_York, + "Egypt Standard Time" => chrono_tz::Tz::Africa__Cairo, + "Egypt" => chrono_tz::Tz::Africa__Cairo, + "Ekaterinburg Standard Time" => chrono_tz::Tz::Asia__Yekaterinburg, + "Ekaterinburg" => chrono_tz::Tz::Asia__Yekaterinburg, + "Eniwetok, Kwajalein, Dateline Time" => chrono_tz::Tz::Pacific__Kwajalein, + "FLE Standard Time" => chrono_tz::Tz::Europe__Kiev, + "FLE" => chrono_tz::Tz::Europe__Helsinki, + "Fiji Islands, Kamchatka, Marshall Is." => chrono_tz::Tz::Pacific__Fiji, + "Fiji Standard Time" => chrono_tz::Tz::Pacific__Fiji, + "Fiji" => chrono_tz::Tz::Pacific__Fiji, + "GMT Standard Time" => chrono_tz::Tz::Europe__London, + "GTB Standard Time" => chrono_tz::Tz::Europe__Bucharest, + "GTB" => chrono_tz::Tz::Europe__Athens, + "Georgian Standard Time" => chrono_tz::Tz::Asia__Tbilisi, + "Georgian" => chrono_tz::Tz::Asia__Tbilisi, + "Greenland Standard Time" => chrono_tz::Tz::America__Godthab, + "Greenland" => chrono_tz::Tz::America__Godthab, + "Greenwich Mean Time: Dublin, Edinburgh, Lisbon, London" => chrono_tz::Tz::Europe__Lisbon, + "Greenwich Mean Time; Dublin, Edinburgh, London" => chrono_tz::Tz::Europe__London, + "Greenwich Standard Time" => chrono_tz::Tz::Atlantic__Reykjavik, + "Greenwich" => chrono_tz::Tz::Atlantic__Reykjavik, + "Guam, Port Moresby" => chrono_tz::Tz::Pacific__Guam, + "Haiti Standard Time" => chrono_tz::Tz::America__PortauPrince, + "Harare, Pretoria" => chrono_tz::Tz::Africa__Harare, + "Hawaii" => chrono_tz::Tz::Pacific__Honolulu, + "Hawaiian Standard Time" => chrono_tz::Tz::Pacific__Honolulu, + "Hawaiian" => chrono_tz::Tz::Pacific__Honolulu, + "Helsinki, Riga, Tallinn" => chrono_tz::Tz::Europe__Helsinki, + "Hobart, Tasmania" => chrono_tz::Tz::Australia__Hobart, + "India Standard Time" => chrono_tz::Tz::Asia__Calcutta, + "India" => chrono_tz::Tz::Asia__Calcutta, + "Indiana (East)" => chrono_tz::Tz::America__Indiana__Indianapolis, + "Iran Standard Time" => chrono_tz::Tz::Asia__Tehran, + "Iran" => chrono_tz::Tz::Asia__Tehran, + "Irkutsk, Ulaan Bataar" => chrono_tz::Tz::Asia__Irkutsk, + "Islamabad, Karachi, Tashkent" => chrono_tz::Tz::Asia__Karachi, + "Israel Standard Time" => chrono_tz::Tz::Asia__Jerusalem, + "Israel" => chrono_tz::Tz::Asia__Jerusalem, + "Israel, Jerusalem Standard Time" => chrono_tz::Tz::Asia__Jerusalem, + "Jordan Standard Time" => chrono_tz::Tz::Asia__Amman, + "Jordan" => chrono_tz::Tz::Asia__Amman, + "Kabul" => chrono_tz::Tz::Asia__Kabul, + "Kaliningrad Standard Time" => chrono_tz::Tz::Europe__Kaliningrad, + "Kathmandu, Nepal" => chrono_tz::Tz::Asia__Kathmandu, + "Kolkata, Chennai, Mumbai, New Delhi, India Standard Time" => chrono_tz::Tz::Asia__Calcutta, + "Korea Standard Time" => chrono_tz::Tz::Asia__Seoul, + "Korea" => chrono_tz::Tz::Asia__Seoul, + "Krasnoyarsk" => chrono_tz::Tz::Asia__Krasnoyarsk, + "Kuala Lumpur, Singapore" => chrono_tz::Tz::Asia__Singapore, + "Libya Standard Time" => chrono_tz::Tz::Africa__Tripoli, + "Line Islands Standard Time" => chrono_tz::Tz::Pacific__Kiritimati, + "Lord Howe Standard Time" => chrono_tz::Tz::Australia__Lord_Howe, + "Magadan Standard Time" => chrono_tz::Tz::Asia__Magadan, + "Magadan, Solomon Is., New Caledonia" => chrono_tz::Tz::Asia__Magadan, + "Magallanes Standard Time" => chrono_tz::Tz::America__Punta_Arenas, + "Marquesas Standard Time" => chrono_tz::Tz::Pacific__Marquesas, + "Mauritius Standard Time" => chrono_tz::Tz::Indian__Mauritius, + "Mauritius" => chrono_tz::Tz::Indian__Mauritius, + "Mexico City, Tegucigalpa" => chrono_tz::Tz::America__Mexico_City, + "Mexico Standard Time 2" => chrono_tz::Tz::America__Chihuahua, + "Mexico" => chrono_tz::Tz::America__Mexico_City, + "Mid-Atlantic" => chrono_tz::Tz::America__Noronha, + "Middle East Standard Time" => chrono_tz::Tz::Asia__Beirut, + "Middle East" => chrono_tz::Tz::Asia__Beirut, + "Midway Island, Samoa" => chrono_tz::Tz::Pacific__Midway, + "Montevideo Standard Time" => chrono_tz::Tz::America__Montevideo, + "Montevideo" => chrono_tz::Tz::America__Montevideo, + "Morocco Standard Time" => chrono_tz::Tz::Africa__Casablanca, + "Morocco" => chrono_tz::Tz::Africa__Casablanca, + "Moscow, St. Petersburg, Volgograd" => chrono_tz::Tz::Europe__Moscow, + "Mountain Standard Time (Mexico)" => chrono_tz::Tz::America__Chihuahua, + "Mountain Standard Time" => chrono_tz::Tz::America__Denver, + "Mountain Time (US & Canada)" => chrono_tz::Tz::America__Denver, + "Mountain Time (US and Canada)" => chrono_tz::Tz::America__Denver, + "Mountain" => chrono_tz::Tz::America__Denver, + "Myanmar Standard Time" => chrono_tz::Tz::Asia__Rangoon, + "Myanmar" => chrono_tz::Tz::Asia__Rangoon, + "N. Central Asia Standard Time" => chrono_tz::Tz::Asia__Novosibirsk, + "N. Central Asia" => chrono_tz::Tz::Asia__Almaty, + "Namibia Standard Time" => chrono_tz::Tz::Africa__Windhoek, + "Namibia" => chrono_tz::Tz::Africa__Windhoek, + "Nepal Standard Time" => chrono_tz::Tz::Asia__Katmandu, + "Nepal" => chrono_tz::Tz::Asia__Kathmandu, + "New Zealand Standard Time" => chrono_tz::Tz::Pacific__Auckland, + "New Zealand" => chrono_tz::Tz::Pacific__Auckland, + "Newfoundland Standard Time" => chrono_tz::Tz::America__St_Johns, + "Newfoundland" => chrono_tz::Tz::America__St_Johns, + "Norfolk Standard Time" => chrono_tz::Tz::Pacific__Norfolk, + "North Asia East Standard Time" => chrono_tz::Tz::Asia__Irkutsk, + "North Asia East" => chrono_tz::Tz::Asia__Irkutsk, + "North Asia Standard Time" => chrono_tz::Tz::Asia__Krasnoyarsk, + "North Asia" => chrono_tz::Tz::Asia__Krasnoyarsk, + "North Korea Standard Time" => chrono_tz::Tz::Asia__Pyongyang, + "Omsk Standard Time" => chrono_tz::Tz::Asia__Omsk, + "Osaka, Sapporo, Tokyo" => chrono_tz::Tz::Asia__Tokyo, + "Japan" => chrono_tz::Tz::Asia__Tokyo, + "Pacific SA Standard Time" => chrono_tz::Tz::America__Santiago, + "Pacific SA" => chrono_tz::Tz::America__Santiago, + "Pacific Standard Time (Mexico)" => chrono_tz::Tz::America__Tijuana, + "Pacific Standard Time" => chrono_tz::Tz::America__Los_Angeles, + "Pacific Time (US & Canada)" => chrono_tz::Tz::America__Los_Angeles, + "Pacific Time (US & Canada); Tijuana" => chrono_tz::Tz::America__Los_Angeles, + "Pacific Time (US and Canada)" => chrono_tz::Tz::America__Los_Angeles, + "Pacific Time (US and Canada); Tijuana" => chrono_tz::Tz::America__Los_Angeles, + "Pacific" => chrono_tz::Tz::America__Los_Angeles, + "Pakistan Standard Time" => chrono_tz::Tz::Asia__Karachi, + "Pakistan" => chrono_tz::Tz::Asia__Karachi, + "Paraguay Standard Time" => chrono_tz::Tz::America__Asuncion, + "Paris, Madrid, Brussels, Copenhagen" => chrono_tz::Tz::Europe__Paris, + "Perth, Western Australia" => chrono_tz::Tz::Australia__Perth, + "Prague, Central Europe" => chrono_tz::Tz::Europe__Prague, + "Qyzylorda Standard Time" => chrono_tz::Tz::Asia__Qyzylorda, + "Rangoon" => chrono_tz::Tz::Asia__Rangoon, + "Romance Standard Time" => chrono_tz::Tz::Europe__Paris, + "Romance" => chrono_tz::Tz::Europe__Paris, + "Russia Time Zone 10" => chrono_tz::Tz::Asia__Srednekolymsk, + "Russia Time Zone 11" => chrono_tz::Tz::Asia__Kamchatka, + "Russia Time Zone 3" => chrono_tz::Tz::Europe__Samara, + "Russian Standard Time" => chrono_tz::Tz::Europe__Moscow, + "Russian" => chrono_tz::Tz::Europe__Moscow, + "SA Eastern Standard Time" => chrono_tz::Tz::America__Cayenne, + "SA Eastern" => chrono_tz::Tz::America__Belem, + "SA Pacific Standard Time" => chrono_tz::Tz::America__Bogota, + "SA Pacific" => chrono_tz::Tz::America__Bogota, + "SA Western Standard Time" => chrono_tz::Tz::America__La_Paz, + "SA Western" => chrono_tz::Tz::America__La_Paz, + "SE Asia Standard Time" => chrono_tz::Tz::Asia__Bangkok, + "SE Asia" => chrono_tz::Tz::Asia__Bangkok, + "Saint Pierre Standard Time" => chrono_tz::Tz::America__Miquelon, + "Sakhalin Standard Time" => chrono_tz::Tz::Asia__Sakhalin, + "Samoa Standard Time" => chrono_tz::Tz::Pacific__Apia, + "Samoa" => chrono_tz::Tz::Pacific__Apia, + "Santiago" => chrono_tz::Tz::America__Santiago, + "Sao Tome Standard Time" => chrono_tz::Tz::Africa__Sao_Tome, + "Sarajevo, Skopje, Sofija, Vilnius, Warsaw, Zagreb" => chrono_tz::Tz::Europe__Sarajevo, + "Saratov Standard Time" => chrono_tz::Tz::Europe__Saratov, + "Saskatchewan" => chrono_tz::Tz::America__Edmonton, + "Seoul, Korea Standard time" => chrono_tz::Tz::Asia__Seoul, + "Singapore Standard Time" => chrono_tz::Tz::Asia__Singapore, + "Singapore" => chrono_tz::Tz::Asia__Singapore, + "South Africa Standard Time" => chrono_tz::Tz::Africa__Johannesburg, + "South Africa" => chrono_tz::Tz::Africa__Harare, + "Sri Jayawardenepura, Sri Lanka" => chrono_tz::Tz::Asia__Colombo, + "Sri Lanka Standard Time" => chrono_tz::Tz::Asia__Colombo, + "Sri Lanka" => chrono_tz::Tz::Asia__Colombo, + "Sudan Standard Time" => chrono_tz::Tz::Africa__Khartoum, + "Syria Standard Time" => chrono_tz::Tz::Asia__Damascus, + "Taipei Standard Time" => chrono_tz::Tz::Asia__Taipei, + "Taipei" => chrono_tz::Tz::Asia__Taipei, + "Tasmania Standard Time" => chrono_tz::Tz::Australia__Hobart, + "Tasmania" => chrono_tz::Tz::Australia__Hobart, + "Tehran" => chrono_tz::Tz::Asia__Tehran, + "Tocantins Standard Time" => chrono_tz::Tz::America__Araguaina, + "Tokyo Standard Time" => chrono_tz::Tz::Asia__Tokyo, + "Tokyo" => chrono_tz::Tz::Asia__Tokyo, + "Tomsk Standard Time" => chrono_tz::Tz::Asia__Tomsk, + "Tonga Standard Time" => chrono_tz::Tz::Pacific__Tongatapu, + "Tonga" => chrono_tz::Tz::Pacific__Tongatapu, + "Transbaikal Standard Time" => chrono_tz::Tz::Asia__Chita, + "Turkey Standard Time" => chrono_tz::Tz::Europe__Istanbul, + "Turks And Caicos Standard Time" => chrono_tz::Tz::America__Grand_Turk, + "US Eastern Standard Time" => chrono_tz::Tz::America__Indianapolis, + "US Eastern" => chrono_tz::Tz::America__Indiana__Indianapolis, + "US Mountain Standard Time" => chrono_tz::Tz::America__Phoenix, + "US Mountain" => chrono_tz::Tz::America__Phoenix, + "UTC" => chrono_tz::Tz::Etc__GMT, + "UTC+12" => chrono_tz::Tz::Etc__GMTMinus12, + "UTC+13" => chrono_tz::Tz::Etc__GMTMinus13, + "UTC-02" => chrono_tz::Tz::Etc__GMTPlus2, + "UTC-08" => chrono_tz::Tz::Etc__GMTPlus8, + "UTC-09" => chrono_tz::Tz::Etc__GMTPlus9, + "UTC-11" => chrono_tz::Tz::Etc__GMTPlus11, + "Ulaanbaatar Standard Time" => chrono_tz::Tz::Asia__Ulaanbaatar, + "Universal Coordinated Time" => chrono_tz::Tz::UTC, + "Venezuela Standard Time" => chrono_tz::Tz::America__Caracas, + "Venezuela" => chrono_tz::Tz::America__Caracas, + "Vladivostok Standard Time" => chrono_tz::Tz::Asia__Vladivostok, + "Vladivostok" => chrono_tz::Tz::Asia__Vladivostok, + "Volgograd Standard Time" => chrono_tz::Tz::Europe__Volgograd, + "W. Australia Standard Time" => chrono_tz::Tz::Australia__Perth, + "W. Australia" => chrono_tz::Tz::Australia__Perth, + "W. Central Africa Standard Time" => chrono_tz::Tz::Africa__Lagos, + "W. Central Africa" => chrono_tz::Tz::Africa__Lagos, + "W. Europe Standard Time" => chrono_tz::Tz::Europe__Berlin, + "W. Europe" => chrono_tz::Tz::Europe__Amsterdam, + "W. Mongolia Standard Time" => chrono_tz::Tz::Asia__Hovd, + "West Asia Standard Time" => chrono_tz::Tz::Asia__Tashkent, + "West Asia" => chrono_tz::Tz::Asia__Tashkent, + "West Bank Standard Time" => chrono_tz::Tz::Asia__Hebron, + "West Central Africa" => chrono_tz::Tz::Africa__Luanda, + "West Pacific Standard Time" => chrono_tz::Tz::Pacific__Port_Moresby, + "West Pacific" => chrono_tz::Tz::Pacific__Guam, + "Yakutsk Standard Time" => chrono_tz::Tz::Asia__Yakutsk, + "Yakutsk" => chrono_tz::Tz::Asia__Yakutsk, + "Yukon Standard Time" => chrono_tz::Tz::America__Whitehorse, + "Nuku'alofa, Tonga" => chrono_tz::Tz::Pacific__Tongatapu, + ); + + if let Some(tz) = result { + return Ok(Self::Tz(*tz)); + } else if let Some(zone_offset) = zone_offset { + let (zone, sign, time) = if let Some((zone, part)) = zone_offset.split_once('+') { + (zone.trim(), '-', part.trim()) + } else if let Some((zone, part)) = zone_offset.split_once('-') { + (zone.trim(), '+', part.trim()) + } else { + return Err(()); + }; + if !zone.eq_ignore_ascii_case("UTC") && !zone.eq_ignore_ascii_case("GMT") { + return Err(()); + } + let mut zone = String::with_capacity(10); + zone.push_str("Etc/GMT"); + zone.push(sign); + + for (pos, ch) in time.chars().enumerate() { + if !ch.is_ascii_digit() { + break; + } else if ch != '0' || pos > 0 { + zone.push(ch); + } + } + + if let Ok(tz) = chrono_tz::Tz::from_str(&zone) { + return Ok(Self::Tz(tz)); + } + } + Err(()) + } +} + +impl PartialEq for Tz { + fn eq(&self, other: &Self) -> bool { + match (self, other) { + (Self::Floating, Self::Floating) => true, + (Self::Tz(l0), Self::Tz(r0)) => l0 == r0, + (Self::Fixed(l0), Self::Fixed(r0)) => l0 == r0, + _ => false, + } + } +} + +impl From for Tz { + fn from(_tz: Utc) -> Self { + Self::Tz(chrono_tz::UTC) + } +} + +impl From for Tz { + fn from(tz: chrono_tz::Tz) -> Self { + Self::Tz(tz) + } +} + +impl PartialOrd for Tz { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl Ord for Tz { + fn cmp(&self, other: &Self) -> std::cmp::Ordering { + self.as_id().cmp(&other.as_id()) + } +} + +impl Hash for Tz { + fn hash(&self, state: &mut H) { + self.as_id().hash(state); + } +} + +impl std::fmt::Debug for Tz { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Floating => write!(f, "Floating"), + Self::Tz(tz) => tz.fmt(f), + Self::Fixed(fixed_offset) => fixed_offset.fmt(f), + } + } +} + +impl std::fmt::Display for Tz { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Floating => write!(f, "Floating"), + Self::Tz(tz) => tz.fmt(f), + Self::Fixed(fixed_offset) => fixed_offset.fmt(f), + } + } +} + +#[derive(Clone, Copy)] +pub enum RRuleOffset { + Fixed(FixedOffset), + Tz(::Offset), + Floating, +} + +impl std::fmt::Debug for RRuleOffset { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Fixed(offset) => offset.fmt(f), + Self::Tz(offset) => offset.fmt(f), + Self::Floating => write!(f, "Floating"), + } + } +} + +impl std::fmt::Display for RRuleOffset { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Fixed(offset) => offset.fmt(f), + Self::Tz(offset) => offset.fmt(f), + Self::Floating => write!(f, "Floating"), + } + } +} + +impl Offset for RRuleOffset { + fn fix(&self) -> FixedOffset { + match self { + Self::Fixed(tz) => tz.fix(), + Self::Tz(tz) => tz.fix(), + Self::Floating => FixedOffset::east_opt(0).unwrap(), + } + } +} + +impl TimeZone for Tz { + type Offset = RRuleOffset; + + fn from_offset(offset: &Self::Offset) -> Self { + match offset { + RRuleOffset::Tz(offset) => Self::Tz(chrono_tz::Tz::from_offset(offset)), + RRuleOffset::Fixed(fixed_offset) => Self::Fixed(*fixed_offset), + RRuleOffset::Floating => Self::Floating, + } + } + + #[allow(deprecated)] + fn offset_from_local_date( + &self, + local: &chrono::NaiveDate, + ) -> chrono::LocalResult { + match self { + Self::Fixed(tz) => tz + .from_local_date(local) + .map(|date| RRuleOffset::Fixed(*date.offset())), + Self::Tz(tz) => tz + .from_local_date(local) + .map(|date| RRuleOffset::Tz(*date.offset())), + Self::Floating => chrono::LocalResult::Single(RRuleOffset::Floating), + } + } + + fn offset_from_local_datetime( + &self, + local: &chrono::NaiveDateTime, + ) -> chrono::LocalResult { + match self { + Self::Fixed(tz) => tz + .from_local_datetime(local) + .map(|date| RRuleOffset::Fixed(*date.offset())), + Self::Tz(tz) => tz + .from_local_datetime(local) + .map(|date| RRuleOffset::Tz(*date.offset())), + Self::Floating => chrono::LocalResult::Single(RRuleOffset::Floating), + } + } + + #[allow(deprecated)] + fn offset_from_utc_date(&self, utc: &chrono::NaiveDate) -> Self::Offset { + match self { + Self::Fixed(tz) => RRuleOffset::Fixed(*tz.from_utc_date(utc).offset()), + Self::Tz(tz) => RRuleOffset::Tz(*tz.from_utc_date(utc).offset()), + Self::Floating => RRuleOffset::Floating, + } + } + + fn offset_from_utc_datetime(&self, utc: &chrono::NaiveDateTime) -> Self::Offset { + match self { + Self::Fixed(tz) => RRuleOffset::Fixed(*tz.from_utc_datetime(utc).offset()), + Self::Tz(tz) => RRuleOffset::Tz(*tz.from_utc_datetime(utc).offset()), + Self::Floating => RRuleOffset::Floating, + } + } +} + +fn chrono_tz_to_id(tz: &chrono_tz::Tz) -> u16 { + match tz { + chrono_tz::Tz::Africa__Abidjan => 0, + chrono_tz::Tz::Africa__Accra => 1, + chrono_tz::Tz::Africa__Addis_Ababa => 2, + chrono_tz::Tz::Africa__Algiers => 3, + chrono_tz::Tz::Africa__Asmara => 4, + chrono_tz::Tz::Africa__Asmera => 5, + chrono_tz::Tz::Africa__Bamako => 6, + chrono_tz::Tz::Africa__Bangui => 7, + chrono_tz::Tz::Africa__Banjul => 8, + chrono_tz::Tz::Africa__Bissau => 9, + chrono_tz::Tz::Africa__Blantyre => 10, + chrono_tz::Tz::Africa__Brazzaville => 11, + chrono_tz::Tz::Africa__Bujumbura => 12, + chrono_tz::Tz::Africa__Cairo => 13, + chrono_tz::Tz::Africa__Casablanca => 14, + chrono_tz::Tz::Africa__Ceuta => 15, + chrono_tz::Tz::Africa__Conakry => 16, + chrono_tz::Tz::Africa__Dakar => 17, + chrono_tz::Tz::Africa__Dar_es_Salaam => 18, + chrono_tz::Tz::Africa__Djibouti => 19, + chrono_tz::Tz::Africa__Douala => 20, + chrono_tz::Tz::Africa__El_Aaiun => 21, + chrono_tz::Tz::Africa__Freetown => 22, + chrono_tz::Tz::Africa__Gaborone => 23, + chrono_tz::Tz::Africa__Harare => 24, + chrono_tz::Tz::Africa__Johannesburg => 25, + chrono_tz::Tz::Africa__Juba => 26, + chrono_tz::Tz::Africa__Kampala => 27, + chrono_tz::Tz::Africa__Khartoum => 28, + chrono_tz::Tz::Africa__Kigali => 29, + chrono_tz::Tz::Africa__Kinshasa => 30, + chrono_tz::Tz::Africa__Lagos => 31, + chrono_tz::Tz::Africa__Libreville => 32, + chrono_tz::Tz::Africa__Lome => 33, + chrono_tz::Tz::Africa__Luanda => 34, + chrono_tz::Tz::Africa__Lubumbashi => 35, + chrono_tz::Tz::Africa__Lusaka => 36, + chrono_tz::Tz::Africa__Malabo => 37, + chrono_tz::Tz::Africa__Maputo => 38, + chrono_tz::Tz::Africa__Maseru => 39, + chrono_tz::Tz::Africa__Mbabane => 40, + chrono_tz::Tz::Africa__Mogadishu => 41, + chrono_tz::Tz::Africa__Monrovia => 42, + chrono_tz::Tz::Africa__Nairobi => 43, + chrono_tz::Tz::Africa__Ndjamena => 44, + chrono_tz::Tz::Africa__Niamey => 45, + chrono_tz::Tz::Africa__Nouakchott => 46, + chrono_tz::Tz::Africa__Ouagadougou => 47, + chrono_tz::Tz::Africa__PortoNovo => 48, + chrono_tz::Tz::Africa__Sao_Tome => 49, + chrono_tz::Tz::Africa__Timbuktu => 50, + chrono_tz::Tz::Africa__Tripoli => 51, + chrono_tz::Tz::Africa__Tunis => 52, + chrono_tz::Tz::Africa__Windhoek => 53, + chrono_tz::Tz::America__Adak => 54, + chrono_tz::Tz::America__Anchorage => 55, + chrono_tz::Tz::America__Anguilla => 56, + chrono_tz::Tz::America__Antigua => 57, + chrono_tz::Tz::America__Araguaina => 58, + chrono_tz::Tz::America__Argentina__Buenos_Aires => 59, + chrono_tz::Tz::America__Argentina__Catamarca => 60, + chrono_tz::Tz::America__Argentina__ComodRivadavia => 61, + chrono_tz::Tz::America__Argentina__Cordoba => 62, + chrono_tz::Tz::America__Argentina__Jujuy => 63, + chrono_tz::Tz::America__Argentina__La_Rioja => 64, + chrono_tz::Tz::America__Argentina__Mendoza => 65, + chrono_tz::Tz::America__Argentina__Rio_Gallegos => 66, + chrono_tz::Tz::America__Argentina__Salta => 67, + chrono_tz::Tz::America__Argentina__San_Juan => 68, + chrono_tz::Tz::America__Argentina__San_Luis => 69, + chrono_tz::Tz::America__Argentina__Tucuman => 70, + chrono_tz::Tz::America__Argentina__Ushuaia => 71, + chrono_tz::Tz::America__Aruba => 72, + chrono_tz::Tz::America__Asuncion => 73, + chrono_tz::Tz::America__Atikokan => 74, + chrono_tz::Tz::America__Atka => 75, + chrono_tz::Tz::America__Bahia => 76, + chrono_tz::Tz::America__Bahia_Banderas => 77, + chrono_tz::Tz::America__Barbados => 78, + chrono_tz::Tz::America__Belem => 79, + chrono_tz::Tz::America__Belize => 80, + chrono_tz::Tz::America__BlancSablon => 81, + chrono_tz::Tz::America__Boa_Vista => 82, + chrono_tz::Tz::America__Bogota => 83, + chrono_tz::Tz::America__Boise => 84, + chrono_tz::Tz::America__Buenos_Aires => 85, + chrono_tz::Tz::America__Cambridge_Bay => 86, + chrono_tz::Tz::America__Campo_Grande => 87, + chrono_tz::Tz::America__Cancun => 88, + chrono_tz::Tz::America__Caracas => 89, + chrono_tz::Tz::America__Catamarca => 90, + chrono_tz::Tz::America__Cayenne => 91, + chrono_tz::Tz::America__Cayman => 92, + chrono_tz::Tz::America__Chicago => 93, + chrono_tz::Tz::America__Chihuahua => 94, + chrono_tz::Tz::America__Ciudad_Juarez => 95, + chrono_tz::Tz::America__Coral_Harbour => 96, + chrono_tz::Tz::America__Cordoba => 97, + chrono_tz::Tz::America__Costa_Rica => 98, + chrono_tz::Tz::America__Coyhaique => 99, + chrono_tz::Tz::America__Creston => 100, + chrono_tz::Tz::America__Cuiaba => 101, + chrono_tz::Tz::America__Curacao => 102, + chrono_tz::Tz::America__Danmarkshavn => 103, + chrono_tz::Tz::America__Dawson => 104, + chrono_tz::Tz::America__Dawson_Creek => 105, + chrono_tz::Tz::America__Denver => 106, + chrono_tz::Tz::America__Detroit => 107, + chrono_tz::Tz::America__Dominica => 108, + chrono_tz::Tz::America__Edmonton => 109, + chrono_tz::Tz::America__Eirunepe => 110, + chrono_tz::Tz::America__El_Salvador => 111, + chrono_tz::Tz::America__Ensenada => 112, + chrono_tz::Tz::America__Fort_Nelson => 113, + chrono_tz::Tz::America__Fort_Wayne => 114, + chrono_tz::Tz::America__Fortaleza => 115, + chrono_tz::Tz::America__Glace_Bay => 116, + chrono_tz::Tz::America__Godthab => 117, + chrono_tz::Tz::America__Goose_Bay => 118, + chrono_tz::Tz::America__Grand_Turk => 119, + chrono_tz::Tz::America__Grenada => 120, + chrono_tz::Tz::America__Guadeloupe => 121, + chrono_tz::Tz::America__Guatemala => 122, + chrono_tz::Tz::America__Guayaquil => 123, + chrono_tz::Tz::America__Guyana => 124, + chrono_tz::Tz::America__Halifax => 125, + chrono_tz::Tz::America__Havana => 126, + chrono_tz::Tz::America__Hermosillo => 127, + chrono_tz::Tz::America__Indiana__Indianapolis => 128, + chrono_tz::Tz::America__Indiana__Knox => 129, + chrono_tz::Tz::America__Indiana__Marengo => 130, + chrono_tz::Tz::America__Indiana__Petersburg => 131, + chrono_tz::Tz::America__Indiana__Tell_City => 132, + chrono_tz::Tz::America__Indiana__Vevay => 133, + chrono_tz::Tz::America__Indiana__Vincennes => 134, + chrono_tz::Tz::America__Indiana__Winamac => 135, + chrono_tz::Tz::America__Indianapolis => 136, + chrono_tz::Tz::America__Inuvik => 137, + chrono_tz::Tz::America__Iqaluit => 138, + chrono_tz::Tz::America__Jamaica => 139, + chrono_tz::Tz::America__Jujuy => 140, + chrono_tz::Tz::America__Juneau => 141, + chrono_tz::Tz::America__Kentucky__Louisville => 142, + chrono_tz::Tz::America__Kentucky__Monticello => 143, + chrono_tz::Tz::America__Knox_IN => 144, + chrono_tz::Tz::America__Kralendijk => 145, + chrono_tz::Tz::America__La_Paz => 146, + chrono_tz::Tz::America__Lima => 147, + chrono_tz::Tz::America__Los_Angeles => 148, + chrono_tz::Tz::America__Louisville => 149, + chrono_tz::Tz::America__Lower_Princes => 150, + chrono_tz::Tz::America__Maceio => 151, + chrono_tz::Tz::America__Managua => 152, + chrono_tz::Tz::America__Manaus => 153, + chrono_tz::Tz::America__Marigot => 154, + chrono_tz::Tz::America__Martinique => 155, + chrono_tz::Tz::America__Matamoros => 156, + chrono_tz::Tz::America__Mazatlan => 157, + chrono_tz::Tz::America__Mendoza => 158, + chrono_tz::Tz::America__Menominee => 159, + chrono_tz::Tz::America__Merida => 160, + chrono_tz::Tz::America__Metlakatla => 161, + chrono_tz::Tz::America__Mexico_City => 162, + chrono_tz::Tz::America__Miquelon => 163, + chrono_tz::Tz::America__Moncton => 164, + chrono_tz::Tz::America__Monterrey => 165, + chrono_tz::Tz::America__Montevideo => 166, + chrono_tz::Tz::America__Montreal => 167, + chrono_tz::Tz::America__Montserrat => 168, + chrono_tz::Tz::America__Nassau => 169, + chrono_tz::Tz::America__New_York => 170, + chrono_tz::Tz::America__Nipigon => 171, + chrono_tz::Tz::America__Nome => 172, + chrono_tz::Tz::America__Noronha => 173, + chrono_tz::Tz::America__North_Dakota__Beulah => 174, + chrono_tz::Tz::America__North_Dakota__Center => 175, + chrono_tz::Tz::America__North_Dakota__New_Salem => 176, + chrono_tz::Tz::America__Nuuk => 177, + chrono_tz::Tz::America__Ojinaga => 178, + chrono_tz::Tz::America__Panama => 179, + chrono_tz::Tz::America__Pangnirtung => 180, + chrono_tz::Tz::America__Paramaribo => 181, + chrono_tz::Tz::America__Phoenix => 182, + chrono_tz::Tz::America__PortauPrince => 183, + chrono_tz::Tz::America__Port_of_Spain => 184, + chrono_tz::Tz::America__Porto_Acre => 185, + chrono_tz::Tz::America__Porto_Velho => 186, + chrono_tz::Tz::America__Puerto_Rico => 187, + chrono_tz::Tz::America__Punta_Arenas => 188, + chrono_tz::Tz::America__Rainy_River => 189, + chrono_tz::Tz::America__Rankin_Inlet => 190, + chrono_tz::Tz::America__Recife => 191, + chrono_tz::Tz::America__Regina => 192, + chrono_tz::Tz::America__Resolute => 193, + chrono_tz::Tz::America__Rio_Branco => 194, + chrono_tz::Tz::America__Rosario => 195, + chrono_tz::Tz::America__Santa_Isabel => 196, + chrono_tz::Tz::America__Santarem => 197, + chrono_tz::Tz::America__Santiago => 198, + chrono_tz::Tz::America__Santo_Domingo => 199, + chrono_tz::Tz::America__Sao_Paulo => 200, + chrono_tz::Tz::America__Scoresbysund => 201, + chrono_tz::Tz::America__Shiprock => 202, + chrono_tz::Tz::America__Sitka => 203, + chrono_tz::Tz::America__St_Barthelemy => 204, + chrono_tz::Tz::America__St_Johns => 205, + chrono_tz::Tz::America__St_Kitts => 206, + chrono_tz::Tz::America__St_Lucia => 207, + chrono_tz::Tz::America__St_Thomas => 208, + chrono_tz::Tz::America__St_Vincent => 209, + chrono_tz::Tz::America__Swift_Current => 210, + chrono_tz::Tz::America__Tegucigalpa => 211, + chrono_tz::Tz::America__Thule => 212, + chrono_tz::Tz::America__Thunder_Bay => 213, + chrono_tz::Tz::America__Tijuana => 214, + chrono_tz::Tz::America__Toronto => 215, + chrono_tz::Tz::America__Tortola => 216, + chrono_tz::Tz::America__Vancouver => 217, + chrono_tz::Tz::America__Virgin => 218, + chrono_tz::Tz::America__Whitehorse => 219, + chrono_tz::Tz::America__Winnipeg => 220, + chrono_tz::Tz::America__Yakutat => 221, + chrono_tz::Tz::America__Yellowknife => 222, + chrono_tz::Tz::Antarctica__Casey => 223, + chrono_tz::Tz::Antarctica__Davis => 224, + chrono_tz::Tz::Antarctica__DumontDUrville => 225, + chrono_tz::Tz::Antarctica__Macquarie => 226, + chrono_tz::Tz::Antarctica__Mawson => 227, + chrono_tz::Tz::Antarctica__McMurdo => 228, + chrono_tz::Tz::Antarctica__Palmer => 229, + chrono_tz::Tz::Antarctica__Rothera => 230, + chrono_tz::Tz::Antarctica__South_Pole => 231, + chrono_tz::Tz::Antarctica__Syowa => 232, + chrono_tz::Tz::Antarctica__Troll => 233, + chrono_tz::Tz::Antarctica__Vostok => 234, + chrono_tz::Tz::Arctic__Longyearbyen => 235, + chrono_tz::Tz::Asia__Aden => 236, + chrono_tz::Tz::Asia__Almaty => 237, + chrono_tz::Tz::Asia__Amman => 238, + chrono_tz::Tz::Asia__Anadyr => 239, + chrono_tz::Tz::Asia__Aqtau => 240, + chrono_tz::Tz::Asia__Aqtobe => 241, + chrono_tz::Tz::Asia__Ashgabat => 242, + chrono_tz::Tz::Asia__Ashkhabad => 243, + chrono_tz::Tz::Asia__Atyrau => 244, + chrono_tz::Tz::Asia__Baghdad => 245, + chrono_tz::Tz::Asia__Bahrain => 246, + chrono_tz::Tz::Asia__Baku => 247, + chrono_tz::Tz::Asia__Bangkok => 248, + chrono_tz::Tz::Asia__Barnaul => 249, + chrono_tz::Tz::Asia__Beirut => 250, + chrono_tz::Tz::Asia__Bishkek => 251, + chrono_tz::Tz::Asia__Brunei => 252, + chrono_tz::Tz::Asia__Calcutta => 253, + chrono_tz::Tz::Asia__Chita => 254, + chrono_tz::Tz::Asia__Choibalsan => 255, + chrono_tz::Tz::Asia__Chongqing => 256, + chrono_tz::Tz::Asia__Chungking => 257, + chrono_tz::Tz::Asia__Colombo => 258, + chrono_tz::Tz::Asia__Dacca => 259, + chrono_tz::Tz::Asia__Damascus => 260, + chrono_tz::Tz::Asia__Dhaka => 261, + chrono_tz::Tz::Asia__Dili => 262, + chrono_tz::Tz::Asia__Dubai => 263, + chrono_tz::Tz::Asia__Dushanbe => 264, + chrono_tz::Tz::Asia__Famagusta => 265, + chrono_tz::Tz::Asia__Gaza => 266, + chrono_tz::Tz::Asia__Harbin => 267, + chrono_tz::Tz::Asia__Hebron => 268, + chrono_tz::Tz::Asia__Ho_Chi_Minh => 269, + chrono_tz::Tz::Asia__Hong_Kong => 270, + chrono_tz::Tz::Asia__Hovd => 271, + chrono_tz::Tz::Asia__Irkutsk => 272, + chrono_tz::Tz::Asia__Istanbul => 273, + chrono_tz::Tz::Asia__Jakarta => 274, + chrono_tz::Tz::Asia__Jayapura => 275, + chrono_tz::Tz::Asia__Jerusalem => 276, + chrono_tz::Tz::Asia__Kabul => 277, + chrono_tz::Tz::Asia__Kamchatka => 278, + chrono_tz::Tz::Asia__Karachi => 279, + chrono_tz::Tz::Asia__Kashgar => 280, + chrono_tz::Tz::Asia__Kathmandu => 281, + chrono_tz::Tz::Asia__Katmandu => 282, + chrono_tz::Tz::Asia__Khandyga => 283, + chrono_tz::Tz::Asia__Kolkata => 284, + chrono_tz::Tz::Asia__Krasnoyarsk => 285, + chrono_tz::Tz::Asia__Kuala_Lumpur => 286, + chrono_tz::Tz::Asia__Kuching => 287, + chrono_tz::Tz::Asia__Kuwait => 288, + chrono_tz::Tz::Asia__Macao => 289, + chrono_tz::Tz::Asia__Macau => 290, + chrono_tz::Tz::Asia__Magadan => 291, + chrono_tz::Tz::Asia__Makassar => 292, + chrono_tz::Tz::Asia__Manila => 293, + chrono_tz::Tz::Asia__Muscat => 294, + chrono_tz::Tz::Asia__Nicosia => 295, + chrono_tz::Tz::Asia__Novokuznetsk => 296, + chrono_tz::Tz::Asia__Novosibirsk => 297, + chrono_tz::Tz::Asia__Omsk => 298, + chrono_tz::Tz::Asia__Oral => 299, + chrono_tz::Tz::Asia__Phnom_Penh => 300, + chrono_tz::Tz::Asia__Pontianak => 301, + chrono_tz::Tz::Asia__Pyongyang => 302, + chrono_tz::Tz::Asia__Qatar => 303, + chrono_tz::Tz::Asia__Qostanay => 304, + chrono_tz::Tz::Asia__Qyzylorda => 305, + chrono_tz::Tz::Asia__Rangoon => 306, + chrono_tz::Tz::Asia__Riyadh => 307, + chrono_tz::Tz::Asia__Saigon => 308, + chrono_tz::Tz::Asia__Sakhalin => 309, + chrono_tz::Tz::Asia__Samarkand => 310, + chrono_tz::Tz::Asia__Seoul => 311, + chrono_tz::Tz::Asia__Shanghai => 312, + chrono_tz::Tz::Asia__Singapore => 313, + chrono_tz::Tz::Asia__Srednekolymsk => 314, + chrono_tz::Tz::Asia__Taipei => 315, + chrono_tz::Tz::Asia__Tashkent => 316, + chrono_tz::Tz::Asia__Tbilisi => 317, + chrono_tz::Tz::Asia__Tehran => 318, + chrono_tz::Tz::Asia__Tel_Aviv => 319, + chrono_tz::Tz::Asia__Thimbu => 320, + chrono_tz::Tz::Asia__Thimphu => 321, + chrono_tz::Tz::Asia__Tokyo => 322, + chrono_tz::Tz::Asia__Tomsk => 323, + chrono_tz::Tz::Asia__Ujung_Pandang => 324, + chrono_tz::Tz::Asia__Ulaanbaatar => 325, + chrono_tz::Tz::Asia__Ulan_Bator => 326, + chrono_tz::Tz::Asia__Urumqi => 327, + chrono_tz::Tz::Asia__UstNera => 328, + chrono_tz::Tz::Asia__Vientiane => 329, + chrono_tz::Tz::Asia__Vladivostok => 330, + chrono_tz::Tz::Asia__Yakutsk => 331, + chrono_tz::Tz::Asia__Yangon => 332, + chrono_tz::Tz::Asia__Yekaterinburg => 333, + chrono_tz::Tz::Asia__Yerevan => 334, + chrono_tz::Tz::Atlantic__Azores => 335, + chrono_tz::Tz::Atlantic__Bermuda => 336, + chrono_tz::Tz::Atlantic__Canary => 337, + chrono_tz::Tz::Atlantic__Cape_Verde => 338, + chrono_tz::Tz::Atlantic__Faeroe => 339, + chrono_tz::Tz::Atlantic__Faroe => 340, + chrono_tz::Tz::Atlantic__Jan_Mayen => 341, + chrono_tz::Tz::Atlantic__Madeira => 342, + chrono_tz::Tz::Atlantic__Reykjavik => 343, + chrono_tz::Tz::Atlantic__South_Georgia => 344, + chrono_tz::Tz::Atlantic__St_Helena => 345, + chrono_tz::Tz::Atlantic__Stanley => 346, + chrono_tz::Tz::Australia__ACT => 347, + chrono_tz::Tz::Australia__Adelaide => 348, + chrono_tz::Tz::Australia__Brisbane => 349, + chrono_tz::Tz::Australia__Broken_Hill => 350, + chrono_tz::Tz::Australia__Canberra => 351, + chrono_tz::Tz::Australia__Currie => 352, + chrono_tz::Tz::Australia__Darwin => 353, + chrono_tz::Tz::Australia__Eucla => 354, + chrono_tz::Tz::Australia__Hobart => 355, + chrono_tz::Tz::Australia__LHI => 356, + chrono_tz::Tz::Australia__Lindeman => 357, + chrono_tz::Tz::Australia__Lord_Howe => 358, + chrono_tz::Tz::Australia__Melbourne => 359, + chrono_tz::Tz::Australia__NSW => 360, + chrono_tz::Tz::Australia__North => 361, + chrono_tz::Tz::Australia__Perth => 362, + chrono_tz::Tz::Australia__Queensland => 363, + chrono_tz::Tz::Australia__South => 364, + chrono_tz::Tz::Australia__Sydney => 365, + chrono_tz::Tz::Australia__Tasmania => 366, + chrono_tz::Tz::Australia__Victoria => 367, + chrono_tz::Tz::Australia__West => 368, + chrono_tz::Tz::Australia__Yancowinna => 369, + chrono_tz::Tz::Brazil__Acre => 370, + chrono_tz::Tz::Brazil__DeNoronha => 371, + chrono_tz::Tz::Brazil__East => 372, + chrono_tz::Tz::Brazil__West => 373, + chrono_tz::Tz::CET => 374, + chrono_tz::Tz::CST6CDT => 375, + chrono_tz::Tz::Canada__Atlantic => 376, + chrono_tz::Tz::Canada__Central => 377, + chrono_tz::Tz::Canada__Eastern => 378, + chrono_tz::Tz::Canada__Mountain => 379, + chrono_tz::Tz::Canada__Newfoundland => 380, + chrono_tz::Tz::Canada__Pacific => 381, + chrono_tz::Tz::Canada__Saskatchewan => 382, + chrono_tz::Tz::Canada__Yukon => 383, + chrono_tz::Tz::Chile__Continental => 384, + chrono_tz::Tz::Chile__EasterIsland => 385, + chrono_tz::Tz::Cuba => 386, + chrono_tz::Tz::EET => 387, + chrono_tz::Tz::EST => 388, + chrono_tz::Tz::EST5EDT => 389, + chrono_tz::Tz::Egypt => 390, + chrono_tz::Tz::Eire => 391, + chrono_tz::Tz::Etc__GMT => 392, + chrono_tz::Tz::Etc__GMTPlus0 => 393, + chrono_tz::Tz::Etc__GMTPlus1 => 394, + chrono_tz::Tz::Etc__GMTPlus10 => 395, + chrono_tz::Tz::Etc__GMTPlus11 => 396, + chrono_tz::Tz::Etc__GMTPlus12 => 397, + chrono_tz::Tz::Etc__GMTPlus2 => 398, + chrono_tz::Tz::Etc__GMTPlus3 => 399, + chrono_tz::Tz::Etc__GMTPlus4 => 400, + chrono_tz::Tz::Etc__GMTPlus5 => 401, + chrono_tz::Tz::Etc__GMTPlus6 => 402, + chrono_tz::Tz::Etc__GMTPlus7 => 403, + chrono_tz::Tz::Etc__GMTPlus8 => 404, + chrono_tz::Tz::Etc__GMTPlus9 => 405, + chrono_tz::Tz::Etc__GMTMinus0 => 406, + chrono_tz::Tz::Etc__GMTMinus1 => 407, + chrono_tz::Tz::Etc__GMTMinus10 => 408, + chrono_tz::Tz::Etc__GMTMinus11 => 409, + chrono_tz::Tz::Etc__GMTMinus12 => 410, + chrono_tz::Tz::Etc__GMTMinus13 => 411, + chrono_tz::Tz::Etc__GMTMinus14 => 412, + chrono_tz::Tz::Etc__GMTMinus2 => 413, + chrono_tz::Tz::Etc__GMTMinus3 => 414, + chrono_tz::Tz::Etc__GMTMinus4 => 415, + chrono_tz::Tz::Etc__GMTMinus5 => 416, + chrono_tz::Tz::Etc__GMTMinus6 => 417, + chrono_tz::Tz::Etc__GMTMinus7 => 418, + chrono_tz::Tz::Etc__GMTMinus8 => 419, + chrono_tz::Tz::Etc__GMTMinus9 => 420, + chrono_tz::Tz::Etc__GMT0 => 421, + chrono_tz::Tz::Etc__Greenwich => 422, + chrono_tz::Tz::Etc__UCT => 423, + chrono_tz::Tz::Etc__UTC => 424, + chrono_tz::Tz::Etc__Universal => 425, + chrono_tz::Tz::Etc__Zulu => 426, + chrono_tz::Tz::Europe__Amsterdam => 427, + chrono_tz::Tz::Europe__Andorra => 428, + chrono_tz::Tz::Europe__Astrakhan => 429, + chrono_tz::Tz::Europe__Athens => 430, + chrono_tz::Tz::Europe__Belfast => 431, + chrono_tz::Tz::Europe__Belgrade => 432, + chrono_tz::Tz::Europe__Berlin => 433, + chrono_tz::Tz::Europe__Bratislava => 434, + chrono_tz::Tz::Europe__Brussels => 435, + chrono_tz::Tz::Europe__Bucharest => 436, + chrono_tz::Tz::Europe__Budapest => 437, + chrono_tz::Tz::Europe__Busingen => 438, + chrono_tz::Tz::Europe__Chisinau => 439, + chrono_tz::Tz::Europe__Copenhagen => 440, + chrono_tz::Tz::Europe__Dublin => 441, + chrono_tz::Tz::Europe__Gibraltar => 442, + chrono_tz::Tz::Europe__Guernsey => 443, + chrono_tz::Tz::Europe__Helsinki => 444, + chrono_tz::Tz::Europe__Isle_of_Man => 445, + chrono_tz::Tz::Europe__Istanbul => 446, + chrono_tz::Tz::Europe__Jersey => 447, + chrono_tz::Tz::Europe__Kaliningrad => 448, + chrono_tz::Tz::Europe__Kiev => 449, + chrono_tz::Tz::Europe__Kirov => 450, + chrono_tz::Tz::Europe__Kyiv => 451, + chrono_tz::Tz::Europe__Lisbon => 452, + chrono_tz::Tz::Europe__Ljubljana => 453, + chrono_tz::Tz::Europe__London => 454, + chrono_tz::Tz::Europe__Luxembourg => 455, + chrono_tz::Tz::Europe__Madrid => 456, + chrono_tz::Tz::Europe__Malta => 457, + chrono_tz::Tz::Europe__Mariehamn => 458, + chrono_tz::Tz::Europe__Minsk => 459, + chrono_tz::Tz::Europe__Monaco => 460, + chrono_tz::Tz::Europe__Moscow => 461, + chrono_tz::Tz::Europe__Nicosia => 462, + chrono_tz::Tz::Europe__Oslo => 463, + chrono_tz::Tz::Europe__Paris => 464, + chrono_tz::Tz::Europe__Podgorica => 465, + chrono_tz::Tz::Europe__Prague => 466, + chrono_tz::Tz::Europe__Riga => 467, + chrono_tz::Tz::Europe__Rome => 468, + chrono_tz::Tz::Europe__Samara => 469, + chrono_tz::Tz::Europe__San_Marino => 470, + chrono_tz::Tz::Europe__Sarajevo => 471, + chrono_tz::Tz::Europe__Saratov => 472, + chrono_tz::Tz::Europe__Simferopol => 473, + chrono_tz::Tz::Europe__Skopje => 474, + chrono_tz::Tz::Europe__Sofia => 475, + chrono_tz::Tz::Europe__Stockholm => 476, + chrono_tz::Tz::Europe__Tallinn => 477, + chrono_tz::Tz::Europe__Tirane => 478, + chrono_tz::Tz::Europe__Tiraspol => 479, + chrono_tz::Tz::Europe__Ulyanovsk => 480, + chrono_tz::Tz::Europe__Uzhgorod => 481, + chrono_tz::Tz::Europe__Vaduz => 482, + chrono_tz::Tz::Europe__Vatican => 483, + chrono_tz::Tz::Europe__Vienna => 484, + chrono_tz::Tz::Europe__Vilnius => 485, + chrono_tz::Tz::Europe__Volgograd => 486, + chrono_tz::Tz::Europe__Warsaw => 487, + chrono_tz::Tz::Europe__Zagreb => 488, + chrono_tz::Tz::Europe__Zaporozhye => 489, + chrono_tz::Tz::Europe__Zurich => 490, + chrono_tz::Tz::GB => 491, + chrono_tz::Tz::GBEire => 492, + chrono_tz::Tz::GMT => 493, + chrono_tz::Tz::GMTPlus0 => 494, + chrono_tz::Tz::GMTMinus0 => 495, + chrono_tz::Tz::GMT0 => 496, + chrono_tz::Tz::Greenwich => 497, + chrono_tz::Tz::HST => 498, + chrono_tz::Tz::Hongkong => 499, + chrono_tz::Tz::Iceland => 500, + chrono_tz::Tz::Indian__Antananarivo => 501, + chrono_tz::Tz::Indian__Chagos => 502, + chrono_tz::Tz::Indian__Christmas => 503, + chrono_tz::Tz::Indian__Cocos => 504, + chrono_tz::Tz::Indian__Comoro => 505, + chrono_tz::Tz::Indian__Kerguelen => 506, + chrono_tz::Tz::Indian__Mahe => 507, + chrono_tz::Tz::Indian__Maldives => 508, + chrono_tz::Tz::Indian__Mauritius => 509, + chrono_tz::Tz::Indian__Mayotte => 510, + chrono_tz::Tz::Indian__Reunion => 511, + chrono_tz::Tz::Iran => 512, + chrono_tz::Tz::Israel => 513, + chrono_tz::Tz::Jamaica => 514, + chrono_tz::Tz::Japan => 515, + chrono_tz::Tz::Kwajalein => 516, + chrono_tz::Tz::Libya => 517, + chrono_tz::Tz::MET => 518, + chrono_tz::Tz::MST => 519, + chrono_tz::Tz::MST7MDT => 520, + chrono_tz::Tz::Mexico__BajaNorte => 521, + chrono_tz::Tz::Mexico__BajaSur => 522, + chrono_tz::Tz::Mexico__General => 523, + chrono_tz::Tz::NZ => 524, + chrono_tz::Tz::NZCHAT => 525, + chrono_tz::Tz::Navajo => 526, + chrono_tz::Tz::PRC => 527, + chrono_tz::Tz::PST8PDT => 528, + chrono_tz::Tz::Pacific__Apia => 529, + chrono_tz::Tz::Pacific__Auckland => 530, + chrono_tz::Tz::Pacific__Bougainville => 531, + chrono_tz::Tz::Pacific__Chatham => 532, + chrono_tz::Tz::Pacific__Chuuk => 533, + chrono_tz::Tz::Pacific__Easter => 534, + chrono_tz::Tz::Pacific__Efate => 535, + chrono_tz::Tz::Pacific__Enderbury => 536, + chrono_tz::Tz::Pacific__Fakaofo => 537, + chrono_tz::Tz::Pacific__Fiji => 538, + chrono_tz::Tz::Pacific__Funafuti => 539, + chrono_tz::Tz::Pacific__Galapagos => 540, + chrono_tz::Tz::Pacific__Gambier => 541, + chrono_tz::Tz::Pacific__Guadalcanal => 542, + chrono_tz::Tz::Pacific__Guam => 543, + chrono_tz::Tz::Pacific__Honolulu => 544, + chrono_tz::Tz::Pacific__Johnston => 545, + chrono_tz::Tz::Pacific__Kanton => 546, + chrono_tz::Tz::Pacific__Kiritimati => 547, + chrono_tz::Tz::Pacific__Kosrae => 548, + chrono_tz::Tz::Pacific__Kwajalein => 549, + chrono_tz::Tz::Pacific__Majuro => 550, + chrono_tz::Tz::Pacific__Marquesas => 551, + chrono_tz::Tz::Pacific__Midway => 552, + chrono_tz::Tz::Pacific__Nauru => 553, + chrono_tz::Tz::Pacific__Niue => 554, + chrono_tz::Tz::Pacific__Norfolk => 555, + chrono_tz::Tz::Pacific__Noumea => 556, + chrono_tz::Tz::Pacific__Pago_Pago => 557, + chrono_tz::Tz::Pacific__Palau => 558, + chrono_tz::Tz::Pacific__Pitcairn => 559, + chrono_tz::Tz::Pacific__Pohnpei => 560, + chrono_tz::Tz::Pacific__Ponape => 561, + chrono_tz::Tz::Pacific__Port_Moresby => 562, + chrono_tz::Tz::Pacific__Rarotonga => 563, + chrono_tz::Tz::Pacific__Saipan => 564, + chrono_tz::Tz::Pacific__Samoa => 565, + chrono_tz::Tz::Pacific__Tahiti => 566, + chrono_tz::Tz::Pacific__Tarawa => 567, + chrono_tz::Tz::Pacific__Tongatapu => 568, + chrono_tz::Tz::Pacific__Truk => 569, + chrono_tz::Tz::Pacific__Wake => 570, + chrono_tz::Tz::Pacific__Wallis => 571, + chrono_tz::Tz::Pacific__Yap => 572, + chrono_tz::Tz::Poland => 573, + chrono_tz::Tz::Portugal => 574, + chrono_tz::Tz::ROC => 575, + chrono_tz::Tz::ROK => 576, + chrono_tz::Tz::Singapore => 577, + chrono_tz::Tz::Turkey => 578, + chrono_tz::Tz::UCT => 579, + chrono_tz::Tz::US__Alaska => 580, + chrono_tz::Tz::US__Aleutian => 581, + chrono_tz::Tz::US__Arizona => 582, + chrono_tz::Tz::US__Central => 583, + chrono_tz::Tz::US__EastIndiana => 584, + chrono_tz::Tz::US__Eastern => 585, + chrono_tz::Tz::US__Hawaii => 586, + chrono_tz::Tz::US__IndianaStarke => 587, + chrono_tz::Tz::US__Michigan => 588, + chrono_tz::Tz::US__Mountain => 589, + chrono_tz::Tz::US__Pacific => 590, + chrono_tz::Tz::US__Samoa => 591, + chrono_tz::Tz::UTC => 592, + chrono_tz::Tz::Universal => 593, + chrono_tz::Tz::WSU => 594, + chrono_tz::Tz::WET => 595, + chrono_tz::Tz::Zulu => 596, + } +} + +fn id_to_chrono_tz(id: u16) -> Option { + match id { + 0 => Some(chrono_tz::Tz::Africa__Abidjan), + 1 => Some(chrono_tz::Tz::Africa__Accra), + 2 => Some(chrono_tz::Tz::Africa__Addis_Ababa), + 3 => Some(chrono_tz::Tz::Africa__Algiers), + 4 => Some(chrono_tz::Tz::Africa__Asmara), + 5 => Some(chrono_tz::Tz::Africa__Asmera), + 6 => Some(chrono_tz::Tz::Africa__Bamako), + 7 => Some(chrono_tz::Tz::Africa__Bangui), + 8 => Some(chrono_tz::Tz::Africa__Banjul), + 9 => Some(chrono_tz::Tz::Africa__Bissau), + 10 => Some(chrono_tz::Tz::Africa__Blantyre), + 11 => Some(chrono_tz::Tz::Africa__Brazzaville), + 12 => Some(chrono_tz::Tz::Africa__Bujumbura), + 13 => Some(chrono_tz::Tz::Africa__Cairo), + 14 => Some(chrono_tz::Tz::Africa__Casablanca), + 15 => Some(chrono_tz::Tz::Africa__Ceuta), + 16 => Some(chrono_tz::Tz::Africa__Conakry), + 17 => Some(chrono_tz::Tz::Africa__Dakar), + 18 => Some(chrono_tz::Tz::Africa__Dar_es_Salaam), + 19 => Some(chrono_tz::Tz::Africa__Djibouti), + 20 => Some(chrono_tz::Tz::Africa__Douala), + 21 => Some(chrono_tz::Tz::Africa__El_Aaiun), + 22 => Some(chrono_tz::Tz::Africa__Freetown), + 23 => Some(chrono_tz::Tz::Africa__Gaborone), + 24 => Some(chrono_tz::Tz::Africa__Harare), + 25 => Some(chrono_tz::Tz::Africa__Johannesburg), + 26 => Some(chrono_tz::Tz::Africa__Juba), + 27 => Some(chrono_tz::Tz::Africa__Kampala), + 28 => Some(chrono_tz::Tz::Africa__Khartoum), + 29 => Some(chrono_tz::Tz::Africa__Kigali), + 30 => Some(chrono_tz::Tz::Africa__Kinshasa), + 31 => Some(chrono_tz::Tz::Africa__Lagos), + 32 => Some(chrono_tz::Tz::Africa__Libreville), + 33 => Some(chrono_tz::Tz::Africa__Lome), + 34 => Some(chrono_tz::Tz::Africa__Luanda), + 35 => Some(chrono_tz::Tz::Africa__Lubumbashi), + 36 => Some(chrono_tz::Tz::Africa__Lusaka), + 37 => Some(chrono_tz::Tz::Africa__Malabo), + 38 => Some(chrono_tz::Tz::Africa__Maputo), + 39 => Some(chrono_tz::Tz::Africa__Maseru), + 40 => Some(chrono_tz::Tz::Africa__Mbabane), + 41 => Some(chrono_tz::Tz::Africa__Mogadishu), + 42 => Some(chrono_tz::Tz::Africa__Monrovia), + 43 => Some(chrono_tz::Tz::Africa__Nairobi), + 44 => Some(chrono_tz::Tz::Africa__Ndjamena), + 45 => Some(chrono_tz::Tz::Africa__Niamey), + 46 => Some(chrono_tz::Tz::Africa__Nouakchott), + 47 => Some(chrono_tz::Tz::Africa__Ouagadougou), + 48 => Some(chrono_tz::Tz::Africa__PortoNovo), + 49 => Some(chrono_tz::Tz::Africa__Sao_Tome), + 50 => Some(chrono_tz::Tz::Africa__Timbuktu), + 51 => Some(chrono_tz::Tz::Africa__Tripoli), + 52 => Some(chrono_tz::Tz::Africa__Tunis), + 53 => Some(chrono_tz::Tz::Africa__Windhoek), + 54 => Some(chrono_tz::Tz::America__Adak), + 55 => Some(chrono_tz::Tz::America__Anchorage), + 56 => Some(chrono_tz::Tz::America__Anguilla), + 57 => Some(chrono_tz::Tz::America__Antigua), + 58 => Some(chrono_tz::Tz::America__Araguaina), + 59 => Some(chrono_tz::Tz::America__Argentina__Buenos_Aires), + 60 => Some(chrono_tz::Tz::America__Argentina__Catamarca), + 61 => Some(chrono_tz::Tz::America__Argentina__ComodRivadavia), + 62 => Some(chrono_tz::Tz::America__Argentina__Cordoba), + 63 => Some(chrono_tz::Tz::America__Argentina__Jujuy), + 64 => Some(chrono_tz::Tz::America__Argentina__La_Rioja), + 65 => Some(chrono_tz::Tz::America__Argentina__Mendoza), + 66 => Some(chrono_tz::Tz::America__Argentina__Rio_Gallegos), + 67 => Some(chrono_tz::Tz::America__Argentina__Salta), + 68 => Some(chrono_tz::Tz::America__Argentina__San_Juan), + 69 => Some(chrono_tz::Tz::America__Argentina__San_Luis), + 70 => Some(chrono_tz::Tz::America__Argentina__Tucuman), + 71 => Some(chrono_tz::Tz::America__Argentina__Ushuaia), + 72 => Some(chrono_tz::Tz::America__Aruba), + 73 => Some(chrono_tz::Tz::America__Asuncion), + 74 => Some(chrono_tz::Tz::America__Atikokan), + 75 => Some(chrono_tz::Tz::America__Atka), + 76 => Some(chrono_tz::Tz::America__Bahia), + 77 => Some(chrono_tz::Tz::America__Bahia_Banderas), + 78 => Some(chrono_tz::Tz::America__Barbados), + 79 => Some(chrono_tz::Tz::America__Belem), + 80 => Some(chrono_tz::Tz::America__Belize), + 81 => Some(chrono_tz::Tz::America__BlancSablon), + 82 => Some(chrono_tz::Tz::America__Boa_Vista), + 83 => Some(chrono_tz::Tz::America__Bogota), + 84 => Some(chrono_tz::Tz::America__Boise), + 85 => Some(chrono_tz::Tz::America__Buenos_Aires), + 86 => Some(chrono_tz::Tz::America__Cambridge_Bay), + 87 => Some(chrono_tz::Tz::America__Campo_Grande), + 88 => Some(chrono_tz::Tz::America__Cancun), + 89 => Some(chrono_tz::Tz::America__Caracas), + 90 => Some(chrono_tz::Tz::America__Catamarca), + 91 => Some(chrono_tz::Tz::America__Cayenne), + 92 => Some(chrono_tz::Tz::America__Cayman), + 93 => Some(chrono_tz::Tz::America__Chicago), + 94 => Some(chrono_tz::Tz::America__Chihuahua), + 95 => Some(chrono_tz::Tz::America__Ciudad_Juarez), + 96 => Some(chrono_tz::Tz::America__Coral_Harbour), + 97 => Some(chrono_tz::Tz::America__Cordoba), + 98 => Some(chrono_tz::Tz::America__Costa_Rica), + 99 => Some(chrono_tz::Tz::America__Coyhaique), + 100 => Some(chrono_tz::Tz::America__Creston), + 101 => Some(chrono_tz::Tz::America__Cuiaba), + 102 => Some(chrono_tz::Tz::America__Curacao), + 103 => Some(chrono_tz::Tz::America__Danmarkshavn), + 104 => Some(chrono_tz::Tz::America__Dawson), + 105 => Some(chrono_tz::Tz::America__Dawson_Creek), + 106 => Some(chrono_tz::Tz::America__Denver), + 107 => Some(chrono_tz::Tz::America__Detroit), + 108 => Some(chrono_tz::Tz::America__Dominica), + 109 => Some(chrono_tz::Tz::America__Edmonton), + 110 => Some(chrono_tz::Tz::America__Eirunepe), + 111 => Some(chrono_tz::Tz::America__El_Salvador), + 112 => Some(chrono_tz::Tz::America__Ensenada), + 113 => Some(chrono_tz::Tz::America__Fort_Nelson), + 114 => Some(chrono_tz::Tz::America__Fort_Wayne), + 115 => Some(chrono_tz::Tz::America__Fortaleza), + 116 => Some(chrono_tz::Tz::America__Glace_Bay), + 117 => Some(chrono_tz::Tz::America__Godthab), + 118 => Some(chrono_tz::Tz::America__Goose_Bay), + 119 => Some(chrono_tz::Tz::America__Grand_Turk), + 120 => Some(chrono_tz::Tz::America__Grenada), + 121 => Some(chrono_tz::Tz::America__Guadeloupe), + 122 => Some(chrono_tz::Tz::America__Guatemala), + 123 => Some(chrono_tz::Tz::America__Guayaquil), + 124 => Some(chrono_tz::Tz::America__Guyana), + 125 => Some(chrono_tz::Tz::America__Halifax), + 126 => Some(chrono_tz::Tz::America__Havana), + 127 => Some(chrono_tz::Tz::America__Hermosillo), + 128 => Some(chrono_tz::Tz::America__Indiana__Indianapolis), + 129 => Some(chrono_tz::Tz::America__Indiana__Knox), + 130 => Some(chrono_tz::Tz::America__Indiana__Marengo), + 131 => Some(chrono_tz::Tz::America__Indiana__Petersburg), + 132 => Some(chrono_tz::Tz::America__Indiana__Tell_City), + 133 => Some(chrono_tz::Tz::America__Indiana__Vevay), + 134 => Some(chrono_tz::Tz::America__Indiana__Vincennes), + 135 => Some(chrono_tz::Tz::America__Indiana__Winamac), + 136 => Some(chrono_tz::Tz::America__Indianapolis), + 137 => Some(chrono_tz::Tz::America__Inuvik), + 138 => Some(chrono_tz::Tz::America__Iqaluit), + 139 => Some(chrono_tz::Tz::America__Jamaica), + 140 => Some(chrono_tz::Tz::America__Jujuy), + 141 => Some(chrono_tz::Tz::America__Juneau), + 142 => Some(chrono_tz::Tz::America__Kentucky__Louisville), + 143 => Some(chrono_tz::Tz::America__Kentucky__Monticello), + 144 => Some(chrono_tz::Tz::America__Knox_IN), + 145 => Some(chrono_tz::Tz::America__Kralendijk), + 146 => Some(chrono_tz::Tz::America__La_Paz), + 147 => Some(chrono_tz::Tz::America__Lima), + 148 => Some(chrono_tz::Tz::America__Los_Angeles), + 149 => Some(chrono_tz::Tz::America__Louisville), + 150 => Some(chrono_tz::Tz::America__Lower_Princes), + 151 => Some(chrono_tz::Tz::America__Maceio), + 152 => Some(chrono_tz::Tz::America__Managua), + 153 => Some(chrono_tz::Tz::America__Manaus), + 154 => Some(chrono_tz::Tz::America__Marigot), + 155 => Some(chrono_tz::Tz::America__Martinique), + 156 => Some(chrono_tz::Tz::America__Matamoros), + 157 => Some(chrono_tz::Tz::America__Mazatlan), + 158 => Some(chrono_tz::Tz::America__Mendoza), + 159 => Some(chrono_tz::Tz::America__Menominee), + 160 => Some(chrono_tz::Tz::America__Merida), + 161 => Some(chrono_tz::Tz::America__Metlakatla), + 162 => Some(chrono_tz::Tz::America__Mexico_City), + 163 => Some(chrono_tz::Tz::America__Miquelon), + 164 => Some(chrono_tz::Tz::America__Moncton), + 165 => Some(chrono_tz::Tz::America__Monterrey), + 166 => Some(chrono_tz::Tz::America__Montevideo), + 167 => Some(chrono_tz::Tz::America__Montreal), + 168 => Some(chrono_tz::Tz::America__Montserrat), + 169 => Some(chrono_tz::Tz::America__Nassau), + 170 => Some(chrono_tz::Tz::America__New_York), + 171 => Some(chrono_tz::Tz::America__Nipigon), + 172 => Some(chrono_tz::Tz::America__Nome), + 173 => Some(chrono_tz::Tz::America__Noronha), + 174 => Some(chrono_tz::Tz::America__North_Dakota__Beulah), + 175 => Some(chrono_tz::Tz::America__North_Dakota__Center), + 176 => Some(chrono_tz::Tz::America__North_Dakota__New_Salem), + 177 => Some(chrono_tz::Tz::America__Nuuk), + 178 => Some(chrono_tz::Tz::America__Ojinaga), + 179 => Some(chrono_tz::Tz::America__Panama), + 180 => Some(chrono_tz::Tz::America__Pangnirtung), + 181 => Some(chrono_tz::Tz::America__Paramaribo), + 182 => Some(chrono_tz::Tz::America__Phoenix), + 183 => Some(chrono_tz::Tz::America__PortauPrince), + 184 => Some(chrono_tz::Tz::America__Port_of_Spain), + 185 => Some(chrono_tz::Tz::America__Porto_Acre), + 186 => Some(chrono_tz::Tz::America__Porto_Velho), + 187 => Some(chrono_tz::Tz::America__Puerto_Rico), + 188 => Some(chrono_tz::Tz::America__Punta_Arenas), + 189 => Some(chrono_tz::Tz::America__Rainy_River), + 190 => Some(chrono_tz::Tz::America__Rankin_Inlet), + 191 => Some(chrono_tz::Tz::America__Recife), + 192 => Some(chrono_tz::Tz::America__Regina), + 193 => Some(chrono_tz::Tz::America__Resolute), + 194 => Some(chrono_tz::Tz::America__Rio_Branco), + 195 => Some(chrono_tz::Tz::America__Rosario), + 196 => Some(chrono_tz::Tz::America__Santa_Isabel), + 197 => Some(chrono_tz::Tz::America__Santarem), + 198 => Some(chrono_tz::Tz::America__Santiago), + 199 => Some(chrono_tz::Tz::America__Santo_Domingo), + 200 => Some(chrono_tz::Tz::America__Sao_Paulo), + 201 => Some(chrono_tz::Tz::America__Scoresbysund), + 202 => Some(chrono_tz::Tz::America__Shiprock), + 203 => Some(chrono_tz::Tz::America__Sitka), + 204 => Some(chrono_tz::Tz::America__St_Barthelemy), + 205 => Some(chrono_tz::Tz::America__St_Johns), + 206 => Some(chrono_tz::Tz::America__St_Kitts), + 207 => Some(chrono_tz::Tz::America__St_Lucia), + 208 => Some(chrono_tz::Tz::America__St_Thomas), + 209 => Some(chrono_tz::Tz::America__St_Vincent), + 210 => Some(chrono_tz::Tz::America__Swift_Current), + 211 => Some(chrono_tz::Tz::America__Tegucigalpa), + 212 => Some(chrono_tz::Tz::America__Thule), + 213 => Some(chrono_tz::Tz::America__Thunder_Bay), + 214 => Some(chrono_tz::Tz::America__Tijuana), + 215 => Some(chrono_tz::Tz::America__Toronto), + 216 => Some(chrono_tz::Tz::America__Tortola), + 217 => Some(chrono_tz::Tz::America__Vancouver), + 218 => Some(chrono_tz::Tz::America__Virgin), + 219 => Some(chrono_tz::Tz::America__Whitehorse), + 220 => Some(chrono_tz::Tz::America__Winnipeg), + 221 => Some(chrono_tz::Tz::America__Yakutat), + 222 => Some(chrono_tz::Tz::America__Yellowknife), + 223 => Some(chrono_tz::Tz::Antarctica__Casey), + 224 => Some(chrono_tz::Tz::Antarctica__Davis), + 225 => Some(chrono_tz::Tz::Antarctica__DumontDUrville), + 226 => Some(chrono_tz::Tz::Antarctica__Macquarie), + 227 => Some(chrono_tz::Tz::Antarctica__Mawson), + 228 => Some(chrono_tz::Tz::Antarctica__McMurdo), + 229 => Some(chrono_tz::Tz::Antarctica__Palmer), + 230 => Some(chrono_tz::Tz::Antarctica__Rothera), + 231 => Some(chrono_tz::Tz::Antarctica__South_Pole), + 232 => Some(chrono_tz::Tz::Antarctica__Syowa), + 233 => Some(chrono_tz::Tz::Antarctica__Troll), + 234 => Some(chrono_tz::Tz::Antarctica__Vostok), + 235 => Some(chrono_tz::Tz::Arctic__Longyearbyen), + 236 => Some(chrono_tz::Tz::Asia__Aden), + 237 => Some(chrono_tz::Tz::Asia__Almaty), + 238 => Some(chrono_tz::Tz::Asia__Amman), + 239 => Some(chrono_tz::Tz::Asia__Anadyr), + 240 => Some(chrono_tz::Tz::Asia__Aqtau), + 241 => Some(chrono_tz::Tz::Asia__Aqtobe), + 242 => Some(chrono_tz::Tz::Asia__Ashgabat), + 243 => Some(chrono_tz::Tz::Asia__Ashkhabad), + 244 => Some(chrono_tz::Tz::Asia__Atyrau), + 245 => Some(chrono_tz::Tz::Asia__Baghdad), + 246 => Some(chrono_tz::Tz::Asia__Bahrain), + 247 => Some(chrono_tz::Tz::Asia__Baku), + 248 => Some(chrono_tz::Tz::Asia__Bangkok), + 249 => Some(chrono_tz::Tz::Asia__Barnaul), + 250 => Some(chrono_tz::Tz::Asia__Beirut), + 251 => Some(chrono_tz::Tz::Asia__Bishkek), + 252 => Some(chrono_tz::Tz::Asia__Brunei), + 253 => Some(chrono_tz::Tz::Asia__Calcutta), + 254 => Some(chrono_tz::Tz::Asia__Chita), + 255 => Some(chrono_tz::Tz::Asia__Choibalsan), + 256 => Some(chrono_tz::Tz::Asia__Chongqing), + 257 => Some(chrono_tz::Tz::Asia__Chungking), + 258 => Some(chrono_tz::Tz::Asia__Colombo), + 259 => Some(chrono_tz::Tz::Asia__Dacca), + 260 => Some(chrono_tz::Tz::Asia__Damascus), + 261 => Some(chrono_tz::Tz::Asia__Dhaka), + 262 => Some(chrono_tz::Tz::Asia__Dili), + 263 => Some(chrono_tz::Tz::Asia__Dubai), + 264 => Some(chrono_tz::Tz::Asia__Dushanbe), + 265 => Some(chrono_tz::Tz::Asia__Famagusta), + 266 => Some(chrono_tz::Tz::Asia__Gaza), + 267 => Some(chrono_tz::Tz::Asia__Harbin), + 268 => Some(chrono_tz::Tz::Asia__Hebron), + 269 => Some(chrono_tz::Tz::Asia__Ho_Chi_Minh), + 270 => Some(chrono_tz::Tz::Asia__Hong_Kong), + 271 => Some(chrono_tz::Tz::Asia__Hovd), + 272 => Some(chrono_tz::Tz::Asia__Irkutsk), + 273 => Some(chrono_tz::Tz::Asia__Istanbul), + 274 => Some(chrono_tz::Tz::Asia__Jakarta), + 275 => Some(chrono_tz::Tz::Asia__Jayapura), + 276 => Some(chrono_tz::Tz::Asia__Jerusalem), + 277 => Some(chrono_tz::Tz::Asia__Kabul), + 278 => Some(chrono_tz::Tz::Asia__Kamchatka), + 279 => Some(chrono_tz::Tz::Asia__Karachi), + 280 => Some(chrono_tz::Tz::Asia__Kashgar), + 281 => Some(chrono_tz::Tz::Asia__Kathmandu), + 282 => Some(chrono_tz::Tz::Asia__Katmandu), + 283 => Some(chrono_tz::Tz::Asia__Khandyga), + 284 => Some(chrono_tz::Tz::Asia__Kolkata), + 285 => Some(chrono_tz::Tz::Asia__Krasnoyarsk), + 286 => Some(chrono_tz::Tz::Asia__Kuala_Lumpur), + 287 => Some(chrono_tz::Tz::Asia__Kuching), + 288 => Some(chrono_tz::Tz::Asia__Kuwait), + 289 => Some(chrono_tz::Tz::Asia__Macao), + 290 => Some(chrono_tz::Tz::Asia__Macau), + 291 => Some(chrono_tz::Tz::Asia__Magadan), + 292 => Some(chrono_tz::Tz::Asia__Makassar), + 293 => Some(chrono_tz::Tz::Asia__Manila), + 294 => Some(chrono_tz::Tz::Asia__Muscat), + 295 => Some(chrono_tz::Tz::Asia__Nicosia), + 296 => Some(chrono_tz::Tz::Asia__Novokuznetsk), + 297 => Some(chrono_tz::Tz::Asia__Novosibirsk), + 298 => Some(chrono_tz::Tz::Asia__Omsk), + 299 => Some(chrono_tz::Tz::Asia__Oral), + 300 => Some(chrono_tz::Tz::Asia__Phnom_Penh), + 301 => Some(chrono_tz::Tz::Asia__Pontianak), + 302 => Some(chrono_tz::Tz::Asia__Pyongyang), + 303 => Some(chrono_tz::Tz::Asia__Qatar), + 304 => Some(chrono_tz::Tz::Asia__Qostanay), + 305 => Some(chrono_tz::Tz::Asia__Qyzylorda), + 306 => Some(chrono_tz::Tz::Asia__Rangoon), + 307 => Some(chrono_tz::Tz::Asia__Riyadh), + 308 => Some(chrono_tz::Tz::Asia__Saigon), + 309 => Some(chrono_tz::Tz::Asia__Sakhalin), + 310 => Some(chrono_tz::Tz::Asia__Samarkand), + 311 => Some(chrono_tz::Tz::Asia__Seoul), + 312 => Some(chrono_tz::Tz::Asia__Shanghai), + 313 => Some(chrono_tz::Tz::Asia__Singapore), + 314 => Some(chrono_tz::Tz::Asia__Srednekolymsk), + 315 => Some(chrono_tz::Tz::Asia__Taipei), + 316 => Some(chrono_tz::Tz::Asia__Tashkent), + 317 => Some(chrono_tz::Tz::Asia__Tbilisi), + 318 => Some(chrono_tz::Tz::Asia__Tehran), + 319 => Some(chrono_tz::Tz::Asia__Tel_Aviv), + 320 => Some(chrono_tz::Tz::Asia__Thimbu), + 321 => Some(chrono_tz::Tz::Asia__Thimphu), + 322 => Some(chrono_tz::Tz::Asia__Tokyo), + 323 => Some(chrono_tz::Tz::Asia__Tomsk), + 324 => Some(chrono_tz::Tz::Asia__Ujung_Pandang), + 325 => Some(chrono_tz::Tz::Asia__Ulaanbaatar), + 326 => Some(chrono_tz::Tz::Asia__Ulan_Bator), + 327 => Some(chrono_tz::Tz::Asia__Urumqi), + 328 => Some(chrono_tz::Tz::Asia__UstNera), + 329 => Some(chrono_tz::Tz::Asia__Vientiane), + 330 => Some(chrono_tz::Tz::Asia__Vladivostok), + 331 => Some(chrono_tz::Tz::Asia__Yakutsk), + 332 => Some(chrono_tz::Tz::Asia__Yangon), + 333 => Some(chrono_tz::Tz::Asia__Yekaterinburg), + 334 => Some(chrono_tz::Tz::Asia__Yerevan), + 335 => Some(chrono_tz::Tz::Atlantic__Azores), + 336 => Some(chrono_tz::Tz::Atlantic__Bermuda), + 337 => Some(chrono_tz::Tz::Atlantic__Canary), + 338 => Some(chrono_tz::Tz::Atlantic__Cape_Verde), + 339 => Some(chrono_tz::Tz::Atlantic__Faeroe), + 340 => Some(chrono_tz::Tz::Atlantic__Faroe), + 341 => Some(chrono_tz::Tz::Atlantic__Jan_Mayen), + 342 => Some(chrono_tz::Tz::Atlantic__Madeira), + 343 => Some(chrono_tz::Tz::Atlantic__Reykjavik), + 344 => Some(chrono_tz::Tz::Atlantic__South_Georgia), + 345 => Some(chrono_tz::Tz::Atlantic__St_Helena), + 346 => Some(chrono_tz::Tz::Atlantic__Stanley), + 347 => Some(chrono_tz::Tz::Australia__ACT), + 348 => Some(chrono_tz::Tz::Australia__Adelaide), + 349 => Some(chrono_tz::Tz::Australia__Brisbane), + 350 => Some(chrono_tz::Tz::Australia__Broken_Hill), + 351 => Some(chrono_tz::Tz::Australia__Canberra), + 352 => Some(chrono_tz::Tz::Australia__Currie), + 353 => Some(chrono_tz::Tz::Australia__Darwin), + 354 => Some(chrono_tz::Tz::Australia__Eucla), + 355 => Some(chrono_tz::Tz::Australia__Hobart), + 356 => Some(chrono_tz::Tz::Australia__LHI), + 357 => Some(chrono_tz::Tz::Australia__Lindeman), + 358 => Some(chrono_tz::Tz::Australia__Lord_Howe), + 359 => Some(chrono_tz::Tz::Australia__Melbourne), + 360 => Some(chrono_tz::Tz::Australia__NSW), + 361 => Some(chrono_tz::Tz::Australia__North), + 362 => Some(chrono_tz::Tz::Australia__Perth), + 363 => Some(chrono_tz::Tz::Australia__Queensland), + 364 => Some(chrono_tz::Tz::Australia__South), + 365 => Some(chrono_tz::Tz::Australia__Sydney), + 366 => Some(chrono_tz::Tz::Australia__Tasmania), + 367 => Some(chrono_tz::Tz::Australia__Victoria), + 368 => Some(chrono_tz::Tz::Australia__West), + 369 => Some(chrono_tz::Tz::Australia__Yancowinna), + 370 => Some(chrono_tz::Tz::Brazil__Acre), + 371 => Some(chrono_tz::Tz::Brazil__DeNoronha), + 372 => Some(chrono_tz::Tz::Brazil__East), + 373 => Some(chrono_tz::Tz::Brazil__West), + 374 => Some(chrono_tz::Tz::CET), + 375 => Some(chrono_tz::Tz::CST6CDT), + 376 => Some(chrono_tz::Tz::Canada__Atlantic), + 377 => Some(chrono_tz::Tz::Canada__Central), + 378 => Some(chrono_tz::Tz::Canada__Eastern), + 379 => Some(chrono_tz::Tz::Canada__Mountain), + 380 => Some(chrono_tz::Tz::Canada__Newfoundland), + 381 => Some(chrono_tz::Tz::Canada__Pacific), + 382 => Some(chrono_tz::Tz::Canada__Saskatchewan), + 383 => Some(chrono_tz::Tz::Canada__Yukon), + 384 => Some(chrono_tz::Tz::Chile__Continental), + 385 => Some(chrono_tz::Tz::Chile__EasterIsland), + 386 => Some(chrono_tz::Tz::Cuba), + 387 => Some(chrono_tz::Tz::EET), + 388 => Some(chrono_tz::Tz::EST), + 389 => Some(chrono_tz::Tz::EST5EDT), + 390 => Some(chrono_tz::Tz::Egypt), + 391 => Some(chrono_tz::Tz::Eire), + 392 => Some(chrono_tz::Tz::Etc__GMT), + 393 => Some(chrono_tz::Tz::Etc__GMTPlus0), + 394 => Some(chrono_tz::Tz::Etc__GMTPlus1), + 395 => Some(chrono_tz::Tz::Etc__GMTPlus10), + 396 => Some(chrono_tz::Tz::Etc__GMTPlus11), + 397 => Some(chrono_tz::Tz::Etc__GMTPlus12), + 398 => Some(chrono_tz::Tz::Etc__GMTPlus2), + 399 => Some(chrono_tz::Tz::Etc__GMTPlus3), + 400 => Some(chrono_tz::Tz::Etc__GMTPlus4), + 401 => Some(chrono_tz::Tz::Etc__GMTPlus5), + 402 => Some(chrono_tz::Tz::Etc__GMTPlus6), + 403 => Some(chrono_tz::Tz::Etc__GMTPlus7), + 404 => Some(chrono_tz::Tz::Etc__GMTPlus8), + 405 => Some(chrono_tz::Tz::Etc__GMTPlus9), + 406 => Some(chrono_tz::Tz::Etc__GMTMinus0), + 407 => Some(chrono_tz::Tz::Etc__GMTMinus1), + 408 => Some(chrono_tz::Tz::Etc__GMTMinus10), + 409 => Some(chrono_tz::Tz::Etc__GMTMinus11), + 410 => Some(chrono_tz::Tz::Etc__GMTMinus12), + 411 => Some(chrono_tz::Tz::Etc__GMTMinus13), + 412 => Some(chrono_tz::Tz::Etc__GMTMinus14), + 413 => Some(chrono_tz::Tz::Etc__GMTMinus2), + 414 => Some(chrono_tz::Tz::Etc__GMTMinus3), + 415 => Some(chrono_tz::Tz::Etc__GMTMinus4), + 416 => Some(chrono_tz::Tz::Etc__GMTMinus5), + 417 => Some(chrono_tz::Tz::Etc__GMTMinus6), + 418 => Some(chrono_tz::Tz::Etc__GMTMinus7), + 419 => Some(chrono_tz::Tz::Etc__GMTMinus8), + 420 => Some(chrono_tz::Tz::Etc__GMTMinus9), + 421 => Some(chrono_tz::Tz::Etc__GMT0), + 422 => Some(chrono_tz::Tz::Etc__Greenwich), + 423 => Some(chrono_tz::Tz::Etc__UCT), + 424 => Some(chrono_tz::Tz::Etc__UTC), + 425 => Some(chrono_tz::Tz::Etc__Universal), + 426 => Some(chrono_tz::Tz::Etc__Zulu), + 427 => Some(chrono_tz::Tz::Europe__Amsterdam), + 428 => Some(chrono_tz::Tz::Europe__Andorra), + 429 => Some(chrono_tz::Tz::Europe__Astrakhan), + 430 => Some(chrono_tz::Tz::Europe__Athens), + 431 => Some(chrono_tz::Tz::Europe__Belfast), + 432 => Some(chrono_tz::Tz::Europe__Belgrade), + 433 => Some(chrono_tz::Tz::Europe__Berlin), + 434 => Some(chrono_tz::Tz::Europe__Bratislava), + 435 => Some(chrono_tz::Tz::Europe__Brussels), + 436 => Some(chrono_tz::Tz::Europe__Bucharest), + 437 => Some(chrono_tz::Tz::Europe__Budapest), + 438 => Some(chrono_tz::Tz::Europe__Busingen), + 439 => Some(chrono_tz::Tz::Europe__Chisinau), + 440 => Some(chrono_tz::Tz::Europe__Copenhagen), + 441 => Some(chrono_tz::Tz::Europe__Dublin), + 442 => Some(chrono_tz::Tz::Europe__Gibraltar), + 443 => Some(chrono_tz::Tz::Europe__Guernsey), + 444 => Some(chrono_tz::Tz::Europe__Helsinki), + 445 => Some(chrono_tz::Tz::Europe__Isle_of_Man), + 446 => Some(chrono_tz::Tz::Europe__Istanbul), + 447 => Some(chrono_tz::Tz::Europe__Jersey), + 448 => Some(chrono_tz::Tz::Europe__Kaliningrad), + 449 => Some(chrono_tz::Tz::Europe__Kiev), + 450 => Some(chrono_tz::Tz::Europe__Kirov), + 451 => Some(chrono_tz::Tz::Europe__Kyiv), + 452 => Some(chrono_tz::Tz::Europe__Lisbon), + 453 => Some(chrono_tz::Tz::Europe__Ljubljana), + 454 => Some(chrono_tz::Tz::Europe__London), + 455 => Some(chrono_tz::Tz::Europe__Luxembourg), + 456 => Some(chrono_tz::Tz::Europe__Madrid), + 457 => Some(chrono_tz::Tz::Europe__Malta), + 458 => Some(chrono_tz::Tz::Europe__Mariehamn), + 459 => Some(chrono_tz::Tz::Europe__Minsk), + 460 => Some(chrono_tz::Tz::Europe__Monaco), + 461 => Some(chrono_tz::Tz::Europe__Moscow), + 462 => Some(chrono_tz::Tz::Europe__Nicosia), + 463 => Some(chrono_tz::Tz::Europe__Oslo), + 464 => Some(chrono_tz::Tz::Europe__Paris), + 465 => Some(chrono_tz::Tz::Europe__Podgorica), + 466 => Some(chrono_tz::Tz::Europe__Prague), + 467 => Some(chrono_tz::Tz::Europe__Riga), + 468 => Some(chrono_tz::Tz::Europe__Rome), + 469 => Some(chrono_tz::Tz::Europe__Samara), + 470 => Some(chrono_tz::Tz::Europe__San_Marino), + 471 => Some(chrono_tz::Tz::Europe__Sarajevo), + 472 => Some(chrono_tz::Tz::Europe__Saratov), + 473 => Some(chrono_tz::Tz::Europe__Simferopol), + 474 => Some(chrono_tz::Tz::Europe__Skopje), + 475 => Some(chrono_tz::Tz::Europe__Sofia), + 476 => Some(chrono_tz::Tz::Europe__Stockholm), + 477 => Some(chrono_tz::Tz::Europe__Tallinn), + 478 => Some(chrono_tz::Tz::Europe__Tirane), + 479 => Some(chrono_tz::Tz::Europe__Tiraspol), + 480 => Some(chrono_tz::Tz::Europe__Ulyanovsk), + 481 => Some(chrono_tz::Tz::Europe__Uzhgorod), + 482 => Some(chrono_tz::Tz::Europe__Vaduz), + 483 => Some(chrono_tz::Tz::Europe__Vatican), + 484 => Some(chrono_tz::Tz::Europe__Vienna), + 485 => Some(chrono_tz::Tz::Europe__Vilnius), + 486 => Some(chrono_tz::Tz::Europe__Volgograd), + 487 => Some(chrono_tz::Tz::Europe__Warsaw), + 488 => Some(chrono_tz::Tz::Europe__Zagreb), + 489 => Some(chrono_tz::Tz::Europe__Zaporozhye), + 490 => Some(chrono_tz::Tz::Europe__Zurich), + 491 => Some(chrono_tz::Tz::GB), + 492 => Some(chrono_tz::Tz::GBEire), + 493 => Some(chrono_tz::Tz::GMT), + 494 => Some(chrono_tz::Tz::GMTPlus0), + 495 => Some(chrono_tz::Tz::GMTMinus0), + 496 => Some(chrono_tz::Tz::GMT0), + 497 => Some(chrono_tz::Tz::Greenwich), + 498 => Some(chrono_tz::Tz::HST), + 499 => Some(chrono_tz::Tz::Hongkong), + 500 => Some(chrono_tz::Tz::Iceland), + 501 => Some(chrono_tz::Tz::Indian__Antananarivo), + 502 => Some(chrono_tz::Tz::Indian__Chagos), + 503 => Some(chrono_tz::Tz::Indian__Christmas), + 504 => Some(chrono_tz::Tz::Indian__Cocos), + 505 => Some(chrono_tz::Tz::Indian__Comoro), + 506 => Some(chrono_tz::Tz::Indian__Kerguelen), + 507 => Some(chrono_tz::Tz::Indian__Mahe), + 508 => Some(chrono_tz::Tz::Indian__Maldives), + 509 => Some(chrono_tz::Tz::Indian__Mauritius), + 510 => Some(chrono_tz::Tz::Indian__Mayotte), + 511 => Some(chrono_tz::Tz::Indian__Reunion), + 512 => Some(chrono_tz::Tz::Iran), + 513 => Some(chrono_tz::Tz::Israel), + 514 => Some(chrono_tz::Tz::Jamaica), + 515 => Some(chrono_tz::Tz::Japan), + 516 => Some(chrono_tz::Tz::Kwajalein), + 517 => Some(chrono_tz::Tz::Libya), + 518 => Some(chrono_tz::Tz::MET), + 519 => Some(chrono_tz::Tz::MST), + 520 => Some(chrono_tz::Tz::MST7MDT), + 521 => Some(chrono_tz::Tz::Mexico__BajaNorte), + 522 => Some(chrono_tz::Tz::Mexico__BajaSur), + 523 => Some(chrono_tz::Tz::Mexico__General), + 524 => Some(chrono_tz::Tz::NZ), + 525 => Some(chrono_tz::Tz::NZCHAT), + 526 => Some(chrono_tz::Tz::Navajo), + 527 => Some(chrono_tz::Tz::PRC), + 528 => Some(chrono_tz::Tz::PST8PDT), + 529 => Some(chrono_tz::Tz::Pacific__Apia), + 530 => Some(chrono_tz::Tz::Pacific__Auckland), + 531 => Some(chrono_tz::Tz::Pacific__Bougainville), + 532 => Some(chrono_tz::Tz::Pacific__Chatham), + 533 => Some(chrono_tz::Tz::Pacific__Chuuk), + 534 => Some(chrono_tz::Tz::Pacific__Easter), + 535 => Some(chrono_tz::Tz::Pacific__Efate), + 536 => Some(chrono_tz::Tz::Pacific__Enderbury), + 537 => Some(chrono_tz::Tz::Pacific__Fakaofo), + 538 => Some(chrono_tz::Tz::Pacific__Fiji), + 539 => Some(chrono_tz::Tz::Pacific__Funafuti), + 540 => Some(chrono_tz::Tz::Pacific__Galapagos), + 541 => Some(chrono_tz::Tz::Pacific__Gambier), + 542 => Some(chrono_tz::Tz::Pacific__Guadalcanal), + 543 => Some(chrono_tz::Tz::Pacific__Guam), + 544 => Some(chrono_tz::Tz::Pacific__Honolulu), + 545 => Some(chrono_tz::Tz::Pacific__Johnston), + 546 => Some(chrono_tz::Tz::Pacific__Kanton), + 547 => Some(chrono_tz::Tz::Pacific__Kiritimati), + 548 => Some(chrono_tz::Tz::Pacific__Kosrae), + 549 => Some(chrono_tz::Tz::Pacific__Kwajalein), + 550 => Some(chrono_tz::Tz::Pacific__Majuro), + 551 => Some(chrono_tz::Tz::Pacific__Marquesas), + 552 => Some(chrono_tz::Tz::Pacific__Midway), + 553 => Some(chrono_tz::Tz::Pacific__Nauru), + 554 => Some(chrono_tz::Tz::Pacific__Niue), + 555 => Some(chrono_tz::Tz::Pacific__Norfolk), + 556 => Some(chrono_tz::Tz::Pacific__Noumea), + 557 => Some(chrono_tz::Tz::Pacific__Pago_Pago), + 558 => Some(chrono_tz::Tz::Pacific__Palau), + 559 => Some(chrono_tz::Tz::Pacific__Pitcairn), + 560 => Some(chrono_tz::Tz::Pacific__Pohnpei), + 561 => Some(chrono_tz::Tz::Pacific__Ponape), + 562 => Some(chrono_tz::Tz::Pacific__Port_Moresby), + 563 => Some(chrono_tz::Tz::Pacific__Rarotonga), + 564 => Some(chrono_tz::Tz::Pacific__Saipan), + 565 => Some(chrono_tz::Tz::Pacific__Samoa), + 566 => Some(chrono_tz::Tz::Pacific__Tahiti), + 567 => Some(chrono_tz::Tz::Pacific__Tarawa), + 568 => Some(chrono_tz::Tz::Pacific__Tongatapu), + 569 => Some(chrono_tz::Tz::Pacific__Truk), + 570 => Some(chrono_tz::Tz::Pacific__Wake), + 571 => Some(chrono_tz::Tz::Pacific__Wallis), + 572 => Some(chrono_tz::Tz::Pacific__Yap), + 573 => Some(chrono_tz::Tz::Poland), + 574 => Some(chrono_tz::Tz::Portugal), + 575 => Some(chrono_tz::Tz::ROC), + 576 => Some(chrono_tz::Tz::ROK), + 577 => Some(chrono_tz::Tz::Singapore), + 578 => Some(chrono_tz::Tz::Turkey), + 579 => Some(chrono_tz::Tz::UCT), + 580 => Some(chrono_tz::Tz::US__Alaska), + 581 => Some(chrono_tz::Tz::US__Aleutian), + 582 => Some(chrono_tz::Tz::US__Arizona), + 583 => Some(chrono_tz::Tz::US__Central), + 584 => Some(chrono_tz::Tz::US__EastIndiana), + 585 => Some(chrono_tz::Tz::US__Eastern), + 586 => Some(chrono_tz::Tz::US__Hawaii), + 587 => Some(chrono_tz::Tz::US__IndianaStarke), + 588 => Some(chrono_tz::Tz::US__Michigan), + 589 => Some(chrono_tz::Tz::US__Mountain), + 590 => Some(chrono_tz::Tz::US__Pacific), + 591 => Some(chrono_tz::Tz::US__Samoa), + 592 => Some(chrono_tz::Tz::UTC), + 593 => Some(chrono_tz::Tz::Universal), + 594 => Some(chrono_tz::Tz::WSU), + 595 => Some(chrono_tz::Tz::WET), + 596 => Some(chrono_tz::Tz::Zulu), + _ => None, + } +} + +#[cfg(test)] +mod tests { + + use super::*; + + #[test] + fn test_tz() { + for (zone_name, tz) in [ + ("America/New_York", chrono_tz::Tz::America__New_York), + ( + "(GMT+10:00) Canberra, Melbourne, Sydney", + chrono_tz::Tz::Australia__Sydney, + ), + ( + "(UTC-05:00) Eastern Time (US & Canada)", + chrono_tz::Tz::America__New_York, + ), + ("(GMT +01:00)", chrono_tz::Tz::Etc__GMTMinus1), + ("(GMT+01.00)", chrono_tz::Tz::Etc__GMTMinus1), + ("(UTC-03:00)", chrono_tz::Tz::Etc__GMTPlus3), + ("/Europe/Stockholm", chrono_tz::Tz::Europe__Stockholm), + ( + "/softwarestudio.org/Olson_20011030_5/America/Chicago", + chrono_tz::Tz::America__Chicago, + ), + ( + "/freeassociation.sourceforge.net/Tzfile/Europe/Ljubljana", + chrono_tz::Tz::Europe__Ljubljana, + ), + ( + "/freeassociation.sourceforge.net/Tzfile/SystemV/EST5EDT", + chrono_tz::Tz::EST5EDT, + ), + ] { + assert_eq!(Tz::from_str(zone_name).expect(zone_name), Tz::Tz(tz)); + } + } + + #[test] + fn test_tz_id_rountrip() { + for tz in [ + Tz::Tz(chrono_tz::Tz::America__New_York), + Tz::Tz(chrono_tz::Tz::Europe__Ljubljana), + Tz::Tz(chrono_tz::Tz::UTC), + Tz::Floating, + Tz::Fixed(chrono::FixedOffset::east_opt(14 * 3600).unwrap()), + Tz::Fixed(chrono::FixedOffset::west_opt(14 * 3600).unwrap()), + Tz::Fixed(chrono::FixedOffset::east_opt(3600).unwrap()), + Tz::Fixed(chrono::FixedOffset::west_opt(3600).unwrap()), + ] { + let id = tz.as_id(); + let tz_from_id = Tz::from_id(id).unwrap(); + assert_eq!(tz, tz_from_id, "failed for {tz:?}"); + } + } +} diff --git a/vendor/calcard/src/common/tokenizer.rs b/vendor/calcard/src/common/tokenizer.rs new file mode 100644 index 00000000..f71b8956 --- /dev/null +++ b/vendor/calcard/src/common/tokenizer.rs @@ -0,0 +1,791 @@ +/* + * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC + * + * SPDX-License-Identifier: Apache-2.0 OR MIT + */ + +use crate::{Parser, common::IanaParse}; +use std::borrow::Cow; + +#[derive(Debug, PartialEq, Eq)] +pub(crate) struct Token<'x> { + pub(crate) text: Cow<'x, [u8]>, + pub(crate) start: usize, + pub(crate) end: usize, + pub(crate) stop_char: StopChar, +} + +#[derive(Debug, PartialEq, Eq, Clone, Copy)] +pub(crate) enum StopChar { + Colon, + Semicolon, + Comma, + Equal, + Dot, + Lf, +} + +impl<'x> Parser<'x> { + pub(crate) fn expect_iana_token(&mut self) { + self.stop_colon = true; + self.stop_semicolon = true; + self.stop_comma = true; + self.stop_equal = true; + self.unfold_qp = false; + self.unfold_b64 = false; + self.unquote = true; + self.stop_dot = false; + self.skip_ws = true; + self.strip_ctl = false; + } + + pub(crate) fn expect_single_value(&mut self) { + self.stop_colon = false; + self.stop_comma = false; + self.stop_equal = false; + self.stop_semicolon = false; + self.unquote = false; + self.unfold_qp = false; + self.unfold_b64 = false; + self.stop_dot = false; + self.skip_ws = false; + self.strip_ctl = false; + } + + pub(crate) fn expect_multi_value_comma(&mut self) { + self.stop_colon = false; + self.stop_comma = true; + self.stop_equal = false; + self.stop_semicolon = false; + self.unquote = false; + self.unfold_qp = false; + self.unfold_b64 = false; + self.stop_dot = false; + self.skip_ws = true; + self.strip_ctl = false; + } + + pub(crate) fn expect_multi_value_semicolon(&mut self) { + self.stop_colon = false; + self.stop_comma = false; + self.stop_equal = false; + self.stop_semicolon = true; + self.unquote = false; + self.unfold_qp = false; + self.unfold_b64 = false; + self.stop_dot = false; + self.skip_ws = false; + self.strip_ctl = false; + } + + pub(crate) fn expect_multi_value_semicolon_and_comma(&mut self) { + self.stop_colon = false; + self.stop_comma = true; + self.stop_equal = false; + self.stop_semicolon = true; + self.unquote = false; + self.unfold_qp = false; + self.unfold_b64 = false; + self.stop_dot = false; + self.skip_ws = false; + self.strip_ctl = false; + } + + pub(crate) fn expect_param_value(&mut self) { + self.stop_colon = true; + self.stop_semicolon = true; + self.stop_comma = true; + self.stop_equal = false; + self.unfold_qp = false; + self.unfold_b64 = false; + self.unquote = true; + self.stop_dot = false; + self.skip_ws = true; + self.strip_ctl = true; + } + + pub(crate) fn expect_rrule_value(&mut self) { + self.stop_colon = true; + self.stop_comma = true; + self.stop_equal = true; + self.stop_semicolon = true; + self.unquote = false; + self.unfold_qp = false; + self.unfold_b64 = false; + self.stop_dot = false; + self.skip_ws = true; + self.strip_ctl = false; + } + + fn try_unfold(&mut self) -> bool { + if let Some((_, next)) = self.iter.peek() + && (**next == b' ' || **next == b'\t') + { + self.iter.next(); + return true; + } + false + } + + fn is_base64_continuation(&self, idx: usize) -> bool { + let mut has_data = false; + + for ch in self.input.get(idx + 1..).unwrap_or_default() { + match ch { + b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'+' | b'/' | b'=' => has_data = true, + b'\r' => {} + b'\n' => break, + _ => return false, + } + } + + has_data + } + + pub(crate) fn token(&mut self) -> Option> { + let mut offset_start = usize::MAX; + let mut offset_end = usize::MAX; + let mut last_idx = 0; + let mut in_quote = false; + let stop_char; + let mut buf: Vec = vec![]; + let mut skipped_ws = usize::MAX; + + 'outer: loop { + let (idx, ch) = if let Some(next) = self.iter.next() { + next + } else if offset_start != usize::MAX { + stop_char = StopChar::Lf; + break; + } else { + return None; + }; + last_idx = idx; + + match ch { + b' ' | b'\t' => { + // Ignore leading and trailing whitespace (unless in a quoted string) + if in_quote || !self.skip_ws || buf.last().is_some_and(|lch| lch != ch) { + if offset_start == usize::MAX { + offset_start = idx; + } + offset_end = idx; + skipped_ws = usize::MAX; + + if !buf.is_empty() { + buf.push(*ch); + } + } else if skipped_ws == usize::MAX { + skipped_ws = idx; + } + } + b'\r' => {} + b'\n' => { + if self.unfold_qp + && buf.last().or_else(|| self.input.get(offset_end)).copied() == Some(b'=') + { + offset_end = idx; + + if !buf.is_empty() { + buf.push(*ch); + } + } else if self.try_unfold() { + if buf.is_empty() && offset_start != usize::MAX { + buf.extend_from_slice(&self.input[offset_start..=offset_end]); + } + + if skipped_ws != usize::MAX && !buf.is_empty() { + buf.extend( + self.input[skipped_ws..idx] + .iter() + .filter(|ch| matches!(ch, b' ' | b'\t')), + ); + } + skipped_ws = usize::MAX; + } else if self.unfold_b64 && self.is_base64_continuation(idx) { + if buf.is_empty() && offset_start != usize::MAX { + buf.extend_from_slice(&self.input[offset_start..=offset_end]); + } + skipped_ws = usize::MAX; + } else { + stop_char = StopChar::Lf; + break; + } + } + b'\\' => { + let mut next_ch = b'\\'; + let mut next_offset_end = idx; + while let Some((idx, ch)) = self.iter.next() { + match ch { + b'\t' | b'\r' => {} + b'\n' => { + if self.try_unfold() { + if let Some((idx, ch)) = self.iter.next() { + next_ch = *ch; + next_offset_end = idx; + break; + } + } else { + stop_char = StopChar::Lf; + offset_end = idx - 1; + break 'outer; + } + } + _ => { + next_ch = *ch; + next_offset_end = idx; + break; + } + } + } + if offset_start != usize::MAX { + if buf.is_empty() { + buf.extend_from_slice(&self.input[offset_start..=offset_end]); + } + + if skipped_ws != usize::MAX { + buf.extend( + self.input[skipped_ws..idx] + .iter() + .filter(|ch| matches!(ch, b' ' | b'\t')), + ); + } + } else { + offset_start = next_offset_end; + } + skipped_ws = usize::MAX; + buf.push(match next_ch { + b'n' | b'N' => b'\n', + b'r' | b'R' => b'\r', + _ => next_ch, + }); + offset_end = next_offset_end; + } + b'"' if self.unquote => { + in_quote = !in_quote; + } + b':' if !in_quote && self.stop_colon => { + stop_char = StopChar::Colon; + break; + } + b';' if !in_quote && self.stop_semicolon => { + stop_char = StopChar::Semicolon; + break; + } + b',' if !in_quote && self.stop_comma => { + stop_char = StopChar::Comma; + break; + } + b'=' if !in_quote && self.stop_equal => { + stop_char = StopChar::Equal; + break; + } + b'.' if !in_quote && self.stop_dot => { + stop_char = StopChar::Dot; + break; + } + 0x00..=0x08 | 0x0B | 0x0C | 0x0E..=0x1F | 0x7F if self.strip_ctl => { + if buf.is_empty() && offset_start != usize::MAX { + buf.extend_from_slice(&self.input[offset_start..=offset_end]); + } + } + _ => { + if offset_start == usize::MAX { + offset_start = idx; + } + offset_end = idx; + skipped_ws = usize::MAX; + + if !buf.is_empty() { + buf.push(*ch); + } + } + } + } + + if offset_start != usize::MAX { + self.last_token_end = offset_end; + } + + if buf.is_empty() { + if offset_start != usize::MAX { + Some(Token { + text: Cow::Borrowed(&self.input[offset_start..=offset_end]), + start: offset_start, + end: offset_end, + stop_char, + }) + } else { + Some(Token { + text: Cow::Borrowed(b"".as_ref()), + start: last_idx, + end: last_idx, + stop_char, + }) + } + } else { + Some(Token { + text: Cow::Owned(buf), + start: offset_start, + end: offset_end, + stop_char, + }) + } + } + + #[inline] + pub(crate) fn token_until_lf(&mut self, last_stop_char: &mut StopChar) -> Option> { + if last_stop_char != &StopChar::Lf { + self.token() + .inspect(|token| *last_stop_char = token.stop_char) + } else { + None + } + } + + #[inline] + pub(crate) fn parse_value_until_lf( + &mut self, + separator: StopChar, + last_stop_char: &mut StopChar, + ) -> Option> + where + T: IanaParse + 'static, + { + if *last_stop_char != separator { + self.token_until_lf(last_stop_char) + .map(|token| T::parse(token.text.as_ref())) + } else { + None + } + } + + pub(crate) fn seek_lf(&mut self) -> bool { + loop { + match self.token() { + Some(Token { + stop_char: StopChar::Lf, + .. + }) => return true, + None => return false, + _ => {} + } + } + } + + pub(crate) fn seek_value_or_eol(&mut self) -> StopChar { + loop { + match self.token() { + Some(Token { + stop_char: StopChar::Colon, + .. + }) => return StopChar::Colon, + Some(Token { + stop_char: StopChar::Lf, + .. + }) + | None => return StopChar::Lf, + _ => {} + } + } + } + + pub(crate) fn seek_param_value_or_eol(&mut self) -> StopChar { + loop { + match self.token() { + Some(Token { + stop_char: stop_char @ (StopChar::Colon | StopChar::Semicolon | StopChar::Equal), + .. + }) => return stop_char, + Some(Token { + stop_char: StopChar::Lf, + .. + }) + | None => return StopChar::Lf, + _ => {} + } + } + } +} + +impl<'x> Token<'x> { + pub fn new(text: Cow<'x, [u8]>) -> Self { + Self { + text, + start: 0, + end: 0, + stop_char: StopChar::Lf, + } + } + + pub fn into_string(self) -> String { + String::from_utf8(self.text.into_owned()) + .unwrap_or_else(|err| String::from_utf8_lossy(&err.into_bytes()).into_owned()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[derive(Debug, PartialEq, Eq)] + enum TextOwner { + Borrowed(&'static str), + Owned(String), + } + + #[test] + fn test_tokenizer() { + for (input, expected, disable_stop) in [ + ( + concat!( + "NOTE:This is a long descrip\n", + " tion that exists o\n", + " n a long line.", + ), + vec![ + (TextOwner::Borrowed("NOTE"), StopChar::Colon), + ( + TextOwner::Owned( + "This is a long description that exists on a long line.".into(), + ), + StopChar::Lf, + ), + ], + b"".as_slice(), + ), + ( + concat!( + "this is a text value\n", + "this is one value,this is another\n", + "this is a single value\\, with a comma encoded\n" + ), + vec![ + (TextOwner::Borrowed("this is a text value"), StopChar::Lf), + (TextOwner::Borrowed("this is one value"), StopChar::Comma), + (TextOwner::Borrowed("this is another"), StopChar::Lf), + ( + TextOwner::Owned("this is a single value, with a comma encoded".into()), + StopChar::Lf, + ), + ], + b"".as_slice(), + ), + ( + "N;ALTID=1;LANGUAGE=en:Yamada;Taro;;;", + vec![ + (TextOwner::Borrowed("N"), StopChar::Semicolon), + (TextOwner::Borrowed("ALTID"), StopChar::Equal), + (TextOwner::Borrowed("1"), StopChar::Semicolon), + (TextOwner::Borrowed("LANGUAGE"), StopChar::Equal), + (TextOwner::Borrowed("en"), StopChar::Colon), + (TextOwner::Borrowed("Yamada"), StopChar::Semicolon), + (TextOwner::Borrowed("Taro"), StopChar::Semicolon), + (TextOwner::Borrowed(""), StopChar::Semicolon), + (TextOwner::Borrowed(""), StopChar::Semicolon), + ], + b"".as_slice(), + ), + ( + "N;SORT-AS=\"Mann,James\":de Mann;Henry,James;;", + vec![ + (TextOwner::Borrowed("N"), StopChar::Semicolon), + (TextOwner::Borrowed("SORT-AS"), StopChar::Equal), + (TextOwner::Borrowed("Mann,James"), StopChar::Colon), + (TextOwner::Borrowed("de Mann"), StopChar::Semicolon), + (TextOwner::Borrowed("Henry"), StopChar::Comma), + (TextOwner::Borrowed("James"), StopChar::Semicolon), + (TextOwner::Borrowed(""), StopChar::Semicolon), + ], + b"".as_slice(), + ), + ( + " hello\\ nworld\\\\", + vec![(TextOwner::Owned("hello nworld\\".into()), StopChar::Lf)], + b"".as_slice(), + ), + ( + concat!( + "X-ABC-MMSUBJ;VALUE=URI;FMTTYPE=audio/basic:http://www.example.\n", + " org/mysubj.au" + ), + vec![ + (TextOwner::Borrowed("X-ABC-MMSUBJ"), StopChar::Semicolon), + (TextOwner::Borrowed("VALUE"), StopChar::Equal), + (TextOwner::Borrowed("URI"), StopChar::Semicolon), + (TextOwner::Borrowed("FMTTYPE"), StopChar::Equal), + (TextOwner::Borrowed("audio/basic"), StopChar::Colon), + (TextOwner::Borrowed("http"), StopChar::Colon), + ( + TextOwner::Owned("//www.example.org/mysubj.au".into()), + StopChar::Lf, + ), + ], + b"".as_slice(), + ), + ( + "RDATE;VALUE=DATE:19970304,19970504,19970704,19970904", + vec![ + (TextOwner::Borrowed("RDATE"), StopChar::Semicolon), + (TextOwner::Borrowed("VALUE"), StopChar::Equal), + (TextOwner::Borrowed("DATE"), StopChar::Colon), + (TextOwner::Borrowed("19970304"), StopChar::Comma), + (TextOwner::Borrowed("19970504"), StopChar::Comma), + (TextOwner::Borrowed("19970704"), StopChar::Comma), + (TextOwner::Borrowed("19970904"), StopChar::Lf), + ], + b"".as_slice(), + ), + ( + " BEGIN; ::\n \n \n test", + vec![ + (TextOwner::Borrowed("BEGIN"), StopChar::Semicolon), + (TextOwner::Borrowed(""), StopChar::Colon), + (TextOwner::Borrowed(""), StopChar::Colon), + (TextOwner::Borrowed("test"), StopChar::Lf), + ], + b"".as_slice(), + ), + ( + concat!( + "DESCRIPTION;Sunday - Partly cloudy with a 20 percent chance of snow show", + "ers. Highs in the lower to mid 40s.\\nMore Information" + ), + vec![ + (TextOwner::Borrowed("DESCRIPTION"), StopChar::Semicolon), + ( + TextOwner::Owned( + concat!( + "Sunday - Partly cloudy with a 20 percent ", + "chance of snow showers. Highs in the lower ", + "to mid 40s.\nMore Information" + ) + .into(), + ), + StopChar::Lf, + ), + ], + b"=:".as_slice(), + ), + ( + concat!( + "ATTACH;FMTTYPE=text/plain;ENCODING=BASE64;VALUE=BINARY:VGhlIH\n", + " F1aWNrIGJyb3duIGZveCBqdW1wcyBvdmVyIHRoZSBsYXp5IGRvZy4" + ), + vec![ + (TextOwner::Borrowed("ATTACH"), StopChar::Semicolon), + (TextOwner::Borrowed("FMTTYPE"), StopChar::Equal), + (TextOwner::Borrowed("text/plain"), StopChar::Semicolon), + (TextOwner::Borrowed("ENCODING"), StopChar::Equal), + (TextOwner::Borrowed("BASE64"), StopChar::Semicolon), + (TextOwner::Borrowed("VALUE"), StopChar::Equal), + (TextOwner::Borrowed("BINARY"), StopChar::Colon), + ( + TextOwner::Owned( + "VGhlIHF1aWNrIGJyb3duIGZveCBqdW1wcyBvdmVyIHRoZSBsYXp5IGRvZy4".into(), + ), + StopChar::Lf, + ), + ], + b"".as_slice(), + ), + ( + concat!( + "DESCRIPTION;ALTREP=\"cid:part1.0001@example.org\":The Fall'98 Wild\n", + " Wizards Conference - - Las Vegas\\, NV\\, USA" + ), + vec![ + (TextOwner::Borrowed("DESCRIPTION"), StopChar::Semicolon), + (TextOwner::Borrowed("ALTREP"), StopChar::Equal), + ( + TextOwner::Borrowed("cid:part1.0001@example.org"), + StopChar::Colon, + ), + ( + TextOwner::Owned( + "The Fall'98 WildWizards Conference - - Las Vegas, NV, USA".to_string(), + ), + StopChar::Lf, + ), + ], + b"".as_slice(), + ), + ( + concat!( + "ATTENDEE;DELEGATED-FROM=\"mailto:jsmith@example.com\":mailto:\n", + " jdoe@example.com" + ), + vec![ + (TextOwner::Borrowed("ATTENDEE"), StopChar::Semicolon), + (TextOwner::Borrowed("DELEGATED-FROM"), StopChar::Equal), + ( + TextOwner::Borrowed("mailto:jsmith@example.com"), + StopChar::Colon, + ), + (TextOwner::Borrowed("mailto"), StopChar::Colon), + (TextOwner::Borrowed("jdoe@example.com"), StopChar::Lf), + ], + b"".as_slice(), + ), + ( + concat!( + "ATTENDEE;DELEGATED-TO=\"mailto:jdoe@example.com\",\"mailto:jqpublic\n", + " @example.com\":mailto:jsmith@example.com" + ), + vec![ + (TextOwner::Borrowed("ATTENDEE"), StopChar::Semicolon), + (TextOwner::Borrowed("DELEGATED-TO"), StopChar::Equal), + ( + TextOwner::Borrowed("mailto:jdoe@example.com"), + StopChar::Comma, + ), + ( + TextOwner::Owned("mailto:jqpublic@example.com".into()), + StopChar::Colon, + ), + (TextOwner::Borrowed("mailto"), StopChar::Colon), + (TextOwner::Borrowed("jsmith@example.com"), StopChar::Lf), + ], + b"".as_slice(), + ), + ( + "CATEGORIES:cat1 , cat2, cat3", + vec![ + (TextOwner::Borrowed("CATEGORIES"), StopChar::Colon), + (TextOwner::Borrowed("cat1"), StopChar::Comma), + (TextOwner::Borrowed("cat2"), StopChar::Comma), + (TextOwner::Borrowed("cat3"), StopChar::Lf), + ], + b"".as_slice(), + ), + ( + concat!("SUMMARY:Meeting\n", "\n\n", "BEGIN:VALARM"), + vec![ + (TextOwner::Borrowed("SUMMARY"), StopChar::Colon), + (TextOwner::Borrowed("Meeting"), StopChar::Lf), + (TextOwner::Borrowed(""), StopChar::Lf), + (TextOwner::Borrowed(""), StopChar::Lf), + (TextOwner::Borrowed("BEGIN"), StopChar::Colon), + (TextOwner::Borrowed("VALARM"), StopChar::Lf), + ], + b"".as_slice(), + ), + ( + concat!( + "FN;CHARSET=UTF-8;ENCODING=QUOTED-PRINTABLE:=D0=B3=D0=BE=D1=80=20", + "=D0=97=D0=B0=D0=BE=D1=80=D1=81=D0=81=D0=\n", + "=BA=\n", + "=D1=96=\n", + " xyz =\n", + "=D1=96\n" + ), + vec![ + (TextOwner::Borrowed("FN"), StopChar::Semicolon), + (TextOwner::Borrowed("CHARSET"), StopChar::Equal), + (TextOwner::Borrowed("UTF-8"), StopChar::Semicolon), + (TextOwner::Borrowed("ENCODING"), StopChar::Equal), + (TextOwner::Borrowed("QUOTED-PRINTABLE"), StopChar::Colon), + ( + TextOwner::Borrowed(concat!( + "=D0=B3=D0=BE=D1=80=20=D0=97=D0=B0=D0=BE=D1=80", + "=D1=81=D0=81=D0=\n=BA=\n=D1=96=\n xyz =\n=D1=96" + )), + StopChar::Lf, + ), + ], + b"".as_slice(), + ), + ( + concat!( + "ADR;LABEL=\"Mr. John Q. Public, Esq.\\nMail Drop: TNE QB\\n123\n", + " Main Street\\nAny Town, CA 91921-1234\\nU.S.A.\":\n", + " ;;123 Main Street;Any Town;CA;91921-1234;U.S.A." + ), + vec![ + (TextOwner::Borrowed("ADR"), StopChar::Semicolon), + (TextOwner::Borrowed("LABEL"), StopChar::Equal), + ( + TextOwner::Owned( + concat!( + "Mr. John Q. Public, Esq.\nMail Drop: TNE QB\n", + "123Main Street\nAny Town, CA 91921-1234\nU.S.A." + ) + .into(), + ), + StopChar::Colon, + ), + (TextOwner::Borrowed(""), StopChar::Semicolon), + (TextOwner::Borrowed(""), StopChar::Semicolon), + (TextOwner::Borrowed("123 Main Street"), StopChar::Semicolon), + (TextOwner::Borrowed("Any Town"), StopChar::Semicolon), + (TextOwner::Borrowed("CA"), StopChar::Semicolon), + (TextOwner::Borrowed("91921-1234"), StopChar::Semicolon), + (TextOwner::Borrowed("U.S.A."), StopChar::Lf), + ], + b"".as_slice(), + ), + ( + "\\", + vec![(TextOwner::Owned("\\".into()), StopChar::Lf)], + b"".as_slice(), + ), + ( + "\\n", + vec![(TextOwner::Owned("\n".into()), StopChar::Lf)], + b"".as_slice(), + ), + ( + "\\nhello", + vec![(TextOwner::Owned("\nhello".into()), StopChar::Lf)], + b"".as_slice(), + ), + ( + ";;\nEND:VCARD\n", + vec![ + (TextOwner::Borrowed(""), StopChar::Semicolon), + (TextOwner::Borrowed(""), StopChar::Semicolon), + (TextOwner::Borrowed(""), StopChar::Lf), + (TextOwner::Borrowed("END"), StopChar::Colon), + (TextOwner::Borrowed("VCARD"), StopChar::Lf), + ], + b"".as_slice(), + ), + ("", vec![], b"".as_slice()), + ] { + let mut parser = Parser::new(input); + let mut tokens = vec![]; + parser.skip_ws = true; + for ch in disable_stop { + match ch { + b'=' => { + parser.stop_equal = false; + } + b':' => { + parser.stop_colon = false; + } + _ => {} + } + } + + while let Some(token) = parser.token() { + if token.text.eq_ignore_ascii_case(b"quoted-printable") { + parser.stop_colon = false; + parser.stop_comma = false; + parser.stop_equal = false; + parser.unquote = false; + parser.unfold_qp = true; + parser.stop_semicolon = true; + parser.skip_ws = false; + } + let text = match token.text { + Cow::Borrowed(text) => TextOwner::Borrowed(std::str::from_utf8(text).unwrap()), + Cow::Owned(text) => TextOwner::Owned(String::from_utf8(text).unwrap()), + }; + tokens.push((text, token.stop_char)); + } + assert_eq!(tokens, expected, "failed for input: {:?}", input); + } + } +} diff --git a/vendor/calcard/src/common/types.rs b/vendor/calcard/src/common/types.rs new file mode 100644 index 00000000..b1ced6db --- /dev/null +++ b/vendor/calcard/src/common/types.rs @@ -0,0 +1,451 @@ +/* + * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC + * + * SPDX-License-Identifier: Apache-2.0 OR MIT + */ + +use super::*; + +impl IanaString for CalendarScale { + fn as_str(&self) -> &'static str { + match self { + CalendarScale::Gregorian => "GREGORIAN", + CalendarScale::Chinese => "CHINESE", + CalendarScale::IslamicCivil => "ISLAMIC-CIVIL", + CalendarScale::Hebrew => "HEBREW", + CalendarScale::Ethiopic => "ETHIOPIC", + } + } +} + +#[cfg(feature = "jmap")] +impl CalendarScale { + pub fn as_js_str(&self) -> &'static str { + match self { + CalendarScale::Gregorian => "gregorian", + CalendarScale::Chinese => "chinese", + CalendarScale::IslamicCivil => "islamic-civil", + CalendarScale::Hebrew => "hebrew", + CalendarScale::Ethiopic => "ethiopic", + } + } +} + +#[cfg(feature = "rkyv")] +impl IanaString for ArchivedCalendarScale { + fn as_str(&self) -> &'static str { + match self { + ArchivedCalendarScale::Gregorian => "GREGORIAN", + ArchivedCalendarScale::Chinese => "CHINESE", + ArchivedCalendarScale::IslamicCivil => "ISLAMIC-CIVIL", + ArchivedCalendarScale::Hebrew => "HEBREW", + ArchivedCalendarScale::Ethiopic => "ETHIOPIC", + } + } +} + +impl IanaParse for CalendarScale { + fn parse(value: &[u8]) -> Option { + hashify::tiny_map_ignore_case!(value, + "gregorian" => CalendarScale::Gregorian, + "chinese" => CalendarScale::Chinese, + "islamic-civil" => CalendarScale::IslamicCivil, + "hebrew" => CalendarScale::Hebrew, + "ethiopic" => CalendarScale::Ethiopic, + ) + } +} + +impl Encoding { + pub fn parse(value: &[u8]) -> Option { + hashify::tiny_map_ignore_case!(value, + b"QUOTED-PRINTABLE" => Encoding::QuotedPrintable, + b"BASE64" => Encoding::Base64, + b"Q" => Encoding::QuotedPrintable, + b"B" => Encoding::Base64, + ) + } +} + +impl PartialDateTime { + pub fn now() -> Self { + Self::from_utc_timestamp(chrono::Utc::now().timestamp()) + } + + pub fn from_utc_timestamp(value: i64) -> Self { + let dt = DateTime::from_timestamp(value); + + PartialDateTime { + year: dt.year.into(), + month: dt.month.into(), + day: dt.day.into(), + hour: dt.hour.into(), + minute: dt.minute.into(), + second: dt.second.into(), + tz_hour: dt.tz_hour.into(), + tz_minute: dt.tz_minute.into(), + tz_minus: false, + } + } + + pub fn from_date_timestamp(value: i64) -> Self { + let dt = DateTime::from_timestamp(value); + + PartialDateTime { + year: dt.year.into(), + month: dt.month.into(), + day: dt.day.into(), + ..Default::default() + } + } + + pub fn from_naive_timestamp(value: i64) -> Self { + let dt = DateTime::from_timestamp(value); + + PartialDateTime { + year: dt.year.into(), + month: dt.month.into(), + day: dt.day.into(), + hour: dt.hour.into(), + minute: dt.minute.into(), + second: dt.second.into(), + ..Default::default() + } + } + + pub fn to_date_time(&self) -> Option { + let mut dt = DateTimeResult { + date_time: NaiveDate::from_ymd_opt( + self.year? as i32, + self.month? as u32, + self.day? as u32, + )? + .and_hms_opt( + self.hour.unwrap_or(0) as u32, + self.minute.unwrap_or(0) as u32, + self.second.unwrap_or(0) as u32, + )?, + offset: None, + }; + if let Some(tz_hour) = self.tz_hour { + let secs = (tz_hour as i32 * 3600) + (self.tz_minute.unwrap_or(0) as i32 * 60); + dt.offset = if self.tz_minus { + FixedOffset::west_opt(secs)? + } else { + FixedOffset::east_opt(secs)? + } + .into(); + } + Some(dt) + } +} + +#[cfg(feature = "rkyv")] +impl ArchivedPartialDateTime { + pub fn to_date_time(&self) -> Option { + let mut dt = DateTimeResult { + date_time: NaiveDate::from_ymd_opt( + self.year.as_ref()?.to_native() as i32, + *self.month.as_ref()? as u32, + *self.day.as_ref()? as u32, + )? + .and_hms_opt( + self.hour.unwrap_or(0) as u32, + self.minute.unwrap_or(0) as u32, + self.second.unwrap_or(0) as u32, + )?, + offset: None, + }; + if let Some(tz_hour) = self.tz_hour.as_ref() { + let secs = (*tz_hour as i32 * 3600) + (self.tz_minute.unwrap_or(0) as i32 * 60); + dt.offset = if self.tz_minus { + FixedOffset::west_opt(secs)? + } else { + FixedOffset::east_opt(secs)? + } + .into(); + } + Some(dt) + } +} + +impl IanaParse for LinkRelation { + fn parse(s: &[u8]) -> Option { + hashify::map_ignore_case!(s, LinkRelation, + "about" => LinkRelation::About, + "acl" => LinkRelation::Acl, + "alternate" => LinkRelation::Alternate, + "amphtml" => LinkRelation::Amphtml, + "api-catalog" => LinkRelation::ApiCatalog, + "appendix" => LinkRelation::Appendix, + "apple-touch-icon" => LinkRelation::AppleTouchIcon, + "apple-touch-startup-image" => LinkRelation::AppleTouchStartupImage, + "archives" => LinkRelation::Archives, + "author" => LinkRelation::Author, + "blocked-by" => LinkRelation::BlockedBy, + "bookmark" => LinkRelation::Bookmark, + "c2pa-manifest" => LinkRelation::C2paManifest, + "canonical" => LinkRelation::Canonical, + "chapter" => LinkRelation::Chapter, + "cite-as" => LinkRelation::CiteAs, + "collection" => LinkRelation::Collection, + "compression-dictionary" => LinkRelation::CompressionDictionary, + "contents" => LinkRelation::Contents, + "convertedfrom" => LinkRelation::Convertedfrom, + "copyright" => LinkRelation::Copyright, + "create-form" => LinkRelation::CreateForm, + "current" => LinkRelation::Current, + "deprecation" => LinkRelation::Deprecation, + "describedby" => LinkRelation::Describedby, + "describes" => LinkRelation::Describes, + "disclosure" => LinkRelation::Disclosure, + "dns-prefetch" => LinkRelation::DnsPrefetch, + "duplicate" => LinkRelation::Duplicate, + "edit" => LinkRelation::Edit, + "edit-form" => LinkRelation::EditForm, + "edit-media" => LinkRelation::EditMedia, + "enclosure" => LinkRelation::Enclosure, + "external" => LinkRelation::External, + "first" => LinkRelation::First, + "geofeed" => LinkRelation::Geofeed, + "glossary" => LinkRelation::Glossary, + "help" => LinkRelation::Help, + "hosts" => LinkRelation::Hosts, + "hub" => LinkRelation::Hub, + "ice-server" => LinkRelation::IceServer, + "icon" => LinkRelation::Icon, + "index" => LinkRelation::Index, + "intervalafter" => LinkRelation::Intervalafter, + "intervalbefore" => LinkRelation::Intervalbefore, + "intervalcontains" => LinkRelation::Intervalcontains, + "intervaldisjoint" => LinkRelation::Intervaldisjoint, + "intervalduring" => LinkRelation::Intervalduring, + "intervalequals" => LinkRelation::Intervalequals, + "intervalfinishedby" => LinkRelation::Intervalfinishedby, + "intervalfinishes" => LinkRelation::Intervalfinishes, + "intervalin" => LinkRelation::Intervalin, + "intervalmeets" => LinkRelation::Intervalmeets, + "intervalmetby" => LinkRelation::Intervalmetby, + "intervaloverlappedby" => LinkRelation::Intervaloverlappedby, + "intervaloverlaps" => LinkRelation::Intervaloverlaps, + "intervalstartedby" => LinkRelation::Intervalstartedby, + "intervalstarts" => LinkRelation::Intervalstarts, + "item" => LinkRelation::Item, + "last" => LinkRelation::Last, + "latest-version" => LinkRelation::LatestVersion, + "license" => LinkRelation::License, + "linkset" => LinkRelation::Linkset, + "lrdd" => LinkRelation::Lrdd, + "manifest" => LinkRelation::Manifest, + "mask-icon" => LinkRelation::MaskIcon, + "me" => LinkRelation::Me, + "media-feed" => LinkRelation::MediaFeed, + "memento" => LinkRelation::Memento, + "micropub" => LinkRelation::Micropub, + "modulepreload" => LinkRelation::Modulepreload, + "monitor" => LinkRelation::Monitor, + "monitor-group" => LinkRelation::MonitorGroup, + "next" => LinkRelation::Next, + "next-archive" => LinkRelation::NextArchive, + "nofollow" => LinkRelation::Nofollow, + "noopener" => LinkRelation::Noopener, + "noreferrer" => LinkRelation::Noreferrer, + "opener" => LinkRelation::Opener, + "openid2.local_id" => LinkRelation::Openid2LocalId, + "openid2.provider" => LinkRelation::Openid2Provider, + "original" => LinkRelation::Original, + "p3pv1" => LinkRelation::P3pv1, + "payment" => LinkRelation::Payment, + "pingback" => LinkRelation::Pingback, + "preconnect" => LinkRelation::Preconnect, + "predecessor-version" => LinkRelation::PredecessorVersion, + "prefetch" => LinkRelation::Prefetch, + "preload" => LinkRelation::Preload, + "prerender" => LinkRelation::Prerender, + "prev" => LinkRelation::Prev, + "preview" => LinkRelation::Preview, + "previous" => LinkRelation::Previous, + "prev-archive" => LinkRelation::PrevArchive, + "privacy-policy" => LinkRelation::PrivacyPolicy, + "profile" => LinkRelation::Profile, + "publication" => LinkRelation::Publication, + "rdap-active" => LinkRelation::RdapActive, + "rdap-bottom" => LinkRelation::RdapBottom, + "rdap-down" => LinkRelation::RdapDown, + "rdap-top" => LinkRelation::RdapTop, + "rdap-up" => LinkRelation::RdapUp, + "related" => LinkRelation::Related, + "restconf" => LinkRelation::Restconf, + "replies" => LinkRelation::Replies, + "ruleinput" => LinkRelation::Ruleinput, + "search" => LinkRelation::Search, + "section" => LinkRelation::Section, + "self" => LinkRelation::Self_, + "service" => LinkRelation::Service, + "service-desc" => LinkRelation::ServiceDesc, + "service-doc" => LinkRelation::ServiceDoc, + "service-meta" => LinkRelation::ServiceMeta, + "sip-trunking-capability" => LinkRelation::SipTrunkingCapability, + "sponsored" => LinkRelation::Sponsored, + "start" => LinkRelation::Start, + "status" => LinkRelation::Status, + "stylesheet" => LinkRelation::Stylesheet, + "subsection" => LinkRelation::Subsection, + "successor-version" => LinkRelation::SuccessorVersion, + "sunset" => LinkRelation::Sunset, + "tag" => LinkRelation::Tag, + "terms-of-service" => LinkRelation::TermsOfService, + "timegate" => LinkRelation::Timegate, + "timemap" => LinkRelation::Timemap, + "type" => LinkRelation::Type, + "ugc" => LinkRelation::Ugc, + "up" => LinkRelation::Up, + "version-history" => LinkRelation::VersionHistory, + "via" => LinkRelation::Via, + "webmention" => LinkRelation::Webmention, + "working-copy" => LinkRelation::WorkingCopy, + "working-copy-of" => LinkRelation::WorkingCopyOf, + ) + .copied() + } +} + +impl IanaString for LinkRelation { + fn as_str(&self) -> &'static str { + match self { + LinkRelation::About => "about", + LinkRelation::Acl => "acl", + LinkRelation::Alternate => "alternate", + LinkRelation::Amphtml => "amphtml", + LinkRelation::ApiCatalog => "api-catalog", + LinkRelation::Appendix => "appendix", + LinkRelation::AppleTouchIcon => "apple-touch-icon", + LinkRelation::AppleTouchStartupImage => "apple-touch-startup-image", + LinkRelation::Archives => "archives", + LinkRelation::Author => "author", + LinkRelation::BlockedBy => "blocked-by", + LinkRelation::Bookmark => "bookmark", + LinkRelation::C2paManifest => "c2pa-manifest", + LinkRelation::Canonical => "canonical", + LinkRelation::Chapter => "chapter", + LinkRelation::CiteAs => "cite-as", + LinkRelation::Collection => "collection", + LinkRelation::CompressionDictionary => "compression-dictionary", + LinkRelation::Contents => "contents", + LinkRelation::Convertedfrom => "convertedfrom", + LinkRelation::Copyright => "copyright", + LinkRelation::CreateForm => "create-form", + LinkRelation::Current => "current", + LinkRelation::Deprecation => "deprecation", + LinkRelation::Describedby => "describedby", + LinkRelation::Describes => "describes", + LinkRelation::Disclosure => "disclosure", + LinkRelation::DnsPrefetch => "dns-prefetch", + LinkRelation::Duplicate => "duplicate", + LinkRelation::Edit => "edit", + LinkRelation::EditForm => "edit-form", + LinkRelation::EditMedia => "edit-media", + LinkRelation::Enclosure => "enclosure", + LinkRelation::External => "external", + LinkRelation::First => "first", + LinkRelation::Geofeed => "geofeed", + LinkRelation::Glossary => "glossary", + LinkRelation::Help => "help", + LinkRelation::Hosts => "hosts", + LinkRelation::Hub => "hub", + LinkRelation::IceServer => "ice-server", + LinkRelation::Icon => "icon", + LinkRelation::Index => "index", + LinkRelation::Intervalafter => "intervalafter", + LinkRelation::Intervalbefore => "intervalbefore", + LinkRelation::Intervalcontains => "intervalcontains", + LinkRelation::Intervaldisjoint => "intervaldisjoint", + LinkRelation::Intervalduring => "intervalduring", + LinkRelation::Intervalequals => "intervalequals", + LinkRelation::Intervalfinishedby => "intervalfinishedby", + LinkRelation::Intervalfinishes => "intervalfinishes", + LinkRelation::Intervalin => "intervalin", + LinkRelation::Intervalmeets => "intervalmeets", + LinkRelation::Intervalmetby => "intervalmetby", + LinkRelation::Intervaloverlappedby => "intervaloverlappedby", + LinkRelation::Intervaloverlaps => "intervaloverlaps", + LinkRelation::Intervalstartedby => "intervalstartedby", + LinkRelation::Intervalstarts => "intervalstarts", + LinkRelation::Item => "item", + LinkRelation::Last => "last", + LinkRelation::LatestVersion => "latest-version", + LinkRelation::License => "license", + LinkRelation::Linkset => "linkset", + LinkRelation::Lrdd => "lrdd", + LinkRelation::Manifest => "manifest", + LinkRelation::MaskIcon => "mask-icon", + LinkRelation::Me => "me", + LinkRelation::MediaFeed => "media-feed", + LinkRelation::Memento => "memento", + LinkRelation::Micropub => "micropub", + LinkRelation::Modulepreload => "modulepreload", + LinkRelation::Monitor => "monitor", + LinkRelation::MonitorGroup => "monitor-group", + LinkRelation::Next => "next", + LinkRelation::NextArchive => "next-archive", + LinkRelation::Nofollow => "nofollow", + LinkRelation::Noopener => "noopener", + LinkRelation::Noreferrer => "noreferrer", + LinkRelation::Opener => "opener", + LinkRelation::Openid2LocalId => "openid2.local_id", + LinkRelation::Openid2Provider => "openid2.provider", + LinkRelation::Original => "original", + LinkRelation::P3pv1 => "p3pv1", + LinkRelation::Payment => "payment", + LinkRelation::Pingback => "pingback", + LinkRelation::Preconnect => "preconnect", + LinkRelation::PredecessorVersion => "predecessor-version", + LinkRelation::Prefetch => "prefetch", + LinkRelation::Preload => "preload", + LinkRelation::Prerender => "prerender", + LinkRelation::Prev => "prev", + LinkRelation::Preview => "preview", + LinkRelation::Previous => "previous", + LinkRelation::PrevArchive => "prev-archive", + LinkRelation::PrivacyPolicy => "privacy-policy", + LinkRelation::Profile => "profile", + LinkRelation::Publication => "publication", + LinkRelation::RdapActive => "rdap-active", + LinkRelation::RdapBottom => "rdap-bottom", + LinkRelation::RdapDown => "rdap-down", + LinkRelation::RdapTop => "rdap-top", + LinkRelation::RdapUp => "rdap-up", + LinkRelation::Related => "related", + LinkRelation::Restconf => "restconf", + LinkRelation::Replies => "replies", + LinkRelation::Ruleinput => "ruleinput", + LinkRelation::Search => "search", + LinkRelation::Section => "section", + LinkRelation::Self_ => "self", + LinkRelation::Service => "service", + LinkRelation::ServiceDesc => "service-desc", + LinkRelation::ServiceDoc => "service-doc", + LinkRelation::ServiceMeta => "service-meta", + LinkRelation::SipTrunkingCapability => "sip-trunking-capability", + LinkRelation::Sponsored => "sponsored", + LinkRelation::Start => "start", + LinkRelation::Status => "status", + LinkRelation::Stylesheet => "stylesheet", + LinkRelation::Subsection => "subsection", + LinkRelation::SuccessorVersion => "successor-version", + LinkRelation::Sunset => "sunset", + LinkRelation::Tag => "tag", + LinkRelation::TermsOfService => "terms-of-service", + LinkRelation::Timegate => "timegate", + LinkRelation::Timemap => "timemap", + LinkRelation::Type => "type", + LinkRelation::Ugc => "ugc", + LinkRelation::Up => "up", + LinkRelation::VersionHistory => "version-history", + LinkRelation::Via => "via", + LinkRelation::Webmention => "webmention", + LinkRelation::WorkingCopy => "working-copy", + LinkRelation::WorkingCopyOf => "working-copy-of", + } + } +} diff --git a/vendor/calcard/src/common/writer.rs b/vendor/calcard/src/common/writer.rs new file mode 100644 index 00000000..be4f9dd8 --- /dev/null +++ b/vendor/calcard/src/common/writer.rs @@ -0,0 +1,276 @@ +/* + * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC + * + * SPDX-License-Identifier: Apache-2.0 OR MIT + */ + +use super::parser::Timestamp; +use crate::vcard::Jscomp; +use mail_builder::encoders::base64::*; +use mail_parser::DateTime; +use std::fmt::{Display, Write}; + +pub(crate) const MAX_LINE_LEN: usize = 75; + +pub(crate) trait LineWriter: Write { + fn write_atomic(&mut self, text: &str) -> std::fmt::Result; +} + +pub(crate) struct FoldingWriter<'x, W: Write> { + out: &'x mut W, + line_len: usize, +} + +impl<'x, W: Write> FoldingWriter<'x, W> { + pub(crate) fn new(out: &'x mut W) -> Self { + Self { out, line_len: 0 } + } + + pub(crate) fn end_line(&mut self) -> std::fmt::Result { + self.line_len = 0; + self.out.write_str("\r\n") + } +} + +impl LineWriter for FoldingWriter<'_, W> { + fn write_atomic(&mut self, text: &str) -> std::fmt::Result { + let len = text.len(); + if len > MAX_LINE_LEN - 1 { + return self.write_str(text); + } + if self.line_len + len > MAX_LINE_LEN { + self.out.write_str("\r\n ")?; + self.line_len = 1; + } + self.out.write_str(text)?; + self.line_len += len; + Ok(()) + } +} + +impl LineWriter for String { + fn write_atomic(&mut self, text: &str) -> std::fmt::Result { + self.push_str(text); + Ok(()) + } +} + +impl Write for FoldingWriter<'_, W> { + fn write_str(&mut self, text: &str) -> std::fmt::Result { + let mut rest = text; + + loop { + let available = MAX_LINE_LEN.saturating_sub(self.line_len); + + if rest.len() <= available { + self.line_len += rest.len(); + return self.out.write_str(rest); + } + + let mut split = available; + while split > 0 && !rest.is_char_boundary(split) { + split -= 1; + } + + if split == 0 { + self.out.write_str("\r\n ")?; + self.line_len = 1; + continue; + } + + self.out.write_str(&rest[..split])?; + self.line_len += split; + rest = &rest[split..]; + } + } + + fn write_char(&mut self, ch: char) -> std::fmt::Result { + let ch_len = ch.len_utf8(); + if self.line_len + ch_len > MAX_LINE_LEN { + self.out.write_str("\r\n ")?; + self.line_len = 1; + } + self.line_len += ch_len; + self.out.write_char(ch) + } +} + +pub(crate) fn write_text( + out: &mut impl LineWriter, + value: &str, + escape_semicolon: bool, + escape_comma: bool, +) -> std::fmt::Result { + for ch in value.chars() { + match ch { + '\r' => out.write_atomic("\\r")?, + '\n' => out.write_atomic("\\n")?, + '\\' => out.write_atomic("\\\\")?, + ';' if escape_semicolon => out.write_atomic("\\;")?, + ',' if escape_comma => out.write_atomic("\\,")?, + _ => out.write_char(ch)?, + } + } + + Ok(()) +} + +pub(crate) fn write_bytes(out: &mut impl Write, value: &[u8]) -> std::fmt::Result { + const CHARPAD: u8 = b'='; + + let mut i = 0; + let mut t1; + let mut t2; + let mut t3; + + if value.len() > 2 { + while i < value.len() - 2 { + t1 = value[i]; + t2 = value[i + 1]; + t3 = value[i + 2]; + + for ch in [ + E0[t1 as usize], + E1[(((t1 & 0x03) << 4) | ((t2 >> 4) & 0x0F)) as usize], + E1[(((t2 & 0x0F) << 2) | ((t3 >> 6) & 0x03)) as usize], + E2[t3 as usize], + ] { + out.write_char(char::from(ch))?; + } + + i += 3; + } + } + + let remaining = value.len() - i; + if remaining > 0 { + t1 = value[i]; + let chs = if remaining == 1 { + [ + E0[t1 as usize], + E1[((t1 & 0x03) << 4) as usize], + CHARPAD, + CHARPAD, + ] + } else { + t2 = value[i + 1]; + [ + E0[t1 as usize], + E1[(((t1 & 0x03) << 4) | ((t2 >> 4) & 0x0F)) as usize], + E2[((t2 & 0x0F) << 2) as usize], + CHARPAD, + ] + }; + + for ch in chs.iter() { + out.write_char(char::from(*ch))?; + } + } + + Ok(()) +} + +pub(crate) trait NeedsQuotes { + fn needs_quotes(&self) -> bool; +} + +impl> NeedsQuotes for T { + fn needs_quotes(&self) -> bool { + self.as_ref() + .iter() + .any(|&ch| matches!(ch, b',' | b':' | b'=' | b' ' | b';' | b'"')) + } +} + +pub(crate) fn write_param_value(out: &mut impl LineWriter, value: &str) -> std::fmt::Result { + let needs_quotes = value.needs_quotes(); + + if needs_quotes { + out.write_atomic("\"")?; + } + + for ch in value.chars() { + match ch as u32 { + 0x0A => out.write_atomic("\\n")?, + 0x0D => out.write_atomic("\\r")?, + 0x5C => out.write_atomic("\\\\")?, + 0x22 => out.write_atomic("\\\"")?, + 0x20 | 0x09 | 0x21 | 0x23..=0x7E | 0x80.. => out.write_char(ch)?, + _ => {} + } + } + + if needs_quotes { + out.write_atomic("\"")?; + } + + Ok(()) +} + +pub(crate) fn write_jscomps(out: &mut impl LineWriter, values: &[Jscomp]) -> std::fmt::Result { + for (pos, item) in values.iter().enumerate() { + if pos > 0 { + out.write_atomic(";")?; + } + match item { + Jscomp::Entry { position, value } => { + write!(out, "{position}")?; + if *value > 0 { + write!(out, ",{value}")?; + } + } + Jscomp::Separator(s) => { + if !s.is_empty() { + out.write_atomic("s,")?; + + for ch in s.chars() { + match ch { + '\\' => out.write_atomic("\\\\")?, + ',' => out.write_atomic("\\,")?, + ':' => out.write_atomic("\\:")?, + '=' => out.write_atomic("\\=")?, + ';' => out.write_atomic("\\;")?, + '"' => out.write_atomic("\\\"")?, + '\r' | '\n' => {} + _ => out.write_char(ch)?, + } + } + } + } + } + } + + Ok(()) +} + +#[cfg(test)] +pub(crate) fn assert_fold_width(text: &str, context: &str) { + let mut lines = text.split("\r\n").peekable(); + + while let Some(line) = lines.next() { + assert!( + line.len() <= MAX_LINE_LEN, + "physical line of {} octets exceeds the {MAX_LINE_LEN} octet fold width in {context}: {line:?}", + line.len() + ); + assert!( + !line.is_empty() || lines.peek().is_none(), + "empty physical line in {context}: {text:?}" + ); + assert!( + line != " ", + "continuation line holding nothing but the fold character in {context}: {text:?}" + ); + } +} + +impl Display for Timestamp { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let dt = DateTime::from_timestamp(self.0); + write!( + f, + "{:04}{:02}{:02}T{:02}{:02}{:02}Z", + dt.year, dt.month, dt.day, dt.hour, dt.minute, dt.second + ) + } +} diff --git a/vendor/calcard/src/datecalc/checks.rs b/vendor/calcard/src/datecalc/checks.rs new file mode 100644 index 00000000..d208964e --- /dev/null +++ b/vendor/calcard/src/datecalc/checks.rs @@ -0,0 +1,20 @@ +/* + * SPDX-FileCopyrightText: 2021 Fredrik Meringdal, Ralph Bisschops + * + * SPDX-License-Identifier: Apache-2.0 OR MIT + */ + +use super::{error::ValidationError, validate::YEAR_RANGE}; + +pub(crate) fn check_year_range(year: i32) -> Result<(), ValidationError> { + if YEAR_RANGE.contains(&year) { + Ok(()) + } else { + Err(ValidationError::InvalidFieldValueRange { + field: "YEAR".into(), + value: year.to_string(), + start_idx: YEAR_RANGE.start().to_string(), + end_idx: YEAR_RANGE.end().to_string(), + }) + } +} diff --git a/vendor/calcard/src/datecalc/counter_date.rs b/vendor/calcard/src/datecalc/counter_date.rs new file mode 100644 index 00000000..9c28c2d8 --- /dev/null +++ b/vendor/calcard/src/datecalc/counter_date.rs @@ -0,0 +1,755 @@ +/* + * SPDX-FileCopyrightText: 2021 Fredrik Meringdal, Ralph Bisschops + * + * SPDX-License-Identifier: Apache-2.0 OR MIT + */ + +use super::{ + checks, + error::{RRuleError, checked_add_u32, checked_mul_u32}, + masks::MASKS, + rrule::RRule, + utils::is_leap_year, +}; +use crate::{common::timezone::Tz, icalendar::ICalendarFrequency}; +use chrono::{Datelike, TimeZone, Timelike, Utc, Weekday}; +use std::collections::HashSet; + +const MINUTES_IN_A_DAY: u32 = 60 * 24; +const SECONDS_IN_A_DAY: u32 = 60 * 60 * 24; + +/// A simple date time type used during iteration. +#[derive(Debug, Clone, PartialEq)] +pub(crate) struct DateTimeIter { + pub year: i32, + pub month: u32, + pub day: u32, + pub hour: u32, + pub minute: u32, + pub second: u32, +} + +impl DateTimeIter { + /// Increments the datetime based on the [`RRule`] values. + /// + /// If `increment_day` is set to `true`, then the method will also make sure that + /// the day is incremented. This is useful is the case where `rrule.freq` + /// is higher than daily (e.g. hourly) where this function might return a date with the + /// same day, but the iterator already knows that the current day can't + /// be part of the result. + pub fn increment(&mut self, rrule: &RRule, increment_day: bool) -> Result<(), RRuleError> { + let RRule { + interval, + week_start, + by_hour, + by_minute, + by_second, + .. + } = rrule; + match rrule.freq { + ICalendarFrequency::Yearly => self.increment_yearly(*interval), + ICalendarFrequency::Monthly => self.increment_monthly(*interval), + ICalendarFrequency::Weekly => self.increment_weekly(*interval, *week_start), + ICalendarFrequency::Daily => self.increment_daily(*interval), + ICalendarFrequency::Hourly => self.increment_hourly(*interval, by_hour, increment_day), + ICalendarFrequency::Minutely => { + self.increment_minutely(*interval, by_hour, by_minute, increment_day) + } + ICalendarFrequency::Secondly => { + self.increment_secondly(*interval, by_hour, by_minute, by_second, increment_day) + } + } + } + + fn increment_yearly(&mut self, interval: u16) -> Result<(), RRuleError> { + self.year += i32::from(interval); + checks::check_year_range(self.year)?; + self.fix_day() + } + + fn increment_monthly(&mut self, interval: u16) -> Result<(), RRuleError> { + self.month += u32::from(interval); + if self.month > 12 { + let mut year_div = u16::try_from(self.month).map_err(|_| { + RRuleError::new_iter_err( + "Encountered a too high new month. Please decrease the rrule interval.", + ) + })? / 12; + self.month %= 12; + if self.month == 0 { + self.month = 12; + year_div -= 1; + } + self.year += i32::from(year_div); + checks::check_year_range(self.year)?; + } + Ok(()) + } + + fn get_weekday(&self) -> u32 { + let month_range_mask = if is_leap_year(self.year) { + &MASKS.month_366_range + } else { + &MASKS.month_365_range + }; + let year_day = u32::from(month_range_mask[self.month as usize - 1]) + self.day - 1; + let year_day_mod = year_day % 7; + let year_start_weekday = Utc + .with_ymd_and_hms(self.year, 1, 1, 0, 0, 0) + // It should never fail, since there is always a 1st of January, is there? + .unwrap() + .weekday() + .num_days_from_monday(); + + (year_day_mod + year_start_weekday) % 7 + } + + fn increment_weekly(&mut self, interval: u16, week_start: Weekday) -> Result<(), RRuleError> { + let weekday = self.get_weekday(); + let option_week_start = week_start.num_days_from_monday(); + let interval = u32::from(interval); + // Calculate the amount of day to move forward. + let day_delta = if option_week_start > weekday { + // `weekday` and `option_week_start` can only be in range `0..=6` + // `option_week_start` > `weekday` so: + // `(weekday + 1 + 6 - option_week_start)` > 0 so can never be negative. + interval * 7 - (weekday + 7 - option_week_start) + } else { + // This can also never be negative + interval * 7 - (weekday - option_week_start) + }; + self.day += day_delta; + self.fix_day() + } + + fn days_in_month(&self) -> u32 { + if self.month == 0 { + 31 + } else { + let month_range_mask = if is_leap_year(self.year) { + &MASKS.month_366_range + } else { + &MASKS.month_365_range + }; + let days_in_month = + month_range_mask[self.month as usize] - month_range_mask[self.month as usize - 1]; + u32::from(days_in_month) + } + } + + fn fix_day(&mut self) -> Result<(), RRuleError> { + if self.day <= 28 { + return Ok(()); + } + + let mut days_in_month = self.days_in_month(); + if self.day <= days_in_month { + return Ok(()); + } + + while self.day > days_in_month { + self.day -= days_in_month; + self.increment_monthly(1)?; + + days_in_month = self.days_in_month(); + } + Ok(()) + } + + fn increment_daily(&mut self, interval: u16) -> Result<(), RRuleError> { + self.day += u32::from(interval); + self.fix_day() + } + + fn increment_hourly( + &mut self, + interval: u16, + by_hour: &[u8], + increment_day: bool, + ) -> Result<(), RRuleError> { + if increment_day { + // Jump to one iteration before the next day + let temp_value = (23 - self.hour) / u32::from(interval); + self.hour += checked_mul_u32( + temp_value, + u32::from(interval), + Some("please decrease `INTERVAL`"), + )?; + } + + let mut prev_hours = HashSet::new(); + loop { + self.hour = checked_add_u32( + self.hour, + u32::from(interval), + Some("please decrease `INTERVAL`"), + )?; + let new_hours = (self.hour % 24) as u8; + if by_hour.is_empty() || by_hour.contains(&new_hours) { + break; + } + if prev_hours.contains(&new_hours) { + return Err(RRuleError::new_iter_err( + "Infinite loop detected. It can be resolved by changing `BYHOUR` or `INTERVAL`", + )); + } + prev_hours.insert(new_hours); + } + + let new_days = u16::try_from(self.hour / 24).map_err(|_| { + RRuleError::new_iter_err("unexpected high hour, please decrease the interval") + })?; + self.hour %= 24; + if new_days > 0 { + self.increment_daily(new_days) + } else { + Ok(()) + } + } + + fn increment_minutely( + &mut self, + interval: u16, + by_hour: &[u8], + by_minute: &[u8], + increment_day: bool, + ) -> Result<(), RRuleError> { + if increment_day { + // Jump to one iteration before the next day + let minutes_total = self.hour * 60 + self.minute; + let temp_value = (MINUTES_IN_A_DAY - 1 - minutes_total) / u32::from(interval); + self.minute += checked_mul_u32( + temp_value, + u32::from(interval), + Some("please decrease `INTERVAL`"), + )?; + } + + let mut prev_values = HashSet::new(); + loop { + self.minute += u32::from(interval); + let hours_div = u16::try_from(self.minute / 60).map_err(|_| { + RRuleError::new_iter_err("unexpected high minute, please decrease interval") + })?; + if hours_div > 0 { + self.minute %= 60; + self.increment_hourly(hours_div, by_hour, increment_day)?; + } + + let hours = (self.hour % 24) as u8; + let minutes = (self.minute % 60) as u8; + + if (by_hour.is_empty() || by_hour.contains(&hours)) + && (by_minute.is_empty() || by_minute.contains(&minutes)) + { + break; + } + + if prev_values.contains(&(hours, minutes)) { + return Err(RRuleError::new_iter_err( + "Infinite loop detected. It can be resolved by changing `BYMINUTE`, `BYHOUR` or `INTERVAL`", + )); + } + prev_values.insert((hours, minutes)); + } + + Ok(()) + } + + fn increment_secondly( + &mut self, + interval: u16, + by_hour: &[u8], + by_minute: &[u8], + by_second: &[u8], + increment_day: bool, + ) -> Result<(), RRuleError> { + if increment_day { + // Jump to one iteration before the next day + let total_seconds = self.hour * 3600 + self.minute * 60 + self.second; + let temp_value = (SECONDS_IN_A_DAY - 1 - total_seconds) / u32::from(interval); + self.second += checked_mul_u32( + temp_value, + u32::from(interval), + Some("please decrease `INTERVAL`"), + )?; + } + + let mut prev_values = HashSet::new(); + loop { + self.second += u32::from(interval); + let minutes_div = u16::try_from(self.second / 60).map_err(|_| { + RRuleError::new_iter_err("unexpected high second, please decrease interval") + })?; + if minutes_div > 0 { + self.second %= 60; + self.increment_minutely(minutes_div, by_hour, by_minute, increment_day)?; + } + + let hours = (self.hour % 24) as u8; + let minutes = (self.minute % 60) as u8; + let seconds = (self.second % 60) as u8; + + if (by_hour.is_empty() || by_hour.contains(&hours)) + && (by_minute.is_empty() || by_minute.contains(&minutes)) + && (by_second.is_empty() || by_second.contains(&seconds)) + { + break; + } + + if prev_values.contains(&(hours, minutes, seconds)) { + return Err(RRuleError::new_iter_err( + "Infinite loop detected. It can be resolved by changing `BYSECOND`, `BYMINUTE`, `BYHOUR` or `INTERVAL`", + )); + } + prev_values.insert((hours, minutes, seconds)); + } + + Ok(()) + } +} + +impl From<&chrono::DateTime> for DateTimeIter { + fn from(dt: &chrono::DateTime) -> Self { + Self { + year: dt.year(), + month: dt.month(), + day: dt.day(), + hour: dt.hour(), + minute: dt.minute(), + second: dt.second(), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use chrono::TimeZone; + + const UTC: Tz = Tz::Tz(chrono_tz::Tz::UTC); + + fn ymd_hms(year: i32, month: u32, day: u32, hour: u32, min: u32, sec: u32) -> DateTimeIter { + let dt = UTC + .with_ymd_and_hms(year, month, day, hour, min, sec) + .unwrap(); + DateTimeIter::from(&dt) + } + + #[test] + fn increments_counter_date_with_yearly_freq() { + let tests = [ + ( + ymd_hms(1997, 1, 1, 0, 0, 0), + 1, + ymd_hms(1998, 1, 1, 0, 0, 0), + ), + ( + ymd_hms(2020, 2, 29, 0, 0, 0), + 1, + ymd_hms(2021, 3, 1, 0, 0, 0), + ), + ( + ymd_hms(1997, 1, 1, 0, 0, 0), + 3, + ymd_hms(2000, 1, 1, 0, 0, 0), + ), + ( + ymd_hms(1997, 1, 1, 0, 0, 0), + 20, + ymd_hms(2017, 1, 1, 0, 0, 0), + ), + ]; + for (mut counter_date, interval, expected_output) in tests { + let rrule = RRule { + interval, + freq: ICalendarFrequency::Yearly, + ..Default::default() + } + .validate(UTC.with_ymd_and_hms(1997, 1, 1, 1, 1, 1).unwrap()) + .unwrap(); + + let res = counter_date.increment(&rrule, false); + assert!(res.is_ok()); + assert_eq!(counter_date, expected_output); + } + } + + #[test] + fn increments_counter_date_with_monthly_freq() { + let tests = [ + ( + ymd_hms(1997, 1, 1, 0, 0, 0), + 1, + ymd_hms(1997, 2, 1, 0, 0, 0), + ), + ( + ymd_hms(1997, 1, 1, 0, 0, 0), + 11, + ymd_hms(1997, 12, 1, 0, 0, 0), + ), + ( + ymd_hms(1997, 1, 1, 0, 0, 0), + 12, + ymd_hms(1998, 1, 1, 0, 0, 0), + ), + ( + ymd_hms(1997, 12, 1, 0, 0, 0), + 1, + ymd_hms(1998, 1, 1, 0, 0, 0), + ), + ( + ymd_hms(1997, 11, 1, 0, 0, 0), + 12 * 10, + ymd_hms(2007, 11, 1, 0, 0, 0), + ), + ]; + for (mut counter_date, interval, expected_output) in tests { + let rrule = RRule { + interval, + freq: ICalendarFrequency::Monthly, + ..Default::default() + } + .validate(UTC.with_ymd_and_hms(1997, 1, 1, 1, 1, 1).unwrap()) + .unwrap(); + + let res = counter_date.increment(&rrule, false); + assert!(res.is_ok()); + assert_eq!(counter_date, expected_output); + } + } + + #[test] + fn increments_counter_date_with_weekly_freq() { + let tests = [ + ( + ymd_hms(1997, 1, 1, 0, 0, 0), + 1, + ymd_hms(1997, 1, 6, 0, 0, 0), + ), + ( + ymd_hms(1997, 1, 1, 0, 0, 0), + 2, + ymd_hms(1997, 1, 13, 0, 0, 0), + ), + ( + ymd_hms(1997, 1, 1, 0, 0, 0), + 5, + ymd_hms(1997, 2, 3, 0, 0, 0), + ), + ( + ymd_hms(1997, 1, 1, 0, 0, 0), + 52, + ymd_hms(1997, 12, 29, 0, 0, 0), + ), + ( + ymd_hms(1997, 1, 1, 0, 0, 0), + 53, + ymd_hms(1998, 1, 5, 0, 0, 0), + ), + ]; + for (mut counter_date, interval, expected_output) in tests { + let rrule = RRule { + interval, + freq: ICalendarFrequency::Weekly, + ..Default::default() + } + .validate(UTC.with_ymd_and_hms(1997, 1, 1, 1, 1, 1).unwrap()) + .unwrap(); + + let res = counter_date.increment(&rrule, false); + assert!(res.is_ok()); + assert_eq!(counter_date, expected_output); + } + } + + #[test] + fn increments_counter_date_with_daily_freq() { + let tests = [ + ( + ymd_hms(1997, 1, 1, 0, 0, 0), + 1, + ymd_hms(1997, 1, 2, 0, 0, 0), + ), + ( + ymd_hms(1997, 1, 1, 0, 0, 0), + 2, + ymd_hms(1997, 1, 3, 0, 0, 0), + ), + ( + ymd_hms(1997, 1, 1, 0, 0, 0), + 31, + ymd_hms(1997, 2, 1, 0, 0, 0), + ), + ( + ymd_hms(1997, 1, 1, 0, 0, 0), + 365, + ymd_hms(1998, 1, 1, 0, 0, 0), + ), + ]; + for (mut counter_date, interval, expected_output) in tests { + let rrule = RRule { + interval, + freq: ICalendarFrequency::Daily, + ..Default::default() + } + .validate(UTC.with_ymd_and_hms(1997, 1, 1, 1, 1, 1).unwrap()) + .unwrap(); + + let res = counter_date.increment(&rrule, false); + assert!(res.is_ok()); + assert_eq!(counter_date, expected_output); + } + } + + #[test] + fn increments_counter_date_with_hourly_freq() { + let tests = [ + ( + ymd_hms(1997, 1, 1, 0, 0, 0), + 1, + vec![], + ymd_hms(1997, 1, 1, 1, 0, 0), + ), + ( + ymd_hms(1997, 1, 1, 0, 0, 0), + 2, + vec![], + ymd_hms(1997, 1, 1, 2, 0, 0), + ), + ( + ymd_hms(1997, 1, 1, 0, 0, 0), + 1, + vec![4], + ymd_hms(1997, 1, 1, 4, 0, 0), + ), + ( + ymd_hms(1997, 1, 1, 10, 0, 0), + 1, + vec![4, 14], + ymd_hms(1997, 1, 1, 14, 0, 0), + ), + ( + ymd_hms(1997, 1, 1, 0, 0, 0), + 24, + vec![], + ymd_hms(1997, 1, 2, 0, 0, 0), + ), + ( + ymd_hms(1997, 1, 1, 0, 0, 0), + 24 * 31, + vec![], + ymd_hms(1997, 2, 1, 0, 0, 0), + ), + ]; + for (mut counter_date, interval, by_hour, expected_output) in tests { + let rrule = RRule { + interval, + freq: ICalendarFrequency::Hourly, + by_hour, + ..Default::default() + } + .validate(UTC.with_ymd_and_hms(1997, 1, 1, 1, 1, 1).unwrap()) + .unwrap(); + + let res = counter_date.increment(&rrule, false); + assert!(res.is_ok()); + assert_eq!(counter_date, expected_output); + } + } + + #[test] + fn increments_counter_date_with_hourly_freq_and_day_increment() { + let tests = [ + ( + ymd_hms(1997, 1, 1, 0, 0, 0), + 1, + ymd_hms(1997, 1, 2, 0, 0, 0), + ), + ( + ymd_hms(1997, 1, 1, 0, 0, 0), + 2, + ymd_hms(1997, 1, 2, 0, 0, 0), + ), + ( + ymd_hms(1997, 1, 1, 0, 0, 0), + 5, + ymd_hms(1997, 1, 2, 1, 0, 0), + ), + ( + ymd_hms(1997, 1, 1, 0, 0, 0), + 24, + ymd_hms(1997, 1, 2, 0, 0, 0), + ), + ( + ymd_hms(1997, 1, 1, 0, 0, 0), + 24 * 31, + ymd_hms(1997, 2, 1, 0, 0, 0), + ), + ( + ymd_hms(1997, 1, 1, 0, 0, 0), + 24 * 31 + 1, + ymd_hms(1997, 2, 1, 1, 0, 0), + ), + ]; + for (mut counter_date, interval, expected_output) in tests { + let rrule = RRule { + interval, + freq: ICalendarFrequency::Hourly, + ..Default::default() + } + .validate(UTC.with_ymd_and_hms(1997, 1, 1, 1, 1, 1).unwrap()) + .unwrap(); + + let res = counter_date.increment(&rrule, true); + assert!(res.is_ok()); + assert_eq!(counter_date, expected_output); + } + } + + #[test] + fn increments_counter_date_with_minutely_freq() { + let tests = [ + ( + ymd_hms(1997, 1, 1, 0, 0, 0), + 1, + ymd_hms(1997, 1, 1, 0, 1, 0), + ), + ( + ymd_hms(1997, 1, 1, 0, 0, 0), + 2, + ymd_hms(1997, 1, 1, 0, 2, 0), + ), + ( + ymd_hms(1997, 1, 1, 0, 0, 0), + 60, + ymd_hms(1997, 1, 1, 1, 0, 0), + ), + ( + ymd_hms(1997, 1, 1, 0, 0, 0), + 60 * 24, + ymd_hms(1997, 1, 2, 0, 0, 0), + ), + ]; + for (mut counter_date, interval, expected_output) in tests { + let rrule = RRule { + interval, + freq: ICalendarFrequency::Minutely, + ..Default::default() + } + .validate(UTC.with_ymd_and_hms(1997, 1, 1, 1, 1, 1).unwrap()) + .unwrap(); + + let res = counter_date.increment(&rrule, false); + assert!(res.is_ok()); + assert_eq!(counter_date, expected_output); + } + } + + #[test] + fn increments_counter_date_with_minutely_freq_and_day_increment() { + let tests = [ + ( + ymd_hms(1997, 1, 1, 0, 0, 0), + 1, + ymd_hms(1997, 1, 2, 0, 0, 0), + ), + ( + ymd_hms(1997, 1, 1, 0, 0, 0), + 2, + ymd_hms(1997, 1, 2, 0, 0, 0), + ), + ( + ymd_hms(1997, 1, 1, 0, 0, 0), + 60 * 24 + 1, + ymd_hms(1997, 1, 2, 0, 1, 0), + ), + ]; + for (mut counter_date, interval, expected_output) in tests { + let rrule = RRule { + interval, + freq: ICalendarFrequency::Minutely, + ..Default::default() + } + .validate(UTC.with_ymd_and_hms(1997, 1, 1, 1, 1, 1).unwrap()) + .unwrap(); + + let res = counter_date.increment(&rrule, true); + assert!(res.is_ok()); + assert_eq!(counter_date, expected_output); + } + } + + #[test] + fn increments_counter_date_with_secondly_freq() { + let tests = [ + ( + ymd_hms(1997, 1, 1, 0, 0, 0), + 1, + ymd_hms(1997, 1, 1, 0, 0, 1), + ), + ( + ymd_hms(1997, 1, 1, 0, 0, 0), + 2, + ymd_hms(1997, 1, 1, 0, 0, 2), + ), + ( + ymd_hms(1997, 1, 1, 0, 0, 0), + 60, + ymd_hms(1997, 1, 1, 0, 1, 0), + ), + ( + ymd_hms(1997, 1, 1, 0, 0, 0), + 60 * 60, + ymd_hms(1997, 1, 1, 1, 0, 0), + ), + ]; + for (mut counter_date, interval, expected_output) in tests { + let rrule = RRule { + interval, + freq: ICalendarFrequency::Secondly, + ..Default::default() + } + .validate(UTC.with_ymd_and_hms(1997, 1, 1, 1, 1, 1).unwrap()) + .unwrap(); + + let res = counter_date.increment(&rrule, false); + assert!(res.is_ok()); + assert_eq!(counter_date, expected_output); + } + } + + #[test] + fn increments_counter_date_with_secondly_freq_and_day_increment() { + let tests = [ + ( + ymd_hms(1997, 1, 1, 0, 0, 0), + 1, + ymd_hms(1997, 1, 2, 0, 0, 0), + ), + ( + ymd_hms(1997, 1, 1, 0, 0, 0), + 2, + ymd_hms(1997, 1, 2, 0, 0, 0), + ), + ( + ymd_hms(1997, 1, 1, 0, 0, 0), + 60 * 24 + 1, + ymd_hms(1997, 1, 2, 0, 1, 0), + ), + ]; + for (mut counter_date, interval, expected_output) in tests { + let rrule = RRule { + interval, + freq: ICalendarFrequency::Secondly, + ..Default::default() + } + .validate(UTC.with_ymd_and_hms(1997, 1, 1, 1, 1, 1).unwrap()) + .unwrap(); + + let res = counter_date.increment(&rrule, true); + assert!(res.is_ok()); + assert_eq!(counter_date, expected_output); + } + } +} diff --git a/vendor/calcard/src/datecalc/easter.rs b/vendor/calcard/src/datecalc/easter.rs new file mode 100644 index 00000000..22997264 --- /dev/null +++ b/vendor/calcard/src/datecalc/easter.rs @@ -0,0 +1,52 @@ +/* + * SPDX-FileCopyrightText: 2021 Fredrik Meringdal, Ralph Bisschops + * + * SPDX-License-Identifier: Apache-2.0 OR MIT + */ + +use super::masks::MASKS; +use super::utils::is_leap_year; + +/// Implementation of the Gauss Easter Algorithm. +#[allow(clippy::many_single_char_names)] +pub(crate) fn easter(year: i32, offset: i16) -> i32 { + let a = year % 19; + let b = year / 100; + let c = year % 100; + let d = b / 4; + let e = b % 4; + let f = (b + 8) / 25; + let g = (b - f + 1) / 3; + let h = (19 * a + b - d - g + 15) % 30; + let i = c / 4; + let k = c % 4; + let l = (32 + 2 * e + 2 * i - h - k) % 7; + let m = (a + 11 * h + 22 * l) / 451; + let month = ((h + l - 7 * m + 114) / 31) as usize; + let day = ((h + l - 7 * m + 114) % 31) + 1 + i32::from(offset); + + let month_range_mask = if is_leap_year(year) { + &MASKS.month_366_range + } else { + &MASKS.month_365_range + }; + + i32::from(month_range_mask[month - 1]) + day - 1 +} + +#[cfg(test)] +mod test_easter_masks { + use super::*; + + #[test] + fn easter_mask() { + let easter_day = easter(1997, 0); + assert_eq!(easter_day, 88); + let easter_day = easter(1998, 0); + assert_eq!(easter_day, 101); + let easter_day = easter(1999, 0); + assert_eq!(easter_day, 93); + let easter_day = easter(2000, 0); + assert_eq!(easter_day, 113); + } +} diff --git a/vendor/calcard/src/datecalc/error.rs b/vendor/calcard/src/datecalc/error.rs new file mode 100644 index 00000000..860227de --- /dev/null +++ b/vendor/calcard/src/datecalc/error.rs @@ -0,0 +1,176 @@ +/* + * SPDX-FileCopyrightText: 2021 Fredrik Meringdal, Ralph Bisschops + * + * SPDX-License-Identifier: Apache-2.0 OR MIT + */ + +use crate::{common::IanaString, icalendar::ICalendarFrequency}; +use std::{ + borrow::Cow, + fmt::{Display, Formatter}, +}; + +#[derive(Debug, Clone, PartialEq, Eq)] +#[cfg_attr(any(test, feature = "serde"), derive(serde::Serialize))] +pub enum RRuleError { + ValidationError(ValidationError), + IterError(String), +} + +#[derive(Debug, Clone, PartialEq, Eq)] +#[cfg_attr(any(test, feature = "serde"), derive(serde::Serialize))] +pub enum ValidationError { + BySetPosWithoutByRule, + InvalidFieldValue { + field: String, + value: String, + }, + InvalidFieldValueRange { + field: String, + value: String, + start_idx: String, + end_idx: String, + }, + InvalidFieldValueRangeWithFreq { + field: String, + value: String, + freq: ICalendarFrequency, + start_idx: String, + end_idx: String, + }, + InvalidByRuleAndFrequency { + by_rule: String, + freq: ICalendarFrequency, + }, + UntilBeforeStart { + until: String, + dt_start: String, + }, + TooBigInterval(u16), + StartYearOutOfRange(i32), + UnableToGenerateTimeset, + InvalidByRuleWithByEaster, + DtStartUntilMismatchTimezone { + dt_start_tz: Cow<'static, str>, + until_tz: Cow<'static, str>, + expected: Vec>, + }, +} + +impl From for RRuleError { + fn from(err: ValidationError) -> Self { + Self::ValidationError(err) + } +} + +impl From for RRuleError { + fn from(err: String) -> Self { + Self::IterError(err) + } +} + +impl RRuleError { + pub fn new_iter_err>(msg: S) -> Self { + Self::IterError(msg.as_ref().to_owned()) + } +} + +pub(crate) fn checked_mul_u32(v1: u32, v2: u32, hint: Option<&str>) -> Result { + v1.checked_mul(v2).ok_or_else(|| match hint { + Some(hint) => RRuleError::new_iter_err(format!( + "Could not multiply number, would overflow (`{} * {}`), {}.", + v1, v2, hint + )), + None => RRuleError::new_iter_err(format!( + "Could not multiply number, would overflow (`{} * {}`).", + v1, v2, + )), + }) +} + +pub(crate) fn checked_add_u32(v1: u32, v2: u32, hint: Option<&str>) -> Result { + v1.checked_add(v2).ok_or_else(|| match hint { + Some(hint) => RRuleError::new_iter_err(format!( + "Could not add numbers, would overflow (`{} + {}`), {}.", + v1, v2, hint + )), + None => RRuleError::new_iter_err(format!( + "Could not add numbers, would overflow (`{} + {}`).", + v1, v2, + )), + }) +} + +impl Display for RRuleError { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + RRuleError::ValidationError(err) => write!(f, "{}", err), + RRuleError::IterError(err) => write!(f, "Iteration error: {}", err), + } + } +} + +impl Display for ValidationError { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + ValidationError::BySetPosWithoutByRule => { + write!(f, "BYSETPOS without BYxxx rule is not allowed") + } + ValidationError::InvalidFieldValue { field, value } => { + write!(f, "Invalid value `{value}` for field `{field}`") + } + ValidationError::InvalidFieldValueRange { + field, + value, + start_idx, + end_idx, + } => write!( + f, + "Invalid value `{value}` for field `{field}`, start index `{start_idx}` and end index `{end_idx}`" + ), + ValidationError::InvalidFieldValueRangeWithFreq { + field, + value, + freq, + start_idx, + end_idx, + } => write!( + f, + "Invalid value `{value}` for field `{field}`, frequency `{}`, start index `{start_idx}` and end index `{end_idx}`", + freq.as_str() + ), + ValidationError::InvalidByRuleAndFrequency { by_rule, freq } => write!( + f, + "Invalid BY rule `{by_rule}` with frequency `{}`", + freq.as_str() + ), + ValidationError::UntilBeforeStart { until, dt_start } => write!( + f, + "Until date `{until}` is before the start date `{dt_start}`" + ), + ValidationError::TooBigInterval(interval) => write!( + f, + "Interval of {interval} is too big. The maximum interval is 32767." + ), + ValidationError::StartYearOutOfRange(year) => write!( + f, + "Start year {year} is out of range. The valid range is 1970 to 2038." + ), + ValidationError::UnableToGenerateTimeset => { + write!(f, "Unable to generate timeset") + } + ValidationError::InvalidByRuleWithByEaster => { + write!(f, "BYEASTER cannot be used with BYxxx rules") + } + ValidationError::DtStartUntilMismatchTimezone { + dt_start_tz, + until_tz, + expected, + } => write!( + f, + "DTSTART timezone `{dt_start_tz}` does not match UNTIL timezone `{until_tz}`, expected timezones: {:?}", + expected + ), + } + } +} diff --git a/vendor/calcard/src/datecalc/filters.rs b/vendor/calcard/src/datecalc/filters.rs new file mode 100644 index 00000000..a292be49 --- /dev/null +++ b/vendor/calcard/src/datecalc/filters.rs @@ -0,0 +1,129 @@ +/* + * SPDX-FileCopyrightText: 2021 Fredrik Meringdal, Ralph Bisschops + * + * SPDX-License-Identifier: Apache-2.0 OR MIT + */ + +use super::{ + iterinfo::IterInfo, + rrule::{NWeekday, RRule}, +}; +use crate::icalendar::ICalendarMonth; + +type RRuleFilter = &'static dyn Fn(&IterInfo, usize, &RRule) -> bool; + +const FILTERS: [RRuleFilter; 7] = [ + &is_filtered_by_month, + &is_filtered_by_week_number, + &is_filtered_by_weekday, + &is_filtered_by_neg_weekday, + &is_filtered_by_easter, + &is_filtered_by_month_day, + &is_filtered_by_year_day, +]; + +pub(crate) fn is_filtered(ii: &IterInfo, current_day: usize) -> bool { + let rrule = ii.rrule(); + FILTERS + .into_iter() + .any(|filter| filter(ii, current_day, rrule)) +} + +fn is_filtered_by_month(ii: &IterInfo, current_day: usize, rrule: &RRule) -> bool { + if rrule.by_month.is_empty() { + return false; + } + + let current_month = ICalendarMonth::new(ii.month_mask()[current_day], false); + !rrule.by_month.contains(¤t_month) +} + +fn is_filtered_by_week_number(ii: &IterInfo, current_day: usize, rrule: &RRule) -> bool { + if rrule.by_week_no.is_empty() { + return false; + } + + matches!(ii.week_no_mask(), Some(week_no_mask) if week_no_mask[current_day] == 0) +} + +fn is_filtered_by_weekday(ii: &IterInfo, current_day: usize, rrule: &RRule) -> bool { + let mut by_weekday_every_week_only = rrule + .by_weekday + .iter() + .filter_map(|by_weekday| match by_weekday { + // Get only `Every` occurrences. + NWeekday::Every(weekday) => Some(weekday.num_days_from_monday()), + NWeekday::Nth(_, _) => None, + }) + .peekable(); + + // Check if empty + if by_weekday_every_week_only.peek().is_none() { + return false; + } + + let current_weekday = ii.weekday_mask()[current_day]; + !by_weekday_every_week_only.any(|el| el == current_weekday) +} + +fn is_filtered_by_neg_weekday(ii: &IterInfo, current_day: usize, _rrule: &RRule) -> bool { + if let Some(neg_weekday_mask) = ii.neg_weekday_mask() { + if neg_weekday_mask.is_empty() { + return false; + } + + let current_neg_weekday = neg_weekday_mask[current_day]; + current_neg_weekday == 0 + } else { + false + } +} + +fn is_filtered_by_easter(ii: &IterInfo, current_day: usize, rrule: &RRule) -> bool { + if rrule.by_easter.is_none() { + return false; + } + match i32::try_from(current_day) { + Ok(current_day) => { + !matches!(ii.easter_mask(), Some(easter_mask) if easter_mask.contains(¤t_day)) + } + _ => true, + } +} + +fn is_filtered_by_month_day(ii: &IterInfo, current_day: usize, rrule: &RRule) -> bool { + if rrule.by_month_day.is_empty() && rrule.by_n_month_day.is_empty() { + return false; + } + + let current_month_day = ii.month_day_mask()[current_day]; + let current_n_month_day = ii.neg_month_day_mask()[current_day]; + let filtered_by_month_day = !rrule.by_month_day.contains(¤t_month_day); + let filtered_by_n_month_day = !rrule.by_n_month_day.contains(¤t_n_month_day); + + filtered_by_month_day && filtered_by_n_month_day +} + +fn is_filtered_by_year_day(ii: &IterInfo, current_day: usize, rrule: &RRule) -> bool { + if rrule.by_year_day.is_empty() { + return false; + } + + let current_day = match i16::try_from(current_day) { + Ok(current_day) => current_day, + _ => return true, + }; + + let year_len = (ii.year_len()) as i16; + let next_year_len = (ii.next_year_len()) as i16; + + if current_day < year_len { + !rrule.by_year_day.contains(&(current_day + 1)) + && !rrule.by_year_day.contains(&(current_day - year_len)) + } else { + !rrule.by_year_day.contains(&(current_day + 1 - year_len)) + && !rrule + .by_year_day + .contains(&(current_day - next_year_len - year_len)) + } +} diff --git a/vendor/calcard/src/datecalc/iterinfo.rs b/vendor/calcard/src/datecalc/iterinfo.rs new file mode 100644 index 00000000..95794289 --- /dev/null +++ b/vendor/calcard/src/datecalc/iterinfo.rs @@ -0,0 +1,278 @@ +/* + * SPDX-FileCopyrightText: 2021 Fredrik Meringdal, Ralph Bisschops + * + * SPDX-License-Identifier: Apache-2.0 OR MIT + */ + +use super::counter_date::DateTimeIter; +use super::easter::easter; +use super::get_month; +use super::rrule::{NWeekday, RRule}; +use super::{monthinfo::MonthInfo, yearinfo::YearInfo}; +use crate::common::timezone::Tz; +use crate::icalendar::ICalendarFrequency; +use chrono::{Datelike, NaiveTime, TimeZone}; + +#[derive(Debug, Clone)] +pub(crate) struct IterInfo { + year_info: YearInfo, + month_info: Option, + easter_mask: Option>, + rrule: RRule, +} + +impl IterInfo { + pub fn new(rrule: &RRule, dt_start: &chrono::DateTime) -> Self { + let year = dt_start.year(); + let month = get_month(dt_start); + + let year_info = YearInfo::new(year, rrule); + let mut ii = Self { + rrule: rrule.clone(), + year_info, + month_info: None, + easter_mask: None, + }; + ii.rebuild_inner(year, month, true); + + ii + } + + fn rebuild_inner(&mut self, year: i32, month: u8, skip_year_info: bool) { + if !skip_year_info + && !matches!(&self.month_info, Some(month_info) if month_info.last_year == year) + { + self.year_info = YearInfo::new(year, &self.rrule); + } + + let contains_nth_by_weekday = self + .rrule + .by_weekday + .iter() + .any(|by_weekday| matches!(by_weekday, NWeekday::Nth(_, _))); + + if contains_nth_by_weekday + && !(matches!(&self.month_info, Some(month_info) if month_info.last_month == month && month_info.last_year == year)) + { + let new_month_info = MonthInfo::new(&self.year_info, month, &self.rrule); + self.month_info = Some(new_month_info); + } + + self.easter_mask = self + .rrule + .by_easter + .map(|by_easter| vec![easter(year, by_easter)]); + } + + pub fn rebuild(&mut self, counter_date: &DateTimeIter) { + self.rebuild_inner(counter_date.year, counter_date.month as u8, false); + } + + pub fn year_len(&self) -> u16 { + self.year_info.year_len + } + + pub fn year_ordinal(&self) -> i64 { + self.year_info.year_ordinal + } + + pub fn month_range(&self) -> &[u16] { + self.year_info.month_range + } + + pub fn easter_mask(&self) -> Option<&Vec> { + self.easter_mask.as_ref() + } + + pub fn weekday_mask(&self) -> &[u32] { + self.year_info.weekday_mask + } + + pub fn month_mask(&self) -> &[u8] { + self.year_info.month_mask + } + + pub fn week_no_mask(&self) -> Option<&Vec> { + self.year_info.week_no_mask.as_ref() + } + + pub fn neg_weekday_mask(&self) -> Option<&Vec> { + self.month_info.as_ref().map(|info| &info.neg_weekday_mask) + } + + pub fn next_year_len(&self) -> u16 { + self.year_info.next_year_len + } + + pub fn month_day_mask(&self) -> &[i8] { + self.year_info.month_day_mask + } + + pub fn neg_month_day_mask(&self) -> &[i8] { + self.year_info.neg_month_day_mask + } + + pub fn year_dayset(&self) -> Vec { + let year_len = usize::from(self.year_len()); + (0..year_len).collect() + } + + pub fn month_dayset(&self, month: u32) -> Vec { + let month_range = self.month_range(); + let month = month as usize; + let start = usize::from(month_range[month - 1]); + let end = usize::from(month_range[month]); + (start..end).collect() + } + + pub fn weekday_set(&self, year: i32, month: u32, day: u32) -> Vec { + let set_len = usize::from(self.year_len() + 7); + + let mut date_ordinal = chrono::Utc + .with_ymd_and_hms(year, month, day, 0, 0, 0) + .unwrap() + .ordinal0() as usize; + + let mut set = vec![]; + + let week_start_num_days_from_monday = self.rrule.week_start.num_days_from_monday(); + + for _ in 0..7 { + if date_ordinal >= set_len { + break; + } + set.push(date_ordinal); + date_ordinal += 1; + if self.weekday_mask()[date_ordinal] == week_start_num_days_from_monday { + break; + } + } + + set + } + + pub fn day_dayset(year: i32, month: u32, day: u32) -> Vec { + let date_ordinal = chrono::Utc + .with_ymd_and_hms(year, month, day, 0, 0, 0) + .unwrap() + .ordinal0(); + + vec![date_ordinal as usize] + } + + pub fn hour_timeset(&self, hour: u8) -> Vec { + self.rrule + .by_minute + .iter() + .flat_map(|minute| self.min_timeset(hour, *minute)) + .collect() + } + + pub fn min_timeset(&self, hour: u8, minute: u8) -> Vec { + self.rrule + .by_second + .iter() + .filter_map(|second| { + NaiveTime::from_hms_opt(u32::from(hour), u32::from(minute), u32::from(*second)) + }) + .collect() + } + + pub fn sec_timeset(hour: u8, minute: u8, second: u8) -> Vec { + if let Some(time) = + NaiveTime::from_hms_opt(u32::from(hour), u32::from(minute), u32::from(second)) + { + vec![time] + } else { + vec![] + } + } + + pub fn get_dayset( + &self, + freq: ICalendarFrequency, + year: i32, + month: u32, + day: u32, + ) -> Vec { + let mut dayset = match freq { + ICalendarFrequency::Yearly => self.year_dayset(), + ICalendarFrequency::Monthly => self.month_dayset(month), + ICalendarFrequency::Weekly => self.weekday_set(year, month, day), + _ => Self::day_dayset(year, month, day), + }; + + // Filter out days according to the RRule filters. + dayset.retain(|day| !super::filters::is_filtered(self, *day)); + + dayset + } + + /// Gets a timeset without checking if the hour, minute and second are valid, according + /// to the `RRule`. + /// + /// This is usually called after calling the `increment_counter_date` where we know + /// that we get a valid `DateTime` back, and there is no need to do any duplicate + /// validation. + pub fn get_timeset_unchecked(&self, hour: u8, minute: u8, second: u8) -> Vec { + match self.rrule.freq { + ICalendarFrequency::Hourly => self.hour_timeset(hour), + ICalendarFrequency::Minutely => self.min_timeset(hour, minute), + ICalendarFrequency::Secondly => Self::sec_timeset(hour, minute, second), + _ => unreachable!( + "This method is never called with an invalid frequency and is not publicly exposed" + ), + } + } + + /// Gets a timeset. + /// + /// An empty set is returned if the hour, minute and second aren't valid, + /// according to the `RRule`. + pub fn get_timeset(&self, hour: u8, minute: u8, second: u8) -> Vec { + match self.rrule.freq { + ICalendarFrequency::Hourly + | ICalendarFrequency::Minutely + | ICalendarFrequency::Secondly => { + let incorrect_hour = self.rrule.freq >= ICalendarFrequency::Hourly + && !self.rrule.by_hour.is_empty() + && !self.rrule.by_hour.contains(&hour); + let incorrect_minute = self.rrule.freq >= ICalendarFrequency::Minutely + && !self.rrule.by_minute.is_empty() + && !self.rrule.by_minute.contains(&minute); + let incorrect_second = self.rrule.freq >= ICalendarFrequency::Secondly + && !self.rrule.by_second.is_empty() + && !self.rrule.by_second.contains(&second); + let date_is_not_a_candidate = + incorrect_hour || incorrect_minute || incorrect_second; + + // If date is not a potential candidate, then we return an empty timeset. + if date_is_not_a_candidate { + return vec![]; + } + + self.get_timeset_unchecked(hour, minute, second) + } + _ => self + .rrule + .by_hour + .iter() + .flat_map(|hour| { + self.rrule.by_minute.iter().flat_map(move |minute| { + self.rrule.by_second.iter().filter_map(move |second| { + NaiveTime::from_hms_opt( + u32::from(*hour), + u32::from(*minute), + u32::from(*second), + ) + }) + }) + }) + .collect(), + } + } + + pub fn rrule(&self) -> &RRule { + &self.rrule + } +} diff --git a/vendor/calcard/src/datecalc/masks.rs b/vendor/calcard/src/datecalc/masks.rs new file mode 100644 index 00000000..0f491898 --- /dev/null +++ b/vendor/calcard/src/datecalc/masks.rs @@ -0,0 +1,280 @@ +/* + * SPDX-FileCopyrightText: 2021 Fredrik Meringdal, Ralph Bisschops + * + * SPDX-License-Identifier: Apache-2.0 OR MIT + */ + +// Every mask is 7 days longer to handle cross-year weekly periods. + +pub(crate) type WdayMask = [u32; 7 * 55]; +pub(crate) type Mask365 = [u8; 365 + 7]; +pub(crate) type Mask366 = [u8; 366 + 7]; +pub(crate) type Mask365Signed = [i8; 365 + 7]; +pub(crate) type Mask366Signed = [i8; 366 + 7]; +pub(crate) type MonthRangeMask = [u16; 13]; + +const M365: Mask365 = [ + // 31 + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + // 28 + 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, + // 31 + 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, + // 30 + 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, + // 31 + 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, + // 30 + 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, + // 31 + 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, + // 31 + 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, + // 30 + 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, + // 31 + 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, + 10, 10, 10, 10, 10, 10, 10, + // 30 ----------------------------------------------------------------------------------------- + 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, + 11, 11, 11, 11, 11, 11, + // 31 ----------------------------------------------------------------------------------------- + 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, + 12, 12, 12, 12, 12, 12, 12, + // 7 ------------------------------------------------------------------------------------------ + 1, 1, 1, 1, 1, 1, 1, +]; +const MDAY365: Mask365Signed = [ + // 31 + 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, 26, + 27, 28, 29, 30, 31, + // 28 ----------------------------------------------------------------------------------------- + 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, 26, + 27, 28, + // 31 --------------------------------------------------------------------------------------- + 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, 26, + 27, 28, 29, 30, 31, + // 30 --------------------------------------------------------------------------------------- + 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, 26, + 27, 28, 29, 30, + // 31 --------------------------------------------------------------------------------------- + 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, 26, + 27, 28, 29, 30, 31, + // 30 --------------------------------------------------------------------------------------- + 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, 26, + 27, 28, 29, 30, + // 31 --------------------------------------------------------------------------------------- + 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, 26, + 27, 28, 29, 30, 31, + // 31 --------------------------------------------------------------------------------------- + 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, 26, + 27, 28, 29, 30, 31, + // 30 --------------------------------------------------------------------------------------- + 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, 26, + 27, 28, 29, 30, + // 31 --------------------------------------------------------------------------------------- + 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, 26, + 27, 28, 29, 30, 31, + // 30 ----------------------------------------------------------------------------------------- + 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, 26, + 27, 28, 29, 30, + // 31 ----------------------------------------------------------------------------------------- + 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, 26, + 27, 28, 29, 30, 31, + // 7 ----------------------------------------------------------------------------------------- + 1, 2, 3, 4, 5, 6, 7, +]; +const NMDAY365: Mask365Signed = [ + // 31 + -31, -30, -29, -28, -27, -26, -25, -24, -23, -22, -21, -20, -19, -18, -17, -16, -15, -14, -13, + -12, -11, -10, -9, -8, -7, -6, -5, -4, -3, -2, -1, + // 28 ----------------------------------------------------------------------------------------- + -28, -27, -26, -25, -24, -23, -22, -21, -20, -19, -18, -17, -16, -15, -14, -13, -12, -11, -10, + -9, -8, -7, -6, -5, -4, -3, -2, -1, + // 31 --------------------------------------------------------------------------------------- + -31, -30, -29, -28, -27, -26, -25, -24, -23, -22, -21, -20, -19, -18, -17, -16, -15, -14, -13, + -12, -11, -10, -9, -8, -7, -6, -5, -4, -3, -2, -1, + // 30 --------------------------------------------------------------------------------------- + -30, -29, -28, -27, -26, -25, -24, -23, -22, -21, -20, -19, -18, -17, -16, -15, -14, -13, -12, + -11, -10, -9, -8, -7, -6, -5, -4, -3, -2, -1, + // 31 --------------------------------------------------------------------------------------- + -31, -30, -29, -28, -27, -26, -25, -24, -23, -22, -21, -20, -19, -18, -17, -16, -15, -14, -13, + -12, -11, -10, -9, -8, -7, -6, -5, -4, -3, -2, -1, + // 30 --------------------------------------------------------------------------------------- + -30, -29, -28, -27, -26, -25, -24, -23, -22, -21, -20, -19, -18, -17, -16, -15, -14, -13, -12, + -11, -10, -9, -8, -7, -6, -5, -4, -3, -2, -1, + // 31 --------------------------------------------------------------------------------------- + -31, -30, -29, -28, -27, -26, -25, -24, -23, -22, -21, -20, -19, -18, -17, -16, -15, -14, -13, + -12, -11, -10, -9, -8, -7, -6, -5, -4, -3, -2, -1, + // 31 --------------------------------------------------------------------------------------- + -31, -30, -29, -28, -27, -26, -25, -24, -23, -22, -21, -20, -19, -18, -17, -16, -15, -14, -13, + -12, -11, -10, -9, -8, -7, -6, -5, -4, -3, -2, -1, + // 30 --------------------------------------------------------------------------------------- + -30, -29, -28, -27, -26, -25, -24, -23, -22, -21, -20, -19, -18, -17, -16, -15, -14, -13, -12, + -11, -10, -9, -8, -7, -6, -5, -4, -3, -2, -1, + // 31 --------------------------------------------------------------------------------------- + -31, -30, -29, -28, -27, -26, -25, -24, -23, -22, -21, -20, -19, -18, -17, -16, -15, -14, -13, + -12, -11, -10, -9, -8, -7, -6, -5, -4, -3, -2, -1, + // 30 ----------------------------------------------------------------------------------------- + -30, -29, -28, -27, -26, -25, -24, -23, -22, -21, -20, -19, -18, -17, -16, -15, -14, -13, -12, + -11, -10, -9, -8, -7, -6, -5, -4, -3, -2, -1, + // 31 ----------------------------------------------------------------------------------------- + -31, -30, -29, -28, -27, -26, -25, -24, -23, -22, -21, -20, -19, -18, -17, -16, -15, -14, -13, + -12, -11, -10, -9, -8, -7, -6, -5, -4, -3, -2, -1, + // 7 ----------------------------------------------------------------------------------------- + -31, -30, -29, -28, -27, -26, -25, +]; +const M365_RANGE: MonthRangeMask = [0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334, 365]; + +const M366: Mask366 = [ + // 31 + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + // 29 + 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, + // 31 + 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, + // 30 + 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, + // 31 + 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, + // 30 + 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, + // 31 + 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, + // 31 + 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, + // 30 + 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, + // 31 + 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, + 10, 10, 10, 10, 10, 10, 10, + // 30 ----------------------------------------------------------------------------------------- + 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, + 11, 11, 11, 11, 11, 11, + // 31 ----------------------------------------------------------------------------------------- + 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, + 12, 12, 12, 12, 12, 12, 12, + // 7 ------------------------------------------------------------------------------------------ + 1, 1, 1, 1, 1, 1, 1, +]; +const MDAY366: Mask366Signed = [ + // 31 + 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, 26, + 27, 28, 29, 30, 31, + // 29 ----------------------------------------------------------------------------------------- + 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, 26, + 27, 28, 29, + // 31 --------------------------------------------------------------------------------------- + 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, 26, + 27, 28, 29, 30, 31, + // 30 --------------------------------------------------------------------------------------- + 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, 26, + 27, 28, 29, 30, + // 31 --------------------------------------------------------------------------------------- + 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, 26, + 27, 28, 29, 30, 31, + // 30 --------------------------------------------------------------------------------------- + 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, 26, + 27, 28, 29, 30, + // 31 --------------------------------------------------------------------------------------- + 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, 26, + 27, 28, 29, 30, 31, + // 31 --------------------------------------------------------------------------------------- + 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, 26, + 27, 28, 29, 30, 31, + // 30 --------------------------------------------------------------------------------------- + 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, 26, + 27, 28, 29, 30, + // 31 --------------------------------------------------------------------------------------- + 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, 26, + 27, 28, 29, 30, 31, + // 30 ----------------------------------------------------------------------------------------- + 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, 26, + 27, 28, 29, 30, + // 31 ----------------------------------------------------------------------------------------- + 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, 26, + 27, 28, 29, 30, 31, + // 7 ----------------------------------------------------------------------------------------- + 1, 2, 3, 4, 5, 6, 7, +]; +const M366_RANGE: MonthRangeMask = [0, 31, 60, 91, 121, 152, 182, 213, 244, 274, 305, 335, 366]; +const NMDAY366: Mask366Signed = [ + // 31 + -31, -30, -29, -28, -27, -26, -25, -24, -23, -22, -21, -20, -19, -18, -17, -16, -15, -14, -13, + -12, -11, -10, -9, -8, -7, -6, -5, -4, -3, -2, -1, + // 29 ----------------------------------------------------------------------------------------- + -29, -28, -27, -26, -25, -24, -23, -22, -21, -20, -19, -18, -17, -16, -15, -14, -13, -12, -11, + -10, -9, -8, -7, -6, -5, -4, -3, -2, -1, + // 31 --------------------------------------------------------------------------------------- + -31, -30, -29, -28, -27, -26, -25, -24, -23, -22, -21, -20, -19, -18, -17, -16, -15, -14, -13, + -12, -11, -10, -9, -8, -7, -6, -5, -4, -3, -2, -1, + // 30 --------------------------------------------------------------------------------------- + -30, -29, -28, -27, -26, -25, -24, -23, -22, -21, -20, -19, -18, -17, -16, -15, -14, -13, -12, + -11, -10, -9, -8, -7, -6, -5, -4, -3, -2, -1, + // 31 --------------------------------------------------------------------------------------- + -31, -30, -29, -28, -27, -26, -25, -24, -23, -22, -21, -20, -19, -18, -17, -16, -15, -14, -13, + -12, -11, -10, -9, -8, -7, -6, -5, -4, -3, -2, -1, + // 30 --------------------------------------------------------------------------------------- + -30, -29, -28, -27, -26, -25, -24, -23, -22, -21, -20, -19, -18, -17, -16, -15, -14, -13, -12, + -11, -10, -9, -8, -7, -6, -5, -4, -3, -2, -1, + // 31 --------------------------------------------------------------------------------------- + -31, -30, -29, -28, -27, -26, -25, -24, -23, -22, -21, -20, -19, -18, -17, -16, -15, -14, -13, + -12, -11, -10, -9, -8, -7, -6, -5, -4, -3, -2, -1, + // 31 --------------------------------------------------------------------------------------- + -31, -30, -29, -28, -27, -26, -25, -24, -23, -22, -21, -20, -19, -18, -17, -16, -15, -14, -13, + -12, -11, -10, -9, -8, -7, -6, -5, -4, -3, -2, -1, + // 30 --------------------------------------------------------------------------------------- + -30, -29, -28, -27, -26, -25, -24, -23, -22, -21, -20, -19, -18, -17, -16, -15, -14, -13, -12, + -11, -10, -9, -8, -7, -6, -5, -4, -3, -2, -1, + // 31 --------------------------------------------------------------------------------------- + -31, -30, -29, -28, -27, -26, -25, -24, -23, -22, -21, -20, -19, -18, -17, -16, -15, -14, -13, + -12, -11, -10, -9, -8, -7, -6, -5, -4, -3, -2, -1, + // 30 ----------------------------------------------------------------------------------------- + -30, -29, -28, -27, -26, -25, -24, -23, -22, -21, -20, -19, -18, -17, -16, -15, -14, -13, -12, + -11, -10, -9, -8, -7, -6, -5, -4, -3, -2, -1, + // 31 ----------------------------------------------------------------------------------------- + -31, -30, -29, -28, -27, -26, -25, -24, -23, -22, -21, -20, -19, -18, -17, -16, -15, -14, -13, + -12, -11, -10, -9, -8, -7, -6, -5, -4, -3, -2, -1, + // 7 ----------------------------------------------------------------------------------------- + -31, -30, -29, -28, -27, -26, -25, +]; +const WDAY: WdayMask = [ + 0, 1, 2, 3, 4, 5, 6, 0, 1, 2, 3, 4, 5, 6, 0, 1, 2, 3, 4, 5, 6, 0, 1, 2, 3, 4, 5, 6, 0, 1, 2, 3, + 4, 5, 6, 0, 1, 2, 3, 4, 5, 6, 0, 1, 2, 3, 4, 5, 6, 0, 1, 2, 3, 4, 5, 6, 0, 1, 2, 3, 4, 5, 6, 0, + 1, 2, 3, 4, 5, 6, 0, 1, 2, 3, 4, 5, 6, 0, 1, 2, 3, 4, 5, 6, 0, 1, 2, 3, 4, 5, 6, 0, 1, 2, 3, 4, + 5, 6, 0, 1, 2, 3, 4, 5, 6, 0, 1, 2, 3, 4, 5, 6, 0, 1, 2, 3, 4, 5, 6, 0, 1, 2, 3, 4, 5, 6, 0, 1, + 2, 3, 4, 5, 6, 0, 1, 2, 3, 4, 5, 6, 0, 1, 2, 3, 4, 5, 6, 0, 1, 2, 3, 4, 5, 6, 0, 1, 2, 3, 4, 5, + 6, 0, 1, 2, 3, 4, 5, 6, 0, 1, 2, 3, 4, 5, 6, 0, 1, 2, 3, 4, 5, 6, 0, 1, 2, 3, 4, 5, 6, 0, 1, 2, + 3, 4, 5, 6, 0, 1, 2, 3, 4, 5, 6, 0, 1, 2, 3, 4, 5, 6, 0, 1, 2, 3, 4, 5, 6, 0, 1, 2, 3, 4, 5, 6, + 0, 1, 2, 3, 4, 5, 6, 0, 1, 2, 3, 4, 5, 6, 0, 1, 2, 3, 4, 5, 6, 0, 1, 2, 3, 4, 5, 6, 0, 1, 2, 3, + 4, 5, 6, 0, 1, 2, 3, 4, 5, 6, 0, 1, 2, 3, 4, 5, 6, 0, 1, 2, 3, 4, 5, 6, 0, 1, 2, 3, 4, 5, 6, 0, + 1, 2, 3, 4, 5, 6, 0, 1, 2, 3, 4, 5, 6, 0, 1, 2, 3, 4, 5, 6, 0, 1, 2, 3, 4, 5, 6, 0, 1, 2, 3, 4, + 5, 6, 0, 1, 2, 3, 4, 5, 6, 0, 1, 2, 3, 4, 5, 6, 0, 1, 2, 3, 4, 5, 6, 0, 1, 2, 3, 4, 5, 6, 0, 1, + 2, 3, 4, 5, 6, 0, 1, 2, 3, 4, 5, 6, 0, 1, 2, 3, 4, 5, 6, 0, 1, 2, 3, 4, 5, 6, 0, 1, 2, 3, 4, 5, + 6, +]; + +#[derive(Clone)] +pub(crate) struct Masks { + pub weekday: WdayMask, + pub month_365: Mask365, + pub month_365_range: MonthRangeMask, + pub month_366: Mask366, + pub month_366_range: MonthRangeMask, + pub month_day_365: Mask365Signed, + pub month_day_366: Mask366Signed, + pub neg_month_day_365: Mask365Signed, + pub neg_month_day_366: Mask366Signed, +} + +pub(crate) const MASKS: Masks = Masks { + weekday: WDAY, + month_365: M365, + month_365_range: M365_RANGE, + month_366: M366, + month_366_range: M366_RANGE, + month_day_365: MDAY365, + month_day_366: MDAY366, + neg_month_day_365: NMDAY365, + neg_month_day_366: NMDAY366, +}; diff --git a/vendor/calcard/src/datecalc/mod.rs b/vendor/calcard/src/datecalc/mod.rs new file mode 100644 index 00000000..c8308f55 --- /dev/null +++ b/vendor/calcard/src/datecalc/mod.rs @@ -0,0 +1,63 @@ +/* + * SPDX-FileCopyrightText: 2021 Fredrik Meringdal, Ralph Bisschops + * + * SPDX-License-Identifier: Apache-2.0 OR MIT + */ + +mod checks; +mod counter_date; +mod easter; +pub mod error; +pub(crate) mod filters; +pub(crate) mod iterinfo; +mod masks; +mod monthinfo; +mod pos_list; +pub mod rrule; +pub(crate) mod rrule_iter; +mod utils; +mod validate; +mod yearinfo; + +use crate::common::timezone::Tz; +use chrono::{Datelike, Duration, NaiveTime, Timelike}; +use iterinfo::IterInfo; +use pos_list::build_pos_list; +pub(crate) use rrule_iter::RRuleIter; + +/// Prevent loops when searching for the next event in the iterator. +/// If after X number of iterations it still has not found an event, +/// we can assume it will not find an event. +static MAX_ITER_LOOP: u32 = 100_000; + +#[inline(always)] +pub(crate) fn duration_from_midnight(time: NaiveTime) -> Duration { + Duration::hours(i64::from(time.hour())) + + Duration::minutes(i64::from(time.minute())) + + Duration::seconds(i64::from(time.second())) +} + +#[inline(always)] +pub(crate) fn get_month(dt: &chrono::DateTime) -> u8 { + dt.month() as u8 +} + +#[inline(always)] +pub(crate) fn get_day(dt: &chrono::DateTime) -> i8 { + dt.day() as i8 +} + +#[inline(always)] +pub(crate) fn get_hour(dt: &chrono::DateTime) -> u8 { + dt.hour() as u8 +} + +#[inline(always)] +pub(crate) fn get_minute(dt: &chrono::DateTime) -> u8 { + dt.minute() as u8 +} + +#[inline(always)] +pub(crate) fn get_second(dt: &chrono::DateTime) -> u8 { + dt.second() as u8 +} diff --git a/vendor/calcard/src/datecalc/monthinfo.rs b/vendor/calcard/src/datecalc/monthinfo.rs new file mode 100644 index 00000000..20e10a2b --- /dev/null +++ b/vendor/calcard/src/datecalc/monthinfo.rs @@ -0,0 +1,227 @@ +/* + * SPDX-FileCopyrightText: 2021 Fredrik Meringdal, Ralph Bisschops + * + * SPDX-License-Identifier: Apache-2.0 OR MIT + */ + +use super::{ + rrule::{NWeekday, RRule}, + utils::pymod, + yearinfo::YearInfo, +}; +use crate::icalendar::ICalendarFrequency; + +#[derive(Debug, Clone)] +pub(crate) struct MonthInfo { + pub last_year: i32, + pub last_month: u8, + pub neg_weekday_mask: Vec, +} + +impl MonthInfo { + pub fn new(year_info: &YearInfo, month: u8, rrule: &RRule) -> Self { + let neg_weekday_mask = Self::get_neg_weekday_mask(year_info, month, rrule); + Self { + last_year: year_info.year, + last_month: month, + neg_weekday_mask, + } + } + + fn get_neg_weekday_mask(year_info: &YearInfo, month: u8, rrule: &RRule) -> Vec { + let YearInfo { + year_len, + month_range, + weekday_mask, + .. + } = year_info; + + // Build up `ranges` + let mut ranges = vec![]; + if rrule.freq == ICalendarFrequency::Yearly { + if rrule.by_month.is_empty() { + ranges.push((0, u32::from(*year_len) - 1)); + } else { + for month in &rrule.by_month { + let month = usize::from(month.month()); + let first = u32::from(month_range[month - 1]); + let last = u32::from(month_range[month]); + ranges.push((first, last - 1)); + } + } + } else if rrule.freq == ICalendarFrequency::Monthly { + let month = usize::from(month); + let first = u32::from(month_range[month - 1]); + let last = u32::from(month_range[month]); + ranges.push((first, last - 1)); + } + + if ranges.is_empty() { + return vec![]; + } + + // Weekly frequency won't get here, so we may not + // care about cross-year weekly periods. + let mut neg_weekday_mask = vec![0; usize::from(*year_len)]; + + // Loop over `ranges` + for (first, last) in ranges { + for by_weekday in &rrule.by_weekday { + // Only check Nth occurrences here + if let NWeekday::Nth(number, weekday) = by_weekday { + let weekday = (weekday.num_days_from_monday()) as i16; + let nth_weekday = if *number < 0 { + let number = match u32::try_from(-number) { + Ok(num) => num, + _ => continue, + }; + let nth_last_week = match last.checked_sub((number - 1) * 7) { + Some(val) => val, + None => continue, + }; + let index = match usize::try_from(nth_last_week) { + Ok(i) => i, + _ => continue, + }; + let nth_last_weekday = match weekday_mask.get(index) { + Some(val) => (*val) as i16, + None => continue, + }; + + // Adjust to get the correct weekday + let modulo = (pymod(nth_last_weekday - weekday, 7)) as u32; + match nth_last_week.checked_sub(modulo) { + Some(val) => val, + None => continue, + } + } else { + let number = match u32::try_from(number - 1) { + Ok(num) => num, + _ => continue, + }; + let nth_first_day = first + number * 7; + let index = match usize::try_from(nth_first_day) { + Ok(i) => i, + _ => continue, + }; + let nth_first_day_weekday = match weekday_mask.get(index) { + Some(val) => (*val) as i16, + None => continue, + }; + + // Adjust to get the correct weekday + let a = (7 - nth_first_day_weekday + weekday) as u32; + nth_first_day + pymod(a, 7) + }; + if first <= nth_weekday + && nth_weekday <= last + && let Ok(nth_weekday) = usize::try_from(nth_weekday) + { + neg_weekday_mask[nth_weekday] = 1; + } + } + } + } + + neg_weekday_mask + } +} + +#[cfg(test)] +mod tests { + use crate::common::timezone::Tz; + + use super::*; + use chrono::{TimeZone, Weekday}; + + const UTC: Tz = Tz::Tz(chrono_tz::Tz::UTC); + + #[test] + fn get_neg_weekday_mask_with_daily_freq() { + let rrule = RRule { + freq: ICalendarFrequency::Daily, + ..Default::default() + } + .validate(UTC.with_ymd_and_hms(1997, 1, 1, 0, 0, 0).unwrap()) + .unwrap(); + + let year_info = YearInfo::new(1997, &rrule); + + let neg_weekday_mask = MonthInfo::get_neg_weekday_mask(&year_info, 1, &rrule); + assert!(neg_weekday_mask.is_empty()); + } + + #[test] + fn get_neg_weekday_mask_with_yearly_freq() { + let rrule = RRule { + freq: ICalendarFrequency::Yearly, + ..Default::default() + } + .validate(UTC.with_ymd_and_hms(1997, 1, 1, 0, 0, 0).unwrap()) + .unwrap(); + + let year_info = YearInfo::new(1997, &rrule); + + let neg_weekday_mask = MonthInfo::get_neg_weekday_mask(&year_info, 1, &rrule); + assert_eq!(neg_weekday_mask.len(), year_info.year_len as usize); + assert!(neg_weekday_mask.into_iter().all(|val| val == 0)); + } + + #[test] + fn get_neg_weekday_mask_with_yearly_freq_and_byweekday() { + let rrule = RRule { + freq: ICalendarFrequency::Yearly, + by_weekday: vec![ + NWeekday::new(None, Weekday::Mon), + NWeekday::new(Some(-2), Weekday::Thu), + NWeekday::new(Some(1), Weekday::Thu), + ], + ..Default::default() + } + .validate(UTC.with_ymd_and_hms(1997, 1, 1, 0, 0, 0).unwrap()) + .unwrap(); + + let year_info = YearInfo::new(1997, &rrule); + + let neg_weekday_mask = MonthInfo::get_neg_weekday_mask(&year_info, 1, &rrule); + assert_eq!(neg_weekday_mask.len(), year_info.year_len as usize); + assert!( + neg_weekday_mask + .into_iter() + .enumerate() + .all(|(idx, val)| match idx { + 1 | 351 => val == 1, + _ => val == 0, + }) + ); + } + + #[test] + fn get_neg_weekday_mask_with_monthly_freq_and_byweekday() { + let rrule = RRule { + freq: ICalendarFrequency::Monthly, + by_weekday: vec![ + NWeekday::new(None, Weekday::Mon), + NWeekday::new(Some(-2), Weekday::Thu), + NWeekday::new(Some(1), Weekday::Thu), + ], + ..Default::default() + } + .validate(UTC.with_ymd_and_hms(1997, 1, 1, 0, 0, 0).unwrap()) + .unwrap(); + + let year_info = YearInfo::new(1997, &rrule); + + let neg_weekday_mask = MonthInfo::get_neg_weekday_mask(&year_info, 1, &rrule); + assert_eq!(neg_weekday_mask.len(), year_info.year_len as usize); + assert!( + neg_weekday_mask + .into_iter() + .enumerate() + .all(|(idx, val)| match idx { + 1 | 22 => val == 1, + _ => val == 0, + }) + ); + } +} diff --git a/vendor/calcard/src/datecalc/pos_list.rs b/vendor/calcard/src/datecalc/pos_list.rs new file mode 100644 index 00000000..7ac89586 --- /dev/null +++ b/vendor/calcard/src/datecalc/pos_list.rs @@ -0,0 +1,66 @@ +/* + * SPDX-FileCopyrightText: 2021 Fredrik Meringdal, Ralph Bisschops + * + * SPDX-License-Identifier: Apache-2.0 OR MIT + */ + +use super::utils::{add_time_to_date, date_from_ordinal, pymod}; +use crate::common::timezone::Tz; +use chrono::NaiveTime; + +pub(crate) fn build_pos_list( + by_set_pos: &[i32], + dayset: &[usize], + timeset: &[NaiveTime], + year_ordinal: i64, + tz: Tz, +) -> Vec> { + let mut pos_list = vec![]; + + if timeset.is_empty() { + return vec![]; + } + + let timeset_len = (timeset.len()) as u32; + let timeset_len_float = f64::from(timeset_len); + let timeset_len_int = timeset_len as i32; + for pos in by_set_pos { + let pos = if *pos > 0 { pos - 1 } else { *pos }; + let day_pos = (f64::from(pos) / timeset_len_float).floor() as isize; + let time_pos = (pymod(pos, timeset_len_int)) as usize; + + let day_idx = if day_pos < 0 { + let dayset_len = dayset.len() as isize; + let index = dayset_len + day_pos; + match usize::try_from(index) { + Ok(day_idx) => day_idx, + Err(_) => continue, + } + } else { + (day_pos) as usize + }; + let day = match dayset.get(day_idx) { + Some(day) => day, + None => continue, + }; + let day = *day as i64; + + // Get ordinal which is UTC + let date = date_from_ordinal(year_ordinal + day); + // Create new Date + Time combination + // Use Time from `timeset`. + let time = timeset[time_pos]; + let res = match add_time_to_date(tz, date, time) { + Some(date) => date, + None => continue, + }; + + if !pos_list.contains(&res) { + pos_list.push(res); + } + } + + pos_list.sort(); + + pos_list +} diff --git a/vendor/calcard/src/datecalc/rrule.rs b/vendor/calcard/src/datecalc/rrule.rs new file mode 100644 index 00000000..44f686fd --- /dev/null +++ b/vendor/calcard/src/datecalc/rrule.rs @@ -0,0 +1,267 @@ +/* + * SPDX-FileCopyrightText: 2021 Fredrik Meringdal, Ralph Bisschops + * + * SPDX-License-Identifier: Apache-2.0 OR MIT + */ + +use super::{ + error::{RRuleError, ValidationError}, + get_day, get_hour, get_minute, get_month, get_second, + validate::validate_rrule_forced, +}; +use crate::{ + common::timezone::Tz, + icalendar::{ICalendarFrequency, ICalendarMonth, ICalendarRecurrenceRule}, +}; +use chrono::{DateTime, Datelike, Weekday}; +use std::cmp::Ordering; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct RRule { + pub(crate) freq: ICalendarFrequency, + pub(crate) interval: u16, + pub(crate) count: Option, + pub(crate) until: Option>, + pub(crate) week_start: Weekday, + pub(crate) by_set_pos: Vec, + pub(crate) by_month: Vec, + pub(crate) by_month_day: Vec, + pub(crate) by_n_month_day: Vec, + pub(crate) by_year_day: Vec, + pub(crate) by_week_no: Vec, + pub(crate) by_weekday: Vec, + pub(crate) by_hour: Vec, + pub(crate) by_minute: Vec, + pub(crate) by_second: Vec, + pub(crate) by_easter: Option, +} + +impl RRule { + pub fn from_floating_ical(ical: &ICalendarRecurrenceRule) -> Option { + Some(RRule { + freq: ical.freq, + interval: ical.interval.unwrap_or(1), + count: ical.count, + until: if let Some(until) = &ical.until { + until.to_date_time_with_tz(Tz::Floating)?.into() + } else { + None + }, + week_start: ical.wkst.map(Into::into).unwrap_or(Weekday::Mon), + by_set_pos: ical.bysetpos.clone(), + by_month: ical.bymonth.clone(), + by_month_day: ical + .bymonthday + .iter() + .filter(|v| **v > 0) + .copied() + .collect(), + by_n_month_day: ical + .bymonthday + .iter() + .filter(|v| **v < 0) + .copied() + .collect(), + by_year_day: ical.byyearday.clone(), + by_week_no: ical.byweekno.clone(), + by_weekday: ical + .byday + .iter() + .map(|wday| NWeekday::new(wday.ordwk, wday.weekday.into())) + .collect(), + by_hour: ical.byhour.clone(), + by_minute: ical.byminute.clone(), + by_second: ical.bysecond.clone(), + by_easter: None, + }) + } +} + +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub enum NWeekday { + Every(Weekday), + Nth(i16, Weekday), +} + +impl RRule { + pub(crate) fn finalize_parsed_rrule(mut self, dt_start: &DateTime) -> Self { + // Can only be set to true if the feature flag is set. + let by_easter_is_some = self.by_easter.is_some(); + + // Add some freq-specific additional properties + if !(!self.by_week_no.is_empty() + || !self.by_year_day.is_empty() + || !self.by_month_day.is_empty() + || !self.by_n_month_day.is_empty() + || !self.by_weekday.is_empty() + || by_easter_is_some) + { + match self.freq { + ICalendarFrequency::Yearly => { + if self.by_month.is_empty() { + let month = get_month(dt_start); + self.by_month = vec![ICalendarMonth::new(month, false)]; + } + let day = get_day(dt_start); + self.by_month_day = vec![day]; + } + ICalendarFrequency::Monthly => { + let day = get_day(dt_start); + self.by_month_day = vec![day]; + } + ICalendarFrequency::Weekly => { + self.by_weekday = vec![NWeekday::Every(dt_start.weekday())]; + } + _ => (), + }; + } + + // by_hour + if self.by_hour.is_empty() && self.freq < ICalendarFrequency::Hourly { + let hour = get_hour(dt_start); + self.by_hour = vec![hour]; + } + + // by_minute + if self.by_minute.is_empty() && self.freq < ICalendarFrequency::Minutely { + let minute = get_minute(dt_start); + self.by_minute = vec![minute]; + } + + // by_second + if self.by_second.is_empty() && self.freq < ICalendarFrequency::Secondly { + let second = get_second(dt_start); + self.by_second = vec![second]; + } + + // make sure all BYXXX are unique and sorted + self.by_hour.sort_unstable(); + self.by_hour.dedup(); + + self.by_minute.sort_unstable(); + self.by_minute.dedup(); + + self.by_second.sort_unstable(); + self.by_second.dedup(); + + self.by_month.sort_unstable(); + self.by_month.dedup(); + + self.by_month_day.sort_unstable(); + self.by_month_day.dedup(); + + self.by_n_month_day.sort_unstable(); + self.by_n_month_day.dedup(); + + self.by_year_day.sort_unstable(); + self.by_year_day.dedup(); + + self.by_week_no.sort_unstable(); + self.by_week_no.dedup(); + + self.by_set_pos.sort_unstable(); + self.by_set_pos.dedup(); + + self.by_weekday.sort_unstable(); + self.by_weekday.dedup(); + + self + } + + pub fn validate(self, dt_start: DateTime) -> Result { + let rrule = self.finalize_parsed_rrule(&dt_start); + + // Validate required checks (defined by RFC 5545) + validate_rrule_forced(&rrule, &dt_start)?; + + // Check if it is possible to generate a timeset + match rrule.freq { + ICalendarFrequency::Hourly => { + if rrule.by_minute.is_empty() && rrule.by_second.is_empty() { + return Err(ValidationError::UnableToGenerateTimeset.into()); + } + } + ICalendarFrequency::Minutely => { + if rrule.by_second.is_empty() { + return Err(ValidationError::UnableToGenerateTimeset.into()); + } + } + ICalendarFrequency::Secondly => {} + _ => { + if rrule.by_hour.is_empty() + && rrule.by_minute.is_empty() + && rrule.by_second.is_empty() + { + return Err(ValidationError::UnableToGenerateTimeset.into()); + } + } + } + + Ok(rrule) + } +} + +impl Default for RRule { + fn default() -> Self { + Self { + freq: ICalendarFrequency::Yearly, + interval: 1, + count: None, + until: None, + week_start: Weekday::Mon, + by_set_pos: Vec::new(), + by_month: Vec::new(), + by_month_day: Vec::new(), + by_n_month_day: Vec::new(), + by_year_day: Vec::new(), + by_week_no: Vec::new(), + by_weekday: Vec::new(), + by_hour: Vec::new(), + by_minute: Vec::new(), + by_second: Vec::new(), + by_easter: None, + } + } +} + +impl PartialOrd for NWeekday { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl Ord for NWeekday { + fn cmp(&self, other: &Self) -> Ordering { + n_weekday_cmp(*self, *other) + } +} + +// The ordering here doesn't really matter as it is only used to sort for display purposes +fn n_weekday_cmp(val1: NWeekday, val2: NWeekday) -> Ordering { + match val1 { + NWeekday::Every(wday) => match val2 { + NWeekday::Every(other_wday) => wday + .num_days_from_monday() + .cmp(&other_wday.num_days_from_monday()), + NWeekday::Nth(_n, _other_wday) => Ordering::Less, + }, + NWeekday::Nth(n, wday) => match val2 { + NWeekday::Every(_) => Ordering::Greater, + NWeekday::Nth(other_n, other_wday) => match n.cmp(&other_n) { + Ordering::Equal => wday + .num_days_from_monday() + .cmp(&other_wday.num_days_from_monday()), + less_or_greater => less_or_greater, + }, + }, + } +} + +impl NWeekday { + pub fn new(number: Option, weekday: Weekday) -> Self { + match number { + Some(number) => Self::Nth(number, weekday), + None => Self::Every(weekday), + } + } +} diff --git a/vendor/calcard/src/datecalc/rrule_iter.rs b/vendor/calcard/src/datecalc/rrule_iter.rs new file mode 100644 index 00000000..fbf9acc9 --- /dev/null +++ b/vendor/calcard/src/datecalc/rrule_iter.rs @@ -0,0 +1,212 @@ +/* + * SPDX-FileCopyrightText: 2021 Fredrik Meringdal, Ralph Bisschops + * + * SPDX-License-Identifier: Apache-2.0 OR MIT + */ + +use super::counter_date::DateTimeIter; +use super::rrule::RRule; +use super::utils::add_time_to_date; +use super::{IterInfo, MAX_ITER_LOOP, build_pos_list, utils::date_from_ordinal}; +use super::{get_hour, get_minute, get_second}; +use crate::common::timezone::Tz; +use crate::icalendar::ICalendarFrequency; +use chrono::NaiveTime; +use std::collections::VecDeque; + +#[derive(Debug, Clone)] +pub(crate) struct RRuleIter { + pub(crate) counter_date: DateTimeIter, + pub(crate) ii: IterInfo, + pub(crate) timeset: Vec, + pub(crate) dt_start: chrono::DateTime, + pub(crate) buffer: VecDeque>, + pub(crate) finished: bool, + pub(crate) count: Option, + pub(crate) limited: bool, + pub(crate) was_limited: bool, +} + +impl RRuleIter { + pub(crate) fn new(rrule: &RRule, dt_start: &chrono::DateTime, limited: bool) -> Self { + let ii = IterInfo::new(rrule, dt_start); + + let hour = get_hour(dt_start); + let minute = get_minute(dt_start); + let second = get_second(dt_start); + let timeset = ii.get_timeset(hour, minute, second); + let count = ii.rrule().count; + + Self { + counter_date: dt_start.into(), + ii, + timeset, + dt_start: *dt_start, + buffer: VecDeque::new(), + finished: false, + count, + limited, + was_limited: false, + } + } + + /// Attempts to add a date to the result. Returns `true` if we should + /// terminate the iteration. + fn try_add_datetime( + dt: chrono::DateTime, + rrule: &RRule, + count: &mut Option, + buffer: &mut VecDeque>, + dt_start: &chrono::DateTime, + ) -> bool { + if matches!(rrule.until, Some(until) if dt > until) { + // We can break because `pos_list` is sorted and + // all the next dates will only be larger than `until`. + return true; + } + + if dt >= *dt_start { + buffer.push_back(dt); + + if let Some(count) = count { + *count -= 1; + + if *count == 0 { + return true; + } + } + } + false + } + + /// Generates a list of dates that will be added to the buffer. + /// Returns true if finished, no more items should/can be returned. + fn generate(&mut self) -> bool { + // Do early check if done (if known) + if self.finished { + return true; + } + // Check if the count is set, and if 0 + if matches!(self.count, Some(count) if count == 0) { + return true; + } + + let rrule = self.ii.rrule(); + + if rrule.interval == 0 { + return true; + } + + let mut loop_counter: u32 = 0; + // Loop until there is at least 1 item in the buffer. + while self.buffer.is_empty() { + // Prevent infinite loops + if self.limited { + loop_counter += 1; + if loop_counter >= MAX_ITER_LOOP { + self.finished = true; + self.was_limited = true; + return true; + } + } + let rrule = self.ii.rrule(); + + let dayset = self.ii.get_dayset( + rrule.freq, + self.counter_date.year, + self.counter_date.month, + self.counter_date.day, + ); + + let tz = self.dt_start.timezone(); + + if rrule.by_set_pos.is_empty() { + // Loop over `start..end` + for current_day in &dayset { + let current_day = *current_day as i64; + let year_ordinal = self.ii.year_ordinal(); + // Ordinal conversion uses UTC: if we apply local-TZ here, then + // just below we'll end up double-applying. + let date = date_from_ordinal(year_ordinal + current_day); + for time in &self.timeset { + let Some(dt) = add_time_to_date(tz, date, *time) else { + continue; + }; + if Self::try_add_datetime( + dt, + rrule, + &mut self.count, + &mut self.buffer, + &self.dt_start, + ) { + return true; + } + } + } + } else { + let pos_list = build_pos_list( + &rrule.by_set_pos, + &dayset, + &self.timeset, + self.ii.year_ordinal(), + self.dt_start.timezone(), + ); + for dt in pos_list { + if Self::try_add_datetime( + dt, + rrule, + &mut self.count, + &mut self.buffer, + &self.dt_start, + ) { + return true; + } + } + } + + let increment_day = dayset.is_empty(); + if self.counter_date.increment(rrule, increment_day).is_err() { + self.finished = true; + return true; + } + + if matches!( + rrule.freq, + ICalendarFrequency::Hourly + | ICalendarFrequency::Minutely + | ICalendarFrequency::Secondly + ) { + let hour = self.counter_date.hour as u8; + let minute = self.counter_date.minute as u8; + let second = self.counter_date.second as u8; + self.timeset = self.ii.get_timeset_unchecked(hour, minute, second); + } + + self.ii.rebuild(&self.counter_date); + } + + // Indicate that there might be more items on the next iteration. + false + } +} + +impl Iterator for RRuleIter { + type Item = chrono::DateTime; + + fn next(&mut self) -> Option { + if !self.buffer.is_empty() { + return self.buffer.pop_front(); + } + + if self.finished { + return None; + } + + self.finished = self.generate(); + + if self.buffer.is_empty() { + self.finished = true; + } + self.buffer.pop_front() + } +} diff --git a/vendor/calcard/src/datecalc/utils.rs b/vendor/calcard/src/datecalc/utils.rs new file mode 100644 index 00000000..e9c5b07f --- /dev/null +++ b/vendor/calcard/src/datecalc/utils.rs @@ -0,0 +1,194 @@ +/* + * SPDX-FileCopyrightText: 2021 Fredrik Meringdal, Ralph Bisschops + * + * SPDX-License-Identifier: Apache-2.0 OR MIT + */ + +use super::duration_from_midnight; +use crate::common::timezone::Tz; +use chrono::{NaiveDate, NaiveTime, Utc}; +use std::ops; + +const DAY_SECS: i64 = 24 * 60 * 60; + +#[inline(always)] +pub(crate) fn date_from_ordinal(ordinal: i64) -> NaiveDate { + chrono::DateTime::::from_timestamp(ordinal * DAY_SECS, 0) + .unwrap_or_default() + .date_naive() +} + +#[inline(always)] +pub(crate) fn days_since_unix_epoch(date: &chrono::DateTime) -> i64 { + date.timestamp() / DAY_SECS +} + +#[inline(always)] +pub(crate) fn is_leap_year(year: i32) -> bool { + year.trailing_zeros() >= 2 && (year % 25 != 0 || year.trailing_zeros() >= 4) +} + +#[inline(always)] +pub(crate) fn get_year_len(year: i32) -> u16 { + if is_leap_year(year) { 366 } else { 365 } +} + +pub(crate) trait DifferentSigns { + fn different_sign(a: Self, b: Self) -> bool; +} + +macro_rules! impl_different_signs { + ($($ty:ty),*) => { + $( + impl DifferentSigns for $ty { + fn different_sign(a: Self, b: Self) -> bool { + a > 0 && b < 0 || a < 0 && b > 0 + } + })* + }; + (@unsigned, $($ty:ty),*) => { + $( + impl DifferentSigns for $ty { + fn different_sign(_: Self, _: Self) -> bool { + false + } + })* + }; +} +impl_different_signs!(isize, i32, i16); +impl_different_signs!(@unsigned, usize, u32, u16, u8); + +pub(crate) fn pymod(a: T, b: T) -> T +where + T: DifferentSigns + Copy + ops::Rem + ops::Add, +{ + let r = a % b; + // If r and b differ in sign, add b to wrap the result to the correct sign. + if T::different_sign(r, b) { r + b } else { r } +} + +pub(crate) fn add_time_to_date( + tz: Tz, + date: NaiveDate, + time: NaiveTime, +) -> Option> { + if let Some(dt) = date.and_time(time).and_local_timezone(tz).single() { + return Some(dt); + } + // If the day is a daylight saving time, the above code might not work, and we + // can try to get a valid datetime by adding the `time` as a duration instead. + let dt = date.and_hms_opt(0, 0, 0)?.and_local_timezone(tz).single()?; + let day_duration = duration_from_midnight(time); + dt.checked_add_signed(day_duration) +} + +#[cfg(test)] +mod test { + use super::*; + use chrono::{Duration, TimeZone}; + + #[test] + fn naive_date_from_ordinal() { + let tests = [ + (-1, NaiveDate::from_ymd_opt(1969, 12, 31).unwrap()), + (0, NaiveDate::from_ymd_opt(1970, 1, 1).unwrap()), + (1, NaiveDate::from_ymd_opt(1970, 1, 2).unwrap()), + (10, NaiveDate::from_ymd_opt(1970, 1, 11).unwrap()), + (365, NaiveDate::from_ymd_opt(1971, 1, 1).unwrap()), + (19877, NaiveDate::from_ymd_opt(2024, 6, 3).unwrap()), + ]; + + for (days, expected) in tests { + assert_eq!(date_from_ordinal(days), expected, "seconds: {}", days); + } + } + + #[test] + fn python_mod() { + assert_eq!(pymod(2, -3), -1); + assert_eq!(pymod(-2, 3), 1); + assert_eq!(pymod(-2, -3), -2); + assert_eq!(pymod(-3, -3), 0); + assert_eq!(pymod(0, 3), 0); + assert_eq!(pymod(1, 3), 1); + assert_eq!(pymod(2, 3), 2); + assert_eq!(pymod(3, 3), 0); + assert_eq!(pymod(4, 3), 1); + assert_eq!(pymod(6, 3), 0); + assert_eq!(pymod(-6, 3), 0); + assert_eq!(pymod(-6, -3), 0); + assert_eq!(pymod(6, -3), 0); + } + + #[test] + fn leap_year() { + let tests = [ + (2015, false), + (2016, true), + (2017, false), + (2018, false), + (2019, false), + (2020, true), + (2021, false), + ]; + + for (year, expected_output) in tests { + let res = is_leap_year(year); + assert_eq!(res, expected_output); + } + } + + #[test] + fn year_length() { + let tests = [(2015, 365), (2016, 366)]; + + for (year, expected_output) in tests { + let res = get_year_len(year); + assert_eq!(res, expected_output); + } + } + + #[test] + fn adds_time_to_date() { + let tests = [ + ( + Tz::Tz(chrono_tz::Tz::UTC), + NaiveDate::from_ymd_opt(2017, 1, 1).unwrap(), + NaiveTime::from_hms_opt(1, 15, 30).unwrap(), + Some( + Tz::Tz(chrono_tz::Tz::UTC) + .with_ymd_and_hms(2017, 1, 1, 1, 15, 30) + .unwrap(), + ), + ), + ( + Tz::Tz(chrono_tz::Tz::America__Vancouver), + NaiveDate::from_ymd_opt(2021, 3, 14).unwrap(), + NaiveTime::from_hms_opt(2, 22, 10).unwrap(), + Some( + Tz::Tz(chrono_tz::Tz::America__Vancouver) + .with_ymd_and_hms(2021, 3, 14, 0, 0, 0) + .unwrap() + + Duration::hours(2) + + Duration::minutes(22) + + Duration::seconds(10), + ), + ), + ( + Tz::Tz(chrono_tz::Tz::America__New_York), + NaiveDate::from_ymd_opt(1997, 10, 26).unwrap(), + NaiveTime::from_hms_opt(9, 0, 0).unwrap(), + Some( + Tz::Tz(chrono_tz::Tz::America__New_York) + .with_ymd_and_hms(1997, 10, 26, 9, 0, 0) + .unwrap(), + ), + ), + ]; + + for (tz, date, time, expected_output) in tests { + let res = add_time_to_date(tz, date, time); + assert_eq!(res, expected_output); + } + } +} diff --git a/vendor/calcard/src/datecalc/validate.rs b/vendor/calcard/src/datecalc/validate.rs new file mode 100644 index 00000000..45c28bcf --- /dev/null +++ b/vendor/calcard/src/datecalc/validate.rs @@ -0,0 +1,647 @@ +/* + * SPDX-FileCopyrightText: 2021 Fredrik Meringdal, Ralph Bisschops + * + * SPDX-License-Identifier: Apache-2.0 OR MIT + */ + +use super::{ + error::ValidationError, + rrule::{NWeekday, RRule}, +}; +use crate::{common::timezone::Tz, icalendar::ICalendarFrequency}; +use std::{borrow::Cow, ops::RangeInclusive}; + +pub(crate) static MONTH_RANGE: RangeInclusive = 1..=12; +pub(crate) static YEAR_RANGE: RangeInclusive = -10_000..=10_000; + +type Validator = &'static dyn Fn(&RRule, &chrono::DateTime) -> Result<(), ValidationError>; + +const VALIDATION_PIPELINE: [Validator; 11] = [ + &validate_until, + &validate_by_set_pos, + &validate_by_month, + &validate_by_month_day, + &validate_by_year_day, + &validate_by_week_number, + &validate_by_weekday, + &validate_by_hour, + &validate_by_minute, + &validate_by_second, + &validate_by_easter, +]; + +/// Check if rules are valid as defined by the RFC and crate limitations. +/// It checks all values in the [`RRule`] and makes sure that they are in +/// the accepted ranges. If the function returns `Ok`, no errors where found. +/// +/// This check should always be done and just enforces limits set by the standard. +/// Validation will always be enforced and can not be disabled using feature flags. +pub(crate) fn validate_rrule_forced( + rrule: &RRule, + dt_start: &chrono::DateTime, +) -> Result<(), ValidationError> { + VALIDATION_PIPELINE + .into_iter() + .try_for_each(|validator| validator(rrule, dt_start)) +} + +// Until: +// - Timezones are correctly synced as specified in the RFC +// - Value should be later than `dt_start`. +fn validate_until(rrule: &RRule, dt_start: &chrono::DateTime) -> Result<(), ValidationError> { + match rrule.until { + Some(until) => { + match dt_start.timezone() { + Tz::Tz(_) => { + if until.timezone() != Tz::UTC { + return Err(ValidationError::DtStartUntilMismatchTimezone { + dt_start_tz: dt_start + .timezone() + .name() + .unwrap_or(Cow::Borrowed("Floating")), + until_tz: until.timezone().name().unwrap_or(Cow::Borrowed("Floating")), + expected: vec!["UTC".into()], + }); + } + } + v => { + let allowed_timezones = vec![v, Tz::UTC]; + if !allowed_timezones.contains(&until.timezone()) { + return Err(ValidationError::DtStartUntilMismatchTimezone { + dt_start_tz: dt_start + .timezone() + .name() + .unwrap_or(Cow::Borrowed("Floating")), + until_tz: until.timezone().name().unwrap_or(Cow::Borrowed("Floating")), + expected: allowed_timezones + .into_iter() + .map(|tz| tz.name().unwrap_or(Cow::Borrowed("Floating"))) + .collect(), + }); + } + } + } + + if until < *dt_start { + return Err(ValidationError::UntilBeforeStart { + until: until.to_rfc3339(), + dt_start: dt_start.to_rfc3339(), + }); + } + Ok(()) + } + _ => Ok(()), + } +} + +// By_set_pos: +// - Can be a value from -366 to -1 and 1 to 366 depending on `freq` +fn validate_by_set_pos( + rrule: &RRule, + _dt_start: &chrono::DateTime, +) -> Result<(), ValidationError> { + validate_not_equal_for_vec(&0, &rrule.by_set_pos, "BYSETPOS")?; + let range = match rrule.freq { + ICalendarFrequency::Yearly | ICalendarFrequency::Daily => -366..=366, // TODO is the daily range correct? + ICalendarFrequency::Monthly => -31..=31, + ICalendarFrequency::Weekly => -53..=53, + ICalendarFrequency::Hourly => -24..=24, + ICalendarFrequency::Minutely | ICalendarFrequency::Secondly => -60..=60, + }; + if let Err(value) = validate_range_for_vec_error(&range, &rrule.by_set_pos) { + return Err(ValidationError::InvalidFieldValueRangeWithFreq { + field: "BYSETPOS".into(), + value: value.to_string(), + freq: rrule.freq, + start_idx: range.start().to_string(), + end_idx: range.end().to_string(), + }); + } + // - It MUST only be used in conjunction with another BYxxx rule part. + if !rrule.by_set_pos.is_empty() + && rrule.by_easter.is_none() + && rrule.by_hour.is_empty() + && rrule.by_minute.is_empty() + && rrule.by_second.is_empty() + && rrule.by_month_day.is_empty() + && rrule.by_month.is_empty() + && rrule.by_year_day.is_empty() + && rrule.by_week_no.is_empty() + && rrule.by_weekday.is_empty() + { + return Err(ValidationError::BySetPosWithoutByRule); + } + Ok(()) +} + +// By_month: +// - Can be a value from 1 to 12. +fn validate_by_month( + rrule: &RRule, + _dt_start: &chrono::DateTime, +) -> Result<(), ValidationError> { + for item in &rrule.by_month { + if !MONTH_RANGE.contains(&item.month()) { + return Err(ValidationError::InvalidFieldValueRange { + field: "BYMONTH".to_string(), + value: item.to_string(), + start_idx: 1.to_string(), + end_idx: 12.to_string(), + }); + } + } + Ok(()) +} + +// By_month_day: +// - Can be a value from -31 to -1 and 1 to 31. +fn validate_by_month_day( + rrule: &RRule, + _dt_start: &chrono::DateTime, +) -> Result<(), ValidationError> { + validate_not_equal_for_vec(&0, &rrule.by_month_day, "BYMONTHDAY")?; + validate_range_for_vec(&(-31..=31), &rrule.by_month_day, "BYMONTHDAY")?; + // - MUST NOT be specified when the FREQ rule part is set to WEEKLY. + // Validated below + if !rrule.by_month_day.is_empty() { + let valid = rrule.freq != ICalendarFrequency::Weekly; + if !valid { + return Err(ValidationError::InvalidByRuleAndFrequency { + by_rule: "BYMONTHDAY".into(), + freq: rrule.freq, + }); + } + } + Ok(()) +} + +// By_year_day: +// - Can be a value from -366 to -1 and 1 to 366. +fn validate_by_year_day( + rrule: &RRule, + _dt_start: &chrono::DateTime, +) -> Result<(), ValidationError> { + validate_not_equal_for_vec(&0, &rrule.by_year_day, "BYYEARDAY")?; + validate_range_for_vec(&(-366..=366), &rrule.by_year_day, "BYYEARDAY")?; + // - MUST NOT be specified when the FREQ rule part is set to DAILY, WEEKLY, or MONTHLY. + // Validated below + if !rrule.by_year_day.is_empty() { + let valid = !matches!( + rrule.freq, + ICalendarFrequency::Monthly | ICalendarFrequency::Weekly | ICalendarFrequency::Daily + ); + if !valid { + return Err(ValidationError::InvalidByRuleAndFrequency { + by_rule: "BYYEARDAY".into(), + freq: rrule.freq, + }); + } + } + Ok(()) +} +// By_week_no: +// - Can be a value from -53 to -1 and 1 to 53. +fn validate_by_week_number( + rrule: &RRule, + _dt_start: &chrono::DateTime, +) -> Result<(), ValidationError> { + validate_not_equal_for_vec(&0, &rrule.by_week_no, "BYWEEKNO")?; + validate_range_for_vec(&(-53..=53), &rrule.by_week_no, "BYWEEKNO")?; + // - MUST NOT be used when the FREQ rule part is set to anything other than YEARLY. + // Validated below + if !rrule.by_week_no.is_empty() { + let valid = rrule.freq == ICalendarFrequency::Yearly; + if !valid { + return Err(ValidationError::InvalidByRuleAndFrequency { + by_rule: "BYWEEKNO".into(), + freq: rrule.freq, + }); + } + } + Ok(()) +} + +// By_weekday: +// - Check if value for `Nth` is within range. +// The Range depends on frequency and can only happen weekly, so `/7` from normal count. +fn validate_by_weekday( + rrule: &RRule, + _dt_start: &chrono::DateTime, +) -> Result<(), ValidationError> { + let range = match rrule.freq { + ICalendarFrequency::Yearly | ICalendarFrequency::Daily => (-366 / 7)..=(366 / 7 + 1), // TODO is the daily range correct? + ICalendarFrequency::Monthly => (-31 / 7)..=(31 / 7 + 1), + ICalendarFrequency::Weekly => (-53 / 7)..=(53 / 7 + 1), + ICalendarFrequency::Hourly => (-24 / 7)..=(24 / 7 + 1), + ICalendarFrequency::Minutely | ICalendarFrequency::Secondly => (-60 / 7)..=(60 / 7 + 1), + }; + for item in &rrule.by_weekday { + if let NWeekday::Nth(number, _weekday) = item { + // If value not in range = error + if !range.contains(number) { + return Err(ValidationError::InvalidFieldValueRangeWithFreq { + field: "BYDAY".into(), + value: number.to_string(), + freq: rrule.freq, + start_idx: range.start().to_string(), + end_idx: range.end().to_string(), + }); + } + } + } + Ok(()) +} + +// By_hour: +// - Can be a value from 0 to 23. +fn validate_by_hour( + rrule: &RRule, + _dt_start: &chrono::DateTime, +) -> Result<(), ValidationError> { + validate_range_for_vec(&(0..=23), &rrule.by_hour, "BYHOUR") +} + +// By_minute: +// - Can be a value from 0 to 59. +fn validate_by_minute( + rrule: &RRule, + _dt_start: &chrono::DateTime, +) -> Result<(), ValidationError> { + validate_range_for_vec(&(0..=59), &rrule.by_minute, "BYMINUTE") +} + +// By_second: +// - Can be a value from 0 to 59. +fn validate_by_second( + rrule: &RRule, + _dt_start: &chrono::DateTime, +) -> Result<(), ValidationError> { + validate_range_for_vec(&(0..=59), &rrule.by_second, "BYSECOND") +} + +fn validate_by_easter( + rrule: &RRule, + _dt_start: &chrono::DateTime, +) -> Result<(), ValidationError> { + // By_easter: + // - Can be a value from -366 to 366. + // Validated below + if let Some(by_easter) = &rrule.by_easter { + validate_range_for_vec(&(-366..=366), &[*by_easter], "BYEASTER")?; + } + // - Can only be used on frequency: Yearly, Monthly, Daily + // Validated below + if rrule.by_easter.is_some() { + let valid = matches!( + rrule.freq, + ICalendarFrequency::Yearly | ICalendarFrequency::Monthly | ICalendarFrequency::Daily + ); + if !valid { + return Err(ValidationError::InvalidByRuleAndFrequency { + by_rule: "BYEASTER".into(), + freq: rrule.freq, + }); + } + } + // - Can only be used when `by_hour`, `by_minute` and `by_second` are used. + // TODO don't know why this is true, but seems to loop forever otherwise. + if rrule.by_easter.is_some() + && (rrule.by_hour.is_empty() || rrule.by_minute.is_empty() || rrule.by_second.is_empty()) + { + return Err(ValidationError::InvalidByRuleWithByEaster); + } + + Ok(()) +} + +fn validate_range_for_vec_error<'a, T: PartialOrd>( + range: &RangeInclusive, + list: &'a [T], +) -> Result<(), &'a T> { + for item in list { + // If value not in range = error + if !range.contains(item) { + return Err(item); + } + } + Ok(()) +} + +fn validate_range_for_vec( + range: &RangeInclusive, + list: &[T], + field: &str, +) -> Result<(), ValidationError> { + for item in list { + // If value not in range = error + if !range.contains(item) { + return Err(ValidationError::InvalidFieldValueRange { + field: field.to_string(), + value: item.to_string(), + start_idx: range.start().to_string(), + end_idx: range.end().to_string(), + }); + } + } + Ok(()) +} + +fn validate_not_equal_for_vec + ToString>( + value: &T, + list: &[T], + field: &str, +) -> Result<(), ValidationError> { + if list.iter().any(|item| item == value) { + return Err(ValidationError::InvalidFieldValue { + field: field.to_string(), + value: value.to_string(), + }); + } + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::icalendar::ICalendarFrequency; + use chrono::TimeZone; + + const UTC: Tz = Tz::UTC; + + #[test] + fn rejects_by_set_pos_without_byxxx_rule() { + let rrule = RRule { + by_set_pos: vec![-1], + ..Default::default() + }; + let dt_start = UTC.with_ymd_and_hms(1970, 1, 1, 0, 0, 0).unwrap(); + let res = validate_rrule_forced(&rrule, &dt_start); + assert!(res.is_err()); + let err = res.unwrap_err(); + assert_eq!(err, ValidationError::BySetPosWithoutByRule); + + // When creating RRule directly, + // if `rrule.by_set_hour` is empty, then it is going to default to + // `dt_start.hour()`, therefore, there is always a BYXXX + // rule, and the rrule is accepted. + let res = rrule.validate(dt_start); + assert!(res.is_ok()); + } + + #[test] + fn rejects_by_rule_field_with_invalid_value() { + let tests = [ + ( + "BYSETPOS", + RRule { + by_set_pos: vec![0], + ..Default::default() + }, + ), + ( + "BYSETPOS", + RRule { + by_set_pos: vec![1, -2, 0], + ..Default::default() + }, + ), + ( + "BYMONTHDAY", + RRule { + by_month_day: vec![0], + ..Default::default() + }, + ), + ( + "BYYEARDAY", + RRule { + by_year_day: vec![0], + ..Default::default() + }, + ), + ]; + for (field, rrule) in tests { + let res = + validate_rrule_forced(&rrule, &UTC.with_ymd_and_hms(1970, 1, 1, 0, 0, 0).unwrap()); + assert!(res.is_err()); + let err = res.unwrap_err(); + assert_eq!( + err, + ValidationError::InvalidFieldValue { + field: field.into(), + value: "0".into() + } + ); + } + } + + #[test] + fn rejects_by_rule_field_with_value_outside_allowed_range() { + let tests = [ + ( + "BYMONTHDAY", + RRule { + by_month_day: vec![34], + ..Default::default() + }, + "34", + "-31", + "31", + ), + ( + "BYYEARDAY", + RRule { + by_year_day: vec![17, 400], + ..Default::default() + }, + "400", + "-366", + "366", + ), + ]; + for (field, rrule, value, start_idx, end_idx) in tests { + let res = + validate_rrule_forced(&rrule, &UTC.with_ymd_and_hms(1970, 1, 1, 0, 0, 0).unwrap()); + assert!(res.is_err()); + let err = res.unwrap_err(); + assert_eq!( + err, + ValidationError::InvalidFieldValueRange { + field: field.into(), + value: value.into(), + start_idx: start_idx.into(), + end_idx: end_idx.into() + } + ); + } + } + + #[test] + fn rejects_by_rule_value_outside_allowed_freq_range() { + let tests = [ + ( + "BYSETPOS", + RRule { + freq: ICalendarFrequency::Hourly, + by_set_pos: vec![30], + ..Default::default() + }, + "30", + "-24", + "24", + ), + ( + "BYSETPOS", + RRule { + freq: ICalendarFrequency::Yearly, + by_set_pos: vec![400], + ..Default::default() + }, + "400", + "-366", + "366", + ), + ]; + for (field, rrule, value, start_idx, end_idx) in tests { + let res = + validate_rrule_forced(&rrule, &UTC.with_ymd_and_hms(1970, 1, 1, 0, 0, 0).unwrap()); + assert!(res.is_err()); + let err = res.unwrap_err(); + assert_eq!( + err, + ValidationError::InvalidFieldValueRangeWithFreq { + field: field.into(), + freq: rrule.freq, + value: value.into(), + start_idx: start_idx.into(), + end_idx: end_idx.into(), + } + ); + } + } + + #[test] + fn rejects_invalid_by_rule_and_freq_combos() { + let tests = [ + ( + "BYMONTHDAY", + RRule { + freq: ICalendarFrequency::Weekly, + by_month_day: vec![-1], + ..Default::default() + }, + ), + ( + "BYYEARDAY", + RRule { + freq: ICalendarFrequency::Monthly, + by_year_day: vec![120], + ..Default::default() + }, + ), + ( + "BYYEARDAY", + RRule { + freq: ICalendarFrequency::Weekly, + by_year_day: vec![120], + ..Default::default() + }, + ), + ( + "BYYEARDAY", + RRule { + freq: ICalendarFrequency::Daily, + by_year_day: vec![120], + ..Default::default() + }, + ), + ]; + for (field, rrule) in tests { + let res = + validate_rrule_forced(&rrule, &UTC.with_ymd_and_hms(1970, 1, 1, 0, 0, 0).unwrap()); + assert!(res.is_err()); + let err = res.unwrap_err(); + assert_eq!( + err, + ValidationError::InvalidByRuleAndFrequency { + by_rule: field.into(), + freq: rrule.freq + } + ); + } + } + + #[test] + fn rejects_start_date_after_until() { + let rrule = RRule { + until: Some(UTC.with_ymd_and_hms(2020, 1, 1, 0, 0, 0).unwrap()), + ..Default::default() + }; + let dt_start = UTC.with_ymd_and_hms(2020, 1, 2, 0, 0, 0).unwrap(); + let res = validate_rrule_forced(&rrule, &dt_start); + assert!(res.is_err()); + let err = res.unwrap_err(); + assert_eq!( + err, + ValidationError::UntilBeforeStart { + until: rrule.until.unwrap().to_rfc3339(), + dt_start: dt_start.to_rfc3339() + } + ); + } + + #[test] + fn allows_until_with_compatible_timezone() { + fn t(start_tz: Tz, until_tz: Tz) -> (chrono::DateTime, chrono::DateTime) { + ( + start_tz.with_ymd_and_hms(2020, 1, 1, 0, 0, 0).unwrap(), + until_tz.with_ymd_and_hms(2020, 1, 1, 0, 0, 0).unwrap(), + ) + } + + let tests = [ + t(Tz::Floating, Tz::Floating), + t(Tz::Floating, UTC), + t(UTC, UTC), + ]; + + for (start_date, until) in tests { + let rrule = RRule { + until: Some(until), + ..Default::default() + }; + let res = validate_rrule_forced(&rrule, &start_date); + assert!(res.is_ok()); + } + } + + #[test] + fn rejects_until_with_incompatible_timezone() { + fn t(start_tz: Tz, until_tz: Tz) -> (chrono::DateTime, chrono::DateTime) { + ( + start_tz.with_ymd_and_hms(2020, 1, 1, 0, 0, 0).unwrap(), + until_tz.with_ymd_and_hms(2020, 1, 1, 0, 0, 0).unwrap(), + ) + } + + let tests = [ + t(Tz::Tz(chrono_tz::Tz::UTC), Tz::Floating), + t(Tz::Tz(chrono_tz::Europe::Berlin), Tz::Floating), + t(Tz::Floating, Tz::Tz(chrono_tz::Europe::Berlin)), + ]; + + for (start_date, until) in tests { + let rrule = RRule { + until: Some(until), + ..Default::default() + }; + let res = validate_rrule_forced(&rrule, &start_date); + assert!(res.is_err()); + let err = res.unwrap_err(); + assert!(matches!( + err, + ValidationError::DtStartUntilMismatchTimezone { .. } + )); + } + } +} diff --git a/vendor/calcard/src/datecalc/yearinfo.rs b/vendor/calcard/src/datecalc/yearinfo.rs new file mode 100644 index 00000000..487f6644 --- /dev/null +++ b/vendor/calcard/src/datecalc/yearinfo.rs @@ -0,0 +1,202 @@ +/* + * SPDX-FileCopyrightText: 2021 Fredrik Meringdal, Ralph Bisschops + * + * SPDX-License-Identifier: Apache-2.0 OR MIT + */ + +use super::{ + masks::MASKS, + rrule::RRule, + utils::{days_since_unix_epoch, get_year_len, pymod}, +}; +use chrono::{Datelike, TimeZone, Utc}; + +#[derive(Debug)] +pub(crate) struct BaseMasks { + month_mask: &'static [u8], + month_day_mask: &'static [i8], + neg_month_day_mask: &'static [i8], + month_range: &'static [u16], + weekday_mask: &'static [u32], +} + +fn base_year_masks(year_weekday: u16, year_len: u16) -> BaseMasks { + if year_len == 365 { + BaseMasks { + month_mask: &MASKS.month_365, + month_day_mask: &MASKS.month_day_365, + neg_month_day_mask: &MASKS.neg_month_day_365, + month_range: &MASKS.month_365_range, + weekday_mask: &MASKS.weekday[usize::from(year_weekday)..], + } + } else { + BaseMasks { + month_mask: &MASKS.month_366, + month_day_mask: &MASKS.month_day_366, + neg_month_day_mask: &MASKS.neg_month_day_366, + month_range: &MASKS.month_366_range, + weekday_mask: &MASKS.weekday[usize::from(year_weekday)..], + } + } +} + +#[derive(Debug, Clone)] +pub(crate) struct YearInfo { + /// The year + pub year: i32, + /// Number of days in the current year (365 or 366) + pub year_len: u16, + /// Number of days in the next year (365 or 366) + pub next_year_len: u16, + /// Number of days since Unix epoch + pub year_ordinal: i64, + pub month_mask: &'static [u8], + pub month_day_mask: &'static [i8], + pub neg_month_day_mask: &'static [i8], + pub month_range: &'static [u16], + pub weekday_mask: &'static [u32], + /// Week number mask + pub week_no_mask: Option>, +} + +impl YearInfo { + pub fn new(year: i32, rrule: &RRule) -> Self { + // It should never fail, since there is always a 1st of January, is there? + let first_year_day = Utc.with_ymd_and_hms(year, 1, 1, 0, 0, 0).unwrap(); + + let year_len = get_year_len(year); + let next_year_len = get_year_len(year + 1); + let year_ordinal = days_since_unix_epoch(&first_year_day); + let year_start_weekday = first_year_day.weekday().num_days_from_monday() as u16; + let base_masks = base_year_masks(year_start_weekday, year_len); + + let mut result = Self { + year, + year_len, + next_year_len, + year_ordinal, + week_no_mask: None, + month_mask: base_masks.month_mask, + month_day_mask: base_masks.month_day_mask, + neg_month_day_mask: base_masks.neg_month_day_mask, + month_range: base_masks.month_range, + weekday_mask: base_masks.weekday_mask, + }; + + if rrule.by_week_no.is_empty() { + return result; + } + + let mut week_no_mask = vec![0; usize::from(year_len) + 7]; + + let rrule_week_start = rrule.week_start.num_days_from_monday() as u16; + let mut no1_week_start = pymod(7 - year_start_weekday + rrule_week_start, 7); + let first_week_start = no1_week_start; + let year_len_ext = if no1_week_start >= 4 { + no1_week_start = 0; + // Number of days in the year, plus the days we got + // from last year. + let diff = i32::from(year_start_weekday) - i32::from(rrule_week_start); + result.year_len + pymod(diff, 7) as u16 + } else { + // Number of days in the year, minus the days we + // left in last year. + year_len - no1_week_start + }; + + let div = year_len_ext / 7; + let year_mod = pymod(year_len_ext, 7); + let num_weeks = div + (year_mod / 4); + + for &(mut n) in &rrule.by_week_no { + let num_weeks = num_weeks as i8; + if n < 0 { + n += num_weeks + 1; + } + if !(n > 0 && n <= num_weeks) { + continue; + } + + let i = if n > 1 { + let n = n as u16; + let mut i = no1_week_start + ((n - 1) * 7); + if no1_week_start != first_week_start { + i -= 7 - first_week_start; + } + i + } else { + no1_week_start + }; + + for j in i..i + 7 { + let j = usize::from(j); + week_no_mask[j] = 1; + if result.weekday_mask[j + 1] == rrule.week_start.num_days_from_monday() { + break; + } + } + } + + if rrule.by_week_no.contains(&1) { + // Check week number 1 of next years as well + let mut i = no1_week_start + num_weeks * 7; + if no1_week_start != first_week_start { + i -= 7 - first_week_start; + } + if i < year_len { + // If week starts in next year, we + // don't care about it. + for j in i..i + 7 { + let j = usize::from(j); + week_no_mask[j] = 1; + if result.weekday_mask[j + 1] == rrule.week_start.num_days_from_monday() { + break; + } + } + } + } + + if no1_week_start > 0 { + // Check last week number of last year as + // well. If no1_week_start is 0, either the year + // started on week start, or week number 1 + // got days from last year, so there are no + // days from last year's last week number in + // this year. + let l_num_weeks = if rrule.by_week_no.contains(&-1) { + -1 + } else { + let l_year_weekday = Utc + .with_ymd_and_hms(year - 1, 1, 1, 0, 0, 0) + // It should never fail, since there is always a 1st of January, is there? + .unwrap() + .weekday() + .num_days_from_monday() as u16; + + let rrule_week_start = rrule.week_start.num_days_from_monday() as u16; + let ln_no1_week_start = pymod(7 - l_year_weekday + rrule_week_start, 7); + + let l_year_len = get_year_len(year - 1); + let week_start = if ln_no1_week_start >= 4 { + l_year_len + + pymod(i32::from(l_year_weekday) - i32::from(rrule_week_start), 7) as u16 + } else { + year_len - no1_week_start + }; + + (52 + pymod(week_start, 7) / 4) as i8 + }; + + if rrule.by_week_no.contains(&l_num_weeks) { + for i in 0..no1_week_start { + let i = usize::from(i); + week_no_mask[i] = 1; + } + } + } + + result.week_no_mask = Some(week_no_mask); + + result + } +} diff --git a/vendor/calcard/src/icalendar/builder.rs b/vendor/calcard/src/icalendar/builder.rs new file mode 100644 index 00000000..c6db9e3a --- /dev/null +++ b/vendor/calcard/src/icalendar/builder.rs @@ -0,0 +1,808 @@ +/* + * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC + * + * SPDX-License-Identifier: Apache-2.0 OR MIT + */ + +use crate::{ + common::{ + IanaType, PartialDateTime, + parser::{Boolean, Integer}, + }, + icalendar::*, +}; +use ahash::{AHashMap, AHashSet}; + +impl ICalendar { + pub fn remove_component_ids(&mut self, component_ids: &[u32]) { + // Validate component IDs + let max_component_id = self.components.len() as u32; + let mut remove_component_ids = AHashSet::from_iter( + component_ids + .iter() + .filter(|id| **id < max_component_id) + .cloned(), + ); + + // Add sub-components to the set + for (component_id, component) in self.components.iter().enumerate() { + if remove_component_ids.contains(&(component_id as u32)) { + remove_component_ids.extend(&component.component_ids); + } + } + + if !remove_component_ids.is_empty() { + let id_mappings = (0..max_component_id) + .filter(|i| !remove_component_ids.contains(i)) + .enumerate() + .map(|(new_id, old_id)| (old_id, new_id as u32)) + .collect::>(); + + for (component_id, mut component) in + std::mem::replace(&mut self.components, Vec::with_capacity(id_mappings.len())) + .into_iter() + .enumerate() + { + if !remove_component_ids.contains(&(component_id as u32)) { + let component_ids = component + .component_ids + .iter() + .filter_map(|id| id_mappings.get(id).cloned()) + .collect(); + component.component_ids = component_ids; + self.components.push(component); + } + } + } + } + + pub fn copy_timezones(&mut self, other: &ICalendar) { + for component in &other.components { + if component.component_type == ICalendarComponentType::VTimezone { + let tz_component_id = self.components.len(); + self.components[0] + .component_ids + .insert(1, tz_component_id as u32); + self.components.push(ICalendarComponent { + component_type: ICalendarComponentType::VTimezone, + entries: component.entries.clone(), + component_ids: vec![], + }); + for component_id in &component.component_ids { + let item_id = self.components.len() as u32; + let item = &other.components[*component_id as usize]; + self.components.push(ICalendarComponent { + component_type: item.component_type.clone(), + entries: item.entries.clone(), + component_ids: vec![], + }); + self.components[tz_component_id].component_ids.push(item_id); + } + } + } + } +} + +impl ICalendarComponent { + pub fn new(component_type: ICalendarComponentType) -> Self { + Self { + component_type, + entries: Vec::new(), + component_ids: Vec::new(), + } + } + + pub fn add_dtstamp(&mut self, dt_stamp: PartialDateTime) { + self.entries.push(ICalendarEntry { + name: ICalendarProperty::Dtstamp, + params: vec![], + values: vec![ICalendarValue::PartialDateTime(Box::new(dt_stamp))], + }); + } + + pub fn add_sequence(&mut self, sequence: i64) { + self.entries.push(ICalendarEntry { + name: ICalendarProperty::Sequence, + params: vec![], + values: vec![ICalendarValue::Integer(sequence)], + }); + } + + pub fn add_uid(&mut self, uid: &str) { + self.entries.push(ICalendarEntry { + name: ICalendarProperty::Uid, + params: vec![], + values: vec![ICalendarValue::Text(uid.to_string())], + }); + } + + pub fn add_property(&mut self, name: ICalendarProperty, value: impl Into) { + self.entries.push(ICalendarEntry { + name, + params: vec![], + values: vec![value.into()], + }); + } + + pub fn add_property_with_params( + &mut self, + name: ICalendarProperty, + params: impl IntoIterator, + value: impl Into, + ) { + self.entries.push(ICalendarEntry { + name, + params: params.into_iter().collect(), + values: vec![value.into()], + }); + } +} + +impl ICalendarEntry { + pub fn new(name: ICalendarProperty) -> Self { + Self { + name, + params: vec![], + values: vec![], + } + } + + pub fn with_params(mut self, params: Vec) -> Self { + self.params = params; + self + } + + pub fn with_value(mut self, value: impl Into) -> Self { + self.values.push(value.into()); + self + } + + pub fn with_values(mut self, values: Vec) -> Self { + self.values = values; + self + } + + pub fn with_param(mut self, param: impl Into) -> Self { + self.params.push(param.into()); + self + } + + pub fn add_param(&mut self, param: impl Into) { + self.params.push(param.into()); + } + + pub fn with_param_opt(mut self, param: Option>) -> Self { + if let Some(param) = param { + self.params.push(param.into()); + } + self + } + + pub fn is_type(&self, typ: &ICalendarValueType) -> bool { + self.parameters(&ICalendarParameterName::Value) + .any(|p| matches!(p, ICalendarParameterValue::Value(v) if v == typ)) + } +} + +impl ICalendarParameter { + pub fn new(name: ICalendarParameterName, value: impl Into) -> Self { + ICalendarParameter { + name, + value: value.into(), + } + } + + pub fn altrep(value: impl Into) -> Self { + ICalendarParameter { + name: ICalendarParameterName::Altrep, + value: value.into(), + } + } + + pub fn cn(value: impl Into) -> Self { + ICalendarParameter { + name: ICalendarParameterName::Cn, + value: value.into(), + } + } + + pub fn cutype(value: impl Into) -> Self { + ICalendarParameter { + name: ICalendarParameterName::Cutype, + value: value.into(), + } + } + + pub fn delegated_from(value: impl Into) -> Self { + ICalendarParameter { + name: ICalendarParameterName::DelegatedFrom, + value: value.into(), + } + } + + pub fn delegated_to(value: impl Into) -> Self { + ICalendarParameter { + name: ICalendarParameterName::DelegatedTo, + value: value.into(), + } + } + + pub fn dir(value: impl Into) -> Self { + ICalendarParameter { + name: ICalendarParameterName::Dir, + value: value.into(), + } + } + + pub fn fmttype(value: impl Into) -> Self { + ICalendarParameter { + name: ICalendarParameterName::Fmttype, + value: value.into(), + } + } + + pub fn fbtype(value: impl Into) -> Self { + ICalendarParameter { + name: ICalendarParameterName::Fbtype, + value: value.into(), + } + } + + pub fn language(value: impl Into) -> Self { + ICalendarParameter { + name: ICalendarParameterName::Language, + value: value.into(), + } + } + + pub fn member(value: impl Into) -> Self { + ICalendarParameter { + name: ICalendarParameterName::Member, + value: value.into(), + } + } + + pub fn partstat(value: impl Into) -> Self { + ICalendarParameter { + name: ICalendarParameterName::Partstat, + value: value.into(), + } + } + + pub fn range(value: impl Into) -> Self { + ICalendarParameter { + name: ICalendarParameterName::Range, + value: value.into(), + } + } + + pub fn related(value: impl Into) -> Self { + ICalendarParameter { + name: ICalendarParameterName::Related, + value: value.into(), + } + } + + pub fn reltype(value: impl Into) -> Self { + ICalendarParameter { + name: ICalendarParameterName::Reltype, + value: value.into(), + } + } + + pub fn role(value: impl Into) -> Self { + ICalendarParameter { + name: ICalendarParameterName::Role, + value: value.into(), + } + } + + pub fn rsvp(value: impl Into) -> Self { + ICalendarParameter { + name: ICalendarParameterName::Rsvp, + value: value.into(), + } + } + + pub fn schedule_agent(value: impl Into) -> Self { + ICalendarParameter { + name: ICalendarParameterName::ScheduleAgent, + value: value.into(), + } + } + + pub fn schedule_force_send(value: impl Into) -> Self { + ICalendarParameter { + name: ICalendarParameterName::ScheduleForceSend, + value: value.into(), + } + } + + pub fn schedule_status(value: impl Into) -> Self { + ICalendarParameter { + name: ICalendarParameterName::ScheduleStatus, + value: value.into(), + } + } + + pub fn sent_by(value: impl Into) -> Self { + ICalendarParameter { + name: ICalendarParameterName::SentBy, + value: value.into(), + } + } + + pub fn tzid(value: impl Into) -> Self { + ICalendarParameter { + name: ICalendarParameterName::Tzid, + value: value.into(), + } + } + + pub fn value(value: impl Into) -> Self { + ICalendarParameter { + name: ICalendarParameterName::Value, + value: value.into(), + } + } + + pub fn display(value: impl Into) -> Self { + ICalendarParameter { + name: ICalendarParameterName::Display, + value: value.into(), + } + } + + pub fn email(value: impl Into) -> Self { + ICalendarParameter { + name: ICalendarParameterName::Email, + value: value.into(), + } + } + + pub fn feature(value: impl Into) -> Self { + ICalendarParameter { + name: ICalendarParameterName::Feature, + value: value.into(), + } + } + + pub fn label(value: impl Into) -> Self { + ICalendarParameter { + name: ICalendarParameterName::Label, + value: value.into(), + } + } + + pub fn size(value: impl Into) -> Self { + ICalendarParameter { + name: ICalendarParameterName::Size, + value: value.into(), + } + } + + pub fn filename(value: impl Into) -> Self { + ICalendarParameter { + name: ICalendarParameterName::Filename, + value: value.into(), + } + } + + pub fn managed_id(value: impl Into) -> Self { + ICalendarParameter { + name: ICalendarParameterName::ManagedId, + value: value.into(), + } + } + + pub fn order(value: impl Into) -> Self { + ICalendarParameter { + name: ICalendarParameterName::Order, + value: value.into(), + } + } + + pub fn schema(value: impl Into) -> Self { + ICalendarParameter { + name: ICalendarParameterName::Schema, + value: value.into(), + } + } + + pub fn derived(value: impl Into) -> Self { + ICalendarParameter { + name: ICalendarParameterName::Derived, + value: value.into(), + } + } + + pub fn gap(value: impl Into) -> Self { + ICalendarParameter { + name: ICalendarParameterName::Gap, + value: value.into(), + } + } + + pub fn linkrel(value: impl Into) -> Self { + ICalendarParameter { + name: ICalendarParameterName::Linkrel, + value: value.into(), + } + } + + pub fn jsptr(value: impl Into) -> Self { + ICalendarParameter { + name: ICalendarParameterName::Jsptr, + value: value.into(), + } + } + + pub fn jsid(value: impl Into) -> Self { + ICalendarParameter { + name: ICalendarParameterName::Jsid, + value: value.into(), + } + } +} + +impl From for ICalendarParameterValue { + fn from(text: String) -> Self { + ICalendarParameterValue::Text(text) + } +} + +impl From for ICalendarParameterValue { + fn from(integer: u64) -> Self { + ICalendarParameterValue::Integer(integer) + } +} + +impl From for ICalendarParameterValue { + fn from(integer: Integer) -> Self { + ICalendarParameterValue::Integer(integer.0.unsigned_abs()) + } +} + +impl From for ICalendarParameterValue { + fn from(boolean: Boolean) -> Self { + ICalendarParameterValue::Bool(boolean.0) + } +} + +impl From for ICalendarParameterValue { + fn from(boolean: bool) -> Self { + ICalendarParameterValue::Bool(boolean) + } +} + +impl From for ICalendarParameterValue { + fn from(uri: Uri) -> Self { + ICalendarParameterValue::Uri(uri) + } +} + +impl From for ICalendarParameterValue { + fn from(cutype: ICalendarUserTypes) -> Self { + ICalendarParameterValue::Cutype(cutype) + } +} + +impl From for ICalendarParameterValue { + fn from(fbtype: ICalendarFreeBusyType) -> Self { + ICalendarParameterValue::Fbtype(fbtype) + } +} + +impl From for ICalendarParameterValue { + fn from(partstat: ICalendarParticipationStatus) -> Self { + ICalendarParameterValue::Partstat(partstat) + } +} + +impl From for ICalendarParameterValue { + fn from(related: ICalendarRelated) -> Self { + ICalendarParameterValue::Related(related) + } +} + +impl From for ICalendarParameterValue { + fn from(reltype: ICalendarRelationshipType) -> Self { + ICalendarParameterValue::Reltype(reltype) + } +} + +impl From for ICalendarParameterValue { + fn from(role: ICalendarParticipationRole) -> Self { + ICalendarParameterValue::Role(role) + } +} + +impl From for ICalendarParameterValue { + fn from(schedule_agent: ICalendarScheduleAgentValue) -> Self { + ICalendarParameterValue::ScheduleAgent(schedule_agent) + } +} + +impl From for ICalendarParameterValue { + fn from(schedule_force_send: ICalendarScheduleForceSendValue) -> Self { + ICalendarParameterValue::ScheduleForceSend(schedule_force_send) + } +} + +impl From for ICalendarParameterValue { + fn from(value: ICalendarValueType) -> Self { + ICalendarParameterValue::Value(value) + } +} + +impl From for ICalendarParameterValue { + fn from(display: ICalendarDisplayType) -> Self { + ICalendarParameterValue::Display(display) + } +} + +impl From for ICalendarParameterValue { + fn from(feature: ICalendarFeatureType) -> Self { + ICalendarParameterValue::Feature(feature) + } +} + +impl From for ICalendarParameterValue { + fn from(duration: ICalendarDuration) -> Self { + ICalendarParameterValue::Duration(duration) + } +} + +impl From for ICalendarParameterValue { + fn from(linkrel: LinkRelation) -> Self { + ICalendarParameterValue::Linkrel(linkrel) + } +} + +impl> From> for ICalendarParameterValue { + fn from(value: IanaType) -> Self { + match value { + IanaType::Iana(v) => v.into(), + IanaType::Other(s) => s.into(), + } + } +} + +impl From> for ICalendarValue { + fn from(value: Vec) -> Self { + ICalendarValue::Binary(value) + } +} + +impl From for ICalendarValue { + fn from(value: bool) -> Self { + ICalendarValue::Boolean(value) + } +} + +impl From for ICalendarValue { + fn from(value: Uri) -> Self { + ICalendarValue::Uri(value) + } +} + +impl From for ICalendarValue { + fn from(value: PartialDateTime) -> Self { + ICalendarValue::PartialDateTime(Box::new(value)) + } +} + +impl From> for ICalendarValue { + fn from(value: Box) -> Self { + ICalendarValue::PartialDateTime(value) + } +} + +impl From for ICalendarValue { + fn from(value: ICalendarDuration) -> Self { + ICalendarValue::Duration(value) + } +} + +impl From for ICalendarValue { + fn from(value: ICalendarRecurrenceRule) -> Self { + ICalendarValue::RecurrenceRule(Box::new(value)) + } +} + +impl From> for ICalendarValue { + fn from(value: Box) -> Self { + ICalendarValue::RecurrenceRule(value) + } +} + +impl From for ICalendarValue { + fn from(value: ICalendarPeriod) -> Self { + ICalendarValue::Period(value) + } +} + +impl From for ICalendarValue { + fn from(value: f64) -> Self { + ICalendarValue::Float(value) + } +} + +impl From for ICalendarValue { + fn from(value: i64) -> Self { + ICalendarValue::Integer(value) + } +} + +impl From for ICalendarValue { + fn from(value: String) -> Self { + ICalendarValue::Text(value) + } +} + +impl From<&str> for ICalendarValue { + fn from(value: &str) -> Self { + ICalendarValue::Text(value.to_string()) + } +} + +impl From for ICalendarValue { + fn from(value: CalendarScale) -> Self { + ICalendarValue::CalendarScale(value) + } +} + +impl From for ICalendarValue { + fn from(value: ICalendarMethod) -> Self { + ICalendarValue::Method(value) + } +} + +impl From for ICalendarValue { + fn from(value: ICalendarClassification) -> Self { + ICalendarValue::Classification(value) + } +} + +impl From for ICalendarValue { + fn from(value: ICalendarStatus) -> Self { + ICalendarValue::Status(value) + } +} + +impl From for ICalendarValue { + fn from(value: ICalendarTransparency) -> Self { + ICalendarValue::Transparency(value) + } +} + +impl From for ICalendarValue { + fn from(value: ICalendarAction) -> Self { + ICalendarValue::Action(value) + } +} + +impl From for ICalendarValue { + fn from(value: ICalendarFreeBusyType) -> Self { + ICalendarValue::BusyType(value) + } +} + +impl From for ICalendarValue { + fn from(value: ICalendarParticipantType) -> Self { + ICalendarValue::ParticipantType(value) + } +} + +impl From for ICalendarValue { + fn from(value: ICalendarResourceType) -> Self { + ICalendarValue::ResourceType(value) + } +} + +impl From for ICalendarValue { + fn from(value: ICalendarProximityValue) -> Self { + ICalendarValue::Proximity(value) + } +} + +#[cfg(test)] +mod tests { + use crate::icalendar::ICalendar; + + #[test] + fn remove_component_ids() { + let mut ical = + ICalendar::parse(std::fs::read_to_string("resources/ical/007.ics").unwrap()).unwrap(); + ical.remove_component_ids(&[2, 5, 7]); + + let max_component_id = ical.components.len() as u32; + for component in &ical.components { + for id in &component.component_ids { + assert!(*id < max_component_id); + } + } + + assert_eq!( + ical.to_string().replace("\r\n", "\n"), + r#"BEGIN:VCALENDAR +PRODID:-//Google Inc//Google Calendar 70.9054//EN +VERSION:2.0 +CALSCALE:GREGORIAN +METHOD:PUBLISH +X-WR-CALNAME:ANONYMOUS +X-WR-TIMEZONE:America/Denver +BEGIN:VTIMEZONE +TZID:America/Denver +X-LIC-LOCATION:America/Denver +BEGIN:STANDARD +TZOFFSETFROM:-0600 +TZOFFSETTO:-0700 +TZNAME:MST +DTSTART:19701101T020000 +RRULE:FREQ=YEARLY;BYDAY=1SU;BYMONTH=11 +END:STANDARD +END:VTIMEZONE +BEGIN:VEVENT +DTSTART;TZID=America/Denver:20200903T094000 +DTEND;TZID=America/Denver:20200903T095000 +RRULE:FREQ=WEEKLY;BYDAY=FR,MO,TH,TU,WE +EXDATE;TZID=America/Denver:20201015T094000 +DTSTAMP:20201124T214551Z +UID:078tk5i2t4all3kk0jcoi3mdmd@google.com +CREATED:20200903T032927Z +DESCRIPTION: +LAST-MODIFIED:20200903T032927Z +LOCATION: +SEQUENCE:0 +STATUS:CONFIRMED +SUMMARY:Brain Break +TRANSP:OPAQUE +END:VEVENT +BEGIN:VEVENT +DTSTART;TZID=America/Denver:20200903T095000 +DTEND;TZID=America/Denver:20200903T104000 +RRULE:FREQ=WEEKLY;BYDAY=FR,MO,TH,TU,WE +EXDATE;TZID=America/Denver:20201015T095000 +DTSTAMP:20201124T214551Z +UID:11le1ep09hvog7dbotn6foj38e@google.com +CREATED:20200903T032956Z +DESCRIPTION: +LAST-MODIFIED:20200903T032957Z +LOCATION: +SEQUENCE:0 +STATUS:CONFIRMED +SUMMARY:Academic Time +TRANSP:OPAQUE +END:VEVENT +BEGIN:VEVENT +DTSTART;TZID=Europe/Budapest:20201102T072000 +DTEND;TZID=Europe/Budapest:20201102T072000 +EXDATE;TZID=Europe/Budapest:20201221T072000 +EXDATE;TZID=Europe/Budapest:20201222T072000 +EXDATE;TZID=Europe/Budapest:20201223T072000 +EXDATE;TZID=Europe/Budapest:20201224T072000 +EXDATE;TZID=Europe/Budapest:20201225T072000 +EXDATE;TZID=Europe/Budapest:20201228T072000 +EXDATE;TZID=Europe/Budapest:20201229T072000 +EXDATE;TZID=Europe/Budapest:20201230T072000 +EXDATE;TZID=Europe/Budapest:20201231T072000 +EXDATE;TZID=Europe/Budapest:20210101T072000 +RRULE;X-BUSYMAC-REGENERATE=TRASH:FREQ=WEEKLY;BYDAY=MO,TU,WE,TH,FR +DTSTAMP:20201230T095550Z +UID:*Masked away* +CREATED:20201102T054749Z +DESCRIPTION: +LAST-MODIFIED:20201221T100211Z +LOCATION: +SEQUENCE:1 +STATUS:CONFIRMED +SUMMARY:Invalid RRULE property +TRANSP:OPAQUE +X-BUSYMAC-LASTMODBY:*Masked away* +END:VEVENT +END:VCALENDAR +"# + ) + } +} diff --git a/vendor/calcard/src/icalendar/dates.rs b/vendor/calcard/src/icalendar/dates.rs new file mode 100644 index 00000000..8ea8fd4b --- /dev/null +++ b/vendor/calcard/src/icalendar/dates.rs @@ -0,0 +1,644 @@ +/* + * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC + * + * SPDX-License-Identifier: Apache-2.0 OR MIT + */ + +use super::{ + ICalendar, ICalendarComponent, ICalendarComponentType, ICalendarPeriod, ICalendarProperty, + ICalendarValue, timezone::TzResolver, +}; +use crate::{ + common::{DateTimeResult, timezone::Tz}, + datecalc::{RRuleIter, error::RRuleError, rrule::RRule}, + icalendar::ICalendarParameterName, +}; +use ahash::{AHashMap, AHashSet}; +use chrono::{DateTime, TimeDelta, TimeZone, Timelike}; +use std::fmt::{Display, Formatter}; + +#[allow(clippy::type_complexity)] +#[derive(Debug, Default, Clone, PartialEq, Eq)] +#[cfg_attr(any(test, feature = "serde"), derive(serde::Serialize))] +pub struct CalendarExpand { + pub events: Vec, TimeOrDelta, TimeDelta>>>, + pub errors: Vec, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +#[cfg_attr(any(test, feature = "serde"), derive(serde::Serialize))] +pub struct CalendarEvent { + pub comp_id: u32, + pub start: S, + pub end: E, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +#[cfg_attr( + any(test, feature = "serde"), + derive(serde::Serialize, serde::Deserialize) +)] +#[cfg_attr(any(test, feature = "serde"), serde(tag = "type", content = "data"))] +pub enum TimeOrDelta { + Time(T), + Delta(D), +} + +#[derive(Debug, Clone, PartialEq, Eq)] +#[cfg_attr(any(test, feature = "serde"), derive(serde::Serialize))] +pub struct CalendarError { + pub comp_id: u32, + pub error: CalendarErrorType, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +#[cfg_attr(any(test, feature = "serde"), derive(serde::Serialize))] +pub enum CalendarErrorType { + MissingDtStart, + InvalidDtStart, + InvalidDtEnd, + InvalidDuration, + RRule(RRuleError), +} + +impl ICalendar { + pub fn expand_dates(&self, default_tz: impl Into, mut limit: usize) -> CalendarExpand { + let tz_resolver = self.build_tz_resolver().with_default(default_tz); + let mut expand = CalendarExpand::default(); + let mut rrules = Vec::new(); + let mut overridden = AHashMap::new(); + + for (comp_id, comp) in self.components.iter().enumerate() { + if comp.component_type.has_time_ranges() { + match comp.build_calendar_date(comp_id as u32, &tz_resolver, &mut expand.events) { + Ok(Some(event)) => { + if event.rrule.is_some() { + rrules.push((comp_id as u32, event)); + } else if let Some(rid) = event.rid { + overridden.insert((event.rrule_seq, rid), (comp_id as u32, event)); + } else if let Some(cal_event) = event.event { + expand.events.push(cal_event); + } + } + Ok(None) => {} + Err(error) => { + expand.errors.push(CalendarError { + comp_id: comp_id as u32, + error, + }); + } + } + } + } + + // Expand recurrences + for (mut comp_id, event) in rrules { + let rrule = event.rrule.unwrap(); + let floating_start = if let Some(floating_start) = Tz::Floating + .from_local_datetime(&event.dt_start.date_time) + .single() + { + floating_start + } else { + expand.errors.push(CalendarError { + comp_id, + error: CalendarErrorType::InvalidDtStart, + }); + continue; + }; + let rrule = match rrule.validate(floating_start) { + Ok(rrule) => rrule, + Err(err) => { + expand.errors.push(CalendarError { + comp_id, + error: CalendarErrorType::RRule(err), + }); + continue; + } + }; + let exdates = event + .exdates + .into_iter() + .filter_map(|(tz_id, dt)| { + dt.to_date_time_with_tz( + tz_resolver.resolve_or_default(tz_id.or(event.dt_start_tzid)), + ) + }) + .collect::>(); + let mut override_offset = None; + + for date in RRuleIter::new(&rrule, &floating_start, true) { + if limit != 0 { + limit -= 1; + } else { + break; + } + let mut date = if date.timezone().is_floating() { + event + .start_tz + .from_local_datetime(&date.naive_local()) + .single() + .unwrap_or(date) + } else { + date + }; + if let Some(override_offset) = override_offset { + date += override_offset; + } + match overridden.remove(&(event.rrule_seq, date)) { + Some((new_comp_id, overridden_event)) => { + if let Some(new_event) = overridden_event.event { + if overridden_event.rid_this_and_future { + comp_id = new_comp_id; + override_offset = Some(new_event.start - date); + } + expand.events.push(new_event); + } + } + None if !exdates.contains(&date) => { + expand.events.push(CalendarEvent { + start: date, + end: TimeOrDelta::Delta(event.default_duration), + comp_id, + }); + } + _ => {} + } + } + } + + // Add missing overridden events (this should not occur unless the iCalendar is malformed) + for (_, event) in overridden.into_values() { + if let Some(cal_event) = event.event { + expand.events.push(cal_event); + } + } + + expand + } +} + +#[allow(clippy::type_complexity)] +struct CalendarEventBuilder<'x> { + event: Option, TimeOrDelta, TimeDelta>>>, + dt_start: DateTimeResult, + dt_start_tzid: Option<&'x str>, + start_tz: Tz, + default_duration: TimeDelta, + rrule: Option, + rrule_seq: i64, + exdates: Vec<(Option<&'x str>, DateTimeResult)>, + rid: Option>, + rid_this_and_future: bool, +} + +impl ICalendarComponent { + #[allow(clippy::type_complexity)] + fn build_calendar_date( + &self, + comp_id: u32, + tz_resolver: &TzResolver<&'_ str>, + events: &mut Vec, TimeOrDelta, TimeDelta>>>, + ) -> Result>, CalendarErrorType> { + let mut dt_start = None; + let mut dt_start_tzid = None; + let mut dt_start_has_time = false; + let mut dt_end: Option = None; + let mut dt_end_tzid = None; + let mut todo_dates = vec![]; + let mut rid: Option = None; + let mut rid_tzid = None; + let mut rid_this_and_future = false; + let mut duration = None; + let mut rrule = None; + let mut rrule_seq = i64::MAX; + let mut rdates = vec![]; + let mut rdates_periods = vec![]; + let mut exdates = vec![]; + + for entry in &self.entries { + match (&entry.name, entry.values.first()) { + (ICalendarProperty::Dtstart, Some(ICalendarValue::PartialDateTime(dt))) => { + dt_start = dt.to_date_time(); + dt_start_tzid = entry.tz_id(); + dt_start_has_time = dt.has_time(); + } + (ICalendarProperty::Dtend, Some(ICalendarValue::PartialDateTime(dt))) => { + if let Some(dt) = dt.to_date_time() { + dt_end = Some(dt); + dt_end_tzid = entry.tz_id(); + } + } + ( + ICalendarProperty::Due + | ICalendarProperty::Completed + | ICalendarProperty::Created, + Some(ICalendarValue::PartialDateTime(dt)), + ) if self.component_type == ICalendarComponentType::VTodo => { + todo_dates.push((&entry.name, dt.to_date_time(), entry.tz_id())); + } + (ICalendarProperty::RecurrenceId, Some(ICalendarValue::PartialDateTime(dt))) => { + if let Some(dt) = dt.to_date_time() { + for param in &entry.params { + match ¶m.name { + ICalendarParameterName::Tzid => { + rid_tzid = param.value.as_text(); + } + ICalendarParameterName::Range => { + rid_this_and_future = true; + } + _ => (), + } + } + + rid = Some(dt); + } + } + (ICalendarProperty::Duration, Some(ICalendarValue::Duration(dur))) => { + duration = Some(dur); + } + (ICalendarProperty::Rrule, Some(ICalendarValue::RecurrenceRule(rule))) => { + rrule = RRule::from_floating_ical(rule); + } + (ICalendarProperty::Sequence, Some(ICalendarValue::Integer(seq))) => { + rrule_seq = *seq; + } + (ICalendarProperty::Rdate, _) => { + let tz_id = entry.tz_id(); + for value in &entry.values { + match value { + ICalendarValue::PartialDateTime(dt) => { + if let Some(dt) = dt.to_date_time() { + rdates.push((tz_id, dt)); + } + } + ICalendarValue::Period(period) => match period { + ICalendarPeriod::Range { start, end } => { + if let (Some(start), Some(end)) = + (start.to_date_time(), end.to_date_time()) + { + rdates_periods.push((tz_id, start, TimeOrDelta::Time(end))); + } + } + ICalendarPeriod::Duration { start, duration } => { + if let (Some(start), Some(duration)) = + (start.to_date_time(), duration.to_time_delta()) + { + rdates_periods.push(( + tz_id, + start, + TimeOrDelta::Delta(duration), + )); + } + } + }, + _ => (), + } + } + } + (ICalendarProperty::Exdate, _) => { + let tz_id = entry.tz_id(); + for value in &entry.values { + if let ICalendarValue::PartialDateTime(dt) = value + && let Some(dt) = dt.to_date_time() + { + exdates.push((tz_id, dt)); + } + } + } + _ => (), + } + } + + let dt_start = match dt_start { + Some(dt_start) => dt_start, + None => match &rid { + Some(rid) => { + dt_start_tzid = rid_tzid; + rid.clone() + } + None => match self.component_type { + ICalendarComponentType::VEvent => { + return Err(CalendarErrorType::MissingDtStart); + } + ICalendarComponentType::VTodo => { + let mut due_idx = None; + let mut completed_idx = None; + let mut created_idx = None; + + for (idx, (prop, dt, _)) in todo_dates.iter().enumerate() { + if dt.is_some() { + match prop { + ICalendarProperty::Due => due_idx = Some(idx), + ICalendarProperty::Completed => completed_idx = Some(idx), + ICalendarProperty::Created => created_idx = Some(idx), + _ => (), + } + } + } + + match (due_idx, completed_idx, created_idx) { + (Some(due_idx), _, _) => { + let due = &mut todo_dates[due_idx]; + dt_start_tzid = due.2; + dt_start_has_time = true; + due.1.take().unwrap() + } + (_, Some(completed_idx), Some(created_idx)) => { + let completed = &mut todo_dates[completed_idx]; + dt_end = completed.1.take(); + dt_end_tzid = completed.2; + + let created = &mut todo_dates[created_idx]; + dt_start_tzid = created.2; + created.1.take().unwrap() + } + (_, Some(date_idx), _) | (_, _, Some(date_idx)) => { + let date = &mut todo_dates[date_idx]; + dt_start_tzid = date.2; + dt_start_has_time = true; + date.1.take().unwrap() + } + _ => { + return Ok(None); + } + } + } + _ => { + return Ok(None); + } + }, + }, + }; + let mut event = None; + let start_tz = tz_resolver.resolve_or_default(dt_start_tzid); + let dt_start_tz = dt_start + .to_date_time_with_tz(start_tz) + .ok_or(CalendarErrorType::InvalidDtStart)?; + let default_duration = if let Some(dt_end) = dt_end { + let end = dt_end + .to_date_time_with_tz(tz_resolver.resolve_or_default(dt_end_tzid.or(dt_start_tzid))) + .ok_or(CalendarErrorType::InvalidDtEnd)?; + + if rrule.is_none() { + event = Some(CalendarEvent { + start: dt_start_tz, + end: TimeOrDelta::Time(end), + comp_id, + }); + } + dt_end.date_time - dt_start.date_time + } else if let Some(duration) = duration { + let duration = duration + .to_time_delta() + .ok_or(CalendarErrorType::InvalidDuration)?; + if rrule.is_none() { + event = Some(CalendarEvent { + start: dt_start_tz, + end: TimeOrDelta::Delta(duration), + comp_id, + }); + } + duration + } else if let Some((due, due_tzid)) = todo_dates + .into_iter() + .filter_map(|(prop, dt, tz_id)| { + if prop == &ICalendarProperty::Due { + dt.map(|dt| (dt, tz_id)) + } else { + None + } + }) + .next() + { + let end = due + .to_date_time_with_tz(tz_resolver.resolve_or_default(due_tzid.or(dt_start_tzid))) + .ok_or(CalendarErrorType::InvalidDtEnd)?; + + if rrule.is_none() { + event = Some(CalendarEvent { + start: dt_start_tz, + end: TimeOrDelta::Time(end), + comp_id, + }); + } + due.date_time - dt_start.date_time + } else { + /* + For cases where a "VEVENT" calendar component + specifies a "DTSTART" property with a DATE value type but no + "DTEND" nor "DURATION" property, the event's duration is taken to + be one day. For cases where a "VEVENT" calendar component + specifies a "DTSTART" property with a DATE-TIME value type but no + "DTEND" property, the event ends on the same calendar date and + time of day specified by the "DTSTART" property. + */ + + let duration = if dt_start_has_time { + // If the start has time, we use the same time for the end + dt_start + .date_time + .with_hour(23) + .and_then(|dt| dt.with_minute(59)) + .and_then(|dt| dt.with_second(59)) + .map(|dt| dt - dt_start.date_time) + .unwrap_or_else(|| TimeDelta::days(1)) + } else { + TimeDelta::days(1) + }; + if rrule.is_none() { + event = Some(CalendarEvent { + start: dt_start_tz, + end: TimeOrDelta::Delta(duration), + comp_id, + }); + } + duration + }; + let rid = if let Some(rid) = rid { + rid.to_date_time_with_tz(tz_resolver.resolve_or_default(rid_tzid.or(dt_start_tzid))) + } else { + None + }; + + // Add rdates + for (tz_id, rdate) in rdates { + if let Some(date_start) = + rdate.to_date_time_with_tz(tz_resolver.resolve_or_default(tz_id.or(dt_start_tzid))) + { + events.push(CalendarEvent { + start: date_start, + end: TimeOrDelta::Delta(default_duration), + comp_id, + }); + } + } + for (tz_id, start, end) in rdates_periods { + let tz = tz_resolver.resolve_or_default(tz_id.or(dt_start_tzid)); + if let (Some(date_start), Some(date_end)) = ( + start.to_date_time_with_tz(tz), + end.into_date_time_with_tz(tz), + ) { + events.push(CalendarEvent { + start: date_start, + end: date_end, + comp_id, + }); + } + } + + Ok(Some(CalendarEventBuilder { + event, + dt_start_tzid, + default_duration, + rrule_seq, + rrule, + exdates, + start_tz, + dt_start, + rid, + rid_this_and_future, + })) + } +} + +impl TimeOrDelta { + pub fn into_date_time_with_tz(self, tz: Tz) -> Option, TimeDelta>> { + match self { + TimeOrDelta::Time(time) => time.to_date_time_with_tz(tz).map(TimeOrDelta::Time), + TimeOrDelta::Delta(delta) => Some(TimeOrDelta::Delta(delta)), + } + } +} + +impl CalendarEvent, TimeOrDelta, TimeDelta>> { + pub fn timestamps(&self) -> (i64, i64) { + let timestamp = self.start.timestamp(); + let end_timestamp = match self.end { + TimeOrDelta::Time(time) => time.timestamp(), + TimeOrDelta::Delta(delta) => timestamp + delta.num_seconds(), + }; + + (timestamp, end_timestamp) + } + + pub fn try_into_date_time(self) -> Option, DateTime>> { + match self.end { + TimeOrDelta::Time(time) => Some(time), + TimeOrDelta::Delta(delta) => self + .start + .naive_local() + .checked_add_signed(delta) + .and_then(|end| end.and_local_timezone(self.start.timezone()).single()), + } + .map(|end| CalendarEvent { + start: self.start, + end, + comp_id: self.comp_id, + }) + } +} + +impl Display for CalendarErrorType { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + CalendarErrorType::MissingDtStart => write!(f, "Missing DTSTART property"), + CalendarErrorType::InvalidDtStart => write!(f, "Invalid DTSTART property"), + CalendarErrorType::InvalidDtEnd => write!(f, "Invalid DTEND property"), + CalendarErrorType::InvalidDuration => write!(f, "Invalid DURATION property"), + CalendarErrorType::RRule(err) => write!(f, "RRule error: {err}"), + } + } +} + +#[cfg(test)] +mod tests { + use crate::{ + Entry, Parser, + common::timezone::Tz, + icalendar::dates::{CalendarError, CalendarEvent}, + }; + use chrono::DateTime; + use serde::Serialize; + use std::{io::Write, time::Instant}; + + #[test] + fn expand_rrule() { + // Read all .ics files in the test directory + for entry in std::fs::read_dir("resources/ical").unwrap() { + let entry = entry.unwrap(); + let path = entry.path(); + if path.extension().is_some_and(|ext| ext == "ics") { + let input = match String::from_utf8(std::fs::read(&path).unwrap()) { + Ok(input) => input, + Err(err) => { + // ISO-8859-1 + err.as_bytes() + .iter() + .map(|&b| b as char) + .collect::() + } + }; + let mut parser = Parser::new(&input); + let mut output = None; + //let mut output_debug = + // std::fs::File::create(path.with_extension("ics.debug")).unwrap(); + let file_name = path.as_path().to_str().unwrap(); + + /*if file_name != "resources/ical/246.ics" { + continue; + }*/ + + #[derive(Serialize)] + struct TestResult { + errors: Vec, + events: Vec, DateTime>>, + } + + print!("Expanding recurrences for {file_name}... "); + let now = Instant::now(); + loop { + match parser.entry() { + Entry::ICalendar(ical) => { + let expanded = ical.expand_dates(chrono_tz::Tz::Pacific__Auckland, 100); + let mut events = expanded + .events + .into_iter() + .filter_map(|event| event.try_into_date_time()) + .collect::>(); + events.sort_by_key(|a| (a.start, a.comp_id)); + + for err in &expanded.errors { + print!("[{}: {:?}] ", err.comp_id, err.error); + } + + if !events.is_empty() || !expanded.errors.is_empty() { + writeln!( + output.get_or_insert_with(|| std::fs::File::create( + path.with_extension("json") + ) + .unwrap()), + "{}", + serde_json::to_string_pretty(&TestResult { + errors: expanded.errors, + events, + }) + .unwrap() + ) + .unwrap(); + } + } + Entry::InvalidLine(_) => {} + Entry::Eof => { + println!(" (done in {:?})", now.elapsed()); + break; + } + other => { + panic!("Expected iCal, got {other:?} for {file_name}"); + } + } + } + } + } + } +} diff --git a/vendor/calcard/src/icalendar/mod.rs b/vendor/calcard/src/icalendar/mod.rs new file mode 100644 index 00000000..9f68dd72 --- /dev/null +++ b/vendor/calcard/src/icalendar/mod.rs @@ -0,0 +1,902 @@ +/* + * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC + * + * SPDX-License-Identifier: Apache-2.0 OR MIT + */ + +use crate::common::{ + CalendarScale, Data, IanaParse, IanaString, IanaType, LinkRelation, PartialDateTime, +}; +use std::hash::{Hash, Hasher}; + +pub mod builder; +pub mod dates; +pub mod parser; +pub mod timezone; +pub mod types; +pub mod utils; +pub mod writer; + +#[cfg(feature = "rkyv")] +pub mod rkyv_timezone; +#[cfg(feature = "rkyv")] +pub mod rkyv_types; +#[cfg(feature = "rkyv")] +pub mod rkyv_utils; +#[cfg(feature = "rkyv")] +pub mod rkyv_writer; + +#[derive(Debug, Default, Clone, PartialEq, Eq)] +#[cfg_attr( + any(test, feature = "serde"), + derive(serde::Serialize, serde::Deserialize) +)] +#[cfg_attr( + feature = "rkyv", + derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive) +)] +#[cfg_attr(feature = "rkyv", rkyv(compare(PartialEq)))] +pub struct ICalendar { + pub components: Vec, +} + +#[derive(Debug, Default, Clone, PartialEq, Eq, PartialOrd, Ord)] +#[cfg_attr( + any(test, feature = "serde"), + derive(serde::Serialize, serde::Deserialize) +)] +#[cfg_attr( + feature = "rkyv", + derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive) +)] +#[cfg_attr(feature = "rkyv", rkyv(compare(PartialEq)))] +pub struct ICalendarComponent { + pub component_type: ICalendarComponentType, + pub entries: Vec, + pub component_ids: Vec, +} + +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] +#[cfg_attr( + any(test, feature = "serde"), + derive(serde::Serialize, serde::Deserialize) +)] +#[cfg_attr( + feature = "rkyv", + derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive) +)] +#[cfg_attr(feature = "rkyv", rkyv(compare(PartialEq)))] +pub struct ICalendarEntry { + pub name: ICalendarProperty, + pub params: Vec, + pub values: Vec, +} + +#[derive(Debug, Clone, PartialEq, PartialOrd)] +#[cfg_attr( + any(test, feature = "serde"), + derive(serde::Serialize, serde::Deserialize) +)] +#[cfg_attr(any(test, feature = "serde"), serde(tag = "type", content = "data"))] +#[cfg_attr( + feature = "rkyv", + derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive) +)] +#[cfg_attr(feature = "rkyv", rkyv(compare(PartialEq)))] +pub enum ICalendarValue { + Binary(Vec), + Boolean(bool), + Uri(Uri), + PartialDateTime(Box), + Duration(ICalendarDuration), + RecurrenceRule(Box), + Period(ICalendarPeriod), + Float(f64), + Integer(i64), + Text(String), + CalendarScale(CalendarScale), + Method(ICalendarMethod), + Classification(ICalendarClassification), + Status(ICalendarStatus), + Transparency(ICalendarTransparency), + Action(ICalendarAction), + BusyType(ICalendarFreeBusyType), + ParticipantType(ICalendarParticipantType), + ResourceType(ICalendarResourceType), + Proximity(ICalendarProximityValue), +} + +#[derive(Debug, Default, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[cfg_attr( + any(test, feature = "serde"), + derive(serde::Serialize, serde::Deserialize) +)] +#[cfg_attr( + feature = "rkyv", + derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive) +)] +#[cfg_attr(feature = "rkyv", rkyv(compare(PartialEq), derive(Debug)))] +pub struct ICalendarRecurrenceRule { + pub freq: ICalendarFrequency, + pub until: Option, + pub count: Option, + pub interval: Option, + pub bysecond: Vec, + pub byminute: Vec, + pub byhour: Vec, + pub byday: Vec, + pub bymonthday: Vec, + pub byyearday: Vec, + pub byweekno: Vec, + pub bymonth: Vec, + pub bysetpos: Vec, + pub wkst: Option, + pub rscale: Option, + pub skip: Option, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[cfg_attr( + any(test, feature = "serde"), + derive(serde::Serialize, serde::Deserialize) +)] +#[cfg_attr( + feature = "rkyv", + derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive) +)] +#[cfg_attr(feature = "rkyv", rkyv(compare(PartialEq), derive(Debug)))] +#[repr(u8)] +pub enum ICalendarSkip { + Omit, + Backward, + Forward, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[cfg_attr( + any(test, feature = "serde"), + derive(serde::Serialize, serde::Deserialize) +)] +#[cfg_attr( + feature = "rkyv", + derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive) +)] +#[cfg_attr(feature = "rkyv", rkyv(compare(PartialEq), derive(Debug)))] +pub struct ICalendarDay { + pub ordwk: Option, + pub weekday: ICalendarWeekday, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[cfg_attr( + any(test, feature = "serde"), + derive(serde::Serialize, serde::Deserialize) +)] +#[cfg_attr( + feature = "rkyv", + derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive) +)] +#[cfg_attr(feature = "rkyv", rkyv(compare(PartialEq), derive(Debug)))] +#[repr(transparent)] +pub struct ICalendarMonth(i8); + +#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[cfg_attr( + any(test, feature = "serde"), + derive(serde::Serialize, serde::Deserialize) +)] +#[cfg_attr( + feature = "rkyv", + derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive) +)] +#[cfg_attr(feature = "rkyv", rkyv(compare(PartialEq), derive(Debug)))] +#[repr(u8)] +pub enum ICalendarFrequency { + Yearly = 0, + Monthly = 1, + Weekly = 2, + #[default] + Daily = 3, + Hourly = 4, + Minutely = 5, + Secondly = 6, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[cfg_attr( + any(test, feature = "serde"), + derive(serde::Serialize, serde::Deserialize) +)] +#[cfg_attr( + feature = "rkyv", + derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive) +)] +#[cfg_attr(feature = "rkyv", rkyv(compare(PartialEq), derive(Debug)))] +pub enum ICalendarWeekday { + Sunday, + Monday, + Tuesday, + Wednesday, + Thursday, + Friday, + Saturday, +} + +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[cfg_attr( + any(test, feature = "serde"), + derive(serde::Serialize, serde::Deserialize) +)] +#[cfg_attr(any(test, feature = "serde"), serde(tag = "type", content = "data"))] +#[cfg_attr( + feature = "rkyv", + derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive) +)] +#[cfg_attr(feature = "rkyv", rkyv(compare(PartialEq), derive(Debug)))] +pub enum ICalendarPeriod { + Range { + start: PartialDateTime, + end: PartialDateTime, + }, + Duration { + start: PartialDateTime, + duration: ICalendarDuration, + }, +} + +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[cfg_attr( + any(test, feature = "serde"), + derive(serde::Serialize, serde::Deserialize) +)] +#[cfg_attr(any(test, feature = "serde"), serde(tag = "type", content = "data"))] +#[cfg_attr( + feature = "rkyv", + derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive) +)] +#[cfg_attr(feature = "rkyv", rkyv(compare(PartialEq), derive(Debug)))] +pub enum ICalendarAction { + Audio, // [RFC5545, Section 3.8.6.1] + Display, // [RFC5545, Section 3.8.6.1] + Email, // [RFC5545, Section 3.8.6.1] + Procedure, // [RFC2445, Section 4.8.6.1] +} + +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[cfg_attr( + any(test, feature = "serde"), + derive(serde::Serialize, serde::Deserialize) +)] +#[cfg_attr(any(test, feature = "serde"), serde(tag = "type", content = "data"))] +#[cfg_attr( + feature = "rkyv", + derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive) +)] +#[cfg_attr(feature = "rkyv", rkyv(compare(PartialEq), derive(Debug)))] +pub enum ICalendarUserTypes { + Individual, // [RFC5545, Section 3.2.3] + Group, // [RFC5545, Section 3.2.3] + Resource, // [RFC5545, Section 3.2.3] + Room, // [RFC5545, Section 3.2.3] + Unknown, // [RFC5545, Section 3.2.3] +} + +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[cfg_attr( + any(test, feature = "serde"), + derive(serde::Serialize, serde::Deserialize) +)] +#[cfg_attr(any(test, feature = "serde"), serde(tag = "type", content = "data"))] +#[cfg_attr( + feature = "rkyv", + derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive) +)] +#[cfg_attr(feature = "rkyv", rkyv(compare(PartialEq), derive(Debug)))] +pub enum ICalendarClassification { + Public, // [RFC5545, Section 3.8.1.3] + Private, // [RFC5545, Section 3.8.1.3] + Confidential, // [RFC5545, Section 3.8.1.3] +} + +#[derive(Debug, Default, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] +#[cfg_attr( + any(test, feature = "serde"), + derive(serde::Serialize, serde::Deserialize) +)] +#[cfg_attr( + feature = "rkyv", + derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive) +)] +#[cfg_attr(feature = "rkyv", rkyv(compare(PartialEq), derive(Debug)))] +pub enum ICalendarComponentType { + Other(String), + #[default] + VCalendar, // [RFC5545, Section 3.4] + VEvent, // [RFC5545, Section 3.6.1] + VTodo, // [RFC5545, Section 3.6.2] + VJournal, // [RFC5545, Section 3.6.3] + VFreebusy, // [RFC5545, Section 3.6.4] + VTimezone, // [RFC5545, Section 3.6.5] + VAlarm, // [RFC5545, Section 3.6.6] + Standard, // [RFC5545, Section 3.6.5] + Daylight, // [RFC5545, Section 3.6.5] + VAvailability, // [RFC7953, Section 3.1] + Available, // [RFC7953, Section 3.1] + Participant, // [RFC9073, Section 7.1] + VLocation, // [RFC9073, Section 7.2] [RFC Errata 7381] + VResource, // [RFC9073, Section 7.3] + VStatus, // draft-ietf-calext-ical-tasks-14 +} + +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[cfg_attr( + any(test, feature = "serde"), + derive(serde::Serialize, serde::Deserialize) +)] +#[cfg_attr(any(test, feature = "serde"), serde(tag = "type", content = "data"))] +#[cfg_attr( + feature = "rkyv", + derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive) +)] +#[cfg_attr(feature = "rkyv", rkyv(compare(PartialEq), derive(Debug)))] +pub enum ICalendarDisplayType { + Badge, // [RFC7986, Section 6.1] + Graphic, // [RFC7986, Section 6.1] + Fullsize, // [RFC7986, Section 6.1] + Thumbnail, // [RFC7986, Section 6.1] +} + +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[cfg_attr( + any(test, feature = "serde"), + derive(serde::Serialize, serde::Deserialize) +)] +#[cfg_attr(any(test, feature = "serde"), serde(tag = "type", content = "data"))] +#[cfg_attr( + feature = "rkyv", + derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive) +)] +#[cfg_attr(feature = "rkyv", rkyv(compare(PartialEq), derive(Debug)))] +pub enum ICalendarFeatureType { + Audio, // [RFC7986, Section 6.3] + Chat, // [RFC7986, Section 6.3] + Feed, // [RFC7986, Section 6.3] + Moderator, // [RFC7986, Section 6.3] + Phone, // [RFC7986, Section 6.3] + Screen, // [RFC7986, Section 6.3] + Video, // [RFC7986, Section 6.3] +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] +#[cfg_attr( + any(test, feature = "serde"), + derive(serde::Serialize, serde::Deserialize) +)] +#[cfg_attr(any(test, feature = "serde"), serde(tag = "type", content = "data"))] +#[cfg_attr( + feature = "rkyv", + derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive) +)] +#[cfg_attr(feature = "rkyv", rkyv(compare(PartialEq), derive(Debug)))] +pub enum ICalendarFreeBusyType { + Free, // [RFC5545, Section 3.2.9] + Busy, // [RFC5545, Section 3.2.9] + BusyUnavailable, // [RFC5545, Section 3.2.9] + BusyTentative, // [RFC5545, Section 3.2.9] +} + +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[cfg_attr( + any(test, feature = "serde"), + derive(serde::Serialize, serde::Deserialize) +)] +#[cfg_attr(any(test, feature = "serde"), serde(tag = "type", content = "data"))] +#[cfg_attr( + feature = "rkyv", + derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive) +)] +#[cfg_attr(feature = "rkyv", rkyv(compare(PartialEq), derive(Debug)))] +pub enum ICalendarMethod { + Publish, // [RFC5546] + Request, // [RFC5546] + Reply, // [RFC5546] + Add, // [RFC5546] + Cancel, // [RFC5546] + Refresh, // [RFC5546] + Counter, // [RFC5546] + Declinecounter, // [RFC5546] +} + +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[cfg_attr( + any(test, feature = "serde"), + derive(serde::Serialize, serde::Deserialize) +)] +#[cfg_attr( + feature = "rkyv", + derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive) +)] +#[cfg_attr(feature = "rkyv", rkyv(compare(PartialEq)))] +pub struct ICalendarParameter { + pub name: ICalendarParameterName, + pub value: ICalendarParameterValue, +} + +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[cfg_attr( + any(test, feature = "serde"), + derive(serde::Serialize, serde::Deserialize) +)] +#[cfg_attr(any(test, feature = "serde"), serde(tag = "type", content = "data"))] +#[cfg_attr( + feature = "rkyv", + derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive) +)] +#[cfg_attr(feature = "rkyv", rkyv(compare(PartialEq)))] +pub enum ICalendarParameterValue { + Text(String), + Integer(u64), + Bool(bool), + Uri(Uri), + Cutype(ICalendarUserTypes), + Fbtype(ICalendarFreeBusyType), + Partstat(ICalendarParticipationStatus), + Related(ICalendarRelated), + Reltype(ICalendarRelationshipType), + Role(ICalendarParticipationRole), + ScheduleAgent(ICalendarScheduleAgentValue), + ScheduleForceSend(ICalendarScheduleForceSendValue), + Value(ICalendarValueType), + Display(ICalendarDisplayType), + Feature(ICalendarFeatureType), + Duration(ICalendarDuration), + Linkrel(LinkRelation), + Null, +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] +#[cfg_attr( + any(test, feature = "serde"), + derive(serde::Serialize, serde::Deserialize) +)] +#[cfg_attr( + feature = "rkyv", + derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive) +)] +#[cfg_attr(feature = "rkyv", rkyv(compare(PartialEq), derive(PartialEq)))] +pub enum ICalendarParameterName { + Other(String), + Altrep, // [RFC5545, Section 3.2.1] + Cn, // [RFC5545, Section 3.2.2] + Cutype, // [RFC5545, Section 3.2.3] + DelegatedFrom, // [RFC5545, Section 3.2.4] + DelegatedTo, // [RFC5545, Section 3.2.5] + Dir, // [RFC5545, Section 3.2.6] + Fmttype, // [RFC5545, Section 3.2.8] + Fbtype, // [RFC5545, Section 3.2.9] + Language, // [RFC5545, Section 3.2.10] + Member, // [RFC5545, Section 3.2.11] + Partstat, // [RFC5545, Section 3.2.12] + Range, // [RFC5545, Section 3.2.13] + Related, // [RFC5545, Section 3.2.14] + Reltype, // [RFC5545, Section 3.2.15] + Role, // [RFC5545, Section 3.2.16] + Rsvp, // [RFC5545, Section 3.2.17] + ScheduleAgent, // [RFC6638, Section 7.1] + ScheduleForceSend, // [RFC6638, Section 7.2] + ScheduleStatus, // [RFC6638, Section 7.3] + SentBy, // [RFC5545, Section 3.2.18] + Tzid, // [RFC5545, Section 3.2.19] + Value, // [RFC5545, Section 3.2.20] + Display, // [RFC7986, Section 6.1] + Email, // [RFC7986, Section 6.2] + Feature, // [RFC7986, Section 6.3] + Label, // [RFC7986, Section 6.4] + Size, // [RFC8607, Section 4.1] + Filename, // [RFC8607, Section 4.2] + ManagedId, // [RFC8607, Section 4.3] + Order, // [RFC9073, Section 5.1] + Schema, // [RFC9073, Section 5.2] + Derived, // [RFC9073, Section 5.3] + Gap, // [RFC9253, Section 6.2] + Linkrel, // [RFC9253, Section 6.1] + Jsptr, // draft-ietf-calext-jscalendar-icalendar + Jsid, // draft-ietf-calext-jscalendar-icalendar +} + +#[derive(Debug, Default, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[cfg_attr( + any(test, feature = "serde"), + derive(serde::Serialize, serde::Deserialize) +)] +#[cfg_attr( + feature = "rkyv", + derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive) +)] +#[cfg_attr(feature = "rkyv", rkyv(compare(PartialEq), derive(Debug)))] +pub struct ICalendarDuration { + pub neg: bool, + pub weeks: u32, + pub days: u32, + pub hours: u32, + pub minutes: u32, + pub seconds: u32, +} + +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[cfg_attr( + any(test, feature = "serde"), + derive(serde::Serialize, serde::Deserialize) +)] +#[cfg_attr(any(test, feature = "serde"), serde(tag = "type", content = "data"))] +#[cfg_attr( + feature = "rkyv", + derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive) +)] +#[cfg_attr(feature = "rkyv", rkyv(compare(PartialEq), derive(Debug)))] +pub enum Uri { + Data(Data), + Location(String), +} + +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[cfg_attr( + any(test, feature = "serde"), + derive(serde::Serialize, serde::Deserialize) +)] +#[cfg_attr( + feature = "rkyv", + derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive) +)] +#[cfg_attr(feature = "rkyv", rkyv(compare(PartialEq), derive(Debug)))] +pub enum ICalendarRelated { + Start, + End, +} + +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[cfg_attr( + any(test, feature = "serde"), + derive(serde::Serialize, serde::Deserialize) +)] +#[cfg_attr(any(test, feature = "serde"), serde(tag = "type", content = "data"))] +#[cfg_attr( + feature = "rkyv", + derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive) +)] +#[cfg_attr(feature = "rkyv", rkyv(compare(PartialEq), derive(Debug)))] +pub enum ICalendarParticipantType { + Active, // [RFC9073, Section 6.2] + Inactive, // [RFC9073, Section 6.2] + Sponsor, // [RFC9073, Section 6.2] + Contact, // [RFC9073, Section 6.2] + BookingContact, // [RFC9073, Section 6.2] + EmergencyContact, // [RFC9073, Section 6.2] + PublicityContact, // [RFC9073, Section 6.2] + PlannerContact, // [RFC9073, Section 6.2] + Performer, // [RFC9073, Section 6.2] + Speaker, // [RFC9073, Section 6.2] +} + +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[cfg_attr( + any(test, feature = "serde"), + derive(serde::Serialize, serde::Deserialize) +)] +#[cfg_attr(any(test, feature = "serde"), serde(tag = "type", content = "data"))] +#[cfg_attr( + feature = "rkyv", + derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive) +)] +#[cfg_attr(feature = "rkyv", rkyv(compare(PartialEq), derive(Debug)))] +pub enum ICalendarParticipationRole { + Chair, // [RFC5545, Section 3.2.16] + ReqParticipant, // [RFC5545, Section 3.2.16] + OptParticipant, // [RFC5545, Section 3.2.16] + NonParticipant, // [RFC5545, Section 3.2.16] + Owner, // JSCalendar +} + +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[cfg_attr( + any(test, feature = "serde"), + derive(serde::Serialize, serde::Deserialize) +)] +#[cfg_attr(any(test, feature = "serde"), serde(tag = "type", content = "data"))] +#[cfg_attr( + feature = "rkyv", + derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive) +)] +#[cfg_attr(feature = "rkyv", rkyv(compare(PartialEq), derive(Debug)))] +pub enum ICalendarStatus { + Tentative, // [RFC5545, Section 3.8.1] + Confirmed, // [RFC5545, Section 3.8.1] + Cancelled, // [RFC5545, Section 3.8.1] + NeedsAction, // [RFC5545, Section 3.8.1] + Completed, // [RFC5545, Section 3.8.1] + InProcess, // [RFC5545, Section 3.8.1] + Draft, // [RFC5545, Section 3.8.1] + Final, // [RFC5545, Section 3.8.1] + Failed, // draft-ietf-calext-ical-tasks + Pending, // draft-ietf-calext-ical-tasks +} + +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[cfg_attr( + any(test, feature = "serde"), + derive(serde::Serialize, serde::Deserialize) +)] +#[cfg_attr(any(test, feature = "serde"), serde(tag = "type", content = "data"))] +#[cfg_attr( + feature = "rkyv", + derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive) +)] +#[cfg_attr(feature = "rkyv", rkyv(compare(PartialEq), derive(Debug)))] +pub enum ICalendarParticipationStatus { + NeedsAction, // [RFC5545, Section 3.2.12] + Accepted, // [RFC5545, Section 3.2.12] + Declined, // [RFC5545, Section 3.2.12] + Tentative, // [RFC5545, Section 3.2.12] + Delegated, // [RFC5545, Section 3.2.12] + Completed, // [RFC5545, Section 3.2.12] + InProcess, // [RFC5545, Section 3.2.12] + Failed, // draft-ietf-calext-ical-tasks +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] +#[cfg_attr( + any(test, feature = "serde"), + derive(serde::Serialize, serde::Deserialize) +)] +#[cfg_attr(any(test, feature = "serde"), serde(tag = "type", content = "data"))] +#[cfg_attr( + feature = "rkyv", + derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive) +)] +#[cfg_attr(feature = "rkyv", rkyv(compare(PartialEq), derive(Debug)))] +pub enum ICalendarProperty { + Begin, + End, + Other(String), + Calscale, // [RFC5545, Section 3.7.1] + Method, // [RFC5545, Section 3.7.2] + Prodid, // [RFC5545, Section 3.7.3] + Version, // [RFC5545, Section 3.7.4] + Attach, // [RFC5545, Section 3.8.1.1] + Categories, // [RFC5545, Section 3.8.1.2] [RFC7986, Section 5.6] + Class, // [RFC5545, Section 3.8.1.3] + Comment, // [RFC5545, Section 3.8.1.4] + Description, // [RFC5545, Section 3.8.1.5] [RFC7986, Section 5.2] + Geo, // [RFC5545, Section 3.8.1.6] + Location, // [RFC5545, Section 3.8.1.7] + PercentComplete, // [RFC5545, Section 3.8.1.8] + Priority, // [RFC5545, Section 3.8.1.9] + Resources, // [RFC5545, Section 3.8.1.10] + Status, // [RFC5545, Section 3.8.1.11] + Summary, // [RFC5545, Section 3.8.1.12] + Completed, // [RFC5545, Section 3.8.2.1] + Dtend, // [RFC5545, Section 3.8.2.2] + Due, // [RFC5545, Section 3.8.2.3] + Dtstart, // [RFC5545, Section 3.8.2.4] + Duration, // [RFC5545, Section 3.8.2.5] + Freebusy, // [RFC5545, Section 3.8.2.6] + Transp, // [RFC5545, Section 3.8.2.7] + Tzid, // [RFC5545, Section 3.8.3.1] + Tzname, // [RFC5545, Section 3.8.3.2] + Tzoffsetfrom, // [RFC5545, Section 3.8.3.3] + Tzoffsetto, // [RFC5545, Section 3.8.3.4] + Tzurl, // [RFC5545, Section 3.8.3.5] + Attendee, // [RFC5545, Section 3.8.4.1] + Contact, // [RFC5545, Section 3.8.4.2] + Organizer, // [RFC5545, Section 3.8.4.3] + RecurrenceId, // [RFC5545, Section 3.8.4.4] + RelatedTo, // [RFC5545, Section 3.8.4.5] [RFC9253, Section 9.1] + Url, // [RFC5545, Section 3.8.4.6] [RFC7986, Section 5.5] + Uid, // [RFC5545, Section 3.8.4.7] [RFC7986, Section 5.3] + Exdate, // [RFC5545, Section 3.8.5.1] + Exrule, // [RFC2445, Section 4.8.5.2] + Rdate, // [RFC5545, Section 3.8.5.2] + Rrule, // [RFC5545, Section 3.8.5.3] + Action, // [RFC5545, Section 3.8.6.1] + Repeat, // [RFC5545, Section 3.8.6.2] + Trigger, // [RFC5545, Section 3.8.6.3] + Created, // [RFC5545, Section 3.8.7.1] + Dtstamp, // [RFC5545, Section 3.8.7.2] + LastModified, // [RFC5545, Section 3.8.7.3] [RFC7986, Section 5.4] + Sequence, // [RFC5545, Section 3.8.7.4] + RequestStatus, // [RFC5545, Section 3.8.8.3] + Xml, // [RFC6321, Section 4.2] + Tzuntil, // [RFC7808, Section 7.1] + TzidAliasOf, // [RFC7808, Section 7.2] + Busytype, // [RFC7953, Section 3.2] + Name, // [RFC7986, Section 5.1] + RefreshInterval, // [RFC7986, Section 5.7] + Source, // [RFC7986, Section 5.8] + Color, // [RFC7986, Section 5.9] + Image, // [RFC7986, Section 5.10] + Conference, // [RFC7986, Section 5.11] + CalendarAddress, // [RFC9073, Section 6.4] + LocationType, // [RFC9073, Section 6.1] + ParticipantType, // [RFC9073, Section 6.2] + ResourceType, // [RFC9073, Section 6.3] + StructuredData, // [RFC9073, Section 6.6] + StyledDescription, // [RFC9073, Section 6.5] + Acknowledged, // [RFC9074, Section 6.1] + Proximity, // [RFC9074, Section 8.1] + Concept, // [RFC9253, Section 8.1] + Link, // [RFC9253, Section 8.2] + Refid, // [RFC9253, Section 8.3] + Coordinates, // draft-ietf-calext-icalendar-jscalendar-extensions + ShowWithoutTime, // draft-ietf-calext-icalendar-jscalendar-extensions + Jsid, // draft-ietf-calext-jscalendar-icalendar + Jsprop, // draft-ietf-calext-jscalendar-icalendar + EstimatedDuration, // draft-ietf-calext-ical-tasks + Reason, // draft-ietf-calext-ical-tasks + Substate, // draft-ietf-calext-ical-tasks + TaskMode, // draft-ietf-calext-ical-tasks +} + +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[cfg_attr( + any(test, feature = "serde"), + derive(serde::Serialize, serde::Deserialize) +)] +#[cfg_attr(any(test, feature = "serde"), serde(tag = "type", content = "data"))] +#[cfg_attr( + feature = "rkyv", + derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive) +)] +#[cfg_attr(feature = "rkyv", rkyv(compare(PartialEq), derive(Debug)))] +pub enum ICalendarProximityValue { + Arrive, // [RFC9074, Section 8.1] + Depart, // [RFC9074, Section 8.1] + Connect, // [RFC9074, Section 8.1] + Disconnect, // [RFC9074, Section 8.1] +} + +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[cfg_attr( + any(test, feature = "serde"), + derive(serde::Serialize, serde::Deserialize) +)] +#[cfg_attr(any(test, feature = "serde"), serde(tag = "type", content = "data"))] +#[cfg_attr( + feature = "rkyv", + derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive) +)] +#[cfg_attr(feature = "rkyv", rkyv(compare(PartialEq), derive(Debug)))] +pub enum ICalendarRelationshipType { + Child, // [RFC5545, Section 3.2.15] + Parent, // [RFC5545, Section 3.2.15] + Sibling, // [RFC5545, Section 3.2.15] + Snooze, // [RFC9074, Section 7.1] + Concept, // [RFC9253, Section 5] + DependsOn, // [RFC9253, Section 5] + Finishtofinish, // [RFC9253, Section 4] + Finishtostart, // [RFC9253, Section 4] + First, // [RFC9253, Section 5] + Next, // [RFC9253, Section 5] + Refid, // [RFC9253, Section 5] + Starttofinish, // [RFC9253, Section 4] + Starttostart, // [RFC9253, Section 4] +} + +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[cfg_attr( + any(test, feature = "serde"), + derive(serde::Serialize, serde::Deserialize) +)] +#[cfg_attr(any(test, feature = "serde"), serde(tag = "type", content = "data"))] +#[cfg_attr( + feature = "rkyv", + derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive) +)] +#[cfg_attr(feature = "rkyv", rkyv(compare(PartialEq), derive(Debug)))] +pub enum ICalendarResourceType { + Projector, // [RFC9073, Section 6.3] + Room, // [RFC9073, Section 6.3] + RemoteConferenceAudio, // [RFC9073, Section 6.3] + RemoteConferenceVideo, // [RFC9073, Section 6.3] +} + +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[cfg_attr( + any(test, feature = "serde"), + derive(serde::Serialize, serde::Deserialize) +)] +#[cfg_attr(any(test, feature = "serde"), serde(tag = "type", content = "data"))] +#[cfg_attr( + feature = "rkyv", + derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive) +)] +#[cfg_attr(feature = "rkyv", rkyv(compare(PartialEq), derive(Debug)))] +pub enum ICalendarScheduleAgentValue { + Server, // [RFC6638, Section 7.1] + Client, // [RFC6638, Section 7.1] + None, // [RFC6638, Section 7.1] +} + +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[cfg_attr( + any(test, feature = "serde"), + derive(serde::Serialize, serde::Deserialize) +)] +#[cfg_attr(any(test, feature = "serde"), serde(tag = "type", content = "data"))] +#[cfg_attr( + feature = "rkyv", + derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive) +)] +#[cfg_attr(feature = "rkyv", rkyv(compare(PartialEq), derive(Debug)))] +pub enum ICalendarScheduleForceSendValue { + Request, // [RFC6638, Section 7.2] + Reply, // [RFC6638, Section 7.2] +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[cfg_attr( + any(test, feature = "serde"), + derive(serde::Serialize, serde::Deserialize) +)] +#[cfg_attr(any(test, feature = "serde"), serde(tag = "type", content = "data"))] +#[cfg_attr( + feature = "rkyv", + derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive) +)] +#[cfg_attr(feature = "rkyv", rkyv(compare(PartialEq), derive(Debug)))] +pub enum ICalendarValueType { + Binary, // [RFC5545, Section 3.3.1] + Boolean, // [RFC5545, Section 3.3.2] + CalAddress, // [RFC5545, Section 3.3.3] + Date, // [RFC5545, Section 3.3.4] + DateTime, // [RFC5545, Section 3.3.5] + Duration, // [RFC5545, Section 3.3.6] + Float, // [RFC5545, Section 3.3.7] + Integer, // [RFC5545, Section 3.3.8] + Period, // [RFC5545, Section 3.3.9] + Recur, // [RFC5545, Section 3.3.10] + Text, // [RFC5545, Section 3.3.11] + Time, // [RFC5545, Section 3.3.12] + Unknown, // [RFC7265, Section 5] + Uri, // [RFC5545, Section 3.3.13] + UtcOffset, // [RFC5545, Section 3.3.14] + XmlReference, // [RFC9253, Section 7] + Uid, // [RFC9253, Section 7] +} + +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[cfg_attr( + any(test, feature = "serde"), + derive(serde::Serialize, serde::Deserialize) +)] +#[cfg_attr(any(test, feature = "serde"), serde(tag = "type", content = "data"))] +#[cfg_attr( + feature = "rkyv", + derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive) +)] +#[cfg_attr(feature = "rkyv", rkyv(compare(PartialEq), derive(Debug)))] +pub enum ICalendarTransparency { + Opaque, + Transparent, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) enum ValueSeparator { + None, + Comma, + Semicolon, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) enum ValueType { + Ical(ICalendarValueType), + CalendarScale, + Method, + Classification, + Status, + Transparency, + Action, + BusyType, + ParticipantType, + ResourceType, + Proximity, +} diff --git a/vendor/calcard/src/icalendar/parser.rs b/vendor/calcard/src/icalendar/parser.rs new file mode 100644 index 00000000..6f96692f --- /dev/null +++ b/vendor/calcard/src/icalendar/parser.rs @@ -0,0 +1,2165 @@ +/* + * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC + * + * SPDX-License-Identifier: Apache-2.0 OR MIT + */ + +use super::*; +use crate::{ + Entry, Parser, StopChar, Token, + common::{ + CalendarScale, Encoding, PartialDateTime, + parser::{Boolean, Integer, parse_digits, parse_small_digits}, + }, + icalendar::{ICalendarDay, ICalendarWeekday}, +}; +use mail_parser::decoders::{ + base64::base64_decode, charsets::map::charset_decoder, + quoted_printable::quoted_printable_decode, +}; +use std::{borrow::Cow, iter::Peekable, slice::Iter}; + +struct Params { + params: Vec, + stop_char: StopChar, + data_type: Option>, + charset: Option, + encoding: Option, +} + +impl Parser<'_> { + pub fn icalendar(&mut self, component_type: ICalendarComponentType) -> Entry { + let mut ical_stack: Vec = Vec::new(); + let mut ical_components = vec![ICalendarComponent { + component_type, + ..Default::default() + }]; + let mut next_component_id: u32 = 1; + let mut ical_idx = 0; + let mut ical = ical_components.last_mut().unwrap(); + + loop { + // Fetch property name + self.expect_iana_token(); + let token = match self.token() { + Some(token) => token, + None => break, + }; + + let mut params = Params { + params: Vec::new(), + stop_char: token.stop_char, + data_type: None, + encoding: None, + charset: None, + }; + + // Parse parameters + let name = token.text; + match params.stop_char { + StopChar::Semicolon => { + self.ical_parameters(&mut params); + } + StopChar::Colon => {} + StopChar::Lf => { + // Invalid line + if name.is_empty() || !self.strict { + continue; + } else { + return Entry::InvalidLine(Token::new(name).into_string()); + } + } + _ => {} + } + + // Invalid stop char, try seeking colon + if !matches!(params.stop_char, StopChar::Colon | StopChar::Lf) { + params.stop_char = self.seek_value_or_eol(); + } + + // Parse property + let name = match ICalendarProperty::parse(name.as_ref()) { + Some(ICalendarProperty::Begin) => { + if params.stop_char == StopChar::Colon { + self.expect_single_value(); + if let Some(token) = self.token() { + let component_type = ICalendarComponentType::parse(token.text.as_ref()) + .unwrap_or_else(|| { + ICalendarComponentType::Other(token.into_string()) + }); + ical_stack.push(ical_idx); + ical.component_ids.push(next_component_id); + ical_components.push(ICalendarComponent { + component_type, + ..Default::default() + }); + ical_idx = next_component_id as usize; + if let Some(id) = next_component_id.checked_add(1) { + next_component_id = id; + } else { + return Entry::TooManyComponents; + } + ical = ical_components.last_mut().unwrap(); + } + } + + if !self.strict { + continue; + } else { + return Entry::InvalidLine("BEGIN".to_string()); + } + } + Some(ICalendarProperty::End) => { + if params.stop_char == StopChar::Colon { + self.expect_single_value(); + if let Some(token) = self.token() { + let component_type = ICalendarComponentType::parse(token.text.as_ref()) + .unwrap_or_else(|| { + ICalendarComponentType::Other(token.into_string()) + }); + if ical.component_type == component_type || !self.strict { + if let Some(parent_ical_idx) = ical_stack.pop() { + ical_idx = parent_ical_idx; + ical = ical_components.get_mut(ical_idx).unwrap(); + } else { + break; + } + } else { + return Entry::UnexpectedComponentEnd { + expected: ical.component_type.clone(), + found: component_type, + }; + } + } + } + + if !self.strict { + continue; + } else { + return Entry::InvalidLine("END".to_string()); + } + } + Some(name) => name, + None => { + if !name.is_empty() { + ICalendarProperty::Other(Token::new(name).into_string()) + } else { + // Invalid line, skip + if params.stop_char != StopChar::Lf { + self.seek_lf(); + } + continue; + } + } + }; + let mut entry = ICalendarEntry { + name, + params: params.params, + values: Vec::new(), + }; + + // Parse value + if params.stop_char != StopChar::Lf { + // Obtain default type and separator + let (default_type, multi_value) = entry.name.default_types(); + match multi_value { + ValueSeparator::None => { + self.expect_single_value(); + } + ValueSeparator::Comma => { + self.expect_multi_value_comma(); + } + ValueSeparator::Semicolon => { + self.expect_multi_value_semicolon(); + } + } + + // Decode + match params.encoding { + Some(Encoding::Base64) if multi_value != ValueSeparator::None => { + self.expect_single_value(); + } + Some(Encoding::QuotedPrintable) => { + self.unfold_qp = true; + } + _ => {} + } + + if matches!( + (¶ms.data_type, &default_type), + (Some(IanaType::Iana(ICalendarValueType::Recur)), _) + | (None, ValueType::Ical(ICalendarValueType::Recur)) + ) { + match self.rrule() { + Ok(rrule) => { + entry + .values + .push(ICalendarValue::RecurrenceRule(Box::new(rrule))); + } + Err(other) => { + entry.values.push(ICalendarValue::Text(other)); + } + } + } else { + while let Some(mut token) = self.token() { + let eol = token.stop_char == StopChar::Lf; + + if token.text.is_empty() + && (matches!(multi_value, ValueSeparator::None) + || matches!(entry.name, ICalendarProperty::Other(_)) + && entry.values.is_empty()) + { + if eol { + break; + } else { + continue; + } + } + + // Decode binary parts + if let Some(encoding) = params.encoding { + let (bytes, default_encoding) = match encoding { + Encoding::Base64 => (base64_decode(&token.text), None), + Encoding::QuotedPrintable => { + (quoted_printable_decode(&token.text), "iso-8859-1".into()) + } + }; + if let Some(bytes) = bytes { + if let Some(decoded) = + params.charset.as_deref().or(default_encoding).and_then( + |charset| { + charset_decoder(charset.as_bytes()) + .map(|decoder| decoder(&bytes)) + }, + ) + { + token.text = Cow::Owned(decoded.into_bytes()); + } else if std::str::from_utf8(&bytes).is_ok() { + token.text = Cow::Owned(bytes); + } else { + entry.values.push(ICalendarValue::Binary(bytes)); + if eol { + break; + } else { + continue; + } + } + } + } + + match &default_type { + ValueType::Ical(default_type) => { + let value = match params + .data_type + .as_ref() + .map(|v| v.iana().unwrap_or(&ICalendarValueType::Text)) + .unwrap_or(default_type) + { + ICalendarValueType::Date => token + .into_ical_date() + .map(|data| ICalendarValue::PartialDateTime(Box::new(data))) + .unwrap_or_else(ICalendarValue::Text), + ICalendarValueType::DateTime => { + match token.into_timestamp(false) { + Ok(timestamp) => { + if !timestamp.has_time() { + params.data_type = Some(IanaType::Iana( + ICalendarValueType::Date, + )); + } + ICalendarValue::PartialDateTime(Box::new(timestamp)) + } + Err(other) => ICalendarValue::Text(other), + } + } + ICalendarValueType::Time => token + .into_ical_time() + .map(|data| ICalendarValue::PartialDateTime(Box::new(data))) + .unwrap_or_else(ICalendarValue::Text), + ICalendarValueType::UtcOffset => token + .into_offset() + .map(|data| ICalendarValue::PartialDateTime(Box::new(data))) + .unwrap_or_else(ICalendarValue::Text), + ICalendarValueType::Boolean => { + ICalendarValue::Boolean(token.into_boolean()) + } + ICalendarValueType::Float => token + .into_float() + .map(ICalendarValue::Float) + .unwrap_or_else(ICalendarValue::Text), + ICalendarValueType::Integer => token + .into_integer() + .map(ICalendarValue::Integer) + .unwrap_or_else(ICalendarValue::Text), + ICalendarValueType::Text + | ICalendarValueType::Binary + | ICalendarValueType::Unknown + | ICalendarValueType::XmlReference + | ICalendarValueType::Uid => { + ICalendarValue::Text(token.into_string()) + } + ICalendarValueType::Uri | ICalendarValueType::CalAddress => { + token + .into_uri_bytes() + .map(|data| ICalendarValue::Uri(Uri::Data(data))) + .unwrap_or_else(|uri| { + ICalendarValue::Uri(Uri::Location(uri)) + }) + } + ICalendarValueType::Duration => { + if let Some(duration) = + ICalendarDuration::parse(token.text.as_ref()) + { + ICalendarValue::Duration(duration) + } else { + ICalendarValue::Text(token.into_string()) + } + } + ICalendarValueType::Period => { + if let Some(period) = + ICalendarPeriod::parse(token.text.as_ref()) + { + ICalendarValue::Period(period) + } else { + ICalendarValue::Text(token.into_string()) + } + } + ICalendarValueType::Recur => unreachable!(), + }; + + entry.values.push(value); + } + ValueType::CalendarScale => { + entry.values.push(match IanaType::from(token) { + IanaType::Iana(value) => ICalendarValue::CalendarScale(value), + IanaType::Other(value) => ICalendarValue::Text(value), + }); + } + ValueType::Method => { + entry.values.push(match IanaType::from(token) { + IanaType::Iana(value) => ICalendarValue::Method(value), + IanaType::Other(value) => ICalendarValue::Text(value), + }); + } + ValueType::Classification => { + entry.values.push(match IanaType::from(token) { + IanaType::Iana(value) => ICalendarValue::Classification(value), + IanaType::Other(value) => ICalendarValue::Text(value), + }); + } + ValueType::Status => { + entry.values.push(match IanaType::from(token) { + IanaType::Iana(value) => ICalendarValue::Status(value), + IanaType::Other(value) => ICalendarValue::Text(value), + }); + } + ValueType::Transparency => { + entry.values.push(match IanaType::from(token) { + IanaType::Iana(value) => ICalendarValue::Transparency(value), + IanaType::Other(value) => ICalendarValue::Text(value), + }); + } + ValueType::Action => { + entry.values.push(match IanaType::from(token) { + IanaType::Iana(value) => ICalendarValue::Action(value), + IanaType::Other(value) => ICalendarValue::Text(value), + }); + } + ValueType::BusyType => { + entry.values.push(match IanaType::from(token) { + IanaType::Iana(value) => ICalendarValue::BusyType(value), + IanaType::Other(value) => ICalendarValue::Text(value), + }); + } + ValueType::ParticipantType => { + entry.values.push(match IanaType::from(token) { + IanaType::Iana(value) => ICalendarValue::ParticipantType(value), + IanaType::Other(value) => ICalendarValue::Text(value), + }); + } + ValueType::ResourceType => { + entry.values.push(match IanaType::from(token) { + IanaType::Iana(value) => ICalendarValue::ResourceType(value), + IanaType::Other(value) => ICalendarValue::Text(value), + }); + } + ValueType::Proximity => { + entry.values.push(match IanaType::from(token) { + IanaType::Iana(value) => ICalendarValue::Proximity(value), + IanaType::Other(value) => ICalendarValue::Text(value), + }); + } + } + + if eol { + break; + } + } + } + } + + // Skip begin and end properties + if !matches!( + entry.name, + ICalendarProperty::Begin | ICalendarProperty::End + ) { + // Add types + if let Some(data_type) = params.data_type { + entry.params.push(ICalendarParameter { + name: ICalendarParameterName::Value, + value: match data_type { + IanaType::Iana(value) => ICalendarParameterValue::Value(value), + IanaType::Other(value) => ICalendarParameterValue::Text(value), + }, + }); + } + + ical.entries.push(entry); + } + } + + if ical_stack.is_empty() || !self.strict { + Entry::ICalendar(ICalendar { + components: ical_components, + }) + } else { + Entry::UnterminatedComponent(ical.component_type.as_str().to_string().into()) + } + } + + fn ical_parameters(&mut self, params: &mut Params) { + while params.stop_char == StopChar::Semicolon { + self.expect_iana_token(); + let token = match self.token() { + Some(token) => token, + None => { + params.stop_char = StopChar::Lf; + break; + } + }; + + // Obtain parameter values + let param_name = token.text; + params.stop_char = token.stop_char; + if !matches!( + params.stop_char, + StopChar::Lf | StopChar::Colon | StopChar::Semicolon + ) { + if params.stop_char != StopChar::Equal { + params.stop_char = self.seek_param_value_or_eol(); + } + if params.stop_char == StopChar::Equal { + self.expect_param_value(); + while !matches!( + params.stop_char, + StopChar::Lf | StopChar::Colon | StopChar::Semicolon + ) { + match self.token() { + Some(token) => { + params.stop_char = token.stop_char; + self.token_buf.push(token); + } + None => { + params.stop_char = StopChar::Lf; + break; + } + } + } + } + } + + let param_values = &mut params.params; + if let Some(param_name) = ICalendarParameterName::try_parse(param_name.as_ref()) { + if self.token_buf.is_empty() { + param_values.push(ICalendarParameter::new( + param_name.clone(), + ICalendarParameterValue::Null, + )); + } + for token in self.token_buf.drain(..) { + match ¶m_name { + ICalendarParameterName::Altrep + | ICalendarParameterName::DelegatedFrom + | ICalendarParameterName::DelegatedTo + | ICalendarParameterName::Dir + | ICalendarParameterName::Member + | ICalendarParameterName::SentBy + | ICalendarParameterName::Schema => { + param_values.push(ICalendarParameter::new( + param_name.clone(), + ICalendarParameterValue::Uri(Uri::from(token)), + )); + } + ICalendarParameterName::Rsvp | ICalendarParameterName::Derived => { + param_values.push(ICalendarParameter::new( + param_name.clone(), + IanaType::::from(token), + )); + } + ICalendarParameterName::Range => { + if token.text.as_ref().eq_ignore_ascii_case(b"THISANDFUTURE") { + param_values.push(ICalendarParameter::new( + param_name.clone(), + ICalendarParameterValue::Bool(true), + )); + } + } + ICalendarParameterName::Size | ICalendarParameterName::Order => { + param_values.push(ICalendarParameter::new( + param_name.clone(), + IanaType::::from(token), + )); + } + ICalendarParameterName::Gap => { + param_values.push(ICalendarParameter::new( + param_name.clone(), + IanaType::::from(token), + )); + } + ICalendarParameterName::Cutype => { + param_values.push(ICalendarParameter::new( + param_name.clone(), + IanaType::::from(token), + )); + } + ICalendarParameterName::Fbtype => { + param_values.push(ICalendarParameter::new( + param_name.clone(), + IanaType::::from(token), + )); + } + ICalendarParameterName::Partstat => { + param_values.push(ICalendarParameter::new( + param_name.clone(), + IanaType::::from(token), + )); + } + ICalendarParameterName::Related => { + param_values.push(ICalendarParameter::new( + param_name.clone(), + IanaType::::from(token), + )); + } + ICalendarParameterName::Reltype => { + param_values.push(ICalendarParameter::new( + param_name.clone(), + IanaType::::from(token), + )); + } + ICalendarParameterName::Role => { + param_values.push(ICalendarParameter::new( + param_name.clone(), + IanaType::::from(token), + )); + } + ICalendarParameterName::ScheduleAgent => { + param_values.push(ICalendarParameter::new( + param_name.clone(), + IanaType::::from(token), + )); + } + ICalendarParameterName::ScheduleForceSend => { + param_values.push(ICalendarParameter::new( + param_name.clone(), + IanaType::::from(token), + )); + } + ICalendarParameterName::Value => { + params.data_type = Some(token.into()); + } + ICalendarParameterName::Display => { + param_values.push(ICalendarParameter::new( + param_name.clone(), + IanaType::::from(token), + )); + } + ICalendarParameterName::Feature => { + param_values.push(ICalendarParameter::new( + param_name.clone(), + IanaType::::from(token), + )); + } + ICalendarParameterName::Linkrel => { + param_values.push(ICalendarParameter::new( + param_name.clone(), + IanaType::::from(token), + )); + } + _ => { + param_values.push(ICalendarParameter::new( + param_name.clone(), + ICalendarParameterValue::Text(token.into_string()), + )); + } + } + } + } else if !param_name.is_empty() { + match param_name.first() { + Some(b'c' | b'C') if param_name.as_ref().eq_ignore_ascii_case(b"charset") => { + for token in self.token_buf.drain(..) { + params.charset = token.into_string().into(); + } + } + Some(b'e' | b'E') if param_name.as_ref().eq_ignore_ascii_case(b"encoding") => { + for token in self.token_buf.drain(..) { + params.encoding = Encoding::parse(token.text.as_ref()); + } + } + _ => { + if params.encoding.is_none() && param_name.eq_ignore_ascii_case(b"base64") { + params.encoding = Some(Encoding::Base64); + } else { + let name = + ICalendarParameterName::Other(Token::new(param_name).into_string()) + .clone(); + + if !self.token_buf.is_empty() { + param_values.extend(self.token_buf.drain(..).map(|token| { + ICalendarParameter::new( + name.clone(), + ICalendarParameterValue::Text(token.into_string()), + ) + })); + } else { + param_values.push(ICalendarParameter::new( + name, + ICalendarParameterValue::Null, + )); + } + } + } + } + } + } + } + + pub(crate) fn rrule(&mut self) -> Result { + self.expect_rrule_value(); + + let mut last_stop_char = StopChar::Equal; + let mut is_valid = true; + let mut has_freq = false; + let mut rrule = ICalendarRecurrenceRule::default(); + + let mut token_start = usize::MAX; + + while let Some(mut token) = self.token_until_lf(&mut last_stop_char) { + if token_start == usize::MAX && !token.text.is_empty() { + token_start = token.start; + } + if !is_valid { + continue; + } + if token.text.is_empty() && token.stop_char != StopChar::Equal { + continue; + } + if token.stop_char != StopChar::Equal { + if !self.strict { + // Ignore unknown tokens + while let Some(token_) = self.token_until_lf(&mut last_stop_char) { + if token_.stop_char == StopChar::Equal { + token = token_; + break; + } + } + if token.stop_char != StopChar::Equal { + is_valid = false; + continue; + } + } else { + is_valid = false; + continue; + } + } + + hashify::fnc_map_ignore_case!(token.text.as_ref(), + b"FREQ" => { + while let Some(value) = self.parse_value_until_lf(StopChar::Semicolon, &mut last_stop_char) { if let Some(value)= value { + rrule.freq = value; + has_freq = true; + } else if !self.strict { + is_valid = false; + } + } + }, + b"UNTIL" => { + while let Some(value) = self.parse_value_until_lf::(StopChar::Semicolon, &mut last_stop_char) { if let Some(value)= value { + rrule.until = Some(value.0); + } else if !self.strict { + is_valid = false; + } + } + }, + b"COUNT" => { + while let Some(value) = self.parse_value_until_lf::(StopChar::Semicolon, &mut last_stop_char) { if let Some(value)= value { + let count = value.0.unsigned_abs() as u32; + if count > 0 { + rrule.count = Some(count); + } + } else if !self.strict { + is_valid = false; + } + } + }, + b"INTERVAL" => { + while let Some(value) = self.parse_value_until_lf::(StopChar::Semicolon, &mut last_stop_char) { if let Some(value)= value { + let interval = value.0.unsigned_abs() as u16; + if interval > 0 { + rrule.interval = Some(interval); + } + } else if !self.strict { + is_valid = false; + } + } + }, + b"BYSECOND" => { + while let Some(value) = self.parse_value_until_lf::(StopChar::Semicolon, &mut last_stop_char) { if let Some(value)= value { + rrule.bysecond.push(value.0.unsigned_abs() as u8); + } else if !self.strict { + is_valid = false; + } + } + }, + b"BYMINUTE" => { + while let Some(value) = self.parse_value_until_lf::(StopChar::Semicolon, &mut last_stop_char) { if let Some(value)= value { + rrule.byminute.push(value.0.unsigned_abs() as u8); + } else if !self.strict { + is_valid = false; + } + } + }, + b"BYHOUR" => { + while let Some(value) = self.parse_value_until_lf::(StopChar::Semicolon, &mut last_stop_char) { if let Some(value)= value { + rrule.byhour.push(value.0.unsigned_abs() as u8); + } else if !self.strict { + is_valid = false; + } + } + }, + b"BYDAY" => { + while let Some(value) = self.parse_value_until_lf::(StopChar::Semicolon, &mut last_stop_char) { if let Some(value)= value { + rrule.byday.push(value); + } else if !self.strict { + is_valid = false; + } + } + }, + b"BYMONTHDAY" => { + while let Some(value) = self.parse_value_until_lf::(StopChar::Semicolon, &mut last_stop_char) { if let Some(value)= value { + rrule.bymonthday.push(value.0 as i8); + } else if !self.strict { + is_valid = false; + } + } + }, + b"BYYEARDAY" => { + while let Some(value) = self.parse_value_until_lf::(StopChar::Semicolon, &mut last_stop_char) { if let Some(value)= value { + rrule.byyearday.push(value.0 as i16); + } else if !self.strict { + is_valid = false; + } + } + }, + b"BYWEEKNO" => { + while let Some(value) = self.parse_value_until_lf::(StopChar::Semicolon, &mut last_stop_char) { if let Some(value)= value { + rrule.byweekno.push(value.0 as i8); + } else if !self.strict { + is_valid = false; + } + } + }, + b"BYMONTH" => { + while let Some(value) = self.parse_value_until_lf::(StopChar::Semicolon, &mut last_stop_char) { if let Some(value)= value { + rrule.bymonth.push(value); + } else if !self.strict { + is_valid = false; + } + } + }, + b"BYSETPOS" => { + while let Some(value) = self.parse_value_until_lf::(StopChar::Semicolon, &mut last_stop_char) { if let Some(value)= value { + rrule.bysetpos.push(value.0 as i32); + } else if !self.strict { + is_valid = false; + } + } + }, + b"WKST" => { + while let Some(value) = self.parse_value_until_lf::(StopChar::Semicolon, &mut last_stop_char) { if let Some(value)= value { + rrule.wkst = Some(value); + } else if !self.strict { + is_valid = false; + } + } + }, + b"RSCALE" => { + while let Some(value) = self.parse_value_until_lf::(StopChar::Semicolon, &mut last_stop_char) { if let Some(value)= value { + rrule.rscale = Some(value); + } else if !self.strict { + is_valid = false; + } + } + }, + b"SKIP" => { + while let Some(value) = self.parse_value_until_lf::(StopChar::Semicolon, &mut last_stop_char) { if let Some(value)= value { + rrule.skip = Some(value); + } else if !self.strict { + is_valid = false; + } + } + }, + _ => { + if !self.strict { + // Ignore unknown tokens + while let Some(token) = self.token_until_lf(&mut last_stop_char) { if token.stop_char == StopChar::Semicolon{ + break; + } + } + } else { + is_valid = false; + } + } + ); + } + + if has_freq && is_valid { + Ok(rrule) + } else if token_start != usize::MAX && self.last_token_end >= token_start { + Err(self + .input + .get(token_start..=self.last_token_end) + .map(|slice| { + let mut buf = Vec::with_capacity(slice.len()); + let mut iter = slice.iter().peekable(); + + while let Some(ch) = iter.next() { + match ch { + b'\r' => {} + b'\n' => { + iter.next_if(|ch| matches!(ch, b' ' | b'\t')); + } + _ => buf.push(*ch), + } + } + + String::from_utf8(buf) + .unwrap_or_else(|err| String::from_utf8_lossy(err.as_bytes()).into_owned()) + }) + .unwrap_or_default()) + } else { + Err("".to_string()) + } + } +} + +impl Token<'_> { + pub(crate) fn into_ical_date(self) -> std::result::Result { + let mut dt = PartialDateTime::default(); + if dt.parse_ical_date(&mut self.text.iter().peekable()) { + Ok(dt) + } else { + Err(self.into_string()) + } + } + + pub(crate) fn into_ical_time(self) -> std::result::Result { + let mut dt = PartialDateTime::default(); + if dt.parse_ical_time(&mut self.text.iter().peekable()) { + Ok(dt) + } else { + Err(self.into_string()) + } + } +} + +impl PartialDateTime { + pub fn parse_ical_date(&mut self, iter: &mut Peekable>) -> bool { + parse_digits(iter, &mut self.year, 4, false) + && parse_small_digits(iter, &mut self.month, 2, false) + && parse_small_digits(iter, &mut self.day, 2, false) + } + + pub fn parse_ical_time(&mut self, iter: &mut Peekable>) -> bool { + if parse_small_digits(iter, &mut self.hour, 2, false) + && parse_small_digits(iter, &mut self.minute, 2, false) + && parse_small_digits(iter, &mut self.second, 2, false) + { + self.parse_zone(iter); + true + } else { + false + } + } +} + +struct ICalendarDateOrTime(PartialDateTime); + +impl IanaParse for ICalendarDateOrTime { + fn parse(value: &[u8]) -> Option { + let mut dt = PartialDateTime::default(); + if dt.parse_timestamp(&mut value.iter().peekable(), false) { + Some(ICalendarDateOrTime(dt)) + } else { + None + } + } +} + +impl IanaParse for ICalendarPeriod { + fn parse(value: &[u8]) -> Option { + let mut iter = value.iter().peekable(); + let mut start = PartialDateTime::default(); + if start.parse_timestamp(&mut iter, true) { + if let Some(duration) = ICalendarDuration::try_parse(&mut iter) { + Some(ICalendarPeriod::Duration { start, duration }) + } else { + let mut end = PartialDateTime::default(); + if end.parse_timestamp(&mut iter, true) { + Some(ICalendarPeriod::Range { start, end }) + } else { + None + } + } + } else { + None + } + } +} + +impl IanaParse for ICalendarMonth { + fn parse(value: &[u8]) -> Option { + let mut num: i8 = 0; + let mut is_leap = false; + + for (pos, ch) in value.iter().enumerate() { + match ch { + b'L' | b'l' if pos > 0 => { + is_leap = true; + } + b'0'..=b'9' => { + num = num.saturating_mul(10).saturating_add((*ch - b'0') as i8); + } + _ => { + if !ch.is_ascii_whitespace() { + return None; + } + } + } + } + + Some(ICalendarMonth(if is_leap { -num } else { num })) + } +} + +impl ICalendarMonth { + pub fn new(month: u8, is_leap: bool) -> Self { + ICalendarMonth(if is_leap { -(month as i8) } else { month as i8 }) + } + + pub fn is_leap(&self) -> bool { + self.0 < 0 + } + + pub fn month(&self) -> u8 { + self.0.unsigned_abs() + } +} + +impl From for u8 { + fn from(value: ICalendarMonth) -> Self { + value.month() + } +} + +impl ICalendarDuration { + fn try_parse(iter: &mut Peekable>) -> Option { + let mut dur = ICalendarDuration::default(); + loop { + match iter.peek() { + Some(b'P' | b'p') => { + iter.next(); + break; + } + Some(b'+') => { + iter.next(); + } + Some(b'-') if !dur.neg => { + iter.next(); + dur.neg = true; + } + Some(b' ' | b'\t') => { + iter.next(); + } + _ => { + return None; + } + } + } + + let mut num: u32 = 0; + let mut saw_component = false; + for ch in iter { + match ch { + b'0'..=b'9' => { + num = num.saturating_mul(10).saturating_add((ch - b'0') as u32); + } + b'T' | b't' => {} + b'W' | b'w' => { + dur.weeks = num; + num = 0; + saw_component = true; + } + b'D' | b'd' => { + dur.days = num; + num = 0; + saw_component = true; + } + b'H' | b'h' => { + dur.hours = num; + num = 0; + saw_component = true; + } + b'M' | b'm' => { + dur.minutes = num; + num = 0; + saw_component = true; + } + b'S' | b's' => { + dur.seconds = num; + num = 0; + saw_component = true; + } + _ => { + if !ch.is_ascii_whitespace() { + return None; + } + } + } + } + + if saw_component { Some(dur) } else { None } + } +} + +impl IanaParse for ICalendarDuration { + fn parse(value: &[u8]) -> Option { + ICalendarDuration::try_parse(&mut value.iter().peekable()) + } +} + +impl From> for Uri { + fn from(token: Token<'_>) -> Self { + token + .into_uri_bytes() + .map(Uri::Data) + .unwrap_or_else(Uri::Location) + } +} + +impl IanaParse for ICalendarDay { + fn parse(value: &[u8]) -> Option { + let mut iter = value.iter().enumerate(); + let mut is_negative = false; + let mut has_ordwk = false; + let mut ordwk: i16 = 0; + + loop { + let (pos, ch) = iter.next()?; + + match ch { + b'0'..=b'9' => { + ordwk = ordwk.saturating_mul(10).saturating_add((ch - b'0') as i16); + has_ordwk = true; + } + b'-' if pos == 0 => { + is_negative = true; + } + b'+' if pos == 0 => {} + b'A'..=b'Z' | b'a'..=b'z' => { + return ICalendarWeekday::parse(value.get(pos..).unwrap_or_default()).map( + |weekday| ICalendarDay { + ordwk: has_ordwk.then_some(if is_negative { -ordwk } else { ordwk }), + weekday, + }, + ); + } + _ => return None, + } + } + } +} + +impl Uri { + pub fn parse(value: impl Into) -> Self { + let uri = value.into(); + Data::try_parse(uri.as_bytes()) + .map(Uri::Data) + .unwrap_or_else(|| Uri::Location(uri)) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use std::io::Write; + + #[test] + fn parse_ical() { + // Read all .ics files in the test directory + for entry in std::fs::read_dir("resources/ical").unwrap() { + let entry = entry.unwrap(); + let path = entry.path(); + if path.extension().is_some_and(|ext| ext == "ics") { + let input = match String::from_utf8(std::fs::read(&path).unwrap()) { + Ok(input) => input, + Err(err) => { + // ISO-8859-1 + err.as_bytes() + .iter() + .map(|&b| b as char) + .collect::() + } + }; + let mut parser = Parser::new(&input); + let mut output = std::fs::File::create(path.with_extension("ics.out")).unwrap(); + //let mut output_debug = + // std::fs::File::create(path.with_extension("ics.debug")).unwrap(); + let file_name = path.as_path().to_str().unwrap(); + + loop { + match parser.entry() { + Entry::ICalendar(mut ical) => { + for item in &mut ical.components { + for item in &mut item.entries { + if item.name == ICalendarProperty::Version { + item.values = vec![ICalendarValue::Text("2.0".into())]; + } + } + } + let ical_text = ical.to_string(); + crate::common::writer::assert_fold_width(&ical_text, file_name); + writeln!(output, "{}", ical_text).unwrap(); + //writeln!(output_debug, "{:#?}", ical).unwrap(); + + // Roundtrip parsing + let mut parser = Parser::new(&ical_text); + match parser.entry() { + Entry::ICalendar(ical_) => { + /*ical.components.iter_mut().for_each(|component| { + component.entries.retain(|entry| { + !matches!(entry.name, ICalendarProperty::Version) + }); + }); + ical_.components.iter_mut().for_each(|component| { + component.entries.retain(|entry| { + !matches!(entry.name, ICalendarProperty::Version) + }); + });*/ + + compare_components(&ical, &ical_, file_name); + } + other => panic!("Expected iCal, got {other:?} for {file_name}"), + } + + // Rkyv archiving tests + #[cfg(feature = "rkyv")] + { + let ical_bytes = + rkyv::to_bytes::(&ical).unwrap(); + let ical_unarchived = rkyv::access::< + crate::icalendar::ArchivedICalendar, + rkyv::rancor::Error, + >(&ical_bytes) + .unwrap(); + assert_eq!(ical_text, ical_unarchived.to_string()); + } + } + Entry::InvalidLine(text) => { + println!("Invalid line in {file_name}: {text}"); + } + Entry::Eof => break, + other => { + panic!("Expected iCal, got {other:?} for {file_name}"); + } + } + } + } + } + } + + fn compare_components(a: &ICalendar, b: &ICalendar, file_name: &str) { + assert_eq!( + a.components.len(), + b.components.len(), + "failed for {file_name}" + ); + + for (a, b) in a.components.iter().zip(b.components.iter()) { + assert_eq!(a, b, "failed for {file_name}"); + } + } + + #[test] + fn test_parse_rrule() { + for (rule, expected) in [ + ( + "FREQ=MONTHLY;BYDAY=MO,TU,WE,TH,FR;BYSETPOS=-1", + ICalendarRecurrenceRule { + freq: ICalendarFrequency::Monthly, + byday: vec![ + ICalendarDay { + ordwk: None, + weekday: ICalendarWeekday::Monday, + }, + ICalendarDay { + ordwk: None, + weekday: ICalendarWeekday::Tuesday, + }, + ICalendarDay { + ordwk: None, + weekday: ICalendarWeekday::Wednesday, + }, + ICalendarDay { + ordwk: None, + weekday: ICalendarWeekday::Thursday, + }, + ICalendarDay { + ordwk: None, + weekday: ICalendarWeekday::Friday, + }, + ], + bysetpos: vec![-1], + ..Default::default() + }, + ), + ( + "FREQ=YEARLY;BYMONTH=1,2", + ICalendarRecurrenceRule { + freq: ICalendarFrequency::Yearly, + bymonth: vec![ICalendarMonth(1), ICalendarMonth(2)], + ..Default::default() + }, + ), + ( + "FREQ=YEARLY;INTERVAL=2;BYMONTH=1;BYDAY=SU;BYHOUR=8,9;BYMINUTE=30", + ICalendarRecurrenceRule { + freq: ICalendarFrequency::Yearly, + interval: Some(2), + byminute: vec![30], + byhour: vec![8, 9], + byday: vec![ICalendarDay { + ordwk: None, + weekday: ICalendarWeekday::Sunday, + }], + bymonth: vec![ICalendarMonth(1)], + ..Default::default() + }, + ), + ( + "FREQ=DAILY;COUNT=10;INTERVAL=2", + ICalendarRecurrenceRule { + freq: ICalendarFrequency::Daily, + count: Some(10), + interval: Some(2), + ..Default::default() + }, + ), + ( + "FREQ=YEARLY", + ICalendarRecurrenceRule { + freq: ICalendarFrequency::Yearly, + ..Default::default() + }, + ), + ( + "FREQ=YEARLY;BYMONTH=4;BYDAY=-1SU;UNTIL=19730429T070000Z", + ICalendarRecurrenceRule { + freq: ICalendarFrequency::Yearly, + until: Some(PartialDateTime { + year: Some(1973), + month: Some(4), + day: Some(29), + hour: Some(7), + minute: Some(0), + second: Some(0), + tz_hour: Some(0), + tz_minute: Some(0), + tz_minus: false, + }), + byday: vec![ICalendarDay { + ordwk: Some(-1), + weekday: ICalendarWeekday::Sunday, + }], + bymonth: vec![ICalendarMonth(4)], + ..Default::default() + }, + ), + ( + "FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU;UNTIL=20061029T060000", + ICalendarRecurrenceRule { + freq: ICalendarFrequency::Yearly, + until: Some(PartialDateTime { + year: Some(2006), + month: Some(10), + day: Some(29), + hour: Some(6), + minute: Some(0), + second: Some(0), + tz_hour: None, + tz_minute: None, + tz_minus: false, + }), + byday: vec![ICalendarDay { + ordwk: Some(-1), + weekday: ICalendarWeekday::Sunday, + }], + bymonth: vec![ICalendarMonth(10)], + ..Default::default() + }, + ), + ( + "FREQ=YEARLY;BYMONTH=3;BYDAY=2SU", + ICalendarRecurrenceRule { + freq: ICalendarFrequency::Yearly, + byday: vec![ICalendarDay { + ordwk: Some(2), + weekday: ICalendarWeekday::Sunday, + }], + bymonth: vec![ICalendarMonth(3)], + ..Default::default() + }, + ), + ( + "FREQ=YEARLY;BYDAY=-1SU;BYMONTH=10", + ICalendarRecurrenceRule { + freq: ICalendarFrequency::Yearly, + byday: vec![ICalendarDay { + ordwk: Some(-1), + weekday: ICalendarWeekday::Sunday, + }], + bymonth: vec![ICalendarMonth(10)], + ..Default::default() + }, + ), + ( + "FREQ=DAILY;COUNT=10", + ICalendarRecurrenceRule { + freq: ICalendarFrequency::Daily, + count: Some(10), + ..Default::default() + }, + ), + ( + "FREQ=DAILY;INTERVAL=10;COUNT=5", + ICalendarRecurrenceRule { + freq: ICalendarFrequency::Daily, + count: Some(5), + interval: Some(10), + ..Default::default() + }, + ), + ( + "FREQ=YEARLY;UNTIL=20000131T140000Z;BYMONTH=1;BYDAY=SU,MO,TU,WE,TH,FR,SA", + ICalendarRecurrenceRule { + freq: ICalendarFrequency::Yearly, + until: Some(PartialDateTime { + year: Some(2000), + month: Some(1), + day: Some(31), + hour: Some(14), + minute: Some(0), + second: Some(0), + tz_hour: Some(0), + tz_minute: Some(0), + tz_minus: false, + }), + byday: vec![ + ICalendarDay { + ordwk: None, + weekday: ICalendarWeekday::Sunday, + }, + ICalendarDay { + ordwk: None, + weekday: ICalendarWeekday::Monday, + }, + ICalendarDay { + ordwk: None, + weekday: ICalendarWeekday::Tuesday, + }, + ICalendarDay { + ordwk: None, + weekday: ICalendarWeekday::Wednesday, + }, + ICalendarDay { + ordwk: None, + weekday: ICalendarWeekday::Thursday, + }, + ICalendarDay { + ordwk: None, + weekday: ICalendarWeekday::Friday, + }, + ICalendarDay { + ordwk: None, + weekday: ICalendarWeekday::Saturday, + }, + ], + bymonth: vec![ICalendarMonth(1)], + ..Default::default() + }, + ), + ( + "FREQ=DAILY;UNTIL=20000131T140000Z;BYMONTH=1", + ICalendarRecurrenceRule { + freq: ICalendarFrequency::Daily, + until: Some(PartialDateTime { + year: Some(2000), + month: Some(1), + day: Some(31), + hour: Some(14), + minute: Some(0), + second: Some(0), + tz_hour: Some(0), + tz_minute: Some(0), + tz_minus: false, + }), + bymonth: vec![ICalendarMonth(1)], + ..Default::default() + }, + ), + ( + "FREQ=WEEKLY;COUNT=10", + ICalendarRecurrenceRule { + freq: ICalendarFrequency::Weekly, + count: Some(10), + ..Default::default() + }, + ), + ( + "FREQ=WEEKLY;INTERVAL=2;WKST=SU", + ICalendarRecurrenceRule { + freq: ICalendarFrequency::Weekly, + interval: Some(2), + wkst: Some(ICalendarWeekday::Sunday), + ..Default::default() + }, + ), + ( + "FREQ=WEEKLY;UNTIL=19971007T000000Z;WKST=SU;BYDAY=TU,TH", + ICalendarRecurrenceRule { + freq: ICalendarFrequency::Weekly, + until: Some(PartialDateTime { + year: Some(1997), + month: Some(10), + day: Some(7), + hour: Some(0), + minute: Some(0), + second: Some(0), + tz_hour: Some(0), + tz_minute: Some(0), + tz_minus: false, + }), + byday: vec![ + ICalendarDay { + ordwk: None, + weekday: ICalendarWeekday::Tuesday, + }, + ICalendarDay { + ordwk: None, + weekday: ICalendarWeekday::Thursday, + }, + ], + wkst: Some(ICalendarWeekday::Sunday), + ..Default::default() + }, + ), + ( + "FREQ=WEEKLY;COUNT=10;WKST=SU;BYDAY=TU,TH", + ICalendarRecurrenceRule { + freq: ICalendarFrequency::Weekly, + count: Some(10), + byday: vec![ + ICalendarDay { + ordwk: None, + weekday: ICalendarWeekday::Tuesday, + }, + ICalendarDay { + ordwk: None, + weekday: ICalendarWeekday::Thursday, + }, + ], + wkst: Some(ICalendarWeekday::Sunday), + ..Default::default() + }, + ), + ( + "FREQ=WEEKLY;INTERVAL=2;UNTIL=19971224T000000Z;WKST=SU;BYDAY=MO,WE,FR", + ICalendarRecurrenceRule { + freq: ICalendarFrequency::Weekly, + until: Some(PartialDateTime { + year: Some(1997), + month: Some(12), + day: Some(24), + hour: Some(0), + minute: Some(0), + second: Some(0), + tz_hour: Some(0), + tz_minute: Some(0), + tz_minus: false, + }), + interval: Some(2), + byday: vec![ + ICalendarDay { + ordwk: None, + weekday: ICalendarWeekday::Monday, + }, + ICalendarDay { + ordwk: None, + weekday: ICalendarWeekday::Wednesday, + }, + ICalendarDay { + ordwk: None, + weekday: ICalendarWeekday::Friday, + }, + ], + wkst: Some(ICalendarWeekday::Sunday), + ..Default::default() + }, + ), + ( + "FREQ=WEEKLY;INTERVAL=2;COUNT=8;WKST=SU;BYDAY=TU,TH", + ICalendarRecurrenceRule { + freq: ICalendarFrequency::Weekly, + count: Some(8), + interval: Some(2), + byday: vec![ + ICalendarDay { + ordwk: None, + weekday: ICalendarWeekday::Tuesday, + }, + ICalendarDay { + ordwk: None, + weekday: ICalendarWeekday::Thursday, + }, + ], + wkst: Some(ICalendarWeekday::Sunday), + ..Default::default() + }, + ), + ( + "FREQ=MONTHLY;COUNT=10;BYDAY=1FR", + ICalendarRecurrenceRule { + freq: ICalendarFrequency::Monthly, + count: Some(10), + byday: vec![ICalendarDay { + ordwk: Some(1), + weekday: ICalendarWeekday::Friday, + }], + ..Default::default() + }, + ), + ( + "FREQ=MONTHLY;INTERVAL=2;COUNT=10;BYDAY=1SU,-1SU", + ICalendarRecurrenceRule { + freq: ICalendarFrequency::Monthly, + count: Some(10), + interval: Some(2), + byday: vec![ + ICalendarDay { + ordwk: Some(1), + weekday: ICalendarWeekday::Sunday, + }, + ICalendarDay { + ordwk: Some(-1), + weekday: ICalendarWeekday::Sunday, + }, + ], + ..Default::default() + }, + ), + ( + "FREQ=MONTHLY;COUNT=6;BYDAY=-2MO", + ICalendarRecurrenceRule { + freq: ICalendarFrequency::Monthly, + count: Some(6), + byday: vec![ICalendarDay { + ordwk: Some(-2), + weekday: ICalendarWeekday::Monday, + }], + ..Default::default() + }, + ), + ( + "FREQ=MONTHLY;BYMONTHDAY=-3", + ICalendarRecurrenceRule { + freq: ICalendarFrequency::Monthly, + bymonthday: vec![-3], + ..Default::default() + }, + ), + ( + "FREQ=MONTHLY;COUNT=10;BYMONTHDAY=2,15", + ICalendarRecurrenceRule { + freq: ICalendarFrequency::Monthly, + count: Some(10), + bymonthday: vec![2, 15], + ..Default::default() + }, + ), + ( + "FREQ=MONTHLY;COUNT=10;BYMONTHDAY=1,-1", + ICalendarRecurrenceRule { + freq: ICalendarFrequency::Monthly, + count: Some(10), + bymonthday: vec![1, -1], + ..Default::default() + }, + ), + ( + "FREQ=MONTHLY;INTERVAL=18;COUNT=10;BYMONTHDAY=10,11,12,13,14,15", + ICalendarRecurrenceRule { + freq: ICalendarFrequency::Monthly, + count: Some(10), + interval: Some(18), + bymonthday: vec![10, 11, 12, 13, 14, 15], + ..Default::default() + }, + ), + ( + "FREQ=MONTHLY;INTERVAL=2;BYDAY=TU", + ICalendarRecurrenceRule { + freq: ICalendarFrequency::Monthly, + interval: Some(2), + byday: vec![ICalendarDay { + ordwk: None, + weekday: ICalendarWeekday::Tuesday, + }], + ..Default::default() + }, + ), + ( + "FREQ=YEARLY;COUNT=10;BYMONTH=6,7", + ICalendarRecurrenceRule { + freq: ICalendarFrequency::Yearly, + count: Some(10), + bymonth: vec![ICalendarMonth(6), ICalendarMonth(7)], + ..Default::default() + }, + ), + ( + "FREQ=YEARLY;INTERVAL=2;COUNT=10;BYMONTH=1,2,3", + ICalendarRecurrenceRule { + freq: ICalendarFrequency::Yearly, + count: Some(10), + interval: Some(2), + bymonth: vec![ICalendarMonth(1), ICalendarMonth(2), ICalendarMonth(3)], + ..Default::default() + }, + ), + ( + "FREQ=YEARLY;INTERVAL=3;COUNT=10;BYYEARDAY=1,100,200", + ICalendarRecurrenceRule { + freq: ICalendarFrequency::Yearly, + count: Some(10), + interval: Some(3), + byyearday: vec![1, 100, 200], + ..Default::default() + }, + ), + ( + "FREQ=YEARLY;BYDAY=20MO", + ICalendarRecurrenceRule { + freq: ICalendarFrequency::Yearly, + byday: vec![ICalendarDay { + ordwk: Some(20), + weekday: ICalendarWeekday::Monday, + }], + ..Default::default() + }, + ), + ( + "FREQ=YEARLY;BYWEEKNO=20;BYDAY=MO", + ICalendarRecurrenceRule { + freq: ICalendarFrequency::Yearly, + byday: vec![ICalendarDay { + ordwk: None, + weekday: ICalendarWeekday::Monday, + }], + byweekno: vec![20], + ..Default::default() + }, + ), + ( + "FREQ=YEARLY;BYMONTH=3;BYDAY=TH", + ICalendarRecurrenceRule { + freq: ICalendarFrequency::Yearly, + byday: vec![ICalendarDay { + ordwk: None, + weekday: ICalendarWeekday::Thursday, + }], + bymonth: vec![ICalendarMonth(3)], + ..Default::default() + }, + ), + ( + "FREQ=YEARLY;BYDAY=TH;BYMONTH=6,7,8", + ICalendarRecurrenceRule { + freq: ICalendarFrequency::Yearly, + byday: vec![ICalendarDay { + ordwk: None, + weekday: ICalendarWeekday::Thursday, + }], + bymonth: vec![ICalendarMonth(6), ICalendarMonth(7), ICalendarMonth(8)], + ..Default::default() + }, + ), + ( + "FREQ=MONTHLY;BYDAY=FR;BYMONTHDAY=13", + ICalendarRecurrenceRule { + freq: ICalendarFrequency::Monthly, + byday: vec![ICalendarDay { + ordwk: None, + weekday: ICalendarWeekday::Friday, + }], + bymonthday: vec![13], + ..Default::default() + }, + ), + ( + "FREQ=MONTHLY;BYDAY=SA;BYMONTHDAY=7,8,9,10,11,12,13", + ICalendarRecurrenceRule { + freq: ICalendarFrequency::Monthly, + byday: vec![ICalendarDay { + ordwk: None, + weekday: ICalendarWeekday::Saturday, + }], + bymonthday: vec![7, 8, 9, 10, 11, 12, 13], + ..Default::default() + }, + ), + ( + "FREQ=YEARLY;INTERVAL=4;BYMONTH=11;BYDAY=TU;BYMONTHDAY=2,3,4,5,6,7,8", + ICalendarRecurrenceRule { + freq: ICalendarFrequency::Yearly, + interval: Some(4), + byday: vec![ICalendarDay { + ordwk: None, + weekday: ICalendarWeekday::Tuesday, + }], + bymonthday: vec![2, 3, 4, 5, 6, 7, 8], + bymonth: vec![ICalendarMonth(11)], + ..Default::default() + }, + ), + ( + "FREQ=MONTHLY;COUNT=3;BYDAY=TU,WE,TH;BYSETPOS=3", + ICalendarRecurrenceRule { + freq: ICalendarFrequency::Monthly, + count: Some(3), + byday: vec![ + ICalendarDay { + ordwk: None, + weekday: ICalendarWeekday::Tuesday, + }, + ICalendarDay { + ordwk: None, + weekday: ICalendarWeekday::Wednesday, + }, + ICalendarDay { + ordwk: None, + weekday: ICalendarWeekday::Thursday, + }, + ], + bysetpos: vec![3], + ..Default::default() + }, + ), + ( + "FREQ=MONTHLY;BYDAY=MO,TU,WE,TH,FR;BYSETPOS=-2", + ICalendarRecurrenceRule { + freq: ICalendarFrequency::Monthly, + byday: vec![ + ICalendarDay { + ordwk: None, + weekday: ICalendarWeekday::Monday, + }, + ICalendarDay { + ordwk: None, + weekday: ICalendarWeekday::Tuesday, + }, + ICalendarDay { + ordwk: None, + weekday: ICalendarWeekday::Wednesday, + }, + ICalendarDay { + ordwk: None, + weekday: ICalendarWeekday::Thursday, + }, + ICalendarDay { + ordwk: None, + weekday: ICalendarWeekday::Friday, + }, + ], + bysetpos: vec![-2], + ..Default::default() + }, + ), + ( + "FREQ=HOURLY;INTERVAL=3;UNTIL=19970902T170000Z", + ICalendarRecurrenceRule { + freq: ICalendarFrequency::Hourly, + until: Some(PartialDateTime { + year: Some(1997), + month: Some(9), + day: Some(2), + hour: Some(17), + minute: Some(0), + second: Some(0), + tz_hour: Some(0), + tz_minute: Some(0), + tz_minus: false, + }), + interval: Some(3), + ..Default::default() + }, + ), + ( + "FREQ=MINUTELY;INTERVAL=15;COUNT=6", + ICalendarRecurrenceRule { + freq: ICalendarFrequency::Minutely, + count: Some(6), + interval: Some(15), + ..Default::default() + }, + ), + ( + "FREQ=MINUTELY;INTERVAL=90;COUNT=4", + ICalendarRecurrenceRule { + freq: ICalendarFrequency::Minutely, + count: Some(4), + interval: Some(90), + ..Default::default() + }, + ), + ( + "FREQ=DAILY;BYHOUR=9,10,11,12,13,14,15,16;BYMINUTE=0,20,40", + ICalendarRecurrenceRule { + freq: ICalendarFrequency::Daily, + byminute: vec![0, 20, 40], + byhour: vec![9, 10, 11, 12, 13, 14, 15, 16], + ..Default::default() + }, + ), + ( + "FREQ=MINUTELY;INTERVAL=20;BYHOUR=9,10,11,12,13,14,15,16", + ICalendarRecurrenceRule { + freq: ICalendarFrequency::Minutely, + interval: Some(20), + byhour: vec![9, 10, 11, 12, 13, 14, 15, 16], + ..Default::default() + }, + ), + ( + "FREQ=WEEKLY;INTERVAL=2;COUNT=4;BYDAY=TU,SU;WKST=MO", + ICalendarRecurrenceRule { + freq: ICalendarFrequency::Weekly, + count: Some(4), + interval: Some(2), + byday: vec![ + ICalendarDay { + ordwk: None, + weekday: ICalendarWeekday::Tuesday, + }, + ICalendarDay { + ordwk: None, + weekday: ICalendarWeekday::Sunday, + }, + ], + wkst: Some(ICalendarWeekday::Monday), + ..Default::default() + }, + ), + ( + "FREQ=MONTHLY;BYMONTHDAY=15,30;COUNT=5", + ICalendarRecurrenceRule { + freq: ICalendarFrequency::Monthly, + count: Some(5), + bymonthday: vec![15, 30], + ..Default::default() + }, + ), + ] { + assert_eq!( + Parser::new(rule).strict().rrule().unwrap(), + expected, + "failed for {rule}" + ); + } + } + + #[test] + fn test_parse_period() { + for (rule, expected) in [ + ( + "19970308T160000Z/PT8H30M", + ICalendarPeriod::Duration { + start: PartialDateTime { + year: Some(1997), + month: Some(3), + day: Some(8), + hour: Some(16), + minute: Some(0), + second: Some(0), + tz_hour: Some(0), + tz_minute: Some(0), + tz_minus: false, + }, + duration: ICalendarDuration { + neg: false, + weeks: 0, + days: 0, + hours: 8, + minutes: 30, + seconds: 0, + }, + }, + ), + ( + "19970308T160000/PT3H", + ICalendarPeriod::Duration { + start: PartialDateTime { + year: Some(1997), + month: Some(3), + day: Some(8), + hour: Some(16), + minute: Some(0), + second: Some(0), + tz_hour: None, + tz_minute: None, + tz_minus: false, + }, + duration: ICalendarDuration { + neg: false, + weeks: 0, + days: 0, + hours: 3, + minutes: 0, + seconds: 0, + }, + }, + ), + ( + "19970308T200000Z/PT1H", + ICalendarPeriod::Duration { + start: PartialDateTime { + year: Some(1997), + month: Some(3), + day: Some(8), + hour: Some(20), + minute: Some(0), + second: Some(0), + tz_hour: Some(0), + tz_minute: Some(0), + tz_minus: false, + }, + duration: ICalendarDuration { + neg: false, + weeks: 0, + days: 0, + hours: 1, + minutes: 0, + seconds: 0, + }, + }, + ), + ( + "19970308T230000Z/19970309T000000Z", + ICalendarPeriod::Range { + start: PartialDateTime { + year: Some(1997), + month: Some(3), + day: Some(8), + hour: Some(23), + minute: Some(0), + second: Some(0), + tz_hour: Some(0), + tz_minute: Some(0), + tz_minus: false, + }, + end: PartialDateTime { + year: Some(1997), + month: Some(3), + day: Some(9), + hour: Some(0), + minute: Some(0), + second: Some(0), + tz_hour: Some(0), + tz_minute: Some(0), + tz_minus: false, + }, + }, + ), + ( + "19970101T180000Z/PT5H30M", + ICalendarPeriod::Duration { + start: PartialDateTime { + year: Some(1997), + month: Some(1), + day: Some(1), + hour: Some(18), + minute: Some(0), + second: Some(0), + tz_hour: Some(0), + tz_minute: Some(0), + tz_minus: false, + }, + duration: ICalendarDuration { + neg: false, + weeks: 0, + days: 0, + hours: 5, + minutes: 30, + seconds: 0, + }, + }, + ), + ( + "19971015T050000Z/PT8H30M", + ICalendarPeriod::Duration { + start: PartialDateTime { + year: Some(1997), + month: Some(10), + day: Some(15), + hour: Some(5), + minute: Some(0), + second: Some(0), + tz_hour: Some(0), + tz_minute: Some(0), + tz_minus: false, + }, + duration: ICalendarDuration { + neg: false, + weeks: 0, + days: 0, + hours: 8, + minutes: 30, + seconds: 0, + }, + }, + ), + ( + "19980314T233000Z/19980315T003000Z", + ICalendarPeriod::Range { + start: PartialDateTime { + year: Some(1998), + month: Some(3), + day: Some(14), + hour: Some(23), + minute: Some(30), + second: Some(0), + tz_hour: Some(0), + tz_minute: Some(0), + tz_minus: false, + }, + end: PartialDateTime { + year: Some(1998), + month: Some(3), + day: Some(15), + hour: Some(0), + minute: Some(30), + second: Some(0), + tz_hour: Some(0), + tz_minute: Some(0), + tz_minus: false, + }, + }, + ), + ] { + let mut parser = Parser::new(rule); + let token = parser.token().unwrap(); + + assert_eq!( + ICalendarPeriod::parse(token.text.as_ref()).unwrap(), + expected + ); + } + } + + #[test] + fn test_parse_duration() { + for (rule, expected) in [ + ( + "P1W3DT4H5M6S", + ICalendarDuration { + neg: false, + weeks: 1, + days: 3, + hours: 4, + minutes: 5, + seconds: 6, + }, + ), + ( + "P15DT5H0M20S", + ICalendarDuration { + neg: false, + weeks: 0, + days: 15, + hours: 5, + minutes: 0, + seconds: 20, + }, + ), + ( + "P7W", + ICalendarDuration { + neg: false, + weeks: 7, + days: 0, + hours: 0, + minutes: 0, + seconds: 0, + }, + ), + ( + "-P7W", + ICalendarDuration { + neg: true, + weeks: 7, + days: 0, + hours: 0, + minutes: 0, + seconds: 0, + }, + ), + ( + "PT0S", + ICalendarDuration { + neg: false, + weeks: 0, + days: 0, + hours: 0, + minutes: 0, + seconds: 0, + }, + ), + ] { + let mut parser = Parser::new(rule); + let token = parser.token().unwrap(); + + assert_eq!( + ICalendarDuration::parse(token.text.as_ref()).expect(rule), + expected, + "Failed to parse: {rule}", + ); + } + + assert!(ICalendarDuration::parse(b"P").is_none()); + + // Duration zero + let zero = ICalendarDuration::default(); + assert_eq!(zero.to_string(), "PT0S"); + + let reparsed = ICalendarDuration::parse(zero.to_string().as_bytes()).unwrap(); + assert_eq!(reparsed, zero); + } +} diff --git a/vendor/calcard/src/icalendar/rkyv_timezone.rs b/vendor/calcard/src/icalendar/rkyv_timezone.rs new file mode 100644 index 00000000..62f78e1a --- /dev/null +++ b/vendor/calcard/src/icalendar/rkyv_timezone.rs @@ -0,0 +1,69 @@ +/* + * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC + * + * SPDX-License-Identifier: Apache-2.0 OR MIT + */ + +use super::{ + ArchivedICalendar, ArchivedICalendarComponent, ArchivedICalendarComponentType, + ArchivedICalendarEntry, ArchivedICalendarProperty, ArchivedICalendarValue, +}; +use crate::{common::timezone::Tz, icalendar::ICalendarParameterName}; +use std::str::FromStr; + +impl ArchivedICalendar { + pub fn timezones(&self) -> impl Iterator { + self.components.iter().filter(|comp| { + matches!( + comp.component_type, + ArchivedICalendarComponentType::VTimezone + ) + }) + } + + pub fn is_timezone(&self) -> bool { + self.timezones().count() == 1 + } +} + +impl ArchivedICalendarComponent { + pub fn timezone(&self) -> Option<(&str, Tz)> { + let mut tz_name = None; + let mut tz_id = None; + let mut tz_lic = None; + let mut tz_cdo_id = None; + + for entry in self.entries.iter() { + match (&entry.name, entry.values.first()) { + (ArchivedICalendarProperty::Tzid, Some(ArchivedICalendarValue::Text(id))) => { + tz_id = Tz::from_str(id.as_str()).ok(); + tz_name = Some(id.as_str()); + } + ( + ArchivedICalendarProperty::Other(value), + Some(ArchivedICalendarValue::Text(id)), + ) => { + hashify::fnc_map!(value.as_bytes(), + "X-LIC-LOCATION" => { + tz_lic = Tz::from_str(id.strip_prefix("SystemV/").unwrap_or(id.as_str())).ok(); + }, + "X-MICROSOFT-CDO-TZID" => { + tz_cdo_id = Tz::from_ms_cdo_zone_id(id); + }, + _ => {} + ); + } + _ => (), + } + } + + tz_name.zip(tz_id.or(tz_lic).or(tz_cdo_id)) + } +} + +impl ArchivedICalendarEntry { + pub fn tz_id(&self) -> Option<&str> { + self.parameters(&ICalendarParameterName::Tzid) + .find_map(|param| param.as_text()) + } +} diff --git a/vendor/calcard/src/icalendar/rkyv_types.rs b/vendor/calcard/src/icalendar/rkyv_types.rs new file mode 100644 index 00000000..c3bdb069 --- /dev/null +++ b/vendor/calcard/src/icalendar/rkyv_types.rs @@ -0,0 +1,871 @@ +/* + * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC + * + * SPDX-License-Identifier: Apache-2.0 OR MIT + */ + +use crate::common::ArchivedLinkRelation; + +use super::*; + +impl ArchivedICalendarProperty { + // Returns the default value type and whether the property is multi-valued. + pub(crate) fn default_types(&self) -> (ArchivedValueType, ValueSeparator) { + match self { + ArchivedICalendarProperty::Calscale => { + (ArchivedValueType::CalendarScale, ValueSeparator::None) + } + ArchivedICalendarProperty::Method => (ArchivedValueType::Method, ValueSeparator::None), + ArchivedICalendarProperty::Prodid => ( + ArchivedValueType::Ical(ArchivedICalendarValueType::Text), + ValueSeparator::None, + ), + ArchivedICalendarProperty::Version => ( + ArchivedValueType::Ical(ArchivedICalendarValueType::Text), + ValueSeparator::None, + ), + ArchivedICalendarProperty::Attach => ( + ArchivedValueType::Ical(ArchivedICalendarValueType::Uri), + ValueSeparator::None, + ), + ArchivedICalendarProperty::Categories => ( + ArchivedValueType::Ical(ArchivedICalendarValueType::Text), + ValueSeparator::Comma, + ), + ArchivedICalendarProperty::Class => { + (ArchivedValueType::Classification, ValueSeparator::None) + } + ArchivedICalendarProperty::Comment => ( + ArchivedValueType::Ical(ArchivedICalendarValueType::Text), + ValueSeparator::None, + ), + ArchivedICalendarProperty::Description => ( + ArchivedValueType::Ical(ArchivedICalendarValueType::Text), + ValueSeparator::None, + ), + ArchivedICalendarProperty::Geo => ( + ArchivedValueType::Ical(ArchivedICalendarValueType::Float), + ValueSeparator::Semicolon, + ), + ArchivedICalendarProperty::Location => ( + ArchivedValueType::Ical(ArchivedICalendarValueType::Text), + ValueSeparator::None, + ), + ArchivedICalendarProperty::PercentComplete => ( + ArchivedValueType::Ical(ArchivedICalendarValueType::Integer), + ValueSeparator::None, + ), + ArchivedICalendarProperty::Priority => ( + ArchivedValueType::Ical(ArchivedICalendarValueType::Integer), + ValueSeparator::None, + ), + ArchivedICalendarProperty::Resources => ( + ArchivedValueType::Ical(ArchivedICalendarValueType::Text), + ValueSeparator::Comma, + ), + ArchivedICalendarProperty::Status => (ArchivedValueType::Status, ValueSeparator::None), + ArchivedICalendarProperty::Summary => ( + ArchivedValueType::Ical(ArchivedICalendarValueType::Text), + ValueSeparator::None, + ), + ArchivedICalendarProperty::Completed => ( + ArchivedValueType::Ical(ArchivedICalendarValueType::DateTime), + ValueSeparator::None, + ), + ArchivedICalendarProperty::Dtend => ( + ArchivedValueType::Ical(ArchivedICalendarValueType::DateTime), + ValueSeparator::None, + ), + ArchivedICalendarProperty::Due => ( + ArchivedValueType::Ical(ArchivedICalendarValueType::DateTime), + ValueSeparator::None, + ), + ArchivedICalendarProperty::Dtstart => ( + ArchivedValueType::Ical(ArchivedICalendarValueType::DateTime), + ValueSeparator::None, + ), + ArchivedICalendarProperty::Duration => ( + ArchivedValueType::Ical(ArchivedICalendarValueType::Duration), + ValueSeparator::None, + ), + ArchivedICalendarProperty::Freebusy => ( + ArchivedValueType::Ical(ArchivedICalendarValueType::Period), + ValueSeparator::Comma, + ), + ArchivedICalendarProperty::Transp => { + (ArchivedValueType::Transparency, ValueSeparator::None) + } + ArchivedICalendarProperty::Tzid => ( + ArchivedValueType::Ical(ArchivedICalendarValueType::Text), + ValueSeparator::None, + ), + ArchivedICalendarProperty::Tzname => ( + ArchivedValueType::Ical(ArchivedICalendarValueType::Text), + ValueSeparator::None, + ), + ArchivedICalendarProperty::Tzoffsetfrom => ( + ArchivedValueType::Ical(ArchivedICalendarValueType::UtcOffset), + ValueSeparator::None, + ), + ArchivedICalendarProperty::Tzoffsetto => ( + ArchivedValueType::Ical(ArchivedICalendarValueType::UtcOffset), + ValueSeparator::None, + ), + ArchivedICalendarProperty::Tzurl => ( + ArchivedValueType::Ical(ArchivedICalendarValueType::Uri), + ValueSeparator::None, + ), + ArchivedICalendarProperty::Attendee => ( + ArchivedValueType::Ical(ArchivedICalendarValueType::Uri), + ValueSeparator::None, + ), + ArchivedICalendarProperty::Contact => ( + ArchivedValueType::Ical(ArchivedICalendarValueType::Text), + ValueSeparator::None, + ), + ArchivedICalendarProperty::Organizer => ( + ArchivedValueType::Ical(ArchivedICalendarValueType::Uri), + ValueSeparator::None, + ), + ArchivedICalendarProperty::RecurrenceId => ( + ArchivedValueType::Ical(ArchivedICalendarValueType::DateTime), + ValueSeparator::None, + ), + ArchivedICalendarProperty::RelatedTo => ( + ArchivedValueType::Ical(ArchivedICalendarValueType::Text), + ValueSeparator::None, + ), + ArchivedICalendarProperty::Url => ( + ArchivedValueType::Ical(ArchivedICalendarValueType::Uri), + ValueSeparator::None, + ), + ArchivedICalendarProperty::Uid => ( + ArchivedValueType::Ical(ArchivedICalendarValueType::Text), + ValueSeparator::None, + ), + ArchivedICalendarProperty::Exdate => ( + ArchivedValueType::Ical(ArchivedICalendarValueType::DateTime), + ValueSeparator::Comma, + ), + ArchivedICalendarProperty::Exrule => ( + ArchivedValueType::Ical(ArchivedICalendarValueType::Recur), + ValueSeparator::None, + ), + ArchivedICalendarProperty::Rdate => ( + ArchivedValueType::Ical(ArchivedICalendarValueType::DateTime), + ValueSeparator::Comma, + ), + ArchivedICalendarProperty::Rrule => ( + ArchivedValueType::Ical(ArchivedICalendarValueType::Recur), + ValueSeparator::None, + ), + ArchivedICalendarProperty::Action => (ArchivedValueType::Action, ValueSeparator::None), + ArchivedICalendarProperty::Repeat => ( + ArchivedValueType::Ical(ArchivedICalendarValueType::Integer), + ValueSeparator::None, + ), + ArchivedICalendarProperty::Trigger => ( + ArchivedValueType::Ical(ArchivedICalendarValueType::Duration), + ValueSeparator::None, + ), + ArchivedICalendarProperty::Created => ( + ArchivedValueType::Ical(ArchivedICalendarValueType::DateTime), + ValueSeparator::None, + ), + ArchivedICalendarProperty::Dtstamp => ( + ArchivedValueType::Ical(ArchivedICalendarValueType::DateTime), + ValueSeparator::None, + ), + ArchivedICalendarProperty::LastModified => ( + ArchivedValueType::Ical(ArchivedICalendarValueType::DateTime), + ValueSeparator::None, + ), + ArchivedICalendarProperty::Sequence => ( + ArchivedValueType::Ical(ArchivedICalendarValueType::Integer), + ValueSeparator::None, + ), + ArchivedICalendarProperty::RequestStatus => ( + ArchivedValueType::Ical(ArchivedICalendarValueType::Text), + ValueSeparator::Semicolon, + ), + ArchivedICalendarProperty::Xml => ( + ArchivedValueType::Ical(ArchivedICalendarValueType::Text), + ValueSeparator::None, + ), + ArchivedICalendarProperty::Tzuntil => ( + ArchivedValueType::Ical(ArchivedICalendarValueType::DateTime), + ValueSeparator::None, + ), + ArchivedICalendarProperty::TzidAliasOf => ( + ArchivedValueType::Ical(ArchivedICalendarValueType::Text), + ValueSeparator::None, + ), + ArchivedICalendarProperty::Busytype => { + (ArchivedValueType::BusyType, ValueSeparator::None) + } + ArchivedICalendarProperty::Name => ( + ArchivedValueType::Ical(ArchivedICalendarValueType::Text), + ValueSeparator::None, + ), + ArchivedICalendarProperty::RefreshInterval => ( + ArchivedValueType::Ical(ArchivedICalendarValueType::Duration), + ValueSeparator::None, + ), + ArchivedICalendarProperty::Source => ( + ArchivedValueType::Ical(ArchivedICalendarValueType::Uri), + ValueSeparator::None, + ), + ArchivedICalendarProperty::Color => ( + ArchivedValueType::Ical(ArchivedICalendarValueType::Text), + ValueSeparator::None, + ), + ArchivedICalendarProperty::Image => ( + ArchivedValueType::Ical(ArchivedICalendarValueType::Uri), + ValueSeparator::None, + ), + ArchivedICalendarProperty::Conference => ( + ArchivedValueType::Ical(ArchivedICalendarValueType::Uri), + ValueSeparator::None, + ), + ArchivedICalendarProperty::CalendarAddress => ( + ArchivedValueType::Ical(ArchivedICalendarValueType::Uri), + ValueSeparator::None, + ), + ArchivedICalendarProperty::LocationType => ( + ArchivedValueType::Ical(ArchivedICalendarValueType::Text), + ValueSeparator::Comma, + ), + ArchivedICalendarProperty::ParticipantType => { + (ArchivedValueType::ParticipantType, ValueSeparator::None) + } + ArchivedICalendarProperty::ResourceType => { + (ArchivedValueType::ResourceType, ValueSeparator::None) + } + ArchivedICalendarProperty::StructuredData => ( + ArchivedValueType::Ical(ArchivedICalendarValueType::Text), + ValueSeparator::None, + ), + ArchivedICalendarProperty::StyledDescription => ( + ArchivedValueType::Ical(ArchivedICalendarValueType::Text), + ValueSeparator::None, + ), + ArchivedICalendarProperty::Acknowledged => ( + ArchivedValueType::Ical(ArchivedICalendarValueType::DateTime), + ValueSeparator::None, + ), + ArchivedICalendarProperty::Proximity => { + (ArchivedValueType::Proximity, ValueSeparator::None) + } + ArchivedICalendarProperty::Concept => ( + ArchivedValueType::Ical(ArchivedICalendarValueType::Uri), + ValueSeparator::None, + ), + ArchivedICalendarProperty::Link => ( + ArchivedValueType::Ical(ArchivedICalendarValueType::Uri), + ValueSeparator::None, + ), + ArchivedICalendarProperty::Refid => ( + ArchivedValueType::Ical(ArchivedICalendarValueType::Text), + ValueSeparator::None, + ), + ArchivedICalendarProperty::Begin => ( + ArchivedValueType::Ical(ArchivedICalendarValueType::Text), + ValueSeparator::None, + ), + ArchivedICalendarProperty::End => ( + ArchivedValueType::Ical(ArchivedICalendarValueType::Text), + ValueSeparator::None, + ), + ArchivedICalendarProperty::Other(_) => ( + ArchivedValueType::Ical(ArchivedICalendarValueType::Text), + ValueSeparator::Semicolon, + ), + ArchivedICalendarProperty::Coordinates => ( + ArchivedValueType::Ical(ArchivedICalendarValueType::Uri), + ValueSeparator::None, + ), + ArchivedICalendarProperty::ShowWithoutTime => ( + ArchivedValueType::Ical(ArchivedICalendarValueType::Boolean), + ValueSeparator::None, + ), + ArchivedICalendarProperty::Jsid | ArchivedICalendarProperty::Jsprop => ( + ArchivedValueType::Ical(ArchivedICalendarValueType::Text), + ValueSeparator::None, + ), + ArchivedICalendarProperty::EstimatedDuration => ( + ArchivedValueType::Ical(ArchivedICalendarValueType::Duration), + ValueSeparator::None, + ), + ArchivedICalendarProperty::Reason => ( + ArchivedValueType::Ical(ArchivedICalendarValueType::Uri), + ValueSeparator::None, + ), + ArchivedICalendarProperty::Substate => ( + ArchivedValueType::Ical(ArchivedICalendarValueType::Text), + ValueSeparator::None, + ), + ArchivedICalendarProperty::TaskMode => ( + ArchivedValueType::Ical(ArchivedICalendarValueType::Text), + ValueSeparator::None, + ), + } + } +} + +impl IanaString for ArchivedICalendarFrequency { + fn as_str(&self) -> &'static str { + match self { + ArchivedICalendarFrequency::Secondly => "SECONDLY", + ArchivedICalendarFrequency::Minutely => "MINUTELY", + ArchivedICalendarFrequency::Hourly => "HOURLY", + ArchivedICalendarFrequency::Daily => "DAILY", + ArchivedICalendarFrequency::Weekly => "WEEKLY", + ArchivedICalendarFrequency::Monthly => "MONTHLY", + ArchivedICalendarFrequency::Yearly => "YEARLY", + } + } +} + +impl ArchivedICalendarSkip { + pub fn as_str(&self) -> &'static str { + match self { + ArchivedICalendarSkip::Omit => "OMIT", + ArchivedICalendarSkip::Backward => "BACKWARD", + ArchivedICalendarSkip::Forward => "FORWARD", + } + } +} + +impl ArchivedICalendarWeekday { + pub fn as_str(&self) -> &'static str { + match self { + ArchivedICalendarWeekday::Sunday => "SU", + ArchivedICalendarWeekday::Monday => "MO", + ArchivedICalendarWeekday::Tuesday => "TU", + ArchivedICalendarWeekday::Wednesday => "WE", + ArchivedICalendarWeekday::Thursday => "TH", + ArchivedICalendarWeekday::Friday => "FR", + ArchivedICalendarWeekday::Saturday => "SA", + } + } +} + +impl ArchivedICalendarAction { + pub fn as_str(&self) -> &'static str { + match self { + ArchivedICalendarAction::Audio => "AUDIO", + ArchivedICalendarAction::Display => "DISPLAY", + ArchivedICalendarAction::Email => "EMAIL", + ArchivedICalendarAction::Procedure => "PROCEDURE", + } + } +} + +impl ArchivedICalendarUserTypes { + pub fn as_str(&self) -> &'static str { + match self { + ArchivedICalendarUserTypes::Individual => "INDIVIDUAL", + ArchivedICalendarUserTypes::Group => "GROUP", + ArchivedICalendarUserTypes::Resource => "RESOURCE", + ArchivedICalendarUserTypes::Room => "ROOM", + ArchivedICalendarUserTypes::Unknown => "UNKNOWN", + } + } +} + +impl ArchivedICalendarClassification { + pub fn as_str(&self) -> &'static str { + match self { + ArchivedICalendarClassification::Public => "PUBLIC", + ArchivedICalendarClassification::Private => "PRIVATE", + ArchivedICalendarClassification::Confidential => "CONFIDENTIAL", + } + } +} + +impl ArchivedICalendarComponentType { + pub fn as_str(&self) -> &str { + match self { + ArchivedICalendarComponentType::VCalendar => "VCALENDAR", + ArchivedICalendarComponentType::VEvent => "VEVENT", + ArchivedICalendarComponentType::VTodo => "VTODO", + ArchivedICalendarComponentType::VJournal => "VJOURNAL", + ArchivedICalendarComponentType::VFreebusy => "VFREEBUSY", + ArchivedICalendarComponentType::VTimezone => "VTIMEZONE", + ArchivedICalendarComponentType::VAlarm => "VALARM", + ArchivedICalendarComponentType::Standard => "STANDARD", + ArchivedICalendarComponentType::Daylight => "DAYLIGHT", + ArchivedICalendarComponentType::VAvailability => "VAVAILABILITY", + ArchivedICalendarComponentType::Available => "AVAILABLE", + ArchivedICalendarComponentType::Participant => "PARTICIPANT", + ArchivedICalendarComponentType::VLocation => "VLOCATION", + ArchivedICalendarComponentType::VResource => "VRESOURCE", + ArchivedICalendarComponentType::VStatus => "VSTATUS", + ArchivedICalendarComponentType::Other(name) => name.as_str(), + } + } +} + +impl ArchivedICalendarDisplayType { + pub fn as_str(&self) -> &'static str { + match self { + ArchivedICalendarDisplayType::Badge => "BADGE", + ArchivedICalendarDisplayType::Graphic => "GRAPHIC", + ArchivedICalendarDisplayType::Fullsize => "FULLSIZE", + ArchivedICalendarDisplayType::Thumbnail => "THUMBNAIL", + } + } +} + +impl ArchivedICalendarFeatureType { + pub fn as_str(&self) -> &'static str { + match self { + ArchivedICalendarFeatureType::Audio => "AUDIO", + ArchivedICalendarFeatureType::Chat => "CHAT", + ArchivedICalendarFeatureType::Feed => "FEED", + ArchivedICalendarFeatureType::Moderator => "MODERATOR", + ArchivedICalendarFeatureType::Phone => "PHONE", + ArchivedICalendarFeatureType::Screen => "SCREEN", + ArchivedICalendarFeatureType::Video => "VIDEO", + } + } +} + +impl ArchivedICalendarFreeBusyType { + pub fn as_str(&self) -> &'static str { + match self { + ArchivedICalendarFreeBusyType::Free => "FREE", + ArchivedICalendarFreeBusyType::Busy => "BUSY", + ArchivedICalendarFreeBusyType::BusyUnavailable => "BUSY-UNAVAILABLE", + ArchivedICalendarFreeBusyType::BusyTentative => "BUSY-TENTATIVE", + } + } +} + +impl ArchivedICalendarMethod { + pub fn as_str(&self) -> &'static str { + match self { + ArchivedICalendarMethod::Publish => "PUBLISH", + ArchivedICalendarMethod::Request => "REQUEST", + ArchivedICalendarMethod::Reply => "REPLY", + ArchivedICalendarMethod::Add => "ADD", + ArchivedICalendarMethod::Cancel => "CANCEL", + ArchivedICalendarMethod::Refresh => "REFRESH", + ArchivedICalendarMethod::Counter => "COUNTER", + ArchivedICalendarMethod::Declinecounter => "DECLINECOUNTER", + } + } +} + +impl ArchivedICalendarParticipantType { + pub fn as_str(&self) -> &'static str { + match self { + ArchivedICalendarParticipantType::Active => "ACTIVE", + ArchivedICalendarParticipantType::Inactive => "INACTIVE", + ArchivedICalendarParticipantType::Sponsor => "SPONSOR", + ArchivedICalendarParticipantType::Contact => "CONTACT", + ArchivedICalendarParticipantType::BookingContact => "BOOKING-CONTACT", + ArchivedICalendarParticipantType::EmergencyContact => "EMERGENCY-CONTACT", + ArchivedICalendarParticipantType::PublicityContact => "PUBLICITY-CONTACT", + ArchivedICalendarParticipantType::PlannerContact => "PLANNER-CONTACT", + ArchivedICalendarParticipantType::Performer => "PERFORMER", + ArchivedICalendarParticipantType::Speaker => "SPEAKER", + } + } +} + +impl ArchivedICalendarParticipationRole { + pub fn as_str(&self) -> &'static str { + match self { + ArchivedICalendarParticipationRole::Chair => "CHAIR", + ArchivedICalendarParticipationRole::ReqParticipant => "REQ-PARTICIPANT", + ArchivedICalendarParticipationRole::OptParticipant => "OPT-PARTICIPANT", + ArchivedICalendarParticipationRole::NonParticipant => "NON-PARTICIPANT", + ArchivedICalendarParticipationRole::Owner => "OWNER", + } + } +} + +impl ArchivedICalendarParticipationStatus { + pub fn as_str(&self) -> &'static str { + match self { + ArchivedICalendarParticipationStatus::NeedsAction => "NEEDS-ACTION", + ArchivedICalendarParticipationStatus::Accepted => "ACCEPTED", + ArchivedICalendarParticipationStatus::Declined => "DECLINED", + ArchivedICalendarParticipationStatus::Tentative => "TENTATIVE", + ArchivedICalendarParticipationStatus::Delegated => "DELEGATED", + ArchivedICalendarParticipationStatus::Completed => "COMPLETED", + ArchivedICalendarParticipationStatus::InProcess => "IN-PROCESS", + ArchivedICalendarParticipationStatus::Failed => "FAILED", + } + } +} + +impl ArchivedICalendarStatus { + pub fn as_str(&self) -> &'static str { + match self { + ArchivedICalendarStatus::Tentative => "TENTATIVE", + ArchivedICalendarStatus::Confirmed => "CONFIRMED", + ArchivedICalendarStatus::Cancelled => "CANCELLED", + ArchivedICalendarStatus::NeedsAction => "NEEDS-ACTION", + ArchivedICalendarStatus::Completed => "COMPLETED", + ArchivedICalendarStatus::InProcess => "IN-PROCESS", + ArchivedICalendarStatus::Draft => "DRAFT", + ArchivedICalendarStatus::Final => "FINAL", + ArchivedICalendarStatus::Failed => "FAILED", + ArchivedICalendarStatus::Pending => "PENDING", + } + } +} + +impl ArchivedICalendarProperty { + pub fn as_str(&self) -> &str { + match self { + ArchivedICalendarProperty::Calscale => "CALSCALE", + ArchivedICalendarProperty::Method => "METHOD", + ArchivedICalendarProperty::Prodid => "PRODID", + ArchivedICalendarProperty::Version => "VERSION", + ArchivedICalendarProperty::Attach => "ATTACH", + ArchivedICalendarProperty::Categories => "CATEGORIES", + ArchivedICalendarProperty::Class => "CLASS", + ArchivedICalendarProperty::Comment => "COMMENT", + ArchivedICalendarProperty::Description => "DESCRIPTION", + ArchivedICalendarProperty::Geo => "GEO", + ArchivedICalendarProperty::Location => "LOCATION", + ArchivedICalendarProperty::PercentComplete => "PERCENT-COMPLETE", + ArchivedICalendarProperty::Priority => "PRIORITY", + ArchivedICalendarProperty::Resources => "RESOURCES", + ArchivedICalendarProperty::Status => "STATUS", + ArchivedICalendarProperty::Summary => "SUMMARY", + ArchivedICalendarProperty::Completed => "COMPLETED", + ArchivedICalendarProperty::Dtend => "DTEND", + ArchivedICalendarProperty::Due => "DUE", + ArchivedICalendarProperty::Dtstart => "DTSTART", + ArchivedICalendarProperty::Duration => "DURATION", + ArchivedICalendarProperty::Freebusy => "FREEBUSY", + ArchivedICalendarProperty::Transp => "TRANSP", + ArchivedICalendarProperty::Tzid => "TZID", + ArchivedICalendarProperty::Tzname => "TZNAME", + ArchivedICalendarProperty::Tzoffsetfrom => "TZOFFSETFROM", + ArchivedICalendarProperty::Tzoffsetto => "TZOFFSETTO", + ArchivedICalendarProperty::Tzurl => "TZURL", + ArchivedICalendarProperty::Attendee => "ATTENDEE", + ArchivedICalendarProperty::Contact => "CONTACT", + ArchivedICalendarProperty::Organizer => "ORGANIZER", + ArchivedICalendarProperty::RecurrenceId => "RECURRENCE-ID", + ArchivedICalendarProperty::RelatedTo => "RELATED-TO", + ArchivedICalendarProperty::Url => "URL", + ArchivedICalendarProperty::Uid => "UID", + ArchivedICalendarProperty::Exdate => "EXDATE", + ArchivedICalendarProperty::Exrule => "EXRULE", + ArchivedICalendarProperty::Rdate => "RDATE", + ArchivedICalendarProperty::Rrule => "RRULE", + ArchivedICalendarProperty::Action => "ACTION", + ArchivedICalendarProperty::Repeat => "REPEAT", + ArchivedICalendarProperty::Trigger => "TRIGGER", + ArchivedICalendarProperty::Created => "CREATED", + ArchivedICalendarProperty::Dtstamp => "DTSTAMP", + ArchivedICalendarProperty::LastModified => "LAST-MODIFIED", + ArchivedICalendarProperty::Sequence => "SEQUENCE", + ArchivedICalendarProperty::RequestStatus => "REQUEST-STATUS", + ArchivedICalendarProperty::Xml => "XML", + ArchivedICalendarProperty::Tzuntil => "TZUNTIL", + ArchivedICalendarProperty::TzidAliasOf => "TZID-ALIAS-OF", + ArchivedICalendarProperty::Busytype => "BUSYTYPE", + ArchivedICalendarProperty::Name => "NAME", + ArchivedICalendarProperty::RefreshInterval => "REFRESH-INTERVAL", + ArchivedICalendarProperty::Source => "SOURCE", + ArchivedICalendarProperty::Color => "COLOR", + ArchivedICalendarProperty::Image => "IMAGE", + ArchivedICalendarProperty::Conference => "CONFERENCE", + ArchivedICalendarProperty::CalendarAddress => "CALENDAR-ADDRESS", + ArchivedICalendarProperty::LocationType => "LOCATION-TYPE", + ArchivedICalendarProperty::ParticipantType => "PARTICIPANT-TYPE", + ArchivedICalendarProperty::ResourceType => "RESOURCE-TYPE", + ArchivedICalendarProperty::StructuredData => "STRUCTURED-DATA", + ArchivedICalendarProperty::StyledDescription => "STYLED-DESCRIPTION", + ArchivedICalendarProperty::Acknowledged => "ACKNOWLEDGED", + ArchivedICalendarProperty::Proximity => "PROXIMITY", + ArchivedICalendarProperty::Concept => "CONCEPT", + ArchivedICalendarProperty::Link => "LINK", + ArchivedICalendarProperty::Refid => "REFID", + ArchivedICalendarProperty::Begin => "BEGIN", + ArchivedICalendarProperty::End => "END", + ArchivedICalendarProperty::Coordinates => "COORDINATES", + ArchivedICalendarProperty::ShowWithoutTime => "SHOW-WITHOUT-TIME", + ArchivedICalendarProperty::Jsid => "JSID", + ArchivedICalendarProperty::Jsprop => "JSPROP", + ArchivedICalendarProperty::EstimatedDuration => "ESTIMATED-DURATION", + ArchivedICalendarProperty::Reason => "REASON", + ArchivedICalendarProperty::Substate => "SUBSTATE", + ArchivedICalendarProperty::TaskMode => "TASK-MODE", + ArchivedICalendarProperty::Other(value) => value.as_str(), + } + } +} + +impl ArchivedICalendarProximityValue { + pub fn as_str(&self) -> &'static str { + match self { + ArchivedICalendarProximityValue::Arrive => "ARRIVE", + ArchivedICalendarProximityValue::Depart => "DEPART", + ArchivedICalendarProximityValue::Connect => "CONNECT", + ArchivedICalendarProximityValue::Disconnect => "DISCONNECT", + } + } +} + +impl ArchivedICalendarRelationshipType { + pub fn as_str(&self) -> &'static str { + match self { + ArchivedICalendarRelationshipType::Child => "CHILD", + ArchivedICalendarRelationshipType::Parent => "PARENT", + ArchivedICalendarRelationshipType::Sibling => "SIBLING", + ArchivedICalendarRelationshipType::Snooze => "SNOOZE", + ArchivedICalendarRelationshipType::Concept => "CONCEPT", + ArchivedICalendarRelationshipType::DependsOn => "DEPENDS-ON", + ArchivedICalendarRelationshipType::Finishtofinish => "FINISHTOFINISH", + ArchivedICalendarRelationshipType::Finishtostart => "FINISHTOSTART", + ArchivedICalendarRelationshipType::First => "FIRST", + ArchivedICalendarRelationshipType::Next => "NEXT", + ArchivedICalendarRelationshipType::Refid => "REFID", + ArchivedICalendarRelationshipType::Starttofinish => "STARTTOFINISH", + ArchivedICalendarRelationshipType::Starttostart => "STARTTOSTART", + } + } +} + +impl ArchivedICalendarResourceType { + pub fn as_str(&self) -> &'static str { + match self { + ArchivedICalendarResourceType::Projector => "PROJECTOR", + ArchivedICalendarResourceType::Room => "ROOM", + ArchivedICalendarResourceType::RemoteConferenceAudio => "REMOTE-CONFERENCE-AUDIO", + ArchivedICalendarResourceType::RemoteConferenceVideo => "REMOTE-CONFERENCE-VIDEO", + } + } +} + +impl ArchivedICalendarScheduleAgentValue { + pub fn as_str(&self) -> &'static str { + match self { + ArchivedICalendarScheduleAgentValue::Server => "SERVER", + ArchivedICalendarScheduleAgentValue::Client => "CLIENT", + ArchivedICalendarScheduleAgentValue::None => "NONE", + } + } +} + +impl ArchivedICalendarScheduleForceSendValue { + pub fn as_str(&self) -> &'static str { + match self { + ArchivedICalendarScheduleForceSendValue::Request => "REQUEST", + ArchivedICalendarScheduleForceSendValue::Reply => "REPLY", + } + } +} + +impl ArchivedICalendarValueType { + pub fn as_str(&self) -> &'static str { + match self { + ArchivedICalendarValueType::Binary => "BINARY", + ArchivedICalendarValueType::Boolean => "BOOLEAN", + ArchivedICalendarValueType::CalAddress => "CAL-ADDRESS", + ArchivedICalendarValueType::Date => "DATE", + ArchivedICalendarValueType::DateTime => "DATE-TIME", + ArchivedICalendarValueType::Duration => "DURATION", + ArchivedICalendarValueType::Float => "FLOAT", + ArchivedICalendarValueType::Integer => "INTEGER", + ArchivedICalendarValueType::Period => "PERIOD", + ArchivedICalendarValueType::Recur => "RECUR", + ArchivedICalendarValueType::Text => "TEXT", + ArchivedICalendarValueType::Time => "TIME", + ArchivedICalendarValueType::Unknown => "UNKNOWN", + ArchivedICalendarValueType::Uri => "URI", + ArchivedICalendarValueType::UtcOffset => "UTC-OFFSET", + ArchivedICalendarValueType::XmlReference => "XML-REFERENCE", + ArchivedICalendarValueType::Uid => "UID", + } + } +} + +impl ArchivedICalendarTransparency { + pub fn as_str(&self) -> &'static str { + match self { + ArchivedICalendarTransparency::Opaque => "OPAQUE", + ArchivedICalendarTransparency::Transparent => "TRANSPARENT", + } + } +} + +impl ArchivedLinkRelation { + pub fn as_str(&self) -> &'static str { + match self { + ArchivedLinkRelation::About => "about", + ArchivedLinkRelation::Acl => "acl", + ArchivedLinkRelation::Alternate => "alternate", + ArchivedLinkRelation::Amphtml => "amphtml", + ArchivedLinkRelation::ApiCatalog => "api-catalog", + ArchivedLinkRelation::Appendix => "appendix", + ArchivedLinkRelation::AppleTouchIcon => "apple-touch-icon", + ArchivedLinkRelation::AppleTouchStartupImage => "apple-touch-startup-image", + ArchivedLinkRelation::Archives => "archives", + ArchivedLinkRelation::Author => "author", + ArchivedLinkRelation::BlockedBy => "blocked-by", + ArchivedLinkRelation::Bookmark => "bookmark", + ArchivedLinkRelation::C2paManifest => "c2pa-manifest", + ArchivedLinkRelation::Canonical => "canonical", + ArchivedLinkRelation::Chapter => "chapter", + ArchivedLinkRelation::CiteAs => "cite-as", + ArchivedLinkRelation::Collection => "collection", + ArchivedLinkRelation::CompressionDictionary => "compression-dictionary", + ArchivedLinkRelation::Contents => "contents", + ArchivedLinkRelation::Convertedfrom => "convertedfrom", + ArchivedLinkRelation::Copyright => "copyright", + ArchivedLinkRelation::CreateForm => "create-form", + ArchivedLinkRelation::Current => "current", + ArchivedLinkRelation::Deprecation => "deprecation", + ArchivedLinkRelation::Describedby => "describedby", + ArchivedLinkRelation::Describes => "describes", + ArchivedLinkRelation::Disclosure => "disclosure", + ArchivedLinkRelation::DnsPrefetch => "dns-prefetch", + ArchivedLinkRelation::Duplicate => "duplicate", + ArchivedLinkRelation::Edit => "edit", + ArchivedLinkRelation::EditForm => "edit-form", + ArchivedLinkRelation::EditMedia => "edit-media", + ArchivedLinkRelation::Enclosure => "enclosure", + ArchivedLinkRelation::External => "external", + ArchivedLinkRelation::First => "first", + ArchivedLinkRelation::Geofeed => "geofeed", + ArchivedLinkRelation::Glossary => "glossary", + ArchivedLinkRelation::Help => "help", + ArchivedLinkRelation::Hosts => "hosts", + ArchivedLinkRelation::Hub => "hub", + ArchivedLinkRelation::IceServer => "ice-server", + ArchivedLinkRelation::Icon => "icon", + ArchivedLinkRelation::Index => "index", + ArchivedLinkRelation::Intervalafter => "intervalafter", + ArchivedLinkRelation::Intervalbefore => "intervalbefore", + ArchivedLinkRelation::Intervalcontains => "intervalcontains", + ArchivedLinkRelation::Intervaldisjoint => "intervaldisjoint", + ArchivedLinkRelation::Intervalduring => "intervalduring", + ArchivedLinkRelation::Intervalequals => "intervalequals", + ArchivedLinkRelation::Intervalfinishedby => "intervalfinishedby", + ArchivedLinkRelation::Intervalfinishes => "intervalfinishes", + ArchivedLinkRelation::Intervalin => "intervalin", + ArchivedLinkRelation::Intervalmeets => "intervalmeets", + ArchivedLinkRelation::Intervalmetby => "intervalmetby", + ArchivedLinkRelation::Intervaloverlappedby => "intervaloverlappedby", + ArchivedLinkRelation::Intervaloverlaps => "intervaloverlaps", + ArchivedLinkRelation::Intervalstartedby => "intervalstartedby", + ArchivedLinkRelation::Intervalstarts => "intervalstarts", + ArchivedLinkRelation::Item => "item", + ArchivedLinkRelation::Last => "last", + ArchivedLinkRelation::LatestVersion => "latest-version", + ArchivedLinkRelation::License => "license", + ArchivedLinkRelation::Linkset => "linkset", + ArchivedLinkRelation::Lrdd => "lrdd", + ArchivedLinkRelation::Manifest => "manifest", + ArchivedLinkRelation::MaskIcon => "mask-icon", + ArchivedLinkRelation::Me => "me", + ArchivedLinkRelation::MediaFeed => "media-feed", + ArchivedLinkRelation::Memento => "memento", + ArchivedLinkRelation::Micropub => "micropub", + ArchivedLinkRelation::Modulepreload => "modulepreload", + ArchivedLinkRelation::Monitor => "monitor", + ArchivedLinkRelation::MonitorGroup => "monitor-group", + ArchivedLinkRelation::Next => "next", + ArchivedLinkRelation::NextArchive => "next-archive", + ArchivedLinkRelation::Nofollow => "nofollow", + ArchivedLinkRelation::Noopener => "noopener", + ArchivedLinkRelation::Noreferrer => "noreferrer", + ArchivedLinkRelation::Opener => "opener", + ArchivedLinkRelation::Openid2LocalId => "openid2.local_id", + ArchivedLinkRelation::Openid2Provider => "openid2.provider", + ArchivedLinkRelation::Original => "original", + ArchivedLinkRelation::P3pv1 => "p3pv1", + ArchivedLinkRelation::Payment => "payment", + ArchivedLinkRelation::Pingback => "pingback", + ArchivedLinkRelation::Preconnect => "preconnect", + ArchivedLinkRelation::PredecessorVersion => "predecessor-version", + ArchivedLinkRelation::Prefetch => "prefetch", + ArchivedLinkRelation::Preload => "preload", + ArchivedLinkRelation::Prerender => "prerender", + ArchivedLinkRelation::Prev => "prev", + ArchivedLinkRelation::Preview => "preview", + ArchivedLinkRelation::Previous => "previous", + ArchivedLinkRelation::PrevArchive => "prev-archive", + ArchivedLinkRelation::PrivacyPolicy => "privacy-policy", + ArchivedLinkRelation::Profile => "profile", + ArchivedLinkRelation::Publication => "publication", + ArchivedLinkRelation::RdapActive => "rdap-active", + ArchivedLinkRelation::RdapBottom => "rdap-bottom", + ArchivedLinkRelation::RdapDown => "rdap-down", + ArchivedLinkRelation::RdapTop => "rdap-top", + ArchivedLinkRelation::RdapUp => "rdap-up", + ArchivedLinkRelation::Related => "related", + ArchivedLinkRelation::Restconf => "restconf", + ArchivedLinkRelation::Replies => "replies", + ArchivedLinkRelation::Ruleinput => "ruleinput", + ArchivedLinkRelation::Search => "search", + ArchivedLinkRelation::Section => "section", + ArchivedLinkRelation::Self_ => "self", + ArchivedLinkRelation::Service => "service", + ArchivedLinkRelation::ServiceDesc => "service-desc", + ArchivedLinkRelation::ServiceDoc => "service-doc", + ArchivedLinkRelation::ServiceMeta => "service-meta", + ArchivedLinkRelation::SipTrunkingCapability => "sip-trunking-capability", + ArchivedLinkRelation::Sponsored => "sponsored", + ArchivedLinkRelation::Start => "start", + ArchivedLinkRelation::Status => "status", + ArchivedLinkRelation::Stylesheet => "stylesheet", + ArchivedLinkRelation::Subsection => "subsection", + ArchivedLinkRelation::SuccessorVersion => "successor-version", + ArchivedLinkRelation::Sunset => "sunset", + ArchivedLinkRelation::Tag => "tag", + ArchivedLinkRelation::TermsOfService => "terms-of-service", + ArchivedLinkRelation::Timegate => "timegate", + ArchivedLinkRelation::Timemap => "timemap", + ArchivedLinkRelation::Type => "type", + ArchivedLinkRelation::Ugc => "ugc", + ArchivedLinkRelation::Up => "up", + ArchivedLinkRelation::VersionHistory => "version-history", + ArchivedLinkRelation::Via => "via", + ArchivedLinkRelation::Webmention => "webmention", + ArchivedLinkRelation::WorkingCopy => "working-copy", + ArchivedLinkRelation::WorkingCopyOf => "working-copy-of", + } + } +} + +impl ArchivedICalendarRelated { + pub fn as_str(&self) -> &'static str { + match self { + ArchivedICalendarRelated::Start => "START", + ArchivedICalendarRelated::End => "END", + } + } +} + +#[derive(Debug)] +pub(crate) enum ArchivedValueType { + Ical(ArchivedICalendarValueType), + CalendarScale, + Method, + Classification, + Status, + Transparency, + Action, + BusyType, + ParticipantType, + ResourceType, + Proximity, +} +impl ArchivedValueType { + pub fn unwrap_ical(self) -> ArchivedICalendarValueType { + match self { + ArchivedValueType::Ical(v) => v, + _ => ArchivedICalendarValueType::Text, + } + } +} diff --git a/vendor/calcard/src/icalendar/rkyv_utils.rs b/vendor/calcard/src/icalendar/rkyv_utils.rs new file mode 100644 index 00000000..504b6a53 --- /dev/null +++ b/vendor/calcard/src/icalendar/rkyv_utils.rs @@ -0,0 +1,433 @@ +/* + * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC + * + * SPDX-License-Identifier: Apache-2.0 OR MIT + */ + +use super::*; +use crate::common::{ArchivedPartialDateTime, timezone::Tz}; +use chrono::DateTime; + +impl ArchivedICalendar { + pub fn uids(&self) -> impl Iterator { + self.components + .iter() + .filter_map(|component| component.uid()) + } + + pub fn size(&self) -> usize { + self.components + .iter() + .map(|component| component.size()) + .sum() + } +} + +impl ArchivedICalendarComponent { + pub fn uid(&self) -> Option<&str> { + self.property(&ICalendarProperty::Uid) + .and_then(|e| e.values.first()) + .and_then(|v| v.as_text()) + } + + pub fn property(&self, prop: &ICalendarProperty) -> Option<&ArchivedICalendarEntry> { + self.entries.iter().find(|entry| &entry.name == prop) + } + + pub fn properties<'x, 'y: 'x>( + &'x self, + prop: &'y ICalendarProperty, + ) -> impl Iterator + 'x { + self.entries.iter().filter(move |entry| &entry.name == prop) + } + + pub fn size(&self) -> usize { + self.entries.iter().map(|entry| entry.size()).sum() + } + + pub fn is_recurrent(&self) -> bool { + self.entries.iter().any(|entry| { + matches!( + entry.name, + ArchivedICalendarProperty::Rrule | ArchivedICalendarProperty::Rdate + ) + }) + } + + pub fn is_recurrence_override(&self) -> bool { + self.entries + .iter() + .any(|entry| matches!(entry.name, ArchivedICalendarProperty::RecurrenceId)) + } + + pub fn is_recurrent_or_override(&self) -> bool { + self.entries.iter().any(|entry| { + matches!( + entry.name, + ArchivedICalendarProperty::Rrule + | ArchivedICalendarProperty::Rdate + | ArchivedICalendarProperty::RecurrenceId + ) + }) + } + + pub fn status(&self) -> Option<&ArchivedICalendarStatus> { + self.entries + .iter() + .find_map(|entry| match (&entry.name, entry.values.first()) { + ( + ArchivedICalendarProperty::Status, + Some(ArchivedICalendarValue::Status(status)), + ) => Some(status), + _ => None, + }) + } + + pub fn transparency(&self) -> Option<&ArchivedICalendarTransparency> { + self.entries + .iter() + .find_map(|entry| match (&entry.name, entry.values.first()) { + ( + ArchivedICalendarProperty::Transp, + Some(ArchivedICalendarValue::Transparency(trans)), + ) => Some(trans), + _ => None, + }) + } +} + +impl ArchivedICalendarValue { + pub fn size(&self) -> usize { + match self { + ArchivedICalendarValue::Binary(value) => value.len(), + ArchivedICalendarValue::Text(value) => value.len(), + ArchivedICalendarValue::PartialDateTime(_) => std::mem::size_of::(), + ArchivedICalendarValue::RecurrenceRule(_) => { + std::mem::size_of::() + } + _ => std::mem::size_of::(), + } + } + + pub fn as_text(&self) -> Option<&str> { + match self { + ArchivedICalendarValue::Text(s) => Some(s.as_str()), + ArchivedICalendarValue::Uri(v) => v.as_str(), + ArchivedICalendarValue::CalendarScale(v) => Some(v.as_str()), + ArchivedICalendarValue::Method(v) => Some(v.as_str()), + ArchivedICalendarValue::Classification(v) => Some(v.as_str()), + ArchivedICalendarValue::Status(v) => Some(v.as_str()), + ArchivedICalendarValue::Transparency(v) => Some(v.as_str()), + ArchivedICalendarValue::Action(v) => Some(v.as_str()), + ArchivedICalendarValue::BusyType(v) => Some(v.as_str()), + ArchivedICalendarValue::ParticipantType(v) => Some(v.as_str()), + ArchivedICalendarValue::ResourceType(v) => Some(v.as_str()), + ArchivedICalendarValue::Proximity(v) => Some(v.as_str()), + _ => None, + } + } + + pub fn as_integer(&self) -> Option { + match self { + ArchivedICalendarValue::Integer(i) => Some(i.to_native()), + _ => None, + } + } + + pub fn as_float(&self) -> Option { + match self { + ArchivedICalendarValue::Float(f) => Some(f.to_native()), + _ => None, + } + } + + pub fn as_boolean(&self) -> Option { + match self { + ArchivedICalendarValue::Boolean(b) => Some(*b), + _ => None, + } + } + + pub fn as_partial_date_time(&self) -> Option<&ArchivedPartialDateTime> { + match self { + ArchivedICalendarValue::PartialDateTime(dt) => Some(dt), + _ => None, + } + } + + pub fn as_binary(&self) -> Option<&[u8]> { + match self { + ArchivedICalendarValue::Binary(d) => Some(d.as_slice()), + _ => None, + } + } +} + +impl ArchivedICalendarParameterName { + pub fn as_str(&self) -> &str { + match self { + ArchivedICalendarParameterName::Altrep => "ALTREP", + ArchivedICalendarParameterName::Cn => "CN", + ArchivedICalendarParameterName::Cutype => "CUTYPE", + ArchivedICalendarParameterName::DelegatedFrom => "DELEGATED-FROM", + ArchivedICalendarParameterName::DelegatedTo => "DELEGATED-TO", + ArchivedICalendarParameterName::Dir => "DIR", + ArchivedICalendarParameterName::Fmttype => "FMTTYPE", + ArchivedICalendarParameterName::Fbtype => "FBTYPE", + ArchivedICalendarParameterName::Language => "LANGUAGE", + ArchivedICalendarParameterName::Member => "MEMBER", + ArchivedICalendarParameterName::Partstat => "PARTSTAT", + ArchivedICalendarParameterName::Range => "RANGE", + ArchivedICalendarParameterName::Related => "RELATED", + ArchivedICalendarParameterName::Reltype => "RELTYPE", + ArchivedICalendarParameterName::Role => "ROLE", + ArchivedICalendarParameterName::Rsvp => "RSVP", + ArchivedICalendarParameterName::ScheduleAgent => "SCHEDULE-AGENT", + ArchivedICalendarParameterName::ScheduleForceSend => "SCHEDULE-FORCE-SEND", + ArchivedICalendarParameterName::ScheduleStatus => "SCHEDULE-STATUS", + ArchivedICalendarParameterName::SentBy => "SENT-BY", + ArchivedICalendarParameterName::Tzid => "TZID", + ArchivedICalendarParameterName::Value => "VALUE", + ArchivedICalendarParameterName::Display => "DISPLAY", + ArchivedICalendarParameterName::Email => "EMAIL", + ArchivedICalendarParameterName::Feature => "FEATURE", + ArchivedICalendarParameterName::Label => "LABEL", + ArchivedICalendarParameterName::Size => "SIZE", + ArchivedICalendarParameterName::Filename => "FILENAME", + ArchivedICalendarParameterName::ManagedId => "MANAGED-ID", + ArchivedICalendarParameterName::Order => "ORDER", + ArchivedICalendarParameterName::Schema => "SCHEMA", + ArchivedICalendarParameterName::Derived => "DERIVED", + ArchivedICalendarParameterName::Gap => "GAP", + ArchivedICalendarParameterName::Linkrel => "LINKREL", + ArchivedICalendarParameterName::Jsptr => "JSPTR", + ArchivedICalendarParameterName::Jsid => "JSID", + ArchivedICalendarParameterName::Other(name) => name.as_str(), + } + } +} + +impl ArchivedICalendarEntry { + #[inline] + pub fn parameters( + &self, + prop: &ICalendarParameterName, + ) -> impl Iterator { + self.params.iter().filter_map(move |param| { + if ¶m.name == prop { + Some(¶m.value) + } else { + None + } + }) + } + + pub fn size(&self) -> usize { + self.values.iter().map(|value| value.size()).sum::() + + self + .params + .iter() + .map(|param| std::mem::size_of::() + param.value.size()) + .sum::() + + self.name.as_str().len() + } +} + +impl ArchivedICalendarParameterValue { + pub fn as_text(&self) -> Option<&str> { + match self { + ArchivedICalendarParameterValue::Text(v) => Some(v.as_str()), + ArchivedICalendarParameterValue::Bool(v) => Some(if *v { "TRUE" } else { "FALSE" }), + ArchivedICalendarParameterValue::Uri(uri) => uri.as_str(), + ArchivedICalendarParameterValue::Cutype(v) => Some(v.as_str()), + ArchivedICalendarParameterValue::Fbtype(v) => Some(v.as_str()), + ArchivedICalendarParameterValue::Partstat(v) => Some(v.as_str()), + ArchivedICalendarParameterValue::Related(v) => Some(v.as_str()), + ArchivedICalendarParameterValue::Reltype(v) => Some(v.as_str()), + ArchivedICalendarParameterValue::Role(v) => Some(v.as_str()), + ArchivedICalendarParameterValue::ScheduleAgent(v) => Some(v.as_str()), + ArchivedICalendarParameterValue::ScheduleForceSend(v) => Some(v.as_str()), + ArchivedICalendarParameterValue::Value(v) => Some(v.as_str()), + ArchivedICalendarParameterValue::Display(v) => Some(v.as_str()), + ArchivedICalendarParameterValue::Feature(v) => Some(v.as_str()), + ArchivedICalendarParameterValue::Linkrel(v) => Some(v.as_str()), + ArchivedICalendarParameterValue::Duration(_) + | ArchivedICalendarParameterValue::Integer(_) + | ArchivedICalendarParameterValue::Null => None, + } + } + + pub fn size(&self) -> usize { + match self { + ArchivedICalendarParameterValue::Text(v) => v.len(), + ArchivedICalendarParameterValue::Uri(v) => v.size(), + _ => std::mem::size_of::(), + } + } +} + +impl ArchivedUri { + pub fn as_str(&self) -> Option<&str> { + match self { + ArchivedUri::Data(data) => data.content_type.as_deref(), + ArchivedUri::Location(loc) => loc.as_str().into(), + } + } + + pub fn size(&self) -> usize { + match self { + ArchivedUri::Data(data) => { + data.data.len() + data.content_type.as_ref().map(|s| s.len()).unwrap_or(0) + } + ArchivedUri::Location(loc) => loc.len(), + } + } +} + +impl ArchivedICalendarPeriod { + pub fn time_range(&self, tz: Tz) -> Option<(DateTime, DateTime)> { + match self { + ArchivedICalendarPeriod::Range { start, end } => { + if let (Some(start), Some(end)) = ( + start + .to_date_time() + .and_then(|start| start.to_date_time_with_tz(tz)), + end.to_date_time() + .and_then(|end| end.to_date_time_with_tz(tz)), + ) { + Some((start, end)) + } else { + None + } + } + ArchivedICalendarPeriod::Duration { start, duration } => start + .to_date_time() + .and_then(|start| start.to_date_time_with_tz(tz)) + .zip(duration.to_time_delta()) + .and_then(|(start, duration)| { + start.checked_add_signed(duration).map(|end| (start, end)) + }), + } + } +} + +impl ArchivedPartialDateTime { + pub fn to_date_time_with_tz(&self, tz: Tz) -> Option> { + self.to_date_time() + .and_then(|dt| dt.to_date_time_with_tz(tz)) + } + + #[inline(always)] + pub fn has_date(&self) -> bool { + self.year.is_some() && self.month.is_some() && self.day.is_some() + } + + #[inline(always)] + pub fn has_time(&self) -> bool { + self.hour.is_some() && self.minute.is_some() + } + + #[inline(always)] + pub fn has_zone(&self) -> bool { + self.tz_hour.is_some() + } + + #[inline(always)] + pub fn has_date_and_time(&self) -> bool { + self.has_date() && self.has_time() + } +} + +impl ArchivedICalendarDuration { + pub fn to_time_delta(&self) -> Option { + chrono::TimeDelta::new(self.as_seconds(), 0) + } + + pub fn as_seconds(&self) -> i64 { + let secs = self.seconds.to_native() as i64 + + self.minutes.to_native() as i64 * 60 + + self.hours.to_native() as i64 * 3600 + + self.days.to_native() as i64 * 86400 + + self.weeks.to_native() as i64 * 604800; + + if self.neg { -secs } else { secs } + } + + pub fn to_native(&self) -> ICalendarDuration { + ICalendarDuration { + neg: self.neg, + weeks: self.weeks.to_native(), + days: self.days.to_native(), + hours: self.hours.to_native(), + minutes: self.minutes.to_native(), + seconds: self.seconds.to_native(), + } + } +} + +impl ArchivedICalendarComponentType { + pub fn has_time_ranges(&self) -> bool { + matches!( + self, + ArchivedICalendarComponentType::VEvent + | ArchivedICalendarComponentType::VTodo + | ArchivedICalendarComponentType::VJournal + | ArchivedICalendarComponentType::VFreebusy + ) + } + + pub fn is_scheduling_object(&self) -> bool { + matches!( + self, + ArchivedICalendarComponentType::VEvent + | ArchivedICalendarComponentType::VTodo + | ArchivedICalendarComponentType::VJournal + | ArchivedICalendarComponentType::VFreebusy + ) + } + + pub fn is_event(&self) -> bool { + matches!(self, ArchivedICalendarComponentType::VEvent) + } + + pub fn is_todo(&self) -> bool { + matches!(self, ArchivedICalendarComponentType::VTodo) + } + + pub fn is_event_or_todo(&self) -> bool { + matches!( + self, + ArchivedICalendarComponentType::VEvent | ArchivedICalendarComponentType::VTodo + ) + } + + pub fn is_journal(&self) -> bool { + matches!(self, ArchivedICalendarComponentType::VJournal) + } + + pub fn is_freebusy(&self) -> bool { + matches!(self, ArchivedICalendarComponentType::VFreebusy) + } + + pub fn is_location(&self) -> bool { + matches!(self, ArchivedICalendarComponentType::VLocation) + } + + pub fn is_alarm(&self) -> bool { + matches!(self, ArchivedICalendarComponentType::VAlarm) + } + + pub fn is_participant(&self) -> bool { + matches!(self, ArchivedICalendarComponentType::Participant) + } +} + +impl ArchivedICalendarMonth { + pub fn is_leap(&self) -> bool { + self.0 < 0 + } + + pub fn month(&self) -> u8 { + self.0.unsigned_abs() + } +} diff --git a/vendor/calcard/src/icalendar/rkyv_writer.rs b/vendor/calcard/src/icalendar/rkyv_writer.rs new file mode 100644 index 00000000..b9375e47 --- /dev/null +++ b/vendor/calcard/src/icalendar/rkyv_writer.rs @@ -0,0 +1,519 @@ +/* + * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC + * + * SPDX-License-Identifier: Apache-2.0 OR MIT + */ + +use super::*; +use crate::{ + common::{ + ArchivedPartialDateTime, CalendarScale, + writer::{FoldingWriter, LineWriter, write_bytes, write_param_value, write_text}, + }, + icalendar::{ + ValueSeparator, + writer::{write_component_begin, write_component_end}, + }, +}; +use std::{ + fmt::{Display, Write}, + slice::Iter, +}; + +impl ArchivedICalendar { + pub fn write_to(&self, out: &mut impl Write) -> std::fmt::Result { + let _v = [0.into()]; + let mut component_iter: Iter<'_, rkyv::primitive::ArchivedU32> = _v.iter(); + let mut component_stack = Vec::with_capacity(4); + + loop { + if let Some(component_id) = component_iter.next() { + let component = self + .components + .get(component_id.to_native() as usize) + .unwrap(); + write_component_begin(out, component.component_type.as_str())?; + + for entry in component.entries.iter() { + if !matches!( + entry.name, + ArchivedICalendarProperty::Begin | ArchivedICalendarProperty::End + ) { + entry.write_to(out, true)?; + } + } + + if !component.component_ids.is_empty() { + component_stack.push((component, component_iter)); + component_iter = component.component_ids.iter(); + } else { + write_component_end(out, component.component_type.as_str())?; + } + } else if let Some((component, iter)) = component_stack.pop() { + write_component_end(out, component.component_type.as_str())?; + component_iter = iter; + } else { + break; + } + } + + Ok(()) + } +} + +impl ArchivedICalendarEntry { + pub fn write_to(&self, out: &mut impl Write, with_value: bool) -> std::fmt::Result { + let mut folded = FoldingWriter::new(out); + let out = &mut folded; + + out.write_atomic(self.name.as_str())?; + + if matches!( + self.values.first().as_ref(), + Some(ArchivedICalendarValue::Binary(_)) + ) { + out.write_atomic(";ENCODING=BASE64")?; + } + + let mut types = None; + let mut last_param: Option<&ArchivedICalendarParameterName> = None; + + for param in self.params.iter() { + if last_param.is_some_and(|last_param| last_param == ¶m.name) { + out.write_atomic(",")?; + } else { + out.write_atomic(";")?; + out.write_atomic(param.name.as_str())?; + if !matches!(param.value, ArchivedICalendarParameterValue::Null) { + out.write_atomic("=")?; + } + last_param = Some(¶m.name); + } + + match ¶m.value { + ArchivedICalendarParameterValue::Text(v) => { + write_param_value(out, v)?; + } + ArchivedICalendarParameterValue::Integer(i) => { + write!(out, "{i}")?; + } + ArchivedICalendarParameterValue::Bool(v) => { + let v = if !matches!(param.name, ArchivedICalendarParameterName::Range) { + if *v { "TRUE" } else { "FALSE" } + } else { + "THISANDFUTURE" + }; + out.write_atomic(v)?; + } + ArchivedICalendarParameterValue::Uri(uri) => { + out.write_atomic("\"")?; + write_uri(out, uri, false)?; + out.write_atomic("\"")?; + } + ArchivedICalendarParameterValue::Cutype(v) => { + write_param_value(out, v.as_str())?; + } + ArchivedICalendarParameterValue::Fbtype(v) => { + write_param_value(out, v.as_str())?; + } + ArchivedICalendarParameterValue::Partstat(v) => { + write_param_value(out, v.as_str())?; + } + ArchivedICalendarParameterValue::Related(v) => { + write_param_value(out, v.as_str())?; + } + ArchivedICalendarParameterValue::Reltype(v) => { + write_param_value(out, v.as_str())?; + } + ArchivedICalendarParameterValue::Role(v) => { + write_param_value(out, v.as_str())?; + } + ArchivedICalendarParameterValue::ScheduleAgent(v) => { + write_param_value(out, v.as_str())?; + } + ArchivedICalendarParameterValue::ScheduleForceSend(v) => { + write_param_value(out, v.as_str())?; + } + ArchivedICalendarParameterValue::Value(v) => { + types = Some(v); + write_param_value(out, v.as_str())?; + } + ArchivedICalendarParameterValue::Display(v) => { + write_param_value(out, v.as_str())?; + } + ArchivedICalendarParameterValue::Feature(v) => { + write_param_value(out, v.as_str())?; + } + ArchivedICalendarParameterValue::Duration(v) => { + write!(out, "{v}")?; + } + ArchivedICalendarParameterValue::Linkrel(v) => { + write_param_value(out, v.as_str())?; + } + ArchivedICalendarParameterValue::Null => { + last_param = None; + } + } + } + + out.write_atomic(":")?; + + if with_value { + let (default_type, separator) = self.name.default_types(); + let separator = if !matches!(separator, ValueSeparator::Comma) { + ";" + } else { + "," + }; + let default_type = default_type.unwrap_ical(); + + for (pos, value) in self.values.iter().enumerate() { + if pos > 0 { + out.write_atomic(separator)?; + } + + let text = match value { + ArchivedICalendarValue::Binary(v) => { + write_bytes(out, v)?; + continue; + } + ArchivedICalendarValue::Boolean(v) => { + out.write_atomic(if *v { "TRUE" } else { "FALSE" })?; + continue; + } + ArchivedICalendarValue::Uri(v) => { + write_uri(out, v, true)?; + continue; + } + ArchivedICalendarValue::PartialDateTime(v) => { + v.format_as_ical(out, types.unwrap_or(&default_type))?; + continue; + } + ArchivedICalendarValue::Duration(v) => { + write!(out, "{}", v)?; + continue; + } + ArchivedICalendarValue::RecurrenceRule(v) => { + write!(out, "{}", v)?; + continue; + } + ArchivedICalendarValue::Period(v) => { + write!(out, "{}", v)?; + continue; + } + ArchivedICalendarValue::Float(v) => { + write!(out, "{v}")?; + continue; + } + ArchivedICalendarValue::Integer(v) => { + write!(out, "{v}")?; + continue; + } + ArchivedICalendarValue::Text(v) => { + let escape = !matches!( + types.unwrap_or(&default_type), + ArchivedICalendarValueType::Recur + ); + write_text(out, v, escape, escape)?; + continue; + } + ArchivedICalendarValue::CalendarScale(v) => v.as_str(), + ArchivedICalendarValue::Method(v) => v.as_str(), + ArchivedICalendarValue::Classification(v) => v.as_str(), + ArchivedICalendarValue::Status(v) => v.as_str(), + ArchivedICalendarValue::Transparency(v) => v.as_str(), + ArchivedICalendarValue::Action(v) => v.as_str(), + ArchivedICalendarValue::BusyType(v) => v.as_str(), + ArchivedICalendarValue::ParticipantType(v) => v.as_str(), + ArchivedICalendarValue::ResourceType(v) => v.as_str(), + ArchivedICalendarValue::Proximity(v) => v.as_str(), + }; + + out.write_atomic(text)?; + } + } + out.end_line() + } +} + +pub(crate) fn write_uri( + out: &mut FoldingWriter<'_, W>, + value: &ArchivedUri, + escape: bool, +) -> std::fmt::Result { + match value { + ArchivedUri::Data(v) => { + let media_type = v.content_type.as_deref().unwrap_or_default(); + out.write_str("data:")?; + out.write_str(media_type)?; + out.write_str(";")?; + if escape { + out.write_atomic("base64\\,")?; + } else { + out.write_atomic("base64,")?; + } + write_bytes(out, &v.data) + } + ArchivedUri::Location(v) => write_text(out, v, escape, escape), + } +} + +impl Display for ArchivedICalendarRecurrenceRule { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "FREQ={}", self.freq.as_str())?; + if let Some(until) = self.until.as_ref() { + write!(f, ";UNTIL=")?; + until.format_as_ical( + f, + if until.has_date_and_time() { + &ArchivedICalendarValueType::DateTime + } else { + &ArchivedICalendarValueType::Date + }, + )?; + } + if let Some(count) = self.count.as_ref().filter(|c| **c > 0) { + write!(f, ";COUNT={}", count)?; + } + if let Some(interval) = self.interval.as_ref() { + write!(f, ";INTERVAL={}", interval)?; + } + if !self.bysecond.is_empty() { + write!(f, ";BYSECOND=")?; + for (pos, item) in self.bysecond.iter().enumerate() { + if pos > 0 { + write!(f, ",")?; + } + write!(f, "{}", item)?; + } + } + if !self.byminute.is_empty() { + write!(f, ";BYMINUTE=")?; + for (pos, item) in self.byminute.iter().enumerate() { + if pos > 0 { + write!(f, ",")?; + } + write!(f, "{}", item)?; + } + } + if !self.byhour.is_empty() { + write!(f, ";BYHOUR=")?; + for (pos, item) in self.byhour.iter().enumerate() { + if pos > 0 { + write!(f, ",")?; + } + write!(f, "{}", item)?; + } + } + if !self.byday.is_empty() { + write!(f, ";BYDAY=")?; + for (pos, item) in self.byday.iter().enumerate() { + if pos > 0 { + write!(f, ",")?; + } + write!(f, "{}", item)?; + } + } + if !self.bymonthday.is_empty() { + write!(f, ";BYMONTHDAY=")?; + for (pos, item) in self.bymonthday.iter().enumerate() { + if pos > 0 { + write!(f, ",")?; + } + write!(f, "{}", item)?; + } + } + if !self.byyearday.is_empty() { + write!(f, ";BYYEARDAY=")?; + for (pos, item) in self.byyearday.iter().enumerate() { + if pos > 0 { + write!(f, ",")?; + } + write!(f, "{}", item)?; + } + } + if !self.byweekno.is_empty() { + write!(f, ";BYWEEKNO=")?; + for (pos, item) in self.byweekno.iter().enumerate() { + if pos > 0 { + write!(f, ",")?; + } + write!(f, "{}", item)?; + } + } + if !self.bymonth.is_empty() { + write!(f, ";BYMONTH=")?; + for (pos, item) in self.bymonth.iter().enumerate() { + if pos > 0 { + write!(f, ",")?; + } + write!(f, "{}", item)?; + } + } + if !self.bysetpos.is_empty() { + write!(f, ";BYSETPOS=")?; + for (pos, item) in self.bysetpos.iter().enumerate() { + if pos > 0 { + write!(f, ",")?; + } + write!(f, "{}", item)?; + } + } + if let Some(wkst) = self.wkst.as_ref() { + write!(f, ";WKST={}", wkst.as_str())?; + } + if let Some(rscale) = self.rscale.as_ref() { + write!(f, ";RSCALE={}", rscale.as_str())?; + } else if self.skip.is_some() { + write!(f, ";RSCALE={}", CalendarScale::Gregorian.as_str())?; + } + if let Some(skip) = self.skip.as_ref() { + write!(f, ";SKIP={}", skip.as_str())?; + } + + Ok(()) + } +} + +impl Display for ArchivedICalendarDay { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + if let Some(ordwk) = self.ordwk.as_ref() { + write!(f, "{}", ordwk)?; + } + write!(f, "{}", self.weekday.as_str()) + } +} + +impl Display for ArchivedICalendarMonth { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + if !self.is_leap() { + write!(f, "{}", self.month()) + } else { + write!(f, "{}L", self.month()) + } + } +} + +impl Display for ArchivedICalendarPeriod { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + ArchivedICalendarPeriod::Range { start, end } => { + start.format_as_ical(f, &ArchivedICalendarValueType::DateTime)?; + write!(f, "/")?; + end.format_as_ical(f, &ArchivedICalendarValueType::DateTime) + } + ArchivedICalendarPeriod::Duration { start, duration } => { + start.format_as_ical(f, &ArchivedICalendarValueType::DateTime)?; + write!(f, "/{}", duration) + } + } + } +} + +impl Display for ArchivedICalendarDuration { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + if self.neg { + write!(f, "-")?; + } + write!(f, "P")?; + if self.weeks == 0 + && self.days == 0 + && self.hours == 0 + && self.minutes == 0 + && self.seconds == 0 + { + return write!(f, "T0S"); + } + if self.weeks != 0 { + write!(f, "{}W", self.weeks)?; + } + if self.days != 0 { + write!(f, "{}D", self.days)?; + } + if self.hours != 0 || self.minutes != 0 || self.seconds != 0 { + write!(f, "T")?; + if self.hours != 0 { + write!(f, "{}H", self.hours)?; + } + if self.minutes != 0 { + write!(f, "{}M", self.minutes)?; + } + if self.seconds != 0 { + write!(f, "{}S", self.seconds)?; + } + } + + Ok(()) + } +} + +impl ArchivedPartialDateTime { + pub fn format_as_ical( + &self, + out: &mut impl Write, + fmt: &ArchivedICalendarValueType, + ) -> std::fmt::Result { + if matches!( + fmt, + ArchivedICalendarValueType::Date | ArchivedICalendarValueType::DateTime + ) { + write!( + out, + "{:04}{:02}{:02}", + self.year + .as_ref() + .map(|n| n.to_native()) + .unwrap_or_default(), + self.month.as_ref().copied().unwrap_or_default(), + self.day.as_ref().copied().unwrap_or_default(), + )?; + } + + if matches!(fmt, ArchivedICalendarValueType::DateTime) { + write!(out, "T")?; + } + + if matches!( + fmt, + ArchivedICalendarValueType::DateTime | ArchivedICalendarValueType::Time + ) { + write!( + out, + "{:02}{:02}{:02}", + self.hour.as_ref().copied().unwrap_or_default(), + self.minute.as_ref().copied().unwrap_or_default(), + self.second.as_ref().copied().unwrap_or_default(), + )?; + + if matches!( + (self.tz_hour.as_ref(), self.tz_minute.as_ref()), + (Some(0), Some(0)) + ) { + write!(out, "Z")?; + } + } + + if matches!(fmt, ArchivedICalendarValueType::UtcOffset) { + if self.tz_minus { + write!(out, "-")?; + } else { + write!(out, "+")?; + } + + write!( + out, + "{:02}{:02}", + self.tz_hour.as_ref().copied().unwrap_or_default(), + self.tz_minute.as_ref().copied().unwrap_or_default(), + )?; + } + + Ok(()) + } +} + +impl Display for ArchivedICalendar { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + self.write_to(f) + } +} diff --git a/vendor/calcard/src/icalendar/timezone.rs b/vendor/calcard/src/icalendar/timezone.rs new file mode 100644 index 00000000..3b02b46e --- /dev/null +++ b/vendor/calcard/src/icalendar/timezone.rs @@ -0,0 +1,1221 @@ +/* + * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC + * + * SPDX-License-Identifier: Apache-2.0 OR MIT + */ + +use super::{ + ICalendar, ICalendarComponent, ICalendarComponentType, ICalendarDay, ICalendarEntry, + ICalendarFrequency, ICalendarMonth, ICalendarPeriod, ICalendarProperty, + ICalendarRecurrenceRule, ICalendarValue, ICalendarWeekday, +}; +use crate::{ + common::{PartialDateTime, timezone::Tz}, + icalendar::ICalendarParameterName, +}; +use chrono::{Datelike, NaiveDate, NaiveDateTime, Offset, TimeZone, Timelike, Utc, Weekday}; +use chrono_tz::{OffsetComponents, OffsetName}; +use std::{collections::HashMap, ops::Range, str::FromStr}; + +pub struct TzResolver { + tzs: HashMap, + default: Tz, +} + +impl TzResolver +where + T: std::borrow::Borrow + std::hash::Hash + Eq, +{ + pub fn resolve(&self, tz_name: &str) -> Option { + self.tzs + .get(tz_name) + .copied() + .or_else(|| Tz::from_str(tz_name).ok()) + } + + pub fn resolve_or_default(&self, tz_name: Option<&str>) -> Tz { + tz_name + .and_then(|tz_name| { + self.tzs + .get(tz_name) + .copied() + .or_else(|| Tz::from_str(tz_name).ok()) + }) + .unwrap_or(self.default) + } + + pub fn with_default(mut self, default: impl Into) -> Self { + self.default = default.into(); + self + } +} + +impl ICalendar { + pub fn timezones(&self) -> impl Iterator { + self.components + .iter() + .filter(|comp| matches!(comp.component_type, ICalendarComponentType::VTimezone)) + } + + pub fn is_timezone(&self) -> bool { + self.timezones().count() == 1 + } + + pub fn build_tz_resolver(&self) -> TzResolver<&'_ str> { + TzResolver { + tzs: self.timezones().filter_map(|tz| tz.timezone()).collect(), + default: Tz::Floating, + } + } + + pub fn build_owned_tz_resolver(&self) -> TzResolver { + TzResolver { + tzs: self + .timezones() + .filter_map(|tz| tz.timezone()) + .map(|(name, tz)| (name.to_string(), tz)) + .collect(), + default: Tz::Floating, + } + } +} + +impl ICalendarComponent { + pub fn timezone(&self) -> Option<(&str, Tz)> { + let mut tz_name = None; + let mut tz_id = None; + let mut tz_lic = None; + let mut tz_cdo_id = None; + + for entry in &self.entries { + match (&entry.name, entry.values.first()) { + (ICalendarProperty::Tzid, Some(ICalendarValue::Text(id))) => { + tz_id = Tz::from_str(id).ok(); + tz_name = Some(id.as_str()); + } + (ICalendarProperty::Other(value), Some(ICalendarValue::Text(id))) => { + hashify::fnc_map!(value.as_bytes(), + "X-LIC-LOCATION" => { + tz_lic = Tz::from_str(id.strip_prefix("SystemV/").unwrap_or(id.as_str())).ok(); + }, + "X-MICROSOFT-CDO-TZID" => { + tz_cdo_id = Tz::from_ms_cdo_zone_id(id); + }, + _ => {} + ); + } + _ => (), + } + } + + tz_name.zip(tz_id.or(tz_lic).or(tz_cdo_id)) + } +} + +impl ICalendarEntry { + pub fn tz_id(&self) -> Option<&str> { + self.parameters(&ICalendarParameterName::Tzid) + .find_map(|v| v.as_text()) + } +} + +const TZ_MIN_YEAR: i32 = 1900; +const TZ_MAX_FUTURE_YEARS: i32 = 100; +const TZ_RANGE_MARGIN: i64 = 86400; +const TZ_SCAN_STEP: i64 = 3 * 86400; +const TZ_RULE_ACTIVE_SECONDS: i64 = 366 * 86400; + +impl ICalendar { + pub fn add_timezone(&mut self, tz_id: &str, from: i64, to: i64) -> Option { + if self + .components + .first() + .is_none_or(|root| root.component_type != ICalendarComponentType::VCalendar) + { + return None; + } + + let observances = match Tz::from_str(tz_id).ok()? { + Tz::Tz(tz) => build_observances(tz, from, to), + Tz::Fixed(offset) => { + let offset = round_to_minute(offset.local_minus_utc()); + vec![TzObservance { + at: from, + from_offset: offset, + to_offset: offset, + is_dst: false, + name: None, + rrule: None, + }] + } + Tz::Floating => return None, + }; + + let insert_at = self.components[0] + .component_ids + .iter() + .position(|component_id| { + self.components + .get(*component_id as usize) + .is_none_or(|comp| comp.component_type != ICalendarComponentType::VTimezone) + }) + .unwrap_or(self.components[0].component_ids.len()); + + let tz_component_id = self.components.len() as u32; + self.components.reserve(observances.len() + 1); + self.components.push(ICalendarComponent { + component_type: ICalendarComponentType::VTimezone, + entries: vec![ + ICalendarEntry::new(ICalendarProperty::Tzid).with_value(tz_id.to_string()), + ], + component_ids: Vec::with_capacity(observances.len()), + }); + + let first_component_id = self.components.len() as u32; + for observance in observances { + self.components.push(observance.into_component()); + } + let last_component_id = self.components.len() as u32; + self.components[tz_component_id as usize] + .component_ids + .extend(first_component_id..last_component_id); + + self.components[0] + .component_ids + .insert(insert_at, tz_component_id); + + Some(tz_component_id) + } + + pub fn add_missing_timezones(&mut self) -> usize { + let mut defined: Vec<&str> = Vec::new(); + for component in self.timezones() { + for entry in &component.entries { + if entry.name == ICalendarProperty::Tzid + && let Some(ICalendarValue::Text(tz_id)) = entry.values.first() + { + defined.push(tz_id.as_str()); + } + } + } + + let mut referenced: Vec = Vec::new(); + let mut range: Option<(i64, i64)> = None; + let mut recurrence_span = 0; + let mut is_unbounded = false; + + for component in &self.components { + if matches!( + component.component_type, + ICalendarComponentType::VTimezone + | ICalendarComponentType::Standard + | ICalendarComponentType::Daylight + ) { + continue; + } + + for entry in &component.entries { + for param in &entry.params { + if param.name == ICalendarParameterName::Tzid + && let Some(tz_id) = param.value.as_text() + && !defined.contains(&tz_id) + && !referenced.iter().any(|id| id == tz_id) + { + referenced.push(tz_id.to_string()); + } + } + + match &entry.name { + ICalendarProperty::Rrule => { + for value in &entry.values { + if let ICalendarValue::RecurrenceRule(rule) = value { + match (&rule.until, &rule.count) { + (Some(until), _) => expand_range(&mut range, until), + (None, Some(count)) => { + recurrence_span = recurrence_span.max(recurrence_span_of( + &rule.freq, + *count, + rule.interval, + )); + } + (None, None) => is_unbounded = true, + } + } + } + } + ICalendarProperty::Dtstart + | ICalendarProperty::Dtend + | ICalendarProperty::Due + | ICalendarProperty::RecurrenceId + | ICalendarProperty::Exdate + | ICalendarProperty::Rdate => { + for value in &entry.values { + match value { + ICalendarValue::PartialDateTime(dt) => { + expand_range(&mut range, dt); + } + ICalendarValue::Period(ICalendarPeriod::Range { start, end }) => { + expand_range(&mut range, start); + expand_range(&mut range, end); + } + ICalendarValue::Period(ICalendarPeriod::Duration { + start, .. + }) => { + expand_range(&mut range, start); + } + _ => (), + } + } + } + _ => (), + } + } + } + + if referenced.is_empty() { + return 0; + } + + let now = Utc::now().timestamp(); + let (min, max) = range.unwrap_or((now, now)); + let max = max.saturating_add(recurrence_span); + let current_year = year_of(now); + let max_year = current_year.saturating_add(TZ_MAX_FUTURE_YEARS); + let from_year = year_of(min).clamp(TZ_MIN_YEAR, max_year); + let to_year = if is_unbounded { + year_of(max).max(current_year) + } else { + year_of(max) + } + .saturating_add(2) + .clamp(from_year.saturating_add(1), max_year.saturating_add(2)); + let from = start_of_year(from_year).saturating_sub(TZ_RANGE_MARGIN); + let to = start_of_year(to_year); + + let mut added = 0; + for tz_id in referenced { + if self.add_timezone(&tz_id, from, to).is_some() { + added += 1; + } + } + added + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +struct TzState { + offset: i32, + is_dst: bool, +} + +#[derive(PartialEq, Eq)] +struct TzRuleKey { + month: u8, + ordwk: i16, + weekday: ICalendarWeekday, + hour: u8, + minute: u8, + second: u8, + from_offset: i32, + to_offset: i32, +} + +struct TzObservance { + at: i64, + from_offset: i32, + to_offset: i32, + is_dst: bool, + name: Option, + rrule: Option, +} + +impl TzObservance { + fn onset(&self) -> NaiveDateTime { + naive_utc(self.at.saturating_add(self.from_offset as i64)) + } + + fn rule_key(&self) -> Option<(i32, TzRuleKey)> { + let onset = self.onset(); + let day = onset.day(); + let ordwk = if day.saturating_add(7) > days_in_month(onset.year(), onset.month()) { + -1 + } else { + ((day - 1) / 7 + 1) as i16 + }; + + Some(( + onset.year(), + TzRuleKey { + month: u8::try_from(onset.month()).ok()?, + ordwk, + weekday: weekday_of(onset.weekday()), + hour: u8::try_from(onset.hour()).ok()?, + minute: u8::try_from(onset.minute()).ok()?, + second: u8::try_from(onset.second()).ok()?, + from_offset: self.from_offset, + to_offset: self.to_offset, + }, + )) + } + + fn into_component(self) -> ICalendarComponent { + let mut component = ICalendarComponent::new(if self.is_dst { + ICalendarComponentType::Daylight + } else { + ICalendarComponentType::Standard + }); + + component + .entries + .push(ICalendarEntry::new(ICalendarProperty::Dtstart).with_value( + PartialDateTime::from_naive_timestamp(self.onset().and_utc().timestamp()), + )); + component.entries.push( + ICalendarEntry::new(ICalendarProperty::Tzoffsetfrom) + .with_value(offset_to_partial(self.from_offset)), + ); + component.entries.push( + ICalendarEntry::new(ICalendarProperty::Tzoffsetto) + .with_value(offset_to_partial(self.to_offset)), + ); + if let Some(name) = self.name { + component + .entries + .push(ICalendarEntry::new(ICalendarProperty::Tzname).with_value(name)); + } + if let Some(rrule) = self.rrule { + component + .entries + .push(ICalendarEntry::new(ICalendarProperty::Rrule).with_value(rrule)); + } + + component + } +} + +fn build_observances(tz: chrono_tz::Tz, from: i64, to: i64) -> Vec { + let (initial, initial_name) = tz_observance_at(tz, from); + let mut observances = Vec::with_capacity(estimated_observances(from, to)); + observances.push(TzObservance { + at: from, + from_offset: round_to_minute(initial.offset), + to_offset: round_to_minute(initial.offset), + is_dst: initial.is_dst, + name: initial_name, + rrule: None, + }); + + let mut previous_at = from; + let mut previous = initial; + let mut at = from; + + while at < to { + at = at.saturating_add(TZ_SCAN_STEP).min(to); + let state = tz_state_at(tz, at); + if state != previous { + let onset = find_transition(tz, previous_at, at, previous); + observances.push(TzObservance { + at: onset, + from_offset: round_to_minute(previous.offset), + to_offset: round_to_minute(state.offset), + is_dst: state.is_dst, + name: tz_name_at(tz, onset), + rrule: None, + }); + previous = state; + } + previous_at = at; + } + + label_observances(&mut observances); + + collapse_observances(observances, to) +} + +fn label_observances(observances: &mut [TzObservance]) { + let mut daylight_offset = None; + let mut standard_offset = None; + + for observance in observances.iter() { + let slot = if observance.is_dst { + &mut daylight_offset + } else { + &mut standard_offset + }; + *slot = Some(slot.map_or(observance.to_offset, |offset: i32| { + offset.max(observance.to_offset) + })); + } + + if let (Some(daylight_offset), Some(standard_offset)) = (daylight_offset, standard_offset) + && daylight_offset < standard_offset + { + for observance in observances.iter_mut() { + observance.is_dst = !observance.is_dst; + } + } +} + +fn collapse_observances(mut observances: Vec, to: i64) -> Vec { + let mut discard = vec![false; observances.len()]; + let mut unbounded: [Option<(usize, i64)>; 2] = [None, None]; + let mut trailing: [Option<(usize, u8, i16, ICalendarWeekday)>; 2] = [None, None]; + + let mut onsets: Vec<(usize, i32, TzRuleKey)> = Vec::new(); + let mut runs: Vec> = Vec::new(); + + for is_dst in [false, true] { + onsets.clear(); + runs.clear(); + + for (index, observance) in observances.iter().enumerate() { + if index == 0 || observance.is_dst != is_dst { + continue; + } + let Some((year, key)) = observance.rule_key() else { + continue; + }; + let extends = onsets + .last() + .is_some_and(|(last_index, last_year, last_key)| { + last_year.saturating_add(1) == year + && last_key == &key + && observances[*last_index].name == observance.name + }); + + onsets.push((index, year, key)); + match runs.last_mut() { + Some(run) if extends => run.end = onsets.len(), + _ => runs.push(onsets.len() - 1..onsets.len()), + } + } + + for (position, run) in runs.iter().enumerate() { + let (Some((first, _, key)), Some((last, _, _))) = + (onsets.get(run.start), onsets.get(run.end - 1)) + else { + continue; + }; + if run.len() < 2 { + continue; + } + + let last_at = observances[*last].at; + let is_expired = last_at.saturating_add(TZ_RULE_ACTIVE_SECONDS) < to; + let is_final = position + 1 == runs.len() && !is_expired; + observances[*first].rrule = Some(ICalendarRecurrenceRule { + freq: ICalendarFrequency::Yearly, + until: (!is_final).then(|| PartialDateTime::from_utc_timestamp(last_at)), + bymonth: vec![ICalendarMonth::new(key.month, false)], + byday: vec![ICalendarDay { + ordwk: Some(key.ordwk), + weekday: key.weekday, + }], + ..Default::default() + }); + if is_final { + unbounded[usize::from(is_dst)] = Some((*first, last_at)); + } + + for (index, _, _) in &onsets[run.start + 1..run.end] { + discard[*index] = true; + } + } + + if let Some(run) = runs.last() + && run.len() == 1 + && let Some((index, _, key)) = onsets.get(run.start) + { + trailing[usize::from(is_dst)] = Some((*index, key.month, key.ordwk, key.weekday)); + } + } + + for is_dst in [false, true] { + let class = usize::from(is_dst); + let (None, Some((other_index, other_at))) = + (unbounded[class], unbounded[usize::from(!is_dst)]) + else { + continue; + }; + + match trailing[class] { + Some((index, month, ordwk, weekday)) + if observances[index].at.saturating_add(TZ_RULE_ACTIVE_SECONDS) >= to => + { + observances[index].rrule = Some(ICalendarRecurrenceRule { + freq: ICalendarFrequency::Yearly, + bymonth: vec![ICalendarMonth::new(month, false)], + byday: vec![ICalendarDay { + ordwk: Some(ordwk), + weekday, + }], + ..Default::default() + }); + unbounded[class] = Some((index, observances[index].at)); + } + _ => { + if let Some(rrule) = observances[other_index].rrule.as_mut() { + rrule.until = Some(PartialDateTime::from_utc_timestamp(other_at)); + unbounded[usize::from(!is_dst)] = None; + } + } + } + } + + let mut index = 0; + observances.retain(|_| { + let keep = !discard[index]; + index += 1; + keep + }); + + observances +} + +fn find_transition(tz: chrono_tz::Tz, mut low: i64, mut high: i64, low_state: TzState) -> i64 { + while high - low > 1 { + let middle = low + (high - low) / 2; + if tz_state_at(tz, middle) == low_state { + low = middle; + } else { + high = middle; + } + } + high +} + +fn tz_state_at(tz: chrono_tz::Tz, timestamp: i64) -> TzState { + let offset = tz.offset_from_utc_datetime(&naive_utc(timestamp)); + + TzState { + offset: offset.fix().local_minus_utc(), + is_dst: offset.dst_offset().num_seconds() != 0, + } +} + +fn tz_observance_at(tz: chrono_tz::Tz, timestamp: i64) -> (TzState, Option) { + let offset = tz.offset_from_utc_datetime(&naive_utc(timestamp)); + + ( + TzState { + offset: offset.fix().local_minus_utc(), + is_dst: offset.dst_offset().num_seconds() != 0, + }, + offset.abbreviation().map(|name| name.to_string()), + ) +} + +fn tz_name_at(tz: chrono_tz::Tz, timestamp: i64) -> Option { + tz.offset_from_utc_datetime(&naive_utc(timestamp)) + .abbreviation() + .map(|name| name.to_string()) +} + +fn estimated_observances(from: i64, to: i64) -> usize { + let years = to.saturating_sub(from) / TZ_RULE_ACTIVE_SECONDS; + + (years.clamp(0, 1024) as usize) * 2 + 2 +} + +fn round_to_minute(offset: i32) -> i32 { + let minutes = (offset.unsigned_abs() + 30) / 60 * 60; + + if offset < 0 { + -(minutes as i32) + } else { + minutes as i32 + } +} + +fn offset_to_partial(offset: i32) -> PartialDateTime { + let seconds = offset.unsigned_abs(); + + PartialDateTime { + tz_hour: u8::try_from(seconds / 3600).ok(), + tz_minute: u8::try_from((seconds % 3600) / 60).ok(), + tz_minus: offset < 0, + ..Default::default() + } +} + +fn recurrence_span_of(freq: &ICalendarFrequency, count: u32, interval: Option) -> i64 { + let unit = match freq { + ICalendarFrequency::Yearly => 366 * 86400, + ICalendarFrequency::Monthly => 31 * 86400, + ICalendarFrequency::Weekly => 7 * 86400, + ICalendarFrequency::Daily => 86400, + ICalendarFrequency::Hourly => 3600, + ICalendarFrequency::Minutely => 60, + ICalendarFrequency::Secondly => 1, + }; + + i64::from(count) + .saturating_mul(i64::from(interval.unwrap_or(1))) + .saturating_mul(unit) +} + +fn expand_range(range: &mut Option<(i64, i64)>, value: &PartialDateTime) { + let Some(timestamp) = value.to_date_time().map(|result| { + result.date_time.and_utc().timestamp() + - result + .offset + .map_or(0, |offset| offset.local_minus_utc() as i64) + }) else { + return; + }; + + match range { + Some((min, max)) => { + *min = (*min).min(timestamp); + *max = (*max).max(timestamp); + } + None => { + *range = Some((timestamp, timestamp)); + } + } +} + +fn naive_utc(timestamp: i64) -> NaiveDateTime { + chrono::DateTime::from_timestamp(timestamp, 0) + .unwrap_or_default() + .naive_utc() +} + +fn year_of(timestamp: i64) -> i32 { + naive_utc(timestamp).year() +} + +fn start_of_year(year: i32) -> i64 { + NaiveDate::from_ymd_opt(year, 1, 1) + .and_then(|date| date.and_hms_opt(0, 0, 0)) + .map_or(0, |date_time| date_time.and_utc().timestamp()) +} + +fn days_in_month(year: i32, month: u32) -> u32 { + let (year, month) = if month == 12 { + (year.saturating_add(1), 1) + } else { + (year, month + 1) + }; + + NaiveDate::from_ymd_opt(year, month, 1) + .and_then(|date| date.pred_opt()) + .map_or(31, |date| date.day()) +} + +fn weekday_of(weekday: Weekday) -> ICalendarWeekday { + match weekday { + Weekday::Mon => ICalendarWeekday::Monday, + Weekday::Tue => ICalendarWeekday::Tuesday, + Weekday::Wed => ICalendarWeekday::Wednesday, + Weekday::Thu => ICalendarWeekday::Thursday, + Weekday::Fri => ICalendarWeekday::Friday, + Weekday::Sat => ICalendarWeekday::Saturday, + Weekday::Sun => ICalendarWeekday::Sunday, + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::icalendar::{ICalendar, ICalendarComponent, ICalendarComponentType}; + + const ORACLE_STEP: i64 = 3600; + const HORIZON_YEARS: i32 = 20; + const MAX_DIVERGENCE: f64 = 0.15; + + fn add_timezones(ical: &str) -> String { + let mut ical = ICalendar::parse(ical).expect("failed to parse iCalendar object"); + ical.add_missing_timezones(); + let result = ical.to_string(); + + let reparsed = ICalendar::parse(&result).expect("generated object failed to re-parse"); + assert_eq!( + reparsed.to_string(), + result, + "generated object is not stable" + ); + + result.replace("\r\n", "\n") + } + + fn event(properties: &str) -> String { + format!( + "BEGIN:VCALENDAR\r\nBEGIN:VEVENT\r\nUID:test\r\n{properties}END:VEVENT\r\nEND:VCALENDAR\r\n" + ) + } + + #[test] + fn test_add_missing_timezones() { + for (properties, expected) in [ + ( + "DTSTART;TZID=Europe/Berlin:20260822T163000\r\nDTEND;TZID=Europe/Berlin:20260822T173000\r\n", + concat!( + "BEGIN:VTIMEZONE\n", + "TZID:Europe/Berlin\n", + "BEGIN:STANDARD\n", + "DTSTART:20251231T010000\nTZOFFSETFROM:+0100\nTZOFFSETTO:+0100\nTZNAME:CET\n", + "END:STANDARD\n", + "BEGIN:DAYLIGHT\n", + "DTSTART:20260329T020000\nTZOFFSETFROM:+0100\nTZOFFSETTO:+0200\nTZNAME:CEST\n", + "RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=3\n", + "END:DAYLIGHT\n", + "BEGIN:STANDARD\n", + "DTSTART:20261025T030000\nTZOFFSETFROM:+0200\nTZOFFSETTO:+0100\nTZNAME:CET\n", + "RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=10\n", + "END:STANDARD\n", + "END:VTIMEZONE\n", + ), + ), + ( + "DTSTART;TZID=America/New_York:20260822T163000\r\n", + concat!( + "BEGIN:VTIMEZONE\n", + "TZID:America/New_York\n", + "BEGIN:STANDARD\n", + "DTSTART:20251230T190000\nTZOFFSETFROM:-0500\nTZOFFSETTO:-0500\nTZNAME:EST\n", + "END:STANDARD\n", + "BEGIN:DAYLIGHT\n", + "DTSTART:20260308T020000\nTZOFFSETFROM:-0500\nTZOFFSETTO:-0400\nTZNAME:EDT\n", + "RRULE:FREQ=YEARLY;BYDAY=2SU;BYMONTH=3\n", + "END:DAYLIGHT\n", + "BEGIN:STANDARD\n", + "DTSTART:20261101T020000\nTZOFFSETFROM:-0400\nTZOFFSETTO:-0500\nTZNAME:EST\n", + "RRULE:FREQ=YEARLY;BYDAY=1SU;BYMONTH=11\n", + "END:STANDARD\n", + "END:VTIMEZONE\n", + ), + ), + ( + "DTSTART;TZID=Australia/Sydney:20260822T163000\r\n", + concat!( + "BEGIN:VTIMEZONE\n", + "TZID:Australia/Sydney\n", + "BEGIN:DAYLIGHT\n", + "DTSTART:20251231T110000\nTZOFFSETFROM:+1100\nTZOFFSETTO:+1100\nTZNAME:AEDT\n", + "END:DAYLIGHT\n", + "BEGIN:STANDARD\n", + "DTSTART:20260405T030000\nTZOFFSETFROM:+1100\nTZOFFSETTO:+1000\nTZNAME:AEST\n", + "RRULE:FREQ=YEARLY;BYDAY=1SU;BYMONTH=4\n", + "END:STANDARD\n", + "BEGIN:DAYLIGHT\n", + "DTSTART:20261004T020000\nTZOFFSETFROM:+1000\nTZOFFSETTO:+1100\nTZNAME:AEDT\n", + "RRULE:FREQ=YEARLY;BYDAY=1SU;BYMONTH=10\n", + "END:DAYLIGHT\n", + "END:VTIMEZONE\n", + ), + ), + ( + "DTSTART;TZID=Asia/Kolkata:20260822T163000\r\n", + concat!( + "BEGIN:VTIMEZONE\n", + "TZID:Asia/Kolkata\n", + "BEGIN:STANDARD\n", + "DTSTART:20251231T053000\nTZOFFSETFROM:+0530\nTZOFFSETTO:+0530\nTZNAME:IST\n", + "END:STANDARD\n", + "END:VTIMEZONE\n", + ), + ), + ( + "DTSTART;TZID=America/Sao_Paulo:20180301T163000\r\nRRULE:FREQ=MONTHLY;UNTIL=20190401T000000Z\r\n", + concat!( + "BEGIN:VTIMEZONE\n", + "TZID:America/Sao_Paulo\n", + "BEGIN:DAYLIGHT\n", + "DTSTART:20171230T220000\nTZOFFSETFROM:-0200\nTZOFFSETTO:-0200\n", + "END:DAYLIGHT\n", + "BEGIN:STANDARD\n", + "DTSTART:20180218T000000\nTZOFFSETFROM:-0200\nTZOFFSETTO:-0300\n", + "RRULE:FREQ=YEARLY;UNTIL=20190217T020000Z;BYDAY=3SU;BYMONTH=2\n", + "END:STANDARD\n", + "BEGIN:DAYLIGHT\n", + "DTSTART:20181104T000000\nTZOFFSETFROM:-0300\nTZOFFSETTO:-0200\n", + "END:DAYLIGHT\n", + "END:VTIMEZONE\n", + ), + ), + ( + // Negative daylight saving: the lower offset is the STANDARD observance, + // matching what other implementations publish for Europe/Dublin + "DTSTART;TZID=Europe/Dublin:20260822T163000\r\n", + concat!( + "BEGIN:VTIMEZONE\n", + "TZID:Europe/Dublin\n", + "BEGIN:STANDARD\n", + "DTSTART:20251231T000000\nTZOFFSETFROM:+0000\nTZOFFSETTO:+0000\nTZNAME:GMT\n", + "END:STANDARD\n", + "BEGIN:DAYLIGHT\n", + "DTSTART:20260329T010000\nTZOFFSETFROM:+0000\nTZOFFSETTO:+0100\nTZNAME:IST\n", + "RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=3\n", + "END:DAYLIGHT\n", + "BEGIN:STANDARD\n", + "DTSTART:20261025T020000\nTZOFFSETFROM:+0100\nTZOFFSETTO:+0000\nTZNAME:GMT\n", + "RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=10\n", + "END:STANDARD\n", + "END:VTIMEZONE\n", + ), + ), + ] { + let result = add_timezones(&event(properties)); + assert!( + result.contains(expected), + "expected:\n{expected}\ngot:\n{result}" + ); + assert!( + result.contains("BEGIN:VTIMEZONE"), + "no definition generated:\n{result}" + ); + assert!( + result.find("BEGIN:VTIMEZONE") < result.find("BEGIN:VEVENT"), + "VTIMEZONE must precede VEVENT:\n{result}" + ); + } + } + + #[test] + fn test_add_missing_timezones_multiple() { + // One definition per referenced identifier, all of them ahead of the event + let result = add_timezones(&event(concat!( + "DTSTART;TZID=Europe/Berlin:20260822T163000\r\n", + "DTEND;TZID=America/New_York:20260822T173000\r\n", + ))); + assert!(result.contains("TZID:Europe/Berlin"), "{result}"); + assert!(result.contains("TZID:America/New_York"), "{result}"); + assert_eq!(result.matches("BEGIN:VTIMEZONE").count(), 2, "{result}"); + assert!( + result.find("BEGIN:VEVENT") > result.rfind("END:VTIMEZONE"), + "every VTIMEZONE must precede VEVENT:\n{result}" + ); + + // A definition added next to an existing one keeps both ahead of the event + let result = add_timezones(concat!( + "BEGIN:VCALENDAR\r\n", + "BEGIN:VTIMEZONE\r\nTZID:Europe/Berlin\r\n", + "BEGIN:STANDARD\r\nDTSTART:19961027T030000\r\nTZOFFSETFROM:+0200\r\nTZOFFSETTO:+0100\r\n", + "END:STANDARD\r\nEND:VTIMEZONE\r\n", + "BEGIN:VEVENT\r\nUID:test\r\n", + "DTSTART;TZID=Europe/Berlin:20260822T163000\r\n", + "DTEND;TZID=Asia/Kolkata:20260822T173000\r\n", + "END:VEVENT\r\nEND:VCALENDAR\r\n" + )); + assert_eq!(result.matches("BEGIN:VTIMEZONE").count(), 2, "{result}"); + assert!( + result.find("BEGIN:VEVENT") > result.rfind("END:VTIMEZONE"), + "every VTIMEZONE must precede VEVENT:\n{result}" + ); + + // Windows style identifiers resolve and are preserved verbatim + for tz_id in [ + "Pacific Standard Time", + "(UTC-08:00) Pacific Time (US & Canada)", + ] { + let result = add_timezones(&event(&format!( + "DTSTART;TZID=\"{tz_id}\":20260822T163000\r\n" + ))); + assert!(result.contains(&format!("TZID:{tz_id}\n")), "{result}"); + assert!(result.contains("TZOFFSETTO:-0700"), "{result}"); + } + + // Fixed offset identifiers yield a single observance + let result = add_timezones(&event("DTSTART;TZID=Etc/GMT+5:20260822T163000\r\n")); + assert!( + result.contains("TZOFFSETFROM:-0500\nTZOFFSETTO:-0500"), + "{result}" + ); + assert_eq!(result.matches("BEGIN:STANDARD").count(), 1, "{result}"); + } + + #[test] + fn test_add_missing_timezones_noop() { + // Time zones that are already defined are left untouched + let defined = concat!( + "BEGIN:VCALENDAR\r\n", + "BEGIN:VTIMEZONE\r\nTZID:Europe/Berlin\r\n", + "BEGIN:STANDARD\r\nDTSTART:19961027T030000\r\nTZOFFSETFROM:+0200\r\nTZOFFSETTO:+0100\r\n", + "END:STANDARD\r\nEND:VTIMEZONE\r\n", + "BEGIN:VEVENT\r\nUID:test\r\nDTSTART;TZID=Europe/Berlin:20260822T163000\r\n", + "END:VEVENT\r\nEND:VCALENDAR\r\n" + ); + let mut ical = ICalendar::parse(defined).unwrap(); + assert_eq!(ical.add_missing_timezones(), 0); + assert_eq!(ical.timezones().count(), 1); + + // Unresolvable time zone identifiers are skipped + let mut ical = + ICalendar::parse(event("DTSTART;TZID=Nowhere/Atlantis:20260822T163000\r\n")).unwrap(); + assert_eq!(ical.add_missing_timezones(), 0); + assert_eq!(ical.timezones().count(), 0); + + // Objects without TZID references need no definitions + let mut ical = ICalendar::parse(event("DTSTART:20260822T163000Z\r\n")).unwrap(); + assert_eq!(ical.add_missing_timezones(), 0); + assert_eq!(ical.timezones().count(), 0); + + // A VTIMEZONE cannot be nested within another component + let mut ical = ICalendar { + components: vec![ICalendarComponent::new(ICalendarComponentType::VEvent)], + }; + assert_eq!(ical.add_timezone("Europe/Berlin", 0, TZ_RANGE_MARGIN), None); + assert_eq!( + ICalendar::default().add_timezone("Europe/Berlin", 0, TZ_RANGE_MARGIN), + None + ); + } + + #[test] + fn test_add_missing_timezones_covers_recurrence_range() { + // The window must reach the last instance of a counted recurrence + let mut ical = ICalendar::parse(event(concat!( + "DTSTART;TZID=Europe/Berlin:20260822T163000\r\n", + "RRULE:FREQ=YEARLY;COUNT=10\r\n" + ))) + .unwrap(); + assert_eq!(ical.add_missing_timezones(), 1); + assert!( + ical.to_string() + .contains("RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=3\r\n"), + "{ical}" + ); + + let tz_component_id = ical + .components + .iter() + .position(|comp| comp.component_type == ICalendarComponentType::VTimezone) + .expect("no definition generated") as u32; + let onsets = implied_onsets(&ical, tz_component_id); + let last = onsets.last().expect("no onsets").0; + assert!( + last >= start_of_year(2035), + "definition ends at {last}, before the last instance" + ); + } + + #[test] + fn test_generated_definitions_match_tzdb() { + for (tz_id, from_year, to_year) in [ + ("Europe/Berlin", 2024, 2028), + ("Europe/Berlin", 1975, 2000), + ("Europe/Dublin", 2024, 2028), + ("America/New_York", 2024, 2028), + ("America/New_York", 1965, 1990), + ("Australia/Sydney", 2024, 2028), + ("Pacific/Chatham", 2024, 2028), + ("Australia/Lord_Howe", 2024, 2028), + ("America/Santiago", 2015, 2025), + ("America/Sao_Paulo", 2015, 2025), + ("Asia/Tehran", 2015, 2025), + ("Asia/Gaza", 2024, 2028), + ("Africa/Casablanca", 2018, 2025), + ("Africa/Monrovia", 1970, 1973), + ("Asia/Kolkata", 2024, 2028), + ("UTC", 2024, 2028), + ] { + let (from, to) = (start_of_year(from_year), start_of_year(to_year)); + let (tz, onsets) = generate(tz_id, from, to); + + // Inside the covered range the definition must reproduce the database exactly + let mut at = from; + while at < to { + let implied = offset_at(&onsets, at) + .unwrap_or_else(|| panic!("{tz_id}: no observance covers {at}")); + let actual = tz_state_at(tz, at).offset; + assert!( + implied.abs_diff(actual) <= 30, + "{tz_id}: offset {implied} does not match {actual} at {at}" + ); + at += ORACLE_STEP; + } + } + } + + #[test] + fn test_generated_definitions_do_not_drift() { + // Rules left unbounded are an approximation of the future, but they must never + // strand the zone on the wrong side of a daylight saving transition + for (tz_id, from_year, to_year) in [ + ("Europe/Berlin", 2024, 2028), + ("Europe/Dublin", 2024, 2028), + ("America/New_York", 2024, 2028), + ("Australia/Sydney", 2024, 2028), + ("Australia/Lord_Howe", 2024, 2028), + ("Asia/Gaza", 2024, 2028), + ("Asia/Hebron", 2024, 2028), + ("Asia/Jerusalem", 2024, 2028), + ("America/Santiago", 2024, 2028), + ("America/Nuuk", 2024, 2028), + ("Africa/Cairo", 2024, 2028), + ("Africa/Casablanca", 2024, 2028), + ("America/Sao_Paulo", 2024, 2028), + ("Asia/Kolkata", 2024, 2028), + ] { + let (from, to) = (start_of_year(from_year), start_of_year(to_year)); + let (tz, onsets) = generate(tz_id, from, to); + + let horizon = start_of_year(to_year + HORIZON_YEARS); + let (mut diverged, mut samples) = (0u32, 0u32); + let mut at = to; + while at < horizon { + if offset_at(&onsets, at) != Some(tz_state_at(tz, at).offset) { + diverged += 1; + } + samples += 1; + at += ORACLE_STEP; + } + + let ratio = f64::from(diverged) / f64::from(samples); + assert!( + ratio < MAX_DIVERGENCE, + "{tz_id}: definition drifts for {:.1}% of the {HORIZON_YEARS} years after the covered range", + ratio * 100.0 + ); + } + } + + fn generate(tz_id: &str, from: i64, to: i64) -> (chrono_tz::Tz, Vec<(i64, i32)>) { + let Ok(Tz::Tz(tz)) = Tz::from_str(tz_id) else { + panic!("{tz_id} did not resolve to an IANA time zone"); + }; + + let mut ical = ICalendar { + components: vec![ICalendarComponent::new(ICalendarComponentType::VCalendar)], + }; + let tz_component_id = ical + .add_timezone(tz_id, from, to) + .unwrap_or_else(|| panic!("no definition generated for {tz_id}")); + + (tz, implied_onsets(&ical, tz_component_id)) + } + + /// Expands the definition into the onsets it describes, exactly as a client would, + /// without reusing anything the generator relies on to find transitions. + fn implied_onsets(ical: &ICalendar, tz_component_id: u32) -> Vec<(i64, i32)> { + let mut onsets = Vec::new(); + let timezone = &ical.components[tz_component_id as usize]; + assert_eq!(timezone.component_type, ICalendarComponentType::VTimezone); + + for component_id in &timezone.component_ids { + let component = &ical.components[*component_id as usize]; + assert!(matches!( + component.component_type, + ICalendarComponentType::Standard | ICalendarComponentType::Daylight + )); + + let date_time = observance_date_time(component); + let from_offset = observance_offset(component, &ICalendarProperty::Tzoffsetfrom); + let to_offset = observance_offset(component, &ICalendarProperty::Tzoffsetto); + onsets.push(( + date_time.and_utc().timestamp() - i64::from(from_offset), + to_offset, + )); + + let Some(rule) = component + .entries + .iter() + .find(|entry| entry.name == ICalendarProperty::Rrule) + .and_then(|entry| match entry.values.first() { + Some(ICalendarValue::RecurrenceRule(rule)) => Some(rule), + _ => None, + }) + else { + continue; + }; + + assert_eq!(rule.freq, ICalendarFrequency::Yearly); + assert_eq!(rule.byday.len(), 1); + assert_eq!(rule.bymonth.len(), 1); + assert!(rule.count.is_none()); + + let day = rule.byday[0]; + let month = rule.bymonth[0].month(); + let until = rule + .until + .as_ref() + .and_then(|until| until.to_date_time()) + .map(|until| until.date_time.and_utc().timestamp()); + + for year in date_time.year() + 1..=date_time.year() + 200 { + let Some(date) = + nth_weekday(year, month, day.ordwk.unwrap_or_default(), day.weekday) + else { + continue; + }; + let onset = + date.and_time(date_time.time()).and_utc().timestamp() - i64::from(from_offset); + if until.is_some_and(|until| onset > until) { + break; + } + onsets.push((onset, to_offset)); + } + } + + onsets.sort_unstable(); + onsets + } + + /// RFC 5545, Section 3.6.5: the offset in effect is the one of the observance with the + /// last onset before the time in question. + fn offset_at(onsets: &[(i64, i32)], at: i64) -> Option { + onsets + .partition_point(|(onset, _)| *onset <= at) + .checked_sub(1) + .map(|position| onsets[position].1) + } + + fn observance_value<'x>( + component: &'x ICalendarComponent, + name: &ICalendarProperty, + ) -> &'x ICalendarValue { + component + .entries + .iter() + .find(|entry| &entry.name == name) + .and_then(|entry| entry.values.first()) + .unwrap_or_else(|| panic!("missing {name:?}")) + } + + fn observance_offset(component: &ICalendarComponent, name: &ICalendarProperty) -> i32 { + let ICalendarValue::PartialDateTime(offset) = observance_value(component, name) else { + panic!("{name:?} is not a UTC offset"); + }; + let seconds = i32::from(offset.tz_hour.unwrap_or_default()) * 3600 + + i32::from(offset.tz_minute.unwrap_or_default()) * 60; + + if offset.tz_minus { -seconds } else { seconds } + } + + fn observance_date_time(component: &ICalendarComponent) -> NaiveDateTime { + let ICalendarValue::PartialDateTime(dtstart) = + observance_value(component, &ICalendarProperty::Dtstart) + else { + panic!("DTSTART is not a date-time"); + }; + assert!( + dtstart.tz_hour.is_none() && dtstart.tz_minute.is_none(), + "DTSTART must be a local time value" + ); + + dtstart.to_date_time().expect("invalid DTSTART").date_time + } + + fn nth_weekday( + year: i32, + month: u8, + ordwk: i16, + weekday: ICalendarWeekday, + ) -> Option { + let matches = (1..=days_in_month(year, u32::from(month))) + .filter_map(|day| NaiveDate::from_ymd_opt(year, u32::from(month), day)) + .filter(|date| weekday_of(date.weekday()) == weekday) + .collect::>(); + + if ordwk < 0 { + matches + .len() + .checked_sub(usize::from(ordwk.unsigned_abs())) + .and_then(|index| matches.get(index)) + .copied() + } else { + matches.get((ordwk as usize).saturating_sub(1)).copied() + } + } +} diff --git a/vendor/calcard/src/icalendar/types.rs b/vendor/calcard/src/icalendar/types.rs new file mode 100644 index 00000000..e719a38b --- /dev/null +++ b/vendor/calcard/src/icalendar/types.rs @@ -0,0 +1,1458 @@ +/* + * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC + * + * SPDX-License-Identifier: Apache-2.0 OR MIT + */ + +use super::*; +use crate::{Entry, Parser}; +use std::borrow::Cow; + +impl IanaParse for ICalendarProperty { + fn parse(value: &[u8]) -> Option { + hashify::tiny_map_ignore_case!(value, + "CALSCALE" => ICalendarProperty::Calscale, + "METHOD" => ICalendarProperty::Method, + "PRODID" => ICalendarProperty::Prodid, + "VERSION" => ICalendarProperty::Version, + "ATTACH" => ICalendarProperty::Attach, + "CATEGORIES" => ICalendarProperty::Categories, + "CLASS" => ICalendarProperty::Class, + "COMMENT" => ICalendarProperty::Comment, + "DESCRIPTION" => ICalendarProperty::Description, + "GEO" => ICalendarProperty::Geo, + "LOCATION" => ICalendarProperty::Location, + "PERCENT-COMPLETE" => ICalendarProperty::PercentComplete, + "PRIORITY" => ICalendarProperty::Priority, + "RESOURCES" => ICalendarProperty::Resources, + "STATUS" => ICalendarProperty::Status, + "SUMMARY" => ICalendarProperty::Summary, + "COMPLETED" => ICalendarProperty::Completed, + "DTEND" => ICalendarProperty::Dtend, + "DUE" => ICalendarProperty::Due, + "DTSTART" => ICalendarProperty::Dtstart, + "DURATION" => ICalendarProperty::Duration, + "FREEBUSY" => ICalendarProperty::Freebusy, + "TRANSP" => ICalendarProperty::Transp, + "TZID" => ICalendarProperty::Tzid, + "TZNAME" => ICalendarProperty::Tzname, + "TZOFFSETFROM" => ICalendarProperty::Tzoffsetfrom, + "TZOFFSETTO" => ICalendarProperty::Tzoffsetto, + "TZURL" => ICalendarProperty::Tzurl, + "ATTENDEE" => ICalendarProperty::Attendee, + "CONTACT" => ICalendarProperty::Contact, + "ORGANIZER" => ICalendarProperty::Organizer, + "RECURRENCE-ID" => ICalendarProperty::RecurrenceId, + "RELATED-TO" => ICalendarProperty::RelatedTo, + "URL" => ICalendarProperty::Url, + "UID" => ICalendarProperty::Uid, + "EXDATE" => ICalendarProperty::Exdate, + "EXRULE" => ICalendarProperty::Exrule, + "RDATE" => ICalendarProperty::Rdate, + "RRULE" => ICalendarProperty::Rrule, + "ACTION" => ICalendarProperty::Action, + "REPEAT" => ICalendarProperty::Repeat, + "TRIGGER" => ICalendarProperty::Trigger, + "CREATED" => ICalendarProperty::Created, + "DTSTAMP" => ICalendarProperty::Dtstamp, + "LAST-MODIFIED" => ICalendarProperty::LastModified, + "SEQUENCE" => ICalendarProperty::Sequence, + "REQUEST-STATUS" => ICalendarProperty::RequestStatus, + "XML" => ICalendarProperty::Xml, + "TZUNTIL" => ICalendarProperty::Tzuntil, + "TZID-ALIAS-OF" => ICalendarProperty::TzidAliasOf, + "BUSYTYPE" => ICalendarProperty::Busytype, + "NAME" => ICalendarProperty::Name, + "REFRESH-INTERVAL" => ICalendarProperty::RefreshInterval, + "SOURCE" => ICalendarProperty::Source, + "COLOR" => ICalendarProperty::Color, + "IMAGE" => ICalendarProperty::Image, + "CONFERENCE" => ICalendarProperty::Conference, + "CALENDAR-ADDRESS" => ICalendarProperty::CalendarAddress, + "LOCATION-TYPE" => ICalendarProperty::LocationType, + "PARTICIPANT-TYPE" => ICalendarProperty::ParticipantType, + "RESOURCE-TYPE" => ICalendarProperty::ResourceType, + "STRUCTURED-DATA" => ICalendarProperty::StructuredData, + "STYLED-DESCRIPTION" => ICalendarProperty::StyledDescription, + "ACKNOWLEDGED" => ICalendarProperty::Acknowledged, + "PROXIMITY" => ICalendarProperty::Proximity, + "CONCEPT" => ICalendarProperty::Concept, + "LINK" => ICalendarProperty::Link, + "REFID" => ICalendarProperty::Refid, + "COORDINATES" => ICalendarProperty::Coordinates, + "SHOW-WITHOUT-TIME" => ICalendarProperty::ShowWithoutTime, + "JSID" => ICalendarProperty::Jsid, + "JSPROP" => ICalendarProperty::Jsprop, + "BEGIN" => ICalendarProperty::Begin, + "END" => ICalendarProperty::End, + "ESTIMATED-DURATION" => ICalendarProperty::EstimatedDuration, + "REASON" => ICalendarProperty::Reason, + "SUBSTATE" => ICalendarProperty::Substate, + "TASK-MODE" => ICalendarProperty::TaskMode, + ) + } +} + +impl ICalendarProperty { + pub fn as_str(&self) -> &str { + match self { + ICalendarProperty::Calscale => "CALSCALE", + ICalendarProperty::Method => "METHOD", + ICalendarProperty::Prodid => "PRODID", + ICalendarProperty::Version => "VERSION", + ICalendarProperty::Attach => "ATTACH", + ICalendarProperty::Categories => "CATEGORIES", + ICalendarProperty::Class => "CLASS", + ICalendarProperty::Comment => "COMMENT", + ICalendarProperty::Description => "DESCRIPTION", + ICalendarProperty::Geo => "GEO", + ICalendarProperty::Location => "LOCATION", + ICalendarProperty::PercentComplete => "PERCENT-COMPLETE", + ICalendarProperty::Priority => "PRIORITY", + ICalendarProperty::Resources => "RESOURCES", + ICalendarProperty::Status => "STATUS", + ICalendarProperty::Summary => "SUMMARY", + ICalendarProperty::Completed => "COMPLETED", + ICalendarProperty::Dtend => "DTEND", + ICalendarProperty::Due => "DUE", + ICalendarProperty::Dtstart => "DTSTART", + ICalendarProperty::Duration => "DURATION", + ICalendarProperty::Freebusy => "FREEBUSY", + ICalendarProperty::Transp => "TRANSP", + ICalendarProperty::Tzid => "TZID", + ICalendarProperty::Tzname => "TZNAME", + ICalendarProperty::Tzoffsetfrom => "TZOFFSETFROM", + ICalendarProperty::Tzoffsetto => "TZOFFSETTO", + ICalendarProperty::Tzurl => "TZURL", + ICalendarProperty::Attendee => "ATTENDEE", + ICalendarProperty::Contact => "CONTACT", + ICalendarProperty::Organizer => "ORGANIZER", + ICalendarProperty::RecurrenceId => "RECURRENCE-ID", + ICalendarProperty::RelatedTo => "RELATED-TO", + ICalendarProperty::Url => "URL", + ICalendarProperty::Uid => "UID", + ICalendarProperty::Exdate => "EXDATE", + ICalendarProperty::Exrule => "EXRULE", + ICalendarProperty::Rdate => "RDATE", + ICalendarProperty::Rrule => "RRULE", + ICalendarProperty::Action => "ACTION", + ICalendarProperty::Repeat => "REPEAT", + ICalendarProperty::Trigger => "TRIGGER", + ICalendarProperty::Created => "CREATED", + ICalendarProperty::Dtstamp => "DTSTAMP", + ICalendarProperty::LastModified => "LAST-MODIFIED", + ICalendarProperty::Sequence => "SEQUENCE", + ICalendarProperty::RequestStatus => "REQUEST-STATUS", + ICalendarProperty::Xml => "XML", + ICalendarProperty::Tzuntil => "TZUNTIL", + ICalendarProperty::TzidAliasOf => "TZID-ALIAS-OF", + ICalendarProperty::Busytype => "BUSYTYPE", + ICalendarProperty::Name => "NAME", + ICalendarProperty::RefreshInterval => "REFRESH-INTERVAL", + ICalendarProperty::Source => "SOURCE", + ICalendarProperty::Color => "COLOR", + ICalendarProperty::Image => "IMAGE", + ICalendarProperty::Conference => "CONFERENCE", + ICalendarProperty::CalendarAddress => "CALENDAR-ADDRESS", + ICalendarProperty::LocationType => "LOCATION-TYPE", + ICalendarProperty::ParticipantType => "PARTICIPANT-TYPE", + ICalendarProperty::ResourceType => "RESOURCE-TYPE", + ICalendarProperty::StructuredData => "STRUCTURED-DATA", + ICalendarProperty::StyledDescription => "STYLED-DESCRIPTION", + ICalendarProperty::Acknowledged => "ACKNOWLEDGED", + ICalendarProperty::Proximity => "PROXIMITY", + ICalendarProperty::Concept => "CONCEPT", + ICalendarProperty::Link => "LINK", + ICalendarProperty::Refid => "REFID", + ICalendarProperty::Begin => "BEGIN", + ICalendarProperty::End => "END", + ICalendarProperty::Coordinates => "COORDINATES", + ICalendarProperty::ShowWithoutTime => "SHOW-WITHOUT-TIME", + ICalendarProperty::Jsid => "JSID", + ICalendarProperty::Jsprop => "JSPROP", + ICalendarProperty::EstimatedDuration => "ESTIMATED-DURATION", + ICalendarProperty::Reason => "REASON", + ICalendarProperty::Substate => "SUBSTATE", + ICalendarProperty::TaskMode => "TASK-MODE", + ICalendarProperty::Other(s) => s.as_str(), + } + } + + pub fn into_string(self) -> Cow<'static, str> { + match self { + ICalendarProperty::Calscale => "CALSCALE", + ICalendarProperty::Method => "METHOD", + ICalendarProperty::Prodid => "PRODID", + ICalendarProperty::Version => "VERSION", + ICalendarProperty::Attach => "ATTACH", + ICalendarProperty::Categories => "CATEGORIES", + ICalendarProperty::Class => "CLASS", + ICalendarProperty::Comment => "COMMENT", + ICalendarProperty::Description => "DESCRIPTION", + ICalendarProperty::Geo => "GEO", + ICalendarProperty::Location => "LOCATION", + ICalendarProperty::PercentComplete => "PERCENT-COMPLETE", + ICalendarProperty::Priority => "PRIORITY", + ICalendarProperty::Resources => "RESOURCES", + ICalendarProperty::Status => "STATUS", + ICalendarProperty::Summary => "SUMMARY", + ICalendarProperty::Completed => "COMPLETED", + ICalendarProperty::Dtend => "DTEND", + ICalendarProperty::Due => "DUE", + ICalendarProperty::Dtstart => "DTSTART", + ICalendarProperty::Duration => "DURATION", + ICalendarProperty::Freebusy => "FREEBUSY", + ICalendarProperty::Transp => "TRANSP", + ICalendarProperty::Tzid => "TZID", + ICalendarProperty::Tzname => "TZNAME", + ICalendarProperty::Tzoffsetfrom => "TZOFFSETFROM", + ICalendarProperty::Tzoffsetto => "TZOFFSETTO", + ICalendarProperty::Tzurl => "TZURL", + ICalendarProperty::Attendee => "ATTENDEE", + ICalendarProperty::Contact => "CONTACT", + ICalendarProperty::Organizer => "ORGANIZER", + ICalendarProperty::RecurrenceId => "RECURRENCE-ID", + ICalendarProperty::RelatedTo => "RELATED-TO", + ICalendarProperty::Url => "URL", + ICalendarProperty::Uid => "UID", + ICalendarProperty::Exdate => "EXDATE", + ICalendarProperty::Exrule => "EXRULE", + ICalendarProperty::Rdate => "RDATE", + ICalendarProperty::Rrule => "RRULE", + ICalendarProperty::Action => "ACTION", + ICalendarProperty::Repeat => "REPEAT", + ICalendarProperty::Trigger => "TRIGGER", + ICalendarProperty::Created => "CREATED", + ICalendarProperty::Dtstamp => "DTSTAMP", + ICalendarProperty::LastModified => "LAST-MODIFIED", + ICalendarProperty::Sequence => "SEQUENCE", + ICalendarProperty::RequestStatus => "REQUEST-STATUS", + ICalendarProperty::Xml => "XML", + ICalendarProperty::Tzuntil => "TZUNTIL", + ICalendarProperty::TzidAliasOf => "TZID-ALIAS-OF", + ICalendarProperty::Busytype => "BUSYTYPE", + ICalendarProperty::Name => "NAME", + ICalendarProperty::RefreshInterval => "REFRESH-INTERVAL", + ICalendarProperty::Source => "SOURCE", + ICalendarProperty::Color => "COLOR", + ICalendarProperty::Image => "IMAGE", + ICalendarProperty::Conference => "CONFERENCE", + ICalendarProperty::CalendarAddress => "CALENDAR-ADDRESS", + ICalendarProperty::LocationType => "LOCATION-TYPE", + ICalendarProperty::ParticipantType => "PARTICIPANT-TYPE", + ICalendarProperty::ResourceType => "RESOURCE-TYPE", + ICalendarProperty::StructuredData => "STRUCTURED-DATA", + ICalendarProperty::StyledDescription => "STYLED-DESCRIPTION", + ICalendarProperty::Acknowledged => "ACKNOWLEDGED", + ICalendarProperty::Proximity => "PROXIMITY", + ICalendarProperty::Concept => "CONCEPT", + ICalendarProperty::Link => "LINK", + ICalendarProperty::Refid => "REFID", + ICalendarProperty::Begin => "BEGIN", + ICalendarProperty::End => "END", + ICalendarProperty::Coordinates => "COORDINATES", + ICalendarProperty::ShowWithoutTime => "SHOW-WITHOUT-TIME", + ICalendarProperty::Jsid => "JSID", + ICalendarProperty::Jsprop => "JSPROP", + ICalendarProperty::EstimatedDuration => "ESTIMATED-DURATION", + ICalendarProperty::Reason => "REASON", + ICalendarProperty::Substate => "SUBSTATE", + ICalendarProperty::TaskMode => "TASK-MODE", + ICalendarProperty::Other(s) => return Cow::Owned(s), + } + .into() + } +} + +impl ICalendarParameterName { + pub fn try_parse(input: &[u8]) -> Option { + hashify::tiny_map_ignore_case!(input, + b"ALTREP" => ICalendarParameterName::Altrep, + b"CN" => ICalendarParameterName::Cn, + b"CUTYPE" => ICalendarParameterName::Cutype, + b"DELEGATED-FROM" => ICalendarParameterName::DelegatedFrom, + b"DELEGATED-TO" => ICalendarParameterName::DelegatedTo, + b"DIR" => ICalendarParameterName::Dir, + b"FMTTYPE" => ICalendarParameterName::Fmttype, + b"FBTYPE" => ICalendarParameterName::Fbtype, + b"LANGUAGE" => ICalendarParameterName::Language, + b"MEMBER" => ICalendarParameterName::Member, + b"PARTSTAT" => ICalendarParameterName::Partstat, + b"RANGE" => ICalendarParameterName::Range, + b"RELATED" => ICalendarParameterName::Related, + b"RELTYPE" => ICalendarParameterName::Reltype, + b"ROLE" => ICalendarParameterName::Role, + b"RSVP" => ICalendarParameterName::Rsvp, + b"SCHEDULE-AGENT" => ICalendarParameterName::ScheduleAgent, + b"SCHEDULE-FORCE-SEND" => ICalendarParameterName::ScheduleForceSend, + b"SCHEDULE-STATUS" => ICalendarParameterName::ScheduleStatus, + b"SENT-BY" => ICalendarParameterName::SentBy, + b"TZID" => ICalendarParameterName::Tzid, + b"VALUE" => ICalendarParameterName::Value, + b"DISPLAY" => ICalendarParameterName::Display, + b"EMAIL" => ICalendarParameterName::Email, + b"FEATURE" => ICalendarParameterName::Feature, + b"LABEL" => ICalendarParameterName::Label, + b"SIZE" => ICalendarParameterName::Size, + b"FILENAME" => ICalendarParameterName::Filename, + b"MANAGED-ID" => ICalendarParameterName::ManagedId, + b"ORDER" => ICalendarParameterName::Order, + b"SCHEMA" => ICalendarParameterName::Schema, + b"DERIVED" => ICalendarParameterName::Derived, + b"GAP" => ICalendarParameterName::Gap, + b"LINKREL" => ICalendarParameterName::Linkrel, + b"JSPTR" => ICalendarParameterName::Jsptr, + b"JSID" => ICalendarParameterName::Jsid, + ) + } + + pub fn as_str(&self) -> &str { + match self { + ICalendarParameterName::Altrep => "ALTREP", + ICalendarParameterName::Cn => "CN", + ICalendarParameterName::Cutype => "CUTYPE", + ICalendarParameterName::DelegatedFrom => "DELEGATED-FROM", + ICalendarParameterName::DelegatedTo => "DELEGATED-TO", + ICalendarParameterName::Dir => "DIR", + ICalendarParameterName::Fmttype => "FMTTYPE", + ICalendarParameterName::Fbtype => "FBTYPE", + ICalendarParameterName::Language => "LANGUAGE", + ICalendarParameterName::Member => "MEMBER", + ICalendarParameterName::Partstat => "PARTSTAT", + ICalendarParameterName::Range => "RANGE", + ICalendarParameterName::Related => "RELATED", + ICalendarParameterName::Reltype => "RELTYPE", + ICalendarParameterName::Role => "ROLE", + ICalendarParameterName::Rsvp => "RSVP", + ICalendarParameterName::ScheduleAgent => "SCHEDULE-AGENT", + ICalendarParameterName::ScheduleForceSend => "SCHEDULE-FORCE-SEND", + ICalendarParameterName::ScheduleStatus => "SCHEDULE-STATUS", + ICalendarParameterName::SentBy => "SENT-BY", + ICalendarParameterName::Tzid => "TZID", + ICalendarParameterName::Value => "VALUE", + ICalendarParameterName::Display => "DISPLAY", + ICalendarParameterName::Email => "EMAIL", + ICalendarParameterName::Feature => "FEATURE", + ICalendarParameterName::Label => "LABEL", + ICalendarParameterName::Size => "SIZE", + ICalendarParameterName::Filename => "FILENAME", + ICalendarParameterName::ManagedId => "MANAGED-ID", + ICalendarParameterName::Order => "ORDER", + ICalendarParameterName::Schema => "SCHEMA", + ICalendarParameterName::Derived => "DERIVED", + ICalendarParameterName::Gap => "GAP", + ICalendarParameterName::Linkrel => "LINKREL", + ICalendarParameterName::Jsptr => "JSPTR", + ICalendarParameterName::Jsid => "JSID", + ICalendarParameterName::Other(name) => name.as_str(), + } + } + + pub fn into_string(self) -> Cow<'static, str> { + match self { + ICalendarParameterName::Altrep => "ALTREP", + ICalendarParameterName::Cn => "CN", + ICalendarParameterName::Cutype => "CUTYPE", + ICalendarParameterName::DelegatedFrom => "DELEGATED-FROM", + ICalendarParameterName::DelegatedTo => "DELEGATED-TO", + ICalendarParameterName::Dir => "DIR", + ICalendarParameterName::Fmttype => "FMTTYPE", + ICalendarParameterName::Fbtype => "FBTYPE", + ICalendarParameterName::Language => "LANGUAGE", + ICalendarParameterName::Member => "MEMBER", + ICalendarParameterName::Partstat => "PARTSTAT", + ICalendarParameterName::Range => "RANGE", + ICalendarParameterName::Related => "RELATED", + ICalendarParameterName::Reltype => "RELTYPE", + ICalendarParameterName::Role => "ROLE", + ICalendarParameterName::Rsvp => "RSVP", + ICalendarParameterName::ScheduleAgent => "SCHEDULE-AGENT", + ICalendarParameterName::ScheduleForceSend => "SCHEDULE-FORCE-SEND", + ICalendarParameterName::ScheduleStatus => "SCHEDULE-STATUS", + ICalendarParameterName::SentBy => "SENT-BY", + ICalendarParameterName::Tzid => "TZID", + ICalendarParameterName::Value => "VALUE", + ICalendarParameterName::Display => "DISPLAY", + ICalendarParameterName::Email => "EMAIL", + ICalendarParameterName::Feature => "FEATURE", + ICalendarParameterName::Label => "LABEL", + ICalendarParameterName::Size => "SIZE", + ICalendarParameterName::Filename => "FILENAME", + ICalendarParameterName::ManagedId => "MANAGED-ID", + ICalendarParameterName::Order => "ORDER", + ICalendarParameterName::Schema => "SCHEMA", + ICalendarParameterName::Derived => "DERIVED", + ICalendarParameterName::Gap => "GAP", + ICalendarParameterName::Linkrel => "LINKREL", + ICalendarParameterName::Jsptr => "JSPTR", + ICalendarParameterName::Jsid => "JSID", + ICalendarParameterName::Other(name) => return Cow::Owned(name), + } + .into() + } + + pub fn parse(input: &str) -> Self { + Self::try_parse(input.as_bytes()) + .unwrap_or_else(|| ICalendarParameterName::Other(input.to_string())) + } +} + +impl IanaParse for ICalendarFrequency { + fn parse(value: &[u8]) -> Option { + hashify::tiny_map_ignore_case!(value, + b"SECONDLY" => ICalendarFrequency::Secondly, + b"MINUTELY" => ICalendarFrequency::Minutely, + b"HOURLY" => ICalendarFrequency::Hourly, + b"DAILY" => ICalendarFrequency::Daily, + b"WEEKLY" => ICalendarFrequency::Weekly, + b"MONTHLY" => ICalendarFrequency::Monthly, + b"YEARLY" => ICalendarFrequency::Yearly, + ) + } +} + +impl IanaString for ICalendarFrequency { + fn as_str(&self) -> &'static str { + match self { + ICalendarFrequency::Secondly => "SECONDLY", + ICalendarFrequency::Minutely => "MINUTELY", + ICalendarFrequency::Hourly => "HOURLY", + ICalendarFrequency::Daily => "DAILY", + ICalendarFrequency::Weekly => "WEEKLY", + ICalendarFrequency::Monthly => "MONTHLY", + ICalendarFrequency::Yearly => "YEARLY", + } + } +} + +impl IanaParse for ICalendarSkip { + fn parse(value: &[u8]) -> Option { + hashify::tiny_map_ignore_case!(value, + b"OMIT" => ICalendarSkip::Omit, + b"BACKWARD" => ICalendarSkip::Backward, + b"FORWARD" => ICalendarSkip::Forward, + ) + } +} + +impl IanaString for ICalendarSkip { + fn as_str(&self) -> &'static str { + match self { + ICalendarSkip::Omit => "OMIT", + ICalendarSkip::Backward => "BACKWARD", + ICalendarSkip::Forward => "FORWARD", + } + } +} + +impl IanaParse for ICalendarWeekday { + fn parse(value: &[u8]) -> Option { + hashify::tiny_map_ignore_case!(value, + b"SU" => ICalendarWeekday::Sunday, + b"MO" => ICalendarWeekday::Monday, + b"TU" => ICalendarWeekday::Tuesday, + b"WE" => ICalendarWeekday::Wednesday, + b"TH" => ICalendarWeekday::Thursday, + b"FR" => ICalendarWeekday::Friday, + b"SA" => ICalendarWeekday::Saturday, + ) + } +} + +impl IanaString for ICalendarWeekday { + fn as_str(&self) -> &'static str { + match self { + ICalendarWeekday::Sunday => "SU", + ICalendarWeekday::Monday => "MO", + ICalendarWeekday::Tuesday => "TU", + ICalendarWeekday::Wednesday => "WE", + ICalendarWeekday::Thursday => "TH", + ICalendarWeekday::Friday => "FR", + ICalendarWeekday::Saturday => "SA", + } + } +} + +impl From for chrono::Weekday { + fn from(value: ICalendarWeekday) -> Self { + match value { + ICalendarWeekday::Sunday => chrono::Weekday::Sun, + ICalendarWeekday::Monday => chrono::Weekday::Mon, + ICalendarWeekday::Tuesday => chrono::Weekday::Tue, + ICalendarWeekday::Wednesday => chrono::Weekday::Wed, + ICalendarWeekday::Thursday => chrono::Weekday::Thu, + ICalendarWeekday::Friday => chrono::Weekday::Fri, + ICalendarWeekday::Saturday => chrono::Weekday::Sat, + } + } +} + +impl IanaParse for ICalendarAction { + fn parse(value: &[u8]) -> Option { + hashify::tiny_map_ignore_case!(value, + "AUDIO" => ICalendarAction::Audio, + "DISPLAY" => ICalendarAction::Display, + "EMAIL" => ICalendarAction::Email, + "PROCEDURE" => ICalendarAction::Procedure, + ) + } +} + +impl IanaString for ICalendarAction { + fn as_str(&self) -> &'static str { + match self { + ICalendarAction::Audio => "AUDIO", + ICalendarAction::Display => "DISPLAY", + ICalendarAction::Email => "EMAIL", + ICalendarAction::Procedure => "PROCEDURE", + } + } +} + +impl IanaParse for ICalendarUserTypes { + fn parse(value: &[u8]) -> Option { + hashify::tiny_map_ignore_case!(value, + "INDIVIDUAL" => ICalendarUserTypes::Individual, + "GROUP" => ICalendarUserTypes::Group, + "RESOURCE" => ICalendarUserTypes::Resource, + "ROOM" => ICalendarUserTypes::Room, + "UNKNOWN" => ICalendarUserTypes::Unknown, + ) + } +} + +impl IanaString for ICalendarUserTypes { + fn as_str(&self) -> &'static str { + match self { + ICalendarUserTypes::Individual => "INDIVIDUAL", + ICalendarUserTypes::Group => "GROUP", + ICalendarUserTypes::Resource => "RESOURCE", + ICalendarUserTypes::Room => "ROOM", + ICalendarUserTypes::Unknown => "UNKNOWN", + } + } +} + +impl IanaParse for ICalendarClassification { + fn parse(value: &[u8]) -> Option { + hashify::tiny_map_ignore_case!(value, + "PUBLIC" => ICalendarClassification::Public, + "PRIVATE" => ICalendarClassification::Private, + "CONFIDENTIAL" => ICalendarClassification::Confidential, + ) + } +} + +impl IanaString for ICalendarClassification { + fn as_str(&self) -> &'static str { + match self { + ICalendarClassification::Public => "PUBLIC", + ICalendarClassification::Private => "PRIVATE", + ICalendarClassification::Confidential => "CONFIDENTIAL", + } + } +} + +impl IanaParse for ICalendarComponentType { + fn parse(value: &[u8]) -> Option { + hashify::tiny_map_ignore_case!(value, + "VCALENDAR" => ICalendarComponentType::VCalendar, + "VEVENT" => ICalendarComponentType::VEvent, + "VTODO" => ICalendarComponentType::VTodo, + "VJOURNAL" => ICalendarComponentType::VJournal, + "VFREEBUSY" => ICalendarComponentType::VFreebusy, + "VTIMEZONE" => ICalendarComponentType::VTimezone, + "VALARM" => ICalendarComponentType::VAlarm, + "STANDARD" => ICalendarComponentType::Standard, + "DAYLIGHT" => ICalendarComponentType::Daylight, + "VAVAILABILITY" => ICalendarComponentType::VAvailability, + "AVAILABLE" => ICalendarComponentType::Available, + "PARTICIPANT" => ICalendarComponentType::Participant, + "VLOCATION" => ICalendarComponentType::VLocation, + "VRESOURCE" => ICalendarComponentType::VResource, + "VSTATUS" => ICalendarComponentType::VStatus + ) + } +} + +impl ICalendarComponentType { + pub fn as_str(&self) -> &str { + match self { + ICalendarComponentType::VCalendar => "VCALENDAR", + ICalendarComponentType::VEvent => "VEVENT", + ICalendarComponentType::VTodo => "VTODO", + ICalendarComponentType::VJournal => "VJOURNAL", + ICalendarComponentType::VFreebusy => "VFREEBUSY", + ICalendarComponentType::VTimezone => "VTIMEZONE", + ICalendarComponentType::VAlarm => "VALARM", + ICalendarComponentType::Standard => "STANDARD", + ICalendarComponentType::Daylight => "DAYLIGHT", + ICalendarComponentType::VAvailability => "VAVAILABILITY", + ICalendarComponentType::Available => "AVAILABLE", + ICalendarComponentType::Participant => "PARTICIPANT", + ICalendarComponentType::VLocation => "VLOCATION", + ICalendarComponentType::VResource => "VRESOURCE", + ICalendarComponentType::VStatus => "VSTATUS", + ICalendarComponentType::Other(s) => s.as_str(), + } + } + + pub fn into_string(self) -> Cow<'static, str> { + match self { + ICalendarComponentType::VCalendar => "VCALENDAR", + ICalendarComponentType::VEvent => "VEVENT", + ICalendarComponentType::VTodo => "VTODO", + ICalendarComponentType::VJournal => "VJOURNAL", + ICalendarComponentType::VFreebusy => "VFREEBUSY", + ICalendarComponentType::VTimezone => "VTIMEZONE", + ICalendarComponentType::VAlarm => "VALARM", + ICalendarComponentType::Standard => "STANDARD", + ICalendarComponentType::Daylight => "DAYLIGHT", + ICalendarComponentType::VAvailability => "VAVAILABILITY", + ICalendarComponentType::Available => "AVAILABLE", + ICalendarComponentType::Participant => "PARTICIPANT", + ICalendarComponentType::VLocation => "VLOCATION", + ICalendarComponentType::VResource => "VRESOURCE", + ICalendarComponentType::VStatus => "VSTATUS", + ICalendarComponentType::Other(s) => return Cow::Owned(s), + } + .into() + } +} + +impl IanaParse for ICalendarDisplayType { + fn parse(value: &[u8]) -> Option { + hashify::tiny_map_ignore_case!(value, + "BADGE" => ICalendarDisplayType::Badge, + "GRAPHIC" => ICalendarDisplayType::Graphic, + "FULLSIZE" => ICalendarDisplayType::Fullsize, + "THUMBNAIL" => ICalendarDisplayType::Thumbnail, + ) + } +} + +impl IanaString for ICalendarDisplayType { + fn as_str(&self) -> &'static str { + match self { + ICalendarDisplayType::Badge => "BADGE", + ICalendarDisplayType::Graphic => "GRAPHIC", + ICalendarDisplayType::Fullsize => "FULLSIZE", + ICalendarDisplayType::Thumbnail => "THUMBNAIL", + } + } +} + +impl IanaParse for ICalendarFeatureType { + fn parse(value: &[u8]) -> Option { + hashify::tiny_map_ignore_case!(value, + "AUDIO" => ICalendarFeatureType::Audio, + "CHAT" => ICalendarFeatureType::Chat, + "FEED" => ICalendarFeatureType::Feed, + "MODERATOR" => ICalendarFeatureType::Moderator, + "PHONE" => ICalendarFeatureType::Phone, + "SCREEN" => ICalendarFeatureType::Screen, + "VIDEO" => ICalendarFeatureType::Video, + ) + } +} + +impl IanaString for ICalendarFeatureType { + fn as_str(&self) -> &'static str { + match self { + ICalendarFeatureType::Audio => "AUDIO", + ICalendarFeatureType::Chat => "CHAT", + ICalendarFeatureType::Feed => "FEED", + ICalendarFeatureType::Moderator => "MODERATOR", + ICalendarFeatureType::Phone => "PHONE", + ICalendarFeatureType::Screen => "SCREEN", + ICalendarFeatureType::Video => "VIDEO", + } + } +} + +impl IanaParse for ICalendarFreeBusyType { + fn parse(value: &[u8]) -> Option { + hashify::tiny_map_ignore_case!(value, + "FREE" => ICalendarFreeBusyType::Free, + "BUSY" => ICalendarFreeBusyType::Busy, + "BUSY-UNAVAILABLE" => ICalendarFreeBusyType::BusyUnavailable, + "BUSY-TENTATIVE" => ICalendarFreeBusyType::BusyTentative, + ) + } +} + +impl IanaString for ICalendarFreeBusyType { + fn as_str(&self) -> &'static str { + match self { + ICalendarFreeBusyType::Free => "FREE", + ICalendarFreeBusyType::Busy => "BUSY", + ICalendarFreeBusyType::BusyUnavailable => "BUSY-UNAVAILABLE", + ICalendarFreeBusyType::BusyTentative => "BUSY-TENTATIVE", + } + } +} + +impl IanaParse for ICalendarMethod { + fn parse(value: &[u8]) -> Option { + hashify::tiny_map_ignore_case!(value, + "PUBLISH" => ICalendarMethod::Publish, + "REQUEST" => ICalendarMethod::Request, + "REPLY" => ICalendarMethod::Reply, + "ADD" => ICalendarMethod::Add, + "CANCEL" => ICalendarMethod::Cancel, + "REFRESH" => ICalendarMethod::Refresh, + "COUNTER" => ICalendarMethod::Counter, + "DECLINECOUNTER" => ICalendarMethod::Declinecounter, + ) + } +} + +impl IanaString for ICalendarMethod { + fn as_str(&self) -> &'static str { + match self { + ICalendarMethod::Publish => "PUBLISH", + ICalendarMethod::Request => "REQUEST", + ICalendarMethod::Reply => "REPLY", + ICalendarMethod::Add => "ADD", + ICalendarMethod::Cancel => "CANCEL", + ICalendarMethod::Refresh => "REFRESH", + ICalendarMethod::Counter => "COUNTER", + ICalendarMethod::Declinecounter => "DECLINECOUNTER", + } + } +} + +impl IanaParse for ICalendarRelated { + fn parse(value: &[u8]) -> Option { + hashify::tiny_map_ignore_case!(value, + "START" => ICalendarRelated::Start, + "END" => ICalendarRelated::End, + ) + } +} + +impl IanaString for ICalendarRelated { + fn as_str(&self) -> &'static str { + match self { + ICalendarRelated::Start => "START", + ICalendarRelated::End => "END", + } + } +} + +impl IanaParse for ICalendarParticipantType { + fn parse(value: &[u8]) -> Option { + hashify::tiny_map_ignore_case!(value, + "ACTIVE" => ICalendarParticipantType::Active, + "INACTIVE" => ICalendarParticipantType::Inactive, + "SPONSOR" => ICalendarParticipantType::Sponsor, + "CONTACT" => ICalendarParticipantType::Contact, + "BOOKING-CONTACT" => ICalendarParticipantType::BookingContact, + "EMERGENCY-CONTACT" => ICalendarParticipantType::EmergencyContact, + "PUBLICITY-CONTACT" => ICalendarParticipantType::PublicityContact, + "PLANNER-CONTACT" => ICalendarParticipantType::PlannerContact, + "PERFORMER" => ICalendarParticipantType::Performer, + "SPEAKER" => ICalendarParticipantType::Speaker, + ) + } +} + +impl IanaString for ICalendarParticipantType { + fn as_str(&self) -> &'static str { + match self { + ICalendarParticipantType::Active => "ACTIVE", + ICalendarParticipantType::Inactive => "INACTIVE", + ICalendarParticipantType::Sponsor => "SPONSOR", + ICalendarParticipantType::Contact => "CONTACT", + ICalendarParticipantType::BookingContact => "BOOKING-CONTACT", + ICalendarParticipantType::EmergencyContact => "EMERGENCY-CONTACT", + ICalendarParticipantType::PublicityContact => "PUBLICITY-CONTACT", + ICalendarParticipantType::PlannerContact => "PLANNER-CONTACT", + ICalendarParticipantType::Performer => "PERFORMER", + ICalendarParticipantType::Speaker => "SPEAKER", + } + } +} + +impl IanaParse for ICalendarParticipationRole { + fn parse(value: &[u8]) -> Option { + hashify::tiny_map_ignore_case!(value, + "CHAIR" => ICalendarParticipationRole::Chair, + "REQ-PARTICIPANT" => ICalendarParticipationRole::ReqParticipant, + "OPT-PARTICIPANT" => ICalendarParticipationRole::OptParticipant, + "NON-PARTICIPANT" => ICalendarParticipationRole::NonParticipant, + "OWNER" => ICalendarParticipationRole::Owner, + ) + } +} + +impl IanaString for ICalendarParticipationRole { + fn as_str(&self) -> &'static str { + match self { + ICalendarParticipationRole::Chair => "CHAIR", + ICalendarParticipationRole::ReqParticipant => "REQ-PARTICIPANT", + ICalendarParticipationRole::OptParticipant => "OPT-PARTICIPANT", + ICalendarParticipationRole::NonParticipant => "NON-PARTICIPANT", + ICalendarParticipationRole::Owner => "OWNER", + } + } +} + +impl IanaParse for ICalendarStatus { + fn parse(value: &[u8]) -> Option { + hashify::tiny_map_ignore_case!(value, + "TENTATIVE" => ICalendarStatus::Tentative, + "CONFIRMED" => ICalendarStatus::Confirmed, + "CANCELLED" => ICalendarStatus::Cancelled, + "NEEDS-ACTION" => ICalendarStatus::NeedsAction, + "COMPLETED" => ICalendarStatus::Completed, + "IN-PROCESS" => ICalendarStatus::InProcess, + "DRAFT" => ICalendarStatus::Draft, + "FINAL" => ICalendarStatus::Final, + "FAILED" => ICalendarStatus::Failed, + "PENDING" => ICalendarStatus::Pending + ) + } +} + +impl IanaString for ICalendarStatus { + fn as_str(&self) -> &'static str { + match self { + ICalendarStatus::Tentative => "TENTATIVE", + ICalendarStatus::Confirmed => "CONFIRMED", + ICalendarStatus::Cancelled => "CANCELLED", + ICalendarStatus::NeedsAction => "NEEDS-ACTION", + ICalendarStatus::Completed => "COMPLETED", + ICalendarStatus::InProcess => "IN-PROCESS", + ICalendarStatus::Draft => "DRAFT", + ICalendarStatus::Final => "FINAL", + ICalendarStatus::Failed => "FAILED", + ICalendarStatus::Pending => "PENDING", + } + } +} + +impl IanaParse for ICalendarParticipationStatus { + fn parse(value: &[u8]) -> Option { + hashify::tiny_map_ignore_case!(value, + "NEEDS-ACTION" => ICalendarParticipationStatus::NeedsAction, + "ACCEPTED" => ICalendarParticipationStatus::Accepted, + "DECLINED" => ICalendarParticipationStatus::Declined, + "TENTATIVE" => ICalendarParticipationStatus::Tentative, + "DELEGATED" => ICalendarParticipationStatus::Delegated, + "COMPLETED" => ICalendarParticipationStatus::Completed, + "IN-PROCESS" => ICalendarParticipationStatus::InProcess, + "FAILED" => ICalendarParticipationStatus::Failed + ) + } +} + +impl IanaString for ICalendarParticipationStatus { + fn as_str(&self) -> &'static str { + match self { + ICalendarParticipationStatus::NeedsAction => "NEEDS-ACTION", + ICalendarParticipationStatus::Accepted => "ACCEPTED", + ICalendarParticipationStatus::Declined => "DECLINED", + ICalendarParticipationStatus::Tentative => "TENTATIVE", + ICalendarParticipationStatus::Delegated => "DELEGATED", + ICalendarParticipationStatus::Completed => "COMPLETED", + ICalendarParticipationStatus::InProcess => "IN-PROCESS", + ICalendarParticipationStatus::Failed => "FAILED", + } + } +} + +impl IanaParse for ICalendarProximityValue { + fn parse(value: &[u8]) -> Option { + hashify::tiny_map_ignore_case!(value, + "ARRIVE" => ICalendarProximityValue::Arrive, + "DEPART" => ICalendarProximityValue::Depart, + "CONNECT" => ICalendarProximityValue::Connect, + "DISCONNECT" => ICalendarProximityValue::Disconnect, + ) + } +} + +impl IanaString for ICalendarProximityValue { + fn as_str(&self) -> &'static str { + match self { + ICalendarProximityValue::Arrive => "ARRIVE", + ICalendarProximityValue::Depart => "DEPART", + ICalendarProximityValue::Connect => "CONNECT", + ICalendarProximityValue::Disconnect => "DISCONNECT", + } + } +} + +impl IanaParse for ICalendarRelationshipType { + fn parse(value: &[u8]) -> Option { + hashify::tiny_map_ignore_case!(value, + "CHILD" => ICalendarRelationshipType::Child, + "PARENT" => ICalendarRelationshipType::Parent, + "SIBLING" => ICalendarRelationshipType::Sibling, + "SNOOZE" => ICalendarRelationshipType::Snooze, + "CONCEPT" => ICalendarRelationshipType::Concept, + "DEPENDS-ON" => ICalendarRelationshipType::DependsOn, + "FINISHTOFINISH" => ICalendarRelationshipType::Finishtofinish, + "FINISHTOSTART" => ICalendarRelationshipType::Finishtostart, + "FIRST" => ICalendarRelationshipType::First, + "NEXT" => ICalendarRelationshipType::Next, + "REFID" => ICalendarRelationshipType::Refid, + "STARTTOFINISH" => ICalendarRelationshipType::Starttofinish, + "STARTTOSTART" => ICalendarRelationshipType::Starttostart, + ) + } +} + +impl IanaString for ICalendarRelationshipType { + fn as_str(&self) -> &'static str { + match self { + ICalendarRelationshipType::Child => "CHILD", + ICalendarRelationshipType::Parent => "PARENT", + ICalendarRelationshipType::Sibling => "SIBLING", + ICalendarRelationshipType::Snooze => "SNOOZE", + ICalendarRelationshipType::Concept => "CONCEPT", + ICalendarRelationshipType::DependsOn => "DEPENDS-ON", + ICalendarRelationshipType::Finishtofinish => "FINISHTOFINISH", + ICalendarRelationshipType::Finishtostart => "FINISHTOSTART", + ICalendarRelationshipType::First => "FIRST", + ICalendarRelationshipType::Next => "NEXT", + ICalendarRelationshipType::Refid => "REFID", + ICalendarRelationshipType::Starttofinish => "STARTTOFINISH", + ICalendarRelationshipType::Starttostart => "STARTTOSTART", + } + } +} + +impl IanaParse for ICalendarResourceType { + fn parse(value: &[u8]) -> Option { + hashify::tiny_map_ignore_case!(value, + "PROJECTOR" => ICalendarResourceType::Projector, + "ROOM" => ICalendarResourceType::Room, + "REMOTE-CONFERENCE-AUDIO" => ICalendarResourceType::RemoteConferenceAudio, + "REMOTE-CONFERENCE-VIDEO" => ICalendarResourceType::RemoteConferenceVideo, + ) + } +} + +impl IanaString for ICalendarResourceType { + fn as_str(&self) -> &'static str { + match self { + ICalendarResourceType::Projector => "PROJECTOR", + ICalendarResourceType::Room => "ROOM", + ICalendarResourceType::RemoteConferenceAudio => "REMOTE-CONFERENCE-AUDIO", + ICalendarResourceType::RemoteConferenceVideo => "REMOTE-CONFERENCE-VIDEO", + } + } +} + +impl IanaParse for ICalendarScheduleAgentValue { + fn parse(value: &[u8]) -> Option { + hashify::tiny_map_ignore_case!(value, + "SERVER" => ICalendarScheduleAgentValue::Server, + "CLIENT" => ICalendarScheduleAgentValue::Client, + "NONE" => ICalendarScheduleAgentValue::None, + ) + } +} + +impl IanaString for ICalendarScheduleAgentValue { + fn as_str(&self) -> &'static str { + match self { + ICalendarScheduleAgentValue::Server => "SERVER", + ICalendarScheduleAgentValue::Client => "CLIENT", + ICalendarScheduleAgentValue::None => "NONE", + } + } +} + +impl IanaParse for ICalendarScheduleForceSendValue { + fn parse(value: &[u8]) -> Option { + hashify::tiny_map_ignore_case!(value, + "REQUEST" => ICalendarScheduleForceSendValue::Request, + "REPLY" => ICalendarScheduleForceSendValue::Reply, + ) + } +} + +impl IanaString for ICalendarScheduleForceSendValue { + fn as_str(&self) -> &'static str { + match self { + ICalendarScheduleForceSendValue::Request => "REQUEST", + ICalendarScheduleForceSendValue::Reply => "REPLY", + } + } +} + +impl IanaParse for ICalendarValueType { + fn parse(value: &[u8]) -> Option { + hashify::tiny_map_ignore_case!(value, + "BINARY" => ICalendarValueType::Binary, + "BOOLEAN" => ICalendarValueType::Boolean, + "CAL-ADDRESS" => ICalendarValueType::CalAddress, + "DATE" => ICalendarValueType::Date, + "DATE-TIME" => ICalendarValueType::DateTime, + "DURATION" => ICalendarValueType::Duration, + "FLOAT" => ICalendarValueType::Float, + "INTEGER" => ICalendarValueType::Integer, + "PERIOD" => ICalendarValueType::Period, + "RECUR" => ICalendarValueType::Recur, + "TEXT" => ICalendarValueType::Text, + "TIME" => ICalendarValueType::Time, + "UNKNOWN" => ICalendarValueType::Unknown, + "URI" => ICalendarValueType::Uri, + "UTC-OFFSET" => ICalendarValueType::UtcOffset, + "XML-REFERENCE" => ICalendarValueType::XmlReference, + "UID" => ICalendarValueType::Uid, + ) + } +} + +impl IanaString for ICalendarValueType { + fn as_str(&self) -> &'static str { + match self { + ICalendarValueType::Binary => "BINARY", + ICalendarValueType::Boolean => "BOOLEAN", + ICalendarValueType::CalAddress => "CAL-ADDRESS", + ICalendarValueType::Date => "DATE", + ICalendarValueType::DateTime => "DATE-TIME", + ICalendarValueType::Duration => "DURATION", + ICalendarValueType::Float => "FLOAT", + ICalendarValueType::Integer => "INTEGER", + ICalendarValueType::Period => "PERIOD", + ICalendarValueType::Recur => "RECUR", + ICalendarValueType::Text => "TEXT", + ICalendarValueType::Time => "TIME", + ICalendarValueType::Unknown => "UNKNOWN", + ICalendarValueType::Uri => "URI", + ICalendarValueType::UtcOffset => "UTC-OFFSET", + ICalendarValueType::XmlReference => "XML-REFERENCE", + ICalendarValueType::Uid => "UID", + } + } +} + +impl IanaParse for ICalendarTransparency { + fn parse(value: &[u8]) -> Option { + hashify::tiny_map_ignore_case!(value, + "OPAQUE" => ICalendarTransparency::Opaque, + "TRANSPARENT" => ICalendarTransparency::Transparent, + ) + } +} + +impl IanaString for ICalendarTransparency { + fn as_str(&self) -> &'static str { + match self { + ICalendarTransparency::Opaque => "OPAQUE", + ICalendarTransparency::Transparent => "TRANSPARENT", + } + } +} + +impl ICalendarProperty { + // Returns the default value type and whether the property is multi-valued. + pub(crate) fn default_types(&self) -> (ValueType, ValueSeparator) { + match self { + ICalendarProperty::Calscale => (ValueType::CalendarScale, ValueSeparator::None), + ICalendarProperty::Method => (ValueType::Method, ValueSeparator::None), + ICalendarProperty::Prodid => ( + ValueType::Ical(ICalendarValueType::Text), + ValueSeparator::None, + ), + ICalendarProperty::Version => ( + ValueType::Ical(ICalendarValueType::Text), + ValueSeparator::None, + ), + ICalendarProperty::Attach => ( + ValueType::Ical(ICalendarValueType::Uri), + ValueSeparator::None, + ), + ICalendarProperty::Categories => ( + ValueType::Ical(ICalendarValueType::Text), + ValueSeparator::Comma, + ), + ICalendarProperty::Class => (ValueType::Classification, ValueSeparator::None), + ICalendarProperty::Comment => ( + ValueType::Ical(ICalendarValueType::Text), + ValueSeparator::None, + ), + ICalendarProperty::Description => ( + ValueType::Ical(ICalendarValueType::Text), + ValueSeparator::None, + ), + ICalendarProperty::Geo => ( + ValueType::Ical(ICalendarValueType::Float), + ValueSeparator::Semicolon, + ), + ICalendarProperty::Location => ( + ValueType::Ical(ICalendarValueType::Text), + ValueSeparator::None, + ), + ICalendarProperty::PercentComplete => ( + ValueType::Ical(ICalendarValueType::Integer), + ValueSeparator::None, + ), + ICalendarProperty::Priority => ( + ValueType::Ical(ICalendarValueType::Integer), + ValueSeparator::None, + ), + ICalendarProperty::Resources => ( + ValueType::Ical(ICalendarValueType::Text), + ValueSeparator::Comma, + ), + ICalendarProperty::Status => (ValueType::Status, ValueSeparator::None), + ICalendarProperty::Summary => ( + ValueType::Ical(ICalendarValueType::Text), + ValueSeparator::None, + ), + ICalendarProperty::Completed => ( + ValueType::Ical(ICalendarValueType::DateTime), + ValueSeparator::None, + ), + ICalendarProperty::Dtend => ( + ValueType::Ical(ICalendarValueType::DateTime), + ValueSeparator::None, + ), + ICalendarProperty::Due => ( + ValueType::Ical(ICalendarValueType::DateTime), + ValueSeparator::None, + ), + ICalendarProperty::Dtstart => ( + ValueType::Ical(ICalendarValueType::DateTime), + ValueSeparator::None, + ), + ICalendarProperty::Duration => ( + ValueType::Ical(ICalendarValueType::Duration), + ValueSeparator::None, + ), + ICalendarProperty::Freebusy => ( + ValueType::Ical(ICalendarValueType::Period), + ValueSeparator::Comma, + ), + ICalendarProperty::Transp => (ValueType::Transparency, ValueSeparator::None), + ICalendarProperty::Tzid => ( + ValueType::Ical(ICalendarValueType::Text), + ValueSeparator::None, + ), + ICalendarProperty::Tzname => ( + ValueType::Ical(ICalendarValueType::Text), + ValueSeparator::None, + ), + ICalendarProperty::Tzoffsetfrom => ( + ValueType::Ical(ICalendarValueType::UtcOffset), + ValueSeparator::None, + ), + ICalendarProperty::Tzoffsetto => ( + ValueType::Ical(ICalendarValueType::UtcOffset), + ValueSeparator::None, + ), + ICalendarProperty::Tzurl => ( + ValueType::Ical(ICalendarValueType::Uri), + ValueSeparator::None, + ), + ICalendarProperty::Attendee => ( + ValueType::Ical(ICalendarValueType::Uri), + ValueSeparator::None, + ), + ICalendarProperty::Contact => ( + ValueType::Ical(ICalendarValueType::Text), + ValueSeparator::None, + ), + ICalendarProperty::Organizer => ( + ValueType::Ical(ICalendarValueType::Uri), + ValueSeparator::None, + ), + ICalendarProperty::RecurrenceId => ( + ValueType::Ical(ICalendarValueType::DateTime), + ValueSeparator::None, + ), + ICalendarProperty::RelatedTo => ( + ValueType::Ical(ICalendarValueType::Text), + ValueSeparator::None, + ), + ICalendarProperty::Url => ( + ValueType::Ical(ICalendarValueType::Uri), + ValueSeparator::None, + ), + ICalendarProperty::Uid => ( + ValueType::Ical(ICalendarValueType::Text), + ValueSeparator::None, + ), + ICalendarProperty::Exdate => ( + ValueType::Ical(ICalendarValueType::DateTime), + ValueSeparator::Comma, + ), + ICalendarProperty::Exrule => ( + ValueType::Ical(ICalendarValueType::Recur), + ValueSeparator::None, + ), + ICalendarProperty::Rdate => ( + ValueType::Ical(ICalendarValueType::DateTime), + ValueSeparator::Comma, + ), + ICalendarProperty::Rrule => ( + ValueType::Ical(ICalendarValueType::Recur), + ValueSeparator::None, + ), + ICalendarProperty::Action => (ValueType::Action, ValueSeparator::None), + ICalendarProperty::Repeat => ( + ValueType::Ical(ICalendarValueType::Integer), + ValueSeparator::None, + ), + ICalendarProperty::Trigger => ( + ValueType::Ical(ICalendarValueType::Duration), + ValueSeparator::None, + ), + ICalendarProperty::Created => ( + ValueType::Ical(ICalendarValueType::DateTime), + ValueSeparator::None, + ), + ICalendarProperty::Dtstamp => ( + ValueType::Ical(ICalendarValueType::DateTime), + ValueSeparator::None, + ), + ICalendarProperty::LastModified => ( + ValueType::Ical(ICalendarValueType::DateTime), + ValueSeparator::None, + ), + ICalendarProperty::Sequence => ( + ValueType::Ical(ICalendarValueType::Integer), + ValueSeparator::None, + ), + ICalendarProperty::RequestStatus => ( + ValueType::Ical(ICalendarValueType::Text), + ValueSeparator::Semicolon, + ), + ICalendarProperty::Xml => ( + ValueType::Ical(ICalendarValueType::Text), + ValueSeparator::None, + ), + ICalendarProperty::Tzuntil => ( + ValueType::Ical(ICalendarValueType::DateTime), + ValueSeparator::None, + ), + ICalendarProperty::TzidAliasOf => ( + ValueType::Ical(ICalendarValueType::Text), + ValueSeparator::None, + ), + ICalendarProperty::Busytype => (ValueType::BusyType, ValueSeparator::None), + ICalendarProperty::Name => ( + ValueType::Ical(ICalendarValueType::Text), + ValueSeparator::None, + ), + ICalendarProperty::RefreshInterval => ( + ValueType::Ical(ICalendarValueType::Duration), + ValueSeparator::None, + ), + ICalendarProperty::Source => ( + ValueType::Ical(ICalendarValueType::Uri), + ValueSeparator::None, + ), + ICalendarProperty::Color => ( + ValueType::Ical(ICalendarValueType::Text), + ValueSeparator::None, + ), + ICalendarProperty::Image => ( + ValueType::Ical(ICalendarValueType::Uri), + ValueSeparator::None, + ), + ICalendarProperty::Conference => ( + ValueType::Ical(ICalendarValueType::Uri), + ValueSeparator::None, + ), + ICalendarProperty::CalendarAddress => ( + ValueType::Ical(ICalendarValueType::Uri), + ValueSeparator::None, + ), + ICalendarProperty::LocationType => ( + ValueType::Ical(ICalendarValueType::Text), + ValueSeparator::Comma, + ), + ICalendarProperty::ParticipantType => { + (ValueType::ParticipantType, ValueSeparator::None) + } + ICalendarProperty::ResourceType => (ValueType::ResourceType, ValueSeparator::None), + ICalendarProperty::StructuredData => ( + ValueType::Ical(ICalendarValueType::Text), + ValueSeparator::None, + ), + ICalendarProperty::StyledDescription => ( + ValueType::Ical(ICalendarValueType::Text), + ValueSeparator::None, + ), + ICalendarProperty::Acknowledged => ( + ValueType::Ical(ICalendarValueType::DateTime), + ValueSeparator::None, + ), + ICalendarProperty::Proximity => (ValueType::Proximity, ValueSeparator::None), + ICalendarProperty::Concept => ( + ValueType::Ical(ICalendarValueType::Uri), + ValueSeparator::None, + ), + ICalendarProperty::Link => ( + ValueType::Ical(ICalendarValueType::Uri), + ValueSeparator::None, + ), + ICalendarProperty::Refid => ( + ValueType::Ical(ICalendarValueType::Text), + ValueSeparator::None, + ), + ICalendarProperty::Begin => ( + ValueType::Ical(ICalendarValueType::Text), + ValueSeparator::None, + ), + ICalendarProperty::End => ( + ValueType::Ical(ICalendarValueType::Text), + ValueSeparator::None, + ), + ICalendarProperty::Other(_) => ( + ValueType::Ical(ICalendarValueType::Text), + ValueSeparator::Semicolon, + ), + ICalendarProperty::Coordinates => ( + ValueType::Ical(ICalendarValueType::Uri), + ValueSeparator::None, + ), + ICalendarProperty::ShowWithoutTime => ( + ValueType::Ical(ICalendarValueType::Boolean), + ValueSeparator::None, + ), + ICalendarProperty::Jsid | ICalendarProperty::Jsprop => ( + ValueType::Ical(ICalendarValueType::Text), + ValueSeparator::None, + ), + ICalendarProperty::EstimatedDuration => ( + ValueType::Ical(ICalendarValueType::Duration), + ValueSeparator::None, + ), + ICalendarProperty::Reason => ( + ValueType::Ical(ICalendarValueType::Uri), + ValueSeparator::None, + ), + ICalendarProperty::Substate => ( + ValueType::Ical(ICalendarValueType::Text), + ValueSeparator::None, + ), + ICalendarProperty::TaskMode => ( + ValueType::Ical(ICalendarValueType::Text), + ValueSeparator::None, + ), + } + } +} + +impl ValueType { + pub fn unwrap_ical(self) -> ICalendarValueType { + match self { + ValueType::Ical(value) => value, + _ => ICalendarValueType::Text, + } + } +} + +impl ICalendar { + pub fn parse(value: impl AsRef) -> Result { + let mut parser = Parser::new(value.as_ref()); + match parser.entry() { + Entry::ICalendar(icalendar) => Ok(icalendar), + other => Err(other), + } + } +} + +impl Hash for ICalendarValue { + fn hash(&self, state: &mut H) { + match self { + ICalendarValue::Binary(value) => { + state.write_u8(0); + value.hash(state); + } + ICalendarValue::Boolean(value) => { + state.write_u8(1); + value.hash(state); + } + ICalendarValue::Uri(value) => { + state.write_u8(2); + value.hash(state); + } + ICalendarValue::PartialDateTime(value) => { + state.write_u8(3); + value.hash(state); + } + ICalendarValue::Duration(value) => { + state.write_u8(4); + value.hash(state); + } + ICalendarValue::RecurrenceRule(value) => { + state.write_u8(5); + value.hash(state); + } + ICalendarValue::Period(value) => { + state.write_u8(6); + value.hash(state); + } + ICalendarValue::Float(value) => { + state.write_u8(7); + value.to_bits().hash(state); + } + ICalendarValue::Integer(value) => { + state.write_u8(8); + value.hash(state); + } + ICalendarValue::Text(value) => { + state.write_u8(9); + value.hash(state); + } + ICalendarValue::CalendarScale(value) => { + state.write_u8(10); + value.hash(state); + } + ICalendarValue::Method(value) => { + state.write_u8(11); + value.hash(state); + } + ICalendarValue::Classification(value) => { + state.write_u8(12); + value.hash(state); + } + ICalendarValue::Status(value) => { + state.write_u8(13); + value.hash(state); + } + ICalendarValue::Transparency(value) => { + state.write_u8(14); + value.hash(state); + } + ICalendarValue::Action(value) => { + state.write_u8(15); + value.hash(state); + } + ICalendarValue::BusyType(value) => { + state.write_u8(16); + value.hash(state); + } + ICalendarValue::ParticipantType(value) => { + state.write_u8(17); + value.hash(state); + } + ICalendarValue::ResourceType(value) => { + state.write_u8(18); + value.hash(state); + } + ICalendarValue::Proximity(value) => { + state.write_u8(19); + value.hash(state); + } + } + } +} + +impl Eq for ICalendarValue {} + +#[allow(clippy::derive_ord_xor_partial_ord)] +impl Ord for ICalendarValue { + fn cmp(&self, other: &Self) -> std::cmp::Ordering { + self.partial_cmp(other).unwrap_or(std::cmp::Ordering::Equal) + } +} diff --git a/vendor/calcard/src/icalendar/utils.rs b/vendor/calcard/src/icalendar/utils.rs new file mode 100644 index 00000000..54dfd3a7 --- /dev/null +++ b/vendor/calcard/src/icalendar/utils.rs @@ -0,0 +1,526 @@ +/* + * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC + * + * SPDX-License-Identifier: Apache-2.0 OR MIT + */ + +use std::borrow::Cow; + +use super::{ + ICalendar, ICalendarComponent, ICalendarComponentType, ICalendarDuration, ICalendarEntry, + ICalendarParameterName, ICalendarProperty, ICalendarRecurrenceRule, ICalendarStatus, + ICalendarTransparency, ICalendarValue, Uri, +}; +use crate::{ + common::{IanaString, IanaType, PartialDateTime}, + icalendar::{ICalendarParameterValue, ICalendarValueType}, +}; + +pub fn strip_mailto_scheme(value: &str) -> &str { + value + .split_once(':') + .filter(|(scheme, _)| scheme.eq_ignore_ascii_case("mailto")) + .map_or(value, |(_, address)| address) +} + +impl ICalendar { + pub fn uids(&self) -> impl Iterator { + self.components + .iter() + .filter_map(|component| component.uid()) + } + + pub fn size(&self) -> usize { + self.components + .iter() + .map(|component| component.size()) + .sum() + } + + pub fn component_by_id(&self, id: u32) -> Option<&ICalendarComponent> { + self.components.get(id as usize) + } + + pub fn alarms_for_id(&self, id: u32) -> impl Iterator { + self.component_by_id(id) + .map_or(&[][..], |c| c.component_ids.as_slice()) + .iter() + .filter_map(|id| { + self.component_by_id(*id) + .filter(|c| c.component_type == ICalendarComponentType::VAlarm) + }) + } +} + +impl ICalendarComponent { + pub fn uid(&self) -> Option<&str> { + self.property(&ICalendarProperty::Uid) + .and_then(|e| e.values.first()) + .and_then(|v| v.as_text()) + } + + pub fn jsid(&self) -> Option<&str> { + self.property(&ICalendarProperty::Jsid) + .and_then(|e| e.values.first()) + .and_then(|v| v.as_text()) + } + + pub fn property(&self, prop: &ICalendarProperty) -> Option<&ICalendarEntry> { + self.entries.iter().find(|entry| &entry.name == prop) + } + + pub fn property_mut(&mut self, prop: &ICalendarProperty) -> Option<&mut ICalendarEntry> { + self.entries.iter_mut().find(|entry| &entry.name == prop) + } + + pub fn has_property(&self, prop: &ICalendarProperty) -> bool { + self.entries.iter().any(|entry| &entry.name == prop) + } + + pub fn properties<'x, 'y: 'x>( + &'x self, + prop: &'y ICalendarProperty, + ) -> impl Iterator + 'x { + self.entries.iter().filter(move |entry| &entry.name == prop) + } + + pub fn properties_mut<'x, 'y: 'x>( + &'x mut self, + prop: &'y ICalendarProperty, + ) -> impl Iterator + 'x { + self.entries + .iter_mut() + .filter(move |entry| &entry.name == prop) + } + + pub fn size(&self) -> usize { + self.entries.iter().map(|entry| entry.size()).sum() + } + + pub fn is_recurrent(&self) -> bool { + self.entries.iter().any(|entry| { + matches!( + entry.name, + ICalendarProperty::Rrule | ICalendarProperty::Rdate + ) + }) + } + + pub fn is_recurrence_override(&self) -> bool { + self.entries + .iter() + .any(|entry| matches!(entry.name, ICalendarProperty::RecurrenceId)) + } + + pub fn is_recurrent_or_override(&self) -> bool { + self.entries.iter().any(|entry| { + matches!( + entry.name, + ICalendarProperty::Rrule + | ICalendarProperty::Rdate + | ICalendarProperty::RecurrenceId + ) + }) + } + + pub fn status(&self) -> Option<&ICalendarStatus> { + self.entries + .iter() + .find_map(|entry| match (&entry.name, entry.values.first()) { + (ICalendarProperty::Status, Some(ICalendarValue::Status(status))) => Some(status), + _ => None, + }) + } + + pub fn transparency(&self) -> Option<&ICalendarTransparency> { + self.entries + .iter() + .find_map(|entry| match (&entry.name, entry.values.first()) { + (ICalendarProperty::Transp, Some(ICalendarValue::Transparency(trans))) => { + Some(trans) + } + _ => None, + }) + } +} + +impl ICalendarValue { + pub fn size(&self) -> usize { + match self { + ICalendarValue::Binary(value) => value.len(), + ICalendarValue::Text(value) => value.len(), + ICalendarValue::PartialDateTime(_) => std::mem::size_of::(), + ICalendarValue::RecurrenceRule(_) => std::mem::size_of::(), + _ => std::mem::size_of::(), + } + } + + pub fn as_text(&self) -> Option<&str> { + match self { + ICalendarValue::Text(s) => Some(s.as_str()), + ICalendarValue::Uri(v) => v.as_str(), + ICalendarValue::CalendarScale(v) => Some(v.as_str()), + ICalendarValue::Method(v) => Some(v.as_str()), + ICalendarValue::Classification(v) => Some(v.as_str()), + ICalendarValue::Status(v) => Some(v.as_str()), + ICalendarValue::Transparency(v) => Some(v.as_str()), + ICalendarValue::Action(v) => Some(v.as_str()), + ICalendarValue::BusyType(v) => Some(v.as_str()), + ICalendarValue::ParticipantType(v) => Some(v.as_str()), + ICalendarValue::ResourceType(v) => Some(v.as_str()), + ICalendarValue::Proximity(v) => Some(v.as_str()), + _ => None, + } + } + + pub fn into_text(self) -> Option> { + match self { + ICalendarValue::Text(s) => Some(Cow::Owned(s)), + ICalendarValue::Uri(v) => Some(Cow::Owned(v.into_unwrapped_string())), + ICalendarValue::CalendarScale(v) => Some(Cow::Borrowed(v.as_str())), + ICalendarValue::Method(v) => Some(Cow::Borrowed(v.as_str())), + ICalendarValue::Classification(v) => Some(Cow::Borrowed(v.as_str())), + ICalendarValue::Status(v) => Some(Cow::Borrowed(v.as_str())), + ICalendarValue::Transparency(v) => Some(Cow::Borrowed(v.as_str())), + ICalendarValue::Action(v) => Some(Cow::Borrowed(v.as_str())), + ICalendarValue::BusyType(v) => Some(Cow::Borrowed(v.as_str())), + ICalendarValue::ParticipantType(v) => Some(Cow::Borrowed(v.as_str())), + ICalendarValue::ResourceType(v) => Some(Cow::Borrowed(v.as_str())), + ICalendarValue::Proximity(v) => Some(Cow::Borrowed(v.as_str())), + ICalendarValue::Integer(i) => Some(Cow::Owned(i.to_string())), + ICalendarValue::Float(f) => Some(Cow::Owned(f.to_string())), + ICalendarValue::Boolean(b) => Some(Cow::Borrowed(if b { "TRUE" } else { "FALSE" })), + ICalendarValue::PartialDateTime(dt) => dt.to_rfc3339().map(Cow::Owned), + ICalendarValue::RecurrenceRule(rrule) => Some(Cow::Owned(rrule.to_string())), + ICalendarValue::Binary(value) => String::from_utf8(value).ok().map(Cow::Owned), + ICalendarValue::Duration(value) => Some(Cow::Owned(value.to_string())), + ICalendarValue::Period(value) => Some(Cow::Owned(value.to_string())), + } + } + + pub fn into_partial_date_time(self) -> Option> { + match self { + ICalendarValue::PartialDateTime(dt) => Some(dt), + _ => None, + } + } + + pub fn as_integer(&self) -> Option { + match self { + ICalendarValue::Integer(i) => Some(*i), + _ => None, + } + } + + pub fn as_float(&self) -> Option { + match self { + ICalendarValue::Float(f) => Some(*f), + _ => None, + } + } + + pub fn as_boolean(&self) -> Option { + match self { + ICalendarValue::Boolean(b) => Some(*b), + _ => None, + } + } + + pub fn as_partial_date_time(&self) -> Option<&PartialDateTime> { + match self { + ICalendarValue::PartialDateTime(dt) => Some(dt), + _ => None, + } + } + + pub fn as_binary(&self) -> Option<&[u8]> { + match self { + ICalendarValue::Binary(d) => Some(d.as_slice()), + _ => None, + } + } +} + +impl ICalendarEntry { + #[inline] + pub fn parameters( + &self, + prop: &ICalendarParameterName, + ) -> impl Iterator { + self.params.iter().filter_map(move |param| { + if ¶m.name == prop { + Some(¶m.value) + } else { + None + } + }) + } + + #[inline] + pub fn parameter(&self, prop: &ICalendarParameterName) -> Option<&ICalendarParameterValue> { + self.params.iter().find_map(move |param| { + if ¶m.name == prop { + Some(¶m.value) + } else { + None + } + }) + } + + #[inline] + pub fn has_parameter(&self, prop: &ICalendarParameterName) -> bool { + self.params.iter().any(|param| ¶m.name == prop) + } + + pub fn jsid(&self) -> Option<&str> { + self.parameter(&ICalendarParameterName::Jsid) + .and_then(|v| v.as_text()) + } + + pub fn is_derived(&self) -> bool { + self.parameter(&ICalendarParameterName::Derived) + .is_some_and(|v| matches!(v, ICalendarParameterValue::Bool(true))) + } + + pub fn calendar_address(&self) -> Option<&str> { + self.values + .first() + .and_then(|v| v.as_text()) + .map(strip_mailto_scheme) + } + + pub fn size(&self) -> usize { + self.values.iter().map(|value| value.size()).sum::() + + self + .params + .iter() + .map(|param| std::mem::size_of::() + param.value.size()) + .sum::() + + self.name.as_str().len() + } +} + +impl ICalendarParameterValue { + pub fn as_text(&self) -> Option<&str> { + match self { + ICalendarParameterValue::Text(v) => Some(v.as_str()), + ICalendarParameterValue::Bool(v) => Some(if *v { "TRUE" } else { "FALSE" }), + ICalendarParameterValue::Uri(uri) => uri.as_str(), + ICalendarParameterValue::Cutype(v) => Some(v.as_str()), + ICalendarParameterValue::Fbtype(v) => Some(v.as_str()), + ICalendarParameterValue::Partstat(v) => Some(v.as_str()), + ICalendarParameterValue::Related(v) => Some(v.as_str()), + ICalendarParameterValue::Reltype(v) => Some(v.as_str()), + ICalendarParameterValue::Role(v) => Some(v.as_str()), + ICalendarParameterValue::ScheduleAgent(v) => Some(v.as_str()), + ICalendarParameterValue::ScheduleForceSend(v) => Some(v.as_str()), + ICalendarParameterValue::Value(v) => Some(v.as_str()), + ICalendarParameterValue::Display(v) => Some(v.as_str()), + ICalendarParameterValue::Feature(v) => Some(v.as_str()), + ICalendarParameterValue::Linkrel(v) => Some(v.as_str()), + ICalendarParameterValue::Duration(_) + | ICalendarParameterValue::Integer(_) + | ICalendarParameterValue::Null => None, + } + } + + pub fn into_text(self) -> Option> { + match self { + ICalendarParameterValue::Text(v) => Some(Cow::Owned(v)), + ICalendarParameterValue::Bool(v) => { + Some(Cow::Borrowed(if v { "TRUE" } else { "FALSE" })) + } + ICalendarParameterValue::Uri(uri) => Some(Cow::Owned(uri.into_unwrapped_string())), + ICalendarParameterValue::Cutype(v) => Some(Cow::Borrowed(v.as_str())), + ICalendarParameterValue::Fbtype(v) => Some(Cow::Borrowed(v.as_str())), + ICalendarParameterValue::Partstat(v) => Some(Cow::Borrowed(v.as_str())), + ICalendarParameterValue::Related(v) => Some(Cow::Borrowed(v.as_str())), + ICalendarParameterValue::Reltype(v) => Some(Cow::Borrowed(v.as_str())), + ICalendarParameterValue::Role(v) => Some(Cow::Borrowed(v.as_str())), + ICalendarParameterValue::ScheduleAgent(v) => Some(Cow::Borrowed(v.as_str())), + ICalendarParameterValue::ScheduleForceSend(v) => Some(Cow::Borrowed(v.as_str())), + ICalendarParameterValue::Value(v) => Some(Cow::Borrowed(v.as_str())), + ICalendarParameterValue::Display(v) => Some(Cow::Borrowed(v.as_str())), + ICalendarParameterValue::Feature(v) => Some(Cow::Borrowed(v.as_str())), + ICalendarParameterValue::Linkrel(v) => Some(Cow::Borrowed(v.as_str())), + ICalendarParameterValue::Duration(v) => Some(Cow::Owned(v.to_string())), + ICalendarParameterValue::Integer(v) => Some(Cow::Owned(v.to_string())), + ICalendarParameterValue::Null => None, + } + } + + pub fn into_value_type(self) -> Option> { + match self { + ICalendarParameterValue::Value(v) => Some(IanaType::Iana(v)), + ICalendarParameterValue::Text(v) => Some(IanaType::Other(v)), + _ => None, + } + } + + pub fn as_integer(&self) -> Option { + match self { + ICalendarParameterValue::Integer(v) => Some(*v), + ICalendarParameterValue::Text(v) => v.parse().ok(), + _ => None, + } + } + + pub fn as_bool(&self) -> Option { + match self { + ICalendarParameterValue::Bool(v) => Some(*v), + _ => None, + } + } + + pub fn size(&self) -> usize { + match self { + ICalendarParameterValue::Text(v) => v.len(), + ICalendarParameterValue::Uri(v) => v.size(), + _ => std::mem::size_of::(), + } + } +} + +impl ICalendarComponentType { + pub fn has_time_ranges(&self) -> bool { + matches!( + self, + ICalendarComponentType::VEvent + | ICalendarComponentType::VTodo + | ICalendarComponentType::VJournal + | ICalendarComponentType::VFreebusy + ) + } + + pub fn is_scheduling_object(&self) -> bool { + matches!( + self, + ICalendarComponentType::VEvent + | ICalendarComponentType::VTodo + | ICalendarComponentType::VJournal + | ICalendarComponentType::VFreebusy + ) + } + + pub fn is_event(&self) -> bool { + matches!(self, ICalendarComponentType::VEvent) + } + + pub fn is_todo(&self) -> bool { + matches!(self, ICalendarComponentType::VTodo) + } + + pub fn is_event_or_todo(&self) -> bool { + matches!( + self, + ICalendarComponentType::VEvent | ICalendarComponentType::VTodo + ) + } + + pub fn is_journal(&self) -> bool { + matches!(self, ICalendarComponentType::VJournal) + } + + pub fn is_freebusy(&self) -> bool { + matches!(self, ICalendarComponentType::VFreebusy) + } + + pub fn is_location(&self) -> bool { + matches!(self, ICalendarComponentType::VLocation) + } + + pub fn is_alarm(&self) -> bool { + matches!(self, ICalendarComponentType::VAlarm) + } + + pub fn is_participant(&self) -> bool { + matches!(self, ICalendarComponentType::Participant) + } +} + +impl Uri { + pub fn as_str(&self) -> Option<&str> { + match self { + Uri::Data(data) => data.content_type.as_deref(), + Uri::Location(loc) => loc.as_str().into(), + } + } + + pub fn size(&self) -> usize { + match self { + Uri::Data(data) => { + data.data.len() + data.content_type.as_ref().map(|s| s.len()).unwrap_or(0) + } + Uri::Location(loc) => loc.len(), + } + } +} + +impl ICalendarDuration { + pub fn from_seconds(seconds: i64) -> Self { + let mut secs = seconds; + let neg = secs < 0; + if neg { + secs = -secs; + } + let weeks = (secs / 604800) as u32; + secs %= 604800; + let days = (secs / 86400) as u32; + secs %= 86400; + let hours = (secs / 3600) as u32; + secs %= 3600; + let minutes = (secs / 60) as u32; + secs %= 60; + let seconds = secs as u32; + + Self { + weeks, + days, + hours, + minutes, + seconds, + neg, + } + } + + pub fn from_days(days: i64) -> Self { + let neg = days < 0; + let days = days.unsigned_abs(); + let (weeks, days) = if days.is_multiple_of(7) { + (days / 7, 0) + } else { + (0, days) + }; + + Self { + weeks: weeks as u32, + days: days as u32, + hours: 0, + minutes: 0, + seconds: 0, + neg, + } + } + + pub fn to_time_delta(&self) -> Option { + chrono::TimeDelta::new(self.as_seconds(), 0) + } + + pub fn as_seconds(&self) -> i64 { + let secs = self.seconds as i64 + + self.minutes as i64 * 60 + + self.hours as i64 * 3600 + + self.days as i64 * 86400 + + self.weeks as i64 * 604800; + + if self.neg { -secs } else { secs } + } + + pub fn is_empty(&self) -> bool { + self.weeks == 0 + && self.days == 0 + && self.hours == 0 + && self.minutes == 0 + && self.seconds == 0 + } +} diff --git a/vendor/calcard/src/icalendar/writer.rs b/vendor/calcard/src/icalendar/writer.rs new file mode 100644 index 00000000..121c5d74 --- /dev/null +++ b/vendor/calcard/src/icalendar/writer.rs @@ -0,0 +1,587 @@ +/* + * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC + * + * SPDX-License-Identifier: Apache-2.0 OR MIT + */ + +use super::{ + ICalendar, ICalendarDay, ICalendarDuration, ICalendarEntry, ICalendarPeriod, + ICalendarRecurrenceRule, ICalendarValueType, +}; +use crate::{ + common::{ + CalendarScale, IanaString, PartialDateTime, + writer::{FoldingWriter, LineWriter, write_bytes, write_param_value, write_text}, + }, + icalendar::{ + ICalendarMonth, ICalendarParameterName, ICalendarParameterValue, ICalendarValue, Uri, + ValueSeparator, + }, +}; +use std::{ + fmt::{Display, Write}, + slice::Iter, +}; + +impl ICalendar { + pub fn write_to(&self, out: &mut impl Write) -> std::fmt::Result { + let mut component_iter: Iter<'_, u32> = [0].iter(); + let mut component_stack = Vec::with_capacity(4); + + loop { + if let Some(component_id) = component_iter.next() { + let component = self.components.get(*component_id as usize).unwrap(); + write_boundary(out, "BEGIN:", component.component_type.as_str())?; + + for entry in &component.entries { + entry.write_to(out)?; + } + + if !component.component_ids.is_empty() { + component_stack.push((component, component_iter)); + component_iter = component.component_ids.iter(); + } else { + write_boundary(out, "END:", component.component_type.as_str())?; + } + } else if let Some((component, iter)) = component_stack.pop() { + write_boundary(out, "END:", component.component_type.as_str())?; + component_iter = iter; + } else { + break; + } + } + + Ok(()) + } +} + +impl ICalendarEntry { + pub fn write_to(&self, out: &mut impl Write) -> std::fmt::Result { + let mut folded = FoldingWriter::new(out); + let out = &mut folded; + + out.write_atomic(self.name.as_str())?; + + if matches!(self.values.first(), Some(ICalendarValue::Binary(_))) { + out.write_atomic(";ENCODING=BASE64")?; + } + + let mut types = None; + let mut last_param: Option<&ICalendarParameterName> = None; + + for param in &self.params { + if last_param.is_some_and(|last_param| last_param == ¶m.name) { + out.write_atomic(",")?; + } else { + out.write_atomic(";")?; + out.write_atomic(param.name.as_str())?; + if !matches!(param.value, ICalendarParameterValue::Null) { + out.write_atomic("=")?; + } + last_param = Some(¶m.name); + } + + match ¶m.value { + ICalendarParameterValue::Text(v) => { + write_param_value(out, v)?; + } + ICalendarParameterValue::Integer(i) => { + write!(out, "{i}")?; + } + ICalendarParameterValue::Bool(v) => { + let v = if !matches!(param.name, ICalendarParameterName::Range) { + if *v { "TRUE" } else { "FALSE" } + } else { + "THISANDFUTURE" + }; + out.write_atomic(v)?; + } + ICalendarParameterValue::Uri(uri) => { + out.write_atomic("\"")?; + write_uri(out, uri, false)?; + out.write_atomic("\"")?; + } + ICalendarParameterValue::Cutype(v) => { + write_param_value(out, v.as_str())?; + } + ICalendarParameterValue::Fbtype(v) => { + write_param_value(out, v.as_str())?; + } + ICalendarParameterValue::Partstat(v) => { + write_param_value(out, v.as_str())?; + } + ICalendarParameterValue::Related(v) => { + write_param_value(out, v.as_str())?; + } + ICalendarParameterValue::Reltype(v) => { + write_param_value(out, v.as_str())?; + } + ICalendarParameterValue::Role(v) => { + write_param_value(out, v.as_str())?; + } + ICalendarParameterValue::ScheduleAgent(v) => { + write_param_value(out, v.as_str())?; + } + ICalendarParameterValue::ScheduleForceSend(v) => { + write_param_value(out, v.as_str())?; + } + ICalendarParameterValue::Value(v) => { + types = Some(v); + write_param_value(out, v.as_str())?; + } + ICalendarParameterValue::Display(v) => { + write_param_value(out, v.as_str())?; + } + ICalendarParameterValue::Feature(v) => { + write_param_value(out, v.as_str())?; + } + ICalendarParameterValue::Duration(v) => { + write!(out, "{v}")?; + } + ICalendarParameterValue::Linkrel(v) => { + write_param_value(out, v.as_str())?; + } + ICalendarParameterValue::Null => { + last_param = None; + } + } + } + + out.write_atomic(":")?; + + let (default_type, separator) = self.name.default_types(); + let separator = if !matches!(separator, ValueSeparator::Comma) { + ";" + } else { + "," + }; + let default_type = default_type.unwrap_ical(); + + for (pos, value) in self.values.iter().enumerate() { + if pos > 0 { + out.write_atomic(separator)?; + } + + let text = match value { + ICalendarValue::Binary(v) => { + write_bytes(out, v)?; + continue; + } + ICalendarValue::Boolean(v) => { + out.write_atomic(if *v { "TRUE" } else { "FALSE" })?; + continue; + } + ICalendarValue::Uri(v) => { + write_uri(out, v, true)?; + continue; + } + ICalendarValue::PartialDateTime(v) => { + v.format_as_ical(out, types.unwrap_or(&default_type))?; + continue; + } + ICalendarValue::Duration(v) => { + write!(out, "{}", v)?; + continue; + } + ICalendarValue::RecurrenceRule(v) => { + write!(out, "{}", v)?; + continue; + } + ICalendarValue::Period(v) => { + write!(out, "{}", v)?; + continue; + } + ICalendarValue::Float(v) => { + write!(out, "{v}")?; + continue; + } + ICalendarValue::Integer(v) => { + write!(out, "{v}")?; + continue; + } + ICalendarValue::Text(v) => { + let escape = + !matches!(types.unwrap_or(&default_type), ICalendarValueType::Recur); + write_text(out, v, escape, escape)?; + continue; + } + ICalendarValue::CalendarScale(v) => v.as_str(), + ICalendarValue::Method(v) => v.as_str(), + ICalendarValue::Classification(v) => v.as_str(), + ICalendarValue::Status(v) => v.as_str(), + ICalendarValue::Transparency(v) => v.as_str(), + ICalendarValue::Action(v) => v.as_str(), + ICalendarValue::BusyType(v) => v.as_str(), + ICalendarValue::ParticipantType(v) => v.as_str(), + ICalendarValue::ResourceType(v) => v.as_str(), + ICalendarValue::Proximity(v) => v.as_str(), + }; + + out.write_atomic(text)?; + } + + out.end_line() + } +} + +pub(crate) fn write_uri( + out: &mut FoldingWriter<'_, W>, + value: &Uri, + escape: bool, +) -> std::fmt::Result { + match value { + Uri::Data(v) => { + let media_type = v.content_type.as_deref().unwrap_or_default(); + out.write_str("data:")?; + out.write_str(media_type)?; + out.write_str(";")?; + if escape { + out.write_atomic("base64\\,")?; + } else { + out.write_atomic("base64,")?; + } + write_bytes(out, &v.data) + } + Uri::Location(v) => write_text(out, v, escape, escape), + } +} + +fn write_boundary(out: &mut impl Write, keyword: &str, name: &str) -> std::fmt::Result { + let mut folded = FoldingWriter::new(out); + folded.write_atomic(keyword)?; + folded.write_atomic(name)?; + folded.end_line() +} + +#[cfg(feature = "rkyv")] +pub(crate) fn write_component_begin(out: &mut impl Write, name: &str) -> std::fmt::Result { + write_boundary(out, "BEGIN:", name) +} + +#[cfg(feature = "rkyv")] +pub(crate) fn write_component_end(out: &mut impl Write, name: &str) -> std::fmt::Result { + write_boundary(out, "END:", name) +} + +impl Uri { + pub fn to_unwrapped_string(&self) -> String { + match self { + Uri::Data(v) => v.to_unwrapped_string(), + Uri::Location(v) => v.to_string(), + } + } + + pub fn into_unwrapped_string(self) -> String { + match self { + Uri::Data(v) => v.to_unwrapped_string(), + Uri::Location(v) => v, + } + } +} + +impl Display for ICalendarRecurrenceRule { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "FREQ={}", self.freq.as_str())?; + if let Some(until) = &self.until { + write!(f, ";UNTIL=")?; + until.format_as_ical( + f, + if until.has_date_and_time() { + &ICalendarValueType::DateTime + } else { + &ICalendarValueType::Date + }, + )?; + } + if let Some(count) = self.count.filter(|c| *c > 0) { + write!(f, ";COUNT={}", count)?; + } + if let Some(interval) = self.interval { + write!(f, ";INTERVAL={}", interval)?; + } + if !self.bysecond.is_empty() { + write!(f, ";BYSECOND=")?; + for (pos, item) in self.bysecond.iter().enumerate() { + if pos > 0 { + write!(f, ",")?; + } + write!(f, "{}", item)?; + } + } + if !self.byminute.is_empty() { + write!(f, ";BYMINUTE=")?; + for (pos, item) in self.byminute.iter().enumerate() { + if pos > 0 { + write!(f, ",")?; + } + write!(f, "{}", item)?; + } + } + if !self.byhour.is_empty() { + write!(f, ";BYHOUR=")?; + for (pos, item) in self.byhour.iter().enumerate() { + if pos > 0 { + write!(f, ",")?; + } + write!(f, "{}", item)?; + } + } + if !self.byday.is_empty() { + write!(f, ";BYDAY=")?; + for (pos, item) in self.byday.iter().enumerate() { + if pos > 0 { + write!(f, ",")?; + } + write!(f, "{}", item)?; + } + } + if !self.bymonthday.is_empty() { + write!(f, ";BYMONTHDAY=")?; + for (pos, item) in self.bymonthday.iter().enumerate() { + if pos > 0 { + write!(f, ",")?; + } + write!(f, "{}", item)?; + } + } + if !self.byyearday.is_empty() { + write!(f, ";BYYEARDAY=")?; + for (pos, item) in self.byyearday.iter().enumerate() { + if pos > 0 { + write!(f, ",")?; + } + write!(f, "{}", item)?; + } + } + if !self.byweekno.is_empty() { + write!(f, ";BYWEEKNO=")?; + for (pos, item) in self.byweekno.iter().enumerate() { + if pos > 0 { + write!(f, ",")?; + } + write!(f, "{}", item)?; + } + } + if !self.bymonth.is_empty() { + write!(f, ";BYMONTH=")?; + for (pos, item) in self.bymonth.iter().enumerate() { + if pos > 0 { + write!(f, ",")?; + } + write!(f, "{}", item)?; + } + } + if !self.bysetpos.is_empty() { + write!(f, ";BYSETPOS=")?; + for (pos, item) in self.bysetpos.iter().enumerate() { + if pos > 0 { + write!(f, ",")?; + } + write!(f, "{}", item)?; + } + } + if let Some(wkst) = self.wkst { + write!(f, ";WKST={}", wkst.as_str())?; + } + if let Some(rscale) = &self.rscale { + write!(f, ";RSCALE={}", rscale.as_str())?; + } else if self.skip.is_some() { + write!(f, ";RSCALE={}", CalendarScale::Gregorian.as_str())?; + } + if let Some(skip) = &self.skip { + write!(f, ";SKIP={}", skip.as_str())?; + } + + Ok(()) + } +} + +impl Display for ICalendarDay { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + if let Some(ordwk) = self.ordwk { + write!(f, "{}", ordwk)?; + } + write!(f, "{}", self.weekday.as_str()) + } +} + +impl Display for ICalendarMonth { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + if !self.is_leap() { + write!(f, "{}", self.month()) + } else { + write!(f, "{}L", self.month()) + } + } +} + +impl Display for ICalendarPeriod { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + ICalendarPeriod::Range { start, end } => { + start.format_as_ical(f, &ICalendarValueType::DateTime)?; + write!(f, "/")?; + end.format_as_ical(f, &ICalendarValueType::DateTime) + } + ICalendarPeriod::Duration { start, duration } => { + start.format_as_ical(f, &ICalendarValueType::DateTime)?; + write!(f, "/{}", duration) + } + } + } +} + +impl Display for ICalendarDuration { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + if self.neg { + write!(f, "-")?; + } + write!(f, "P")?; + if self.is_empty() { + return write!(f, "T0S"); + } + if self.weeks != 0 { + write!(f, "{}W", self.weeks)?; + } + if self.days != 0 { + write!(f, "{}D", self.days)?; + } + if self.hours != 0 || self.minutes != 0 || self.seconds != 0 { + write!(f, "T")?; + if self.hours != 0 { + write!(f, "{}H", self.hours)?; + } + if self.minutes != 0 { + write!(f, "{}M", self.minutes)?; + } + if self.seconds != 0 { + write!(f, "{}S", self.seconds)?; + } + } + + Ok(()) + } +} + +impl PartialDateTime { + pub fn format_as_ical( + &self, + out: &mut impl Write, + fmt: &ICalendarValueType, + ) -> std::fmt::Result { + if matches!(fmt, ICalendarValueType::Date | ICalendarValueType::DateTime) { + write!( + out, + "{:04}{:02}{:02}", + self.year.unwrap_or_default(), + self.month.unwrap_or_default(), + self.day.unwrap_or_default() + )?; + } + + if matches!(fmt, ICalendarValueType::DateTime) { + write!(out, "T")?; + } + + if matches!(fmt, ICalendarValueType::DateTime | ICalendarValueType::Time) { + write!( + out, + "{:02}{:02}{:02}", + self.hour.unwrap_or_default(), + self.minute.unwrap_or_default(), + self.second.unwrap_or_default() + )?; + + if matches!((self.tz_hour, self.tz_minute), (Some(0), Some(0))) { + write!(out, "Z")?; + } + } + + if matches!(fmt, ICalendarValueType::UtcOffset) { + if self.tz_minus { + write!(out, "-")?; + } else { + write!(out, "+")?; + } + + write!( + out, + "{:02}{:02}", + self.tz_hour.unwrap_or_default(), + self.tz_minute.unwrap_or_default(), + )?; + } + + Ok(()) + } +} + +impl Display for ICalendar { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + self.write_to(f) + } +} + +#[cfg(test)] +mod tests { + use crate::{Entry, Parser, icalendar::ICalendar}; + + fn parse(input: &str) -> ICalendar { + let mut parser = Parser::new(input); + let Entry::ICalendar(ical) = parser.entry() else { + panic!("expected iCalendar for {input}"); + }; + ical + } + + fn write(ical: &ICalendar) -> String { + let out = ical.to_string(); + + #[cfg(feature = "rkyv")] + { + let bytes = rkyv::to_bytes::(ical).unwrap(); + let archived = + rkyv::access::(&bytes) + .unwrap(); + assert_eq!(out, archived.to_string(), "archived writer diverged"); + } + + out + } + + #[test] + fn test_write_fold_width() { + for input in [ + "RRULE:FREQ=WEEKLY;UNTIL=20210309T080000Z;INTERVAL=1;BYDAY=MO,TU,WE,TH,FR,SA;WKST=MO", + "RDATE;VALUE=PERIOD:19970101T180000Z/19970102T070000Z,19970109T180000Z/PT5H30M", + "FREEBUSY;FBTYPE=BUSY:20120103T091500Z/20120103T101500Z,20120113T130000Z/20120113T150000Z", + "EXDATE;TZID=US/Central:20170706T090000,20170713T090000,20170720T090000,20170803T090000", + "DTSTART;TZID=/softwarestudio.org/Olson_20011030_5/America/Chicago:20030515T183000", + "REQUEST-STATUS:EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE;;", + ] { + let ical = parse(&format!( + "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//x//x//EN\r\n\ + BEGIN:VEVENT\r\nUID:1\r\n{input}\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n" + )); + + let out = write(&ical); + crate::common::writer::assert_fold_width(&out, input); + + assert_eq!(write(&parse(&out)), out, "not idempotent for {input}"); + } + } + + #[test] + fn test_write_component_boundary_fold_width() { + let name = "X".repeat(90); + let ical = parse(&format!( + "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//x//x//EN\r\n\ + BEGIN:{name}\r\nEND:{name}\r\nEND:VCALENDAR\r\n" + )); + + let out = write(&ical); + crate::common::writer::assert_fold_width(&out, &name); + + assert_eq!(write(&parse(&out)), out, "not idempotent for {name}"); + } +} diff --git a/vendor/calcard/src/jscalendar/export/convert.rs b/vendor/calcard/src/jscalendar/export/convert.rs new file mode 100644 index 00000000..64e9169e --- /dev/null +++ b/vendor/calcard/src/jscalendar/export/convert.rs @@ -0,0 +1,2392 @@ +/* + * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC + * + * SPDX-License-Identifier: Apache-2.0 OR MIT + */ + +use crate::{ + common::{ + IanaParse, PartialDateTime, + timezone::{Tz, TzTimestamp}, + }, + icalendar::*, + jscalendar::{ + export::{ConvertedComponent, State}, + *, + }, +}; +use ahash::{AHashMap, AHashSet}; +use chrono::{TimeDelta, TimeZone}; +use jmap_tools::{JsonPointer, JsonPointerHandler, JsonPointerItem, Key, Map, Value}; +use std::str::FromStr; + +impl<'x, I: JSCalendarId, B: JSCalendarId> JSCalendar<'x, I, B> { + pub fn into_icalendar(self) -> Option { + self.0.into_object().map(|entries| { + let mut ical = ICalendar::default(); + + ical.from_jscalendar( + State { + tz: Default::default(), + tz_end: Default::default(), + tz_rid: Default::default(), + start: Default::default(), + recurrence_id: Default::default(), + uid: Default::default(), + entries, + is_date: false, + default_component_type: ICalendarComponentType::VCalendar, + }, + None, + ); + + ical + }) + } + + pub fn into_inner(self) -> Value<'x, JSCalendarProperty, JSCalendarValue> { + self.0 + } +} + +impl ICalendar { + #[allow(clippy::wrong_self_convention)] + pub(super) fn from_jscalendar( + &mut self, + mut state: State<'_, I, B>, + mut parent_component: Option<&mut ICalendarComponent>, + ) { + let mut root_conversions = None; + let mut locale = None; + let mut uid = state.uid.map(Cow::Borrowed); + let mut organizer_address = None; + let mut organizer_params: Vec = Vec::new(); + let mut organizer_sent_by = None; + let mut privacy = None; + let mut organizer_participant_id = None; + let mut has_organizer_participant = false; + let mut main_location_id = None; + let mut start = None; + let mut component_type = None; + let mut description_content_type = None; + let mut overrides = None; + let mut is_show_without_time = false; + let mut has_time_component = false; + let mut has_time_zone = false; + + for (key, value) in state.entries.as_mut_vec() { + match (key, value) { + (Key::Property(JSCalendarProperty::ICalendar), Value::Object(obj)) => { + root_conversions = + ConvertedComponent::try_from_object(std::mem::take(obj.as_mut_vec())); + } + (Key::Property(JSCalendarProperty::MainLocationId), Value::Str(text)) => { + main_location_id = Some(std::mem::take(text)); + } + (Key::Property(JSCalendarProperty::TimeZone), Value::Str(text)) => { + state.tz = Tz::from_str(text.as_ref()).ok(); + has_time_zone = true; + } + (Key::Property(JSCalendarProperty::EndTimeZone), Value::Str(text)) => { + state.tz_end = Tz::from_str(text.as_ref()).ok(); + } + (Key::Property(JSCalendarProperty::RecurrenceIdTimeZone), Value::Str(text)) => { + state.tz_rid = Tz::from_str(text.as_ref()).ok(); + } + (Key::Property(JSCalendarProperty::Uid), Value::Str(text)) => { + uid = Some(std::mem::take(text)); + } + (Key::Property(JSCalendarProperty::Locale), Value::Str(text)) => { + locale = Some(std::mem::take(text)); + } + (Key::Property(JSCalendarProperty::OrganizerCalendarAddress), Value::Str(text)) => { + organizer_address = Some(std::mem::take(text)); + } + (Key::Property(JSCalendarProperty::SentBy), Value::Str(text)) => { + organizer_sent_by = Some(std::mem::take(text)); + } + ( + Key::Property(JSCalendarProperty::Privacy), + Value::Element(JSCalendarValue::Privacy(value)), + ) => { + privacy = Some(*value); + } + (Key::Property(JSCalendarProperty::Privacy), Value::Str(_)) => { + privacy = Some(JSCalendarPrivacy::Private); + } + (Key::Property(JSCalendarProperty::DescriptionContentType), Value::Str(text)) => { + description_content_type = Some(std::mem::take(text)); + } + ( + Key::Property(JSCalendarProperty::Start), + Value::Element(JSCalendarValue::DateTime(dt)), + ) if matches!( + state.default_component_type, + ICalendarComponentType::VEvent | ICalendarComponentType::VTodo + ) => + { + has_time_component |= !dt.is_start_of_day(); + start = dt.to_naive_date_time(); + } + ( + Key::Property(JSCalendarProperty::Due | JSCalendarProperty::RecurrenceId), + Value::Element(JSCalendarValue::DateTime(dt)), + ) => { + has_time_component |= !dt.is_start_of_day(); + } + ( + Key::Property( + JSCalendarProperty::Duration | JSCalendarProperty::EstimatedDuration, + ), + Value::Element(JSCalendarValue::Duration(duration)), + ) => { + has_time_component |= + duration.hours != 0 || duration.minutes != 0 || duration.seconds != 0; + } + (Key::Property(JSCalendarProperty::ShowWithoutTime), Value::Bool(value)) => { + is_show_without_time = *value; + } + (Key::Property(JSCalendarProperty::RecurrenceRule), Value::Object(obj)) => { + if let Some(Value::Element(JSCalendarValue::DateTime(dt))) = + obj.get(&Key::Property(JSCalendarProperty::Until)) + { + has_time_component |= !dt.is_start_of_day(); + } + } + ( + Key::Property(JSCalendarProperty::Type), + Value::Element(JSCalendarValue::Type(typ)), + ) => { + component_type = typ.to_icalendar_component_type(); + } + (Key::Property(JSCalendarProperty::RecurrenceOverrides), Value::Object(obj)) + if matches!( + state.default_component_type, + ICalendarComponentType::VEvent | ICalendarComponentType::VTodo + ) => + { + has_time_component |= obj.iter().any(|(key, _)| { + matches!(key, Key::Property(JSCalendarProperty::DateTime(dt)) + if !dt.is_start_of_day()) + }); + overrides = Some(std::mem::take(obj)); + } + _ => (), + } + } + + // Apply override patch objects + if let Some(overrides) = &mut overrides { + for (_, value) in overrides.as_mut_vec().iter_mut() { + let Value::Object(obj) = value else { + continue; + }; + + let patch_count = obj.len(); + obj.as_mut_vec().retain(|(key, value)| match key { + Key::Property(JSCalendarProperty::Pointer(pointer)) => { + !JSCalendarProperty::is_forbidden_override_pointer(pointer) + } + Key::Property(JSCalendarProperty::Privacy) => { + let override_privacy = match value { + Value::Element(JSCalendarValue::Privacy(value)) => *value, + _ => JSCalendarPrivacy::Private, + }; + if override_privacy > privacy.unwrap_or(JSCalendarPrivacy::Public) { + privacy = Some(override_privacy); + } + false + } + Key::Property(property) => !property.is_forbidden_override_patch(), + _ => true, + }); + if obj.is_empty() && patch_count > 0 { + *value = Value::Null; + continue; + } + + let mut patched_obj = None; + + for (key, value) in obj.as_mut_vec() { + if let Key::Property(JSCalendarProperty::Pointer(ptr)) = key { + patched_obj + .get_or_insert_with(|| Value::Object(state.entries.clone())) + .patch_jptr(ptr.iter(), std::mem::take(value)); + } + } + + if let Some(patched_obj) = patched_obj { + let mut patched_props: AHashMap< + Key<'static, JSCalendarProperty>, + Vec>>, + > = AHashMap::new(); + + for (key, value) in std::mem::take(obj.as_mut_vec()) { + if let Key::Property(JSCalendarProperty::Pointer(ptr)) = key { + let mut ptr = ptr.into_iter(); + if let ( + Some(JsonPointerItem::Key(key)), + Some(JsonPointerItem::Key(subkey)), + ) = (ptr.next(), ptr.next()) + { + patched_props.entry(key).or_default().push(subkey); + } + } else { + obj.insert_unchecked(key, value); + } + } + + for (key, patched_value) in patched_obj.into_expanded_object() { + if let Some(keys) = patched_props.get(&key) { + let Value::Object(obj) = + obj.insert_or_get_mut(key, Value::Object(Map::default())) + else { + continue; + }; + for (key, value) in patched_value.into_expanded_object() { + if keys.contains(&key) { + obj.insert_unchecked(key, value); + } + } + } + } + } + } + } + + /* + In the iCalendar component, the value data types of the DTSTART, DUE and + RECURRENCE-ID properties all MUST be of the same form, either DATE, or DATE WITH + LOCAL TIME, or DATE WITH UTC TIME. It MUST be DATE if the "showWithoutTime" + property value is "true", the "timeZone" property value is not set, and the time + component is zero in the values of the "start", "due", "duration", + "estimatedDuration", "recurrenceId" properties, and the RecurrenceRule object's + "until" property, and in any key of the "recurrenceOverrides" property value. + */ + + state.is_date |= is_show_without_time && !has_time_zone && !has_time_component; + + // Build component + let mut component = + ICalendarComponent::new(component_type.unwrap_or(state.default_component_type)); + if parent_component.is_none() { + debug_assert!(self.components.is_empty()); + self.components + .push(ICalendarComponent::new(ICalendarComponentType::VCalendar)); + } + + // Add start date + if let Some(start) = start.and_then(|dt| { + state + .tz + .unwrap_or_default() + .from_local_datetime(&dt) + .single() + }) { + state.start = Some(start); + } + if let Some(dt) = state.start { + component.entries.push( + ICalendarEntry::new(ICalendarProperty::Dtstart) + .import_converted(&[JSCalendarProperty::Start], &mut root_conversions) + .with_date(dt, state.is_date), + ); + } + + // Add UID + if let Some(uid) = &uid { + component.entries.push( + ICalendarEntry::new(ICalendarProperty::Uid) + .import_converted(&[JSCalendarProperty::Uid], &mut root_conversions) + .with_value(uid.clone().into_owned()), + ); + } + + let mut show_without_time = None; + let mut add_recurrence_id = state.recurrence_id.is_some(); + for (key, value) in state.entries.into_vec() { + let Key::Property(property) = key else { + component.insert_jsprop(&[key.to_string().as_ref()], value); + continue; + }; + + match (&property, value, &component.component_type) { + ( + JSCalendarProperty::Links, + Value::Object(obj), + ICalendarComponentType::VEvent + | ICalendarComponentType::VTodo + | ICalendarComponentType::VCalendar, + ) => { + component.import_links(obj, &mut root_conversions); + } + ( + JSCalendarProperty::Participants, + Value::Object(obj), + ICalendarComponentType::VEvent | ICalendarComponentType::VTodo, + ) => { + for (name, value) in obj.into_vec() { + let Value::Object(mut value) = value else { + continue; + }; + + let mut item_conversions = ConvertedComponent::build(&mut value); + let mut entry = ICalendarEntry::new(ICalendarProperty::Attendee); + let mut participant = + ICalendarComponent::new(ICalendarComponentType::Participant); + let mut participant_name = None; + let mut calendar_address = None; + let mut status = None; + let mut progress = None; + let mut description = None; + let mut description_content_type = None; + let mut is_uuid5_key = false; + + for (sub_property, value) in value.into_vec() { + match (sub_property, value) { + ( + Key::Property(JSCalendarProperty::CalendarAddress), + Value::Str(text), + ) => { + is_uuid5_key = uuid5(text.as_ref()) == name.to_string(); + calendar_address = Some(text); + } + ( + Key::Property(JSCalendarProperty::DelegatedFrom), + Value::Str(text), + ) => { + entry.params.push(ICalendarParameter::delegated_from( + Uri::parse(text.into_owned()), + )); + } + ( + Key::Property(JSCalendarProperty::DelegatedTo), + Value::Str(text), + ) => { + entry.params.push(ICalendarParameter::delegated_to( + Uri::parse(text.into_owned()), + )); + } + (Key::Property(JSCalendarProperty::Email), Value::Str(text)) => { + entry.params.push(ICalendarParameter::email(Uri::parse( + text.into_owned(), + ))); + } + ( + Key::Property(JSCalendarProperty::ExpectReply), + Value::Bool(value), + ) => { + entry.params.push(ICalendarParameter::rsvp(value)); + } + ( + Key::Property(JSCalendarProperty::Kind), + Value::Element(JSCalendarValue::ParticipantKind(kind)), + ) => { + entry.params.push(ICalendarParameter::cutype(match kind { + JSCalendarParticipantKind::Individual => { + ICalendarUserTypes::Individual + } + JSCalendarParticipantKind::Group => { + ICalendarUserTypes::Group + } + JSCalendarParticipantKind::Resource => { + ICalendarUserTypes::Resource + } + JSCalendarParticipantKind::Location => { + ICalendarUserTypes::Room + } + })); + } + (Key::Property(JSCalendarProperty::Kind), Value::Str(text)) => { + entry + .params + .push(ICalendarParameter::cutype(text.to_lowercase())); + } + ( + Key::Property(JSCalendarProperty::MemberOf), + Value::Object(obj), + ) => { + for key in obj.into_expanded_boolean_set() { + entry.params.push(ICalendarParameter::member(Uri::parse( + key.into_string(), + ))); + } + } + (Key::Property(JSCalendarProperty::Name), Value::Str(text)) => { + participant_name = Some(text); + } + ( + Key::Property(JSCalendarProperty::ParticipationStatus), + Value::Element(JSCalendarValue::ParticipationStatus(status_)), + ) => { + status = Some(status_); + } + ( + Key::Property(JSCalendarProperty::Progress), + Value::Element(JSCalendarValue::Progress(progress_)), + ) => { + progress = Some(progress_); + } + (Key::Property(JSCalendarProperty::Roles), Value::Object(obj)) => { + for key in obj.into_expanded_boolean_set() { + match key { + Key::Property(JSCalendarProperty::ParticipantRole( + role, + )) => { + let role = match role { + JSCalendarParticipantRole::Optional => { + ICalendarParticipationRole::OptParticipant + } + JSCalendarParticipantRole::Informational => { + ICalendarParticipationRole::NonParticipant + } + JSCalendarParticipantRole::Chair => { + ICalendarParticipationRole::Chair + } + JSCalendarParticipantRole::Required => { + ICalendarParticipationRole::ReqParticipant + } + JSCalendarParticipantRole::Owner => { + ICalendarParticipationRole::Owner + } + }; + entry.params.push(ICalendarParameter::role(role)); + } + key => { + component.insert_jsprop::( + &[ + property.to_string().as_ref(), + name.to_string().as_ref(), + JSCalendarProperty::Roles:: + .to_string() + .as_ref(), + key.to_string().as_ref(), + ], + Value::Bool(true), + ); + } + } + } + } + (Key::Property(JSCalendarProperty::SentBy), Value::Str(text)) => { + entry.params.push(ICalendarParameter::sent_by(Uri::parse( + text.into_owned(), + ))); + } + ( + Key::Property(JSCalendarProperty::Description), + Value::Str(text), + ) => { + description = Some(text); + } + ( + Key::Property(JSCalendarProperty::DescriptionContentType), + Value::Str(text), + ) => { + description_content_type = Some(text); + } + (Key::Property(JSCalendarProperty::Links), Value::Object(obj)) => { + participant.import_links(obj, &mut item_conversions); + } + ( + Key::Property(JSCalendarProperty::PercentComplete), + Value::Number(number), + ) => { + participant.entries.push( + ICalendarEntry::new(ICalendarProperty::PercentComplete) + .with_value(number.cast_to_i64()) + .import_converted( + &[JSCalendarProperty::PercentComplete], + &mut item_conversions, + ), + ); + } + ( + Key::Property( + JSCalendarProperty::Type | JSCalendarProperty::ICalendar, + ), + _, + ) => {} + (sub_property, value) => { + if item_conversions.is_none() { + component.insert_jsprop( + &[ + property.to_string().as_ref(), + name.to_string().as_ref(), + sub_property.to_string().as_ref(), + ], + value, + ); + } else { + participant.insert_jsprop( + &[ + property.to_string().as_ref(), + name.to_string().as_ref(), + sub_property.to_string().as_ref(), + ], + value, + ); + } + } + } + } + + match (description, description_content_type) { + (Some(description), Some(content_type)) => { + participant.entries.push( + ICalendarEntry::new(ICalendarProperty::StyledDescription) + .with_param(ICalendarParameter::fmttype( + content_type.into_owned(), + )) + .with_value(description.into_owned()) + .import_converted( + &[JSCalendarProperty::Description], + &mut item_conversions, + ), + ); + } + (Some(description), None) => { + participant.entries.push( + ICalendarEntry::new(ICalendarProperty::Description) + .with_value(description.into_owned()) + .import_converted( + &[JSCalendarProperty::Description], + &mut item_conversions, + ), + ); + } + _ => {} + } + + match (status, progress) { + ( + Some(JSCalendarParticipationStatus::Accepted) | None, + Some(progress), + ) => { + let partstat = match progress { + JSCalendarProgress::NeedsAction => { + Some(ICalendarParticipationStatus::NeedsAction) + } + JSCalendarProgress::InProcess => { + Some(ICalendarParticipationStatus::InProcess) + } + JSCalendarProgress::Completed => { + Some(ICalendarParticipationStatus::Completed) + } + JSCalendarProgress::Failed => { + Some(ICalendarParticipationStatus::Failed) + } + // Not a valid Participant progress value per section 4.4.5 + JSCalendarProgress::Cancelled => None, + }; + if let Some(partstat) = partstat { + entry.params.push(ICalendarParameter::partstat(partstat)); + } + } + (Some(status), _) => { + entry + .params + .push(ICalendarParameter::partstat(match status { + JSCalendarParticipationStatus::Accepted => { + ICalendarParticipationStatus::Accepted + } + JSCalendarParticipationStatus::Declined => { + ICalendarParticipationStatus::Declined + } + JSCalendarParticipationStatus::NeedsAction => { + ICalendarParticipationStatus::NeedsAction + } + JSCalendarParticipationStatus::Tentative => { + ICalendarParticipationStatus::Tentative + } + JSCalendarParticipationStatus::Delegated => { + ICalendarParticipationStatus::Delegated + } + })); + } + _ => {} + } + + let has_component = + !participant.entries.is_empty() || item_conversions.is_some(); + let has_entry = !entry.params.is_empty() || item_conversions.is_none(); + let is_organizer = organizer_address.is_some() + && organizer_address == calendar_address + && !has_organizer_participant; + has_organizer_participant |= is_organizer; + + /* + The "owner" role needs not convert to the ROLE parameter of the + ATTENDEE property if the Participant object converts to both the + ATTENDEE and ORGANIZER property. + */ + + if is_organizer { + entry.params.retain(|param| { + !matches!( + (¶m.name, ¶m.value), + ( + ICalendarParameterName::Role, + ICalendarParameterValue::Role( + ICalendarParticipationRole::Owner + ) + ) + ) + }); + } + + if let Some(calendar_address) = calendar_address { + let calendar_address = + ICalendarValue::Uri(Uri::parse(calendar_address.into_owned())); + + if has_component { + participant.entries.push( + ICalendarEntry::new(ICalendarProperty::CalendarAddress) + .with_value(calendar_address.clone()), + ); + } + if has_entry { + entry.values.push(calendar_address); + } + } + + if let Some(participant_name) = participant_name { + if has_component { + participant.entries.push( + ICalendarEntry::new(ICalendarProperty::Summary) + .with_value(participant_name.as_ref().to_string()) + .import_converted( + &[JSCalendarProperty::Name], + &mut item_conversions, + ), + ); + } + if has_entry { + entry + .params + .push(ICalendarParameter::cn(participant_name.into_owned())); + } + } + + debug_assert!(has_component || !entry.values.is_empty()); + + /* + The Participant object converts to an ATTENDEE property and PARTICIPANT + component, unless it is fully represented by the ORGANIZER property: + its "calendarAddress" property value matches the + "organizerCalendarAddress" property value, it does not have any role + but the "owner" role set, and it only has properties set that convert + to ORGANIZER parameters. + */ + + let is_organizer_only = is_organizer + && !has_component + && entry.params.iter().all(|param| { + matches!( + param.name, + ICalendarParameterName::Cn + | ICalendarParameterName::Email + | ICalendarParameterName::SentBy + ) + }); + + let participant_id = name.into_string(); + + if is_organizer_only { + organizer_params.append(&mut entry.params); + if !is_uuid5_key { + organizer_params + .push(ICalendarParameter::jsid(participant_id.clone())); + } + organizer_participant_id = Some(participant_id.clone()); + } else if is_organizer { + organizer_params.extend( + entry + .params + .iter() + .filter(|param| { + matches!( + param.name, + ICalendarParameterName::Cn + | ICalendarParameterName::Email + | ICalendarParameterName::SentBy + ) + }) + .cloned(), + ); + } + + if has_component { + if !is_uuid5_key { + participant.entries.push( + ICalendarEntry::new(ICalendarProperty::Jsid) + .with_value(participant_id.clone()), + ); + } + if let Some(item_conversions) = item_conversions { + participant = item_conversions.apply_conversions(participant, self); + } + component + .component_ids + .push(self.push_component(participant)); + } + + if !entry.values.is_empty() && !is_organizer_only { + let mut attendee = entry.import_converted_with_id( + &[JSCalendarProperty::Participants], + &mut root_conversions, + Some(participant_id.as_str()), + ); + if !is_uuid5_key { + attendee.add_param(ICalendarParameter::jsid(participant_id)); + } + + component.entries.push(attendee); + } + } + } + ( + JSCalendarProperty::Alerts, + Value::Object(obj), + ICalendarComponentType::VEvent | ICalendarComponentType::VTodo, + ) => { + let mut alerts = Vec::new(); + let mut has_related_to = false; + + for (name, value) in obj.into_vec() { + let Value::Object(mut value) = value else { + continue; + }; + + let mut item_conversions = ConvertedComponent::build(&mut value); + let mut alert = ICalendarComponent::new(ICalendarComponentType::VAlarm); + + for (sub_property, value) in value.into_vec() { + match (sub_property, value) { + ( + Key::Property( + JSCalendarProperty::Type | JSCalendarProperty::ICalendar, + ), + _, + ) => {} + ( + Key::Property(JSCalendarProperty::Acknowledged), + Value::Element(JSCalendarValue::DateTime(dt)), + ) => { + alert.entries.push( + ICalendarEntry::new(ICalendarProperty::Acknowledged) + .with_value(PartialDateTime::from_utc_timestamp( + dt.timestamp, + )) + .import_converted( + &[JSCalendarProperty::Acknowledged], + &mut item_conversions, + ), + ); + } + ( + Key::Property(JSCalendarProperty::Action), + Value::Element(JSCalendarValue::AlertAction(action)), + ) => { + alert.entries.push( + ICalendarEntry::new(ICalendarProperty::Action) + .with_value(ICalendarValue::Action(match action { + JSCalendarAlertAction::Display => { + ICalendarAction::Display + } + JSCalendarAlertAction::Email => { + ICalendarAction::Email + } + })) + .import_converted( + &[JSCalendarProperty::Action], + &mut item_conversions, + ), + ); + } + ( + Key::Property(JSCalendarProperty::RelatedTo), + Value::Object(obj), + ) => { + has_related_to = true; + alert.import_relations(obj, &mut item_conversions); + } + ( + Key::Property(JSCalendarProperty::Trigger), + Value::Object(obj), + ) => { + let mut offset = None; + let mut rel_to = None; + let mut when = None; + + for (key, value) in obj.into_vec() { + match (key, value) { + ( + Key::Property(JSCalendarProperty::Offset), + Value::Element(JSCalendarValue::Duration(value)), + ) => { + offset = Some(value); + } + ( + Key::Property(JSCalendarProperty::RelativeTo), + Value::Element(JSCalendarValue::RelativeTo(value)), + ) => { + rel_to = Some(value); + } + ( + Key::Property(JSCalendarProperty::When), + Value::Element(JSCalendarValue::DateTime(value)), + ) => { + when = Some(value); + } + (Key::Property(JSCalendarProperty::Type), _) => {} + (key, value) => { + alert.insert_jsprop( + &[ + JSCalendarProperty::Trigger:: + .to_string() + .as_ref(), + key.to_string().as_ref(), + ], + value, + ); + } + } + } + + if let Some(when) = when { + alert.entries.push( + ICalendarEntry::new(ICalendarProperty::Trigger) + .with_param(ICalendarParameter::value( + ICalendarValueType::DateTime, + )) + .with_value(PartialDateTime::from_utc_timestamp( + when.timestamp, + )) + .import_converted( + &[ + JSCalendarProperty::Trigger, + JSCalendarProperty::When, + ], + &mut item_conversions, + ), + ); + } else if let Some(offset) = offset { + alert.entries.push( + ICalendarEntry::new(ICalendarProperty::Trigger) + .with_param_opt(rel_to.map(|rel_to| { + ICalendarParameter::related(match rel_to { + JSCalendarRelativeTo::Start => { + ICalendarRelated::Start + } + JSCalendarRelativeTo::End => { + ICalendarRelated::End + } + }) + })) + .with_value(offset) + .import_converted( + &[ + JSCalendarProperty::Trigger, + JSCalendarProperty::Offset, + ], + &mut item_conversions, + ), + ); + } + } + (sub_property, value) => { + alert + .insert_jsprop(&[sub_property.to_string().as_ref()], value); + } + } + } + + if let Some(item_conversions) = item_conversions { + alert = item_conversions.apply_conversions(alert, self); + } + + if !alert.entries.is_empty() { + alert.entries.push( + ICalendarEntry::new(ICalendarProperty::Jsid) + .with_value(name.to_string().into_owned()), + ); + alerts.push(alert); + } + } + + // Map RELATED-TO properties + let mut alert_mappings = Vec::new(); + if has_related_to { + alert_mappings = alerts + .iter() + .filter_map(|alert| { + Some((alert.jsid()?.to_string(), alert.uid()?.to_string())) + }) + .collect(); + } + for mut alert in alerts { + if has_related_to { + for related_to in alert.properties_mut(&ICalendarProperty::RelatedTo) { + if let Some(ICalendarValue::Text(related_to)) = + related_to.values.first_mut() + && let Some((_, uid)) = + alert_mappings.iter().find(|(jsid, _)| jsid == related_to) + { + *related_to = uid.clone(); + } + } + } + component.component_ids.push(self.push_component(alert)); + } + } + ( + JSCalendarProperty::Keywords, + Value::Object(obj), + ICalendarComponentType::VEvent + | ICalendarComponentType::VTodo + | ICalendarComponentType::VCalendar, + ) => { + component.entries.push( + ICalendarEntry::new(ICalendarProperty::Categories) + .with_values( + obj.into_expanded_boolean_set() + .map(|v| ICalendarValue::Text(v.into_string())) + .collect(), + ) + .import_converted( + &[JSCalendarProperty::Keywords], + &mut root_conversions, + ), + ); + } + ( + JSCalendarProperty::Privacy, + Value::Element(JSCalendarValue::Privacy(_)), + ICalendarComponentType::VEvent | ICalendarComponentType::VTodo, + ) => {} + ( + JSCalendarProperty::Color, + Value::Str(text), + ICalendarComponentType::VEvent + | ICalendarComponentType::VTodo + | ICalendarComponentType::VCalendar, + ) => { + component.entries.push( + ICalendarEntry::new(ICalendarProperty::Color) + .with_value(text.into_owned()) + .import_converted(&[JSCalendarProperty::Color], &mut root_conversions), + ); + } + ( + JSCalendarProperty::Categories, + Value::Object(obj), + ICalendarComponentType::VEvent + | ICalendarComponentType::VTodo + | ICalendarComponentType::VCalendar, + ) => { + for value in obj.into_expanded_boolean_set() { + component.entries.push( + ICalendarEntry::new(ICalendarProperty::Concept) + .with_value(ICalendarValue::Text(value.into_string())) + .import_converted( + &[JSCalendarProperty::Categories], + &mut root_conversions, + ), + ); + } + } + ( + JSCalendarProperty::VirtualLocations, + Value::Object(obj), + ICalendarComponentType::VEvent | ICalendarComponentType::VTodo, + ) => { + for (name, value) in obj.into_vec() { + let mut entry = ICalendarEntry::new(ICalendarProperty::Conference); + let mut is_uuid5_key = false; + + for (sub_property, value) in value.into_expanded_object() { + match (sub_property, value) { + ( + Key::Property(JSCalendarProperty::Features), + Value::Object(obj), + ) => { + for key in obj.into_expanded_boolean_set() { + let value = match key { + Key::Property( + JSCalendarProperty::VirtualLocationFeature(feature), + ) => ICalendarParameterValue::Feature(match feature { + JSCalendarVirtualLocationFeature::Audio => { + ICalendarFeatureType::Audio + } + JSCalendarVirtualLocationFeature::Chat => { + ICalendarFeatureType::Chat + } + JSCalendarVirtualLocationFeature::Feed => { + ICalendarFeatureType::Feed + } + JSCalendarVirtualLocationFeature::Moderator => { + ICalendarFeatureType::Moderator + } + JSCalendarVirtualLocationFeature::Phone => { + ICalendarFeatureType::Phone + } + JSCalendarVirtualLocationFeature::Screen => { + ICalendarFeatureType::Screen + } + JSCalendarVirtualLocationFeature::Video => { + ICalendarFeatureType::Video + } + }), + other => ICalendarParameterValue::Text( + other.to_string().into_owned(), + ), + }; + entry.params.push(ICalendarParameter::feature(value)); + } + } + (Key::Property(JSCalendarProperty::Name), Value::Str(text)) => { + entry + .params + .push(ICalendarParameter::label(text.into_owned())); + } + (Key::Property(JSCalendarProperty::Uri), Value::Str(text)) => { + is_uuid5_key = uuid5(text.as_ref()) == name.to_string(); + entry + .values + .push(ICalendarValue::Uri(Uri::parse(text.into_owned()))); + } + ( + Key::Property( + JSCalendarProperty::Type | JSCalendarProperty::ICalendar, + ), + _, + ) => {} + (sub_property, value) => { + component.insert_jsprop( + &[ + JSCalendarProperty::VirtualLocations:: + .to_string() + .as_ref(), + name.to_string().as_ref(), + sub_property.to_string().as_ref(), + ], + value, + ); + } + } + } + + if !is_uuid5_key { + entry.add_param(ICalendarParameter::jsid(name.into_string())); + } + + component.entries.push(entry.import_converted( + &[JSCalendarProperty::VirtualLocations], + &mut root_conversions, + )); + } + } + ( + JSCalendarProperty::Title, + Value::Str(text), + ICalendarComponentType::VEvent | ICalendarComponentType::VTodo, + ) => { + component.entries.push( + ICalendarEntry::new(ICalendarProperty::Summary) + .with_param_opt( + locale + .as_ref() + .map(|locale| ICalendarParameter::language(locale.to_string())), + ) + .with_value(text.into_owned()) + .import_converted(&[JSCalendarProperty::Title], &mut root_conversions), + ); + } + ( + JSCalendarProperty::Title, + Value::Str(text), + ICalendarComponentType::VCalendar, + ) => { + component.entries.push( + ICalendarEntry::new(ICalendarProperty::Name) + .with_param_opt( + locale + .as_ref() + .map(|locale| ICalendarParameter::language(locale.to_string())), + ) + .with_value(text.into_owned()) + .import_converted(&[JSCalendarProperty::Title], &mut root_conversions), + ); + } + ( + JSCalendarProperty::Locations, + Value::Object(obj), + ICalendarComponentType::VEvent | ICalendarComponentType::VTodo, + ) => { + let has_multi_location = obj.len() > 1; + for (name, value) in obj.into_vec() { + let Value::Object(mut value) = value else { + continue; + }; + let mut item_conversions = ConvertedComponent::build(&mut value); + let is_main_location = main_location_id + .as_ref() + .is_some_and(|l| l == &name.to_string()); + let mut is_uuid5_key = false; + let mut location = (item_conversions + .as_ref() + .is_some_and(|v| v.name.is_location()) + || value.iter().any(|(k, v)| match (k, v) { + ( + Key::Property( + JSCalendarProperty::Links + | JSCalendarProperty::LocationTypes + | JSCalendarProperty::Description, + ), + _, + ) => true, + ( + Key::Property(JSCalendarProperty::Coordinates), + Value::Str(uri), + ) => uri.strip_prefix("geo:").is_none_or(|v| { + !v.as_bytes() + .iter() + .all(|b| matches!(b, b'0'..=b'9' | b'.' | b',' | b'-')) + }), + _ => false, + }) + || (has_multi_location && !is_main_location)) + .then_some(ICalendarComponent::new(ICalendarComponentType::VLocation)); + + for (sub_property, value) in value.into_vec() { + match (sub_property, value) { + ( + Key::Property(JSCalendarProperty::LocationTypes), + Value::Object(obj), + ) => { + if let Some(location) = &mut location { + location.entries.push( + ICalendarEntry::new(ICalendarProperty::LocationType) + .import_converted( + &[JSCalendarProperty::LocationTypes], + &mut item_conversions, + ) + .with_values( + obj.into_expanded_boolean_set() + .map(|v| { + ICalendarValue::Text(v.into_string()) + }) + .collect(), + ), + ); + } + } + (Key::Property(JSCalendarProperty::Name), Value::Str(text)) => { + is_uuid5_key |= uuid5(text.as_ref()) == name.to_string(); + + if let Some(location) = &mut location { + location.entries.push( + ICalendarEntry::new(ICalendarProperty::Name) + .import_converted( + &[JSCalendarProperty::Name], + &mut item_conversions, + ) + .with_value(text.clone().into_owned()), + ); + } + + if location.is_none() || is_main_location { + component.entries.push( + ICalendarEntry::new(ICalendarProperty::Location) + .with_param_opt((!is_uuid5_key).then(|| { + ICalendarParameter::jsid( + name.clone().into_string(), + ) + })) + .with_param_opt( + (has_multi_location) + .then(|| ICalendarParameter::derived(true)), + ) + .with_value(text.into_owned()) + .import_converted( + &[JSCalendarProperty::Locations], + &mut root_conversions, + ), + ); + } + } + ( + Key::Property(JSCalendarProperty::Description), + Value::Str(text), + ) => { + if let Some(location) = &mut location { + location.entries.push( + ICalendarEntry::new(ICalendarProperty::Description) + .import_converted( + &[JSCalendarProperty::Description], + &mut item_conversions, + ) + .with_value(text.clone().into_owned()), + ); + } + } + ( + Key::Property(JSCalendarProperty::Coordinates), + Value::Str(text), + ) => { + is_uuid5_key |= uuid5(text.as_ref()) == name.to_string(); + if let Some(location) = &mut location { + let entry = + ICalendarEntry::new(ICalendarProperty::Coordinates) + .import_converted( + &[JSCalendarProperty::Coordinates], + &mut item_conversions, + ); + location.entries.push( + if entry.name == ICalendarProperty::Geo { + entry.with_values(parse_geo(text)) + } else { + entry.with_value(Uri::parse(text.into_owned())) + }, + ); + } else { + component.entries.push( + ICalendarEntry::new(ICalendarProperty::Geo) + .with_param_opt((!is_uuid5_key).then(|| { + ICalendarParameter::jsid( + name.clone().into_string(), + ) + })) + .with_values(parse_geo(text)) + .import_converted( + &[JSCalendarProperty::Locations], + &mut root_conversions, + ), + ); + } + } + (Key::Property(JSCalendarProperty::Links), Value::Object(obj)) => { + if let Some(location) = &mut location { + location.import_links(obj, &mut item_conversions); + } + } + ( + Key::Property( + JSCalendarProperty::Type | JSCalendarProperty::ICalendar, + ), + _, + ) => {} + (sub_property, value) => { + if let Some(location) = &mut location { + location.insert_jsprop( + &[sub_property.to_string().as_ref()], + value, + ); + } else { + component.insert_jsprop( + &[ + JSCalendarProperty::Locations:: + .to_string() + .as_ref(), + name.to_string().as_ref(), + sub_property.to_string().as_ref(), + ], + value, + ); + } + } + } + } + + if let Some(mut location) = location { + if !is_uuid5_key { + location.entries.push( + ICalendarEntry::new(ICalendarProperty::Jsid) + .with_value(name.to_string().into_owned()), + ); + } + if let Some(item_conversions) = item_conversions { + location = item_conversions.apply_conversions(location, self); + } + component.component_ids.push(self.push_component(location)); + } + } + } + ( + JSCalendarProperty::Created, + Value::Element(JSCalendarValue::DateTime(dt)), + ICalendarComponentType::VEvent + | ICalendarComponentType::VTodo + | ICalendarComponentType::VCalendar, + ) => { + component.entries.push( + ICalendarEntry::new(ICalendarProperty::Created) + .with_value(PartialDateTime::from_utc_timestamp(dt.timestamp)) + .import_converted( + &[JSCalendarProperty::Created], + &mut root_conversions, + ), + ); + } + ( + JSCalendarProperty::Description, + Value::Str(text), + ICalendarComponentType::VEvent + | ICalendarComponentType::VTodo + | ICalendarComponentType::VCalendar, + ) => { + if let Some(description_content_type) = description_content_type.take() { + component.entries.push( + ICalendarEntry::new(ICalendarProperty::StyledDescription) + .with_value(text.into_owned()) + .with_param(ICalendarParameter::fmttype( + description_content_type.into_owned(), + )) + .import_converted( + &[JSCalendarProperty::Description], + &mut root_conversions, + ), + ); + } else { + component.entries.push( + ICalendarEntry::new(ICalendarProperty::Description) + .with_value(text.into_owned()) + .import_converted( + &[JSCalendarProperty::Description], + &mut root_conversions, + ), + ); + } + } + ( + JSCalendarProperty::RecurrenceRule, + Value::Object(obj), + ICalendarComponentType::VEvent | ICalendarComponentType::VTodo, + ) => { + let mut rrule = ICalendarRecurrenceRule::default(); + + for (key, value) in obj.into_vec() { + let Key::Property(key) = key else { + continue; + }; + match (key, value) { + ( + JSCalendarProperty::Frequency, + Value::Element(JSCalendarValue::Frequency(value)), + ) => { + rrule.freq = value; + } + ( + JSCalendarProperty::Until, + Value::Element(JSCalendarValue::DateTime(value)), + ) => { + rrule.until = value + .to_naive_date_time() + .and_then(|dt| { + state + .tz + .unwrap_or_default() + .from_local_datetime(&dt) + .single() + }) + .map(|dt| { + if state.is_date { + PartialDateTime::from_date_timestamp( + dt.to_naive_timestamp(), + ) + } else { + PartialDateTime::from_utc_timestamp(dt.timestamp()) + } + }); + } + (JSCalendarProperty::Count, Value::Number(value)) => { + rrule.count = Some(value.cast_to_u64() as u32); + } + (JSCalendarProperty::Interval, Value::Number(value)) => { + rrule.interval = Some(value.cast_to_u64() as u16); + } + (JSCalendarProperty::BySecond, Value::Array(value)) => { + rrule.bysecond = value + .into_iter() + .filter_map(|v| v.as_i64().and_then(|n| u8::try_from(n).ok())) + .collect(); + } + (JSCalendarProperty::ByMinute, Value::Array(value)) => { + rrule.byminute = value + .into_iter() + .filter_map(|v| v.as_i64().and_then(|n| u8::try_from(n).ok())) + .collect(); + } + (JSCalendarProperty::ByHour, Value::Array(value)) => { + rrule.byhour = value + .into_iter() + .filter_map(|v| v.as_i64().and_then(|n| u8::try_from(n).ok())) + .collect(); + } + (JSCalendarProperty::ByDay, Value::Array(value)) => { + for item in value { + let mut weekday = None; + let mut ordwk = None; + + for (key, value) in item.into_expanded_object() { + match (key, value) { + ( + Key::Property(JSCalendarProperty::Day), + Value::Element(JSCalendarValue::Weekday(value)), + ) => { + weekday = Some(value); + } + ( + Key::Property(JSCalendarProperty::NthOfPeriod), + Value::Number(value), + ) => { + ordwk = i16::try_from(value.cast_to_i64()).ok(); + } + _ => {} + } + } + + if let Some(weekday) = weekday { + rrule.byday.push(ICalendarDay { weekday, ordwk }); + } + } + } + (JSCalendarProperty::ByMonthDay, Value::Array(value)) => { + rrule.bymonthday = value + .into_iter() + .filter_map(|v| v.as_i64().and_then(|n| i8::try_from(n).ok())) + .collect(); + } + (JSCalendarProperty::ByYearDay, Value::Array(value)) => { + rrule.byyearday = value + .into_iter() + .filter_map(|v| v.as_i64().and_then(|n| i16::try_from(n).ok())) + .collect(); + } + (JSCalendarProperty::ByWeekNo, Value::Array(value)) => { + rrule.byweekno = value + .into_iter() + .filter_map(|v| v.as_i64().and_then(|n| i8::try_from(n).ok())) + .collect(); + } + (JSCalendarProperty::ByMonth, Value::Array(value)) => { + rrule.bymonth = value + .into_iter() + .filter_map(|v| { + v.as_str().and_then(|v| ICalendarMonth::parse(v.as_bytes())) + }) + .collect(); + } + (JSCalendarProperty::BySetPosition, Value::Array(value)) => { + rrule.bysetpos = value + .into_iter() + .filter_map(|v| v.as_i64().and_then(|n| i32::try_from(n).ok())) + .collect(); + } + ( + JSCalendarProperty::FirstDayOfWeek, + Value::Element(JSCalendarValue::Weekday(value)), + ) => { + rrule.wkst = Some(value); + } + ( + JSCalendarProperty::Rscale, + Value::Element(JSCalendarValue::CalendarScale(value)), + ) => { + rrule.rscale = Some(value); + } + ( + JSCalendarProperty::Skip, + Value::Element(JSCalendarValue::Skip(value)), + ) => { + rrule.skip = Some(value); + } + (JSCalendarProperty::Type | JSCalendarProperty::ICalendar, _) => {} + (key, value) => { + component.insert_jsprop( + &[ + JSCalendarProperty::RecurrenceRule:: + .to_string() + .as_ref(), + key.to_string().as_ref(), + ], + value, + ); + } + } + } + + component.entries.push( + ICalendarEntry::new(ICalendarProperty::Rrule) + .with_value(rrule) + .import_converted( + &[JSCalendarProperty::RecurrenceRule], + &mut root_conversions, + ), + ); + } + ( + JSCalendarProperty::Updated, + Value::Element(JSCalendarValue::DateTime(dt)), + ICalendarComponentType::VEvent | ICalendarComponentType::VTodo, + ) => { + component.entries.push( + ICalendarEntry::new(ICalendarProperty::Dtstamp) + .with_value(PartialDateTime::from_utc_timestamp(dt.timestamp)) + .import_converted( + &[JSCalendarProperty::Updated], + &mut root_conversions, + ), + ); + } + ( + JSCalendarProperty::Updated, + Value::Element(JSCalendarValue::DateTime(dt)), + ICalendarComponentType::VCalendar, + ) => { + component.entries.push( + ICalendarEntry::new(ICalendarProperty::LastModified) + .with_value(PartialDateTime::from_utc_timestamp(dt.timestamp)) + .import_converted( + &[JSCalendarProperty::Updated], + &mut root_conversions, + ), + ); + } + ( + JSCalendarProperty::Due, + Value::Element(JSCalendarValue::DateTime(dt)), + ICalendarComponentType::VTodo, + ) => { + if let Some(dt) = dt.to_naive_date_time().and_then(|dt| { + state + .tz + .unwrap_or_default() + .from_local_datetime(&dt) + .single() + }) { + component.entries.push( + ICalendarEntry::new(ICalendarProperty::Due) + .import_converted(&[JSCalendarProperty::Due], &mut root_conversions) + .with_date(dt, state.is_date), + ); + } + } + ( + JSCalendarProperty::RecurrenceId, + Value::Element(JSCalendarValue::DateTime(dt)), + ICalendarComponentType::VEvent | ICalendarComponentType::VTodo, + ) => { + if let Some(dt) = dt.to_naive_date_time().and_then(|dt| { + state + .tz_rid + .or(state.tz) + .unwrap_or_default() + .from_local_datetime(&dt) + .single() + }) { + add_recurrence_id = false; + component.entries.push( + ICalendarEntry::new(ICalendarProperty::RecurrenceId) + .import_converted( + &[JSCalendarProperty::RecurrenceId], + &mut root_conversions, + ) + .with_date(dt, state.is_date), + ); + } + } + ( + JSCalendarProperty::Duration, + Value::Element(JSCalendarValue::Duration(duration)), + ICalendarComponentType::VEvent | ICalendarComponentType::VTodo, + ) => { + let entry = ICalendarEntry::new(if state.tz_end.is_none() { + ICalendarProperty::Duration + } else { + ICalendarProperty::Dtend + }) + .import_converted(&[JSCalendarProperty::Duration], &mut root_conversions); + if entry.name == ICalendarProperty::Dtend { + if let Some(end) = state.start.and_then(|start| { + start.checked_add_signed(TimeDelta::seconds(duration.as_seconds())) + }) { + component.entries.push( + entry.with_date( + state + .tz_end + .map(|tz_end| end.with_timezone(&tz_end)) + .unwrap_or(end), + state.is_date, + ), + ); + } else { + component.insert_jsprop::( + &[property.to_string().as_ref()], + Value::Element(JSCalendarValue::Duration(duration)), + ); + } + } else { + component.entries.push(entry.with_value(duration)); + } + } + ( + JSCalendarProperty::EstimatedDuration, + Value::Element(JSCalendarValue::Duration(duration)), + ICalendarComponentType::VEvent | ICalendarComponentType::VTodo, + ) => { + component.entries.push( + ICalendarEntry::new(ICalendarProperty::EstimatedDuration) + .with_value(duration) + .import_converted( + &[JSCalendarProperty::EstimatedDuration], + &mut root_conversions, + ), + ); + } + ( + JSCalendarProperty::Method, + Value::Element(JSCalendarValue::Method(method)), + ICalendarComponentType::VEvent | ICalendarComponentType::VTodo, + ) => { + if let Some(parent_component) = &mut parent_component + && !parent_component.has_property(&ICalendarProperty::Method) + { + parent_component.entries.push( + ICalendarEntry::new(ICalendarProperty::Method).with_value(method), + ); + } + } + ( + JSCalendarProperty::PercentComplete, + Value::Number(number), + ICalendarComponentType::VTodo, + ) => { + component.entries.push( + ICalendarEntry::new(ICalendarProperty::PercentComplete) + .with_value(number.cast_to_i64()) + .import_converted( + &[JSCalendarProperty::PercentComplete], + &mut root_conversions, + ), + ); + } + ( + JSCalendarProperty::Priority, + Value::Number(number), + ICalendarComponentType::VEvent | ICalendarComponentType::VTodo, + ) => { + component.entries.push( + ICalendarEntry::new(ICalendarProperty::Priority) + .with_value(number.cast_to_i64()) + .import_converted( + &[JSCalendarProperty::Priority], + &mut root_conversions, + ), + ); + } + ( + JSCalendarProperty::Sequence, + Value::Number(number), + ICalendarComponentType::VEvent | ICalendarComponentType::VTodo, + ) => { + component.entries.push( + ICalendarEntry::new(ICalendarProperty::Sequence) + .with_value(number.cast_to_i64()) + .import_converted( + &[JSCalendarProperty::Sequence], + &mut root_conversions, + ), + ); + } + ( + JSCalendarProperty::ProdId, + Value::Str(text), + ICalendarComponentType::VCalendar, + ) => { + component.entries.push( + ICalendarEntry::new(ICalendarProperty::Prodid) + .with_value(text.into_owned()) + .import_converted(&[JSCalendarProperty::ProdId], &mut root_conversions), + ); + } + ( + JSCalendarProperty::RelatedTo, + Value::Object(obj), + ICalendarComponentType::VEvent | ICalendarComponentType::VTodo, + ) => { + component.import_relations(obj, &mut root_conversions); + } + ( + JSCalendarProperty::ShowWithoutTime, + Value::Bool(value), + ICalendarComponentType::VEvent | ICalendarComponentType::VTodo, + ) => { + show_without_time = Some( + ICalendarEntry::new(ICalendarProperty::ShowWithoutTime) + .with_value(value) + .import_converted( + &[JSCalendarProperty::ShowWithoutTime], + &mut root_conversions, + ), + ); + } + ( + JSCalendarProperty::Progress, + Value::Element(JSCalendarValue::Progress(value)), + ICalendarComponentType::VTodo, + ) => { + component.entries.push( + ICalendarEntry::new(ICalendarProperty::Status) + .with_value(match value { + JSCalendarProgress::NeedsAction => ICalendarStatus::NeedsAction, + JSCalendarProgress::InProcess => ICalendarStatus::InProcess, + JSCalendarProgress::Completed => ICalendarStatus::Completed, + JSCalendarProgress::Failed => ICalendarStatus::Failed, + JSCalendarProgress::Cancelled => ICalendarStatus::Cancelled, + }) + .import_converted( + &[JSCalendarProperty::Progress], + &mut root_conversions, + ), + ); + } + ( + JSCalendarProperty::Status, + Value::Element(JSCalendarValue::EventStatus(value)), + ICalendarComponentType::VEvent, + ) => { + component.entries.push( + ICalendarEntry::new(ICalendarProperty::Status) + .with_value(match value { + JSCalendarEventStatus::Confirmed => ICalendarStatus::Confirmed, + JSCalendarEventStatus::Cancelled => ICalendarStatus::Cancelled, + JSCalendarEventStatus::Tentative => ICalendarStatus::Tentative, + }) + .import_converted(&[JSCalendarProperty::Status], &mut root_conversions), + ); + } + ( + JSCalendarProperty::Status | JSCalendarProperty::Progress, + Value::Str(text), + ICalendarComponentType::VEvent | ICalendarComponentType::VTodo, + ) => { + component.entries.push( + ICalendarEntry::new(ICalendarProperty::Status) + .with_value( + ICalendarStatus::parse(text.as_bytes()) + .map(ICalendarValue::Status) + .unwrap_or_else(|| ICalendarValue::Text(text.into_owned())), + ) + .import_converted(&[JSCalendarProperty::Status], &mut root_conversions), + ); + } + ( + JSCalendarProperty::Source, + Value::Str(text), + ICalendarComponentType::VCalendar, + ) => { + component.entries.push( + ICalendarEntry::new(ICalendarProperty::Source) + .with_value(Uri::parse(text.into_owned())) + .import_converted(&[JSCalendarProperty::Source], &mut root_conversions), + ); + } + ( + JSCalendarProperty::FreeBusyStatus, + Value::Element(JSCalendarValue::FreeBusyStatus(value)), + ICalendarComponentType::VEvent | ICalendarComponentType::VTodo, + ) => { + component.entries.push( + ICalendarEntry::new(ICalendarProperty::Transp) + .with_value(match value { + JSCalendarFreeBusyStatus::Free => { + ICalendarTransparency::Transparent + } + JSCalendarFreeBusyStatus::Busy => ICalendarTransparency::Opaque, + }) + .import_converted( + &[JSCalendarProperty::FreeBusyStatus], + &mut root_conversions, + ), + ); + } + ( + JSCalendarProperty::Entries, + Value::Array(items), + ICalendarComponentType::VCalendar, + ) => { + for item in items { + if let Some(entries) = item.into_object() { + self.from_jscalendar( + State { + tz: None, + tz_end: None, + tz_rid: None, + start: None, + recurrence_id: None, + uid: None, + entries, + is_date: false, + default_component_type: ICalendarComponentType::VEvent, + }, + Some(&mut component), + ); + } + } + } + + // Skip previously processed properties + ( + JSCalendarProperty::Type + | JSCalendarProperty::ICalendar + | JSCalendarProperty::Uid + | JSCalendarProperty::MainLocationId + | JSCalendarProperty::Start + | JSCalendarProperty::TimeZone + | JSCalendarProperty::EndTimeZone + | JSCalendarProperty::RecurrenceIdTimeZone + | JSCalendarProperty::Locale + | JSCalendarProperty::OrganizerCalendarAddress + | JSCalendarProperty::SentBy + | JSCalendarProperty::DescriptionContentType + | JSCalendarProperty::Version + | JSCalendarProperty::RecurrenceOverrides, + _, + _, + ) + | (_, Value::Null, _) => {} + (property, value, _) => { + component.insert_jsprop(&[property.to_string().as_ref()], value); + } + } + } + + // Process recurrence overrides + if let Some(overrides) = overrides { + let mut exdates = Vec::new(); + let mut rdates = Vec::new(); + let converted_props = root_conversions.as_ref().map(|conv| { + conv.converted_props + .iter() + .filter_map(|(keys, _)| match (keys.first()?, keys.get(1)?) { + ( + Key::Property(JSCalendarProperty::RecurrenceOverrides), + Key::Property(JSCalendarProperty::DateTime(dt)), + ) => Some(*dt), + _ => None, + }) + .collect::>() + }); + + for (key, value) in overrides.into_vec() { + let (Key::Property(JSCalendarProperty::DateTime(jsdt)), Value::Object(mut obj)) = + (key, value) + else { + continue; + }; + let Some(dt) = jsdt.to_naive_date_time().and_then(|dt| { + state + .tz + .unwrap_or_default() + .from_local_datetime(&dt) + .single() + }) else { + continue; + }; + let has_converted_prop = converted_props + .as_ref() + .is_some_and(|props| props.contains(&jsdt)); + + // RDATE with value type PERIOD + if has_converted_prop + && obj.len() == 1 + && let Some(Value::Element(JSCalendarValue::Duration(duration))) = + obj.get(&Key::Property(JSCalendarProperty::Duration)) + { + let duration = duration.clone(); + let mut entry = ICalendarEntry::new(ICalendarProperty::Rdate) + .import_converted( + &[ + JSCalendarProperty::RecurrenceOverrides, + JSCalendarProperty::DateTime(jsdt), + ], + &mut root_conversions, + ) + .with_period(dt, duration); + entry.name = ICalendarProperty::Rdate; + component.entries.push(entry); + continue; + } + + if !obj.is_empty() { + if !obj.contains_key_value( + &Key::Property(JSCalendarProperty::Excluded), + &Value::Bool(true), + ) { + if let Some(privacy) = privacy { + obj.insert_unchecked( + Key::Property(JSCalendarProperty::Privacy), + Value::Element(JSCalendarValue::Privacy(privacy)), + ); + } + if let Some(address) = &organizer_address + && obj + .get(&Key::Property(JSCalendarProperty::Participants)) + .and_then(Value::as_object) + .is_some_and(|participants| { + participants.iter().any(|(_, participant)| { + matches!( + participant.as_object().and_then(|participant| { + participant.get(&Key::Property( + JSCalendarProperty::CalendarAddress, + )) + }), + Some(Value::Str(participant_address)) + if participant_address == address + ) + }) + }) + { + obj.insert_unchecked( + Key::Property(JSCalendarProperty::OrganizerCalendarAddress), + Value::Str(address.clone()), + ); + if let Some(sent_by) = &organizer_sent_by { + obj.insert_unchecked( + Key::Property(JSCalendarProperty::SentBy), + Value::Str(sent_by.clone()), + ); + } + } + self.from_jscalendar( + State { + tz: state.tz, + tz_end: state.tz_end, + tz_rid: state.tz_rid, + start: None, + recurrence_id: Some(dt), + entries: obj, + uid: uid.as_deref(), + is_date: state.is_date, + default_component_type: component.component_type.clone(), + }, + parent_component.as_deref_mut(), + ); + } else { + // EXDATE + if has_converted_prop { + component.entries.push( + ICalendarEntry::new(ICalendarProperty::Exdate) + .import_converted( + &[ + JSCalendarProperty::RecurrenceOverrides, + JSCalendarProperty::DateTime(jsdt), + ], + &mut root_conversions, + ) + .with_date(dt, state.is_date), + ); + } else { + exdates.push(dt); + } + } + } else { + // RDATE + if has_converted_prop { + component.entries.push( + ICalendarEntry::new(ICalendarProperty::Rdate) + .import_converted( + &[ + JSCalendarProperty::RecurrenceOverrides, + JSCalendarProperty::DateTime(jsdt), + ], + &mut root_conversions, + ) + .with_date(dt, state.is_date), + ); + } else { + rdates.push(dt) + } + } + } + + for (prop, values) in [ + (ICalendarProperty::Rdate, rdates), + (ICalendarProperty::Exdate, exdates), + ] { + if !values.is_empty() { + component + .entries + .push(ICalendarEntry::new(prop).with_dates(values, state.is_date)); + } + } + } + + // Add organizer, if not present in participants + if let Some(organizer_address) = organizer_address { + let mut organizer = ICalendarEntry::new(ICalendarProperty::Organizer); + if let Some(sent_by) = organizer_sent_by + && !organizer_params + .iter() + .any(|param| param.name == ICalendarParameterName::SentBy) + { + organizer_params.push(ICalendarParameter::sent_by(Uri::parse( + sent_by.into_owned(), + ))); + } + organizer.params = organizer_params; + organizer.values = vec![ICalendarValue::Text(organizer_address.into_owned())]; + + // Restore the parameters of Participant objects that only convert to ORGANIZER + if let Some(participant_id) = organizer_participant_id { + organizer = organizer.import_converted_with_id( + &[JSCalendarProperty::Participants], + &mut root_conversions, + Some(participant_id.as_str()), + ); + organizer.name = ICalendarProperty::Organizer; + } + + component.entries.push(organizer); + } + + // Add parent recurrence ID + if add_recurrence_id && let Some(dt) = state.recurrence_id { + component.entries.push( + ICalendarEntry::new(ICalendarProperty::RecurrenceId) + .import_converted(&[JSCalendarProperty::RecurrenceId], &mut root_conversions) + .with_date(dt, state.is_date), + ); + } + + // Add showWithoutTime: true unless there are times with DATE types + if let Some(show_without_time) = show_without_time + && (!show_without_time.params.is_empty() + || matches!( + show_without_time.values.first(), + Some(ICalendarValue::Boolean(false)) + ) + || !component.entries.iter().any(|e| { + matches!( + e.name, + ICalendarProperty::Dtstart + | ICalendarProperty::Dtend + | ICalendarProperty::Due + ) && matches!( + e.parameter(&ICalendarParameterName::Value), + Some(ICalendarParameterValue::Value(ICalendarValueType::Date)) + ) + })) + { + component.entries.push(show_without_time); + } + + if matches!( + component.component_type, + ICalendarComponentType::VEvent | ICalendarComponentType::VTodo + ) { + let has_preserved_class = root_conversions.as_mut().is_some_and(|conversions| { + conversions.retain_class_property(matches!( + privacy, + None | Some(JSCalendarPrivacy::Private) + )) + }); + + if !has_preserved_class && let Some(privacy) = privacy { + component.entries.push( + ICalendarEntry::new(ICalendarProperty::Class) + .with_value(match privacy { + JSCalendarPrivacy::Public => ICalendarClassification::Public, + JSCalendarPrivacy::Private => ICalendarClassification::Private, + JSCalendarPrivacy::Secret => ICalendarClassification::Confidential, + }) + .import_converted(&[JSCalendarProperty::Privacy], &mut root_conversions), + ); + } + } + + if let Some(root_conversions) = root_conversions { + component = root_conversions.apply_conversions(component, self); + } + + // The VERSION property is required by RFC 5545 and has no JSCalendar counterpart + if component.component_type == ICalendarComponentType::VCalendar + && !component + .entries + .iter() + .any(|entry| entry.name == ICalendarProperty::Version) + { + component.entries.push( + ICalendarEntry::new(ICalendarProperty::Version).with_value("2.0".to_string()), + ); + } + + if let Some(parent_component) = parent_component { + parent_component + .component_ids + .push(self.push_component(component)); + } else { + self.components[0] = component; + } + } + + pub fn push_component(&mut self, component: ICalendarComponent) -> u32 { + let comp_num = self.components.len(); + self.components.push(component); + comp_num as u32 + } +} + +impl ICalendarComponent { + fn import_links( + &mut self, + obj: Map<'_, JSCalendarProperty, JSCalendarValue>, + conversion: &mut Option>, + ) { + let mut has_url = self + .entries + .iter() + .any(|entry| entry.name == ICalendarProperty::Url); + + for (name, value) in obj.into_vec() { + let mut entry = ICalendarEntry::new(ICalendarProperty::Link); + let mut link_rel = None; + let mut title = None; + let mut has_display = false; + let mut is_uuid5_key = false; + + for (sub_property, value) in value.into_expanded_object() { + match (sub_property, value) { + ( + Key::Property(JSCalendarProperty::Type | JSCalendarProperty::ICalendar), + _, + ) => {} + (Key::Property(JSCalendarProperty::Href), Value::Str(text)) => { + is_uuid5_key = uuid5(text.as_ref()) == name.to_string(); + entry + .values + .push(ICalendarValue::Uri(Uri::parse(text.into_owned()))); + } + (Key::Property(JSCalendarProperty::ContentType), Value::Str(text)) => { + entry + .params + .push(ICalendarParameter::fmttype(text.into_owned())); + } + (Key::Property(JSCalendarProperty::Size), Value::Number(number)) => { + entry + .params + .push(ICalendarParameter::size(number.cast_to_u64())); + } + ( + Key::Property(JSCalendarProperty::Rel), + Value::Element(JSCalendarValue::LinkRelation(relation)), + ) => { + link_rel = Some(relation); + } + (Key::Property(JSCalendarProperty::Display), Value::Object(obj)) => { + has_display = true; + for key in obj.into_expanded_boolean_set() { + let value = match key { + Key::Property(JSCalendarProperty::LinkDisplay(display)) => { + ICalendarParameterValue::Display(match display { + JSCalendarLinkDisplay::Badge => ICalendarDisplayType::Badge, + JSCalendarLinkDisplay::Graphic => { + ICalendarDisplayType::Graphic + } + JSCalendarLinkDisplay::Fullsize => { + ICalendarDisplayType::Fullsize + } + JSCalendarLinkDisplay::Thumbnail => { + ICalendarDisplayType::Thumbnail + } + }) + } + other => { + ICalendarParameterValue::Text(other.to_string().into_owned()) + } + }; + entry.params.push(ICalendarParameter::display(value)); + } + } + (Key::Property(JSCalendarProperty::Title), Value::Str(text)) => { + title = Some(text.into_owned()); + } + (sub_property, value) => { + self.insert_jsprop( + &[ + JSCalendarProperty::Links::.to_string().as_ref(), + name.to_string().as_ref(), + sub_property.to_string().as_ref(), + ], + value, + ); + } + } + } + + /* + Which iCalendar property to choose is implementation-specific. As a guideline, + stopping at the first matching criteria: + + 1. Choose ATTACH if the "rel" property value is "enclosure". + 2. Choose IMAGE if the "rel" property value is "icon". + 3. Choose URL if the "rel" property value is "describedby" and no URL property + is set in the iCalendar component. + 4. Choose LINK otherwise. + */ + + entry.name = match link_rel { + Some(LinkRelation::Enclosure) => ICalendarProperty::Attach, + Some(LinkRelation::Icon) => ICalendarProperty::Image, + Some(LinkRelation::Describedby) => ICalendarProperty::Url, + None if has_display => ICalendarProperty::Image, + _ => ICalendarProperty::Link, + }; + + let link_id = name.into_string(); + let mut entry = entry.import_converted_with_id( + &[JSCalendarProperty::Links], + conversion, + Some(link_id.as_str()), + ); + if !is_uuid5_key { + entry.add_param(ICalendarParameter::jsid(link_id)); + }; + + // At most one URL property is allowed per component + if entry.name == ICalendarProperty::Url { + if !has_url { + has_url = true; + } else { + entry.name = ICalendarProperty::Link; + } + } + + /* + The LINKREL parameter is not specified for the ATTACH, IMAGE and URL properties. + For interoperability, implementations SHOULD NOT set the LINKREL parameter on + these properties. + */ + + if let Some(relation) = link_rel + && entry.name == ICalendarProperty::Link + { + entry.add_param(ICalendarParameter::linkrel(relation)); + } + + /* + The "title" property converts to the FILENAME parameter for the ATTACH and IMAGE + properties. It converts to the LABEL parameter for the LINK and URL properties. + */ + + if let Some(title) = title { + entry.add_param( + if matches!( + entry.name, + ICalendarProperty::Attach | ICalendarProperty::Image + ) { + ICalendarParameter::filename(title) + } else { + ICalendarParameter::label(title) + }, + ); + } + + self.entries.push(entry); + } + } + + fn import_relations( + &mut self, + obj: Map<'_, JSCalendarProperty, JSCalendarValue>, + conversion: &mut Option>, + ) { + for (name, value) in obj.into_vec() { + let mut entry = ICalendarEntry::new(ICalendarProperty::RelatedTo); + + for (sub_property, value) in value.into_expanded_object() { + match (sub_property, value) { + (Key::Property(JSCalendarProperty::Relation), Value::Object(obj)) => { + for key in obj.into_expanded_boolean_set() { + let value = match key { + Key::Property(JSCalendarProperty::RelationValue(relation)) => { + ICalendarParameterValue::Reltype(match relation { + JSCalendarRelation::First => { + ICalendarRelationshipType::First + } + JSCalendarRelation::Next => ICalendarRelationshipType::Next, + JSCalendarRelation::Child => { + ICalendarRelationshipType::Child + } + JSCalendarRelation::Parent => { + ICalendarRelationshipType::Parent + } + JSCalendarRelation::Snooze => { + ICalendarRelationshipType::Snooze + } + }) + } + other => { + ICalendarParameterValue::Text(other.to_string().into_owned()) + } + }; + entry.params.push(ICalendarParameter::reltype(value)); + } + } + ( + Key::Property(JSCalendarProperty::Type | JSCalendarProperty::ICalendar), + _, + ) => {} + (sub_property, value) => { + self.insert_jsprop( + &[ + JSCalendarProperty::RelatedTo::.to_string().as_ref(), + name.to_string().as_ref(), + sub_property.to_string().as_ref(), + ], + value, + ); + } + } + } + + self.entries.push( + entry + .with_value(name.into_string()) + .import_converted(&[JSCalendarProperty::RelatedTo], conversion), + ); + } + } + + fn insert_jsprop( + &mut self, + path: &[&str], + value: Value<'_, JSCalendarProperty, JSCalendarValue>, + ) { + self.entries.push( + ICalendarEntry::new(ICalendarProperty::Jsprop) + .with_param(ICalendarParameter::jsptr(JsonPointer::< + JSCalendarProperty, + >::encode(path))) + .with_value(serde_json::to_string(&value).unwrap_or_default()), + ); + } +} + +fn parse_geo(text: Cow<'_, str>) -> Vec { + if let Some((a, b)) = text + .strip_prefix("geo:") + .and_then(|v| v.trim().split_once(',')) + .and_then(|(a, b)| { + let a = a.parse::().ok()?; + let b = b.parse::().ok()?; + Some((a, b)) + }) + { + vec![ICalendarValue::Float(a), ICalendarValue::Float(b)] + } else { + vec![ICalendarValue::Text(text.into_owned())] + } +} diff --git a/vendor/calcard/src/jscalendar/export/mod.rs b/vendor/calcard/src/jscalendar/export/mod.rs new file mode 100644 index 00000000..873ce317 --- /dev/null +++ b/vendor/calcard/src/jscalendar/export/mod.rs @@ -0,0 +1,41 @@ +/* + * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC + * + * SPDX-License-Identifier: Apache-2.0 OR MIT + */ + +use crate::{ + common::timezone::Tz, + icalendar::ICalendarComponentType, + jscalendar::{JSCalendarId, JSCalendarProperty, JSCalendarValue}, +}; +use chrono::DateTime; +use jmap_tools::{Key, Map, Value}; + +pub mod convert; +pub mod params; +pub mod props; + +struct State<'x, I: JSCalendarId, B: JSCalendarId> { + entries: Map<'x, JSCalendarProperty, JSCalendarValue>, + default_component_type: ICalendarComponentType, + uid: Option<&'x str>, + tz: Option, + tz_end: Option, + tz_rid: Option, + start: Option>, + recurrence_id: Option>, + is_date: bool, +} + +#[allow(clippy::type_complexity)] +struct ConvertedComponent<'x, I: JSCalendarId, B: JSCalendarId> { + pub(super) name: ICalendarComponentType, + pub(super) converted_props: Vec<( + Vec>>, + Value<'x, JSCalendarProperty, JSCalendarValue>, + )>, + pub(super) converted_props_count: usize, + pub(super) properties: Vec, JSCalendarValue>>, + pub(super) components: Vec, JSCalendarValue>>, +} diff --git a/vendor/calcard/src/jscalendar/export/params.rs b/vendor/calcard/src/jscalendar/export/params.rs new file mode 100644 index 00000000..8d7276a1 --- /dev/null +++ b/vendor/calcard/src/jscalendar/export/params.rs @@ -0,0 +1,434 @@ +/* + * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC + * + * SPDX-License-Identifier: Apache-2.0 OR MIT + */ + +use crate::{ + common::{ + IanaParse, LinkRelation, PartialDateTime, + parser::Boolean, + timezone::{Tz, TzTimestamp}, + }, + icalendar::*, + jscalendar::{ + JSCalendarDateTime, JSCalendarId, JSCalendarProperty, JSCalendarValue, + export::ConvertedComponent, + }, + jscontact::export::params::ParamValue, +}; +use chrono::{DateTime, NaiveDateTime, TimeZone}; +use jmap_tools::{Key, Value}; +use std::str::FromStr; + +impl ICalendarEntry { + pub(super) fn import_converted( + self, + path: &[JSCalendarProperty], + conversions: &mut Option>, + ) -> Self { + self.import_converted_with_id(path, conversions, None) + } + + pub(super) fn import_converted_with_id( + mut self, + path: &[JSCalendarProperty], + conversions: &mut Option>, + id: Option<&str>, + ) -> Self { + let Some(conversions) = conversions + .as_mut() + .filter(|c| c.converted_props_count < c.converted_props.len()) + else { + return self; + }; + + // Obtain jsid + let value = self.values.first().and_then(|v| v.as_text()); + let jsid = if matches!(self.name, ICalendarProperty::RelatedTo) { + value + } else { + id.or_else(|| self.jsid()) + }; + + let mut matched_once = false; + + 'outer: for (keys, value) in conversions.converted_props.iter_mut() { + if matches!(value, Value::Null) { + continue; + } + + for (pos, item) in path.iter().enumerate() { + if !keys + .iter() + .any(|k| matches!(k, Key::Property(p) if p == item)) + { + if pos == 0 && matched_once { + // Array is sorted, so if we didn't match the first item, + // we won't match any further. + break 'outer; + } else { + continue 'outer; + } + } else { + matched_once = true; + } + } + + if jsid + .map(Key::Borrowed) + .is_none_or(|prop_id| keys.iter().any(|k| k == &prop_id)) + { + self.import_converted_properties(std::mem::take(value)); + conversions.converted_props_count += 1; + break; + } + } + + // Update binary contents + if self + .parameter(&ICalendarParameterName::Value) + .is_some_and(|v| { + matches!( + v, + ICalendarParameterValue::Value(ICalendarValueType::Binary) + ) + }) + && let Some(ICalendarValue::Uri(Uri::Data(data))) = self.values.first_mut() + { + let bin = ICalendarValue::Binary(std::mem::take(&mut data.data)); + if let Some(content_type) = data.content_type.take() + && !self.has_parameter(&ICalendarParameterName::Fmttype) + { + self.params.push(ICalendarParameter::fmttype(content_type)); + } + self.values[0] = bin; + } + + self + } + + pub(super) fn import_converted_properties( + &mut self, + props: Value<'_, JSCalendarProperty, JSCalendarValue>, + ) { + for (key, value) in props.into_expanded_object() { + match key { + Key::Property(JSCalendarProperty::Name) => { + if let Some(name) = value.into_string() { + self.name = ICalendarProperty::parse(name.as_bytes()) + .unwrap_or(ICalendarProperty::Other(name.to_ascii_uppercase())); + } + } + Key::Property(JSCalendarProperty::Parameters) => { + self.import_jcal_params(value); + } + _ => {} + } + } + } + + pub(super) fn import_jcal_params( + &mut self, + params: Value<'_, JSCalendarProperty, JSCalendarValue>, + ) { + for (key, value) in params.into_expanded_object() { + let mut values = match value { + Value::Array(values) => values.into_iter().filter_map(ParamValue::try_from_value), + value => vec![value] + .into_iter() + .filter_map(ParamValue::try_from_value), + } + .peekable(); + + if values.peek().is_none() { + continue; + } + + let key = key.to_string(); + let Some(param) = ICalendarParameterName::try_parse(key.as_bytes()) else { + let key = key.into_owned(); + + for value in values { + self.params.push(ICalendarParameter { + name: ICalendarParameterName::Other(key.to_ascii_uppercase()), + value: value.into_string().into_owned().into(), + }); + } + + continue; + }; + + for value in values { + let value = match ¶m { + ICalendarParameterName::Value => { + let value = value.into_string(); + ICalendarValueType::parse(value.as_bytes()) + .map(ICalendarParameterValue::Value) + .unwrap_or_else(|| ICalendarParameterValue::Text(value.into_owned())) + } + ICalendarParameterName::Size | ICalendarParameterName::Order => { + match value.into_number() { + Ok(n) => ICalendarParameterValue::Integer(n.unsigned_abs()), + Err(value) => { + ICalendarParameterValue::Text(value.into_string().into_owned()) + } + } + } + ICalendarParameterName::Rsvp | ICalendarParameterName::Derived => { + let value = value.into_string(); + Boolean::parse(value.as_bytes()) + .map(|v| ICalendarParameterValue::Bool(v.0)) + .unwrap_or_else(|| ICalendarParameterValue::Text(value.into_owned())) + } + ICalendarParameterName::Altrep + | ICalendarParameterName::DelegatedFrom + | ICalendarParameterName::DelegatedTo + | ICalendarParameterName::Dir + | ICalendarParameterName::Member + | ICalendarParameterName::SentBy + | ICalendarParameterName::Schema => { + ICalendarParameterValue::Uri(Uri::parse(value.into_string().into_owned())) + } + ICalendarParameterName::Range => { + let value = value.into_string(); + if value.eq_ignore_ascii_case("THISANDFUTURE") { + ICalendarParameterValue::Bool(true) + } else { + ICalendarParameterValue::Text(value.into_owned()) + } + } + ICalendarParameterName::Gap => { + let value = value.into_string(); + ICalendarDuration::parse(value.as_bytes()) + .map(ICalendarParameterValue::Duration) + .unwrap_or_else(|| ICalendarParameterValue::Text(value.into_owned())) + } + ICalendarParameterName::Cutype => { + let value = value.into_string(); + ICalendarUserTypes::parse(value.as_bytes()) + .map(ICalendarParameterValue::Cutype) + .unwrap_or_else(|| ICalendarParameterValue::Text(value.into_owned())) + } + ICalendarParameterName::Fbtype => { + let value = value.into_string(); + ICalendarFreeBusyType::parse(value.as_bytes()) + .map(ICalendarParameterValue::Fbtype) + .unwrap_or_else(|| ICalendarParameterValue::Text(value.into_owned())) + } + ICalendarParameterName::Partstat => { + let value = value.into_string(); + ICalendarParticipationStatus::parse(value.as_bytes()) + .map(ICalendarParameterValue::Partstat) + .unwrap_or_else(|| ICalendarParameterValue::Text(value.into_owned())) + } + ICalendarParameterName::Related => { + let value = value.into_string(); + ICalendarRelated::parse(value.as_bytes()) + .map(ICalendarParameterValue::Related) + .unwrap_or_else(|| ICalendarParameterValue::Text(value.into_owned())) + } + ICalendarParameterName::Reltype => { + let value = value.into_string(); + ICalendarRelationshipType::parse(value.as_bytes()) + .map(ICalendarParameterValue::Reltype) + .unwrap_or_else(|| ICalendarParameterValue::Text(value.into_owned())) + } + ICalendarParameterName::Role => { + let value = value.into_string(); + ICalendarParticipationRole::parse(value.as_bytes()) + .map(ICalendarParameterValue::Role) + .unwrap_or_else(|| ICalendarParameterValue::Text(value.into_owned())) + } + ICalendarParameterName::ScheduleAgent => { + let value = value.into_string(); + ICalendarScheduleAgentValue::parse(value.as_bytes()) + .map(ICalendarParameterValue::ScheduleAgent) + .unwrap_or_else(|| ICalendarParameterValue::Text(value.into_owned())) + } + ICalendarParameterName::ScheduleForceSend => { + let value = value.into_string(); + ICalendarScheduleForceSendValue::parse(value.as_bytes()) + .map(ICalendarParameterValue::ScheduleForceSend) + .unwrap_or_else(|| ICalendarParameterValue::Text(value.into_owned())) + } + ICalendarParameterName::Display => { + let value = value.into_string(); + ICalendarDisplayType::parse(value.as_bytes()) + .map(ICalendarParameterValue::Display) + .unwrap_or_else(|| ICalendarParameterValue::Text(value.into_owned())) + } + ICalendarParameterName::Feature => { + let value = value.into_string(); + ICalendarFeatureType::parse(value.as_bytes()) + .map(ICalendarParameterValue::Feature) + .unwrap_or_else(|| ICalendarParameterValue::Text(value.into_owned())) + } + ICalendarParameterName::Linkrel => { + let value = value.into_string(); + LinkRelation::parse(value.as_bytes()) + .map(ICalendarParameterValue::Linkrel) + .unwrap_or_else(|| ICalendarParameterValue::Text(value.into_owned())) + } + _ => ICalendarParameterValue::Text(value.into_string().into_owned()), + }; + + self.params.push(ICalendarParameter { + name: param.clone(), + value, + }); + } + } + } +} + +impl ICalendarEntry { + /* + A DATE value has no time and no timezone: the TZID parameter MUST NOT be applied to + it (Section 3.2.19 of [RFC5545]). + */ + + pub(super) fn with_date(mut self, dt: DateTime, is_date: bool) -> Self { + if !is_date { + return self.with_date_time(dt); + } + + debug_assert!(self.values.is_empty()); + if !self.has_parameter(&ICalendarParameterName::Value) { + self.params + .push(ICalendarParameter::value(ICalendarValueType::Date)); + } + self.values + .push(PartialDateTime::from_date_timestamp(dt.to_naive_timestamp()).into()); + self + } + + pub(super) fn with_dates(mut self, dts: Vec>, is_date: bool) -> Self { + if !is_date { + return self.with_date_times(dts); + } + + debug_assert!(self.values.is_empty()); + if !self.has_parameter(&ICalendarParameterName::Value) { + self.params + .push(ICalendarParameter::value(ICalendarValueType::Date)); + } + for dt in dts { + self.values + .push(PartialDateTime::from_date_timestamp(dt.to_naive_timestamp()).into()); + } + self + } + + pub(super) fn with_date_time(mut self, dt: DateTime) -> Self { + debug_assert!(self.values.is_empty()); + + // A date-time value cannot have the PERIOD value type + self.params.retain(|param| { + !matches!( + (¶m.name, ¶m.value), + ( + ICalendarParameterName::Value, + ICalendarParameterValue::Value(ICalendarValueType::Period) + ) + ) + }); + + // Best effort to restore the original timezone + let tz_id = self.tz_id(); + let has_tz_id = tz_id.is_some(); + self.insert_date( + dt, + has_tz_id, + !has_tz_id, + tz_id.and_then(|id| Tz::from_str(id).ok()), + ); + self + } + + pub(super) fn with_period(mut self, dt: DateTime, duration: ICalendarDuration) -> Self { + self = self.with_date_time(dt); + + if let Some(ICalendarValue::PartialDateTime(start)) = self.values.pop() { + self.values + .push(ICalendarValue::Period(ICalendarPeriod::Duration { + start: *start, + duration, + })); + } + + if !self.has_parameter(&ICalendarParameterName::Value) { + self.params + .push(ICalendarParameter::value(ICalendarValueType::Period)); + } + + self + } + + pub(super) fn with_date_times(mut self, dts: Vec>) -> Self { + debug_assert!(self.values.is_empty()); + + let tz_id = self.tz_id(); + let has_tz_id = tz_id.is_some(); + let entry_tz = tz_id + .and_then(|id| Tz::from_str(id).ok()) + .or_else(|| dts.first().map(|dt| dt.timezone())); + + for (pos, dt) in dts.into_iter().enumerate() { + self.insert_date(dt, has_tz_id, pos == 0, entry_tz); + } + + self + } + + fn insert_date( + &mut self, + mut dt: DateTime, + has_tz_id: bool, + add_tz_id: bool, + entry_tz: Option, + ) { + if let Some(tz) = entry_tz + && tz != dt.timezone() + { + dt = dt.with_timezone(&tz); + } + + let tz = dt.timezone(); + if has_tz_id { + self.values + .push(PartialDateTime::from_naive_timestamp(dt.to_naive_timestamp()).into()); + } else if tz.is_utc() { + self.values + .push(PartialDateTime::from_utc_timestamp(dt.timestamp()).into()); + } else { + if add_tz_id && let Some(tz_name) = tz.name() { + self.params + .push(ICalendarParameter::tzid(tz_name.into_owned())); + } + self.values + .push(PartialDateTime::from_naive_timestamp(dt.to_naive_timestamp()).into()); + } + } +} + +impl JSCalendarDateTime { + pub fn to_utc_date_time(&self) -> Option> { + DateTime::from_timestamp(self.timestamp, 0) + .and_then(|local| Tz::UTC.from_local_datetime(&local.naive_utc()).single()) + } + + pub fn to_naive_date_time(&self) -> Option { + DateTime::from_timestamp(self.timestamp, 0).map(|local| local.naive_utc()) + } +} + +impl From for PartialDateTime { + fn from(dt: JSCalendarDateTime) -> Self { + if !dt.is_local { + Self::from_utc_timestamp(dt.timestamp) + } else { + Self::from_naive_timestamp(dt.timestamp) + } + } +} diff --git a/vendor/calcard/src/jscalendar/export/props.rs b/vendor/calcard/src/jscalendar/export/props.rs new file mode 100644 index 00000000..8ab39466 --- /dev/null +++ b/vendor/calcard/src/jscalendar/export/props.rs @@ -0,0 +1,414 @@ +/* + * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC + * + * SPDX-License-Identifier: Apache-2.0 OR MIT + */ + +use crate::{ + Parser, + common::{CalendarScale, IanaParse, IanaType, PartialDateTime, parser::Integer}, + icalendar::*, + jscalendar::{JSCalendarId, JSCalendarProperty, JSCalendarValue, export::ConvertedComponent}, +}; +use jmap_tools::{JsonPointer, JsonPointerItem, Key, Map, Value}; + +impl ConvertedComponent<'_, I, B> { + pub(super) fn apply_conversions( + self, + mut component: ICalendarComponent, + ical: &mut ICalendar, + ) -> ICalendarComponent { + if !self.properties.is_empty() { + component.import_properties(self.properties); + } + if !self.components.is_empty() { + let mut jcal_components = self.components.into_iter(); + let mut stack = Vec::new(); + + loop { + if let Some(jcal_component) = jcal_components.next() { + let Some(items) = jcal_component.into_array() else { + continue; + }; + let mut items = items.into_iter(); + + if let ( + Some(Value::Str(name)), + Some(Value::Array(properties)), + Some(Value::Array(child_components)), + ) = (items.next(), items.next(), items.next()) + { + // Process the component + let mut sub_component = ICalendarComponent::new( + ICalendarComponentType::parse(name.as_bytes()).unwrap_or_else(|| { + ICalendarComponentType::Other(name.to_ascii_uppercase()) + }), + ); + sub_component.import_properties(properties); + if !child_components.is_empty() { + stack.push((jcal_components, component)); + jcal_components = child_components.into_iter(); + component = sub_component; + } else { + component + .component_ids + .push(ical.push_component(sub_component)); + } + } + } else if let Some((next_components, mut parent_component)) = stack.pop() { + parent_component + .component_ids + .push(ical.push_component(component)); + jcal_components = next_components; + component = parent_component; + } else { + break; + } + } + } + + component + } + + pub(super) fn retain_class_property(&mut self, keep_unknown: bool) -> bool { + let mut has_class = false; + self.properties.retain(|property| { + let Some([Value::Str(name), _, _, value, ..]) = property.as_array() else { + return true; + }; + if ICalendarProperty::parse(name.as_bytes()) != Some(ICalendarProperty::Class) { + return true; + } + let keep = keep_unknown + && !has_class + && matches!(value, Value::Str(value) + if ICalendarClassification::parse(value.as_bytes()).is_none()); + has_class |= keep; + keep + }); + has_class + } +} + +impl ICalendarComponent { + pub(super) fn import_properties( + &mut self, + props: Vec, JSCalendarValue>>, + ) { + for prop in props.into_iter().flat_map(|prop| prop.into_array()) { + let mut prop = prop.into_iter(); + let Some(name) = prop.next().and_then(|v| v.into_string()).map(|name| { + ICalendarProperty::parse(name.as_bytes()) + .unwrap_or(ICalendarProperty::Other(name.to_ascii_uppercase())) + }) else { + continue; + }; + let Some(params) = prop.next() else { + continue; + }; + let Some(value_type) = + prop.next() + .and_then(|v| v.into_string()) + .map(|v| match ICalendarValueType::parse(v.as_bytes()) { + Some(v) => IanaType::Iana(v), + None => IanaType::Other(v.to_ascii_uppercase()), + }) + else { + continue; + }; + + let (default_type, _) = name.default_types(); + let convert_type = value_type + .iana() + .filter(|&v| v != &ICalendarValueType::Unknown) + .map(|v| ValueType::Ical(*v)) + .unwrap_or(default_type); + let Some(values) = prop.next().and_then(|v| match v { + Value::Array(arr) => Some( + arr.into_iter() + .filter_map(|v| convert_value(v, &convert_type).ok()) + .collect::>(), + ), + v => convert_value(v, &convert_type).ok().map(|v| vec![v]), + }) else { + continue; + }; + + let mut entry = ICalendarEntry::new(name); + entry.import_jcal_params(params); + entry.values = values; + if convert_type != default_type { + entry.params.push(ICalendarParameter::value(value_type)); + } + self.entries.push(entry); + } + } +} + +pub(super) fn convert_value<'x, I: JSCalendarId, B: JSCalendarId>( + value: Value<'x, JSCalendarProperty, JSCalendarValue>, + value_type: &'_ ValueType, +) -> Result, JSCalendarValue>> { + match value { + Value::Element(e) => match e { + JSCalendarValue::CalendarScale(v) => Ok(ICalendarValue::CalendarScale(v)), + JSCalendarValue::DateTime(v) => Ok(ICalendarValue::PartialDateTime(Box::new(v.into()))), + JSCalendarValue::Duration(v) => Ok(ICalendarValue::Duration(v)), + JSCalendarValue::Method(v) => Ok(ICalendarValue::Method(v)), + JSCalendarValue::AlertAction(_) + | JSCalendarValue::FreeBusyStatus(_) + | JSCalendarValue::ParticipantKind(_) + | JSCalendarValue::ParticipationStatus(_) + | JSCalendarValue::Privacy(_) + | JSCalendarValue::Progress(_) + | JSCalendarValue::RelativeTo(_) + | JSCalendarValue::ScheduleAgent(_) + | JSCalendarValue::EventStatus(_) + | JSCalendarValue::Frequency(_) + | JSCalendarValue::Skip(_) + | JSCalendarValue::Weekday(_) + | JSCalendarValue::Month(_) + | JSCalendarValue::LinkRelation(_) + | JSCalendarValue::Type(_) + | JSCalendarValue::Id(_) + | JSCalendarValue::BlobId(_) + | JSCalendarValue::IdReference(_) => Err(Value::Element(e)), + }, + Value::Str(s) => { + match value_type { + ValueType::CalendarScale => { + if let Some(value) = CalendarScale::parse(s.as_ref().as_bytes()) { + return Ok(ICalendarValue::CalendarScale(value)); + } + } + ValueType::Method => { + if let Some(value) = ICalendarMethod::parse(s.as_ref().as_bytes()) { + return Ok(ICalendarValue::Method(value)); + } + } + ValueType::Classification => { + if let Some(value) = ICalendarClassification::parse(s.as_ref().as_bytes()) { + return Ok(ICalendarValue::Classification(value)); + } + } + ValueType::Status => { + if let Some(value) = ICalendarStatus::parse(s.as_ref().as_bytes()) { + return Ok(ICalendarValue::Status(value)); + } + } + ValueType::Transparency => { + if let Some(value) = ICalendarTransparency::parse(s.as_ref().as_bytes()) { + return Ok(ICalendarValue::Transparency(value)); + } + } + ValueType::Action => { + if let Some(value) = ICalendarAction::parse(s.as_ref().as_bytes()) { + return Ok(ICalendarValue::Action(value)); + } + } + ValueType::BusyType => { + if let Some(value) = ICalendarFreeBusyType::parse(s.as_ref().as_bytes()) { + return Ok(ICalendarValue::BusyType(value)); + } + } + ValueType::ParticipantType => { + if let Some(value) = ICalendarParticipantType::parse(s.as_ref().as_bytes()) { + return Ok(ICalendarValue::ParticipantType(value)); + } + } + ValueType::ResourceType => { + if let Some(value) = ICalendarResourceType::parse(s.as_ref().as_bytes()) { + return Ok(ICalendarValue::ResourceType(value)); + } + } + ValueType::Proximity => { + if let Some(value) = ICalendarProximityValue::parse(s.as_ref().as_bytes()) { + return Ok(ICalendarValue::Proximity(value)); + } + } + ValueType::Ical(typ) => match typ { + ICalendarValueType::Uri | ICalendarValueType::CalAddress => { + return Ok(ICalendarValue::Uri(Uri::parse(s))); + } + ICalendarValueType::Date => { + let mut dt = PartialDateTime::default(); + if dt.parse_ical_date(&mut s.as_ref().as_bytes().iter().peekable()) { + return Ok(ICalendarValue::PartialDateTime(Box::new(dt))); + } + } + ICalendarValueType::Time => { + let mut dt = PartialDateTime::default(); + if dt.parse_ical_time(&mut s.as_ref().as_bytes().iter().peekable()) { + return Ok(ICalendarValue::PartialDateTime(Box::new(dt))); + } + } + ICalendarValueType::DateTime => { + let mut dt = PartialDateTime::default(); + if dt.parse_timestamp(&mut s.as_ref().as_bytes().iter().peekable(), false) { + return Ok(ICalendarValue::PartialDateTime(Box::new(dt))); + } + } + ICalendarValueType::UtcOffset => { + let mut dt = PartialDateTime::default(); + if dt.parse_zone(&mut s.as_ref().as_bytes().iter().peekable()) { + return Ok(ICalendarValue::PartialDateTime(Box::new(dt))); + } + } + ICalendarValueType::Duration => { + if let Some(duration) = ICalendarDuration::parse(s.as_ref().as_bytes()) { + return Ok(ICalendarValue::Duration(duration)); + } + } + ICalendarValueType::Float => { + if let Ok(float) = s.as_ref().parse::() { + return Ok(ICalendarValue::Float(float)); + } + } + ICalendarValueType::Integer => { + if let Some(integer) = Integer::parse(s.as_ref().as_bytes()) { + return Ok(ICalendarValue::Integer(integer.0)); + } + } + ICalendarValueType::Period => { + if let Some(period) = ICalendarPeriod::parse(s.as_ref().as_bytes()) { + return Ok(ICalendarValue::Period(period)); + } + } + ICalendarValueType::Recur => { + let mut parser = Parser::new(s.as_ref()); + if let Ok(recur) = parser.rrule() { + return Ok(ICalendarValue::RecurrenceRule(Box::new(recur))); + } + } + ICalendarValueType::Boolean => { + if s.eq_ignore_ascii_case("true") { + return Ok(ICalendarValue::Boolean(true)); + } else if s.eq_ignore_ascii_case("false") { + return Ok(ICalendarValue::Boolean(false)); + } + } + ICalendarValueType::Binary => { + return Ok(match Uri::parse(s) { + Uri::Data(data) => ICalendarValue::Binary(data.data), + Uri::Location(text) => ICalendarValue::Text(text), + }); + } + ICalendarValueType::Text + | ICalendarValueType::Unknown + | ICalendarValueType::XmlReference + | ICalendarValueType::Uid => (), + }, + } + + Ok(ICalendarValue::Text(s.into_owned())) + } + Value::Bool(b) => Ok(ICalendarValue::Boolean(b)), + Value::Number(n) => match n.try_cast_to_i64() { + Ok(i) => Ok(ICalendarValue::Integer(i)), + Err(f) => Ok(ICalendarValue::Float(f)), + }, + value => Err(value), + } +} + +impl<'x, I: JSCalendarId, B: JSCalendarId> ConvertedComponent<'x, I, B> { + #[allow(clippy::type_complexity)] + pub(super) fn try_from_object( + obj: Vec<( + Key<'x, JSCalendarProperty>, + Value<'x, JSCalendarProperty, JSCalendarValue>, + )>, + ) -> Option { + let mut converted = ConvertedComponent { + name: ICalendarComponentType::Other(String::new()), + converted_props: Vec::new(), + converted_props_count: 0, + properties: Vec::new(), + components: Vec::new(), + }; + let mut has_name = false; + for (sub_property, value) in obj { + match (sub_property, value) { + (Key::Property(JSCalendarProperty::ConvertedProperties), Value::Object(obj)) => { + for (key, value) in obj.into_vec() { + let ptr = match key { + Key::Property(JSCalendarProperty::Pointer(ptr)) => ptr, + _ => JsonPointer::parse(key.to_string().as_ref()), + }; + + let mut keys = Vec::with_capacity(2); + for item in ptr.into_iter() { + match item { + JsonPointerItem::Key(key) => { + let key = match &key { + Key::Borrowed(v) if v.contains('/') => v, + Key::Owned(v) if v.contains('/') => v.as_str(), + _ => { + keys.push(key); + continue; + } + }; + for item in JsonPointer::parse(key).into_iter() { + keys.push(match item { + JsonPointerItem::Key(k) => k, + JsonPointerItem::Number(n) => Key::Owned(n.to_string()), + JsonPointerItem::Root | JsonPointerItem::Wildcard => { + continue; + } + }); + } + } + JsonPointerItem::Number(v) => { + keys.push(Key::Owned(v.to_string())); + } + JsonPointerItem::Root | JsonPointerItem::Wildcard => (), + } + } + + converted.converted_props.push((keys, value)); + } + } + (Key::Property(JSCalendarProperty::Properties), Value::Array(array)) => { + converted.properties = array; + } + (Key::Property(JSCalendarProperty::Components), Value::Array(array)) => { + converted.components = array; + } + (Key::Property(JSCalendarProperty::Name), Value::Str(text)) => { + converted.name = + ICalendarComponentType::parse(text.as_bytes()).unwrap_or_else(|| { + ICalendarComponentType::Other(text.to_ascii_uppercase()) + }); + has_name = true; + } + _ => {} + } + } + + if !converted.converted_props.is_empty() { + converted + .converted_props + .sort_unstable_by(|a, b| a.0.cmp(&b.0)); + } + + (!converted.properties.is_empty() + || !converted.components.is_empty() + || has_name + || !converted.converted_props.is_empty()) + .then_some(converted) + } + + pub(super) fn build( + entries: &mut Map<'x, JSCalendarProperty, JSCalendarValue>, + ) -> Option { + for (property, value) in entries.as_mut_vec() { + if let (Key::Property(JSCalendarProperty::ICalendar), Value::Object(obj)) = + (property, value) + { + return Self::try_from_object(std::mem::take(obj.as_mut_vec())); + } + } + + None + } +} diff --git a/vendor/calcard/src/jscalendar/import/convert.rs b/vendor/calcard/src/jscalendar/import/convert.rs new file mode 100644 index 00000000..72137e7b --- /dev/null +++ b/vendor/calcard/src/jscalendar/import/convert.rs @@ -0,0 +1,2049 @@ +/* + * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC + * + * SPDX-License-Identifier: Apache-2.0 OR MIT + */ + +use crate::{ + common::timezone::{Tz, TzTimestamp}, + icalendar::{timezone::TzResolver, *}, + jscalendar::{ + import::{ConversionOptions, EntryState, State, params::ExtractParams}, + *, + }, +}; +use chrono::DateTime; +use jmap_tools::{JsonPointer, Key, Map, Value}; + +impl ICalendar { + pub fn into_jscalendar( + mut self, + ) -> JSCalendar<'static, I, B> { + JSCalendar( + self.to_jscalendar( + &self.build_owned_tz_resolver(), + 0, + &ConversionOptions::default(), + ) + .into_object(), + ) + } + + pub fn into_jscalendar_with_opt( + mut self, + options: ConversionOptions, + ) -> JSCalendar<'static, I, B> { + JSCalendar( + self.to_jscalendar(&self.build_owned_tz_resolver(), 0, &options) + .into_object(), + ) + } +} + +impl ICalendar { + #[allow(clippy::wrong_self_convention)] + pub(super) fn to_jscalendar( + &mut self, + tz_resolver: &TzResolver, + component_id: u32, + options: &ConversionOptions, + ) -> State { + let mut state = State { + include_ical_components: options.include_ical_components, + ..Default::default() + }; + + // Take component + let Some(component) = self.components.get_mut(component_id as usize) else { + debug_assert!(false, "Invalid component ID {component_id}"); + return state; + }; + let mut entries = std::mem::take(&mut component.entries); + state.component_type = component.component_type.clone(); + + // Process subcomponents + let mut group_components = Vec::new(); + let mut alarm_component_ids = Vec::new(); + let mut alarm_with_related = false; + let mut unsupported_component_ids = Vec::new(); + let mut uid_jsid_mappings = Vec::new(); + let mut has_locations = false; + + for component_id in std::mem::take(&mut component.component_ids) { + let component = &mut self.components[component_id as usize]; + match (&state.component_type, &component.component_type) { + ( + ICalendarComponentType::VCalendar, + ICalendarComponentType::VEvent | ICalendarComponentType::VTodo, + ) => { + group_components.push(self.to_jscalendar(tz_resolver, component_id, options)); + } + ( + ICalendarComponentType::VEvent | ICalendarComponentType::VTodo, + component_type @ (ICalendarComponentType::Participant + | ICalendarComponentType::VLocation), + ) => { + let entries = state.get_mut_object_or_insert(match component_type { + ICalendarComponentType::Participant => JSCalendarProperty::Participants, + ICalendarComponentType::VLocation => { + has_locations = true; + JSCalendarProperty::Locations + } + _ => unreachable!(), + }); + let mut subcomponent_state = + self.to_jscalendar(tz_resolver, component_id, options); + let jsid = subcomponent_state.jsid.take(); + let subcomponent = subcomponent_state.into_object(); + entries.insert_named(jsid, subcomponent); + } + ( + ICalendarComponentType::VEvent | ICalendarComponentType::VTodo, + ICalendarComponentType::VAlarm, + ) => { + let mut jsid = None; + + for entry in &mut component.entries { + match &entry.name { + ICalendarProperty::RelatedTo => { + alarm_with_related = true; + } + ICalendarProperty::Jsid => { + jsid = std::mem::take(&mut entry.values) + .pop() + .and_then(|v| v.into_text()); + } + _ => {} + } + } + let jsid = jsid.unwrap_or_else(|| { + Cow::Owned(format!("k{}", alarm_component_ids.len() + 1)) + }); + if let Some(uid) = component.uid() { + uid_jsid_mappings.push((uid.to_string(), jsid.clone())); + } + alarm_component_ids.push((component_id, jsid)); + } + _ => { + unsupported_component_ids.push(component_id); + } + } + } + + // Process alarms + if !alarm_component_ids.is_empty() { + for (component_id, jsid) in alarm_component_ids { + if alarm_with_related { + for entry in &mut self.components[component_id as usize].entries { + if matches!(entry.name, ICalendarProperty::RelatedTo) { + for value in &mut entry.values { + if let Some((_, jsid)) = value.as_text().and_then(|uid| { + uid_jsid_mappings.iter().find(|(u, _)| u == uid) + }) { + *value = ICalendarValue::Text(jsid.to_string()); + } + } + } + } + } + + state + .get_mut_object_or_insert(JSCalendarProperty::Alerts) + .insert_unchecked( + Key::from(jsid), + self.to_jscalendar(tz_resolver, component_id, options) + .into_object(), + ); + } + } + + // Build group component list + let mut group_objects = Vec::with_capacity(group_components.len()); + if !group_components.is_empty() { + // Order by UID placing the main components first + group_components.sort_unstable_by(|a, b| match a.uid.cmp(&b.uid) { + std::cmp::Ordering::Equal => { + a.recurrence_id.is_some().cmp(&b.recurrence_id.is_some()) + } + other => other, + }); + let mut group_components = group_components.into_iter().peekable(); + + // Bundle recurrence overrides together by UID + while let Some(mut component) = group_components.next() { + let mut instances = Vec::new(); + + if component.uid.is_some() && component.recurrence_id.is_none() { + let mut privacy = component.privacy(); + + while let Some(mut recurrence) = group_components + .next_if(|r| r.uid == component.uid && r.recurrence_id.is_some()) + { + let Some(recurrence_id) = recurrence.recurrence_id.take() else { + continue; + }; + let recurrence_id = recurrence_id + .with_timezone(&component.tz_start.unwrap_or_default()) + .to_naive_timestamp(); + let _ = recurrence.uid.take(); + + privacy = privacy.max(recurrence.privacy()); + recurrence.set_is_recurrence_instance(); + recurrence.remove_forbidden_override_patches(); + + component + .get_mut_object_or_insert(JSCalendarProperty::RecurrenceOverrides) + .insert( + Key::Property(JSCalendarProperty::DateTime( + JSCalendarDateTime::new(recurrence_id, true), + )), + recurrence.into_object(), + ); + } + + if let Some(privacy) = privacy { + component.raise_privacy(privacy); + } + } else if component.uid.is_some() { + while let Some(instance) = + group_components.next_if(|next| next.uid == component.uid) + { + instances.push(instance); + } + + if let Some(privacy) = instances + .iter() + .filter_map(State::privacy) + .max() + .max(component.privacy()) + { + component.raise_privacy(privacy); + for instance in &mut instances { + instance.raise_privacy(privacy); + } + } + } + + if options.return_first { + return component; + } + group_objects.push(component.into_object()); + group_objects.extend(instances.into_iter().map(State::into_object)); + } + } + + // Build unsupported component jcal + if options.include_ical_components && !unsupported_component_ids.is_empty() { + let mut component_iter = unsupported_component_ids.into_iter(); + let mut component_stack = Vec::with_capacity(4); + let mut components = vec![]; + + loop { + if let Some(component_id) = component_iter.next() { + let component = self.components.get_mut(component_id as usize).unwrap(); + + let mut entries = Vec::with_capacity(component.entries.len()); + for entry in std::mem::take(&mut component.entries) { + entries.push(EntryState::new(entry).into_jcal()); + } + + components.push(Value::Array(vec![ + Value::Str( + std::mem::take(&mut component.component_type) + .into_string() + .to_ascii_lowercase() + .into(), + ), + Value::Array(entries), + Value::Array(vec![]), + ])); + + if !component.component_ids.is_empty() { + component_stack.push((components, component_iter)); + component_iter = std::mem::take(&mut component.component_ids).into_iter(); + components = vec![]; + } + } else if let Some((mut parent_components, iter)) = component_stack.pop() { + if let Some(parent_component) = parent_components + .last_mut() + .and_then(|v| v.as_array_mut()) + .and_then(|v| v.last_mut()) + .and_then(|v| v.as_array_mut()) + { + if !parent_component.is_empty() { + parent_component.extend(components); + } else { + *parent_component = components; + } + } else { + debug_assert!(false, "Invalid component stack state"); + } + components = parent_components; + component_iter = iter; + } else { + break; + } + } + + state.ical_components = Some(Value::Array(components)); + } + + let mut main_location_id = None; + + entries.sort_by_key(|entry| match &entry.name { + ICalendarProperty::Dtstart | ICalendarProperty::Jsid => 0, + ICalendarProperty::RecurrenceId => 1, + ICalendarProperty::Dtend + | ICalendarProperty::Due + | ICalendarProperty::Location + | ICalendarProperty::Name + | ICalendarProperty::Attendee => 2, + _ => 3, + }); + let mut start_date = None; + let mut has_owner = false; + + for entry in entries { + let mut entry = EntryState::new(entry); + let mut values = std::mem::take(&mut entry.entry.values).into_iter(); + + match ( + &entry.entry.name, + values.next().map(|v| v.uri_to_string()), + &state.component_type, + ) { + ( + ICalendarProperty::Acknowledged, + Some(ICalendarValue::PartialDateTime(value)), + ICalendarComponentType::VAlarm, + ) if value.has_date_and_time() => { + state.entries.insert( + Key::Property(JSCalendarProperty::Acknowledged), + Value::Element(JSCalendarValue::DateTime(JSCalendarDateTime::new( + value.to_timestamp().unwrap(), + false, + ))), + ); + entry.set_converted_to::(&[JSCalendarProperty::Acknowledged:: + .to_string() + .as_ref()]); + } + ( + ICalendarProperty::Action, + Some(ICalendarValue::Action( + value @ (ICalendarAction::Display | ICalendarAction::Email), + )), + ICalendarComponentType::VAlarm, + ) => { + state.entries.insert( + Key::Property(JSCalendarProperty::Action), + Value::Element(JSCalendarValue::AlertAction(match value { + ICalendarAction::Display => JSCalendarAlertAction::Display, + ICalendarAction::Email => JSCalendarAlertAction::Email, + _ => unreachable!(), + })), + ); + entry.set_converted_to::(&[JSCalendarProperty::Action:: + .to_string() + .as_ref()]); + } + ( + ICalendarProperty::Attach, + Some(ICalendarValue::Text(uri)), + ICalendarComponentType::VEvent + | ICalendarComponentType::VTodo + | ICalendarComponentType::Participant + | ICalendarComponentType::VLocation + | ICalendarComponentType::VCalendar, + ) => { + state.map_named_entry( + &mut entry, + &[ + ICalendarParameterName::Fmttype, + ICalendarParameterName::Filename, + ICalendarParameterName::Size, + ICalendarParameterName::Jsid, + ], + JSCalendarProperty::Links, + [ + ( + Key::Property(JSCalendarProperty::Href), + Value::Str(uri.into()), + ), + ( + Key::Property(JSCalendarProperty::Type), + Value::Element(JSCalendarValue::Type(JSCalendarType::Link)), + ), + ( + Key::Property(JSCalendarProperty::Rel), + Value::Element(JSCalendarValue::LinkRelation( + LinkRelation::Enclosure, + )), + ), + ], + ); + entry.set_map_name(); + } + ( + ICalendarProperty::Image, + Some(ICalendarValue::Text(uri)), + ICalendarComponentType::VEvent + | ICalendarComponentType::VTodo + | ICalendarComponentType::Participant + | ICalendarComponentType::VLocation + | ICalendarComponentType::VCalendar, + ) => { + state.map_named_entry( + &mut entry, + &[ + ICalendarParameterName::Display, + ICalendarParameterName::Fmttype, + ICalendarParameterName::Filename, + ICalendarParameterName::Size, + ICalendarParameterName::Jsid, + ], + JSCalendarProperty::Links, + [ + ( + Key::Property(JSCalendarProperty::Href), + Value::Str(uri.into()), + ), + ( + Key::Property(JSCalendarProperty::Type), + Value::Element(JSCalendarValue::Type(JSCalendarType::Link)), + ), + ( + Key::Property(JSCalendarProperty::Rel), + Value::Element(JSCalendarValue::LinkRelation(LinkRelation::Icon)), + ), + ], + ); + entry.set_map_name(); + } + ( + ICalendarProperty::Link, + Some(ICalendarValue::Text(uri)), + ICalendarComponentType::VEvent + | ICalendarComponentType::VTodo + | ICalendarComponentType::Participant + | ICalendarComponentType::VLocation + | ICalendarComponentType::VCalendar, + ) => { + state.map_named_entry( + &mut entry, + &[ + ICalendarParameterName::Fmttype, + ICalendarParameterName::Label, + ICalendarParameterName::Linkrel, + ICalendarParameterName::Jsid, + ], + JSCalendarProperty::Links, + [ + ( + Key::Property(JSCalendarProperty::Href), + Value::Str(uri.into()), + ), + ( + Key::Property(JSCalendarProperty::Type), + Value::Element(JSCalendarValue::Type(JSCalendarType::Link)), + ), + ], + ); + entry.set_map_name(); + } + ( + ICalendarProperty::Url, + Some(ICalendarValue::Text(uri)), + ICalendarComponentType::VEvent + | ICalendarComponentType::VTodo + | ICalendarComponentType::Participant + | ICalendarComponentType::VLocation + | ICalendarComponentType::VCalendar, + ) => { + state.map_named_entry( + &mut entry, + &[ICalendarParameterName::Label, ICalendarParameterName::Jsid], + JSCalendarProperty::Links, + [ + ( + Key::Property(JSCalendarProperty::Href), + Value::Str(uri.into()), + ), + ( + Key::Property(JSCalendarProperty::Type), + Value::Element(JSCalendarValue::Type(JSCalendarType::Link)), + ), + ( + Key::Property(JSCalendarProperty::Rel), + Value::Element(JSCalendarValue::LinkRelation( + LinkRelation::Describedby, + )), + ), + ], + ); + entry.set_map_name(); + } + ( + ICalendarProperty::Attendee, + Some(ICalendarValue::Text(uri)), + ICalendarComponentType::VEvent | ICalendarComponentType::VTodo, + ) => { + let is_task = matches!(&state.component_type, ICalendarComponentType::VTodo); + let mut progress = None; + + for param in &mut entry.entry.params { + match (¶m.name, &mut param.value) { + ( + ICalendarParameterName::Partstat, + ICalendarParameterValue::Partstat(partstat), + ) if is_task => { + let status = match partstat { + ICalendarParticipationStatus::Completed => { + JSCalendarProgress::Completed + } + ICalendarParticipationStatus::InProcess => { + JSCalendarProgress::InProcess + } + ICalendarParticipationStatus::Failed => { + JSCalendarProgress::Failed + } + _ => { + continue; + } + }; + *partstat = ICalendarParticipationStatus::Accepted; + progress = Some(( + Key::Property(JSCalendarProperty::Progress), + Value::Element(JSCalendarValue::Progress(status)), + )); + } + (ICalendarParameterName::Role, ICalendarParameterValue::Role(role)) => { + has_owner |= matches!(role, ICalendarParticipationRole::Owner); + } + _ => {} + } + } + + state.map_named_entry( + &mut entry, + &[ + ICalendarParameterName::Cn, + ICalendarParameterName::Cutype, + ICalendarParameterName::DelegatedFrom, + ICalendarParameterName::DelegatedTo, + ICalendarParameterName::Email, + ICalendarParameterName::Member, + ICalendarParameterName::Partstat, + ICalendarParameterName::Role, + ICalendarParameterName::Rsvp, + ICalendarParameterName::SentBy, + ICalendarParameterName::Jsid, + ], + JSCalendarProperty::Participants, + [ + Some(( + Key::Property(JSCalendarProperty::CalendarAddress), + Value::Str(uri.to_string().into()), + )), + Some(( + Key::Property(JSCalendarProperty::Type), + Value::Element(JSCalendarValue::Type(JSCalendarType::Participant)), + )), + progress, + ] + .into_iter() + .flatten(), + ); + } + ( + ICalendarProperty::Organizer, + Some(ICalendarValue::Text(uri)), + ICalendarComponentType::VEvent | ICalendarComponentType::VTodo, + ) => { + state.entries.insert( + Key::Property(JSCalendarProperty::OrganizerCalendarAddress), + Value::Str(uri.clone().into()), + ); + + if let Some(sent_by) = entry + .entry + .params + .iter() + .find(|param| param.name == ICalendarParameterName::SentBy) + .and_then(|param| param.value.as_text()) + { + state.entries.insert( + Key::Property(JSCalendarProperty::SentBy), + Value::Str(sent_by.to_string().into()), + ); + } + + /* + It also converts to a Participant object in the "participants" property, + if any of its parameters convert to a Participant object property, or + if none of the ATTENDEE properties in the iCalendar component have + the ROLE parameter set to "OWNER" + + An ORGANIZER property, an ATTENDEE property, and a PARTICIPANT component + in the same iCalendar component all convert to the same Participant + object, if their converted "calendarAddress" property values are equal. + */ + + if !has_owner + || entry.entry.params.iter().any(|p| { + matches!( + p.name, + ICalendarParameterName::Cn + | ICalendarParameterName::Email + | ICalendarParameterName::SentBy + | ICalendarParameterName::Jsid + ) + }) + { + let participant_id = state.find_participant_by_address(uri.as_ref()); + + state.map_named_entry_with_id( + &mut entry, + &[ + ICalendarParameterName::Cn, + ICalendarParameterName::Email, + ICalendarParameterName::SentBy, + ICalendarParameterName::Jsid, + ], + JSCalendarProperty::Participants, + [ + ( + Key::Property(JSCalendarProperty::CalendarAddress), + Value::Str(uri.to_string().into()), + ), + ( + Key::Property(JSCalendarProperty::Type), + Value::Element(JSCalendarValue::Type( + JSCalendarType::Participant, + )), + ), + ( + Key::Property(JSCalendarProperty::Roles), + Value::Object(Map::from(vec![( + Key::Property(JSCalendarProperty::ParticipantRole( + JSCalendarParticipantRole::Owner, + )), + Value::Bool(true), + )])), + ), + ], + participant_id, + ); + } else { + continue; + } + } + ( + ICalendarProperty::CalendarAddress, + Some(ICalendarValue::Text(value)), + ICalendarComponentType::Participant, + ) => { + if state.jsid.is_none() { + state.jsid = Some(uuid5(&value)); + } + state.entries.insert( + Key::Property(JSCalendarProperty::CalendarAddress), + Value::Str(value.into()), + ); + entry.set_converted_to::(&[JSCalendarProperty::CalendarAddress:: + .to_string() + .as_ref()]); + entry.set_map_name(); + } + ( + ICalendarProperty::Categories, + Some(ICalendarValue::Text(value)), + ICalendarComponentType::VEvent + | ICalendarComponentType::VTodo + | ICalendarComponentType::VCalendar, + ) => { + let obj = state + .entries + .entry(Key::Property(JSCalendarProperty::Keywords)) + .or_insert_with(Value::new_object) + .as_object_mut() + .unwrap(); + obj.insert(Key::Owned(value), Value::Bool(true)); + for value in values { + if let Some(value) = value.into_text() { + obj.insert(Key::from(value), Value::Bool(true)); + } + } + entry.set_converted_to::(&[JSCalendarProperty::Keywords:: + .to_string() + .as_ref()]); + } + ( + ICalendarProperty::Class, + Some(ICalendarValue::Classification(value)), + ICalendarComponentType::VEvent | ICalendarComponentType::VTodo, + ) => { + state.merge_privacy(match value { + ICalendarClassification::Public => JSCalendarPrivacy::Public, + ICalendarClassification::Private => JSCalendarPrivacy::Private, + ICalendarClassification::Confidential => JSCalendarPrivacy::Secret, + }); + entry.set_converted_to::(&[JSCalendarProperty::Privacy:: + .to_string() + .as_ref()]); + } + ( + ICalendarProperty::Class, + Some(ICalendarValue::Text(value)), + ICalendarComponentType::VEvent | ICalendarComponentType::VTodo, + ) => { + state.merge_privacy(JSCalendarPrivacy::Private); + entry.entry.values = [ICalendarValue::Text(value)] + .into_iter() + .chain(values) + .collect(); + } + ( + ICalendarProperty::Color, + Some(ICalendarValue::Text(value)), + ICalendarComponentType::VEvent + | ICalendarComponentType::VTodo + | ICalendarComponentType::VCalendar, + ) => { + state.entries.insert( + Key::Property(JSCalendarProperty::Color), + Value::Str(value.into()), + ); + entry.set_converted_to::(&[JSCalendarProperty::Color:: + .to_string() + .as_ref()]); + } + ( + ICalendarProperty::Concept, + Some(ICalendarValue::Text(value)), + ICalendarComponentType::VEvent + | ICalendarComponentType::VTodo + | ICalendarComponentType::VCalendar, + ) => { + let obj = state + .entries + .entry(Key::Property(JSCalendarProperty::Categories)) + .or_insert_with(Value::new_object) + .as_object_mut() + .unwrap(); + obj.insert(Key::Owned(value), Value::Bool(true)); + for value in values { + if let Some(value) = value.into_text() { + obj.insert(Key::from(value), Value::Bool(true)); + } + } + entry.set_converted_to::(&[JSCalendarProperty::Categories:: + .to_string() + .as_ref()]); + } + ( + ICalendarProperty::Conference, + Some(ICalendarValue::Text(value)), + ICalendarComponentType::VEvent | ICalendarComponentType::VTodo, + ) => { + state.map_named_entry( + &mut entry, + &[ + ICalendarParameterName::Jsid, + ICalendarParameterName::Feature, + ICalendarParameterName::Label, + ], + JSCalendarProperty::VirtualLocations, + [ + ( + Key::Property(JSCalendarProperty::Uri), + Value::Str(value.into()), + ), + ( + Key::Property(JSCalendarProperty::Type), + Value::Element(JSCalendarValue::Type( + JSCalendarType::VirtualLocation, + )), + ), + ], + ); + } + ( + ICalendarProperty::Coordinates, + Some(ICalendarValue::Text(value)), + ICalendarComponentType::VLocation, + ) => { + if state.jsid.is_none() { + state.jsid = Some(uuid5(&value)); + } + state.entries.insert( + Key::Property(JSCalendarProperty::Coordinates), + Value::Str(value.into()), + ); + entry.set_converted_to::(&[JSCalendarProperty::Coordinates:: + .to_string() + .as_ref()]); + } + ( + ICalendarProperty::Geo, + Some(ICalendarValue::Float(coord1)), + ICalendarComponentType::VLocation, + ) if !entry.entry.is_derived() => { + let coord2 = values.next().and_then(|v| v.as_float()).unwrap_or_default(); + + state.entries.insert( + Key::Property(JSCalendarProperty::Coordinates), + Value::Str(format!("geo:{coord1},{coord2}").into()), + ); + entry.set_converted_to::(&[JSCalendarProperty::Coordinates:: + .to_string() + .as_ref()]); + entry.set_map_name(); + } + ( + ICalendarProperty::Geo, + Some(ICalendarValue::Float(coord1)), + ICalendarComponentType::VEvent | ICalendarComponentType::VTodo, + ) if !entry.entry.is_derived() => { + let coord2 = values.next().and_then(|v| v.as_float()).unwrap_or_default(); + if entry.entry.jsid().is_none() + && let Some(main_location_id) = main_location_id.take() + { + entry + .entry + .add_param(ICalendarParameter::jsid(main_location_id)); + } + + state.map_named_entry( + &mut entry, + &[ICalendarParameterName::Jsid], + JSCalendarProperty::Locations, + [ + ( + Key::Property(JSCalendarProperty::Coordinates), + Value::Str(format!("geo:{coord1},{coord2}").into()), + ), + ( + Key::Property(JSCalendarProperty::Type), + Value::Element(JSCalendarValue::Type(JSCalendarType::Location)), + ), + ], + ); + entry.set_map_name(); + } + ( + ICalendarProperty::Name, + Some(ICalendarValue::Text(value)), + ICalendarComponentType::VLocation, + ) => { + if state.jsid.is_none() { + state.jsid = Some(uuid5(&value)); + } + state.entries.insert( + Key::Property(JSCalendarProperty::Name), + Value::Str(value.into()), + ); + entry.set_converted_to::(&[JSCalendarProperty::Name:: + .to_string() + .as_ref()]); + state.set_map_component(); + } + ( + ICalendarProperty::Name, + Some(ICalendarValue::Text(value)), + ICalendarComponentType::VCalendar, + ) + | ( + ICalendarProperty::Summary, + Some(ICalendarValue::Text(value)), + ICalendarComponentType::VEvent | ICalendarComponentType::VTodo, + ) => { + state + .entries + .extract_params(&mut entry.entry, &[ICalendarParameterName::Language]); + state.entries.insert( + Key::Property(JSCalendarProperty::Title), + Value::Str(value.into()), + ); + entry.set_converted_to::(&[JSCalendarProperty::Title:: + .to_string() + .as_ref()]); + } + ( + ICalendarProperty::Summary, + Some(ICalendarValue::Text(value)), + ICalendarComponentType::Participant, + ) => { + state.entries.insert( + Key::Property(JSCalendarProperty::Name), + Value::Str(value.into()), + ); + entry.set_converted_to::(&[JSCalendarProperty::Name:: + .to_string() + .as_ref()]); + state.set_map_component(); + } + ( + ICalendarProperty::Location, + Some(ICalendarValue::Text(value)), + ICalendarComponentType::VEvent | ICalendarComponentType::VTodo, + ) => { + let location_id = if let Some(location_id) = entry.entry.jsid() { + main_location_id = Some(location_id.to_string()); + location_id + } else { + main_location_id = Some(uuid5(&value)); + main_location_id.as_deref().unwrap() + }; + + if has_locations { + state.entries.insert( + Key::Property(JSCalendarProperty::MainLocationId), + Value::Str(location_id.to_string().into()), + ); + } + + state.map_named_entry( + &mut entry, + &[ + ICalendarParameterName::Jsid, + ICalendarParameterName::Derived, + ], + JSCalendarProperty::Locations, + [ + ( + Key::Property(JSCalendarProperty::Name), + Value::Str(value.into()), + ), + ( + Key::Property(JSCalendarProperty::Type), + Value::Element(JSCalendarValue::Type(JSCalendarType::Location)), + ), + ], + ); + } + ( + ICalendarProperty::LocationType, + Some(ICalendarValue::Text(value)), + ICalendarComponentType::VLocation, + ) => { + let obj = state + .entries + .entry(Key::Property(JSCalendarProperty::LocationTypes)) + .or_insert_with(Value::new_object) + .as_object_mut() + .unwrap(); + obj.insert(Key::Owned(value), Value::Bool(true)); + for value in values { + if let Some(value) = value.into_text() { + obj.insert(Key::from(value), Value::Bool(true)); + } + } + entry.set_converted_to::(&[JSCalendarProperty::LocationTypes:: + .to_string() + .as_ref()]); + } + + ( + ICalendarProperty::LastModified, + Some(ICalendarValue::PartialDateTime(value)), + ICalendarComponentType::VCalendar, + ) if value.has_date_and_time() => { + state.entries.insert( + Key::Property(JSCalendarProperty::Updated), + Value::Element(JSCalendarValue::DateTime(JSCalendarDateTime::new( + value.to_timestamp().unwrap(), + false, + ))), + ); + entry.set_converted_to::(&[JSCalendarProperty::Updated:: + .to_string() + .as_ref()]); + } + ( + ICalendarProperty::Created, + Some(ICalendarValue::PartialDateTime(value)), + ICalendarComponentType::VEvent + | ICalendarComponentType::VTodo + | ICalendarComponentType::VCalendar, + ) if value.has_date_and_time() => { + state.entries.insert( + Key::Property(JSCalendarProperty::Created), + Value::Element(JSCalendarValue::DateTime(JSCalendarDateTime::new( + value.to_timestamp().unwrap(), + false, + ))), + ); + entry.set_converted_to::(&[JSCalendarProperty::Created:: + .to_string() + .as_ref()]); + } + ( + ICalendarProperty::Description, + Some(ICalendarValue::Text(value)), + ICalendarComponentType::VEvent + | ICalendarComponentType::VTodo + | ICalendarComponentType::Participant + | ICalendarComponentType::VCalendar + | ICalendarComponentType::VLocation, + ) if !entry.entry.is_derived() => { + state.entries.insert( + Key::Property(JSCalendarProperty::Description), + Value::Str(value.into()), + ); + entry.set_converted_to::(&[JSCalendarProperty::Description:: + .to_string() + .as_ref()]); + if matches!(state.component_type, ICalendarComponentType::Participant) { + state.set_map_component(); + } + } + ( + ICalendarProperty::StyledDescription, + Some(ICalendarValue::Text(value)), + ICalendarComponentType::VEvent | ICalendarComponentType::VTodo, + ) if !entry.entry.is_derived() + && entry + .entry + .parameter(&ICalendarParameterName::Fmttype) + .is_none_or(|v| v.as_text().is_some_and(|v| v.starts_with("text/"))) => + { + state + .entries + .extract_params(&mut entry.entry, &[ICalendarParameterName::Fmttype]); + state.entries.insert( + Key::Property(JSCalendarProperty::Description), + Value::Str(value.into()), + ); + entry.set_converted_to::(&[JSCalendarProperty::Description:: + .to_string() + .as_ref()]); + if !state + .entries + .contains_key(&Key::Property(JSCalendarProperty::DescriptionContentType)) + { + entry.set_map_name(); + } + } + ( + ICalendarProperty::Dtstart, + Some(ICalendarValue::PartialDateTime(value)), + ICalendarComponentType::VEvent | ICalendarComponentType::VTodo, + ) if value.has_date() => { + let tzid = entry.entry.tz_id(); + state.tz_start = tzid.and_then(|v| tz_resolver.resolve(v)); + if let Some(dt) = value + .to_date_time() + .and_then(|dt| dt.to_date_time_with_tz(state.tz_start.unwrap_or_default())) + { + state.has_dates = true; + state.entries.insert( + Key::Property(JSCalendarProperty::Start), + Value::Element(JSCalendarValue::DateTime(JSCalendarDateTime::new( + dt.to_naive_timestamp(), + true, + ))), + ); + + // Remove IANA TZ references + if tzid.is_some() + && state.tz_start.and_then(|tz| tz.name()).as_deref() == tzid + { + entry + .entry + .params + .retain(|p| p.name != ICalendarParameterName::Tzid); + } + + if !value.has_time() { + state.entries.insert( + Key::Property(JSCalendarProperty::ShowWithoutTime), + Value::Bool(true), + ); + } + entry.set_converted_to::(&[JSCalendarProperty::Start:: + .to_string() + .as_ref()]); + start_date = Some(dt); + + if state.tz_start.is_none() { + state.tz_start = dt.timezone().to_resolved(); + } + } else { + state.tz_start = None; + entry.entry.values = [ICalendarValue::PartialDateTime(value)] + .into_iter() + .chain(values) + .collect(); + } + } + ( + ICalendarProperty::Dtend, + Some(ICalendarValue::PartialDateTime(value)), + ICalendarComponentType::VEvent, + ) if value.has_date() && start_date.is_some() => { + let tzid = entry.entry.tz_id(); + state.tz_end = tzid.and_then(|v| tz_resolver.resolve(v)).or(state.tz_start); + if let Some((delta, dt)) = value + .to_date_time() + .and_then(|dt| dt.to_date_time_with_tz(state.tz_end.unwrap_or_default())) + .and_then(|dt| { + let delta = dt.signed_duration_since(start_date.unwrap()).num_seconds(); + (delta > 0).then_some((delta, dt)) + }) + { + state.has_dates = true; + + /* + If the value type of the DTEND property is DATE, then the duration is + the number of days or weeks between the DTSTART and DTEND property + values. If the value type is DATE-TIME, then the duration is the + timespan between the DTSTART and DTEND property values when converted + to UTC time. + */ + + let days = if !value.has_time() { + dt.date_naive() + .signed_duration_since(start_date.unwrap().date_naive()) + .num_days() + } else { + 0 + }; + let duration = if days > 0 { + ICalendarDuration::from_days(days) + } else { + ICalendarDuration::from_seconds(delta) + }; + + state.entries.insert( + Key::Property(JSCalendarProperty::Duration), + Value::Element(JSCalendarValue::Duration(duration)), + ); + + // Remove IANA TZ references + if tzid.is_some() + && state.tz_end.and_then(|tz| tz.name()).as_deref() == tzid + { + entry + .entry + .params + .retain(|p| p.name != ICalendarParameterName::Tzid); + } + + if !value.has_time() { + state.entries.insert( + Key::Property(JSCalendarProperty::ShowWithoutTime), + Value::Bool(true), + ); + } + entry.set_converted_to::(&[JSCalendarProperty::Duration:: + .to_string() + .as_ref()]); + entry.set_map_name(); + + if state.tz_end.is_none() { + state.tz_end = dt.timezone().to_resolved(); + } + if state.tz_start.is_none() { + state.tz_start = state.tz_end; + } + } else { + state.tz_end = None; + entry.entry.values = [ICalendarValue::PartialDateTime(value)] + .into_iter() + .chain(values) + .collect(); + } + } + ( + ICalendarProperty::Due, + Some(ICalendarValue::PartialDateTime(value)), + ICalendarComponentType::VTodo, + ) if value.has_date() => { + let tzid = entry.entry.tz_id(); + let due_tz = tzid.and_then(|v| tz_resolver.resolve(v)).or(state.tz_start); + if let Some(dt) = value + .to_date_time() + .and_then(|dt| dt.to_date_time_with_tz(due_tz.unwrap_or_default())) + { + // Remove IANA TZ references + if tzid.is_some() + && due_tz != state.tz_start + && due_tz.and_then(|tz| tz.name()).as_deref() == tzid + { + entry + .entry + .params + .retain(|p| p.name != ICalendarParameterName::Tzid); + } + + state.has_dates = true; + if state.tz_start.is_none() { + state.tz_start = dt.timezone().to_resolved(); + } + + state.entries.insert( + Key::Property(JSCalendarProperty::Due), + Value::Element(JSCalendarValue::DateTime(JSCalendarDateTime::new( + dt.with_timezone(&state.tz_start.unwrap_or_default()) + .to_naive_timestamp(), + true, + ))), + ); + + if !value.has_time() { + state.entries.insert( + Key::Property(JSCalendarProperty::ShowWithoutTime), + Value::Bool(true), + ); + } + + entry.set_converted_to::(&[JSCalendarProperty::Due:: + .to_string() + .as_ref()]); + } else { + entry.entry.values = [ICalendarValue::PartialDateTime(value)] + .into_iter() + .chain(values) + .collect(); + } + } + ( + ICalendarProperty::Dtstamp, + Some(ICalendarValue::PartialDateTime(value)), + ICalendarComponentType::VEvent | ICalendarComponentType::VTodo, + ) if value.has_date_and_time() => { + state.entries.insert( + Key::Property(JSCalendarProperty::Updated), + Value::Element(JSCalendarValue::DateTime(JSCalendarDateTime::new( + value.to_timestamp().unwrap_or_default(), + false, + ))), + ); + + entry.set_converted_to::(&[JSCalendarProperty::Updated:: + .to_string() + .as_ref()]); + } + ( + ICalendarProperty::Duration, + Some(ICalendarValue::Duration(value)), + ICalendarComponentType::VEvent | ICalendarComponentType::VTodo, + ) => { + state.entries.insert( + Key::Property(JSCalendarProperty::Duration), + Value::Element(JSCalendarValue::Duration(value)), + ); + + entry.set_converted_to::(&[JSCalendarProperty::Duration:: + .to_string() + .as_ref()]); + } + ( + ICalendarProperty::EstimatedDuration, + Some(ICalendarValue::Duration(value)), + ICalendarComponentType::VTodo, + ) => { + state.entries.insert( + Key::Property(JSCalendarProperty::EstimatedDuration), + Value::Element(JSCalendarValue::Duration(value)), + ); + + entry.set_converted_to::(&[JSCalendarProperty::EstimatedDuration:: + .to_string() + .as_ref()]); + } + ( + ICalendarProperty::RecurrenceId, + Some(ICalendarValue::PartialDateTime(value)), + ICalendarComponentType::VEvent | ICalendarComponentType::VTodo, + ) if value.has_date_and_time() => { + let tzid = entry.entry.tz_id(); + let rid_tz = tzid.and_then(|v| tz_resolver.resolve(v)).or(state.tz_start); + + if let Some(dt) = value + .to_date_time() + .and_then(|dt| dt.to_date_time_with_tz(rid_tz.unwrap_or_default())) + { + state.has_dates = true; + state.recurrence_id = Some(dt); + + // Remove IANA TZ references + if tzid.is_some() && rid_tz.and_then(|tz| tz.name()).as_deref() == tzid { + entry + .entry + .params + .retain(|p| p.name != ICalendarParameterName::Tzid); + } + + entry.set_converted_to::(&[JSCalendarProperty::RecurrenceId:: + .to_string() + .as_ref()]); + + if state.tz_start.is_none() { + state.tz_start = dt.timezone().to_resolved(); + } + } else { + entry.entry.values = [ICalendarValue::PartialDateTime(value)] + .into_iter() + .chain(values) + .collect(); + } + } + ( + ICalendarProperty::Rdate, + Some(value @ ICalendarValue::Period(_)), + ICalendarComponentType::VEvent | ICalendarComponentType::VTodo, + ) => { + /* + The property value converts to the key in the "recurrenceOverrides" + property value, for values of type PERIOD this only applies to the + period start. The duration of a PERIOD converts to the PatchObject's + "duration" property. + */ + + let tz = entry + .entry + .tz_id() + .and_then(|v| tz_resolver.resolve(v)) + .or(state.tz_start) + .unwrap_or_default(); + + // Restore the values, they are preserved as-is unless every period converts + entry.entry.values = [value].into_iter().chain(values).collect(); + + if let Some((start, _)) = entry + .entry + .values + .first() + .and_then(|value| period_to_date_time(value, tz)) + && entry + .entry + .values + .iter() + .skip(1) + .all(|value| period_to_date_time(value, tz).is_some()) + { + state.has_dates = true; + if state.tz_start.is_none() { + state.tz_start = start.timezone().to_resolved(); + } + + let tz_start = state.tz_start.unwrap_or_default(); + let overrides_name = + JSCalendarProperty::RecurrenceOverrides::.to_string(); + + // Each period converts to its own recurrence override + for pos in 0..entry.entry.values.len() { + let (dt, duration) = + period_to_date_time(&entry.entry.values[pos], tz).unwrap(); + let key = Key::Property(JSCalendarProperty::DateTime( + JSCalendarDateTime::new( + dt.with_timezone(&tz_start).to_naive_timestamp(), + true, + ), + )); + let key_name = key.to_string().into_owned(); + + state.insert_recurrence_override( + key, + Value::Object(Map::from(vec![( + Key::Property(JSCalendarProperty::Duration), + Value::Element(JSCalendarValue::Duration(duration)), + )])), + ); + + if pos == 0 { + entry.set_converted_to::(&[ + overrides_name.as_ref(), + key_name.as_str(), + ]); + entry.set_map_name(); + } else { + state.add_period_conversion_prop(format!( + "{}/{}", + overrides_name.as_ref(), + key_name + )); + } + } + } + } + ( + ICalendarProperty::Rdate | ICalendarProperty::Exdate, + Some(ICalendarValue::PartialDateTime(value)), + ICalendarComponentType::VEvent | ICalendarComponentType::VTodo, + ) if value.has_date() => { + let tzid = entry.entry.tz_id(); + let tz = tzid.and_then(|v| tz_resolver.resolve(v)).or(state.tz_start); + + if let Some(dt) = value + .to_date_time() + .and_then(|dt| dt.to_date_time_with_tz(tz.unwrap_or_default())) + { + state.has_dates = true; + + // Remove IANA TZ references + if tzid.is_some() + && tz == state.tz_start + && tz.and_then(|tz| tz.name()).as_deref() == tzid + { + entry + .entry + .params + .retain(|p| p.name != ICalendarParameterName::Tzid); + } + + if state.tz_start.is_none() { + state.tz_start = dt.timezone().to_resolved(); + } + + let overrides = state + .entries + .entry(Key::Property(JSCalendarProperty::RecurrenceOverrides)) + .or_insert_with(Value::new_object) + .as_object_mut() + .unwrap(); + let value = Value::Object(Map::from( + if matches!(entry.entry.name, ICalendarProperty::Exdate) { + vec![( + Key::Property(JSCalendarProperty::Excluded), + Value::Bool(true), + )] + } else { + vec![] + }, + )); + + for (pos, dt) in [dt] + .into_iter() + .chain(values.filter_map(|v| { + v.into_partial_date_time() + .and_then(|dt| dt.to_date_time()) + .and_then(|dt| dt.to_date_time_with_tz(tz.unwrap_or_default())) + })) + .enumerate() + { + let key = Key::Property(JSCalendarProperty::DateTime( + JSCalendarDateTime::new( + dt.with_timezone(&state.tz_start.unwrap_or_default()) + .to_naive_timestamp(), + true, + ), + )); + + if pos == 0 { + entry.set_converted_to::(&[ + JSCalendarProperty::RecurrenceOverrides:: + .to_string() + .as_ref(), + key.to_string().as_ref().to_string().as_ref(), + ]); + } + + overrides.insert(key, value.clone()); + } + } else { + entry.entry.values = [ICalendarValue::PartialDateTime(value)] + .into_iter() + .chain(values) + .collect(); + } + } + ( + ICalendarProperty::Rrule, + Some(ICalendarValue::RecurrenceRule(value)), + ICalendarComponentType::VEvent | ICalendarComponentType::VTodo, + ) => { + let mut rrule = Map::from(Vec::with_capacity(4)); + + rrule.insert_unchecked( + JSCalendarProperty::Frequency, + Value::Element(JSCalendarValue::Frequency(value.freq)), + ); + + for (key, value) in [ + ( + JSCalendarProperty::Until, + value + .until + .as_ref() + .and_then(|v| { + v.to_date_time_with_tz(state.tz_start.unwrap_or_default()) + }) + .map(|dt| { + Value::Element(JSCalendarValue::DateTime( + JSCalendarDateTime::new( + dt.with_timezone(&state.tz_start.unwrap_or_default()) + .to_naive_timestamp(), + true, + ), + )) + }), + ), + ( + JSCalendarProperty::Count, + value.count.map(|v| Value::Number((v as u64).into())), + ), + ( + JSCalendarProperty::Interval, + value.interval.map(|v| Value::Number((v as u64).into())), + ), + ( + JSCalendarProperty::FirstDayOfWeek, + value + .wkst + .map(|v| Value::Element(JSCalendarValue::Weekday(v))), + ), + ( + JSCalendarProperty::Rscale, + value + .rscale + .map(|v| Value::Element(JSCalendarValue::CalendarScale(v))), + ), + ( + JSCalendarProperty::Skip, + value.skip.map(|v| Value::Element(JSCalendarValue::Skip(v))), + ), + ] { + if let Some(value) = value { + rrule.insert_unchecked(key, value); + } + } + + for (key, values) in [ + ( + JSCalendarProperty::BySecond, + (!value.bysecond.is_empty()).then(|| { + value + .bysecond + .iter() + .map(|&v| Value::Number((v as u64).into())) + .collect::>() + }), + ), + ( + JSCalendarProperty::ByMinute, + (!value.byminute.is_empty()).then(|| { + value + .byminute + .iter() + .map(|&v| Value::Number((v as u64).into())) + .collect::>() + }), + ), + ( + JSCalendarProperty::ByHour, + (!value.byhour.is_empty()).then(|| { + value + .byhour + .iter() + .map(|&v| Value::Number((v as u64).into())) + .collect::>() + }), + ), + ( + JSCalendarProperty::ByDay, + (!value.byday.is_empty()).then(|| { + value + .byday + .iter() + .map(|v| { + Value::Object(Map::from_iter( + [ + Some(( + Key::Property(JSCalendarProperty::Day), + Value::Element(JSCalendarValue::Weekday( + v.weekday, + )), + )), + v.ordwk.map(|v| { + ( + Key::Property( + JSCalendarProperty::NthOfPeriod, + ), + Value::Number((v as i64).into()), + ) + }), + ] + .into_iter() + .flatten(), + )) + }) + .collect::>() + }), + ), + ( + JSCalendarProperty::ByMonthDay, + (!value.bymonthday.is_empty()).then(|| { + value + .bymonthday + .iter() + .map(|&v| Value::Number((v as i64).into())) + .collect::>() + }), + ), + ( + JSCalendarProperty::ByYearDay, + (!value.byyearday.is_empty()).then(|| { + value + .byyearday + .iter() + .map(|&v| Value::Number((v as i64).into())) + .collect::>() + }), + ), + ( + JSCalendarProperty::ByWeekNo, + (!value.byweekno.is_empty()).then(|| { + value + .byweekno + .iter() + .map(|&v| Value::Number((v as i64).into())) + .collect::>() + }), + ), + ( + JSCalendarProperty::ByMonth, + (!value.bymonth.is_empty()).then(|| { + value + .bymonth + .iter() + .map(|&v| Value::Element(JSCalendarValue::Month(v))) + .collect::>() + }), + ), + ( + JSCalendarProperty::BySetPosition, + (!value.bysetpos.is_empty()).then(|| { + value + .bysetpos + .iter() + .map(|&v| Value::Number((v as i64).into())) + .collect::>() + }), + ), + ] { + if let Some(values) = values { + rrule.insert_unchecked(key, Value::Array(values)); + } + } + + state.entries.insert( + Key::Property(JSCalendarProperty::RecurrenceRule), + Value::Object(rrule), + ); + + entry.set_converted_to::(&[JSCalendarProperty::RecurrenceRule:: + .to_string() + .as_ref()]); + } + ( + ICalendarProperty::Method, + Some(ICalendarValue::Method(value)), + ICalendarComponentType::VCalendar, + ) => { + for object in &mut group_objects { + object.as_object_mut().unwrap().insert_unchecked( + Key::Property(JSCalendarProperty::Method), + Value::Element(JSCalendarValue::Method(value.clone())), + ); + } + continue; + } + ( + ICalendarProperty::PercentComplete, + Some(ICalendarValue::Integer(value)), + ICalendarComponentType::VTodo, + ) => { + state.entries.insert( + Key::Property(JSCalendarProperty::PercentComplete), + Value::Number(value.into()), + ); + entry.set_converted_to::(&[JSCalendarProperty::PercentComplete:: + .to_string() + .as_ref()]); + } + ( + ICalendarProperty::Priority, + Some(ICalendarValue::Integer(value)), + ICalendarComponentType::VEvent | ICalendarComponentType::VTodo, + ) => { + state.entries.insert( + Key::Property(JSCalendarProperty::Priority), + Value::Number(value.into()), + ); + entry.set_converted_to::(&[JSCalendarProperty::Priority:: + .to_string() + .as_ref()]); + } + ( + ICalendarProperty::Sequence, + Some(ICalendarValue::Integer(value)), + ICalendarComponentType::VEvent | ICalendarComponentType::VTodo, + ) => { + state.entries.insert( + Key::Property(JSCalendarProperty::Sequence), + Value::Number(value.into()), + ); + entry.set_converted_to::(&[JSCalendarProperty::Sequence:: + .to_string() + .as_ref()]); + } + ( + ICalendarProperty::Prodid, + Some(ICalendarValue::Text(value)), + ICalendarComponentType::VCalendar, + ) => { + state.entries.insert( + Key::Property(JSCalendarProperty::ProdId), + Value::Str(value.into()), + ); + entry.set_converted_to::(&[JSCalendarProperty::ProdId:: + .to_string() + .as_ref()]); + } + ( + ICalendarProperty::RelatedTo, + Some(ICalendarValue::Text(value)), + ICalendarComponentType::VEvent + | ICalendarComponentType::VTodo + | ICalendarComponentType::VAlarm, + ) => { + let mut rels: Map<'_, JSCalendarProperty, JSCalendarValue> = + Map::from(vec![]); + for param in std::mem::take(&mut entry.entry.params) { + match (param.name, param.value) { + ( + ICalendarParameterName::Reltype, + ICalendarParameterValue::Reltype(value), + ) => { + rels.insert( + match value { + ICalendarRelationshipType::Child => { + Key::Property(JSCalendarProperty::RelationValue( + JSCalendarRelation::Child, + )) + } + ICalendarRelationshipType::Parent => { + Key::Property(JSCalendarProperty::RelationValue( + JSCalendarRelation::Parent, + )) + } + ICalendarRelationshipType::Snooze => { + Key::Property(JSCalendarProperty::RelationValue( + JSCalendarRelation::Snooze, + )) + } + ICalendarRelationshipType::First => { + Key::Property(JSCalendarProperty::RelationValue( + JSCalendarRelation::First, + )) + } + ICalendarRelationshipType::Next => { + Key::Property(JSCalendarProperty::RelationValue( + JSCalendarRelation::Next, + )) + } + other => Key::Borrowed(other.as_str()), + }, + Value::Bool(true), + ); + } + ( + ICalendarParameterName::Reltype, + ICalendarParameterValue::Text(value), + ) => { + rels.insert(Key::Owned(value), Value::Bool(true)); + } + (name, value) => { + entry.entry.params.push(ICalendarParameter { name, value }); + } + } + } + + entry.set_converted_to::(&[ + JSCalendarProperty::RelatedTo::.to_string().as_ref(), + value.as_str(), + ]); + + state + .entries + .entry(Key::Property(JSCalendarProperty::RelatedTo)) + .or_insert_with(Value::new_object) + .as_object_mut() + .unwrap() + .insert( + Key::Owned(value), + Value::Object(Map::from(if !rels.is_empty() { + vec![( + Key::Property(JSCalendarProperty::Relation), + Value::Object(rels), + )] + } else { + vec![] + })), + ); + } + ( + ICalendarProperty::ShowWithoutTime, + Some(ICalendarValue::Boolean(value)), + ICalendarComponentType::VEvent | ICalendarComponentType::VTodo, + ) => { + state.entries.insert( + Key::Property(JSCalendarProperty::ShowWithoutTime), + Value::Bool(value), + ); + entry.set_converted_to::(&[JSCalendarProperty::ShowWithoutTime:: + .to_string() + .as_ref()]); + } + ( + ICalendarProperty::Status, + Some(ICalendarValue::Status(value)), + ICalendarComponentType::VEvent, + ) => { + state.entries.insert( + Key::Property(JSCalendarProperty::Status), + match value { + ICalendarStatus::Tentative => Value::Element( + JSCalendarValue::EventStatus(JSCalendarEventStatus::Tentative), + ), + ICalendarStatus::Confirmed => Value::Element( + JSCalendarValue::EventStatus(JSCalendarEventStatus::Confirmed), + ), + ICalendarStatus::Cancelled => Value::Element( + JSCalendarValue::EventStatus(JSCalendarEventStatus::Cancelled), + ), + other => Value::Str(other.as_str().into()), + }, + ); + entry.set_converted_to::(&[JSCalendarProperty::Status:: + .to_string() + .as_ref()]); + } + ( + ICalendarProperty::Status, + Some(ICalendarValue::Status(value)), + ICalendarComponentType::VTodo, + ) => { + state.entries.insert( + Key::Property(JSCalendarProperty::Progress), + match value { + ICalendarStatus::NeedsAction => Value::Element( + JSCalendarValue::Progress(JSCalendarProgress::NeedsAction), + ), + ICalendarStatus::Completed => Value::Element( + JSCalendarValue::Progress(JSCalendarProgress::Completed), + ), + ICalendarStatus::InProcess => Value::Element( + JSCalendarValue::Progress(JSCalendarProgress::InProcess), + ), + ICalendarStatus::Failed => Value::Element(JSCalendarValue::Progress( + JSCalendarProgress::Failed, + )), + ICalendarStatus::Cancelled => Value::Element( + JSCalendarValue::Progress(JSCalendarProgress::Cancelled), + ), + other => Value::Str(other.as_str().into()), + }, + ); + entry.set_converted_to::(&[JSCalendarProperty::Progress:: + .to_string() + .as_ref()]); + } + ( + ICalendarProperty::Status, + Some(ICalendarValue::Text(value)), + ICalendarComponentType::VCalendar | ICalendarComponentType::VTodo, + ) => { + let prop = if matches!(state.component_type, ICalendarComponentType::VTodo) { + JSCalendarProperty::Progress + } else { + JSCalendarProperty::Status + }; + + entry.set_converted_to::(&[prop.to_string().as_ref()]); + state + .entries + .insert(Key::Property(prop), Value::Str(value.into())); + } + ( + ICalendarProperty::Source, + Some(ICalendarValue::Text(value)), + ICalendarComponentType::VCalendar, + ) => { + state.entries.insert( + Key::Property(JSCalendarProperty::Source), + Value::Str(value.into()), + ); + entry.set_converted_to::(&[JSCalendarProperty::Source:: + .to_string() + .as_ref()]); + } + ( + ICalendarProperty::Transp, + Some(ICalendarValue::Transparency(value)), + ICalendarComponentType::VEvent | ICalendarComponentType::VTodo, + ) => { + state.entries.insert( + Key::Property(JSCalendarProperty::FreeBusyStatus), + Value::Element(JSCalendarValue::FreeBusyStatus(match value { + ICalendarTransparency::Opaque => JSCalendarFreeBusyStatus::Busy, + ICalendarTransparency::Transparent => JSCalendarFreeBusyStatus::Free, + })), + ); + entry.set_converted_to::(&[JSCalendarProperty::FreeBusyStatus:: + .to_string() + .as_ref()]); + } + ( + ICalendarProperty::Trigger, + Some(ICalendarValue::Duration(value)), + ICalendarComponentType::VAlarm, + ) => { + let mut obj = Map::from(vec![ + ( + Key::Property(JSCalendarProperty::Type), + Value::Element(JSCalendarValue::Type(JSCalendarType::OffsetTrigger)), + ), + ( + Key::Property(JSCalendarProperty::Offset), + Value::Element(JSCalendarValue::Duration(value)), + ), + ]); + + for param in std::mem::take(&mut entry.entry.params) { + match (param.name, param.value) { + ( + ICalendarParameterName::Related, + ICalendarParameterValue::Related(value), + ) => { + obj.insert( + Key::Property(JSCalendarProperty::RelativeTo), + Value::Element(JSCalendarValue::RelativeTo(match value { + ICalendarRelated::Start => JSCalendarRelativeTo::Start, + ICalendarRelated::End => JSCalendarRelativeTo::End, + })), + ); + } + (name, value) => { + entry.entry.params.push(ICalendarParameter { name, value }); + } + } + } + + state.entries.insert( + Key::Property(JSCalendarProperty::Trigger), + Value::Object(obj), + ); + entry.set_converted_to::(&[JSCalendarProperty::Trigger:: + .to_string() + .as_ref()]); + } + ( + ICalendarProperty::Trigger, + Some(ICalendarValue::PartialDateTime(value)), + ICalendarComponentType::VAlarm, + ) if value.has_date_and_time() => { + state.entries.insert( + Key::Property(JSCalendarProperty::Trigger), + Value::Object(Map::from(vec![ + ( + Key::Property(JSCalendarProperty::Type), + Value::Element(JSCalendarValue::Type( + JSCalendarType::AbsoluteTrigger, + )), + ), + ( + Key::Property(JSCalendarProperty::When), + Value::Element(JSCalendarValue::DateTime(JSCalendarDateTime::new( + value.to_timestamp().unwrap_or_default(), + false, + ))), + ), + ])), + ); + entry.set_converted_to::(&[JSCalendarProperty::Trigger:: + .to_string() + .as_ref()]); + } + ( + ICalendarProperty::Uid, + Some(ICalendarValue::Text(value)), + ICalendarComponentType::VCalendar + | ICalendarComponentType::VEvent + | ICalendarComponentType::VTodo, + ) => { + state.uid = Some(value); + continue; + } + (ICalendarProperty::Jsid, Some(ICalendarValue::Text(value)), _) => { + state.jsid = Some(value); + continue; + } + (ICalendarProperty::Jsid, _, _) => { + continue; + } + (ICalendarProperty::Jsprop, Some(ICalendarValue::Text(value)), _) => { + if let Some(ICalendarParameter { + name: ICalendarParameterName::Jsptr, + value: ICalendarParameterValue::Text(ptr), + }) = entry.entry.params.first() + { + let ptr = JsonPointer::>::parse(ptr); + + if let Ok(jscalendar) = JSCalendar::parse(&value) { + state.patch_objects.push((ptr, jscalendar.0.into_owned())); + continue; + } + } + entry.entry.values = [ICalendarValue::Text(value)] + .into_iter() + .chain(values) + .collect(); + } + (ICalendarProperty::Begin | ICalendarProperty::End, _, _) => { + continue; + } + + (_, value, _) => { + entry.entry.values = [value].into_iter().flatten().chain(values).collect(); + } + } + + state.add_conversion_props(entry); + } + + if !group_objects.is_empty() { + state.entries.insert( + Key::Property(JSCalendarProperty::Entries), + Value::Array(group_objects), + ); + } + + state + } +} + +fn period_to_date_time( + value: &ICalendarValue, + tz: Tz, +) -> Option<(DateTime, ICalendarDuration)> { + match value { + ICalendarValue::Period(ICalendarPeriod::Range { start, end }) => { + let start = start.to_date_time()?.to_date_time_with_tz(tz)?; + let end = end.to_date_time()?.to_date_time_with_tz(tz)?; + + Some(( + start, + ICalendarDuration::from_seconds(end.signed_duration_since(start).num_seconds()), + )) + } + ICalendarValue::Period(ICalendarPeriod::Duration { start, duration }) => Some(( + start.to_date_time()?.to_date_time_with_tz(tz)?, + duration.clone(), + )), + _ => None, + } +} diff --git a/vendor/calcard/src/jscalendar/import/mod.rs b/vendor/calcard/src/jscalendar/import/mod.rs new file mode 100644 index 00000000..25df8fd5 --- /dev/null +++ b/vendor/calcard/src/jscalendar/import/mod.rs @@ -0,0 +1,96 @@ +/* + * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC + * + * SPDX-License-Identifier: Apache-2.0 OR MIT + */ + +use ahash::AHashMap; +use chrono::DateTime; +use jmap_tools::{JsonPointer, Key, Value}; + +use crate::{ + common::timezone::Tz, + icalendar::{ + ICalendarComponentType, ICalendarEntry, ICalendarParameterName, ICalendarProperty, + }, + jscalendar::{JSCalendarId, JSCalendarProperty, JSCalendarValue}, +}; + +pub mod convert; +pub mod params; +pub mod props; + +#[derive(Debug, Clone, Copy)] +pub struct ConversionOptions { + pub include_ical_components: bool, + pub return_first: bool, +} + +#[derive(Default)] +#[allow(clippy::type_complexity)] +struct State { + component_type: ICalendarComponentType, + entries: AHashMap< + Key<'static, JSCalendarProperty>, + Value<'static, JSCalendarProperty, JSCalendarValue>, + >, + ical_converted_properties: AHashMap>, + ical_properties: Vec, JSCalendarValue>>, + ical_components: Option, JSCalendarValue>>, + patch_objects: Vec<( + JsonPointer>, + Value<'static, JSCalendarProperty, JSCalendarValue>, + )>, + jsid: Option, + uid: Option, + recurrence_id: Option>, + tz_start: Option, + tz_end: Option, + has_dates: bool, + map_component: bool, + is_recurrence_instance: bool, + include_ical_components: bool, +} + +#[derive(Debug, Default)] +struct ICalendarConvertedProperty { + name: Option, + params: ICalendarParams, +} + +#[derive(Debug, Default)] +#[allow(clippy::type_complexity)] +struct ICalendarParams( + AHashMap< + ICalendarParameterName, + Vec, JSCalendarValue>>, + >, +); + +#[derive(Debug, Clone)] +struct EntryState { + entry: ICalendarEntry, + converted_to: Option, + map_name: bool, +} + +impl Default for ConversionOptions { + fn default() -> Self { + Self { + include_ical_components: true, + return_first: false, + } + } +} + +impl ConversionOptions { + pub fn include_ical_components(mut self, include: bool) -> Self { + self.include_ical_components = include; + self + } + + pub fn return_first(mut self, return_first: bool) -> Self { + self.return_first = return_first; + self + } +} diff --git a/vendor/calcard/src/jscalendar/import/params.rs b/vendor/calcard/src/jscalendar/import/params.rs new file mode 100644 index 00000000..7b4ef45f --- /dev/null +++ b/vendor/calcard/src/jscalendar/import/params.rs @@ -0,0 +1,423 @@ +/* + * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC + * + * SPDX-License-Identifier: Apache-2.0 OR MIT + */ + +use crate::{ + common::{Data, IanaString, IanaType}, + icalendar::{ + ICalendarDisplayType, ICalendarEntry, ICalendarFeatureType, ICalendarParameter, + ICalendarParameterName, ICalendarParameterValue, ICalendarParticipationRole, + ICalendarParticipationStatus, ICalendarProperty, ICalendarRelated, ICalendarUserTypes, + ICalendarValue, ICalendarValueType, Uri, + }, + jscalendar::{ + JSCalendarId, JSCalendarLinkDisplay, JSCalendarParticipantKind, JSCalendarParticipantRole, + JSCalendarParticipationStatus, JSCalendarProperty, JSCalendarRelativeTo, JSCalendarValue, + JSCalendarVirtualLocationFeature, import::ICalendarParams, + }, +}; +use ahash::AHashMap; +use jmap_tools::{Key, Map, Value}; + +pub(super) trait ExtractParams { + fn extract_params( + &mut self, + entry: &mut ICalendarEntry, + extract: &[ICalendarParameterName], + ) -> Option; +} + +impl ExtractParams + for AHashMap< + Key<'static, JSCalendarProperty>, + Value<'static, JSCalendarProperty, JSCalendarValue>, + > +{ + fn extract_params( + &mut self, + entry: &mut ICalendarEntry, + extract: &[ICalendarParameterName], + ) -> Option { + let mut jsid = None; + + for param in std::mem::take(&mut entry.params) { + if !extract.contains(¶m.name) { + entry.params.push(param); + continue; + } + + match param.name { + ICalendarParameterName::Cn => { + if let Some(text) = param.value.into_text() { + self.insert(Key::Property(JSCalendarProperty::Name), Value::Str(text)); + } + } + ICalendarParameterName::Cutype => { + self.insert( + Key::Property(JSCalendarProperty::Kind), + match param.value { + ICalendarParameterValue::Text(value) => Value::Str(value.into()), + ICalendarParameterValue::Cutype(value) => match value { + ICalendarUserTypes::Individual => { + Value::Element(JSCalendarValue::ParticipantKind( + JSCalendarParticipantKind::Individual, + )) + } + ICalendarUserTypes::Group => { + Value::Element(JSCalendarValue::ParticipantKind( + JSCalendarParticipantKind::Group, + )) + } + ICalendarUserTypes::Resource => { + Value::Element(JSCalendarValue::ParticipantKind( + JSCalendarParticipantKind::Resource, + )) + } + ICalendarUserTypes::Room => { + Value::Element(JSCalendarValue::ParticipantKind( + JSCalendarParticipantKind::Location, + )) + } + ICalendarUserTypes::Unknown => { + Value::Str(value.as_str().to_lowercase().into()) + } + }, + _ => continue, + }, + ); + } + ICalendarParameterName::DelegatedFrom => { + if let Some(text) = param.value.into_text() { + self.insert( + Key::Property(JSCalendarProperty::DelegatedFrom), + Value::Str(text), + ); + } + } + ICalendarParameterName::DelegatedTo => { + if let Some(text) = param.value.into_text() { + self.insert( + Key::Property(JSCalendarProperty::DelegatedTo), + Value::Str(text), + ); + } + } + ICalendarParameterName::Email => { + if let Some(text) = param.value.into_text() { + self.insert(Key::Property(JSCalendarProperty::Email), Value::Str(text)); + } + } + ICalendarParameterName::Rsvp => { + if let Some(boolean) = param.value.as_bool() { + self.insert( + Key::Property(JSCalendarProperty::ExpectReply), + Value::Bool(boolean), + ); + } + } + ICalendarParameterName::Member => { + if let Some(text) = param.value.into_text() { + self.entry(Key::Property(JSCalendarProperty::MemberOf)) + .or_insert_with(Value::new_object) + .as_object_mut() + .unwrap() + .insert(Key::from(text), Value::Bool(true)); + } + } + ICalendarParameterName::Partstat => { + self.insert( + Key::Property(JSCalendarProperty::ParticipationStatus), + match param.value { + ICalendarParameterValue::Partstat(value) => { + Value::Element(JSCalendarValue::ParticipationStatus(match value { + ICalendarParticipationStatus::NeedsAction => { + JSCalendarParticipationStatus::NeedsAction + } + ICalendarParticipationStatus::Declined => { + JSCalendarParticipationStatus::Declined + } + ICalendarParticipationStatus::Tentative => { + JSCalendarParticipationStatus::Tentative + } + ICalendarParticipationStatus::Delegated => { + JSCalendarParticipationStatus::Delegated + } + ICalendarParticipationStatus::Completed + | ICalendarParticipationStatus::InProcess + | ICalendarParticipationStatus::Failed + | ICalendarParticipationStatus::Accepted => { + JSCalendarParticipationStatus::Accepted + } + })) + } + ICalendarParameterValue::Text(value) => Value::Str(value.into()), + _ => continue, + }, + ); + } + ICalendarParameterName::Role => { + let role = match param.value { + ICalendarParameterValue::Role(value) => { + Key::Property(JSCalendarProperty::ParticipantRole(match value { + ICalendarParticipationRole::Chair => { + JSCalendarParticipantRole::Chair + } + ICalendarParticipationRole::ReqParticipant => { + JSCalendarParticipantRole::Required + } + ICalendarParticipationRole::OptParticipant => { + JSCalendarParticipantRole::Optional + } + ICalendarParticipationRole::NonParticipant => { + JSCalendarParticipantRole::Informational + } + ICalendarParticipationRole::Owner => { + JSCalendarParticipantRole::Owner + } + })) + } + ICalendarParameterValue::Text(value) => Key::Owned(value), + _ => continue, + }; + self.entry(Key::Property(JSCalendarProperty::Roles)) + .or_insert_with(Value::new_object) + .as_object_mut() + .unwrap() + .insert(role, Value::Bool(true)); + } + ICalendarParameterName::SentBy => { + if let Some(text) = param.value.into_text() { + self.insert(Key::Property(JSCalendarProperty::SentBy), Value::Str(text)); + } + } + ICalendarParameterName::Fmttype => { + if let Some(text) = param.value.into_text() { + self.insert( + Key::Property( + if matches!(entry.name, ICalendarProperty::StyledDescription) { + JSCalendarProperty::DescriptionContentType + } else { + JSCalendarProperty::ContentType + }, + ), + Value::Str(text), + ); + } + } + ICalendarParameterName::Label | ICalendarParameterName::Filename => { + if let Some(text) = param.value.into_text() { + self.insert( + Key::Property(if matches!(entry.name, ICalendarProperty::Conference) { + JSCalendarProperty::Name + } else { + JSCalendarProperty::Title + }), + Value::Str(text), + ); + } + } + ICalendarParameterName::Size => { + if let Some(number) = param.value.as_integer() { + self.insert( + Key::Property(JSCalendarProperty::Size), + Value::Number(number.into()), + ); + } else { + entry.params.push(ICalendarParameter::size(param.value)); + } + } + ICalendarParameterName::Linkrel => match param.value { + ICalendarParameterValue::Linkrel(linkrel) => { + self.insert( + Key::Property(JSCalendarProperty::Rel), + Value::Element(JSCalendarValue::LinkRelation(linkrel)), + ); + } + ICalendarParameterValue::Text(value) => { + self.insert( + Key::Property(JSCalendarProperty::Rel), + Value::Str(value.into()), + ); + } + value => { + entry.params.push(ICalendarParameter::linkrel(value)); + } + }, + ICalendarParameterName::Related => { + if let ICalendarParameterValue::Related(related) = param.value { + self.insert( + Key::Property(JSCalendarProperty::RelativeTo), + Value::Element(JSCalendarValue::RelativeTo(match related { + ICalendarRelated::Start => JSCalendarRelativeTo::Start, + ICalendarRelated::End => JSCalendarRelativeTo::End, + })), + ); + } else { + entry.params.push(ICalendarParameter::related(param.value)); + } + } + ICalendarParameterName::Feature => { + let feature = match param.value { + ICalendarParameterValue::Feature(value) => { + Key::Property(JSCalendarProperty::VirtualLocationFeature(match value { + ICalendarFeatureType::Audio => { + JSCalendarVirtualLocationFeature::Audio + } + ICalendarFeatureType::Chat => { + JSCalendarVirtualLocationFeature::Chat + } + ICalendarFeatureType::Feed => { + JSCalendarVirtualLocationFeature::Feed + } + ICalendarFeatureType::Moderator => { + JSCalendarVirtualLocationFeature::Moderator + } + ICalendarFeatureType::Phone => { + JSCalendarVirtualLocationFeature::Phone + } + ICalendarFeatureType::Screen => { + JSCalendarVirtualLocationFeature::Screen + } + ICalendarFeatureType::Video => { + JSCalendarVirtualLocationFeature::Video + } + })) + } + ICalendarParameterValue::Text(value) => Key::Owned(value), + _ => continue, + }; + self.entry(Key::Property(JSCalendarProperty::Features)) + .or_insert_with(Value::new_object) + .as_object_mut() + .unwrap() + .insert(feature, Value::Bool(true)); + } + ICalendarParameterName::Language => { + if let Some(text) = param.value.into_text() { + self.insert(Key::Property(JSCalendarProperty::Locale), Value::Str(text)); + } + } + ICalendarParameterName::Display => { + let display = match param.value { + ICalendarParameterValue::Display(value) => { + Key::Property(JSCalendarProperty::LinkDisplay(match value { + ICalendarDisplayType::Badge => JSCalendarLinkDisplay::Badge, + ICalendarDisplayType::Graphic => JSCalendarLinkDisplay::Graphic, + ICalendarDisplayType::Fullsize => JSCalendarLinkDisplay::Fullsize, + ICalendarDisplayType::Thumbnail => JSCalendarLinkDisplay::Thumbnail, + })) + } + ICalendarParameterValue::Text(value) => Key::Owned(value), + value => { + entry.params.push(ICalendarParameter::display(value)); + continue; + } + }; + self.entry(Key::Property(JSCalendarProperty::Display)) + .or_insert_with(Value::new_object) + .as_object_mut() + .unwrap() + .insert(display, Value::Bool(true)); + } + ICalendarParameterName::Jsid => { + jsid = param.value.into_text().map(|v| v.into_owned()); + } + ICalendarParameterName::Range + | ICalendarParameterName::Reltype + | ICalendarParameterName::Dir + | ICalendarParameterName::Fbtype + | ICalendarParameterName::ScheduleAgent + | ICalendarParameterName::ScheduleForceSend + | ICalendarParameterName::ScheduleStatus + | ICalendarParameterName::Tzid + | ICalendarParameterName::Value + | ICalendarParameterName::ManagedId + | ICalendarParameterName::Order + | ICalendarParameterName::Schema + | ICalendarParameterName::Derived + | ICalendarParameterName::Gap + | ICalendarParameterName::Jsptr + | ICalendarParameterName::Other(_) + | ICalendarParameterName::Altrep => {} + } + } + + jsid + } +} + +impl ICalendarParams { + pub(super) fn into_jscalendar_value( + self, + ) -> Option, JSCalendarValue>> { + if !self.0.is_empty() { + let mut obj = Map::from(Vec::with_capacity(self.0.len())); + + for (param, value) in self.0 { + let value = if value.len() > 1 { + Value::Array(value) + } else { + value.into_iter().next().unwrap() + }; + obj.insert_unchecked(Key::Owned(param.into_string().to_ascii_lowercase()), value); + } + Some(obj) + } else { + None + } + } +} + +impl ICalendarValue { + pub(super) fn into_jscalendar_value( + self, + value_type: Option<&IanaType>, + ) -> Value<'static, JSCalendarProperty, JSCalendarValue> { + match self { + ICalendarValue::Text(v) => Value::Str(v.into()), + ICalendarValue::Integer(v) => Value::Number(v.into()), + ICalendarValue::Float(v) => Value::Number(v.into()), + ICalendarValue::Boolean(v) => Value::Bool(v), + ICalendarValue::PartialDateTime(v) => { + let mut out = String::new(); + let _ = v.format_as_ical( + &mut out, + value_type.and_then(|v| v.iana()).unwrap_or( + match (v.has_date(), v.has_time(), v.has_zone()) { + (true, true, _) => &ICalendarValueType::DateTime, + (true, _, _) => &ICalendarValueType::Date, + (_, true, _) => &ICalendarValueType::Time, + (_, _, true) => &ICalendarValueType::UtcOffset, + _ => &ICalendarValueType::Text, + }, + ), + ); + Value::Str(out.into()) + } + ICalendarValue::Binary(v) => Value::Str( + Uri::Data(Data { + content_type: None, + data: v, + }) + .into_unwrapped_string() + .into(), + ), + ICalendarValue::Uri(v) => Value::Str(v.into_unwrapped_string().into()), + ICalendarValue::Duration(v) => Value::Str(v.to_string().into()), + ICalendarValue::RecurrenceRule(v) => Value::Str(v.to_string().into()), + ICalendarValue::Period(v) => Value::Str(v.to_string().into()), + ICalendarValue::CalendarScale(v) => Value::Str(v.as_str().into()), + ICalendarValue::Method(v) => Value::Str(v.as_str().into()), + ICalendarValue::Classification(v) => Value::Str(v.as_str().into()), + ICalendarValue::Status(v) => Value::Str(v.as_str().into()), + ICalendarValue::Transparency(v) => Value::Str(v.as_str().into()), + ICalendarValue::Action(v) => Value::Str(v.as_str().into()), + ICalendarValue::BusyType(v) => Value::Str(v.as_str().into()), + ICalendarValue::ParticipantType(v) => Value::Str(v.as_str().into()), + ICalendarValue::ResourceType(v) => Value::Str(v.as_str().into()), + ICalendarValue::Proximity(v) => Value::Str(v.as_str().into()), + } + } +} diff --git a/vendor/calcard/src/jscalendar/import/props.rs b/vendor/calcard/src/jscalendar/import/props.rs new file mode 100644 index 00000000..372a1a5f --- /dev/null +++ b/vendor/calcard/src/jscalendar/import/props.rs @@ -0,0 +1,491 @@ +/* + * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC + * + * SPDX-License-Identifier: Apache-2.0 OR MIT + */ + +use crate::{ + common::{Data, IanaString, IanaType, timezone::TzTimestamp}, + icalendar::{ + ICalendarEntry, ICalendarParameterName, ICalendarProperty, ICalendarValue, + ICalendarValueType, Uri, + }, + jscalendar::{ + JSCalendarDateTime, JSCalendarId, JSCalendarPrivacy, JSCalendarProperty, JSCalendarValue, + import::{ + EntryState, ICalendarConvertedProperty, ICalendarParams, State, params::ExtractParams, + }, + uuid5, + }, +}; +use ahash::AHashMap; +use jmap_tools::{JsonPointer, JsonPointerHandler, JsonPointerItem, Key, Map, Property, Value}; +use std::{borrow::Cow, collections::hash_map::Entry, str::FromStr}; + +impl State { + pub(super) fn map_named_entry( + &mut self, + entry: &mut EntryState, + extract: &[ICalendarParameterName], + top_property_name: JSCalendarProperty, + values: impl IntoIterator< + Item = ( + Key<'static, JSCalendarProperty>, + Value<'static, JSCalendarProperty, JSCalendarValue>, + ), + >, + ) { + self.map_named_entry_with_id(entry, extract, top_property_name, values, None) + } + + pub(super) fn map_named_entry_with_id( + &mut self, + entry: &mut EntryState, + extract: &[ICalendarParameterName], + top_property_name: JSCalendarProperty, + values: impl IntoIterator< + Item = ( + Key<'static, JSCalendarProperty>, + Value<'static, JSCalendarProperty, JSCalendarValue>, + ), + >, + default_id: Option, + ) { + // Obtain main property and value + let mut values = values.into_iter().peekable(); + let (property, value) = match values.peek() { + Some((property, Value::Str(s))) => (property, s.as_ref()), + Some((property, _)) => { + debug_assert!(false, "Cannot generate jsid without a string value"); + (property, "unknown") + } + _ => { + panic!("Cannot generate jsid without a value"); + } + }; + + // Obtain or calculate JSID + let mut parameters = AHashMap::new(); + let js_id = parameters + .extract_params(&mut entry.entry, extract) + .or(default_id) + .unwrap_or_else(|| uuid5(value)); + + // Set converted props + entry.set_converted_to::(&[ + top_property_name.to_cow().as_ref(), + js_id.as_str(), + property.to_string().as_ref(), + ]); + + let obj = self + .get_mut_object_or_insert(top_property_name) + .insert_or_get_mut(Key::Owned(js_id), Value::new_object()) + .as_object_mut() + .unwrap(); + + for (key, value) in values.chain(parameters) { + if let Some(current_value) = obj.get_mut(&key) { + match (value, current_value) { + (Value::Object(new_obj), Value::Object(existing_obj)) => { + for (key, value) in new_obj.into_vec() { + existing_obj.insert(key, value); + } + } + (value, current_value) => { + *current_value = value; + } + } + } else { + obj.insert_unchecked(key, value); + } + } + } + + pub(super) fn insert_recurrence_override( + &mut self, + key: Key<'static, JSCalendarProperty>, + value: Value<'static, JSCalendarProperty, JSCalendarValue>, + ) { + self.entries + .entry(Key::Property(JSCalendarProperty::RecurrenceOverrides)) + .or_insert_with(Value::new_object) + .as_object_mut() + .unwrap() + .insert(key, value); + } + + pub(super) fn add_period_conversion_prop(&mut self, converted_to: String) { + if self.include_ical_components { + let mut params = ICalendarParams::default(); + params.0.insert( + ICalendarParameterName::Value, + vec![Value::Str(ICalendarValueType::Period.as_str().into())], + ); + self.ical_converted_properties + .entry(converted_to) + .or_insert(ICalendarConvertedProperty { + name: Some(ICalendarProperty::Rdate), + params, + }); + } + } + + pub(super) fn add_conversion_props(&mut self, mut entry: EntryState) { + if self.include_ical_components { + if let Some(converted_to) = entry.converted_to.take() { + if entry.map_name || !entry.entry.params.is_empty() { + let mut value_type = None; + + match self.ical_converted_properties.entry(converted_to) { + Entry::Occupied(mut conv_prop) => { + entry.jcal_parameters(&mut conv_prop.get_mut().params, &mut value_type); + } + Entry::Vacant(conv_prop) => { + let mut params = ICalendarParams::default(); + entry.jcal_parameters(&mut params, &mut value_type); + if let Some(value_type) = value_type { + params.0.insert( + ICalendarParameterName::Value, + vec![Value::Str(value_type.into_string())], + ); + } + if !params.0.is_empty() || entry.map_name { + conv_prop.insert(ICalendarConvertedProperty { + name: if entry.map_name { + Some(entry.entry.name) + } else { + None + }, + params, + }); + } + } + } + } + } else { + self.ical_properties.push(entry.into_jcal()); + } + } + } + + pub(super) fn into_object( + mut self, + ) -> Value<'static, JSCalendarProperty, JSCalendarValue> { + let mut ical_obj = Map::from(Vec::new()); + if !self.ical_converted_properties.is_empty() { + let mut converted_properties = + Map::from(Vec::with_capacity(self.ical_converted_properties.len())); + + for (converted_to, props) in self.ical_converted_properties { + let mut obj = Map::from(Vec::with_capacity(2)); + if let Some(params) = props.params.into_jscalendar_value() { + obj.insert( + Key::Property(JSCalendarProperty::Parameters), + Value::Object(params), + ); + } + if let Some(name) = props.name { + obj.insert( + Key::Property(JSCalendarProperty::Name), + Value::Str(name.into_string().to_ascii_lowercase().into()), + ); + } + + converted_properties.insert_unchecked(Key::Owned(converted_to), Value::Object(obj)); + } + + ical_obj.insert_unchecked( + Key::Property(JSCalendarProperty::ConvertedProperties), + Value::Object(converted_properties), + ); + } + + if !self.ical_properties.is_empty() { + ical_obj.insert_unchecked( + Key::Property(JSCalendarProperty::Properties), + Value::Array(self.ical_properties), + ); + } + + if let Some(components) = self.ical_components { + ical_obj.insert_unchecked(Key::Property(JSCalendarProperty::Components), components); + } + + if !ical_obj.is_empty() || self.map_component { + ical_obj.insert_unchecked( + Key::Property(JSCalendarProperty::Name), + Value::Str(self.component_type.as_str().to_ascii_lowercase().into()), + ); + self.entries.insert( + Key::Property(JSCalendarProperty::ICalendar), + Value::Object(ical_obj), + ); + } + + if self.has_dates { + /* + If a date-time value does not have a timezone, then the timezone is not set + in JSCalendar. + */ + + if !self.is_recurrence_instance + && let Some(tz) = self.tz_start.and_then(|tz| tz.name()) + { + self.entries + .insert(Key::Property(JSCalendarProperty::TimeZone), Value::Str(tz)); + } + + if self.tz_end.is_some() + && self.tz_start.is_some() + && self.tz_end != self.tz_start + && let Some(tz) = self.tz_end.and_then(|tz| tz.name()) + { + self.entries.insert( + Key::Property(JSCalendarProperty::EndTimeZone), + Value::Str(tz), + ); + } + + if let Some(recurrence_id) = self.recurrence_id { + self.entries.insert( + Key::Property(JSCalendarProperty::RecurrenceId), + Value::Element(JSCalendarValue::DateTime(JSCalendarDateTime::new( + recurrence_id + .with_timezone(&self.tz_start.unwrap_or_default()) + .to_naive_timestamp(), + true, + ))), + ); + if !self.is_recurrence_instance + && let Some(tz) = recurrence_id + .timezone() + .to_resolved() + .and_then(|tz| tz.name()) + { + self.entries.insert( + Key::Property(JSCalendarProperty::RecurrenceIdTimeZone), + Value::Str(tz), + ); + } + } + } + + if let Some(uid) = self.uid { + self.entries.insert( + Key::Property(JSCalendarProperty::Uid), + Value::Str(uid.into()), + ); + } + + if !self.is_recurrence_instance + && let Some(component_type) = self.component_type.to_jscalendar_type() + { + self.entries.insert( + Key::Property(JSCalendarProperty::Type), + Value::Element(JSCalendarValue::Type(component_type)), + ); + } + + let mut obj = Value::Object(self.entries.into_iter().collect()); + if !self.patch_objects.is_empty() { + for (ptr, patch) in self.patch_objects { + obj.patch_jptr(ptr.iter(), patch); + } + } + + obj + } + + pub(super) fn set_map_component(&mut self) { + self.map_component = true; + } + + pub(super) fn set_is_recurrence_instance(&mut self) { + self.is_recurrence_instance = true; + } + + pub(super) fn privacy(&self) -> Option { + match self + .entries + .get(&Key::Property(JSCalendarProperty::Privacy))? + { + Value::Element(JSCalendarValue::Privacy(privacy)) => Some(*privacy), + _ => Some(JSCalendarPrivacy::Private), + } + } + + pub(super) fn raise_privacy(&mut self, privacy: JSCalendarPrivacy) { + if self.privacy().unwrap_or(JSCalendarPrivacy::Public) < privacy { + self.entries.insert( + Key::Property(JSCalendarProperty::Privacy), + Value::Element(JSCalendarValue::Privacy(privacy)), + ); + self.patch_objects.retain(|(pointer, _)| { + !matches!( + pointer.first(), + Some(JsonPointerItem::Key(Key::Property( + JSCalendarProperty::Privacy + ))) + ) + }); + } + } + + pub(super) fn merge_privacy(&mut self, privacy: JSCalendarPrivacy) { + let privacy = self + .privacy() + .map_or(privacy, |current| current.max(privacy)); + self.entries.insert( + Key::Property(JSCalendarProperty::Privacy), + Value::Element(JSCalendarValue::Privacy(privacy)), + ); + } + + pub(super) fn remove_forbidden_override_patches(&mut self) { + self.entries.retain(|key, _| { + !matches!(key, Key::Property(property) if property.is_forbidden_override_patch()) + }); + self.patch_objects + .retain(|(pointer, _)| !JSCalendarProperty::is_forbidden_override_pointer(pointer)); + self.ical_converted_properties.retain(|converted_to, _| { + !matches!( + converted_to + .split('/') + .next() + .and_then(|name| JSCalendarProperty::::from_str(name).ok()), + Some(JSCalendarProperty::RecurrenceOverrides | JSCalendarProperty::RecurrenceRule) + ) + }); + } + + pub(super) fn find_participant_by_address(&self, address: &str) -> Option { + self.entries + .get(&Key::Property(JSCalendarProperty::Participants))? + .as_object()? + .iter() + .find_map(|(key, value)| { + match value + .as_object()? + .get(&Key::Property(JSCalendarProperty::CalendarAddress))? + { + Value::Str(value) if value == address => Some(key.to_string().into_owned()), + _ => None, + } + }) + } + + #[inline] + pub(super) fn get_mut_object_or_insert( + &mut self, + key: JSCalendarProperty, + ) -> &mut Map<'static, JSCalendarProperty, JSCalendarValue> { + self.entries + .entry(Key::Property(key)) + .or_insert_with(|| Value::Object(Map::from(Vec::new()))) + .as_object_mut() + .unwrap() + } +} + +impl EntryState { + pub(super) fn new(entry: ICalendarEntry) -> Self { + Self { + entry, + converted_to: None, + map_name: false, + } + } + + pub(super) fn set_converted_to(&mut self, converted_to: &[&str]) { + self.converted_to = Some(JsonPointer::>::encode(converted_to)); + } + + pub(super) fn set_map_name(&mut self) { + self.map_name = true; + } + + pub(super) fn into_jcal( + mut self, + ) -> Value<'static, JSCalendarProperty, JSCalendarValue> { + let mut value_type = None; + let mut params = ICalendarParams::default(); + + self.jcal_parameters(&mut params, &mut value_type); + + let values = if self.entry.values.len() == 1 { + self.entry + .values + .into_iter() + .next() + .unwrap() + .into_jscalendar_value(value_type.as_ref()) + } else { + let mut values = Vec::with_capacity(self.entry.values.len()); + for value in self.entry.values { + values.push(value.into_jscalendar_value(value_type.as_ref())); + } + Value::Array(values) + }; + Value::Array(vec![ + Value::Str(self.entry.name.as_str().to_ascii_lowercase().into()), + Value::Object( + params + .into_jscalendar_value() + .unwrap_or(Map::from(Vec::new())), + ), + Value::Str( + value_type + .map(|v| v.into_string()) + .unwrap_or(Cow::Borrowed("unknown")), + ), + values, + ]) + } + + pub(super) fn jcal_parameters( + &mut self, + params: &mut ICalendarParams, + value_type: &mut Option>, + ) { + if self.entry.params.is_empty() { + return; + } + let (default_type, _) = self.entry.name.default_types(); + let default_type = default_type.unwrap_ical(); + + for param in std::mem::take(&mut self.entry.params) { + if matches!(param.name, ICalendarParameterName::Value) { + if let Some(v) = param + .value + .into_value_type() + .filter(|v| !v.is_iana_and(|v| v == &default_type)) + { + *value_type = Some(v); + } + } else if let Some(value) = param.value.into_text() { + params + .0 + .entry(param.name) + .or_default() + .push(Value::Str(value)); + } + } + } +} + +impl ICalendarValue { + pub(super) fn uri_to_string(self) -> Self { + match self { + ICalendarValue::Uri(uri) => ICalendarValue::Text(uri.into_unwrapped_string()), + ICalendarValue::Binary(data) => ICalendarValue::Text( + Uri::Data(Data { + content_type: None, + data, + }) + .into_unwrapped_string(), + ), + other => other, + } + } +} diff --git a/vendor/calcard/src/jscalendar/mod.rs b/vendor/calcard/src/jscalendar/mod.rs new file mode 100644 index 00000000..25c9b908 --- /dev/null +++ b/vendor/calcard/src/jscalendar/mod.rs @@ -0,0 +1,799 @@ +/* + * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC + * + * SPDX-License-Identifier: Apache-2.0 OR MIT + */ + +pub mod export; +pub mod import; +pub mod parser; +pub mod types; + +use crate::{ + common::{CalendarScale, IanaString, LinkRelation}, + icalendar::{ + ICalendarComponentType, ICalendarDuration, ICalendarFrequency, ICalendarMethod, + ICalendarMonth, ICalendarSkip, ICalendarWeekday, + }, +}; +use jmap_tools::{JsonPointer, Key, Map, Value}; +use mail_parser::DateTime; +use serde::Serialize; +use std::{borrow::Cow, fmt::Debug, fmt::Display, hash::Hash, str::FromStr}; + +#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize)] +#[repr(transparent)] +pub struct JSCalendar<'x, I: JSCalendarId, B: JSCalendarId>( + pub Value<'x, JSCalendarProperty, JSCalendarValue>, +); + +pub trait JSCalendarId: + FromStr + Sized + Serialize + Display + Clone + Eq + Hash + Ord + Debug + Default +{ +} + +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum JSCalendarValue { + Type(JSCalendarType), + DateTime(JSCalendarDateTime), + Duration(ICalendarDuration), + AlertAction(JSCalendarAlertAction), + FreeBusyStatus(JSCalendarFreeBusyStatus), + ParticipantKind(JSCalendarParticipantKind), + ParticipationStatus(JSCalendarParticipationStatus), + Privacy(JSCalendarPrivacy), + Progress(JSCalendarProgress), + RelativeTo(JSCalendarRelativeTo), + ScheduleAgent(JSCalendarScheduleAgent), + EventStatus(JSCalendarEventStatus), + LinkRelation(LinkRelation), + Frequency(ICalendarFrequency), + CalendarScale(CalendarScale), + Skip(ICalendarSkip), + Weekday(ICalendarWeekday), + Month(ICalendarMonth), + Method(ICalendarMethod), + Id(I), + BlobId(B), + IdReference(String), +} + +impl Default for JSCalendarValue { + fn default() -> Self { + JSCalendarValue::Type(JSCalendarType::default()) + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct JSCalendarDateTime { + pub timestamp: i64, + pub is_local: bool, +} + +#[derive(Debug, Default, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum JSCalendarProperty { + // JMAP for Calendar Properties + Id, + BaseEventId, + CalendarIds, + IsDraft, + IsOrigin, + UtcStart, + UtcEnd, + UseDefaultAlerts, + MayInviteSelf, + MayInviteOthers, + HideAttendees, + BlobId, + + // JSCalendar Properties + #[default] + Type, + Acknowledged, + Action, + Alerts, + ByDay, + ByHour, + ByMinute, + ByMonth, + ByMonthDay, + BySecond, + BySetPosition, + ByWeekNo, + ByYearDay, + CalendarAddress, + Categories, + Color, + ContentType, + Coordinates, + Count, + Created, + Day, + DelegatedFrom, + DelegatedTo, + Description, + DescriptionContentType, + Display, + Due, + Duration, + Email, + Entries, + EstimatedDuration, + Excluded, + ExpectReply, + Features, + FirstDayOfWeek, + FreeBusyStatus, + Frequency, + Href, + Interval, + InvitedBy, + Keywords, + Kind, + Links, + Locale, + Locations, + LocationTypes, + MemberOf, + Method, + Name, + NthOfPeriod, + Offset, + Participants, + ParticipationComment, + ParticipationStatus, + PercentComplete, + Priority, + Privacy, + ProdId, + Progress, + RecurrenceId, + RecurrenceIdTimeZone, + RecurrenceOverrides, + Rel, + RelatedTo, + Relation, + RelativeTo, + ReplyTo, + RequestStatus, + Roles, + Rscale, + SentBy, + ScheduleAgent, + ScheduleForceSend, + ScheduleSequence, + ScheduleStatus, + ScheduleUpdated, + SendTo, + Sequence, + ShowWithoutTime, + Size, + Skip, + Source, + Start, + Status, + TimeZone, + Title, + Trigger, + Uid, + Until, + Updated, + Uri, + Version, + VirtualLocations, + When, + EndTimeZone, + MainLocationId, + OrganizerCalendarAddress, + RecurrenceRule, + ICalendar, + Properties, + Parameters, + ConvertedProperties, + ValueType, + Components, + DateTime(JSCalendarDateTime), + LinkDisplay(JSCalendarLinkDisplay), + VirtualLocationFeature(JSCalendarVirtualLocationFeature), + ParticipantRole(JSCalendarParticipantRole), + RelationValue(JSCalendarRelation), + LinkRelation(LinkRelation), + Pointer(JsonPointer>), + IdValue(I), + IdReference(String), +} + +impl JSCalendarId for T where + T: FromStr + Sized + Serialize + Display + Clone + Eq + Hash + Ord + Debug + Default +{ +} + +impl From for JSCalendarProperty { + fn from(id: I) -> Self { + JSCalendarProperty::IdValue(id) + } +} + +impl From for JSCalendarValue { + fn from(id: I) -> Self { + JSCalendarValue::Id(id) + } +} + +impl<'x, I: JSCalendarId, B: JSCalendarId> Default for JSCalendar<'x, I, B> { + fn default() -> Self { + Self(Value::Object(Map::from(vec![ + ( + Key::Property(JSCalendarProperty::Type), + Value::Element(JSCalendarValue::Type(JSCalendarType::Group)), + ), + ( + Key::Property(JSCalendarProperty::Entries), + Value::Array(vec![]), + ), + ]))) + } +} + +#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum JSCalendarType { + #[default] + Event, + Task, + Group, + Alert, + Boolean, + Duration, + Id, + Int, + LocalDateTime, + Link, + Location, + NDay, + Number, + Participant, + PatchObject, + RecurrenceRule, + Relation, + SignedDuration, + String, + TimeZone, + TimeZoneId, + TimeZoneRule, + UnsignedInt, + UTCDateTime, + VirtualLocation, + ICalComponent, + OffsetTrigger, + AbsoluteTrigger, +} + +// JSCalendar Enum Values for action (Context: Alert) +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum JSCalendarAlertAction { + Display, + Email, +} + +// JSCalendar Enum Values for display (Context: Link) +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum JSCalendarLinkDisplay { + Badge, + Graphic, + Fullsize, + Thumbnail, +} + +// JSCalendar Enum Values for features (Context: VirtualLocation) +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum JSCalendarVirtualLocationFeature { + Audio, + Chat, + Feed, + Moderator, + Phone, + Screen, + Video, +} + +// JSCalendar Enum Values for freeBusyStatus (Context: Event, Task) +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum JSCalendarFreeBusyStatus { + Free, + Busy, +} + +// JSCalendar Enum Values for kind (Context: Participant) +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum JSCalendarParticipantKind { + Individual, + Group, + Resource, + Location, +} + +// JSCalendar Enum Values for participationStatus (Context: Participant) +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum JSCalendarParticipationStatus { + NeedsAction, + Accepted, + Declined, + Tentative, + Delegated, +} + +// JSCalendar Enum Values for privacy (Context: Event, Task) +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum JSCalendarPrivacy { + Public, + Private, + Secret, +} + +// JSCalendar Enum Values for progress (Context: Task, Participant) +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum JSCalendarProgress { + NeedsAction, + InProcess, + Completed, + Failed, + Cancelled, +} + +// JSCalendar Enum Values for relation (Context: Relation) +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum JSCalendarRelation { + First, + Next, + Child, + Parent, + Snooze, +} + +// JSCalendar Enum Values for relativeTo (Context: OffsetTrigger, Location) +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum JSCalendarRelativeTo { + Start, + End, +} + +// JSCalendar Enum Values for roles (Context: Participant) +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum JSCalendarParticipantRole { + Owner, + Optional, + Informational, + Chair, + Required, +} + +// JSCalendar Enum Values for scheduleAgent (Context: Participant) +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum JSCalendarScheduleAgent { + Server, + Client, + None, +} + +// JSCalendar Enum Values for status (Context: Event) +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum JSCalendarEventStatus { + Confirmed, + Cancelled, + Tentative, +} + +impl JSCalendarDateTime { + pub fn new(timestamp: i64, is_local: bool) -> Self { + Self { + timestamp, + is_local, + } + } + + pub fn is_start_of_day(&self) -> bool { + self.timestamp.rem_euclid(86400) == 0 + } + + pub fn to_rfc3339(&self) -> String { + let dt = DateTime::from_timestamp(self.timestamp); + if !self.is_local { + dt.to_rfc3339() + } else { + format!( + "{:04}-{:02}-{:02}T{:02}:{:02}:{:02}", + dt.year, dt.month, dt.day, dt.hour, dt.minute, dt.second, + ) + } + } +} + +impl ICalendarComponentType { + pub fn to_jscalendar_type(&self) -> Option { + match &self { + ICalendarComponentType::VCalendar => Some(JSCalendarType::Group), + ICalendarComponentType::VEvent => Some(JSCalendarType::Event), + ICalendarComponentType::VTodo => Some(JSCalendarType::Task), + ICalendarComponentType::VAlarm => Some(JSCalendarType::Alert), + ICalendarComponentType::Participant => Some(JSCalendarType::Participant), + ICalendarComponentType::VLocation => Some(JSCalendarType::Location), + ICalendarComponentType::Standard + | ICalendarComponentType::Daylight + | ICalendarComponentType::VAvailability + | ICalendarComponentType::Available + | ICalendarComponentType::VResource + | ICalendarComponentType::VStatus + | ICalendarComponentType::VJournal + | ICalendarComponentType::VFreebusy + | ICalendarComponentType::VTimezone + | ICalendarComponentType::Other(_) => None, + } + } +} + +impl JSCalendarType { + pub fn to_icalendar_component_type(&self) -> Option { + match &self { + JSCalendarType::Group => Some(ICalendarComponentType::VCalendar), + JSCalendarType::Event => Some(ICalendarComponentType::VEvent), + JSCalendarType::Task => Some(ICalendarComponentType::VTodo), + JSCalendarType::Alert => Some(ICalendarComponentType::VAlarm), + JSCalendarType::Participant => Some(ICalendarComponentType::Participant), + JSCalendarType::Location => Some(ICalendarComponentType::VLocation), + _ => None, + } + } +} + +// 7f1e1965-ae73-4454-b088-232c90730ce2 +static JSCAL_NAMESPACE: uuid::Uuid = uuid::Uuid::from_bytes([ + 127, 30, 25, 101, 174, 115, 68, 84, 176, 136, 35, 44, 144, 115, 12, 226, +]); + +#[inline] +pub(crate) fn uuid5(text: impl AsRef<[u8]>) -> String { + uuid::Uuid::new_v5(&JSCAL_NAMESPACE, text.as_ref()) + .hyphenated() + .to_string() +} + +#[cfg(test)] +impl std::fmt::Display for JSCalendar<'_, I, B> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let s = serde_json::to_string_pretty(&self.0).map_err(|_| std::fmt::Error)?; + write!(f, "{}", s) + } +} + +impl Display for JSCalendarProperty { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str(self.to_string().as_ref()) + } +} + +#[cfg(test)] +mod tests { + use crate::{ + icalendar::{ICalendar, ICalendarComponent, ICalendarProperty}, + jscalendar::{JSCalendar, JSCalendarProperty, JSCalendarValue}, + }; + use jmap_tools::Value; + + #[derive(Debug, Default)] + struct Test { + comment: String, + test: String, + expect: String, + roundtrip: String, + line_num: usize, + } + + #[test] + fn export_version_property() { + let js = JSCalendar::::parse( + r#"{"@type": "Group", "version": "2.0", "prodId": "-//Example//EN", + "entries": [{"@type": "Event", "uid": "a", "title": "b"}]}"#, + ) + .unwrap(); + let ical = js.into_icalendar().unwrap().to_string(); + + assert!(ical.contains("VERSION:2.0"), "{ical}"); + assert!(ical.contains("PRODID:-//Example//EN"), "{ical}"); + assert!(!ical.contains("JSPROP"), "{ical}"); + + // The VERSION property restored from the iCalendar property is not duplicated + let ical = ICalendar::parse(concat!( + "BEGIN:VCALENDAR\r\n", + "VERSION:2.0\r\n", + "BEGIN:VEVENT\r\nUID:a\r\nSUMMARY:b\r\nEND:VEVENT\r\n", + "END:VCALENDAR\r\n" + )) + .unwrap() + .into_jscalendar::() + .into_icalendar() + .unwrap() + .to_string(); + + assert_eq!(ical.matches("VERSION:2.0").count(), 1, "{ical}"); + } + + #[test] + fn convert_jscalendar() { + // Read all test files in the test directory + for entry in std::fs::read_dir("resources/jscalendar").unwrap() { + let entry = entry.unwrap(); + let path = entry.path(); + let input = std::fs::read_to_string(&path).unwrap(); + let mut test = Test::default(); + let mut cur_command = ""; + let mut cur_value = &mut test.test; + + for (line_num, line) in input.lines().enumerate() { + if line.trim().is_empty() { + continue; + } + if let Some(line) = line.strip_prefix("> ") { + let (command, comment) = line.split_once(' ').unwrap_or((line, "")); + + match (command, cur_command) { + ("test", _) => { + if !test.test.is_empty() { + test.run(); + } + test = Test::default(); + cur_value = &mut test.test; + cur_command = "test"; + test.comment = comment.to_string(); + test.line_num = line_num + 1; + } + ("convert", "test") => { + cur_command = "convert"; + cur_value = &mut test.expect; + } + ("convert", "convert") => { + cur_command = "convert"; + cur_value = &mut test.roundtrip; + } + _ => { + panic!( + "Unexpected command '{}' in file '{}' at line {}", + command, + path.display(), + line_num + 1 + ); + } + } + } else { + cur_value.push_str(line); + cur_value.push('\n'); + } + } + + if !test.test.is_empty() { + test.run(); + } + } + } + + impl Test { + fn run(mut self) { + if self.expect.is_empty() { + panic!( + "Test '{}' at line {} has no expected output", + self.comment, self.line_num + ); + } + + println!("Running test '{}' at line {}", self.comment, self.line_num); + + if is_jscalendar(&self.test) { + fix_jscalendar(&mut self.test); + fix_icalendar(&mut self.expect); + let source = + sanitize_jscalendar(parse_jscalendar(&self.comment, self.line_num, &self.test)); + let expect = + sanitize_icalendar(parse_icalendar(&self.comment, self.line_num, &self.expect)); + let roundtrip = if !self.roundtrip.is_empty() { + fix_jscalendar(&mut self.roundtrip); + sanitize_jscalendar(parse_jscalendar( + &self.comment, + self.line_num, + &self.roundtrip, + )) + } else { + source.clone() + }; + + let first_convert = + sanitize_icalendar(source.into_icalendar().unwrap_or_else(|| { + panic!( + "Failed to convert JSCalendar to iCalendar: test {} on line {}: {}", + self.comment, self.line_num, self.test + ) + })); + if first_convert != expect { + let first_convert = + sanitize_icalendar(ICalendar::parse(first_convert.to_string()).unwrap()); + + if first_convert != expect { + panic!( + "JSCalendar to iCalendar conversion failed: test {} on line {}, expected: {}, got: {}", + self.comment, self.line_num, expect, first_convert + ); + } + } + let roundtrip_convert = sanitize_jscalendar(first_convert.into_jscalendar()); + if roundtrip_convert != roundtrip { + let roundtrip_convert = roundtrip_convert.to_string(); + let roundtrip = roundtrip.to_string(); + + if roundtrip_convert != roundtrip { + panic!( + "iCalendar to JSCalendar conversion failed: test {} on line {}, expected: {}, got: {}", + self.comment, self.line_num, roundtrip, roundtrip_convert + ); + } + } + } else { + fix_icalendar(&mut self.test); + fix_jscalendar(&mut self.expect); + let source = + sanitize_icalendar(parse_icalendar(&self.comment, self.line_num, &self.test)); + let expect = sanitize_jscalendar(parse_jscalendar( + &self.comment, + self.line_num, + &self.expect, + )); + let roundtrip = if !self.roundtrip.is_empty() { + fix_icalendar(&mut self.roundtrip); + sanitize_icalendar(parse_icalendar( + &self.comment, + self.line_num, + &self.roundtrip, + )) + } else { + source.clone() + }; + + let first_convert = sanitize_jscalendar(source.into_jscalendar()); + if first_convert != expect { + let first_convert = first_convert.to_string(); + let expect = expect.to_string(); + + if first_convert != expect { + panic!( + "iCalendar to JSCalendar conversion failed: test {} on line {}, expected: {}, got: {}", + self.comment, self.line_num, expect, first_convert + ); + } + } + let roundtrip_convert = + sanitize_icalendar(first_convert.into_icalendar().unwrap_or_else(|| { + panic!( + "Failed to convert JSCalendar to iCalendar: test {} on line {}: {}", + self.comment, self.line_num, self.test + ) + })); + if roundtrip_convert != roundtrip { + let roundtrip_convert = sanitize_icalendar( + ICalendar::parse(roundtrip_convert.to_string()).unwrap(), + ); + if roundtrip_convert != roundtrip { + panic!( + "JSCalendar to iCalendar conversion failed: test {} on line {}, expected: {}, got: {}", + self.comment, self.line_num, roundtrip, roundtrip_convert + ); + } + } + } + } + } + + fn is_jscalendar(s: &str) -> bool { + s.starts_with("{") || s.starts_with("\"") + } + + fn fix_icalendar(s: &mut String) { + if s.starts_with("BEGIN:") { + if !s.starts_with("BEGIN:VCALENDAR") { + let mut v = "BEGIN:VCALENDAR\nVERSION:2.0\n".to_string(); + v.push_str(s); + v.push_str("END:VCALENDAR\n"); + *s = v; + } + } else { + let mut v = "BEGIN:VCALENDAR\nVERSION:2.0\nBEGIN:VEVENT\n".to_string(); + v.push_str(s); + v.push_str("END:VEVENT\nEND:VCALENDAR\n"); + *s = v; + } + } + + fn fix_jscalendar(s: &mut String) { + let (prefix, suffix) = if !s.starts_with("{") { + ("{", "}") + } else { + ("", "") + }; + + if s.contains(r#""@type": "Group""#) { + if !prefix.is_empty() { + *s = format!("{prefix}{s}{suffix}"); + } + } else if s.contains(r#""@type": "Event""#) || s.contains(r#""@type": "Task""#) { + *s = format!("{{\"@type\": \"Group\", \"entries\": [{prefix}{s}{suffix}]}}"); + } else { + *s = format!( + "{{\"@type\": \"Group\", \"entries\": [{prefix}\"@type\": \"Event\", {s}{suffix}]}}" + ); + } + } + + fn parse_icalendar(test_name: &str, line_num: usize, s: &str) -> ICalendar { + ICalendar::parse(s).unwrap_or_else(|_| { + panic!( + "Failed to parse iCalendar: {} on line {}, test {}", + s, line_num, test_name + ) + }) + } + + fn parse_jscalendar<'x>( + test_name: &str, + line_num: usize, + s: &'x str, + ) -> JSCalendar<'x, String, String> { + JSCalendar::parse(s).unwrap_or_else(|_| { + panic!( + "Failed to parse JSCalendar: {} on line {}, test {}", + s, line_num, test_name + ) + }) + } + + fn sanitize_icalendar(mut icalendar: ICalendar) -> ICalendar { + for component in &mut icalendar.components { + sanitize_icalendar_component(component); + } + + icalendar + } + + fn sanitize_icalendar_component(component: &mut ICalendarComponent) { + component + .entries + .retain(|e| !matches!(e.name, ICalendarProperty::Version)); + component + .entries + .sort_unstable_by(|a, b| a.name.cmp(&b.name)); + } + + fn sanitize_jscalendar( + mut jscalendar: JSCalendar<'_, String, String>, + ) -> JSCalendar<'_, String, String> { + sort_jscalendar_properties(&mut jscalendar.0); + jscalendar + } + + fn sort_jscalendar_properties( + value: &mut Value<'_, JSCalendarProperty, JSCalendarValue>, + ) { + match value { + Value::Array(value) => { + for item in value { + sort_jscalendar_properties(item); + } + } + Value::Object(obj) => { + //obj.as_mut_vec() + // .retain(|(k, _)| !matches!(k, Key::Property(JSCalendarProperty::Type))); + obj.as_mut_vec() + .sort_unstable_by(|a, b| a.0.to_string().cmp(&b.0.to_string())); + for (_, item) in obj.as_mut_vec() { + sort_jscalendar_properties(item); + } + } + _ => {} + } + } +} diff --git a/vendor/calcard/src/jscalendar/parser.rs b/vendor/calcard/src/jscalendar/parser.rs new file mode 100644 index 00000000..1424e627 --- /dev/null +++ b/vendor/calcard/src/jscalendar/parser.rs @@ -0,0 +1,226 @@ +/* + * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC + * + * SPDX-License-Identifier: Apache-2.0 OR MIT + */ + +use crate::{ + common::{CalendarScale, IanaParse, IanaString, IdReference, LinkRelation}, + icalendar::{ + ICalendarDuration, ICalendarFrequency, ICalendarMethod, ICalendarMonth, ICalendarSkip, + ICalendarWeekday, + }, + jscalendar::{ + JSCalendar, JSCalendarAlertAction, JSCalendarDateTime, JSCalendarEventStatus, + JSCalendarFreeBusyStatus, JSCalendarId, JSCalendarLinkDisplay, JSCalendarParticipantKind, + JSCalendarParticipantRole, JSCalendarParticipationStatus, JSCalendarPrivacy, + JSCalendarProgress, JSCalendarProperty, JSCalendarRelation, JSCalendarRelativeTo, + JSCalendarScheduleAgent, JSCalendarType, JSCalendarValue, JSCalendarVirtualLocationFeature, + }, +}; +use jmap_tools::{Element, JsonPointer, JsonPointerItem, Key, Value}; +use mail_parser::DateTime; +use std::{borrow::Cow, str::FromStr}; + +impl<'x, I: JSCalendarId, B: JSCalendarId> JSCalendar<'x, I, B> { + pub fn parse(json: &'x str) -> Result { + Value::parse_json(json).map(JSCalendar) + } + + pub fn to_string_pretty(&self) -> String { + serde_json::to_string_pretty(&self.0).unwrap_or_default() + } +} + +impl Element for JSCalendarValue { + type Property = JSCalendarProperty; + + fn try_parse

(key: &Key<'_, Self::Property>, value: &str) -> Option { + if let Key::Property(prop) = key { + match prop.patch_or_prop() { + JSCalendarProperty::Type => JSCalendarType::from_str(value) + .ok() + .map(JSCalendarValue::Type), + JSCalendarProperty::Created + | JSCalendarProperty::Updated + | JSCalendarProperty::Acknowledged + | JSCalendarProperty::ScheduleUpdated + | JSCalendarProperty::When + | JSCalendarProperty::UtcStart + | JSCalendarProperty::UtcEnd => DateTime::parse_rfc3339(value).map(|dt| { + JSCalendarValue::DateTime(JSCalendarDateTime { + timestamp: dt.to_timestamp_local(), + is_local: false, + }) + }), + JSCalendarProperty::Due + | JSCalendarProperty::RecurrenceId + | JSCalendarProperty::Start + | JSCalendarProperty::Until => DateTime::parse_rfc3339(value).map(|dt| { + JSCalendarValue::DateTime(JSCalendarDateTime { + timestamp: dt.to_timestamp_local(), + is_local: true, + }) + }), + JSCalendarProperty::Duration + | JSCalendarProperty::EstimatedDuration + | JSCalendarProperty::Offset => { + ICalendarDuration::parse(value.as_bytes()).map(JSCalendarValue::Duration) + } + JSCalendarProperty::Action => JSCalendarAlertAction::from_str(value) + .ok() + .map(JSCalendarValue::AlertAction), + JSCalendarProperty::FreeBusyStatus => JSCalendarFreeBusyStatus::from_str(value) + .ok() + .map(JSCalendarValue::FreeBusyStatus), + JSCalendarProperty::Kind => JSCalendarParticipantKind::from_str(value) + .ok() + .map(JSCalendarValue::ParticipantKind), + JSCalendarProperty::ParticipationStatus => { + JSCalendarParticipationStatus::from_str(value) + .ok() + .map(JSCalendarValue::ParticipationStatus) + } + JSCalendarProperty::Privacy => JSCalendarPrivacy::from_str(value) + .ok() + .map(JSCalendarValue::Privacy), + JSCalendarProperty::Progress => JSCalendarProgress::from_str(value) + .ok() + .map(JSCalendarValue::Progress), + JSCalendarProperty::RelativeTo => JSCalendarRelativeTo::from_str(value) + .ok() + .map(JSCalendarValue::RelativeTo), + JSCalendarProperty::ScheduleAgent => JSCalendarScheduleAgent::from_str(value) + .ok() + .map(JSCalendarValue::ScheduleAgent), + JSCalendarProperty::Status => JSCalendarEventStatus::from_str(value) + .ok() + .map(JSCalendarValue::EventStatus), + JSCalendarProperty::Rel => { + LinkRelation::parse(value.as_bytes()).map(JSCalendarValue::LinkRelation) + } + JSCalendarProperty::Frequency => { + ICalendarFrequency::parse(value.as_bytes()).map(JSCalendarValue::Frequency) + } + JSCalendarProperty::FirstDayOfWeek | JSCalendarProperty::Day => { + ICalendarWeekday::parse(value.as_bytes()).map(JSCalendarValue::Weekday) + } + JSCalendarProperty::Skip => { + ICalendarSkip::parse(value.as_bytes()).map(JSCalendarValue::Skip) + } + JSCalendarProperty::Rscale => { + CalendarScale::parse(value.as_bytes()).map(JSCalendarValue::CalendarScale) + } + JSCalendarProperty::ByMonth => { + ICalendarMonth::parse(value.as_bytes()).map(JSCalendarValue::Month) + } + JSCalendarProperty::Method => { + ICalendarMethod::parse(value.as_bytes()).map(JSCalendarValue::Method) + } + JSCalendarProperty::Id | JSCalendarProperty::BaseEventId => { + match IdReference::parse(value) { + IdReference::Value(value) => JSCalendarValue::Id(value).into(), + IdReference::Reference(value) => JSCalendarValue::IdReference(value).into(), + IdReference::Error => None, + } + } + JSCalendarProperty::BlobId => match IdReference::parse(value) { + IdReference::Value(value) => JSCalendarValue::BlobId(value).into(), + IdReference::Reference(value) => JSCalendarValue::IdReference(value).into(), + IdReference::Error => None, + }, + _ => None, + } + } else { + None + } + } + + fn to_cow(&self) -> Cow<'static, str> { + match self { + JSCalendarValue::Type(v) => v.as_str().into(), + JSCalendarValue::DateTime(v) => v.to_rfc3339().into(), + JSCalendarValue::Duration(v) => v.to_string().into(), + JSCalendarValue::AlertAction(v) => v.as_str().into(), + JSCalendarValue::FreeBusyStatus(v) => v.as_str().into(), + JSCalendarValue::ParticipantKind(v) => v.as_str().into(), + JSCalendarValue::ParticipationStatus(v) => v.as_str().into(), + JSCalendarValue::Privacy(v) => v.as_str().into(), + JSCalendarValue::Progress(v) => v.as_str().into(), + JSCalendarValue::RelativeTo(v) => v.as_str().into(), + JSCalendarValue::ScheduleAgent(v) => v.as_str().into(), + JSCalendarValue::EventStatus(v) => v.as_str().into(), + JSCalendarValue::LinkRelation(v) => v.as_str().into(), + JSCalendarValue::Frequency(v) => v.as_js_str().into(), + JSCalendarValue::CalendarScale(v) => v.as_js_str().into(), + JSCalendarValue::Skip(v) => v.as_js_str().into(), + JSCalendarValue::Weekday(v) => v.as_js_str().into(), + JSCalendarValue::Month(v) => v.to_string().into(), + JSCalendarValue::Method(v) => v.as_js_str().into(), + JSCalendarValue::Id(v) => v.to_string().into(), + JSCalendarValue::BlobId(v) => v.to_string().into(), + JSCalendarValue::IdReference(s) => format!("#{}", s).into(), + } + } +} + +impl jmap_tools::Property for JSCalendarProperty { + fn try_parse(key: Option<&Key<'_, Self>>, value: &str) -> Option { + match key { + Some(Key::Property(key)) => match key.patch_or_prop() { + JSCalendarProperty::RecurrenceOverrides => { + DateTime::parse_rfc3339(value).map(|dt| { + JSCalendarProperty::DateTime(JSCalendarDateTime { + timestamp: dt.to_timestamp_local(), + is_local: true, + }) + }) + } + JSCalendarProperty::Display => JSCalendarLinkDisplay::from_str(value) + .ok() + .map(JSCalendarProperty::LinkDisplay), + JSCalendarProperty::Features => JSCalendarVirtualLocationFeature::from_str(value) + .ok() + .map(JSCalendarProperty::VirtualLocationFeature), + JSCalendarProperty::Roles => JSCalendarParticipantRole::from_str(value) + .ok() + .map(JSCalendarProperty::ParticipantRole), + JSCalendarProperty::Relation => JSCalendarRelation::from_str(value) + .ok() + .map(JSCalendarProperty::RelationValue), + JSCalendarProperty::ConvertedProperties => { + JSCalendarProperty::Pointer(JsonPointer::parse(value)).into() + } + JSCalendarProperty::DateTime(_) if value.contains('/') => { + JSCalendarProperty::Pointer(JsonPointer::parse(value)).into() + } + JSCalendarProperty::CalendarIds => match IdReference::parse(value) { + IdReference::Value(value) => JSCalendarProperty::IdValue(value).into(), + IdReference::Reference(value) => JSCalendarProperty::IdReference(value).into(), + IdReference::Error => None, + }, + _ => JSCalendarProperty::from_str(value).ok(), + }, + None if value.contains('/') => { + JSCalendarProperty::Pointer(JsonPointer::parse(value)).into() + } + _ => JSCalendarProperty::from_str(value).ok(), + } + } + + fn to_cow(&self) -> Cow<'static, str> { + self.to_string() + } +} + +impl JSCalendarProperty { + fn patch_or_prop(&self) -> &JSCalendarProperty { + if let JSCalendarProperty::Pointer(ptr) = self + && let Some(JsonPointerItem::Key(Key::Property(prop))) = ptr.last() + { + prop + } else { + self + } + } +} diff --git a/vendor/calcard/src/jscalendar/types.rs b/vendor/calcard/src/jscalendar/types.rs new file mode 100644 index 00000000..53ef217d --- /dev/null +++ b/vendor/calcard/src/jscalendar/types.rs @@ -0,0 +1,756 @@ +/* + * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC + * + * SPDX-License-Identifier: Apache-2.0 OR MIT + */ + +use crate::jscalendar::*; +use jmap_tools::{JsonPointer, JsonPointerItem, Key}; +use std::str::FromStr; + +impl FromStr for JSCalendarProperty { + type Err = (); + + fn from_str(s: &str) -> Result { + hashify::tiny_map!(s.as_bytes(), + "@type" => JSCalendarProperty::Type, + "acknowledged" => JSCalendarProperty::Acknowledged, + "action" => JSCalendarProperty::Action, + "alerts" => JSCalendarProperty::Alerts, + "baseEventId" => JSCalendarProperty::BaseEventId, + "byDay" => JSCalendarProperty::ByDay, + "byHour" => JSCalendarProperty::ByHour, + "byMinute" => JSCalendarProperty::ByMinute, + "byMonth" => JSCalendarProperty::ByMonth, + "byMonthDay" => JSCalendarProperty::ByMonthDay, + "bySecond" => JSCalendarProperty::BySecond, + "bySetPosition" => JSCalendarProperty::BySetPosition, + "byWeekNo" => JSCalendarProperty::ByWeekNo, + "byYearDay" => JSCalendarProperty::ByYearDay, + "calendarAddress" => JSCalendarProperty::CalendarAddress, + "calendarIds" => JSCalendarProperty::CalendarIds, + "categories" => JSCalendarProperty::Categories, + "color" => JSCalendarProperty::Color, + "contentType" => JSCalendarProperty::ContentType, + "coordinates" => JSCalendarProperty::Coordinates, + "count" => JSCalendarProperty::Count, + "created" => JSCalendarProperty::Created, + "day" => JSCalendarProperty::Day, + "delegatedFrom" => JSCalendarProperty::DelegatedFrom, + "delegatedTo" => JSCalendarProperty::DelegatedTo, + "description" => JSCalendarProperty::Description, + "descriptionContentType" => JSCalendarProperty::DescriptionContentType, + "display" => JSCalendarProperty::Display, + "due" => JSCalendarProperty::Due, + "duration" => JSCalendarProperty::Duration, + "email" => JSCalendarProperty::Email, + "entries" => JSCalendarProperty::Entries, + "estimatedDuration" => JSCalendarProperty::EstimatedDuration, + "excluded" => JSCalendarProperty::Excluded, + "expectReply" => JSCalendarProperty::ExpectReply, + "features" => JSCalendarProperty::Features, + "firstDayOfWeek" => JSCalendarProperty::FirstDayOfWeek, + "freeBusyStatus" => JSCalendarProperty::FreeBusyStatus, + "frequency" => JSCalendarProperty::Frequency, + "hideAttendees" => JSCalendarProperty::HideAttendees, + "href" => JSCalendarProperty::Href, + "id" => JSCalendarProperty::Id, + "interval" => JSCalendarProperty::Interval, + "invitedBy" => JSCalendarProperty::InvitedBy, + "isDraft" => JSCalendarProperty::IsDraft, + "isOrigin" => JSCalendarProperty::IsOrigin, + "keywords" => JSCalendarProperty::Keywords, + "kind" => JSCalendarProperty::Kind, + "links" => JSCalendarProperty::Links, + "locale" => JSCalendarProperty::Locale, + "locations" => JSCalendarProperty::Locations, + "locationTypes" => JSCalendarProperty::LocationTypes, + "mayInviteOthers" => JSCalendarProperty::MayInviteOthers, + "mayInviteSelf" => JSCalendarProperty::MayInviteSelf, + "memberOf" => JSCalendarProperty::MemberOf, + "method" => JSCalendarProperty::Method, + "name" => JSCalendarProperty::Name, + "nthOfPeriod" => JSCalendarProperty::NthOfPeriod, + "offset" => JSCalendarProperty::Offset, + "participants" => JSCalendarProperty::Participants, + "participationComment" => JSCalendarProperty::ParticipationComment, + "participationStatus" => JSCalendarProperty::ParticipationStatus, + "percentComplete" => JSCalendarProperty::PercentComplete, + "priority" => JSCalendarProperty::Priority, + "privacy" => JSCalendarProperty::Privacy, + "prodId" => JSCalendarProperty::ProdId, + "progress" => JSCalendarProperty::Progress, + "recurrenceId" => JSCalendarProperty::RecurrenceId, + "recurrenceIdTimeZone" => JSCalendarProperty::RecurrenceIdTimeZone, + "recurrenceOverrides" => JSCalendarProperty::RecurrenceOverrides, + "rel" => JSCalendarProperty::Rel, + "relatedTo" => JSCalendarProperty::RelatedTo, + "relation" => JSCalendarProperty::Relation, + "relativeTo" => JSCalendarProperty::RelativeTo, + "replyTo" => JSCalendarProperty::ReplyTo, + "requestStatus" => JSCalendarProperty::RequestStatus, + "roles" => JSCalendarProperty::Roles, + "rscale" => JSCalendarProperty::Rscale, + "sentBy" => JSCalendarProperty::SentBy, + "scheduleAgent" => JSCalendarProperty::ScheduleAgent, + "scheduleForceSend" => JSCalendarProperty::ScheduleForceSend, + "scheduleSequence" => JSCalendarProperty::ScheduleSequence, + "scheduleStatus" => JSCalendarProperty::ScheduleStatus, + "scheduleUpdated" => JSCalendarProperty::ScheduleUpdated, + "sendTo" => JSCalendarProperty::SendTo, + "sequence" => JSCalendarProperty::Sequence, + "showWithoutTime" => JSCalendarProperty::ShowWithoutTime, + "size" => JSCalendarProperty::Size, + "skip" => JSCalendarProperty::Skip, + "source" => JSCalendarProperty::Source, + "start" => JSCalendarProperty::Start, + "status" => JSCalendarProperty::Status, + "timeZone" => JSCalendarProperty::TimeZone, + "title" => JSCalendarProperty::Title, + "trigger" => JSCalendarProperty::Trigger, + "uid" => JSCalendarProperty::Uid, + "until" => JSCalendarProperty::Until, + "updated" => JSCalendarProperty::Updated, + "uri" => JSCalendarProperty::Uri, + "useDefaultAlerts" => JSCalendarProperty::UseDefaultAlerts, + "utcEnd" => JSCalendarProperty::UtcEnd, + "utcStart" => JSCalendarProperty::UtcStart, + "version" => JSCalendarProperty::Version, + "virtualLocations" => JSCalendarProperty::VirtualLocations, + "when" => JSCalendarProperty::When, + "endTimeZone" => JSCalendarProperty::EndTimeZone, + "mainLocationId" => JSCalendarProperty::MainLocationId, + "organizerCalendarAddress" => JSCalendarProperty::OrganizerCalendarAddress, + "recurrenceRule" => JSCalendarProperty::RecurrenceRule, + "properties" => JSCalendarProperty::Properties, + "components" => JSCalendarProperty::Components, + "valueType" => JSCalendarProperty::ValueType, + "convertedProperties" => JSCalendarProperty::ConvertedProperties, + "parameters" => JSCalendarProperty::Parameters, + "iCalendar" => JSCalendarProperty::ICalendar, + "blobId" => JSCalendarProperty::BlobId, + ) + .ok_or(()) + } +} + +impl JSCalendarProperty { + pub fn to_string(&self) -> Cow<'static, str> { + match self { + JSCalendarProperty::Type => "@type", + JSCalendarProperty::Acknowledged => "acknowledged", + JSCalendarProperty::Action => "action", + JSCalendarProperty::Alerts => "alerts", + JSCalendarProperty::BaseEventId => "baseEventId", + JSCalendarProperty::ByDay => "byDay", + JSCalendarProperty::ByHour => "byHour", + JSCalendarProperty::ByMinute => "byMinute", + JSCalendarProperty::ByMonth => "byMonth", + JSCalendarProperty::ByMonthDay => "byMonthDay", + JSCalendarProperty::BySecond => "bySecond", + JSCalendarProperty::BySetPosition => "bySetPosition", + JSCalendarProperty::ByWeekNo => "byWeekNo", + JSCalendarProperty::ByYearDay => "byYearDay", + JSCalendarProperty::CalendarAddress => "calendarAddress", + JSCalendarProperty::CalendarIds => "calendarIds", + JSCalendarProperty::Categories => "categories", + JSCalendarProperty::Color => "color", + JSCalendarProperty::ContentType => "contentType", + JSCalendarProperty::Coordinates => "coordinates", + JSCalendarProperty::Count => "count", + JSCalendarProperty::Created => "created", + JSCalendarProperty::Day => "day", + JSCalendarProperty::DelegatedFrom => "delegatedFrom", + JSCalendarProperty::DelegatedTo => "delegatedTo", + JSCalendarProperty::Description => "description", + JSCalendarProperty::DescriptionContentType => "descriptionContentType", + JSCalendarProperty::Display => "display", + JSCalendarProperty::Due => "due", + JSCalendarProperty::Duration => "duration", + JSCalendarProperty::Email => "email", + JSCalendarProperty::Entries => "entries", + JSCalendarProperty::EstimatedDuration => "estimatedDuration", + JSCalendarProperty::Excluded => "excluded", + JSCalendarProperty::ExpectReply => "expectReply", + JSCalendarProperty::Features => "features", + JSCalendarProperty::FirstDayOfWeek => "firstDayOfWeek", + JSCalendarProperty::FreeBusyStatus => "freeBusyStatus", + JSCalendarProperty::Frequency => "frequency", + JSCalendarProperty::HideAttendees => "hideAttendees", + JSCalendarProperty::Href => "href", + JSCalendarProperty::Id => "id", + JSCalendarProperty::Interval => "interval", + JSCalendarProperty::InvitedBy => "invitedBy", + JSCalendarProperty::IsDraft => "isDraft", + JSCalendarProperty::IsOrigin => "isOrigin", + JSCalendarProperty::Keywords => "keywords", + JSCalendarProperty::Kind => "kind", + JSCalendarProperty::Links => "links", + JSCalendarProperty::Locale => "locale", + JSCalendarProperty::Locations => "locations", + JSCalendarProperty::LocationTypes => "locationTypes", + JSCalendarProperty::MayInviteOthers => "mayInviteOthers", + JSCalendarProperty::MayInviteSelf => "mayInviteSelf", + JSCalendarProperty::MemberOf => "memberOf", + JSCalendarProperty::Method => "method", + JSCalendarProperty::Name => "name", + JSCalendarProperty::NthOfPeriod => "nthOfPeriod", + JSCalendarProperty::Offset => "offset", + JSCalendarProperty::Participants => "participants", + JSCalendarProperty::ParticipationComment => "participationComment", + JSCalendarProperty::ParticipationStatus => "participationStatus", + JSCalendarProperty::PercentComplete => "percentComplete", + JSCalendarProperty::Priority => "priority", + JSCalendarProperty::Privacy => "privacy", + JSCalendarProperty::ProdId => "prodId", + JSCalendarProperty::Progress => "progress", + JSCalendarProperty::RecurrenceId => "recurrenceId", + JSCalendarProperty::RecurrenceIdTimeZone => "recurrenceIdTimeZone", + JSCalendarProperty::RecurrenceOverrides => "recurrenceOverrides", + JSCalendarProperty::Rel => "rel", + JSCalendarProperty::RelatedTo => "relatedTo", + JSCalendarProperty::Relation => "relation", + JSCalendarProperty::RelativeTo => "relativeTo", + JSCalendarProperty::ReplyTo => "replyTo", + JSCalendarProperty::RequestStatus => "requestStatus", + JSCalendarProperty::Roles => "roles", + JSCalendarProperty::Rscale => "rscale", + JSCalendarProperty::SentBy => "sentBy", + JSCalendarProperty::ScheduleAgent => "scheduleAgent", + JSCalendarProperty::ScheduleForceSend => "scheduleForceSend", + JSCalendarProperty::ScheduleSequence => "scheduleSequence", + JSCalendarProperty::ScheduleStatus => "scheduleStatus", + JSCalendarProperty::ScheduleUpdated => "scheduleUpdated", + JSCalendarProperty::SendTo => "sendTo", + JSCalendarProperty::Sequence => "sequence", + JSCalendarProperty::ShowWithoutTime => "showWithoutTime", + JSCalendarProperty::Size => "size", + JSCalendarProperty::Skip => "skip", + JSCalendarProperty::Source => "source", + JSCalendarProperty::Start => "start", + JSCalendarProperty::Status => "status", + JSCalendarProperty::TimeZone => "timeZone", + JSCalendarProperty::Title => "title", + JSCalendarProperty::Trigger => "trigger", + JSCalendarProperty::Uid => "uid", + JSCalendarProperty::Until => "until", + JSCalendarProperty::Updated => "updated", + JSCalendarProperty::Uri => "uri", + JSCalendarProperty::UseDefaultAlerts => "useDefaultAlerts", + JSCalendarProperty::UtcEnd => "utcEnd", + JSCalendarProperty::UtcStart => "utcStart", + JSCalendarProperty::Version => "version", + JSCalendarProperty::VirtualLocations => "virtualLocations", + JSCalendarProperty::When => "when", + JSCalendarProperty::EndTimeZone => "endTimeZone", + JSCalendarProperty::MainLocationId => "mainLocationId", + JSCalendarProperty::OrganizerCalendarAddress => "organizerCalendarAddress", + JSCalendarProperty::RecurrenceRule => "recurrenceRule", + JSCalendarProperty::Properties => "properties", + JSCalendarProperty::Components => "components", + JSCalendarProperty::ValueType => "valueType", + JSCalendarProperty::ConvertedProperties => "convertedProperties", + JSCalendarProperty::Parameters => "parameters", + JSCalendarProperty::ICalendar => "iCalendar", + JSCalendarProperty::BlobId => "blobId", + JSCalendarProperty::LinkDisplay(v) => v.as_str(), + JSCalendarProperty::VirtualLocationFeature(v) => v.as_str(), + JSCalendarProperty::ParticipantRole(v) => v.as_str(), + JSCalendarProperty::RelationValue(v) => v.as_str(), + JSCalendarProperty::LinkRelation(v) => return v.as_str().to_string().into(), + JSCalendarProperty::DateTime(dt) => return dt.to_rfc3339().into(), + JSCalendarProperty::Pointer(pointer) => return Cow::Owned(pointer.to_string()), + JSCalendarProperty::IdValue(id) => return id.to_string().into(), + JSCalendarProperty::IdReference(s) => return format!("#{}", s).into(), + } + .into() + } + + pub(crate) fn is_forbidden_override_patch(&self) -> bool { + matches!( + self, + JSCalendarProperty::Type + | JSCalendarProperty::Method + | JSCalendarProperty::OrganizerCalendarAddress + | JSCalendarProperty::Privacy + | JSCalendarProperty::ProdId + | JSCalendarProperty::RecurrenceId + | JSCalendarProperty::RecurrenceIdTimeZone + | JSCalendarProperty::SentBy + | JSCalendarProperty::Uid + | JSCalendarProperty::RecurrenceOverrides + | JSCalendarProperty::RecurrenceRule + ) + } + + pub(crate) fn is_forbidden_override_pointer(pointer: &JsonPointer) -> bool { + let mut items = pointer + .iter() + .filter(|item| !matches!(item, JsonPointerItem::Root)); + + match (items.next(), items.next(), items.next(), items.next()) { + ( + Some(JsonPointerItem::Key(Key::Property( + JSCalendarProperty::RecurrenceOverrides | JSCalendarProperty::RecurrenceRule, + ))), + .., + ) => true, + (Some(JsonPointerItem::Key(Key::Property(property))), None, ..) => { + property.is_forbidden_override_patch() + } + ( + Some(JsonPointerItem::Key(Key::Property(JSCalendarProperty::Participants))), + Some(_), + Some(JsonPointerItem::Key(Key::Property(JSCalendarProperty::CalendarAddress))), + None, + ) => true, + _ => false, + } + } +} + +impl FromStr for JSCalendarType { + type Err = (); + fn from_str(s: &str) -> Result { + hashify::tiny_map!(s.as_bytes(), + "Event" => JSCalendarType::Event, + "Task" => JSCalendarType::Task, + "Group" => JSCalendarType::Group, + "Alert" => JSCalendarType::Alert, + "Boolean" => JSCalendarType::Boolean, + "Duration" => JSCalendarType::Duration, + "Id" => JSCalendarType::Id, + "Int" => JSCalendarType::Int, + "LocalDateTime" => JSCalendarType::LocalDateTime, + "Link" => JSCalendarType::Link, + "Location" => JSCalendarType::Location, + "NDay" => JSCalendarType::NDay, + "Number" => JSCalendarType::Number, + "Participant" => JSCalendarType::Participant, + "PatchObject" => JSCalendarType::PatchObject, + "RecurrenceRule" => JSCalendarType::RecurrenceRule, + "Relation" => JSCalendarType::Relation, + "SignedDuration" => JSCalendarType::SignedDuration, + "String" => JSCalendarType::String, + "TimeZone" => JSCalendarType::TimeZone, + "TimeZoneId" => JSCalendarType::TimeZoneId, + "TimeZoneRule" => JSCalendarType::TimeZoneRule, + "UnsignedInt" => JSCalendarType::UnsignedInt, + "UTCDateTime" => JSCalendarType::UTCDateTime, + "VirtualLocation" => JSCalendarType::VirtualLocation, + "ICalComponent" => JSCalendarType::ICalComponent, + "AbsoluteTrigger" => JSCalendarType::AbsoluteTrigger, + "OffsetTrigger" => JSCalendarType::OffsetTrigger, + ) + .ok_or(()) + } +} + +impl JSCalendarType { + pub fn as_str(&self) -> &'static str { + match self { + JSCalendarType::Event => "Event", + JSCalendarType::Task => "Task", + JSCalendarType::Group => "Group", + JSCalendarType::Alert => "Alert", + JSCalendarType::Boolean => "Boolean", + JSCalendarType::Duration => "Duration", + JSCalendarType::Id => "Id", + JSCalendarType::Int => "Int", + JSCalendarType::LocalDateTime => "LocalDateTime", + JSCalendarType::Link => "Link", + JSCalendarType::Location => "Location", + JSCalendarType::NDay => "NDay", + JSCalendarType::Number => "Number", + JSCalendarType::Participant => "Participant", + JSCalendarType::PatchObject => "PatchObject", + JSCalendarType::RecurrenceRule => "RecurrenceRule", + JSCalendarType::Relation => "Relation", + JSCalendarType::SignedDuration => "SignedDuration", + JSCalendarType::String => "String", + JSCalendarType::TimeZone => "TimeZone", + JSCalendarType::TimeZoneId => "TimeZoneId", + JSCalendarType::TimeZoneRule => "TimeZoneRule", + JSCalendarType::UnsignedInt => "UnsignedInt", + JSCalendarType::UTCDateTime => "UTCDateTime", + JSCalendarType::VirtualLocation => "VirtualLocation", + JSCalendarType::ICalComponent => "ICalComponent", + JSCalendarType::OffsetTrigger => "OffsetTrigger", + JSCalendarType::AbsoluteTrigger => "AbsoluteTrigger", + } + } +} + +impl FromStr for JSCalendarAlertAction { + type Err = (); + + fn from_str(s: &str) -> Result { + hashify::tiny_map!(s.as_bytes(), + "display" => JSCalendarAlertAction::Display, + "email" => JSCalendarAlertAction::Email + ) + .ok_or(()) + } +} + +impl JSCalendarAlertAction { + pub fn as_str(&self) -> &'static str { + match self { + JSCalendarAlertAction::Display => "display", + JSCalendarAlertAction::Email => "email", + } + } +} + +impl FromStr for JSCalendarLinkDisplay { + type Err = (); + + fn from_str(s: &str) -> Result { + hashify::tiny_map!(s.as_bytes(), + "badge" => JSCalendarLinkDisplay::Badge, + "graphic" => JSCalendarLinkDisplay::Graphic, + "fullsize" => JSCalendarLinkDisplay::Fullsize, + "thumbnail" => JSCalendarLinkDisplay::Thumbnail + ) + .ok_or(()) + } +} + +impl JSCalendarLinkDisplay { + pub fn as_str(&self) -> &'static str { + match self { + JSCalendarLinkDisplay::Badge => "badge", + JSCalendarLinkDisplay::Graphic => "graphic", + JSCalendarLinkDisplay::Fullsize => "fullsize", + JSCalendarLinkDisplay::Thumbnail => "thumbnail", + } + } +} + +impl FromStr for JSCalendarVirtualLocationFeature { + type Err = (); + + fn from_str(s: &str) -> Result { + hashify::tiny_map!(s.as_bytes(), + "audio" => JSCalendarVirtualLocationFeature::Audio, + "chat" => JSCalendarVirtualLocationFeature::Chat, + "feed" => JSCalendarVirtualLocationFeature::Feed, + "moderator" => JSCalendarVirtualLocationFeature::Moderator, + "phone" => JSCalendarVirtualLocationFeature::Phone, + "screen" => JSCalendarVirtualLocationFeature::Screen, + "video" => JSCalendarVirtualLocationFeature::Video + ) + .ok_or(()) + } +} + +impl JSCalendarVirtualLocationFeature { + pub fn as_str(&self) -> &'static str { + match self { + JSCalendarVirtualLocationFeature::Audio => "audio", + JSCalendarVirtualLocationFeature::Chat => "chat", + JSCalendarVirtualLocationFeature::Feed => "feed", + JSCalendarVirtualLocationFeature::Moderator => "moderator", + JSCalendarVirtualLocationFeature::Phone => "phone", + JSCalendarVirtualLocationFeature::Screen => "screen", + JSCalendarVirtualLocationFeature::Video => "video", + } + } +} + +impl FromStr for JSCalendarFreeBusyStatus { + type Err = (); + + fn from_str(s: &str) -> Result { + hashify::tiny_map!(s.as_bytes(), + "free" => JSCalendarFreeBusyStatus::Free, + "busy" => JSCalendarFreeBusyStatus::Busy + ) + .ok_or(()) + } +} + +impl JSCalendarFreeBusyStatus { + pub fn as_str(&self) -> &'static str { + match self { + JSCalendarFreeBusyStatus::Free => "free", + JSCalendarFreeBusyStatus::Busy => "busy", + } + } +} + +impl FromStr for JSCalendarParticipantKind { + type Err = (); + + fn from_str(s: &str) -> Result { + hashify::tiny_map!(s.as_bytes(), + "individual" => JSCalendarParticipantKind::Individual, + "group" => JSCalendarParticipantKind::Group, + "resource" => JSCalendarParticipantKind::Resource, + "location" => JSCalendarParticipantKind::Location + ) + .ok_or(()) + } +} + +impl JSCalendarParticipantKind { + pub fn as_str(&self) -> &'static str { + match self { + JSCalendarParticipantKind::Individual => "individual", + JSCalendarParticipantKind::Group => "group", + JSCalendarParticipantKind::Resource => "resource", + JSCalendarParticipantKind::Location => "location", + } + } +} + +impl FromStr for JSCalendarParticipationStatus { + type Err = (); + + fn from_str(s: &str) -> Result { + hashify::tiny_map!(s.as_bytes(), + "needs-action" => JSCalendarParticipationStatus::NeedsAction, + "accepted" => JSCalendarParticipationStatus::Accepted, + "declined" => JSCalendarParticipationStatus::Declined, + "tentative" => JSCalendarParticipationStatus::Tentative, + "delegated" => JSCalendarParticipationStatus::Delegated + ) + .ok_or(()) + } +} + +impl JSCalendarParticipationStatus { + pub fn as_str(&self) -> &'static str { + match self { + JSCalendarParticipationStatus::NeedsAction => "needs-action", + JSCalendarParticipationStatus::Accepted => "accepted", + JSCalendarParticipationStatus::Declined => "declined", + JSCalendarParticipationStatus::Tentative => "tentative", + JSCalendarParticipationStatus::Delegated => "delegated", + } + } +} + +impl FromStr for JSCalendarPrivacy { + type Err = (); + + fn from_str(s: &str) -> Result { + hashify::tiny_map!(s.as_bytes(), + "public" => JSCalendarPrivacy::Public, + "private" => JSCalendarPrivacy::Private, + "secret" => JSCalendarPrivacy::Secret + ) + .ok_or(()) + } +} + +impl JSCalendarPrivacy { + pub fn as_str(&self) -> &'static str { + match self { + JSCalendarPrivacy::Public => "public", + JSCalendarPrivacy::Private => "private", + JSCalendarPrivacy::Secret => "secret", + } + } +} + +impl FromStr for JSCalendarProgress { + type Err = (); + + fn from_str(s: &str) -> Result { + hashify::tiny_map!(s.as_bytes(), + "needs-action" => JSCalendarProgress::NeedsAction, + "in-process" => JSCalendarProgress::InProcess, + "completed" => JSCalendarProgress::Completed, + "failed" => JSCalendarProgress::Failed, + "cancelled" => JSCalendarProgress::Cancelled, + ) + .ok_or(()) + } +} + +impl JSCalendarProgress { + pub fn as_str(&self) -> &'static str { + match self { + JSCalendarProgress::NeedsAction => "needs-action", + JSCalendarProgress::InProcess => "in-process", + JSCalendarProgress::Completed => "completed", + JSCalendarProgress::Failed => "failed", + JSCalendarProgress::Cancelled => "cancelled", + } + } +} + +impl FromStr for JSCalendarRelation { + type Err = (); + + fn from_str(s: &str) -> Result { + hashify::tiny_map!(s.as_bytes(), + "first" => JSCalendarRelation::First, + "next" => JSCalendarRelation::Next, + "child" => JSCalendarRelation::Child, + "parent" => JSCalendarRelation::Parent, + "snooze" => JSCalendarRelation::Snooze, + ) + .ok_or(()) + } +} + +impl JSCalendarRelation { + pub fn as_str(&self) -> &'static str { + match self { + JSCalendarRelation::First => "first", + JSCalendarRelation::Next => "next", + JSCalendarRelation::Child => "child", + JSCalendarRelation::Parent => "parent", + JSCalendarRelation::Snooze => "snooze", + } + } +} + +impl FromStr for JSCalendarRelativeTo { + type Err = (); + + fn from_str(s: &str) -> Result { + hashify::tiny_map!(s.as_bytes(), + "start" => JSCalendarRelativeTo::Start, + "end" => JSCalendarRelativeTo::End + ) + .ok_or(()) + } +} + +impl JSCalendarRelativeTo { + pub fn as_str(&self) -> &'static str { + match self { + JSCalendarRelativeTo::Start => "start", + JSCalendarRelativeTo::End => "end", + } + } +} + +impl FromStr for JSCalendarParticipantRole { + type Err = (); + + fn from_str(s: &str) -> Result { + hashify::tiny_map!(s.as_bytes(), + "owner" => JSCalendarParticipantRole::Owner, + "optional" => JSCalendarParticipantRole::Optional, + "informational" => JSCalendarParticipantRole::Informational, + "chair" => JSCalendarParticipantRole::Chair, + "required" => JSCalendarParticipantRole::Required, + ) + .ok_or(()) + } +} + +impl JSCalendarParticipantRole { + pub fn as_str(&self) -> &'static str { + match self { + JSCalendarParticipantRole::Owner => "owner", + JSCalendarParticipantRole::Optional => "optional", + JSCalendarParticipantRole::Informational => "informational", + JSCalendarParticipantRole::Chair => "chair", + JSCalendarParticipantRole::Required => "required", + } + } +} + +impl FromStr for JSCalendarScheduleAgent { + type Err = (); + + fn from_str(s: &str) -> Result { + hashify::tiny_map!(s.as_bytes(), + "server" => JSCalendarScheduleAgent::Server, + "client" => JSCalendarScheduleAgent::Client, + "none" => JSCalendarScheduleAgent::None + ) + .ok_or(()) + } +} + +impl JSCalendarScheduleAgent { + pub fn as_str(&self) -> &'static str { + match self { + JSCalendarScheduleAgent::Server => "server", + JSCalendarScheduleAgent::Client => "client", + JSCalendarScheduleAgent::None => "none", + } + } +} + +impl FromStr for JSCalendarEventStatus { + type Err = (); + + fn from_str(s: &str) -> Result { + hashify::tiny_map!(s.as_bytes(), + "confirmed" => JSCalendarEventStatus::Confirmed, + "cancelled" => JSCalendarEventStatus::Cancelled, + "tentative" => JSCalendarEventStatus::Tentative + ) + .ok_or(()) + } +} + +impl JSCalendarEventStatus { + pub fn as_str(&self) -> &'static str { + match self { + JSCalendarEventStatus::Confirmed => "confirmed", + JSCalendarEventStatus::Cancelled => "cancelled", + JSCalendarEventStatus::Tentative => "tentative", + } + } +} + +impl ICalendarFrequency { + pub fn as_js_str(&self) -> &'static str { + match self { + ICalendarFrequency::Secondly => "secondly", + ICalendarFrequency::Minutely => "minutely", + ICalendarFrequency::Hourly => "hourly", + ICalendarFrequency::Daily => "daily", + ICalendarFrequency::Weekly => "weekly", + ICalendarFrequency::Monthly => "monthly", + ICalendarFrequency::Yearly => "yearly", + } + } +} + +impl ICalendarSkip { + pub fn as_js_str(&self) -> &'static str { + match self { + ICalendarSkip::Omit => "omit", + ICalendarSkip::Backward => "backward", + ICalendarSkip::Forward => "forward", + } + } +} + +impl ICalendarWeekday { + pub fn as_js_str(&self) -> &'static str { + match self { + ICalendarWeekday::Sunday => "su", + ICalendarWeekday::Monday => "mo", + ICalendarWeekday::Tuesday => "tu", + ICalendarWeekday::Wednesday => "we", + ICalendarWeekday::Thursday => "th", + ICalendarWeekday::Friday => "fr", + ICalendarWeekday::Saturday => "sa", + } + } +} + +impl ICalendarMethod { + pub fn as_js_str(&self) -> &'static str { + match self { + ICalendarMethod::Publish => "publish", + ICalendarMethod::Request => "request", + ICalendarMethod::Reply => "reply", + ICalendarMethod::Add => "add", + ICalendarMethod::Cancel => "cancel", + ICalendarMethod::Refresh => "refresh", + ICalendarMethod::Counter => "counter", + ICalendarMethod::Declinecounter => "declinecounter", + } + } +} diff --git a/vendor/calcard/src/jscontact/export/convert.rs b/vendor/calcard/src/jscontact/export/convert.rs new file mode 100644 index 00000000..bb319796 --- /dev/null +++ b/vendor/calcard/src/jscontact/export/convert.rs @@ -0,0 +1,1959 @@ +/* + * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC + * + * SPDX-License-Identifier: Apache-2.0 OR MIT + */ + +use crate::{ + common::{Data, IanaParse, IanaType, timezone::Tz}, + jscontact::{ + JSContact, JSContactId, JSContactKind, JSContactProperty, JSContactValue, + export::{ + State, + props::{ + build_path, convert_anniversary, convert_types, convert_value, find_text_param, + map_kind, + }, + }, + }, + vcard::{ + Jscomp, VCard, VCardEntry, VCardLevel, VCardParameter, VCardParameterValue, VCardPhonetic, + VCardProperty, VCardType, VCardValue, VCardValueType, ValueType, + }, +}; +use jmap_tools::{JsonPointer, JsonPointerItem, Key, Map, Value}; +use std::{collections::HashMap, str::FromStr}; + +impl<'x, I, B> JSContact<'x, I, B> +where + I: JSContactId, + B: JSContactId, +{ + pub fn into_vcard(self) -> Option { + let mut state = State::::default(); + let mut properties = self.0.into_object()?.into_vec(); + let mut localized_properties: HashMap> = HashMap::new(); + + for (property, value) in &mut properties { + match (property, value) { + (Key::Property(JSContactProperty::Localizations), Value::Object(obj)) => { + for (lang, value) in std::mem::take(obj.as_mut_vec()) { + let mut localizations: Value< + '_, + JSContactProperty, + JSContactValue, + > = Value::Object(Map::from(Vec::with_capacity(3))); + for (key, value) in value.into_expanded_object() { + let ptr = match key { + Key::Property(JSContactProperty::Pointer(ptr)) => ptr, + _ => JsonPointer::parse(key.to_string().as_ref()), + }; + + let ptr_path = ptr.to_string(); + if let Some(value) = build_path( + &mut localizations, + ptr.into_inner().into_iter().peekable(), + value, + ) { + state.insert_jsprop( + &[ + JSContactProperty::Localizations::.to_string().as_ref(), + lang.to_string().as_ref(), + ptr_path.as_str(), + ], + value, + ); + } + } + if let Some(localizations) = + localizations.into_object().filter(|x| !x.is_empty()) + { + localized_properties + .insert(lang.into_string(), localizations.into_vec()); + } + } + } + (Key::Property(JSContactProperty::VCard), Value::Object(obj)) => { + for (sub_property, value) in std::mem::take(obj.as_mut_vec()) { + match sub_property { + Key::Property(JSContactProperty::ConvertedProperties) => { + for (key, value) in value.into_expanded_object() { + let ptr = match key { + Key::Property(JSContactProperty::Pointer(ptr)) => ptr, + _ => JsonPointer::parse(key.to_string().as_ref()), + }; + + let mut keys = Vec::with_capacity(2); + for item in ptr.into_iter() { + match item { + JsonPointerItem::Key(key) => { + let key = match &key { + Key::Borrowed(v) if v.contains('/') => v, + Key::Owned(v) if v.contains('/') => v.as_str(), + _ => { + keys.push(key); + continue; + } + }; + for item in JsonPointer::parse(key).into_iter() { + keys.push(match item { + JsonPointerItem::Key(k) => k, + JsonPointerItem::Number(n) => { + Key::Owned(n.to_string()) + } + JsonPointerItem::Root + | JsonPointerItem::Wildcard => continue, + }); + } + } + JsonPointerItem::Number(v) => { + keys.push(Key::Owned(v.to_string())); + } + JsonPointerItem::Root | JsonPointerItem::Wildcard => (), + } + } + + state.converted_props.push((keys, value)); + } + } + Key::Property(JSContactProperty::Properties) => { + if let Some(value) = value.into_array() { + state.import_properties(value); + } + } + _ => { + state.insert_jsprop( + &[ + JSContactProperty::VCard::.to_string().as_ref(), + sub_property.to_string().as_ref(), + ], + value, + ); + } + } + } + + if !state.converted_props.is_empty() { + state.converted_props.sort_unstable_by(|a, b| a.0.cmp(&b.0)); + } + } + _ => {} + } + } + + // Resolve organizational mappings + let mut org_mappings: HashMap = HashMap::new(); + for (keys, value) in &state.converted_props { + match (keys.first(), keys.get(2)) { + ( + Some(Key::Property(JSContactProperty::Titles)), + Some(Key::Property(JSContactProperty::Name)), + ) => { + if let Some(group) = find_text_param(value, "group") { + org_mappings.entry(group.into_owned()).or_default().0 = + keys[1].to_string().into_owned(); + } + } + (Some(Key::Property(JSContactProperty::Organizations)), _) => { + if let Some(group) = find_text_param(value, "group") { + org_mappings.entry(group.into_owned()).or_default().1 = true; + } + } + _ => {} + } + } + + // Localization maps + #[cfg(test)] + let mut has_fn = crate::jscontact::fn_derivation_disabled(); + #[cfg(not(test))] + let mut has_fn = false; + + let has_localizations = !localized_properties.is_empty(); + let mut name_pos_map: [Option; 20] = [None; 20]; + let mut adr_pos_map: HashMap; 20]> = HashMap::new(); + let mut places_map: HashMap)> = + HashMap::new(); + + for (properties, language) in [(properties, None)].into_iter().chain( + localized_properties + .into_iter() + .map(|(lang, properties)| (properties, Some(lang))), + ) { + let is_localized = language.is_some(); + state.language = language; + + for (property, value) in properties { + let Key::Property(property) = property else { + state.insert_jsprop(&[property.to_string().as_ref()], value); + continue; + }; + + match property { + JSContactProperty::Uid + | JSContactProperty::Kind + | JSContactProperty::Language + | JSContactProperty::ProdId + | JSContactProperty::Created + | JSContactProperty::Updated => { + match convert_value(value, &ValueType::Vcard(VCardValueType::Text)) { + Ok(value) => { + let vcard_property = match property { + JSContactProperty::Uid => VCardProperty::Uid, + JSContactProperty::Kind => VCardProperty::Kind, + JSContactProperty::Language => VCardProperty::Language, + JSContactProperty::ProdId => VCardProperty::Prodid, + JSContactProperty::Created => VCardProperty::Created, + JSContactProperty::Updated => VCardProperty::Rev, + _ => unreachable!(), + }; + state.insert_vcard( + &[property], + VCardEntry::new(vcard_property).with_value(value), + ); + } + Err(value) => { + state.insert_jsprop(&[property.to_string().as_ref()], value); + } + } + } + JSContactProperty::Directories => { + for (name, value) in value.into_expanded_object() { + let mut entry = VCardEntry::new(VCardProperty::OrgDirectory); + + for (sub_property, value) in value.into_expanded_object() { + match sub_property { + Key::Property(JSContactProperty::Kind) => { + if matches!( + value, + Value::Element(JSContactValue::Kind( + JSContactKind::Entry + )) + ) { + entry.name = VCardProperty::Source; + } + } + Key::Property(JSContactProperty::Uri) => { + if let Some(text) = value.into_string() { + entry.values.push(VCardValue::Text(text.into_owned())); + } + } + Key::Property(JSContactProperty::Pref) => { + if let Some(pref) = value.as_u64() { + entry.params.push(VCardParameter::pref(pref as u32)); + } + } + Key::Property(JSContactProperty::ListAs) => { + if let Some(index) = value.as_u64() { + entry.params.push(VCardParameter::index(index as u32)); + } + } + Key::Property(JSContactProperty::MediaType) => { + if let Some(text) = value.into_string() { + entry + .params + .push(VCardParameter::mediatype(text.into_owned())); + } + } + Key::Property(JSContactProperty::Label) => { + if let Some(text) = value.into_string() { + entry.values.push(VCardValue::Text(text.into_owned())); + } + } + Key::Property(JSContactProperty::Contexts) => { + if let Some(types) = convert_types(value, true) { + for typ in types { + entry.params.push(VCardParameter::typ(typ)); + } + } + } + _ => { + state.insert_jsprop( + &[ + property.to_string().as_ref(), + name.to_string().as_ref(), + sub_property.to_string().as_ref(), + ], + value, + ); + } + } + } + + state.insert_vcard( + &[JSContactProperty::Directories], + entry.with_param(VCardParameter::prop_id(name.into_string())), + ); + } + } + JSContactProperty::Anniversaries => { + for (name, value) in value.into_expanded_object() { + if let Some((vcard_property, mut vcard_place_property)) = map_kind( + &value, + [ + ( + JSContactKind::Birth, + (VCardProperty::Bday, Some(VCardProperty::Birthplace)), + ), + ( + JSContactKind::Death, + (VCardProperty::Deathdate, Some(VCardProperty::Deathplace)), + ), + (JSContactKind::Wedding, (VCardProperty::Anniversary, None)), + ], + ) + .or_else(|| { + if is_localized { + places_map.get(name.to_string().as_ref()).cloned() + } else { + None + } + }) { + let mut date = None; + let mut place = None; + let mut calscale = None; + + for (sub_property, value) in value.into_expanded_object() { + match sub_property { + Key::Property(JSContactProperty::Kind) => {} + Key::Property(JSContactProperty::Date) => { + if let Ok((date_, calscale_)) = + convert_anniversary(value) + { + date = Some(date_); + calscale = calscale_; + } + } + Key::Property(JSContactProperty::Place) + if vcard_place_property.is_some() + && value.is_object_and_contains_any_key(&[ + Key::Property(JSContactProperty::Full), + Key::Property(JSContactProperty::Coordinates), + ]) => + { + if has_localizations && !is_localized { + places_map + .entry(name.to_string().into_owned()) + .or_insert_with(|| { + ( + vcard_property.clone(), + vcard_place_property.clone(), + ) + }); + } + + for (place_property, value) in + value.into_expanded_object() + { + match place_property { + Key::Property(JSContactProperty::Full) + if vcard_place_property.is_some() => + { + if let Some(text) = value.into_string() { + place = Some( + VCardEntry::new( + vcard_place_property + .take() + .unwrap(), + ) + .with_value(text.into_owned()), + ); + } + } + Key::Property( + JSContactProperty::Coordinates, + ) if vcard_place_property.is_some() => { + if let Some(text) = value.into_string() { + place = Some( + VCardEntry::new( + vcard_place_property + .take() + .unwrap(), + ) + .with_value(text.into_owned()) + .with_param(VCardParameter::value( + VCardValueType::Uri, + )), + ); + } + } + _ => { + state.insert_jsprop( + &[ + property.to_string().as_ref(), + name.to_string().as_ref(), + sub_property.to_string().as_ref(), + place_property.to_string().as_ref(), + ], + value, + ); + } + } + } + } + _ => { + state.insert_jsprop( + &[ + JSContactProperty::Anniversaries:: + .to_string() + .as_ref(), + name.to_string().as_ref(), + sub_property.to_string().as_ref(), + ], + value, + ); + } + } + } + + // Add place if it exists + if let Some(place) = place { + state.insert_vcard( + &[ + JSContactProperty::Anniversaries, + JSContactProperty::Place, + ], + place.with_param(VCardParameter::prop_id( + name.to_string().into_owned(), + )), + ); + } + + // Add date + if let Some(date) = date { + let mut entry = VCardEntry::new(vcard_property); + if let Some(calscale) = calscale { + entry.params.push(VCardParameter::calscale(calscale)); + } + state.insert_vcard( + &[ + JSContactProperty::Anniversaries, + JSContactProperty::Date, + ], + entry + .with_param(VCardParameter::prop_id(name.into_string())) + .with_value(VCardValue::PartialDateTime(date)), + ); + } + } else { + state.insert_jsprop( + &[property.to_string().as_ref(), name.to_string().as_ref()], + value, + ); + } + } + } + JSContactProperty::Name => { + let mut params = Vec::new(); + let mut parts: Vec>> = vec![None; 7]; + let mut jscomps = vec![Jscomp::Separator(String::new())]; + let mut num_parts = 0; + + for (sub_property, value) in value.into_expanded_object() { + match sub_property { + Key::Property(JSContactProperty::Components) => { + for (index, components) in value + .into_array() + .unwrap_or_default() + .into_iter() + .enumerate() + { + let mut comp_value = None; + let mut comp_phonetic = None; + let mut comp_pos = None; + let mut is_separator = false; + + for (component, value) in components.into_expanded_object() + { + match (component, value) { + ( + Key::Property(JSContactProperty::Kind), + Value::Element(JSContactValue::Kind( + JSContactKind::Separator, + )), + ) => { + is_separator = true; + } + ( + Key::Property(JSContactProperty::Kind), + Value::Element(JSContactValue::Kind(kind)), + ) => { + comp_pos = kind.to_vcard_n_pos(); + } + ( + Key::Property(JSContactProperty::Value), + value, + ) => { + comp_value = value.into_string(); + } + ( + Key::Property(JSContactProperty::Phonetic), + value, + ) if is_localized => { + comp_phonetic = value.into_string(); + } + (component, value) => { + state.insert_jsprop( + &[ + JSContactProperty::Name:: + .to_string() + .as_ref(), + JSContactProperty::Components:: + .to_string() + .as_ref(), + index.to_string().as_str(), + component.to_string().as_ref(), + ], + value, + ); + } + } + } + + if has_localizations + && !is_localized + && let Some(value) = name_pos_map.get_mut(index) + { + *value = comp_pos; + } + + if let Some(comp_value) = comp_value.or(comp_phonetic) { + if let Some(comp_pos) = comp_pos.or_else(|| { + if is_localized && !is_separator { + name_pos_map + .get(index) + .copied() + .unwrap_or_default() + } else { + None + } + }) { + let part = &mut parts[comp_pos]; + if let Some(part) = part { + let value = part.len() as u32; + part.push(comp_value.into_owned()); + jscomps.push(Jscomp::Entry { + position: comp_pos as u32, + value, + }); + } else { + *part = Some(vec![comp_value.into_owned()]); + jscomps.push(Jscomp::Entry { + position: comp_pos as u32, + value: 0, + }); + } + num_parts = num_parts.max(comp_pos + 1); + } else if is_separator { + jscomps.push(Jscomp::Separator( + comp_value.into_owned(), + )); + } + } + } + } + Key::Property(JSContactProperty::Full) => { + if let Some(text) = value.into_string() { + has_fn = true; + state.insert_vcard( + &[JSContactProperty::Name, JSContactProperty::Full], + VCardEntry::new(VCardProperty::Fn) + .with_value(text.into_owned()), + ); + } + } + Key::Property(JSContactProperty::SortAs) => { + let mut surname = None; + let mut given = None; + + for (key, value) in value.into_expanded_object() { + match key { + Key::Property(JSContactProperty::SortAsKind( + JSContactKind::Surname, + )) => { + surname = value.into_string(); + } + Key::Property(JSContactProperty::SortAsKind( + JSContactKind::Given, + )) => { + given = value.into_string(); + } + _ => { + state.insert_jsprop( + &[ + JSContactProperty::Name:: + .to_string() + .as_ref(), + JSContactProperty::SortAs:: + .to_string() + .as_ref(), + key.to_string().as_ref(), + ], + value, + ); + } + } + } + + if let (Some(surname), Some(given)) = + (surname.as_ref(), given.as_ref()) + { + params.push(VCardParameter::sort_as(format!( + "{surname},{given}" + ))); + } else if let Some(surname) = surname.or(given) { + params.push(VCardParameter::sort_as(surname.into_owned())); + } + } + Key::Property(JSContactProperty::PhoneticSystem) => { + if let Ok(phonetic_system) = + IanaType::::try_from(value) + { + params.push(VCardParameter::phonetic(phonetic_system)); + } + } + Key::Property(JSContactProperty::PhoneticScript) => { + if let Some(phonetic_script) = value.into_string() { + params.push(VCardParameter::script( + phonetic_script.into_owned(), + )); + } + } + Key::Property(JSContactProperty::DefaultSeparator) => { + if let Some(separator) = value.into_string() { + jscomps[0] = Jscomp::Separator(separator.into_owned()); + } + } + Key::Property(JSContactProperty::IsOrdered) => {} + _ => { + state.insert_jsprop( + &[ + property.to_string().as_ref(), + sub_property.to_string().as_ref(), + ], + value, + ); + } + } + } + + if !has_fn { + let default_separator = match jscomps.first() { + Some(Jscomp::Separator(sep)) if !sep.is_empty() => sep.as_str(), + _ => " ", + }; + let mut full = String::new(); + let mut prev_value = false; + for jscomp in jscomps.iter().skip(1) { + match jscomp { + Jscomp::Entry { position, value } => { + if let Some(component) = parts + .get(*position as usize) + .and_then(|part| part.as_ref()) + .and_then(|part| part.get(*value as usize)) + { + if prev_value { + full.push_str(default_separator); + } + full.push_str(component); + prev_value = true; + } + } + Jscomp::Separator(sep) => { + full.push_str(sep); + prev_value = false; + } + } + } + + if !full.is_empty() { + state.insert_vcard( + &[JSContactProperty::Name, JSContactProperty::Full], + VCardEntry::new(VCardProperty::Fn) + .with_param(VCardParameter::derived(true)) + .with_value(full), + ); + has_fn = true; + } + } + + if num_parts > 0 || !params.is_empty() { + if jscomps.len() > 1 { + params.push(VCardParameter::jscomps(jscomps)); + } + + // To vCard: add any "surname2" NameComponent to the Family name component, after all "surname" values. + // To vCard: add any "generation" NameComponent to the Honorific suffix component. + for (from_pos, to_pos) in [(5, 0), (6, 4)] { + if let Some(v) = &parts[from_pos] { + let v = v.clone(); + if let Some(to) = &mut parts[to_pos] { + to.extend(v); + } else { + parts[to_pos] = Some(v); + } + } + } + + state.insert_vcard( + &[JSContactProperty::Name, JSContactProperty::Components], + VCardEntry::new(VCardProperty::N) + .with_params(params) + .with_values( + parts + .into_iter() + .map(|v| { + if let Some(v) = v { + if v.len() > 1 { + VCardValue::Component(v) + } else { + VCardValue::Text( + v.into_iter().next().unwrap(), + ) + } + } else { + VCardValue::Text(Default::default()) + } + }) + .collect(), + ), + ); + } + } + JSContactProperty::SpeakToAs => { + for (sub_property, value) in value.into_expanded_object() { + match sub_property { + Key::Property(JSContactProperty::GrammaticalGender) => { + if let Ok(gender) = convert_value(value, &ValueType::GramGender) + { + state.insert_vcard( + &[ + JSContactProperty::SpeakToAs, + JSContactProperty::GrammaticalGender, + ], + VCardEntry::new(VCardProperty::Gramgender) + .with_value(gender), + ); + } + } + Key::Property(JSContactProperty::Pronouns) => { + for (name, value) in value.into_expanded_object() { + let mut entry = VCardEntry::new(VCardProperty::Pronouns); + + for (pronoun_property, value) in + value.into_expanded_object() + { + match pronoun_property { + Key::Property(JSContactProperty::Pronouns) => { + if let Some(value) = value + .into_owned_string() + .map(VCardValue::Text) + { + entry.values = vec![value]; + } + } + Key::Property(JSContactProperty::Pref) => { + if let Some(pref) = value.as_i64() { + entry.add_param(VCardParameter::pref( + pref as u32, + )); + } + } + _ => { + state.insert_jsprop( + &[ + property.to_string().as_ref(), + sub_property.to_string().as_ref(), + name.to_string().as_ref(), + pronoun_property.to_string().as_ref(), + ], + value, + ); + } + } + } + + state.insert_vcard( + &[ + JSContactProperty::SpeakToAs, + JSContactProperty::Pronouns, + ], + entry.with_param(VCardParameter::prop_id( + name.into_string(), + )), + ); + } + } + _ => { + state.insert_jsprop( + &[ + property.to_string().as_ref(), + sub_property.to_string().as_ref(), + ], + value, + ); + } + } + } + } + JSContactProperty::Nicknames => { + for (name, value) in value.into_expanded_object() { + let mut entry = VCardEntry::new(VCardProperty::Nickname); + + for (sub_property, value) in value.into_expanded_object() { + match sub_property { + Key::Property(JSContactProperty::Name) => { + if let Some(name) = + value.into_owned_string().map(VCardValue::Text) + { + entry.values = vec![name]; + } + } + Key::Property(JSContactProperty::Pref) => { + if let Some(pref) = value.as_i64() { + entry.add_param(VCardParameter::pref(pref as u32)); + } + } + Key::Property(JSContactProperty::Contexts) => { + if let Some(types) = convert_types(value, true) { + for typ in types { + entry.params.push(VCardParameter::typ(typ)); + } + } + } + _ => { + state.insert_jsprop( + &[ + property.to_string().as_ref(), + name.to_string().as_ref(), + sub_property.to_string().as_ref(), + ], + value, + ); + } + } + } + + state.insert_vcard( + &[JSContactProperty::Nicknames], + entry.with_param(VCardParameter::prop_id(name.into_string())), + ); + } + } + JSContactProperty::Media => { + for (name, value) in value.into_expanded_object() { + if let Some(entry_type) = map_kind( + &value, + [ + (JSContactKind::Photo, VCardProperty::Photo), + (JSContactKind::Logo, VCardProperty::Logo), + (JSContactKind::Sound, VCardProperty::Sound), + ], + ) { + let mut entry = VCardEntry::new(entry_type); + + for (sub_property, value) in value.into_expanded_object() { + match sub_property { + Key::Property(JSContactProperty::Uri) => { + if let Some(uri) = value.into_string() { + let uri = match Data::try_parse(uri.as_bytes()) { + Some(data) => VCardValue::Binary(data), + None => VCardValue::Text(uri.into_owned()), + }; + + entry.values = vec![uri]; + } + } + Key::Property(JSContactProperty::MediaType) => { + if let Some(text) = value.into_string() { + entry.params.push(VCardParameter::mediatype( + text.into_owned(), + )); + } + } + Key::Property(JSContactProperty::Label) => { + if let Some(text) = value.into_string() { + entry + .params + .push(VCardParameter::label(text.into_owned())); + } + } + Key::Property(JSContactProperty::Pref) => { + if let Some(pref) = value.as_i64() { + entry.add_param(VCardParameter::pref(pref as u32)); + } + } + Key::Property(JSContactProperty::Kind) => {} + _ => { + state.insert_jsprop( + &[ + property.to_string().as_ref(), + name.to_string().as_ref(), + sub_property.to_string().as_ref(), + ], + value, + ); + } + } + } + + state.insert_vcard( + &[JSContactProperty::Media], + entry.with_param(VCardParameter::prop_id(name.into_string())), + ); + } else { + state.insert_jsprop( + &[property.to_string().as_ref(), name.to_string().as_ref()], + value, + ); + } + } + } + JSContactProperty::Addresses => { + for (name, value) in value.into_expanded_object() { + let mut params = vec![]; + let mut parts: Vec>> = vec![None; 18]; + let mut jscomps = vec![Jscomp::Separator(String::new())]; + let mut num_parts = 0; + let mut timezone = None; + let mut geo = None; + + for (sub_property, value) in value.into_expanded_object() { + match sub_property { + Key::Property(JSContactProperty::Components) => { + for (index, components) in value + .into_array() + .unwrap_or_default() + .into_iter() + .enumerate() + { + let mut comp_value = None; + let mut comp_phonetic = None; + let mut comp_pos = None; + let mut is_separator = false; + + for (key, value) in components.into_expanded_object() { + match (key, value) { + ( + Key::Property(JSContactProperty::Kind), + Value::Element(JSContactValue::Kind( + JSContactKind::Separator, + )), + ) => { + is_separator = true; + } + ( + Key::Property(JSContactProperty::Kind), + Value::Element(JSContactValue::Kind(kind)), + ) => { + comp_pos = kind.to_vcard_adr_pos(); + } + ( + Key::Property(JSContactProperty::Value), + value, + ) => { + comp_value = value.into_string(); + } + ( + Key::Property(JSContactProperty::Phonetic), + value, + ) if is_localized => { + comp_phonetic = value.into_string(); + } + (key, value) => { + state.insert_jsprop( + &[ + property.to_string().as_ref(), + name.to_string().as_ref(), + sub_property.to_string().as_ref(), + index.to_string().as_str(), + key.to_string().as_ref(), + ], + value, + ); + } + } + } + + if has_localizations + && !is_localized + && let Some(value) = adr_pos_map + .entry(name.to_string().into_owned()) + .or_default() + .get_mut(index) + { + *value = comp_pos; + } + + if let Some(comp_value) = comp_value.or(comp_phonetic) { + if let Some(comp_pos) = comp_pos.or_else(|| { + if is_localized && !is_separator { + adr_pos_map + .get(name.to_string().as_ref()) + .and_then(|map| map.get(index).copied()) + .unwrap_or_default() + } else { + None + } + }) { + let part = &mut parts[comp_pos]; + if let Some(part) = part { + let value = part.len() as u32; + part.push(comp_value.into_owned()); + jscomps.push(Jscomp::Entry { + position: comp_pos as u32, + value, + }); + } else { + *part = Some(vec![comp_value.into_owned()]); + jscomps.push(Jscomp::Entry { + position: comp_pos as u32, + value: 0, + }); + } + num_parts = num_parts.max(comp_pos + 1); + } else if is_separator { + jscomps.push(Jscomp::Separator( + comp_value.into_owned(), + )); + } + } + } + } + Key::Property(JSContactProperty::Full) => { + if let Some(value) = value.into_string() { + params.push(VCardParameter::label(value.into_owned())); + } + } + Key::Property(JSContactProperty::Coordinates) => { + geo = value.into_string(); + } + Key::Property(JSContactProperty::TimeZone) => { + timezone = value.into_string(); + } + Key::Property(JSContactProperty::CountryCode) => { + if let Some(value) = value.into_string() { + params.push(VCardParameter::cc(value.into_owned())); + } + } + Key::Property(JSContactProperty::Pref) => { + if let Some(value) = value.as_i64() { + params.push(VCardParameter::pref(value as u32)); + } + } + Key::Property(JSContactProperty::PhoneticSystem) => { + if let Ok(phonetic_system) = + IanaType::::try_from(value) + { + params.push(VCardParameter::phonetic(phonetic_system)); + } + } + Key::Property(JSContactProperty::PhoneticScript) => { + if let Some(value) = value.into_string() { + params.push(VCardParameter::script(value.into_owned())); + } + } + Key::Property(JSContactProperty::Contexts) => { + if let Some(types) = convert_types(value, true) { + for typ in types { + params.push(VCardParameter::typ(typ)); + } + } + } + Key::Property(JSContactProperty::DefaultSeparator) => { + if let Some(value) = value.into_string() { + jscomps[0] = Jscomp::Separator(value.into_owned()); + } + } + Key::Property(JSContactProperty::IsOrdered) => {} + _ => { + state.insert_jsprop( + &[ + property.to_string().as_ref(), + name.to_string().as_ref(), + sub_property.to_string().as_ref(), + ], + value, + ); + } + } + } + + if let Some(timezone) = timezone { + if state.converted_props.iter().any(|(prop, _)| { + prop.len() == 3 + && prop[0] == Key::Property(JSContactProperty::Addresses) + && prop[1] == name + && prop[2] == Key::Property(JSContactProperty::TimeZone) + }) { + state.insert_vcard( + &[ + JSContactProperty::Addresses, + JSContactProperty::TimeZone, + ], + VCardEntry::new(VCardProperty::Tz).with_value( + Tz::from_str(&timezone) + .map(|tz| { + VCardValue::PartialDateTime(tz.offset_parts()) + }) + .unwrap_or_else(|_| { + VCardValue::Text(timezone.into_owned()) + }), + ), + ); + } else { + params.push(VCardParameter::tz(timezone.into_owned())); + } + } + + if let Some(geo) = geo { + if state.converted_props.iter().any(|(prop, _)| { + prop.len() == 3 + && prop[0] == Key::Property(JSContactProperty::Addresses) + && prop[1] == name + && prop[2] == Key::Property(JSContactProperty::Coordinates) + }) { + state.insert_vcard( + &[ + JSContactProperty::Addresses, + JSContactProperty::Coordinates, + ], + VCardEntry::new(VCardProperty::Geo) + .with_value(geo.into_owned()), + ); + } else { + params.push(VCardParameter::geo(geo.into_owned())); + } + } + + if num_parts > 0 || !params.is_empty() { + params.push(VCardParameter::prop_id(name.to_string().to_string())); + + if jscomps.len() > 1 { + params.push(VCardParameter::jscomps(jscomps)); + } + + /* + + apartment To vCard: set the values of the following components: + room, floor, apartment, building + + name To vCard: set the values of the following components: + number, name, block, direction, landmark, subdistrict, district + + */ + + for (comp_target, source) in [ + (1, [7, 8, 9, 12].as_slice()), + (2, [10, 11, 13, 17, 16, 14, 15].as_slice()), + ] { + let mut target = vec![]; + + for comp_pos in source { + if let Some(v) = &parts[*comp_pos] { + target.extend(v.iter().cloned()); + } + } + + if !target.is_empty() { + parts[comp_target] = Some(target); + } + } + + state.insert_vcard( + &[JSContactProperty::Addresses, JSContactProperty::Components], + VCardEntry::new(VCardProperty::Adr) + .with_params(params) + .with_values( + parts + .into_iter() + .map(|v| { + if let Some(v) = v { + if v.len() > 1 { + VCardValue::Component(v) + } else { + VCardValue::Text( + v.into_iter().next().unwrap(), + ) + } + } else { + VCardValue::Text(Default::default()) + } + }) + .collect(), + ), + ); + } + } + } + JSContactProperty::Organizations => { + for (name, value) in value.into_expanded_object() { + let mut entry = VCardEntry::new(VCardProperty::Org); + + for (sub_property, value) in value.into_expanded_object() { + match sub_property { + Key::Property(JSContactProperty::Name) => { + if let Some(value) = + value.into_owned_string().map(VCardValue::Text) + { + if entry.values.is_empty() { + entry.values = vec![value]; + } else { + entry.values.insert(0, value); + } + } + } + Key::Property(JSContactProperty::Units) => { + for (index, item) in value + .into_array() + .unwrap_or_default() + .into_iter() + .enumerate() + { + for (key, value) in item.into_expanded_object() { + match key { + Key::Property(JSContactProperty::Name) => { + if let Some(value) = value + .into_owned_string() + .map(VCardValue::Text) + { + entry.values.push(value); + } + } + _ => { + state.insert_jsprop( + &[ + property.to_string().as_ref(), + name.to_string().as_ref(), + sub_property.to_string().as_ref(), + index.to_string().as_ref(), + key.to_string().as_ref(), + ], + value, + ); + } + } + } + } + } + Key::Property(JSContactProperty::SortAs) => { + if let Some(sort_as) = value.into_string() { + entry.add_param(VCardParameter::sort_as( + sort_as.into_owned(), + )); + } + } + Key::Property(JSContactProperty::Contexts) => { + if let Some(types) = convert_types(value, true) { + for typ in types { + entry.params.push(VCardParameter::typ(typ)); + } + } + } + _ => { + state.insert_jsprop( + &[ + property.to_string().as_ref(), + name.to_string().as_ref(), + sub_property.to_string().as_ref(), + ], + value, + ); + } + } + } + + state.insert_vcard( + &[JSContactProperty::Organizations], + entry.with_param(VCardParameter::prop_id(name.into_string())), + ); + } + } + JSContactProperty::Members => { + let mut has_members = false; + for name in value.into_expanded_boolean_set() { + has_members = true; + state.insert_vcard( + &[JSContactProperty::Members], + VCardEntry::new(VCardProperty::Member) + .with_value(name.into_string()), + ); + } + + if !has_members { + state.insert_jsprop( + &[property.to_string().as_ref()], + Value::Object(Map::from(Vec::new())), + ); + } + } + JSContactProperty::Keywords => { + let mut keywords = Vec::new(); + for name in value.into_expanded_boolean_set() { + keywords.push(VCardValue::Text(name.into_string())); + } + + state.insert_vcard( + &[JSContactProperty::Keywords], + VCardEntry::new(VCardProperty::Categories).with_values(keywords), + ); + } + JSContactProperty::Titles => { + for (name, value) in value.into_expanded_object() { + let mut entry = VCardEntry::new( + map_kind( + &value, + [ + (JSContactKind::Title, VCardProperty::Title), + (JSContactKind::Role, VCardProperty::Role), + ], + ) + .unwrap_or(VCardProperty::Title), + ); + + for (sub_property, value) in value.into_expanded_object() { + match sub_property { + Key::Property(JSContactProperty::Name) => { + if let Some(value) = + value.into_owned_string().map(VCardValue::Text) + { + entry.values = vec![value]; + } + } + Key::Property(JSContactProperty::Pref) => { + if let Some(pref) = value.as_i64() { + entry.add_param(VCardParameter::pref(pref as u32)); + } + } + Key::Property(JSContactProperty::Contexts) => { + if let Some(types) = convert_types(value, true) { + for typ in types { + entry.params.push(VCardParameter::typ(typ)); + } + } + } + Key::Property(JSContactProperty::OrganizationId) + if org_mappings.values().any(|(id, has_mapping)| { + *has_mapping && id == name.to_string().as_ref() + }) => {} + Key::Property(JSContactProperty::Kind) => {} + _ => { + state.insert_jsprop( + &[ + property.to_string().as_ref(), + name.to_string().as_ref(), + sub_property.to_string().as_ref(), + ], + value, + ); + } + } + } + + state.insert_vcard( + &[JSContactProperty::Titles], + entry.with_param(VCardParameter::prop_id(name.into_string())), + ); + } + } + JSContactProperty::Emails => { + for (name, value) in value.into_expanded_object() { + let mut entry = VCardEntry::new(VCardProperty::Email); + + for (sub_property, value) in value.into_expanded_object() { + match sub_property { + Key::Property(JSContactProperty::Address) => { + if let Some(email) = value.into_string() { + entry.values = + vec![VCardValue::Text(email.into_owned())]; + } + } + Key::Property(JSContactProperty::Contexts) => { + if let Some(types) = convert_types(value, true) { + for typ in types { + entry.params.push(VCardParameter::typ(typ)); + } + } + } + Key::Property(JSContactProperty::Pref) => { + if let Some(pref) = value.as_i64() { + entry.add_param(VCardParameter::pref(pref as u32)); + } + } + Key::Property(JSContactProperty::Label) => { + if let Some(label) = value.into_string() { + entry + .params + .push(VCardParameter::label(label.into_owned())); + } + } + _ => { + state.insert_jsprop( + &[ + property.to_string().as_ref(), + name.to_string().as_ref(), + sub_property.to_string().as_ref(), + ], + value, + ); + } + } + } + + state.insert_vcard( + &[JSContactProperty::Emails], + entry.with_param(VCardParameter::prop_id(name.into_string())), + ); + } + } + JSContactProperty::OnlineServices => { + for (name, value) in value.into_expanded_object() { + let mut entry = VCardEntry::new(VCardProperty::Socialprofile); + + for (sub_property, value) in value.into_expanded_object() { + match sub_property { + Key::Property(JSContactProperty::Uri) => { + if let Some(service) = value.into_string() { + entry.values = + vec![VCardValue::Text(service.into_owned())]; + } + } + Key::Property(JSContactProperty::Service) => { + if let Some(service) = value.into_string() { + entry.add_param(VCardParameter::service_type( + service.into_owned(), + )); + } + } + Key::Property(JSContactProperty::User) => { + if let Some(username) = value.into_string() { + entry.add_param(VCardParameter::username( + username.into_owned(), + )); + } + } + Key::Property(JSContactProperty::Contexts) => { + if let Some(types) = convert_types(value, true) { + for typ in types { + entry.params.push(VCardParameter::typ(typ)); + } + } + } + Key::Property(JSContactProperty::Label) => { + if let Some(label) = value.into_string() { + entry.add_param(VCardParameter::label( + label.into_owned(), + )); + } + } + Key::Property(JSContactProperty::Pref) => { + if let Some(pref) = value.as_i64() { + entry.add_param(VCardParameter::pref(pref as u32)); + } + } + _ => { + state.insert_jsprop( + &[ + property.to_string().as_ref(), + name.to_string().as_ref(), + sub_property.to_string().as_ref(), + ], + value, + ); + } + } + } + + state.insert_vcard( + &[JSContactProperty::OnlineServices], + entry.with_param(VCardParameter::prop_id(name.into_string())), + ); + } + } + JSContactProperty::Phones => { + for (name, value) in value.into_expanded_object() { + let mut entry = VCardEntry::new(VCardProperty::Tel); + let mut types = vec![]; + + for (sub_property, value) in value.into_expanded_object() { + match sub_property { + Key::Property(JSContactProperty::Number) => { + if let Some(email) = value.into_string() { + entry.values = + vec![VCardValue::Text(email.into_owned())]; + } + } + Key::Property( + JSContactProperty::Contexts | JSContactProperty::Features, + ) => { + if let Some(types_) = convert_types( + value, + matches!( + sub_property, + Key::Property(JSContactProperty::Contexts) + ), + ) { + if types.is_empty() { + types = types_; + } else { + types.extend(types_); + } + } + } + Key::Property(JSContactProperty::Pref) => { + if let Some(pref) = value.as_i64() { + entry.add_param(VCardParameter::pref(pref as u32)); + } + } + Key::Property(JSContactProperty::Label) => { + if let Some(label) = value.into_string() { + entry + .params + .push(VCardParameter::label(label.into_owned())); + } + } + _ => { + state.insert_jsprop( + &[ + property.to_string().as_ref(), + name.to_string().as_ref(), + sub_property.to_string().as_ref(), + ], + value, + ); + } + } + } + + if !types.is_empty() { + for typ in types { + entry.params.push(VCardParameter::typ(typ)); + } + } + state.insert_vcard( + &[JSContactProperty::Phones], + entry.with_param(VCardParameter::prop_id(name.into_string())), + ); + } + } + JSContactProperty::PreferredLanguages => { + for (name, value) in value.into_expanded_object() { + let mut entry = VCardEntry::new(VCardProperty::Lang); + + for (sub_property, value) in value.into_expanded_object() { + match sub_property { + Key::Property(JSContactProperty::Language) => { + if let Some(lang) = value.into_string() { + entry.values = + vec![VCardValue::Text(lang.into_owned())]; + } + } + Key::Property(JSContactProperty::Pref) => { + if let Some(pref) = value.as_i64() { + entry.add_param(VCardParameter::pref(pref as u32)); + } + } + Key::Property(JSContactProperty::Contexts) => { + if let Some(types) = convert_types(value, true) { + for typ in types { + entry.params.push(VCardParameter::typ(typ)); + } + } + } + Key::Property(JSContactProperty::Label) => { + if let Some(label) = value.into_string() { + entry + .params + .push(VCardParameter::label(label.into_owned())); + } + } + _ => { + state.insert_jsprop( + &[ + property.to_string().as_ref(), + name.to_string().as_ref(), + sub_property.to_string().as_ref(), + ], + value, + ); + } + } + } + + state.insert_vcard( + &[JSContactProperty::PreferredLanguages], + entry.with_param(VCardParameter::prop_id(name.into_string())), + ); + } + } + JSContactProperty::Calendars => { + for (name, value) in value.into_expanded_object() { + if let Some(entry_type) = map_kind( + &value, + [ + (JSContactKind::FreeBusy, VCardProperty::Fburl), + (JSContactKind::Calendar, VCardProperty::Caluri), + ], + ) { + let mut entry = VCardEntry::new(entry_type); + + for (sub_property, value) in value.into_expanded_object() { + match sub_property { + Key::Property(JSContactProperty::Uri) => { + if let Some(value) = + value.into_owned_string().map(VCardValue::Text) + { + entry.values = vec![value]; + } + } + Key::Property(JSContactProperty::MediaType) => { + if let Some(text) = value.into_string() { + entry.params.push(VCardParameter::mediatype( + text.into_owned(), + )); + } + } + Key::Property(JSContactProperty::Label) => { + if let Some(text) = value.into_string() { + entry + .params + .push(VCardParameter::label(text.into_owned())); + } + } + Key::Property(JSContactProperty::Pref) => { + if let Some(pref) = value.as_i64() { + entry.add_param(VCardParameter::pref(pref as u32)); + } + } + Key::Property(JSContactProperty::Contexts) => { + if let Some(types) = convert_types(value, true) { + for typ in types { + entry.params.push(VCardParameter::typ(typ)); + } + } + } + Key::Property(JSContactProperty::Kind) => {} + _ => { + state.insert_jsprop( + &[ + property.to_string().as_ref(), + name.to_string().as_ref(), + sub_property.to_string().as_ref(), + ], + value, + ); + } + } + } + + state.insert_vcard( + &[JSContactProperty::Calendars], + entry.with_param(VCardParameter::prop_id(name.into_string())), + ); + } else { + state.insert_jsprop( + &[property.to_string().as_ref(), name.to_string().as_ref()], + value, + ); + } + } + } + JSContactProperty::SchedulingAddresses + | JSContactProperty::CryptoKeys + | JSContactProperty::Links => { + let vcard_property = match property { + JSContactProperty::SchedulingAddresses => VCardProperty::Caladruri, + JSContactProperty::CryptoKeys => VCardProperty::Key, + JSContactProperty::Links => VCardProperty::Url, + _ => unreachable!(), + }; + + for (name, value) in value.into_expanded_object() { + let mut entry = VCardEntry::new(vcard_property.clone()); + + for (sub_property, value) in value.into_expanded_object() { + match sub_property { + Key::Property(JSContactProperty::Uri) => { + if let Some(value) = + value.into_owned_string().map(VCardValue::Text) + { + entry.values = vec![value]; + } + } + Key::Property(JSContactProperty::MediaType) => { + if let Some(text) = value.into_string() { + entry + .params + .push(VCardParameter::mediatype(text.into_owned())); + } + } + Key::Property(JSContactProperty::Label) => { + if let Some(text) = value.into_string() { + entry + .params + .push(VCardParameter::label(text.into_owned())); + } + } + Key::Property(JSContactProperty::Pref) => { + if let Some(pref) = value.as_i64() { + entry.add_param(VCardParameter::pref(pref as u32)); + } + } + Key::Property(JSContactProperty::Contexts) => { + if let Some(types) = convert_types(value, true) { + for typ in types { + entry.params.push(VCardParameter::typ(typ)); + } + } + } + Key::Property(JSContactProperty::Kind) => { + if matches!( + value, + Value::Element(JSContactValue::Kind( + JSContactKind::Contact + )) + ) { + entry.name = VCardProperty::ContactUri; + } + } + _ => { + state.insert_jsprop( + &[ + property.to_string().as_ref(), + name.to_string().as_ref(), + sub_property.to_string().as_ref(), + ], + value, + ); + } + } + } + + state.insert_vcard( + std::slice::from_ref(&property), + entry.with_param(VCardParameter::prop_id(name.into_string())), + ); + } + } + JSContactProperty::Notes => { + for (name, value) in value.into_expanded_object() { + let mut entry = VCardEntry::new(VCardProperty::Note); + + for (sub_property, value) in value.into_expanded_object() { + match sub_property { + Key::Property(JSContactProperty::Note) => { + if let Some(text) = value.into_string() { + entry.values = + vec![VCardValue::Text(text.into_owned())]; + } + } + Key::Property(JSContactProperty::Created) => { + if let Value::Element(JSContactValue::Timestamp(t)) = value + { + entry.params.push(VCardParameter::created( + VCardParameterValue::Timestamp(t), + )); + } + } + Key::Property(JSContactProperty::Author) => { + for (name, value) in value.into_expanded_object() { + match name { + Key::Property(JSContactProperty::Name) => { + if let Some(name) = value.into_string() { + entry.add_param( + VCardParameter::author_name( + name.into_owned(), + ), + ); + } + } + Key::Property(JSContactProperty::Uri) => { + if let Some(uri) = value.into_string() { + entry.add_param(VCardParameter::author( + uri.into_owned(), + )); + } + } + _ => { + state.insert_jsprop( + &[ + property.to_string().as_ref(), + name.to_string().as_ref(), + sub_property.to_string().as_ref(), + name.to_string().as_ref(), + ], + value, + ); + } + } + } + } + + _ => { + state.insert_jsprop( + &[ + property.to_string().as_ref(), + name.to_string().as_ref(), + sub_property.to_string().as_ref(), + ], + value, + ); + } + } + } + + state.insert_vcard( + &[JSContactProperty::Notes], + entry.with_param(VCardParameter::prop_id(name.into_string())), + ); + } + } + JSContactProperty::PersonalInfo => { + for (name, value) in value.into_expanded_object() { + if let Some(entry_type) = map_kind( + &value, + [ + (JSContactKind::Expertise, VCardProperty::Expertise), + (JSContactKind::Hobby, VCardProperty::Hobby), + (JSContactKind::Interest, VCardProperty::Interest), + ], + ) { + let mut entry = VCardEntry::new(entry_type); + + for (sub_property, value) in value.into_expanded_object() { + match sub_property { + Key::Property(JSContactProperty::Value) => { + if let Some(value) = + value.into_owned_string().map(VCardValue::Text) + { + entry.values = vec![value]; + } + } + Key::Property(JSContactProperty::Label) => { + if let Some(label) = value.into_string() { + entry.add_param(VCardParameter::label( + label.into_owned(), + )); + } + } + Key::Property(JSContactProperty::ListAs) => { + if let Some(pref) = value.as_i64() { + entry.add_param(VCardParameter::index(pref as u32)); + } + } + Key::Property(JSContactProperty::Level) => { + if let Ok(level) = + IanaType::::try_from(value) + { + let level = if matches!( + entry.name, + VCardProperty::Expertise + ) { + match level { + IanaType::Iana(VCardLevel::High) => { + IanaType::Iana(VCardLevel::Expert) + } + IanaType::Iana(VCardLevel::Medium) => { + IanaType::Iana(VCardLevel::Average) + } + IanaType::Iana(VCardLevel::Low) => { + IanaType::Iana(VCardLevel::Beginner) + } + _ => level, + } + } else { + level + }; + entry.params.push(VCardParameter::level(level)); + } + } + Key::Property(JSContactProperty::Kind) => {} + _ => { + state.insert_jsprop( + &[ + property.to_string().as_ref(), + name.to_string().as_ref(), + sub_property.to_string().as_ref(), + ], + value, + ); + } + } + } + + state.insert_vcard( + &[JSContactProperty::PersonalInfo], + entry.with_param(VCardParameter::prop_id(name.into_string())), + ); + } else { + state.insert_jsprop( + &[property.to_string().as_ref(), name.to_string().as_ref()], + value, + ); + } + } + } + JSContactProperty::RelatedTo => { + for (name, value) in value.into_expanded_object() { + let mut entry = VCardEntry::new(VCardProperty::Related) + .with_value(name.to_string().into_owned()); + + for (sub_property, value) in value.into_expanded_object() { + if let Key::Property(JSContactProperty::Relation) = sub_property { + for typ in value.into_expanded_boolean_set() { + let typ = typ.to_string(); + match VCardType::parse(typ.as_ref().as_bytes()) { + Some(typ) => { + entry.params.push(VCardParameter::typ(typ)) + } + None => { + entry + .params + .push(VCardParameter::typ(typ.into_owned())); + } + } + } + } else { + state.insert_jsprop( + &[ + property.to_string().as_ref(), + name.to_string().as_ref(), + sub_property.to_string().as_ref(), + ], + value, + ); + } + } + + state.insert_vcard(&[JSContactProperty::RelatedTo], entry); + } + } + JSContactProperty::Type + | JSContactProperty::Version + | JSContactProperty::Localizations + | JSContactProperty::VCard => (), + _ => { + if !matches!(value, Value::Null) { + state.insert_jsprop(&[property.to_string().as_ref()], value); + } + } + } + } + } + + if !has_fn { + state.insert_vcard( + &[JSContactProperty::Name, JSContactProperty::Full], + VCardEntry::new(VCardProperty::Fn) + .with_param(VCardParameter::derived(true)) + .with_value(String::new()), + ); + } + + Some(state.into_vcard()) + } + + pub fn into_inner(self) -> Value<'x, JSContactProperty, JSContactValue> { + self.0 + } +} diff --git a/vendor/calcard/src/jscontact/export/entry.rs b/vendor/calcard/src/jscontact/export/entry.rs new file mode 100644 index 00000000..beae0a1b --- /dev/null +++ b/vendor/calcard/src/jscontact/export/entry.rs @@ -0,0 +1,144 @@ +/* + * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC + * + * SPDX-License-Identifier: Apache-2.0 OR MIT + */ + +use crate::{ + common::{ + CalendarScale, IanaParse, + parser::{Boolean, Timestamp}, + }, + jscontact::{JSContactId, JSContactProperty, JSContactValue, export::params::ParamValue}, + vcard::{ + VCardEntry, VCardLevel, VCardParameter, VCardParameterName, VCardParameterValue, + VCardPhonetic, VCardProperty, VCardType, VCardValueType, + }, +}; +use jmap_tools::{Key, Value}; + +impl VCardEntry { + pub(super) fn import_converted_properties( + &mut self, + props: Value<'_, JSContactProperty, JSContactValue>, + ) where + I: JSContactId, + B: JSContactId, + { + for (key, value) in props.into_expanded_object() { + match key { + Key::Property(JSContactProperty::Name) => { + if let Some(name) = value.into_string() { + self.name = VCardProperty::parse(name.as_bytes()) + .unwrap_or(VCardProperty::Other(name.to_ascii_uppercase())); + } + } + Key::Property(JSContactProperty::Parameters) => { + self.import_jcard_params(value); + } + _ => {} + } + } + } + + pub(super) fn import_jcard_params( + &mut self, + params: Value<'_, JSContactProperty, JSContactValue>, + ) where + I: JSContactId, + B: JSContactId, + { + for (key, value) in params.into_expanded_object() { + let mut values = match value { + Value::Array(values) => values.into_iter().filter_map(ParamValue::try_from_value), + value => vec![value] + .into_iter() + .filter_map(ParamValue::try_from_value), + } + .peekable(); + + if values.peek().is_none() { + continue; + } + + let key = key.to_string(); + let Some(param) = VCardParameterName::try_parse(key.as_bytes()) else { + let key = key.into_owned(); + + for value in values { + self.params.push(VCardParameter { + name: VCardParameterName::Other(key.to_ascii_uppercase()), + value: value.into_string().into_owned().into(), + }); + } + + continue; + }; + + for value in values { + let value = match ¶m { + VCardParameterName::Value => { + let value = value.into_string(); + VCardValueType::parse(value.as_bytes()) + .map(VCardParameterValue::ValueType) + .unwrap_or_else(|| VCardParameterValue::Text(value.into_owned())) + } + VCardParameterName::Pref | VCardParameterName::Index => { + match value.into_number() { + Ok(n) => VCardParameterValue::Integer(n as u32), + Err(value) => { + VCardParameterValue::Text(value.into_string().into_owned()) + } + } + } + VCardParameterName::Calscale => { + let value = value.into_string(); + CalendarScale::parse(value.as_bytes()) + .map(VCardParameterValue::Calscale) + .unwrap_or_else(|| VCardParameterValue::Text(value.into_owned())) + } + VCardParameterName::Phonetic => { + let value = value.into_string(); + VCardPhonetic::parse(value.as_bytes()) + .map(VCardParameterValue::Phonetic) + .unwrap_or_else(|| VCardParameterValue::Text(value.into_owned())) + } + VCardParameterName::Level => { + let value = value.into_string(); + VCardLevel::parse(value.as_bytes()) + .map(VCardParameterValue::Level) + .unwrap_or_else(|| VCardParameterValue::Text(value.into_owned())) + } + VCardParameterName::Created => { + let value = value.into_string(); + Timestamp::parse(value.as_bytes()) + .map(|v| VCardParameterValue::Timestamp(v.0)) + .unwrap_or_else(|| VCardParameterValue::Text(value.into_owned())) + } + VCardParameterName::Derived => { + let value = value.into_string(); + Boolean::parse(value.as_bytes()) + .map(|v| VCardParameterValue::Bool(v.0)) + .unwrap_or_else(|| VCardParameterValue::Text(value.into_owned())) + } + VCardParameterName::Type => { + let value = value.into_string(); + VCardType::parse(value.as_bytes()) + .map(VCardParameterValue::Type) + .unwrap_or_else(|| VCardParameterValue::Text(value.into_owned())) + } + VCardParameterName::Group => { + self.group = Some(value.into_string().into_owned()); + continue; + } + _ => VCardParameterValue::Text(value.into_string().into_owned()), + }; + + self.params.push(VCardParameter { + name: param.clone(), + value, + }); + } + } + } +} diff --git a/vendor/calcard/src/jscontact/export/mod.rs b/vendor/calcard/src/jscontact/export/mod.rs new file mode 100644 index 00000000..3743ba4b --- /dev/null +++ b/vendor/calcard/src/jscontact/export/mod.rs @@ -0,0 +1,32 @@ +/* + * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC + * + * SPDX-License-Identifier: Apache-2.0 OR MIT + */ + +use crate::{ + jscontact::{JSContactId, JSContactProperty, JSContactValue}, + vcard::VCard, +}; +use jmap_tools::{Key, Value}; + +pub mod convert; +pub mod entry; +pub mod params; +pub mod props; + +#[derive(Default)] +#[allow(clippy::type_complexity)] +struct State<'x, I, B> +where + I: JSContactId, + B: JSContactId, +{ + pub(super) vcard: VCard, + pub(super) converted_props: Vec<( + Vec>>, + Value<'x, JSContactProperty, JSContactValue>, + )>, + pub(super) converted_props_count: usize, + pub(super) language: Option, +} diff --git a/vendor/calcard/src/jscontact/export/params.rs b/vendor/calcard/src/jscontact/export/params.rs new file mode 100644 index 00000000..b878a34e --- /dev/null +++ b/vendor/calcard/src/jscontact/export/params.rs @@ -0,0 +1,244 @@ +/* + * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC + * + * SPDX-License-Identifier: Apache-2.0 OR MIT + */ + +use crate::{ + common::{IanaParse, IanaType}, + jscontact::{ + JSContactId, JSContactProperty, JSContactValue, + export::{State, props::convert_value}, + }, + vcard::{ + VCard, VCardEntry, VCardParameter, VCardParameterName, VCardParameterValue, VCardProperty, + VCardValueType, ValueType, + }, +}; +use jmap_tools::{Element, JsonPointer, Key, Property, Value}; +use std::borrow::Cow; + +impl<'x, I, B> State<'x, I, B> +where + I: JSContactId, + B: JSContactId, +{ + pub(super) fn insert_vcard(&mut self, path: &[JSContactProperty], mut entry: VCardEntry) { + if self.converted_props_count < self.converted_props.len() { + // Obtain propId + let mut prop_id = + if matches!(entry.name, VCardProperty::Member | VCardProperty::Related) { + entry.values.first().and_then(|v| v.as_text()) + } else { + entry.prop_id() + }; + + // Try mapping X-ABLabel + if let Some(prop_id_) = prop_id { + let mut remove_pos = None; + for (param_pos, param) in entry.params.iter().enumerate() { + if let (VCardParameterName::Label, VCardParameterValue::Text(label)) = + (¶m.name, ¶m.value) + { + if self.converted_props.iter().any(|(prop, _)| { + prop.len() == 3 + && prop[0].to_string() == path[0].to_string() + && prop[1] == prop_id_ + && prop[2] == Key::Property(JSContactProperty::Label) + }) { + self.insert_vcard( + &[path[0].clone(), JSContactProperty::Label], + VCardEntry::new(VCardProperty::Other("X-ABLabel".into())) + .with_value(label.to_string()), + ); + remove_pos = Some(param_pos); + } + break; + } + } + + if let Some(pos) = remove_pos { + entry.params.swap_remove(pos); + prop_id = + if matches!(entry.name, VCardProperty::Member | VCardProperty::Related) { + entry.values.first().and_then(|v| v.as_text()) + } else { + entry.prop_id() + }; + } + } + + let skip_tz_geo = matches!(entry.name, VCardProperty::Adr); + let mut matched_once = false; + + 'outer: for (keys, value) in self.converted_props.iter_mut() { + let is_localized_key = keys + .first() + .is_some_and(|k| matches!(k, Key::Property(JSContactProperty::Localizations))); + + if let Some(lang) = &self.language { + if !is_localized_key || keys.get(1).is_none_or(|k| &k.to_string() != lang) { + continue; + } + } else if is_localized_key { + continue; + } + if matches!(value, Value::Null) { + continue; + } + + for (pos, item) in path.iter().enumerate() { + if !keys + .iter() + .any(|k| matches!(k, Key::Property(p) if p == item)) + { + if pos == 0 && matched_once { + // Array is sorted, so if we didn't match the first item, + // we won't match any further. + break 'outer; + } else { + continue 'outer; + } + } else { + matched_once = true; + } + } + + if prop_id + .map(Key::Borrowed) + .is_none_or(|prop_id| keys.iter().any(|k| k == &prop_id)) + && (!skip_tz_geo + || !keys.iter().any(|k| { + matches!( + k, + Key::Property( + JSContactProperty::TimeZone | JSContactProperty::Coordinates + ) + ) + })) + { + entry.import_converted_properties(std::mem::take(value)); + self.converted_props_count += 1; + break; + } + } + } + + if let Some(lang) = &self.language { + entry.params.push(VCardParameter::language(lang.clone())); + } + + self.vcard.entries.push(entry); + } + + pub(super) fn insert_jsprop( + &mut self, + path: &[&str], + value: Value<'x, JSContactProperty, JSContactValue>, + ) { + let path = if let Some(lang) = &self.language { + JsonPointer::>::encode([ + JSContactProperty::Localizations::.to_string().as_ref(), + lang.as_str(), + JsonPointer::>::encode(path).as_str(), + ]) + } else { + JsonPointer::>::encode(path) + }; + + self.vcard.entries.push( + VCardEntry::new(VCardProperty::Jsprop) + .with_param(VCardParameter::jsptr(path)) + .with_value(serde_json::to_string(&value).unwrap_or_default()), + ); + } + + pub(super) fn import_properties( + &mut self, + props: Vec, JSContactValue>>, + ) { + for prop in props.into_iter().flat_map(|prop| prop.into_array()) { + let mut prop = prop.into_iter(); + let Some(name) = prop.next().and_then(|v| v.into_string()).map(|name| { + VCardProperty::parse(name.as_bytes()) + .unwrap_or(VCardProperty::Other(name.to_ascii_uppercase())) + }) else { + continue; + }; + let Some(params) = prop.next() else { + continue; + }; + let Some(value_type) = prop.next().and_then(|v| v.into_string()).map(|v| { + match VCardValueType::parse(v.as_bytes()) { + Some(v) => IanaType::Iana(v), + None => IanaType::Other(v.to_ascii_uppercase()), + } + }) else { + continue; + }; + + let (default_type, _) = name.default_types(); + let convert_type = value_type + .iana() + .map(|v| ValueType::Vcard(*v)) + .unwrap_or(default_type); + + let Some(values) = prop.next().and_then(|v| match v { + Value::Array(arr) => Some( + arr.into_iter() + .filter_map(|v| convert_value(v, &convert_type).ok()) + .collect::>(), + ), + v => convert_value(v, &convert_type).ok().map(|v| vec![v]), + }) else { + continue; + }; + + let mut entry = VCardEntry::new(name); + entry.import_jcard_params(params); + entry.values = values; + if convert_type != default_type { + entry.params.push(VCardParameter::value(value_type)); + } + self.vcard.entries.push(entry); + } + } + + pub(super) fn into_vcard(self) -> VCard { + self.vcard + } +} + +pub(crate) enum ParamValue<'x> { + Text(Cow<'x, str>), + Number(i64), + Bool(bool), +} + +impl<'x> ParamValue<'x> { + pub(crate) fn try_from_value(value: Value<'x, P, E>) -> Option { + match value { + Value::Str(s) => Some(Self::Text(s)), + Value::Number(n) => Some(Self::Number(n.cast_to_i64())), + Value::Bool(b) => Some(Self::Bool(b)), + Value::Element(e) => Some(Self::Text(e.to_cow().to_string().into())), + _ => None, + } + } + + pub(crate) fn into_string(self) -> Cow<'x, str> { + match self { + Self::Text(s) => s, + Self::Number(n) => n.to_string().into(), + Self::Bool(b) => if b { "true" } else { "false" }.to_string().into(), + } + } + + pub(crate) fn into_number(self) -> Result { + match self { + Self::Number(n) => Ok(n), + Self::Text(s) => s.parse().map_err(|_| Self::Text(s)), + _ => Err(self), + } + } +} diff --git a/vendor/calcard/src/jscontact/export/props.rs b/vendor/calcard/src/jscontact/export/props.rs new file mode 100644 index 00000000..46edbc35 --- /dev/null +++ b/vendor/calcard/src/jscontact/export/props.rs @@ -0,0 +1,399 @@ +/* + * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC + * + * SPDX-License-Identifier: Apache-2.0 OR MIT + */ + +use crate::{ + common::{CalendarScale, IanaParse, IanaType, PartialDateTime, parser::Integer}, + icalendar::Uri, + jscontact::{ + JSContactGrammaticalGender, JSContactId, JSContactKind, JSContactLevel, + JSContactPhoneticSystem, JSContactProperty, JSContactValue, + }, + vcard::{ + VCardGramGender, VCardKind, VCardLevel, VCardPhonetic, VCardSex, VCardType, VCardValue, + VCardValueType, ValueType, + }, +}; +use jmap_tools::{JsonPointerItem, Key, Map, Value}; +use std::{borrow::Cow, iter::Peekable, vec::IntoIter}; + +pub(super) fn build_path<'x, I, B>( + obj: &mut Value<'x, JSContactProperty, JSContactValue>, + mut ptr: Peekable>>>, + value: Value<'x, JSContactProperty, JSContactValue>, +) -> Option, JSContactValue>> +where + I: JSContactId, + B: JSContactId, +{ + if let Some(item) = ptr.next() { + match item { + JsonPointerItem::Root | JsonPointerItem::Wildcard => {} + JsonPointerItem::Key(key) => match obj { + Value::Object(obj) => { + return build_path( + obj.insert_or_get_mut( + key, + if matches!(ptr.peek(), Some(JsonPointerItem::Key(_))) { + Value::Object(Map::from(Vec::new())) + } else { + Value::Array(Vec::new()) + }, + ), + ptr, + value, + ); + } + Value::Null => { + *obj = Value::Object(Map::from(vec![(key, Value::Null)])); + return build_path( + &mut obj + .as_object_mut() + .unwrap() + .as_mut_vec() + .last_mut() + .unwrap() + .1, + ptr, + value, + ); + } + _ => {} + }, + JsonPointerItem::Number(idx) => { + if let Some(arr) = obj.as_array_mut() { + if (idx as usize) < arr.len() { + return build_path(&mut arr[idx as usize], ptr, value); + } + + if idx < 20 { + arr.resize_with(idx as usize + 1, || Value::Null); + return build_path(&mut arr[idx as usize], ptr, value); + } + } + } + } + Some(value) + } else { + *obj = value; + None + } +} + +#[allow(clippy::type_complexity)] +pub(super) fn convert_anniversary( + value: Value<'_, JSContactProperty, JSContactValue>, +) -> Result< + (PartialDateTime, Option), + Value<'_, JSContactProperty, JSContactValue>, +> +where + I: JSContactId, + B: JSContactId, +{ + let mut date = PartialDateTime::default(); + let mut calendar_scale = None; + let Some(object) = value.as_object() else { + return Err(value); + }; + + for (key, value) in object.as_vec() { + match key { + Key::Property(JSContactProperty::Day) => { + if let Value::Number(day) = value { + date.day = Some(day.cast_to_i64() as u8); + } + } + Key::Property(JSContactProperty::Month) => { + if let Value::Number(month) = value { + date.month = Some(month.cast_to_i64() as u8); + } + } + Key::Property(JSContactProperty::Year) => { + if let Value::Number(year) = value { + date.year = Some(year.cast_to_i64() as u16); + } + } + Key::Property(JSContactProperty::CalendarScale) => { + if let Value::Element(JSContactValue::CalendarScale(scale)) = value { + calendar_scale = Some(scale.clone()); + } + } + Key::Property(JSContactProperty::Utc) => { + if let Value::Element(JSContactValue::Timestamp(timestamp)) = value { + return Ok((PartialDateTime::from_utc_timestamp(*timestamp), None)); + } + } + _ => {} + } + } + + if date.year.is_some() || date.month.is_some() || date.day.is_some() { + Ok((date, calendar_scale)) + } else { + Err(value) + } +} + +pub(super) fn convert_value<'x, I, B>( + value: Value<'x, JSContactProperty, JSContactValue>, + value_type: &'_ ValueType, +) -> Result, JSContactValue>> +where + I: JSContactId, + B: JSContactId, +{ + match value { + Value::Element(e) => match e { + JSContactValue::Timestamp(t) => Ok(VCardValue::PartialDateTime( + PartialDateTime::from_utc_timestamp(t), + )), + JSContactValue::GrammaticalGender(g) => Ok(VCardValue::GramGender(match g { + JSContactGrammaticalGender::Animate => VCardGramGender::Animate, + JSContactGrammaticalGender::Common => VCardGramGender::Common, + JSContactGrammaticalGender::Feminine => VCardGramGender::Feminine, + JSContactGrammaticalGender::Inanimate => VCardGramGender::Inanimate, + JSContactGrammaticalGender::Masculine => VCardGramGender::Masculine, + JSContactGrammaticalGender::Neuter => VCardGramGender::Neuter, + })), + JSContactValue::Kind(k) => match k { + JSContactKind::Individual => Ok(VCardValue::Kind(VCardKind::Individual)), + JSContactKind::Group => Ok(VCardValue::Kind(VCardKind::Group)), + JSContactKind::Location => Ok(VCardValue::Kind(VCardKind::Location)), + JSContactKind::Org => Ok(VCardValue::Kind(VCardKind::Org)), + JSContactKind::Application => Ok(VCardValue::Kind(VCardKind::Application)), + JSContactKind::Device => Ok(VCardValue::Kind(VCardKind::Device)), + _ => Err(Value::Element(JSContactValue::Kind(k))), + }, + JSContactValue::Level(_) + | JSContactValue::Type(_) + | JSContactValue::Relation(_) + | JSContactValue::PhoneticSystem(_) + | JSContactValue::CalendarScale(_) + | JSContactValue::BlobId(_) + | JSContactValue::Id(_) + | JSContactValue::IdReference(_) => Err(Value::Element(e)), + }, + Value::Str(s) => { + match value_type { + ValueType::Kind => { + if let Some(kind) = VCardKind::parse(s.as_ref().as_bytes()) { + return Ok(VCardValue::Kind(kind)); + } + } + ValueType::Sex => { + if let Some(sex) = VCardSex::parse(s.as_ref().as_bytes()) { + return Ok(VCardValue::Sex(sex)); + } + } + ValueType::GramGender => { + if let Some(gender) = VCardGramGender::parse(s.as_ref().as_bytes()) { + return Ok(VCardValue::GramGender(gender)); + } + } + ValueType::Vcard(typ) => match typ { + VCardValueType::Boolean => { + if s.eq_ignore_ascii_case("true") { + return Ok(VCardValue::Boolean(true)); + } else if s.eq_ignore_ascii_case("false") { + return Ok(VCardValue::Boolean(false)); + } + } + VCardValueType::Date => { + let mut dt = PartialDateTime::default(); + dt.parse_vcard_date(&mut s.as_ref().as_bytes().iter().peekable()); + if !dt.is_null() { + return Ok(VCardValue::PartialDateTime(dt)); + } + } + VCardValueType::DateAndOrTime => { + let mut dt = PartialDateTime::default(); + dt.parse_vcard_date_and_or_time( + &mut s.as_ref().as_bytes().iter().peekable(), + ); + if !dt.is_null() { + return Ok(VCardValue::PartialDateTime(dt)); + } + } + VCardValueType::DateTime => { + let mut dt = PartialDateTime::default(); + dt.parse_vcard_date_time(&mut s.as_ref().as_bytes().iter().peekable()); + if !dt.is_null() { + return Ok(VCardValue::PartialDateTime(dt)); + } + } + VCardValueType::Time => { + let mut dt = PartialDateTime::default(); + dt.parse_vcard_time(&mut s.as_ref().as_bytes().iter().peekable(), false); + if !dt.is_null() { + return Ok(VCardValue::PartialDateTime(dt)); + } + } + VCardValueType::Timestamp => { + let mut dt = PartialDateTime::default(); + if dt.parse_timestamp(&mut s.as_ref().as_bytes().iter().peekable(), true) { + return Ok(VCardValue::PartialDateTime(dt)); + } + } + VCardValueType::UtcOffset => { + let mut dt = PartialDateTime::default(); + dt.parse_zone(&mut s.as_ref().as_bytes().iter().peekable()); + if !dt.is_null() { + return Ok(VCardValue::PartialDateTime(dt)); + } + } + VCardValueType::Float => { + if let Ok(float) = s.as_ref().parse::() { + return Ok(VCardValue::Float(float)); + } + } + VCardValueType::Integer => { + if let Some(integer) = Integer::parse(s.as_ref().as_bytes()) { + return Ok(VCardValue::Integer(integer.0)); + } + } + VCardValueType::Uri => { + return Ok(match Uri::parse(s) { + Uri::Data(data) => VCardValue::Binary(data), + Uri::Location(text) => VCardValue::Text(text), + }); + } + VCardValueType::LanguageTag | VCardValueType::Text => (), + }, + } + + Ok(VCardValue::Text(s.into_owned())) + } + Value::Bool(b) => Ok(VCardValue::Boolean(b)), + Value::Number(n) => match n.try_cast_to_i64() { + Ok(i) => Ok(VCardValue::Integer(i)), + Err(f) => Ok(VCardValue::Float(f)), + }, + value => Err(value), + } +} + +pub(super) fn convert_types( + value: Value<'_, JSContactProperty, JSContactValue>, + is_context: bool, +) -> Option>> +where + I: JSContactId, + B: JSContactId, +{ + let mut types = Vec::new(); + for typ in value.into_expanded_boolean_set() { + let typ = typ.to_string(); + match VCardType::parse(typ.as_ref().as_bytes()) { + Some(typ) => types.push(IanaType::Iana(typ)), + None => { + if is_context && typ.eq_ignore_ascii_case("private") { + types.push(IanaType::Iana(VCardType::Home)); + } else if !is_context && typ.eq_ignore_ascii_case("mobile") { + types.push(IanaType::Iana(VCardType::Cell)); + } else { + types.push(IanaType::Other(typ.to_ascii_uppercase())); + } + } + } + } + if !types.is_empty() { Some(types) } else { None } +} + +pub(super) fn map_kind( + value: &Value<'_, JSContactProperty, JSContactValue>, + types: impl IntoIterator, +) -> Option +where + I: JSContactId, + B: JSContactId, +{ + value + .as_object() + .and_then(|obj| obj.get(&Key::Property(JSContactProperty::Kind))) + .and_then(|v| match v { + Value::Element(JSContactValue::Kind(kind)) => { + types.into_iter().find_map(|(js_kind, vcard_property)| { + if js_kind == *kind { + Some(vcard_property) + } else { + None + } + }) + } + + _ => None, + }) +} + +impl TryFrom, JSContactValue>> + for IanaType +where + I: JSContactId, + B: JSContactId, +{ + type Error = (); + fn try_from( + value: Value<'_, JSContactProperty, JSContactValue>, + ) -> Result { + match value { + Value::Element(JSContactValue::PhoneticSystem(system)) => { + Ok(IanaType::Iana(match system { + JSContactPhoneticSystem::Ipa => VCardPhonetic::Ipa, + JSContactPhoneticSystem::Jyut => VCardPhonetic::Jyut, + JSContactPhoneticSystem::Piny => VCardPhonetic::Piny, + JSContactPhoneticSystem::Script => VCardPhonetic::Script, + })) + } + Value::Str(text) => match VCardPhonetic::parse(text.as_ref().as_bytes()) { + Some(phonetic) => Ok(IanaType::Iana(phonetic)), + None => Ok(IanaType::Other(text.to_ascii_uppercase())), + }, + _ => Err(()), + } + } +} + +impl TryFrom, JSContactValue>> + for IanaType +where + I: JSContactId, + B: JSContactId, +{ + type Error = (); + + fn try_from( + value: Value<'_, JSContactProperty, JSContactValue>, + ) -> Result { + match value { + Value::Element(JSContactValue::Level(level)) => Ok(IanaType::Iana(match level { + JSContactLevel::High => VCardLevel::High, + JSContactLevel::Low => VCardLevel::Low, + JSContactLevel::Medium => VCardLevel::Medium, + })), + Value::Str(text) => match VCardLevel::parse(text.as_ref().as_bytes()) { + Some(level) => Ok(IanaType::Iana(level)), + None => Ok(IanaType::Other(text.to_ascii_uppercase())), + }, + _ => Err(()), + } + } +} + +pub(super) fn find_text_param<'x, I, B>( + value: &'x Value<'x, JSContactProperty, JSContactValue>, + name: &str, +) -> Option> +where + I: JSContactId, + B: JSContactId, +{ + value + .as_object() + .and_then(|obj| obj.get(&Key::Property(JSContactProperty::Parameters))) + .and_then(|obj| obj.as_object()) + .and_then(|obj| obj.get_ignore_case(name)) + .and_then(|obj| obj.as_str()) +} diff --git a/vendor/calcard/src/jscontact/import/convert.rs b/vendor/calcard/src/jscontact/import/convert.rs new file mode 100644 index 00000000..a3456a59 --- /dev/null +++ b/vendor/calcard/src/jscontact/import/convert.rs @@ -0,0 +1,1276 @@ +/* + * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC + * + * SPDX-License-Identifier: Apache-2.0 OR MIT + */ + +use crate::{ + jscontact::{ + JSContact, JSContactId, JSContactKind, JSContactProperty, JSContactValue, + import::{ConversionOptions, EntryState, GetObjectOrCreate, State}, + }, + vcard::{ + Jscomp, VCard, VCardParameter, VCardParameterName, VCardParameterValue, VCardProperty, + VCardValue, VCardValueType, + }, +}; +use jmap_tools::{JsonPointer, Key, Map, Property, Value}; + +impl VCard { + pub fn into_jscontact(self) -> JSContact<'static, I, B> + where + I: JSContactId, + B: JSContactId, + { + self.into_jscontact_with_options(ConversionOptions::default()) + } + + pub fn into_jscontact_with_options( + mut self, + options: ConversionOptions, + ) -> JSContact<'static, I, B> + where + I: JSContactId, + B: JSContactId, + { + let mut state = State::new(&mut self, options.include_vcard_parameters); + + for entry in self.entries { + let mut entry = EntryState::new(entry); + + match &entry.entry.name { + VCardProperty::Kind => { + if !state.has_property(JSContactProperty::Kind) + && let Some(kind) = entry.to_kind() + { + state + .entries + .insert(Key::Property(JSContactProperty::Kind), kind); + entry.set_converted_to::(&[JSContactProperty::Kind:: + .to_string() + .as_ref()]); + } + } + VCardProperty::Source => { + state.map_named_entry( + &mut entry, + &[ + VCardParameterName::PropId, + VCardParameterName::Pref, + VCardParameterName::Mediatype, + ], + JSContactProperty::Directories, + JSContactProperty::Uri, + [( + Key::Property(JSContactProperty::Kind), + Value::Element(JSContactValue::Kind(JSContactKind::Entry)), + )], + ); + } + VCardProperty::OrgDirectory => { + state.map_named_entry( + &mut entry, + &[ + VCardParameterName::PropId, + VCardParameterName::Pref, + VCardParameterName::Index, + VCardParameterName::Label, + ], + JSContactProperty::Directories, + JSContactProperty::Uri, + [( + Key::Property(JSContactProperty::Kind), + Value::Element(JSContactValue::Kind(JSContactKind::Directory)), + )], + ); + } + VCardProperty::Anniversary | VCardProperty::Bday | VCardProperty::Deathdate => { + let kind = match &entry.entry.name { + VCardProperty::Anniversary => JSContactKind::Wedding, + VCardProperty::Bday => JSContactKind::Birth, + VCardProperty::Deathdate => JSContactKind::Death, + _ => unreachable!(), + }; + + state.map_named_entry( + &mut entry, + &[VCardParameterName::PropId, VCardParameterName::Calscale], + JSContactProperty::Anniversaries, + JSContactProperty::Date, + [( + Key::Property(JSContactProperty::Kind), + Value::Element(JSContactValue::Kind(kind)), + )], + ); + } + VCardProperty::Birthplace | VCardProperty::Deathplace => { + // Only text and GEO can be mapped + let is_geo = matches!(entry.entry.values.first(), Some(VCardValue::Text(uri)) if uri.starts_with("geo:")); + if (is_geo || !entry.entry.is_type(&VCardValueType::Uri)) + && let Some(text) = entry.to_text() + { + // Extract language and value type + let mut params = state.extract_params( + &mut entry.entry.params, + &[VCardParameterName::Language, VCardParameterName::PropId], + ); + + let prop_id = params.prop_id(); + let alt_id = params.alt_id(); + + let kind = Value::Element(JSContactValue::Kind( + if entry.entry.name == VCardProperty::Birthplace { + JSContactKind::Birth + } else { + JSContactKind::Death + }, + )); + + let patch_prop = if params.language.is_some() { + &entry.entry.name + } else if entry.entry.name == VCardProperty::Birthplace { + &VCardProperty::Bday + } else { + &VCardProperty::Deathdate + }; + let mut patch_id = prop_id + .as_deref() + .filter(|prop_id| state.has_prop_id(patch_prop, prop_id)) + .or_else(|| { + state.find_prop_id( + patch_prop, + entry.entry.group.as_deref(), + alt_id.as_deref(), + ) + }) + .map(|prop_id| prop_id.to_string()); + + if patch_id.is_none() && params.language.is_some() { + entry + .entry + .params + .push(VCardParameter::language(params.language.take().unwrap())); + } + + let entries = + state.get_mut_object_or_insert(JSContactProperty::Anniversaries); + + if let Some(lang) = params.language() { + let path = format!( + "{}/{}/{}/{}", + JSContactProperty::Anniversaries::.to_cow().as_ref(), + patch_id.unwrap(), + JSContactProperty::Place::.to_cow().as_ref(), + JSContactProperty::Full::.to_cow().as_ref() + ); + + entry.set_converted_to::(&[ + JSContactProperty::Localizations::.to_cow().as_ref(), + lang.as_str(), + path.as_str(), + ]); + + state + .localizations + .entry(lang) + .or_default() + .push((path, text)); + } else { + // Place needs to be wrapped in an option to dance around the borrow checker + let prop_name = if !is_geo { + JSContactProperty::Full + } else { + JSContactProperty::Coordinates + }; + let mut place = + Some(Map::from(vec![(Key::Property(prop_name.clone()), text)])); + + if let Some(anniversary) = patch_id.clone().and_then(|patch_id| { + entries + .get_mut(&Key::Owned(patch_id)) + .and_then(|v| v.as_object_mut()) + }) { + anniversary.insert( + Key::Property(JSContactProperty::Place), + Value::Object(place.take().unwrap()), + ); + } + + if place.is_some() + && let Some((key, anniversary)) = + entries.iter_mut().find_map(|(k, v)| { + let obj = v.as_object_mut()?; + if obj.iter().any(|(k, v)| { + k == &Key::Property(JSContactProperty::Kind) + && v == &kind + }) { + Some((k, obj)) + } else { + None + } + }) + { + anniversary.insert( + Key::Property(JSContactProperty::Place), + Value::Object(place.take().unwrap()), + ); + patch_id = Some(key.to_string().into_owned()); + } + + if let Some(place) = place { + patch_id = + entries.insert_named(prop_id, Value::Object(place)).into(); + } + + let patch_id = patch_id.unwrap(); + + entry.set_converted_to::(&[ + JSContactProperty::Anniversaries::.to_cow().as_ref(), + patch_id.as_ref(), + JSContactProperty::Place::.to_cow().as_ref(), + prop_name.to_cow().as_ref(), + ]); + + state.track_prop( + &entry.entry, + JSContactProperty::Anniversaries, + alt_id, + patch_id, + ); + } + } + } + VCardProperty::Fn => { + if (!state.has_fn + || (entry + .non_default_language(state.default_language.as_deref()) + .is_some() + && !state.has_fn_localization)) + && let Some(text) = entry.to_text() + { + if let Some(lang) = state + .extract_params( + &mut entry.entry.params, + &[VCardParameterName::Language], + ) + .language + { + let path = format!( + "{}/{}", + JSContactProperty::Name::.to_cow().as_ref(), + JSContactProperty::Full::.to_cow().as_ref() + ); + + entry.set_converted_to::(&[ + JSContactProperty::Localizations::.to_cow().as_ref(), + lang.as_str(), + path.as_str(), + ]); + state.has_fn_localization = true; + state + .localizations + .entry(lang) + .or_default() + .push((path, text)); + } else { + state.has_fn = true; + state + .get_mut_object_or_insert(JSContactProperty::Name) + .insert(Key::Property(JSContactProperty::Full), text); + entry.set_converted_to::(&[ + JSContactProperty::Name::.to_cow().as_ref(), + JSContactProperty::Full::.to_cow().as_ref(), + ]); + } + } + } + VCardProperty::N => { + /* + Only process this N component if it matches the main ALTID and: + - It is the first N component + - Or it has a language that has not been processed yet + */ + if state.name_alt_id.as_deref() == entry.entry.alt_id() + && (!state.has_n + || (entry + .non_default_language(state.default_language.as_deref()) + .is_some() + && !state.has_n_localization)) + { + let mut params = state.extract_params( + &mut entry.entry.params, + &[ + VCardParameterName::Language, + VCardParameterName::Phonetic, + VCardParameterName::Script, + VCardParameterName::SortAs, + VCardParameterName::Jscomps, + ], + ); + + if params.language.is_some() && !state.has_n { + entry + .entry + .params + .push(VCardParameter::language(params.language.take().unwrap())); + } + + /* + + 0 = family names (also known as surnames); + 1 = given names; + 2 = additional names; + 3 = honorific prefixes; + 4 = honorific suffixes; + 5 = secondary surname; + 6 = generation + + */ + + let mut default_separator = None; + let mut is_ordered = false; + let mut components = Vec::new(); + + if !params.jscomps.is_empty() { + let mut is_valid = true; + let mut max_pos = 0; + + for (pos, jscomp) in + std::mem::take(&mut params.jscomps).into_iter().enumerate() + { + match jscomp { + Jscomp::Entry { position, value } => { + if let Some(kind) = + JSContactKind::from_vcard_n_pos(position as usize) + { + let value = + match entry.entry.values.get(position as usize) { + Some(VCardValue::Text(text)) if value == 0 => { + Some(text.as_str()) + } + Some(VCardValue::Component(values)) => values + .get(value as usize) + .map(|v| v.as_str()), + _ => None, + }; + + max_pos = std::cmp::max(max_pos, position); + if let Some(value) = value { + components.push(Value::Object(Map::from(vec![ + ( + Key::Property(JSContactProperty::Kind), + Value::Element(JSContactValue::Kind(kind)), + ), + ( + Key::Property(JSContactProperty::Value), + Value::Str(value.to_string().into()), + ), + ]))); + continue; + } + } + + is_valid = false; + break; + } + Jscomp::Separator(sep) => { + if !sep.is_empty() { + if pos > 0 { + components.push(Value::Object(Map::from(vec![ + ( + Key::Property(JSContactProperty::Kind), + Value::Element(JSContactValue::Kind( + JSContactKind::Separator, + )), + ), + ( + Key::Property(JSContactProperty::Value), + Value::Str(sep.into()), + ), + ]))); + } else { + default_separator = sep.into(); + } + } + } + } + } + + if is_valid + && !components.is_empty() + && max_pos <= (entry.entry.values.len() - 1) as u32 + { + is_ordered = true; + entry.entry.values.clear(); + } + } + + if !is_ordered { + let mut components_ = Vec::with_capacity(7); + for (comp_id, value) in entry.entry.values.iter().enumerate() { + if let Some(kind) = JSContactKind::from_vcard_n_pos(comp_id) { + match value { + VCardValue::Text(text) if !text.is_empty() => { + components_.push((kind, text.as_str())); + } + VCardValue::Component(text_list) => { + for text in text_list { + components_.push((kind, text.as_str())); + } + } + _ => {} + } + } + } + + components = components_ + .iter() + .filter(|(kind, value)| { + match kind { + JSContactKind::Credential => { + // 'credential' From vCard: ignore any value that also occurs in the Generation component. + !components_ + .contains(&(JSContactKind::Generation, value)) + } + JSContactKind::Surname => { + // 'surname': From vCard: ignore any value that also occurs in the Secondary surname component. + !components_.contains(&(JSContactKind::Surname2, value)) + } + _ => true, + } + }) + .map(|(kind, value)| { + Value::Object(Map::from(vec![ + ( + Key::Property(JSContactProperty::Kind), + Value::Element(JSContactValue::Kind(*kind)), + ), + ( + Key::Property(JSContactProperty::Value), + Value::Str(value.to_string().into()), + ), + ])) + }) + .collect::>(); + } + + let name = state + .entries + .get_mut_object_or_insert(JSContactProperty::Name); + + if let Some(lang) = params.language() { + let path = format!( + "{}/{}", + JSContactProperty::Name::.to_cow().as_ref(), + JSContactProperty::Components::.to_cow().as_ref(), + ); + + entry.set_converted_to::(&[ + JSContactProperty::Localizations::.to_cow().as_ref(), + lang.as_str(), + path.as_str(), + ]); + + state.has_n_localization = true; + let locale = state.localizations.entry(lang).or_default(); + locale.push((path, Value::Array(components))); + + for (prop, value) in params.into_iter(&entry.entry.name) { + locale.push(( + format!( + "{}/{}", + JSContactProperty::Name::.to_cow().as_ref(), + prop.to_string() + ), + value, + )); + } + } else { + state.has_n = true; + name.extend(params.into_iter(&entry.entry.name)); + name.insert( + Key::Property(JSContactProperty::Components), + Value::Array(components), + ); + if let Some(default_separator) = default_separator { + name.insert_unchecked( + Key::Property(JSContactProperty::DefaultSeparator), + Value::Str(default_separator.into()), + ); + } + if is_ordered { + name.insert_unchecked( + Key::Property(JSContactProperty::IsOrdered), + Value::Bool(true), + ); + } + entry.set_converted_to::(&[ + JSContactProperty::Name::.to_cow().as_ref(), + JSContactProperty::Components::.to_cow().as_ref(), + ]); + } + } + } + VCardProperty::Gramgender => { + if !state.has_gram_gender + && let Some(gram_gender) = entry.to_gram_gender() + { + state.extract_params(&mut entry.entry.params, &[]); + state + .get_mut_object_or_insert(JSContactProperty::SpeakToAs) + .insert(JSContactProperty::GrammaticalGender, gram_gender); + state.has_gram_gender = true; + entry.set_converted_to::(&[ + JSContactProperty::SpeakToAs::.to_cow().as_ref(), + JSContactProperty::GrammaticalGender::.to_cow().as_ref(), + ]); + } + } + VCardProperty::Pronouns => { + state.map_named_entry( + &mut entry, + &[ + VCardParameterName::Language, + VCardParameterName::PropId, + VCardParameterName::Altid, + VCardParameterName::Pref, + ], + JSContactProperty::SpeakToAs, + JSContactProperty::Pronouns, + [], + ); + } + VCardProperty::Nickname => { + for value in std::mem::take(&mut entry.entry.values) { + let mut value_entry = entry.clone(); + value_entry.entry.values = vec![value]; + + state.map_named_entry( + &mut value_entry, + &[ + VCardParameterName::Type, + VCardParameterName::Pref, + VCardParameterName::PropId, + VCardParameterName::Altid, + VCardParameterName::Language, + ], + JSContactProperty::Nicknames, + JSContactProperty::Name, + [], + ); + state.add_conversion_props(value_entry); + } + continue; + } + VCardProperty::Photo | VCardProperty::Logo | VCardProperty::Sound => { + let kind = match &entry.entry.name { + VCardProperty::Photo => JSContactKind::Photo, + VCardProperty::Logo => JSContactKind::Logo, + VCardProperty::Sound => JSContactKind::Sound, + _ => unreachable!(), + }; + state.map_named_entry( + &mut entry, + &[ + VCardParameterName::Mediatype, + VCardParameterName::Pref, + VCardParameterName::PropId, + VCardParameterName::Label, + ], + JSContactProperty::Media, + JSContactProperty::Uri, + [( + Key::Property(JSContactProperty::Kind), + Value::Element(JSContactValue::Kind(kind)), + )], + ); + } + VCardProperty::Adr => { + let mut params = state.extract_params( + &mut entry.entry.params, + &[ + VCardParameterName::Language, + VCardParameterName::Phonetic, + VCardParameterName::Script, + VCardParameterName::Label, + VCardParameterName::Geo, + VCardParameterName::Tz, + VCardParameterName::Cc, + VCardParameterName::PropId, + VCardParameterName::Altid, + VCardParameterName::Pref, + VCardParameterName::Type, + VCardParameterName::Jscomps, + ], + ); + + let prop_id = params.prop_id(); + let alt_id = params.alt_id(); + + // Locate the address to patch or reset language + let addr_patch = if params.language.is_some() { + let addr_patch = prop_id + .as_deref() + .filter(|prop_id| state.has_prop_id(&entry.entry.name, prop_id)) + .or_else(|| { + state.find_prop_id( + &entry.entry.name, + entry.entry.group.as_deref(), + alt_id.as_deref(), + ) + }); + + if addr_patch.is_none() { + entry + .entry + .params + .push(VCardParameter::language(params.language().unwrap())); + } + addr_patch + } else { + None + }; + + /* + + 0 = ADR-component-pobox ";" + 1 = ADR-component-ext ";" + 2 = ADR-component-street ";" + 3 = ADR-component-locality ";" + 4 = ADR-component-region ";" + 5 = ADR-component-code ";" + 6 = ADR-component-country ";" + 7 = ADR-component-room ";" + 8 = ADR-component-apartment ";" + 9 = ADR-component-floor ";" + 10 = ADR-component-streetnumber ";" + 11 = ADR-component-streetname ";" + 12 = ADR-component-building ";" + 13 = ADR-component-block ";" + 14 = ADR-component-subdistrict ";" + 15 = ADR-component-district ";" + 16 = ADR-component-landmark ";" + 17 = ADR-component-direction ";" + + */ + + let mut default_separator = None; + let mut is_ordered = false; + let mut components = Vec::new(); + + if !params.jscomps.is_empty() { + let mut is_valid = true; + let mut max_pos = 0; + + for (pos, jscomp) in + std::mem::take(&mut params.jscomps).into_iter().enumerate() + { + match jscomp { + Jscomp::Entry { position, value } => { + if let Some(kind) = + JSContactKind::from_vcard_adr_pos(position as usize) + { + let value = match entry.entry.values.get(position as usize) + { + Some(VCardValue::Text(text)) if value == 0 => { + Some(text.as_str()) + } + Some(VCardValue::Component(values)) => { + values.get(value as usize).map(|v| v.as_str()) + } + _ => None, + }; + + max_pos = std::cmp::max(max_pos, position); + if let Some(value) = value { + components.push(Value::Object(Map::from(vec![ + ( + Key::Property(JSContactProperty::Kind), + Value::Element(JSContactValue::Kind(kind)), + ), + ( + Key::Property(JSContactProperty::Value), + Value::Str(value.to_string().into()), + ), + ]))); + continue; + } + } + + is_valid = false; + break; + } + Jscomp::Separator(sep) => { + if !sep.is_empty() { + if pos > 0 { + components.push(Value::Object(Map::from(vec![ + ( + Key::Property(JSContactProperty::Kind), + Value::Element(JSContactValue::Kind( + JSContactKind::Separator, + )), + ), + ( + Key::Property(JSContactProperty::Value), + Value::Str(sep.into()), + ), + ]))); + } else { + default_separator = sep.into(); + } + } + } + } + } + + if is_valid + && !components.is_empty() + && max_pos <= (entry.entry.values.len() - 1) as u32 + { + is_ordered = true; + entry.entry.values.clear(); + } + } + + if !is_ordered { + components.clear(); + + let is_rfc9554_adr = entry.entry.values.iter().skip(7).any(|v| match v { + VCardValue::Text(text) => !text.is_empty(), + VCardValue::Component(text_list) => !text_list.is_empty(), + _ => false, + }); + + for (pos, value) in entry.entry.values.drain(..).enumerate() { + if is_rfc9554_adr && (pos == 1 || pos == 2) { + // From vCard: ignore if the ADR structured value is of the format defined in [RFC9554]. Otherwise, convert to "apartment". + // From vCard: ignore if the ADR structured value is of the format defined in [RFC9554]. Otherwise, convert to "name". + continue; + } + + if let Some(kind) = JSContactKind::from_vcard_adr_pos(pos) { + match value { + VCardValue::Text(text) if !text.is_empty() => { + components.push(Value::Object(Map::from(vec![ + ( + Key::Property(JSContactProperty::Kind), + Value::Element(JSContactValue::Kind(kind)), + ), + ( + Key::Property(JSContactProperty::Value), + Value::Str(text.into()), + ), + ]))); + } + VCardValue::Component(text_list) => { + for text in text_list { + components.push(Value::Object(Map::from(vec![ + ( + Key::Property(JSContactProperty::Kind), + Value::Element(JSContactValue::Kind(kind)), + ), + ( + Key::Property(JSContactProperty::Value), + Value::Str(text.into()), + ), + ]))); + } + } + _ => {} + } + } + } + } + + if let Some(lang) = params.language() { + let path = format!( + "{}/{}/{}", + JSContactProperty::Addresses::.to_cow().as_ref(), + addr_patch.unwrap(), + JSContactProperty::Components::.to_cow().as_ref(), + ); + entry.set_converted_to::(&[ + JSContactProperty::Localizations::.to_cow().as_ref(), + lang.as_str(), + path.as_str(), + ]); + + let locale = state.localizations.entry(lang).or_default(); + + let base_path = path.rsplit_once('/').unwrap().0; + for (prop_name, value) in params.into_iter(&entry.entry.name) { + locale + .push((format!("{}/{}", base_path, prop_name.to_string()), value)); + } + + locale.push((path, Value::Array(components))); + } else { + let entries = state.get_mut_object_or_insert(JSContactProperty::Addresses); + let mut addr = Map::from(Vec::with_capacity(4)); + + addr.extend(params.into_iter(&entry.entry.name)); + addr.insert_unchecked( + Key::Property(JSContactProperty::Components), + Value::Array(components), + ); + if let Some(default_separator) = default_separator { + addr.insert_unchecked( + Key::Property(JSContactProperty::DefaultSeparator), + Value::Str(default_separator.into()), + ); + } + if is_ordered { + addr.insert_unchecked( + Key::Property(JSContactProperty::IsOrdered), + Value::Bool(true), + ); + } + + let prop_id = entries.insert_named(prop_id, Value::Object(addr)); + + entry.set_converted_to::(&[ + JSContactProperty::Addresses::.to_cow().as_ref(), + prop_id.as_str(), + JSContactProperty::Components::.to_cow().as_ref(), + ]); + + state.track_prop( + &entry.entry, + JSContactProperty::Addresses, + alt_id, + prop_id, + ); + } + } + VCardProperty::Email => { + state.map_named_entry( + &mut entry, + &[ + VCardParameterName::Type, + VCardParameterName::Pref, + VCardParameterName::PropId, + VCardParameterName::Label, + ], + JSContactProperty::Emails, + JSContactProperty::Address, + [], + ); + } + VCardProperty::Impp => { + state.map_named_entry( + &mut entry, + &[ + VCardParameterName::Type, + VCardParameterName::Pref, + VCardParameterName::PropId, + VCardParameterName::Label, + VCardParameterName::ServiceType, + VCardParameterName::Username, + ], + JSContactProperty::OnlineServices, + JSContactProperty::Uri, + [], + ); + entry.set_map_name(); + } + VCardProperty::Lang => { + state.map_named_entry( + &mut entry, + &[ + VCardParameterName::Type, + VCardParameterName::Pref, + VCardParameterName::PropId, + VCardParameterName::Label, + ], + JSContactProperty::PreferredLanguages, + JSContactProperty::Language, + [], + ); + } + VCardProperty::Language | VCardProperty::Prodid | VCardProperty::Uid => { + let key = Key::Property(match &entry.entry.name { + VCardProperty::Language => JSContactProperty::Language, + VCardProperty::Prodid => JSContactProperty::ProdId, + VCardProperty::Uid => JSContactProperty::Uid, + _ => unreachable!(), + }); + + if !state.entries.contains_key(&key) + && let Some(text) = entry.to_text() + { + entry.set_converted_to::(&[key.to_string().as_ref()]); + state.entries.insert(key, text); + } + } + VCardProperty::Socialprofile => { + state.map_named_entry( + &mut entry, + &[ + VCardParameterName::Type, + VCardParameterName::Pref, + VCardParameterName::PropId, + VCardParameterName::Label, + VCardParameterName::ServiceType, + VCardParameterName::Username, + ], + JSContactProperty::OnlineServices, + JSContactProperty::Uri, + [], + ); + } + VCardProperty::Tel => { + state.map_named_entry( + &mut entry, + &[ + VCardParameterName::Type, + VCardParameterName::Pref, + VCardParameterName::PropId, + VCardParameterName::Label, + ], + JSContactProperty::Phones, + JSContactProperty::Number, + [], + ); + } + VCardProperty::ContactUri => { + state.map_named_entry( + &mut entry, + &[ + VCardParameterName::Type, + VCardParameterName::Pref, + VCardParameterName::PropId, + VCardParameterName::Label, + VCardParameterName::Mediatype, + ], + JSContactProperty::Links, + JSContactProperty::Uri, + [( + Key::Property(JSContactProperty::Kind), + Value::Element(JSContactValue::Kind(JSContactKind::Contact)), + )], + ); + } + VCardProperty::Title => { + state.map_named_entry( + &mut entry, + &[ + VCardParameterName::PropId, + VCardParameterName::Altid, + VCardParameterName::Language, + ], + JSContactProperty::Titles, + JSContactProperty::Name, + [( + Key::Property(JSContactProperty::Kind), + Value::Element(JSContactValue::Kind(JSContactKind::Title)), + )], + ); + } + VCardProperty::Role => { + let prop_id = state + .find_prop_id( + &VCardProperty::Org, + entry.entry.group.as_deref(), + entry.entry.alt_id(), + ) + .map(|s| s.to_string()); + state.map_named_entry( + &mut entry, + &[ + VCardParameterName::PropId, + VCardParameterName::Altid, + VCardParameterName::Language, + ], + JSContactProperty::Titles, + JSContactProperty::Name, + [ + Some(( + Key::Property(JSContactProperty::Kind), + Value::Element(JSContactValue::Kind(JSContactKind::Role)), + )), + prop_id.map(|prop_id| { + ( + Key::Property(JSContactProperty::OrganizationId), + Value::Str(prop_id.into()), + ) + }), + ] + .into_iter() + .flatten(), + ); + } + VCardProperty::Hobby | VCardProperty::Interest | VCardProperty::Expertise => { + let kind = match &entry.entry.name { + VCardProperty::Expertise => JSContactKind::Expertise, + VCardProperty::Hobby => JSContactKind::Hobby, + VCardProperty::Interest => JSContactKind::Interest, + _ => unreachable!(), + }; + state.map_named_entry( + &mut entry, + &[ + VCardParameterName::Level, + VCardParameterName::Index, + VCardParameterName::Label, + VCardParameterName::PropId, + VCardParameterName::Altid, + VCardParameterName::Language, + ], + JSContactProperty::PersonalInfo, + JSContactProperty::Value, + [( + Key::Property(JSContactProperty::Kind), + Value::Element(JSContactValue::Kind(kind)), + )], + ); + } + VCardProperty::Org => { + let units = entry + .text_parts_borrowed() + .skip(1) + .map(|unit| { + Value::Object(Map::from(vec![( + Key::Property(JSContactProperty::Name), + Value::Str(unit.to_string().into()), + )])) + }) + .collect::>(); + + state.map_named_entry( + &mut entry, + &[ + VCardParameterName::Type, + VCardParameterName::PropId, + VCardParameterName::SortAs, + ], + JSContactProperty::Organizations, + JSContactProperty::Name, + [(!units.is_empty()).then_some({ + (Key::Property(JSContactProperty::Units), Value::Array(units)) + })] + .into_iter() + .flatten(), + ); + } + VCardProperty::Member | VCardProperty::Categories => { + let key = if entry.entry.name == VCardProperty::Member { + JSContactProperty::Members + } else { + JSContactProperty::Keywords + }; + + entry.set_converted_to::(&[key.to_cow().as_ref()]); + + let obj = state.get_mut_object_or_insert(key); + + for key in entry.entry.values.drain(..).filter_map(|v| v.into_text()) { + obj.insert(Key::from(key), Value::Bool(true)); + } + } + VCardProperty::Created | VCardProperty::Rev => { + let key = Key::Property(match &entry.entry.name { + VCardProperty::Created => JSContactProperty::Created, + VCardProperty::Rev => JSContactProperty::Updated, + _ => unreachable!(), + }); + + if !state.entries.contains_key(&key) + && let Some(text) = entry.to_timestamp() + { + entry.set_converted_to::(&[key.to_string().as_ref()]); + state.entries.insert(key, text); + } + } + VCardProperty::Note => { + state.map_named_entry( + &mut entry, + &[ + VCardParameterName::Author, + VCardParameterName::AuthorName, + VCardParameterName::Created, + VCardParameterName::Language, + VCardParameterName::PropId, + ], + JSContactProperty::Notes, + JSContactProperty::Note, + [], + ); + } + VCardProperty::Url | VCardProperty::Key | VCardProperty::Caladruri => { + let prop = match &entry.entry.name { + VCardProperty::Url => JSContactProperty::Links, + VCardProperty::Key => JSContactProperty::CryptoKeys, + VCardProperty::Caladruri => JSContactProperty::SchedulingAddresses, + _ => unreachable!(), + }; + state.map_named_entry( + &mut entry, + &[ + VCardParameterName::Type, + VCardParameterName::Pref, + VCardParameterName::PropId, + VCardParameterName::Label, + VCardParameterName::Mediatype, + ], + prop, + JSContactProperty::Uri, + [], + ); + } + VCardProperty::Fburl | VCardProperty::Caluri => { + let kind = match &entry.entry.name { + VCardProperty::Fburl => JSContactKind::FreeBusy, + VCardProperty::Caluri => JSContactKind::Calendar, + _ => unreachable!(), + }; + state.map_named_entry( + &mut entry, + &[ + VCardParameterName::Type, + VCardParameterName::Pref, + VCardParameterName::PropId, + VCardParameterName::Label, + VCardParameterName::Mediatype, + ], + JSContactProperty::Calendars, + JSContactProperty::Uri, + [( + Key::Property(JSContactProperty::Kind), + Value::Element(JSContactValue::Kind(kind)), + )], + ); + } + VCardProperty::Related => { + if let Some(text) = entry.to_string() { + let mut params = state + .extract_params(&mut entry.entry.params, &[VCardParameterName::Type]); + entry.set_converted_to::(&[ + JSContactProperty::RelatedTo::.to_cow().as_ref(), + text.as_ref(), + ]); + state + .get_mut_object_or_insert(JSContactProperty::RelatedTo) + .insert( + Key::from(text), + Value::Object(Map::from(vec![( + Key::Property(JSContactProperty::Relation), + Value::Object(Map::from_iter(params.types().into_iter().map( + |t| { + ( + Key::Owned(t.into_string().to_ascii_lowercase()), + Value::Bool(true), + ) + }, + ))), + )])), + ); + } + } + VCardProperty::Tz | VCardProperty::Geo => { + let (key, value) = match &entry.entry.name { + VCardProperty::Tz => { + (Key::Property(JSContactProperty::TimeZone), entry.to_tz()) + } + VCardProperty::Geo => ( + Key::Property(JSContactProperty::Coordinates), + entry.to_text(), + ), + _ => unreachable!(), + }; + + if let Some(value) = value { + let prop_id = state + .find_prop_id( + &VCardProperty::Adr, + entry.entry.group.as_deref(), + entry.entry.alt_id(), + ) + .map(|s| s.to_string()); + + entry.set_map_name(); + + let addresses = + state.get_mut_object_or_insert(JSContactProperty::Addresses); + if let Some(addr) = prop_id + .clone() + .and_then(|prop_id| addresses.get_mut(&Key::Owned(prop_id))) + .and_then(|v| v.as_object_mut()) + .filter(|v| !v.contains_key(&key)) + { + entry.set_converted_to::(&[ + JSContactProperty::Addresses::.to_cow().as_ref(), + prop_id.clone().unwrap().as_str(), + key.to_string().as_ref(), + ]); + addr.insert(key, value); + } else { + let prop_id = addresses.insert_named( + None, + Value::Object(Map::from(vec![(key.clone(), value)])), + ); + entry.set_converted_to::(&[ + JSContactProperty::Addresses::.to_cow().as_ref(), + prop_id.as_str(), + key.to_string().as_ref(), + ]); + } + } + } + VCardProperty::Other(name) + if name.eq_ignore_ascii_case("X-ABLabel") && entry.entry.group.is_some() => + { + if let Some(prop) = state.find_entry_by_group(entry.entry.group.as_deref()) { + let prop_id = prop.prop_id.to_string(); + let prop_js = Key::Property(prop.prop_js.clone()); + + if let Some(obj) = state + .entries + .get_mut(&prop_js) + .and_then(|v| v.as_object_mut()) + .and_then(|v| v.get_mut(&Key::Owned(prop_id.clone()))) + .and_then(|v| v.as_object_mut()) + .filter(|v| !v.contains_key(&Key::Property(JSContactProperty::Label))) + && let Some(value) = entry.to_text() + { + obj.insert_unchecked(Key::Property(JSContactProperty::Label), value); + entry.set_map_name(); + entry.set_converted_to::(&[ + prop_js.to_string().as_ref(), + prop_id.as_str(), + JSContactProperty::Label::.to_cow().as_ref(), + ]); + } + } + } + VCardProperty::Jsprop => { + if let Some(VCardParameter { + name: VCardParameterName::Jsptr, + value: VCardParameterValue::Text(ptr), + }) = entry.entry.params.first() + { + let ptr = JsonPointer::>::parse(ptr); + + if let Some(VCardValue::Text(text)) = entry.entry.values.first() + && let Ok(jscontact) = JSContact::parse(text) + { + state.patch_objects.push((ptr, jscontact.0.into_owned())); + continue; + } + } + } + VCardProperty::Version + | VCardProperty::Xml + | VCardProperty::Gender + | VCardProperty::Clientpidmap + | VCardProperty::Other(_) => (), + VCardProperty::Begin | VCardProperty::End => { + continue; + } + } + + state.add_conversion_props(entry); + } + + state.into_jscontact() + } +} diff --git a/vendor/calcard/src/jscontact/import/entry.rs b/vendor/calcard/src/jscontact/import/entry.rs new file mode 100644 index 00000000..fafa1c4e --- /dev/null +++ b/vendor/calcard/src/jscontact/import/entry.rs @@ -0,0 +1,271 @@ +/* + * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC + * + * SPDX-License-Identifier: Apache-2.0 OR MIT + */ + +use crate::{ + common::{IanaString, IanaType, writer::write_jscomps}, + jscontact::{ + JSContactGrammaticalGender, JSContactId, JSContactKind, JSContactProperty, JSContactType, + JSContactValue, + import::{EntryState, VCardParams}, + }, + vcard::{ + VCardEntry, VCardGramGender, VCardKind, VCardParameterName, VCardValue, VCardValueType, + }, +}; +use jmap_tools::{JsonPointer, Key, Map, Value}; +use std::borrow::Cow; + +#[allow(clippy::wrong_self_convention)] +impl EntryState { + pub(super) fn new(entry: VCardEntry) -> Self { + Self { + entry, + converted_to: None, + map_name: false, + } + } + + pub(super) fn jcal_parameters( + &mut self, + params: &mut VCardParams, + value_type: &mut Option>, + ) { + if self.entry.params.is_empty() && self.entry.group.is_none() { + return; + } + let (default_type, _) = self.entry.name.default_types(); + let default_type = default_type.unwrap_vcard(); + + for param in std::mem::take(&mut self.entry.params) { + let value = match ¶m.name { + VCardParameterName::Value => { + if let Some(v) = param + .value + .into_value_type() + .filter(|v| !v.is_iana_and(|v| v == &default_type)) + { + *value_type = Some(v); + } + continue; + } + + VCardParameterName::Created => { + let Some(IanaType::Iana(timestamp)) = param.value.into_timestamp() else { + continue; + }; + + Value::Element(JSContactValue::Timestamp(timestamp)) + } + VCardParameterName::Jscomps => { + let Some(v) = param.value.into_jscomps() else { + continue; + }; + let mut jscomps = String::new(); + let _ = write_jscomps(&mut jscomps, &v); + Value::Str(jscomps.into()) + } + VCardParameterName::Other(name) if name.eq_ignore_ascii_case("group") => { + // The GROUP parameter (Section 7.1 of [RFC7095]) does not convert to JSContact. + // It exclusively is for use in jCard and MUST NOT be set in a vCard. + continue; + } + VCardParameterName::Group => { + // The GROUP parameter (Section 7.1 of [RFC7095]) does not convert to JSContact. + // It exclusively is for use in jCard and MUST NOT be set in a vCard. + continue; + } + _ => Value::Str(param.value.into_text()), + }; + + params.0.entry(param.name).or_default().push(value); + } + + if let Some(group) = self.entry.group.take() { + params + .0 + .insert(VCardParameterName::Group, vec![group.into()]); + } + } + + pub(super) fn set_converted_to(&mut self, converted_to: &[&str]) { + self.converted_to = Some(JsonPointer::>::encode(converted_to)); + } + + pub(super) fn set_map_name(&mut self) { + self.map_name = true; + } + + pub(super) fn to_text( + &mut self, + ) -> Option, JSContactValue>> { + self.to_string().map(Into::into) + } + + pub(super) fn to_string(&mut self) -> Option> { + let mut values = std::mem::take(&mut self.entry.values).into_iter(); + match values.next()? { + VCardValue::Text(v) => Some(Cow::Owned(v)), + VCardValue::Component(v) => Some(Cow::Owned(v.join(","))), + VCardValue::Binary(data) => Some(Cow::Owned(data.to_unwrapped_string())), + VCardValue::Sex(v) => Some(v.as_str().into()), + VCardValue::GramGender(v) => Some(v.as_str().into()), + VCardValue::Kind(v) => Some(v.as_str().into()), + other => { + self.entry.values.push(other); + self.entry.values.extend(values); + None + } + } + } + + pub(super) fn text_parts_borrowed(&self) -> impl Iterator + '_ { + self.entry.values.iter().filter_map(|value| value.as_text()) + } + + pub(super) fn to_kind( + &mut self, + ) -> Option, JSContactValue>> { + let mut values = std::mem::take(&mut self.entry.values).into_iter(); + match values.next()? { + VCardValue::Kind(v) => Value::Element(JSContactValue::Kind(match v { + VCardKind::Application => JSContactKind::Application, + VCardKind::Device => JSContactKind::Device, + VCardKind::Group => JSContactKind::Group, + VCardKind::Individual => JSContactKind::Individual, + VCardKind::Location => JSContactKind::Location, + VCardKind::Org => JSContactKind::Org, + })) + .into(), + VCardValue::Text(v) => Value::Str(v.into()).into(), + VCardValue::Component(v) => Value::Str(v.join(",").into()).into(), + other => { + self.entry.values.push(other); + self.entry.values.extend(values); + None + } + } + } + + pub(super) fn to_gram_gender( + &mut self, + ) -> Option, JSContactValue>> { + let mut values = std::mem::take(&mut self.entry.values).into_iter(); + match values.next()? { + VCardValue::GramGender(v) => { + Value::Element(JSContactValue::GrammaticalGender(match v { + VCardGramGender::Animate => JSContactGrammaticalGender::Animate, + VCardGramGender::Common => JSContactGrammaticalGender::Common, + VCardGramGender::Feminine => JSContactGrammaticalGender::Feminine, + VCardGramGender::Inanimate => JSContactGrammaticalGender::Inanimate, + VCardGramGender::Masculine => JSContactGrammaticalGender::Masculine, + VCardGramGender::Neuter => JSContactGrammaticalGender::Neuter, + })) + .into() + } + VCardValue::Text(v) => Value::Str(v.into()).into(), + VCardValue::Component(v) => Value::Str(v.join(",").into()).into(), + other => { + self.entry.values.push(other); + self.entry.values.extend(values); + None + } + } + } + + pub(super) fn to_timestamp( + &mut self, + ) -> Option, JSContactValue>> { + if let Some(VCardValue::PartialDateTime(dt)) = self.entry.values.first() + && let Some(timestamp) = dt.to_timestamp() + { + return Value::Element(JSContactValue::Timestamp(timestamp)).into(); + } + + None + } + + pub(super) fn to_tz( + &mut self, + ) -> Option, JSContactValue>> { + let mut values = std::mem::take(&mut self.entry.values).into_iter(); + match values.next()? { + VCardValue::PartialDateTime(v) if v.has_zone() => { + let hour = v.tz_hour.unwrap_or_default(); + + Value::Str( + if hour != 0 { + format!("Etc/GMT{}{}", if v.tz_minus { "+" } else { "-" }, hour) + } else { + "Etc/UTC".to_string() + } + .into(), + ) + .into() + } + VCardValue::Text(v) => Value::Str(v.into()).into(), + VCardValue::Component(v) => Value::Str(v.join(",").into()).into(), + other => { + self.entry.values.push(other); + self.entry.values.extend(values); + None + } + } + } + + pub(super) fn to_anniversary( + &mut self, + ) -> Option, JSContactValue>> { + if let Some(VCardValue::PartialDateTime(dt)) = self.entry.values.first() { + if let Some(timestamp) = dt.to_timestamp() { + Some(Value::Object(Map::from(vec![ + ( + Key::Property(JSContactProperty::Type), + Value::Element(JSContactValue::Type(JSContactType::Timestamp)), + ), + ( + Key::Property(JSContactProperty::Utc), + Value::Element(JSContactValue::Timestamp(timestamp)), + ), + ]))) + } else if dt.day.is_some() || dt.month.is_some() || dt.year.is_some() { + let mut props = Map::from(vec![( + Key::Property(JSContactProperty::Type), + Value::Element(JSContactValue::Type(JSContactType::PartialDate)), + )]); + if let Some(year) = dt.year { + props.insert_unchecked( + Key::Property(JSContactProperty::Year), + Value::Number((year as u64).into()), + ); + } + if let Some(month) = dt.month { + props.insert_unchecked( + Key::Property(JSContactProperty::Month), + Value::Number((month as u64).into()), + ); + } + if let Some(day) = dt.day { + props.insert_unchecked( + Key::Property(JSContactProperty::Day), + Value::Number((day as u64).into()), + ); + } + + Some(Value::Object(props)) + } else { + None + } + } else { + None + } + } + + pub(super) fn non_default_language(&self, default_language: Option<&str>) -> Option<&str> { + self.entry + .language() + .filter(|&lang| Some(lang) != default_language) + } +} diff --git a/vendor/calcard/src/jscontact/import/mod.rs b/vendor/calcard/src/jscontact/import/mod.rs new file mode 100644 index 00000000..949505e6 --- /dev/null +++ b/vendor/calcard/src/jscontact/import/mod.rs @@ -0,0 +1,170 @@ +/* + * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC + * + * SPDX-License-Identifier: Apache-2.0 OR MIT + */ + +use crate::{ + common::{CalendarScale, IanaType}, + jscontact::{JSContactId, JSContactProperty, JSContactValue}, + vcard::{ + Jscomp, VCardEntry, VCardLevel, VCardParameterName, VCardPhonetic, VCardProperty, VCardType, + }, +}; +use ahash::AHashMap; +use jmap_tools::{JsonPointer, Key, Map, Value}; +use std::collections::HashMap; + +pub mod convert; +pub mod entry; +pub mod params; +pub mod props; + +#[derive(Debug, Clone, Copy)] +pub struct ConversionOptions { + pub include_vcard_parameters: bool, +} + +#[allow(clippy::type_complexity)] +struct State +where + I: JSContactId, + B: JSContactId, +{ + entries: AHashMap< + Key<'static, JSContactProperty>, + Value<'static, JSContactProperty, JSContactValue>, + >, + vcard_converted_properties: AHashMap>, + vcard_properties: Vec, JSContactValue>>, + patch_objects: Vec<( + JsonPointer>, + Value<'static, JSContactProperty, JSContactValue>, + )>, + localizations: HashMap< + String, + Vec<( + String, + Value<'static, JSContactProperty, JSContactValue>, + )>, + >, + default_language: Option, + prop_ids: Vec>, + name_alt_id: Option, + has_fn: bool, + has_fn_localization: bool, + has_n: bool, + has_n_localization: bool, + has_gram_gender: bool, + include_vcard_converted: bool, +} + +#[derive(Debug, Clone)] +struct EntryState { + entry: VCardEntry, + converted_to: Option, + map_name: bool, +} + +struct PropIdKey +where + I: JSContactId, +{ + prop: VCardProperty, + prop_js: JSContactProperty, + group: Option, + alt_id: Option, + prop_id: String, +} + +#[derive(Debug, Default)] +struct VCardConvertedProperty +where + I: JSContactId, + B: JSContactId, +{ + name: Option, + params: VCardParams, +} + +#[derive(Debug, Default)] +#[allow(clippy::type_complexity)] +struct VCardParams( + AHashMap, JSContactValue>>>, +) +where + I: JSContactId, + B: JSContactId; + +#[derive(Default)] +struct ExtractedParams { + language: Option, + pref: Option, + author: Option, + author_name: Option, + phonetic_system: Option>, + phonetic_script: Option, + media_type: Option, + calscale: Option>, + sort_as: Option, + prop_id: Option, + alt_id: Option, + types: Vec>, + geo: Option, + tz: Option, + index: Option, + level: Option>, + country_code: Option, + created: Option, + label: Option, + service_type: Option, + username: Option, + jscomps: Vec, +} + +trait GetObjectOrCreate +where + I: JSContactId, + B: JSContactId, +{ + fn get_mut_object_or_insert( + &mut self, + key: JSContactProperty, + ) -> &mut Map<'static, JSContactProperty, JSContactValue>; +} + +impl GetObjectOrCreate + for AHashMap< + Key<'static, JSContactProperty>, + Value<'static, JSContactProperty, JSContactValue>, + > +where + I: JSContactId, + B: JSContactId, +{ + #[inline] + fn get_mut_object_or_insert( + &mut self, + key: JSContactProperty, + ) -> &mut Map<'static, JSContactProperty, JSContactValue> { + self.entry(Key::Property(key)) + .or_insert_with(|| Value::Object(Map::from(Vec::new()))) + .as_object_mut() + .unwrap() + } +} + +impl Default for ConversionOptions { + fn default() -> Self { + Self { + include_vcard_parameters: true, + } + } +} + +impl ConversionOptions { + pub fn include_vcard_parameters(mut self, include: bool) -> Self { + self.include_vcard_parameters = include; + self + } +} diff --git a/vendor/calcard/src/jscontact/import/params.rs b/vendor/calcard/src/jscontact/import/params.rs new file mode 100644 index 00000000..e80e70ed --- /dev/null +++ b/vendor/calcard/src/jscontact/import/params.rs @@ -0,0 +1,280 @@ +/* + * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC + * + * SPDX-License-Identifier: Apache-2.0 OR MIT + */ + +use crate::{ + common::IanaType, + jscontact::{ + JSContactId, JSContactKind, JSContactLevel, JSContactPhoneticSystem, JSContactProperty, + JSContactValue, + import::{ExtractedParams, VCardParams}, + }, + vcard::{VCardLevel, VCardPhonetic, VCardProperty, VCardType}, +}; +use jmap_tools::{Key, Map, Value}; + +impl ExtractedParams { + pub(super) fn prop_id(&mut self) -> Option { + self.prop_id.take() + } + + pub(super) fn alt_id(&mut self) -> Option { + self.alt_id.take() + } + + pub(super) fn language(&mut self) -> Option { + self.language.take() + } + + pub(super) fn types(&mut self) -> Vec> { + std::mem::take(&mut self.types) + } + + #[allow(clippy::type_complexity)] + pub(super) fn into_iter( + mut self, + property: &VCardProperty, + ) -> impl Iterator< + Item = ( + Key<'static, JSContactProperty>, + Value<'static, JSContactProperty, JSContactValue>, + ), + > { + let mut contexts: Option< + Vec<( + Key<'static, JSContactProperty>, + Value<'static, JSContactProperty, JSContactValue>, + )>, + > = None; + let mut features: Option< + Vec<( + Key<'static, JSContactProperty>, + Value<'static, JSContactProperty, JSContactValue>, + )>, + > = None; + + if !self.types.is_empty() { + let is_phone = matches!(property, VCardProperty::Tel); + + for typ in std::mem::take(&mut self.types) { + if is_phone + && matches!( + typ, + IanaType::Iana( + VCardType::Fax + | VCardType::Cell + | VCardType::Video + | VCardType::Pager + | VCardType::Textphone + | VCardType::MainNumber + | VCardType::Text + | VCardType::Voice + ) + ) + { + features.get_or_insert_default() + } else { + contexts.get_or_insert_default() + } + .push(( + match typ { + IanaType::Iana(VCardType::Home) => Key::Borrowed("private"), + IanaType::Iana(VCardType::Cell) => Key::Borrowed("mobile"), + _ => Key::Owned(typ.into_string().to_ascii_lowercase()), + }, + Value::Bool(true), + )); + } + } + + let author = if self.author.is_some() || self.author_name.is_some() { + Value::Object(Map::from_iter( + [ + self.author_name.map(|name| { + ( + Key::Property(JSContactProperty::Name), + Value::Str(name.into()), + ) + }), + self.author.map(|author| { + ( + Key::Property(JSContactProperty::Uri), + Value::Str(author.into()), + ) + }), + ] + .into_iter() + .flatten(), + )) + .into() + } else { + None + }; + + [ + ( + Key::Property(JSContactProperty::Contexts), + contexts.map(|v| Value::Object(Map::from(v))), + ), + ( + Key::Property(JSContactProperty::Features), + features.map(|v| Value::Object(Map::from(v))), + ), + ( + Key::Property(JSContactProperty::Language), + self.language.map(Into::into).map(Value::Str), + ), + ( + Key::Property(JSContactProperty::Pref), + self.pref.map(|v| Value::Number((v as u64).into())), + ), + (Key::Property(JSContactProperty::Author), author), + ( + Key::Property(JSContactProperty::MediaType), + self.media_type.map(Into::into).map(Value::Str), + ), + ( + Key::Property(JSContactProperty::PhoneticSystem), + self.phonetic_system.map(|v| match v { + IanaType::Iana(value) => { + Value::Element(JSContactValue::PhoneticSystem(match value { + VCardPhonetic::Ipa => JSContactPhoneticSystem::Ipa, + VCardPhonetic::Jyut => JSContactPhoneticSystem::Jyut, + VCardPhonetic::Piny => JSContactPhoneticSystem::Piny, + VCardPhonetic::Script => JSContactPhoneticSystem::Script, + })) + } + IanaType::Other(value) => Value::Str(value.into()), + }), + ), + ( + Key::Property(JSContactProperty::PhoneticScript), + self.phonetic_script.map(Into::into).map(Value::Str), + ), + ( + Key::Property(JSContactProperty::CalendarScale), + self.calscale.map(|value| match value { + IanaType::Iana(value) => Value::Element(JSContactValue::CalendarScale(value)), + IanaType::Other(value) => Value::Str(value.into()), + }), + ), + ( + Key::Property(JSContactProperty::SortAs), + self.sort_as.map(|sort_as| { + if matches!(property, VCardProperty::N) { + if let Some((surname, given)) = + sort_as.split_once(',').and_then(|(surname, given)| { + let surname = surname.trim(); + let given = given.trim(); + + if surname.is_empty() || given.is_empty() { + None + } else { + Some((surname.to_string(), given.to_string())) + } + }) + { + Value::Object(Map::from_iter(vec![ + ( + Key::Property(JSContactProperty::SortAsKind( + JSContactKind::Surname, + )), + Value::Str(surname.into()), + ), + ( + Key::Property(JSContactProperty::SortAsKind( + JSContactKind::Given, + )), + Value::Str(given.into()), + ), + ])) + } else { + Value::Object(Map::from_iter(vec![( + Key::Property(JSContactProperty::SortAsKind( + JSContactKind::Surname, + )), + Value::Str(sort_as.into()), + )])) + } + } else { + Value::Str(sort_as.into()) + } + }), + ), + ( + Key::Property(JSContactProperty::Coordinates), + self.geo.map(Into::into).map(Value::Str), + ), + ( + Key::Property(JSContactProperty::TimeZone), + self.tz.map(Into::into).map(Value::Str), + ), + ( + Key::Property(JSContactProperty::ListAs), + self.index.map(|v| Value::Number((v as u64).into())), + ), + ( + Key::Property(JSContactProperty::Level), + self.level.map(|value| match value { + IanaType::Iana(value) => Value::Element(JSContactValue::Level(match value { + VCardLevel::Beginner | VCardLevel::Low => JSContactLevel::Low, + VCardLevel::Average | VCardLevel::Medium => JSContactLevel::Medium, + VCardLevel::Expert | VCardLevel::High => JSContactLevel::High, + })), + IanaType::Other(value) => Value::Str(value.into()), + }), + ), + ( + Key::Property(JSContactProperty::CountryCode), + self.country_code.map(Into::into).map(Value::Str), + ), + ( + Key::Property(JSContactProperty::Created), + self.created + .map(|v| Value::Element(JSContactValue::Timestamp(v))), + ), + ( + Key::Property(if matches!(property, VCardProperty::Adr) { + JSContactProperty::Full + } else { + JSContactProperty::Label + }), + self.label.map(Into::into).map(Value::Str), + ), + ( + Key::Property(JSContactProperty::Service), + self.service_type.map(Into::into).map(Value::Str), + ), + ( + Key::Property(JSContactProperty::User), + self.username.map(Into::into).map(Value::Str), + ), + ] + .into_iter() + .filter_map(|(k, v)| v.map(|v| (k, v))) + } +} + +impl VCardParams { + pub(super) fn into_jscontact_value( + self, + ) -> Option, JSContactValue>> { + if !self.0.is_empty() { + let mut obj = Map::from(Vec::with_capacity(self.0.len())); + + for (param, value) in self.0 { + let value = if value.len() > 1 { + Value::Array(value) + } else { + value.into_iter().next().unwrap() + }; + obj.insert_unchecked(Key::Owned(param.into_string().to_ascii_lowercase()), value); + } + Some(obj) + } else { + None + } + } +} diff --git a/vendor/calcard/src/jscontact/import/props.rs b/vendor/calcard/src/jscontact/import/props.rs new file mode 100644 index 00000000..66fa9b86 --- /dev/null +++ b/vendor/calcard/src/jscontact/import/props.rs @@ -0,0 +1,655 @@ +/* + * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC + * + * SPDX-License-Identifier: Apache-2.0 OR MIT + */ + +use crate::{ + common::{IanaString, IanaType}, + jscontact::{ + JSContact, JSContactId, JSContactProperty, JSContactType, JSContactValue, + import::{ + EntryState, ExtractedParams, PropIdKey, State, VCardConvertedProperty, VCardParams, + }, + }, + vcard::{ + VCard, VCardEntry, VCardParameter, VCardParameterName, VCardProperty, VCardValue, + VCardValueType, + }, +}; +use ahash::AHashMap; +use jmap_tools::{JsonPointerHandler, Key, Map, Property, Value}; +use std::{ + borrow::Cow, + collections::{HashMap, HashSet, hash_map::Entry}, +}; + +impl State +where + I: JSContactId, + B: JSContactId, +{ + pub(super) fn new(vcard: &mut VCard, include_vcard_converted: bool) -> Self { + let mut entries = AHashMap::with_capacity(vcard.entries.len()); + + entries.extend([ + ( + Key::Property(JSContactProperty::Type), + Value::Element(JSContactValue::Type(JSContactType::Card)), + ), + ( + Key::Property(JSContactProperty::Version), + Value::Str("1.0".into()), + ), + ]); + + // Find the default language and the most "popular" alt ids + let mut default_language = None; + let mut language_map: HashMap = HashMap::new(); + let mut language_count = 0; + let mut language_first_found = None; + let mut langful_names: HashSet<&VCardProperty> = HashSet::new(); + let mut alt_ids: AHashMap<(&VCardProperty, &str), usize> = AHashMap::new(); + for entry in &vcard.entries { + if let Some(lang) = entry.language() { + *language_map.entry(lang.to_ascii_lowercase()).or_default() += 1; + if language_first_found.is_none() { + language_first_found = Some(lang); + } + language_count += 1; + langful_names.insert(&entry.name); + } + + match &entry.name { + VCardProperty::Language => { + if let Some(VCardValue::Text(lang)) = entry.values.first() { + default_language = Some(lang.to_ascii_lowercase()); + } + } + VCardProperty::N | VCardProperty::Adr => { + if let Some(alt_id) = entry.alt_id() { + *alt_ids.entry((&entry.name, alt_id)).or_default() += 1; + } + } + _ => (), + } + } + + // Find the alt ids with the highest count + let mut name_alt_id = None; + let mut name_alt_id_count = 0; + for (&(prop, alt_id), &count) in &alt_ids { + match prop { + VCardProperty::N if count > name_alt_id_count => { + name_alt_id = Some(alt_id.to_string()); + name_alt_id_count = count; + } + _ => (), + } + } + + // Find the dominant language + if default_language.is_none() + && language_count > 1 + && let Some((_, &min_count)) = language_map.iter().min_by_key(|&(_, count)| count) + && let Some((mut lang, max_count)) = + language_map.into_iter().max_by_key(|&(_, count)| count) + && max_count + > vcard + .entries + .iter() + .filter(|entry| { + entry.language().is_none() && langful_names.contains(&entry.name) + }) + .count() + { + if max_count == min_count { + lang = language_first_found.unwrap().to_ascii_lowercase(); + } + + let lang = lang.to_ascii_lowercase(); + default_language = Some(lang.clone()); + vcard + .entries + .push(VCardEntry::new(VCardProperty::Language).with_value(lang)); + } + + // Move entries without a language to the top + vcard.entries.sort_unstable_by_key(|entry| { + let lang = entry.language(); + let weight = u32::from(lang.is_some() && default_language.as_deref() != lang); + + match &entry.name { + VCardProperty::Birthplace | VCardProperty::Deathplace | VCardProperty::Role => { + weight + 2 + } + VCardProperty::Other(name) if name.eq_ignore_ascii_case("X-ABLabel") => weight + 3, + _ => weight, + } + }); + + Self { + entries, + default_language, + localizations: Default::default(), + prop_ids: Default::default(), + vcard_converted_properties: Default::default(), + vcard_properties: Default::default(), + patch_objects: Default::default(), + name_alt_id, + has_fn: false, + has_n: false, + has_n_localization: false, + has_fn_localization: false, + has_gram_gender: false, + include_vcard_converted, + } + } + + #[allow(clippy::too_many_arguments)] + pub(super) fn map_named_entry( + &mut self, + entry: &mut EntryState, + extract: &[VCardParameterName], + top_property_name: JSContactProperty, + value_property_name: JSContactProperty, + extra_properties: impl IntoIterator< + Item = ( + Key<'static, JSContactProperty>, + Value<'static, JSContactProperty, JSContactValue>, + ), + >, + ) { + let value = if !matches!( + entry.entry.name, + VCardProperty::Anniversary | VCardProperty::Bday | VCardProperty::Deathdate + ) { + entry.to_text() + } else { + entry.to_anniversary() + }; + + if let Some(value) = value { + let mut params = self.extract_params(&mut entry.entry.params, extract); + let prop_id = params.prop_id(); + let alt_id = params.alt_id(); + let sub_property = top_property_name.sub_property(); + + // Locate the address to patch or reset language + if let Some(language) = params.language() { + if let Some(patch) = prop_id + .as_deref() + .filter(|prop_id| self.has_prop_id(&entry.entry.name, prop_id)) + .or_else(|| { + self.find_prop_id( + &entry.entry.name, + entry.entry.group.as_deref(), + alt_id.as_deref(), + ) + }) + .map(|prop_id| { + if let Some(sub_property) = sub_property.as_ref() { + format!( + "{}/{}/{}/{}", + top_property_name.to_cow().as_ref(), + sub_property.to_cow().as_ref(), + prop_id, + value_property_name.to_cow().as_ref() + ) + } else { + format!( + "{}/{}/{}", + top_property_name.to_cow().as_ref(), + prop_id, + value_property_name.to_cow().as_ref() + ) + } + }) + { + entry.set_converted_to::(&[ + JSContactProperty::Localizations::.to_cow().as_ref(), + language.as_str(), + patch.as_str(), + ]); + + let localizations = self.localizations.entry(language).or_default(); + let mut base_path = None; + + for (prop, value) in params.into_iter(&entry.entry.name) { + let base_path = base_path.get_or_insert_with(|| { + patch.rsplit_once('/').map(|(base, _)| base).unwrap() + }); + localizations.push((format!("{}/{}", base_path, prop.to_string()), value)); + } + + localizations.push((patch, value)); + return; + } else { + entry.entry.params.push(VCardParameter::language(language)); + } + } + + let mut entries = self.get_mut_object_or_insert(top_property_name.clone()); + if let Some(sub_property) = sub_property.clone() { + entries = entries + .insert_or_get_mut(sub_property, Value::Object(Map::from(vec![]))) + .as_object_mut() + .unwrap(); + } + + let mut obj = vec![(Key::Property(value_property_name.clone()), value)]; + obj.extend(extra_properties); + obj.extend(params.into_iter(&entry.entry.name)); + let prop_id = entries.insert_named(prop_id, Value::Object(Map::from(obj))); + + if let Some(sub_property) = sub_property { + entry.set_converted_to::(&[ + top_property_name.to_cow().as_ref(), + sub_property.to_cow().as_ref(), + prop_id.as_str(), + value_property_name.to_cow().as_ref(), + ]); + } else { + entry.set_converted_to::(&[ + top_property_name.to_cow().as_ref(), + prop_id.as_str(), + value_property_name.to_cow().as_ref(), + ]); + } + + self.track_prop(&entry.entry, top_property_name, alt_id, prop_id); + } + } + + pub(super) fn extract_params( + &self, + params: &mut Vec, + extract: &[VCardParameterName], + ) -> ExtractedParams { + let mut p = ExtractedParams::default(); + + for param in std::mem::take(params) { + match ¶m.name { + VCardParameterName::Language => { + let v = param.value.into_text().to_ascii_lowercase(); + if p.language.is_none() + && self.default_language.as_ref().is_none_or(|lang| lang != &v) + && extract.contains(&VCardParameterName::Language) + { + p.language = Some(v); + } else { + params.push(VCardParameter::language(v)); + } + } + VCardParameterName::Pref + if p.pref.is_none() && extract.contains(&VCardParameterName::Pref) => + { + p.pref = param.value.as_integer().and_then(|v| v.into_iana()); + } + VCardParameterName::Author + if p.author.is_none() && extract.contains(&VCardParameterName::Author) => + { + p.author = Some(param.value.into_text().into_owned()); + } + VCardParameterName::AuthorName + if p.author_name.is_none() + && extract.contains(&VCardParameterName::AuthorName) => + { + p.author_name = Some(param.value.into_text().into_owned()); + } + VCardParameterName::Mediatype + if p.media_type.is_none() + && extract.contains(&VCardParameterName::Mediatype) => + { + p.media_type = Some(param.value.into_text().into_owned()); + } + VCardParameterName::Calscale + if p.calscale.is_none() && extract.contains(&VCardParameterName::Calscale) => + { + p.calscale = param.value.into_calscale(); + } + VCardParameterName::SortAs + if p.sort_as.is_none() && extract.contains(&VCardParameterName::SortAs) => + { + p.sort_as = Some(param.value.into_text().into_owned()); + } + VCardParameterName::Geo + if p.geo.is_none() && extract.contains(&VCardParameterName::Geo) => + { + p.geo = Some(param.value.into_text().into_owned()); + } + VCardParameterName::Tz + if p.tz.is_none() && extract.contains(&VCardParameterName::Tz) => + { + p.tz = Some(param.value.into_text().into_owned()); + } + VCardParameterName::Index + if p.index.is_none() && extract.contains(&VCardParameterName::Index) => + { + p.index = param.value.as_integer().and_then(|v| v.into_iana()); + } + VCardParameterName::Level + if p.level.is_none() && extract.contains(&VCardParameterName::Level) => + { + p.level = param.value.into_level(); + } + VCardParameterName::Cc + if p.country_code.is_none() && extract.contains(&VCardParameterName::Cc) => + { + p.country_code = Some(param.value.into_text().into_owned()); + } + VCardParameterName::Created + if p.created.is_none() && extract.contains(&VCardParameterName::Created) => + { + p.created = param.value.into_timestamp().and_then(|v| v.into_iana()); + } + VCardParameterName::Label + if p.label.is_none() && extract.contains(&VCardParameterName::Label) => + { + p.label = Some(param.value.into_text().into_owned()); + } + VCardParameterName::Phonetic + if p.phonetic_system.is_none() + && extract.contains(&VCardParameterName::Phonetic) => + { + p.phonetic_system = param.value.into_phonetic(); + } + VCardParameterName::Script + if p.phonetic_script.is_none() + && extract.contains(&VCardParameterName::Script) => + { + p.phonetic_script = Some(param.value.into_text().into_owned()); + } + VCardParameterName::ServiceType + if p.service_type.is_none() + && extract.contains(&VCardParameterName::ServiceType) => + { + p.service_type = Some(param.value.into_text().into_owned()); + } + VCardParameterName::Username + if p.username.is_none() && extract.contains(&VCardParameterName::Username) => + { + p.username = Some(param.value.into_text().into_owned()); + } + VCardParameterName::PropId + if p.prop_id.is_none() && extract.contains(&VCardParameterName::PropId) => + { + p.prop_id = Some(param.value.into_text().into_owned()); + } + VCardParameterName::Altid if p.alt_id.is_none() => { + p.alt_id = param.value.as_text().map(|v| v.to_string()); + params.push(param); + } + VCardParameterName::Type if extract.contains(&VCardParameterName::Type) => { + if let Some(typ) = param.value.into_type() { + if p.types.is_empty() { + p.types = vec![typ]; + } else { + p.types.push(typ); + } + } + } + VCardParameterName::Jscomps if extract.contains(&VCardParameterName::Jscomps) => { + if let Some(jscomps) = param.value.into_jscomps() { + p.jscomps = jscomps; + } + } + _ => { + params.push(param); + } + } + } + + p + } + + #[inline] + pub(super) fn get_mut_object_or_insert( + &mut self, + key: JSContactProperty, + ) -> &mut Map<'static, JSContactProperty, JSContactValue> { + self.entries + .entry(Key::Property(key)) + .or_insert_with(|| Value::Object(Map::from(Vec::new()))) + .as_object_mut() + .unwrap() + } + + #[inline] + pub(super) fn has_property(&self, key: JSContactProperty) -> bool { + self.entries.contains_key(&Key::Property(key)) + } + + pub(super) fn add_conversion_props(&mut self, mut entry: EntryState) { + if self.include_vcard_converted { + if let Some(converted_to) = entry.converted_to.take() { + if entry.map_name || !entry.entry.params.is_empty() || entry.entry.group.is_some() { + let mut value_type = None; + + match self.vcard_converted_properties.entry(converted_to) { + Entry::Occupied(mut conv_prop) => { + entry.jcal_parameters(&mut conv_prop.get_mut().params, &mut value_type); + } + Entry::Vacant(conv_prop) => { + let mut params = VCardParams::default(); + entry.jcal_parameters(&mut params, &mut value_type); + if let Some(value_type) = value_type { + params.0.insert( + VCardParameterName::Value, + vec![Value::Str(value_type.into_string())], + ); + } + if !params.0.is_empty() || entry.map_name { + conv_prop.insert(VCardConvertedProperty { + name: if entry.map_name { + Some(entry.entry.name) + } else { + None + }, + params, + }); + } + } + } + } + } else { + let mut value_type = None; + let mut params = VCardParams::default(); + + entry.jcal_parameters(&mut params, &mut value_type); + + let values = if entry.entry.values.len() == 1 { + entry + .entry + .values + .into_iter() + .next() + .unwrap() + .into_jscontact_value(value_type.as_ref()) + } else { + let mut values = Vec::with_capacity(entry.entry.values.len()); + for value in entry.entry.values { + values.push(value.into_jscontact_value(value_type.as_ref())); + } + Value::Array(values) + }; + self.vcard_properties.push(Value::Array(vec![ + Value::Str(entry.entry.name.as_str().to_ascii_lowercase().into()), + Value::Object( + params + .into_jscontact_value() + .unwrap_or(Map::from(Vec::new())), + ), + Value::Str( + value_type + .map(|v| v.into_string()) + .unwrap_or(Cow::Borrowed("unknown")), + ), + values, + ])); + } + } + } + + #[inline] + pub(super) fn track_prop( + &mut self, + entry: &VCardEntry, + prop_js: JSContactProperty, + alt_id: Option, + prop_id: String, + ) { + self.prop_ids.push(PropIdKey { + prop_id, + prop_js, + prop: entry.name.clone(), + group: entry.group.clone(), + alt_id, + }); + } + + pub(super) fn find_prop_id( + &self, + prop: &VCardProperty, + group: Option<&str>, + alt_id: Option<&str>, + ) -> Option<&str> { + self.prop_ids + .iter() + .find(|p| { + p.prop == *prop && p.group.as_deref() == group && p.alt_id.as_deref() == alt_id + }) + .map(|p| p.prop_id.as_str()) + } + + pub(super) fn find_entry_by_group(&self, group: Option<&str>) -> Option<&PropIdKey> { + self.prop_ids.iter().find(|p| p.group.as_deref() == group) + } + + pub(super) fn has_prop_id(&self, prop: &VCardProperty, prop_id: &str) -> bool { + self.prop_ids + .iter() + .any(|p| p.prop == *prop && p.prop_id == prop_id) + } + + pub(super) fn into_jscontact(mut self) -> JSContact<'static, I, B> { + if !self.localizations.is_empty() { + self.entries.insert( + Key::Property(JSContactProperty::Localizations), + Value::Object( + self.localizations + .into_iter() + .map(|(lang, locals)| { + ( + Key::Owned(lang), + Value::Object( + locals + .into_iter() + .map(|(key, value)| (Key::Owned(key), value)) + .collect(), + ), + ) + }) + .collect(), + ), + ); + } + + let mut vcard_obj = Map::from(Vec::new()); + if !self.vcard_converted_properties.is_empty() { + let mut converted_properties = + Map::from(Vec::with_capacity(self.vcard_converted_properties.len())); + + for (converted_to, props) in self.vcard_converted_properties { + let mut obj = Map::from(Vec::with_capacity(2)); + if let Some(params) = props.params.into_jscontact_value() { + obj.insert( + Key::Property(JSContactProperty::Parameters), + Value::Object(params), + ); + } + if let Some(name) = props.name { + obj.insert( + Key::Property(JSContactProperty::Name), + Value::Str(name.as_str().to_ascii_lowercase().into()), + ); + } + + converted_properties.insert_unchecked(Key::Owned(converted_to), Value::Object(obj)); + } + + vcard_obj.insert_unchecked( + Key::Property(JSContactProperty::ConvertedProperties), + Value::Object(converted_properties), + ); + } + + if !self.vcard_properties.is_empty() { + vcard_obj.insert_unchecked( + Key::Property(JSContactProperty::Properties), + Value::Array(self.vcard_properties), + ); + } + + if !vcard_obj.is_empty() { + self.entries.insert( + Key::Property(JSContactProperty::VCard), + Value::Object(vcard_obj), + ); + } + + let mut obj = Value::Object(self.entries.into_iter().collect()); + if !self.patch_objects.is_empty() { + for (ptr, patch) in self.patch_objects { + obj.patch_jptr(ptr.iter(), patch); + } + } + + JSContact(obj) + } +} + +impl JSContactProperty +where + I: JSContactId, +{ + pub(super) fn sub_property(&self) -> Option> { + match self { + JSContactProperty::SpeakToAs => Some(JSContactProperty::Pronouns), + _ => None, + } + } +} + +impl VCardValue { + pub(super) fn into_jscontact_value( + self, + value_type: Option<&IanaType>, + ) -> Value<'static, JSContactProperty, JSContactValue> { + match self { + VCardValue::Text(v) => Value::Str(v.into()), + VCardValue::Component(v) => Value::Str(v.join(",").into()), + VCardValue::Integer(v) => Value::Number(v.into()), + VCardValue::Float(v) => Value::Number(v.into()), + VCardValue::Boolean(v) => Value::Bool(v), + VCardValue::PartialDateTime(v) => { + let mut out = String::new(); + let _ = v.format_as_vcard( + &mut out, + value_type + .and_then(|v| v.iana()) + .unwrap_or(if v.has_date() && v.has_time() { + &VCardValueType::Timestamp + } else { + &VCardValueType::DateAndOrTime + }), + ); + Value::Str(out.into()) + } + VCardValue::Binary(v) => Value::Str(v.to_unwrapped_string().into()), + VCardValue::Sex(v) => Value::Str(v.as_str().into()), + VCardValue::GramGender(v) => Value::Str(v.as_str().into()), + VCardValue::Kind(v) => Value::Str(v.as_str().into()), + } + } +} diff --git a/vendor/calcard/src/jscontact/mod.rs b/vendor/calcard/src/jscontact/mod.rs new file mode 100644 index 00000000..52fee34d --- /dev/null +++ b/vendor/calcard/src/jscontact/mod.rs @@ -0,0 +1,707 @@ +/* + * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC + * + * SPDX-License-Identifier: Apache-2.0 OR MIT + */ + +use crate::common::CalendarScale; +use jmap_tools::{JsonPointer, Value}; +use serde::Serialize; +use std::{ + borrow::Cow, + fmt::{Debug, Display}, + hash::Hash, + str::FromStr, +}; + +pub mod export; +pub mod import; +pub mod parser; +pub mod types; + +#[cfg(test)] +thread_local! { + static FN_DERIVATION_DISABLED: std::cell::Cell = const { std::cell::Cell::new(false) }; +} + +#[cfg(test)] +pub(crate) fn fn_derivation_disabled() -> bool { + FN_DERIVATION_DISABLED.with(|flag| flag.get()) +} + +#[cfg(test)] +pub(crate) fn set_fn_derivation_disabled(disabled: bool) { + FN_DERIVATION_DISABLED.with(|flag| flag.set(disabled)); +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize)] +#[repr(transparent)] +pub struct JSContact<'x, I, B>(pub Value<'x, JSContactProperty, JSContactValue>) +where + I: JSContactId, + B: JSContactId; + +pub trait JSContactId: + FromStr + Sized + Serialize + Display + Clone + Eq + Hash + Ord + Debug + Default +{ +} + +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum JSContactValue { + Id(I), + BlobId(B), + IdReference(String), + Timestamp(i64), + Type(JSContactType), + GrammaticalGender(JSContactGrammaticalGender), + Kind(JSContactKind), + Level(JSContactLevel), + Relation(JSContactRelation), + PhoneticSystem(JSContactPhoneticSystem), + CalendarScale(CalendarScale), +} + +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum JSContactProperty { + Type, + Address, + AddressBookIds, + Addresses, + Anniversaries, + Author, + BlobId, + Calendars, + CalendarScale, + Components, + Contexts, + ConvertedProperties, + Coordinates, + CountryCode, + Created, + CryptoKeys, + Date, + Day, + DefaultSeparator, + Directories, + Emails, + Extra, + Features, + Full, + GrammaticalGender, + Id, + IsOrdered, + Keywords, + Kind, + Label, + Language, + Level, + Links, + ListAs, + Localizations, + Media, + MediaType, + Members, + Month, + Name, + Nicknames, + Note, + Notes, + Number, + OnlineServices, + OrganizationId, + Organizations, + Parameters, + PersonalInfo, + Phones, + Phonetic, + PhoneticScript, + PhoneticSystem, + Place, + Pref, + PreferredLanguages, + ProdId, + Pronouns, + Properties, + RelatedTo, + Relation, + SchedulingAddresses, + Service, + SortAs, + SpeakToAs, + TimeZone, + Titles, + Uid, + Units, + Updated, + Uri, + User, + Utc, + VCard, + Value, + Version, + Year, + IdValue(I), + IdReference(String), + Context(Context), + Feature(Feature), + SortAsKind(JSContactKind), + Pointer(JsonPointer>), +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum JSContactType { + Address, + AddressComponent, + Anniversary, + Author, + Boolean, + Calendar, + Card, + CryptoKey, + Directory, + EmailAddress, + Id, + Int, + JCardProp, + LanguagePref, + Link, + Media, + Name, + NameComponent, + Nickname, + Note, + Number, + OnlineService, + Organization, + OrgUnit, + PartialDate, + PatchObject, + PersonalInfo, + Phone, + Pronouns, + Relation, + Resource, + SchedulingAddress, + SpeakToAs, + String, + Timestamp, + Title, + UnsignedInt, + UTCDateTime, +} + +impl FromStr for JSContactType { + type Err = (); + fn from_str(s: &str) -> Result { + hashify::map!(s.as_bytes(), JSContactType, + "Address" => JSContactType::Address, + "AddressComponent" => JSContactType::AddressComponent, + "Anniversary" => JSContactType::Anniversary, + "Author" => JSContactType::Author, + "Boolean" => JSContactType::Boolean, + "Calendar" => JSContactType::Calendar, + "Card" => JSContactType::Card, + "CryptoKey" => JSContactType::CryptoKey, + "Directory" => JSContactType::Directory, + "EmailAddress" => JSContactType::EmailAddress, + "Id" => JSContactType::Id, + "Int" => JSContactType::Int, + "JCardProp" => JSContactType::JCardProp, + "LanguagePref" => JSContactType::LanguagePref, + "Link" => JSContactType::Link, + "Media" => JSContactType::Media, + "Name" => JSContactType::Name, + "NameComponent" => JSContactType::NameComponent, + "Nickname" => JSContactType::Nickname, + "Note" => JSContactType::Note, + "Number" => JSContactType::Number, + "OnlineService" => JSContactType::OnlineService, + "Organization" => JSContactType::Organization, + "OrgUnit" => JSContactType::OrgUnit, + "PartialDate" => JSContactType::PartialDate, + "PatchObject" => JSContactType::PatchObject, + "PersonalInfo" => JSContactType::PersonalInfo, + "Phone" => JSContactType::Phone, + "Pronouns" => JSContactType::Pronouns, + "Relation" => JSContactType::Relation, + "Resource" => JSContactType::Resource, + "SchedulingAddress" => JSContactType::SchedulingAddress, + "SpeakToAs" => JSContactType::SpeakToAs, + "String" => JSContactType::String, + "Timestamp" => JSContactType::Timestamp, + "Title" => JSContactType::Title, + "UnsignedInt" => JSContactType::UnsignedInt, + "UTCDateTime" => JSContactType::UTCDateTime, + ) + .copied() + .ok_or(()) + } +} + +impl JSContactType { + pub fn as_str(&self) -> &'static str { + match self { + JSContactType::Address => "Address", + JSContactType::AddressComponent => "AddressComponent", + JSContactType::Anniversary => "Anniversary", + JSContactType::Author => "Author", + JSContactType::Boolean => "Boolean", + JSContactType::Calendar => "Calendar", + JSContactType::Card => "Card", + JSContactType::CryptoKey => "CryptoKey", + JSContactType::Directory => "Directory", + JSContactType::EmailAddress => "EmailAddress", + JSContactType::Id => "Id", + JSContactType::Int => "Int", + JSContactType::JCardProp => "JCardProp", + JSContactType::LanguagePref => "LanguagePref", + JSContactType::Link => "Link", + JSContactType::Media => "Media", + JSContactType::Name => "Name", + JSContactType::NameComponent => "NameComponent", + JSContactType::Nickname => "Nickname", + JSContactType::Note => "Note", + JSContactType::Number => "Number", + JSContactType::OnlineService => "OnlineService", + JSContactType::Organization => "Organization", + JSContactType::OrgUnit => "OrgUnit", + JSContactType::PartialDate => "PartialDate", + JSContactType::PatchObject => "PatchObject", + JSContactType::PersonalInfo => "PersonalInfo", + JSContactType::Phone => "Phone", + JSContactType::Pronouns => "Pronouns", + JSContactType::Relation => "Relation", + JSContactType::Resource => "Resource", + JSContactType::SchedulingAddress => "SchedulingAddress", + JSContactType::SpeakToAs => "SpeakToAs", + JSContactType::String => "String", + JSContactType::Timestamp => "Timestamp", + JSContactType::Title => "Title", + JSContactType::UnsignedInt => "UnsignedInt", + JSContactType::UTCDateTime => "UTCDateTime", + } + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum Context { + Billing, + Delivery, + Private, + Work, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum Feature { + Fax, + MainNumber, + Mobile, + Pager, + Text, + TextPhone, + Video, + Voice, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum JSContactGrammaticalGender { + Animate, + Common, + Feminine, + Inanimate, + Masculine, + Neuter, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum JSContactKind { + Apartment, + Block, + Building, + Country, + Direction, + District, + Floor, + Landmark, + Locality, + Name, + Number, + Postcode, + PostOfficeBox, + Region, + Room, + Separator, + Subdistrict, + Birth, + Death, + Wedding, + Calendar, + FreeBusy, + Application, + Device, + Group, + Individual, + Location, + Org, + Directory, + Entry, + Contact, + Logo, + Photo, + Sound, + Credential, + Generation, + Given, + Given2, + Surname, + Surname2, + Title, + Expertise, + Hobby, + Interest, + Role, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum JSContactLevel { + High, + Low, + Medium, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum JSContactRelation { + Acquaintance, + Agent, + Child, + Colleague, + Contact, + CoResident, + CoWorker, + Crush, + Date, + Emergency, + Friend, + Kin, + Me, + Met, + Muse, + Neighbor, + Parent, + Sibling, + Spouse, + Sweetheart, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum JSContactPhoneticSystem { + Ipa, + Jyut, + Piny, + Script, +} + +#[cfg(test)] +impl std::fmt::Display for JSContact<'_, I, B> +where + I: JSContactId, + B: JSContactId, +{ + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let s = serde_json::to_string_pretty(&self.0).map_err(|_| std::fmt::Error)?; + write!(f, "{}", s) + } +} + +impl JSContactId for T where + T: FromStr + Sized + Serialize + Display + Clone + Eq + Hash + Ord + Debug + Default +{ +} + +impl From for JSContactProperty { + fn from(id: I) -> Self { + JSContactProperty::IdValue(id) + } +} + +impl From for JSContactValue { + fn from(id: I) -> Self { + JSContactValue::Id(id) + } +} + +impl<'x, I: JSContactId, B: JSContactId> Default for JSContact<'x, I, B> { + fn default() -> Self { + Self(Value::Object(jmap_tools::Map::new())) + } +} + +impl Display for JSContactProperty { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str(self.to_string().as_ref()) + } +} + +#[cfg(test)] +mod tests { + use jmap_tools::{Key, Value}; + + use crate::{ + jscontact::{JSContact, JSContactProperty, JSContactValue}, + vcard::{VCard, VCardProperty}, + }; + + #[derive(Debug, Default)] + struct Test { + comment: String, + test: String, + expect: String, + roundtrip: String, + line_num: usize, + } + + #[test] + fn convert_jscontact() { + // Read all test files in the test directory + for entry in std::fs::read_dir("resources/jscontact").unwrap() { + let entry = entry.unwrap(); + let path = entry.path(); + let input = std::fs::read_to_string(&path).unwrap(); + super::set_fn_derivation_disabled( + path.file_name().and_then(|name| name.to_str()) != Some("003_names.txt"), + ); + let mut test = Test::default(); + let mut cur_command = ""; + let mut cur_value = &mut test.test; + + for (line_num, line) in input.lines().enumerate() { + if line.trim().is_empty() { + continue; + } + if let Some(line) = line.strip_prefix("> ") { + let (command, comment) = line.split_once(' ').unwrap_or((line, "")); + + match (command, cur_command) { + ("test", _) => { + if !test.test.is_empty() { + test.run(); + } + test = Test::default(); + cur_value = &mut test.test; + cur_command = "test"; + test.comment = comment.to_string(); + test.line_num = line_num + 1; + } + ("convert", "test") => { + cur_command = "convert"; + cur_value = &mut test.expect; + } + ("convert", "convert") => { + cur_command = "convert"; + cur_value = &mut test.roundtrip; + } + _ => { + panic!( + "Unexpected command '{}' in file '{}' at line {}", + command, + path.display(), + line_num + 1 + ); + } + } + } else { + cur_value.push_str(line); + cur_value.push('\n'); + } + } + + if !test.test.is_empty() { + test.run(); + } + } + } + + impl Test { + fn run(mut self) { + if self.expect.is_empty() { + panic!( + "Test '{}' at line {} has no expected output", + self.comment, self.line_num + ); + } + + println!("Running test '{}' at line {}", self.comment, self.line_num); + + if is_jscontact(&self.test) { + fix_jscontact(&mut self.test); + fix_vcard(&mut self.expect); + let source = + sanitize_jscontact(parse_jscontact(&self.comment, self.line_num, &self.test)); + let expect = + sanitize_vcard(parse_vcard(&self.comment, self.line_num, &self.expect)); + let roundtrip = if !self.roundtrip.is_empty() { + fix_jscontact(&mut self.roundtrip); + sanitize_jscontact(parse_jscontact( + &self.comment, + self.line_num, + &self.roundtrip, + )) + } else { + source.clone() + }; + + let first_convert = sanitize_vcard(source.into_vcard().unwrap_or_else(|| { + panic!( + "Failed to convert JSContact to vCard: test {} on line {}: {}", + self.comment, self.line_num, self.test + ) + })); + if first_convert != expect { + let first_convert = + sanitize_vcard(VCard::parse(first_convert.to_string()).unwrap()); + + if first_convert != expect { + panic!( + "JSContact to vCard conversion failed: test {} on line {}, expected: {}, got: {}", + self.comment, self.line_num, expect, first_convert + ); + } + } + let roundtrip_convert = sanitize_jscontact(first_convert.into_jscontact()); + if roundtrip_convert != roundtrip { + let roundtrip_convert = roundtrip_convert.to_string(); + let roundtrip = roundtrip.to_string(); + + if roundtrip_convert != roundtrip { + panic!( + "vCard to JSContact conversion failed: test {} on line {}, expected: {}, got: {}", + self.comment, self.line_num, roundtrip, roundtrip_convert + ); + } + } + } else { + fix_vcard(&mut self.test); + fix_jscontact(&mut self.expect); + let source = sanitize_vcard(parse_vcard(&self.comment, self.line_num, &self.test)); + let expect = + sanitize_jscontact(parse_jscontact(&self.comment, self.line_num, &self.expect)); + let roundtrip = if !self.roundtrip.is_empty() { + fix_vcard(&mut self.roundtrip); + sanitize_vcard(parse_vcard(&self.comment, self.line_num, &self.roundtrip)) + } else { + source.clone() + }; + let first_convert = sanitize_jscontact(source.into_jscontact()); + if first_convert != expect { + let first_convert = first_convert.to_string(); + let expect = expect.to_string(); + + if first_convert != expect { + panic!( + "vCard to JSContact conversion failed: test {} on line {}, expected: {}, got: {}", + self.comment, self.line_num, expect, first_convert + ); + } + } + let roundtrip_convert = + sanitize_vcard(first_convert.into_vcard().unwrap_or_else(|| { + panic!( + "Failed to convert JSContact to vCard: test {} on line {}: {}", + self.comment, self.line_num, self.test + ) + })); + if roundtrip_convert != roundtrip { + let roundtrip_convert = + sanitize_vcard(VCard::parse(roundtrip_convert.to_string()).unwrap()); + if roundtrip_convert != roundtrip { + panic!( + "JSContact to vCard conversion failed: test {} on line {}, expected: {}, got: {}", + self.comment, self.line_num, roundtrip, roundtrip_convert + ); + } + } + } + } + } + + fn is_jscontact(s: &str) -> bool { + s.starts_with("{") || s.starts_with("\"") + } + + fn fix_vcard(s: &mut String) { + if !s.starts_with("BEGIN:VCARD") { + let mut v = "BEGIN:VCARD\nVERSION:4.0\n".to_string(); + v.push_str(s); + v.push_str("END:VCARD\n"); + *s = v; + } + } + + fn fix_jscontact(s: &mut String) { + if !s.starts_with("{") { + let mut v = "{\"@type\": \"Card\", \"version\": \"1.0\",\n".to_string(); + v.push_str(s); + v.push_str("\n}\n"); + *s = v; + } + } + + fn parse_vcard(test_name: &str, line_num: usize, s: &str) -> VCard { + VCard::parse(s).unwrap_or_else(|_| { + panic!( + "Failed to parse vCard: {} on line {}, test {}", + s, line_num, test_name + ) + }) + } + + fn parse_jscontact<'x>( + test_name: &str, + line_num: usize, + s: &'x str, + ) -> JSContact<'x, String, String> { + JSContact::parse(s).unwrap_or_else(|_| { + panic!( + "Failed to parse JSContact: {} on line {}, test {}", + s, line_num, test_name + ) + }) + } + + fn sanitize_vcard(mut vcard: VCard) -> VCard { + vcard + .entries + .retain(|e| !matches!(e.name, VCardProperty::Version)); + vcard.entries.sort_unstable_by(|a, b| a.name.cmp(&b.name)); + vcard + } + + fn sanitize_jscontact( + mut jscontact: JSContact<'_, String, String>, + ) -> JSContact<'_, String, String> { + sort_jscontact_properties(&mut jscontact.0); + jscontact + } + + fn sort_jscontact_properties( + value: &mut Value<'_, JSContactProperty, JSContactValue>, + ) { + match value { + Value::Array(value) => { + for item in value { + sort_jscontact_properties(item); + } + } + Value::Object(obj) => { + obj.as_mut_vec() + .retain(|(k, _)| !matches!(k, Key::Property(JSContactProperty::Type))); + obj.as_mut_vec() + .sort_unstable_by(|a, b| a.0.to_string().cmp(&b.0.to_string())); + for (_, item) in obj.as_mut_vec() { + sort_jscontact_properties(item); + } + } + _ => {} + } + } +} diff --git a/vendor/calcard/src/jscontact/parser.rs b/vendor/calcard/src/jscontact/parser.rs new file mode 100644 index 00000000..f29068e9 --- /dev/null +++ b/vendor/calcard/src/jscontact/parser.rs @@ -0,0 +1,150 @@ +/* + * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC + * + * SPDX-License-Identifier: Apache-2.0 OR MIT + */ + +use crate::{ + common::{CalendarScale, IanaParse, IdReference}, + jscontact::{ + Context, Feature, JSContact, JSContactGrammaticalGender, JSContactId, JSContactKind, + JSContactLevel, JSContactPhoneticSystem, JSContactProperty, JSContactRelation, + JSContactType, JSContactValue, + }, +}; +use chrono::DateTime; +use jmap_tools::{Element, JsonPointer, JsonPointerItem, Key, Value}; +use std::{borrow::Cow, str::FromStr}; + +impl<'x, I, B> JSContact<'x, I, B> +where + I: JSContactId, + B: JSContactId, +{ + pub fn parse(json: &'x str) -> Result { + Value::parse_json(json).map(JSContact) + } + + pub fn to_string_pretty(&self) -> String { + serde_json::to_string_pretty(&self.0).unwrap_or_default() + } +} + +impl Element for JSContactValue +where + I: JSContactId, + B: JSContactId, +{ + type Property = JSContactProperty; + + fn try_parse

(key: &Key<'_, Self::Property>, value: &str) -> Option { + if let Key::Property(prop) = key { + match prop { + JSContactProperty::Type => JSContactType::from_str(value) + .ok() + .map(JSContactValue::Type), + JSContactProperty::CalendarScale => { + CalendarScale::parse(value.as_bytes()).map(JSContactValue::CalendarScale) + } + JSContactProperty::Created + | JSContactProperty::Updated + | JSContactProperty::Utc => DateTime::parse_from_rfc3339(value) + .map(|dt| JSContactValue::Timestamp(dt.timestamp())) + .ok(), + JSContactProperty::Kind => JSContactKind::from_str(value) + .ok() + .map(JSContactValue::Kind), + JSContactProperty::GrammaticalGender => JSContactGrammaticalGender::from_str(value) + .ok() + .map(JSContactValue::GrammaticalGender), + JSContactProperty::PhoneticSystem => JSContactPhoneticSystem::from_str(value) + .ok() + .map(JSContactValue::PhoneticSystem), + JSContactProperty::Relation => JSContactRelation::from_str(value) + .ok() + .map(JSContactValue::Relation), + JSContactProperty::Level => JSContactLevel::from_str(value) + .ok() + .map(JSContactValue::Level), + JSContactProperty::BlobId => match IdReference::parse(value) { + IdReference::Value(value) => JSContactValue::BlobId(value).into(), + IdReference::Reference(value) => JSContactValue::IdReference(value).into(), + IdReference::Error => None, + }, + JSContactProperty::Id => match IdReference::parse(value) { + IdReference::Value(value) => JSContactValue::Id(value).into(), + IdReference::Reference(value) => JSContactValue::IdReference(value).into(), + IdReference::Error => None, + }, + _ => None, + } + } else { + None + } + } + + fn to_cow(&self) -> Cow<'static, str> { + match self { + JSContactValue::Type(v) => v.as_str().into(), + JSContactValue::GrammaticalGender(v) => v.as_str().into(), + JSContactValue::Kind(v) => v.as_str().into(), + JSContactValue::Level(v) => v.as_str().into(), + JSContactValue::Relation(v) => v.as_str().into(), + JSContactValue::PhoneticSystem(v) => v.as_str().into(), + JSContactValue::Timestamp(v) => mail_parser::DateTime::from_timestamp(*v) + .to_rfc3339() + .into(), + JSContactValue::CalendarScale(v) => v.as_js_str().into(), + JSContactValue::Id(v) => v.to_string().into(), + JSContactValue::BlobId(v) => v.to_string().into(), + JSContactValue::IdReference(s) => format!("#{}", s).into(), + } + } +} + +impl jmap_tools::Property for JSContactProperty { + fn try_parse(key: Option<&Key<'_, Self>>, value: &str) -> Option { + match key { + Some(Key::Property(key)) => match key.patch_or_prop() { + JSContactProperty::Contexts => Context::from_str(value) + .ok() + .map(JSContactProperty::Context), + JSContactProperty::Features => Feature::from_str(value) + .ok() + .map(JSContactProperty::Feature), + JSContactProperty::SortAs => JSContactKind::from_str(value) + .ok() + .map(JSContactProperty::SortAsKind), + JSContactProperty::ConvertedProperties | JSContactProperty::Localizations => { + JSContactProperty::Pointer(JsonPointer::parse(value)).into() + } + JSContactProperty::AddressBookIds => match IdReference::parse(value) { + IdReference::Value(value) => JSContactProperty::IdValue(value).into(), + IdReference::Reference(value) => JSContactProperty::IdReference(value).into(), + IdReference::Error => None, + }, + _ => JSContactProperty::from_str(value).ok(), + }, + None if value.contains('/') => { + JSContactProperty::Pointer(JsonPointer::parse(value)).into() + } + _ => JSContactProperty::from_str(value).ok(), + } + } + + fn to_cow(&self) -> Cow<'static, str> { + self.to_string() + } +} + +impl JSContactProperty { + fn patch_or_prop(&self) -> &JSContactProperty { + if let JSContactProperty::Pointer(ptr) = self + && let Some(JsonPointerItem::Key(Key::Property(prop))) = ptr.last() + { + prop + } else { + self + } + } +} diff --git a/vendor/calcard/src/jscontact/types.rs b/vendor/calcard/src/jscontact/types.rs new file mode 100644 index 00000000..a0675072 --- /dev/null +++ b/vendor/calcard/src/jscontact/types.rs @@ -0,0 +1,561 @@ +/* + * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC + * + * SPDX-License-Identifier: Apache-2.0 OR MIT + */ + +use crate::jscontact::*; +use std::str::FromStr; + +impl FromStr for JSContactProperty { + type Err = (); + fn from_str(s: &str) -> Result { + hashify::tiny_map!(s.as_bytes(), + "@type" => JSContactProperty::Type, + "address" => JSContactProperty::Address, + "addressBookIds" => JSContactProperty::AddressBookIds, + "addresses" => JSContactProperty::Addresses, + "anniversaries" => JSContactProperty::Anniversaries, + "author" => JSContactProperty::Author, + "blobId" => JSContactProperty::BlobId, + "calendars" => JSContactProperty::Calendars, + "calendarScale" => JSContactProperty::CalendarScale, + "components" => JSContactProperty::Components, + "contexts" => JSContactProperty::Contexts, + "convertedProperties" => JSContactProperty::ConvertedProperties, + "coordinates" => JSContactProperty::Coordinates, + "countryCode" => JSContactProperty::CountryCode, + "created" => JSContactProperty::Created, + "cryptoKeys" => JSContactProperty::CryptoKeys, + "date" => JSContactProperty::Date, + "day" => JSContactProperty::Day, + "defaultSeparator" => JSContactProperty::DefaultSeparator, + "directories" => JSContactProperty::Directories, + "emails" => JSContactProperty::Emails, + "extra" => JSContactProperty::Extra, + "features" => JSContactProperty::Features, + "full" => JSContactProperty::Full, + "grammaticalGender" => JSContactProperty::GrammaticalGender, + "id" => JSContactProperty::Id, + "isOrdered" => JSContactProperty::IsOrdered, + "keywords" => JSContactProperty::Keywords, + "kind" => JSContactProperty::Kind, + "label" => JSContactProperty::Label, + "language" => JSContactProperty::Language, + "level" => JSContactProperty::Level, + "links" => JSContactProperty::Links, + "listAs" => JSContactProperty::ListAs, + "localizations" => JSContactProperty::Localizations, + "media" => JSContactProperty::Media, + "mediaType" => JSContactProperty::MediaType, + "members" => JSContactProperty::Members, + "month" => JSContactProperty::Month, + "name" => JSContactProperty::Name, + "nicknames" => JSContactProperty::Nicknames, + "note" => JSContactProperty::Note, + "notes" => JSContactProperty::Notes, + "number" => JSContactProperty::Number, + "onlineServices" => JSContactProperty::OnlineServices, + "organizationId" => JSContactProperty::OrganizationId, + "organizations" => JSContactProperty::Organizations, + "parameters" => JSContactProperty::Parameters, + "personalInfo" => JSContactProperty::PersonalInfo, + "phones" => JSContactProperty::Phones, + "phonetic" => JSContactProperty::Phonetic, + "phoneticScript" => JSContactProperty::PhoneticScript, + "phoneticSystem" => JSContactProperty::PhoneticSystem, + "place" => JSContactProperty::Place, + "pref" => JSContactProperty::Pref, + "preferredLanguages" => JSContactProperty::PreferredLanguages, + "prodId" => JSContactProperty::ProdId, + "pronouns" => JSContactProperty::Pronouns, + "properties" => JSContactProperty::Properties, + "relatedTo" => JSContactProperty::RelatedTo, + "relation" => JSContactProperty::Relation, + "schedulingAddresses" => JSContactProperty::SchedulingAddresses, + "service" => JSContactProperty::Service, + "sortAs" => JSContactProperty::SortAs, + "speakToAs" => JSContactProperty::SpeakToAs, + "timeZone" => JSContactProperty::TimeZone, + "titles" => JSContactProperty::Titles, + "uid" => JSContactProperty::Uid, + "units" => JSContactProperty::Units, + "updated" => JSContactProperty::Updated, + "uri" => JSContactProperty::Uri, + "user" => JSContactProperty::User, + "utc" => JSContactProperty::Utc, + "value" => JSContactProperty::Value, + "vCard" => JSContactProperty::VCard, + "version" => JSContactProperty::Version, + "year" => JSContactProperty::Year, + ) + .ok_or(()) + } +} + +impl JSContactProperty { + pub fn to_string(&self) -> Cow<'static, str> { + match self { + JSContactProperty::Type => "@type", + JSContactProperty::Address => "address", + JSContactProperty::AddressBookIds => "addressBookIds", + JSContactProperty::Addresses => "addresses", + JSContactProperty::Anniversaries => "anniversaries", + JSContactProperty::Author => "author", + JSContactProperty::BlobId => "blobId", + JSContactProperty::Calendars => "calendars", + JSContactProperty::CalendarScale => "calendarScale", + JSContactProperty::Components => "components", + JSContactProperty::Contexts => "contexts", + JSContactProperty::ConvertedProperties => "convertedProperties", + JSContactProperty::Coordinates => "coordinates", + JSContactProperty::CountryCode => "countryCode", + JSContactProperty::Created => "created", + JSContactProperty::CryptoKeys => "cryptoKeys", + JSContactProperty::Date => "date", + JSContactProperty::Day => "day", + JSContactProperty::DefaultSeparator => "defaultSeparator", + JSContactProperty::Directories => "directories", + JSContactProperty::Emails => "emails", + JSContactProperty::Extra => "extra", + JSContactProperty::Features => "features", + JSContactProperty::Full => "full", + JSContactProperty::GrammaticalGender => "grammaticalGender", + JSContactProperty::Id => "id", + JSContactProperty::IsOrdered => "isOrdered", + JSContactProperty::Keywords => "keywords", + JSContactProperty::Kind => "kind", + JSContactProperty::Label => "label", + JSContactProperty::Language => "language", + JSContactProperty::Level => "level", + JSContactProperty::Links => "links", + JSContactProperty::ListAs => "listAs", + JSContactProperty::Localizations => "localizations", + JSContactProperty::Media => "media", + JSContactProperty::MediaType => "mediaType", + JSContactProperty::Members => "members", + JSContactProperty::Month => "month", + JSContactProperty::Name => "name", + JSContactProperty::Nicknames => "nicknames", + JSContactProperty::Note => "note", + JSContactProperty::Notes => "notes", + JSContactProperty::Number => "number", + JSContactProperty::OnlineServices => "onlineServices", + JSContactProperty::OrganizationId => "organizationId", + JSContactProperty::Organizations => "organizations", + JSContactProperty::Parameters => "parameters", + JSContactProperty::PersonalInfo => "personalInfo", + JSContactProperty::Phones => "phones", + JSContactProperty::Phonetic => "phonetic", + JSContactProperty::PhoneticScript => "phoneticScript", + JSContactProperty::PhoneticSystem => "phoneticSystem", + JSContactProperty::Place => "place", + JSContactProperty::Pref => "pref", + JSContactProperty::PreferredLanguages => "preferredLanguages", + JSContactProperty::ProdId => "prodId", + JSContactProperty::Pronouns => "pronouns", + JSContactProperty::Properties => "properties", + JSContactProperty::RelatedTo => "relatedTo", + JSContactProperty::Relation => "relation", + JSContactProperty::SchedulingAddresses => "schedulingAddresses", + JSContactProperty::Service => "service", + JSContactProperty::SortAs => "sortAs", + JSContactProperty::SpeakToAs => "speakToAs", + JSContactProperty::TimeZone => "timeZone", + JSContactProperty::Titles => "titles", + JSContactProperty::Uid => "uid", + JSContactProperty::Units => "units", + JSContactProperty::Updated => "updated", + JSContactProperty::Uri => "uri", + JSContactProperty::User => "user", + JSContactProperty::Utc => "utc", + JSContactProperty::Value => "value", + JSContactProperty::VCard => "vCard", + JSContactProperty::Version => "version", + JSContactProperty::Year => "year", + JSContactProperty::Context(context) => context.as_str(), + JSContactProperty::Feature(feature) => feature.as_str(), + JSContactProperty::SortAsKind(kind) => kind.as_str(), + JSContactProperty::Pointer(pointer) => return Cow::Owned(pointer.to_string()), + JSContactProperty::IdValue(id) => return Cow::Owned(id.to_string()), + JSContactProperty::IdReference(s) => return Cow::Owned(format!("#{}", s)), + } + .into() + } +} + +impl FromStr for Context { + type Err = (); + fn from_str(s: &str) -> Result { + hashify::map!(s.as_bytes(), Context, + "billing" => Context::Billing, + "delivery" => Context::Delivery, + "private" => Context::Private, + "work" => Context::Work, + ) + .copied() + .ok_or(()) + } +} + +impl Context { + pub fn as_str(&self) -> &'static str { + match self { + Context::Billing => "billing", + Context::Delivery => "delivery", + Context::Private => "private", + Context::Work => "work", + } + } +} + +impl FromStr for Feature { + type Err = (); + fn from_str(s: &str) -> Result { + hashify::map!(s.as_bytes(), Feature, + "fax" => Feature::Fax, + "main-number" => Feature::MainNumber, + "mobile" => Feature::Mobile, + "pager" => Feature::Pager, + "text" => Feature::Text, + "textphone" => Feature::TextPhone, + "video" => Feature::Video, + "voice" => Feature::Voice, + ) + .copied() + .ok_or(()) + } +} + +impl Feature { + pub fn as_str(&self) -> &'static str { + match self { + Feature::Fax => "fax", + Feature::MainNumber => "main-number", + Feature::Mobile => "mobile", + Feature::Pager => "pager", + Feature::Text => "text", + Feature::TextPhone => "textphone", + Feature::Video => "video", + Feature::Voice => "voice", + } + } +} + +impl FromStr for JSContactGrammaticalGender { + type Err = (); + fn from_str(s: &str) -> Result { + hashify::map!(s.as_bytes(), JSContactGrammaticalGender, + "animate" => JSContactGrammaticalGender::Animate, + "common" => JSContactGrammaticalGender::Common, + "feminine" => JSContactGrammaticalGender::Feminine, + "inanimate" => JSContactGrammaticalGender::Inanimate, + "masculine" => JSContactGrammaticalGender::Masculine, + "neuter" => JSContactGrammaticalGender::Neuter, + ) + .copied() + .ok_or(()) + } +} + +impl JSContactGrammaticalGender { + pub fn as_str(&self) -> &'static str { + match self { + JSContactGrammaticalGender::Animate => "animate", + JSContactGrammaticalGender::Common => "common", + JSContactGrammaticalGender::Feminine => "feminine", + JSContactGrammaticalGender::Inanimate => "inanimate", + JSContactGrammaticalGender::Masculine => "masculine", + JSContactGrammaticalGender::Neuter => "neuter", + } + } +} + +impl FromStr for JSContactKind { + type Err = (); + fn from_str(s: &str) -> Result { + hashify::map!(s.as_bytes(), JSContactKind, + "apartment" => JSContactKind::Apartment, + "block" => JSContactKind::Block, + "building" => JSContactKind::Building, + "country" => JSContactKind::Country, + "direction" => JSContactKind::Direction, + "district" => JSContactKind::District, + "floor" => JSContactKind::Floor, + "landmark" => JSContactKind::Landmark, + "locality" => JSContactKind::Locality, + "name" => JSContactKind::Name, + "number" => JSContactKind::Number, + "postcode" => JSContactKind::Postcode, + "postOfficeBox" => JSContactKind::PostOfficeBox, + "region" => JSContactKind::Region, + "room" => JSContactKind::Room, + "separator" => JSContactKind::Separator, + "subdistrict" => JSContactKind::Subdistrict, + "birth" => JSContactKind::Birth, + "death" => JSContactKind::Death, + "wedding" => JSContactKind::Wedding, + "calendar" => JSContactKind::Calendar, + "freeBusy" => JSContactKind::FreeBusy, + "application" => JSContactKind::Application, + "device" => JSContactKind::Device, + "group" => JSContactKind::Group, + "individual" => JSContactKind::Individual, + "location" => JSContactKind::Location, + "org" => JSContactKind::Org, + "directory" => JSContactKind::Directory, + "entry" => JSContactKind::Entry, + "contact" => JSContactKind::Contact, + "logo" => JSContactKind::Logo, + "photo" => JSContactKind::Photo, + "sound" => JSContactKind::Sound, + "credential" => JSContactKind::Credential, + "generation" => JSContactKind::Generation, + "given" => JSContactKind::Given, + "given2" => JSContactKind::Given2, + "surname" => JSContactKind::Surname, + "surname2" => JSContactKind::Surname2, + "title" => JSContactKind::Title, + "expertise" => JSContactKind::Expertise, + "hobby" => JSContactKind::Hobby, + "interest" => JSContactKind::Interest, + "role" => JSContactKind::Role, + ) + .copied() + .ok_or(()) + } +} + +#[allow(clippy::wrong_self_convention)] +impl JSContactKind { + pub fn as_str(&self) -> &'static str { + match self { + JSContactKind::Apartment => "apartment", + JSContactKind::Block => "block", + JSContactKind::Building => "building", + JSContactKind::Country => "country", + JSContactKind::Direction => "direction", + JSContactKind::District => "district", + JSContactKind::Floor => "floor", + JSContactKind::Landmark => "landmark", + JSContactKind::Locality => "locality", + JSContactKind::Name => "name", + JSContactKind::Number => "number", + JSContactKind::Postcode => "postcode", + JSContactKind::PostOfficeBox => "postOfficeBox", + JSContactKind::Region => "region", + JSContactKind::Room => "room", + JSContactKind::Separator => "separator", + JSContactKind::Subdistrict => "subdistrict", + JSContactKind::Birth => "birth", + JSContactKind::Death => "death", + JSContactKind::Wedding => "wedding", + JSContactKind::Calendar => "calendar", + JSContactKind::FreeBusy => "freeBusy", + JSContactKind::Application => "application", + JSContactKind::Device => "device", + JSContactKind::Group => "group", + JSContactKind::Individual => "individual", + JSContactKind::Location => "location", + JSContactKind::Org => "org", + JSContactKind::Directory => "directory", + JSContactKind::Entry => "entry", + JSContactKind::Contact => "contact", + JSContactKind::Logo => "logo", + JSContactKind::Photo => "photo", + JSContactKind::Sound => "sound", + JSContactKind::Credential => "credential", + JSContactKind::Generation => "generation", + JSContactKind::Given => "given", + JSContactKind::Given2 => "given2", + JSContactKind::Surname => "surname", + JSContactKind::Surname2 => "surname2", + JSContactKind::Title => "title", + JSContactKind::Expertise => "expertise", + JSContactKind::Hobby => "hobby", + JSContactKind::Interest => "interest", + JSContactKind::Role => "role", + } + } + + pub(crate) fn from_vcard_adr_pos(pos: usize) -> Option { + match pos { + 0 => JSContactKind::PostOfficeBox, + 1 => JSContactKind::Apartment, + 2 => JSContactKind::Name, + 3 => JSContactKind::Locality, + 4 => JSContactKind::Region, + 5 => JSContactKind::Postcode, + 6 => JSContactKind::Country, + 7 => JSContactKind::Room, + 8 => JSContactKind::Apartment, + 9 => JSContactKind::Floor, + 10 => JSContactKind::Number, + 11 => JSContactKind::Name, + 12 => JSContactKind::Building, + 13 => JSContactKind::Block, + 14 => JSContactKind::Subdistrict, + 15 => JSContactKind::District, + 16 => JSContactKind::Landmark, + 17 => JSContactKind::Direction, + _ => return None, + } + .into() + } + + pub(crate) fn to_vcard_adr_pos(&self) -> Option { + match self { + JSContactKind::PostOfficeBox => 0, + JSContactKind::Apartment => 8, + JSContactKind::Name => 11, + JSContactKind::Locality => 3, + JSContactKind::Region => 4, + JSContactKind::Postcode => 5, + JSContactKind::Country => 6, + JSContactKind::Room => 7, + JSContactKind::Floor => 9, + JSContactKind::Number => 10, + JSContactKind::Building => 12, + JSContactKind::Block => 13, + JSContactKind::Subdistrict => 14, + JSContactKind::District => 15, + JSContactKind::Landmark => 16, + JSContactKind::Direction => 17, + _ => return None, + } + .into() + } + + pub(crate) fn from_vcard_n_pos(pos: usize) -> Option { + match pos { + 0 => JSContactKind::Surname, + 1 => JSContactKind::Given, + 2 => JSContactKind::Given2, + 3 => JSContactKind::Title, + 4 => JSContactKind::Credential, + 5 => JSContactKind::Surname2, + 6 => JSContactKind::Generation, + _ => return None, + } + .into() + } + + pub(crate) fn to_vcard_n_pos(&self) -> Option { + match self { + JSContactKind::Surname => 0, + JSContactKind::Given => 1, + JSContactKind::Given2 => 2, + JSContactKind::Title => 3, + JSContactKind::Credential => 4, + JSContactKind::Surname2 => 5, + JSContactKind::Generation => 6, + _ => return None, + } + .into() + } +} + +impl FromStr for JSContactLevel { + type Err = (); + fn from_str(s: &str) -> Result { + hashify::map!(s.as_bytes(), JSContactLevel, + "high" => JSContactLevel::High, + "low" => JSContactLevel::Low, + "medium" => JSContactLevel::Medium, + ) + .copied() + .ok_or(()) + } +} + +impl JSContactLevel { + pub fn as_str(&self) -> &'static str { + match self { + JSContactLevel::High => "high", + JSContactLevel::Low => "low", + JSContactLevel::Medium => "medium", + } + } +} + +impl FromStr for JSContactRelation { + type Err = (); + fn from_str(s: &str) -> Result { + hashify::map!(s.as_bytes(), JSContactRelation, + "acquaintance" => JSContactRelation::Acquaintance, + "agent" => JSContactRelation::Agent, + "child" => JSContactRelation::Child, + "colleague" => JSContactRelation::Colleague, + "contact" => JSContactRelation::Contact, + "co-resident" => JSContactRelation::CoResident, + "co-worker" => JSContactRelation::CoWorker, + "crush" => JSContactRelation::Crush, + "date" => JSContactRelation::Date, + "emergency" => JSContactRelation::Emergency, + "friend" => JSContactRelation::Friend, + "kin" => JSContactRelation::Kin, + "me" => JSContactRelation::Me, + "met" => JSContactRelation::Met, + "muse" => JSContactRelation::Muse, + "neighbor" => JSContactRelation::Neighbor, + "parent" => JSContactRelation::Parent, + "sibling" => JSContactRelation::Sibling, + "spouse" => JSContactRelation::Spouse, + "sweetheart" => JSContactRelation::Sweetheart, + ) + .copied() + .ok_or(()) + } +} + +impl JSContactRelation { + pub fn as_str(&self) -> &'static str { + match self { + JSContactRelation::Acquaintance => "acquaintance", + JSContactRelation::Agent => "agent", + JSContactRelation::Child => "child", + JSContactRelation::Colleague => "colleague", + JSContactRelation::Contact => "contact", + JSContactRelation::CoResident => "co-resident", + JSContactRelation::CoWorker => "co-worker", + JSContactRelation::Crush => "crush", + JSContactRelation::Date => "date", + JSContactRelation::Emergency => "emergency", + JSContactRelation::Friend => "friend", + JSContactRelation::Kin => "kin", + JSContactRelation::Me => "me", + JSContactRelation::Met => "met", + JSContactRelation::Muse => "muse", + JSContactRelation::Neighbor => "neighbor", + JSContactRelation::Parent => "parent", + JSContactRelation::Sibling => "sibling", + JSContactRelation::Spouse => "spouse", + JSContactRelation::Sweetheart => "sweetheart", + } + } +} + +impl FromStr for JSContactPhoneticSystem { + type Err = (); + fn from_str(s: &str) -> Result { + hashify::map!(s.as_bytes(), JSContactPhoneticSystem, + "ipa" => JSContactPhoneticSystem::Ipa, + "jyut" => JSContactPhoneticSystem::Jyut, + "piny" => JSContactPhoneticSystem::Piny, + "script" => JSContactPhoneticSystem::Script, + ) + .copied() + .ok_or(()) + } +} + +impl JSContactPhoneticSystem { + pub fn as_str(&self) -> &'static str { + match self { + JSContactPhoneticSystem::Ipa => "ipa", + JSContactPhoneticSystem::Jyut => "jyut", + JSContactPhoneticSystem::Piny => "piny", + JSContactPhoneticSystem::Script => "script", + } + } +} diff --git a/vendor/calcard/src/lib.rs b/vendor/calcard/src/lib.rs new file mode 100644 index 00000000..845ea9e2 --- /dev/null +++ b/vendor/calcard/src/lib.rs @@ -0,0 +1,151 @@ +/* + * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC + * + * SPDX-License-Identifier: Apache-2.0 OR MIT + */ +#![doc = include_str!("../README.md")] +#![deny(rust_2018_idioms)] +#![forbid(unsafe_code)] +use common::tokenizer::{StopChar, Token}; +use icalendar::{ICalendar, ICalendarComponentType}; +use std::{ + borrow::Cow, + iter::{Enumerate, Peekable}, + slice::Iter, +}; +use vcard::VCard; + +pub mod common; +pub mod datecalc; +pub mod icalendar; +#[cfg(feature = "jmap")] +pub mod jscalendar; +#[cfg(feature = "jmap")] +pub mod jscontact; +pub mod vcard; + +#[derive(Debug, Clone, PartialEq, Eq)] +#[non_exhaustive] +pub enum Entry { + VCard(VCard), + ICalendar(ICalendar), + InvalidLine(String), + UnexpectedComponentEnd { + expected: ICalendarComponentType, + found: ICalendarComponentType, + }, + UnterminatedComponent(Cow<'static, str>), + TooManyComponents, + Eof, +} + +pub struct Parser<'x> { + pub(crate) input: &'x [u8], + pub(crate) iter: Peekable>>, + pub(crate) strict: bool, + pub(crate) stop_colon: bool, + pub(crate) stop_semicolon: bool, + pub(crate) stop_comma: bool, + pub(crate) stop_equal: bool, + pub(crate) stop_dot: bool, + pub(crate) unfold_qp: bool, + pub(crate) unfold_b64: bool, + pub(crate) unquote: bool, + pub(crate) skip_ws: bool, + pub(crate) strip_ctl: bool, + pub(crate) token_buf: Vec>, + pub(crate) last_token_end: usize, +} + +impl<'x> Parser<'x> { + pub fn new(input: &'x str) -> Self { + let input = input.as_bytes(); + Self { + input, + iter: input.iter().enumerate().peekable(), + strict: false, + stop_colon: true, + stop_semicolon: true, + stop_comma: true, + stop_equal: true, + stop_dot: false, + unfold_qp: false, + unfold_b64: false, + unquote: true, + skip_ws: false, + strip_ctl: false, + token_buf: Vec::with_capacity(10), + last_token_end: usize::MAX, + } + } + + pub fn strict(mut self) -> Self { + self.strict = true; + self + } + + pub fn entry(&mut self) -> Entry { + self.expect_iana_token(); + + loop { + if let Some(token) = self.token() { + if (token.text.eq_ignore_ascii_case(b"BEGIN") + || token.text.eq_ignore_ascii_case("\u{feff}BEGIN".as_bytes())) + && token.stop_char == StopChar::Colon + { + if let Some(token) = self.token() { + if token.stop_char == StopChar::Lf { + hashify::fnc_map_ignore_case!(token.text.as_ref(), + b"VCARD" => { return self.vcard(); }, + b"VCALENDAR" => { return self.icalendar(ICalendarComponentType::VCalendar); }, + b"VEVENT" => { return self.icalendar(ICalendarComponentType::VEvent); }, + b"VTODO" => { return self.icalendar(ICalendarComponentType::VTodo); }, + b"VJOURNAL" => { return self.icalendar(ICalendarComponentType::VJournal); }, + b"VFREEBUSY" => { return self.icalendar(ICalendarComponentType::VFreebusy); }, + b"VTIMEZONE" => { return self.icalendar(ICalendarComponentType::VTimezone); }, + b"VALARM" => { return self.icalendar(ICalendarComponentType::VAlarm); }, + b"STANDARD" => { return self.icalendar(ICalendarComponentType::Standard); }, + b"DAYLIGHT" => { return self.icalendar(ICalendarComponentType::Daylight); }, + b"VAVAILABILITY" => { return self.icalendar(ICalendarComponentType::VAvailability); }, + b"AVAILABLE" => { return self.icalendar(ICalendarComponentType::Available); }, + b"PARTICIPANT" => { return self.icalendar(ICalendarComponentType::Participant); }, + b"VLOCATION" => { return self.icalendar(ICalendarComponentType::VLocation); }, + b"VRESOURCE" => { return self.icalendar(ICalendarComponentType::VResource); }, + _ => { + return self.icalendar(ICalendarComponentType::Other(token.into_string())); + } + ) + } + } else { + return Entry::Eof; + } + } + + let token_start = token.start; + let mut token_end = token.end; + + if token.stop_char != StopChar::Lf { + self.expect_single_value(); + while let Some(token) = self.token() { + token_end = token.end; + if token.stop_char == StopChar::Lf { + break; + } + } + } else if token.text.is_empty() { + continue; + } + + return Entry::InvalidLine( + std::str::from_utf8( + self.input.get(token_start..=token_end).unwrap_or_default(), + ) + .unwrap_or_default() + .to_string(), + ); + } else { + return Entry::Eof; + } + } + } +} diff --git a/vendor/calcard/src/vcard/builder.rs b/vendor/calcard/src/vcard/builder.rs new file mode 100644 index 00000000..b63d6186 --- /dev/null +++ b/vendor/calcard/src/vcard/builder.rs @@ -0,0 +1,347 @@ +/* + * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC + * + * SPDX-License-Identifier: Apache-2.0 OR MIT + */ + +use crate::{ + common::parser::{Boolean, Timestamp}, + vcard::*, +}; + +impl VCardEntry { + pub fn new(name: VCardProperty) -> Self { + Self { + group: None, + name, + params: vec![], + values: vec![], + } + } + + pub fn with_params(mut self, params: Vec) -> Self { + self.params = params; + self + } + + pub fn with_group(mut self, group: Option) -> Self { + self.group = group; + self + } + + pub fn with_value(mut self, value: impl Into) -> Self { + self.values.push(value.into()); + self + } + + pub fn with_values(mut self, values: Vec) -> Self { + self.values = values; + self + } + + pub fn with_param(mut self, param: impl Into) -> Self { + self.params.push(param.into()); + self + } + + pub fn add_param(&mut self, param: impl Into) { + self.params.push(param.into()); + } + + pub fn with_param_opt(mut self, param: Option>) -> Self { + if let Some(param) = param { + self.params.push(param.into()); + } + self + } + + pub fn is_type(&self, typ: &VCardValueType) -> bool { + self.parameters(&VCardParameterName::Value) + .any(|p| matches!(p, VCardParameterValue::ValueType(v) if v == typ)) + } +} + +impl VCardParameter { + pub fn new(name: VCardParameterName, value: impl Into) -> Self { + VCardParameter { + name, + value: value.into(), + } + } + + pub fn language(value: impl Into) -> Self { + VCardParameter { + name: VCardParameterName::Language, + value: value.into(), + } + } + + pub fn value(value: impl Into) -> Self { + VCardParameter { + name: VCardParameterName::Value, + value: value.into(), + } + } + + pub fn pref(value: impl Into) -> Self { + VCardParameter { + name: VCardParameterName::Pref, + value: value.into(), + } + } + + pub fn altid(value: impl Into) -> Self { + VCardParameter { + name: VCardParameterName::Altid, + value: value.into(), + } + } + + pub fn pid(value: impl Into) -> Self { + VCardParameter { + name: VCardParameterName::Pid, + value: value.into(), + } + } + + pub fn typ(value: impl Into) -> Self { + VCardParameter { + name: VCardParameterName::Type, + value: value.into(), + } + } + + pub fn mediatype(value: impl Into) -> Self { + VCardParameter { + name: VCardParameterName::Mediatype, + value: value.into(), + } + } + + pub fn calscale(value: impl Into) -> Self { + VCardParameter { + name: VCardParameterName::Calscale, + value: value.into(), + } + } + + pub fn sort_as(value: impl Into) -> Self { + VCardParameter { + name: VCardParameterName::SortAs, + value: value.into(), + } + } + + pub fn geo(value: impl Into) -> Self { + VCardParameter { + name: VCardParameterName::Geo, + value: value.into(), + } + } + + pub fn tz(value: impl Into) -> Self { + VCardParameter { + name: VCardParameterName::Tz, + value: value.into(), + } + } + + pub fn index(value: impl Into) -> Self { + VCardParameter { + name: VCardParameterName::Index, + value: value.into(), + } + } + + pub fn level(value: impl Into) -> Self { + VCardParameter { + name: VCardParameterName::Level, + value: value.into(), + } + } + + pub fn group(value: impl Into) -> Self { + VCardParameter { + name: VCardParameterName::Group, + value: value.into(), + } + } + + pub fn cc(value: impl Into) -> Self { + VCardParameter { + name: VCardParameterName::Cc, + value: value.into(), + } + } + + pub fn author(value: impl Into) -> Self { + VCardParameter { + name: VCardParameterName::Author, + value: value.into(), + } + } + + pub fn author_name(value: impl Into) -> Self { + VCardParameter { + name: VCardParameterName::AuthorName, + value: value.into(), + } + } + + pub fn created(value: impl Into) -> Self { + VCardParameter { + name: VCardParameterName::Created, + value: value.into(), + } + } + + pub fn derived(value: impl Into) -> Self { + VCardParameter { + name: VCardParameterName::Derived, + value: value.into(), + } + } + + pub fn label(value: impl Into) -> Self { + VCardParameter { + name: VCardParameterName::Label, + value: value.into(), + } + } + + pub fn phonetic(value: impl Into) -> Self { + VCardParameter { + name: VCardParameterName::Phonetic, + value: value.into(), + } + } + + pub fn prop_id(value: impl Into) -> Self { + VCardParameter { + name: VCardParameterName::PropId, + value: value.into(), + } + } + + pub fn script(value: impl Into) -> Self { + VCardParameter { + name: VCardParameterName::Script, + value: value.into(), + } + } + + pub fn service_type(value: impl Into) -> Self { + VCardParameter { + name: VCardParameterName::ServiceType, + value: value.into(), + } + } + + pub fn username(value: impl Into) -> Self { + VCardParameter { + name: VCardParameterName::Username, + value: value.into(), + } + } + + pub fn jsptr(value: impl Into) -> Self { + VCardParameter { + name: VCardParameterName::Jsptr, + value: value.into(), + } + } + + pub fn jscomps(value: impl Into) -> Self { + VCardParameter { + name: VCardParameterName::Jscomps, + value: value.into(), + } + } +} + +impl From for VCardValue { + fn from(text: String) -> Self { + VCardValue::Text(text) + } +} + +impl From for VCardValue { + fn from(kind: VCardKind) -> Self { + VCardValue::Kind(kind) + } +} + +impl From for VCardParameterValue { + fn from(value: String) -> Self { + VCardParameterValue::Text(value) + } +} + +impl From for VCardParameterValue { + fn from(value: u32) -> Self { + VCardParameterValue::Integer(value) + } +} + +impl From for VCardParameterValue { + fn from(value: VCardType) -> Self { + VCardParameterValue::Type(value) + } +} + +impl From for VCardParameterValue { + fn from(value: VCardValueType) -> Self { + VCardParameterValue::ValueType(value) + } +} + +impl From for VCardParameterValue { + fn from(value: CalendarScale) -> Self { + VCardParameterValue::Calscale(value) + } +} + +impl From for VCardParameterValue { + fn from(value: VCardPhonetic) -> Self { + VCardParameterValue::Phonetic(value) + } +} + +impl From for VCardParameterValue { + fn from(value: VCardLevel) -> Self { + VCardParameterValue::Level(value) + } +} + +impl From for VCardParameterValue { + fn from(value: bool) -> Self { + VCardParameterValue::Bool(value) + } +} + +impl From> for VCardParameterValue { + fn from(value: Vec) -> Self { + VCardParameterValue::Jscomps(value) + } +} + +impl From for VCardParameterValue { + fn from(value: Timestamp) -> Self { + VCardParameterValue::Timestamp(value.0) + } +} + +impl From for VCardParameterValue { + fn from(value: Boolean) -> Self { + VCardParameterValue::Bool(value.0) + } +} + +impl> From> for VCardParameterValue { + fn from(value: IanaType) -> Self { + match value { + IanaType::Iana(v) => v.into(), + IanaType::Other(s) => s.into(), + } + } +} diff --git a/vendor/calcard/src/vcard/media_type.rs b/vendor/calcard/src/vcard/media_type.rs new file mode 100644 index 00000000..4c16fc35 --- /dev/null +++ b/vendor/calcard/src/vcard/media_type.rs @@ -0,0 +1,406 @@ +/* + * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC + * + * SPDX-License-Identifier: Apache-2.0 OR MIT + */ + +use std::borrow::Cow; + +pub(crate) fn media_type_from_legacy(property: &str, token: &str) -> Option> { + if token.contains('/') { + Some(Cow::Owned(token.to_ascii_lowercase())) + } else if let Some(media_type) = legacy_alias(property, token.as_bytes()) { + Some(Cow::Borrowed(media_type)) + } else { + let subtype = token.to_ascii_lowercase(); + match property { + "PHOTO" | "LOGO" if is_image_subtype(subtype.as_bytes()) => { + Some(Cow::Owned(format!("image/{subtype}"))) + } + "SOUND" if is_audio_subtype(subtype.as_bytes()) => { + Some(Cow::Owned(format!("audio/{subtype}"))) + } + _ => None, + } + } +} + +pub(crate) fn legacy_media_type(property: &str, media_type: &str) -> Option { + let token = match media_type { + "application/pgp-keys" => "PGP".to_string(), + "application/x-x509-user-cert" => "X509".to_string(), + _ => media_type.split_once('/')?.1.to_ascii_uppercase(), + }; + + (media_type_from_legacy(property, &token).as_deref() == Some(media_type)).then_some(token) +} + +fn legacy_alias(property: &str, token: &[u8]) -> Option<&'static str> { + match property { + "PHOTO" | "LOGO" => hashify::tiny_map_ignore_case!(token, + b"JPG" => "image/jpeg", + b"TIF" => "image/tiff", + ), + "SOUND" => hashify::tiny_map_ignore_case!(token, + b"AIF" => "audio/aiff", + b"AIFF" => "audio/aiff", + b"MP3" => "audio/mpeg", + b"PCM" => "audio/l16", + b"WAV" => "audio/wav", + b"WAVE" => "audio/wav", + ), + "KEY" => hashify::tiny_map_ignore_case!(token, + b"GPG" => "application/pgp-keys", + b"PGP" => "application/pgp-keys", + b"X509" => "application/x-x509-user-cert", + ), + _ => None, + } +} + +fn is_image_subtype(subtype: &[u8]) -> bool { + hashify::set!( + subtype, + "aces", + "apng", + "avci", + "avcs", + "avif", + "bmp", + "cgm", + "dicom-rle", + "dpx", + "emf", + "example", + "fits", + "g3fax", + "gif", + "heic", + "heic-sequence", + "heif", + "heif-sequence", + "hej2k", + "hsj2", + "ief", + "j2c", + "jaii", + "jais", + "jls", + "jp2", + "jpeg", + "jph", + "jphc", + "jpm", + "jpx", + "jxl", + "jxr", + "jxra", + "jxrs", + "jxs", + "jxsc", + "jxsi", + "jxss", + "ktx", + "ktx2", + "naplps", + "png", + "prs.aimg", + "prs.btif", + "prs.pti", + "pwg-raster", + "svg+xml", + "t38", + "tiff", + "tiff-fx", + "vnd.adobe.photoshop", + "vnd.airzip.accelerator.azv", + "vnd.blockfact.facti", + "vnd.clip", + "vnd.cns.inf2", + "vnd.dece.graphic", + "vnd.djvu", + "vnd.dvb.subtitle", + "vnd.dwg", + "vnd.dxf", + "vnd.fastbidsheet", + "vnd.fpx", + "vnd.fst", + "vnd.fujixerox.edmics-mmr", + "vnd.fujixerox.edmics-rlc", + "vnd.globalgraphics.pgb", + "vnd.microsoft.icon", + "vnd.mix", + "vnd.mozilla.apng", + "vnd.ms-modi", + "vnd.net-fpx", + "vnd.pco.b16", + "vnd.radiance", + "vnd.sealed.png", + "vnd.sealedmedia.softseal.gif", + "vnd.sealedmedia.softseal.jpg", + "vnd.sld", + "vnd.svf", + "vnd.tencent.tap", + "vnd.valve.source.texture", + "vnd.wap.wbmp", + "vnd.xiff", + "vnd.zbrush.pcx", + "webp", + "wmf", + "x-emf", + "x-wmf", + ) +} + +fn is_audio_subtype(subtype: &[u8]) -> bool { + hashify::set!( + subtype, + "1d-interleaved-parityfec", + "32kadpcm", + "3gpp", + "3gpp2", + "amr", + "amr-wb", + "atrac-advanced-lossless", + "atrac-x", + "atrac3", + "bv16", + "bv32", + "cn", + "dat12", + "dv", + "dvi4", + "evrc", + "evrc-qcp", + "evrc0", + "evrc1", + "evrcb", + "evrcb0", + "evrcb1", + "evrcnw", + "evrcnw0", + "evrcnw1", + "evrcwb", + "evrcwb0", + "evrcwb1", + "evs", + "g711-0", + "g719", + "g722", + "g7221", + "g723", + "g726-16", + "g726-24", + "g726-32", + "g726-40", + "g728", + "g729", + "g7291", + "g729d", + "g729e", + "gsm", + "gsm-efr", + "gsm-hr-08", + "l16", + "l20", + "l24", + "l8", + "lpc", + "melp", + "melp1200", + "melp2400", + "melp600", + "mp4a-latm", + "mpa", + "pcma", + "pcma-wb", + "pcmu", + "pcmu-wb", + "qcelp", + "red", + "smv", + "smv-qcp", + "smv0", + "tetra_acelp", + "tetra_acelp_bb", + "tsvcis", + "uemclip", + "vdvi", + "vmr-wb", + "aac", + "ac3", + "amr-wb+", + "aptx", + "asc", + "basic", + "clearmode", + "dls", + "dsr-es201108", + "dsr-es202050", + "dsr-es202211", + "dsr-es202212", + "eac3", + "encaprtp", + "example", + "flac", + "flexfec", + "fwdred", + "ilbc", + "ip-mr_v2.5", + "matroska", + "mhas", + "midi-clip", + "mobile-xmf", + "mp4", + "mpa-robust", + "mpeg", + "mpeg4-generic", + "ogg", + "opus", + "parityfec", + "prs.aaud", + "prs.sid", + "raptorfec", + "rtp-enc-aescm128", + "rtp-midi", + "rtploopback", + "rtx", + "scip", + "sofa", + "soundfont", + "sp-midi", + "speex", + "t140c", + "t38", + "telephone-event", + "tone", + "ulpfec", + "usac", + "vnd.3gpp.iufp", + "vnd.4sb", + "vnd.celp", + "vnd.audiokoz", + "vnd.blockfact.facta", + "vnd.cisco.nse", + "vnd.cmles.radio-events", + "vnd.cns.anp1", + "vnd.cns.inf1", + "vnd.dece.audio", + "vnd.digital-winds", + "vnd.dlna.adts", + "vnd.dolby.heaac.1", + "vnd.dolby.heaac.2", + "vnd.dolby.mlp", + "vnd.dolby.mps", + "vnd.dolby.pl2", + "vnd.dolby.pl2x", + "vnd.dolby.pl2z", + "vnd.dolby.pulse.1", + "vnd.dra", + "vnd.dts", + "vnd.dts.hd", + "vnd.dts.uhd", + "vnd.dvb.file", + "vnd.everad.plj", + "vnd.hns.audio", + "vnd.lucent.voice", + "vnd.ms-playready.media.pya", + "vnd.nokia.mobile-xmf", + "vnd.nortel.vbk", + "vnd.nuera.ecelp4800", + "vnd.nuera.ecelp7470", + "vnd.nuera.ecelp9600", + "vnd.octel.sbc", + "vnd.presonus.multitrack", + "vnd.qcelp", + "vnd.rhetorex.32kadpcm", + "vnd.rip", + "vnd.sealedmedia.softseal.mpeg", + "vnd.vmx.cvsd", + "vorbis", + "vorbis-config", + ) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_media_type_from_legacy() { + for (property, token, expected) in [ + ("PHOTO", "JPEG", Some("image/jpeg")), + ("PHOTO", "jpeg", Some("image/jpeg")), + ("PHOTO", "JPG", Some("image/jpeg")), + ("PHOTO", "PNG", Some("image/png")), + ("PHOTO", "GIF", Some("image/gif")), + ("PHOTO", "TIF", Some("image/tiff")), + ("PHOTO", "WEBP", Some("image/webp")), + ("PHOTO", "SVG+XML", Some("image/svg+xml")), + ("PHOTO", "image/jpeg", Some("image/jpeg")), + ("PHOTO", "WORK", None), + ("PHOTO", "HOME", None), + ("PHOTO", "PREF", None), + ("PHOTO", "X-CUSTOM", None), + ("LOGO", "PNG", Some("image/png")), + ("SOUND", "BASIC", Some("audio/basic")), + ("SOUND", "WAVE", Some("audio/wav")), + ("SOUND", "PCM", Some("audio/l16")), + ("SOUND", "MP3", Some("audio/mpeg")), + ("SOUND", "PNG", None), + ("KEY", "PGP", Some("application/pgp-keys")), + ("KEY", "X509", Some("application/x-x509-user-cert")), + ("KEY", "WORK", None), + ("FN", "JPEG", None), + ] { + assert_eq!( + media_type_from_legacy(property, token).as_deref(), + expected, + "failed for {property};TYPE={token}" + ); + } + } + + #[test] + fn test_legacy_media_type() { + for (property, media_type, expected) in [ + ("PHOTO", "image/jpeg", Some("JPEG")), + ("PHOTO", "image/png", Some("PNG")), + ("PHOTO", "image/svg+xml", Some("SVG+XML")), + ("PHOTO", "text/plain", None), + ("PHOTO", "application/pgp-keys", None), + ("PHOTO", "notamediatype", None), + ("SOUND", "audio/basic", Some("BASIC")), + ("SOUND", "audio/wav", Some("WAV")), + ("KEY", "application/pgp-keys", Some("PGP")), + ("KEY", "application/x-x509-user-cert", Some("X509")), + ] { + assert_eq!( + legacy_media_type(property, media_type).as_deref(), + expected, + "failed for {property} {media_type}" + ); + } + } + + #[test] + fn test_legacy_media_type_roundtrip() { + for (property, token) in [ + ("PHOTO", "JPEG"), + ("PHOTO", "JPG"), + ("PHOTO", "PNG"), + ("PHOTO", "TIF"), + ("SOUND", "WAVE"), + ("SOUND", "PCM"), + ("KEY", "PGP"), + ] { + let media_type = media_type_from_legacy(property, token).expect("no media type"); + let token = legacy_media_type(property, &media_type).expect("no legacy token"); + assert_eq!( + media_type_from_legacy(property, &token).as_deref(), + Some(media_type.as_ref()), + "unstable roundtrip for {property} {media_type}" + ); + } + } +} diff --git a/vendor/calcard/src/vcard/mod.rs b/vendor/calcard/src/vcard/mod.rs new file mode 100644 index 00000000..4a9bbbc5 --- /dev/null +++ b/vendor/calcard/src/vcard/mod.rs @@ -0,0 +1,452 @@ +/* + * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC + * + * SPDX-License-Identifier: Apache-2.0 OR MIT + */ + +use crate::{ + Entry, Parser, + common::{CalendarScale, Data, IanaParse, IanaString, IanaType, PartialDateTime}, +}; + +pub mod builder; +pub(crate) mod media_type; +pub mod parser; +pub mod types; +pub mod utils; +pub mod writer; + +#[cfg(feature = "rkyv")] +pub mod rkyv_types; +#[cfg(feature = "rkyv")] +pub mod rkyv_utils; +#[cfg(feature = "rkyv")] +pub mod rkyv_writer; + +#[derive(Debug, Default, Clone, PartialEq, Eq)] +#[cfg_attr( + any(test, feature = "serde"), + derive(serde::Serialize, serde::Deserialize) +)] +#[cfg_attr( + feature = "rkyv", + derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive) +)] +#[cfg_attr(feature = "rkyv", rkyv(compare(PartialEq)))] +pub struct VCard { + pub entries: Vec, +} + +#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum VCardVersion { + V2_0 = 20, + V2_1 = 21, + V3_0 = 30, + #[default] + V4_0 = 40, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +#[cfg_attr( + any(test, feature = "serde"), + derive(serde::Serialize, serde::Deserialize) +)] +#[cfg_attr( + feature = "rkyv", + derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive) +)] +#[cfg_attr(feature = "rkyv", rkyv(compare(PartialEq)))] +pub struct VCardEntry { + pub group: Option, + pub name: VCardProperty, + pub params: Vec, + pub values: Vec, +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] +#[cfg_attr( + any(test, feature = "serde"), + derive(serde::Serialize, serde::Deserialize) +)] +#[cfg_attr(any(test, feature = "serde"), serde(tag = "type", content = "data"))] +#[cfg_attr( + feature = "rkyv", + derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive) +)] +#[cfg_attr(feature = "rkyv", rkyv(compare(PartialEq), derive(Debug)))] +pub enum VCardProperty { + Other(String), + Begin, + End, + Source, // [RFC6350, Section 6.1.3] + Kind, // [RFC6350, Section 6.1.4] + Xml, // [RFC6350, Section 6.1.5] + Fn, // [RFC6350, Section 6.2.1] + N, // [RFC6350, Section 6.2.2][RFC9554, Section 2.2] + Nickname, // [RFC6350, Section 6.2.3] + Photo, // [RFC6350, Section 6.2.4] + Bday, // [RFC6350, Section 6.2.5] + Anniversary, // [RFC6350, Section 6.2.6] + Gender, // [RFC6350, Section 6.2.7] + Adr, // [RFC6350, Section 6.3.1][RFC9554, Section 2.1] + Tel, // [RFC6350, Section 6.4.1] + Email, // [RFC6350, Section 6.4.2] + Impp, // [RFC6350, Section 6.4.3] + Lang, // [RFC6350, Section 6.4.4] + Tz, // [RFC6350, Section 6.5.1] + Geo, // [RFC6350, Section 6.5.2] + Title, // [RFC6350, Section 6.6.1] + Role, // [RFC6350, Section 6.6.2] + Logo, // [RFC6350, Section 6.6.3] + Org, // [RFC6350, Section 6.6.4] + Member, // [RFC6350, Section 6.6.5] + Related, // [RFC6350, Section 6.6.6] + Categories, // [RFC6350, Section 6.7.1] + Note, // [RFC6350, Section 6.7.2] + Prodid, // [RFC6350, Section 6.7.3] + Rev, // [RFC6350, Section 6.7.4] + Sound, // [RFC6350, Section 6.7.5] + Uid, // [RFC6350, Section 6.7.6] + Clientpidmap, // [RFC6350, Section 6.7.7] + Url, // [RFC6350, Section 6.7.8] + Version, // [RFC6350, Section 6.7.9] + Key, // [RFC6350, Section 6.8.1] + Fburl, // [RFC6350, Section 6.9.1] + Caladruri, // [RFC6350, Section 6.9.2] + Caluri, // [RFC6350, Section 6.9.3] + Birthplace, // [RFC6474, Section 2.1] + Deathplace, // [RFC6474, Section 2.2] + Deathdate, // [RFC6474, Section 2.3] + Expertise, // [RFC6715, Section 2.1] + Hobby, // [RFC6715, Section 2.2] + Interest, // [RFC6715, Section 2.3] + OrgDirectory, // [RFC6715, Section 2.4][RFC Errata 3341] + ContactUri, // [RFC8605, Section 2.1] + Created, // [RFC9554, Section 3.1] + Gramgender, // [RFC9554, Section 3.2] + Language, // [RFC9554, Section 3.3] + Pronouns, // [RFC9554, Section 3.4] + Socialprofile, // [RFC9554, Section 3.5] + Jsprop, // [RFC9555, Section 3.2.1] +} + +#[derive(Debug, Clone, PartialEq)] +#[cfg_attr( + any(test, feature = "serde"), + derive(serde::Serialize, serde::Deserialize) +)] +#[cfg_attr(any(test, feature = "serde"), serde(tag = "type", content = "data"))] +#[cfg_attr( + feature = "rkyv", + derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive) +)] +#[cfg_attr(feature = "rkyv", rkyv(compare(PartialEq)))] +pub enum VCardValue { + Text(String), + Integer(i64), + Float(f64), + Boolean(bool), + PartialDateTime(PartialDateTime), + Binary(Data), + Sex(VCardSex), + GramGender(VCardGramGender), + Kind(VCardKind), + Component(Vec), +} + +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[cfg_attr( + any(test, feature = "serde"), + derive(serde::Serialize, serde::Deserialize) +)] +#[cfg_attr( + feature = "rkyv", + derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive) +)] +#[cfg_attr(feature = "rkyv", rkyv(compare(PartialEq)))] +pub struct VCardParameter { + pub name: VCardParameterName, + pub value: VCardParameterValue, +} + +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[cfg_attr( + any(test, feature = "serde"), + derive(serde::Serialize, serde::Deserialize) +)] +#[cfg_attr(any(test, feature = "serde"), serde(tag = "type", content = "data"))] +#[cfg_attr( + feature = "rkyv", + derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive) +)] +#[cfg_attr(feature = "rkyv", rkyv(compare(PartialEq)))] +pub enum VCardParameterValue { + Text(String), + Integer(u32), + Timestamp(i64), + Bool(bool), + ValueType(VCardValueType), + Type(VCardType), + Calscale(CalendarScale), + Level(VCardLevel), + Phonetic(VCardPhonetic), + Jscomps(Vec), + Null, +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] +#[cfg_attr( + any(test, feature = "serde"), + derive(serde::Serialize, serde::Deserialize) +)] +#[cfg_attr( + feature = "rkyv", + derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive) +)] +#[cfg_attr(feature = "rkyv", rkyv(compare(PartialEq), derive(Debug)))] +pub enum Jscomp { + Entry { position: u32, value: u32 }, + Separator(String), +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] +#[cfg_attr( + any(test, feature = "serde"), + derive(serde::Serialize, serde::Deserialize) +)] +#[cfg_attr( + feature = "rkyv", + derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive) +)] +#[cfg_attr(feature = "rkyv", rkyv(compare(PartialEq), derive(PartialEq)))] +pub enum VCardParameterName { + Other(String), + Language, // [RFC6350, Section 5.1] + Value, // [RFC6350, Section 5.2] + Pref, // [RFC6350, Section 5.3] + Altid, // [RFC6350, Section 5.4] + Pid, // [RFC6350, Section 5.5] + Type, // [RFC6350, Section 5.6] + Mediatype, // [RFC6350, Section 5.7] + Calscale, // [RFC6350, Section 5.8] + SortAs, // [RFC6350, Section 5.9] + Geo, // [RFC6350, Section 5.10] + Tz, // [RFC6350, Section 5.11] + Index, // [RFC6715, Section 3.1] + Level, // [RFC6715, Section 3.2] + Group, // [RFC7095, Section 8.1] + Cc, // [RFC8605, Section 3.1] + Author, // [RFC9554, Section 4.1] + AuthorName, // [RFC9554, Section 4.2] + Created, // [RFC9554, Section 4.3] + Derived, // [RFC9554, Section 4.4] + Label, // [RFC6350, Section 6.3.1][RFC9554, Section 4.5] + Phonetic, // [RFC9554, Section 4.6] + PropId, // [RFC9554, Section 4.7] + Script, // [RFC9554, Section 4.8] + ServiceType, // [RFC9554, Section 4.9] + Username, // [RFC9554, Section 4.10] + Jsptr, // [RFC9555, Section 3.3.2] + Jscomps, // [RFC9555, Section 3.3.2] +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[cfg_attr( + any(test, feature = "serde"), + derive(serde::Serialize, serde::Deserialize) +)] +#[cfg_attr(any(test, feature = "serde"), serde(tag = "type", content = "data"))] +#[cfg_attr( + feature = "rkyv", + derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive) +)] +#[cfg_attr(feature = "rkyv", rkyv(compare(PartialEq), derive(Debug)))] +pub enum VCardValueType { + Boolean, // [RFC6350, Section 4.4] + Date, // [RFC6350, Section 4.3.1] + DateAndOrTime, // [RFC6350, Section 4.3.4] + DateTime, // [RFC6350, Section 4.3.3] + Float, // [RFC6350, Section 4.6] + Integer, // [RFC6350, Section 4.5] + LanguageTag, // [RFC6350, Section 4.8] + Text, // [RFC6350, Section 4.1] + Time, // [RFC6350, Section 4.3.2] + Timestamp, // [RFC6350, Section 4.3.5] + Uri, // [RFC6350, Section 4.2] + UtcOffset, // [RFC6350, Section 4.7] +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[cfg_attr( + any(test, feature = "serde"), + derive(serde::Serialize, serde::Deserialize) +)] +#[cfg_attr( + feature = "rkyv", + derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive) +)] +#[cfg_attr(feature = "rkyv", rkyv(compare(PartialEq), derive(Debug)))] +pub enum VCardLevel { + Beginner, // [RFC6715, Section 3.2] + Average, // [RFC6715, Section 3.2] + Expert, // [RFC6715, Section 3.2] + High, // [RFC6715, Section 3.2] + Medium, // [RFC6715, Section 3.2] + Low, // [RFC6715, Section 3.2] +} + +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[cfg_attr( + any(test, feature = "serde"), + derive(serde::Serialize, serde::Deserialize) +)] +#[cfg_attr(any(test, feature = "serde"), serde(tag = "type", content = "data"))] +#[cfg_attr( + feature = "rkyv", + derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive) +)] +#[cfg_attr(feature = "rkyv", rkyv(compare(PartialEq), derive(Debug)))] +pub enum VCardPhonetic { + Ipa, // [RFC9554, Section 4.6] + Jyut, // [RFC9554, Section 4.6] + Piny, // [RFC9554, Section 4.6] + Script, // [RFC9554, Section 4.6] +} + +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[cfg_attr( + any(test, feature = "serde"), + derive(serde::Serialize, serde::Deserialize) +)] +#[cfg_attr(any(test, feature = "serde"), serde(tag = "type", content = "data"))] +#[cfg_attr( + feature = "rkyv", + derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive) +)] +#[cfg_attr(feature = "rkyv", rkyv(compare(PartialEq), derive(Debug)))] +pub enum VCardType { + Work, // [RFC6350, Section 5.6] + Home, // [RFC6350, Section 5.6] + Billing, // [RFC9554, Section 5.1] + Delivery, // [RFC9554, Section 5.2] + Contact, // [RFC6350, Section 6.6.6] + Acquaintance, // [RFC6350, Section 6.6.6] + Friend, // [RFC6350, Section 6.6.6] + Met, // [RFC6350, Section 6.6.6] + CoWorker, // [RFC6350, Section 6.6.6] + Colleague, // [RFC6350, Section 6.6.6] + CoResident, // [RFC6350, Section 6.6.6] + Neighbor, // [RFC6350, Section 6.6.6] + Child, // [RFC6350, Section 6.6.6] + Parent, // [RFC6350, Section 6.6.6] + Sibling, // [RFC6350, Section 6.6.6] + Spouse, // [RFC6350, Section 6.6.6] + Kin, // [RFC6350, Section 6.6.6] + Muse, // [RFC6350, Section 6.6.6] + Crush, // [RFC6350, Section 6.6.6] + Date, // [RFC6350, Section 6.6.6] + Sweetheart, // [RFC6350, Section 6.6.6] + Me, // [RFC6350, Section 6.6.6] + Agent, // [RFC6350, Section 6.6.6] + Emergency, // [RFC6350, Section 6.6.6] + Text, // [RFC6350, Section 6.4.1] + Voice, // [RFC6350, Section 6.4.1] + Fax, // [RFC6350, Section 6.4.1] + Cell, // [RFC6350, Section 6.4.1] + Video, // [RFC6350, Section 6.4.1] + Pager, // [RFC6350, Section 6.4.1] + Textphone, // [RFC6350, Section 6.4.1] + MainNumber, // [RFC7852] +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[cfg_attr( + any(test, feature = "serde"), + derive(serde::Serialize, serde::Deserialize) +)] +#[cfg_attr( + feature = "rkyv", + derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive) +)] +#[cfg_attr(feature = "rkyv", rkyv(compare(PartialEq), derive(Debug)))] +pub enum VCardGramGender { + Animate, // [RFC9554, Section 3.2] + Common, // [RFC9554, Section 3.2] + Feminine, // [RFC9554, Section 3.2] + Inanimate, // [RFC9554, Section 3.2] + Masculine, // [RFC9554, Section 3.2] + Neuter, // [RFC9554, Section 3.2] +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[cfg_attr( + any(test, feature = "serde"), + derive(serde::Serialize, serde::Deserialize) +)] +#[cfg_attr( + feature = "rkyv", + derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive) +)] +#[cfg_attr(feature = "rkyv", rkyv(compare(PartialEq), derive(Debug)))] +pub enum VCardSex { + Male, + Female, + Other, + NoneOrNotApplicable, + Unknown, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[cfg_attr( + any(test, feature = "serde"), + derive(serde::Serialize, serde::Deserialize) +)] +#[cfg_attr( + feature = "rkyv", + derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive) +)] +#[cfg_attr(feature = "rkyv", rkyv(compare(PartialEq), derive(Debug)))] +pub enum VCardKind { + Individual, // [RFC6350, Section 6.1.4] + Group, // [RFC6350, Section 6.1.4] + Org, // [RFC6350, Section 6.1.4] + Location, // [RFC6350, Section 6.1.4] + Application, // [RFC6473, Section 3] + Device, // [RFC6869, Section 3] +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) enum ValueSeparator { + None, + Comma, + Semicolon, + SemicolonAndComma, + Skip, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) enum ValueType { + Vcard(VCardValueType), + Kind, + Sex, + GramGender, +} + +impl ValueType { + pub fn unwrap_vcard(self) -> VCardValueType { + match self { + ValueType::Vcard(v) => v, + _ => VCardValueType::Text, + } + } +} + +impl VCard { + pub fn parse(value: impl AsRef) -> Result { + let mut parser = Parser::new(value.as_ref()); + match parser.entry() { + Entry::VCard(vcard) => Ok(vcard), + other => Err(other), + } + } +} diff --git a/vendor/calcard/src/vcard/parser.rs b/vendor/calcard/src/vcard/parser.rs new file mode 100644 index 00000000..877f6744 --- /dev/null +++ b/vendor/calcard/src/vcard/parser.rs @@ -0,0 +1,1341 @@ +/* + * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC + * + * SPDX-License-Identifier: Apache-2.0 OR MIT + */ + +use super::{ + PartialDateTime, VCard, VCardEntry, VCardParameter, VCardParameterName, VCardType, VCardValue, + VCardValueType, ValueSeparator, ValueType, +}; +use crate::{ + Entry, Parser, Token, + common::{ + CalendarScale, Data, Encoding, IanaParse, IanaType, + parser::{Boolean, Integer, Timestamp, parse_digits, parse_small_digits}, + tokenizer::StopChar, + }, + vcard::{ + Jscomp, VCardGramGender, VCardKind, VCardLevel, VCardParameterValue, VCardPhonetic, + VCardProperty, VCardSex, + }, +}; +use mail_parser::decoders::{ + base64::base64_decode, charsets::map::charset_decoder, + quoted_printable::quoted_printable_decode, +}; +use std::{borrow::Cow, iter::Peekable, slice::Iter}; + +struct Params { + params: Vec, + stop_char: StopChar, + data_types: Vec>, + charset: Option, + encoding: Option, + group_name: Option, +} + +impl Parser<'_> { + pub fn vcard(&mut self) -> Entry { + let mut vcard = VCard::default(); + let mut is_v4 = true; + let mut is_valid = false; + + 'outer: loop { + // Fetch property name + self.expect_iana_token(); + self.stop_dot = true; + let mut token = match self.token() { + Some(token) => token, + None => break, + }; + self.stop_dot = false; + + let mut params = Params { + params: Vec::new(), + stop_char: token.stop_char, + data_types: Vec::new(), + group_name: None, + encoding: None, + charset: None, + }; + + // Parse group name + if matches!(token.stop_char, StopChar::Dot) { + params.group_name = token.into_string().into(); + token = match self.token() { + Some(token) => token, + None => break, + }; + params.stop_char = token.stop_char; + } + + // Parse parameters + let name = token.text; + match params.stop_char { + StopChar::Semicolon => { + self.vcard_parameters(&mut params); + } + StopChar::Colon => {} + StopChar::Lf => { + // Invalid line + if name.is_empty() || !self.strict { + continue; + } else { + return Entry::InvalidLine(Token::new(name).into_string()); + } + } + _ => {} + } + + // Invalid stop char, try seeking colon + if !matches!(params.stop_char, StopChar::Colon | StopChar::Lf) { + params.stop_char = self.seek_value_or_eol(); + } + + // Parse property + let name = match VCardProperty::parse(name.as_ref()) { + Some(name) => name, + None => { + if !name.is_empty() { + VCardProperty::Other(Token::new(name).into_string()) + } else { + // Invalid line, skip + if params.stop_char != StopChar::Lf { + self.seek_lf(); + } + continue; + } + } + }; + let mut entry = VCardEntry { + group: params.group_name, + name, + params: params.params, + values: Vec::new(), + }; + + // Parse value + if params.stop_char != StopChar::Lf { + let (default_type, multi_value) = entry.name.default_types(); + + let is_structured = match multi_value { + ValueSeparator::None => { + self.expect_single_value(); + false + } + ValueSeparator::Comma => { + self.expect_multi_value_comma(); + false + } + ValueSeparator::Semicolon => { + self.expect_multi_value_semicolon(); + false + } + ValueSeparator::SemicolonAndComma => { + self.expect_multi_value_semicolon_and_comma(); + true + } + ValueSeparator::Skip => { + is_valid = entry.name == VCardProperty::End; + self.expect_single_value(); + self.token(); + break 'outer; + } + }; + match params.encoding { + Some(Encoding::Base64) => { + if multi_value != ValueSeparator::None { + self.expect_single_value(); + } + self.unfold_b64 = true; + } + Some(Encoding::QuotedPrintable) => { + self.unfold_qp = true; + } + _ => {} + } + + let mut data_types = params.data_types.iter(); + let mut token_idx = 0; + let mut last_is_comma = false; + + while let Some(mut token) = self.token() { + let (is_eol, is_comma) = match token.stop_char { + StopChar::Lf => (true, false), + StopChar::Comma => (false, true), + _ => (false, false), + }; + + // Decode old vCard + if let Some(encoding) = params.encoding { + let (bytes, default_encoding) = match encoding { + Encoding::Base64 => (base64_decode(&token.text), None), + Encoding::QuotedPrintable => { + (quoted_printable_decode(&token.text), "iso-8859-1".into()) + } + }; + if let Some(bytes) = bytes { + if let Some(decoded) = params + .charset + .as_deref() + .or(default_encoding) + .and_then(|charset| { + charset_decoder(charset.as_bytes()) + .map(|decoder| decoder(&bytes)) + }) + { + token.text = Cow::Owned(decoded.into_bytes()); + } else if std::str::from_utf8(&bytes).is_ok() { + token.text = Cow::Owned(bytes); + } else { + entry.values.push(VCardValue::Binary(Data { + data: bytes, + content_type: None, + })); + if is_eol { + break; + } else { + continue; + } + } + } + } + + let default_type = match &default_type { + ValueType::Vcard(default_type) => default_type, + ValueType::Kind if token_idx == 0 => { + if let Some(value) = VCardKind::parse(token.text.as_ref()) { + entry.values.push(VCardValue::Kind(value)); + if is_eol { + break; + } else { + continue; + } + } + &VCardValueType::Text + } + ValueType::Sex if token_idx == 0 => { + if let Some(value) = VCardSex::parse(token.text.as_ref()) { + entry.values.push(VCardValue::Sex(value)); + if is_eol { + break; + } else { + continue; + } + } + &VCardValueType::Text + } + ValueType::GramGender if token_idx == 0 => { + if let Some(value) = VCardGramGender::parse(token.text.as_ref()) { + entry.values.push(VCardValue::GramGender(value)); + if is_eol { + break; + } else { + continue; + } + } + &VCardValueType::Text + } + _ => &VCardValueType::Text, + }; + + let value = match data_types.next().unwrap_or(&IanaType::Iana(*default_type)) { + IanaType::Iana(value) => match value { + VCardValueType::Date if is_v4 => token + .into_vcard_date() + .map(VCardValue::PartialDateTime) + .unwrap_or_else(VCardValue::Text), + VCardValueType::DateAndOrTime if is_v4 => token + .into_vcard_date_and_or_datetime() + .map(VCardValue::PartialDateTime) + .unwrap_or_else(VCardValue::Text), + VCardValueType::DateTime if is_v4 => token + .into_vcard_date_time() + .map(VCardValue::PartialDateTime) + .unwrap_or_else(VCardValue::Text), + VCardValueType::Time if is_v4 => token + .into_vcard_time() + .map(VCardValue::PartialDateTime) + .unwrap_or_else(VCardValue::Text), + VCardValueType::Timestamp if is_v4 => token + .into_timestamp(true) + .map(VCardValue::PartialDateTime) + .unwrap_or_else(VCardValue::Text), + VCardValueType::UtcOffset if is_v4 => token + .into_offset() + .map(VCardValue::PartialDateTime) + .unwrap_or_else(VCardValue::Text), + VCardValueType::Boolean => VCardValue::Boolean(token.into_boolean()), + VCardValueType::Float => token + .into_float() + .map(VCardValue::Float) + .unwrap_or_else(VCardValue::Text), + VCardValueType::Integer => token + .into_integer() + .map(VCardValue::Integer) + .unwrap_or_else(VCardValue::Text), + VCardValueType::LanguageTag => VCardValue::Text(token.into_string()), + VCardValueType::Text => { + if is_v4 + && matches!( + (&entry.name, token.text.first()), + (VCardProperty::Version, Some(b'1'..=b'3')) + ) + { + is_v4 = false; + } + + VCardValue::Text(token.into_string()) + } + VCardValueType::Uri => token + .into_uri_bytes() + .map(VCardValue::Binary) + .unwrap_or_else(VCardValue::Text), + // VCard 3.0 and older + VCardValueType::Date + | VCardValueType::DateAndOrTime + | VCardValueType::DateTime + | VCardValueType::Time => token + .into_vcard_datetime_or_legacy() + .map(VCardValue::PartialDateTime) + .unwrap_or_else(VCardValue::Text), + VCardValueType::Timestamp => token + .into_vcard_timestamp_or_legacy() + .map(VCardValue::PartialDateTime) + .unwrap_or_else(VCardValue::Text), + VCardValueType::UtcOffset => token + .into_vcard_offset_or_legacy() + .map(VCardValue::PartialDateTime) + .unwrap_or_else(VCardValue::Text), + }, + IanaType::Other(_) => VCardValue::Text(token.into_string()), + }; + + if is_structured { + match (last_is_comma, entry.values.last_mut(), value) { + ( + true, + Some(VCardValue::Component(structured)), + VCardValue::Text(value), + ) => { + structured.push(value); + } + (true, Some(VCardValue::Text(prev_value)), VCardValue::Text(value)) => { + *entry.values.last_mut().unwrap() = + VCardValue::Component(vec![std::mem::take(prev_value), value]); + } + (_, _, value) => { + entry.values.push(value); + } + } + + last_is_comma = is_comma; + } else { + entry.values.push(value); + } + + if is_eol { + break; + } + + token_idx += 1; + } + } else { + entry.values.push(VCardValue::Text(String::new())); + } + + // Add types + if !params.data_types.is_empty() { + entry + .params + .extend(params.data_types.into_iter().map(|dt| VCardParameter { + name: VCardParameterName::Value, + value: match dt { + IanaType::Iana(v) => VCardParameterValue::ValueType(v), + IanaType::Other(v) => VCardParameterValue::Text(v), + }, + })); + } + + if !is_v4 { + entry.normalize_legacy_media_type(); + } + + vcard.entries.push(entry); + } + + if is_valid || !self.strict { + Entry::VCard(vcard) + } else { + Entry::UnterminatedComponent("BEGIN".into()) + } + } + + fn vcard_parameters(&mut self, params: &mut Params) { + while params.stop_char == StopChar::Semicolon { + self.expect_iana_token(); + let token = match self.token() { + Some(token) => token, + None => { + params.stop_char = StopChar::Lf; + break; + } + }; + + // Obtain parameter values + let param_name = token.text; + params.stop_char = token.stop_char; + if !matches!( + params.stop_char, + StopChar::Lf | StopChar::Colon | StopChar::Semicolon + ) { + if params.stop_char != StopChar::Equal { + params.stop_char = self.seek_param_value_or_eol(); + } + if params.stop_char == StopChar::Equal { + self.expect_param_value(); + while !matches!( + params.stop_char, + StopChar::Lf | StopChar::Colon | StopChar::Semicolon + ) { + match self.token() { + Some(token) => { + params.stop_char = token.stop_char; + self.token_buf.push(token); + } + None => { + params.stop_char = StopChar::Lf; + break; + } + } + } + } + } + + let param_values = &mut params.params; + if let Some(param_name) = VCardParameterName::try_parse(param_name.as_ref()) { + if self.token_buf.is_empty() { + param_values.push(VCardParameter::new(param_name, VCardParameterValue::Null)); + continue; + } + + match param_name { + VCardParameterName::Value => { + params + .data_types + .extend(self.token_buf.drain(..).map(Into::into)); + } + VCardParameterName::Pref | VCardParameterName::Index => { + if let Some(value) = self.buf_parse_one::>() { + param_values.push(VCardParameter { + name: param_name, + value: match value { + IanaType::Iana(value) => { + VCardParameterValue::Integer(value.0 as u32) + } + IanaType::Other(value) => VCardParameterValue::Text(value), + }, + }); + } + } + VCardParameterName::Calscale => { + if let Some(value) = self.buf_parse_one::>() + { + param_values.push(VCardParameter::new(param_name, value)); + } + } + VCardParameterName::Phonetic => { + if let Some(value) = self.buf_parse_one::>() + { + param_values.push(VCardParameter::new(param_name, value)); + } + } + VCardParameterName::Level => { + if let Some(value) = self.buf_parse_one::>() { + param_values.push(VCardParameter::new(param_name, value)); + } + } + VCardParameterName::Created => { + if let Some(value) = self.buf_parse_one::>() { + param_values.push(VCardParameter::new(param_name, value)); + } + } + VCardParameterName::Derived => { + if let Some(value) = self.buf_parse_one::>() { + param_values.push(VCardParameter::new(param_name, value)); + } + } + VCardParameterName::Type => { + let mut types: Vec> = self.buf_parse_many(); + + // RFC6350 has many mistakes, this is a workaround for the "TYPE" values + // which in the examples sometimes appears between quotes. + match types.first() { + Some(IanaType::Other(text)) + if types.len() == 1 && text.contains(",") => + { + let mut types_ = Vec::with_capacity(2); + for text in text.split(',') { + if let Some(typ) = VCardType::parse(text.as_bytes()) { + types_.push(IanaType::Iana(typ)); + } + } + types = types_; + } + _ => {} + } + + param_values.extend(types.into_iter().map(VCardParameter::typ)); + } + VCardParameterName::Jscomps => { + if let Some(text) = self.raw_token() { + let mut jscomps = Vec::with_capacity(4); + for (item_pos, item) in text.split(';').enumerate() { + if let Some(item) = item.strip_prefix("s,") { + let mut sep = String::with_capacity(item.len()); + let mut last_is_escape = false; + + for ch in item.chars() { + if ch == '\\' && !last_is_escape { + last_is_escape = true; + continue; + } + last_is_escape = false; + sep.push(ch); + } + + jscomps.push(Jscomp::Separator(sep)); + } else if item_pos != 0 { + let mut position = None; + let mut value = None; + + for (pos, item) in item.split(',').enumerate() { + if pos == 0 { + position = item.parse::().ok(); + } else if pos == 1 { + value = item.parse::().ok(); + } + } + + if let Some(position) = position { + jscomps.push(Jscomp::Entry { + position, + value: value.unwrap_or_default(), + }); + } + } else { + jscomps.push(Jscomp::Separator(item.to_string())); + } + } + + param_values.push(VCardParameter::jscomps( + VCardParameterValue::Jscomps(jscomps), + )); + } + self.token_buf.clear(); + } + _ => { + param_values.extend(self.token_buf.drain(..).map(|token| { + VCardParameter::new( + param_name.clone(), + VCardParameterValue::Text(token.into_string()), + ) + })); + } + } + } else if !param_name.is_empty() { + match VCardType::parse(param_name.as_ref()) { + Some(typ) if self.token_buf.is_empty() => { + param_values.push(VCardParameter::typ(VCardParameterValue::Type(typ))); + } + _ => match param_name.first() { + Some(b'c' | b'C') + if param_name.as_ref().eq_ignore_ascii_case(b"charset") => + { + for token in self.token_buf.drain(..) { + params.charset = token.into_string().into(); + } + } + Some(b'e' | b'E') + if param_name.as_ref().eq_ignore_ascii_case(b"encoding") => + { + for token in self.token_buf.drain(..) { + params.encoding = Encoding::parse(token.text.as_ref()); + } + } + _ => { + if params.encoding.is_none() + && param_name.eq_ignore_ascii_case(b"base64") + { + params.encoding = Some(Encoding::Base64); + } else { + let name = + VCardParameterName::Other(Token::new(param_name).into_string()) + .clone(); + if !self.token_buf.is_empty() { + param_values.extend(self.token_buf.drain(..).map(|token| { + VCardParameter::new( + name.clone(), + VCardParameterValue::Text(token.into_string()), + ) + })); + } else { + param_values + .push(VCardParameter::new(name, VCardParameterValue::Null)); + } + } + } + }, + } + } + } + } +} + +impl Token<'_> { + pub(crate) fn into_vcard_date(self) -> std::result::Result { + let mut dt = PartialDateTime::default(); + dt.parse_vcard_date(&mut self.text.iter().peekable()); + if !dt.is_null() { + Ok(dt) + } else { + Err(self.into_string()) + } + } + + pub(crate) fn into_vcard_date_and_or_datetime( + self, + ) -> std::result::Result { + let mut dt = PartialDateTime::default(); + dt.parse_vcard_date_and_or_time(&mut self.text.iter().peekable()); + if !dt.is_null() { + Ok(dt) + } else { + Err(self.into_string()) + } + } + + pub(crate) fn into_vcard_date_time(self) -> std::result::Result { + let mut dt = PartialDateTime::default(); + dt.parse_vcard_date_time(&mut self.text.iter().peekable()); + if !dt.is_null() { + Ok(dt) + } else { + Err(self.into_string()) + } + } + + pub(crate) fn into_vcard_time(self) -> std::result::Result { + let mut dt = PartialDateTime::default(); + dt.parse_vcard_time(&mut self.text.iter().peekable(), false); + if !dt.is_null() { + Ok(dt) + } else { + Err(self.into_string()) + } + } + + pub(crate) fn into_vcard_timestamp_or_legacy( + self, + ) -> std::result::Result { + let mut dt = PartialDateTime::default(); + if dt.parse_timestamp(&mut self.text.iter().peekable(), false) { + Ok(dt) + } else { + let mut dt = PartialDateTime::default(); + if dt.parse_vcard_date_legacy(&mut self.text.iter().peekable()) { + Ok(dt) + } else { + Err(self.into_string()) + } + } + } + + pub(crate) fn into_vcard_datetime_or_legacy( + self, + ) -> std::result::Result { + let mut dt = PartialDateTime::default(); + if dt.parse_vcard_date_legacy(&mut self.text.iter().peekable()) { + Ok(dt) + } else { + self.into_vcard_date_and_or_datetime() + } + } + + pub(crate) fn into_vcard_offset_or_legacy( + self, + ) -> std::result::Result { + let mut dt = PartialDateTime::default(); + if dt.parse_vcard_zone_legacy(&mut self.text.iter().peekable()) { + Ok(dt) + } else { + self.into_offset() + } + } +} + +impl PartialDateTime { + pub fn parse_vcard_date_legacy(&mut self, iter: &mut Peekable>) -> bool { + let mut idx = 0; + + for ch in iter { + match ch { + b'0'..=b'9' => { + let value = match idx { + 0 => { + if let Some(value) = &mut self.year { + *value = + value.saturating_mul(10).saturating_add((ch - b'0') as u16); + } else { + self.year = Some((ch - b'0') as u16); + } + continue; + } + 1 => &mut self.month, + 2 => &mut self.day, + 3 => &mut self.hour, + 4 => &mut self.minute, + 5 => &mut self.second, + 6 => &mut self.tz_hour, + 7 => &mut self.tz_minute, + _ => return false, + }; + + if let Some(value) = value { + *value = value.saturating_mul(10).saturating_add(ch - b'0'); + } else { + *value = Some(ch - b'0'); + } + } + b'T' | b't' if idx < 3 => { + idx = 3; + } + b'+' if idx <= 5 => { + idx = 6; + } + b'Z' | b'z' if idx == 5 => { + self.tz_hour = Some(0); + self.tz_minute = Some(0); + break; + } + b'-' if idx <= 2 => { + idx += 1; + } + b'-' if idx <= 5 => { + self.tz_minus = true; + idx = 6; + } + b':' if (3..=6).contains(&idx) => { + idx += 1; + } + b' ' | b'\t' | b'\r' | b'\n' => { + continue; + } + _ => return false, + } + } + + self.has_date() || self.has_zone() + } + + pub fn parse_vcard_zone_legacy(&mut self, iter: &mut Peekable>) -> bool { + let mut idx = 0; + + for ch in iter { + match ch { + b'0'..=b'9' => { + let value = match idx { + 0 => &mut self.tz_hour, + 1 => &mut self.tz_minute, + _ => return false, + }; + + if let Some(value) = value { + *value = value.saturating_mul(10).saturating_add(ch - b'0'); + } else { + *value = Some(ch - b'0'); + } + } + b'+' if self.tz_hour.is_none() => {} + b'-' if self.tz_hour.is_none() => { + self.tz_minus = true; + } + b'Z' | b'z' if self.tz_hour.is_none() => { + self.tz_hour = Some(0); + self.tz_minute = Some(0); + break; + } + b':' => { + idx += 1; + } + b' ' | b'\t' | b'\r' | b'\n' => { + continue; + } + _ => return false, + } + } + + self.tz_hour.is_some() && self.tz_minute.is_some() + } + + pub fn parse_vcard_date_time(&mut self, iter: &mut Peekable>) { + self.parse_vcard_date_noreduc(iter); + if matches!(iter.peek(), Some(&&b'T' | &&b't')) { + iter.next(); + self.parse_vcard_time(iter, true); + } + } + + pub fn parse_vcard_date_and_or_time(&mut self, iter: &mut Peekable>) { + self.parse_vcard_date(iter); + if matches!(iter.peek(), Some(&&b'T' | &&b't')) { + iter.next(); + self.parse_vcard_time(iter, false); + } + } + + pub fn parse_vcard_date(&mut self, iter: &mut Peekable>) { + parse_digits(iter, &mut self.year, 4, true); + if self.year.is_some() && iter.peek() == Some(&&b'-') { + iter.next(); + parse_small_digits(iter, &mut self.month, 2, true); + } else { + parse_small_digits(iter, &mut self.month, 2, true); + parse_small_digits(iter, &mut self.day, 2, false); + } + } + + pub fn parse_vcard_date_noreduc(&mut self, iter: &mut Peekable>) { + parse_digits(iter, &mut self.year, 4, true); + parse_small_digits(iter, &mut self.month, 2, true); + parse_small_digits(iter, &mut self.day, 2, false); + } + + pub fn parse_vcard_time(&mut self, iter: &mut Peekable>, mut notrunc: bool) { + for part in [&mut self.hour, &mut self.minute, &mut self.second] { + match iter.peek() { + Some(b'0'..=b'9') => { + notrunc = true; + parse_small_digits(iter, part, 2, false); + } + Some(b'-') if !notrunc => { + iter.next(); + } + _ => break, + } + } + self.parse_zone(iter); + } +} + +#[cfg(test)] +mod tests { + use crate::Entry; + + use super::*; + use std::io::Write; + + #[test] + fn parse_vcard() { + // Read all .vcf files in the test directory + for entry in std::fs::read_dir("resources/vcard").unwrap() { + let entry = entry.unwrap(); + let path = entry.path(); + if path.extension().is_some_and(|ext| ext == "vcf") { + let input = match String::from_utf8(std::fs::read(&path).unwrap()) { + Ok(input) => input, + Err(err) => { + // ISO-8859-1 + err.as_bytes() + .iter() + .map(|&b| b as char) + .collect::() + } + }; + let mut parser = Parser::new(&input); + let mut output = std::fs::File::create(path.with_extension("vcf.out")).unwrap(); + let file_name = path.as_path().to_str().unwrap(); + + loop { + match parser.entry() { + Entry::VCard(mut vcard) => { + /*for item in &mut vcard.entries { + if item.name == VCardProperty::Version { + item.values = vec![VCardValue::Text("4.0".into())]; + } + }*/ + let vcard_text = vcard.to_string(); + crate::common::writer::assert_fold_width(&vcard_text, file_name); + writeln!(output, "{}", vcard_text).unwrap(); + let _vcard_orig = vcard.clone(); + + // Roundtrip parsing + let mut parser = Parser::new(&vcard_text); + match parser.entry() { + Entry::VCard(mut vcard_) => { + vcard.entries.retain(|entry| { + !matches!(entry.name, VCardProperty::Version) + }); + vcard_.entries.retain(|entry| { + !matches!(entry.name, VCardProperty::Version) + }); + assert_eq!(vcard.entries.len(), vcard_.entries.len()); + + if !file_name.contains("003.vcf") { + for (entry, entry_) in + vcard.entries.iter().zip(vcard_.entries.iter()) + { + if entry != entry_ + && matches!( + (entry.values.first(), entry_.values.first()), + ( + Some(VCardValue::Binary(_),), + Some(VCardValue::Text(_)) + ) + ) + { + continue; + } + assert_eq!(entry, entry_, "failed for {file_name}"); + } + } + } + other => panic!("Expected VCard, got {other:?} for {file_name}"), + } + + // Rkyv archiving tests + #[cfg(feature = "rkyv")] + { + let vcard_bytes = + rkyv::to_bytes::(&_vcard_orig).unwrap(); + let vcard_unarchived = rkyv::access::< + crate::vcard::ArchivedVCard, + rkyv::rancor::Error, + >( + &vcard_bytes + ) + .unwrap(); + assert_eq!(vcard_text, vcard_unarchived.to_string()); + + for version in [ + crate::vcard::VCardVersion::V3_0, + crate::vcard::VCardVersion::V4_0, + ] { + let mut native = String::new(); + let mut archived = String::new(); + _vcard_orig.write_to(&mut native, version).unwrap(); + vcard_unarchived.write_to(&mut archived, version).unwrap(); + assert_eq!( + native, archived, + "archived writer diverged at {version} for {file_name}" + ); + crate::common::writer::assert_fold_width(&native, file_name); + } + } + } + Entry::InvalidLine(text) => { + println!("Invalid line in {file_name}: {text}"); + } + Entry::Eof => break, + other => { + panic!("Expected VCard, got {other:?} for {file_name}"); + } + } + } + } + } + } + + #[test] + fn test_parse_dates() { + for (input, typ, expected) in [ + ( + "19850412", + VCardValueType::Date, + PartialDateTime { + year: Some(1985), + month: Some(4), + day: Some(12), + ..Default::default() + }, + ), + ( + "1985-04", + VCardValueType::Date, + PartialDateTime { + year: Some(1985), + month: Some(4), + ..Default::default() + }, + ), + ( + "1985", + VCardValueType::Date, + PartialDateTime { + year: Some(1985), + ..Default::default() + }, + ), + ( + "--0412", + VCardValueType::Date, + PartialDateTime { + month: Some(4), + day: Some(12), + ..Default::default() + }, + ), + ( + "---12", + VCardValueType::Date, + PartialDateTime { + day: Some(12), + ..Default::default() + }, + ), + ( + "102200", + VCardValueType::Time, + PartialDateTime { + hour: Some(10), + minute: Some(22), + second: Some(0), + ..Default::default() + }, + ), + ( + "1022", + VCardValueType::Time, + PartialDateTime { + hour: Some(10), + minute: Some(22), + ..Default::default() + }, + ), + ( + "10", + VCardValueType::Time, + PartialDateTime { + hour: Some(10), + ..Default::default() + }, + ), + ( + "-2200", + VCardValueType::Time, + PartialDateTime { + minute: Some(22), + second: Some(0), + ..Default::default() + }, + ), + ( + "--00", + VCardValueType::Time, + PartialDateTime { + second: Some(0), + ..Default::default() + }, + ), + ( + "102200Z", + VCardValueType::Time, + PartialDateTime { + hour: Some(10), + minute: Some(22), + second: Some(0), + tz_hour: Some(0), + tz_minute: Some(0), + ..Default::default() + }, + ), + ( + "102200-0800", + VCardValueType::Time, + PartialDateTime { + hour: Some(10), + minute: Some(22), + second: Some(0), + tz_hour: Some(8), + tz_minute: Some(0), + tz_minus: true, + ..Default::default() + }, + ), + ( + "19961022T140000", + VCardValueType::DateTime, + PartialDateTime { + year: Some(1996), + month: Some(10), + day: Some(22), + hour: Some(14), + minute: Some(0), + second: Some(0), + ..Default::default() + }, + ), + ( + "--1022T1400", + VCardValueType::DateTime, + PartialDateTime { + month: Some(10), + day: Some(22), + hour: Some(14), + minute: Some(0), + ..Default::default() + }, + ), + ( + "---22T14", + VCardValueType::DateTime, + PartialDateTime { + day: Some(22), + hour: Some(14), + ..Default::default() + }, + ), + ( + "19961022T140000", + VCardValueType::DateAndOrTime, + PartialDateTime { + year: Some(1996), + month: Some(10), + day: Some(22), + hour: Some(14), + minute: Some(0), + second: Some(0), + ..Default::default() + }, + ), + ( + "--1022T1400", + VCardValueType::DateAndOrTime, + PartialDateTime { + month: Some(10), + day: Some(22), + hour: Some(14), + minute: Some(0), + ..Default::default() + }, + ), + ( + "---22T14", + VCardValueType::DateAndOrTime, + PartialDateTime { + day: Some(22), + hour: Some(14), + ..Default::default() + }, + ), + ( + "19850412", + VCardValueType::DateAndOrTime, + PartialDateTime { + year: Some(1985), + month: Some(4), + day: Some(12), + ..Default::default() + }, + ), + ( + "1985-04", + VCardValueType::DateAndOrTime, + PartialDateTime { + year: Some(1985), + month: Some(4), + ..Default::default() + }, + ), + ( + "1985", + VCardValueType::DateAndOrTime, + PartialDateTime { + year: Some(1985), + ..Default::default() + }, + ), + ( + "--0412", + VCardValueType::DateAndOrTime, + PartialDateTime { + month: Some(4), + day: Some(12), + ..Default::default() + }, + ), + ( + "---12", + VCardValueType::DateAndOrTime, + PartialDateTime { + day: Some(12), + ..Default::default() + }, + ), + ( + "T102200", + VCardValueType::DateAndOrTime, + PartialDateTime { + hour: Some(10), + minute: Some(22), + second: Some(0), + ..Default::default() + }, + ), + ( + "T1022", + VCardValueType::DateAndOrTime, + PartialDateTime { + hour: Some(10), + minute: Some(22), + ..Default::default() + }, + ), + ( + "T10", + VCardValueType::DateAndOrTime, + PartialDateTime { + hour: Some(10), + ..Default::default() + }, + ), + ( + "T-2200", + VCardValueType::DateAndOrTime, + PartialDateTime { + minute: Some(22), + second: Some(0), + ..Default::default() + }, + ), + ( + "T--00", + VCardValueType::DateAndOrTime, + PartialDateTime { + second: Some(0), + ..Default::default() + }, + ), + ( + "T102200Z", + VCardValueType::DateAndOrTime, + PartialDateTime { + hour: Some(10), + minute: Some(22), + second: Some(0), + tz_hour: Some(0), + tz_minute: Some(0), + ..Default::default() + }, + ), + ( + "T102200-0800", + VCardValueType::DateAndOrTime, + PartialDateTime { + hour: Some(10), + minute: Some(22), + second: Some(0), + tz_hour: Some(8), + tz_minute: Some(0), + tz_minus: true, + ..Default::default() + }, + ), + ( + "19961022T140000", + VCardValueType::Timestamp, + PartialDateTime { + year: Some(1996), + month: Some(10), + day: Some(22), + hour: Some(14), + minute: Some(0), + second: Some(0), + ..Default::default() + }, + ), + ( + "19961022T140000Z", + VCardValueType::Timestamp, + PartialDateTime { + year: Some(1996), + month: Some(10), + day: Some(22), + hour: Some(14), + minute: Some(0), + second: Some(0), + tz_hour: Some(0), + tz_minute: Some(0), + ..Default::default() + }, + ), + ( + "19961022T140000-05", + VCardValueType::Timestamp, + PartialDateTime { + year: Some(1996), + month: Some(10), + day: Some(22), + hour: Some(14), + minute: Some(0), + second: Some(0), + tz_hour: Some(5), + tz_minus: true, + ..Default::default() + }, + ), + ( + "19961022T140000-0500", + VCardValueType::Timestamp, + PartialDateTime { + year: Some(1996), + month: Some(10), + day: Some(22), + hour: Some(14), + minute: Some(0), + second: Some(0), + tz_hour: Some(5), + tz_minute: Some(0), + tz_minus: true, + }, + ), + ( + "-0500", + VCardValueType::UtcOffset, + PartialDateTime { + tz_hour: Some(5), + tz_minute: Some(0), + tz_minus: true, + ..Default::default() + }, + ), + ] { + let mut iter = input.as_bytes().iter().peekable(); + let mut dt = PartialDateTime::default(); + + match typ { + VCardValueType::Date => dt.parse_vcard_date(&mut iter), + VCardValueType::DateAndOrTime => dt.parse_vcard_date_and_or_time(&mut iter), + VCardValueType::DateTime => dt.parse_vcard_date_time(&mut iter), + VCardValueType::Time => dt.parse_vcard_time(&mut iter, false), + VCardValueType::Timestamp => { + dt.parse_timestamp(&mut iter, true); + } + VCardValueType::UtcOffset => { + dt.parse_zone(&mut iter); + } + _ => unreachable!(), + } + + assert_eq!(dt, expected, "failed for {input:?} with type {typ:?}"); + let mut dt_str = String::new(); + dt.format_as_vcard(&mut dt_str, &typ).unwrap(); + + assert_eq!( + input, dt_str, + "roundtrip failed for {input} with type {typ:?} {dt:?}" + ); + } + } +} diff --git a/vendor/calcard/src/vcard/rkyv_types.rs b/vendor/calcard/src/vcard/rkyv_types.rs new file mode 100644 index 00000000..232f4866 --- /dev/null +++ b/vendor/calcard/src/vcard/rkyv_types.rs @@ -0,0 +1,414 @@ +/* + * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC + * + * SPDX-License-Identifier: Apache-2.0 OR MIT + */ + +use super::*; + +impl ArchivedVCardProperty { + pub fn as_str(&self) -> &str { + match self { + ArchivedVCardProperty::Source => "SOURCE", + ArchivedVCardProperty::Kind => "KIND", + ArchivedVCardProperty::Xml => "XML", + ArchivedVCardProperty::Fn => "FN", + ArchivedVCardProperty::N => "N", + ArchivedVCardProperty::Nickname => "NICKNAME", + ArchivedVCardProperty::Photo => "PHOTO", + ArchivedVCardProperty::Bday => "BDAY", + ArchivedVCardProperty::Anniversary => "ANNIVERSARY", + ArchivedVCardProperty::Gender => "GENDER", + ArchivedVCardProperty::Adr => "ADR", + ArchivedVCardProperty::Tel => "TEL", + ArchivedVCardProperty::Email => "EMAIL", + ArchivedVCardProperty::Impp => "IMPP", + ArchivedVCardProperty::Lang => "LANG", + ArchivedVCardProperty::Tz => "TZ", + ArchivedVCardProperty::Geo => "GEO", + ArchivedVCardProperty::Title => "TITLE", + ArchivedVCardProperty::Role => "ROLE", + ArchivedVCardProperty::Logo => "LOGO", + ArchivedVCardProperty::Org => "ORG", + ArchivedVCardProperty::Member => "MEMBER", + ArchivedVCardProperty::Related => "RELATED", + ArchivedVCardProperty::Categories => "CATEGORIES", + ArchivedVCardProperty::Note => "NOTE", + ArchivedVCardProperty::Prodid => "PRODID", + ArchivedVCardProperty::Rev => "REV", + ArchivedVCardProperty::Sound => "SOUND", + ArchivedVCardProperty::Uid => "UID", + ArchivedVCardProperty::Clientpidmap => "CLIENTPIDMAP", + ArchivedVCardProperty::Url => "URL", + ArchivedVCardProperty::Version => "VERSION", + ArchivedVCardProperty::Key => "KEY", + ArchivedVCardProperty::Fburl => "FBURL", + ArchivedVCardProperty::Caladruri => "CALADRURI", + ArchivedVCardProperty::Caluri => "CALURI", + ArchivedVCardProperty::Birthplace => "BIRTHPLACE", + ArchivedVCardProperty::Deathplace => "DEATHPLACE", + ArchivedVCardProperty::Deathdate => "DEATHDATE", + ArchivedVCardProperty::Expertise => "EXPERTISE", + ArchivedVCardProperty::Hobby => "HOBBY", + ArchivedVCardProperty::Interest => "INTEREST", + ArchivedVCardProperty::OrgDirectory => "ORG-DIRECTORY", + ArchivedVCardProperty::ContactUri => "CONTACT-URI", + ArchivedVCardProperty::Created => "CREATED", + ArchivedVCardProperty::Gramgender => "GRAMGENDER", + ArchivedVCardProperty::Language => "LANGUAGE", + ArchivedVCardProperty::Pronouns => "PRONOUNS", + ArchivedVCardProperty::Socialprofile => "SOCIALPROFILE", + ArchivedVCardProperty::Jsprop => "JSPROP", + ArchivedVCardProperty::Begin => "BEGIN", + ArchivedVCardProperty::End => "END", + ArchivedVCardProperty::Other(s) => s.as_str(), + } + } +} + +impl ArchivedVCardProperty { + // Returns the default value type and whether the property is multi-valued. + pub(crate) fn default_types(&self) -> (ArchivedValueType, ValueSeparator) { + match self { + ArchivedVCardProperty::Source => ( + ArchivedValueType::Vcard(ArchivedVCardValueType::Uri), + ValueSeparator::None, + ), + ArchivedVCardProperty::Kind => (ArchivedValueType::Kind, ValueSeparator::None), + ArchivedVCardProperty::Xml => ( + ArchivedValueType::Vcard(ArchivedVCardValueType::Text), + ValueSeparator::None, + ), + ArchivedVCardProperty::Fn => ( + ArchivedValueType::Vcard(ArchivedVCardValueType::Text), + ValueSeparator::None, + ), + ArchivedVCardProperty::N => ( + ArchivedValueType::Vcard(ArchivedVCardValueType::Text), + ValueSeparator::SemicolonAndComma, + ), + ArchivedVCardProperty::Nickname => ( + ArchivedValueType::Vcard(ArchivedVCardValueType::Text), + ValueSeparator::Comma, + ), + ArchivedVCardProperty::Photo => ( + ArchivedValueType::Vcard(ArchivedVCardValueType::Uri), + ValueSeparator::None, + ), + ArchivedVCardProperty::Bday => ( + ArchivedValueType::Vcard(ArchivedVCardValueType::DateAndOrTime), + ValueSeparator::None, + ), + ArchivedVCardProperty::Anniversary => ( + ArchivedValueType::Vcard(ArchivedVCardValueType::DateAndOrTime), + ValueSeparator::None, + ), + ArchivedVCardProperty::Gender => (ArchivedValueType::Sex, ValueSeparator::Semicolon), + ArchivedVCardProperty::Adr => ( + ArchivedValueType::Vcard(ArchivedVCardValueType::Text), + ValueSeparator::SemicolonAndComma, + ), + ArchivedVCardProperty::Tel => ( + ArchivedValueType::Vcard(ArchivedVCardValueType::Text), + ValueSeparator::None, + ), + ArchivedVCardProperty::Email => ( + ArchivedValueType::Vcard(ArchivedVCardValueType::Text), + ValueSeparator::None, + ), + ArchivedVCardProperty::Impp => ( + ArchivedValueType::Vcard(ArchivedVCardValueType::Uri), + ValueSeparator::None, + ), + ArchivedVCardProperty::Lang => ( + ArchivedValueType::Vcard(ArchivedVCardValueType::Text), + ValueSeparator::None, + ), + ArchivedVCardProperty::Tz => ( + ArchivedValueType::Vcard(ArchivedVCardValueType::UtcOffset), + ValueSeparator::None, + ), + ArchivedVCardProperty::Geo => ( + ArchivedValueType::Vcard(ArchivedVCardValueType::Uri), + ValueSeparator::None, + ), + ArchivedVCardProperty::Title => ( + ArchivedValueType::Vcard(ArchivedVCardValueType::Text), + ValueSeparator::None, + ), + ArchivedVCardProperty::Role => ( + ArchivedValueType::Vcard(ArchivedVCardValueType::Text), + ValueSeparator::None, + ), + ArchivedVCardProperty::Logo => ( + ArchivedValueType::Vcard(ArchivedVCardValueType::Uri), + ValueSeparator::None, + ), + ArchivedVCardProperty::Org => ( + ArchivedValueType::Vcard(ArchivedVCardValueType::Text), + ValueSeparator::Semicolon, + ), + ArchivedVCardProperty::Member => ( + ArchivedValueType::Vcard(ArchivedVCardValueType::Uri), + ValueSeparator::None, + ), + ArchivedVCardProperty::Related => ( + ArchivedValueType::Vcard(ArchivedVCardValueType::Uri), + ValueSeparator::None, + ), + ArchivedVCardProperty::Categories => ( + ArchivedValueType::Vcard(ArchivedVCardValueType::Text), + ValueSeparator::Comma, + ), + ArchivedVCardProperty::Note => ( + ArchivedValueType::Vcard(ArchivedVCardValueType::Text), + ValueSeparator::None, + ), + ArchivedVCardProperty::Prodid => ( + ArchivedValueType::Vcard(ArchivedVCardValueType::Text), + ValueSeparator::None, + ), + ArchivedVCardProperty::Rev => ( + ArchivedValueType::Vcard(ArchivedVCardValueType::Timestamp), + ValueSeparator::None, + ), + ArchivedVCardProperty::Sound => ( + ArchivedValueType::Vcard(ArchivedVCardValueType::Uri), + ValueSeparator::None, + ), + ArchivedVCardProperty::Uid => ( + ArchivedValueType::Vcard(ArchivedVCardValueType::Uri), + ValueSeparator::None, + ), + ArchivedVCardProperty::Clientpidmap => ( + ArchivedValueType::Vcard(ArchivedVCardValueType::Text), + ValueSeparator::Semicolon, + ), + ArchivedVCardProperty::Url => ( + ArchivedValueType::Vcard(ArchivedVCardValueType::Uri), + ValueSeparator::None, + ), + ArchivedVCardProperty::Version => ( + ArchivedValueType::Vcard(ArchivedVCardValueType::Text), + ValueSeparator::None, + ), + ArchivedVCardProperty::Key => ( + ArchivedValueType::Vcard(ArchivedVCardValueType::Uri), + ValueSeparator::None, + ), + ArchivedVCardProperty::Fburl => ( + ArchivedValueType::Vcard(ArchivedVCardValueType::Uri), + ValueSeparator::None, + ), + ArchivedVCardProperty::Caladruri => ( + ArchivedValueType::Vcard(ArchivedVCardValueType::Uri), + ValueSeparator::None, + ), + ArchivedVCardProperty::Caluri => ( + ArchivedValueType::Vcard(ArchivedVCardValueType::Uri), + ValueSeparator::None, + ), + ArchivedVCardProperty::Birthplace => ( + ArchivedValueType::Vcard(ArchivedVCardValueType::Text), + ValueSeparator::None, + ), + ArchivedVCardProperty::Deathplace => ( + ArchivedValueType::Vcard(ArchivedVCardValueType::Text), + ValueSeparator::None, + ), + ArchivedVCardProperty::Deathdate => ( + ArchivedValueType::Vcard(ArchivedVCardValueType::DateAndOrTime), + ValueSeparator::None, + ), + ArchivedVCardProperty::Expertise => ( + ArchivedValueType::Vcard(ArchivedVCardValueType::Text), + ValueSeparator::None, + ), + ArchivedVCardProperty::Hobby => ( + ArchivedValueType::Vcard(ArchivedVCardValueType::Text), + ValueSeparator::None, + ), + ArchivedVCardProperty::Interest => ( + ArchivedValueType::Vcard(ArchivedVCardValueType::Text), + ValueSeparator::None, + ), + ArchivedVCardProperty::OrgDirectory => ( + ArchivedValueType::Vcard(ArchivedVCardValueType::Uri), + ValueSeparator::None, + ), + ArchivedVCardProperty::ContactUri => ( + ArchivedValueType::Vcard(ArchivedVCardValueType::Uri), + ValueSeparator::None, + ), + ArchivedVCardProperty::Created => ( + ArchivedValueType::Vcard(ArchivedVCardValueType::Timestamp), + ValueSeparator::None, + ), + ArchivedVCardProperty::Gramgender => { + (ArchivedValueType::GramGender, ValueSeparator::None) + } + ArchivedVCardProperty::Language => ( + ArchivedValueType::Vcard(ArchivedVCardValueType::LanguageTag), + ValueSeparator::None, + ), + ArchivedVCardProperty::Pronouns => ( + ArchivedValueType::Vcard(ArchivedVCardValueType::Text), + ValueSeparator::None, + ), + ArchivedVCardProperty::Socialprofile => ( + ArchivedValueType::Vcard(ArchivedVCardValueType::Uri), + ValueSeparator::None, + ), + ArchivedVCardProperty::Jsprop => ( + ArchivedValueType::Vcard(ArchivedVCardValueType::Text), + ValueSeparator::None, + ), + ArchivedVCardProperty::Begin | ArchivedVCardProperty::End => ( + ArchivedValueType::Vcard(ArchivedVCardValueType::Text), + ValueSeparator::Skip, + ), + ArchivedVCardProperty::Other(_) => ( + ArchivedValueType::Vcard(ArchivedVCardValueType::Text), + ValueSeparator::None, + ), + } + } +} + +impl IanaString for ArchivedVCardValueType { + fn as_str(&self) -> &'static str { + match self { + ArchivedVCardValueType::Boolean => "BOOLEAN", + ArchivedVCardValueType::Date => "DATE", + ArchivedVCardValueType::DateAndOrTime => "DATE-AND-OR-TIME", + ArchivedVCardValueType::DateTime => "DATE-TIME", + ArchivedVCardValueType::Float => "FLOAT", + ArchivedVCardValueType::Integer => "INTEGER", + ArchivedVCardValueType::LanguageTag => "LANGUAGE-TAG", + ArchivedVCardValueType::Text => "TEXT", + ArchivedVCardValueType::Time => "TIME", + ArchivedVCardValueType::Timestamp => "TIMESTAMP", + ArchivedVCardValueType::Uri => "URI", + ArchivedVCardValueType::UtcOffset => "UTC-OFFSET", + } + } +} + +impl IanaString for ArchivedVCardLevel { + fn as_str(&self) -> &'static str { + match self { + ArchivedVCardLevel::Beginner => "BEGINNER", + ArchivedVCardLevel::Average => "AVERAGE", + ArchivedVCardLevel::Expert => "EXPERT", + ArchivedVCardLevel::High => "HIGH", + ArchivedVCardLevel::Medium => "MEDIUM", + ArchivedVCardLevel::Low => "LOW", + } + } +} + +impl IanaString for ArchivedVCardPhonetic { + fn as_str(&self) -> &'static str { + match self { + ArchivedVCardPhonetic::Ipa => "IPA", + ArchivedVCardPhonetic::Jyut => "JYUT", + ArchivedVCardPhonetic::Piny => "PINY", + ArchivedVCardPhonetic::Script => "SCRIPT", + } + } +} + +impl IanaString for ArchivedVCardType { + fn as_str(&self) -> &'static str { + match self { + ArchivedVCardType::Work => "WORK", + ArchivedVCardType::Home => "HOME", + ArchivedVCardType::Billing => "BILLING", + ArchivedVCardType::Delivery => "DELIVERY", + ArchivedVCardType::Contact => "CONTACT", + ArchivedVCardType::Acquaintance => "ACQUAINTANCE", + ArchivedVCardType::Friend => "FRIEND", + ArchivedVCardType::Met => "MET", + ArchivedVCardType::CoWorker => "CO-WORKER", + ArchivedVCardType::Colleague => "COLLEAGUE", + ArchivedVCardType::CoResident => "CO-RESIDENT", + ArchivedVCardType::Neighbor => "NEIGHBOR", + ArchivedVCardType::Child => "CHILD", + ArchivedVCardType::Parent => "PARENT", + ArchivedVCardType::Sibling => "SIBLING", + ArchivedVCardType::Spouse => "SPOUSE", + ArchivedVCardType::Kin => "KIN", + ArchivedVCardType::Muse => "MUSE", + ArchivedVCardType::Crush => "CRUSH", + ArchivedVCardType::Date => "DATE", + ArchivedVCardType::Sweetheart => "SWEETHEART", + ArchivedVCardType::Me => "ME", + ArchivedVCardType::Agent => "AGENT", + ArchivedVCardType::Emergency => "EMERGENCY", + ArchivedVCardType::Text => "TEXT", + ArchivedVCardType::Voice => "VOICE", + ArchivedVCardType::Fax => "FAX", + ArchivedVCardType::Cell => "CELL", + ArchivedVCardType::Video => "VIDEO", + ArchivedVCardType::Pager => "PAGER", + ArchivedVCardType::Textphone => "TEXTPHONE", + ArchivedVCardType::MainNumber => "MAIN-NUMBER", + } + } +} + +impl IanaString for ArchivedVCardGramGender { + fn as_str(&self) -> &'static str { + match self { + ArchivedVCardGramGender::Animate => "ANIMATE", + ArchivedVCardGramGender::Common => "COMMON", + ArchivedVCardGramGender::Feminine => "FEMININE", + ArchivedVCardGramGender::Inanimate => "INANIMATE", + ArchivedVCardGramGender::Masculine => "MASCULINE", + ArchivedVCardGramGender::Neuter => "NEUTER", + } + } +} + +impl IanaString for ArchivedVCardSex { + fn as_str(&self) -> &'static str { + match self { + ArchivedVCardSex::Male => "M", + ArchivedVCardSex::Female => "F", + ArchivedVCardSex::Other => "O", + ArchivedVCardSex::NoneOrNotApplicable => "N", + ArchivedVCardSex::Unknown => "U", + } + } +} + +impl IanaString for ArchivedVCardKind { + fn as_str(&self) -> &'static str { + match self { + ArchivedVCardKind::Individual => "INDIVIDUAL", + ArchivedVCardKind::Group => "GROUP", + ArchivedVCardKind::Org => "ORG", + ArchivedVCardKind::Location => "LOCATION", + ArchivedVCardKind::Application => "APPLICATION", + ArchivedVCardKind::Device => "DEVICE", + } + } +} + +#[derive(Debug)] + +pub(crate) enum ArchivedValueType { + Vcard(ArchivedVCardValueType), + Kind, + Sex, + GramGender, +} + +impl ArchivedValueType { + pub fn unwrap_vcard(self) -> ArchivedVCardValueType { + match self { + ArchivedValueType::Vcard(v) => v, + _ => ArchivedVCardValueType::Text, + } + } +} diff --git a/vendor/calcard/src/vcard/rkyv_utils.rs b/vendor/calcard/src/vcard/rkyv_utils.rs new file mode 100644 index 00000000..617de3c5 --- /dev/null +++ b/vendor/calcard/src/vcard/rkyv_utils.rs @@ -0,0 +1,216 @@ +/* + * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC + * + * SPDX-License-Identifier: Apache-2.0 OR MIT + */ + +use super::*; +use crate::common::{ArchivedCalendarScale, ArchivedData, ArchivedPartialDateTime}; + +impl ArchivedVCard { + pub fn uid(&self) -> Option<&str> { + self.property(&VCardProperty::Uid) + .and_then(|e| e.values.first()) + .and_then(|v| v.as_text()) + } + + pub fn property(&self, prop: &VCardProperty) -> Option<&ArchivedVCardEntry> { + self.entries.iter().find(|entry| &entry.name == prop) + } + + pub fn properties<'x, 'y: 'x>( + &'x self, + prop: &'y VCardProperty, + ) -> impl Iterator + 'x { + self.entries.iter().filter(move |entry| &entry.name == prop) + } + + pub fn version(&self) -> Option { + self.entries + .iter() + .find(|e| e.name == VCardProperty::Version) + .and_then(|e| { + e.values + .first() + .and_then(|v| v.as_text()) + .and_then(VCardVersion::try_parse) + }) + } +} + +impl ArchivedVCardEntry { + #[inline] + pub fn parameters( + &self, + prop: &VCardParameterName, + ) -> impl Iterator { + self.params.iter().filter_map(move |param| { + if ¶m.name == prop { + Some(¶m.value) + } else { + None + } + }) + } +} + +impl ArchivedVCardValue { + pub fn as_text(&self) -> Option<&str> { + match self { + ArchivedVCardValue::Text(s) => Some(s), + _ => None, + } + } + + pub fn as_integer(&self) -> Option { + match self { + ArchivedVCardValue::Integer(i) => Some(i.to_native()), + _ => None, + } + } + + pub fn as_float(&self) -> Option { + match self { + ArchivedVCardValue::Float(f) => Some(f.to_native()), + _ => None, + } + } + + pub fn as_boolean(&self) -> Option { + match self { + ArchivedVCardValue::Boolean(b) => Some(*b), + _ => None, + } + } + + pub fn as_partial_date_time(&self) -> Option<&ArchivedPartialDateTime> { + match self { + ArchivedVCardValue::PartialDateTime(dt) => Some(dt), + _ => None, + } + } + + pub fn as_binary(&self) -> Option<&ArchivedData> { + match self { + ArchivedVCardValue::Binary(d) => Some(d), + _ => None, + } + } +} + +impl ArchivedVCardParameterName { + pub fn as_str(&self) -> &str { + match self { + ArchivedVCardParameterName::Language => "LANGUAGE", + ArchivedVCardParameterName::Value => "VALUE", + ArchivedVCardParameterName::Pref => "PREF", + ArchivedVCardParameterName::Altid => "ALTID", + ArchivedVCardParameterName::Pid => "PID", + ArchivedVCardParameterName::Type => "TYPE", + ArchivedVCardParameterName::Mediatype => "MEDIATYPE", + ArchivedVCardParameterName::Calscale => "CALSCALE", + ArchivedVCardParameterName::SortAs => "SORT-AS", + ArchivedVCardParameterName::Geo => "GEO", + ArchivedVCardParameterName::Tz => "TZ", + ArchivedVCardParameterName::Index => "INDEX", + ArchivedVCardParameterName::Level => "LEVEL", + ArchivedVCardParameterName::Group => "GROUP", + ArchivedVCardParameterName::Cc => "CC", + ArchivedVCardParameterName::Author => "AUTHOR", + ArchivedVCardParameterName::AuthorName => "AUTHOR-NAME", + ArchivedVCardParameterName::Created => "CREATED", + ArchivedVCardParameterName::Derived => "DERIVED", + ArchivedVCardParameterName::Label => "LABEL", + ArchivedVCardParameterName::Phonetic => "PHONETIC", + ArchivedVCardParameterName::PropId => "PROP-ID", + ArchivedVCardParameterName::Script => "SCRIPT", + ArchivedVCardParameterName::ServiceType => "SERVICE-TYPE", + ArchivedVCardParameterName::Username => "USERNAME", + ArchivedVCardParameterName::Jsptr => "JSPTR", + ArchivedVCardParameterName::Jscomps => "JSCOMPS", + ArchivedVCardParameterName::Other(name) => name.as_str(), + } + } +} + +impl ArchivedVCardParameterValue { + pub fn as_text(&self) -> Option<&str> { + match self { + ArchivedVCardParameterValue::Text(v) => Some(v.as_str()), + ArchivedVCardParameterValue::ValueType(v) => Some(v.as_str()), + ArchivedVCardParameterValue::Type(v) => Some(v.as_str()), + ArchivedVCardParameterValue::Calscale(v) => Some(v.as_str()), + ArchivedVCardParameterValue::Level(v) => Some(v.as_str()), + ArchivedVCardParameterValue::Phonetic(v) => Some(v.as_str()), + ArchivedVCardParameterValue::Jscomps(_) + | ArchivedVCardParameterValue::Integer(_) + | ArchivedVCardParameterValue::Timestamp(_) + | ArchivedVCardParameterValue::Bool(_) + | ArchivedVCardParameterValue::Null => None, + } + } + + pub fn as_phonetic(&self) -> Option> { + match self { + ArchivedVCardParameterValue::Phonetic(v) => Some(IanaType::Iana(v)), + ArchivedVCardParameterValue::Text(v) => Some(IanaType::Other(v.as_str())), + _ => None, + } + } + + pub fn as_calscale(&self) -> Option> { + match self { + ArchivedVCardParameterValue::Calscale(v) => Some(IanaType::Iana(v)), + ArchivedVCardParameterValue::Text(v) => Some(IanaType::Other(v.as_str())), + _ => None, + } + } + + pub fn as_level(&self) -> Option> { + match self { + ArchivedVCardParameterValue::Level(v) => Some(IanaType::Iana(v)), + ArchivedVCardParameterValue::Text(v) => Some(IanaType::Other(v.as_str())), + _ => None, + } + } + + pub fn as_type(&self) -> Option> { + match self { + ArchivedVCardParameterValue::Type(v) => Some(IanaType::Iana(v)), + ArchivedVCardParameterValue::Text(v) => Some(IanaType::Other(v.as_str())), + _ => None, + } + } + + pub fn as_value_type(&self) -> Option> { + match self { + ArchivedVCardParameterValue::ValueType(v) => Some(IanaType::Iana(v)), + ArchivedVCardParameterValue::Text(v) => Some(IanaType::Other(v.as_str())), + _ => None, + } + } + + pub fn as_bool(&self) -> Option> { + match self { + ArchivedVCardParameterValue::Bool(b) => Some(IanaType::Iana(*b)), + ArchivedVCardParameterValue::Text(v) => Some(IanaType::Other(v.as_str())), + _ => None, + } + } + + pub fn as_integer(&self) -> Option> { + match self { + ArchivedVCardParameterValue::Integer(i) => Some(IanaType::Iana(i.to_native())), + ArchivedVCardParameterValue::Text(v) => Some(IanaType::Other(v.as_str())), + _ => None, + } + } + + pub fn as_timestamp(&self) -> Option> { + match self { + ArchivedVCardParameterValue::Timestamp(t) => Some(IanaType::Iana(t.to_native())), + ArchivedVCardParameterValue::Text(v) => Some(IanaType::Other(v.as_str())), + _ => None, + } + } +} diff --git a/vendor/calcard/src/vcard/rkyv_writer.rs b/vendor/calcard/src/vcard/rkyv_writer.rs new file mode 100644 index 00000000..faf8b3ef --- /dev/null +++ b/vendor/calcard/src/vcard/rkyv_writer.rs @@ -0,0 +1,596 @@ +/* + * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC + * + * SPDX-License-Identifier: Apache-2.0 OR MIT + */ + +use crate::{ + common::{ + parser::Timestamp, + writer::{FoldingWriter, LineWriter, write_bytes, write_param_value, write_text}, + }, + vcard::{media_type::legacy_media_type, *}, +}; +use std::fmt::{Display, Write}; + +impl ArchivedVCard { + pub fn write_to(&self, out: &mut impl Write, version: VCardVersion) -> std::fmt::Result { + write!(out, "BEGIN:VCARD\r\n")?; + write!(out, "VERSION:{version}\r\n")?; + let is_v4 = matches!(version, VCardVersion::V4_0); + for entry in self.entries.iter() { + if !matches!( + entry.name, + ArchivedVCardProperty::Version + | ArchivedVCardProperty::Begin + | ArchivedVCardProperty::End + ) { + entry.write_to(out, true, is_v4)?; + } + } + + write!(out, "END:VCARD\r\n") + } +} + +impl ArchivedVCardEntry { + pub fn write_to( + &self, + out: &mut impl Write, + with_value: bool, + is_v4: bool, + ) -> std::fmt::Result { + let mut folded = FoldingWriter::new(out); + let out = &mut folded; + + if let Some(group_name) = self.group.as_ref() { + out.write_atomic(group_name)?; + out.write_atomic(".")?; + } + + out.write_atomic(self.name.as_str())?; + let mut types = None; + let mut last_param: Option<&ArchivedVCardParameterName> = None; + + for param in self.params.iter() { + if last_param.is_some_and(|last_param| last_param == ¶m.name) { + out.write_atomic(",")?; + } else { + out.write_atomic(";")?; + out.write_atomic(param.name.as_str())?; + if !matches!(param.value, ArchivedVCardParameterValue::Null) { + out.write_atomic("=")?; + } + last_param = Some(¶m.name); + } + + match ¶m.value { + ArchivedVCardParameterValue::Text(v) => { + write_param_value(out, v)?; + } + ArchivedVCardParameterValue::Integer(i) => { + write!(out, "{}", i)?; + } + ArchivedVCardParameterValue::Timestamp(v) => { + write!(out, "{}", Timestamp(v.to_native()))?; + } + ArchivedVCardParameterValue::Bool(v) => { + out.write_atomic(if *v { "TRUE" } else { "FALSE" })?; + } + ArchivedVCardParameterValue::ValueType(v) => { + if types.is_none() { + types = Some(v); + } + write_param_value(out, v.as_str())?; + } + ArchivedVCardParameterValue::Type(v) => { + write_param_value(out, v.as_str())?; + } + ArchivedVCardParameterValue::Calscale(v) => { + write_param_value(out, v.as_str())?; + } + ArchivedVCardParameterValue::Level(v) => { + write_param_value(out, v.as_str())?; + } + ArchivedVCardParameterValue::Phonetic(v) => { + write_param_value(out, v.as_str())?; + } + ArchivedVCardParameterValue::Jscomps(v) => { + out.write_atomic("\"")?; + write_jscomps(out, v)?; + out.write_atomic("\"")?; + last_param = None; + } + ArchivedVCardParameterValue::Null => { + last_param = None; + } + } + } + + if !is_v4 { + if let Some(data) = self.values.iter().find_map(|v| match v { + ArchivedVCardValue::Binary(data) => Some(data), + _ => None, + }) { + out.write_atomic(";ENCODING=b")?; + + if let Some(media_type) = data.content_type.as_deref() + && !self + .params + .iter() + .any(|param| param.name == VCardParameterName::Type) + && let Some(token) = legacy_media_type(self.name.as_str(), media_type) + { + out.write_atomic(";TYPE=")?; + out.write_atomic(&token)?; + } + } + + if self.values.iter().any(|v| match v { + ArchivedVCardValue::Text(s) => !s.is_ascii(), + ArchivedVCardValue::Component(items) => items.iter().any(|s| !s.is_ascii()), + _ => false, + }) { + out.write_atomic(";CHARSET=UTF-8")?; + } + } + + out.write_atomic(":")?; + + if with_value { + let (default_type, value_separator) = self.name.default_types(); + let default_type = default_type.unwrap_vcard(); + + let mut separator = ";"; + let mut escape_semicolon = + matches!(types.unwrap_or(&default_type), ArchivedVCardValueType::Text); + let mut escape_comma = escape_semicolon; + + match value_separator { + ValueSeparator::Comma => { + escape_comma = true; + separator = ","; + } + ValueSeparator::Semicolon => escape_semicolon = true, + ValueSeparator::SemicolonAndComma => { + escape_semicolon = true; + escape_comma = true; + } + _ => {} + } + + for (pos, value) in self.values.iter().enumerate() { + if pos > 0 { + out.write_atomic(separator)?; + } + + match value { + ArchivedVCardValue::Text(v) => { + write_text(out, v, escape_semicolon, escape_comma)?; + } + ArchivedVCardValue::Component(v) => { + for (pos, item) in v.iter().enumerate() { + if pos > 0 { + out.write_atomic(",")?; + } + write_text(out, item, true, true)?; + } + } + ArchivedVCardValue::Integer(v) => { + write!(out, "{v}")?; + } + ArchivedVCardValue::Float(v) => { + write!(out, "{v}")?; + } + ArchivedVCardValue::Boolean(v) => { + out.write_atomic(if *v { "TRUE" } else { "FALSE" })?; + } + ArchivedVCardValue::PartialDateTime(v) => { + let typ = if pos == 0 { + types + } else { + self.parameters(&VCardParameterName::Value) + .nth(pos) + .and_then(|v| v.as_value_type()) + .and_then(|v| v.iana().copied()) + } + .unwrap_or(&default_type); + if is_v4 { + v.format_as_vcard(out, typ)?; + } else { + v.format_as_legacy_vcard(out, typ)?; + } + } + ArchivedVCardValue::Binary(v) => { + if is_v4 { + let media_type = v.content_type.as_deref().unwrap_or_default(); + out.write_str("data:")?; + out.write_str(media_type)?; + out.write_str(";")?; + out.write_atomic("base64\\,")?; + } + write_bytes(out, &v.data)?; + } + ArchivedVCardValue::Sex(v) => { + out.write_atomic(v.as_str())?; + } + ArchivedVCardValue::GramGender(v) => { + out.write_atomic(v.as_str())?; + } + ArchivedVCardValue::Kind(v) => { + out.write_atomic(v.as_str())?; + } + } + } + } + out.end_line() + } +} + +impl crate::common::ArchivedPartialDateTime { + pub fn format_as_vcard( + &self, + out: &mut impl Write, + fmt: &ArchivedVCardValueType, + ) -> std::fmt::Result { + use ArchivedVCardValueType; + use rkyv::option::ArchivedOption; + + if matches!(fmt, ArchivedVCardValueType::Timestamp) { + write!( + out, + "{:04}{:02}{:02}T{:02}{:02}{:02}", + self.year.as_ref().map(u16::from).unwrap_or_default(), + self.month.as_ref().copied().unwrap_or_default(), + self.day.as_ref().copied().unwrap_or_default(), + self.hour.as_ref().copied().unwrap_or_default(), + self.minute.as_ref().copied().unwrap_or_default(), + self.second.as_ref().copied().unwrap_or_default() + )?; + + if let Some(tz_hour) = self.tz_hour.as_ref().copied() { + let tz_minute = self.tz_minute.as_ref().copied().unwrap_or_default(); + if tz_hour == 0 && tz_minute == 0 { + write!(out, "Z")?; + } else { + write!( + out, + "{}{:02}", + if self.tz_minus { "-" } else { "+" }, + tz_hour, + )?; + + if let Some(tz_minute) = self.tz_minute.as_ref() { + write!(out, "{:02}", tz_minute)?; + } + } + } + Ok(()) + } else { + let missing_time = + self.hour.is_none() && self.minute.is_none() && self.second.is_none(); + let missing_tz = self.tz_hour.is_none(); + + if matches!( + fmt, + ArchivedVCardValueType::Date + | ArchivedVCardValueType::DateAndOrTime + | ArchivedVCardValueType::DateTime + ) { + match (self.year, self.month, self.day) { + ( + ArchivedOption::Some(year), + ArchivedOption::Some(month), + ArchivedOption::Some(day), + ) => { + write!(out, "{:04}{:02}{:02}", year, month, day)?; + } + ( + ArchivedOption::Some(year), + ArchivedOption::Some(month), + ArchivedOption::None, + ) => { + if missing_time && missing_tz { + write!(out, "{:04}-{:02}", year, month)?; + } else { + write!(out, "{:04}{:02}", year, month)?; + } + } + ( + ArchivedOption::None, + ArchivedOption::Some(month), + ArchivedOption::Some(day), + ) => { + write!(out, "--{:02}{:02}", month, day)?; + } + (ArchivedOption::None, ArchivedOption::None, ArchivedOption::Some(day)) => { + write!(out, "---{:02}", day)?; + } + (ArchivedOption::Some(year), ArchivedOption::None, ArchivedOption::None) => { + write!(out, "{:04}", year)?; + } + (ArchivedOption::None, ArchivedOption::Some(month), ArchivedOption::None) => { + write!(out, "--{month}")?; + } + _ => {} + } + } + + if matches!( + fmt, + ArchivedVCardValueType::DateAndOrTime + | ArchivedVCardValueType::DateTime + | ArchivedVCardValueType::Time + ) && !missing_time + { + if matches!( + fmt, + ArchivedVCardValueType::DateAndOrTime | ArchivedVCardValueType::DateTime + ) { + write!(out, "T")?; + } + let mut last_is_some = false; + for value in [&self.hour, &self.minute, &self.second].iter() { + if let ArchivedOption::Some(value) = value { + write!(out, "{:02}", value)?; + last_is_some = true; + } else if !last_is_some { + write!(out, "-")?; + } + } + } + + if matches!( + fmt, + ArchivedVCardValueType::DateAndOrTime + | ArchivedVCardValueType::DateTime + | ArchivedVCardValueType::Time + | ArchivedVCardValueType::UtcOffset + ) { + match (self.tz_hour.as_ref(), self.tz_minute.as_ref()) { + (Some(0), Some(0)) | (Some(0), _) => { + write!(out, "Z")?; + } + (Some(hour), Some(minute)) => { + if self.tz_minus { + write!(out, "-")?; + } else { + write!(out, "+")?; + } + write!(out, "{hour:02}{minute:02}")?; + } + (Some(hour), None) => { + if self.tz_minus { + write!(out, "-")?; + } else { + write!(out, "+")?; + } + write!(out, "{hour:02}")?; + } + _ => {} + } + } + + Ok(()) + } + } + + pub fn format_as_legacy_vcard( + &self, + out: &mut impl Write, + fmt: &ArchivedVCardValueType, + ) -> std::fmt::Result { + if matches!(fmt, ArchivedVCardValueType::Timestamp) { + write!( + out, + "{:04}{:02}{:02}", + self.year.as_ref().map(u16::from).unwrap_or_default(), + self.month.as_ref().copied().unwrap_or_default(), + self.day.as_ref().copied().unwrap_or_default(), + )?; + + if self.hour.is_some() { + write!( + out, + "T{:02}{:02}{:02}", + self.hour.as_ref().copied().unwrap_or_default(), + self.minute.as_ref().copied().unwrap_or_default(), + self.second.as_ref().copied().unwrap_or_default() + )?; + } + + if let Some(tz_hour) = self.tz_hour.as_ref().copied() { + let tz_minute = self.tz_minute.as_ref().copied().unwrap_or_default(); + if tz_hour == 0 && tz_minute == 0 { + write!(out, "Z")?; + } else { + write!( + out, + "{}{:02}", + if self.tz_minus { "-" } else { "+" }, + tz_hour, + )?; + + if let Some(tz_minute) = self.tz_minute.as_ref().copied() { + write!(out, "{:02}", tz_minute)?; + } + } + } + Ok(()) + } else { + let missing_time = + self.hour.is_none() && self.minute.is_none() && self.second.is_none(); + + if matches!( + fmt, + ArchivedVCardValueType::Date + | ArchivedVCardValueType::DateAndOrTime + | ArchivedVCardValueType::DateTime + ) { + match (self.year.as_ref(), self.month.as_ref(), self.day.as_ref()) { + (Some(year), Some(month), Some(day)) => { + write!(out, "{:04}-{:02}-{:02}", year, month, day)?; + } + (Some(year), Some(month), None) => { + write!(out, "{:04}-{:02}", year, month)?; + } + (None, Some(month), Some(day)) => { + write!(out, "--{:02}-{:02}", month, day)?; + } + (None, None, Some(day)) => { + write!(out, "---{:02}", day)?; + } + (Some(year), None, None) => { + write!(out, "{:04}", year)?; + } + (None, Some(month), None) => { + write!(out, "--{month}")?; + } + _ => {} + } + } + + if matches!( + fmt, + ArchivedVCardValueType::DateAndOrTime + | ArchivedVCardValueType::DateTime + | ArchivedVCardValueType::Time + ) && !missing_time + { + if matches!( + fmt, + ArchivedVCardValueType::DateAndOrTime | ArchivedVCardValueType::DateTime + ) { + write!(out, "T")?; + } + let mut last_is_some = false; + for value in [&self.hour, &self.minute, &self.second].iter() { + if let Some(value) = value.as_ref() { + if last_is_some { + write!(out, ":")?; + } + write!(out, "{:02}", value)?; + last_is_some = true; + } else if !last_is_some { + write!(out, "-")?; + } + } + } + + if matches!( + fmt, + ArchivedVCardValueType::DateAndOrTime + | ArchivedVCardValueType::DateTime + | ArchivedVCardValueType::Time + | ArchivedVCardValueType::UtcOffset + ) { + match (self.tz_hour.as_ref(), self.tz_minute.as_ref()) { + (Some(0), Some(0)) | (Some(0), _) => { + write!(out, "Z")?; + } + (Some(hour), Some(minute)) => { + if self.tz_minus { + write!(out, "-")?; + } else { + write!(out, "+")?; + } + write!(out, "{hour:02}:{minute:02}")?; + } + (Some(hour), None) => { + if self.tz_minus { + write!(out, "-")?; + } else { + write!(out, "+")?; + } + write!(out, "{hour:02}")?; + } + _ => {} + } + } + + Ok(()) + } + } +} + +impl Display for ArchivedVCard { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + self.write_to(f, self.version().unwrap_or_default()) + } +} + +pub(crate) fn write_jscomps( + out: &mut impl LineWriter, + values: &[ArchivedJscomp], +) -> std::fmt::Result { + for (pos, item) in values.iter().enumerate() { + if pos > 0 { + out.write_atomic(";")?; + } + match item { + ArchivedJscomp::Entry { position, value } => { + write!(out, "{position}")?; + if *value > 0 { + write!(out, ",{value}")?; + } + } + ArchivedJscomp::Separator(s) => { + if !s.is_empty() { + out.write_atomic("s,")?; + + for ch in s.chars() { + match ch { + '\\' => out.write_atomic("\\\\")?, + ',' => out.write_atomic("\\,")?, + ':' => out.write_atomic("\\:")?, + '=' => out.write_atomic("\\=")?, + ';' => out.write_atomic("\\;")?, + '"' => out.write_atomic("\\\"")?, + '\r' | '\n' => {} + _ => out.write_char(ch)?, + } + } + } + } + } + } + + Ok(()) +} + +#[cfg(test)] +mod tests { + use crate::{ + Entry, Parser, + vcard::{ArchivedVCard, VCardVersion}, + }; + + #[test] + fn archived_v3_emits_charset_for_non_ascii() { + let input = "BEGIN:VCARD\r\nVERSION:3.0\r\nFN:José\r\nEND:VCARD\r\n".to_string(); + let mut parser = Parser::new(&input); + let Entry::VCard(vcard) = parser.entry() else { + panic!("expected vcard"); + }; + + let mut owned = String::new(); + vcard.write_to(&mut owned, VCardVersion::V3_0).unwrap(); + + let bytes = rkyv::to_bytes::(&vcard).unwrap(); + let archived = rkyv::access::(&bytes).unwrap(); + let mut archived_out = String::new(); + archived + .write_to(&mut archived_out, VCardVersion::V3_0) + .unwrap(); + + assert!( + owned.contains(";CHARSET=UTF-8"), + "owned missing charset: {owned}" + ); + assert!( + archived_out.contains(";CHARSET=UTF-8"), + "archived missing charset: {archived_out}" + ); + assert_eq!(owned, archived_out); + } +} diff --git a/vendor/calcard/src/vcard/types.rs b/vendor/calcard/src/vcard/types.rs new file mode 100644 index 00000000..fe39ca4f --- /dev/null +++ b/vendor/calcard/src/vcard/types.rs @@ -0,0 +1,608 @@ +/* + * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC + * + * SPDX-License-Identifier: Apache-2.0 OR MIT + */ + +use std::borrow::Cow; + +use super::*; + +impl IanaParse for VCardProperty { + fn parse(value: &[u8]) -> Option { + hashify::tiny_map_ignore_case!(value, + "BEGIN" => VCardProperty::Begin, + "END" => VCardProperty::End, + "SOURCE" => VCardProperty::Source, + "KIND" => VCardProperty::Kind, + "XML" => VCardProperty::Xml, + "FN" => VCardProperty::Fn, + "N" => VCardProperty::N, + "NICKNAME" => VCardProperty::Nickname, + "PHOTO" => VCardProperty::Photo, + "BDAY" => VCardProperty::Bday, + "ANNIVERSARY" => VCardProperty::Anniversary, + "GENDER" => VCardProperty::Gender, + "ADR" => VCardProperty::Adr, + "TEL" => VCardProperty::Tel, + "EMAIL" => VCardProperty::Email, + "IMPP" => VCardProperty::Impp, + "LANG" => VCardProperty::Lang, + "TZ" => VCardProperty::Tz, + "GEO" => VCardProperty::Geo, + "TITLE" => VCardProperty::Title, + "ROLE" => VCardProperty::Role, + "LOGO" => VCardProperty::Logo, + "ORG" => VCardProperty::Org, + "MEMBER" => VCardProperty::Member, + "RELATED" => VCardProperty::Related, + "CATEGORIES" => VCardProperty::Categories, + "NOTE" => VCardProperty::Note, + "PRODID" => VCardProperty::Prodid, + "REV" => VCardProperty::Rev, + "SOUND" => VCardProperty::Sound, + "UID" => VCardProperty::Uid, + "CLIENTPIDMAP" => VCardProperty::Clientpidmap, + "URL" => VCardProperty::Url, + "VERSION" => VCardProperty::Version, + "KEY" => VCardProperty::Key, + "FBURL" => VCardProperty::Fburl, + "CALADRURI" => VCardProperty::Caladruri, + "CALURI" => VCardProperty::Caluri, + "BIRTHPLACE" => VCardProperty::Birthplace, + "DEATHPLACE" => VCardProperty::Deathplace, + "DEATHDATE" => VCardProperty::Deathdate, + "EXPERTISE" => VCardProperty::Expertise, + "HOBBY" => VCardProperty::Hobby, + "INTEREST" => VCardProperty::Interest, + "ORG-DIRECTORY" => VCardProperty::OrgDirectory, + "CONTACT-URI" => VCardProperty::ContactUri, + "CREATED" => VCardProperty::Created, + "GRAMGENDER" => VCardProperty::Gramgender, + "LANGUAGE" => VCardProperty::Language, + "PRONOUNS" => VCardProperty::Pronouns, + "SOCIALPROFILE" => VCardProperty::Socialprofile, + "JSPROP" => VCardProperty::Jsprop, + ) + } +} + +impl VCardProperty { + pub fn as_str(&self) -> &str { + match self { + VCardProperty::Source => "SOURCE", + VCardProperty::Kind => "KIND", + VCardProperty::Xml => "XML", + VCardProperty::Fn => "FN", + VCardProperty::N => "N", + VCardProperty::Nickname => "NICKNAME", + VCardProperty::Photo => "PHOTO", + VCardProperty::Bday => "BDAY", + VCardProperty::Anniversary => "ANNIVERSARY", + VCardProperty::Gender => "GENDER", + VCardProperty::Adr => "ADR", + VCardProperty::Tel => "TEL", + VCardProperty::Email => "EMAIL", + VCardProperty::Impp => "IMPP", + VCardProperty::Lang => "LANG", + VCardProperty::Tz => "TZ", + VCardProperty::Geo => "GEO", + VCardProperty::Title => "TITLE", + VCardProperty::Role => "ROLE", + VCardProperty::Logo => "LOGO", + VCardProperty::Org => "ORG", + VCardProperty::Member => "MEMBER", + VCardProperty::Related => "RELATED", + VCardProperty::Categories => "CATEGORIES", + VCardProperty::Note => "NOTE", + VCardProperty::Prodid => "PRODID", + VCardProperty::Rev => "REV", + VCardProperty::Sound => "SOUND", + VCardProperty::Uid => "UID", + VCardProperty::Clientpidmap => "CLIENTPIDMAP", + VCardProperty::Url => "URL", + VCardProperty::Version => "VERSION", + VCardProperty::Key => "KEY", + VCardProperty::Fburl => "FBURL", + VCardProperty::Caladruri => "CALADRURI", + VCardProperty::Caluri => "CALURI", + VCardProperty::Birthplace => "BIRTHPLACE", + VCardProperty::Deathplace => "DEATHPLACE", + VCardProperty::Deathdate => "DEATHDATE", + VCardProperty::Expertise => "EXPERTISE", + VCardProperty::Hobby => "HOBBY", + VCardProperty::Interest => "INTEREST", + VCardProperty::OrgDirectory => "ORG-DIRECTORY", + VCardProperty::ContactUri => "CONTACT-URI", + VCardProperty::Created => "CREATED", + VCardProperty::Gramgender => "GRAMGENDER", + VCardProperty::Language => "LANGUAGE", + VCardProperty::Pronouns => "PRONOUNS", + VCardProperty::Socialprofile => "SOCIALPROFILE", + VCardProperty::Jsprop => "JSPROP", + VCardProperty::Begin => "BEGIN", + VCardProperty::End => "END", + VCardProperty::Other(v) => v.as_str(), + } + } +} + +impl VCardProperty { + // Returns the default value type and whether the property is multi-valued. + pub(crate) fn default_types(&self) -> (ValueType, ValueSeparator) { + match self { + VCardProperty::Source => (ValueType::Vcard(VCardValueType::Uri), ValueSeparator::None), + VCardProperty::Kind => (ValueType::Kind, ValueSeparator::None), + VCardProperty::Xml => (ValueType::Vcard(VCardValueType::Text), ValueSeparator::None), + VCardProperty::Fn => (ValueType::Vcard(VCardValueType::Text), ValueSeparator::None), + VCardProperty::N => ( + ValueType::Vcard(VCardValueType::Text), + ValueSeparator::SemicolonAndComma, + ), + VCardProperty::Nickname => ( + ValueType::Vcard(VCardValueType::Text), + ValueSeparator::Comma, + ), + VCardProperty::Photo => (ValueType::Vcard(VCardValueType::Uri), ValueSeparator::None), + VCardProperty::Bday => ( + ValueType::Vcard(VCardValueType::DateAndOrTime), + ValueSeparator::None, + ), + VCardProperty::Anniversary => ( + ValueType::Vcard(VCardValueType::DateAndOrTime), + ValueSeparator::None, + ), + VCardProperty::Gender => (ValueType::Sex, ValueSeparator::Semicolon), + VCardProperty::Adr => ( + ValueType::Vcard(VCardValueType::Text), + ValueSeparator::SemicolonAndComma, + ), + VCardProperty::Tel => (ValueType::Vcard(VCardValueType::Text), ValueSeparator::None), + VCardProperty::Email => (ValueType::Vcard(VCardValueType::Text), ValueSeparator::None), + VCardProperty::Impp => (ValueType::Vcard(VCardValueType::Uri), ValueSeparator::None), + VCardProperty::Lang => (ValueType::Vcard(VCardValueType::Text), ValueSeparator::None), + VCardProperty::Tz => ( + ValueType::Vcard(VCardValueType::UtcOffset), + ValueSeparator::None, + ), + VCardProperty::Geo => (ValueType::Vcard(VCardValueType::Uri), ValueSeparator::None), + VCardProperty::Title => (ValueType::Vcard(VCardValueType::Text), ValueSeparator::None), + VCardProperty::Role => (ValueType::Vcard(VCardValueType::Text), ValueSeparator::None), + VCardProperty::Logo => (ValueType::Vcard(VCardValueType::Uri), ValueSeparator::None), + VCardProperty::Org => ( + ValueType::Vcard(VCardValueType::Text), + ValueSeparator::Semicolon, + ), + VCardProperty::Member => (ValueType::Vcard(VCardValueType::Uri), ValueSeparator::None), + VCardProperty::Related => (ValueType::Vcard(VCardValueType::Uri), ValueSeparator::None), + VCardProperty::Categories => ( + ValueType::Vcard(VCardValueType::Text), + ValueSeparator::Comma, + ), + VCardProperty::Note => (ValueType::Vcard(VCardValueType::Text), ValueSeparator::None), + VCardProperty::Prodid => (ValueType::Vcard(VCardValueType::Text), ValueSeparator::None), + VCardProperty::Rev => ( + ValueType::Vcard(VCardValueType::Timestamp), + ValueSeparator::None, + ), + VCardProperty::Sound => (ValueType::Vcard(VCardValueType::Uri), ValueSeparator::None), + VCardProperty::Uid => (ValueType::Vcard(VCardValueType::Uri), ValueSeparator::None), + VCardProperty::Clientpidmap => ( + ValueType::Vcard(VCardValueType::Text), + ValueSeparator::Semicolon, + ), + VCardProperty::Url => (ValueType::Vcard(VCardValueType::Uri), ValueSeparator::None), + VCardProperty::Version => { + (ValueType::Vcard(VCardValueType::Text), ValueSeparator::None) + } + VCardProperty::Key => (ValueType::Vcard(VCardValueType::Uri), ValueSeparator::None), + VCardProperty::Fburl => (ValueType::Vcard(VCardValueType::Uri), ValueSeparator::None), + VCardProperty::Caladruri => { + (ValueType::Vcard(VCardValueType::Uri), ValueSeparator::None) + } + VCardProperty::Caluri => (ValueType::Vcard(VCardValueType::Uri), ValueSeparator::None), + VCardProperty::Birthplace => { + (ValueType::Vcard(VCardValueType::Text), ValueSeparator::None) + } + VCardProperty::Deathplace => { + (ValueType::Vcard(VCardValueType::Text), ValueSeparator::None) + } + VCardProperty::Deathdate => ( + ValueType::Vcard(VCardValueType::DateAndOrTime), + ValueSeparator::None, + ), + VCardProperty::Expertise => { + (ValueType::Vcard(VCardValueType::Text), ValueSeparator::None) + } + VCardProperty::Hobby => (ValueType::Vcard(VCardValueType::Text), ValueSeparator::None), + VCardProperty::Interest => { + (ValueType::Vcard(VCardValueType::Text), ValueSeparator::None) + } + VCardProperty::OrgDirectory => { + (ValueType::Vcard(VCardValueType::Uri), ValueSeparator::None) + } + VCardProperty::ContactUri => { + (ValueType::Vcard(VCardValueType::Uri), ValueSeparator::None) + } + VCardProperty::Created => ( + ValueType::Vcard(VCardValueType::Timestamp), + ValueSeparator::None, + ), + VCardProperty::Gramgender => (ValueType::GramGender, ValueSeparator::None), + VCardProperty::Language => ( + ValueType::Vcard(VCardValueType::LanguageTag), + ValueSeparator::None, + ), + VCardProperty::Pronouns => { + (ValueType::Vcard(VCardValueType::Text), ValueSeparator::None) + } + VCardProperty::Socialprofile => { + (ValueType::Vcard(VCardValueType::Uri), ValueSeparator::None) + } + VCardProperty::Jsprop => (ValueType::Vcard(VCardValueType::Text), ValueSeparator::None), + VCardProperty::Other(_) => { + (ValueType::Vcard(VCardValueType::Text), ValueSeparator::None) + } + VCardProperty::Begin | VCardProperty::End => { + (ValueType::Vcard(VCardValueType::Text), ValueSeparator::Skip) + } + } + } +} + +impl Eq for VCardValue {} + +impl IanaString for VCardValueType { + fn as_str(&self) -> &'static str { + match self { + VCardValueType::Boolean => "BOOLEAN", + VCardValueType::Date => "DATE", + VCardValueType::DateAndOrTime => "DATE-AND-OR-TIME", + VCardValueType::DateTime => "DATE-TIME", + VCardValueType::Float => "FLOAT", + VCardValueType::Integer => "INTEGER", + VCardValueType::LanguageTag => "LANGUAGE-TAG", + VCardValueType::Text => "TEXT", + VCardValueType::Time => "TIME", + VCardValueType::Timestamp => "TIMESTAMP", + VCardValueType::Uri => "URI", + VCardValueType::UtcOffset => "UTC-OFFSET", + } + } +} + +impl IanaParse for VCardValueType { + fn parse(value: &[u8]) -> Option { + hashify::tiny_map_ignore_case!(value, + "BOOLEAN" => VCardValueType::Boolean, + "DATE" => VCardValueType::Date, + "DATE-AND-OR-TIME" => VCardValueType::DateAndOrTime, + "DATE-TIME" => VCardValueType::DateTime, + "FLOAT" => VCardValueType::Float, + "INTEGER" => VCardValueType::Integer, + "LANGUAGE-TAG" => VCardValueType::LanguageTag, + "TEXT" => VCardValueType::Text, + "TIME" => VCardValueType::Time, + "TIMESTAMP" => VCardValueType::Timestamp, + "URI" => VCardValueType::Uri, + "UTC-OFFSET" => VCardValueType::UtcOffset, + ) + } +} + +impl IanaParse for VCardLevel { + fn parse(value: &[u8]) -> Option { + hashify::tiny_map_ignore_case!(value, + "beginner" => VCardLevel::Beginner, + "average" => VCardLevel::Average, + "expert" => VCardLevel::Expert, + "high" => VCardLevel::High, + "medium" => VCardLevel::Medium, + "low" => VCardLevel::Low, + ) + } +} + +impl IanaString for VCardLevel { + fn as_str(&self) -> &'static str { + match self { + VCardLevel::Beginner => "BEGINNER", + VCardLevel::Average => "AVERAGE", + VCardLevel::Expert => "EXPERT", + VCardLevel::High => "HIGH", + VCardLevel::Medium => "MEDIUM", + VCardLevel::Low => "LOW", + } + } +} + +impl IanaParse for VCardPhonetic { + fn parse(value: &[u8]) -> Option { + hashify::tiny_map_ignore_case!(value, + "ipa" => VCardPhonetic::Ipa, + "jyut" => VCardPhonetic::Jyut, + "piny" => VCardPhonetic::Piny, + "script" => VCardPhonetic::Script, + ) + } +} + +impl IanaString for VCardPhonetic { + fn as_str(&self) -> &'static str { + match self { + VCardPhonetic::Ipa => "IPA", + VCardPhonetic::Jyut => "JYUT", + VCardPhonetic::Piny => "PINY", + VCardPhonetic::Script => "SCRIPT", + } + } +} + +impl IanaParse for VCardType { + fn parse(value: &[u8]) -> Option { + hashify::tiny_map_ignore_case!(value, + "work" => VCardType::Work, + "home" => VCardType::Home, + "billing" => VCardType::Billing, + "delivery" => VCardType::Delivery, + "contact" => VCardType::Contact, + "acquaintance" => VCardType::Acquaintance, + "friend" => VCardType::Friend, + "met" => VCardType::Met, + "co-worker" => VCardType::CoWorker, + "colleague" => VCardType::Colleague, + "co-resident" => VCardType::CoResident, + "neighbor" => VCardType::Neighbor, + "child" => VCardType::Child, + "parent" => VCardType::Parent, + "sibling" => VCardType::Sibling, + "spouse" => VCardType::Spouse, + "kin" => VCardType::Kin, + "muse" => VCardType::Muse, + "crush" => VCardType::Crush, + "date" => VCardType::Date, + "sweetheart" => VCardType::Sweetheart, + "me" => VCardType::Me, + "agent" => VCardType::Agent, + "emergency" => VCardType::Emergency, + "text" => VCardType::Text, + "voice" => VCardType::Voice, + "fax" => VCardType::Fax, + "cell" => VCardType::Cell, + "video" => VCardType::Video, + "pager" => VCardType::Pager, + "textphone" => VCardType::Textphone, + "main-number" => VCardType::MainNumber, + ) + } +} + +impl IanaString for VCardType { + fn as_str(&self) -> &'static str { + match self { + VCardType::Work => "WORK", + VCardType::Home => "HOME", + VCardType::Billing => "BILLING", + VCardType::Delivery => "DELIVERY", + VCardType::Contact => "CONTACT", + VCardType::Acquaintance => "ACQUAINTANCE", + VCardType::Friend => "FRIEND", + VCardType::Met => "MET", + VCardType::CoWorker => "CO-WORKER", + VCardType::Colleague => "COLLEAGUE", + VCardType::CoResident => "CO-RESIDENT", + VCardType::Neighbor => "NEIGHBOR", + VCardType::Child => "CHILD", + VCardType::Parent => "PARENT", + VCardType::Sibling => "SIBLING", + VCardType::Spouse => "SPOUSE", + VCardType::Kin => "KIN", + VCardType::Muse => "MUSE", + VCardType::Crush => "CRUSH", + VCardType::Date => "DATE", + VCardType::Sweetheart => "SWEETHEART", + VCardType::Me => "ME", + VCardType::Agent => "AGENT", + VCardType::Emergency => "EMERGENCY", + VCardType::Text => "TEXT", + VCardType::Voice => "VOICE", + VCardType::Fax => "FAX", + VCardType::Cell => "CELL", + VCardType::Video => "VIDEO", + VCardType::Pager => "PAGER", + VCardType::Textphone => "TEXTPHONE", + VCardType::MainNumber => "MAIN-NUMBER", + } + } +} + +impl IanaParse for VCardGramGender { + fn parse(value: &[u8]) -> Option { + hashify::tiny_map_ignore_case!(value, + "animate" => VCardGramGender::Animate, + "common" => VCardGramGender::Common, + "feminine" => VCardGramGender::Feminine, + "inanimate" => VCardGramGender::Inanimate, + "masculine" => VCardGramGender::Masculine, + "neuter" => VCardGramGender::Neuter, + ) + } +} + +impl IanaString for VCardGramGender { + fn as_str(&self) -> &'static str { + match self { + VCardGramGender::Animate => "ANIMATE", + VCardGramGender::Common => "COMMON", + VCardGramGender::Feminine => "FEMININE", + VCardGramGender::Inanimate => "INANIMATE", + VCardGramGender::Masculine => "MASCULINE", + VCardGramGender::Neuter => "NEUTER", + } + } +} + +impl IanaParse for VCardSex { + fn parse(value: &[u8]) -> Option { + hashify::tiny_map_ignore_case!(value, + "M" => VCardSex::Male, + "F" => VCardSex::Female, + "O" => VCardSex::Other, + "N" => VCardSex::NoneOrNotApplicable, + "U" => VCardSex::Unknown, + ) + } +} + +impl IanaString for VCardSex { + fn as_str(&self) -> &'static str { + match self { + VCardSex::Male => "M", + VCardSex::Female => "F", + VCardSex::Other => "O", + VCardSex::NoneOrNotApplicable => "N", + VCardSex::Unknown => "U", + } + } +} + +impl IanaParse for VCardKind { + fn parse(value: &[u8]) -> Option { + hashify::tiny_map_ignore_case!(value, + "individual" => VCardKind::Individual, + "group" => VCardKind::Group, + "org" => VCardKind::Org, + "location" => VCardKind::Location, + "application" => VCardKind::Application, + "device" => VCardKind::Device, + ) + } +} + +impl IanaString for VCardKind { + fn as_str(&self) -> &'static str { + match self { + VCardKind::Individual => "INDIVIDUAL", + VCardKind::Group => "GROUP", + VCardKind::Org => "ORG", + VCardKind::Location => "LOCATION", + VCardKind::Application => "APPLICATION", + VCardKind::Device => "DEVICE", + } + } +} + +impl VCardParameterName { + pub fn try_parse(input: &[u8]) -> Option { + hashify::tiny_map_ignore_case!(input, + b"LANGUAGE" => VCardParameterName::Language, + b"VALUE" => VCardParameterName::Value, + b"PREF" => VCardParameterName::Pref, + b"ALTID" => VCardParameterName::Altid, + b"PID" => VCardParameterName::Pid, + b"TYPE" => VCardParameterName::Type, + b"MEDIATYPE" => VCardParameterName::Mediatype, + b"CALSCALE" => VCardParameterName::Calscale, + b"SORT-AS" => VCardParameterName::SortAs, + b"GEO" => VCardParameterName::Geo, + b"TZ" => VCardParameterName::Tz, + b"INDEX" => VCardParameterName::Index, + b"LEVEL" => VCardParameterName::Level, + b"GROUP" => VCardParameterName::Group, + b"CC" => VCardParameterName::Cc, + b"AUTHOR" => VCardParameterName::Author, + b"AUTHOR-NAME" => VCardParameterName::AuthorName, + b"CREATED" => VCardParameterName::Created, + b"DERIVED" => VCardParameterName::Derived, + b"LABEL" => VCardParameterName::Label, + b"PHONETIC" => VCardParameterName::Phonetic, + b"PROP-ID" => VCardParameterName::PropId, + b"SCRIPT" => VCardParameterName::Script, + b"SERVICE-TYPE" => VCardParameterName::ServiceType, + b"USERNAME" => VCardParameterName::Username, + b"JSPTR" => VCardParameterName::Jsptr, + b"JSCOMPS" => VCardParameterName::Jscomps, + ) + } + + pub fn parse(input: &str) -> Self { + Self::try_parse(input.as_bytes()).unwrap_or_else(|| VCardParameterName::Other(input.into())) + } + + pub fn as_str(&self) -> &str { + match self { + VCardParameterName::Language => "LANGUAGE", + VCardParameterName::Value => "VALUE", + VCardParameterName::Pref => "PREF", + VCardParameterName::Altid => "ALTID", + VCardParameterName::Pid => "PID", + VCardParameterName::Type => "TYPE", + VCardParameterName::Mediatype => "MEDIATYPE", + VCardParameterName::Calscale => "CALSCALE", + VCardParameterName::SortAs => "SORT-AS", + VCardParameterName::Geo => "GEO", + VCardParameterName::Tz => "TZ", + VCardParameterName::Index => "INDEX", + VCardParameterName::Level => "LEVEL", + VCardParameterName::Group => "GROUP", + VCardParameterName::Cc => "CC", + VCardParameterName::Author => "AUTHOR", + VCardParameterName::AuthorName => "AUTHOR-NAME", + VCardParameterName::Created => "CREATED", + VCardParameterName::Derived => "DERIVED", + VCardParameterName::Label => "LABEL", + VCardParameterName::Phonetic => "PHONETIC", + VCardParameterName::PropId => "PROP-ID", + VCardParameterName::Script => "SCRIPT", + VCardParameterName::ServiceType => "SERVICE-TYPE", + VCardParameterName::Username => "USERNAME", + VCardParameterName::Jsptr => "JSPTR", + VCardParameterName::Jscomps => "JSCOMPS", + VCardParameterName::Other(name) => name, + } + } + + pub fn into_string(self) -> Cow<'static, str> { + match self { + VCardParameterName::Language => "LANGUAGE".into(), + VCardParameterName::Value => "VALUE".into(), + VCardParameterName::Pref => "PREF".into(), + VCardParameterName::Altid => "ALTID".into(), + VCardParameterName::Pid => "PID".into(), + VCardParameterName::Type => "TYPE".into(), + VCardParameterName::Mediatype => "MEDIATYPE".into(), + VCardParameterName::Calscale => "CALSCALE".into(), + VCardParameterName::SortAs => "SORT-AS".into(), + VCardParameterName::Geo => "GEO".into(), + VCardParameterName::Tz => "TZ".into(), + VCardParameterName::Index => "INDEX".into(), + VCardParameterName::Level => "LEVEL".into(), + VCardParameterName::Group => "GROUP".into(), + VCardParameterName::Cc => "CC".into(), + VCardParameterName::Author => "AUTHOR".into(), + VCardParameterName::AuthorName => "AUTHOR-NAME".into(), + VCardParameterName::Created => "CREATED".into(), + VCardParameterName::Derived => "DERIVED".into(), + VCardParameterName::Label => "LABEL".into(), + VCardParameterName::Phonetic => "PHONETIC".into(), + VCardParameterName::PropId => "PROP-ID".into(), + VCardParameterName::Script => "SCRIPT".into(), + VCardParameterName::ServiceType => "SERVICE-TYPE".into(), + VCardParameterName::Username => "USERNAME".into(), + VCardParameterName::Jsptr => "JSPTR".into(), + VCardParameterName::Jscomps => "JSCOMPS".into(), + VCardParameterName::Other(name) => name.into(), + } + } +} + +impl VCardVersion { + pub fn try_parse(input: &str) -> Option { + hashify::tiny_map!(input.as_bytes(), + b"4.0" => VCardVersion::V4_0, + b"3.0" => VCardVersion::V3_0, + b"2.1" => VCardVersion::V2_1, + b"2.0" => VCardVersion::V2_0, + ) + } +} diff --git a/vendor/calcard/src/vcard/utils.rs b/vendor/calcard/src/vcard/utils.rs new file mode 100644 index 00000000..272c1ce1 --- /dev/null +++ b/vendor/calcard/src/vcard/utils.rs @@ -0,0 +1,424 @@ +/* + * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC + * + * SPDX-License-Identifier: Apache-2.0 OR MIT + */ + +use super::{VCard, VCardEntry, VCardParameterName, VCardProperty, VCardValue, VCardVersion}; +use crate::{ + common::{ + CalendarScale, Data, IanaString, IanaType, PartialDateTime, + writer::{write_bytes, write_jscomps}, + }, + vcard::{ + Jscomp, VCardLevel, VCardParameter, VCardParameterValue, VCardPhonetic, VCardType, + VCardValueType, media_type::media_type_from_legacy, + }, +}; +use std::borrow::Cow; + +impl VCard { + pub fn uid(&self) -> Option<&str> { + self.property(&VCardProperty::Uid) + .and_then(|e| e.values.first()) + .and_then(|v| v.as_text()) + } + + pub fn property(&self, prop: &VCardProperty) -> Option<&VCardEntry> { + self.entries.iter().find(|entry| &entry.name == prop) + } + + pub fn properties<'x, 'y: 'x>( + &'x self, + prop: &'y VCardProperty, + ) -> impl Iterator + 'x { + self.entries.iter().filter(move |entry| &entry.name == prop) + } + + pub fn version(&self) -> Option { + self.entries + .iter() + .find(|e| e.name == VCardProperty::Version) + .and_then(|e| { + e.values + .first() + .and_then(|v| v.as_text()) + .and_then(VCardVersion::try_parse) + }) + } + + pub fn size(&self) -> usize { + self.entries.iter().map(|e| e.size()).sum() + } +} + +impl VCardEntry { + #[inline] + pub fn parameters( + &self, + prop: &VCardParameterName, + ) -> impl Iterator { + self.params.iter().filter_map(move |param| { + if ¶m.name == prop { + Some(¶m.value) + } else { + None + } + }) + } + + pub fn language(&self) -> Option<&str> { + self.parameters(&VCardParameterName::Language) + .find_map(|v| v.as_text()) + } + + pub fn alt_id(&self) -> Option<&str> { + self.parameters(&VCardParameterName::Altid) + .find_map(|v| v.as_text()) + } + + pub fn prop_id(&self) -> Option<&str> { + self.parameters(&VCardParameterName::PropId) + .find_map(|v| v.as_text()) + } + + pub fn phonetic_system(&self) -> Option> { + self.parameters(&VCardParameterName::Phonetic) + .find_map(|v| v.as_phonetic()) + } + + pub fn phonetic_script(&self) -> Option<&str> { + self.parameters(&VCardParameterName::Script) + .find_map(|v| v.as_text()) + } + + pub fn size(&self) -> usize { + self.group.as_ref().map_or(0, |g| g.len()) + + self.name.as_str().len() + + self.params.iter().map(|p| p.size()).sum::() + + self.values.iter().map(|v| v.size()).sum::() + } + + pub(crate) fn normalize_legacy_media_type(&mut self) { + if self + .values + .iter() + .any(|value| matches!(value, VCardValue::Binary(data) if data.content_type.is_none())) + { + let property = self.name.as_str(); + let media_type = + self.params + .iter() + .find_map(|param| match (¶m.name, ¶m.value) { + (VCardParameterName::Mediatype, VCardParameterValue::Text(text)) => { + Some(Cow::Owned(text.to_ascii_lowercase())) + } + (VCardParameterName::Type, VCardParameterValue::Text(text)) => { + media_type_from_legacy(property, text) + } + _ => None, + }); + + if let Some(media_type) = media_type { + for value in &mut self.values { + if let VCardValue::Binary(data) = value + && data.content_type.is_none() + { + data.content_type = Some(media_type.to_string()); + } + } + } + } + } +} + +impl VCardValue { + pub fn as_text(&self) -> Option<&str> { + match self { + VCardValue::Text(v) => v.as_str().into(), + VCardValue::Sex(v) => v.as_str().into(), + VCardValue::GramGender(v) => v.as_str().into(), + VCardValue::Kind(v) => v.as_str().into(), + VCardValue::Component(v) => v.first().map(|s| s.as_str()), + VCardValue::Integer(_) + | VCardValue::Float(_) + | VCardValue::Boolean(_) + | VCardValue::PartialDateTime(_) + | VCardValue::Binary(_) => None, + } + } + + pub fn into_text(self) -> Option> { + match self { + VCardValue::Text(s) => Some(Cow::Owned(s)), + VCardValue::Sex(v) => Some(Cow::Borrowed(v.as_str())), + VCardValue::GramGender(v) => Some(Cow::Borrowed(v.as_str())), + VCardValue::Kind(v) => Some(Cow::Borrowed(v.as_str())), + VCardValue::Component(v) => Some(Cow::Owned(v.join(","))), + VCardValue::Integer(_) + | VCardValue::Float(_) + | VCardValue::Boolean(_) + | VCardValue::PartialDateTime(_) + | VCardValue::Binary(_) => None, + } + } + + pub fn into_uri(self) -> Option { + match self { + VCardValue::Binary(v) => Some(v.to_unwrapped_string()), + VCardValue::Text(v) => Some(v), + _ => None, + } + } + + pub fn as_integer(&self) -> Option { + match self { + VCardValue::Integer(i) => Some(*i), + _ => None, + } + } + + pub fn as_float(&self) -> Option { + match self { + VCardValue::Float(f) => Some(*f), + _ => None, + } + } + + pub fn as_boolean(&self) -> Option { + match self { + VCardValue::Boolean(b) => Some(*b), + _ => None, + } + } + + pub fn as_partial_date_time(&self) -> Option<&PartialDateTime> { + match self { + VCardValue::PartialDateTime(dt) => Some(dt), + _ => None, + } + } + + pub fn as_binary(&self) -> Option<&Data> { + match self { + VCardValue::Binary(d) => Some(d), + _ => None, + } + } + + pub fn size(&self) -> usize { + match self { + VCardValue::Text(v) => v.len(), + VCardValue::Sex(v) => v.as_str().len(), + VCardValue::GramGender(v) => v.as_str().len(), + VCardValue::Kind(v) => v.as_str().len(), + VCardValue::Component(v) => v.iter().map(|s| s.len()).sum::(), + VCardValue::Integer(_) => std::mem::size_of::(), + VCardValue::Float(_) => std::mem::size_of::(), + VCardValue::Boolean(_) => std::mem::size_of::(), + VCardValue::PartialDateTime(_) => std::mem::size_of::(), + VCardValue::Binary(d) => { + d.content_type.as_ref().map_or(0, |ct| ct.len()) + d.data.len() + } + } + } +} + +impl Data { + pub fn to_unwrapped_string(&self) -> String { + use std::fmt::Write; + let media_type = self.content_type.as_deref().unwrap_or_default(); + let mut out = String::with_capacity(self.data.len().div_ceil(4) + media_type.len() + 14); + let _ = write!(&mut out, "data:{media_type};base64,"); + let _ = write_bytes(&mut out, &self.data); + out + } +} + +impl VCardParameterValue { + pub fn into_text(self) -> Cow<'static, str> { + match self { + VCardParameterValue::Text(s) => Cow::Owned(s), + VCardParameterValue::ValueType(v) => Cow::Borrowed(v.as_str()), + VCardParameterValue::Type(v) => Cow::Borrowed(v.as_str()), + VCardParameterValue::Calscale(v) => Cow::Borrowed(v.as_str()), + VCardParameterValue::Level(v) => Cow::Borrowed(v.as_str()), + VCardParameterValue::Phonetic(v) => Cow::Borrowed(v.as_str()), + VCardParameterValue::Jscomps(v) => { + let mut jscomps = String::new(); + let _ = write_jscomps(&mut jscomps, &v); + Cow::Owned(jscomps) + } + VCardParameterValue::Integer(i) => Cow::Owned(i.to_string()), + VCardParameterValue::Timestamp(t) => Cow::Owned(t.to_string()), + VCardParameterValue::Bool(b) => Cow::Borrowed(if b { "true" } else { "false" }), + VCardParameterValue::Null => Cow::Borrowed(""), + } + } + + pub fn as_text(&self) -> Option<&str> { + match self { + VCardParameterValue::Text(v) => Some(v.as_str()), + VCardParameterValue::ValueType(v) => Some(v.as_str()), + VCardParameterValue::Type(v) => Some(v.as_str()), + VCardParameterValue::Calscale(v) => Some(v.as_str()), + VCardParameterValue::Level(v) => Some(v.as_str()), + VCardParameterValue::Phonetic(v) => Some(v.as_str()), + VCardParameterValue::Jscomps(_) + | VCardParameterValue::Integer(_) + | VCardParameterValue::Timestamp(_) + | VCardParameterValue::Bool(_) + | VCardParameterValue::Null => None, + } + } + + pub fn into_phonetic(self) -> Option> { + match self { + VCardParameterValue::Phonetic(v) => Some(IanaType::Iana(v)), + VCardParameterValue::Text(v) => Some(IanaType::Other(v)), + _ => None, + } + } + + pub fn as_phonetic(&self) -> Option> { + match self { + VCardParameterValue::Phonetic(v) => Some(IanaType::Iana(v)), + VCardParameterValue::Text(v) => Some(IanaType::Other(v.as_str())), + _ => None, + } + } + + pub fn into_calscale(self) -> Option> { + match self { + VCardParameterValue::Calscale(v) => Some(IanaType::Iana(v)), + VCardParameterValue::Text(v) => Some(IanaType::Other(v)), + _ => None, + } + } + + pub fn as_calscale(&self) -> Option> { + match self { + VCardParameterValue::Calscale(v) => Some(IanaType::Iana(v)), + VCardParameterValue::Text(v) => Some(IanaType::Other(v.as_str())), + _ => None, + } + } + + pub fn into_level(self) -> Option> { + match self { + VCardParameterValue::Level(v) => Some(IanaType::Iana(v)), + VCardParameterValue::Text(v) => Some(IanaType::Other(v)), + _ => None, + } + } + + pub fn as_level(&self) -> Option> { + match self { + VCardParameterValue::Level(v) => Some(IanaType::Iana(v)), + VCardParameterValue::Text(v) => Some(IanaType::Other(v.as_str())), + _ => None, + } + } + + pub fn into_type(self) -> Option> { + match self { + VCardParameterValue::Type(v) => Some(IanaType::Iana(v)), + VCardParameterValue::Text(v) => Some(IanaType::Other(v)), + _ => None, + } + } + + pub fn as_type(&self) -> Option> { + match self { + VCardParameterValue::Type(v) => Some(IanaType::Iana(v)), + VCardParameterValue::Text(v) => Some(IanaType::Other(v.as_str())), + _ => None, + } + } + + pub fn into_value_type(self) -> Option> { + match self { + VCardParameterValue::ValueType(v) => Some(IanaType::Iana(v)), + VCardParameterValue::Text(v) => Some(IanaType::Other(v)), + _ => None, + } + } + + pub fn as_value_type(&self) -> Option> { + match self { + VCardParameterValue::ValueType(v) => Some(IanaType::Iana(v)), + VCardParameterValue::Text(v) => Some(IanaType::Other(v.as_str())), + _ => None, + } + } + + pub fn as_bool(&self) -> Option> { + match self { + VCardParameterValue::Bool(b) => Some(IanaType::Iana(*b)), + VCardParameterValue::Text(v) => Some(IanaType::Other(v.as_str())), + _ => None, + } + } + + pub fn as_integer(&self) -> Option> { + match self { + VCardParameterValue::Integer(i) => Some(IanaType::Iana(*i)), + VCardParameterValue::Text(v) => Some(IanaType::Other(v.as_str())), + _ => None, + } + } + + pub fn into_timestamp(self) -> Option> { + match self { + VCardParameterValue::Timestamp(t) => Some(IanaType::Iana(t)), + VCardParameterValue::Text(v) => Some(IanaType::Other(v)), + _ => None, + } + } + + pub fn as_timestamp(&self) -> Option> { + match self { + VCardParameterValue::Timestamp(t) => Some(IanaType::Iana(*t)), + VCardParameterValue::Text(v) => Some(IanaType::Other(v.as_str())), + _ => None, + } + } + + pub fn into_jscomps(self) -> Option> { + match self { + VCardParameterValue::Jscomps(v) => Some(v), + _ => None, + } + } + + pub fn size(&self) -> usize { + match self { + VCardParameterValue::Text(v) => v.len(), + VCardParameterValue::ValueType(v) => v.as_str().len(), + VCardParameterValue::Type(v) => v.as_str().len(), + VCardParameterValue::Calscale(v) => v.as_str().len(), + VCardParameterValue::Level(v) => v.as_str().len(), + VCardParameterValue::Phonetic(v) => v.as_str().len(), + VCardParameterValue::Jscomps(v) => v.iter().map(|j| j.size()).sum::(), + VCardParameterValue::Integer(_) => std::mem::size_of::(), + VCardParameterValue::Timestamp(_) => std::mem::size_of::(), + VCardParameterValue::Bool(_) => std::mem::size_of::(), + VCardParameterValue::Null => 0, + } + } +} + +impl VCardParameter { + pub fn size(&self) -> usize { + self.name.as_str().len() + self.value.size() + } +} + +impl Jscomp { + pub fn size(&self) -> usize { + match self { + Jscomp::Entry { .. } => std::mem::size_of::(), + Jscomp::Separator(value) => value.len(), + } + } +} diff --git a/vendor/calcard/src/vcard/writer.rs b/vendor/calcard/src/vcard/writer.rs new file mode 100644 index 00000000..ff395b10 --- /dev/null +++ b/vendor/calcard/src/vcard/writer.rs @@ -0,0 +1,651 @@ +/* + * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC + * + * SPDX-License-Identifier: Apache-2.0 OR MIT + */ + +use super::{PartialDateTime, VCard, VCardEntry, VCardValueType, VCardVersion}; +use crate::{ + common::{ + IanaString, + parser::Timestamp, + writer::{ + FoldingWriter, LineWriter, write_bytes, write_jscomps, write_param_value, write_text, + }, + }, + vcard::{ + VCardParameterName, VCardParameterValue, VCardProperty, VCardValue, ValueSeparator, + media_type::legacy_media_type, + }, +}; +use std::fmt::{Display, Write}; + +impl VCard { + pub fn write_to(&self, out: &mut impl Write, version: VCardVersion) -> std::fmt::Result { + write!(out, "BEGIN:VCARD\r\n")?; + write!(out, "VERSION:{version}\r\n")?; + let is_v4 = matches!(version, VCardVersion::V4_0); + for entry in &self.entries { + if !matches!( + entry.name, + VCardProperty::Begin | VCardProperty::End | VCardProperty::Version + ) { + entry.write_to(out, is_v4)?; + } + } + write!(out, "END:VCARD\r\n") + } +} + +impl VCardEntry { + pub fn write_to(&self, out: &mut impl Write, is_v4: bool) -> std::fmt::Result { + let mut folded = FoldingWriter::new(out); + let out = &mut folded; + + if let Some(group_name) = &self.group { + out.write_atomic(group_name)?; + out.write_atomic(".")?; + } + + out.write_atomic(self.name.as_str())?; + let mut types = None; + let mut last_param: Option<&VCardParameterName> = None; + + for param in &self.params { + if last_param.is_some_and(|last_param| last_param == ¶m.name) { + out.write_atomic(",")?; + } else { + out.write_atomic(";")?; + out.write_atomic(param.name.as_str())?; + if !matches!(param.value, VCardParameterValue::Null) { + out.write_atomic("=")?; + } + last_param = Some(¶m.name); + } + + match ¶m.value { + VCardParameterValue::Text(v) => { + write_param_value(out, v)?; + } + VCardParameterValue::Integer(i) => { + write!(out, "{i}")?; + } + VCardParameterValue::Timestamp(v) => { + write!(out, "{}", Timestamp(*v))?; + } + VCardParameterValue::Bool(v) => { + out.write_atomic(if *v { "TRUE" } else { "FALSE" })?; + } + VCardParameterValue::ValueType(v) => { + if types.is_none() { + types = Some(v); + } + write_param_value(out, v.as_str())?; + } + VCardParameterValue::Type(v) => { + write_param_value(out, v.as_str())?; + } + VCardParameterValue::Calscale(v) => { + write_param_value(out, v.as_str())?; + } + VCardParameterValue::Level(v) => { + write_param_value(out, v.as_str())?; + } + VCardParameterValue::Phonetic(v) => { + write_param_value(out, v.as_str())?; + } + VCardParameterValue::Jscomps(v) => { + out.write_atomic("\"")?; + write_jscomps(out, v)?; + out.write_atomic("\"")?; + last_param = None; + } + VCardParameterValue::Null => { + last_param = None; + } + } + } + + if !is_v4 { + if let Some(data) = self.values.iter().find_map(|v| match v { + VCardValue::Binary(data) => Some(data), + _ => None, + }) { + out.write_atomic(";ENCODING=b")?; + + if let Some(media_type) = data.content_type.as_deref() + && !self + .params + .iter() + .any(|param| param.name == VCardParameterName::Type) + && let Some(token) = legacy_media_type(self.name.as_str(), media_type) + { + out.write_atomic(";TYPE=")?; + out.write_atomic(&token)?; + } + } + + if self.values.iter().any(|v| match v { + VCardValue::Text(s) => !s.is_ascii(), + VCardValue::Component(items) => items.iter().any(|s| !s.is_ascii()), + _ => false, + }) { + out.write_atomic(";CHARSET=UTF-8")?; + } + } + + out.write_atomic(":")?; + + let (default_type, value_separator) = self.name.default_types(); + let default_type = default_type.unwrap_vcard(); + + let mut separator = ";"; + let mut escape_semicolon = matches!(types.unwrap_or(&default_type), VCardValueType::Text); + let mut escape_comma = escape_semicolon; + + match value_separator { + ValueSeparator::Comma => { + escape_comma = true; + separator = ","; + } + ValueSeparator::Semicolon => escape_semicolon = true, + ValueSeparator::SemicolonAndComma => { + escape_semicolon = true; + escape_comma = true; + } + _ => {} + } + + for (pos, value) in self.values.iter().enumerate() { + if pos > 0 { + out.write_atomic(separator)?; + } + + match value { + VCardValue::Text(v) => { + write_text(out, v, escape_semicolon, escape_comma)?; + } + VCardValue::Component(v) => { + for (pos, item) in v.iter().enumerate() { + if pos > 0 { + out.write_atomic(",")?; + } + write_text(out, item, true, true)?; + } + } + VCardValue::Integer(v) => { + write!(out, "{v}")?; + } + VCardValue::Float(v) => { + write!(out, "{v}")?; + } + VCardValue::Boolean(v) => { + out.write_atomic(if *v { "TRUE" } else { "FALSE" })?; + } + VCardValue::PartialDateTime(v) => { + let typ = if pos == 0 { + types + } else { + self.parameters(&VCardParameterName::Value) + .nth(pos) + .and_then(|v| v.as_value_type()) + .and_then(|v| v.iana().copied()) + } + .unwrap_or(&default_type); + if is_v4 { + v.format_as_vcard(out, typ)?; + } else { + v.format_as_legacy_vcard(out, typ)?; + } + } + VCardValue::Binary(v) => { + if is_v4 { + let media_type = v.content_type.as_deref().unwrap_or_default(); + out.write_str("data:")?; + out.write_str(media_type)?; + out.write_str(";")?; + out.write_atomic("base64\\,")?; + } + write_bytes(out, &v.data)?; + } + VCardValue::Sex(v) => { + out.write_atomic(v.as_str())?; + } + VCardValue::GramGender(v) => { + out.write_atomic(v.as_str())?; + } + VCardValue::Kind(v) => { + out.write_atomic(v.as_str())?; + } + } + } + + out.end_line() + } +} + +impl PartialDateTime { + pub fn format_as_vcard(&self, out: &mut impl Write, fmt: &VCardValueType) -> std::fmt::Result { + if matches!(fmt, VCardValueType::Timestamp) { + write!( + out, + "{:04}{:02}{:02}T{:02}{:02}{:02}", + self.year.unwrap_or_default(), + self.month.unwrap_or_default(), + self.day.unwrap_or_default(), + self.hour.unwrap_or_default(), + self.minute.unwrap_or_default(), + self.second.unwrap_or_default() + )?; + + if let Some(tz_hour) = self.tz_hour { + let tz_minute = self.tz_minute.unwrap_or_default(); + if tz_hour == 0 && tz_minute == 0 { + write!(out, "Z")?; + } else { + write!( + out, + "{}{:02}", + if self.tz_minus { "-" } else { "+" }, + tz_hour, + )?; + + if let Some(tz_minute) = self.tz_minute { + write!(out, "{:02}", tz_minute)?; + } + } + } + Ok(()) + } else { + let missing_time = + self.hour.is_none() && self.minute.is_none() && self.second.is_none(); + let missing_tz = self.tz_hour.is_none(); + + if matches!( + fmt, + VCardValueType::Date | VCardValueType::DateAndOrTime | VCardValueType::DateTime + ) { + match (self.year, self.month, self.day) { + (Some(year), Some(month), Some(day)) => { + write!(out, "{:04}{:02}{:02}", year, month, day)?; + } + (Some(year), Some(month), None) => { + if missing_time && missing_tz { + write!(out, "{:04}-{:02}", year, month)?; + } else { + write!(out, "{:04}{:02}", year, month)?; + } + } + (None, Some(month), Some(day)) => { + write!(out, "--{:02}{:02}", month, day)?; + } + (None, None, Some(day)) => { + write!(out, "---{:02}", day)?; + } + (Some(year), None, None) => { + write!(out, "{:04}", year)?; + } + (None, Some(month), None) => { + write!(out, "--{month}")?; + } + _ => {} + } + } + + if matches!( + fmt, + VCardValueType::DateAndOrTime | VCardValueType::DateTime | VCardValueType::Time + ) && !missing_time + { + if matches!( + fmt, + VCardValueType::DateAndOrTime | VCardValueType::DateTime + ) { + write!(out, "T")?; + } + let mut last_is_some = false; + for value in [&self.hour, &self.minute, &self.second].iter() { + if let Some(value) = value { + write!(out, "{:02}", value)?; + last_is_some = true; + } else if !last_is_some { + write!(out, "-")?; + } + } + } + + if matches!( + fmt, + VCardValueType::DateAndOrTime + | VCardValueType::DateTime + | VCardValueType::Time + | VCardValueType::UtcOffset + ) { + match (self.tz_hour, self.tz_minute) { + (Some(0), Some(0)) | (Some(0), _) => { + write!(out, "Z")?; + } + (Some(hour), Some(minute)) => { + if self.tz_minus { + write!(out, "-")?; + } else { + write!(out, "+")?; + } + write!(out, "{hour:02}{minute:02}")?; + } + (Some(hour), None) => { + if self.tz_minus { + write!(out, "-")?; + } else { + write!(out, "+")?; + } + write!(out, "{hour:02}")?; + } + _ => {} + } + } + + Ok(()) + } + } + + pub fn format_as_legacy_vcard( + &self, + out: &mut impl Write, + fmt: &VCardValueType, + ) -> std::fmt::Result { + if matches!(fmt, VCardValueType::Timestamp) { + write!( + out, + "{:04}{:02}{:02}", + self.year.unwrap_or_default(), + self.month.unwrap_or_default(), + self.day.unwrap_or_default(), + )?; + + if self.hour.is_some() { + write!( + out, + "T{:02}{:02}{:02}", + self.hour.unwrap_or_default(), + self.minute.unwrap_or_default(), + self.second.unwrap_or_default() + )?; + } + + if let Some(tz_hour) = self.tz_hour { + let tz_minute = self.tz_minute.unwrap_or_default(); + if tz_hour == 0 && tz_minute == 0 { + write!(out, "Z")?; + } else { + write!( + out, + "{}{:02}", + if self.tz_minus { "-" } else { "+" }, + tz_hour, + )?; + + if let Some(tz_minute) = self.tz_minute { + write!(out, "{:02}", tz_minute)?; + } + } + } + Ok(()) + } else { + let missing_time = + self.hour.is_none() && self.minute.is_none() && self.second.is_none(); + + if matches!( + fmt, + VCardValueType::Date | VCardValueType::DateAndOrTime | VCardValueType::DateTime + ) { + match (self.year, self.month, self.day) { + (Some(year), Some(month), Some(day)) => { + write!(out, "{:04}-{:02}-{:02}", year, month, day)?; + } + (Some(year), Some(month), None) => { + write!(out, "{:04}-{:02}", year, month)?; + } + (None, Some(month), Some(day)) => { + write!(out, "--{:02}-{:02}", month, day)?; + } + (None, None, Some(day)) => { + write!(out, "---{:02}", day)?; + } + (Some(year), None, None) => { + write!(out, "{:04}", year)?; + } + (None, Some(month), None) => { + write!(out, "--{month}")?; + } + _ => {} + } + } + + if matches!( + fmt, + VCardValueType::DateAndOrTime | VCardValueType::DateTime | VCardValueType::Time + ) && !missing_time + { + if matches!( + fmt, + VCardValueType::DateAndOrTime | VCardValueType::DateTime + ) { + write!(out, "T")?; + } + let mut last_is_some = false; + for value in [&self.hour, &self.minute, &self.second].iter() { + if let Some(value) = value { + if last_is_some { + write!(out, ":")?; + } + write!(out, "{:02}", value)?; + last_is_some = true; + } else if !last_is_some { + write!(out, "-")?; + } + } + } + + if matches!( + fmt, + VCardValueType::DateAndOrTime + | VCardValueType::DateTime + | VCardValueType::Time + | VCardValueType::UtcOffset + ) { + match (self.tz_hour, self.tz_minute) { + (Some(0), Some(0)) | (Some(0), _) => { + write!(out, "Z")?; + } + (Some(hour), Some(minute)) => { + if self.tz_minus { + write!(out, "-")?; + } else { + write!(out, "+")?; + } + write!(out, "{hour:02}:{minute:02}")?; + } + (Some(hour), None) => { + if self.tz_minus { + write!(out, "-")?; + } else { + write!(out, "+")?; + } + write!(out, "{hour:02}")?; + } + _ => {} + } + } + + Ok(()) + } + } +} + +impl Display for VCard { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + self.write_to(f, self.version().unwrap_or_default()) + } +} + +impl Display for VCardVersion { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + VCardVersion::V2_0 => write!(f, "2.0"), + VCardVersion::V2_1 => write!(f, "2.1"), + VCardVersion::V3_0 => write!(f, "3.0"), + VCardVersion::V4_0 => write!(f, "4.0"), + } + } +} + +#[cfg(test)] +mod tests { + use crate::{ + Entry, Parser, + vcard::{VCard, VCardVersion}, + }; + + fn parse(input: &str) -> VCard { + let mut parser = Parser::new(input); + let Entry::VCard(vcard) = parser.entry() else { + panic!("expected vcard for {input}"); + }; + vcard + } + + fn write(vcard: &VCard, version: VCardVersion) -> String { + let mut out = String::new(); + vcard.write_to(&mut out, version).unwrap(); + + #[cfg(feature = "rkyv")] + { + let bytes = rkyv::to_bytes::(vcard).unwrap(); + let archived = + rkyv::access::(&bytes).unwrap(); + let mut archived_out = String::new(); + archived.write_to(&mut archived_out, version).unwrap(); + assert_eq!(out, archived_out, "archived writer diverged at {version}"); + } + + out + } + + #[test] + fn test_write_binary_data_uri() { + let photo = "iVBORw0KGgoAAAANSUhEUgAAAAsAAAALCAQAAAADpb+tAAAAQklEQVQI122PQQ4AMAj\ + CKv//Mzs4M0zmRYKkamEwWQVoRJogk4PuRoOoMC/EK8nYb+l08WGvSxKlNHO5kxnp/\ + WXrAzsSERN1N6q5AAAAAElFTkSuQmCC"; + + for (input, version, expect) in [ + ( + format!("PHOTO;TYPE=JPEG;ENCODING=b:{photo}"), + VCardVersion::V4_0, + format!("PHOTO;TYPE=JPEG:data:image/jpeg;base64\\,{photo}"), + ), + ( + format!("PHOTO;TYPE=JPEG;ENCODING=b:{photo}"), + VCardVersion::V3_0, + format!("PHOTO;TYPE=JPEG;ENCODING=b:{photo}"), + ), + ( + format!("PHOTO;ENCODING=b:{photo}"), + VCardVersion::V4_0, + format!("PHOTO:data:;base64\\,{photo}"), + ), + ( + format!("SOUND;TYPE=WAVE;ENCODING=b:{photo}"), + VCardVersion::V4_0, + format!("SOUND;TYPE=WAVE:data:audio/wav;base64\\,{photo}"), + ), + ( + format!("PHOTO;TYPE=WORK;ENCODING=b:{photo}"), + VCardVersion::V4_0, + format!("PHOTO;TYPE=WORK:data:;base64\\,{photo}"), + ), + ] { + let vcard = parse(&format!( + "BEGIN:VCARD\r\nVERSION:3.0\r\nFN:T\r\n{input}\r\nEND:VCARD\r\n" + )); + let out = write(&vcard, version).replace("\r\n ", ""); + + assert!( + out.contains(expect.as_str()), + "expected {expect} in {out} for {input}" + ); + } + } + + #[test] + fn test_write_binary_data_uri_v4_to_v3() { + let vcard = parse( + "BEGIN:VCARD\r\nVERSION:4.0\r\nFN:T\r\n\ + PHOTO:data:image/png;base64\\,iVBORw0KGgo=\r\nEND:VCARD\r\n", + ); + + assert!( + write(&vcard, VCardVersion::V3_0).contains("PHOTO;ENCODING=b;TYPE=PNG:iVBORw0KGgo="), + "{}", + write(&vcard, VCardVersion::V3_0) + ); + } + + #[test] + fn test_write_fold_width() { + let filler = "A".repeat(73); + let long_param = "B".repeat(64); + let component = "C".repeat(71); + + for input in [ + format!("N:{filler};B;C;D;E"), + format!("N:{filler};;;;"), + format!("ADR:{component};;;;;;"), + format!("N:{component},,,,,;;;;"), + format!("NOTE;X-FOO={long_param}:hello"), + format!("NOTE:{}", "D".repeat(500)), + ] { + let vcard = parse(&format!( + "BEGIN:VCARD\r\nVERSION:4.0\r\nFN:T\r\n{input}\r\nEND:VCARD\r\n" + )); + + for version in [VCardVersion::V3_0, VCardVersion::V4_0] { + let out = write(&vcard, version); + crate::common::writer::assert_fold_width(&out, &input); + + let reparsed = parse(&out); + assert_eq!( + write(&reparsed, version), + out, + "not idempotent for {input} at version {version}" + ); + } + } + } + + #[test] + fn test_write_jscomps() { + let filler = "-".repeat(55); + let folded_separator = format!("{}\r\n {}", "-".repeat(60), "=".repeat(10)); + + for input in [ + format!("N;JSCOMPS=\"s,{filler};1;2;3;0\":a;b;c;d;e"), + format!("N;JSCOMPS=\"s,{folded_separator};1;2\":a;b;c;d;e"), + format!("N;JSCOMPS=\"s,{};12;34,5\":a;b;c;d;e", "-".repeat(54)), + "N;JSCOMPS=\"s,==========;1;2\":a;b;c;d;e".to_string(), + "N;JSCOMPS=\"s,\\\\;1;2\":a;b;c;d;e".to_string(), + ] { + let vcard = parse(&format!( + "BEGIN:VCARD\r\nVERSION:4.0\r\nFN:T\r\n{input}\r\nEND:VCARD\r\n" + )); + let out = write(&vcard, VCardVersion::V4_0); + crate::common::writer::assert_fold_width(&out, &input); + + assert_eq!( + parse(&out).entries, + vcard.entries, + "JSCOMPS did not survive a round trip for {input}\n{out}" + ); + } + } +} -- 2.54.0 From 0e532cddcac5b7dde2b119e23a78ded8924116e9 Mon Sep 17 00:00:00 2001 From: John Coffey Date: Thu, 29 Oct 2026 01:42:13 -0700 Subject: [PATCH 05/10] Calendars: give each recurrence expansion one work budget A calendar's recurrence rules were each allowed 100,000 passes that find nothing before calcard gave up on them, counted afresh for every rule and after every instance found, and nothing bounded the total. Anyone who can put an object on a calendar (a CalDAV or JMAP upload, or an iMIP invitation the server processes) could send a few thousand components whose rules never match, such as FREQ=MONTHLY;BYMONTH=2;BYMONTHDAY=31, or rules that match rarely, and storing or processing that object cost tens of seconds of CPU. The per-period candidate check doesn't catch these: each period is small, there are just very many of them. Expansion now has one budget of 1,000,000 work units for all the rules of the object together (about 50 ms in a release build). Every pass over a period costs a unit, a pass that finds nothing a unit per 16 days it scanned, candidates looked at but not produced a unit each, and so does each time skipped while looking for the next BYHOUR/BYMINUTE/BYSECOND match. When it runs out, expansion stops the way it does at the instance limit: the object is kept and the instances found so far are indexed. The expansion also reports it, so the server logs a calendar rule expansion error naming it. Rules are no longer started once the instance limit is reached. Ordinary rules use a small part of the budget: 3,000 daily, weekly, monthly, yearly or every-leap-day instances expand as before. Test: in calcard's new budget tests, 40 rules that never match took 3,999,960 iterations on the vendored copy and a sparse daily rule 2,922,306 (both over the 1,000,000 asserted); both now stop within the budget and report it. 4,000 never-matching components (412 KB) took 13.7 s (daily), 31.7 s (yearly) and 41.8 s (monthly) in a release build, now 36 ms, 45 ms and 57 ms. --- Cargo.toml | 6 +- crates/groupware/src/calendar/dates.rs | 31 +++++ vendor/calcard/VENDORED.md | 19 +++ vendor/calcard/src/datecalc/counter_date.rs | 42 +++++-- vendor/calcard/src/datecalc/mod.rs | 11 ++ vendor/calcard/src/datecalc/rrule_iter.rs | 56 ++++++++- vendor/calcard/src/icalendar/dates.rs | 133 +++++++++++++++++++- 7 files changed, 285 insertions(+), 13 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 5102a1a6..29ffb291 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -86,9 +86,9 @@ rpath = false # vendor/sieve-rs is the published 0.7.3 with those renamed; see its # VENDORED.md. Re-vendor when the version in Cargo.lock moves. # -# inbuxa: vendor/calcard is the published 0.3.14, vendored so the server can -# carry fixes to it; see its VENDORED.md. Re-vendor when the version in -# Cargo.lock moves. +# inbuxa: vendor/calcard is the published 0.3.14 with one work budget shared +# by all the recurrence rules an expansion runs; see its VENDORED.md. +# Re-vendor when the version in Cargo.lock moves. # # inbuxa: hickory and opentelemetry are upstream's patched branches, vendored # at the commits upstream pins so builds never fetch from upstream's diff --git a/crates/groupware/src/calendar/dates.rs b/crates/groupware/src/calendar/dates.rs index be30d03c..175ed008 100644 --- a/crates/groupware/src/calendar/dates.rs +++ b/crates/groupware/src/calendar/dates.rs @@ -355,3 +355,34 @@ impl ArchivedTimezone { } } } + +#[cfg(test)] +mod tests { + use calcard::{ + common::timezone::Tz, + icalendar::{ICalendar, dates::CalendarErrorType}, + }; + + // The shared expansion budget is carried by vendor/calcard, so this stops + // building if Cargo.lock moves calcard past the vendored copy + #[test] + fn rule_expansion_shares_one_budget() { + let mut ical = "BEGIN:VCALENDAR\r\nVERSION:2.0\r\n".to_string(); + for _ in 0..40 { + ical.push_str( + "BEGIN:VEVENT\r\nUID:budget\r\nDTSTART:20000101T000000Z\r\n\ + RRULE:FREQ=DAILY;BYMONTH=2;BYMONTHDAY=31\r\nEND:VEVENT\r\n", + ); + } + ical.push_str("END:VCALENDAR\r\n"); + + let expanded = ICalendar::parse(&ical).unwrap().expand_dates(Tz::UTC, 3000); + assert!(expanded.events.is_empty()); + assert!( + expanded + .errors + .iter() + .any(|err| err.error == CalendarErrorType::ExpansionBudgetExhausted) + ); + } +} diff --git a/vendor/calcard/VENDORED.md b/vendor/calcard/VENDORED.md index b2c80282..b21b3567 100644 --- a/vendor/calcard/VENDORED.md +++ b/vendor/calcard/VENDORED.md @@ -25,6 +25,25 @@ Every changed file carries a Coffey Labs notice in its header. - `.gitignore`: `VENDORED.md` is tracked (upstream ignores every `*.md` but the changelog and README). +- `src/datecalc/mod.rs`, `src/datecalc/rrule_iter.rs`, + `src/datecalc/counter_date.rs`, `src/icalendar/dates.rs`: one + `expand_dates` call has a single work budget for all its recurrence rules + (`MAX_EXPANSION_WORK`, 1,000,000 units, about 50 ms in a release build). + Upstream only stops a rule after 100,000 passes that find nothing, counted + afresh for every instance and every rule, so a calendar of a few thousand + rules that never match (`FREQ=MONTHLY;BYMONTH=2;BYMONTHDAY=31`), or one + sparse rule, took seconds to minutes. A pass over a period costs one unit, + a pass that finds nothing one per 16 days it scanned, each candidate a + pass looks at without producing it one, and each time skipped while + looking for the next `BYHOUR`/`BYMINUTE`/`BYSECOND` match one + (`DateTimeIter::increment_counted` counts those). When the budget runs + out, expansion stops: the instances found so far are kept, as when the + instance limit is reached, later rules aren't expanded, and the result + carries a `CalendarErrorType::ExpansionBudgetExhausted` error for the + component it stopped at. Rules are no longer started once the instance + limit is reached (they couldn't add anything). The `budget_tests` in + `dates.rs` cover it, and `rule_expansion_shares_one_budget` in + `crates/groupware` fails to build if the server stops using this copy. ## Updating diff --git a/vendor/calcard/src/datecalc/counter_date.rs b/vendor/calcard/src/datecalc/counter_date.rs index 9c28c2d8..44289635 100644 --- a/vendor/calcard/src/datecalc/counter_date.rs +++ b/vendor/calcard/src/datecalc/counter_date.rs @@ -2,6 +2,8 @@ * SPDX-FileCopyrightText: 2021 Fredrik Meringdal, Ralph Bisschops * * SPDX-License-Identifier: Apache-2.0 OR MIT + * + * Modified by Coffey Labs in 2026 for INBUXA. */ use super::{ @@ -37,7 +39,20 @@ impl DateTimeIter { /// is higher than daily (e.g. hourly) where this function might return a date with the /// same day, but the iterator already knows that the current day can't /// be part of the result. + #[cfg(test)] pub fn increment(&mut self, rrule: &RRule, increment_day: bool) -> Result<(), RRuleError> { + self.increment_counted(rrule, increment_day, &mut 0) + } + + /// inbuxa: `increment`, adding to `steps` each pass of the search loops + /// that skip the times `BYHOUR`, `BYMINUTE` and `BYSECOND` exclude, so the + /// caller can charge them against its expansion budget. + pub fn increment_counted( + &mut self, + rrule: &RRule, + increment_day: bool, + steps: &mut u32, + ) -> Result<(), RRuleError> { let RRule { interval, week_start, @@ -51,13 +66,20 @@ impl DateTimeIter { ICalendarFrequency::Monthly => self.increment_monthly(*interval), ICalendarFrequency::Weekly => self.increment_weekly(*interval, *week_start), ICalendarFrequency::Daily => self.increment_daily(*interval), - ICalendarFrequency::Hourly => self.increment_hourly(*interval, by_hour, increment_day), + ICalendarFrequency::Hourly => { + self.increment_hourly(*interval, by_hour, increment_day, steps) + } ICalendarFrequency::Minutely => { - self.increment_minutely(*interval, by_hour, by_minute, increment_day) - } - ICalendarFrequency::Secondly => { - self.increment_secondly(*interval, by_hour, by_minute, by_second, increment_day) + self.increment_minutely(*interval, by_hour, by_minute, increment_day, steps) } + ICalendarFrequency::Secondly => self.increment_secondly( + *interval, + by_hour, + by_minute, + by_second, + increment_day, + steps, + ), } } @@ -166,6 +188,7 @@ impl DateTimeIter { interval: u16, by_hour: &[u8], increment_day: bool, + steps: &mut u32, ) -> Result<(), RRuleError> { if increment_day { // Jump to one iteration before the next day @@ -179,6 +202,7 @@ impl DateTimeIter { let mut prev_hours = HashSet::new(); loop { + *steps = steps.saturating_add(1); self.hour = checked_add_u32( self.hour, u32::from(interval), @@ -213,6 +237,7 @@ impl DateTimeIter { by_hour: &[u8], by_minute: &[u8], increment_day: bool, + steps: &mut u32, ) -> Result<(), RRuleError> { if increment_day { // Jump to one iteration before the next day @@ -227,13 +252,14 @@ impl DateTimeIter { let mut prev_values = HashSet::new(); loop { + *steps = steps.saturating_add(1); self.minute += u32::from(interval); let hours_div = u16::try_from(self.minute / 60).map_err(|_| { RRuleError::new_iter_err("unexpected high minute, please decrease interval") })?; if hours_div > 0 { self.minute %= 60; - self.increment_hourly(hours_div, by_hour, increment_day)?; + self.increment_hourly(hours_div, by_hour, increment_day, steps)?; } let hours = (self.hour % 24) as u8; @@ -263,6 +289,7 @@ impl DateTimeIter { by_minute: &[u8], by_second: &[u8], increment_day: bool, + steps: &mut u32, ) -> Result<(), RRuleError> { if increment_day { // Jump to one iteration before the next day @@ -277,13 +304,14 @@ impl DateTimeIter { let mut prev_values = HashSet::new(); loop { + *steps = steps.saturating_add(1); self.second += u32::from(interval); let minutes_div = u16::try_from(self.second / 60).map_err(|_| { RRuleError::new_iter_err("unexpected high second, please decrease interval") })?; if minutes_div > 0 { self.second %= 60; - self.increment_minutely(minutes_div, by_hour, by_minute, increment_day)?; + self.increment_minutely(minutes_div, by_hour, by_minute, increment_day, steps)?; } let hours = (self.hour % 24) as u8; diff --git a/vendor/calcard/src/datecalc/mod.rs b/vendor/calcard/src/datecalc/mod.rs index c8308f55..59f4451d 100644 --- a/vendor/calcard/src/datecalc/mod.rs +++ b/vendor/calcard/src/datecalc/mod.rs @@ -2,6 +2,8 @@ * SPDX-FileCopyrightText: 2021 Fredrik Meringdal, Ralph Bisschops * * SPDX-License-Identifier: Apache-2.0 OR MIT + * + * Modified by Coffey Labs in 2026 for INBUXA. */ mod checks; @@ -30,6 +32,15 @@ pub(crate) use rrule_iter::RRuleIter; /// we can assume it will not find an event. static MAX_ITER_LOOP: u32 = 100_000; +/// inbuxa: the work one `expand_dates` call may spend on all its recurrence +/// rules together. Every pass over a period costs one unit, a pass that finds +/// nothing costs one per 16 days it scanned, candidates a pass looks at +/// without producing them cost one each, and so does every time the search +/// for the next `BYHOUR`/`BYMINUTE`/`BYSECOND` match skips one. Rules that +/// rarely or never match, spread over many components, can't add up past this +/// (about 50 ms of work in a release build). +pub(crate) const MAX_EXPANSION_WORK: u32 = 1_000_000; + #[inline(always)] pub(crate) fn duration_from_midnight(time: NaiveTime) -> Duration { Duration::hours(i64::from(time.hour())) diff --git a/vendor/calcard/src/datecalc/rrule_iter.rs b/vendor/calcard/src/datecalc/rrule_iter.rs index fbf9acc9..fef97c31 100644 --- a/vendor/calcard/src/datecalc/rrule_iter.rs +++ b/vendor/calcard/src/datecalc/rrule_iter.rs @@ -2,6 +2,8 @@ * SPDX-FileCopyrightText: 2021 Fredrik Meringdal, Ralph Bisschops * * SPDX-License-Identifier: Apache-2.0 OR MIT + * + * Modified by Coffey Labs in 2026 for INBUXA. */ use super::counter_date::DateTimeIter; @@ -14,6 +16,12 @@ use crate::icalendar::ICalendarFrequency; use chrono::NaiveTime; use std::collections::VecDeque; +// inbuxa: passes plus skipped times, counted for the expansion budget tests +#[cfg(test)] +thread_local! { + pub(crate) static ITERATIONS: std::cell::Cell = const { std::cell::Cell::new(0) }; +} + #[derive(Debug, Clone)] pub(crate) struct RRuleIter { pub(crate) counter_date: DateTimeIter, @@ -25,6 +33,9 @@ pub(crate) struct RRuleIter { pub(crate) count: Option, pub(crate) limited: bool, pub(crate) was_limited: bool, + // inbuxa: work left from the expansion budget, see `MAX_EXPANSION_WORK` + pub(crate) budget: u32, + pub(crate) budget_exhausted: bool, } impl RRuleIter { @@ -47,9 +58,18 @@ impl RRuleIter { count, limited, was_limited: false, + budget: u32::MAX, + budget_exhausted: false, } } + /// inbuxa: limits the work this iterator may spend to `budget` units; once + /// it runs out, the iterator ends and `budget_exhausted` is set. + pub(crate) fn with_budget(mut self, budget: u32) -> Self { + self.budget = budget; + self + } + /// Attempts to add a date to the result. Returns `true` if we should /// terminate the iteration. fn try_add_datetime( @@ -164,8 +184,42 @@ impl RRuleIter { } } + // inbuxa: what this pass cost; the buffer was empty when it began + let produced = self.buffer.len(); + let candidates = dayset.len().saturating_mul(self.timeset.len()); + let mut work = if produced == 0 { + let days = match rrule.freq { + ICalendarFrequency::Yearly => usize::from(self.ii.year_len()), + ICalendarFrequency::Monthly => 31, + ICalendarFrequency::Weekly => 7, + _ => 1, + }; + // one unit per 16 days scanned keeps a unit near the cost of a + // daily pass + days.div_ceil(16).saturating_add(candidates) + } else { + 1 + candidates.saturating_sub(produced) + }; + let increment_day = dayset.is_empty(); - if self.counter_date.increment(rrule, increment_day).is_err() { + let mut steps = 0; + let incremented = self + .counter_date + .increment_counted(rrule, increment_day, &mut steps); + work = work.saturating_add(steps as usize); + #[cfg(test)] + ITERATIONS.with(|count| count.set(count.get() + 1 + u64::from(steps))); + let Some(budget) = self + .budget + .checked_sub(u32::try_from(work).unwrap_or(u32::MAX)) + else { + self.budget = 0; + self.budget_exhausted = true; + self.finished = true; + return true; + }; + self.budget = budget; + if incremented.is_err() { self.finished = true; return true; } diff --git a/vendor/calcard/src/icalendar/dates.rs b/vendor/calcard/src/icalendar/dates.rs index 8ea8fd4b..6180369c 100644 --- a/vendor/calcard/src/icalendar/dates.rs +++ b/vendor/calcard/src/icalendar/dates.rs @@ -2,6 +2,8 @@ * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC * * SPDX-License-Identifier: Apache-2.0 OR MIT + * + * Modified by Coffey Labs in 2026 for INBUXA. */ use super::{ @@ -10,7 +12,7 @@ use super::{ }; use crate::{ common::{DateTimeResult, timezone::Tz}, - datecalc::{RRuleIter, error::RRuleError, rrule::RRule}, + datecalc::{MAX_EXPANSION_WORK, RRuleIter, error::RRuleError, rrule::RRule}, icalendar::ICalendarParameterName, }; use ahash::{AHashMap, AHashSet}; @@ -59,6 +61,9 @@ pub enum CalendarErrorType { InvalidDtEnd, InvalidDuration, RRule(RRuleError), + // inbuxa: expansion stopped at this component's rule because all rules + // together used up `MAX_EXPANSION_WORK`; later rules weren't expanded + ExpansionBudgetExhausted, } impl ICalendar { @@ -92,6 +97,7 @@ impl ICalendar { } // Expand recurrences + let mut budget = MAX_EXPANSION_WORK; for (mut comp_id, event) in rrules { let rrule = event.rrule.unwrap(); let floating_start = if let Some(floating_start) = Tz::Floating @@ -127,7 +133,14 @@ impl ICalendar { .collect::>(); let mut override_offset = None; - for date in RRuleIter::new(&rrule, &floating_start, true) { + // inbuxa: no instance can be added once the limit is reached + if limit == 0 { + continue; + } + let rule_comp_id = comp_id; + let mut dates = RRuleIter::new(&rrule, &floating_start, true).with_budget(budget); + + for date in dates.by_ref() { if limit != 0 { limit -= 1; } else { @@ -165,6 +178,15 @@ impl ICalendar { _ => {} } } + + budget = dates.budget; + if dates.budget_exhausted { + expand.errors.push(CalendarError { + comp_id: rule_comp_id, + error: CalendarErrorType::ExpansionBudgetExhausted, + }); + break; + } } // Add missing overridden events (this should not occur unless the iCalendar is malformed) @@ -547,6 +569,9 @@ impl Display for CalendarErrorType { CalendarErrorType::InvalidDtEnd => write!(f, "Invalid DTEND property"), CalendarErrorType::InvalidDuration => write!(f, "Invalid DURATION property"), CalendarErrorType::RRule(err) => write!(f, "RRule error: {err}"), + CalendarErrorType::ExpansionBudgetExhausted => { + write!(f, "Recurrence expansion stopped: work limit reached") + } } } } @@ -642,3 +667,107 @@ mod tests { } } } + +// inbuxa: one budget bounds the work of all the rules in a calendar +#[cfg(test)] +mod budget_tests { + use crate::{ + datecalc::{MAX_EXPANSION_WORK, rrule_iter::ITERATIONS}, + icalendar::{ + ICalendar, + dates::{CalendarErrorType, CalendarExpand}, + }, + }; + + fn calendar(rule: &str, components: usize) -> ICalendar { + let mut ical = "BEGIN:VCALENDAR\r\nVERSION:2.0\r\n".to_string(); + for _ in 0..components { + ical.push_str(&format!( + "BEGIN:VEVENT\r\nUID:budget\r\nDTSTART:20000101T000000Z\r\n\ + DTEND:20000101T010000Z\r\nRRULE:{rule}\r\nEND:VEVENT\r\n" + )); + } + ical.push_str("END:VCALENDAR\r\n"); + ICalendar::parse(&ical).unwrap() + } + + // Expands and returns the result with the passes and skipped times it took + fn expand(rule: &str, components: usize, limit: usize) -> (CalendarExpand, u64) { + let ical = calendar(rule, components); + ITERATIONS.with(|count| count.set(0)); + let expanded = ical.expand_dates(chrono_tz::Tz::UTC, limit); + (expanded, ITERATIONS.with(|count| count.get())) + } + + // The budget, and the pass that runs over it + fn within_budget() -> u64 { + u64::from(MAX_EXPANSION_WORK) + 10_000 + } + + fn stopped(expanded: &CalendarExpand) -> bool { + expanded + .errors + .iter() + .any(|err| err.error == CalendarErrorType::ExpansionBudgetExhausted) + } + + #[test] + fn never_matching_rules_share_one_budget() { + // Each rule alone spends calcard's 100,000 passes looking for a 31 February + let (expanded, iterations) = expand("FREQ=DAILY;BYMONTH=2;BYMONTHDAY=31", 40, 3000); + println!("40 never-matching rules: {iterations} iterations"); + assert!(iterations <= within_budget(), "{iterations}"); + assert!(expanded.events.is_empty()); + assert!(stopped(&expanded)); + } + + #[test] + fn sparse_rules_share_one_budget() { + // One instance every four years, each found after ~1,460 daily passes, + // and one a year found by skipping every other minute of the year + for rule in [ + "FREQ=DAILY;BYMONTH=2;BYMONTHDAY=29", + "FREQ=MINUTELY;BYMONTH=1;BYMONTHDAY=1;BYHOUR=1;BYMINUTE=1", + ] { + let (expanded, iterations) = expand(rule, 1, 3000); + println!( + "{rule}: {} instances, {iterations} iterations", + expanded.events.len() + ); + assert!(iterations <= within_budget(), "{rule}: {iterations}"); + assert!(!expanded.events.is_empty(), "{rule}"); + assert!(stopped(&expanded), "{rule}"); + } + + // A year scanned for nothing costs more than a day scanned for nothing + let (expanded, iterations) = expand("FREQ=YEARLY;BYMONTH=2;BYMONTHDAY=31", 20, 3000); + println!("20 never-matching yearly rules: {iterations} passes"); + assert!(iterations < 100_000, "{iterations}"); + assert!(stopped(&expanded)); + } + + #[test] + fn ordinary_rules_expand_in_full() { + for (rule, instances) in [ + ("FREQ=DAILY", 3000), + ("FREQ=WEEKLY;BYDAY=MO,WE,FR", 3000), + ("FREQ=MONTHLY;BYDAY=-1FR", 3000), + ("FREQ=MONTHLY;BYDAY=MO,TU,WE,TH,FR;BYSETPOS=-1", 3000), + ("FREQ=YEARLY", 3000), + // every leap day to the year 10000 + ("FREQ=YEARLY;BYMONTH=2;BYMONTHDAY=29", 1941), + ("FREQ=HOURLY;BYHOUR=9,17", 3000), + ("FREQ=MINUTELY;INTERVAL=15;BYHOUR=9,10,11", 3000), + ] { + let (expanded, _) = expand(rule, 1, 3000); + assert_eq!(expanded.events.len(), instances, "{rule}"); + assert!(expanded.errors.is_empty(), "{rule}: {:?}", expanded.errors); + } + + // Rules after the instance limit is reached cost nothing + let (expanded, iterations) = expand("FREQ=DAILY", 200, 3000); + assert_eq!(expanded.events.len(), 3000); + assert!(expanded.errors.is_empty()); + assert!(iterations < 4000, "{iterations}"); + } +} -- 2.54.0 From d6f6fcf1efbe206887adc5e720e03f65f4cba65d Mon Sep 17 00:00:00 2001 From: John Coffey Date: Thu, 29 Oct 2026 02:00:41 -0700 Subject: [PATCH 06/10] OIDC directories: a new one must name the audience its tokens are for An external OpenID Connect directory with no required audience accepted any token its provider issued, to any application: an access token minted for another app at the same provider, or an unmarked ID token (Google, Entra), signed in as that user. Anyone holding such a token, which includes every other application the user signs in to there, could use it as a bearer token here. Creating an OIDC directory without requireAudience is now refused with a validation error that says what to set, whether it is created through x:Directory/set, by changing another directory into an OIDC one, or at first-run setup. An audience, once set, can't be removed. A directory saved before this keeps working without one and stays editable, and each time it loads (startup, reload, every node) the server logs an auth warning naming its issuer and description. x:Directory/get shows requireAudience as null for such a directory, which is what the admin console reads to warn about it. A later release will require the audience on every directory. Test: on main x:Directory/set created an OIDC directory without an audience (directory suite: "Missing not created item 0"). Now it is refused, a directory stored without one still signs John in and can be edited, and removing the audience from one that has it is refused (directory suite and a unit test of the rules). --- crates/directory/src/backend/oidc/config.rs | 14 +++ crates/jmap/src/inbuxa/mod.rs | 1 + crates/jmap/src/inbuxa/oidc_directory.rs | 75 ++++++++++++++++ crates/jmap/src/registry/mapping/bootstrap.rs | 9 ++ crates/jmap/src/registry/set.rs | 31 +++++-- resources/schema/schema.json.gz | Bin 153616 -> 153685 bytes resources/schema/schema.json.sha256 | 2 +- tests/src/directory/oidc.rs | 80 +++++++++++++++++- 8 files changed, 203 insertions(+), 9 deletions(-) create mode 100644 crates/jmap/src/inbuxa/oidc_directory.rs diff --git a/crates/directory/src/backend/oidc/config.rs b/crates/directory/src/backend/oidc/config.rs index 0084a9c8..ae2a876a 100644 --- a/crates/directory/src/backend/oidc/config.rs +++ b/crates/directory/src/backend/oidc/config.rs @@ -24,6 +24,20 @@ const DISCOVERY_RETRY_INTERVAL: Duration = Duration::from_secs(3); impl OpenIdDirectory { pub async fn open(config: structs::OidcDirectory) -> Result { + // inbuxa: http-directory-services-13: only a directory saved before + // the audience was required has none; it keeps working, with a + // warning every time it loads + if config.require_audience.is_none() { + trc::event!( + Auth(AuthEvent::Warning), + Url = config.issuer_url.clone(), + Details = config.description.clone(), + Reason = "No required audience is set, so tokens this provider issued to any \ + other application sign in. Set requireAudience to the client ID \ + registered for this server; a later release will require it." + ); + } + Self::new(OidcConfig { issue_url: config.issuer_url, require_aud: config.require_audience, diff --git a/crates/jmap/src/inbuxa/mod.rs b/crates/jmap/src/inbuxa/mod.rs index 0eb21160..3e6ff895 100644 --- a/crates/jmap/src/inbuxa/mod.rs +++ b/crates/jmap/src/inbuxa/mod.rs @@ -30,6 +30,7 @@ pub mod log_settings; pub mod mail_rule; pub mod masked_email; pub mod masked_secrets; // inbuxa: "****" is never saved as a secret +pub mod oidc_directory; // inbuxa: http-directory-services-13 pub mod protocol_policy; pub mod scheduled_reports; // inbuxa: scheduled reports and the weekly digest pub mod security_acceptance; diff --git a/crates/jmap/src/inbuxa/oidc_directory.rs b/crates/jmap/src/inbuxa/oidc_directory.rs new file mode 100644 index 00000000..40f1f50d --- /dev/null +++ b/crates/jmap/src/inbuxa/oidc_directory.rs @@ -0,0 +1,75 @@ +/* + * SPDX-FileCopyrightText: 2026 Coffey Labs LLC + * + * SPDX-License-Identifier: AGPL-3.0-only + */ + +//! What an external OpenID Connect directory must name before it is saved +//! (http-directory-services-13). +//! +//! A directory with no required audience accepts any token its provider +//! issued, to any application, so a new one must name the audience: one +//! made by `x:Directory/set`, made by changing another directory's type, or +//! made at setup. A directory saved before this rule keeps working without +//! one and logs a warning each time it loads; it can still be edited, but an +//! audience it has can't be removed. Requiring it of every directory is left +//! to a later release. + +use jmap_proto::error::set::{SetError, SetErrorType}; +use registry::{ + schema::{prelude::Property, structs::Directory}, + types::error::ValidationError, +}; + +/// `stored` is the directory as saved before this write, None for a new one. +pub fn validate( + directory: &Directory, + stored: Option<&Directory>, +) -> Result<(), SetError> { + let Directory::Oidc(directory) = directory else { + return Ok(()); + }; + let may_omit_audience = matches!( + stored, + Some(Directory::Oidc(stored)) if stored.require_audience.is_none() + ); + if directory.require_audience.is_none() && !may_omit_audience { + return Err(SetError::new(SetErrorType::ValidationFailed) + .with_validation_errors(vec![ValidationError::required(Property::RequireAudience)]) + .with_description( + "Set the required audience: the client ID or resource identifier registered \ + for this server at the provider. Without it, tokens the provider issued to any \ + other application would sign in.", + )); + } + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + use registry::schema::structs::{LdapDirectory, OidcDirectory}; + + fn oidc(audience: Option<&str>) -> Directory { + Directory::Oidc(OidcDirectory { + require_audience: audience.map(str::to_string), + ..Default::default() + }) + } + + #[test] + fn new_directories_name_their_audience() { + let ldap = Directory::Ldap(LdapDirectory::default()); + + // New, or made from a directory of another type + assert!(validate(&oidc(None), None).is_err()); + assert!(validate(&oidc(None), Some(&ldap)).is_err()); + assert!(validate(&oidc(Some("inbuxa")), None).is_ok()); + assert!(validate(&ldap, None).is_ok()); + + // Saved before the audience was required: still editable without one + assert!(validate(&oidc(None), Some(&oidc(None))).is_ok()); + // ... but an audience, once set, stays + assert!(validate(&oidc(None), Some(&oidc(Some("inbuxa")))).is_err()); + } +} diff --git a/crates/jmap/src/registry/mapping/bootstrap.rs b/crates/jmap/src/registry/mapping/bootstrap.rs index c47c7e0a..452e4ada 100644 --- a/crates/jmap/src/registry/mapping/bootstrap.rs +++ b/crates/jmap/src/registry/mapping/bootstrap.rs @@ -125,6 +125,15 @@ pub(crate) async fn bootstrap_set( break; } + // inbuxa: http-directory-services-13: a directory made at setup is a + // new one, and names its audience + if let Some(directory) = map_directory(&bootstrap.directory) + && let Err(err) = crate::inbuxa::oidc_directory::validate(&directory, None) + { + set.response.not_updated.append(id, err); + break; + } + // Validate domain name and hostname let server_hostname = bootstrap .server_hostname diff --git a/crates/jmap/src/registry/set.rs b/crates/jmap/src/registry/set.rs index 5fbd3d5c..859cacf7 100644 --- a/crates/jmap/src/registry/set.rs +++ b/crates/jmap/src/registry/set.rs @@ -564,13 +564,30 @@ impl RegistrySet for Server { Ok(ObjectResponse::default()) } } - ObjectInner::Directory(_) if is_create => { - validate_tenant_quota( - self, - access_token, - TenantStorageQuota::MaxDirectories, - ) - .await? + ObjectInner::Directory(directory) => { + // inbuxa: http-directory-services-13: a new OIDC + // directory names its audience + let stored = match &modification { + Modification::Update { object, .. } => match &object.inner { + ObjectInner::Directory(stored) => Some(stored), + _ => None, + }, + Modification::Create { .. } => None, + }; + if let Err(err) = + crate::inbuxa::oidc_directory::validate(directory, stored) + { + Err(err) + } else if is_create { + validate_tenant_quota( + self, + access_token, + TenantStorageQuota::MaxDirectories, + ) + .await? + } else { + Ok(ObjectResponse::default()) + } } // inbuxa: legacy-protocols LP-4 ObjectInner::NetworkListener(listener) => { diff --git a/resources/schema/schema.json.gz b/resources/schema/schema.json.gz index 9f29577f278d026d4081b488615b355692473c41..3d7c4e13b656a44903fe68bce588a3932afb2362 100644 GIT binary patch delta 119969 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⋘>Ff4-*0#-|T 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⪻LX8R z4Dz?uD)Nn`R)D$2f;A95Bu@DUzsOGKC?~l@jR9Cw)~(K$%D<3{M?xK_u3q>hk|P7H z;9G)Bkt|Yy`Ym|Nyf;7jdNGxpOY#4xZnof7BXDgcZ-3(735}Q<=?nQW7-@Qvov&4> z4*G(h-$bQu^wXcqkv_mzYr#gCG+%;~=#qbpP9iC-2sLFjof7P%OY(1pE{S+qr^?uI zg$P=Z{W2_d8^mf}S){EFt=J9zsT5xr6{?V#>4#7ar6~V_BDFQ$gemh@udrLjnZcmA zsOVi26@RRBZZ?E3Qs?6>{}5?))coIaItRS~xLmLTq-3}R_$_#@!4Bzp7%*&b{#_~h zA+IGF{3#xG7>nvvKK-S}G#vHl)k!(q{pq4Ce{Dvm;8z8b zLHjyv^Xzs=n`iJCgHvrllWn_yZkyw>oZIGN0`CSj1|Heq&}AqDf6YyqWBR`M=do&Y zQ$Lm%Qt7n^2O2z@@YA%^wI4Rc&6!{Qsekz2X5@;YF(UPDQ(~bj8U>* z(29heZi?eG-RUGl^v6|=9z0lLbhbpvFK{u0w%)uS*4CSHzKIyw6sP|jRvlChh<`>L zRzG_;toqr*KEP0h%6bg*m6Kz5{=>k^oATGN?)TTj>V6N(mogf%4?hm8{b7e-H(~9I zH^XXQR1t#HO zKMd^p2T<#7!ph@RWpB%tyID=V$A9Z)d^)V(@h&m-u%r%y4!&%CLW5P1|CB{VWMYUxeYdNiy3_I+#I%jAxW-2cw10w*803poV)vJ zaJlT^u@9FTeCWAbz81&op!*7kS^AuVFO#zV1!#GNh=i-xVMFJ)TC&HB(0^8{b7zp| zWKtDxeo}@XfRqJwnjQi9HLQ72Zdcjr7f5q4o^I;(w!f`q@y|=u&T>-vwfi>1aoepV2D}00a-jdC=5D@y5T0-s~0+Ww@NokGkf3q8IWT zaI&pm&8pAn=m^DM4_wiJntu^C)cgWqbM-UoU)>C?e^sr6bjy>v`1H}QUbhQ4^^>(f zVHSq1-XIfLQK!mu31WAd5hU;vWG9%1E$9Krw~%1JDNg*#cl896oU3H*o5R~f4B9_| zD8;Ci^P#OIXxJ2Q{jL25R4z@r^4kq!yB%sHO7XY9{73Mh58f@bH-AUR%PJC^Cz~(9 z=ryxuDBW%GM(v}fUDX+``5W0j-dCHi)kE_X4dQ;KUnp-N+NJA-a;UZL#qp_9xx;%~ zZ^Yx(tik6;SWjIDiF)yU08hogeD~+>x8m6j0~6!EFby8W+0~_f<{%3-gVBc|#K$FmqO2PZKnQ1aFe*a#h^@s5GZ&#f1^W9UDBZ zIB^q8H|b41-OP)R{`x-IfSMeJ<_-lzc+i?Zcbi9;`}BeR@qdvr5qlPQJp3 zg|zXHN%?EqPKuigUmp%AuDeNa?SR^~46?e+?hH4~43s1v6ryT)M4eVE1ZCH?s^a)gdDmcqPPL_Xs530XT5rxeYk^4e zQ16!Emj3gf|9{f2{sFW2Vixvy^1idbG3;0n)S~%-ny1~A@-O?a)_H1FQfD^q+_3V+ zd1sdwH7q=^jDU3{oJ6SmvAgc>3kY(6bQ=o%dT8T!Q)XnFT3=45fG#vtu$jRwQKNf- zK5g(o+JrkLImn-j%TIpKb8tMJ*Km_c-xr>c2;fZcRev8wv#-S)f0jRorAS9Pq$ z!)pKRsRqI7|E$UsmpAVR(tCqXtMwX1fUu46N&ksIuJ$0%fFng*d+5d)gS$$(Mws!S zg@5*}-mJPC>t1%(O+2BoR?j~BXALj$$x}e7Z-2HqYY{pD@k;_$!)M9yZTG9>7?3H^ zjiL-IzPawM_y#P>pX;dF$whaylX)EzdD7DtxX?85I6}SKzItG#9^FRs1~otS)Z9&? zI;TBUf;$Kx%VYYLq{Ij%)jPmP7$%S{tJ*Xbt^T#$mG@5j zgy4vk-gj2Yz4Yz_6<=I4q1l5<>b(_LO24;w*=;+Q9b|q1KMziOlv;PYjecal-EC4@ zAzf*&A6(-SA$;F%YFe`RXt)BWr@8&*qWcL3&0Sq6_Umx%T|AUpd_La{ z5T0e|w8TDS^g87DhnapijHmFF8GpS42|Q!<%BxWxs;^o_`mXw>`Z`!g+HS;zqwcSY zAH;0GX=D3Mq`CbZiflyI-7vCJ-Vlba%d=d=CqKPQQc4=Ct3y_mwG# zY<0%CmZQRz(Nyg8bL!phmlEA)BmKeKU=1J>bCMbEsxFW!Ey?x0`l5x4M) z<}Iz7#bUalRi&vonL`%lZQX)BZa$crP4Ql-;WrJCpx2-Lrt5|3s*mgSaCOxjjx2uz ze%Mrd#60puXH0*j>+Fj6{(trI9&(ID#!LP&=xkpcYyS(d(&r36@=-NV2d0X1MB_$i zvt8M9CDt=^ZuV==&}}LdiOjFM?t|VL=MR&nZ)4PtT1+?xu-Cf$wMR~e|M|$^M4M|D-$gLQWoF) zR^;yin`)Kw%b<^{`eRwV)0*&R^N?Fo@-b*&qMZXil#~6U8thT__*i_<)xVaHN?hPy z+E7pX2s+n`KZjl%k#6G!?h}u@;!cgm4su0- zoG6YX+LsAZ#_9d@_@wXYlV-CTpb~!E7a#nS=)->Vu>YF0E%2vk)h#CaX}}KwaQ1%) zE&uj|uK!Sce5dL^psiHBy6G2PtJY5~2(+0_zPi~1LRb zeD9gJ0Gq+7ss%UPqSV^|Wiv%_ZL0PHnPow`*8=CFDK7jo^rCFE>qQfgy)7;@)!X^5 z3{?>JS6%mWaq&s@wf!7cq@Q#xc-1KB?t&6DLkfu(33$u$L2tiH*l~m9BZzPcYXhsG z7kEZ}<*(FPKY#XDTHaU1rN4E+#j9@j+F{|c++lu}z`VA6C@$Zro)^G5p$mI8XZYJR z@El!&Sg1_zmn9x~op$$St*s-MWs~*FN*hU*^{-mVd09g?!fjePlh$@Fe|ZYeS`bKw z&edaa`9X02IXWY4oBdVSU4+YIQSRY?%b|V4b5hmEjei?TMCQk!X0$?A^)o!`maC`Y z@=|x5b}j@z>1DW3Heugz4h`R2pbGyf9JF-b_$69#+v4(0SKsDjIQ$mW-fOns>^51? z+!X7Owlr|x0Zq$Q`9kjl4uVUw$jRC!m{Pr)s_niN;pDr3mzsUorT)tlyV`QQJ~ zLJ0`?Ie$=7!<`j5A-tli>@{f7zcj1lt(SV?VN3ev9MV$P{RyvKb&%3oIrJD^%~#s$ za)NoOKpl2f!o9U!MK0g}5J)J8Maum9`mwBDBK^_x%uG(qguHN+VL03A5Vz6dj_^+G#9Vu*QQ-Hg2-*Wi)xR`uA3>|S$s)V)ru;>(n%nXh2z;3jf241k;?_TE zZUJo~6x-xCU2msXIj`uxMGm!;pKYR6%8hpORNVS2=tB+r6^^}CMip}V6g_C$V0(?h zTKfo+|G;5t%em%vT%w2HtDnlp-S&0N^=kCCnn~J`prLI<$hI8(CTgepAv`WI;eU1S zOSqi{or21Iu&4=@V@1sKTBsuFO0JT!T-HOcwf3Ra@9A;51OW4GPI;JqShpz7TxzVg z;X->mZ?ztEJl}z{;<`so+}6d#2R(Z8G84Hn-jMC+F>GO`FT-&?%LU)0EpLy-Sdp-3 zK9|a6lz-8c7RC5Hs8of^&HSiqE`N(3$H81>#+g3p%8d@RAHxBZ*&-&?KCG|DDqlZ6 zR*jY%I_k%w15_Sg539_dg6h8(#}_K$^`y+3T=-+yfO?8|HjsXigOxUEpzXKGLe1N8 zcTN5&X%$XZ<%p)92HHU4eL@TD5D^ukL<3oQsBqhU)_yeOr)oC9j|QrZ&wskB;ZH#@ zZ2J_f$fuGuVVx`Sskr-Rccr^>^)+5WUjw0Q4BnJ&f6&P`xFhc1a9<3(BA~81A@g<9 z=i?S)S9IsQ#cpWdfiW0zUW4d1wDLo7_q`(gm-;cIc7QLj$hifTt#28+o0ocu<_Oan z4)r{E(nG1uZs3c4TWi;nyMG#DEdcg$uy+YIa5uPt)CmjG1u+fPa00-I;ZjZZ=5eVN zyYf?FyaPYBJJ2Oyw(aM*v26o??mG%t7<6oGo8sEqW4 zge@g3u=F8W|75>eB!3&30+_`Dl*B{Y`CRE`_^Ap^qS;mW8G_*bq5D)W_ ztE<%d#2*3VxXEaBQ>w+Cc(5e~A?Lf68HT4a2xtUSTJ4sQb3%D47y1r>s$iab#uf0? zXy@f0A^hUL&w2UFTuewUCaj3QgV#X4v2}x;?x;JkfP`dO7`*gvRV@A~3m!+Xe&D-%>`r z)l`8Ihnd(;>3{F8JZ|s#2W84#K|(TB=+gY8Y&xG6H2f9)%_Jh2e-hRH211oU&Km!^ zP^wD)@dpL>ce&T4miwRpe6Cf(SomIV2Lj_WjsL&I#}{bTw_x#CIMbOd4N{ykgPW{1 zoa+`n;r&XGEVK>b3wNt$wQF)Sm)U#|L4&7=RQ$45M}LqPkC8iPBZ4;8A=j%!s|fCo zLP(AIDMtcdeT9jc5|l=1qy1KB=s-Jy!zKwfUJ(%oCL{a72$IMjz7DeRH4l=OBR7=N ztfbepr7_X8%jYWV03k`k9aK)2)gca45vJ}sKDj&#hmgxC{Bcu#+cX85ehW;8uv~97 zf@m|sZ-1jyo3?z|EC9&?-qEyyD5EOKB!bP*=aKuIu3oPQy)#|ed&VKC2RzsUJH=6SLuO8=-1@x0YGd%<-caX4f@)nKCdV$#A^btBk1%HxGZ-G^1^k9fl5L4j2=lp4ejo8nb z>eJ7eiYK4KDs#Y4}lzS|9_w>$v0A)#TtF2e7Wc$os%r&yAQmVAFGOP0; zyMGLv|9&HaWG2Z7GP7ju-aWWLL+xTje91S1!Cj@(R+Po?90@UAnJA?PHTH$$-gs2Ho!@{imo;W;HP^_sJFS9Tf9Ej{@I%aq=eC=tBx49?=6!K?EP?+vH)HZ{`d3G9JC3<_&ViX9b}tJxjs2Y;IY zDA5oVmX&#;1fpQl%3{gNON)rIE* zCO4YL4)6w4lLrAo>v%6H2>Og3mGA?gn{jM->FEij8N-kwO`yWXvS$!KpeJho6BiP= z3;>U!fVoGHTknPUIS~gGn=6(Z^?xjZ{alI$&3cSolPu)2Wwk>YwH@CR0u*q>Ce=+GH_-f9DL3zMWtR?xLCJr_X z8v%}!ViwLcF^OjbCbm^qV}DeE{fr*bnPp#BS>_sNjt8cMn3A4JY;s(>gJ3tfP)|Tv zSD+JRGjQT?w?rp7Uai1q0{fLDy~8L2N+8h!B4i_~PK!&UDkWvWCgDKfG)(>31Htr) zF4bs#Y8o8e@!d*rJ8}WX-SH?Kyq5K1Cp?!0-{=Lj$%e_5S%+TRQGZdxbg6l0)swlHNm-pp}wXW8O7n0$tg3JG3xm>luG?*V(G%&@WRv4fktyTFq|GjmF$arJGU{IZT&(SbI;7MBVjheng00vxG z-Os>|uz~$IDAayaA%7Xo3>&ojD2@P--I<=!(KJj7Z{EmRnd6f;!Cb(D*kIOJ&(u-J z%zc7P^;dNWf)%2f8Ax8D>IK=v>8x+u#(fyI=kWRfdp++(-(hF!iMF+$rJf-%3}Fb^ zeBZa5FoL3k<+bTD5eBxj(iW(Pfq7Zp0;!r&7A<2S^Z<^`1b^RK#*CdXjMVJGYxe6n zkm3tW6+If*zozF6n8)*nmBqGr0YI!cVVmdRrAzR!oDGxMC@2X;X1$?rgbF_Nj!Wutevk;`fvl2ZwOV0>YOi|!Pz56(ZmFaK}l$e`VJmi z4~zF4K1R^qtbYK)7H5(kb{GBJ=OYZ;i}3WQb^9QOQSXBYLq62=mQ%8$IK!Cu-q|P| zGL0gwzpYEory>amw)a6m3rweqHgIE5~at4GNw;T z;@S+nUP$r%Leq27Gx(A)0~OUeBH1(e&(N|B!G$`U$yN{2R)9o&eclog6fE@>QI%KE z>PB>Vl9iQtl8tHeBrB-%`k7s3lPsGT+9>523&D(OY*EZi2twM7y24m&#!BwC1juW9 zlwp$UX@7u%eP1>TWtienAv2o=6gIOBFgtjm0{mPlQ&6E)O94^6&c6yP2h z3<+8PIfmY#CyIznJ}QT!ZrXPDf_#J;X1#uU3x75-O-nG$Z7T^+v0M{Uz-mn(1C1H2 zJz2?SO%Q&gksKu^@M01gjMl+|;}$SfaH4U+W(VcS0+Na-i++WsgG*!RUnH!d|6}&1 zr(!5RJ)wiMiLjXF+cemC^nz-v@kn?|;}KGz@dzZ+c%H1(cs_)C-rxhU;|AK#qJdrc z-+#QI`FO^>cFc54K04CTZwzOKu17Oyl@b`5F;w*&PR||r23ks4PSHYoKt2bIU@is3 z>3K|-U6b)#&N2CreD=o>xar|={3HVRJO|+4)$r0G2Y!Rehv3kbc$aQD)Fp0!fpQ4i zV-6P*IMfGBJ_M)2i>Msu3X{q9h0ECicz=-8hDJ18a06%i1H(|S4j|ATeXaTI`sVaQP-|8q zp4M!OaNSFPMbes;U_@(HvZB_kWFuO$lI2>n669L*XIvz<5Qn5fTXSI-@Hbo`6n`lp zk}qaF($f0A!*~vj;DSIP1A`CIud!(00JhBkzFBC=#6b)`gl*J@Amy$@ux-*NJk1dU zY22xRK{Q^6(f|)-^#v>uR9}ub5fZC5neZKkeqFM#K7=OZ-Y}r(+hm89~5X#4Fxghe0uHs?&-QL4uy`Ih) z!S?qp6N+=AD+0;-L=tv!FmTrahz_>esndp(FbMz$N;7kOCeO?qPqa}0a|kQ7On*Y# z_RTvTjup#7Q?vuXhUSz3t>zv9{iB1;qXqIi{vH!r7S(d*OTLE0^<`vIg`Hc+H}0Vfx3rR_}yJODkn; z0U_@w>zasaZ7A5;)fPkZ;&Bz+Ns4V^FNkk}Y$NMK2+`fs2JqO{R`*hx?VW`}@8NpK zXrv2r&J~;46cl|U$hi&#`#f*d^@n)(FloF9xI8MVvCl>1Awo~~HGkwdd54~NEuM}L z8+P&$xJ|kZ=0>buBZyl&_qtg_C*ZW@+fK(pd&VqEJEGrSD&(o29 zizBT&biYd7dvFgFGT=VxiQ=^A0PbaHHr!dC6$k^z>^S95XYLbxD&pIcRb8oqxpl^fZH`}FT!XG!{J1KO=lZ8H3+x*?L0w- zSi5@|Ld*LgQrmkdy!Cw$q5VCKKe`kjEzF%QFvIzM;F7S|cz-nVd>DcVsCcv}zzO3w zXPtZtcWaY@f}X2@G+W#*61BccNVMbz(P+&*5YeawnuBWuVTdwo;kn+27yQ)dQc(1vY`CK0sCKFC2di3wTRmjHWd4M4S!@zSFUWk<+;I~4m?AM#zkp^ zI#8|qXdHJ}?Ouh>`>wC9Uq#RRNO;P5A0Y+K`v@emRq|w|J(3UMYPUWB+Z63O@PE;l z9d{JBl>5+M15aY}1TG(PEPf%rmfy2gdZtXSyLxb%{k2L-hMSq9 ztgWYnw31XU2#z>jz=Iia3%*xw^j1huR63ZtJnM%RYd2tDU-Nr)N?1{arY9?4V7az! zfPb-7TY*ejwq>hgBZEo1X(bxoN(<6tcu1WKaS{r>)oNHnh!~;Ak0c3o($_Q0;@gr4&J|1?@-iG{~E3ZF!}exjR6G} zfFPF_b9fx6Kz042XJQz^*<6AA5uDSTZGYGSkuK#%N5^)e10o@XM#VPnfau9eI;{`k z>a;!p(`iYZwf;raf6;vMxfW%@gKL##a|d&K5bRcSj3IrO#-Z|pLn53H91(?S+q|Pk$dx z{`Qgd@uLI2jykd9lzgU$sKgbgXG9}Y<7@moF#XV zdz_kxj0=U?c1tc4J&k=;+qAK51%HdSYT9ZGAVLl0>ae{@_rCnGic|+2YGsC3^$?2B zUj3D}NDRmRGTV6IMiG>0;{&3@5XRw-@B@U2(;_&)D-?ZGY}UisFyrL9gkRSt9yJFzl3_NA1Jrdxuq`UGVO*@I90OkDp}`u%pA*>OE>-;Oz7IoQbv= zR9*}gxF$NF)(00rU>6qm;D5RY>`isoN(up{ zlZY?CL$@HiPDRriOvS#8=sk{N5#Mnty5zBrauI)Z$|LhQS0}5!zd7BrdjTg;DK!$i zstJUIc&KHVPm(461*}$CM4spY0J8f^-zOK+(eIPE_O|V4KG~gu?!5Uec`; zyo*WEip;HHI+6%gDSsJeNvl{dSp&Z5i6^gx_9KApzWIYsCKnj?J0!AGk;5gqA*gsr z7z+2$2zkB1^l&yC3OmlbCyXx-t0d%uA)&@^m3-(k=D;9UKosd(GFz7sXFlGrbi(9FHW5D&u* zoPaEz#&}zaVF^-1Y?cU2zZUszbC z3k4C1nLWtETVi~vsZf0`3JOuc2gM)^SD?_#kO6#D>C-?UFlK>K!*CAZ8w!{!%3H&m z7`9QoEj@?OOvTmglkh>$!HaQO7eT-7c_u-AhEwA*V8+;_4}5w; z_p+YKg@4hff7HuFJO1}?Of*-JasQ!-6CN(~M2Hgl7G_c}OoC1R6)yE!!PTgBKk3@MmfE`vXu~%9 z_rH#VWhVFQ4?0SuRp7@X=fN?eCr8fPVm}KUKYuG!+^{atP1ZtuY4iX5lQSW{{F_C4 z`Rb)h{KX<3{e0vS|IH%)bo8S`Jo>8*d*P5?+Mq7UF8Yry>6JzL#UjIqtOj-doBhIe}v^lWPjsa&0v z7A8(-sKK$&Q%RK&whJ+HT5hfgG`XS~YUFM&A<{usAAFoR1jWWWe zGzXG%{j9lEzTqlBtVcPq;UO$M-hlXz>q`GY4L!LLVL5IqP)+O_K*O@wmWpP;=gg z=CmK$tKmh{BljgblVp8yNrood`k)f*O!+Je#1Bp1I{5966}y$b0rkr zMq0gEf_mxFYcGXbJy`VOz+_N zQh>9*g=cVdmCXNWb$3T!YTy{3qOyX+Gz8|T^UD9|%fC8b1@lntSEBRDrmZWw1ongv z{$KxR|3&N+a2lqshXvjUbv!liOS(}~Id4qLD(puX2=gLO8v#H)O@E%LXcWj}d=4@% z(j3|ySM5A^&bw8j3Q#vcQ)i%F%t0{vK_nqJ_u%N6;A^1SCN1vJe39zoDKulaPmk(L zfj2xN%IiH2gWlzIxI);72rA%?0MJ>Hc*yrz2BJvI^Ky@-NN3drn*zDsLNKM44nfh1 z+5CkpgU~otqipbSUw?Xd6BInM$)IB|rq~1v*BSK`)BcWc$*xrsHB3T>hTuY)Ajp}A z^XB*hVB>w#yuYWvx^kZjw4V`a=&B;YOXzx&3B7CL7r?Iq9{X0v*bXYNz~ry+iWok0 ztxr93D{PYdQFMIIGZs_~Z-gzZ3t_)zepo83aNQGj?D`m-K!5p-AfX;(B;bqzt7JHw z|H7e1G2c@yGRzA+|J^H}D7wUYFfg2N!PK`Gs$pA6mB0YCgI2UEx5kM}RUkDHh7-nz z`?3(?F}VUY3>Dm=!GS_K3N<)$YIOJklOR5Dklg1uXd8TtI{^MW`oJ(~eQb-z2m>eu z^rqfc(*>I}27h^Irj;{}_S6X0#At`=a~nXI*nP%eMmkGS{FbikM7xMuy7axCz(7+l zldD*PP3jdi*-pscv<^&Yle4L2_Q|;%?YSCnwA&77hh;V1WmVZnJ1k$$df|zEHG(kG zO(T-W)sBl{qN!E9-lvruVe#*A&bD4a1BIlL4HjvbbbpQ1KgsBEuJIZLgP~MVaK4yc ziANm9j~I_SJRehpQypYcLI9$yjrMfq*`nJwX_Q1`buAzIY>gUfyB|R%`%tcVLqptP zP41E!FrPO2_wb_~)XlFj$PLuQ?m8K7r1OsAP`_F9SHUMtYSz8+K&yC|2hJA*{`6UH z_}H}h&VTvHw5iYg#FGm(VLr5~ZraS~6o3?bC5b>UKIXnG+YJrbp{nT@F>jjuWNU zhlafcIXVjUB2@IU4Q8Y=<(4e)JCitCJwLzQAhf~6truo z2{9Fe9y@O?%D+Bjy_;R#gnag)e%;L0p&w`%Ze)eZ3 z^*H+@ql4T*k04DP=~LlGhIU>ulo8WI!i(p#B4shJb+|?f(|+Xys8cPJPkwhYhPg>)CTmSab?Z>z__}`;GbBS5tYrU zuF2g=7bA*xV9%NYIkA&Xfr=<_UQ`s%p-Rs7rp=&4rOr{a&A*j&Ksw?&rH){Hi`pT@I zCs&I&8?=bNNK3drlEG*fJ=3W1s>e*|1Va$v2^egOIGg02hoUEbpnnA=s*)`qL~=-k z2X*>P>EQL5^uoNlXU#w7cifv!!wrvg{hty5VPO=BI+-W&A5 z=0!GV7$%GYjyYU{umhMl5WFu5Q zWb}hqCMHw=3wn6wy?Ys-*OiJmjHSJ(1QLSlFn8v85L?f0z;%AQi;Quv@Wo}w-I^~H zi>z>Oz9~eEMKMfs`q3AN_N4-&@F@0bAf#LO&9FEY+=tl*pnu?oUfVT+b~8Z35a@6Z zg#C-DD+;Wtg|#%p8iAO0Q1?S*JX$6GgxUKG{kYHIA1a~?`~!yMi$61X(+Bw{3=c+66-Zr&>wh=W~&;Y2!y$`=I?@n1SIkijB0;-Y@}BYDaZ3s1L|5 zIAncgR2)qcF0#10ySsbf#VxqIy9AeDgS!*lA;B$paCdiicY;9QjJ-gd8 zTTe}Q)id2yGw8&~L^!P?AlNhR0}Q+wwrpZro?$wK5(gZt{N3PYpu&}4fX8tU0ue>R zd3h5)XX$LgTp_0%01v)S48^OMEo34==nW{cwbGt<`)XSW)hI2 zU?vb~gof-KS3mF-u&>Pfy*)7()}gs;oDOF(GimZ{%!Ku9B&vm+mP8z`87-!`OA&<3 z*Ed#XmA7pB<$Y@x?oke^t%4cwweUwS{*3Apo4*ymlw`-E;wBezx+tWy`Bl8!a|>eVCGQ=8y`Gi4tD1$KN)J)RFIqhP zZen%+&9c5#u=m;xH%PtrWN)k@WwGff1xqk(mER4iuyG_V7cy$MpF(2*IQ9kkv@(Zm zq$U#x@SnOj>^n9gP2neK_)koDVyjAyWi7i3e0s{c8WpC^TNQ1DgSVcY{=2XJyGHoS zYRt^i{HCzL7j^BcPcIvOt)C1w=&5^RgflqOU#x7Nc_FTDUhy>bNTzw;gCwk|IG2#8 z5DNuqwi7}k!uB;_1@L_0AVqAP6$VHMD6v#&k+I=M3E836R^XF3rHE4L&jw7neh>Wa zr8;u@Ij_zl9X_MZVGhj@`hCZ_Z~BWPzhagBzZqE_fN`)$G9gAGcOMJuFFC#f>=6Ot zEz?*$L_Wjo@Xm@?bpLVK?mM*#+hDJHAOFFK13N0aDdmZnS$`9LHuQT{Y}`Myy&Ez% zGlzo)gShw^O5T%v2t1-|%#B|2PVY4pvf5>Y;v5_kzfe3l6z9hvxsW#$P|^2Io;MYQ zA7lP-fWdz~A$h*h%hTVSx&3z#=`7InzAU(j@vRCklytRQ4HNZRtOS}4w-ya6FNmlFtJB0n8@vksSy8Mdvz~@&U5tNMn)zNrz^Ya=W%!E!Jm1UjYUY#@zKLJ zI|-~Q#%KSnin52R0q6QqsG!lXKCw!wmVxob0BF22nSn9V z41{U4&E=~*;-Zd|iMC$tHcfFJPMX_Oz$FWP<12S`U+HygKog6^dfz1ONnahmnWFd* z1{fX8(w|i$lyTl%``(bTz{nm}hTNs=M@vaR{4FqwCAnB1>xzCHcv}z>aoJrl&7OgH82csE%$@A(9O~V_6cT&^ofOKUYk&`1KhHYhy_q zdKYAcDO;v2R(0RWJ~Tr)cdb*VK>MO zl$>3IPJa4$OM-v0v$_<_la++j1Q@*jTrt!J7kYWIR-UTerz~WX203vu3;e@fY>d9t zRNDdAQ(Gup9yf_L7Z%Q*DX#;MO;7MozCa7w_)y(N$P=cUHFH;UmOU%7uiD-+18IvY z+p%M;_x($5{{*Z)-0QzT3-;3OGWu1TjuU>Z=3k==)?QgFEM_2e#eqXuEaH5f-QSew z=od&gO8UD6c?&rj>G%|)#3Ep{wGg6Vd6>YDWkeXXaI%H3!%8&BL9%0blcueH4Rp}nT z53SI-cxY8}tgE?e1bG){yLL23hH$2x#HuS;jh4O=D54jJ?>w?t6s5-=#NOFSsEAic z-=q~8ucw40qD)-;#X{#heGn;`8e+@F|AR^Lav_+zzh%e>P)^6t)W*AAXa~;gjDkIS z8!Mjb{`f4D5;!M()h8_XaQwMmBp~gEdO6jAPcA}HU#|-td4|dR2N>AF&h~kyjC{~L z?Y3^mPqqp!I6={csRV z4!%nd8jwxi!sL4VAQjV@5|HiAif{>S%Gtkf@W+?Y8xfePi;qZpW(1~OCMa2pLW^j; z?TZ|l&{*`7oT2?#a-cHisd$azCJU0cHtx(_kVZ7|{{ z?ZkP!w|YCT$PTH#rsA_8j1cWEkhvB3g?X(P!l(ib2X)~XO<4eGlJM?3uFWdMDVyN}ysi&)HLo_0PkYzcw#Ns${R3*ga0*J;rqZbq-I*8`c> zBc7bG7A)9smSi^4_R1^L_Qzi^8|eJ*wb%&2%my)Yrz2O=HNeyBNQ*TX=LfOO*|R9B zTFGx2p$Y_#Wiul?5P~OWrxx~R+dt(i;`?E}Z%5hDQ+n*N)$8o>eqM;n)iazd1e%(J zPW^!d3vlKv0R8T1Nj5w~4vn+yrWqE+cVD@Tl0w?x*GX!z&zjo6m_S8>O&lIe=j*v2 zSiE8wFT3AB*^!+J{5kVCv=)w-jNxZgZtBb-*yYN?vx2Z(d9`vTW6lqqKAvdzU%-c_ z|9db1OV7kUy!eoM_Z+KILgA)}yuVB{X4r;+x)EmFaTJ2#qPhJ)_`QkZgfE#htY4|{ z)$2J?qVf>3e$YHf&tSJgyYeBpd&Ao3Z9ZJg2cebExIFi2U0ba*KQ(-(x7hW9T(!U+ zSh$-#$&E3z)Q}kb}=hkOyr>T*zw(xX6p~ zhJ!%<859p|&cxBSy~wiXJ(vnGw86wREn2r8_9jS5(a__eqn^aq)PsD$E$2mKutI=k z({2ukFR%Bsp!k(TOiFW&)XEI#+B10-$+m_OCaC%IyBDh!$~xpXBzE6?zYP>MlLP!5 zwm)$)S_OIufgYCV$1YB=MhG(${Pjfz${99WDv$HI;dz+^f=u2_66%2Q49#=AheyhU zujYJ;nRAsn47x{m7VKCITgX{k2JD!SHpJq6wQXI%6Jp?*iJX}1eWEuYLX`sX^kGE{ zdH;r@2zBC?kp)`0=HbPl7H)g%5NFufqjdeXp?g!IO&=-xvdcacJ;8ZT^` zCUSyg3H^;v^EN}uZEF=ppa1EloO-zeJ>$ZwxNfzMdR}vezp@ zq5%_v=vkj~Aa-LiM9C>|gC&}&g&C-^DGa3(RN7H$T*)n_tZ_;S<%9@1Kp%lYZ1fK* z{?r?TxNr`pzz8Ll{C5_vbK@0VG4~;)y1cp)>6OS;Xp$g_p*P|3WO=|R8>MhwK0mC| zCHm6OsH>X#JbPkm@$jM9o6qDff$yk%RGor?nzxl7N&O+_UsW6cWCR-@ctvOn7+eU& zQ98)Ksx;(TVmv5wWJXX0goc#F(r~@iTsK2L2w_mR*v4g+EM>RxoG>?z>(bLQyrK!C z@yBM!yNWn1A1Pn9#z~+vGg3L|4>PZhPI6fJUZBInHayUu9aUh9hUJJOaANjP_6A*^ z>vlw~GzOC1XM0?L1(QUTspi`|{@5XWJCYwHP6zbj-ywTR&pyh8k-ve{_j7$4cKzR@ zMtJ5#<#8{d*0)W&sEWMX@mHypyfDG2C%i{=pS{oDhF~-{1RtLa7LuI{-^`>>2fpBe ziV}7hRuV!1EruN12!4nLZYXXYlvPk{{2W);RBkG}L~IIZY?5OI>CT4+dF(5IjwZ3C zc~N*U)|~3uZ_tmpk=Fy~g9~CAgQF07vnPwfDZ;Ff3LhtV5p-;t(Ve#)QUZTjAyJ@# zPh|1w!>@4bgVIXOntpObTzDmOHSd^Q1l0}s;N1+7Mwm_BvF^kRqYe@s7>Ge< zd(}%k@&hw)v4StmO-i!UIuD^wu@B1L(`)dd&%!K0vyL63@=_hU^VkWY^Heu~9X!OyUA&D0J-VqYQ^pQ%y8Gj~gb93^*7aIU>I%cQzv9Y!#Rhf;Zq_Ntt~M z=?vLfh0I5;L3^g0p%qO-sN3cFsf4pOt1?9KohrJl1f>-E4i-B|@l6p9mrz;qYW>|GPK!SNV-btv6xpj zJOIyx3CHnR!VZ6pNj&Ht0Ux^!F5=DO53yHeAkK{*NhfyA6UXCU(^?p{aHq6e;_6cR z5Ckq}KO0}+_`xvr^KZZ`;TS#TIW1?|72~{%uGC0tn_9{K-zjS7Y*ZeS)oWwUnlo_8 zBBW^j-P7zJv?tk&!k|7)CCK5j92fc;4**f6Bx0x#G^pNlhBM8NKQ9bJU+o(nSktBz zC#suoCec3kZsoWw6P}+S?CAa9A<{H|pZ|z8(VoanL!1lQGXdNmv=4>ioZ#yB_}b)F z6luARrjw6kdSrWQNV%37etdg;NQIU$etain$jQb5{+w+HsEmpa^aT`Twxf`(CZGt* zXW5DZ)C<#TFClV*LEH-s7FfZ;C}gL72J)W$Wj=61r3eCNB{3oD z2z7QgX+IO##tf>MI8RyBQWIdS$`eb-qI0cDeFk-Om)?piuZ0$sokxfRKXTxyYKRT# znxNVU_lzs6oq55!$xXL)+G^)DaoJJE3F$ck?+10cfG!^_HN7BmrMCOfE=V(?_n~5n zKj-HKdxzL60o=hEY6#a^JcK7g0;%=`bl#rTukK3j^7q%1pP^3|t z-aj4P|Hj6rQ*kI@X>!<*Y(60G_zRq8m?0jA)As}Cq8bAEi3JiQ{uWz9&g2NkIL{lt z&oZ5ZuO|TJuP=mmJt2mxtnfn@C(h*IwA;0uRv({2R2@GQsQI;MdNk;@_m5 zPe$E*WL^{uJx14BMdr$^o8>@L7`~bPUF;P5%Kzeh*f*ZS3y!y7jNw0#J0NJE#hg2~ zkjyZD?m+fZ`2GVT5Qk!i#1+OIp)4Y@prP&yHwH{JEQs!wDU zWP?i2IsE#E6#JTYmuV^mB8(G`jb9lGyG`-H=Z;!-$uSs6&z9L8^6;?R<8e)3TtfCT zgUgDK$HVYtW4rb_-I*Ay$$0$9AsaBMAPcIfPNLzw zl(ViYm@q_`t-08+ioW@G8eC*lr!DBj=U+8pKBLzN4op+t16WNQiz6ZEf$n|fk1 z%J2KW9CqkVyo5;TEdio9Jgc~f^fd`K$aG3nB=iv)5n=(|5Q_18QeS0x7fawA=tKNh zLXiuO1OzwZkfg#^1V~{%o7N05Iviq+i4aRQvxWjNu`{F!^yb@SUAed_A-u)E`e7QGUXeMTjQ?uUwGo~)QR4SI13DU z$JWw&TuTfB$FFeC;(VD9NL$Wm)+QUTK5Es1%qd)MJZR7GBq|gaakRem*QSAkKY4`d zl~3>iMrtI#)-b&IThaw1VWsPFA&$z^Xr_Jv&wUMRu79g0;cmL8qW#Zv@9+k;cd(+< zYksB56n)Mwm#aZH%t62`?h;h4v|Ip^$)w$ zex2QfT&jw2=zLao`qbx80g0|DJH$w{#1?2akt4%9JmWIhBy&vG+0wqb10ER7#S=-D zrfewvHEN3m{Od@6N|CO=KF~_&#&eXBg8y~#H|+xwwUm&^($G%c6&VIq%A%kt(;8I3 zEWbmjy@yMepTurafS~TgVY3IydvPYk3AzYb)-J}N(O!7G6+As!18}n3F z${SByQH8HE&ffM&{3U*1e{K?{plcDJlC3&~A>MrCl%M@#tY859__3=+aPS1DZ<#oa zVV4>d;8dUq;nyYGihQ90Gf$y|Q^hmGUMamR#GO zq-Gf+-k`yw)ef3JXsnv5iAWUlyaiROE;1OmjAJ-Y>voXD#i0&C!XEKp*x`G!$dKzJ zjeg(O2?r{BUbYK9X&wY7pIK-yTNZgp-4^}c$rXNt*&$|OIHh!fcRu!Sh0@p%Fj*U_ z_TdWgdTjg&^GLUz(+ADn@;4(-xgLXBWT@lYLNW6U7`ZX=?M8K|(;xoo zux@D-{GLwyUhhhC-78cV5tG&T_ZQUYx-`H>RYswqcO0&ZPE{3E2-W2;f~@o0_&Lz`&s#OS0BwWQM0ra}~N^b>ph?El?zmEj6i4%NP60(z6?8#D)uABjGGw+2sY2h)O{+y ziRH^7loVYtgeC6QWW!G$H}m_#iFM}g?Z&Q|a5YB;2Y*-Ph!{f^7;PbDX}#x$0ZYxb zxbM0&3HWl8pNwHz4MOG9&Uzw89-3w_0ECRR6FG)&u-3#uO%2dvAmrTN!rdMg4)=lK z61(e=dWEa>^ucMG#kJAh+@W!+fX1mj{U>tPI{(Xta`)9=pXtBbg5eG8cdML^>^4*` z>&7`PCW2DOOy{O5sRvO#aHdCNUf!dJib-4R`617Lk(M8MA~1dyG47_$Hro>yd)EhL zKaVr_M{=*>E`0-Bb7mgka%fOVFz5VLf7xI4CbSgY{;oMdB7f}CGZ}Rpxj3=%6%)8U z9}D%WpH-AC193J%LpSp+lWBVsu2`vsvr{RyKuxy&Igd7TVx)Al+R-mFg{L&S-VTf+ z-Fiss@`t0Qm#@^lGy8{!zgD-j=>2F~yjzr!w!&U)Bh(M96^r%bOI9%JjF{j$RcdOk zEP~klFXAsqj!ySpN2WlW9e?qS7!>Y*KUIEEw|{N#`yL?vEAeZW`ufeDp(Pnq8E6vj zIY#^Ezpb8!xr+PlyDot+%llt6TRaWO$yRKa3G~fWL6;+zZy6koYy>hA;MSy=kWvDN zor~T|U?t$*tX9r?^!^Rf1)G=G-iObg9c)@JG)i9LQO82@Eh)FGrv6(N#EsmiVnM}; zJ*uKRnzB2oiCz?6DUhT3@obLzHybiv8r`9@RiT3 zDp!nuoTV1$Y#+1a;2Kt|Jg_94)J?Z2W(Z3a^>?*ycOuvG$-1v>FW6>$phn@o{ipWj zp?O79+_mXF_Ez+M9jAF<_{X#lnfglHK@|TB!Dli1zocn4?jv%_W(N6}k8kL^o7d)y zQwe|`HMe4b!k|m>FY*52WWi{8k{7ig%qZxw8?vw$;rE`6%CJ85I;!v7l%rh?N;M%C zCh;EB3DVRFv{g5DE~V!-Lo|Dec{o#PeDAI^wfmtLRVx7L(Kz9HJTNLddhtQF|WEGWWU@fnc?s)e708g{! zg0dMU zaiaSL#Z0osp`Iq~`__Nq{}!H}RvkACt4upe+=ZxUbYwX@ek9gX2Qd}Vh4+4Ydmscf z4x|$^?V}BaHJP*&aqK<&4|%_&rt%1Vqj6Nnx?&FVYfSA-sG?3=9GmKsH`5jTQmaj= zK0oVYwI1dk9j8(4NOf%goJm~CQouJgw>We+obeQu41Xcabx)Jk4wuZB6vwyGGG&PL zeN@IW1;An*qC-+>UNPbOlFs>#+nfyG_NGDDL*y~*NaXo-Zk^p1KFk*N5N;EBkMzTq zZRxx@_)k6<7j!N^U!k&w>(=TBy;3=>>(_B&WEoP^ZK|T~?j{admx8lG!xnSS4LH{~ zB5_f7dp6H?<}NkcoegYH=Q>;%)?DV{zedkmb5%W_ig8u2H#?XXJ-|dSx-|oj{jb#g zUDZ(n>k@SY!$>!|a%z8cdp}8&OZ0CDF{3hXuulr5IanSnLhw_r<4pZ2KKedtlUy3_ z{gv;$VaNZqg%SU5{9GRAuij4UJnZ}^NH+4A#(sF@{Y}$#QR_5D$X4aJd9+ng>*AyMtF8>?1 zkvIE;$ARPWxc0lo%o?J0am*O87rIoQy>-9-^(??%v%2BI0fvJ>l*H``fQe2cCeo`v z@jT$WWbRxiNi@yPv4k)>DsNy^lXu1e2gFJSl>V0 zlHtD#sb;XTI%gK>Y=^D}8eCHQDK%3Xiixu%$I_^F{l9bLo91H&>si)Oj6+>Abnr4*#|w8*gOzJU)-;6xf`4! zxh*@KWrjSPVbf(-4KM4*#3}Xs=IdS)iI|p@U1apt4n1xx0N#e9f+zSO%))fh zc{bb*v|HQ#QR|vI=L46buHn%2FaD%_5Pcpe9->VFMUl_+PuRXXAIHb62kVCN)owZAUw>5cl6BRs;eYp zw&aoA*1x~Jo|dNnvuemM!yYTcvN?jn7HKZ^onIOxS_gb?j~VdjP!KTm)%mKI_NquCWYzUgGH?iTjya935z4XNs_&kDv51~H!5g8eB5?$v1ycaN07p%A+l_>N{e#h z4(`Dei4b(<<9Wy+s3Ba4tn_`6L;xnb$RAirjIl7ReY(7Atf^dckzM>2BNzU|VpDZt zmUNDR*a6e;WlL3cyx440JJlAG$E}CoefS*K!QIueoM=e}b%t;|FVFbH8C(O85mH@g zwFq)LH0yFczj!`;6UB1QRv9`iO4H`5bP| ziu7%%Y`WS`Saz|xoK`2L>bMrWzVpH`k!2XtH=-ZzSR-?-O7?7$V}xs(>UsE0;ID}f z&K@Rlb2u3`1Tmg>8c6?z*#LB`aodrs{ap6gEAzglY3ZNKq02p%E~--IRISP8qRriES(| z+74o2G>@QKl6FEuBi?O=YfF1L&*XolxL~PxQJKuxm;6P@kDh||KcHL0yp~}GeP)^~ z_QC&F02Trw_gZW+*nrfN1Fe-zO?!4m2pR9-kko-`&2^T=!?4fpv=%~|?TN>OIHaKw zFXnaJHY(LM77Z!f>q7y)-k)ix{r0Z=yn_iK86@qqrxFa0=lr=JuM{!h;uZVJF%l>L z8^#2yB9MOJ=k*@s86cro)swemWow)N*(=N2EWfeK9rk)#eS-VPz z3O(0p?(`YWB34cdX9iF9AD!dn)hWFXDb7ejoC2*gwTULT8SpOFVZPV5S@wgSe9+t{ z)KgXXhk40YzJmonIf{}Q1=TI>vaDvo9kQ(g2{-O|*InN&skDjC^k6T4YK9YD{l+WR z8+yzTS&bo;`lV&vo6nTyx<(Tf^Iud7!Cl)Z%>qM^WF>Pvbdar>4G@FgUz-DvGmuBksJP+YzeD)=$Y>Gg||H zW-DsAgQ`-s`NUK6?S;ioqRKF65zlC|-fM58f|TQwg5=TFK8*0)};@TFZQ{YNz@^04fqbKi(Fm^Tjd4933-S*$X`V8{GQaRUe|Br zaPLG>4GB=c6QFt#Ah|QZ{vk$jB0zBzAY5>j7afl(WZpMrmVwrR4c3kit^OgQMT%Rc zhN$sfHge3{du+DnM{mIoWq26^6shmt=wmt%V(3 zG{69_k(v)>9_m6yH1=P68oGKSId%=D9oYGOJcZA>2-UvzLho$fIt+<4bU@?H-ABA| z8Uvn1Yu=z^#?GQDEe}R`mHM8p@OS?#w)tPwp#zc_FtbobU7mK6RPdB2Ri1MZ8g2B6 zFd$CV9gj9eGDg;dd77ZVUE?&6@^~;JUSR-z;At?#%IUlky2@#=D5TrwmhkWceXvSy zz+0M#_zjZW6eVKOruE5DuEsQUR18V!j?1W&M*ToMuYgXcnp{aKLa_4?{V4b@Py<}h zrg&GVoRd=SDXRYj%BQWIE9>6bLazPXN9Rh!sM4mNI{IrfPr*XS)i5eHaK z+*zIV{IW{F(@l_+N)^!?oG9j6vaC3ObARlLcNg`J3F|*H7@V-gdX-0h>8@*bh^bz1Mr?I0I4(C%Z%v1NmO8=&o zJT=&X((`6euopDMT=A%;7d=geDC5bc76ogKwP)^`6D0PtP1*EvOE&*hE?FmvP>E5_ z77?#)00Cg&j-`O7Ea(UIpMizs$8{vpudNY8G^yHeK^%3mJpTucIX1C*su4s2{U-a7K4xPVY1!h0AH8M z%@k90H0OgVBa&D1v4_Sdnnm*Im&&>V34)NW8C&6t-LiDQZ4o)WXP8j$8?|axm>Ytw zyec)th$I}5sA^SdRPYnvO|Dgh2>awJDitBDKvmQl8)X(NP8u=`GpS^Y?X(jeqUmhl zp7LMc2fWh}zBx%y_41-CvhuBDPyjTJCs~*(v1;Po{k)=)f4uea7e#D!>@z52x5XCz z=|dt<5G*O9ANzr_Jv2Jjb@HAcSkPow+W_)Dovf5hY*p% ztZ3eep#dIK@~9(oYg%>)nY84TlLvMf?K3Zbb}gn*|AwkgE%4G-iwhL1E0yn~*+`RH zRzvVjCWj5eT2oF(cQ<6FR>M+6K>!q<_WWe)qw&$hihWf3No|66YVm_9>I5-Z8aY4o z#B5&O8KP{X=|M>(;>HqsH}j&cDlUw6 zkj91`8n>=<{^zVf9(fKe;?=y`Cou*nsl~*h^J_OEXE-II@Ywb&NDfqk&?~iAlxSrX zLmwH8q=;H}^v~vJQX^&(3)E9hRhP6$Br!&JAWb_q4njT^l&hk4oqusa1_KlfY2sk1 zInc3yEiImRu+si~z~w(eI*%V&Gz>u=4`t`@Tbj_DQq+VMI?M$ph=bTPF=-3)lxEZX zlN=FK4D$sViX;-LtPozjTkP}jVP1v0Fi@4wW-KV?XQnUwIVLE{t1dGl5!#o30OA}< z2rJ5Yu%{;xmG39#cf!P0YIgk0{3%u%2%v!4I=x6m5!KdfA3G#cT6L?Oyukjsi{lt%DxR`A9lPZFXq?S6UY8d56Z)DgK&oQ5o6`~ zMH@+J$w-upLPNg51DVO1*d!RS*-c+eUyrU+2epnDLm%CDg*)YYXh4`n4=)yv1h^2b zgP|?}mjV-k!L&QnrH!!#f3k$|VlIg;MShN!0jO5dMHd<0Y2xTjV)L}d8=4@E;+Ex6 zM`<3)ZzbV_E!*BL_tKI>5ecD)OA{wqV<3?Jc@`H^j1p#GClu$M7CJbKKGLY93n}4i zqGUZ={-^Ha8p`M#4D3{W!K-(G)_A1YniRd&VI5Z+L4*5hwI$mO0qzmnQF{Ji%#sw8n z;W;)BpO`e3MbaB*?Rrf|`k_w+y+QFUhp|?XgYpl_-I3;rU!B2)vf_ZsdP8G&`Jj5T z2LTj%enq?T4ohx2B&dz9>uw{?G5l=aPcxzmt#@170~hxr5ht6GzOpQt{fivJoCzg% zSX36eCU&^ceEv}@m6PNt=Kc&rJ$V4M$L|Q`@g=q*+Gs|^^c(8F(uVpGJypWdD9W#U z_!Qz5_KN6ISBLv9is*n;R>g_li%=GIRPT2Ne(^N3>>Yw~TKOVP=n#n?^Wforil&fc znlO1MSyG8xfZ8YqOIH~&^03~&n+|56LfPUs@|adsVGIP017@2zZVjeX3W$SOwPAxV z_YONkHf?}UU@JJac2oY&MIovzl`w}p^qiF{wS(gt6JyDr3Z33Q3RA_{?%(;b-q*CekC+jFufLSYq$ELaViEF0YP8=tj+)p_iK0UVaAZL|DtvUE5osBK#c@HR*bW zp0c=u7!pN|dOP-y#YHzTba!SXN?efCy%%I@{x)sN4d~vK1lz86e)L4u(+i_Kp0*(_ z^30~3GwMvo_&?CxSpH85yLQ?XF$`b@K`PZ$!s#l_JBSqpqEvNkOOyrDcvG~hKHx9n zesjIwbeDSN9?}u*xDqzkERX$u{_+JmDi%kn>5*q2+dGBjK%bjIs{&LJhGG zRZB%&yM|{&&Kip78g(RbMCUHJiRl-IwDa;o4!E;kwvvN+@RnAjj-cQTFO+K#bT8fu z&&NmDi6tOJ@upX{=)l^t+&fRGt@PY}q zvraMnlDx`QAW}zkTW)fc$0LSBcPMu#nn%H&=g~#$z+#RejhiQYFd8Z|2_8F0jG7t) zJ&>YhYx|Bf+nrVg2aUnWJY@n{>9zk;^@vWD>>-prFmOXi=djw+9;l z+rMQvbBd21Ot}cxmpw!!LF6@RF3;A}hZ%n+E>lETn`Dy|wmZEvE^6ZoyYUx*A&#RhC^v#T z|JDbZt|*_pfG~SDDj7Wtq3{|igHy){77A;1pOx}()0_O6vb%rB++UoAQ|o@2MZLN6 zWInU!bG-}V*koxAtbEAvXaB);`IL#3JO4JT)CzU~BdIeo1ikVc0#A4;);h5F9GKT` z`{xKVyhpM=x|V}6={L~V=+nKMrIru2ktL|opINqFp@X7CRoXax%c48kAq{|Z{9v(OK2?A)MsncqP_nupa46I1Kh>B*#(O8blbJ$Ye&fhRaJZ4DH`gx5 zy!%Gwdd&;v`%q_%6efRAB~Dc?ng1aC1=blY0@0Z!XBt^j0U1b&62Ay@&6IZwRm2>S zXTNAprPida{aAj-F28C9>ZF&-V+r!2x+hu@SHQ+V=871rSAWY6Hdjcz4zkShVY(MH z3(%pXd9r{n!-TwGDu;CebI@qxy|A!AQjGWvqzJP@{bYQexJ^nHP}%WSo>U6c2ZgmY zSLpDKS`N7eK>PKr5hH;T>5`LQzh*OAftN>BL+)8IYcZ9+jGKl@`I;xr&a8|rhb4B7Nh9n9#`pFUY9sO zmifI01kDphb=%2YCfkgEu_LxF{gwyBlojT-9<#_@LdFJw`DRIc%Xzt+1T0QW#-deKeeW zW(~@igHzjiQJfj9NG1o0)Ay6$5skh&e=C@)k~zky4LnhG^}zYws^X}67ex1^$0QWt zmy82YC% zumOC(U?BYIVjd+`?~C8!i)7HHK7&dJ*gNE3d~sI;4iNiN4hPZl^3!t1GL<>6hDKx! zF1S6SEx!`=dL8d&-b z?u#M+K$!CT>HUCt0e1FjK(||-c*!=e#8s^Rt8L2qB3ic*KiMop&T03*)CRGvo@X@^ zKL$?uL^ewW3=L`FWSs{_WsWF!244gNy6n;OVnxG}aDyZyVz)4ZNHbAZwhn4s-@ zY8e*6`uVItBk0061$9v33-xacCwKU(950}+En(qu^Q+gx<`TqbNiBx_&G1k5mjv?2 z0yFsVQ#JH6xnTTfi$ArFLZ=20;*T4Dvhu-bU!5 zZ02#$Nf#Uo2eTC;hzsT-y(40%a6sryTEc>34LlL~Wt4?dX9{*cu^=*6)&=!e^pkjk zt&Bg3(%6`{Z4+VqC7@%t{JyXrG%@CMrDLe;8)QViZqJQr5ZIe`YK7;_VC0Z!Rf{4@ zfqC?2b-8wnBItW$V=4^rXd}M?2m}|bm#?#vK)(@>6i9>G3Myi#^CKg=_cg`v5v9$WC=?1Bs^cR8|IRap|&CAZtXj*>`=2#@!uDVvvpMV7qOQS-&eTWR%3DCc>7zdH>m$+BQt=85l>{x z8x6k|Ov^B*Rr`h|fq}H62?$T7@kF+!yj2&(^M;%5@$e3wu!P0LYnSCCNH5Tc`U7zt zUzG&nmcj&rd*|BJ5vb(MeBtxhjubIkM-*Xi_m~Qykb7}mgbSy>;FUF$DCO!e3ig`q zy8H_DR_p#HbS!k%GI(?K^_AegD%Wr?(TbyQAK&-Q{H*2UOM&^XZs+QIys5+vo#%mW z!yIqqAA21lksPKwyc5UR({fzLFyaZ+F)YN><_SH3R6yAB{zeF0f=QIYXZs*5SU(rf zm+wo3mDt$`4&?clvn5!}Y3D@iR^i(*gqVR&%}VFD;@-pgg~; zQ&1t8)$YO|@blhX+eTZ65tsZI$@~Y08bu1*bw8{1G&m3(bWwAQVgC85UoOgMh}rWt zxAOutrHA@tkw3!{_f)paqjO=)(f`Z&iU|%ny)1o>yA}szgYhNTBccWjTlf{RH(1?% z51~k529xJFrz0=Th+{UE)!%5PVYZ{6K%txTb5L_7lS}WSZ0xATQb%@v{i-XAMV&;P z&fLn!*pw;tLz2lrlA1#*Q8M%}XdcuXbMFRdfA!bKl1w5D_TsUnk|zN3BemT>=Wgae zVh5O70p)R;ZPbZgYLlw<14g~=`C;661Yjb84Yjlg8O#RQH`Q^SCvNKU3e+5`A@ zXb_>Vj4D2YXyN-5aN+6U9Cl^Uz5hQcSqJ?n`Y?Yb=V76Vxdz9ZP(9o;yc z_pBH;INq+9i&%_eW%4?x$28Ph=&}+Fs2#snX-zzTu%v0Lax#TK_%mKUZy^2gi-2EX zS*iArTl)q|0n@Cuy0lZ`N#Pt`B_r0j0f=K7DWD|Ymh4%0a)V?DQLB&6wu zqiO}ikyIm~3KyDPpb={cJwST6adldqL7yF3`BX2!EsrWTN-vNHc`zAYIz0&iu`3e) zECRk~iNfX337NHUopjCy2<5YU!-F!`Em!U=-;ni8$DyYkCv1dt%m17B z2$ZCVU=C$^&xP>S?vpsek$icFb&9m_!Tnk?j<}1~Ui2`IIQ-;+v@l*_=-y6}6?pN! zX|Y~0d9Qz-NaWq#TDA65wQ6+FGr$cArj83-5XmP;#q{exT-FrBgPp)F!SLh#AgWae zBt+Def$DDj9^sER=yTvz5QNZ_VT-Cuiikw0H4jVSnwqM`rAwc%6J8fCY^K>IcUR zc-PV;3kgZX+%-bIEm-7#olr!@k zo?H$Z4w!t>@~~ zF<8;FD{wM7r@JVF^IScwF^oW#V<%9C19>+2KvIs;Q`)Pd>v(nWtB0)2kuB{b+*xi zev4Lv>&;D1(b_u-=^JK|>fY$~`R}KfmzNLu{+Y(-o8VQ3fSdpOao5k8xaq5X1aNpQ^`L7E{LnRE@HN5me zU~}+NVmvzfe^Fq?REgQyi}cLy#>QgCO_D4?6hX+{p&0>hM`kh4UjEyWEI<@N=%<&G zz)vqj%rm*rhJq~${j0u#C8zPbFcRE;SSPg~6m`9IFKpxQ94`StXJy>SptV<9=TaOd zXglrPDtfScg~0Re%`@e&eERCm8Ad@XfD`W?J28zZ;jFDLCJ(o zur3Ce=wTuU5VsKUT0#cdTSGA*!r6UhRXhYqf1yYF`f|Xv2<_Nr6$Q=$y9`P~(lh$C z9w||v`b`cX$E$3IEpS)kFf%p-JfLrqbw~RVEb0ydxm4>Hn8h6!e!)c)J_eiY43W`0 zoPM}X21+t7DPm77XxibupO$vx>zP;Vzi6g)Q~F7Dh8B+Rs-L<2X|S0rw|Rp@3sixV zf5oNJWz=VyM1AM`D(f0wX#;6qU#mjBf)MH9s?_3OJg_&cR5X6jD`AU8Myw1!2i;zM zgQj4-P}I6}pOtM08jG_hs77drgAtNviq}k{wH^ge>)Kdp2B}fIv;Vw)gQW+n+v)+x ztO+m7`~V-DQV&}*mrb3kaAL(A(4O**e>-L2DyG*hh&}kleW2>HPvNW9;^G!|$q@3J zI+gBo14L|I-L|_IN^%BbWwyfJ)J*GvcEz1t%%7MAaQS$iEL>k*zj|60cGd3Hvt$AH z+)vBGrhTS{SiqgD?1`NLMV}`V*dg0WMPSfb*g*j6Y;@6}4mo^#M)vWk6QVa!f3yDi zq#RNI{13e%X(6J}>r$kchyQ+k-aN`duG zfHDi~ixixyx4SliV2lNnpndwd2C)J=EK7xuD|7}r59ip2kjG{Lj`{yuwuwn zMYSwe6^@T5_ItaYuv0&=v)V8;e~Gy`TYP^to2fZ!65c;q-#)NT%uskrM~ANut2x-Y z(OKVif>{hn!Y*_JT^f|qC8~|2+ZerjlV`- z9Nrx6(k<_!U=(ULuI)k0m{I95+yHbR_~`bzGk9__zd25{eY~-=2;NWkMK*CD!SiVJ zw%$zbEnckQ{_9HrP)-rXf5!Z@Ze-?umEB&x-HVHhjeD+eJ z@5l3(5wbeFxxO7Ko2&xhFqo+kH%#A43iu$*-l>WJIC1(IS&){sdec%wi%vHqz-kH# zAjjt?Cx87YJbVF&Zir30-B5eky=-*(r0)EjBgrl>I=wtw_jML)f1_wQXI6a@U#x#u zmLqQT!Buev?jxD<@pw+#kF!G1el1yK%C(npe`y2#NH?RtgkC>Qf44$|_FY%>Rx47joipuq+@a3jbMaS(GknB z+Z896j{61UyL+WwJnUtMcO4r+>t;`C{b(Ka6KJa@(5LLce<#kPc6s#d{>0^mTkHFY z*)AKY0kFb+n^dq^;|C999W@2Ek~Gdr$NA;jN#)AQ8TD|!M5E`)GHtR$cWuwHjM3T! z3zmX}WxCA1z~PlSVoLNU(L=H#?-=-TkhKBMLBHY-+%0kg52KP>^g#v2NbX-}&sl6b zu1{c@E@&D~e^kB_`cT%8C%XFfP!-J~#Rqrfu6 z3?p0V*~Bt~ZDjeM)WrCyCze!d+NTxuKdX5UD6BlQQkN#b+vHq*Y0Bh^wf?k` zY|pJ!f1W3fkcsmuHS4n#Z@d>zD&B-15-Z+6kyt>OsZO`0aW$snyREbJ+*W0-o^yk* zZFMOjc<#UaeejbuKAo0XjlgGXi-PW)m$vUIMEcrQ_{_r(1Hk{p;sXTUO9tJ*`Uc#z zG6Cp3kIx}rblvFhDtmwF-D0)ZrkjPYf7j_he~@b}wYVhxD;(hLg0e&n3okGrG*}(g zv2WJIYe8`j%t2UWLG=r7*08K;;<>qCBO=_wJ6uAQsN}F1xhDHUH^#A86uF|avEu^Y z+GCjeG ze20PQ{-?1U0kAK`Ajt4%cUooyle_E)|MWzxu<8j-hXr@ZfZ{D!EwZ8ME{~0eU zwf=V%7B6c)y|BLYKO;TmYKfbyVOHSBKqG~~E}7`tPUD@|Fa}Em7a`dUhgT&dMZhn> zQ)J3|<@&rpA0-#+kuq4fJ0kfXzSHxE2Q@08aI+cYs(Ng;1>S)*Tr;3e74WEEf3Y5$ zB#=?@1;OCO0i_4Tp^%NZ;srg&s}I*T-Huh7f2_F0?gm|4!kELQ)(DJ!zf~T}-8yd* zbrXJN5~e%)ZKnb1|J-J z&}5xwPo+LqS92`SE3Q9&{k!J6f2{B+6#-@5yU}TsZxp!7Q)ShjKuxly#97rJZi!5Q9Mu+Udi|S-lU9>hi`$41kISqFwXlg`cwWkpb zJ)*|r?6cz4AIg#D9?_`4*YU}9LAS~y`R&~nnbZ&Xr)LO4;1hhXCMCFef0SS-eQaI+ zO}VR%E@<1FZw-C}$JQV4k0u9mdfhdfpfC}Gq7d(5fpbk8ln0JRaYPhK?Um~woNd!x z^us6f555ehrjL;cc5#{fH++EkjD2q2lq1~@toIWXBrx$`#jR2gLGi|{a zG0sXw9YN9Ro{V8%k&hS`l&wjI&_o6ybvR|&Z+jJG%uIuBx)8IdEAPeD>!C=0GmLRP z=@`SRbh~jl!x$cW(1Q6RQ@MSto^2?2#0H}ydc+}nHVZO57ac>~e=k;jDlszT=8mhS zy^Do9uHgWnY@fVTdD!6w(+0j=u(T}MCs|0>OySa= zzCFK+L!55(`%ZFgf4#qb8<(G(2gKz`Zhm&2yt%o$h=atpVmHU&B7Hkjj+@mkQV%Ed z+j(66#Ka_#>k$kkrST~BgjdF>+AsMOIfS)zv?#%dkMGi>94%5SYNjSoa*qK?VpY|$+--V4 zZu)qAK1H?ZZJz8SV7yos6Q&%pkHipaZT^U6*qrN+Wf;3MZ0fmtg0dATf6{l^{s*e3 zI%bj5k$#iFk+}>=B4elN?{Ch*46xQ)xJk(G>tc|I8f@sG;D9`9k+D3({E#JK-5QaQ zrRRuGf0J}(Z#mTHUt?OQv4T0DCafRmui=PjXcWG%iD2|B5$n06yjT!p_Sw^NRquC_ zl5c@nOtJL_CTNJJSc77x?bCMjCQY2M{0y9)nM9{YW+75z1v8M5wK&3?*CMUAO$dRg z5n&x>_cgB0zLh}HUh9QBRE^kmQ8`9`BUKb*f9`=Z%xpph((nj$P^69a%;C%KxYEh7 zFa!py;qBp|kp3owdT6huDhE<_gbj7=&>Q51A&Rp{*f-0MB0%VTtrt zcVtH1k%L`b<^?&0Hj(lwj*+6qV35IpL|Sh+m@?BDxFWVV;7GbR*^c z+JD?_10;?i45u5g5|6Fa6Jcf)Nqh|KgANNW9gi>i+OuoZ%qPMbPS`!wHs+fJ3031BQ1GxqOM|lxyt9e=8U`?rY#ZSAa|V{b zt{DZgSR)4>aA>-bHn5PcHn8EbnAL()Fl_dBus}4I z*bR&>E34=BI^3(=Qt!4@T^mWMfBr6zqxwoyJq27GG#EjDw9u4hq}3S#?L(@@06IH> z2GLj==?>ob^Uw@;&n#DZLr!n19CqE~{%8h4Q-mp#WYqRjtUx-AMty@i>M*`lI(NKP z?8q%wA{rdvfwx_expmme2Bce@lF;LTL`2BzZV3~L>7&*dYyRj`?th@z52@?Ymizw& Lre;9-hx!fx3h&U) diff --git a/resources/schema/schema.json.sha256 b/resources/schema/schema.json.sha256 index 8da7aa5d..fd5c7f7d 100644 --- a/resources/schema/schema.json.sha256 +++ b/resources/schema/schema.json.sha256 @@ -1 +1 @@ -F0Ba3aWEThPbP2e1Uy6eryGOZ-UxeLsPNJn9y2yGp4Y \ No newline at end of file +zRdHuaHJJPGvPP4l53ZLsmhGRGYZKD0WJ8OHJSp9DhY \ No newline at end of file diff --git a/tests/src/directory/oidc.rs b/tests/src/directory/oidc.rs index 7bcbe71c..e2d0d41c 100644 --- a/tests/src/directory/oidc.rs +++ b/tests/src/directory/oidc.rs @@ -10,7 +10,7 @@ //! runs from `directory_tests`. Each check names its test number or //! requirement. -use crate::utils::{server::TestServerBuilder, smtp::SmtpConnection}; +use crate::utils::{registry::UnwrapRegistryId, server::TestServerBuilder, smtp::SmtpConnection}; use base64::{Engine, engine::general_purpose::URL_SAFE_NO_PAD}; use common::{BuildServer, auth::AuthRequest}; use directory::Credentials; @@ -27,6 +27,7 @@ use registry::{ }; use serde_json::json; use std::net::IpAddr; +use store::registry::write::RegistryWrite; use trc::{Collector, MetricType}; use types::id::Id; use utils::map::vec_map::VecMap; @@ -169,6 +170,23 @@ pub async fn test() { // example.org signs in against Keycloak; there is no server default let keycloak = admin.registry_create_object(directory(None)).await; + + // http-directory-services-13: a new directory names the audience its + // tokens must be issued for, or tokens minted for any other application + // at the provider would sign in + let mut no_audience = directory(None); + if let structs::Directory::Oidc(oidc) = &mut no_audience { + oidc.require_audience = None; + } + let refused = admin.registry_create_object_expect_err(no_audience).await; + assert!( + refused + .description + .as_deref() + .is_some_and(|reason| reason.contains("audience")), + "a new directory without an audience: {refused:?}" + ); + let domain = admin.find_or_create_domain("example.org").await; admin .registry_update_object( @@ -222,6 +240,66 @@ pub async fn test() { "test 5: the named user" ); + // http-directory-services-13: a directory saved before the audience was + // required keeps signing people in, and can still be edited without + // one; an audience, once set, stays + let mut legacy = directory(None); + if let structs::Directory::Oidc(oidc) = &mut legacy { + oidc.description = "Keycloak, saved without an audience".to_string(); + oidc.require_audience = None; + } + let legacy = test + .server + .registry() + .write(RegistryWrite::insert(&legacy.into())) + .await + .unwrap() + .unwrap_id(trc::location!()); + admin.reload_settings().await; + admin + .registry_update_object( + ObjectType::Directory, + legacy, + json!({ Property::Description: "Keycloak, still without an audience" }), + ) + .await; + admin + .registry_update_object( + ObjectType::Domain, + domain, + json!({ Property::DirectoryId: legacy.to_string() }), + ) + .await; + let fresh = token("stalwart", "stalwart-secret", JOHN, "openid email profile") + .await + .unwrap(); + assert_eq!( + bearer(&test, None, &fresh).await.unwrap(), + john, + "a directory saved without an audience" + ); + admin + .registry_update_object( + ObjectType::Domain, + domain, + json!({ Property::DirectoryId: keycloak.to_string() }), + ) + .await; + let refused = admin + .registry_update_object_expect_err( + ObjectType::Directory, + keycloak, + json!({ Property::RequireAudience: null }), + ) + .await; + assert!( + refused + .description + .as_deref() + .is_some_and(|reason| reason.contains("audience")), + "removing the audience: {refused:?}" + ); + // Test 17: no password sign-in on an OIDC domain (DIR-29) assert!( server(&test) -- 2.54.0 From 39c5351739e609803e916eaf645885e5738d0a73 Mon Sep 17 00:00:00 2001 From: John Coffey Date: Thu, 29 Oct 2026 02:10:07 -0700 Subject: [PATCH 07/10] OIDC directories: opaque tokens sign in only by the directory's policy A bearer token that isn't a JWT was accepted whenever the provider's user info endpoint answered 200 for it, which it does for any token it issued to any application. So, with no audience or scope check possible on that path, an access token another application at the same provider received for a user signed that user in here. Each OIDC directory now has opaqueTokens: reject, userinfo or introspection. introspection posts the token to the provider's RFC 7662 endpoint (introspectionUrl, or the discovery document's) with HTTP Basic client authentication (introspectionClientId and introspectionClientSecret), and signs in only an active token whose iss is the directory's issuer, whose aud includes requireAudience when set, that hasn't expired, isn't marked as an ID or refresh token and carries the required scopes. A refusal of the server's own credentials is a provider error, not a failed sign-in. Left unset, a directory uses introspection when it has an introspection endpoint and rejects opaque tokens otherwise; saving one that would introspect without a client ID and secret is refused. Directories saved before this read as userinfo, so an upgrade changes nothing until an administrator chooses. The new fields are stored at version 2 of x:Directory and of x:Bootstrap (the two objects an OIDC directory is stored in); older records read without them. The admin console gets the fields, their descriptions and an Opaque Tokens form section from the served schema, and the privacy catalog lists the client secret as a credential. The per-domain directories spec now describes the audience rule and the policy. Test: on main a directory signed in an opaque token that the user info endpoint accepted (unit test: "an opaque token signed in through the user info endpoint"). Now a new directory refuses it, one saved before the policy still takes it, and introspection signs in only an active, same-issuer, right-audience access token (unit tests with a local provider; directory suite against Keycloak, including a refresh token and a token Keycloak never issued; records stored at the old versions read back as userinfo). --- crates/directory/src/backend/oidc/config.rs | 88 +++- crates/directory/src/backend/oidc/lookup.rs | 384 ++++++++++++++++-- crates/directory/src/backend/oidc/mod.rs | 25 ++ crates/jmap/src/inbuxa/oidc_directory.rs | 73 +++- crates/registry/src/inbuxa_tests.rs | 110 +++++ crates/registry/src/lib.rs | 5 + crates/registry/src/schema/enums.rs | 11 + crates/registry/src/schema/enums_impl.rs | 53 +++ crates/registry/src/schema/properties.rs | 7 + crates/registry/src/schema/properties_impl.rs | 16 +- crates/registry/src/schema/structs.rs | 11 + crates/registry/src/schema/structs_impl.rs | 65 ++- docs/spec/features/per-domain-directories.md | 28 +- resources/privacy/catalog.toml | 2 + resources/schema/schema.json.gz | Bin 153685 -> 154196 bytes resources/schema/schema.json.sha256 | 2 +- tests/src/directory/oidc.rs | 131 +++++- tests/src/directory/unavailable.rs | 3 + tests/src/scim/oidc.rs | 3 + 19 files changed, 946 insertions(+), 71 deletions(-) create mode 100644 crates/registry/src/inbuxa_tests.rs diff --git a/crates/directory/src/backend/oidc/config.rs b/crates/directory/src/backend/oidc/config.rs index ae2a876a..8d0a5136 100644 --- a/crates/directory/src/backend/oidc/config.rs +++ b/crates/directory/src/backend/oidc/config.rs @@ -9,10 +9,11 @@ use crate::Directory; use crate::backend::oidc::lookup::fetch_jwks_keys; use crate::backend::oidc::{ - CachedKey, DiscoveryDocument, JwksCache, OidcConfig, OidcDiscovery, OidcError, OpenIdDirectory, + CachedKey, DiscoveryDocument, Introspection, JwksCache, OidcConfig, OidcDiscovery, OidcError, + OpaqueTokens, OpenIdDirectory, }; use ahash::AHashMap; -use registry::schema::structs; +use registry::schema::{enums::OidcOpaqueTokens, structs}; use std::sync::Arc; use std::time::{Duration, Instant}; use tokio::sync::RwLock; @@ -22,6 +23,20 @@ use utils::Client; const DISCOVERY_RETRY_FOR: Duration = Duration::from_secs(30); const DISCOVERY_RETRY_INTERVAL: Duration = Duration::from_secs(3); +/// inbuxa: http-directory-services-13: what a bearer token that isn't a JWT +/// signs in by. Unset, as on a new directory: introspection when an +/// introspection endpoint is set, else nothing. A directory saved before the +/// setting existed reads as userinfo, which it always used. +pub fn opaque_token_policy(config: &structs::OidcDirectory) -> OidcOpaqueTokens { + config + .opaque_tokens + .unwrap_or(if config.introspection_url.is_some() { + OidcOpaqueTokens::Introspection + } else { + OidcOpaqueTokens::Reject + }) +} + impl OpenIdDirectory { pub async fn open(config: structs::OidcDirectory) -> Result { // inbuxa: http-directory-services-13: only a directory saved before @@ -38,6 +53,27 @@ impl OpenIdDirectory { ); } + let opaque_tokens = match opaque_token_policy(&config) { + OidcOpaqueTokens::Reject => OpaqueTokens::Reject, + OidcOpaqueTokens::Userinfo => OpaqueTokens::Userinfo, + OidcOpaqueTokens::Introspection => OpaqueTokens::Introspection(Introspection { + url: config.introspection_url, + client_id: config.introspection_client_id.ok_or_else(|| { + "Opaque tokens are checked by introspection, which needs a client ID" + .to_string() + })?, + client_secret: config + .introspection_client_secret + .secret() + .await? + .map(|secret| secret.into_owned()) + .ok_or_else(|| { + "Opaque tokens are checked by introspection, which needs a client secret" + .to_string() + })?, + }), + }; + Self::new(OidcConfig { issue_url: config.issuer_url, require_aud: config.require_audience, @@ -46,13 +82,14 @@ impl OpenIdDirectory { claim_name: config.claim_name, claim_groups: config.claim_groups, default_domain: config.username_domain, + opaque_tokens, }) .await .map(Directory::OpenId) .map_err(|err| err.to_string()) } - pub async fn new(config: OidcConfig) -> Result { + pub async fn new(mut config: OidcConfig) -> Result { let http = utils::http::http_client_builder(false) .user_agent("inbuxa/1.0") // types::brand!(); this crate does not depend on types .timeout(Duration::from_secs(30)) @@ -78,6 +115,19 @@ impl OpenIdDirectory { } }; + // inbuxa: http-directory-services-13 + if let OpaqueTokens::Introspection(introspection) = &mut config.opaque_tokens + && introspection.url.is_none() + { + introspection.url = Some(document.introspection_endpoint.clone().ok_or_else(|| { + OidcError::Config( + "Opaque tokens are checked by introspection, but no introspection endpoint \ + is set and the provider's discovery document names none" + .to_string(), + ) + })?); + } + Ok(Self { discovery: OidcDiscovery { url: config.issue_url.clone(), @@ -191,3 +241,35 @@ impl OpenIdDirectory { Ok((discovery, keys)) } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn new_directories_refuse_opaque_tokens_unless_they_name_an_endpoint() { + let new = structs::OidcDirectory::default(); + assert_eq!(opaque_token_policy(&new), OidcOpaqueTokens::Reject); + assert_eq!( + opaque_token_policy(&structs::OidcDirectory { + introspection_url: Some("https://idp.example.org/introspect".to_string()), + ..new.clone() + }), + OidcOpaqueTokens::Introspection + ); + for chosen in [ + OidcOpaqueTokens::Reject, + OidcOpaqueTokens::Userinfo, + OidcOpaqueTokens::Introspection, + ] { + assert_eq!( + opaque_token_policy(&structs::OidcDirectory { + opaque_tokens: Some(chosen), + introspection_url: Some("https://idp.example.org/introspect".to_string()), + ..new.clone() + }), + chosen + ); + } + } +} diff --git a/crates/directory/src/backend/oidc/lookup.rs b/crates/directory/src/backend/oidc/lookup.rs index 1f78b4fb..c9e471f3 100644 --- a/crates/directory/src/backend/oidc/lookup.rs +++ b/crates/directory/src/backend/oidc/lookup.rs @@ -8,7 +8,7 @@ use crate::{ Account, Credentials, - backend::oidc::{CachedKey, OidcError, OpenIdDirectory}, + backend::oidc::{CachedKey, Introspection, OidcError, OpaqueTokens, OpenIdDirectory}, }; use ahash::AHashMap; use jsonwebtoken::{ @@ -87,37 +87,7 @@ impl OpenIdDirectory { check_token_type(&token_data.claims)?; self.validate_scopes(&token_data.claims)?; - let mut claims = token_data.claims; - let missing_username = - is_claim_missing(&claims, Some(&self.config.claim_email)); - let jwt_email = self.resolve_email(&claims).ok(); - let missing_profile = - is_claim_missing(&claims, self.config.claim_name.as_ref()) - || is_claim_missing(&claims, self.config.claim_groups.as_ref()); - - if jwt_email.is_none() || missing_username || missing_profile { - match self.fetch_userinfo(token).await { - Ok(userinfo) => { - if let (Some(base), Value::Object(extra)) = - (claims.as_object_mut(), userinfo) - { - for (key, value) in extra { - if base.get(&key).is_none_or(Value::is_null) { - base.insert(key, value); - } - } - } - } - Err(err) if jwt_email.is_none() => return Err(err), - Err(_) => {} - } - } - - let email = match jwt_email { - Some(email) if !missing_username => email, - _ => self.resolve_email(&claims)?, - }; - return self.build_account(email, &claims); + return self.account_from_claims(token, token_data.claims).await; } Err(e) => { last_err = Some(e); @@ -131,9 +101,146 @@ impl OpenIdDirectory { ))) } + /// inbuxa: the account a validated token's claims name, with what they + /// lack taken from the user info endpoint (shared by JWTs and + /// introspected tokens). + async fn account_from_claims( + &self, + token: &str, + mut claims: serde_json::Value, + ) -> Result { + let missing_username = is_claim_missing(&claims, Some(&self.config.claim_email)); + let jwt_email = self.resolve_email(&claims).ok(); + let missing_profile = is_claim_missing(&claims, self.config.claim_name.as_ref()) + || is_claim_missing(&claims, self.config.claim_groups.as_ref()); + + if jwt_email.is_none() || missing_username || missing_profile { + match self.fetch_userinfo(token).await { + Ok(userinfo) => { + if let (Some(base), Value::Object(extra)) = (claims.as_object_mut(), userinfo) { + for (key, value) in extra { + if base.get(&key).is_none_or(Value::is_null) { + base.insert(key, value); + } + } + } + } + Err(err) if jwt_email.is_none() => return Err(err), + Err(_) => {} + } + } + + let email = match jwt_email { + Some(email) if !missing_username => email, + _ => self.resolve_email(&claims)?, + }; + self.build_account(email, &claims) + } + async fn authenticate_opaque(&self, token: &str) -> Result { - let claims = self.fetch_userinfo(token).await?; - self.build_account(self.resolve_email(&claims)?, &claims) + // inbuxa: http-directory-services-13: by the directory's policy + match &self.config.opaque_tokens { + OpaqueTokens::Reject => Err(OidcError::TokenValidation( + "This directory accepts only JWT access tokens".to_string(), + )), + OpaqueTokens::Userinfo => { + let claims = self.fetch_userinfo(token).await?; + self.build_account(self.resolve_email(&claims)?, &claims) + } + OpaqueTokens::Introspection(introspection) => { + let claims = self.introspect(token, introspection).await?; + check_token_type(&claims)?; + self.validate_scopes(&claims)?; + self.account_from_claims(token, claims).await + } + } + } + + /// inbuxa: http-directory-services-13: asks the provider about a token + /// (RFC 7662), and returns what it says only for an active token it + /// issued, for the required audience when one is set. + async fn introspect( + &self, + token: &str, + introspection: &Introspection, + ) -> Result { + let url = introspection + .url + .as_deref() + .ok_or_else(|| OidcError::Config("No introspection endpoint is known".to_string()))?; + let resp = self + .http + .post(url) + // RFC 6749, section 2.3.1: both form-encoded first + .basic_auth( + form_encode(&introspection.client_id), + Some(form_encode(&introspection.client_secret)), + ) + .header( + reqwest::header::CONTENT_TYPE, + "application/x-www-form-urlencoded", + ) + .body(format!( + "token={}&token_type_hint=access_token", + form_encode(token) + )) + .send() + .await + .map_err(|e| OidcError::Network(format!("Introspection request failed: {e}")))?; + + // A refusal here is of this server's client credentials, not the token + let status = resp.status(); + if !status.is_success() { + return Err(OidcError::Provider(format!( + "Introspection returned HTTP {status}" + ))); + } + let bytes = resp + .bytes() + .await + .map_err(|e| OidcError::Provider(format!("Introspection HTTP error: {e}")))?; + let claims = serde_json::from_slice::(&bytes) + .map_err(|e| OidcError::Provider(format!("Introspection JSON parse error: {e}")))?; + + if claims.get("active") != Some(&Value::Bool(true)) { + return Err(OidcError::AuthorizationFailed( + "The provider says the token is not active".to_string(), + )); + } + let issuer = self.discovery.document.issuer.trim_end_matches('/'); + if claims + .get("iss") + .and_then(|iss| iss.as_str()) + .is_none_or(|iss| iss.trim_end_matches('/') != issuer) + { + return Err(OidcError::TokenValidation( + "The token was not issued by this directory's provider".to_string(), + )); + } + if let Some(audience) = &self.config.require_aud { + let found = match claims.get("aud") { + Some(Value::String(aud)) => aud == audience, + Some(Value::Array(auds)) => auds.iter().any(|aud| aud.as_str() == Some(audience)), + _ => false, + }; + if !found { + return Err(OidcError::TokenValidation( + "The token was not issued for the required audience".to_string(), + )); + } + } + if let Some(exp) = claims.get("exp").and_then(|exp| exp.as_u64()) + && exp + 60 + < std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .map_or(0, |now| now.as_secs()) + { + return Err(OidcError::TokenValidation( + "The token has expired".to_string(), + )); + } + + Ok(claims) } async fn get_key(&self, kid: Option<&str>) -> Result>, OidcError> { @@ -409,6 +516,21 @@ pub(super) async fn fetch_jwks_keys( Ok(map) } +/// inbuxa: application/x-www-form-urlencoded, for the introspection request. +fn form_encode(value: &str) -> String { + let mut out = String::with_capacity(value.len()); + for byte in value.bytes() { + match byte { + b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'*' | b'-' | b'.' | b'_' => { + out.push(byte as char) + } + b' ' => out.push('+'), + _ => out.push_str(&format!("%{byte:02X}")), + } + } + out +} + fn extract_scopes(claims: &serde_json::Value) -> Vec { match claims.get("scope") { Some(serde_json::Value::String(s)) => s.split_whitespace().map(|s| s.to_string()).collect(), @@ -482,6 +604,19 @@ MCowBQYDK2VwAyEAXNXjqSjw780GFXxFztdFBUMpxRoFqsYWN55FXnvncCE= -----END PUBLIC KEY-----"; fn test_directory() -> OpenIdDirectory { + test_directory_at("https://idp.example.org") + } + + /// The test directory, with its endpoints at `base`. + fn test_directory_at(base: &str) -> OpenIdDirectory { + test_directory_with(base, None, OpaqueTokens::Reject) + } + + fn test_directory_with( + base: &str, + require_aud: Option<&str>, + opaque_tokens: OpaqueTokens, + ) -> OpenIdDirectory { let mut keys = AHashMap::new(); keys.insert( "test".to_string(), @@ -493,22 +628,24 @@ MCowBQYDK2VwAyEAXNXjqSjw780GFXxFztdFBUMpxRoFqsYWN55FXnvncCE= OpenIdDirectory { config: crate::backend::oidc::OidcConfig { issue_url: "https://idp.example.org".to_string(), - require_aud: None, + require_aud: require_aud.map(str::to_string), require_scopes: Vec::new(), claim_email: "email".to_string(), claim_name: None, claim_groups: None, default_domain: None, + opaque_tokens, }, discovery: crate::backend::oidc::OidcDiscovery { url: "https://idp.example.org".to_string(), document: crate::backend::oidc::DiscoveryDocument { issuer: "https://idp.example.org".to_string(), - jwks_uri: "https://idp.example.org/jwks".to_string(), - userinfo_endpoint: "https://idp.example.org/userinfo".to_string(), - token_endpoint: "https://idp.example.org/token".to_string(), - authorization_endpoint: "https://idp.example.org/auth".to_string(), + jwks_uri: format!("{base}/jwks"), + userinfo_endpoint: format!("{base}/userinfo"), + token_endpoint: format!("{base}/token"), + authorization_endpoint: format!("{base}/auth"), end_session_endpoint: None, + introspection_endpoint: None, scopes_supported: None, claims_supported: None, code_challenge_methods_supported: None, @@ -543,6 +680,52 @@ MCowBQYDK2VwAyEAXNXjqSjw780GFXxFztdFBUMpxRoFqsYWN55FXnvncCE= .unwrap() } + /// A provider on 127.0.0.1 that answers every request with what + /// `answer` returns for it (the request line, headers and body). + async fn provider(answer: impl Fn(&str) -> (u16, String) + Send + Sync + 'static) -> String { + use tokio::io::{AsyncReadExt, AsyncWriteExt}; + + let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap(); + let base = format!("http://{}", listener.local_addr().unwrap()); + let answer = Arc::new(answer); + tokio::spawn(async move { + while let Ok((mut stream, _)) = listener.accept().await { + let answer = answer.clone(); + tokio::spawn(async move { + let mut request = Vec::new(); + let mut buf = [0u8; 4096]; + loop { + let read = stream.read(&mut buf).await.unwrap_or(0); + if read == 0 { + break; + } + request.extend_from_slice(&buf[..read]); + if let Some(end) = request.windows(4).position(|w| w == b"\r\n\r\n") { + let head = String::from_utf8_lossy(&request[..end]).to_lowercase(); + let length = head + .lines() + .find_map(|line| line.strip_prefix("content-length:")) + .and_then(|value| value.trim().parse::().ok()) + .unwrap_or(0); + if request.len() >= end + 4 + length { + break; + } + } + } + let (status, body) = answer(&String::from_utf8_lossy(&request)); + // No Content-Length: the body ends when the connection does + let head = format!( + "HTTP/1.1 {status} Status\r\nContent-Type: application/json\r\nConnection: close\r\n\r\n" + ); + let _ = stream.write_all(head.as_bytes()).await; + let _ = stream.write_all(body.as_bytes()).await; + let _ = stream.shutdown().await; + }); + } + }); + base + } + async fn sign_in(directory: &OpenIdDirectory, token: String) -> trc::Result { directory .authenticate(&Credentials::Bearer { @@ -587,6 +770,129 @@ MCowBQYDK2VwAyEAXNXjqSjw780GFXxFztdFBUMpxRoFqsYWN55FXnvncCE= } } + // http-directory-services-13: any token the provider issued, to any + // application, passes its user info endpoint, so a new directory + // refuses tokens that aren't JWTs + #[tokio::test] + async fn new_directories_refuse_opaque_tokens() { + let base = provider(|_| (200, json!({"email": "john@example.org"}).to_string())).await; + let directory = test_directory_at(&base); + assert!( + sign_in(&directory, "an-opaque-token".to_string()) + .await + .is_err(), + "an opaque token signed in through the user info endpoint" + ); + + // ... and a directory saved before the policy keeps doing so + let directory = test_directory_with(&base, None, OpaqueTokens::Userinfo); + assert_eq!( + sign_in(&directory, "an-opaque-token".to_string()) + .await + .map(|account| account.email) + .ok() + .as_deref(), + Some("john@example.org") + ); + } + + // http-directory-services-13: introspection signs in only an active + // token this provider issued for the required audience + #[tokio::test] + async fn introspection_checks_what_the_provider_says() { + let base = provider(|request| { + let (head, body) = request.split_once("\r\n\r\n").unwrap_or_default(); + // RFC 6749 2.3.1: id and secret form-encoded, then Basic + let authorized = head.lines().any(|line| { + line.eq_ignore_ascii_case(&format!( + "authorization: Basic {}", + base64::Engine::encode( + &base64::engine::general_purpose::STANDARD, + "inbuxa:se+cret%2F%2B" + ) + )) + }); + if !head.starts_with("POST /introspect ") || !authorized { + return (401, json!({"error": "invalid_client"}).to_string()); + } + let now = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap() + .as_secs(); + let mut claims = json!({ + "active": true, + "iss": "https://idp.example.org", + "aud": ["inbuxa", "account"], + "exp": now + 300, + "email": "john@example.org", + }); + match body + .strip_prefix("token=") + .and_then(|body| body.split('&').next()) + .unwrap_or_default() + { + "good%2Btoken" => {} + "inactive" => claims = json!({"active": false}), + "foreign" => claims["iss"] = json!("https://other.example.org"), + "anonymous" => { + claims.as_object_mut().unwrap().remove("iss"); + } + "elsewhere" => claims["aud"] = json!("another-app"), + "unaddressed" => { + claims.as_object_mut().unwrap().remove("aud"); + } + "refresh" => claims["typ"] = json!("Refresh"), + "expired" => claims["exp"] = json!(1), + _ => claims = json!({"active": "true"}), + } + (200, claims.to_string()) + }) + .await; + let introspection = |client_secret: &str| { + OpaqueTokens::Introspection(Introspection { + url: Some(format!("{base}/introspect")), + client_id: "inbuxa".to_string(), + client_secret: client_secret.to_string(), + }) + }; + let directory = test_directory_with(&base, Some("inbuxa"), introspection("se cret/+")); + + assert_eq!( + sign_in(&directory, "good+token".to_string()) + .await + .map(|account| account.email) + .ok() + .as_deref(), + Some("john@example.org") + ); + for token in [ + "inactive", + "foreign", + "anonymous", + "elsewhere", + "unaddressed", + "refresh", + "expired", + "not-a-boolean", + ] { + let err = sign_in(&directory, token.to_string()).await.unwrap_err(); + assert!( + err.matches(trc::EventType::Auth(AuthEvent::Failed)), + "{token}: {err:?}" + ); + } + + // A refusal of the server's own credentials is not the person's fault + let directory = test_directory_with(&base, Some("inbuxa"), introspection("wrong")); + let err = sign_in(&directory, "good+token".to_string()) + .await + .unwrap_err(); + assert!( + err.matches(trc::EventType::Auth(AuthEvent::Error)), + "{err:?}" + ); + } + #[test] fn extract_string_list_distinguishes_absent_from_empty() { assert_eq!( diff --git a/crates/directory/src/backend/oidc/mod.rs b/crates/directory/src/backend/oidc/mod.rs index 0ddb9310..03338c54 100644 --- a/crates/directory/src/backend/oidc/mod.rs +++ b/crates/directory/src/backend/oidc/mod.rs @@ -2,6 +2,8 @@ * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC * * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL + * + * Modified by Coffey Labs in 2026 for INBUXA. */ use ahash::AHashMap; @@ -22,6 +24,26 @@ pub struct OidcConfig { pub claim_name: Option, pub claim_groups: Option, pub default_domain: Option, + pub opaque_tokens: OpaqueTokens, +} + +/// inbuxa: http-directory-services-13: what a bearer token that isn't a +/// JWT signs in by. +pub enum OpaqueTokens { + /// Nothing: refused. + Reject, + /// The provider's user info endpoint answering for it, which it does for + /// any token it issued, to any application. + Userinfo, + /// The provider's token introspection endpoint (RFC 7662). + Introspection(Introspection), +} + +pub struct Introspection { + /// None until discovery: then the discovery document's, if not set. + pub url: Option, + pub client_id: String, + pub client_secret: String, } pub struct OidcDiscovery { @@ -38,6 +60,9 @@ pub struct DiscoveryDocument { pub authorization_endpoint: String, #[serde(skip_serializing_if = "Option::is_none")] pub end_session_endpoint: Option, + // inbuxa: read for opaque tokens, not passed on to clients + #[serde(default, skip_serializing)] + pub introspection_endpoint: Option, #[serde(skip_serializing_if = "Option::is_none")] pub scopes_supported: Option>, #[serde(skip_serializing_if = "Option::is_none")] diff --git a/crates/jmap/src/inbuxa/oidc_directory.rs b/crates/jmap/src/inbuxa/oidc_directory.rs index 40f1f50d..8ff8375c 100644 --- a/crates/jmap/src/inbuxa/oidc_directory.rs +++ b/crates/jmap/src/inbuxa/oidc_directory.rs @@ -14,10 +14,18 @@ //! one and logs a warning each time it loads; it can still be edited, but an //! audience it has can't be removed. Requiring it of every directory is left //! to a later release. +//! +//! A directory that checks opaque tokens by introspection names the client +//! ID and secret it calls the provider with. +use directory::backend::oidc::config::opaque_token_policy; use jmap_proto::error::set::{SetError, SetErrorType}; use registry::{ - schema::{prelude::Property, structs::Directory}, + schema::{ + enums::OidcOpaqueTokens, + prelude::Property, + structs::{Directory, SecretKeyOptional}, + }, types::error::ValidationError, }; @@ -42,6 +50,29 @@ pub fn validate( other application would sign in.", )); } + + if opaque_token_policy(directory) == OidcOpaqueTokens::Introspection { + let mut missing = Vec::new(); + if directory.introspection_client_id.is_none() { + missing.push(ValidationError::required(Property::IntrospectionClientId)); + } + if matches!( + directory.introspection_client_secret, + SecretKeyOptional::None + ) { + missing.push(ValidationError::required( + Property::IntrospectionClientSecret, + )); + } + if !missing.is_empty() { + return Err(SetError::new(SetErrorType::ValidationFailed) + .with_validation_errors(missing) + .with_description( + "Opaque tokens are checked at the introspection endpoint, which needs the \ + client ID and secret this server signs in to the provider with.", + )); + } + } Ok(()) } @@ -72,4 +103,44 @@ mod tests { // ... but an audience, once set, stays assert!(validate(&oidc(None), Some(&oidc(Some("inbuxa")))).is_err()); } + + #[test] + fn introspection_names_its_client() { + let with = |policy: Option, url: bool, client: bool| { + Directory::Oidc(OidcDirectory { + require_audience: Some("inbuxa".to_string()), + opaque_tokens: policy, + introspection_url: url.then(|| "https://idp.example.org/introspect".to_string()), + introspection_client_id: client.then(|| "inbuxa".to_string()), + introspection_client_secret: if client { + SecretKeyOptional::Value(registry::schema::structs::SecretKeyValue { + secret: "secret".to_string(), + }) + } else { + SecretKeyOptional::None + }, + ..Default::default() + }) + }; + + assert!(validate(&with(None, false, false), None).is_ok()); + assert!(validate(&with(None, true, false), None).is_err()); + assert!(validate(&with(None, true, true), None).is_ok()); + assert!( + validate( + &with(Some(OidcOpaqueTokens::Introspection), false, false), + None + ) + .is_err() + ); + assert!( + validate( + &with(Some(OidcOpaqueTokens::Introspection), false, true), + None + ) + .is_ok() + ); + assert!(validate(&with(Some(OidcOpaqueTokens::Reject), true, false), None).is_ok()); + assert!(validate(&with(Some(OidcOpaqueTokens::Userinfo), false, false), None).is_ok()); + } } diff --git a/crates/registry/src/inbuxa_tests.rs b/crates/registry/src/inbuxa_tests.rs new file mode 100644 index 00000000..8ea4807a --- /dev/null +++ b/crates/registry/src/inbuxa_tests.rs @@ -0,0 +1,110 @@ +/* + * SPDX-FileCopyrightText: 2026 Coffey Labs LLC + * + * SPDX-License-Identifier: AGPL-3.0-only + */ + +//! Stored formats inbuxa changed. + +use crate::{ + pickle::{Pickle, PickledStream}, + schema::{ + enums::OidcOpaqueTokens, + prelude::{ObjectImpl, ObjectInner, ObjectType}, + structs::{Bootstrap, Directory, DirectoryBootstrap, OidcDirectory}, + }, +}; + +/// An OIDC directory as it was stored before the opaque-token fields. +fn pickle_without_opaque_tokens(directory: &OidcDirectory, out: &mut Vec) { + directory.description.pickle(out); + directory.issuer_url.pickle(out); + directory.require_audience.pickle(out); + directory.require_scopes.pickle(out); + directory.claim_username.pickle(out); + directory.username_domain.pickle(out); + directory.claim_name.pickle(out); + directory.claim_groups.pickle(out); + directory.member_tenant_id.pickle(out); +} + +fn read(object: ObjectType, bytes: &[u8]) -> ObjectInner { + ObjectInner::unpickle(object, &mut PickledStream::new(bytes).unwrap()).unwrap() +} + +// http-directory-services-13: a directory saved before the opaque-token +// policy existed keeps today's behavior (userinfo); a new one starts unset +#[test] +fn oidc_directories_saved_before_the_opaque_token_policy_keep_userinfo() { + let saved = OidcDirectory { + description: "Keycloak".to_string(), + issuer_url: "https://sso.example.org/realms/example".to_string(), + require_audience: Some("inbuxa".to_string()), + ..Default::default() + }; + let as_read = OidcDirectory { + opaque_tokens: Some(OidcOpaqueTokens::Userinfo), + ..saved.clone() + }; + + // x:Directory, version 0 + let mut bytes = vec![0u8]; + 2u16.pickle(&mut bytes); + pickle_without_opaque_tokens(&saved, &mut bytes); + assert_eq!( + read(ObjectType::Directory, &bytes), + ObjectInner::Directory(Directory::Oidc(as_read.clone())) + ); + + // x:Bootstrap, version 1, where more fields follow the directory + let bootstrap = Bootstrap { + server_hostname: "mx.example.org".to_string(), + directory: DirectoryBootstrap::Oidc(saved.clone()), + ..Default::default() + }; + let mut bytes = vec![1u8]; + bootstrap.server_hostname.pickle(&mut bytes); + bootstrap.default_domain.pickle(&mut bytes); + bootstrap.request_tls_certificate.pickle(&mut bytes); + bootstrap.generate_dkim_keys.pickle(&mut bytes); + bootstrap.data_store.pickle(&mut bytes); + bootstrap.blob_store.pickle(&mut bytes); + bootstrap.search_store.pickle(&mut bytes); + bootstrap.in_memory_store.pickle(&mut bytes); + 3u16.pickle(&mut bytes); + pickle_without_opaque_tokens(&saved, &mut bytes); + bootstrap.tracer.pickle(&mut bytes); + bootstrap.dns_server.pickle(&mut bytes); + assert_eq!( + read(ObjectType::Bootstrap, &bytes), + ObjectInner::Bootstrap(Bootstrap { + directory: DirectoryBootstrap::Oidc(as_read), + ..bootstrap.clone() + }) + ); + + // Saved now, unset or not, both read back as they were + for directory in [ + saved.clone(), + OidcDirectory { + opaque_tokens: Some(OidcOpaqueTokens::Introspection), + introspection_url: Some("https://sso.example.org/introspect".to_string()), + introspection_client_id: Some("inbuxa".to_string()), + ..saved.clone() + }, + ] { + let bootstrap = Bootstrap { + directory: DirectoryBootstrap::Oidc(directory.clone()), + ..bootstrap.clone() + }; + let directory = Directory::Oidc(directory); + assert_eq!( + read(ObjectType::Directory, &directory.to_pickled_vec()), + ObjectInner::Directory(directory) + ); + assert_eq!( + read(ObjectType::Bootstrap, &bootstrap.to_pickled_vec()), + ObjectInner::Bootstrap(bootstrap) + ); + } +} diff --git a/crates/registry/src/lib.rs b/crates/registry/src/lib.rs index 0772aef0..51cafd4a 100644 --- a/crates/registry/src/lib.rs +++ b/crates/registry/src/lib.rs @@ -2,6 +2,8 @@ * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC * * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL + * + * Modified by Coffey Labs in 2026 for INBUXA. */ #![warn(clippy::large_futures)] @@ -11,3 +13,6 @@ pub mod pickle; pub mod schema; pub mod types; pub mod utils; + +#[cfg(test)] +mod inbuxa_tests; diff --git a/crates/registry/src/schema/enums.rs b/crates/registry/src/schema/enums.rs index 758a586d..0552e0cc 100644 --- a/crates/registry/src/schema/enums.rs +++ b/crates/registry/src/schema/enums.rs @@ -1472,6 +1472,17 @@ pub enum NetworkListenerProtocol { ManageSieve = 5, } +// inbuxa: http-directory-services-13: what an OIDC directory does with a +// bearer token that isn't a JWT +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash)] +#[repr(u16)] +pub enum OidcOpaqueTokens { + #[default] + Reject = 0, + Userinfo = 1, + Introspection = 2, +} + #[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash)] #[repr(u16)] pub enum OvhEndpoint { diff --git a/crates/registry/src/schema/enums_impl.rs b/crates/registry/src/schema/enums_impl.rs index 11904695..0a2716e1 100644 --- a/crates/registry/src/schema/enums_impl.rs +++ b/crates/registry/src/schema/enums_impl.rs @@ -6675,6 +6675,59 @@ impl<'de> serde::Deserialize<'de> for NetworkListenerProtocol { } } +impl EnumImpl for OidcOpaqueTokens { + fn parse(value: &str) -> Option { + hashify::tiny_map! { + value.as_bytes(), + b"reject" => OidcOpaqueTokens::Reject, + b"userinfo" => OidcOpaqueTokens::Userinfo, + b"introspection" => OidcOpaqueTokens::Introspection, + } + } + + fn as_str(&self) -> &'static str { + match self { + OidcOpaqueTokens::Reject => "reject", + OidcOpaqueTokens::Userinfo => "userinfo", + OidcOpaqueTokens::Introspection => "introspection", + } + } + + fn to_id(&self) -> u16 { + *self as u16 + } + + fn from_id(id: u16) -> Option { + match id { + 0 => Some(OidcOpaqueTokens::Reject), + 1 => Some(OidcOpaqueTokens::Userinfo), + 2 => Some(OidcOpaqueTokens::Introspection), + _ => None, + } + } + + const COUNT: usize = 3; +} + +impl serde::Serialize for OidcOpaqueTokens { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + serializer.serialize_str(self.as_str()) + } +} + +impl<'de> serde::Deserialize<'de> for OidcOpaqueTokens { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + let s = Cow::::deserialize(deserializer)?; + Self::parse(&s).ok_or_else(|| serde::de::Error::unknown_variant(&s, &[])) + } +} + impl EnumImpl for OvhEndpoint { fn parse(value: &str) -> Option { hashify::tiny_map! { diff --git a/crates/registry/src/schema/properties.rs b/crates/registry/src/schema/properties.rs index e7469dfa..529c205c 100644 --- a/crates/registry/src/schema/properties.rs +++ b/crates/registry/src/schema/properties.rs @@ -2,6 +2,8 @@ * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC * * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL + * + * Modified by Coffey Labs in 2026 for INBUXA. */ // This file is auto-generated. Do not edit directly. @@ -982,6 +984,11 @@ pub enum Property { RequestTlsCertificate = 123, Require = 551, RequireAudience = 607, + // inbuxa: http-directory-services-13 + OpaqueTokens = 935, + IntrospectionUrl = 936, + IntrospectionClientId = 937, + IntrospectionClientSecret = 938, RequireClientRegistration = 615, RequireScopes = 608, RequireTls = 525, diff --git a/crates/registry/src/schema/properties_impl.rs b/crates/registry/src/schema/properties_impl.rs index 461418df..0f53ecb0 100644 --- a/crates/registry/src/schema/properties_impl.rs +++ b/crates/registry/src/schema/properties_impl.rs @@ -2,6 +2,8 @@ * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC * * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL + * + * Modified by Coffey Labs in 2026 for INBUXA. */ // This file is auto-generated. Do not edit directly. @@ -1135,6 +1137,10 @@ impl EnumImpl for Property { b"requestTlsCertificate" => Property::RequestTlsCertificate, b"require" => Property::Require, b"requireAudience" => Property::RequireAudience, + b"opaqueTokens" => Property::OpaqueTokens, + b"introspectionUrl" => Property::IntrospectionUrl, + b"introspectionClientId" => Property::IntrospectionClientId, + b"introspectionClientSecret" => Property::IntrospectionClientSecret, b"requireClientRegistration" => Property::RequireClientRegistration, b"requireScopes" => Property::RequireScopes, b"requireTls" => Property::RequireTls, @@ -2075,6 +2081,10 @@ impl EnumImpl for Property { Property::RequestTlsCertificate => "requestTlsCertificate", Property::Require => "require", Property::RequireAudience => "requireAudience", + Property::OpaqueTokens => "opaqueTokens", + Property::IntrospectionUrl => "introspectionUrl", + Property::IntrospectionClientId => "introspectionClientId", + Property::IntrospectionClientSecret => "introspectionClientSecret", Property::RequireClientRegistration => "requireClientRegistration", Property::RequireScopes => "requireScopes", Property::RequireTls => "requireTls", @@ -3019,6 +3029,10 @@ impl EnumImpl for Property { 123 => Some(Property::RequestTlsCertificate), 551 => Some(Property::Require), 607 => Some(Property::RequireAudience), + 935 => Some(Property::OpaqueTokens), + 936 => Some(Property::IntrospectionUrl), + 937 => Some(Property::IntrospectionClientId), + 938 => Some(Property::IntrospectionClientSecret), 615 => Some(Property::RequireClientRegistration), 608 => Some(Property::RequireScopes), 525 => Some(Property::RequireTls), @@ -3230,7 +3244,7 @@ impl EnumImpl for Property { } } - const COUNT: usize = 935; + const COUNT: usize = 939; } impl serde::Serialize for Property { diff --git a/crates/registry/src/schema/structs.rs b/crates/registry/src/schema/structs.rs index 7066a572..acf2361d 100644 --- a/crates/registry/src/schema/structs.rs +++ b/crates/registry/src/schema/structs.rs @@ -2,6 +2,8 @@ * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC * * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL + * + * Modified by Coffey Labs in 2026 for INBUXA. */ // This file is auto-generated. Do not edit directly. @@ -4142,6 +4144,15 @@ pub struct OidcDirectory { pub claim_groups: Option, #[serde(rename = "memberTenantId")] pub member_tenant_id: Option, + // inbuxa: http-directory-services-13 + #[serde(rename = "opaqueTokens")] + pub opaque_tokens: Option, + #[serde(rename = "introspectionUrl")] + pub introspection_url: Option, + #[serde(rename = "introspectionClientId")] + pub introspection_client_id: Option, + #[serde(rename = "introspectionClientSecret")] + pub introspection_client_secret: SecretKeyOptional, } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] diff --git a/crates/registry/src/schema/structs_impl.rs b/crates/registry/src/schema/structs_impl.rs index 96704b7e..02c72ef8 100644 --- a/crates/registry/src/schema/structs_impl.rs +++ b/crates/registry/src/schema/structs_impl.rs @@ -4069,7 +4069,9 @@ impl RegistryJsonPropertyPatch for BlockedIp { impl ObjectImpl for Bootstrap { const FLAGS: u64 = OBJ_SINGLETON; - const VERSION: u8 = 1; + // inbuxa: 2, as x:Directory: OidcDirectory gained fields at version 2 + // of both + const VERSION: u8 = 2; const OBJECT: ObjectType = ObjectType::Bootstrap; fn validate(&self, errors: &mut Vec) -> bool { @@ -6990,7 +6992,9 @@ impl RegistryJsonPropertyPatch for DeliveryError { impl ObjectImpl for Directory { const FLAGS: u64 = OBJ_FILTER_TENANT; - const VERSION: u8 = 0; + // inbuxa: 2, as x:Bootstrap: OidcDirectory gained fields at version 2 + // of both + const VERSION: u8 = 2; const OBJECT: ObjectType = ObjectType::Directory; fn validate(&self, errors: &mut Vec) -> bool { @@ -30639,6 +30643,18 @@ impl OidcDirectory { errors.push(ValidationError::required(Property::MemberTenantId)); } } + if let Some(value) = &self.introspection_url { + if value.is_empty() { + errors.push(ValidationError::required(Property::IntrospectionUrl)); + } + } + if let Some(value) = &self.introspection_client_id { + if value.is_empty() { + errors.push(ValidationError::required(Property::IntrospectionClientId)); + } + } + let value = &self.introspection_client_secret; + value.validate(errors); errors.len() == neb } @@ -30661,6 +30677,10 @@ impl Pickle for OidcDirectory { self.claim_name.pickle(out); self.claim_groups.pickle(out); self.member_tenant_id.pickle(out); + self.opaque_tokens.pickle(out); + self.introspection_url.pickle(out); + self.introspection_client_id.pickle(out); + self.introspection_client_secret.pickle(out); } fn unpickle(stream: &mut crate::pickle::PickledStream<'_>) -> Option { @@ -30674,6 +30694,18 @@ impl Pickle for OidcDirectory { this.claim_name = Pickle::unpickle(stream)?; this.claim_groups = Pickle::unpickle(stream)?; this.member_tenant_id = Pickle::unpickle(stream)?; + // inbuxa: http-directory-services-13: version 2 of both x:Directory + // and x:Bootstrap, the two objects this is stored in. A directory + // saved before keeps accepting opaque tokens its user info endpoint + // accepts, as it always did; a new one refuses them by default. + if stream.version() >= 2 { + this.opaque_tokens = Pickle::unpickle(stream)?; + this.introspection_url = Pickle::unpickle(stream)?; + this.introspection_client_id = Pickle::unpickle(stream)?; + this.introspection_client_secret = Pickle::unpickle(stream)?; + } else { + this.opaque_tokens = Some(OidcOpaqueTokens::Userinfo); + } Some(this) } } @@ -30690,13 +30722,17 @@ impl Default for OidcDirectory { claim_name: Some("name".to_string()), claim_groups: Default::default(), member_tenant_id: Default::default(), + opaque_tokens: Default::default(), + introspection_url: Default::default(), + introspection_client_id: Default::default(), + introspection_client_secret: Default::default(), } } } impl IntoValue for OidcDirectory { fn into_value(self) -> JmapValue<'static> { - let mut map = jmap_tools::Map::with_capacity(11); + let mut map = jmap_tools::Map::with_capacity(15); map.insert_unchecked(Property::Description, self.description.into_value()); map.insert_unchecked(Property::IssuerUrl, self.issuer_url.into_value()); map.insert_unchecked( @@ -30709,6 +30745,19 @@ impl IntoValue for OidcDirectory { map.insert_unchecked(Property::ClaimName, self.claim_name.into_value()); map.insert_unchecked(Property::ClaimGroups, self.claim_groups.into_value()); map.insert_unchecked(Property::MemberTenantId, self.member_tenant_id.into_value()); + map.insert_unchecked(Property::OpaqueTokens, self.opaque_tokens.into_value()); + map.insert_unchecked( + Property::IntrospectionUrl, + self.introspection_url.into_value(), + ); + map.insert_unchecked( + Property::IntrospectionClientId, + self.introspection_client_id.into_value(), + ); + map.insert_unchecked( + Property::IntrospectionClientSecret, + self.introspection_client_secret.into_value(), + ); JmapValue::Object(map) } } @@ -30745,6 +30794,16 @@ impl RegistryJsonPropertyPatch for OidcDirectory { Some(Property::MemberTenantId) => self .member_tenant_id .patch(pointer.assert_can_set_tenant()?, value), + Some(Property::OpaqueTokens) => self.opaque_tokens.patch(pointer, value), + Some(Property::IntrospectionUrl) => self + .introspection_url + .patch(pointer.with_validators(&[StringValidator::Trim]), value), + Some(Property::IntrospectionClientId) => self + .introspection_client_id + .patch(pointer.with_validators(&[StringValidator::Trim]), value), + Some(Property::IntrospectionClientSecret) => { + self.introspection_client_secret.patch(pointer, value) + } Some(Property::Type) => Ok(MaybeUnpatched::Unpatched { property: Property::Type, value, diff --git a/docs/spec/features/per-domain-directories.md b/docs/spec/features/per-domain-directories.md index 4bfdff63..0a32c80d 100644 --- a/docs/spec/features/per-domain-directories.md +++ b/docs/spec/features/per-domain-directories.md @@ -148,13 +148,17 @@ Enterprise upstream. Feature 1 covers that field. `TenantStorageQuota` has |---|---|---| | `description` | string | Label | | `issuerUrl` | URI | The provider's issuer. Discovery is fetched from `/.well-known/openid-configuration` | -| `requireAudience` | string, nullable | If set, a token's `aud` must include it | +| `requireAudience` | string, nullable | A token's `aud` must include it. Required on a new directory; one saved without it keeps working, logs a warning each time it loads, and can't have an audience removed once set | | `requireScopes` | set of strings, default `openid`, `email` | Every listed scope must be in the token | | `claimUsername` | string, default `preferred_username` | Claim that names the account | | `usernameDomain` | string, nullable | Appended as `@domain` when the username claim has no `@`. Also appended to bare group names | | `claimName` | string, nullable, default `name` | Claim for the display name | | `claimGroups` | string, nullable | Claim for group memberships | | `memberTenantId` | `Id`, nullable | Tenant (Feature 1) | +| `opaqueTokens` | `reject`, `userinfo` or `introspection`, nullable | What a token that isn't a JWT signs in by (DIR-27). Unset: `introspection` with an `introspectionUrl`, else `reject`. A directory saved before the field existed reads as `userinfo` | +| `introspectionUrl` | URI, nullable | RFC 7662 endpoint. Unset: the discovery document's `introspection_endpoint` | +| `introspectionClientId` | string, nullable | Client ID introspection authenticates with. Required for `introspection` | +| `introspectionClientSecret` | `x:SecretKeyOptional` | Its secret. Required for `introspection` | The LDAP and SQL variants are unchanged, and this feature adds nothing to them. @@ -359,10 +363,21 @@ check it and the fork keeps it. - If `requireAudience` is set, `aud` must be present and include it. - Every `requireScopes` entry must appear in `scope`, which may be a space-separated string or an array. -- **DIR-27. Opaque tokens.** A token that isn't a JWT is sent to the - provider's userinfo endpoint (OIDC Core §5.3). `401` or `403` means sign-in - fails. Token introspection (RFC 7662) isn't used (public docs), and no - audience or scope check is possible on this path (open question 2). +- **DIR-27. Opaque tokens.** A token that isn't a JWT signs in by the + directory's `opaqueTokens`: + - `reject`: it doesn't. + - `introspection`: it is posted to the introspection endpoint (RFC 7662) + with HTTP Basic client authentication (RFC 6749 §2.3.1). Only + `"active": true` with `iss` equal to the discovery issuer, `aud` + including `requireAudience` when set, `exp` not past, no ID or refresh + marker (as for JWTs) and every `requireScopes` entry signs in. A non-2xx + answer is a provider error, not a failed sign-in. The claims are then + used as a JWT's are (DIR-28). + - `userinfo`: it is sent to the provider's userinfo endpoint (OIDC Core + §5.3), and `401` or `403` means sign-in fails. No audience or scope check + is possible, and any token the provider issued to any application + passes; only directories saved before `opaqueTokens` existed get this + without choosing it. - **DIR-28. Identity.** - The account address is the `claimUsername` claim if it contains `@`. Otherwise it is that claim plus `@usernameDomain`, or failing that the @@ -512,6 +527,9 @@ data. introspection (which needs client credentials the schema has no field for)? The docs name `requireAudience`'s default as `stalwart`, and the schema has no default. Observe which one a new directory gets. + **Settled** (http-directory-services-13): a new directory must set + `requireAudience`, and `opaqueTokens` offers introspection; see the + table and DIR-27. 3. **Local credentials outlive the directory.** App passwords and API keys keep working for a user disabled in the directory (DIR-3). Should synchronization, or a failed directory sign-in, suspend them? diff --git a/resources/privacy/catalog.toml b/resources/privacy/catalog.toml index 9428a47a..436620b8 100644 --- a/resources/privacy/catalog.toml +++ b/resources/privacy/catalog.toml @@ -1649,6 +1649,8 @@ secret = ["credential"] [object."x:OidcDirectory"] default = "none" +[object."x:OidcDirectory".properties] +introspectionClientSecret = ["credential"] [object."x:OidcProvider"] default = "none" diff --git a/resources/schema/schema.json.gz b/resources/schema/schema.json.gz index 3d7c4e13b656a44903fe68bce588a3932afb2362..ba26e8f9ab0fbae6fa6157acdd77f302b4b5705d 100644 GIT binary patch delta 120909 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 z ze=#c1z5%XYgb1g^j8gS&#)HTF zd+Jsrnuesg*mPkK>z~?=@yw!IT6DD?&Bx1oz7+lS=2Ca_)hEihPEmL3jq zzKV7-J}2;QfV%=#+Y*UJ)lI(202?I(+{88PaJX?63%m#>+2uVgQe1ZM2UhgQ zvmS&>hkpkS{Jle~*sGe32VSJKc$+pht%GmDJg#U-L%DL9P4ZQqTGP>Ch)K7JthX{0 zDw2Hoc^NF1axdmR>b5A(LxtryFE)XvwAsVM!lWmr%^VT-V8Dr?QaGf10KldHO98Vu z<6Q?w#bR~CV~#D)rXcjABQid6vJKI*ptoA(<9{3E7l-gE|FCr#8Re)A$ty>dIohQ! zGQHSa=GfTK7(;c4y%c-=F5|T5`GhRQsT~M+d*bkz)Yyz60{|Ter_5#~=IjPmH_wvW zJh*RaQnc0-?(r7@MT+c7m!2Mui^%BjnbDIopgvaj^`OtLxLVW#Qu@iTJAIG9|$3`>U+B-wkp?jJW<$E`Hab%!Lf&L*EGM(P5CoJ~o zvIs+ZU$zu$5gEAdy2HzVV{AArvthI9B2c>aZj4@yTc3?4FOV}V@)_8XlyMmYWM(ec z>+Fhr(rU3e_rV%v!nJt&a^$g3_=eocAb<4C=Q+QN=0!=jQf{A2bGUEtz~vzE*-c(& z_?!a{8&jcNYJYkk4@VNeD;+!zy%t$V57W0>3@-uBf>7CMaoeKOGelW|CE%P#+UT`+ zD`f!m1qhZTUe~6y+sO30Y1ExNJ(8>HuJwGHp=3w8K~2h)=HrTKT|`FdV-CTqV}GqV zqgqo3Tp`32vrxww}@5jf?9F%GKE# zqDSY*P~Da}vru@8gZ|^N_c3@109f{}iT^>xGydn#$kwM8scK&If>rl5UaVX!yx}pu zj?ii*J7k54(xl2Uy$56G*<+vHi+@~P^!;4&1rxix7OqHhM5R&1Uj_HZonu1iLlzpOsU_Rb_*Q^@CQBuU9KmNT~XkHbAVt=)~*BS?0 zbCi+U9A!M#S{ret;y}QfrY2R@^o_TQR?8{yLs01o7kg?6c_dNY)O2x^i`x-@d(FKp zR(}3I{*>aQGieTF3sC4C#1wWsGoJAbH@eCc|HXZ*ir{bK0}HX@StN}j4zPH5Fc1#U9E)UJw{ zkgxRk{XinuA~E{eX4!fittaB3v}|<0z*)M*EB&bCk5hLHz4KMhU?N#U2Y-Lhy=Z(I@d8$JO#P zNTi{goC0w23WVYJ`@zDF>Xt^)wM;*~oN%(YliJYJNYf)?#E_npjU*8Cau~ z9x!Kd18V5E~zM-*d7^u-&gfE<(S&P#MECLZ|i&Rled=nP%%->V6aey;LdHZtK z_84P12g;#|9DhX4Gq(a+>0;U$-)h$JFPaw zF@G|u)x{Lcf+#64@8Q0`?^n{7WR8GCl`A6p z)q0S*4eO68o(}vfQ0lDkS*>5xq!NYlfu{PS$uLsoNeqA4(>bP9Qc=aZnF~WBnPEzb z%}1I#JtwY=ziCga>Qf6QA06zc0It~ne8j(M^rBVm8J!JB)+uCTA|UO!s$GM zH-DWrO(BiO0&_vVaJN!J5!mAKaI|knL#4KmEt^)#*1+n5N)S}}LTYH$l`yWE%}$md z=C?(<@Zuiz|2?M<_qz7sF=#R4gJBvZpE(RKZga0u?k30cur86*E;NT&3gF$Zd={2!3Gf zX3>1MPRnb$oHos>aBC^PWOg>6QPHz$byH0a;1CtNmr^rM))pgo*#!+lyvf(a>I}bM z8^+i%Y!aujCbFiY<`!Q?KG;+j6Mut(*=zKZf5sePIshUxI4z^Z2b?F`I7c2c-kl-m z>CySXE3JJ>nW)3#L`?q&x0?JS$rZrl2P)q02t=dPP+!O^RSQm;ST~AkN5bg^l*1t8 z%krxg^He)R$wC{hWk_&O@}n+zvIgdZ9<9=_!(xSNFJC6ejbK7<9!#ktM}JO#UEc%; z4kadz8zNW$L1wH?7HoKpCjyIzcZYZ+P@|p1;@&@F=*7`UiFe>U0*-Grb!V`z!=5`M zWcfYh19BJTIj;4>Ru27A2}6|nX2dNJB7B?V4>FK~Sz0k$tbWSH%iN^KeVm}w85z1E ziC)((S(Tu7nEDE<1!Lb}lYdsxQ|}{h)G(+|u|YU(0u`%nQfHj3?ON-ba96*{c*H~F zJ4$b5g}BFbl!qExlcAdpKI!GveMrvfstI7{z|H&WBluk&#(e}p0Eaa44M+6aCdp7H z_nKCckx`7Zl}}jFaDMAr3^p+ItFFeMidC?SD`Zp9y_K_D*asqalz-XEin#qvOK+v- zeFQ@M9<&V_%5b@njtD&R+0@}J}#;$4YO9gjoDt=!Oe@i}nR z?`^-OUdg02(}OYpkbgILs7c3B<0iZPVzWpOZxE(0`5})qZVlW2r>SZS7aEgKUfzHku5yI;Z@qNFMC?$wzd0 z^c&~U^yqgP_NQ`S+VR1*@b=Q^=Oj^4uU8a$r8UR}$;4`5xY!iQ4)ay99hO4JX$66H zV9d~~;hk1eUE#8m9Ii*I)bOT-`}dI+?g!vvn1TJgXpu3|H1|Q1UYFn|0WblJmk}oc zn|}{lePOJMEau+5Rtox%2{7%AYWi%JY@(J4wXpTF8^d)v3ix~AW2G|6rHJ?t691Vr-z<4PsJ&NBiz$j)^Op6&Qi%3<+gaxVE3be19 zAzTK4CoR^~sk275P#Pszzs#CN#`ppT?tf>3b~nWx|2u7<-)8sZdMd|JJv1q)&nEI7 zBS4ay>c2iEy+z&;$t7hIl(b@7T#oft@(oON3HVP zBo=tn8Od0sdf{_{=^<>^h(D0t%Go%I9&Ka@V{RpnAEiap>MlbRX<{>P0aqqQ3V)Z_ zkIO~cGeQpk^`L){H7mapGF8PEB4TTW2D7z0HkB@_U&G^l5&tTg2;)7LK$J* zTg`Zb0kD2z9R`3bt$*tS@KRa*hdH#V32in5f~Mjlr)Y5~Zgx)I4f_2;qsh@-2`RBBQV;_eMX*yt%Cp|d>F)91;!H?o$BWe{QtjZNKiVB#XgG(E z@US=PX%{EGB8&DlS;rP-EmoNY0BuB!&V;H3J*G{7IqP7Y)!G2G5q#jcS$L{l$43~D+cXECP0WIav4i7i3aCVxDA^v2Q~#eH=W-+f@hwU#gJrmvRH?X@&@Hq zn!MHBU6{NwLDl&^0kW7+Xh_`1U?GM71uByMgf$iEGWYj!sku89+m?WnzNVIfC$WvF zMH*qSo8a(ekp60Vzqz4ppSRM>)dFR?r% zDZ26V33}j`HG9??lQh8cT0=q5miJHkN2e#}C+9zcll%J*=YOJq4UYc&;bd?GW-2=w zzz*+ZM}O&`?|mE`|8*xj{xJG5I2s(C9`%MtkPXE@9u4;Y%5Utm(Qbcmaxpj={WbZ{ z3Hla!?vx7{Xa#N7Z+{YnLHK(9NfI8|B%zr5c@4*rG{+fmqPEAGS3`2ITFGk9t6eYM zKT>M3(uU^}q+%g-9WatY_O=w;564*O4b})g1BmMo_ErEqQ+j4I#Q8=mRH%5&swEYV zcZsh0p)U4pltt9BtNcBPBh$*jQ7=P+cE2#Gg|Tm-s3Dep2Y zeb%+9y+H0IQGN&OIqGptx*uRC{=mUkqo>f=>4*VB8Ys^1;C#?6_66_($E2P*S}uh< z=E&q=$2jkKo{Dafoy-wJeKKqy^P1zHa`>z>`N?Ck^ogwpq;riF(kurkXQ=GWv4bF^ z=*K6_gB$NjM}LnnUu0H%1^dgwTBXlDYc|>9O3`i7%qO)U{r){D?+na_R9qf*yc5kx zJImtq1+3zy0>zmIvgs7^ptaIof{mxkED&q4XmPJ%7aFvwql_&3fo@=v~y){JQRq z)hg$N>=-ix>V7(csx7g#akqVc$1Id1H$&*l9C2SZw1>HbM1aIkfO(k=E4g+`IYs%G|`{@u8>^I;7u85R?Livj~IAl`$G z?qOIZ@_!DsUf2Xig|%0<$@IKS9qO)QL-&mYINe$vUEFpZW{+OKo}9pSOmMuuRmg^E z7Mq)Pb!5gip7pMm|L})Bz1Z1_GBp_1#qwD@KMUhX;4$aIOZe-iixcY8j?#cu^Y#jU zDi=zDDH#u*7MNr%`mA!Fph;(U>0AFVc;!^nb$^ocEe-C9chEUWK;JK{oVo9iGZ6Fs zmyDAXA$T*FH=wK#NEDd3OQ}}6xby3dAq5Ld&no6}z|p9BB{1G?r2kLEHNE@(VL{DP z(aFl$f>KzNCWc*IP$J8+52N=PWvAE(B2(BoMieyD&~kX| z4}W{ZQ(y+r;JvJ8fLwodRZ#Q^Vi;6KWsN~yCg=hPZr~iRP=nDqALLrVgwtrCl-(>~|o z!d0AR&4n7SHHWtit#86)05^_|S3H(pu1#!(+(L$$N?Jmj$R2chcN*`Ypd z7rN0G?c8GcJ44e&LYBqY%w%Mu2;vDiycCEt6v{nqPD1tx*L&7xEdnDi>yA_{?zDz} zxVTCd*&X(eRWUrh3}j4DOrC7J7JperR$vC}By6b3**(!sMSqfjT05Cgmq7SG$LI`^ z9%0t5j@*EiTZGk76@J6!?T)H&>mGqhK2%PjZNcWNm&mLrHFfdY7-vK*FV*8eAD(9h z;M1L^n~qV!u%Uy&oTPZI)l_{lo|oU9Z7y7GnXoL-NAtXzAOAd&fl$bI6Mr&xu}=>^ zN6(B?mY57QddSe|q2q1V(1+(Gb1}&2fD-5z!!a={WAm5?hw1#h^zg+VI4iC#si_G& z7n>h!ioEBjtpnReY0l2MhJ^`B!5TDZ+Yg?n0hpWw^Mv*$+32Fvz?bX zs!-#KNRUG2irQ(?V-r~rCx2yO>Um;$GH>8Do5n#?9ZR5>cL*u1?~%}(lEu9-kIHZr zIJ)>k=zfJZ75yrFthA^f^>zW3K65tY$z^0I8IYX;yXzZc{b#jgXiMd;R$D8 zjxN_1Ksnr?9PX+>v2eKa5zuA{!Ig0ZQWO3{ye5G5k(Wqv1qGdo41X`$#{18C>Qrw* z5qu+}l-{+Li`ixi!kECt__RROW!s;x#f-q~exo5EN}iwB0#M5`{4Yvlr6@ReHt_D_ zxIi-(Ft~@k^CM|MKugm%9-v7STWkdoLQ)tq9Q6jHCl!RWYeuGZ1PMugo_i9pCbS*$ z=H3{0v29ms@}&jX7fhLb`c%qB4UnsV=|lM0bGeJ0LT1DiURUpjKTkIXMcnLsdsEe` zO_%@?L7lS6yp;AYhc@YpOTe5HF#$;j&sTL9EVT2N zH8KGw8h#3f8pE1I&P z9I3stCNo<0pj?3DYPc>}MYK5Q*PJhF2)WJo@R$Nj_D^vnM(Eu&gjp_l<6bPK2#@ou zXr)?CLKJ6=)CQ58X_+N_am{0gNDVB*c~iheaIR=ss`}5YJg4 z6mp9Hj$lYr0a*=?hzLo4Re$`>Cqf82c#IAilrSuSG9NzJv|t-DvdiGe<5EU+94%<_ zl4Z{;9b@$zBgSD;!iaOMF37? zJZ=YrX;i1u<=u)5h0nVed%YFvbyMhfg9_S@26#7r2FxOUyw5_Hw@iXSDxQeTie+AaT-_Vp-->X(+t5A>D9@|l{8ts6Gio^;0UUQNz%xhY z(quIYoC0XKA(t{mI*>{m(D6ea-hop}#%^8@#ij8OxQiqPckp(ixXmen_Ev9S9zP6l zKfi8&huchJY$nh#|UeTE^(1OERli2b2-O&wn1EKq&yFRS&-BRblCz= zycKib_x-v(kryVp(37sfP_4-6DRIfa-F#!I+~EyKJG4 zWgdt%O)1FA^tBy3`CNhI8r(B~qim2?M$!w+py3$8c=#Q_$d$5DTdVrGTZtRNXHuTj zD}fF<Uh?8z}>>B$3lwhy~Ed_Vd=Y;0F zAhZTK0jhqMPXN1LOWggh>aVx9Md{$ie{0?NUkKC8TV=Q*NO~*|Cf>y+N!ip$MQb^a zmIfn@XzB02`DRu>CX#`YZPW5-?;vu2=6H~&xN~x}|Dr5<$Jj;N#};~MWFxuhFt{wl z4VymkuCvu<;r26e+Rj4{F-Xny2KYzatZslhG@&-M9h+gvyEf&YbKO5@aF?zEmVdsH zM}e0wwQTM#g9O~>l4*Sh4$pdngPn|k0(*mFR)SUJ%*BYp7GfW+i*d0)fde~#4CK@M zU(l`ED$ORvRSr&c*2SnKS^3In&&VzI8ts>I^1*e`Ean?2Et-RCptv2$RVlSkT4{cz!}R zB4fT2lpqfnRe*x>7~dF8j~nzSu#b|CuxxIh;klEU%H**B-XFCVY#z#dn9YX%BvzGj zAlSkrX9?AuOyqlOMZSKQk2wJufAL)HzLo1z+1;PVz~Vw{6%b(s(b!OqYh$sgcM3!t zyo4}o30Z6FD`8vK@Rowt_s|RUC*`KcaqTn8%Q$m;?vTs{R9taN(-H1jm#TEjlop{R zPbt>t3u^YD3hWRJE@W81M9bFnz%PSyeFGUsDD0Jatz^oc2UTlnY;vZa)op&q~pkezm9SdJ z(k1vT$kfrFjqZ?(^8{;`ZCC7=tJJzAPDVp3`h4Ib!4ux0ZI(1_o~tC?LQ4`f(v{uN zPP-aAv@^72we7T(w6v2+!e$z4*!EVA%r@EF$XH+}t0*>=q#J1{e}YEZl95LG$43xE zg_GN3sNh@V6201B>(bi;kB#)90(RvE`%g!X6#q5E1*zi`#?gF&04hWDb%Jl+cQ7vU zNTLULXBF}GY-RE0`b~C5p4wT=Y^m-9I<|>VTts<1#oE0HtEZvmKR~}$E~`2EUw_P( zB>^u0TR^10rq#!WAg)2?On)WGvt_ak|D$tW_|uI918uQy!|BIok-uo}&ehn@&Ul?V?}9⋘>Ff4-*0#-|T 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 -> structs::Directory { "openid".to_string(), ]), member_tenant_id: tenant, + // http-directory-services-13: opaque tokens go to Keycloak's + // introspection endpoint, found by discovery + opaque_tokens: Some(OidcOpaqueTokens::Introspection), + introspection_url: None, + introspection_client_id: Some("stalwart".to_string()), + introspection_client_secret: SecretKeyOptional::Value(SecretKeyValue { + secret: "stalwart-secret".to_string(), + }), }) } @@ -242,11 +250,15 @@ pub async fn test() { // http-directory-services-13: a directory saved before the audience was // required keeps signing people in, and can still be edited without - // one; an audience, once set, stays + // one; an audience, once set, stays. It also still takes opaque tokens + // its user info endpoint accepts, as it always did. let mut legacy = directory(None); if let structs::Directory::Oidc(oidc) = &mut legacy { oidc.description = "Keycloak, saved without an audience".to_string(); oidc.require_audience = None; + oidc.opaque_tokens = Some(OidcOpaqueTokens::Userinfo); + oidc.introspection_client_id = None; + oidc.introspection_client_secret = SecretKeyOptional::None; } let legacy = test .server @@ -263,28 +275,111 @@ pub async fn test() { json!({ Property::Description: "Keycloak, still without an audience" }), ) .await; - admin - .registry_update_object( - ObjectType::Domain, - domain, - json!({ Property::DirectoryId: legacy.to_string() }), - ) - .await; + let sign_in_through = async |directory: Id, token: &str| { + admin + .registry_update_object( + ObjectType::Domain, + domain, + json!({ Property::DirectoryId: directory.to_string() }), + ) + .await; + // In test mode a leading "." makes any token opaque + let result = bearer(&test, Some(JOHN), token).await; + admin + .registry_update_object( + ObjectType::Domain, + domain, + json!({ Property::DirectoryId: keycloak.to_string() }), + ) + .await; + result + }; let fresh = token("stalwart", "stalwart-secret", JOHN, "openid email profile") .await .unwrap(); assert_eq!( - bearer(&test, None, &fresh).await.unwrap(), + sign_in_through(legacy, &fresh).await.unwrap(), john, "a directory saved without an audience" ); - admin - .registry_update_object( - ObjectType::Domain, - domain, - json!({ Property::DirectoryId: keycloak.to_string() }), + assert_eq!( + sign_in_through(legacy, &format!(".{fresh}")).await.unwrap(), + john, + "an opaque token, through a directory saved before the policy" + ); + + // ... a new directory that doesn't say refuses opaque tokens + let mut unset = directory(None); + if let structs::Directory::Oidc(oidc) = &mut unset { + oidc.description = "Keycloak, opaque tokens unset".to_string(); + oidc.opaque_tokens = None; + oidc.introspection_client_id = None; + oidc.introspection_client_secret = SecretKeyOptional::None; + } + let unset = admin.registry_create_object(unset).await; + assert!( + sign_in_through(unset, &format!(".{fresh}")).await.is_err(), + "an opaque token, through a new directory" + ); + assert_eq!( + sign_in_through(unset, &fresh).await.unwrap(), + john, + "a JWT, through a new directory" + ); + + // ... and introspection takes only an active access token + assert_eq!( + bearer(&test, Some(JOHN), &format!(".{fresh}")) + .await + .unwrap(), + john, + "an opaque token, by introspection" + ); + let refresh = reqwest::Client::new() + .post("http://localhost:9080/realms/stalwart/protocol/openid-connect/token") + .form(&[ + ("grant_type", "password"), + ("client_id", "stalwart"), + ("client_secret", "stalwart-secret"), + ("username", JOHN), + ("password", PASSWORD), + ("scope", "openid email profile"), + ]) + .send() + .await + .unwrap() + .json::() + .await + .unwrap()["refresh_token"] + .as_str() + .unwrap() + .to_string(); + for (what, token) in [ + ("a refresh token", format!(".{refresh}")), + ( + "a token the provider never issued", + ".never-issued".to_string(), + ), + ] { + assert!( + bearer(&test, Some(JOHN), &token).await.is_err(), + "by introspection: {what}" + ); + } + let refused = admin + .registry_update_object_expect_err( + ObjectType::Directory, + keycloak, + json!({ Property::IntrospectionClientId: null }), ) .await; + assert!( + refused + .description + .as_deref() + .is_some_and(|reason| reason.contains("client ID")), + "introspection without a client: {refused:?}" + ); let refused = admin .registry_update_object_expect_err( ObjectType::Directory, @@ -473,8 +568,8 @@ pub async fn test() { ) .await; admin.reload_settings().await; - // An opaque token has to reach the provider's userinfo endpoint, so with - // the provider stopped this is an outage, not a bad token + // An opaque token has to reach the provider's introspection endpoint, so + // with the provider stopped this is an outage, not a bad token crate::utils::containers::docker("stop", "stalwart-test-keycloak"); for attempt in 0..8 { let err = bearer(&test, Some(JOHN), "an-opaque-token") diff --git a/tests/src/directory/unavailable.rs b/tests/src/directory/unavailable.rs index 23f0fabe..0e0e1b1d 100644 --- a/tests/src/directory/unavailable.rs +++ b/tests/src/directory/unavailable.rs @@ -2,6 +2,8 @@ * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC * * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL + * + * Modified by Coffey Labs in 2026 for INBUXA. */ use directory::{Credentials, Directory, UnavailableDirectory, backend::oidc::OpenIdDirectory}; @@ -27,6 +29,7 @@ pub async fn test() { require_audience: None, require_scopes: Map::new(vec![]), member_tenant_id: None, + ..Default::default() }) .await .expect_err("Discovery against an unreachable issuer must fail"); diff --git a/tests/src/scim/oidc.rs b/tests/src/scim/oidc.rs index 62ea9114..736085a6 100644 --- a/tests/src/scim/oidc.rs +++ b/tests/src/scim/oidc.rs @@ -2,6 +2,8 @@ * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC * * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL + * + * Modified by Coffey Labs in 2026 for INBUXA. */ use crate::{ @@ -57,6 +59,7 @@ async fn bind_directory(test: &TestServer) -> Id { "openid".to_string(), ]), member_tenant_id: None, + ..Default::default() })) .await; -- 2.54.0 From c53936d05f1472feac30be93ecee937bb4225fd2 Mon Sep 17 00:00:00 2001 From: John Coffey Date: Thu, 29 Oct 2026 02:12:29 -0700 Subject: [PATCH 08/10] OIDC directories: read at most 1 MiB from the provider, and refetch keys for kid-less tokens The discovery document, the JWKS, user info answers and their error text were read whole, so a provider that misbehaved, or anyone able to answer in its place, could make the server hold any amount of memory per sign-in. Each is now read up to 1 MiB (introspection answers too), checking the announced length first and counting as it reads; anything larger is a provider error. A JWT without a kid was tried against the cached keys only, so once the provider rotated its key every such token failed until a restart or reload. When no cached key verifies its signature, the keys are now fetched again once, at most once every 300 seconds as for an unknown kid; a failed fetch waits out the interval too, so tokens can't make the server hammer a provider that is down. Test: on main a user info answer over 1 MiB signed in (unit test: "user info"), and a kid-less token signed with a rotated-in key was refused (unit test: left None, right Some("john@example.org")). Now user info, introspection and JWKS answers over 1 MiB are refused, the kid-less token signs in after one fetch, and a forged one right after doesn't fetch again (unit tests with a local provider). --- crates/directory/src/backend/oidc/config.rs | 22 +- crates/directory/src/backend/oidc/lookup.rs | 318 +++++++++++++++---- docs/spec/features/per-domain-directories.md | 5 +- 3 files changed, 280 insertions(+), 65 deletions(-) diff --git a/crates/directory/src/backend/oidc/config.rs b/crates/directory/src/backend/oidc/config.rs index 8d0a5136..3d6cd1e4 100644 --- a/crates/directory/src/backend/oidc/config.rs +++ b/crates/directory/src/backend/oidc/config.rs @@ -7,7 +7,7 @@ */ use crate::Directory; -use crate::backend::oidc::lookup::fetch_jwks_keys; +use crate::backend::oidc::lookup::{fetch_jwks_keys, read_body}; use crate::backend::oidc::{ CachedKey, DiscoveryDocument, Introspection, JwksCache, OidcConfig, OidcDiscovery, OidcError, OpaqueTokens, OpenIdDirectory, @@ -150,16 +150,16 @@ impl OpenIdDirectory { "{}/.well-known/openid-configuration", config.issue_url.trim_end_matches('/') ); - let discovery_bytes = http - .get(&discovery_url) - .send() - .await - .map_err(|e| OidcError::Network(format!("Discovery fetch failed: {e}")))? - .error_for_status() - .map_err(|e| OidcError::Provider(format!("Discovery HTTP error: {e}")))? - .bytes() - .await - .map_err(|e| OidcError::Provider(format!("Discovery HTTP error: {e}")))?; + let discovery_bytes = read_body( + http.get(&discovery_url) + .send() + .await + .map_err(|e| OidcError::Network(format!("Discovery fetch failed: {e}")))? + .error_for_status() + .map_err(|e| OidcError::Provider(format!("Discovery HTTP error: {e}")))?, + "Discovery", + ) + .await?; let discovery: DiscoveryDocument = serde_json::from_slice(&discovery_bytes) .map_err(|e| OidcError::Provider(format!("Discovery JSON parse error: {e}")))?; diff --git a/crates/directory/src/backend/oidc/lookup.rs b/crates/directory/src/backend/oidc/lookup.rs index c9e471f3..2899de66 100644 --- a/crates/directory/src/backend/oidc/lookup.rs +++ b/crates/directory/src/backend/oidc/lookup.rs @@ -13,6 +13,7 @@ use crate::{ use ahash::AHashMap; use jsonwebtoken::{ Algorithm, DecodingKey, Header, Validation, decode, decode_header, + errors::ErrorKind, jwk::{self, JwkSet}, }; use reqwest::Client; @@ -21,6 +22,14 @@ use std::time::Instant; use std::{sync::Arc, time::Duration}; use trc::AuthEvent; +/// How often the provider's keys may be fetched again for a token they don't +/// verify. +const KEY_REFRESH_INTERVAL: Duration = Duration::from_secs(300); + +/// inbuxa: http-directory-services-13: what a provider sends back is read up +/// to this much. +const MAX_RESPONSE_SIZE: usize = 1024 * 1024; + impl OpenIdDirectory { pub async fn authenticate(&self, credentials: &Credentials) -> trc::Result { match credentials { @@ -59,46 +68,72 @@ impl OpenIdDirectory { )); } - let candidates = self.get_key(header.kid.as_deref()).await?; - let mut last_err = None; - for cached in &candidates { - let dk = &cached.decoding_key; - let alg = cached.algorithm; - let mut validation = Validation::new(alg); + let mut candidates = self.get_key(header.kid.as_deref()).await?; + let mut refreshed = false; + loop { + let mut last_err = None; + // inbuxa: whether every key failed only for being the wrong key + let mut wrong_keys = true; + for cached in &candidates { + let dk = &cached.decoding_key; + let alg = cached.algorithm; + let mut validation = Validation::new(alg); - if let Some(aud) = &self.config.require_aud { - validation.set_audience(&[aud]); - } else { - validation.validate_aud = false; - } + if let Some(aud) = &self.config.require_aud { + validation.set_audience(&[aud]); + } else { + validation.validate_aud = false; + } - validation.set_issuer(&[&self.discovery.document.issuer]); - validation.leeway = 60; + validation.set_issuer(&[&self.discovery.document.issuer]); + validation.leeway = 60; - match decode::(token, dk, &validation) { - Ok(token_data) => { - if self.config.require_aud.is_some() && token_data.claims.get("aud").is_none() { - last_err = Some(jsonwebtoken::errors::Error::from( - jsonwebtoken::errors::ErrorKind::InvalidAudience, - )); - continue; + match decode::(token, dk, &validation) { + Ok(token_data) => { + if self.config.require_aud.is_some() + && token_data.claims.get("aud").is_none() + { + last_err = Some(jsonwebtoken::errors::Error::from( + jsonwebtoken::errors::ErrorKind::InvalidAudience, + )); + wrong_keys = false; + continue; + } + + check_token_type(&token_data.claims)?; + self.validate_scopes(&token_data.claims)?; + + return self.account_from_claims(token, token_data.claims).await; + } + Err(e) => { + wrong_keys &= matches!( + e.kind(), + ErrorKind::InvalidSignature | ErrorKind::InvalidAlgorithm + ); + last_err = Some(e); } - - check_token_type(&token_data.claims)?; - self.validate_scopes(&token_data.claims)?; - - return self.account_from_claims(token, token_data.claims).await; - } - Err(e) => { - last_err = Some(e); } } - } - Err(OidcError::TokenValidation(format!( - "JWT validation failed: {}", - last_err.map(|e| e.to_string()).unwrap_or_default() - ))) + // inbuxa: http-directory-services-13: a token without a kid that + // no cached key verifies may be signed with a key the provider + // rotated in, so the keys are fetched again, once, as for an + // unknown kid (and as rarely) + if header.kid.is_none() + && !refreshed + && wrong_keys + && let Some(keys) = self.refresh_keys().await + { + candidates = keys; + refreshed = true; + continue; + } + + return Err(OidcError::TokenValidation(format!( + "JWT validation failed: {}", + last_err.map(|e| e.to_string()).unwrap_or_default() + ))); + } } /// inbuxa: the account a validated token's claims name, with what they @@ -195,10 +230,7 @@ impl OpenIdDirectory { "Introspection returned HTTP {status}" ))); } - let bytes = resp - .bytes() - .await - .map_err(|e| OidcError::Provider(format!("Introspection HTTP error: {e}")))?; + let bytes = read_body(resp, "Introspection").await?; let claims = serde_json::from_slice::(&bytes) .map_err(|e| OidcError::Provider(format!("Introspection JSON parse error: {e}")))?; @@ -252,7 +284,7 @@ impl OpenIdDirectory { return Ok(vec![cached.clone()]); } - if guard.last_updated.elapsed() < Duration::from_secs(300) { + if guard.last_updated.elapsed() < KEY_REFRESH_INTERVAL { return Err(OidcError::TokenValidation("Unknown key id".to_string())); } } else { @@ -291,6 +323,35 @@ impl OpenIdDirectory { } } + /// inbuxa: every key, fetched again unless that was tried within + /// `KEY_REFRESH_INTERVAL`; None when it was, or the fetch failed (a + /// failed fetch also waits out the interval, so tokens can't make the + /// server hammer a struggling provider). + async fn refresh_keys(&self) -> Option>> { + { + let mut guard = self.cache.write().await; + if guard.last_updated.elapsed() < KEY_REFRESH_INTERVAL { + return None; + } + guard.last_updated = Instant::now(); + } + match fetch_jwks_keys(&self.http, &self.discovery.document.jwks_uri).await { + Ok(keys) => { + let all = keys.values().cloned().collect(); + self.cache.write().await.keys = keys; + Some(all) + } + Err(err) => { + trc::event!( + Auth(AuthEvent::Warning), + Url = self.discovery.document.jwks_uri.clone(), + Reason = format!("JWKS refresh failed: {err}") + ); + None + } + } + } + async fn fetch_userinfo(&self, token: &str) -> Result { let resp = self .http @@ -302,7 +363,10 @@ impl OpenIdDirectory { let status = resp.status(); if status == reqwest::StatusCode::UNAUTHORIZED || status == reqwest::StatusCode::FORBIDDEN { - let reason = resp.text().await.unwrap_or_default(); + let reason = read_body(resp, "UserInfo") + .await + .map(|body| String::from_utf8_lossy(&body).into_owned()) + .unwrap_or_default(); return Err(OidcError::AuthorizationFailed(format!( "Token rejected by UserInfo endpoint with status {status}: {reason}" ))); @@ -313,10 +377,7 @@ impl OpenIdDirectory { ))); } - let bytes = resp - .bytes() - .await - .map_err(|e| OidcError::Provider(format!("UserInfo HTTP error: {e}")))?; + let bytes = read_body(resp, "UserInfo").await?; serde_json::from_slice::(&bytes) .map_err(|e| OidcError::Provider(format!("UserInfo JSON parse error: {e}"))) @@ -403,16 +464,16 @@ pub(super) async fn fetch_jwks_keys( http: &Client, jwks_uri: &str, ) -> Result>, OidcError> { - let jwks_bytes = http - .get(jwks_uri) - .send() - .await - .map_err(|e| OidcError::Network(format!("JWKS fetch failed: {e}")))? - .error_for_status() - .map_err(|e| OidcError::Provider(format!("JWKS HTTP error: {e}")))? - .bytes() - .await - .map_err(|e| OidcError::Provider(format!("JWKS HTTP error: {e}")))?; + let jwks_bytes = read_body( + http.get(jwks_uri) + .send() + .await + .map_err(|e| OidcError::Network(format!("JWKS fetch failed: {e}")))? + .error_for_status() + .map_err(|e| OidcError::Provider(format!("JWKS HTTP error: {e}")))?, + "JWKS", + ) + .await?; let jwks: JwkSet = serde_json::from_slice(&jwks_bytes) .map_err(|e| OidcError::Provider(format!("JWKS JSON parse error: {e}")))?; @@ -516,6 +577,37 @@ pub(super) async fn fetch_jwks_keys( Ok(map) } +/// inbuxa: http-directory-services-13: a response body, refused once it +/// passes `MAX_RESPONSE_SIZE` instead of read whole. +pub(super) async fn read_body( + mut resp: reqwest::Response, + what: &str, +) -> Result, OidcError> { + let too_large = || { + OidcError::Provider(format!( + "{what} response is larger than {MAX_RESPONSE_SIZE} bytes" + )) + }; + if resp + .content_length() + .is_some_and(|length| length > MAX_RESPONSE_SIZE as u64) + { + return Err(too_large()); + } + let mut body = Vec::new(); + while let Some(chunk) = resp + .chunk() + .await + .map_err(|e| OidcError::Provider(format!("{what} HTTP error: {e}")))? + { + if body.len() + chunk.len() > MAX_RESPONSE_SIZE { + return Err(too_large()); + } + body.extend_from_slice(&chunk); + } + Ok(body) +} + /// inbuxa: application/x-www-form-urlencoded, for the introspection request. fn form_encode(value: &str) -> String { let mut out = String::with_capacity(value.len()); @@ -893,6 +985,126 @@ MCowBQYDK2VwAyEAXNXjqSjw780GFXxFztdFBUMpxRoFqsYWN55FXnvncCE= ); } + /// A body over 1 MiB that is otherwise a good answer. + fn oversized(mut body: serde_json::Value) -> String { + body["padding"] = json!("x".repeat(1024 * 1024)); + body.to_string() + } + + // http-directory-services-13: what the provider sends back is read up to + // 1 MiB, never whole + #[tokio::test] + async fn provider_answers_are_capped() { + let base = provider(|request| { + let now = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap() + .as_secs(); + let body = if request.starts_with("POST /introspect ") { + json!({ + "active": true, + "iss": "https://idp.example.org", + "exp": now + 300, + "email": "john@example.org", + }) + } else if request.starts_with("GET /jwks ") { + json!({"keys": []}) + } else { + json!({"email": "john@example.org"}) + }; + (200, oversized(body)) + }) + .await; + + let directory = test_directory_with(&base, None, OpaqueTokens::Userinfo); + assert!( + sign_in(&directory, "an-opaque-token".to_string()) + .await + .is_err(), + "user info" + ); + let directory = test_directory_with( + &base, + None, + OpaqueTokens::Introspection(Introspection { + url: Some(format!("{base}/introspect")), + client_id: "inbuxa".to_string(), + client_secret: "secret".to_string(), + }), + ); + assert!( + sign_in(&directory, "an-opaque-token".to_string()) + .await + .is_err(), + "introspection" + ); + assert!( + fetch_jwks_keys(&Client::new(), &format!("{base}/jwks")) + .await + .is_err(), + "JWKS" + ); + } + + // http-directory-services-13: a token without a kid that no cached key + // verifies fetches the JWKS again, as an unknown kid does, so a provider + // that rotated its only key keeps working + #[tokio::test] + async fn tokens_without_a_kid_refresh_the_keys_once() { + const ROTATED_KEY: &str = "-----BEGIN PRIVATE KEY----- +MC4CAQAwBQYDK2VwBCIEIGJCOx1xrTLtywhRlvhdTe5Sm7UNQlVFXzqomt76r12T +-----END PRIVATE KEY-----"; + let fetches = Arc::new(std::sync::atomic::AtomicUsize::new(0)); + let counter = fetches.clone(); + let base = provider(move |_| { + counter.fetch_add(1, std::sync::atomic::Ordering::Relaxed); + let key = json!({ + "kty": "OKP", + "crv": "Ed25519", + "x": "b2naX3qFwv_RDOHZMPsYSii82JicgY3srqJSZfranzE", + "use": "sig", + }); + (200, json!({"keys": [key]}).to_string()) + }) + .await; + let directory = test_directory_at(&base); + directory.cache.write().await.last_updated = Instant::now() - Duration::from_secs(600); + + let rotated = |claims: serde_json::Value| { + let mut claims = claims; + claims["iss"] = json!("https://idp.example.org"); + claims["exp"] = json!( + std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap() + .as_secs() + + 300 + ); + claims["email"] = json!("john@example.org"); + jsonwebtoken::encode( + &jsonwebtoken::Header::new(Algorithm::EdDSA), + &claims, + &jsonwebtoken::EncodingKey::from_ed_pem(ROTATED_KEY.as_bytes()).unwrap(), + ) + .unwrap() + }; + assert_eq!( + sign_in(&directory, rotated(json!({}))) + .await + .map(|account| account.email) + .ok() + .as_deref(), + Some("john@example.org") + ); + assert_eq!(fetches.load(std::sync::atomic::Ordering::Relaxed), 1); + + // A forged one right after doesn't fetch again + let mut forged = rotated(json!({})); + forged.replace_range(forged.len() - 4.., "AAAA"); + assert!(sign_in(&directory, forged).await.is_err()); + assert_eq!(fetches.load(std::sync::atomic::Ordering::Relaxed), 1); + } + #[test] fn extract_string_list_distinguishes_absent_from_empty() { assert_eq!( diff --git a/docs/spec/features/per-domain-directories.md b/docs/spec/features/per-domain-directories.md index 0a32c80d..1d368efe 100644 --- a/docs/spec/features/per-domain-directories.md +++ b/docs/spec/features/per-domain-directories.md @@ -357,7 +357,10 @@ check it and the fork keeps it. - `HS*` algorithms are refused, and HMAC keys in the JWKS are skipped. RSA (RS and PS), EC P-256 and P-384, and EdDSA are accepted. - The key is found by `kid`. An unknown `kid` refetches the JWKS, at most - once every 300 seconds. With no `kid`, every key is tried. + once every 300 seconds. With no `kid`, every key is tried, and if none + verifies the signature the JWKS is refetched once, under the same limit. + - Discovery, JWKS, userinfo and introspection responses are read up to + 1 MiB; a larger one is a provider error. - `iss` must equal the discovery document's issuer. - `exp` is checked, with 60 seconds of leeway. - If `requireAudience` is set, `aud` must be present and include it. -- 2.54.0 From f731089a794647cc547987f703d8062105cf9858 Mon Sep 17 00:00:00 2001 From: John Coffey Date: Thu, 29 Oct 2026 06:08:30 -0700 Subject: [PATCH 09/10] Calendars: leave the object out of the log when expansion runs out of budget A calendar object whose recurrence rules use up the expansion work budget was logged with the whole object as the rule expansion error's details. Anyone who can get an object stored, a sharee or a sender whose invitation is processed, can cause that on purpose and repeatedly, copying calendar content into the server log each time. The error is still logged, with its reason and limit, but without the object. Other rule expansion errors keep logging it as before. Test: on the previous commit the budget test's object (40 events) came back as the log details; now it is left out, and an event with no start date still logs its object. --- crates/groupware/src/calendar/dates.rs | 44 ++++++++++++++++++++++++-- 1 file changed, 41 insertions(+), 3 deletions(-) diff --git a/crates/groupware/src/calendar/dates.rs b/crates/groupware/src/calendar/dates.rs index 175ed008..19e2a910 100644 --- a/crates/groupware/src/calendar/dates.rs +++ b/crates/groupware/src/calendar/dates.rs @@ -15,7 +15,8 @@ use calcard::{ common::timezone::Tz, icalendar::{ ICalendar, ICalendarComponentType, ICalendarFrequency, ICalendarRecurrenceRule, - ICalendarValue, dates::TimeOrDelta, + ICalendarValue, + dates::{CalendarError, CalendarErrorType, TimeOrDelta}, }, }; use compact_str::ToCompactString; @@ -27,6 +28,21 @@ use store::{ const MAX_TIME_SPAN: i64 = u32::MAX as i64; +// inbuxa: the object to log with a rule expansion error. Not when the work +// budget ran out: anyone who can get an object stored (a sharee, or a sender +// whose invitation is processed) can cause that on purpose, and every time it +// would copy up to the whole object into the log +fn expansion_error_details(ical: &ICalendar, errors: &[CalendarError]) -> Option { + if errors + .iter() + .any(|err| err.error == CalendarErrorType::ExpansionBudgetExhausted) + { + None + } else { + Some(ical.to_string()) + } +} + // calcard builds every candidate of one period (days x BYHOUR x BYMINUTE x // BYSECOND) before the expansion limit applies, so a rule is refused when that // set is larger than this. @@ -233,6 +249,7 @@ impl CalendarEventData { } if !expanded.errors.is_empty() { + let details = expansion_error_details(&ical, &expanded.errors); trc::event!( Calendar(trc::CalendarEvent::RuleExpansionError), Reason = expanded @@ -240,7 +257,7 @@ impl CalendarEventData { .into_iter() .map(|e| e.error.to_compact_string()) .collect::>(), - Details = ical.to_string(), + Details = details, Limit = max_expansions, ); } @@ -376,7 +393,8 @@ mod tests { } ical.push_str("END:VCALENDAR\r\n"); - let expanded = ICalendar::parse(&ical).unwrap().expand_dates(Tz::UTC, 3000); + let parsed = ICalendar::parse(&ical).unwrap(); + let expanded = parsed.expand_dates(Tz::UTC, 3000); assert!(expanded.events.is_empty()); assert!( expanded @@ -384,5 +402,25 @@ mod tests { .iter() .any(|err| err.error == CalendarErrorType::ExpansionBudgetExhausted) ); + + // Anyone who can get an object stored can use up the budget, so its + // log entry leaves the object out + assert_eq!( + super::expansion_error_details(&parsed, &expanded.errors), + None + ); + } + + #[test] + fn other_expansion_errors_keep_the_object() { + let ical = "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nBEGIN:VEVENT\r\nUID:x\r\n\ + RRULE:FREQ=DAILY\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n"; + let parsed = ICalendar::parse(ical).unwrap(); + let expanded = parsed.expand_dates(Tz::UTC, 3000); + assert!(!expanded.errors.is_empty()); + assert!( + super::expansion_error_details(&parsed, &expanded.errors) + .is_some_and(|details| details.contains("UID:x")) + ); } } -- 2.54.0 From c183e8f05e8dd86f442ccaece5676704416cef92 Mon Sep 17 00:00:00 2001 From: John Coffey Date: Thu, 29 Oct 2026 06:09:26 -0700 Subject: [PATCH 10/10] Version 2026.10.29.1 --- crates/types/src/branding.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/types/src/branding.rs b/crates/types/src/branding.rs index 362dbfc2..cd2e38b4 100644 --- a/crates/types/src/branding.rs +++ b/crates/types/src/branding.rs @@ -87,7 +87,7 @@ fn legacy_setting(name: &str, is_set: impl Fn(&str) -> bool) -> Option { #[macro_export] macro_rules! brand_version { () => { - "2026.10.29" + "2026.10.29.1" }; } -- 2.54.0