From 2e89b63cd2136a7f34be5d485bcb98c12c333878 Mon Sep 17 00:00:00 2001 From: Lennart K <18233294+lennart-k@users.noreply.github.com> Date: Sun, 28 Dec 2025 13:24:10 +0100 Subject: [PATCH] Outsource lots of stuff to ical library --- Cargo.lock | 36 +- Cargo.toml | 5 +- crates/caldav/src/calendar/methods/get.rs | 20 +- .../report/calendar_query/comp_filter.rs | 11 +- .../report/calendar_query/prop_filter.rs | 16 +- crates/caldav/src/calendar/resource.rs | 2 +- crates/caldav/src/calendar_object/resource.rs | 13 +- crates/ical/src/address_object.rs | 23 +- crates/ical/src/error.rs | 3 +- crates/ical/src/icalendar/event.rs | 144 ------- crates/ical/src/icalendar/mod.rs | 4 +- crates/ical/src/icalendar/object.rs | 292 +------------ crates/ical/src/icalendar/object_type.rs | 56 +++ crates/ical/src/lib.rs | 2 - crates/ical/src/timestamp.rs | 393 +----------------- crates/ical/src/timezone.rs | 92 ---- crates/ical/tests/test_cal_object.rs | 2 +- crates/store_sqlite/Cargo.toml | 1 + crates/store_sqlite/src/calendar_store.rs | 21 +- ...ation_tests__caldav__propfind_depth_1.snap | 188 --------- 20 files changed, 156 insertions(+), 1168 deletions(-) create mode 100644 crates/ical/src/icalendar/object_type.rs delete mode 100644 crates/ical/src/timezone.rs diff --git a/Cargo.lock b/Cargo.lock index 1c85b34..cb19fcd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -275,9 +275,9 @@ dependencies = [ [[package]] name = "async-lock" -version = "3.4.1" +version = "3.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5fd03604047cee9b6ce9de9f70c6cd540a0520c813cbd49bae61f33ab80ed1dc" +checksum = "290f7f2596bd5b78a9fec8088ccd89180d7f9f55b94b0576823bbbdc72ee8311" dependencies = [ "event-listener 5.4.1", "event-listener-strategy", @@ -360,9 +360,9 @@ checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" [[package]] name = "axum" -version = "0.8.7" +version = "0.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b098575ebe77cb6d14fc7f32749631a6e44edbef6b796f89b020e99ba20d425" +checksum = "8b52af3cb4058c895d37317bb27508dccc8e5f2d39454016b297bf4a400597b8" dependencies = [ "axum-core", "bytes", @@ -412,9 +412,9 @@ dependencies = [ [[package]] name = "axum-extra" -version = "0.12.2" +version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dbfe9f610fe4e99cf0cfcd03ccf8c63c28c616fe714d80475ef731f3b13dd21b" +checksum = "6dfbd6109d91702d55fc56df06aae7ed85c465a7a451db6c0e54a4b9ca5983d1" dependencies = [ "axum", "axum-core", @@ -983,18 +983,18 @@ dependencies = [ [[package]] name = "derive_more" -version = "2.1.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "10b768e943bed7bf2cab53df09f4bc34bfd217cdb57d971e769874c9a6710618" +checksum = "d751e9e49156b02b44f9c1815bcb94b984cdcc4396ecc32521c739452808b134" dependencies = [ "derive_more-impl", ] [[package]] name = "derive_more-impl" -version = "2.1.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d286bfdaf75e988b4a78e013ecd79c581e06399ab53fbacd2d916c2f904f30b" +checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb" dependencies = [ "convert_case", "proc-macro2", @@ -1761,13 +1761,14 @@ dependencies = [ [[package]] name = "ical" version = "0.11.0" -source = "git+https://github.com/lennart-k/ical-rs#d384dd45495722d69c7f76d62a54a8d6481e90ee" dependencies = [ "chrono", "chrono-tz", + "derive_more", "itertools 0.14.0", "lazy_static", "regex", + "rrule", "thiserror 2.0.17", ] @@ -1972,9 +1973,9 @@ dependencies = [ [[package]] name = "itoa" -version = "1.0.15" +version = "1.0.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" +checksum = "7ee5b5339afb4c41626dde77b7a611bd4f2c202b897852b4bcf5d03eddc61010" [[package]] name = "js-sys" @@ -3540,6 +3541,7 @@ dependencies = [ "chrono", "criterion", "derive_more", + "ical", "password-auth", "password-hash", "pbkdf2", @@ -3620,9 +3622,9 @@ checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" [[package]] name = "ryu" -version = "1.0.20" +version = "1.0.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" +checksum = "62049b2877bf12821e8f9ad256ee38fdc31db7387ec2d3b3f403024de2034aea" [[package]] name = "same-file" @@ -3725,9 +3727,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.145" +version = "1.0.146" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c" +checksum = "217ca874ae0207aac254aa02c957ded05585a90892cc8d87f9e5fa49669dadd8" dependencies = [ "itoa", "memchr", diff --git a/Cargo.toml b/Cargo.toml index 20ea682..48d7d64 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -107,9 +107,12 @@ strum = "0.27" strum_macros = "0.27" serde_json = { version = "1.0", features = ["raw_value"] } sqlx-sqlite = { version = "0.8", features = ["bundled"] } -ical = { git = "https://github.com/lennart-k/ical-rs", features = [ +ical = { path = "../ical-rs/", features = [ "chrono-tz", ] } +# ical = { git = "https://github.com/lennart-k/ical-rs", features = [ +# "chrono-tz", +# ] } toml = "0.9" tower = "0.5" tower-http = { version = "0.6", features = [ diff --git a/crates/caldav/src/calendar/methods/get.rs b/crates/caldav/src/calendar/methods/get.rs index f69843e..de684a0 100644 --- a/crates/caldav/src/calendar/methods/get.rs +++ b/crates/caldav/src/calendar/methods/get.rs @@ -6,10 +6,10 @@ use axum::{extract::Path, response::Response}; use headers::{ContentType, HeaderMapExt}; use http::{HeaderValue, Method, StatusCode, header}; use ical::builder::calendar::IcalCalendarBuilder; +use ical::component::CalendarInnerData; use ical::generator::Emitter; use ical::property::Property; use percent_encoding::{CONTROLS, utf8_percent_encode}; -use rustical_ical::{CalendarObjectComponent, EventObject}; use rustical_store::{CalendarStore, SubscriptionStore, auth::Principal}; use std::collections::HashMap; use std::str::FromStr; @@ -62,21 +62,21 @@ pub async fn route_get( } for object in &objects { - vtimezones.extend(object.get_vtimezones()); - match object.get_data() { - CalendarObjectComponent::Event(EventObject { event, .. }, overrides) => { + vtimezones.extend(object.get_inner().get_vtimezones()); + match object.get_inner().get_inner() { + CalendarInnerData::Event(main, overrides) => { ical_calendar_builder = ical_calendar_builder - .add_event(event.clone()) - .add_events(overrides.iter().map(|ev| ev.event.clone())); + .add_event(main.clone()) + .add_events(overrides.iter().cloned()); } - CalendarObjectComponent::Todo(todo, overrides) => { + CalendarInnerData::Todo(main, overrides) => { ical_calendar_builder = ical_calendar_builder - .add_todo(todo.clone()) + .add_todo(main.clone()) .add_todos(overrides.iter().cloned()); } - CalendarObjectComponent::Journal(journal, overrides) => { + CalendarInnerData::Journal(main, overrides) => { ical_calendar_builder = ical_calendar_builder - .add_journal(journal.clone()) + .add_journal(main.clone()) .add_journals(overrides.iter().cloned()); } } diff --git a/crates/caldav/src/calendar/methods/report/calendar_query/comp_filter.rs b/crates/caldav/src/calendar/methods/report/calendar_query/comp_filter.rs index dc0e2e5..5009f0e 100644 --- a/crates/caldav/src/calendar/methods/report/calendar_query/comp_filter.rs +++ b/crates/caldav/src/calendar/methods/report/calendar_query/comp_filter.rs @@ -2,8 +2,8 @@ use crate::calendar::methods::report::calendar_query::{ TimeRangeElement, prop_filter::{PropFilterElement, PropFilterable}, }; -use ical::parser::ical::component::IcalTimeZone; -use rustical_ical::{CalendarObject, CalendarObjectComponent, CalendarObjectType}; +use ical::{component::IcalCalendarObject, parser::ical::component::IcalTimeZone}; +use rustical_ical::{CalendarObject, CalendarObjectType}; use rustical_xml::XmlDeserialize; #[derive(XmlDeserialize, Clone, Debug, PartialEq)] @@ -80,10 +80,11 @@ impl CompFilterable for CalendarObject { fn match_subcomponents(&self, comp_filter: &CompFilterElement) -> bool { let mut matches = self + .get_inner() .get_vtimezones() .values() .map(|tz| tz.matches(comp_filter)) - .chain([self.get_data().matches(comp_filter)]); + .chain([self.matches(comp_filter)]); if comp_filter.is_not_defined.is_some() { matches.all(|x| x) @@ -107,7 +108,7 @@ impl CompFilterable for IcalTimeZone { } } -impl CompFilterable for CalendarObjectComponent { +impl CompFilterable for IcalCalendarObject { fn get_comp_name(&self) -> &'static str { CalendarObjectType::from(self).as_str() } @@ -120,7 +121,7 @@ impl CompFilterable for CalendarObjectComponent { return false; } if let Some(end) = &time_range.end - && let Some(first_occurence) = self.get_first_occurence().unwrap_or(None) + && let Some(first_occurence) = self.get_dtstart().unwrap_or(None) && **end < first_occurence.utc() { return false; diff --git a/crates/caldav/src/calendar/methods/report/calendar_query/prop_filter.rs b/crates/caldav/src/calendar/methods/report/calendar_query/prop_filter.rs index a4ae647..dae7fa9 100644 --- a/crates/caldav/src/calendar/methods/report/calendar_query/prop_filter.rs +++ b/crates/caldav/src/calendar/methods/report/calendar_query/prop_filter.rs @@ -1,14 +1,16 @@ use super::{ParamFilterElement, TimeRangeElement}; use ical::{ + component::{CalendarInnerData, IcalCalendarObject}, generator::{IcalCalendar, IcalEvent}, parser::{ Component, ical::component::{IcalJournal, IcalTimeZone, IcalTodo}, }, property::Property, + types::CalDateTime, }; use rustical_dav::xml::TextMatchElement; -use rustical_ical::{CalDateTime, CalendarObject, CalendarObjectComponent, UtcDateTime}; +use rustical_ical::{CalendarObject, UtcDateTime}; use rustical_xml::XmlDeserialize; use std::collections::HashMap; @@ -79,7 +81,7 @@ pub trait PropFilterable { impl PropFilterable for CalendarObject { fn get_property(&self, name: &str) -> Option<&Property> { - Self::get_property(self, name) + self.get_property(name) } } @@ -113,12 +115,12 @@ impl PropFilterable for IcalTimeZone { } } -impl PropFilterable for CalendarObjectComponent { +impl PropFilterable for IcalCalendarObject { fn get_property(&self, name: &str) -> Option<&Property> { - match self { - Self::Event(event, _) => PropFilterable::get_property(&event.event, name), - Self::Todo(todo, _) => PropFilterable::get_property(todo, name), - Self::Journal(journal, _) => PropFilterable::get_property(journal, name), + match self.get_inner() { + CalendarInnerData::Event(event, _) => PropFilterable::get_property(event, name), + CalendarInnerData::Todo(todo, _) => PropFilterable::get_property(todo, name), + CalendarInnerData::Journal(journal, _) => PropFilterable::get_property(journal, name), } } } diff --git a/crates/caldav/src/calendar/resource.rs b/crates/caldav/src/calendar/resource.rs index 65d5912..9fa6f43 100644 --- a/crates/caldav/src/calendar/resource.rs +++ b/crates/caldav/src/calendar/resource.rs @@ -4,6 +4,7 @@ use crate::calendar::prop::{ReportMethod, SupportedCollationSet}; use chrono::{DateTime, Utc}; use derive_more::derive::{From, Into}; use ical::IcalParser; +use ical::types::CalDateTime; use rustical_dav::extensions::{ CommonPropertiesExtension, CommonPropertiesProp, SyncTokenExtension, SyncTokenExtensionProp, }; @@ -11,7 +12,6 @@ use rustical_dav::privileges::UserPrivilegeSet; use rustical_dav::resource::{PrincipalUri, Resource, ResourceName}; use rustical_dav::xml::{HrefElement, Resourcetype, ResourcetypeInner, SupportedReportSet}; use rustical_dav_push::{DavPushExtension, DavPushExtensionProp}; -use rustical_ical::CalDateTime; use rustical_store::Calendar; use rustical_store::auth::Principal; use rustical_xml::{EnumVariants, PropName}; diff --git a/crates/caldav/src/calendar_object/resource.rs b/crates/caldav/src/calendar_object/resource.rs index 15af050..e4953ae 100644 --- a/crates/caldav/src/calendar_object/resource.rs +++ b/crates/caldav/src/calendar_object/resource.rs @@ -4,6 +4,7 @@ use super::prop::{ }; use crate::Error; use derive_more::derive::{From, Into}; +use ical::generator::Emitter; use rustical_dav::{ extensions::CommonPropertiesExtension, privileges::UserPrivilegeSet, @@ -52,10 +53,14 @@ impl Resource for CalendarObjectResource { } CalendarObjectPropName::CalendarData(CalendarData { expand, .. }) => { CalendarObjectProp::CalendarData(if let Some(expand) = expand.as_ref() { - self.object.expand_recurrence( - Some(expand.start.to_utc()), - Some(expand.end.to_utc()), - )? + self.object + .get_inner() + .expand_recurrence( + Some(expand.start.to_utc()), + Some(expand.end.to_utc()), + ) + .map_err(rustical_ical::Error::ParserError)? + .generate() } else { self.object.get_ics().to_owned() }) diff --git a/crates/ical/src/address_object.rs b/crates/ical/src/address_object.rs index b0c5450..a775da2 100644 --- a/crates/ical/src/address_object.rs +++ b/crates/ical/src/address_object.rs @@ -1,4 +1,3 @@ -use crate::{CalDateTime, LOCAL_DATE}; use crate::{CalendarObject, Error}; use chrono::Datelike; use ical::generator::Emitter; @@ -6,6 +5,7 @@ use ical::parser::{ Component, vcard::{self, component::VcardContact}, }; +use ical::types::CalDate; use sha2::{Digest, Sha256}; use std::{collections::HashMap, io::BufReader}; @@ -64,15 +64,15 @@ impl AddressObject { } #[must_use] - pub fn get_anniversary(&self) -> Option<(CalDateTime, bool)> { + pub fn get_anniversary(&self) -> Option<(CalDate, bool)> { let prop = self.vcard.get_property("ANNIVERSARY")?.value.as_deref()?; - CalDateTime::parse_vcard(prop).ok() + CalDate::parse_vcard(prop).ok() } #[must_use] - pub fn get_birthday(&self) -> Option<(CalDateTime, bool)> { + pub fn get_birthday(&self) -> Option<(CalDate, bool)> { let prop = self.vcard.get_property("BDAY")?.value.as_deref()?; - CalDateTime::parse_vcard(prop).ok() + CalDate::parse_vcard(prop).ok() } #[must_use] @@ -87,13 +87,9 @@ impl AddressObject { let Some(fullname) = self.get_full_name() else { return Ok(None); }; - let anniversary = anniversary.date(); let year = contains_year.then_some(anniversary.year()); - let anniversary_start = anniversary.format(LOCAL_DATE); - let anniversary_end = anniversary - .succ_opt() - .unwrap_or(anniversary) - .format(LOCAL_DATE); + let anniversary_start = anniversary.format(); + let anniversary_end = anniversary.succ_opt().unwrap_or(anniversary).format(); let uid = format!("{}-anniversary", self.get_id()); let year_suffix = year.map(|year| format!(" ({year})")).unwrap_or_default(); @@ -132,10 +128,9 @@ END:VCALENDAR", let Some(fullname) = self.get_full_name() else { return Ok(None); }; - let birthday = birthday.date(); let year = contains_year.then_some(birthday.year()); - let birthday_start = birthday.format(LOCAL_DATE); - let birthday_end = birthday.succ_opt().unwrap_or(birthday).format(LOCAL_DATE); + let birthday_start = birthday.format(); + let birthday_end = birthday.succ_opt().unwrap_or(birthday).format(); let uid = format!("{}-birthday", self.get_id()); let year_suffix = year.map(|year| format!(" ({year})")).unwrap_or_default(); diff --git a/crates/ical/src/error.rs b/crates/ical/src/error.rs index 94b41fe..4149b37 100644 --- a/crates/ical/src/error.rs +++ b/crates/ical/src/error.rs @@ -1,6 +1,5 @@ use axum::{http::StatusCode, response::IntoResponse}; - -use crate::CalDateTimeError; +use ical::types::CalDateTimeError; #[derive(Debug, thiserror::Error, PartialEq, Eq)] pub enum Error { diff --git a/crates/ical/src/icalendar/event.rs b/crates/ical/src/icalendar/event.rs index 9449554..2c2f30b 100644 --- a/crates/ical/src/icalendar/event.rs +++ b/crates/ical/src/icalendar/event.rs @@ -20,22 +20,6 @@ impl EventObject { self.event.get_uid() } - pub fn get_dtstart(&self) -> Result, Error> { - if let Some(dtstart) = self.event.get_dtstart() { - Ok(Some(CalDateTime::parse_prop(dtstart, &self.timezones)?)) - } else { - Ok(None) - } - } - - pub fn get_dtend(&self) -> Result, Error> { - if let Some(dtend) = self.event.get_dtend() { - Ok(Some(CalDateTime::parse_prop(dtend, &self.timezones)?)) - } else { - Ok(None) - } - } - pub fn get_last_occurence(&self) -> Result, Error> { if self.event.get_rrule().is_some() { // TODO: understand recurrence rules @@ -51,134 +35,6 @@ impl EventObject { let first_occurence = self.get_dtstart()?; Ok(first_occurence.map(|first_occurence| first_occurence + duration)) } - - pub fn recurrence_ruleset(&self) -> Result, Error> { - let dtstart: DateTime = if let Some(dtstart) = self.get_dtstart()? { - if let Some(dtend) = self.get_dtend()? { - // DTSTART and DTEND MUST have the same timezone - assert_eq!(dtstart.timezone(), dtend.timezone()); - } - - dtstart - .as_datetime() - .with_timezone(&dtstart.timezone().into()) - } else { - return Ok(None); - }; - - let mut rrule_set = RRuleSet::new(dtstart); - - for prop in &self.event.properties { - rrule_set = match prop.name.as_str() { - "RRULE" => { - let rrule = RRule::from_str(prop.value.as_ref().ok_or_else(|| { - Error::RRuleError(rrule::ParseError::MissingDateGenerationRules.into()) - })?)? - .validate(dtstart) - .unwrap(); - rrule_set.rrule(rrule) - } - "RDATE" => { - let rdate = CalDateTime::parse_prop(prop, &self.timezones)?.into(); - rrule_set.rdate(rdate) - } - "EXDATE" => { - let exdate = CalDateTime::parse_prop(prop, &self.timezones)?.into(); - rrule_set.exdate(exdate) - } - _ => rrule_set, - } - } - - Ok(Some(rrule_set)) - } - - pub fn expand_recurrence( - &self, - start: Option>, - end: Option>, - overrides: &[Self], - ) -> Result, Error> { - let Some(mut rrule_set) = self.recurrence_ruleset()? else { - return Ok(vec![self.event.clone()]); - }; - - if let Some(start) = start { - rrule_set = rrule_set.after(start.with_timezone(&rrule::Tz::UTC)); - } - if let Some(end) = end { - rrule_set = rrule_set.before(end.with_timezone(&rrule::Tz::UTC)); - } - let mut events = vec![]; - let dates = rrule_set.all(2048).dates; - let dtstart = self.get_dtstart()?.expect("We must have a DTSTART here"); - let computed_duration = self - .get_dtend()? - .map(|dtend| dtend.as_datetime().into_owned() - dtstart.as_datetime().as_ref()); - - 'recurrence: for date in dates { - let date = CalDateTime::from(date); - let dateformat = if dtstart.is_date() { - date.format_date() - } else { - date.format() - }; - - for ev_override in overrides { - if let Some(override_id) = &ev_override - .event - .get_recurrence_id() - .as_ref() - .expect("overrides have a recurrence id") - .value - && override_id == &dateformat - { - // We have an override for this occurence - // - events.push(ev_override.event.clone()); - continue 'recurrence; - } - } - - let mut ev = self.event.clone().mutable(); - ev.remove_property("RRULE"); - ev.remove_property("RDATE"); - ev.remove_property("EXDATE"); - ev.remove_property("EXRULE"); - let dtstart_prop = ev - .get_property("DTSTART") - .expect("We must have a DTSTART here") - .clone(); - ev.remove_property("DTSTART"); - ev.remove_property("DTEND"); - - ev.set_property(Property { - name: "RECURRENCE-ID".to_string(), - value: Some(dateformat.clone()), - params: vec![], - }); - ev.set_property(Property { - name: "DTSTART".to_string(), - value: Some(dateformat), - params: dtstart_prop.params.clone(), - }); - if let Some(duration) = computed_duration { - let dtend = date + duration; - let dtendformat = if dtstart.is_date() { - dtend.format_date() - } else { - dtend.format() - }; - ev.set_property(Property { - name: "DTEND".to_string(), - value: Some(dtendformat), - params: dtstart_prop.params, - }); - } - events.push(ev.verify()?); - } - Ok(events) - } } #[cfg(test)] diff --git a/crates/ical/src/icalendar/mod.rs b/crates/ical/src/icalendar/mod.rs index 4883949..5eb7355 100644 --- a/crates/ical/src/icalendar/mod.rs +++ b/crates/ical/src/icalendar/mod.rs @@ -1,5 +1,5 @@ -mod event; mod object; +mod object_type; -pub use event::*; pub use object::*; +pub use object_type::*; diff --git a/crates/ical/src/icalendar/object.rs b/crates/ical/src/icalendar/object.rs index d720d20..14a2520 100644 --- a/crates/ical/src/icalendar/object.rs +++ b/crates/ical/src/icalendar/object.rs @@ -1,195 +1,21 @@ -use super::EventObject; -use crate::CalDateTime; +use crate::CalendarObjectType; use crate::Error; -use chrono::DateTime; -use chrono::Utc; -use derive_more::Display; -use ical::generator::{Emitter, IcalCalendar}; -use ical::parser::ical::component::IcalJournal; -use ical::parser::ical::component::IcalTimeZone; -use ical::parser::ical::component::IcalTodo; +use ical::component::IcalCalendarObject; +use ical::parser::Component; use ical::property::Property; -use serde::Deserialize; -use serde::Serialize; use sha2::{Digest, Sha256}; -use std::{collections::HashMap, io::BufReader}; - -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Display)] -// specified in https://datatracker.ietf.org/doc/html/rfc5545#section-3.6 -pub enum CalendarObjectType { - #[serde(rename = "VEVENT")] - Event = 0, - #[serde(rename = "VTODO")] - Todo = 1, - #[serde(rename = "VJOURNAL")] - Journal = 2, -} - -impl CalendarObjectType { - #[must_use] - pub const fn as_str(&self) -> &'static str { - match self { - Self::Event => "VEVENT", - Self::Todo => "VTODO", - Self::Journal => "VJOURNAL", - } - } -} - -impl rustical_xml::ValueSerialize for CalendarObjectType { - fn serialize(&self) -> String { - self.as_str().to_owned() - } -} - -impl rustical_xml::ValueDeserialize for CalendarObjectType { - fn deserialize(val: &str) -> std::result::Result { - match ::deserialize(val)?.as_str() { - "VEVENT" => Ok(Self::Event), - "VTODO" => Ok(Self::Todo), - "VJOURNAL" => Ok(Self::Journal), - _ => Err(rustical_xml::XmlError::InvalidValue( - rustical_xml::ParseValueError::Other(format!( - "Invalid value '{val}', must be VEVENT, VTODO, or VJOURNAL" - )), - )), - } - } -} - -#[derive(Debug, Clone)] -pub enum CalendarObjectComponent { - Event(EventObject, Vec), - Todo(IcalTodo, Vec), - Journal(IcalJournal, Vec), -} - -impl CalendarObjectComponent { - #[must_use] - pub fn get_uid(&self) -> &str { - match &self { - // We've made sure before that the first component exists and all components share the - // same UID - Self::Todo(todo, _) => todo.get_uid(), - Self::Event(event, _) => event.event.get_uid(), - Self::Journal(journal, _) => journal.get_uid(), - } - } -} - -impl From<&CalendarObjectComponent> for CalendarObjectType { - fn from(value: &CalendarObjectComponent) -> Self { - match value { - CalendarObjectComponent::Event(..) => Self::Event, - CalendarObjectComponent::Todo(..) => Self::Todo, - CalendarObjectComponent::Journal(..) => Self::Journal, - } - } -} - -impl CalendarObjectComponent { - fn from_events(mut events: Vec) -> Result { - let main_event = events - .extract_if(.., |event| event.event.get_recurrence_id().is_none()) - .next() - .expect("there must be one main event"); - let overrides = events; - for event in &overrides { - if event.get_uid() != main_event.get_uid() { - return Err(Error::InvalidData( - "Calendar object contains multiple UIDs".to_owned(), - )); - } - if event.event.get_recurrence_id().is_none() { - return Err(Error::InvalidData( - "Calendar object can only contain one main component".to_owned(), - )); - } - } - Ok(Self::Event(main_event, overrides)) - } - fn from_todos(mut todos: Vec) -> Result { - let main_todo = todos - .extract_if(.., |todo| todo.get_recurrence_id().is_none()) - .next() - .expect("there must be one main event"); - let overrides = todos; - for todo in &overrides { - if todo.get_uid() != main_todo.get_uid() { - return Err(Error::InvalidData( - "Calendar object contains multiple UIDs".to_owned(), - )); - } - if todo.get_recurrence_id().is_none() { - return Err(Error::InvalidData( - "Calendar object can only contain one main component".to_owned(), - )); - } - } - Ok(Self::Todo(main_todo, overrides)) - } - fn from_journals(mut journals: Vec) -> Result { - let main_journal = journals - .extract_if(.., |journal| journal.get_recurrence_id().is_none()) - .next() - .expect("there must be one main event"); - let overrides = journals; - for journal in &overrides { - if journal.get_uid() != main_journal.get_uid() { - return Err(Error::InvalidData( - "Calendar object contains multiple UIDs".to_owned(), - )); - } - if journal.get_recurrence_id().is_none() { - return Err(Error::InvalidData( - "Calendar object can only contain one main component".to_owned(), - )); - } - } - Ok(Self::Journal(main_journal, overrides)) - } - - pub fn get_first_occurence(&self) -> Result, Error> { - match &self { - Self::Event(main_event, overrides) => Ok(overrides - .iter() - .chain(std::iter::once(main_event)) - .map(super::event::EventObject::get_dtstart) - .collect::, _>>()? - .into_iter() - .flatten() - .min()), - _ => Ok(None), - } - } - - pub fn get_last_occurence(&self) -> Result, Error> { - match &self { - Self::Event(main_event, overrides) => Ok(overrides - .iter() - .chain(std::iter::once(main_event)) - .map(super::event::EventObject::get_last_occurence) - .collect::, _>>()? - .into_iter() - .flatten() - .max()), - _ => Ok(None), - } - } -} +use std::io::BufReader; #[derive(Debug, Clone)] pub struct CalendarObject { - data: CalendarObjectComponent, - properties: Vec, id: String, ics: String, - vtimezones: HashMap, + inner: IcalCalendarObject, } impl CalendarObject { pub fn from_ics(ics: String, id: Option) -> Result { - let mut parser = ical::IcalParser::new(BufReader::new(ics.as_bytes())); + let mut parser = ical::IcalObjectParser::new(BufReader::new(ics.as_bytes())); let cal = parser.next().ok_or(Error::MissingCalendar)??; if parser.next().is_some() { return Err(Error::InvalidData( @@ -197,74 +23,16 @@ impl CalendarObject { )); } - if u8::from(!cal.events.is_empty()) - + u8::from(!cal.todos.is_empty()) - + u8::from(!cal.journals.is_empty()) - + u8::from(!cal.free_busys.is_empty()) - != 1 - { - // https://datatracker.ietf.org/doc/html/rfc4791#section-4.1 - return Err(Error::InvalidData( - "iCalendar object must have exactly one component type".to_owned(), - )); - } - - let timezones: HashMap> = cal - .timezones - .clone() - .into_iter() - .map(|timezone| (timezone.get_tzid().to_owned(), (&timezone).try_into().ok())) - .collect(); - - let vtimezones = cal - .timezones - .clone() - .into_iter() - .map(|timezone| (timezone.get_tzid().to_owned(), timezone)) - .collect(); - - let data = if !cal.events.is_empty() { - CalendarObjectComponent::from_events( - cal.events - .into_iter() - .map(|event| EventObject { - event, - timezones: timezones.clone(), - }) - .collect(), - )? - } else if !cal.todos.is_empty() { - CalendarObjectComponent::from_todos(cal.todos)? - } else if !cal.journals.is_empty() { - CalendarObjectComponent::from_journals(cal.journals)? - } else { - return Err(Error::InvalidData( - "iCalendar component type not supported :(".to_owned(), - )); - }; - Ok(Self { - id: id.unwrap_or_else(|| data.get_uid().to_owned()), - data, - properties: cal.properties, + id: id.unwrap_or_else(|| cal.get_uid().to_owned()), ics, - vtimezones, + inner: cal, }) } #[must_use] - pub const fn get_vtimezones(&self) -> &HashMap { - &self.vtimezones - } - - #[must_use] - pub const fn get_data(&self) -> &CalendarObjectComponent { - &self.data - } - - #[must_use] - pub fn get_uid(&self) -> &str { - self.data.get_uid() + pub const fn get_inner(&self) -> &IcalCalendarObject { + &self.inner } #[must_use] @@ -275,7 +43,7 @@ impl CalendarObject { #[must_use] pub fn get_etag(&self) -> String { let mut hasher = Sha256::new(); - hasher.update(self.get_uid()); + hasher.update(self.inner.get_uid()); hasher.update(self.get_ics()); format!("\"{:x}\"", hasher.finalize()) } @@ -285,47 +53,13 @@ impl CalendarObject { &self.ics } - #[must_use] - pub fn get_component_name(&self) -> &str { - self.get_object_type().as_str() - } - #[must_use] pub fn get_object_type(&self) -> CalendarObjectType { - (&self.data).into() - } - - pub fn get_first_occurence(&self) -> Result, Error> { - self.data.get_first_occurence() - } - - pub fn get_last_occurence(&self) -> Result, Error> { - self.data.get_last_occurence() - } - - pub fn expand_recurrence( - &self, - start: Option>, - end: Option>, - ) -> Result { - // Only events can be expanded - match &self.data { - CalendarObjectComponent::Event(main_event, overrides) => { - let cal = IcalCalendar { - properties: self.properties.clone(), - events: main_event.expand_recurrence(start, end, overrides)?, - ..Default::default() - }; - Ok(cal.generate()) - } - _ => Ok(self.get_ics().to_string()), - } + (&self.inner).into() } #[must_use] pub fn get_property(&self, name: &str) -> Option<&Property> { - self.properties - .iter() - .find(|property| property.name == name) + self.inner.get_property(name) } } diff --git a/crates/ical/src/icalendar/object_type.rs b/crates/ical/src/icalendar/object_type.rs new file mode 100644 index 0000000..821c9e8 --- /dev/null +++ b/crates/ical/src/icalendar/object_type.rs @@ -0,0 +1,56 @@ +use derive_more::Display; +use ical::component::{CalendarInnerData, IcalCalendarObject}; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Display)] +// specified in https://datatracker.ietf.org/doc/html/rfc5545#section-3.6 +pub enum CalendarObjectType { + #[serde(rename = "VEVENT")] + Event = 0, + #[serde(rename = "VTODO")] + Todo = 1, + #[serde(rename = "VJOURNAL")] + Journal = 2, +} + +impl CalendarObjectType { + #[must_use] + pub const fn as_str(&self) -> &'static str { + match self { + Self::Event => "VEVENT", + Self::Todo => "VTODO", + Self::Journal => "VJOURNAL", + } + } +} + +impl From<&IcalCalendarObject> for CalendarObjectType { + fn from(value: &IcalCalendarObject) -> Self { + match value.get_inner() { + CalendarInnerData::Event(..) => Self::Event, + CalendarInnerData::Todo(..) => Self::Todo, + CalendarInnerData::Journal(..) => Self::Journal, + } + } +} + +impl rustical_xml::ValueSerialize for CalendarObjectType { + fn serialize(&self) -> String { + self.as_str().to_owned() + } +} + +impl rustical_xml::ValueDeserialize for CalendarObjectType { + fn deserialize(val: &str) -> std::result::Result { + match ::deserialize(val)?.as_str() { + "VEVENT" => Ok(Self::Event), + "VTODO" => Ok(Self::Todo), + "VJOURNAL" => Ok(Self::Journal), + _ => Err(rustical_xml::XmlError::InvalidValue( + rustical_xml::ParseValueError::Other(format!( + "Invalid value '{val}', must be VEVENT, VTODO, or VJOURNAL" + )), + )), + } + } +} diff --git a/crates/ical/src/lib.rs b/crates/ical/src/lib.rs index 9c00d52..42f646f 100644 --- a/crates/ical/src/lib.rs +++ b/crates/ical/src/lib.rs @@ -1,9 +1,7 @@ #![warn(clippy::all, clippy::pedantic, clippy::nursery)] #![allow(clippy::missing_errors_doc, clippy::missing_panics_doc)] mod timestamp; -mod timezone; pub use timestamp::*; -pub use timezone::*; mod icalendar; pub use icalendar::*; diff --git a/crates/ical/src/timestamp.rs b/crates/ical/src/timestamp.rs index 623bc26..4fd5eb7 100644 --- a/crates/ical/src/timestamp.rs +++ b/crates/ical/src/timestamp.rs @@ -1,35 +1,8 @@ -use super::timezone::ICalTimezone; -use chrono::{DateTime, Datelike, Duration, Local, NaiveDate, NaiveDateTime, NaiveTime, Utc}; -use chrono_tz::Tz; +use chrono::{DateTime, NaiveDateTime, Utc}; use derive_more::derive::Deref; -use ical::property::Property; use rustical_xml::{ValueDeserialize, ValueSerialize}; -use std::{borrow::Cow, collections::HashMap, ops::Add, sync::LazyLock}; -static RE_VCARD_DATE_MM_DD: LazyLock = - LazyLock::new(|| regex::Regex::new(r"^--(?\d{2})(?\d{2})$").unwrap()); - -const LOCAL_DATE_TIME: &str = "%Y%m%dT%H%M%S"; const UTC_DATE_TIME: &str = "%Y%m%dT%H%M%SZ"; -pub const LOCAL_DATE: &str = "%Y%m%d"; - -#[derive(Debug, thiserror::Error, PartialEq, Eq)] -pub enum CalDateTimeError { - #[error( - "Timezone has X-LIC-LOCATION property to specify a timezone from the Olson database, however its value {0} is invalid" - )] - InvalidOlson(String), - #[error("TZID {0} does not refer to a valid timezone")] - InvalidTZID(String), - #[error("Timestamp doesn't exist because of gap in local time")] - LocalTimeGap, - #[error("Datetime string {0} has an invalid format")] - InvalidDatetimeFormat(String), - #[error("Could not parse datetime {0}")] - ParseError(String), - #[error("Duration string {0} has an invalid format")] - InvalidDurationFormat(String), -} #[derive(Debug, Clone, Deref, PartialEq, Eq, Hash)] pub struct UtcDateTime(pub DateTime); @@ -54,367 +27,3 @@ impl ValueSerialize for UtcDateTime { format!("{}", self.0.format(UTC_DATE_TIME)) } } - -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum CalDateTime { - // Form 1, example: 19980118T230000 -> Local - // Form 2, example: 19980119T070000Z -> UTC - // Form 3, example: TZID=America/New_York:19980119T020000 -> Olson - // https://en.wikipedia.org/wiki/Tz_database - DateTime(DateTime), - Date(NaiveDate, ICalTimezone), -} - -impl From for DateTime { - fn from(value: CalDateTime) -> Self { - value - .as_datetime() - .into_owned() - .with_timezone(&value.timezone().into()) - } -} - -impl From> for CalDateTime { - fn from(value: DateTime) -> Self { - Self::DateTime(value.with_timezone(&value.timezone().into())) - } -} - -impl PartialOrd for CalDateTime { - fn partial_cmp(&self, other: &Self) -> Option { - Some(self.cmp(other)) - } -} - -impl Ord for CalDateTime { - fn cmp(&self, other: &Self) -> std::cmp::Ordering { - match (&self, &other) { - (Self::DateTime(a), Self::DateTime(b)) => a.cmp(b), - (Self::DateTime(a), Self::Date(..)) => a.cmp(&other.as_datetime()), - (Self::Date(..), Self::DateTime(b)) => self.as_datetime().as_ref().cmp(b), - (Self::Date(..), Self::Date(..)) => self.as_datetime().cmp(&other.as_datetime()), - } - } -} - -impl From> for CalDateTime { - fn from(value: DateTime) -> Self { - Self::DateTime(value.with_timezone(&ICalTimezone::Local)) - } -} - -impl From> for CalDateTime { - fn from(value: DateTime) -> Self { - Self::DateTime(value.with_timezone(&ICalTimezone::Olson(chrono_tz::UTC))) - } -} - -impl Add for CalDateTime { - type Output = Self; - - fn add(self, duration: Duration) -> Self::Output { - match self { - Self::DateTime(datetime) => Self::DateTime(datetime + duration), - Self::Date(date, tz) => Self::DateTime( - date.and_time(NaiveTime::default()) - .and_local_timezone(tz) - .earliest() - .expect("Local timezone has constant offset") - + duration, - ), - } - } -} - -impl CalDateTime { - pub fn parse_prop( - prop: &Property, - timezones: &HashMap>, - ) -> Result { - let prop_value = prop - .value - .as_ref() - .ok_or_else(|| CalDateTimeError::InvalidDatetimeFormat("empty property".into()))?; - - let timezone = if let Some(tzid) = prop.get_param("TZID") { - if let Some(timezone) = timezones.get(tzid) { - timezone.to_owned() - } else { - // TZID refers to timezone that does not exist - return Err(CalDateTimeError::InvalidTZID(tzid.to_string())); - } - } else { - // No explicit timezone specified. - // This is valid and will be localtime or UTC depending on the value - // We will stick to this default as documented in https://github.com/lennart-k/rustical/issues/102 - None - }; - - Self::parse(prop_value, timezone) - } - - #[must_use] - pub fn format(&self) -> String { - match self { - Self::DateTime(datetime) => match datetime.timezone() { - ICalTimezone::Olson(chrono_tz::UTC) => datetime.format(UTC_DATE_TIME).to_string(), - _ => datetime.format(LOCAL_DATE_TIME).to_string(), - }, - Self::Date(date, _) => date.format(LOCAL_DATE).to_string(), - } - } - - #[must_use] - pub fn format_date(&self) -> String { - match self { - Self::DateTime(datetime) => datetime.format(LOCAL_DATE).to_string(), - Self::Date(date, _) => date.format(LOCAL_DATE).to_string(), - } - } - - #[must_use] - pub fn date(&self) -> NaiveDate { - match self { - Self::DateTime(datetime) => datetime.date_naive(), - Self::Date(date, _) => date.to_owned(), - } - } - - #[must_use] - pub const fn is_date(&self) -> bool { - matches!(&self, Self::Date(_, _)) - } - - #[must_use] - pub fn as_datetime(&self) -> Cow<'_, DateTime> { - match self { - Self::DateTime(datetime) => Cow::Borrowed(datetime), - Self::Date(date, tz) => Cow::Owned( - date.and_time(NaiveTime::default()) - .and_local_timezone(tz.to_owned()) - .earliest() - .expect("Midnight always exists"), - ), - } - } - - pub fn parse(value: &str, timezone: Option) -> Result { - if let Ok(datetime) = NaiveDateTime::parse_from_str(value, LOCAL_DATE_TIME) { - if let Some(timezone) = timezone { - return Ok(Self::DateTime( - datetime - .and_local_timezone(timezone.into()) - .earliest() - .ok_or(CalDateTimeError::LocalTimeGap)?, - )); - } - return Ok(Self::DateTime( - datetime - .and_local_timezone(ICalTimezone::Local) - .earliest() - .ok_or(CalDateTimeError::LocalTimeGap)?, - )); - } - - if let Ok(datetime) = NaiveDateTime::parse_from_str(value, UTC_DATE_TIME) { - return Ok(datetime.and_utc().into()); - } - let timezone = timezone.map_or(ICalTimezone::Local, ICalTimezone::Olson); - if let Ok(date) = NaiveDate::parse_from_str(value, LOCAL_DATE) { - return Ok(Self::Date(date, timezone)); - } - - if let Ok(date) = NaiveDate::parse_from_str(value, "%Y-%m-%d") { - return Ok(Self::Date(date, timezone)); - } - if let Ok(date) = NaiveDate::parse_from_str(value, "%Y%m%d") { - return Ok(Self::Date(date, timezone)); - } - - Err(CalDateTimeError::InvalidDatetimeFormat(value.to_string())) - } - - // Also returns whether the date contains a year - pub fn parse_vcard(value: &str) -> Result<(Self, bool), CalDateTimeError> { - if let Ok(datetime) = Self::parse(value, None) { - return Ok((datetime, true)); - } - - if let Some(captures) = RE_VCARD_DATE_MM_DD.captures(value) { - // Because 1972 is a leap year - let year = 1972; - // Cannot fail because of the regex - let month = captures.name("m").unwrap().as_str().parse().ok().unwrap(); - let day = captures.name("d").unwrap().as_str().parse().ok().unwrap(); - - return Ok(( - Self::Date( - NaiveDate::from_ymd_opt(year, month, day) - .ok_or_else(|| CalDateTimeError::ParseError(value.to_string()))?, - ICalTimezone::Local, - ), - false, - )); - } - Err(CalDateTimeError::InvalidDatetimeFormat(value.to_string())) - } - - #[must_use] - pub fn utc(&self) -> DateTime { - self.as_datetime().to_utc() - } - - #[must_use] - pub fn timezone(&self) -> ICalTimezone { - match &self { - Self::DateTime(datetime) => datetime.timezone(), - Self::Date(_, tz) => tz.to_owned(), - } - } -} - -impl From for DateTime { - fn from(value: CalDateTime) -> Self { - value.utc() - } -} - -impl Datelike for CalDateTime { - fn year(&self) -> i32 { - match &self { - Self::DateTime(datetime) => datetime.year(), - Self::Date(date, _) => date.year(), - } - } - fn month(&self) -> u32 { - match &self { - Self::DateTime(datetime) => datetime.month(), - Self::Date(date, _) => date.month(), - } - } - - fn month0(&self) -> u32 { - match &self { - Self::DateTime(datetime) => datetime.month0(), - Self::Date(date, _) => date.month0(), - } - } - fn day(&self) -> u32 { - match &self { - Self::DateTime(datetime) => datetime.day(), - Self::Date(date, _) => date.day(), - } - } - fn day0(&self) -> u32 { - match &self { - Self::DateTime(datetime) => datetime.day0(), - Self::Date(date, _) => date.day0(), - } - } - fn ordinal(&self) -> u32 { - match &self { - Self::DateTime(datetime) => datetime.ordinal(), - Self::Date(date, _) => date.ordinal(), - } - } - fn ordinal0(&self) -> u32 { - match &self { - Self::DateTime(datetime) => datetime.ordinal0(), - Self::Date(date, _) => date.ordinal0(), - } - } - fn weekday(&self) -> chrono::Weekday { - match &self { - Self::DateTime(datetime) => datetime.weekday(), - Self::Date(date, _) => date.weekday(), - } - } - fn iso_week(&self) -> chrono::IsoWeek { - match &self { - Self::DateTime(datetime) => datetime.iso_week(), - Self::Date(date, _) => date.iso_week(), - } - } - fn with_year(&self, year: i32) -> Option { - match &self { - Self::DateTime(datetime) => Some(Self::DateTime(datetime.with_year(year)?)), - Self::Date(date, tz) => Some(Self::Date(date.with_year(year)?, tz.to_owned())), - } - } - fn with_month(&self, month: u32) -> Option { - match &self { - Self::DateTime(datetime) => Some(Self::DateTime(datetime.with_month(month)?)), - Self::Date(date, tz) => Some(Self::Date(date.with_month(month)?, tz.to_owned())), - } - } - fn with_month0(&self, month0: u32) -> Option { - match &self { - Self::DateTime(datetime) => Some(Self::DateTime(datetime.with_month0(month0)?)), - Self::Date(date, tz) => Some(Self::Date(date.with_month0(month0)?, tz.to_owned())), - } - } - fn with_day(&self, day: u32) -> Option { - match &self { - Self::DateTime(datetime) => Some(Self::DateTime(datetime.with_day(day)?)), - Self::Date(date, tz) => Some(Self::Date(date.with_day(day)?, tz.to_owned())), - } - } - fn with_day0(&self, day0: u32) -> Option { - match &self { - Self::DateTime(datetime) => Some(Self::DateTime(datetime.with_day0(day0)?)), - Self::Date(date, tz) => Some(Self::Date(date.with_day0(day0)?, tz.to_owned())), - } - } - fn with_ordinal(&self, ordinal: u32) -> Option { - match &self { - Self::DateTime(datetime) => Some(Self::DateTime(datetime.with_ordinal(ordinal)?)), - Self::Date(date, tz) => Some(Self::Date(date.with_ordinal(ordinal)?, tz.to_owned())), - } - } - fn with_ordinal0(&self, ordinal0: u32) -> Option { - match &self { - Self::DateTime(datetime) => Some(Self::DateTime(datetime.with_ordinal0(ordinal0)?)), - Self::Date(date, tz) => Some(Self::Date(date.with_ordinal0(ordinal0)?, tz.to_owned())), - } - } -} - -#[cfg(test)] -mod tests { - use crate::CalDateTime; - use chrono::NaiveDate; - - #[test] - fn test_vcard_date() { - assert_eq!( - CalDateTime::parse_vcard("19850412").unwrap(), - ( - CalDateTime::Date( - NaiveDate::from_ymd_opt(1985, 4, 12).unwrap(), - crate::ICalTimezone::Local - ), - true - ) - ); - assert_eq!( - CalDateTime::parse_vcard("1985-04-12").unwrap(), - ( - CalDateTime::Date( - NaiveDate::from_ymd_opt(1985, 4, 12).unwrap(), - crate::ICalTimezone::Local - ), - true - ) - ); - assert_eq!( - CalDateTime::parse_vcard("--0412").unwrap(), - ( - CalDateTime::Date( - NaiveDate::from_ymd_opt(1972, 4, 12).unwrap(), - crate::ICalTimezone::Local - ), - false - ) - ); - } -} diff --git a/crates/ical/src/timezone.rs b/crates/ical/src/timezone.rs deleted file mode 100644 index aea5c9e..0000000 --- a/crates/ical/src/timezone.rs +++ /dev/null @@ -1,92 +0,0 @@ -use chrono::{Local, NaiveDate, NaiveDateTime, TimeZone}; -use chrono_tz::Tz; -use derive_more::{Display, From}; - -#[derive(Debug, Clone, From, PartialEq, Eq)] -pub enum ICalTimezone { - Local, - Olson(Tz), -} - -impl From for rrule::Tz { - fn from(value: ICalTimezone) -> Self { - match value { - ICalTimezone::Local => Self::LOCAL, - ICalTimezone::Olson(tz) => Self::Tz(tz), - } - } -} - -impl From for ICalTimezone { - fn from(value: rrule::Tz) -> Self { - match value { - rrule::Tz::Local(_) => Self::Local, - rrule::Tz::Tz(tz) => Self::Olson(tz), - } - } -} - -#[derive(Debug, Clone, PartialEq, Eq, Display)] -pub enum CalTimezoneOffset { - Local(chrono::FixedOffset), - Olson(chrono_tz::TzOffset), -} - -impl chrono::Offset for CalTimezoneOffset { - fn fix(&self) -> chrono::FixedOffset { - match self { - Self::Local(local) => local.fix(), - Self::Olson(olson) => olson.fix(), - } - } -} - -impl TimeZone for ICalTimezone { - type Offset = CalTimezoneOffset; - - fn from_offset(offset: &Self::Offset) -> Self { - match offset { - CalTimezoneOffset::Local(_) => Self::Local, - CalTimezoneOffset::Olson(offset) => Self::Olson(Tz::from_offset(offset)), - } - } - - fn offset_from_local_date(&self, local: &NaiveDate) -> chrono::MappedLocalTime { - match self { - Self::Local => Local - .offset_from_local_date(local) - .map(CalTimezoneOffset::Local), - Self::Olson(tz) => tz - .offset_from_local_date(local) - .map(CalTimezoneOffset::Olson), - } - } - - fn offset_from_local_datetime( - &self, - local: &NaiveDateTime, - ) -> chrono::MappedLocalTime { - match self { - Self::Local => Local - .offset_from_local_datetime(local) - .map(CalTimezoneOffset::Local), - Self::Olson(tz) => tz - .offset_from_local_datetime(local) - .map(CalTimezoneOffset::Olson), - } - } - - fn offset_from_utc_datetime(&self, utc: &NaiveDateTime) -> Self::Offset { - match self { - Self::Local => CalTimezoneOffset::Local(Local.offset_from_utc_datetime(utc)), - Self::Olson(tz) => CalTimezoneOffset::Olson(tz.offset_from_utc_datetime(utc)), - } - } - - fn offset_from_utc_date(&self, utc: &NaiveDate) -> Self::Offset { - match self { - Self::Local => CalTimezoneOffset::Local(Local.offset_from_utc_date(utc)), - Self::Olson(tz) => CalTimezoneOffset::Olson(tz.offset_from_utc_date(utc)), - } - } -} diff --git a/crates/ical/tests/test_cal_object.rs b/crates/ical/tests/test_cal_object.rs index 9fc4ffb..89a5c65 100644 --- a/crates/ical/tests/test_cal_object.rs +++ b/crates/ical/tests/test_cal_object.rs @@ -26,5 +26,5 @@ END:VCALENDAR #[test] fn parse_calendar_object() { let object = CalendarObject::from_ics(MULTI_VEVENT.to_string(), None).unwrap(); - object.expand_recurrence(None, None).unwrap(); + object.get_inner().expand_recurrence(None, None).unwrap(); } diff --git a/crates/store_sqlite/Cargo.toml b/crates/store_sqlite/Cargo.toml index 47f7625..f3cf47c 100644 --- a/crates/store_sqlite/Cargo.toml +++ b/crates/store_sqlite/Cargo.toml @@ -36,3 +36,4 @@ pbkdf2.workspace = true rustical_ical.workspace = true rstest = { workspace = true, optional = true } sha2.workspace = true +ical.workspace = true diff --git a/crates/store_sqlite/src/calendar_store.rs b/crates/store_sqlite/src/calendar_store.rs index ea4d2a8..3141d40 100644 --- a/crates/store_sqlite/src/calendar_store.rs +++ b/crates/store_sqlite/src/calendar_store.rs @@ -3,7 +3,8 @@ use crate::BEGIN_IMMEDIATE; use async_trait::async_trait; use chrono::TimeDelta; use derive_more::derive::Constructor; -use rustical_ical::{CalDateTime, CalendarObject, CalendarObjectType}; +use ical::types::CalDateOrDateTime; +use rustical_ical::{CalendarObject, CalendarObjectType}; use rustical_store::calendar_store::CalendarQuery; use rustical_store::synctoken::format_synctoken; use rustical_store::{Calendar, CalendarMetadata, CalendarStore, CollectionMetadata, Error}; @@ -25,12 +26,12 @@ impl TryFrom for CalendarObject { fn try_from(value: CalendarObjectRow) -> Result { let object = Self::from_ics(value.ics, Some(value.id))?; - if object.get_uid() != value.uid { + if object.get_inner().get_uid() != value.uid { return Err(rustical_store::Error::IcalError( rustical_ical::Error::InvalidData(format!( "uid={} and UID={} don't match", value.uid, - object.get_uid() + object.get_inner().get_uid() )), )); } @@ -455,20 +456,26 @@ impl SqliteCalendarStore { object: &CalendarObject, overwrite: bool, ) -> Result<(), Error> { - let (object_id, uid, ics) = (object.get_id(), object.get_uid(), object.get_ics()); + let (object_id, uid, ics) = ( + object.get_id(), + object.get_inner().get_uid(), + object.get_ics(), + ); let first_occurence = object - .get_first_occurence() + .get_inner() + .get_dtstart() .ok() .flatten() .as_ref() - .map(CalDateTime::date); + .map(CalDateOrDateTime::date_floor); let last_occurence = object + .get_inner() .get_last_occurence() .ok() .flatten() .as_ref() - .map(CalDateTime::date); + .map(CalDateOrDateTime::date_ceil); let etag = object.get_etag(); let object_type = object.get_object_type() as u8; diff --git a/src/integration_tests/caldav/snapshots/rustical__integration_tests__caldav__propfind_depth_1.snap b/src/integration_tests/caldav/snapshots/rustical__integration_tests__caldav__propfind_depth_1.snap index 7e33986..dfabdac 100644 --- a/src/integration_tests/caldav/snapshots/rustical__integration_tests__caldav__propfind_depth_1.snap +++ b/src/integration_tests/caldav/snapshots/rustical__integration_tests__caldav__propfind_depth_1.snap @@ -50,192 +50,4 @@ expression: body HTTP/1.1 200 OK - - /caldav/principal/user/calendar/ - - - #00FF00 - Description - BEGIN:VCALENDAR -PRODID:-//github.com/lennart-k/vzic-rs//RustiCal Calendar server//EN -VERSION:2.0 -BEGIN:VTIMEZONE -TZID:Europe/Berlin -LAST-MODIFIED:20250723T190331Z -X-LIC-LOCATION:Europe/Berlin -X-PROLEPTIC-TZNAME:LMT -BEGIN:STANDARD -TZNAME:CET -TZOFFSETFROM:+005328 -TZOFFSETTO:+0100 -DTSTART:18930401T000000 -END:STANDARD -BEGIN:DAYLIGHT -TZNAME:CEST -TZOFFSETFROM:+0100 -TZOFFSETTO:+0200 -DTSTART:19160430T230000 -RDATE:19400401T020000 -RDATE:19430329T020000 -RDATE:19460414T020000 -RDATE:19470406T030000 -RDATE:19480418T020000 -RDATE:19490410T020000 -RDATE:19800406T020000 -END:DAYLIGHT -BEGIN:STANDARD -TZNAME:CET -TZOFFSETFROM:+0200 -TZOFFSETTO:+0100 -DTSTART:19161001T010000 -RDATE:19421102T030000 -RDATE:19431004T030000 -RDATE:19441002T030000 -RDATE:19451118T030000 -RDATE:19461007T030000 -END:STANDARD -BEGIN:DAYLIGHT -TZNAME:CEST -TZOFFSETFROM:+0100 -TZOFFSETTO:+0200 -DTSTART:19170416T020000 -RRULE:FREQ=YEARLY;BYMONTH=4;BYDAY=3MO;UNTIL=19180415T010000Z -END:DAYLIGHT -BEGIN:STANDARD -TZNAME:CET -TZOFFSETFROM:+0200 -TZOFFSETTO:+0100 -DTSTART:19170917T030000 -RRULE:FREQ=YEARLY;BYMONTH=9;BYDAY=3MO;UNTIL=19180916T010000Z -END:STANDARD -BEGIN:DAYLIGHT -TZNAME:CEST -TZOFFSETFROM:+0100 -TZOFFSETTO:+0200 -DTSTART:19440403T020000 -RRULE:FREQ=YEARLY;BYMONTH=4;BYDAY=1MO;UNTIL=19450402T010000Z -END:DAYLIGHT -BEGIN:DAYLIGHT -TZNAME:CEMT -TZOFFSETFROM:+0200 -TZOFFSETTO:+0300 -DTSTART:19450524T020000 -RDATE:19470511T030000 -END:DAYLIGHT -BEGIN:DAYLIGHT -TZNAME:CEST -TZOFFSETFROM:+0300 -TZOFFSETTO:+0200 -DTSTART:19450924T030000 -RDATE:19470629T030000 -END:DAYLIGHT -BEGIN:STANDARD -TZNAME:CET -TZOFFSETFROM:+0100 -TZOFFSETTO:+0100 -DTSTART:19460101T000000 -RDATE:19800101T000000 -END:STANDARD -BEGIN:STANDARD -TZNAME:CET -TZOFFSETFROM:+0200 -TZOFFSETTO:+0100 -DTSTART:19471005T030000 -RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=1SU;UNTIL=19491002T010000Z -END:STANDARD -BEGIN:STANDARD -TZNAME:CET -TZOFFSETFROM:+0200 -TZOFFSETTO:+0100 -DTSTART:19800928T030000 -RRULE:FREQ=YEARLY;BYMONTH=9;BYDAY=-1SU;UNTIL=19950924T010000Z -END:STANDARD -BEGIN:DAYLIGHT -TZNAME:CEST -TZOFFSETFROM:+0100 -TZOFFSETTO:+0200 -DTSTART:19810329T020000 -RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU -END:DAYLIGHT -BEGIN:STANDARD -TZNAME:CET -TZOFFSETFROM:+0200 -TZOFFSETTO:+0100 -DTSTART:19961027T030000 -RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU -END:STANDARD -END:VTIMEZONE -END:VCALENDAR - - - https://www.iana.org/time-zones - - Europe/Berlin - 0 - - - - - - - - - - i;ascii-casemap - i;octet - - 10000000 - - - - - - - - - - - - - - - - - - -2621430101T000000Z - +2621421231T235959Z - github.com/lennart-k/rustical/ns/0 - github.com/lennart-k/rustical/ns/0 - - - - [PUSH_TOPIC] - - - 1 - - - 1 - - - - - - - Calendar - - /caldav/principal/user/ - - - - - - - - /caldav/principal/user/ - - - HTTP/1.1 200 OK - -