fix: parsing components & times

This commit is contained in:
Quentin 2024-05-22 15:02:53 +02:00
parent 194e34d4e1
commit 6ca7082197
Signed by: quentin
GPG key ID: E9602264D639FF68
4 changed files with 130 additions and 110 deletions

View file

@ -287,7 +287,7 @@ impl QRead<Property> for Property {
.is_some() .is_some()
{ {
let dtstr = xml.tag_string().await?; 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?; xml.close().await?;
return Ok(Property::MaxDateTime(dt)); return Ok(Property::MaxDateTime(dt));
} }
@ -653,8 +653,8 @@ impl QRead<Expand> for Expand {
_ => return Err(ParsingError::MissingAttribute), _ => return Err(ParsingError::MissingAttribute),
}; };
let start = NaiveDateTime::parse_from_str(rstart.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(), ICAL_DATETIME_FMT)?.and_utc(); let end = NaiveDateTime::parse_from_str(rend.as_str(), CALDAV_DATETIME_FMT)?.and_utc();
if start > end { if start > end {
return Err(ParsingError::InvalidValue); return Err(ParsingError::InvalidValue);
} }
@ -672,8 +672,8 @@ impl QRead<LimitRecurrenceSet> for LimitRecurrenceSet {
_ => return Err(ParsingError::MissingAttribute), _ => return Err(ParsingError::MissingAttribute),
}; };
let start = NaiveDateTime::parse_from_str(rstart.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(), ICAL_DATETIME_FMT)?.and_utc(); let end = NaiveDateTime::parse_from_str(rend.as_str(), CALDAV_DATETIME_FMT)?.and_utc();
if start > end { if start > end {
return Err(ParsingError::InvalidValue); return Err(ParsingError::InvalidValue);
} }
@ -691,8 +691,8 @@ impl QRead<LimitFreebusySet> for LimitFreebusySet {
_ => return Err(ParsingError::MissingAttribute), _ => return Err(ParsingError::MissingAttribute),
}; };
let start = NaiveDateTime::parse_from_str(rstart.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(), ICAL_DATETIME_FMT)?.and_utc(); let end = NaiveDateTime::parse_from_str(rend.as_str(), CALDAV_DATETIME_FMT)?.and_utc();
if start > end { if start > end {
return Err(ParsingError::InvalidValue); return Err(ParsingError::InvalidValue);
} }
@ -918,13 +918,13 @@ impl QRead<TimeRange> for TimeRange {
let start = match xml.prev_attr("start") { let start = match xml.prev_attr("start") {
Some(r) => { 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, _ => None,
}; };
let end = match xml.prev_attr("end") { let end = match xml.prev_attr("end") {
Some(r) => { 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, _ => None,
}; };

View file

@ -178,7 +178,7 @@ impl QWrite for Property {
let start = xml.create_cal_element("min-date-time"); let start = xml.create_cal_element("min-date-time");
let end = start.to_end(); 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::Start(start.clone())).await?;
xml.q xml.q
.write_event_async(Event::Text(BytesText::new(dtstr.as_str()))) .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 start = xml.create_cal_element("max-date-time");
let end = start.to_end(); 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::Start(start.clone())).await?;
xml.q xml.q
.write_event_async(Event::Text(BytesText::new(dtstr.as_str()))) .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"); let mut empty = xml.create_cal_element("expand");
empty.push_attribute(( empty.push_attribute((
"start", "start",
format!("{}", self.0.format(ICAL_DATETIME_FMT)).as_str(), format!("{}", self.0.format(CALDAV_DATETIME_FMT)).as_str(),
)); ));
empty.push_attribute(( empty.push_attribute((
"end", "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 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"); let mut empty = xml.create_cal_element("limit-recurrence-set");
empty.push_attribute(( empty.push_attribute((
"start", "start",
format!("{}", self.0.format(ICAL_DATETIME_FMT)).as_str(), format!("{}", self.0.format(CALDAV_DATETIME_FMT)).as_str(),
)); ));
empty.push_attribute(( empty.push_attribute((
"end", "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 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"); let mut empty = xml.create_cal_element("limit-freebusy-set");
empty.push_attribute(( empty.push_attribute((
"start", "start",
format!("{}", self.0.format(ICAL_DATETIME_FMT)).as_str(), format!("{}", self.0.format(CALDAV_DATETIME_FMT)).as_str(),
)); ));
empty.push_attribute(( empty.push_attribute((
"end", "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 xml.q.write_event_async(Event::Empty(empty)).await
} }
@ -737,18 +737,21 @@ impl QWrite for TimeRange {
match self { match self {
Self::OnlyStart(start) => empty.push_attribute(( Self::OnlyStart(start) => empty.push_attribute((
"start", "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) => { Self::FullRange(start, end) => {
empty.push_attribute(( empty.push_attribute((
"start", "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 xml.q.write_event_async(Event::Empty(empty)).await

View file

@ -3,7 +3,8 @@
use super::types as dav; use super::types as dav;
use chrono::{DateTime, Utc}; 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 ACL (rfc3744) is missing, required
//@FIXME Versioning (rfc3253) is missing, required //@FIXME Versioning (rfc3253) is missing, required

View file

@ -353,81 +353,49 @@ fn apply_filter<'a>(
}; };
// Do checks // 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; let root_filter = &filter.0;
let fake_vcal_component = icalendar::parser::Component {
// Find the component in the filter name: cal::Component::VCalendar.as_str().into(),
let maybe_comp = ics properties: ics.properties,
.components components: 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)
}
}; };
tracing::debug!(filter=?root_filter, "calendar-query filter");
// Adjust return value according to filter // Adjust return value according to filter
match is_keep { match is_component_match(&[fake_vcal_component], root_filter) {
true => Some(Ok(single_node)), true => Some(Ok(single_node)),
_ => None, _ => None,
} }
}) })
} }
fn component_date( fn prop_date(
component: &icalendar::parser::Component, properties: &[icalendar::parser::Property],
prop: &str, name: &str,
) -> Option<chrono::DateTime<chrono::Utc>> { ) -> Option<chrono::DateTime<chrono::Utc>> {
component properties
.find_prop(prop) .iter()
.find(|candidate| candidate.name.as_str() == name)
.map(|p| p.val.as_str()) .map(|p| p.val.as_str())
.map(|raw_dtstart| { .map(|raw_time| {
NaiveDateTime::parse_from_str(raw_dtstart, cal::ICAL_DATETIME_FMT) tracing::trace!(raw_time = raw_time, "VEVENT raw time");
NaiveDateTime::parse_from_str(raw_time, cal::ICAL_DATETIME_FMT)
.ok() .ok()
.map(|v| v.and_utc()) .map(|v| v.and_utc())
}) })
.flatten() .flatten()
} }
use chrono::NaiveDateTime; fn is_properties_match(props: &[icalendar::parser::Property], filters: &[cal::PropFilter]) -> bool {
fn is_component_match( filters.iter().all(|single_filter| {
component: &icalendar::parser::Component, // Find the property
matcher: &cal::CompFilterMatch, let single_prop = props
) -> bool { .iter()
if let Some(time_range) = &matcher.time_range { .find(|candidate| candidate.name.as_str() == single_filter.name.0.as_str());
let (dtstart, dtend) = match ( match (&single_filter.additional_rules, single_prop) {
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()),
) {
(None, Some(_)) | (Some(cal::PropFilterRules::IsNotDefined), None) => true, (None, Some(_)) | (Some(cal::PropFilterRules::IsNotDefined), None) => true,
(None, None) (None, None)
| (Some(cal::PropFilterRules::IsNotDefined), Some(_)) | (Some(cal::PropFilterRules::IsNotDefined), Some(_))
@ -436,11 +404,16 @@ fn is_component_match(
// check value // check value
match &pattern.time_or_text { match &pattern.time_or_text {
Some(cal::TimeOrText::Time(time_range)) => { Some(cal::TimeOrText::Time(time_range)) => {
// try parse entry as date let maybe_parsed_date = NaiveDateTime::parse_from_str(
let parsed_date = prop.val.as_str(),
match component_date(component, single_prop_filter.name.0.as_str()) { cal::ICAL_DATETIME_FMT,
Some(v) => v, )
.ok()
.map(|v| v.and_utc());
let parsed_date = match maybe_parsed_date {
None => return false, None => return false,
Some(v) => v,
}; };
// see if entry is in range // see if entry is in range
@ -501,27 +474,70 @@ fn is_component_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; return false;
} }
};
matcher.comp_filter.iter().all(|single_comp_filter| { tracing::trace!(event_start=?dtstart, event_end=?dtend, filter=?time_range, "apply filter on VEVENT");
// Find the component match time_range {
let maybe_comp = component cal::TimeRange::OnlyStart(after) => &dtend >= after,
.components 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() .iter()
.find(|candidate| candidate.name.as_str() == single_comp_filter.name.as_str()); .find(|candidate| candidate.name.as_str() == filter.name.as_str());
// Filter according to rules // Filter according to rules
match (maybe_comp, &single_comp_filter.additional_rules) { match (maybe_comp, &filter.additional_rules) {
(Some(_), None) => true, (Some(_), None) => true,
(None, Some(cal::CompFilterRules::IsNotDefined)) => true, (None, Some(cal::CompFilterRules::IsNotDefined)) => true,
(None, None) => false, (None, None) => false,
(Some(_), Some(cal::CompFilterRules::IsNotDefined)) => false, (Some(_), Some(cal::CompFilterRules::IsNotDefined)) => false,
(None, Some(cal::CompFilterRules::Matches(_))) => false, (None, Some(cal::CompFilterRules::Matches(_))) => false,
(Some(inner_comp), Some(cal::CompFilterRules::Matches(comp_match))) => { (Some(component), Some(cal::CompFilterRules::Matches(matcher))) => {
is_component_match(inner_comp, comp_match) // 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)
}) })
}
}
} }