From 6ca7082197aa60288c3295387bfdf47d8adbed2d Mon Sep 17 00:00:00 2001 From: Quentin Dufour Date: Wed, 22 May 2024 15:02:53 +0200 Subject: [PATCH] fix: parsing components & times --- aero-dav/src/caldecoder.rs | 18 +-- aero-dav/src/calencoder.rs | 33 +++--- aero-dav/src/caltypes.rs | 3 +- aero-proto/src/dav/controller.rs | 186 +++++++++++++++++-------------- 4 files changed, 130 insertions(+), 110 deletions(-) diff --git a/aero-dav/src/caldecoder.rs b/aero-dav/src/caldecoder.rs index b4391a4..1de4552 100644 --- a/aero-dav/src/caldecoder.rs +++ b/aero-dav/src/caldecoder.rs @@ -287,7 +287,7 @@ impl QRead for Property { .is_some() { let dtstr = xml.tag_string().await?; - let dt = NaiveDateTime::parse_from_str(dtstr.as_str(), ICAL_DATETIME_FMT)?.and_utc(); + let dt = NaiveDateTime::parse_from_str(dtstr.as_str(), CALDAV_DATETIME_FMT)?.and_utc(); xml.close().await?; return Ok(Property::MaxDateTime(dt)); } @@ -653,8 +653,8 @@ impl QRead for Expand { _ => return Err(ParsingError::MissingAttribute), }; - let start = NaiveDateTime::parse_from_str(rstart.as_str(), ICAL_DATETIME_FMT)?.and_utc(); - let end = NaiveDateTime::parse_from_str(rend.as_str(), ICAL_DATETIME_FMT)?.and_utc(); + let start = NaiveDateTime::parse_from_str(rstart.as_str(), CALDAV_DATETIME_FMT)?.and_utc(); + let end = NaiveDateTime::parse_from_str(rend.as_str(), CALDAV_DATETIME_FMT)?.and_utc(); if start > end { return Err(ParsingError::InvalidValue); } @@ -672,8 +672,8 @@ impl QRead for LimitRecurrenceSet { _ => return Err(ParsingError::MissingAttribute), }; - let start = NaiveDateTime::parse_from_str(rstart.as_str(), ICAL_DATETIME_FMT)?.and_utc(); - let end = NaiveDateTime::parse_from_str(rend.as_str(), ICAL_DATETIME_FMT)?.and_utc(); + let start = NaiveDateTime::parse_from_str(rstart.as_str(), CALDAV_DATETIME_FMT)?.and_utc(); + let end = NaiveDateTime::parse_from_str(rend.as_str(), CALDAV_DATETIME_FMT)?.and_utc(); if start > end { return Err(ParsingError::InvalidValue); } @@ -691,8 +691,8 @@ impl QRead for LimitFreebusySet { _ => return Err(ParsingError::MissingAttribute), }; - let start = NaiveDateTime::parse_from_str(rstart.as_str(), ICAL_DATETIME_FMT)?.and_utc(); - let end = NaiveDateTime::parse_from_str(rend.as_str(), ICAL_DATETIME_FMT)?.and_utc(); + let start = NaiveDateTime::parse_from_str(rstart.as_str(), CALDAV_DATETIME_FMT)?.and_utc(); + let end = NaiveDateTime::parse_from_str(rend.as_str(), CALDAV_DATETIME_FMT)?.and_utc(); if start > end { return Err(ParsingError::InvalidValue); } @@ -918,13 +918,13 @@ impl QRead for TimeRange { let start = match xml.prev_attr("start") { Some(r) => { - Some(NaiveDateTime::parse_from_str(r.as_str(), ICAL_DATETIME_FMT)?.and_utc()) + Some(NaiveDateTime::parse_from_str(r.as_str(), CALDAV_DATETIME_FMT)?.and_utc()) } _ => None, }; let end = match xml.prev_attr("end") { Some(r) => { - Some(NaiveDateTime::parse_from_str(r.as_str(), ICAL_DATETIME_FMT)?.and_utc()) + Some(NaiveDateTime::parse_from_str(r.as_str(), CALDAV_DATETIME_FMT)?.and_utc()) } _ => None, }; diff --git a/aero-dav/src/calencoder.rs b/aero-dav/src/calencoder.rs index 4467f7c..f145628 100644 --- a/aero-dav/src/calencoder.rs +++ b/aero-dav/src/calencoder.rs @@ -178,7 +178,7 @@ impl QWrite for Property { let start = xml.create_cal_element("min-date-time"); let end = start.to_end(); - let dtstr = format!("{}", dt.format(ICAL_DATETIME_FMT)); + let dtstr = format!("{}", dt.format(CALDAV_DATETIME_FMT)); xml.q.write_event_async(Event::Start(start.clone())).await?; xml.q .write_event_async(Event::Text(BytesText::new(dtstr.as_str()))) @@ -189,7 +189,7 @@ impl QWrite for Property { let start = xml.create_cal_element("max-date-time"); let end = start.to_end(); - let dtstr = format!("{}", dt.format(ICAL_DATETIME_FMT)); + let dtstr = format!("{}", dt.format(CALDAV_DATETIME_FMT)); xml.q.write_event_async(Event::Start(start.clone())).await?; xml.q .write_event_async(Event::Text(BytesText::new(dtstr.as_str()))) @@ -493,11 +493,11 @@ impl QWrite for Expand { let mut empty = xml.create_cal_element("expand"); empty.push_attribute(( "start", - format!("{}", self.0.format(ICAL_DATETIME_FMT)).as_str(), + format!("{}", self.0.format(CALDAV_DATETIME_FMT)).as_str(), )); empty.push_attribute(( "end", - format!("{}", self.1.format(ICAL_DATETIME_FMT)).as_str(), + format!("{}", self.1.format(CALDAV_DATETIME_FMT)).as_str(), )); xml.q.write_event_async(Event::Empty(empty)).await } @@ -508,11 +508,11 @@ impl QWrite for LimitRecurrenceSet { let mut empty = xml.create_cal_element("limit-recurrence-set"); empty.push_attribute(( "start", - format!("{}", self.0.format(ICAL_DATETIME_FMT)).as_str(), + format!("{}", self.0.format(CALDAV_DATETIME_FMT)).as_str(), )); empty.push_attribute(( "end", - format!("{}", self.1.format(ICAL_DATETIME_FMT)).as_str(), + format!("{}", self.1.format(CALDAV_DATETIME_FMT)).as_str(), )); xml.q.write_event_async(Event::Empty(empty)).await } @@ -523,11 +523,11 @@ impl QWrite for LimitFreebusySet { let mut empty = xml.create_cal_element("limit-freebusy-set"); empty.push_attribute(( "start", - format!("{}", self.0.format(ICAL_DATETIME_FMT)).as_str(), + format!("{}", self.0.format(CALDAV_DATETIME_FMT)).as_str(), )); empty.push_attribute(( "end", - format!("{}", self.1.format(ICAL_DATETIME_FMT)).as_str(), + format!("{}", self.1.format(CALDAV_DATETIME_FMT)).as_str(), )); xml.q.write_event_async(Event::Empty(empty)).await } @@ -737,18 +737,21 @@ impl QWrite for TimeRange { match self { Self::OnlyStart(start) => empty.push_attribute(( "start", - format!("{}", start.format(ICAL_DATETIME_FMT)).as_str(), + format!("{}", start.format(CALDAV_DATETIME_FMT)).as_str(), + )), + Self::OnlyEnd(end) => empty.push_attribute(( + "end", + format!("{}", end.format(CALDAV_DATETIME_FMT)).as_str(), )), - Self::OnlyEnd(end) => { - empty.push_attribute(("end", format!("{}", end.format(ICAL_DATETIME_FMT)).as_str())) - } Self::FullRange(start, end) => { empty.push_attribute(( "start", - format!("{}", start.format(ICAL_DATETIME_FMT)).as_str(), + format!("{}", start.format(CALDAV_DATETIME_FMT)).as_str(), + )); + empty.push_attribute(( + "end", + format!("{}", end.format(CALDAV_DATETIME_FMT)).as_str(), )); - empty - .push_attribute(("end", format!("{}", end.format(ICAL_DATETIME_FMT)).as_str())); } } xml.q.write_event_async(Event::Empty(empty)).await diff --git a/aero-dav/src/caltypes.rs b/aero-dav/src/caltypes.rs index 717086b..924b651 100644 --- a/aero-dav/src/caltypes.rs +++ b/aero-dav/src/caltypes.rs @@ -3,7 +3,8 @@ use super::types as dav; use chrono::{DateTime, Utc}; -pub const ICAL_DATETIME_FMT: &str = "%Y%m%dT%H%M%SZ"; +pub const ICAL_DATETIME_FMT: &str = "%Y%m%dT%H%M%S"; +pub const CALDAV_DATETIME_FMT: &str = "%Y%m%dT%H%M%SZ"; //@FIXME ACL (rfc3744) is missing, required //@FIXME Versioning (rfc3253) is missing, required diff --git a/aero-proto/src/dav/controller.rs b/aero-proto/src/dav/controller.rs index e5a1cff..306b035 100644 --- a/aero-proto/src/dav/controller.rs +++ b/aero-proto/src/dav/controller.rs @@ -353,81 +353,49 @@ fn apply_filter<'a>( }; // Do checks + // @FIXME: icalendar does not consider VCALENDAR as a component + // but WebDAV does... + // Build a fake VCALENDAR component for icalendar compatibility, it's a hack let root_filter = &filter.0; - - // Find the component in the filter - let maybe_comp = ics - .components - .iter() - .find(|candidate| candidate.name.as_str() == root_filter.name.as_str()); - - // Apply additional rules - let is_keep = match (maybe_comp, &root_filter.additional_rules) { - (Some(_), None) => true, - (None, Some(cal::CompFilterRules::IsNotDefined)) => true, - (None, None) => false, - (None, Some(cal::CompFilterRules::Matches(_))) => false, - (Some(_), Some(cal::CompFilterRules::IsNotDefined)) => false, - (Some(inner_comp), Some(cal::CompFilterRules::Matches(filter))) => { - is_component_match(inner_comp, filter) - } + let fake_vcal_component = icalendar::parser::Component { + name: cal::Component::VCalendar.as_str().into(), + properties: ics.properties, + components: ics.components, }; + tracing::debug!(filter=?root_filter, "calendar-query filter"); // Adjust return value according to filter - match is_keep { + match is_component_match(&[fake_vcal_component], root_filter) { true => Some(Ok(single_node)), _ => None, } }) } -fn component_date( - component: &icalendar::parser::Component, - prop: &str, +fn prop_date( + properties: &[icalendar::parser::Property], + name: &str, ) -> Option> { - component - .find_prop(prop) + properties + .iter() + .find(|candidate| candidate.name.as_str() == name) .map(|p| p.val.as_str()) - .map(|raw_dtstart| { - NaiveDateTime::parse_from_str(raw_dtstart, cal::ICAL_DATETIME_FMT) + .map(|raw_time| { + tracing::trace!(raw_time = raw_time, "VEVENT raw time"); + NaiveDateTime::parse_from_str(raw_time, cal::ICAL_DATETIME_FMT) .ok() .map(|v| v.and_utc()) }) .flatten() } -use chrono::NaiveDateTime; -fn is_component_match( - component: &icalendar::parser::Component, - matcher: &cal::CompFilterMatch, -) -> bool { - if let Some(time_range) = &matcher.time_range { - let (dtstart, dtend) = match ( - component_date(component, "DTSTART"), - component_date(component, "DTEND"), - ) { - (Some(start), None) => (start, start), - (None, Some(end)) => (end, end), - (Some(start), Some(end)) => (start, end), - _ => return false, - }; - - let is_in_range = match time_range { - cal::TimeRange::OnlyStart(after) => &dtend >= after, - cal::TimeRange::OnlyEnd(before) => &dtstart <= before, - cal::TimeRange::FullRange(after, before) => &dtend >= after && &dtstart <= before, - }; - - if !is_in_range { - return false; - } - } - - if !matcher.prop_filter.iter().all(|single_prop_filter| { - match ( - &single_prop_filter.additional_rules, - component.find_prop(single_prop_filter.name.0.as_str()), - ) { +fn is_properties_match(props: &[icalendar::parser::Property], filters: &[cal::PropFilter]) -> bool { + filters.iter().all(|single_filter| { + // Find the property + let single_prop = props + .iter() + .find(|candidate| candidate.name.as_str() == single_filter.name.0.as_str()); + match (&single_filter.additional_rules, single_prop) { (None, Some(_)) | (Some(cal::PropFilterRules::IsNotDefined), None) => true, (None, None) | (Some(cal::PropFilterRules::IsNotDefined), Some(_)) @@ -436,12 +404,17 @@ fn is_component_match( // check value match &pattern.time_or_text { Some(cal::TimeOrText::Time(time_range)) => { - // try parse entry as date - let parsed_date = - match component_date(component, single_prop_filter.name.0.as_str()) { - Some(v) => v, - None => return false, - }; + let maybe_parsed_date = NaiveDateTime::parse_from_str( + prop.val.as_str(), + cal::ICAL_DATETIME_FMT, + ) + .ok() + .map(|v| v.and_utc()); + + let parsed_date = match maybe_parsed_date { + None => return false, + Some(v) => v, + }; // see if entry is in range let is_in_range = match time_range { @@ -501,27 +474,70 @@ fn is_component_match( }) } } - }) { - return false; - } - - matcher.comp_filter.iter().all(|single_comp_filter| { - // Find the component - let maybe_comp = component - .components - .iter() - .find(|candidate| candidate.name.as_str() == single_comp_filter.name.as_str()); - - // Filter according to rules - match (maybe_comp, &single_comp_filter.additional_rules) { - (Some(_), None) => true, - (None, Some(cal::CompFilterRules::IsNotDefined)) => true, - (None, None) => false, - (Some(_), Some(cal::CompFilterRules::IsNotDefined)) => false, - (None, Some(cal::CompFilterRules::Matches(_))) => false, - (Some(inner_comp), Some(cal::CompFilterRules::Matches(comp_match))) => { - is_component_match(inner_comp, comp_match) - } - } }) } + +fn is_in_time_range( + properties: &[icalendar::parser::Property], + time_range: &cal::TimeRange, +) -> bool { + //@FIXME too naive: https://datatracker.ietf.org/doc/html/rfc4791#section-9.9 + + let (dtstart, dtend) = match ( + prop_date(properties, "DTSTART"), + prop_date(properties, "DTEND"), + ) { + (Some(start), None) => (start, start), + (None, Some(end)) => (end, end), + (Some(start), Some(end)) => (start, end), + _ => { + tracing::warn!("unable to extract DTSTART and DTEND from VEVENT"); + return false; + } + }; + + tracing::trace!(event_start=?dtstart, event_end=?dtend, filter=?time_range, "apply filter on VEVENT"); + match time_range { + cal::TimeRange::OnlyStart(after) => &dtend >= after, + cal::TimeRange::OnlyEnd(before) => &dtstart <= before, + cal::TimeRange::FullRange(after, before) => &dtend >= after && &dtstart <= before, + } +} + +use chrono::NaiveDateTime; +fn is_component_match( + components: &[icalendar::parser::Component], + filter: &cal::CompFilter, +) -> bool { + // Find the component among the list + let maybe_comp = components + .iter() + .find(|candidate| candidate.name.as_str() == filter.name.as_str()); + + // Filter according to rules + match (maybe_comp, &filter.additional_rules) { + (Some(_), None) => true, + (None, Some(cal::CompFilterRules::IsNotDefined)) => true, + (None, None) => false, + (Some(_), Some(cal::CompFilterRules::IsNotDefined)) => false, + (None, Some(cal::CompFilterRules::Matches(_))) => false, + (Some(component), Some(cal::CompFilterRules::Matches(matcher))) => { + // check time range + if let Some(time_range) = &matcher.time_range { + if !is_in_time_range(component.properties.as_ref(), time_range) { + return false; + } + } + + // check properties + if !is_properties_match(component.properties.as_ref(), matcher.prop_filter.as_ref()) { + return false; + } + + // check inner components + matcher.comp_filter.iter().all(|inner_filter| { + is_component_match(component.components.as_ref(), &inner_filter) + }) + } + } +}