support multiple same name components, properties & parameters

This commit is contained in:
Quentin 2024-05-26 11:03:39 +02:00
parent 6b9720844a
commit d5a222967d
Signed by: quentin
GPG key ID: E9602264D639FF68
3 changed files with 244 additions and 110 deletions

View file

@ -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::<Vec<_>>();
// 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<T: std::str::FromStr>(
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::<Vec<_>>();
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 &param.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::<Vec<_>>();
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 &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())
}
}
})
}
}
})
})
}
}

View file

@ -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 <calendar-data/> 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#"<?xml version="1.0" encoding="utf-8" ?>
<C:calendar-multiget xmlns:D="DAV:" xmlns:C="urn:ietf:params:xml:ns:caldav">
<D:prop>
<D:getetag/>
<C:calendar-data/>
</D:prop>
<D:href>/alice/calendar/Personal/rfc1.ics</D:href>
<D:href>/alice/calendar/Personal/rfc3.ics</D:href>
</C:calendar-multiget>"#;
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(), 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#"<?xml version="1.0" encoding="utf-8" ?>
<C:calendar-query xmlns:C="urn:ietf:params:xml:ns:caldav">
@ -709,12 +751,13 @@ fn rfc4791_webdav_caldav() {
.send()?;
assert_eq!(resp.status(), 207);
let multistatus = dav_deserialize::<dav::Multistatus<All>>(&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::<dav::Multistatus<All>>(&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#"<?xml version="1.0" encoding="utf-8" ?>
<C:calendar-multiget xmlns:D="DAV:" xmlns:C="urn:ietf:params:xml:ns:caldav">
<D:prop>
<C:calendar-query xmlns:C="urn:ietf:params:xml:ns:caldav">
<D:prop xmlns:D="DAV:">
<D:getetag/>
<C:calendar-data/>
</D:prop>
<D:href>/alice/calendar/Personal/rfc1.ics</D:href>
<D:href>/alice/calendar/Personal/rfc3.ics</D:href>
</C:calendar-multiget>"#;
<C:filter>
<C:comp-filter name="VCALENDAR">
<C:comp-filter name="VEVENT">
<C:prop-filter name="UID">
<C:text-match collation="i;octet">DC6C50A017428C5216A2F1CD@example.com</C:text-match>
</C:prop-filter>
</C:comp-filter>
</C:comp-filter>
</C:filter>
</C:calendar-query>"#;
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::<dav::Multistatus<All>>(&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#"<?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="VEVENT">
<C:prop-filter name="ATTENDEE">
<C:text-match collation="i;ascii-casemap">mailto:lisa@example.com</C:text-match>
<C:param-filter name="PARTSTAT">
<C:text-match collation="i;ascii-casemap">NEEDS-ACTION</C:text-match>
</C:param-filter>
</C:prop-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);
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(())
})

View file

@ -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
"#;