fix: parsing components & times
This commit is contained in:
parent
194e34d4e1
commit
6ca7082197
4 changed files with 130 additions and 110 deletions
|
@ -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,
|
||||||
};
|
};
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
})
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue