aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorQuentin Dufour <quentin@deuxfleurs.fr>2024-05-25 19:30:59 +0200
committerQuentin Dufour <quentin@deuxfleurs.fr>2024-05-25 19:30:59 +0200
commit52f870633c2cab8a4aeeec74792774931139b8b5 (patch)
tree878d4ff16cdebd7fdfc50a278dbadedd7eb63480
parentff823a10f049e06c711537560ba10f3dc826afcd (diff)
downloadaerogramme-52f870633c2cab8a4aeeec74792774931139b8b5.tar.gz
aerogramme-52f870633c2cab8a4aeeec74792774931139b8b5.zip
add a new aero-ical module
-rw-r--r--Cargo.lock16
-rw-r--r--Cargo.toml1
-rw-r--r--aero-ical/Cargo.toml15
-rw-r--r--aero-ical/src/lib.rs8
-rw-r--r--aero-ical/src/parser.rs138
-rw-r--r--aero-ical/src/query.rs280
-rw-r--r--aero-proto/Cargo.toml1
-rw-r--r--aero-proto/src/dav/controller.rs183
-rw-r--r--aerogramme/tests/behavior.rs40
9 files changed, 500 insertions, 182 deletions
diff --git a/Cargo.lock b/Cargo.lock
index 9f8ccb6..d22a5fc 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -73,11 +73,23 @@ dependencies = [
]
[[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 <alex@adnab.me>", "Quentin Dufour <quentin@dufour.io>"]
+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<chrono::DateTime<chrono::Utc>> {
+ 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<chrono::DateTime<chrono::Utc>> {
+ properties
+ .iter()
+ .find(|candidate| candidate.name.as_str() == name)
+ .map(|p| p.val.as_str())
+ .map(parser::date_time)
+ .flatten()
+}
+
+fn prop_parse<T: std::str::FromStr>(
+ properties: &[icalendar::parser::Property],
+ name: &str,
+) -> Option<T> {
+ properties
+ .iter()
+ .find(|candidate| candidate.name.as_str() == name)
+ .map(|p| p.val.as_str().parse::<T>().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 &param.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<chrono::DateTime<chrono::Utc>> {
+ // 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::<chrono::Utc>::MAX_UTC),
+ cal::TimeRange::OnlyEnd(end) => (&chrono::DateTime::<chrono::Utc>::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::<i64>(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::<i64>(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<User>;
@@ -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<chrono::DateTime<chrono::Utc>> {
- 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<chrono::DateTime<chrono::Utc>> {
- 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 &param.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#" <?xml version="1.0" encoding="utf-8" ?>
+ let cal_query = r#"<?xml version="1.0" encoding="utf-8" ?>
<C:calendar-query xmlns:D="DAV:" xmlns:C="urn:ietf:params:xml:ns:caldav">
<D:prop>
<D:getetag/>
@@ -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#"<?xml version="1.0" encoding="utf-8" ?>
+ <C:calendar-query xmlns:C="urn:ietf:params:xml:ns:caldav">
+ <D:prop xmlns:D="DAV:">
+ <D:getetag/>
+ <C:calendar-data/>
+ </D:prop>
+ <C:filter>
+ <C:comp-filter name="VCALENDAR">
+ <C:comp-filter name="VTODO">
+ <C:comp-filter name="VALARM">
+ <C:time-range start="20060106T100000Z" end="20060107T100000Z"/>
+ </C:comp-filter>
+ </C:comp-filter>
+ </C:comp-filter>
+ </C:filter>
+ </C:calendar-query>"#;
+ 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::<dav::Multistatus<All>>(&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#"<?xml version="1.0" encoding="utf-8" ?>