mirror of
https://github.com/lennart-k/rustical.git
synced 2025-12-26 21:59:25 +00:00
Compare commits
4 Commits
cd9993cd97
...
c42f8e5614
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c42f8e5614 | ||
|
|
f72559d027 | ||
|
|
167492318f | ||
|
|
32f43951ac |
@@ -139,8 +139,10 @@ mod tests {
|
||||
use rustical_ical::{CalendarObject, UtcDateTime};
|
||||
|
||||
use crate::calendar::methods::report::calendar_query::{
|
||||
CompFilterable, TextMatchElement, TimeRangeElement, comp_filter::CompFilterElement,
|
||||
CompFilterable, TextMatchElement, TimeRangeElement,
|
||||
comp_filter::CompFilterElement,
|
||||
prop_filter::PropFilterElement,
|
||||
text_match::{NegateCondition, TextCollation},
|
||||
};
|
||||
|
||||
const ICS: &str = r"BEGIN:VCALENDAR
|
||||
@@ -217,8 +219,8 @@ END:VCALENDAR";
|
||||
time_range: None,
|
||||
text_match: Some(TextMatchElement {
|
||||
needle: "2.0".to_string(),
|
||||
collation: None,
|
||||
negate_condition: None,
|
||||
collation: TextCollation::default(),
|
||||
negate_condition: NegateCondition::default(),
|
||||
}),
|
||||
param_filter: vec![],
|
||||
},
|
||||
@@ -239,8 +241,8 @@ END:VCALENDAR";
|
||||
name: "SUMMARY".to_string(),
|
||||
time_range: None,
|
||||
text_match: Some(TextMatchElement {
|
||||
collation: None,
|
||||
negate_condition: None,
|
||||
collation: TextCollation::default(),
|
||||
negate_condition: NegateCondition(false),
|
||||
needle: "weekly".to_string(),
|
||||
}),
|
||||
param_filter: vec![],
|
||||
@@ -326,8 +328,8 @@ END:VCALENDAR";
|
||||
name: "TZID".to_string(),
|
||||
time_range: None,
|
||||
text_match: Some(TextMatchElement {
|
||||
collation: None,
|
||||
negate_condition: None,
|
||||
collation: TextCollation::AsciiCasemap,
|
||||
negate_condition: NegateCondition::default(),
|
||||
needle: "Europe/Berlin".to_string(),
|
||||
}),
|
||||
param_filter: vec![],
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
use crate::{
|
||||
calendar::methods::report::calendar_query::comp_filter::{CompFilterElement, CompFilterable},
|
||||
calendar::methods::report::calendar_query::{
|
||||
TextMatchElement,
|
||||
comp_filter::{CompFilterElement, CompFilterable},
|
||||
},
|
||||
calendar_object::CalendarObjectPropWrapperName,
|
||||
};
|
||||
use rustical_dav::xml::PropfindType;
|
||||
@@ -29,18 +32,6 @@ pub struct ParamFilterElement {
|
||||
pub(crate) name: String,
|
||||
}
|
||||
|
||||
#[derive(XmlDeserialize, Clone, Debug, PartialEq, Eq)]
|
||||
#[allow(dead_code)]
|
||||
pub struct TextMatchElement {
|
||||
#[xml(ty = "attr")]
|
||||
pub(crate) collation: Option<String>,
|
||||
#[xml(ty = "attr")]
|
||||
// "yes" or "no", default: "no"
|
||||
pub(crate) negate_condition: Option<String>,
|
||||
#[xml(ty = "text")]
|
||||
pub(crate) needle: String,
|
||||
}
|
||||
|
||||
#[derive(XmlDeserialize, Clone, Debug, PartialEq)]
|
||||
#[allow(dead_code)]
|
||||
// https://datatracker.ietf.org/doc/html/rfc4791#section-9.7
|
||||
@@ -51,6 +42,7 @@ pub struct FilterElement {
|
||||
}
|
||||
|
||||
impl FilterElement {
|
||||
#[must_use]
|
||||
pub fn matches(&self, cal_object: &CalendarObject) -> bool {
|
||||
cal_object.matches(&self.comp_filter)
|
||||
}
|
||||
|
||||
@@ -5,11 +5,14 @@ use rustical_store::CalendarStore;
|
||||
mod comp_filter;
|
||||
mod elements;
|
||||
mod prop_filter;
|
||||
pub mod text_match;
|
||||
#[allow(unused_imports)]
|
||||
pub use comp_filter::{CompFilterElement, CompFilterable};
|
||||
pub use elements::*;
|
||||
#[allow(unused_imports)]
|
||||
pub use prop_filter::{PropFilterElement, PropFilterable};
|
||||
#[allow(unused_imports)]
|
||||
pub use text_match::TextMatchElement;
|
||||
|
||||
pub async fn get_objects_calendar_query<C: CalendarStore>(
|
||||
cal_query: &CalendarQueryRequest,
|
||||
@@ -36,7 +39,9 @@ mod tests {
|
||||
ReportRequest,
|
||||
calendar_query::{
|
||||
CalendarQueryRequest, FilterElement, ParamFilterElement, TextMatchElement,
|
||||
comp_filter::CompFilterElement, prop_filter::PropFilterElement,
|
||||
comp_filter::CompFilterElement,
|
||||
prop_filter::PropFilterElement,
|
||||
text_match::{NegateCondition, TextCollation},
|
||||
},
|
||||
},
|
||||
calendar_object::{CalendarData, CalendarObjectPropName, CalendarObjectPropWrapperName},
|
||||
@@ -96,8 +101,8 @@ mod tests {
|
||||
prop_filter: vec![PropFilterElement {
|
||||
name: "ATTENDEE".to_owned(),
|
||||
text_match: Some(TextMatchElement {
|
||||
collation: Some("i;ascii-casemap".to_owned()),
|
||||
negate_condition: None,
|
||||
collation: TextCollation::AsciiCasemap,
|
||||
negate_condition: NegateCondition(false),
|
||||
needle: "mailto:lisa@example.com".to_string()
|
||||
}),
|
||||
is_not_defined: None,
|
||||
@@ -105,8 +110,8 @@ mod tests {
|
||||
is_not_defined: None,
|
||||
name: "PARTSTAT".to_owned(),
|
||||
text_match: Some(TextMatchElement {
|
||||
collation: Some("i;ascii-casemap".to_owned()),
|
||||
negate_condition: None,
|
||||
collation: TextCollation::AsciiCasemap,
|
||||
negate_condition: NegateCondition(false),
|
||||
needle: "NEEDS-ACTION".to_string()
|
||||
}),
|
||||
}],
|
||||
|
||||
@@ -64,28 +64,10 @@ impl PropFilterElement {
|
||||
return true;
|
||||
}
|
||||
|
||||
if let Some(TextMatchElement {
|
||||
collation: _collation,
|
||||
negate_condition,
|
||||
needle,
|
||||
}) = &self.text_match
|
||||
if let Some(text_match) = &self.text_match
|
||||
&& !text_match.match_property(property)
|
||||
{
|
||||
let mut matches = property
|
||||
.value
|
||||
.as_ref()
|
||||
.is_some_and(|haystack| haystack.contains(needle));
|
||||
match negate_condition.as_deref() {
|
||||
None | Some("no") => {}
|
||||
Some("yes") => {
|
||||
matches = !matches;
|
||||
}
|
||||
// Invalid value
|
||||
_ => return false,
|
||||
}
|
||||
|
||||
if !matches {
|
||||
return false;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// TODO: param-filter
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
use ical::property::Property;
|
||||
use rustical_xml::{ValueDeserialize, XmlDeserialize};
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Default)]
|
||||
pub enum TextCollation {
|
||||
#[default]
|
||||
AsciiCasemap,
|
||||
Octet,
|
||||
}
|
||||
|
||||
impl TextCollation {
|
||||
// Check whether a haystack contains a needle respecting the collation
|
||||
#[must_use]
|
||||
pub fn match_text(&self, needle: &str, haystack: &str) -> bool {
|
||||
match self {
|
||||
// https://datatracker.ietf.org/doc/html/rfc4790#section-9.2
|
||||
Self::AsciiCasemap => haystack
|
||||
.to_ascii_uppercase()
|
||||
.contains(&needle.to_ascii_uppercase()),
|
||||
Self::Octet => haystack.contains(needle),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<str> for TextCollation {
|
||||
fn as_ref(&self) -> &str {
|
||||
match self {
|
||||
Self::AsciiCasemap => "i;ascii-casemap",
|
||||
Self::Octet => "i;octet",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ValueDeserialize for TextCollation {
|
||||
fn deserialize(val: &str) -> Result<Self, rustical_xml::XmlError> {
|
||||
match val {
|
||||
"i;ascii-casemap" => Ok(Self::AsciiCasemap),
|
||||
"i;octet" => Ok(Self::Octet),
|
||||
_ => Err(rustical_xml::XmlError::InvalidVariant(format!(
|
||||
"Invalid collation: {val}"
|
||||
))),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Default)]
|
||||
pub struct NegateCondition(pub bool);
|
||||
|
||||
impl ValueDeserialize for NegateCondition {
|
||||
fn deserialize(val: &str) -> Result<Self, rustical_xml::XmlError> {
|
||||
match val {
|
||||
"yes" => Ok(Self(true)),
|
||||
"no" => Ok(Self(false)),
|
||||
_ => Err(rustical_xml::XmlError::InvalidVariant(format!(
|
||||
"Invalid negate-condition parameter: {val}"
|
||||
))),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(XmlDeserialize, Clone, Debug, PartialEq, Eq)]
|
||||
#[allow(dead_code)]
|
||||
pub struct TextMatchElement {
|
||||
#[xml(ty = "attr", default = "Default::default")]
|
||||
pub collation: TextCollation,
|
||||
#[xml(ty = "attr", default = "Default::default")]
|
||||
pub(crate) negate_condition: NegateCondition,
|
||||
#[xml(ty = "text")]
|
||||
pub(crate) needle: String,
|
||||
}
|
||||
|
||||
impl TextMatchElement {
|
||||
#[must_use]
|
||||
pub fn match_property(&self, property: &Property) -> bool {
|
||||
let Self {
|
||||
collation,
|
||||
negate_condition,
|
||||
needle,
|
||||
} = self;
|
||||
|
||||
let matches = property
|
||||
.value
|
||||
.as_ref()
|
||||
.is_some_and(|haystack| collation.match_text(needle, haystack));
|
||||
|
||||
// XOR
|
||||
negate_condition.0 ^ matches
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::calendar::methods::report::calendar_query::text_match::TextCollation;
|
||||
|
||||
#[test]
|
||||
fn test_collation() {
|
||||
assert!(TextCollation::AsciiCasemap.match_text("GrüN", "grün"));
|
||||
assert!(!TextCollation::AsciiCasemap.match_text("GrÜN", "grün"));
|
||||
assert!(!TextCollation::Octet.match_text("GrÜN", "grün"));
|
||||
assert!(TextCollation::Octet.match_text("hallo", "hallo"));
|
||||
assert!(TextCollation::AsciiCasemap.match_text("HaLlo", "hAllo"));
|
||||
}
|
||||
}
|
||||
@@ -27,7 +27,7 @@ use sync_collection::handle_sync_collection;
|
||||
use tracing::instrument;
|
||||
|
||||
mod calendar_multiget;
|
||||
mod calendar_query;
|
||||
pub mod calendar_query;
|
||||
mod sync_collection;
|
||||
|
||||
#[derive(XmlDeserialize, XmlDocument, Clone, Debug, PartialEq)]
|
||||
|
||||
@@ -3,6 +3,8 @@ use rustical_ical::CalendarObjectType;
|
||||
use rustical_xml::{XmlDeserialize, XmlSerialize};
|
||||
use strum_macros::VariantArray;
|
||||
|
||||
use crate::calendar::methods::report::calendar_query::text_match::TextCollation;
|
||||
|
||||
#[derive(Debug, Clone, XmlSerialize, XmlDeserialize, PartialEq, Eq, From, Into)]
|
||||
pub struct SupportedCalendarComponent {
|
||||
#[xml(ty = "attr")]
|
||||
@@ -36,6 +38,28 @@ impl From<SupportedCalendarComponentSet> for Vec<CalendarObjectType> {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, XmlSerialize, XmlDeserialize, PartialEq, Eq, From, Into)]
|
||||
pub struct SupportedCollation(#[xml(ty = "text")] pub TextCollation);
|
||||
|
||||
#[derive(Debug, Clone, XmlSerialize, XmlDeserialize, PartialEq, Eq)]
|
||||
pub struct SupportedCollationSet(
|
||||
#[xml(
|
||||
ns = "rustical_dav::namespace::NS_CALDAV",
|
||||
flatten,
|
||||
rename = "supported-collation"
|
||||
)]
|
||||
pub Vec<SupportedCollation>,
|
||||
);
|
||||
|
||||
impl Default for SupportedCollationSet {
|
||||
fn default() -> Self {
|
||||
Self(vec![
|
||||
SupportedCollation(TextCollation::AsciiCasemap),
|
||||
SupportedCollation(TextCollation::Octet),
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, XmlSerialize, PartialEq, Eq)]
|
||||
pub struct CalendarData {
|
||||
#[xml(ty = "attr")]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use super::prop::{SupportedCalendarComponentSet, SupportedCalendarData};
|
||||
use crate::Error;
|
||||
use crate::calendar::prop::ReportMethod;
|
||||
use crate::calendar::prop::{ReportMethod, SupportedCollationSet};
|
||||
use chrono::{DateTime, Utc};
|
||||
use derive_more::derive::{From, Into};
|
||||
use ical::IcalParser;
|
||||
@@ -39,6 +39,8 @@ pub enum CalendarProp {
|
||||
SupportedCalendarComponentSet(SupportedCalendarComponentSet),
|
||||
#[xml(ns = "rustical_dav::namespace::NS_CALDAV", skip_deserializing)]
|
||||
SupportedCalendarData(SupportedCalendarData),
|
||||
#[xml(ns = "rustical_dav::namespace::NS_CALDAV", skip_deserializing)]
|
||||
SupportedCollationSet(SupportedCollationSet),
|
||||
#[xml(ns = "rustical_dav::namespace::NS_DAV")]
|
||||
MaxResourceSize(i64),
|
||||
#[xml(skip_deserializing)]
|
||||
@@ -156,6 +158,9 @@ impl Resource for CalendarResource {
|
||||
CalendarPropName::SupportedCalendarData => {
|
||||
CalendarProp::SupportedCalendarData(SupportedCalendarData::default())
|
||||
}
|
||||
CalendarPropName::SupportedCollationSet => {
|
||||
CalendarProp::SupportedCollationSet(SupportedCollationSet::default())
|
||||
}
|
||||
CalendarPropName::MaxResourceSize => CalendarProp::MaxResourceSize(10_000_000),
|
||||
CalendarPropName::SupportedReportSet => {
|
||||
CalendarProp::SupportedReportSet(SupportedReportSet::all())
|
||||
@@ -244,6 +249,7 @@ impl Resource for CalendarResource {
|
||||
}
|
||||
CalendarProp::TimezoneServiceSet(_)
|
||||
| CalendarProp::SupportedCalendarData(_)
|
||||
| CalendarProp::SupportedCollationSet(_)
|
||||
| CalendarProp::MaxResourceSize(_)
|
||||
| CalendarProp::SupportedReportSet(_)
|
||||
| CalendarProp::Source(_)
|
||||
@@ -283,6 +289,7 @@ impl Resource for CalendarResource {
|
||||
}
|
||||
CalendarPropName::TimezoneServiceSet
|
||||
| CalendarPropName::SupportedCalendarData
|
||||
| CalendarPropName::SupportedCollationSet
|
||||
| CalendarPropName::MaxResourceSize
|
||||
| CalendarPropName::SupportedReportSet
|
||||
| CalendarPropName::Source
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
<calendar-order xmlns="http://apple.com/ns/ical/"/>
|
||||
<supported-calendar-component-set xmlns="urn:ietf:params:xml:ns:caldav"/>
|
||||
<supported-calendar-data xmlns="urn:ietf:params:xml:ns:caldav"/>
|
||||
<supported-collation-set xmlns="urn:ietf:params:xml:ns:caldav"/>
|
||||
<max-resource-size xmlns="DAV:"/>
|
||||
<supported-report-set xmlns="DAV:"/>
|
||||
<source xmlns="http://calendarserver.org/ns/"/>
|
||||
@@ -160,6 +161,10 @@ END:VCALENDAR
|
||||
<CAL:supported-calendar-data>
|
||||
<CAL:calendar-data content-type="text/calendar" version="2.0"/>
|
||||
</CAL:supported-calendar-data>
|
||||
<CAL:supported-collation-set>
|
||||
<CAL:supported-collation>i;ascii-casemap</CAL:supported-collation>
|
||||
<CAL:supported-collation>i;octet</CAL:supported-collation>
|
||||
</CAL:supported-collation-set>
|
||||
<max-resource-size>10000000</max-resource-size>
|
||||
<supported-report-set>
|
||||
<supported-report>
|
||||
|
||||
@@ -313,7 +313,7 @@ impl Field {
|
||||
}
|
||||
}),
|
||||
(FieldType::Text, false) => Some(quote! {
|
||||
writer.write_event(Event::Text(BytesText::new(&self.#target_field_index)))?;
|
||||
writer.write_event(Event::Text(BytesText::new(self.#target_field_index.as_ref())))?;
|
||||
}),
|
||||
(FieldType::Tag, true) => {
|
||||
let field_name = self.xml_name();
|
||||
|
||||
Reference in New Issue
Block a user