JSPropPointer for JsonPointer
{
+ fn parse_jsprop_value>(
+ &self,
+ json: &str,
+ ) -> Option> {
+ let items = self.as_slice();
+ if items
+ .iter()
+ .any(|item| matches!(item, JsonPointerItem::Root | JsonPointerItem::Wildcard))
+ {
+ return Value::parse_json(json).ok().map(Value::into_owned);
+ }
+
+ let mut wrapped = Vec::with_capacity(json.len() + items.len() * KEY_CAPACITY);
+ for item in items {
+ wrapped.push(b'{');
+ match item {
+ JsonPointerItem::Key(key) => serde_json::to_writer(&mut wrapped, key).ok()?,
+ JsonPointerItem::Number(number) => write!(wrapped, "\"{number}\"").ok()?,
+ JsonPointerItem::Root | JsonPointerItem::Wildcard | JsonPointerItem::Invalid(_) => {
+ return None;
+ }
+ }
+ wrapped.push(b':');
+ }
+ wrapped.extend_from_slice(json.as_bytes());
+ wrapped.extend(repeat_n(b'}', items.len()));
+
+ let mut value = Value::::parse_json(std::str::from_utf8(&wrapped).ok()?).ok()?;
+ for _ in items {
+ let [(_, inner)] = <[_; 1]>::try_from(value.into_object()?.into_vec()).ok()?;
+ value = inner;
+ }
+ Some(value.into_owned())
+ }
+}
diff --git a/crates/calcard/src/common/jsprop/ordered.rs b/crates/calcard/src/common/jsprop/ordered.rs
new file mode 100644
index 00000000..a0ece9f0
--- /dev/null
+++ b/crates/calcard/src/common/jsprop/ordered.rs
@@ -0,0 +1,174 @@
+/*
+ * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
+ */
+
+use ahash::{AHashMap, RandomState};
+use std::{borrow::Borrow, hash::Hash, vec::IntoIter};
+
+pub(crate) const INDEX_THRESHOLD: usize = 16;
+
+#[derive(Debug)]
+pub(crate) struct OrderedMap {
+ entries: Vec<(K, V)>,
+ index: Option>,
+}
+
+#[derive(Debug)]
+struct HashIndex {
+ state: RandomState,
+ positions: AHashMap,
+}
+
+impl Default for OrderedMap {
+ fn default() -> Self {
+ Self {
+ entries: Vec::new(),
+ index: None,
+ }
+ }
+}
+
+impl OrderedMap {
+ pub(crate) fn len(&self) -> usize {
+ self.entries.len()
+ }
+
+ pub(crate) fn is_empty(&self) -> bool {
+ self.entries.is_empty()
+ }
+
+ pub(crate) fn iter(&self) -> impl Iterator- {
+ self.entries.iter().map(|(key, value)| (key, value))
+ }
+
+ pub(crate) fn contains_key
(&self, key: &Q) -> bool
+ where
+ K: Borrow,
+ Q: Hash + Eq + ?Sized,
+ {
+ self.position(key).is_some()
+ }
+
+ pub(crate) fn get_mut(&mut self, key: &Q) -> Option<&mut V>
+ where
+ K: Borrow,
+ Q: Hash + Eq + ?Sized,
+ {
+ let position = self.position(key)?;
+ self.entries.get_mut(position).map(|(_, value)| value)
+ }
+
+ pub(crate) fn push(&mut self, key: K, value: V) {
+ if let Some(index) = &mut self.index {
+ index.insert(&key, self.entries.len());
+ }
+ self.entries.push((key, value));
+ if self.index.is_none() {
+ self.index = HashIndex::build(&self.entries);
+ }
+ }
+
+ pub(crate) fn insert(&mut self, key: K, value: V) {
+ match self.get_mut(&key) {
+ Some(existing) => *existing = value,
+ None => self.push(key, value),
+ }
+ }
+
+ pub(crate) fn insert_if_absent(&mut self, key: K, value: V) {
+ if !self.contains_key(&key) {
+ self.push(key, value);
+ }
+ }
+
+ pub(crate) fn upsert(
+ &mut self,
+ key: K,
+ value: impl FnOnce() -> V,
+ update: impl FnOnce(&mut V),
+ ) {
+ match self.get_mut(&key) {
+ Some(existing) => update(existing),
+ None => {
+ let mut value = value();
+ update(&mut value);
+ self.push(key, value);
+ }
+ }
+ }
+
+ pub(crate) fn retain(&mut self, mut keep: impl FnMut(&K, &mut V) -> bool) {
+ let len = self.entries.len();
+ self.entries.retain_mut(|(key, value)| keep(key, value));
+ if self.entries.len() != len {
+ self.index = HashIndex::build(&self.entries);
+ }
+ }
+
+ fn position(&self, key: &Q) -> Option
+ where
+ K: Borrow,
+ Q: Hash + Eq + ?Sized,
+ {
+ let Some(index) = &self.index else {
+ return self.scan(key);
+ };
+ let position = index.get(key)?;
+ match self.entries.get(position) {
+ Some((item, _)) if item.borrow() == key => Some(position),
+ _ => self.scan(key),
+ }
+ }
+
+ fn scan(&self, key: &Q) -> Option
+ where
+ K: Borrow,
+ Q: Hash + Eq + ?Sized,
+ {
+ self.entries
+ .iter()
+ .position(|(item, _)| item.borrow() == key)
+ }
+}
+
+impl IntoIterator for OrderedMap {
+ type Item = (K, V);
+ type IntoIter = IntoIter<(K, V)>;
+
+ fn into_iter(self) -> Self::IntoIter {
+ self.entries.into_iter()
+ }
+}
+
+impl HashIndex {
+ #[inline]
+ fn build(entries: &[(K, V)]) -> Option> {
+ (entries.len() > INDEX_THRESHOLD).then(|| Self::index(entries))
+ }
+
+ #[cold]
+ #[inline(never)]
+ fn index(entries: &[(K, V)]) -> Box {
+ let mut index = Box::new(HashIndex {
+ state: RandomState::new(),
+ positions: AHashMap::with_capacity(entries.len()),
+ });
+ for (position, (key, _)) in entries.iter().enumerate() {
+ index.insert(key, position);
+ }
+ index
+ }
+
+ fn insert(&mut self, key: &Q, position: usize) {
+ self.positions
+ .entry(self.state.hash_one(key))
+ .or_insert(position);
+ }
+
+ fn get(&self, key: &Q) -> Option {
+ self.positions.get(&self.state.hash_one(key)).copied()
+ }
+}
+
diff --git a/crates/calcard/src/common/jsprop/text.rs b/crates/calcard/src/common/jsprop/text.rs
new file mode 100644
index 00000000..5509dd77
--- /dev/null
+++ b/crates/calcard/src/common/jsprop/text.rs
@@ -0,0 +1,287 @@
+/*
+ * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
+ */
+
+use crate::common::format::AsciiPush;
+use jmap_tools::{JsonPointer, JsonPointerItem, Key, Property};
+use std::{
+ borrow::Cow,
+ fmt::{self, Display, Write},
+};
+
+const POINTER_CAPACITY: usize = 48;
+const CONVERTED_KEYS_CAPACITY: usize = 4;
+
+pub(crate) trait PointerText: Write {
+ fn reserve_escapes(&mut self, _escapes: usize) {}
+
+ fn push_pointer_token(&mut self, token: &str) -> fmt::Result {
+ let escapes = token
+ .bytes()
+ .filter(|byte| matches!(byte, b'~' | b'/'))
+ .count();
+ if escapes == 0 {
+ return self.write_str(token);
+ }
+ self.reserve_escapes(escapes);
+ for piece in token.split_inclusive(['~', '/']) {
+ match piece.strip_suffix('~') {
+ Some(head) => {
+ self.write_str(head)?;
+ self.write_str("~0")?;
+ }
+ None => match piece.strip_suffix('/') {
+ Some(head) => {
+ self.write_str(head)?;
+ self.write_str("~1")?;
+ }
+ None => self.write_str(piece)?,
+ },
+ }
+ }
+ Ok(())
+ }
+}
+
+pub(crate) trait PointerString: Sized {
+ fn from_pointer<'x>(parts: impl IntoIterator- + Clone) -> Self;
+}
+
+pub(crate) trait PointerTextExt {
+ fn to_text(&self) -> String;
+ fn text_eq(&self, text: &str) -> bool;
+}
+
+pub(crate) trait DisplayEq {
+ fn display_eq(&self, text: &str) -> bool;
+}
+
+pub(crate) trait IdReferenceText {
+ fn to_id_reference(&self) -> String;
+}
+
+pub(crate) trait IntoAsciiLowercase {
+ fn into_ascii_lowercase(self) -> String;
+}
+
+pub(crate) trait PointerProperty: Property {
+ fn into_pointer(self) -> Result, Self>;
+}
+
+pub(crate) trait ConvertedKeys {
+ fn into_converted_keys(self) -> Vec>;
+}
+
+pub(crate) struct Expect<'x>(&'x [u8]);
+
+pub(crate) struct AsciiString(String);
+
+impl PointerText for String {
+ fn reserve_escapes(&mut self, escapes: usize) {
+ self.reserve_exact(self.capacity() - self.len() + escapes);
+ }
+}
+
+impl PointerString for String {
+ fn from_pointer<'x>(parts: impl IntoIterator
- + Clone) -> Self {
+ let len = parts
+ .clone()
+ .into_iter()
+ .map(|part| part.len() + 1)
+ .sum::();
+ let mut text = String::with_capacity(len.saturating_sub(1));
+ for (position, part) in parts.into_iter().enumerate() {
+ if position > 0 {
+ text.push('/');
+ }
+ let _ = text.push_pointer_token(part);
+ }
+ text
+ }
+}
+
+impl PointerTextExt
for JsonPointer
{
+ fn to_text(&self) -> String {
+ let mut text = String::with_capacity(POINTER_CAPACITY);
+ let _ = write!(text, "{self}");
+ text
+ }
+
+ fn text_eq(&self, text: &str) -> bool {
+ if text.bytes().any(|byte| matches!(byte, b'/' | b'~')) {
+ return self.display_eq(text);
+ }
+ match self.as_slice() {
+ [] | [JsonPointerItem::Root] => text.is_empty(),
+ [JsonPointerItem::Wildcard] => text == "*",
+ [JsonPointerItem::Invalid(invalid)] => invalid == text,
+ [JsonPointerItem::Key(key)] => *key == text,
+ [JsonPointerItem::Number(number)] => number.display_eq(text),
+ _ => false,
+ }
+ }
+}
+
+impl DisplayEq for T {
+ fn display_eq(&self, text: &str) -> bool {
+ let mut expect = Expect::new(text);
+ write!(expect, "{self}").is_ok() && expect.is_complete()
+ }
+}
+
+impl IdReferenceText for str {
+ fn to_id_reference(&self) -> String {
+ let mut text = String::with_capacity(self.len() + 1);
+ text.push('#');
+ text.push_str(self);
+ text
+ }
+}
+
+impl IntoAsciiLowercase for Cow<'_, str> {
+ fn into_ascii_lowercase(self) -> String {
+ let mut text = self.into_owned();
+ text.make_ascii_lowercase();
+ text
+ }
+}
+
+impl AsciiString {
+ pub(crate) fn with_capacity(capacity: usize) -> Self {
+ AsciiString(String::with_capacity(capacity))
+ }
+
+ pub(crate) fn into_string(self) -> String {
+ self.0
+ }
+}
+
+impl From for AsciiString {
+ fn from(text: String) -> Self {
+ AsciiString(text)
+ }
+}
+
+impl AsciiPush for AsciiString {
+ #[inline(always)]
+ fn push_ascii(&mut self, bytes: &[u8]) -> fmt::Result {
+ self.0.extend(bytes.iter().copied().map(char::from));
+ Ok(())
+ }
+}
+
+impl<'x> Expect<'x> {
+ pub(crate) fn new(text: &'x str) -> Self {
+ Expect(text.as_bytes())
+ }
+
+ pub(crate) fn is_complete(&self) -> bool {
+ self.0.is_empty()
+ }
+}
+
+impl AsciiPush for Expect<'_> {
+ #[inline(always)]
+ fn push_ascii(&mut self, bytes: &[u8]) -> fmt::Result {
+ self.0 = self.0.strip_prefix(bytes).ok_or(fmt::Error)?;
+ Ok(())
+ }
+}
+
+impl Write for Expect<'_> {
+ #[inline(always)]
+ fn write_str(&mut self, text: &str) -> fmt::Result {
+ self.push_ascii(text.as_bytes())
+ }
+}
+
+impl ConvertedKeys for Key<'_, P> {
+ fn into_converted_keys(self) -> Vec> {
+ match self {
+ Key::Property(property) => match property.into_pointer() {
+ Ok(pointer) => pointer.into_converted_keys(),
+ Err(property) => property.to_cow().as_ref().into_converted_keys(),
+ },
+ Key::Borrowed(text) => text.into_converted_keys(),
+ Key::Owned(text) => text.as_str().into_converted_keys(),
+ }
+ }
+}
+
+impl ConvertedKeys for JsonPointer
{
+ fn into_converted_keys(self) -> Vec> {
+ let mut keys = Vec::with_capacity(self.len());
+ for item in self.into_iter() {
+ match item {
+ JsonPointerItem::Key(key) => {
+ match key.as_string_key() {
+ Some(text) if text.contains('/') => {
+ keys.extend(JsonPointer::::parse(text).into_iter().filter_map(
+ |item| match item {
+ JsonPointerItem::Key(key) => Some(key),
+ JsonPointerItem::Number(number) => {
+ Some(Key::Owned(number.to_string()))
+ }
+ JsonPointerItem::Root
+ | JsonPointerItem::Wildcard
+ | JsonPointerItem::Invalid(_) => None,
+ },
+ ));
+ }
+ _ => keys.push(key),
+ }
+ }
+ JsonPointerItem::Number(number) => keys.push(Key::Owned(number.to_string())),
+ JsonPointerItem::Root | JsonPointerItem::Wildcard | JsonPointerItem::Invalid(_) => {
+ }
+ }
+ }
+ keys
+ }
+}
+
+impl ConvertedKeys for &str {
+ fn into_converted_keys(self) -> Vec> {
+ if self.contains('~') {
+ return JsonPointer::::parse(self).into_converted_keys();
+ }
+ let mut keys: Vec> = Vec::with_capacity(CONVERTED_KEYS_CAPACITY);
+ let mut parent_is_key = false;
+ let first = usize::from(self.starts_with('/'));
+ let mut segments = self.split('/').enumerate();
+ for (position, segment) in segments.by_ref() {
+ let parent = if parent_is_key { keys.last() } else { None };
+ let (key, is_key) = match segment {
+ "" if position == 0 => continue,
+ "" => (Key::from(""), true),
+ "*" => {
+ parent_is_key = false;
+ continue;
+ }
+ _ => match P::try_parse(parent, segment) {
+ Some(property) => (Key::Property(property), true),
+ None => (
+ Key::Owned(segment.to_string()),
+ !segment.bytes().all(|byte| byte.is_ascii_digit())
+ || (segment != "0" && segment.starts_with('0'))
+ || segment.parse::().is_err(),
+ ),
+ },
+ };
+ let opaque = position == first
+ && key
+ .as_property()
+ .is_some_and(|property| property.is_opaque());
+ keys.push(key);
+ parent_is_key = is_key;
+ if opaque {
+ break;
+ }
+ }
+ keys.extend(segments.map(|(_, segment)| Key::Owned(segment.to_string())));
+ keys
+ }
+}
+
diff --git a/crates/calcard/src/common/mod.rs b/crates/calcard/src/common/mod.rs
new file mode 100644
index 00000000..28dad2f0
--- /dev/null
+++ b/crates/calcard/src/common/mod.rs
@@ -0,0 +1,296 @@
+/*
+ * 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 jiff::{civil, tz::Offset};
+use mail_parser::DateTime;
+
+#[cfg(feature = "rkyv")]
+pub mod archive;
+#[cfg(feature = "jmap")]
+pub mod blob;
+pub(crate) mod decode;
+#[cfg(feature = "jmap")]
+pub(crate) mod elements;
+pub mod embedded;
+#[cfg(feature = "jmap")]
+pub mod export;
+pub(crate) mod format;
+pub mod iana;
+#[cfg(feature = "jmap")]
+pub(crate) mod jsprop;
+pub mod parser;
+pub(crate) mod scan;
+pub(crate) mod stack;
+pub mod timezone;
+pub mod tokenizer;
+pub mod types;
+mod tzdb;
+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(all(test, inbuxa_tests), 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(all(test, inbuxa_tests), feature = "serde"),
+ derive(serde::Serialize, serde::Deserialize)
+)]
+#[cfg_attr(
+ any(all(test, inbuxa_tests), 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(all(test, inbuxa_tests), 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,
+}
+
+#[cfg(feature = "rkyv")]
+impl PartialEq> for ArchivedData {
+ fn eq(&self, other: &Box) -> bool {
+ self == other.as_ref()
+ }
+}
+
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub struct DateTimeResult {
+ pub date_time: civil::DateTime,
+ pub offset: Option,
+}
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
+#[cfg_attr(
+ any(all(test, inbuxa_tests), feature = "serde"),
+ derive(serde::Serialize, serde::Deserialize)
+)]
+#[cfg_attr(
+ any(all(test, inbuxa_tests), 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,
+}
+
+#[cfg(feature = "jmap")]
+pub(crate) enum IdReference {
+ Value(T),
+ Reference(String),
+ Error,
+}
+
+#[cfg(feature = "jmap")]
+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/crates/calcard/src/common/parser.rs b/crates/calcard/src/common/parser.rs
new file mode 100644
index 00000000..99b56e0e
--- /dev/null
+++ b/crates/calcard/src/common/parser.rs
@@ -0,0 +1,534 @@
+/*
+ * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
+ */
+
+use super::{Data, Encoding, PartialDateTime, tokenizer::Token};
+use crate::{
+ Parser,
+ common::{IanaParse, IanaString, IanaType},
+ icalendar::Uri,
+};
+use encodify::hex;
+use mail_parser::DateTime;
+use std::{borrow::Cow, iter::Peekable, slice::Iter, str::FromStr};
+
+impl<'x> Parser<'x> {
+ pub(crate) fn raw_token(&mut self) -> Option> {
+ let (first, last) = (self.token_buf.first()?, self.token_buf.last()?);
+ self.source.get(first.start..=last.end).map(unfold)
+ }
+
+ 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 {
+ match PartialDateTime::from_timestamp_bytes(&self.text, require_time) {
+ (dt, true) => Ok(dt),
+ (_, false) => 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(float) = self.text.as_str().parse::()
+ && float.is_finite()
+ {
+ return Ok(float);
+ }
+
+ Err(self.into_string())
+ }
+
+ pub(crate) fn into_integer(self) -> std::result::Result {
+ if let Ok(integer) = self.text.as_str().parse::() {
+ return Ok(integer);
+ }
+
+ 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 {
+ const BASE64: &[u8] = b"base64";
+
+ let (scheme, text) = text.split_at_checked(5)?;
+ if !scheme.eq_ignore_ascii_case(b"data:") {
+ return None;
+ }
+ let mut parts = text.splitn(2, |ch| *ch == b',');
+ let header = parts.next()?;
+ let data = parts.next().filter(|data| !data.is_empty())?;
+ let header = header.strip_suffix(b"\\").unwrap_or(header);
+ let mut segments = header.rsplitn(2, |ch| *ch == b';');
+ let (media_type, is_base64) = match (segments.next(), segments.next()) {
+ (Some(last), media_type) if last.eq_ignore_ascii_case(BASE64) => {
+ (media_type.unwrap_or_default(), true)
+ }
+ _ => (header, false),
+ };
+
+ Some(Data {
+ content_type: std::str::from_utf8(media_type)
+ .ok()
+ .filter(|media_type| !media_type.is_empty())
+ .map(str::to_string),
+ data: if is_base64 {
+ Encoding::Base64.decode(data)?.into_owned()
+ } else {
+ hex::PERCENT.decode(data).ok()?.into_owned()
+ },
+ })
+ }
+}
+
+impl PartialDateTime {
+ pub(crate) fn from_timestamp_bytes(bytes: &[u8], require_time: bool) -> (Self, bool) {
+ if let Some(dt) = Self::from_basic_format(bytes) {
+ let valid = !require_time || dt.has_time();
+ (dt, valid)
+ } else {
+ let mut dt = Self::default();
+ let valid = dt.parse_timestamp(&mut bytes.iter().peekable(), require_time);
+ (dt, valid)
+ }
+ }
+
+ fn from_basic_format(bytes: &[u8]) -> Option {
+ let (date, time) = bytes.split_first_chunk::<8>()?;
+ let mut dt = Self::from_basic_date(date)?;
+ match time {
+ [] => {}
+ [b'T' | b't', h0, h1, m0, m1, s0, s1, zone @ ..] => {
+ dt.hour = Some(Self::two_digits(*h0, *h1)?);
+ dt.minute = Some(Self::two_digits(*m0, *m1)?);
+ dt.second = Some(Self::two_digits(*s0, *s1)?);
+ match zone {
+ [] => {}
+ [b'Z' | b'z'] => {
+ dt.tz_hour = Some(0);
+ dt.tz_minute = Some(0);
+ }
+ _ => return None,
+ }
+ }
+ _ => return None,
+ }
+ Some(dt)
+ }
+
+ pub(crate) fn from_basic_date(&[y0, y1, y2, y3, m0, m1, d0, d1]: &[u8; 8]) -> Option {
+ Some(PartialDateTime {
+ year: Some(
+ u16::from(Self::two_digits(y0, y1)?) * 100 + u16::from(Self::two_digits(y2, y3)?),
+ ),
+ month: Some(Self::two_digits(m0, m1)?),
+ day: Some(Self::two_digits(d0, d1)?),
+ ..Default::default()
+ })
+ }
+
+ #[inline(always)]
+ fn two_digits(tens: u8, ones: u8) -> Option