Outsource lots of stuff to ical library

This commit is contained in:
Lennart K
2025-12-28 13:24:10 +01:00
parent 1cfc8e7c23
commit 2e89b63cd2
20 changed files with 156 additions and 1168 deletions

View File

@@ -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<C: CalendarStore, S: SubscriptionStore>(
}
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());
}
}

View File

@@ -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;

View File

@@ -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),
}
}
}

View File

@@ -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};

View File

@@ -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()
})

View File

@@ -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();

View File

@@ -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 {

View File

@@ -20,22 +20,6 @@ impl EventObject {
self.event.get_uid()
}
pub fn get_dtstart(&self) -> Result<Option<CalDateTime>, 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<Option<CalDateTime>, 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<Option<CalDateTime>, 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<Option<rrule::RRuleSet>, Error> {
let dtstart: DateTime<rrule::Tz> = 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<DateTime<Utc>>,
end: Option<DateTime<Utc>>,
overrides: &[Self],
) -> Result<Vec<IcalEvent>, 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)]

View File

@@ -1,5 +1,5 @@
mod event;
mod object;
mod object_type;
pub use event::*;
pub use object::*;
pub use object_type::*;

View File

@@ -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<Self, rustical_xml::XmlError> {
match <String as rustical_xml::ValueDeserialize>::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<EventObject>),
Todo(IcalTodo, Vec<IcalTodo>),
Journal(IcalJournal, Vec<IcalJournal>),
}
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<EventObject>) -> Result<Self, Error> {
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<IcalTodo>) -> Result<Self, Error> {
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<IcalJournal>) -> Result<Self, Error> {
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<Option<CalDateTime>, Error> {
match &self {
Self::Event(main_event, overrides) => Ok(overrides
.iter()
.chain(std::iter::once(main_event))
.map(super::event::EventObject::get_dtstart)
.collect::<Result<Vec<_>, _>>()?
.into_iter()
.flatten()
.min()),
_ => Ok(None),
}
}
pub fn get_last_occurence(&self) -> Result<Option<CalDateTime>, 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::<Result<Vec<_>, _>>()?
.into_iter()
.flatten()
.max()),
_ => Ok(None),
}
}
}
use std::io::BufReader;
#[derive(Debug, Clone)]
pub struct CalendarObject {
data: CalendarObjectComponent,
properties: Vec<Property>,
id: String,
ics: String,
vtimezones: HashMap<String, IcalTimeZone>,
inner: IcalCalendarObject,
}
impl CalendarObject {
pub fn from_ics(ics: String, id: Option<String>) -> Result<Self, Error> {
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<String, Option<chrono_tz::Tz>> = 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<String, IcalTimeZone> {
&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<Option<CalDateTime>, Error> {
self.data.get_first_occurence()
}
pub fn get_last_occurence(&self) -> Result<Option<CalDateTime>, Error> {
self.data.get_last_occurence()
}
pub fn expand_recurrence(
&self,
start: Option<DateTime<Utc>>,
end: Option<DateTime<Utc>>,
) -> Result<String, Error> {
// 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)
}
}

View File

@@ -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<Self, rustical_xml::XmlError> {
match <String as rustical_xml::ValueDeserialize>::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"
)),
)),
}
}
}

View File

@@ -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::*;

View File

@@ -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<regex::Regex> =
LazyLock::new(|| regex::Regex::new(r"^--(?<m>\d{2})(?<d>\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<Utc>);
@@ -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<ICalTimezone>),
Date(NaiveDate, ICalTimezone),
}
impl From<CalDateTime> for DateTime<rrule::Tz> {
fn from(value: CalDateTime) -> Self {
value
.as_datetime()
.into_owned()
.with_timezone(&value.timezone().into())
}
}
impl From<DateTime<rrule::Tz>> for CalDateTime {
fn from(value: DateTime<rrule::Tz>) -> Self {
Self::DateTime(value.with_timezone(&value.timezone().into()))
}
}
impl PartialOrd for CalDateTime {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
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<DateTime<Local>> for CalDateTime {
fn from(value: DateTime<Local>) -> Self {
Self::DateTime(value.with_timezone(&ICalTimezone::Local))
}
}
impl From<DateTime<Utc>> for CalDateTime {
fn from(value: DateTime<Utc>) -> Self {
Self::DateTime(value.with_timezone(&ICalTimezone::Olson(chrono_tz::UTC)))
}
}
impl Add<Duration> 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<String, Option<chrono_tz::Tz>>,
) -> Result<Self, CalDateTimeError> {
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<ICalTimezone>> {
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<Tz>) -> Result<Self, CalDateTimeError> {
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<Utc> {
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<CalDateTime> for DateTime<Utc> {
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<Self> {
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<Self> {
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<Self> {
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<Self> {
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<Self> {
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<Self> {
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<Self> {
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
)
);
}
}

View File

@@ -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<ICalTimezone> for rrule::Tz {
fn from(value: ICalTimezone) -> Self {
match value {
ICalTimezone::Local => Self::LOCAL,
ICalTimezone::Olson(tz) => Self::Tz(tz),
}
}
}
impl From<rrule::Tz> 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<Self::Offset> {
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<Self::Offset> {
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)),
}
}
}

View File

@@ -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();
}

View File

@@ -36,3 +36,4 @@ pbkdf2.workspace = true
rustical_ical.workspace = true
rstest = { workspace = true, optional = true }
sha2.workspace = true
ical.workspace = true

View File

@@ -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<CalendarObjectRow> for CalendarObject {
fn try_from(value: CalendarObjectRow) -> Result<Self, Self::Error> {
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;