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, filter: &cal::CompFilter,
) -> bool { ) -> bool {
// Find the component among the list // Find the component among the list
//@FIXME do not handle correctly multiple entities (eg. 3 VEVENT) let maybe_comps = components
let maybe_comp = components
.iter() .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 // Filter according to rules
match (maybe_comp, &filter.additional_rules) { match (&maybe_comps[..], &filter.additional_rules) {
(Some(_), None) => true, ([_, ..], None) => true,
(None, Some(cal::CompFilterRules::IsNotDefined)) => true, ([], Some(cal::CompFilterRules::IsNotDefined)) => true,
(None, None) => false, ([], None) => false,
(Some(_), Some(cal::CompFilterRules::IsNotDefined)) => false, ([_, ..], Some(cal::CompFilterRules::IsNotDefined)) => false,
(None, Some(cal::CompFilterRules::Matches(_))) => false, (comps, Some(cal::CompFilterRules::Matches(matcher))) => comps.iter().any(|component| {
(Some(component), Some(cal::CompFilterRules::Matches(matcher))) => {
// check time range // check time range
if let Some(time_range) = &matcher.time_range { if let Some(time_range) = &matcher.time_range {
if !is_in_time_range( if !is_in_time_range(
@ -41,7 +40,7 @@ pub fn is_component_match(
matcher.comp_filter.iter().all(|inner_filter| { matcher.comp_filter.iter().all(|inner_filter| {
is_component_match(component, component.components.as_ref(), &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 { fn is_properties_match(props: &[icalendar::parser::Property], filters: &[cal::PropFilter]) -> bool {
filters.iter().all(|single_filter| { filters.iter().all(|single_filter| {
// Find the property // Find the property
let single_prop = props let candidate_props = props
.iter() .iter()
.find(|candidate| candidate.name.as_str() == single_filter.name.0.as_str()); .filter(|candidate| candidate.name.as_str() == single_filter.name.0.as_str())
match (&single_filter.additional_rules, single_prop) { .collect::<Vec<_>>();
(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 { match (&single_filter.additional_rules, &candidate_props[..]) {
None => return false, (None, [_, ..]) | (Some(cal::PropFilterRules::IsNotDefined), []) => true,
Some(v) => v, (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 parsed_date = match maybe_parsed_date {
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, None => return false,
Some(v) => v,
}; };
match txt_match.negate_condition { // see if entry is in range
None | Some(false) => { let is_in_range = match time_range {
param_val.as_str().contains(txt_match.text.as_str()) 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"); println!("🧪 rfc4791_webdav_caldav");
common::aerogramme_provider_daemon_dev(|_imap, _lmtp, http| { common::aerogramme_provider_daemon_dev(|_imap, _lmtp, http| {
// --- INITIAL TEST SETUP --- // --- INITIAL TEST SETUP ---
// Add entries (3 VEVENT, 1 FREEBUSY, 1 VTODO) // Add entries
let resp = http let resp = http
.put("http://localhost:8087/alice/calendar/Personal/rfc1.ics") .put("http://localhost:8087/alice/calendar/Personal/rfc1.ics")
.header("If-None-Match", "*") .header("If-None-Match", "*")
@ -595,7 +595,14 @@ fn rfc4791_webdav_caldav() {
.header("If-None-Match", "*") .header("If-None-Match", "*")
.body(ICAL_RFC6) .body(ICAL_RFC6)
.send()?; .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); assert_eq!(resp.status(), 201);
// A generic function to check a <calendar-data/> query result // A generic function to check a <calendar-data/> query result
@ -684,9 +691,44 @@ fn rfc4791_webdav_caldav() {
.send()?; .send()?;
//@FIXME not yet supported. returns DAV: 1 ; expects DAV: 1 calendar-access //@FIXME not yet supported. returns DAV: 1 ; expects DAV: 1 calendar-access
// Not used by any client I know, so not implementing it now. // 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 --- // --- REPORT calendar-query, only filtering ---
//@FIXME missing support for calendar-data...
// 7.8.8. Example: Retrieval of Events Only // 7.8.8. Example: Retrieval of Events Only
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:C="urn:ietf:params:xml:ns:caldav"> <C:calendar-query xmlns:C="urn:ietf:params:xml:ns:caldav">
@ -709,12 +751,13 @@ fn rfc4791_webdav_caldav() {
.send()?; .send()?;
assert_eq!(resp.status(), 207); assert_eq!(resp.status(), 207);
let multistatus = dav_deserialize::<dav::Multistatus<All>>(&resp.text()?); 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/rfc1.ics", obj1_etag, ICAL_RFC1),
("/alice/calendar/Personal/rfc2.ics", obj2_etag, ICAL_RFC2), ("/alice/calendar/Personal/rfc2.ics", obj2_etag, ICAL_RFC2),
("/alice/calendar/Personal/rfc3.ics", obj3_etag, ICAL_RFC3), ("/alice/calendar/Personal/rfc3.ics", obj3_etag, ICAL_RFC3),
("/alice/calendar/Personal/rfc7.ics", obj7_etag, ICAL_RFC7),
] ]
.iter() .iter()
.for_each(|(ref_path, ref_etag, ref_ical)| { .for_each(|(ref_path, ref_etag, ref_ical)| {
@ -788,26 +831,32 @@ fn rfc4791_webdav_caldav() {
assert_eq!(resp.status(), 207); assert_eq!(resp.status(), 207);
let multistatus = dav_deserialize::<dav::Multistatus<All>>(&resp.text()?); let multistatus = dav_deserialize::<dav::Multistatus<All>>(&resp.text()?);
assert_eq!(multistatus.responses.len(), 1); 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 // 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" ?> 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"> <C:calendar-query xmlns:C="urn:ietf:params:xml:ns:caldav">
<D:prop> <D:prop xmlns:D="DAV:">
<D:getetag/> <D:getetag/>
<C:calendar-data/> <C:calendar-data/>
</D:prop> </D:prop>
<D:href>/alice/calendar/Personal/rfc1.ics</D:href> <C:filter>
<D:href>/alice/calendar/Personal/rfc3.ics</D:href> <C:comp-filter name="VCALENDAR">
</C:calendar-multiget>"#; <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 let resp = http
.request( .request(
reqwest::Method::from_bytes(b"REPORT")?, reqwest::Method::from_bytes(b"REPORT")?,
@ -817,22 +866,61 @@ fn rfc4791_webdav_caldav() {
.send()?; .send()?;
assert_eq!(resp.status(), 207); assert_eq!(resp.status(), 207);
let multistatus = dav_deserialize::<dav::Multistatus<All>>(&resp.text()?); let multistatus = dav_deserialize::<dav::Multistatus<All>>(&resp.text()?);
assert_eq!(multistatus.responses.len(), 2); assert_eq!(multistatus.responses.len(), 1);
[ check_cal(
("/alice/calendar/Personal/rfc1.ics", obj1_etag, ICAL_RFC1), &multistatus,
("/alice/calendar/Personal/rfc3.ics", obj3_etag, ICAL_RFC3), (
] "/alice/calendar/Personal/rfc3.ics",
.iter() Some(obj3_etag.to_str().expect("etag header convertible to str")),
.for_each(|(ref_path, ref_etag, ref_ical)| { Some(ICAL_RFC3),
check_cal( ),
&multistatus, );
(
ref_path,
Some(ref_etag.to_str().expect("etag header convertible to str")), // 7.8.7. Example: Retrieval of Events by PARTSTAT
Some(ref_ical), 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(()) Ok(())
}) })

View file

@ -175,3 +175,41 @@ END:VALARM
END:VTODO END:VTODO
END:VCALENDAR 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
"#;