From 52f870633c2cab8a4aeeec74792774931139b8b5 Mon Sep 17 00:00:00 2001 From: Quentin Dufour Date: Sat, 25 May 2024 19:30:59 +0200 Subject: add a new aero-ical module --- Cargo.lock | 16 ++- Cargo.toml | 1 + aero-ical/Cargo.toml | 15 +++ aero-ical/src/lib.rs | 8 ++ aero-ical/src/parser.rs | 138 +++++++++++++++++++ aero-ical/src/query.rs | 280 +++++++++++++++++++++++++++++++++++++++ aero-proto/Cargo.toml | 1 + aero-proto/src/dav/controller.rs | 183 +------------------------ aerogramme/tests/behavior.rs | 40 +++++- 9 files changed, 500 insertions(+), 182 deletions(-) create mode 100644 aero-ical/Cargo.toml create mode 100644 aero-ical/src/lib.rs create mode 100644 aero-ical/src/parser.rs create mode 100644 aero-ical/src/query.rs diff --git a/Cargo.lock b/Cargo.lock index 9f8ccb6..d22a5fc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -72,12 +72,24 @@ dependencies = [ "tokio", ] +[[package]] +name = "aero-ical" +version = "0.3.0" +dependencies = [ + "aero-dav", + "chrono", + "icalendar", + "nom 7.1.3", + "tracing", +] + [[package]] name = "aero-proto" version = "0.3.0" dependencies = [ "aero-collections", "aero-dav", + "aero-ical", "aero-sasl", "aero-user", "anyhow", @@ -1110,9 +1122,9 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "chrono" -version = "0.4.35" +version = "0.4.38" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8eaf5903dcbc0a39312feb77df2ff4c76387d591b9fc7b04a238dcf8bb62639a" +checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401" dependencies = [ "android-tzdata", "iana-time-zone", diff --git a/Cargo.toml b/Cargo.toml index 0ee7889..91c6413 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,6 +19,7 @@ aero-user = { version = "0.3.0", path = "aero-user" } aero-bayou = { version = "0.3.0", path = "aero-bayou" } aero-sasl = { version = "0.3.0", path = "aero-sasl" } aero-dav = { version = "0.3.0", path = "aero-dav" } +aero-ical = { version = "0.3.0", path = "aero-ical" } aero-collections = { version = "0.3.0", path = "aero-collections" } aero-proto = { version = "0.3.0", path = "aero-proto" } aerogramme = { version = "0.3.0", path = "aerogramme" } diff --git a/aero-ical/Cargo.toml b/aero-ical/Cargo.toml new file mode 100644 index 0000000..6cfe882 --- /dev/null +++ b/aero-ical/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "aero-ical" +version = "0.3.0" +authors = ["Alex Auvolat ", "Quentin Dufour "] +edition = "2021" +license = "EUPL-1.2" +description = "An iCalendar parser" + +[dependencies] +aero-dav.workspace = true + +icalendar.workspace = true +nom.workspace = true +chrono.workspace = true +tracing.workspace = true diff --git a/aero-ical/src/lib.rs b/aero-ical/src/lib.rs new file mode 100644 index 0000000..808c885 --- /dev/null +++ b/aero-ical/src/lib.rs @@ -0,0 +1,8 @@ +/// The iCalendar module is not yet properly rewritten +/// Instead we heavily rely on the icalendar library +/// However, for many reason, it's not satisfying: +/// the goal will be to rewrite it in the end so it better +/// integrates into Aerogramme + +pub mod parser; +pub mod query; diff --git a/aero-ical/src/parser.rs b/aero-ical/src/parser.rs new file mode 100644 index 0000000..4354737 --- /dev/null +++ b/aero-ical/src/parser.rs @@ -0,0 +1,138 @@ +use chrono::TimeDelta; + +use nom::IResult; +use nom::branch::alt; +use nom::bytes::complete::{tag, tag_no_case}; +use nom::combinator::{value, opt, map, map_opt}; +use nom::sequence::{pair, tuple}; +use nom::character::complete as nomchar; + +use aero_dav::caltypes as cal; + +//@FIXME too simple, we have 4 cases in practices: +// - floating datetime +// - floating datetime with a tzid as param so convertible to tz datetime +// - utc datetime +// - floating(?) date (without time) +pub fn date_time(dt: &str) -> Option> { + tracing::trace!(raw_time = dt, "VEVENT raw time"); + let tmpl = match dt.chars().last() { + Some('Z') => cal::UTC_DATETIME_FMT, + Some(_) => { + tracing::warn!(raw_time=dt, "floating datetime is not properly supported yet"); + cal::FLOATING_DATETIME_FMT + }, + None => return None + }; + + chrono::NaiveDateTime::parse_from_str(dt, tmpl) + .ok() + .map(|v| v.and_utc()) +} + +/// RFC3389 Duration Value +/// +/// ```abnf +/// dur-value = (["+"] / "-") "P" (dur-date / dur-time / dur-week) +/// dur-date = dur-day [dur-time] +/// dur-time = "T" (dur-hour / dur-minute / dur-second) +/// dur-week = 1*DIGIT "W" +/// dur-hour = 1*DIGIT "H" [dur-minute] +/// dur-minute = 1*DIGIT "M" [dur-second] +/// dur-second = 1*DIGIT "S" +/// dur-day = 1*DIGIT "D" +/// ``` +pub fn dur_value(text: &str) -> IResult<&str, TimeDelta> { + map_opt(tuple(( + dur_sign, + tag_no_case("P"), + alt(( + dur_date, + dur_time, + dur_week, + )) + )), |(sign, _, delta)| { + delta.checked_mul(sign) + })(text) +} + +fn dur_sign(text: &str) -> IResult<&str, i32> { + map(opt(alt((value(1, tag("+")), value(-1, tag("-"))))), |x| x.unwrap_or(1))(text) +} +fn dur_date(text: &str) -> IResult<&str, TimeDelta> { + map(pair(dur_day, opt(dur_time)), |(day, time)| day + time.unwrap_or(TimeDelta::zero()))(text) +} +fn dur_time(text: &str) -> IResult<&str, TimeDelta> { + map(pair(tag_no_case("T"), alt((dur_hour, dur_minute, dur_second))), |(_, x)| x)(text) +} +fn dur_week(text: &str) -> IResult<&str, TimeDelta> { + map_opt(pair(nomchar::i64, tag_no_case("W")), |(i, _)| TimeDelta::try_weeks(i))(text) +} +fn dur_day(text: &str) -> IResult<&str, TimeDelta> { + map_opt(pair(nomchar::i64, tag_no_case("D")), |(i, _)| TimeDelta::try_days(i))(text) +} +fn dur_hour(text: &str) -> IResult<&str, TimeDelta> { + map_opt(tuple((nomchar::i64, tag_no_case("H"), opt(dur_minute))), |(i, _, mm)| { + TimeDelta::try_hours(i).map(|hours| hours + mm.unwrap_or(TimeDelta::zero())) + })(text) +} +fn dur_minute(text: &str) -> IResult<&str, TimeDelta> { + map_opt(tuple((nomchar::i64, tag_no_case("M"), opt(dur_second))), |(i, _, ms)| { + TimeDelta::try_minutes(i).map(|min| min + ms.unwrap_or(TimeDelta::zero())) + })(text) +} +fn dur_second(text: &str) -> IResult<&str, TimeDelta> { + map_opt(pair(nomchar::i64, tag_no_case("S")), |(i, _)| TimeDelta::try_seconds(i))(text) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn rfc5545_example1() { + // A duration of 15 days, 5 hours, and 20 seconds would be: + let to_parse = "P15DT5H0M20S"; + let (_, time_delta) = dur_value(to_parse).unwrap(); + assert_eq!( + time_delta, + TimeDelta::try_days(15).unwrap() + TimeDelta::try_hours(5).unwrap() + TimeDelta::try_seconds(20).unwrap()); + } + + #[test] + fn rfc5545_example2() { + // A duration of 7 weeks would be: + let to_parse = "P7W"; + let (_, time_delta) = dur_value(to_parse).unwrap(); + assert_eq!( + time_delta, + TimeDelta::try_weeks(7).unwrap() + ); + } + + #[test] + fn rfc4791_example1() { + // 10 minutes before + let to_parse = "-PT10M"; + + let (_, time_delta) = dur_value(to_parse).unwrap(); + assert_eq!( + time_delta, + TimeDelta::try_minutes(-10).unwrap() + ); + } + + + #[test] + fn ical_org_example1() { + // The following example is for a "VALARM" calendar component that specifies an email alarm + // that will trigger 2 days before the scheduled due DATE-TIME of a to-do with which it is associated. + let to_parse = "-P2D"; + + let (_, time_delta) = dur_value(to_parse).unwrap(); + assert_eq!( + time_delta, + TimeDelta::try_days(-2).unwrap() + ); + } +} diff --git a/aero-ical/src/query.rs b/aero-ical/src/query.rs new file mode 100644 index 0000000..5d857bb --- /dev/null +++ b/aero-ical/src/query.rs @@ -0,0 +1,280 @@ +use aero_dav::caltypes as cal; +use crate::parser as parser; + +pub fn is_component_match( + parent: &icalendar::parser::Component, + 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(&filter.name, parent, 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, component.components.as_ref(), &inner_filter) + }) + } + } +} + +fn prop_date( + properties: &[icalendar::parser::Property], + name: &str, +) -> Option> { + properties + .iter() + .find(|candidate| candidate.name.as_str() == name) + .map(|p| p.val.as_str()) + .map(parser::date_time) + .flatten() +} + +fn prop_parse( + properties: &[icalendar::parser::Property], + name: &str, +) -> Option { + properties + .iter() + .find(|candidate| candidate.name.as_str() == name) + .map(|p| p.val.as_str().parse::().ok()) + .flatten() +} + +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(_)) + | (Some(cal::PropFilterRules::Match(_)), None) => false, + (Some(cal::PropFilterRules::Match(pattern)), Some(prop)) => { + // check value + match &pattern.time_or_text { + Some(cal::TimeOrText::Time(time_range)) => { + let maybe_parsed_date = parser::date_time(prop.val.as_str()); + + 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 { + cal::TimeRange::OnlyStart(after) => &parsed_date >= after, + cal::TimeRange::OnlyEnd(before) => &parsed_date <= before, + cal::TimeRange::FullRange(after, before) => { + &parsed_date >= after && &parsed_date <= before + } + }; + if !is_in_range { + return false; + } + + // if you are here, this subcondition is valid + } + Some(cal::TimeOrText::Text(txt_match)) => { + //@FIXME ignoring collation + let is_match = match txt_match.negate_condition { + None | Some(false) => { + prop.val.as_str().contains(txt_match.text.as_str()) + } + Some(true) => !prop.val.as_str().contains(txt_match.text.as_str()), + }; + if !is_match { + return false; + } + } + None => (), // if not filter on value is set, continue + }; + + // check parameters + pattern.param_filter.iter().all(|single_param_filter| { + let maybe_param = prop.params.iter().find(|candidate| { + candidate.key.as_str() == single_param_filter.name.as_str() + }); + + match (maybe_param, &single_param_filter.additional_rules) { + (Some(_), None) => true, + (None, None) => false, + (Some(_), Some(cal::ParamFilterMatch::IsNotDefined)) => false, + (None, Some(cal::ParamFilterMatch::IsNotDefined)) => true, + (None, Some(cal::ParamFilterMatch::Match(_))) => false, + (Some(param), Some(cal::ParamFilterMatch::Match(txt_match))) => { + let param_val = match ¶m.val { + Some(v) => v, + None => return false, + }; + + match txt_match.negate_condition { + None | Some(false) => { + param_val.as_str().contains(txt_match.text.as_str()) + } + Some(true) => !param_val.as_str().contains(txt_match.text.as_str()), + } + } + } + }) + } + } + }) +} + +fn resolve_trigger( + parent: &icalendar::parser::Component, + properties: &[icalendar::parser::Property] +) -> Option> { + // A. Do we have a TRIGGER property? If not, returns early + let maybe_trigger_prop = properties + .iter() + .find(|candidate| candidate.name.as_str() == "TRIGGER"); + + let trigger_prop = match maybe_trigger_prop { + None => return None, + Some(v) => v, + }; + + // B.1 Is it an absolute datetime? If so, returns early + let maybe_absolute = trigger_prop.params.iter() + .find(|param| param.key.as_str() == "VALUE") + .map(|param| param.val.as_ref()).flatten() + .map(|v| v.as_str() == "DATE-TIME"); + + if maybe_absolute.is_some() { + return prop_date(properties, "TRIGGER"); + } + + // B.2 Otherwise it's a timedelta relative to a parent field. + // C.1 Parse the timedelta value, returns early if invalid + + // C.2 Get the parent reference absolute datetime, returns early if invalid + let maybe_related_field = trigger_prop + .params + .iter() + .find(|param| param.key.as_str() == "RELATED") + .map(|param| param.val.as_ref()) + .flatten(); + let related_field = maybe_related_field.map(|v| v.as_str()).unwrap_or("DTSTART"); + let parent_date = match prop_date(parent.properties.as_ref(), related_field) { + Some(v) => v, + _ => return None, + }; + + // C.3 Compute the final date from the base date + timedelta + + todo!() +} + +fn is_in_time_range( + component: &cal::Component, + parent: &icalendar::parser::Component, + properties: &[icalendar::parser::Property], + time_range: &cal::TimeRange, +) -> bool { + //@FIXME timezones are not properly handled currently (everything is UTC) + //@FIXME does not support repeat + //ref: https://datatracker.ietf.org/doc/html/rfc4791#section-9.9 + let (start, end) = match time_range { + cal::TimeRange::OnlyStart(start) => (start, &chrono::DateTime::::MAX_UTC), + cal::TimeRange::OnlyEnd(end) => (&chrono::DateTime::::MIN_UTC, end), + cal::TimeRange::FullRange(start, end) => (start, end), + }; + + match component { + cal::Component::VEvent => { + let dtstart = match prop_date(properties, "DTSTART") { + Some(v) => v, + _ => return false, + }; + let maybe_dtend = prop_date(properties, "DTEND"); + let maybe_duration = prop_parse::(properties, "DURATION").map(|d| chrono::TimeDelta::new(std::cmp::max(d, 0), 0)).flatten(); + + //@FIXME missing "date" management (only support "datetime") + match (&maybe_dtend, &maybe_duration) { + // | Y | N | N | * | (start < DTEND AND end > DTSTART) | + (Some(dtend), _) => start < dtend && end > &dtstart, + // | N | Y | Y | * | (start < DTSTART+DURATION AND end > DTSTART) | + (_, Some(duration)) => *start <= dtstart + *duration && end > &dtstart, + // | N | N | N | Y | (start <= DTSTART AND end > DTSTART) | + _ => start <= &dtstart && end > &dtstart, + } + }, + cal::Component::VTodo => { + let maybe_dtstart = prop_date(properties, "DTSTART"); + let maybe_due = prop_date(properties, "DUE"); + let maybe_completed = prop_date(properties, "COMPLETED"); + let maybe_created = prop_date(properties, "CREATED"); + let maybe_duration = prop_parse::(properties, "DURATION").map(|d| chrono::TimeDelta::new(d, 0)).flatten(); + + match (maybe_dtstart, maybe_duration, maybe_due, maybe_completed, maybe_created) { + // | Y | Y | N | * | * | (start <= DTSTART+DURATION) AND | + // | | | | | | ((end > DTSTART) OR | + // | | | | | | (end >= DTSTART+DURATION)) | + (Some(dtstart), Some(duration), None, _, _) => *start <= dtstart + duration && (*end > dtstart || *end >= dtstart + duration), + // | Y | N | Y | * | * | ((start < DUE) OR (start <= DTSTART)) | + // | | | | | | AND | + // | | | | | | ((end > DTSTART) OR (end >= DUE)) | + (Some(dtstart), None, Some(due), _, _) => (*start < due || *start <= dtstart) && (*end > dtstart || *end >= due), + // | Y | N | N | * | * | (start <= DTSTART) AND (end > DTSTART) | + (Some(dtstart), None, None, _, _) => *start <= dtstart && *end > dtstart, + // | N | N | Y | * | * | (start < DUE) AND (end >= DUE) | + (None, None, Some(due), _, _) => *start < due && *end >= due, + // | N | N | N | Y | Y | ((start <= CREATED) OR (start <= COMPLETED))| + // | | | | | | AND | + // | | | | | | ((end >= CREATED) OR (end >= COMPLETED))| + (None, None, None, Some(completed), Some(created)) => (*start <= created || *start <= completed) && (*end >= created || *end >= completed), + // | N | N | N | Y | N | (start <= COMPLETED) AND (end >= COMPLETED) | + (None, None, None, Some(completed), None) => *start <= completed && *end >= completed, + // | N | N | N | N | Y | (end > CREATED) | + (None, None, None, None, Some(created)) => *end > created, + // | N | N | N | N | N | TRUE | + _ => true, + } + }, + cal::Component::VJournal => { + let maybe_dtstart = prop_date(properties, "DTSTART"); + match maybe_dtstart { + // | Y | Y | (start <= DTSTART) AND (end > DTSTART) | + Some(dtstart) => *start <= dtstart && *end > dtstart, + // | N | * | FALSE | + None => false, + } + }, + cal::Component::VFreeBusy => { + //@FIXME freebusy is not supported yet + false + }, + cal::Component::VAlarm => { + //@FIXME does not support REPEAT + let maybe_trigger = resolve_trigger(parent, properties); + // (start <= trigger-time) AND (end > trigger-time) + false + }, + _ => false, + } +} diff --git a/aero-proto/Cargo.toml b/aero-proto/Cargo.toml index b6f6336..e8d6b8f 100644 --- a/aero-proto/Cargo.toml +++ b/aero-proto/Cargo.toml @@ -7,6 +7,7 @@ license = "EUPL-1.2" description = "Binding between Aerogramme's internal components and well-known protocols" [dependencies] +aero-ical.workspace = true aero-sasl.workspace = true aero-dav.workspace = true aero-user.workspace = true diff --git a/aero-proto/src/dav/controller.rs b/aero-proto/src/dav/controller.rs index 4cf520e..873f768 100644 --- a/aero-proto/src/dav/controller.rs +++ b/aero-proto/src/dav/controller.rs @@ -1,6 +1,6 @@ use anyhow::Result; use futures::stream::{StreamExt, TryStreamExt}; -use http_body_util::combinators::{BoxBody, UnsyncBoxBody}; +use http_body_util::combinators::UnsyncBoxBody; use http_body_util::BodyStream; use http_body_util::StreamBody; use hyper::body::Frame; @@ -11,10 +11,11 @@ use aero_collections::user::User; use aero_dav::caltypes as cal; use aero_dav::realization::All; use aero_dav::types as dav; +use aero_ical::query::is_component_match; use crate::dav::codec; use crate::dav::codec::{depth, deserialize, serialize, text_body}; -use crate::dav::node::{DavNode, PutPolicy}; +use crate::dav::node::DavNode; use crate::dav::resource::RootNode; pub(super) type ArcUser = std::sync::Arc; @@ -373,185 +374,9 @@ fn apply_filter<'a>( tracing::debug!(filter=?root_filter, "calendar-query filter"); // Adjust return value according to filter - match is_component_match(&[fake_vcal_component], root_filter) { + match is_component_match(&fake_vcal_component, &[fake_vcal_component.clone()], root_filter) { true => Some(Ok(single_node)), _ => None, } }) } - -fn ical_parse_date(dt: &str) -> Option> { - tracing::trace!(raw_time = dt, "VEVENT raw time"); - let tmpl = match dt.chars().last() { - Some('Z') => cal::UTC_DATETIME_FMT, - Some(_) => { - tracing::warn!(raw_time=dt, "floating datetime is not properly supported yet"); - cal::FLOATING_DATETIME_FMT - }, - None => return None - }; - - NaiveDateTime::parse_from_str(dt, tmpl) - .ok() - .map(|v| v.and_utc()) -} - -fn prop_date( - properties: &[icalendar::parser::Property], - name: &str, -) -> Option> { - properties - .iter() - .find(|candidate| candidate.name.as_str() == name) - .map(|p| p.val.as_str()) - .map(ical_parse_date) - .flatten() -} - -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(_)) - | (Some(cal::PropFilterRules::Match(_)), None) => false, - (Some(cal::PropFilterRules::Match(pattern)), Some(prop)) => { - // check value - match &pattern.time_or_text { - Some(cal::TimeOrText::Time(time_range)) => { - let maybe_parsed_date = ical_parse_date(prop.val.as_str()); - - 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 { - cal::TimeRange::OnlyStart(after) => &parsed_date >= after, - cal::TimeRange::OnlyEnd(before) => &parsed_date <= before, - cal::TimeRange::FullRange(after, before) => { - &parsed_date >= after && &parsed_date <= before - } - }; - if !is_in_range { - return false; - } - - // if you are here, this subcondition is valid - } - Some(cal::TimeOrText::Text(txt_match)) => { - //@FIXME ignoring collation - let is_match = match txt_match.negate_condition { - None | Some(false) => { - prop.val.as_str().contains(txt_match.text.as_str()) - } - Some(true) => !prop.val.as_str().contains(txt_match.text.as_str()), - }; - if !is_match { - return false; - } - } - None => (), // if not filter on value is set, continue - }; - - // check parameters - pattern.param_filter.iter().all(|single_param_filter| { - let maybe_param = prop.params.iter().find(|candidate| { - candidate.key.as_str() == single_param_filter.name.as_str() - }); - - match (maybe_param, &single_param_filter.additional_rules) { - (Some(_), None) => true, - (None, None) => false, - (Some(_), Some(cal::ParamFilterMatch::IsNotDefined)) => false, - (None, Some(cal::ParamFilterMatch::IsNotDefined)) => true, - (None, Some(cal::ParamFilterMatch::Match(_))) => false, - (Some(param), Some(cal::ParamFilterMatch::Match(txt_match))) => { - let param_val = match ¶m.val { - Some(v) => v, - None => return false, - }; - - match txt_match.negate_condition { - None | Some(false) => { - param_val.as_str().contains(txt_match.text.as_str()) - } - Some(true) => !param_val.as_str().contains(txt_match.text.as_str()), - } - } - } - }) - } - } - }) -} - -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) - }) - } - } -} diff --git a/aerogramme/tests/behavior.rs b/aerogramme/tests/behavior.rs index b6c1c6e..0e6dab6 100644 --- a/aerogramme/tests/behavior.rs +++ b/aerogramme/tests/behavior.rs @@ -684,6 +684,7 @@ fn rfc4791_webdav_caldav() { ) .send()?; //@FIXME not yet supported. returns DAV: 1 ; expects DAV: 1 calendar-access + // Not used by any client I know, so not implementing it now. // --- REPORT calendar-query --- //@FIXME missing support for calendar-data... @@ -729,7 +730,7 @@ fn rfc4791_webdav_caldav() { }); // 8.2.1.2. Synchronize by Time Range (here: July 2006) - let cal_query = r#" + let cal_query = r#" @@ -754,6 +755,43 @@ fn rfc4791_webdav_caldav() { assert_eq!(multistatus.responses.len(), 1); check_cal(&multistatus, ("/alice/calendar/Personal/rfc2.ics", Some(obj2_etag.to_str().expect("etag header convertible to str")), None)); + // 7.8.5. Example: Retrieval of To-Dos by Alarm Time Range + let cal_query = r#" + + + + + + + + + + + + + + + "#; + let resp = http + .request( + reqwest::Method::from_bytes(b"REPORT")?, + "http://localhost:8087/alice/calendar/Personal/", + ) + .body(cal_query) + .send()?; + assert_eq!(resp.status(), 207); + let multistatus = dav_deserialize::>(&resp.text()?); + //assert_eq!(multistatus.responses.len(), 1); + + // 7.8.6. Example: Retrieval of Event by UID + // @TODO + + // 7.8.7. Example: Retrieval of Events by PARTSTAT + // @TODO + + // 7.8.9. Example: Retrieval of All Pending To-Dos + // @TODO + // --- REPORT calendar-multiget --- let cal_query = r#" -- cgit v1.2.3