From d5a222967dbc774ad04cff572a0d901c832b36bf Mon Sep 17 00:00:00 2001 From: Quentin Dufour Date: Sun, 26 May 2024 11:03:39 +0200 Subject: [PATCH] support multiple same name components, properties & parameters --- aero-ical/src/query.rs | 160 ++++++++++++++------------- aerogramme/tests/behavior.rs | 156 ++++++++++++++++++++------ aerogramme/tests/common/constants.rs | 38 +++++++ 3 files changed, 244 insertions(+), 110 deletions(-) diff --git a/aero-ical/src/query.rs b/aero-ical/src/query.rs index 440441f..d69a919 100644 --- a/aero-ical/src/query.rs +++ b/aero-ical/src/query.rs @@ -7,19 +7,18 @@ pub fn is_component_match( filter: &cal::CompFilter, ) -> bool { // Find the component among the list - //@FIXME do not handle correctly multiple entities (eg. 3 VEVENT) - let maybe_comp = components + let maybe_comps = components .iter() - .find(|candidate| candidate.name.as_str() == filter.name.as_str()); + .filter(|candidate| candidate.name.as_str() == filter.name.as_str()) + .collect::>(); // 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))) => { + match (&maybe_comps[..], &filter.additional_rules) { + ([_, ..], None) => true, + ([], Some(cal::CompFilterRules::IsNotDefined)) => true, + ([], None) => false, + ([_, ..], Some(cal::CompFilterRules::IsNotDefined)) => false, + (comps, Some(cal::CompFilterRules::Matches(matcher))) => comps.iter().any(|component| { // check time range if let Some(time_range) = &matcher.time_range { if !is_in_time_range( @@ -41,7 +40,7 @@ pub fn is_component_match( matcher.comp_filter.iter().all(|inner_filter| { is_component_match(component, component.components.as_ref(), &inner_filter) }) - } + }), } } @@ -71,80 +70,89 @@ fn prop_parse( fn is_properties_match(props: &[icalendar::parser::Property], filters: &[cal::PropFilter]) -> bool { filters.iter().all(|single_filter| { // Find the property - let single_prop = props + let candidate_props = 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()); + .filter(|candidate| candidate.name.as_str() == single_filter.name.0.as_str()) + .collect::>(); - let parsed_date = match maybe_parsed_date { - None => return false, - Some(v) => v, - }; + match (&single_filter.additional_rules, &candidate_props[..]) { + (None, [_, ..]) | (Some(cal::PropFilterRules::IsNotDefined), []) => true, + (None, []) | (Some(cal::PropFilterRules::IsNotDefined), [_, ..]) => false, + (Some(cal::PropFilterRules::Match(pattern)), multi_props) => { + multi_props.iter().any(|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()); - // 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, + let parsed_date = match maybe_parsed_date { None => return false, + Some(v) => v, }; - match txt_match.negate_condition { - None | Some(false) => { - param_val.as_str().contains(txt_match.text.as_str()) + // 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 } - Some(true) => !param_val.as_str().contains(txt_match.text.as_str()), + }; + 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 multi_param = prop + .params + .iter() + .filter(|candidate| { + candidate.key.as_str() == single_param_filter.name.as_str() + }) + .collect::>(); + + match (&multi_param[..], &single_param_filter.additional_rules) { + ([.., _], None) => true, + ([], None) => false, + ([.., _], Some(cal::ParamFilterMatch::IsNotDefined)) => false, + ([], Some(cal::ParamFilterMatch::IsNotDefined)) => true, + (many_params, Some(cal::ParamFilterMatch::Match(txt_match))) => { + many_params.iter().any(|param| { + 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()) + } + } + }) + } + } + }) }) } } diff --git a/aerogramme/tests/behavior.rs b/aerogramme/tests/behavior.rs index d6c73e3..ef58182 100644 --- a/aerogramme/tests/behavior.rs +++ b/aerogramme/tests/behavior.rs @@ -554,7 +554,7 @@ fn rfc4791_webdav_caldav() { println!("🧪 rfc4791_webdav_caldav"); common::aerogramme_provider_daemon_dev(|_imap, _lmtp, http| { // --- INITIAL TEST SETUP --- - // Add entries (3 VEVENT, 1 FREEBUSY, 1 VTODO) + // Add entries let resp = http .put("http://localhost:8087/alice/calendar/Personal/rfc1.ics") .header("If-None-Match", "*") @@ -595,7 +595,14 @@ fn rfc4791_webdav_caldav() { .header("If-None-Match", "*") .body(ICAL_RFC6) .send()?; - let _obj6_etag = resp.headers().get("etag").expect("etag must be set"); + let obj6_etag = resp.headers().get("etag").expect("etag must be set"); + assert_eq!(resp.status(), 201); + let resp = http + .put("http://localhost:8087/alice/calendar/Personal/rfc7.ics") + .header("If-None-Match", "*") + .body(ICAL_RFC7) + .send()?; + let obj7_etag = resp.headers().get("etag").expect("etag must be set"); assert_eq!(resp.status(), 201); // A generic function to check a query result @@ -684,9 +691,44 @@ 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-multiget --- + let cal_query = r#" + + + + + + /alice/calendar/Personal/rfc1.ics + /alice/calendar/Personal/rfc3.ics + "#; + 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(), 2); + [ + ("/alice/calendar/Personal/rfc1.ics", obj1_etag, ICAL_RFC1), + ("/alice/calendar/Personal/rfc3.ics", obj3_etag, ICAL_RFC3), + ] + .iter() + .for_each(|(ref_path, ref_etag, ref_ical)| { + check_cal( + &multistatus, + ( + ref_path, + Some(ref_etag.to_str().expect("etag header convertible to str")), + Some(ref_ical), + ), + ) + }); - // --- REPORT calendar-query --- - //@FIXME missing support for calendar-data... + // --- REPORT calendar-query, only filtering --- // 7.8.8. Example: Retrieval of Events Only let cal_query = r#" @@ -709,12 +751,13 @@ fn rfc4791_webdav_caldav() { .send()?; assert_eq!(resp.status(), 207); let multistatus = dav_deserialize::>(&resp.text()?); - assert_eq!(multistatus.responses.len(), 3); + assert_eq!(multistatus.responses.len(), 4); [ ("/alice/calendar/Personal/rfc1.ics", obj1_etag, ICAL_RFC1), ("/alice/calendar/Personal/rfc2.ics", obj2_etag, ICAL_RFC2), ("/alice/calendar/Personal/rfc3.ics", obj3_etag, ICAL_RFC3), + ("/alice/calendar/Personal/rfc7.ics", obj7_etag, ICAL_RFC7), ] .iter() .for_each(|(ref_path, ref_etag, ref_ical)| { @@ -788,26 +831,32 @@ fn rfc4791_webdav_caldav() { assert_eq!(resp.status(), 207); let multistatus = dav_deserialize::>(&resp.text()?); assert_eq!(multistatus.responses.len(), 1); + check_cal( + &multistatus, + ( + "/alice/calendar/Personal/rfc6.ics", + Some(obj6_etag.to_str().expect("etag header convertible to str")), + Some(ICAL_RFC6), + ), + ); // 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#" - - + + - /alice/calendar/Personal/rfc1.ics - /alice/calendar/Personal/rfc3.ics - "#; + + + + + DC6C50A017428C5216A2F1CD@example.com + + + + + "#; let resp = http .request( reqwest::Method::from_bytes(b"REPORT")?, @@ -817,22 +866,61 @@ fn rfc4791_webdav_caldav() { .send()?; assert_eq!(resp.status(), 207); let multistatus = dav_deserialize::>(&resp.text()?); - assert_eq!(multistatus.responses.len(), 2); - [ - ("/alice/calendar/Personal/rfc1.ics", obj1_etag, ICAL_RFC1), - ("/alice/calendar/Personal/rfc3.ics", obj3_etag, ICAL_RFC3), - ] - .iter() - .for_each(|(ref_path, ref_etag, ref_ical)| { - check_cal( - &multistatus, - ( - ref_path, - Some(ref_etag.to_str().expect("etag header convertible to str")), - Some(ref_ical), - ), + assert_eq!(multistatus.responses.len(), 1); + check_cal( + &multistatus, + ( + "/alice/calendar/Personal/rfc3.ics", + Some(obj3_etag.to_str().expect("etag header convertible to str")), + Some(ICAL_RFC3), + ), + ); + + + // 7.8.7. Example: Retrieval of Events by PARTSTAT + let cal_query = r#" + + + + + + + + + + mailto:lisa@example.com + + NEEDS-ACTION + + + + + + "#; + 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); + check_cal( + &multistatus, + ( + "/alice/calendar/Personal/rfc7.ics", + Some(obj7_etag.to_str().expect("etag header convertible to str")), + Some(ICAL_RFC7), + ), + ); + + // 7.8.9. Example: Retrieval of All Pending To-Dos + // @TODO + + // --- REPORT calendar-query, with calendar-data tx --- + //@FIXME add support for calendar-data... Ok(()) }) diff --git a/aerogramme/tests/common/constants.rs b/aerogramme/tests/common/constants.rs index 91ee159..c04bae0 100644 --- a/aerogramme/tests/common/constants.rs +++ b/aerogramme/tests/common/constants.rs @@ -175,3 +175,41 @@ END:VALARM END:VTODO END:VCALENDAR "#; + +pub static ICAL_RFC7: &[u8] = br#"BEGIN:VCALENDAR +VERSION:2.0 +PRODID:-//Example Corp.//CalDAV Client//EN +BEGIN:VTIMEZONE +LAST-MODIFIED:20040110T032845Z +TZID:US/Eastern +BEGIN:DAYLIGHT +DTSTART:20000404T020000 +RRULE:FREQ=YEARLY;BYDAY=1SU;BYMONTH=4 +TZNAME:EDT +TZOFFSETFROM:-0500 +TZOFFSETTO:-0400 +END:DAYLIGHT +BEGIN:STANDARD +DTSTART:20001026T020000 +RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=10 +TZNAME:EST +TZOFFSETFROM:-0400 +TZOFFSETTO:-0500 +END:STANDARD +END:VTIMEZONE +BEGIN:VEVENT +ATTENDEE;PARTSTAT=ACCEPTED;ROLE=CHAIR:mailto:cyrus@example.com +ATTENDEE;PARTSTAT=NEEDS-ACTION:mailto:lisa@example.com +DTSTAMP:20090206T001220Z +DTSTART;TZID=US/Eastern:20090104T100000 +DURATION:PT1H +LAST-MODIFIED:20090206T001330Z +ORGANIZER:mailto:cyrus@example.com +SEQUENCE:1 +STATUS:TENTATIVE +SUMMARY:Event #3 +UID:DC6C50A017428C5216A2F1CA@example.com +X-ABC-GUID:E1CX5Dr-0007ym-Hz@example.com +END:VEVENT +END:VCALENDAR +"#;