diff options
author | Quentin Dufour <quentin@deuxfleurs.fr> | 2024-03-08 08:17:03 +0100 |
---|---|---|
committer | Quentin Dufour <quentin@deuxfleurs.fr> | 2024-03-08 08:17:03 +0100 |
commit | 1a43ce5ac7033c148f64a033f2b1d335e95e11d5 (patch) | |
tree | 60b234604170fe207248458a9c4cdd3f4b7c36f2 /aero-dav/src | |
parent | bb9cb386b65834c44cae86bd100f800883022062 (diff) | |
download | aerogramme-1a43ce5ac7033c148f64a033f2b1d335e95e11d5.tar.gz aerogramme-1a43ce5ac7033c148f64a033f2b1d335e95e11d5.zip |
WIP refactor
Diffstat (limited to 'aero-dav/src')
-rw-r--r-- | aero-dav/src/acltypes.rs | 4 | ||||
-rw-r--r-- | aero-dav/src/caldecoder.rs | 33 | ||||
-rw-r--r-- | aero-dav/src/calencoder.rs | 886 | ||||
-rw-r--r-- | aero-dav/src/caltypes.rs | 1453 | ||||
-rw-r--r-- | aero-dav/src/decoder.rs | 947 | ||||
-rw-r--r-- | aero-dav/src/encoder.rs | 1112 | ||||
-rw-r--r-- | aero-dav/src/error.rs | 42 | ||||
-rw-r--r-- | aero-dav/src/lib.rs | 25 | ||||
-rw-r--r-- | aero-dav/src/realization.rs | 42 | ||||
-rw-r--r-- | aero-dav/src/types.rs | 949 | ||||
-rw-r--r-- | aero-dav/src/versioningtypes.rs | 3 | ||||
-rw-r--r-- | aero-dav/src/xml.rs | 274 |
12 files changed, 5770 insertions, 0 deletions
diff --git a/aero-dav/src/acltypes.rs b/aero-dav/src/acltypes.rs new file mode 100644 index 0000000..f356813 --- /dev/null +++ b/aero-dav/src/acltypes.rs @@ -0,0 +1,4 @@ +//@FIXME required for a full DAV implementation +// See section 6. of the CalDAV RFC +// It seems mainly required for free-busy that I will not implement now. +// It can also be used for discovering main calendar, not sure it is used. diff --git a/aero-dav/src/caldecoder.rs b/aero-dav/src/caldecoder.rs new file mode 100644 index 0000000..5f40c4b --- /dev/null +++ b/aero-dav/src/caldecoder.rs @@ -0,0 +1,33 @@ +use super::types as dav; +use super::caltypes::*; +use super::xml; +use super::error; + +// ---- ROOT ELEMENTS --- + +// ---- EXTENSIONS --- +impl xml::QRead<Violation> for Violation { + async fn qread(xml: &mut xml::Reader<impl xml::IRead>) -> Result<Self, error::ParsingError> { + unreachable!(); + } +} + +impl xml::QRead<Property> for Property { + async fn qread(xml: &mut xml::Reader<impl xml::IRead>) -> Result<Self, error::ParsingError> { + unreachable!(); + } +} + +impl xml::QRead<PropertyRequest> for PropertyRequest { + async fn qread(xml: &mut xml::Reader<impl xml::IRead>) -> Result<Self, error::ParsingError> { + unreachable!(); + } +} + +impl xml::QRead<ResourceType> for ResourceType { + async fn qread(xml: &mut xml::Reader<impl xml::IRead>) -> Result<Self, error::ParsingError> { + unreachable!(); + } +} + +// ---- INNER XML ---- diff --git a/aero-dav/src/calencoder.rs b/aero-dav/src/calencoder.rs new file mode 100644 index 0000000..ff6eb24 --- /dev/null +++ b/aero-dav/src/calencoder.rs @@ -0,0 +1,886 @@ +use quick_xml::Error as QError; +use quick_xml::events::{Event, BytesEnd, BytesStart, BytesText}; +use quick_xml::name::PrefixDeclaration; +use tokio::io::AsyncWrite; + +use super::caltypes::*; +use super::xml::{Node, QWrite, IWrite, Writer}; +use super::types::Extension; + +const ICAL_DATETIME_FMT: &str = "%Y%m%dT%H%M%SZ"; + +// ==================== Calendar Types Serialization ========================= + +// -------------------- MKCALENDAR METHOD ------------------------------------ +impl<E: Extension> QWrite for MkCalendar<E> { + async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> { + let start = xml.create_cal_element("mkcalendar"); + let end = start.to_end(); + + xml.q.write_event_async(Event::Start(start.clone())).await?; + self.0.qwrite(xml).await?; + xml.q.write_event_async(Event::End(end)).await + } +} + +impl<E: Extension, N: Node<N>> QWrite for MkCalendarResponse<E,N> { + async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> { + let start = xml.create_cal_element("mkcalendar-response"); + let end = start.to_end(); + + xml.q.write_event_async(Event::Start(start.clone())).await?; + for propstat in self.0.iter() { + propstat.qwrite(xml).await?; + } + xml.q.write_event_async(Event::End(end)).await + } +} + +// ----------------------- REPORT METHOD ------------------------------------- + +impl<E: Extension> QWrite for CalendarQuery<E> { + async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> { + let start = xml.create_cal_element("calendar-query"); + let end = start.to_end(); + + xml.q.write_event_async(Event::Start(start.clone())).await?; + if let Some(selector) = &self.selector { + selector.qwrite(xml).await?; + } + self.filter.qwrite(xml).await?; + if let Some(tz) = &self.timezone { + tz.qwrite(xml).await?; + } + xml.q.write_event_async(Event::End(end)).await + } +} + +impl<E: Extension> QWrite for CalendarMultiget<E> { + async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> { + let start = xml.create_cal_element("calendar-multiget"); + let end = start.to_end(); + + xml.q.write_event_async(Event::Start(start.clone())).await?; + if let Some(selector) = &self.selector { + selector.qwrite(xml).await?; + } + for href in self.href.iter() { + href.qwrite(xml).await?; + } + xml.q.write_event_async(Event::End(end)).await + } +} + +impl QWrite for FreeBusyQuery { + async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> { + let start = xml.create_cal_element("free-busy-query"); + let end = start.to_end(); + + xml.q.write_event_async(Event::Start(start.clone())).await?; + self.0.qwrite(xml).await?; + xml.q.write_event_async(Event::End(end)).await + } +} + +// -------------------------- DAV::prop -------------------------------------- +impl QWrite for PropertyRequest { + async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> { + let mut atom = async |c| { + let empty_tag = xml.create_cal_element(c); + xml.q.write_event_async(Event::Empty(empty_tag)).await + }; + + match self { + Self::CalendarDescription => atom("calendar-description").await, + Self::CalendarTimezone => atom("calendar-timezone").await, + Self::SupportedCalendarComponentSet => atom("supported-calendar-component-set").await, + Self::SupportedCalendarData => atom("supported-calendar-data").await, + Self::MaxResourceSize => atom("max-resource-size").await, + Self::MinDateTime => atom("min-date-time").await, + Self::MaxDateTime => atom("max-date-time").await, + Self::MaxInstances => atom("max-instances").await, + Self::MaxAttendeesPerInstance => atom("max-attendees-per-instance").await, + Self::SupportedCollationSet => atom("supported-collation-set").await, + Self::CalendarData(req) => req.qwrite(xml).await, + } + } +} +impl QWrite for Property { + async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> { + match self { + Self::CalendarDescription { lang, text } => { + let mut start = xml.create_cal_element("calendar-description"); + if let Some(the_lang) = lang { + start.push_attribute(("xml:lang", the_lang.as_str())); + } + let end = start.to_end(); + + xml.q.write_event_async(Event::Start(start.clone())).await?; + xml.q.write_event_async(Event::Text(BytesText::new(text))).await?; + xml.q.write_event_async(Event::End(end)).await + }, + Self::CalendarTimezone(payload) => { + let start = xml.create_cal_element("calendar-timezone"); + let end = start.to_end(); + + xml.q.write_event_async(Event::Start(start.clone())).await?; + xml.q.write_event_async(Event::Text(BytesText::new(payload))).await?; + xml.q.write_event_async(Event::End(end)).await + }, + Self::SupportedCalendarComponentSet(many_comp) => { + let start = xml.create_cal_element("supported-calendar-component-set"); + let end = start.to_end(); + + xml.q.write_event_async(Event::Start(start.clone())).await?; + for comp in many_comp.iter() { + comp.qwrite(xml).await?; + } + xml.q.write_event_async(Event::End(end)).await + }, + Self::SupportedCalendarData(many_mime) => { + let start = xml.create_cal_element("supported-calendar-data"); + let end = start.to_end(); + + xml.q.write_event_async(Event::Start(start.clone())).await?; + for mime in many_mime.iter() { + mime.qwrite(xml).await?; + } + xml.q.write_event_async(Event::End(end)).await + }, + Self::MaxResourceSize(bytes) => { + let start = xml.create_cal_element("max-resource-size"); + let end = start.to_end(); + + xml.q.write_event_async(Event::Start(start.clone())).await?; + xml.q.write_event_async(Event::Text(BytesText::new(bytes.to_string().as_str()))).await?; + xml.q.write_event_async(Event::End(end)).await + }, + Self::MinDateTime(dt) => { + let start = xml.create_cal_element("min-date-time"); + let end = start.to_end(); + + let dtstr = format!("{}", dt.format(ICAL_DATETIME_FMT)); + xml.q.write_event_async(Event::Start(start.clone())).await?; + xml.q.write_event_async(Event::Text(BytesText::new(dtstr.as_str()))).await?; + xml.q.write_event_async(Event::End(end)).await + }, + Self::MaxDateTime(dt) => { + let start = xml.create_cal_element("max-date-time"); + let end = start.to_end(); + + let dtstr = format!("{}", dt.format(ICAL_DATETIME_FMT)); + xml.q.write_event_async(Event::Start(start.clone())).await?; + xml.q.write_event_async(Event::Text(BytesText::new(dtstr.as_str()))).await?; + xml.q.write_event_async(Event::End(end)).await + }, + Self::MaxInstances(count) => { + let start = xml.create_cal_element("max-instances"); + let end = start.to_end(); + + xml.q.write_event_async(Event::Start(start.clone())).await?; + xml.q.write_event_async(Event::Text(BytesText::new(count.to_string().as_str()))).await?; + xml.q.write_event_async(Event::End(end)).await + }, + Self::MaxAttendeesPerInstance(count) => { + let start = xml.create_cal_element("max-attendees-per-instance"); + let end = start.to_end(); + + xml.q.write_event_async(Event::Start(start.clone())).await?; + xml.q.write_event_async(Event::Text(BytesText::new(count.to_string().as_str()))).await?; + xml.q.write_event_async(Event::End(end)).await + }, + Self::SupportedCollationSet(many_collations) => { + let start = xml.create_cal_element("supported-collation-set"); + let end = start.to_end(); + + xml.q.write_event_async(Event::Start(start.clone())).await?; + for collation in many_collations.iter() { + collation.qwrite(xml).await?; + } + xml.q.write_event_async(Event::End(end)).await + }, + Self::CalendarData(inner) => inner.qwrite(xml).await, + } + } +} + +// ---------------------- DAV::resourcetype ---------------------------------- +impl QWrite for ResourceType { + async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> { + match self { + Self::Calendar => { + let empty_tag = xml.create_dav_element("calendar"); + xml.q.write_event_async(Event::Empty(empty_tag)).await + }, + } + } +} + +// --------------------------- DAV::error ------------------------------------ +impl QWrite for Violation { + async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> { + let mut atom = async |c| { + let empty_tag = xml.create_cal_element(c); + xml.q.write_event_async(Event::Empty(empty_tag)).await + }; + + match self { + //@FIXME + // DAV elements, should not be here but in RFC3744 on ACLs + // (we do not use atom as this error is in the DAV namespace, not the caldav one) + Self::NeedPrivileges => { + let empty_tag = xml.create_dav_element("need-privileges"); + xml.q.write_event_async(Event::Empty(empty_tag)).await + }, + + // Regular CalDAV errors + Self::ResourceMustBeNull => atom("resource-must-be-null").await, + Self::CalendarCollectionLocationOk => atom("calendar-collection-location-ok").await, + Self::ValidCalendarData => atom("valid-calendar-data").await, + Self::InitializeCalendarCollection => atom("initialize-calendar-collection").await, + Self::SupportedCalendarData => atom("supported-calendar-data").await, + Self::ValidCalendarObjectResource => atom("valid-calendar-object-resource").await, + Self::SupportedCalendarComponent => atom("supported-calendar-component").await, + Self::NoUidConflict(href) => { + let start = xml.create_cal_element("no-uid-conflict"); + let end = start.to_end(); + + xml.q.write_event_async(Event::Start(start.clone())).await?; + href.qwrite(xml).await?; + xml.q.write_event_async(Event::End(end)).await + }, + Self::MaxResourceSize => atom("max-resource-size").await, + Self::MinDateTime => atom("min-date-time").await, + Self::MaxDateTime => atom("max-date-time").await, + Self::MaxInstances => atom("max-instances").await, + Self::MaxAttendeesPerInstance => atom("max-attendees-per-instance").await, + Self::ValidFilter => atom("valid-filter").await, + Self::SupportedFilter { comp, prop, param } => { + let start = xml.create_cal_element("supported-filter"); + let end = start.to_end(); + + xml.q.write_event_async(Event::Start(start.clone())).await?; + for comp_item in comp.iter() { + comp_item.qwrite(xml).await?; + } + for prop_item in prop.iter() { + prop_item.qwrite(xml).await?; + } + for param_item in param.iter() { + param_item.qwrite(xml).await?; + } + xml.q.write_event_async(Event::End(end)).await + }, + Self::NumberOfMatchesWithinLimits => atom("number-of-matches-within-limits").await, + } + } +} + + +// ---------------------------- Inner XML ------------------------------------ +impl QWrite for SupportedCollation { + async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> { + let start = xml.create_cal_element("supported-collation"); + let end = start.to_end(); + + xml.q.write_event_async(Event::Start(start.clone())).await?; + self.0.qwrite(xml).await?; + xml.q.write_event_async(Event::End(end)).await + + } +} + +impl QWrite for Collation { + async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> { + let col = match self { + Self::AsciiCaseMap => "i;ascii-casemap", + Self::Octet => "i;octet", + Self::Unknown(v) => v.as_str(), + }; + + xml.q.write_event_async(Event::Text(BytesText::new(col))).await + } +} + +impl QWrite for CalendarDataPayload { + async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> { + let mut start = xml.create_cal_element("calendar-data"); + if let Some(mime) = &self.mime { + start.push_attribute(("content-type", mime.content_type.as_str())); + start.push_attribute(("version", mime.version.as_str())); + } + let end = start.to_end(); + + xml.q.write_event_async(Event::Start(start.clone())).await?; + xml.q.write_event_async(Event::Text(BytesText::new(self.payload.as_str()))).await?; + xml.q.write_event_async(Event::End(end)).await + } +} + +impl QWrite for CalendarDataRequest { + async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> { + let mut start = xml.create_cal_element("calendar-data"); + if let Some(mime) = &self.mime { + start.push_attribute(("content-type", mime.content_type.as_str())); + start.push_attribute(("version", mime.version.as_str())); + } + let end = start.to_end(); + xml.q.write_event_async(Event::Start(start.clone())).await?; + if let Some(comp) = &self.comp { + comp.qwrite(xml).await?; + } + if let Some(recurrence) = &self.recurrence { + recurrence.qwrite(xml).await?; + } + if let Some(freebusy) = &self.limit_freebusy_set { + freebusy.qwrite(xml).await?; + } + xml.q.write_event_async(Event::End(end)).await + } +} + +impl QWrite for CalendarDataEmpty { + async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> { + let mut empty = xml.create_cal_element("calendar-data"); + if let Some(mime) = &self.0 { + empty.push_attribute(("content-type", mime.content_type.as_str())); + empty.push_attribute(("version", mime.version.as_str())); + } + xml.q.write_event_async(Event::Empty(empty)).await + } +} + +impl QWrite for Comp { + async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> { + let mut start = xml.create_cal_element("comp"); + start.push_attribute(("name", self.name.as_str())); + match &self.additional_rules { + None => xml.q.write_event_async(Event::Empty(start)).await, + Some(rules) => { + let end = start.to_end(); + xml.q.write_event_async(Event::Start(start.clone())).await?; + rules.prop_kind.qwrite(xml).await?; + rules.comp_kind.qwrite(xml).await?; + xml.q.write_event_async(Event::End(end)).await + }, + } + } +} + +impl QWrite for CompSupport { + async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> { + let mut empty = xml.create_cal_element("comp"); + empty.push_attribute(("name", self.0.as_str())); + xml.q.write_event_async(Event::Empty(empty)).await + } +} + +impl QWrite for CompKind { + async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> { + match self { + Self::AllComp => { + let empty_tag = xml.create_cal_element("allcomp"); + xml.q.write_event_async(Event::Empty(empty_tag)).await + }, + Self::Comp(many_comp) => { + for comp in many_comp.iter() { + // Required: recursion in an async fn requires boxing + // rustc --explain E0733 + Box::pin(comp.qwrite(xml)).await?; + } + Ok(()) + } + } + } +} + +impl QWrite for PropKind { + async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> { + match self { + Self::AllProp => { + let empty_tag = xml.create_cal_element("allprop"); + xml.q.write_event_async(Event::Empty(empty_tag)).await + }, + Self::Prop(many_prop) => { + for prop in many_prop.iter() { + prop.qwrite(xml).await?; + } + Ok(()) + } + } + } +} + +impl QWrite for CalProp { + async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> { + let mut empty = xml.create_cal_element("prop"); + empty.push_attribute(("name", self.name.0.as_str())); + match self.novalue { + None => (), + Some(true) => empty.push_attribute(("novalue", "yes")), + Some(false) => empty.push_attribute(("novalue", "no")), + } + xml.q.write_event_async(Event::Empty(empty)).await + } +} + +impl QWrite for RecurrenceModifier { + async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> { + match self { + Self::Expand(exp) => exp.qwrite(xml).await, + Self::LimitRecurrenceSet(lrs) => lrs.qwrite(xml).await, + } + } +} + +impl QWrite for Expand { + async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> { + let mut empty = xml.create_cal_element("expand"); + empty.push_attribute(("start", format!("{}", self.0.format(ICAL_DATETIME_FMT)).as_str())); + empty.push_attribute(("end", format!("{}", self.1.format(ICAL_DATETIME_FMT)).as_str())); + xml.q.write_event_async(Event::Empty(empty)).await + } +} + +impl QWrite for LimitRecurrenceSet { + async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> { + let mut empty = xml.create_cal_element("limit-recurrence-set"); + empty.push_attribute(("start", format!("{}", self.0.format(ICAL_DATETIME_FMT)).as_str())); + empty.push_attribute(("end", format!("{}", self.1.format(ICAL_DATETIME_FMT)).as_str())); + xml.q.write_event_async(Event::Empty(empty)).await + } +} + +impl QWrite for LimitFreebusySet { + async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> { + let mut empty = xml.create_cal_element("limit-freebusy-set"); + empty.push_attribute(("start", format!("{}", self.0.format(ICAL_DATETIME_FMT)).as_str())); + empty.push_attribute(("end", format!("{}", self.1.format(ICAL_DATETIME_FMT)).as_str())); + xml.q.write_event_async(Event::Empty(empty)).await + } +} + +impl<E: Extension> QWrite for CalendarSelector<E> { + async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> { + match self { + Self::AllProp => { + let empty_tag = xml.create_dav_element("allprop"); + xml.q.write_event_async(Event::Empty(empty_tag)).await + }, + Self::PropName => { + let empty_tag = xml.create_dav_element("propname"); + xml.q.write_event_async(Event::Empty(empty_tag)).await + }, + Self::Prop(prop) => prop.qwrite(xml).await, + } + } +} + +impl QWrite for CompFilter { + async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> { + let mut start = xml.create_cal_element("comp-filter"); + start.push_attribute(("name", self.name.as_str())); + + match &self.additional_rules { + None => xml.q.write_event_async(Event::Empty(start)).await, + Some(rules) => { + let end = start.to_end(); + + xml.q.write_event_async(Event::Start(start.clone())).await?; + rules.qwrite(xml).await?; + xml.q.write_event_async(Event::End(end)).await + } + } + } +} + +impl QWrite for CompFilterRules { + async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> { + match self { + Self::IsNotDefined => { + let empty_tag = xml.create_dav_element("is-not-defined"); + xml.q.write_event_async(Event::Empty(empty_tag)).await + }, + Self::Matches(cfm) => cfm.qwrite(xml).await, + } + } +} + +impl QWrite for CompFilterMatch { + async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> { + if let Some(time_range) = &self.time_range { + time_range.qwrite(xml).await?; + } + + for prop_item in self.prop_filter.iter() { + prop_item.qwrite(xml).await?; + } + for comp_item in self.comp_filter.iter() { + // Required: recursion in an async fn requires boxing + // rustc --explain E0733 + Box::pin(comp_item.qwrite(xml)).await?; + } + Ok(()) + } +} + +impl QWrite for PropFilter { + async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> { + let mut start = xml.create_cal_element("prop-filter"); + start.push_attribute(("name", self.name.as_str())); + + match &self.additional_rules { + None => xml.q.write_event_async(Event::Empty(start.clone())).await, + Some(rules) => { + let end = start.to_end(); + xml.q.write_event_async(Event::Start(start.clone())).await?; + rules.qwrite(xml).await?; + xml.q.write_event_async(Event::End(end)).await + } + } + } +} + +impl QWrite for PropFilterRules { + async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> { + match self { + Self::IsNotDefined => { + let empty_tag = xml.create_dav_element("is-not-defined"); + xml.q.write_event_async(Event::Empty(empty_tag)).await + }, + Self::Match(prop_match) => prop_match.qwrite(xml).await, + } + } +} + +impl QWrite for PropFilterMatch { + async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> { + if let Some(time_range) = &self.time_range { + time_range.qwrite(xml).await?; + } + if let Some(time_or_text) = &self.time_or_text { + time_or_text.qwrite(xml).await?; + } + for param_item in self.param_filter.iter() { + param_item.qwrite(xml).await?; + } + Ok(()) + } +} + +impl QWrite for TimeOrText { + async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> { + match self { + Self::Time(time) => time.qwrite(xml).await, + Self::Text(txt) => txt.qwrite(xml).await, + } + } +} + +impl QWrite for TextMatch { + async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> { + let mut start = xml.create_cal_element("text-match"); + if let Some(collation) = &self.collation { + start.push_attribute(("collation", collation.as_str())); + } + match self.negate_condition { + None => (), + Some(true) => start.push_attribute(("negate-condition", "yes")), + Some(false) => start.push_attribute(("negate-condition", "no")), + } + let end = start.to_end(); + + xml.q.write_event_async(Event::Start(start.clone())).await?; + xml.q.write_event_async(Event::Text(BytesText::new(self.text.as_str()))).await?; + xml.q.write_event_async(Event::End(end)).await + } +} + +impl QWrite for ParamFilter { + async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> { + let mut start = xml.create_cal_element("param-filter"); + start.push_attribute(("name", self.name.as_str())); + + match &self.additional_rules { + None => xml.q.write_event_async(Event::Empty(start)).await, + Some(rules) => { + let end = start.to_end(); + xml.q.write_event_async(Event::Start(start.clone())).await?; + rules.qwrite(xml).await?; + xml.q.write_event_async(Event::End(end)).await + } + } + } +} + +impl QWrite for ParamFilterMatch { + async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> { + match self { + Self::IsNotDefined => { + let empty_tag = xml.create_dav_element("is-not-defined"); + xml.q.write_event_async(Event::Empty(empty_tag)).await + }, + Self::Match(tm) => tm.qwrite(xml).await, + } + } +} + +impl QWrite for TimeZone { + async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> { + let mut start = xml.create_cal_element("timezone"); + let end = start.to_end(); + + xml.q.write_event_async(Event::Start(start.clone())).await?; + xml.q.write_event_async(Event::Text(BytesText::new(self.0.as_str()))).await?; + xml.q.write_event_async(Event::End(end)).await + } +} + +impl QWrite for Filter { + async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> { + let mut start = xml.create_cal_element("filter"); + let end = start.to_end(); + + xml.q.write_event_async(Event::Start(start.clone())).await?; + self.0.qwrite(xml).await?; + xml.q.write_event_async(Event::End(end)).await + } +} + +impl QWrite for TimeRange { + async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> { + let mut empty = xml.create_cal_element("time-range"); + match self { + Self::OnlyStart(start) => empty.push_attribute(("start", format!("{}", start.format(ICAL_DATETIME_FMT)).as_str())), + Self::OnlyEnd(end) => empty.push_attribute(("end", format!("{}", end.format(ICAL_DATETIME_FMT)).as_str())), + Self::FullRange(start, end) => { + empty.push_attribute(("start", format!("{}", start.format(ICAL_DATETIME_FMT)).as_str())); + empty.push_attribute(("end", format!("{}", end.format(ICAL_DATETIME_FMT)).as_str())); + } + } + xml.q.write_event_async(Event::Empty(empty)).await + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::types as dav; + use crate::realization::Calendar; + use tokio::io::AsyncWriteExt; + use chrono::{Utc,TimeZone,DateTime}; + + async fn serialize(elem: &impl QWrite) -> String { + let mut buffer = Vec::new(); + let mut tokio_buffer = tokio::io::BufWriter::new(&mut buffer); + let q = quick_xml::writer::Writer::new_with_indent(&mut tokio_buffer, b' ', 4); + let ns_to_apply = vec![ + ("xmlns:D".into(), "DAV:".into()), + ("xmlns:C".into(), "urn:ietf:params:xml:ns:caldav".into()), + ]; + let mut writer = Writer { q, ns_to_apply }; + + elem.qwrite(&mut writer).await.expect("xml serialization"); + tokio_buffer.flush().await.expect("tokio buffer flush"); + let got = std::str::from_utf8(buffer.as_slice()).unwrap(); + + return got.into() + } + + #[tokio::test] + async fn basic_violation() { + let got = serialize( + &dav::Error::<Calendar>(vec![ + dav::Violation::Extension(Violation::ResourceMustBeNull), + ]) + ).await; + + let expected = r#"<D:error xmlns:D="DAV:" xmlns:C="urn:ietf:params:xml:ns:caldav"> + <C:resource-must-be-null/> +</D:error>"#; + + assert_eq!(&got, expected, "\n---GOT---\n{got}\n---EXP---\n{expected}\n"); + } + + #[tokio::test] + async fn rfc_calendar_query1_req() { + let got = serialize( + &CalendarQuery::<Calendar> { + selector: Some(CalendarSelector::Prop(dav::PropName(vec![ + dav::PropertyRequest::GetEtag, + dav::PropertyRequest::Extension(PropertyRequest::CalendarData(CalendarDataRequest { + mime: None, + comp: Some(Comp { + name: Component::VCalendar, + additional_rules: Some(CompInner { + prop_kind: PropKind::Prop(vec![ + CalProp { + name: ComponentProperty("VERSION".into()), + novalue: None, + } + ]), + comp_kind: CompKind::Comp(vec![ + Comp { + name: Component::VEvent, + additional_rules: Some(CompInner { + prop_kind: PropKind::Prop(vec![ + CalProp { name: ComponentProperty("SUMMARY".into()), novalue: None }, + CalProp { name: ComponentProperty("UID".into()), novalue: None }, + CalProp { name: ComponentProperty("DTSTART".into()), novalue: None }, + CalProp { name: ComponentProperty("DTEND".into()), novalue: None }, + CalProp { name: ComponentProperty("DURATION".into()), novalue: None }, + CalProp { name: ComponentProperty("RRULE".into()), novalue: None }, + CalProp { name: ComponentProperty("RDATE".into()), novalue: None }, + CalProp { name: ComponentProperty("EXRULE".into()), novalue: None }, + CalProp { name: ComponentProperty("EXDATE".into()), novalue: None }, + CalProp { name: ComponentProperty("RECURRENCE-ID".into()), novalue: None }, + ]), + comp_kind: CompKind::Comp(vec![]), + }), + }, + Comp { + name: Component::VTimeZone, + additional_rules: None, + } + ]), + }), + }), + recurrence: None, + limit_freebusy_set: None, + })), + ]))), + filter: Filter(CompFilter { + name: Component::VCalendar, + additional_rules: Some(CompFilterRules::Matches(CompFilterMatch { + time_range: None, + prop_filter: vec![], + comp_filter: vec![ + CompFilter { + name: Component::VEvent, + additional_rules: Some(CompFilterRules::Matches(CompFilterMatch { + time_range: Some(TimeRange::FullRange( + Utc.with_ymd_and_hms(2006,1,4,0,0,0).unwrap(), + Utc.with_ymd_and_hms(2006,1,5,0,0,0).unwrap(), + )), + prop_filter: vec![], + comp_filter: vec![], + })), + }, + ], + })), + }), + timezone: None, + } + ).await; + + let expected = r#"<C:calendar-query xmlns:D="DAV:" xmlns:C="urn:ietf:params:xml:ns:caldav"> + <D:prop> + <D:getetag/> + <C:calendar-data> + <C:comp name="VCALENDAR"> + <C:prop name="VERSION"/> + <C:comp name="VEVENT"> + <C:prop name="SUMMARY"/> + <C:prop name="UID"/> + <C:prop name="DTSTART"/> + <C:prop name="DTEND"/> + <C:prop name="DURATION"/> + <C:prop name="RRULE"/> + <C:prop name="RDATE"/> + <C:prop name="EXRULE"/> + <C:prop name="EXDATE"/> + <C:prop name="RECURRENCE-ID"/> + </C:comp> + <C:comp name="VTIMEZONE"/> + </C:comp> + </C:calendar-data> + </D:prop> + <C:filter> + <C:comp-filter name="VCALENDAR"> + <C:comp-filter name="VEVENT"> + <C:time-range start="20060104T000000Z" end="20060105T000000Z"/> + </C:comp-filter> + </C:comp-filter> + </C:filter> +</C:calendar-query>"#; + + assert_eq!(&got, expected, "\n---GOT---\n{got}\n---EXP---\n{expected}\n"); + } + + #[tokio::test] + async fn rfc_calendar_query1_res() { + let got = serialize( + &dav::Multistatus::<Calendar,dav::PropValue<Calendar>> { + responses: vec![ + dav::Response { + status_or_propstat: dav::StatusOrPropstat::PropStat( + dav::Href("http://cal.example.com/bernard/work/abcd2.ics".into()), + vec![dav::PropStat { + prop: dav::PropValue(vec![ + dav::Property::GetEtag("\"fffff-abcd2\"".into()), + dav::Property::Extension(Property::CalendarData(CalendarDataPayload { + mime: None, + payload: "PLACEHOLDER".into() + })), + ]), + status: dav::Status(http::status::StatusCode::OK), + error: None, + responsedescription: None, + }] + ), + location: None, + error: None, + responsedescription: None, + }, + dav::Response { + status_or_propstat: dav::StatusOrPropstat::PropStat( + dav::Href("http://cal.example.com/bernard/work/abcd3.ics".into()), + vec![dav::PropStat { + prop: dav::PropValue(vec![ + dav::Property::GetEtag("\"fffff-abcd3\"".into()), + dav::Property::Extension(Property::CalendarData(CalendarDataPayload{ + mime: None, + payload: "PLACEHOLDER".into(), + })), + ]), + status: dav::Status(http::status::StatusCode::OK), + error: None, + responsedescription: None, + }] + ), + location: None, + error: None, + responsedescription: None, + }, + ], + responsedescription: None, + }, + ).await; + + let expected = r#"<D:multistatus xmlns:D="DAV:" xmlns:C="urn:ietf:params:xml:ns:caldav"> + <D:response> + <D:href>http://cal.example.com/bernard/work/abcd2.ics</D:href> + <D:propstat> + <D:prop> + <D:getetag>"fffff-abcd2"</D:getetag> + <C:calendar-data>PLACEHOLDER</C:calendar-data> + </D:prop> + <D:status>HTTP/1.1 200 OK</D:status> + </D:propstat> + </D:response> + <D:response> + <D:href>http://cal.example.com/bernard/work/abcd3.ics</D:href> + <D:propstat> + <D:prop> + <D:getetag>"fffff-abcd3"</D:getetag> + <C:calendar-data>PLACEHOLDER</C:calendar-data> + </D:prop> + <D:status>HTTP/1.1 200 OK</D:status> + </D:propstat> + </D:response> +</D:multistatus>"#; + + + assert_eq!(&got, expected, "\n---GOT---\n{got}\n---EXP---\n{expected}\n"); + } +} diff --git a/aero-dav/src/caltypes.rs b/aero-dav/src/caltypes.rs new file mode 100644 index 0000000..9b9091e --- /dev/null +++ b/aero-dav/src/caltypes.rs @@ -0,0 +1,1453 @@ +#![allow(dead_code)] + +use chrono::{DateTime,Utc}; +use super::types as dav; +use super::xml; + +//@FIXME ACL (rfc3744) is missing, required +//@FIXME Versioning (rfc3253) is missing, required +//@FIXME WebDAV sync (rfc6578) is missing, optional +// For reference, SabreDAV guide gives high-level & real-world overview: +// https://sabre.io/dav/building-a-caldav-client/ +// For reference, non-official extensions documented by SabreDAV: +// https://github.com/apple/ccs-calendarserver/tree/master/doc/Extensions + + +// ----- Root elements ----- + +// --- (MKCALENDAR PART) --- + +/// If a request body is included, it MUST be a CALDAV:mkcalendar XML +/// element. Instruction processing MUST occur in the order +/// instructions are received (i.e., from top to bottom). +/// Instructions MUST either all be executed or none executed. Thus, +/// if any error occurs during processing, all executed instructions +/// MUST be undone and a proper error result returned. Instruction +/// processing details can be found in the definition of the DAV:set +/// instruction in Section 12.13.2 of [RFC2518]. +/// +/// ```xmlschema +/// <!ELEMENT mkcalendar (DAV:set)> +/// ``` +#[derive(Debug, PartialEq)] +pub struct MkCalendar<E: dav::Extension>(pub dav::Set<E>); + + +/// If a response body for a successful request is included, it MUST +/// be a CALDAV:mkcalendar-response XML element. +/// +/// <!ELEMENT mkcalendar-response ANY> +/// +/// ---- +/// +/// ANY is not satisfying, so looking at RFC5689 +/// https://www.rfc-editor.org/rfc/rfc5689.html#section-5.2 +/// +/// Definition: +/// +/// <!ELEMENT mkcol-response (propstat+)> +#[derive(Debug, PartialEq)] +pub struct MkCalendarResponse<E: dav::Extension, N: xml::Node<N>>(pub Vec<dav::PropStat<E,N>>); + +// --- (REPORT PART) --- + +/// Name: calendar-query +/// +/// Namespace: urn:ietf:params:xml:ns:caldav +/// +/// Purpose: Defines a report for querying calendar object resources. +/// +/// Description: See Section 7.8. +/// +/// Definition: +/// +/// <!ELEMENT calendar-query ((DAV:allprop | +/// DAV:propname | +/// DAV:prop)?, filter, timezone?)> +#[derive(Debug, PartialEq)] +pub struct CalendarQuery<E: dav::Extension> { + pub selector: Option<CalendarSelector<E>>, + pub filter: Filter, + pub timezone: Option<TimeZone>, +} + +/// Name: calendar-multiget +/// +/// Namespace: urn:ietf:params:xml:ns:caldav +/// +/// Purpose: CalDAV report used to retrieve specific calendar object +/// resources. +/// +/// Description: See Section 7.9. +/// +/// Definition: +/// +/// <!ELEMENT calendar-multiget ((DAV:allprop | +/// DAV:propname | +/// DAV:prop)?, DAV:href+)> +#[derive(Debug, PartialEq)] +pub struct CalendarMultiget<E: dav::Extension> { + pub selector: Option<CalendarSelector<E>>, + pub href: Vec<dav::Href>, +} + +/// Name: free-busy-query +/// +/// Namespace: urn:ietf:params:xml:ns:caldav +/// +/// Purpose: CalDAV report used to generate a VFREEBUSY to determine +/// busy time over a specific time range. +/// +/// Description: See Section 7.10. +/// +/// Definition: +/// <!ELEMENT free-busy-query (time-range)> +#[derive(Debug, PartialEq)] +pub struct FreeBusyQuery(pub TimeRange); + +// ----- Hooks ----- +#[derive(Debug, PartialEq)] +pub enum ResourceType { + Calendar, +} + +/// Check the matching Property object for documentation +#[derive(Debug, PartialEq)] +pub enum PropertyRequest { + CalendarDescription, + CalendarTimezone, + SupportedCalendarComponentSet, + SupportedCalendarData, + MaxResourceSize, + MinDateTime, + MaxDateTime, + MaxInstances, + MaxAttendeesPerInstance, + SupportedCollationSet, + CalendarData(CalendarDataRequest), +} + +#[derive(Debug, PartialEq)] +pub enum Property { + /// Name: calendar-description + /// + /// Namespace: urn:ietf:params:xml:ns:caldav + /// + /// Purpose: Provides a human-readable description of the calendar + /// collection. + /// + /// Conformance: This property MAY be defined on any calendar + /// collection. If defined, it MAY be protected and SHOULD NOT be + /// returned by a PROPFIND DAV:allprop request (as defined in Section + /// 12.14.1 of [RFC2518]). An xml:lang attribute indicating the human + /// language of the description SHOULD be set for this property by + /// clients or through server provisioning. Servers MUST return any + /// xml:lang attribute if set for the property. + /// + /// Description: If present, the property contains a description of the + /// calendar collection that is suitable for presentation to a user. + /// If not present, the client should assume no description for the + /// calendar collection. + /// + /// Definition: + /// + /// <!ELEMENT calendar-description (#PCDATA)> + /// PCDATA value: string + /// + /// Example: + /// + /// <C:calendar-description xml:lang="fr-CA" + /// xmlns:C="urn:ietf:params:xml:ns:caldav" + /// >Calendrier de Mathilde Desruisseaux</C:calendar-description> + CalendarDescription { + lang: Option<String>, + text: String, + }, + + /// 5.2.2. CALDAV:calendar-timezone Property + /// + /// Name: calendar-timezone + /// + /// Namespace: urn:ietf:params:xml:ns:caldav + /// + /// Purpose: Specifies a time zone on a calendar collection. + /// + /// Conformance: This property SHOULD be defined on all calendar + /// collections. If defined, it SHOULD NOT be returned by a PROPFIND + /// DAV:allprop request (as defined in Section 12.14.1 of [RFC2518]). + /// + /// Description: The CALDAV:calendar-timezone property is used to + /// specify the time zone the server should rely on to resolve "date" + /// values and "date with local time" values (i.e., floating time) to + /// "date with UTC time" values. The server will require this + /// information to determine if a calendar component scheduled with + /// "date" values or "date with local time" values overlaps a CALDAV: + /// time-range specified in a CALDAV:calendar-query REPORT. The + /// server will also require this information to compute the proper + /// FREEBUSY time period as "date with UTC time" in the VFREEBUSY + /// component returned in a response to a CALDAV:free-busy-query + /// REPORT request that takes into account calendar components + /// scheduled with "date" values or "date with local time" values. In + /// the absence of this property, the server MAY rely on the time zone + /// of their choice. + /// + /// Note: The iCalendar data embedded within the CALDAV:calendar- + /// timezone XML element MUST follow the standard XML character data + /// encoding rules, including use of <, >, & etc. entity + /// encoding or the use of a <![CDATA[ ... ]]> construct. In the + /// later case, the iCalendar data cannot contain the character + /// sequence "]]>", which is the end delimiter for the CDATA section. + /// + /// Definition: + /// + /// ```xmlschema + /// <!ELEMENT calendar-timezone (#PCDATA)> + /// PCDATA value: an iCalendar object with exactly one VTIMEZONE component. + /// ``` + /// + /// Example: + /// + /// ```xmlschema + /// <C:calendar-timezone + /// xmlns:C="urn:ietf:params:xml:ns:caldav">BEGIN:VCALENDAR + /// PRODID:-//Example Corp.//CalDAV Client//EN + /// VERSION:2.0 + /// BEGIN:VTIMEZONE + /// TZID:US-Eastern + /// LAST-MODIFIED:19870101T000000Z + /// BEGIN:STANDARD + /// DTSTART:19671029T020000 + /// RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=10 + /// TZOFFSETFROM:-0400 + /// TZOFFSETTO:-0500 + /// TZNAME:Eastern Standard Time (US & Canada) + /// END:STANDARD + /// BEGIN:DAYLIGHT + /// DTSTART:19870405T020000 + /// RRULE:FREQ=YEARLY;BYDAY=1SU;BYMONTH=4 + /// TZOFFSETFROM:-0500 + /// TZOFFSETTO:-0400 + /// TZNAME:Eastern Daylight Time (US & Canada) + /// END:DAYLIGHT + /// END:VTIMEZONE + /// END:VCALENDAR + /// </C:calendar-timezone> + /// ``` + //@FIXME we might want to put a buffer here or an iCal parsed object + CalendarTimezone(String), + + /// Name: supported-calendar-component-set + /// + /// Namespace: urn:ietf:params:xml:ns:caldav + /// + /// Purpose: Specifies the calendar component types (e.g., VEVENT, + /// VTODO, etc.) that calendar object resources can contain in the + /// calendar collection. + /// + /// Conformance: This property MAY be defined on any calendar + /// collection. If defined, it MUST be protected and SHOULD NOT be + /// returned by a PROPFIND DAV:allprop request (as defined in Section + /// 12.14.1 of [RFC2518]). + /// + /// Description: The CALDAV:supported-calendar-component-set property is + /// used to specify restrictions on the calendar component types that + /// calendar object resources may contain in a calendar collection. + /// Any attempt by the client to store calendar object resources with + /// component types not listed in this property, if it exists, MUST + /// result in an error, with the CALDAV:supported-calendar-component + /// precondition (Section 5.3.2.1) being violated. Since this + /// property is protected, it cannot be changed by clients using a + /// PROPPATCH request. However, clients can initialize the value of + /// this property when creating a new calendar collection with + /// MKCALENDAR. The empty-element tag <C:comp name="VTIMEZONE"/> MUST + /// only be specified if support for calendar object resources that + /// only contain VTIMEZONE components is provided or desired. Support + /// for VTIMEZONE components in calendar object resources that contain + /// VEVENT or VTODO components is always assumed. In the absence of + /// this property, the server MUST accept all component types, and the + /// client can assume that all component types are accepted. + /// + /// Definition: + /// + /// <!ELEMENT supported-calendar-component-set (comp+)> + /// + /// Example: + /// + /// <C:supported-calendar-component-set + /// xmlns:C="urn:ietf:params:xml:ns:caldav"> + /// <C:comp name="VEVENT"/> + /// <C:comp name="VTODO"/> + /// </C:supported-calendar-component-set> + SupportedCalendarComponentSet(Vec<CompSupport>), + + /// Name: supported-calendar-data + /// + /// Namespace: urn:ietf:params:xml:ns:caldav + /// + /// Purpose: Specifies what media types are allowed for calendar object + /// resources in a calendar collection. + /// + /// Conformance: This property MAY be defined on any calendar + /// collection. If defined, it MUST be protected and SHOULD NOT be + /// returned by a PROPFIND DAV:allprop request (as defined in Section + /// 12.14.1 of [RFC2518]). + /// + /// Description: The CALDAV:supported-calendar-data property is used to + /// specify the media type supported for the calendar object resources + /// contained in a given calendar collection (e.g., iCalendar version + /// 2.0). Any attempt by the client to store calendar object + /// resources with a media type not listed in this property MUST + /// result in an error, with the CALDAV:supported-calendar-data + /// precondition (Section 5.3.2.1) being violated. In the absence of + /// this property, the server MUST only accept data with the media + /// type "text/calendar" and iCalendar version 2.0, and clients can + /// assume that the server will only accept this data. + /// + /// Definition: + /// + /// <!ELEMENT supported-calendar-data (calendar-data+)> + /// + /// Example: + /// + /// <C:supported-calendar-data + /// xmlns:C="urn:ietf:params:xml:ns:caldav"> + /// <C:calendar-data content-type="text/calendar" version="2.0"/> + /// </C:supported-calendar-data> + /// + /// ----- + /// + /// <!ELEMENT calendar-data EMPTY> + /// + /// when nested in the CALDAV:supported-calendar-data property + /// to specify a supported media type for calendar object + /// resources; + SupportedCalendarData(Vec<CalendarDataEmpty>), + + /// Name: max-resource-size + /// + /// Namespace: urn:ietf:params:xml:ns:caldav + /// + /// Purpose: Provides a numeric value indicating the maximum size of a + /// resource in octets that the server is willing to accept when a + /// calendar object resource is stored in a calendar collection. + /// + /// Conformance: This property MAY be defined on any calendar + /// collection. If defined, it MUST be protected and SHOULD NOT be + /// returned by a PROPFIND DAV:allprop request (as defined in Section + /// 12.14.1 of [RFC2518]). + /// + /// Description: The CALDAV:max-resource-size is used to specify a + /// numeric value that represents the maximum size in octets that the + /// server is willing to accept when a calendar object resource is + /// stored in a calendar collection. Any attempt to store a calendar + /// object resource exceeding this size MUST result in an error, with + /// the CALDAV:max-resource-size precondition (Section 5.3.2.1) being + /// violated. In the absence of this property, the client can assume + /// that the server will allow storing a resource of any reasonable + /// size. + /// + /// Definition: + /// + /// <!ELEMENT max-resource-size (#PCDATA)> + /// PCDATA value: a numeric value (positive integer) + /// + /// Example: + /// + /// <C:max-resource-size xmlns:C="urn:ietf:params:xml:ns:caldav"> + /// 102400 + /// </C:max-resource-size> + MaxResourceSize(u64), + + /// CALDAV:min-date-time Property + /// + /// Name: min-date-time + /// + /// Namespace: urn:ietf:params:xml:ns:caldav + /// + /// Purpose: Provides a DATE-TIME value indicating the earliest date and + /// time (in UTC) that the server is willing to accept for any DATE or + /// DATE-TIME value in a calendar object resource stored in a calendar + /// collection. + /// + /// Conformance: This property MAY be defined on any calendar + /// collection. If defined, it MUST be protected and SHOULD NOT be + /// returned by a PROPFIND DAV:allprop request (as defined in Section + /// 12.14.1 of [RFC2518]). + /// + /// Description: The CALDAV:min-date-time is used to specify an + /// iCalendar DATE-TIME value in UTC that indicates the earliest + /// inclusive date that the server is willing to accept for any + /// explicit DATE or DATE-TIME value in a calendar object resource + /// stored in a calendar collection. Any attempt to store a calendar + /// object resource using a DATE or DATE-TIME value earlier than this + /// value MUST result in an error, with the CALDAV:min-date-time + /// precondition (Section 5.3.2.1) being violated. Note that servers + /// MUST accept recurring components that specify instances beyond + /// this limit, provided none of those instances have been overridden. + /// In that case, the server MAY simply ignore those instances outside + /// of the acceptable range when processing reports on the calendar + /// object resource. In the absence of this property, the client can + /// assume any valid iCalendar date may be used at least up to the + /// CALDAV:max-date-time value, if that is defined. + /// + /// Definition: + /// + /// <!ELEMENT min-date-time (#PCDATA)> + /// PCDATA value: an iCalendar format DATE-TIME value in UTC + /// + /// Example: + /// + /// <C:min-date-time xmlns:C="urn:ietf:params:xml:ns:caldav"> + /// 19000101T000000Z + /// </C:min-date-time> + MinDateTime(DateTime<Utc>), + + /// CALDAV:max-date-time Property + /// + /// Name: max-date-time + /// + /// Namespace: urn:ietf:params:xml:ns:caldav + /// + /// Purpose: Provides a DATE-TIME value indicating the latest date and + /// time (in UTC) that the server is willing to accept for any DATE or + /// DATE-TIME value in a calendar object resource stored in a calendar + /// collection. + /// + /// Conformance: This property MAY be defined on any calendar + /// collection. If defined, it MUST be protected and SHOULD NOT be + /// returned by a PROPFIND DAV:allprop request (as defined in Section + /// 12.14.1 of [RFC2518]). + /// + /// Description: The CALDAV:max-date-time is used to specify an + /// iCalendar DATE-TIME value in UTC that indicates the inclusive + /// latest date that the server is willing to accept for any date or + /// time value in a calendar object resource stored in a calendar + /// collection. Any attempt to store a calendar object resource using + /// a DATE or DATE-TIME value later than this value MUST result in an + /// error, with the CALDAV:max-date-time precondition + /// (Section 5.3.2.1) being violated. Note that servers MUST accept + /// recurring components that specify instances beyond this limit, + /// provided none of those instances have been overridden. In that + /// case, the server MAY simply ignore those instances outside of the + /// acceptable range when processing reports on the calendar object + /// resource. In the absence of this property, the client can assume + /// any valid iCalendar date may be used at least down to the CALDAV: + /// min-date-time value, if that is defined. + /// + /// Definition: + /// + /// <!ELEMENT max-date-time (#PCDATA)> + /// PCDATA value: an iCalendar format DATE-TIME value in UTC + /// + /// Example: + /// + /// <C:max-date-time xmlns:C="urn:ietf:params:xml:ns:caldav"> + /// 20491231T235959Z + /// </C:max-date-time> + MaxDateTime(DateTime<Utc>), + + /// CALDAV:max-instances Property + /// + /// Name: max-instances + /// + /// Namespace: urn:ietf:params:xml:ns:caldav + /// + /// Purpose: Provides a numeric value indicating the maximum number of + /// recurrence instances that a calendar object resource stored in a + /// calendar collection can generate. + /// + /// Conformance: This property MAY be defined on any calendar + /// collection. If defined, it MUST be protected and SHOULD NOT be + /// returned by a PROPFIND DAV:allprop request (as defined in Section + /// 12.14.1 of [RFC2518]). + /// + /// Description: The CALDAV:max-instances is used to specify a numeric + /// value that indicates the maximum number of recurrence instances + /// that a calendar object resource stored in a calendar collection + /// can generate. Any attempt to store a calendar object resource + /// with a recurrence pattern that generates more instances than this + /// value MUST result in an error, with the CALDAV:max-instances + /// precondition (Section 5.3.2.1) being violated. In the absence of + /// this property, the client can assume that the server has no limits + /// on the number of recurrence instances it can handle or expand. + /// + /// Definition: + /// + /// <!ELEMENT max-instances (#PCDATA)> + /// PCDATA value: a numeric value (integer greater than zero) + /// + /// Example: + /// + /// <C:max-instances xmlns:C="urn:ietf:params:xml:ns:caldav"> + /// 100 + /// </C:max-instances> + MaxInstances(u64), + + /// CALDAV:max-attendees-per-instance Property + /// + /// Name: max-attendees-per-instance + /// + /// Namespace: urn:ietf:params:xml:ns:caldav + /// + /// Purpose: Provides a numeric value indicating the maximum number of + /// ATTENDEE properties in any instance of a calendar object resource + /// stored in a calendar collection. + /// + /// Conformance: This property MAY be defined on any calendar + /// collection. If defined, it MUST be protected and SHOULD NOT be + /// returned by a PROPFIND DAV:allprop request (as defined in Section + /// 12.14.1 of [RFC2518]). + /// + /// Description: The CALDAV:max-attendees-per-instance is used to + /// specify a numeric value that indicates the maximum number of + /// iCalendar ATTENDEE properties on any one instance of a calendar + /// object resource stored in a calendar collection. Any attempt to + /// store a calendar object resource with more ATTENDEE properties per + /// instance than this value MUST result in an error, with the CALDAV: + /// max-attendees-per-instance precondition (Section 5.3.2.1) being + /// violated. In the absence of this property, the client can assume + /// that the server can handle any number of ATTENDEE properties in a + /// calendar component. + /// + /// Definition: + /// + /// <!ELEMENT max-attendees-per-instance (#PCDATA)> + /// PCDATA value: a numeric value (integer greater than zero) + /// + /// Example: + /// + /// <C:max-attendees-per-instance + /// xmlns:C="urn:ietf:params:xml:ns:caldav"> + /// 25 + /// </C:max-attendees-per-instance> + MaxAttendeesPerInstance(u64), + + /// Name: supported-collation-set + /// + /// Namespace: urn:ietf:params:xml:ns:caldav + /// + /// Purpose: Identifies the set of collations supported by the server + /// for text matching operations. + /// + /// Conformance: This property MUST be defined on any resource that + /// supports a report that does text matching. If defined, it MUST be + /// protected and SHOULD NOT be returned by a PROPFIND DAV:allprop + /// request (as defined in Section 12.14.1 of [RFC2518]). + /// + /// Description: The CALDAV:supported-collation-set property contains + /// zero or more CALDAV:supported-collation elements, which specify + /// the collection identifiers of the collations supported by the + /// server. + /// + /// Definition: + /// + /// <!ELEMENT supported-collation-set (supported-collation*)> + /// <!ELEMENT supported-collation (#PCDATA)> + /// + /// Example: + /// + /// <C:supported-collation-set + /// xmlns:C="urn:ietf:params:xml:ns:caldav"> + /// <C:supported-collation>i;ascii-casemap</C:supported-collation> + /// <C:supported-collation>i;octet</C:supported-collation> + /// </C:supported-collation-set> + SupportedCollationSet(Vec<SupportedCollation>), + + /// Name: calendar-data + /// + /// Namespace: urn:ietf:params:xml:ns:caldav + /// + /// Purpose: Specified one of the following: + /// + /// 1. A supported media type for calendar object resources when + /// nested in the CALDAV:supported-calendar-data property; + /// + /// 2. The parts of a calendar object resource should be returned by + /// a calendaring report; + /// + /// 3. The content of a calendar object resource in a response to a + /// calendaring report. + /// + /// Description: When nested in the CALDAV:supported-calendar-data + /// property, the CALDAV:calendar-data XML element specifies a media + /// type supported by the CalDAV server for calendar object resources. + /// + /// When used in a calendaring REPORT request, the CALDAV:calendar- + /// data XML element specifies which parts of calendar object + /// resources need to be returned in the response. If the CALDAV: + /// calendar-data XML element doesn't contain any CALDAV:comp element, + /// calendar object resources will be returned in their entirety. + /// + /// Finally, when used in a calendaring REPORT response, the CALDAV: + /// calendar-data XML element specifies the content of a calendar + /// object resource. Given that XML parsers normalize the two- + /// character sequence CRLF (US-ASCII decimal 13 and US-ASCII decimal + /// 10) to a single LF character (US-ASCII decimal 10), the CR + /// character (US-ASCII decimal 13) MAY be omitted in calendar object + /// resources specified in the CALDAV:calendar-data XML element. + /// Furthermore, calendar object resources specified in the CALDAV: + /// calendar-data XML element MAY be invalid per their media type + /// specification if the CALDAV:calendar-data XML element part of the + /// calendaring REPORT request did not specify required properties + /// (e.g., UID, DTSTAMP, etc.), or specified a CALDAV:prop XML element + /// with the "novalue" attribute set to "yes". + /// + /// Note: The CALDAV:calendar-data XML element is specified in requests + /// and responses inside the DAV:prop XML element as if it were a + /// WebDAV property. However, the CALDAV:calendar-data XML element is + /// not a WebDAV property and, as such, is not returned in PROPFIND + /// responses, nor used in PROPPATCH requests. + /// + /// Note: The iCalendar data embedded within the CALDAV:calendar-data + /// XML element MUST follow the standard XML character data encoding + /// rules, including use of <, >, & etc. entity encoding or + /// the use of a <![CDATA[ ... ]]> construct. In the later case, the + /// iCalendar data cannot contain the character sequence "]]>", which + /// is the end delimiter for the CDATA section. + CalendarData(CalendarDataPayload), +} + +#[derive(Debug, PartialEq)] +pub enum Violation { + /// (DAV:resource-must-be-null): A resource MUST NOT exist at the + /// Request-URI; + ResourceMustBeNull, + + /// (CALDAV:calendar-collection-location-ok): The Request-URI MUST + /// identify a location where a calendar collection can be created; + CalendarCollectionLocationOk, + + /// (CALDAV:valid-calendar-data): The time zone specified in CALDAV: + /// calendar-timezone property MUST be a valid iCalendar object + /// containing a single valid VTIMEZONE component. + ValidCalendarData, + + ///@FIXME should not be here but in RFC3744 + /// !!! ERRATA 1002 !!! + /// (DAV:need-privileges): The DAV:bind privilege MUST be granted to + /// the current user on the parent collection of the Request-URI. + NeedPrivileges, + + /// (CALDAV:initialize-calendar-collection): A new calendar collection + /// exists at the Request-URI. The DAV:resourcetype of the calendar + /// collection MUST contain both DAV:collection and CALDAV:calendar + /// XML elements. + InitializeCalendarCollection, + + /// (CALDAV:supported-calendar-data): The resource submitted in the + /// PUT request, or targeted by a COPY or MOVE request, MUST be a + /// supported media type (i.e., iCalendar) for calendar object + /// resources; + SupportedCalendarData, + + /// (CALDAV:valid-calendar-object-resource): The resource submitted in + /// the PUT request, or targeted by a COPY or MOVE request, MUST obey + /// all restrictions specified in Section 4.1 (e.g., calendar object + /// resources MUST NOT contain more than one type of calendar + /// component, calendar object resources MUST NOT specify the + /// iCalendar METHOD property, etc.); + ValidCalendarObjectResource, + + /// (CALDAV:supported-calendar-component): The resource submitted in + /// the PUT request, or targeted by a COPY or MOVE request, MUST + /// contain a type of calendar component that is supported in the + /// targeted calendar collection; + SupportedCalendarComponent, + + /// (CALDAV:no-uid-conflict): The resource submitted in the PUT + /// request, or targeted by a COPY or MOVE request, MUST NOT specify + /// an iCalendar UID property value already in use in the targeted + /// calendar collection or overwrite an existing calendar object + /// resource with one that has a different UID property value. + /// Servers SHOULD report the URL of the resource that is already + /// making use of the same UID property value in the DAV:href element; + /// + /// <!ELEMENT no-uid-conflict (DAV:href)> + NoUidConflict(dav::Href), + + /// (CALDAV:max-resource-size): The resource submitted in the PUT + /// request, or targeted by a COPY or MOVE request, MUST have an octet + /// size less than or equal to the value of the CALDAV:max-resource- + /// size property value (Section 5.2.5) on the calendar collection + /// where the resource will be stored; + MaxResourceSize, + + /// (CALDAV:min-date-time): The resource submitted in the PUT request, + /// or targeted by a COPY or MOVE request, MUST have all of its + /// iCalendar DATE or DATE-TIME property values (for each recurring + /// instance) greater than or equal to the value of the CALDAV:min- + /// date-time property value (Section 5.2.6) on the calendar + /// collection where the resource will be stored; + MinDateTime, + + /// (CALDAV:max-date-time): The resource submitted in the PUT request, + /// or targeted by a COPY or MOVE request, MUST have all of its + /// iCalendar DATE or DATE-TIME property values (for each recurring + /// instance) less than the value of the CALDAV:max-date-time property + /// value (Section 5.2.7) on the calendar collection where the + /// resource will be stored; + MaxDateTime, + + /// (CALDAV:max-instances): The resource submitted in the PUT request, + /// or targeted by a COPY or MOVE request, MUST generate a number of + /// recurring instances less than or equal to the value of the CALDAV: + /// max-instances property value (Section 5.2.8) on the calendar + /// collection where the resource will be stored; + MaxInstances, + + /// (CALDAV:max-attendees-per-instance): The resource submitted in the + /// PUT request, or targeted by a COPY or MOVE request, MUST have a + /// number of ATTENDEE properties on any one instance less than or + /// equal to the value of the CALDAV:max-attendees-per-instance + /// property value (Section 5.2.9) on the calendar collection where + /// the resource will be stored; + MaxAttendeesPerInstance, + + /// (CALDAV:valid-filter): The CALDAV:filter XML element (see + /// Section 9.7) specified in the REPORT request MUST be valid. For + /// instance, a CALDAV:filter cannot nest a <C:comp name="VEVENT"> + /// element in a <C:comp name="VTODO"> element, and a CALDAV:filter + /// cannot nest a <C:time-range start="..." end="..."> element in a + /// <C:prop name="SUMMARY"> element. + ValidFilter, + + /// (CALDAV:supported-filter): The CALDAV:comp-filter (see + /// Section 9.7.1), CALDAV:prop-filter (see Section 9.7.2), and + /// CALDAV:param-filter (see Section 9.7.3) XML elements used in the + /// CALDAV:filter XML element (see Section 9.7) in the REPORT request + /// only make reference to components, properties, and parameters for + /// which queries are supported by the server, i.e., if the CALDAV: + /// filter element attempts to reference an unsupported component, + /// property, or parameter, this precondition is violated. Servers + /// SHOULD report the CALDAV:comp-filter, CALDAV:prop-filter, or + /// CALDAV:param-filter for which it does not provide support. + /// + /// <!ELEMENT supported-filter (comp-filter*, + /// prop-filter*, + /// param-filter*)> + SupportedFilter { + comp: Vec<CompFilter>, + prop: Vec<PropFilter>, + param: Vec<ParamFilter>, + }, + + /// (DAV:number-of-matches-within-limits): The number of matching + /// calendar object resources must fall within server-specific, + /// predefined limits. For example, this condition might be triggered + /// if a search specification would cause the return of an extremely + /// large number of responses. + NumberOfMatchesWithinLimits, +} + +// -------- Inner XML elements --------- + +/// Some of the reports defined in this section do text matches of +/// character strings provided by the client and are compared to stored +/// calendar data. Since iCalendar data is, by default, encoded in the +/// UTF-8 charset and may include characters outside the US-ASCII charset +/// range in some property and parameter values, there is a need to +/// ensure that text matching follows well-defined rules. +/// +/// To deal with this, this specification makes use of the IANA Collation +/// Registry defined in [RFC4790] to specify collations that may be used +/// to carry out the text comparison operations with a well-defined rule. +/// +/// The comparisons used in CalDAV are all "substring" matches, as per +/// [RFC4790], Section 4.2. Collations supported by the server MUST +/// support "substring" match operations. +/// +/// CalDAV servers are REQUIRED to support the "i;ascii-casemap" and +/// "i;octet" collations, as described in [RFC4790], and MAY support +/// other collations. +/// +/// Servers MUST advertise the set of collations that they support via +/// the CALDAV:supported-collation-set property defined on any resource +/// that supports reports that use collations. +/// +/// Clients MUST only use collations from the list advertised by the +/// server. +/// +/// In the absence of a collation explicitly specified by the client, or +/// if the client specifies the "default" collation identifier (as +/// defined in [RFC4790], Section 3.1), the server MUST default to using +/// "i;ascii-casemap" as the collation. +/// +/// Wildcards (as defined in [RFC4790], Section 3.2) MUST NOT be used in +/// the collation identifier. +/// +/// If the client chooses a collation not supported by the server, the +/// server MUST respond with a CALDAV:supported-collation precondition +/// error response. +#[derive(Debug, PartialEq)] +pub struct SupportedCollation(pub Collation); + +/// <!ELEMENT calendar-data (#PCDATA)> +/// PCDATA value: iCalendar object +/// +/// when nested in the DAV:prop XML element in a calendaring +/// REPORT response to specify the content of a returned +/// calendar object resource. +#[derive(Debug, PartialEq)] +pub struct CalendarDataPayload { + pub mime: Option<CalendarDataSupport>, + pub payload: String, +} + +/// <!ELEMENT calendar-data (comp?, +/// (expand | limit-recurrence-set)?, +/// limit-freebusy-set?)> +/// +/// when nested in the DAV:prop XML element in a calendaring +/// REPORT request to specify which parts of calendar object +/// resources should be returned in the response; +#[derive(Debug, PartialEq)] +pub struct CalendarDataRequest { + pub mime: Option<CalendarDataSupport>, + pub comp: Option<Comp>, + pub recurrence: Option<RecurrenceModifier>, + pub limit_freebusy_set: Option<LimitFreebusySet>, +} + +/// calendar-data specialization for Property +/// +/// <!ELEMENT calendar-data EMPTY> +/// +/// when nested in the CALDAV:supported-calendar-data property +/// to specify a supported media type for calendar object +/// resources; +#[derive(Debug, PartialEq)] +pub struct CalendarDataEmpty(pub Option<CalendarDataSupport>); + +/// <!ATTLIST calendar-data content-type CDATA "text/calendar" +/// version CDATA "2.0"> +/// content-type value: a MIME media type +/// version value: a version string +/// attributes can be used on all three variants of the +/// CALDAV:calendar-data XML element. +#[derive(Debug, PartialEq)] +pub struct CalendarDataSupport { + pub content_type: String, + pub version: String, +} + +/// Name: comp +/// +/// Namespace: urn:ietf:params:xml:ns:caldav +/// +/// Purpose: Defines which component types to return. +/// +/// Description: The name value is a calendar component name (e.g., +/// VEVENT). +/// +/// Definition: +/// +/// <!ELEMENT comp ((allprop | prop*), (allcomp | comp*))> +/// <!ATTLIST comp name CDATA #REQUIRED> +/// name value: a calendar component name +/// +/// Note: The CALDAV:prop and CALDAV:allprop elements have the same name +/// as the DAV:prop and DAV:allprop elements defined in [RFC2518]. +/// However, the CALDAV:prop and CALDAV:allprop elements are defined +/// in the "urn:ietf:params:xml:ns:caldav" namespace instead of the +/// "DAV:" namespace. +#[derive(Debug, PartialEq)] +pub struct Comp { + pub name: Component, + pub additional_rules: Option<CompInner>, +} + +#[derive(Debug, PartialEq)] +pub struct CompInner { + pub prop_kind: PropKind, + pub comp_kind: CompKind, +} + +/// For SupportedCalendarComponentSet +/// +/// Definition: +/// +/// <!ELEMENT supported-calendar-component-set (comp+)> +/// +/// Example: +/// +/// <C:supported-calendar-component-set +/// xmlns:C="urn:ietf:params:xml:ns:caldav"> +/// <C:comp name="VEVENT"/> +/// <C:comp name="VTODO"/> +/// </C:supported-calendar-component-set> +#[derive(Debug, PartialEq)] +pub struct CompSupport(pub Component); + +/// Name: allcomp +/// +/// Namespace: urn:ietf:params:xml:ns:caldav +/// +/// Purpose: Specifies that all components shall be returned. +/// +/// Description: The CALDAV:allcomp XML element can be used when the +/// client wants all types of components returned by a calendaring +/// REPORT request. +/// +/// Definition: +/// +/// <!ELEMENT allcomp EMPTY> +#[derive(Debug, PartialEq)] +pub enum CompKind { + AllComp, + Comp(Vec<Comp>), +} + +/// Name: allprop +/// +/// Namespace: urn:ietf:params:xml:ns:caldav +/// +/// Purpose: Specifies that all properties shall be returned. +/// +/// Description: The CALDAV:allprop XML element can be used when the +/// client wants all properties of components returned by a +/// calendaring REPORT request. +/// +/// Definition: +/// +/// <!ELEMENT allprop EMPTY> +/// +/// Note: The CALDAV:allprop element has the same name as the DAV: +/// allprop element defined in [RFC2518]. However, the CALDAV:allprop +/// element is defined in the "urn:ietf:params:xml:ns:caldav" +/// namespace instead of the "DAV:" namespace. +#[derive(Debug, PartialEq)] +pub enum PropKind { + AllProp, + Prop(Vec<CalProp>), +} + +/// Name: prop +/// +/// Namespace: urn:ietf:params:xml:ns:caldav +/// +/// Purpose: Defines which properties to return in the response. +/// +/// Description: The "name" attribute specifies the name of the calendar +/// property to return (e.g., ATTENDEE). The "novalue" attribute can +/// be used by clients to request that the actual value of the +/// property not be returned (if the "novalue" attribute is set to +/// "yes"). In that case, the server will return just the iCalendar +/// property name and any iCalendar parameters and a trailing ":" +/// without the subsequent value data. +/// +/// Definition: +/// <!ELEMENT prop EMPTY> +/// <!ATTLIST prop name CDATA #REQUIRED novalue (yes | no) "no"> +/// name value: a calendar property name +/// novalue value: "yes" or "no" +/// +/// Note: The CALDAV:prop element has the same name as the DAV:prop +/// element defined in [RFC2518]. However, the CALDAV:prop element is +/// defined in the "urn:ietf:params:xml:ns:caldav" namespace instead +/// of the "DAV:" namespace. +#[derive(Debug, PartialEq)] +pub struct CalProp { + pub name: ComponentProperty, + pub novalue: Option<bool>, +} + +#[derive(Debug, PartialEq)] +pub enum RecurrenceModifier { + Expand(Expand), + LimitRecurrenceSet(LimitRecurrenceSet), +} + +/// Name: expand +/// +/// Namespace: urn:ietf:params:xml:ns:caldav +/// +/// Purpose: Forces the server to expand recurring components into +/// individual recurrence instances. +/// +/// Description: The CALDAV:expand XML element specifies that for a +/// given calendaring REPORT request, the server MUST expand the +/// recurrence set into calendar components that define exactly one +/// recurrence instance, and MUST return only those whose scheduled +/// time intersect a specified time range. +/// +/// The "start" attribute specifies the inclusive start of the time +/// range, and the "end" attribute specifies the non-inclusive end of +/// the time range. Both attributes are specified as date with UTC +/// time value. The value of the "end" attribute MUST be greater than +/// the value of the "start" attribute. +/// +/// The server MUST use the same logic as defined for CALDAV:time- +/// range to determine if a recurrence instance intersects the +/// specified time range. +/// +/// Recurring components, other than the initial instance, MUST +/// include a RECURRENCE-ID property indicating which instance they +/// refer to. +/// +/// The returned calendar components MUST NOT use recurrence +/// properties (i.e., EXDATE, EXRULE, RDATE, and RRULE) and MUST NOT +/// have reference to or include VTIMEZONE components. Date and local +/// time with reference to time zone information MUST be converted +/// into date with UTC time. +/// +/// Definition: +/// +/// <!ELEMENT expand EMPTY> +/// <!ATTLIST expand start CDATA #REQUIRED +/// end CDATA #REQUIRED> +/// start value: an iCalendar "date with UTC time" +/// end value: an iCalendar "date with UTC time" +#[derive(Debug, PartialEq)] +pub struct Expand(pub DateTime<Utc>, pub DateTime<Utc>); + +/// CALDAV:limit-recurrence-set XML Element +/// +/// Name: limit-recurrence-set +/// +/// Namespace: urn:ietf:params:xml:ns:caldav +/// +/// Purpose: Specifies a time range to limit the set of "overridden +/// components" returned by the server. +/// +/// Description: The CALDAV:limit-recurrence-set XML element specifies +/// that for a given calendaring REPORT request, the server MUST +/// return, in addition to the "master component", only the +/// "overridden components" that impact a specified time range. An +/// overridden component impacts a time range if its current start and +/// end times overlap the time range, or if the original start and end +/// times -- the ones that would have been used if the instance were +/// not overridden -- overlap the time range. +/// +/// The "start" attribute specifies the inclusive start of the time +/// range, and the "end" attribute specifies the non-inclusive end of +/// the time range. Both attributes are specified as date with UTC +/// time value. The value of the "end" attribute MUST be greater than +/// the value of the "start" attribute. +/// +/// The server MUST use the same logic as defined for CALDAV:time- +/// range to determine if the current or original scheduled time of an +/// "overridden" recurrence instance intersects the specified time +/// range. +/// +/// Overridden components that have a RANGE parameter on their +/// RECURRENCE-ID property may specify one or more instances in the +/// recurrence set, and some of those instances may fall within the +/// specified time range or may have originally fallen within the +/// specified time range prior to being overridden. If that is the +/// case, the overridden component MUST be included in the results, as +/// it has a direct impact on the interpretation of instances within +/// the specified time range. +/// +/// Definition: +/// +/// <!ELEMENT limit-recurrence-set EMPTY> +/// <!ATTLIST limit-recurrence-set start CDATA #REQUIRED +/// end CDATA #REQUIRED> +/// start value: an iCalendar "date with UTC time" +/// end value: an iCalendar "date with UTC time" +#[derive(Debug, PartialEq)] +pub struct LimitRecurrenceSet(pub DateTime<Utc>, pub DateTime<Utc>); + +/// Name: limit-freebusy-set +/// +/// Namespace: urn:ietf:params:xml:ns:caldav +/// +/// Purpose: Specifies a time range to limit the set of FREEBUSY values +/// returned by the server. +/// +/// Description: The CALDAV:limit-freebusy-set XML element specifies +/// that for a given calendaring REPORT request, the server MUST only +/// return the FREEBUSY property values of a VFREEBUSY component that +/// intersects a specified time range. +/// +/// The "start" attribute specifies the inclusive start of the time +/// range, and the "end" attribute specifies the non-inclusive end of +/// the time range. Both attributes are specified as "date with UTC +/// time" value. The value of the "end" attribute MUST be greater +/// than the value of the "start" attribute. +/// +/// The server MUST use the same logic as defined for CALDAV:time- +/// range to determine if a FREEBUSY property value intersects the +/// specified time range. +/// +/// Definition: +/// <!ELEMENT limit-freebusy-set EMPTY> +/// <!ATTLIST limit-freebusy-set start CDATA #REQUIRED +/// end CDATA #REQUIRED> +/// start value: an iCalendar "date with UTC time" +/// end value: an iCalendar "date with UTC time" +#[derive(Debug, PartialEq)] +pub struct LimitFreebusySet(pub DateTime<Utc>, pub DateTime<Utc>); + +/// Used by CalendarQuery & CalendarMultiget +#[derive(Debug, PartialEq)] +pub enum CalendarSelector<E: dav::Extension> { + AllProp, + PropName, + Prop(dav::PropName<E>), +} + +/// Name: comp-filter +/// +/// Namespace: urn:ietf:params:xml:ns:caldav +/// +/// Purpose: Specifies search criteria on calendar components. +/// +/// Description: The CALDAV:comp-filter XML element specifies a query +/// targeted at the calendar object (i.e., VCALENDAR) or at a specific +/// calendar component type (e.g., VEVENT). The scope of the +/// CALDAV:comp-filter XML element is the calendar object when used as +/// a child of the CALDAV:filter XML element. The scope of the +/// CALDAV:comp-filter XML element is the enclosing calendar component +/// when used as a child of another CALDAV:comp-filter XML element. A +/// CALDAV:comp-filter is said to match if: +/// +/// * The CALDAV:comp-filter XML element is empty and the calendar +/// object or calendar component type specified by the "name" +/// attribute exists in the current scope; +/// +/// or: +/// +/// * The CALDAV:comp-filter XML element contains a CALDAV:is-not- +/// defined XML element and the calendar object or calendar +/// component type specified by the "name" attribute does not exist +/// in the current scope; +/// +/// or: +/// +/// * The CALDAV:comp-filter XML element contains a CALDAV:time-range +/// XML element and at least one recurrence instance in the +/// targeted calendar component is scheduled to overlap the +/// specified time range, and all specified CALDAV:prop-filter and +/// CALDAV:comp-filter child XML elements also match the targeted +/// calendar component; +/// +/// or: +/// +/// * The CALDAV:comp-filter XML element only contains CALDAV:prop- +/// filter and CALDAV:comp-filter child XML elements that all match +/// the targeted calendar component. +/// +/// Definition: +/// +/// ```xmlschema +/// <!ELEMENT comp-filter (is-not-defined | (time-range?, +/// prop-filter*, comp-filter*))> +/// +/// <!ATTLIST comp-filter name CDATA #REQUIRED> +/// name value: a calendar object or calendar component +/// type (e.g., VEVENT) +/// ``` +#[derive(Debug, PartialEq)] +pub struct CompFilter { + pub name: Component, + // Option 1 = None, Option 2, 3, 4 = Some + pub additional_rules: Option<CompFilterRules>, +} +#[derive(Debug, PartialEq)] +pub enum CompFilterRules { + // Option 2 + IsNotDefined, + // Options 3 & 4 + Matches(CompFilterMatch), +} +#[derive(Debug, PartialEq)] +pub struct CompFilterMatch { + pub time_range: Option<TimeRange>, + pub prop_filter: Vec<PropFilter>, + pub comp_filter: Vec<CompFilter>, +} + +/// Name: prop-filter +/// +/// Namespace: urn:ietf:params:xml:ns:caldav +/// +/// Purpose: Specifies search criteria on calendar properties. +/// +/// Description: The CALDAV:prop-filter XML element specifies a query +/// targeted at a specific calendar property (e.g., CATEGORIES) in the +/// scope of the enclosing calendar component. A calendar property is +/// said to match a CALDAV:prop-filter if: +/// +/// * The CALDAV:prop-filter XML element is empty and a property of +/// the type specified by the "name" attribute exists in the +/// enclosing calendar component; +/// +/// or: +/// +/// * The CALDAV:prop-filter XML element contains a CALDAV:is-not- +/// defined XML element and no property of the type specified by +/// the "name" attribute exists in the enclosing calendar +/// component; +/// +/// or: +/// +/// * The CALDAV:prop-filter XML element contains a CALDAV:time-range +/// XML element and the property value overlaps the specified time +/// range, and all specified CALDAV:param-filter child XML elements +/// also match the targeted property; +/// +/// or: +/// +/// * The CALDAV:prop-filter XML element contains a CALDAV:text-match +/// XML element and the property value matches it, and all +/// specified CALDAV:param-filter child XML elements also match the +/// targeted property; +/// +/// Definition: +/// +/// ```xmlschema +/// <!ELEMENT prop-filter (is-not-defined | +/// ((time-range | text-match)?, +/// param-filter*))> +/// +/// <!ATTLIST prop-filter name CDATA #REQUIRED> +/// name value: a calendar property name (e.g., ATTENDEE) +/// ``` +#[derive(Debug, PartialEq)] +pub struct PropFilter { + pub name: Component, + // None = Option 1, Some() = Option 2, 3 & 4 + pub additional_rules: Option<PropFilterRules>, +} +#[derive(Debug, PartialEq)] +pub enum PropFilterRules { + // Option 2 + IsNotDefined, + // Options 3 & 4 + Match(PropFilterMatch), +} +#[derive(Debug, PartialEq)] +pub struct PropFilterMatch { + pub time_range: Option<TimeRange>, + pub time_or_text: Option<TimeOrText>, + pub param_filter: Vec<ParamFilter>, +} +#[derive(Debug, PartialEq)] +pub enum TimeOrText { + Time(TimeRange), + Text(TextMatch), +} + +/// Name: text-match +/// +/// Namespace: urn:ietf:params:xml:ns:caldav +/// +/// Purpose: Specifies a substring match on a property or parameter +/// value. +/// +/// Description: The CALDAV:text-match XML element specifies text used +/// for a substring match against the property or parameter value +/// specified in a calendaring REPORT request. +/// +/// The "collation" attribute is used to select the collation that the +/// server MUST use for character string matching. In the absence of +/// this attribute, the server MUST use the "i;ascii-casemap" +/// collation. +/// +/// The "negate-condition" attribute is used to indicate that this +/// test returns a match if the text matches when the attribute value +/// is set to "no", or return a match if the text does not match, if +/// the attribute value is set to "yes". For example, this can be +/// used to match components with a STATUS property not set to +/// CANCELLED. +/// +/// Definition: +/// <!ELEMENT text-match (#PCDATA)> +/// PCDATA value: string +/// <!ATTLIST text-match collation CDATA "i;ascii-casemap" +/// negate-condition (yes | no) "no"> +#[derive(Debug, PartialEq)] +pub struct TextMatch { + pub collation: Option<Collation>, + pub negate_condition: Option<bool>, + pub text: String, +} + +/// Name: param-filter +/// +/// Namespace: urn:ietf:params:xml:ns:caldav +/// +/// Purpose: Limits the search to specific parameter values. +/// +/// Description: The CALDAV:param-filter XML element specifies a query +/// targeted at a specific calendar property parameter (e.g., +/// PARTSTAT) in the scope of the calendar property on which it is +/// defined. A calendar property parameter is said to match a CALDAV: +/// param-filter if: +/// +/// * The CALDAV:param-filter XML element is empty and a parameter of +/// the type specified by the "name" attribute exists on the +/// calendar property being examined; +/// +/// or: +/// +/// * The CALDAV:param-filter XML element contains a CALDAV:is-not- +/// defined XML element and no parameter of the type specified by +/// the "name" attribute exists on the calendar property being +/// examined; +/// +/// Definition: +/// +/// ```xmlschema +/// <!ELEMENT param-filter (is-not-defined | text-match?)> +/// +/// <!ATTLIST param-filter name CDATA #REQUIRED> +/// name value: a property parameter name (e.g., PARTSTAT) +/// ``` +#[derive(Debug, PartialEq)] +pub struct ParamFilter { + pub name: PropertyParameter, + pub additional_rules: Option<ParamFilterMatch>, +} +#[derive(Debug, PartialEq)] +pub enum ParamFilterMatch { + IsNotDefined, + Match(TextMatch), +} + +/// CALDAV:is-not-defined XML Element +/// +/// Name: is-not-defined +/// +/// Namespace: urn:ietf:params:xml:ns:caldav +/// +/// Purpose: Specifies that a match should occur if the enclosing +/// component, property, or parameter does not exist. +/// +/// Description: The CALDAV:is-not-defined XML element specifies that a +/// match occurs if the enclosing component, property, or parameter +/// value specified in a calendaring REPORT request does not exist in +/// the calendar data being tested. +/// +/// Definition: +/// <!ELEMENT is-not-defined EMPTY> +/* CURRENTLY INLINED */ + + + +/// Name: timezone +/// +/// Namespace: urn:ietf:params:xml:ns:caldav +/// +/// Purpose: Specifies the time zone component to use when determining +/// the results of a report. +/// +/// Description: The CALDAV:timezone XML element specifies that for a +/// given calendaring REPORT request, the server MUST rely on the +/// specified VTIMEZONE component instead of the CALDAV:calendar- +/// timezone property of the calendar collection, in which the +/// calendar object resource is contained to resolve "date" values and +/// "date with local time" values (i.e., floating time) to "date with +/// UTC time" values. The server will require this information to +/// determine if a calendar component scheduled with "date" values or +/// "date with local time" values intersects a CALDAV:time-range +/// specified in a CALDAV:calendar-query REPORT. +/// +/// Note: The iCalendar data embedded within the CALDAV:timezone XML +/// element MUST follow the standard XML character data encoding +/// rules, including use of <, >, & etc. entity encoding or +/// the use of a <![CDATA[ ... ]]> construct. In the later case, the +/// +/// iCalendar data cannot contain the character sequence "]]>", which +/// is the end delimiter for the CDATA section. +/// +/// Definition: +/// +/// <!ELEMENT timezone (#PCDATA)> +/// PCDATA value: an iCalendar object with exactly one VTIMEZONE +#[derive(Debug, PartialEq)] +pub struct TimeZone(pub String); + +/// Name: filter +/// +/// Namespace: urn:ietf:params:xml:ns:caldav +/// +/// Purpose: Specifies a filter to limit the set of calendar components +/// returned by the server. +/// +/// Description: The CALDAV:filter XML element specifies the search +/// filter used to limit the calendar components returned by a +/// calendaring REPORT request. +/// +/// Definition: +/// <!ELEMENT filter (comp-filter)> +#[derive(Debug, PartialEq)] +pub struct Filter(pub CompFilter); + +/// Name: time-range +/// +/// Definition: +/// +/// <!ELEMENT time-range EMPTY> +/// <!ATTLIST time-range start CDATA #IMPLIED +/// end CDATA #IMPLIED> +/// start value: an iCalendar "date with UTC time" +/// end value: an iCalendar "date with UTC time" +#[derive(Debug, PartialEq)] +pub enum TimeRange { + OnlyStart(DateTime<Utc>), + OnlyEnd(DateTime<Utc>), + FullRange(DateTime<Utc>, DateTime<Utc>), +} + +// ----------------------- ENUM ATTRIBUTES --------------------- + +/// Known components +#[derive(Debug, PartialEq)] +pub enum Component { + VCalendar, + VJournal, + VFreeBusy, + VEvent, + VTodo, + VAlarm, + VTimeZone, + Unknown(String), +} +impl Component { + pub fn as_str<'a>(&'a self) -> &'a str { + match self { + Self::VCalendar => "VCALENDAR", + Self::VJournal => "VJOURNAL", + Self::VFreeBusy => "VFREEBUSY", + Self::VEvent => "VEVENT", + Self::VTodo => "VTODO", + Self::VAlarm => "VALARM", + Self::VTimeZone => "VTIMEZONE", + Self::Unknown(c) => c, + } + } +} + +/// name="VERSION", name="SUMMARY", etc. +/// Can be set on different objects: VCalendar, VEvent, etc. +/// Might be replaced by an enum later +#[derive(Debug, PartialEq)] +pub struct ComponentProperty(pub String); + +/// like PARSTAT +#[derive(Debug, PartialEq)] +pub struct PropertyParameter(pub String); +impl PropertyParameter { + pub fn as_str<'a>(&'a self) -> &'a str { + self.0.as_str() + } +} + +#[derive(Default,Debug,PartialEq)] +pub enum Collation { + #[default] + AsciiCaseMap, + Octet, + Unknown(String), +} +impl Collation { + pub fn as_str<'a>(&'a self) -> &'a str { + match self { + Self::AsciiCaseMap => "i;ascii-casemap", + Self::Octet => "i;octet", + Self::Unknown(c) => c.as_str(), + } + } +} diff --git a/aero-dav/src/decoder.rs b/aero-dav/src/decoder.rs new file mode 100644 index 0000000..65cb712 --- /dev/null +++ b/aero-dav/src/decoder.rs @@ -0,0 +1,947 @@ +use std::future::Future; + +use quick_xml::events::Event; +use quick_xml::events::attributes::AttrError; +use quick_xml::name::{Namespace, QName, PrefixDeclaration, ResolveResult, ResolveResult::*}; +use quick_xml::reader::NsReader; +use tokio::io::AsyncBufRead; + +use super::types::*; +use super::error::ParsingError; +use super::xml::{Node, QRead, Reader, IRead, DAV_URN, CAL_URN}; + +//@TODO (1) Rewrite all objects as Href, +// where we return Ok(None) instead of trying to find the object at any cost. +// Add a xml.find<E: Qread>() -> Result<Option<E>, ParsingError> or similar for the cases we +// really need the object +// (2) Rewrite QRead and replace Result<Option<_>, _> with Result<_, _>, not found being a possible +// error. +// (3) Rewrite vectors with xml.collect<E: QRead>() -> Result<Vec<E>, _> +// (4) Something for alternatives would be great but no idea yet + +// ---- ROOT ---- + +/// Propfind request +impl<E: Extension> QRead<PropFind<E>> for PropFind<E> { + async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> { + xml.open(DAV_URN, "propfind").await?; + let propfind: PropFind<E> = loop { + // allprop + if let Some(_) = xml.maybe_open(DAV_URN, "allprop").await? { + let includ = xml.maybe_find::<Include<E>>().await?; + xml.close().await?; + break PropFind::AllProp(includ) + } + + // propname + if let Some(_) = xml.maybe_open(DAV_URN, "propname").await? { + xml.close().await?; + break PropFind::PropName + } + + // prop + let (mut maybe_prop, mut dirty) = (None, false); + xml.maybe_read::<PropName<E>>(&mut maybe_prop, &mut dirty).await?; + if let Some(prop) = maybe_prop { + break PropFind::Prop(prop) + } + + // not found, skipping + xml.skip().await?; + }; + xml.close().await?; + + Ok(propfind) + } +} + +/// PROPPATCH request +impl<E: Extension> QRead<PropertyUpdate<E>> for PropertyUpdate<E> { + async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> { + xml.open(DAV_URN, "propertyupdate").await?; + let collected_items = xml.collect::<PropertyUpdateItem<E>>().await?; + xml.close().await?; + Ok(PropertyUpdate(collected_items)) + } +} + +/// Generic response +impl<E: Extension, N: Node<N>> QRead<Multistatus<E,N>> for Multistatus<E,N> { + async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> { + xml.open(DAV_URN, "multistatus").await?; + let mut responses = Vec::new(); + let mut responsedescription = None; + + loop { + let mut dirty = false; + xml.maybe_push(&mut responses, &mut dirty).await?; + xml.maybe_read(&mut responsedescription, &mut dirty).await?; + if !dirty { + match xml.peek() { + Event::End(_) => break, + _ => xml.skip().await?, + }; + } + } + + xml.close().await?; + Ok(Multistatus { responses, responsedescription }) + } +} + +// LOCK REQUEST +impl QRead<LockInfo> for LockInfo { + async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> { + xml.open(DAV_URN, "lockinfo").await?; + let (mut m_scope, mut m_type, mut owner) = (None, None, None); + loop { + let mut dirty = false; + xml.maybe_read::<LockScope>(&mut m_scope, &mut dirty).await?; + xml.maybe_read::<LockType>(&mut m_type, &mut dirty).await?; + xml.maybe_read::<Owner>(&mut owner, &mut dirty).await?; + + if !dirty { + match xml.peek() { + Event::End(_) => break, + _ => xml.skip().await?, + }; + } + } + xml.close().await?; + match (m_scope, m_type) { + (Some(lockscope), Some(locktype)) => Ok(LockInfo { lockscope, locktype, owner }), + _ => Err(ParsingError::MissingChild), + } + } +} + +// LOCK RESPONSE +impl<E: Extension> QRead<PropValue<E>> for PropValue<E> { + async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> { + xml.open(DAV_URN, "prop").await?; + let mut acc = xml.collect::<Property<E>>().await?; + xml.close().await?; + Ok(PropValue(acc)) + } +} + + +/// Error response +impl<E: Extension> QRead<Error<E>> for Error<E> { + async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> { + xml.open(DAV_URN, "error").await?; + let violations = xml.collect::<Violation<E>>().await?; + xml.close().await?; + Ok(Error(violations)) + } +} + + + +// ---- INNER XML +impl<E: Extension, N: Node<N>> QRead<Response<E,N>> for Response<E,N> { + async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> { + xml.open(DAV_URN, "response").await?; + let (mut status, mut error, mut responsedescription, mut location) = (None, None, None, None); + let mut href = Vec::new(); + let mut propstat = Vec::new(); + + loop { + let mut dirty = false; + xml.maybe_read::<Status>(&mut status, &mut dirty).await?; + xml.maybe_push::<Href>(&mut href, &mut dirty).await?; + xml.maybe_push::<PropStat<E,N>>(&mut propstat, &mut dirty).await?; + xml.maybe_read::<Error<E>>(&mut error, &mut dirty).await?; + xml.maybe_read::<ResponseDescription>(&mut responsedescription, &mut dirty).await?; + xml.maybe_read::<Location>(&mut location, &mut dirty).await?; + + if !dirty { + match xml.peek() { + Event::End(_) => break, + _ => { xml.skip().await? }, + }; + } + } + + xml.close().await?; + match (status, &propstat[..], &href[..]) { + (Some(status), &[], &[_, ..]) => Ok(Response { + status_or_propstat: StatusOrPropstat::Status(href, status), + error, responsedescription, location, + }), + (None, &[_, ..], &[_, ..]) => Ok(Response { + status_or_propstat: StatusOrPropstat::PropStat(href.into_iter().next().unwrap(), propstat), + error, responsedescription, location, + }), + (Some(_), &[_, ..], _) => Err(ParsingError::InvalidValue), + _ => Err(ParsingError::MissingChild), + } + } +} + +impl<E: Extension, N: Node<N>> QRead<PropStat<E,N>> for PropStat<E,N> { + async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> { + xml.open(DAV_URN, "propstat").await?; + + let (mut m_prop, mut m_status, mut error, mut responsedescription) = (None, None, None, None); + + loop { + let mut dirty = false; + xml.maybe_read::<N>(&mut m_prop, &mut dirty).await?; + xml.maybe_read::<Status>(&mut m_status, &mut dirty).await?; + xml.maybe_read::<Error<E>>(&mut error, &mut dirty).await?; + xml.maybe_read::<ResponseDescription>(&mut responsedescription, &mut dirty).await?; + + if !dirty { + match xml.peek() { + Event::End(_) => break, + _ => xml.skip().await?, + }; + } + } + + xml.close().await?; + match (m_prop, m_status) { + (Some(prop), Some(status)) => Ok(PropStat { prop, status, error, responsedescription }), + _ => Err(ParsingError::MissingChild), + } + } +} + +impl QRead<Status> for Status { + async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> { + xml.open(DAV_URN, "status").await?; + let fullcode = xml.tag_string().await?; + let txtcode = fullcode.splitn(3, ' ').nth(1).ok_or(ParsingError::InvalidValue)?; + let code = http::status::StatusCode::from_bytes(txtcode.as_bytes()).or(Err(ParsingError::InvalidValue))?; + xml.close().await?; + Ok(Status(code)) + } +} + +impl QRead<ResponseDescription> for ResponseDescription { + async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> { + xml.open(DAV_URN, "responsedescription").await?; + let cnt = xml.tag_string().await?; + xml.close().await?; + Ok(ResponseDescription(cnt)) + } +} + +impl QRead<Location> for Location { + async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> { + xml.open(DAV_URN, "location").await?; + let href = xml.find::<Href>().await?; + xml.close().await?; + Ok(Location(href)) + } +} + +impl<E: Extension> QRead<PropertyUpdateItem<E>> for PropertyUpdateItem<E> { + async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> { + match Remove::qread(xml).await { + Err(ParsingError::Recoverable) => (), + otherwise => return otherwise.map(PropertyUpdateItem::Remove), + } + Set::qread(xml).await.map(PropertyUpdateItem::Set) + } +} + +impl<E: Extension> QRead<Remove<E>> for Remove<E> { + async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> { + xml.open(DAV_URN, "remove").await?; + let propname = xml.find::<PropName<E>>().await?; + xml.close().await?; + Ok(Remove(propname)) + } +} + +impl<E: Extension> QRead<Set<E>> for Set<E> { + async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> { + xml.open(DAV_URN, "set").await?; + let propvalue = xml.find::<PropValue<E>>().await?; + xml.close().await?; + Ok(Set(propvalue)) + } +} + +impl<E: Extension> QRead<Violation<E>> for Violation<E> { + async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> { + if xml.maybe_open(DAV_URN, "lock-token-matches-request-uri").await?.is_some() { + xml.close().await?; + Ok(Violation::LockTokenMatchesRequestUri) + } else if xml.maybe_open(DAV_URN, "lock-token-submitted").await?.is_some() { + let links = xml.collect::<Href>().await?; + xml.close().await?; + Ok(Violation::LockTokenSubmitted(links)) + } else if xml.maybe_open(DAV_URN, "no-conflicting-lock").await?.is_some() { + let links = xml.collect::<Href>().await?; + xml.close().await?; + Ok(Violation::NoConflictingLock(links)) + } else if xml.maybe_open(DAV_URN, "no-external-entities").await?.is_some() { + xml.close().await?; + Ok(Violation::NoExternalEntities) + } else if xml.maybe_open(DAV_URN, "preserved-live-properties").await?.is_some() { + xml.close().await?; + Ok(Violation::PreservedLiveProperties) + } else if xml.maybe_open(DAV_URN, "propfind-finite-depth").await?.is_some() { + xml.close().await?; + Ok(Violation::PropfindFiniteDepth) + } else if xml.maybe_open(DAV_URN, "cannot-modify-protected-property").await?.is_some() { + xml.close().await?; + Ok(Violation::CannotModifyProtectedProperty) + } else { + E::Error::qread(xml).await.map(Violation::Extension) + } + } +} + +impl<E: Extension> QRead<Include<E>> for Include<E> { + async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> { + xml.open(DAV_URN, "include").await?; + let acc = xml.collect::<PropertyRequest<E>>().await?; + xml.close().await?; + Ok(Include(acc)) + } +} + +impl<E: Extension> QRead<PropName<E>> for PropName<E> { + async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> { + xml.open(DAV_URN, "prop").await?; + let acc = xml.collect::<PropertyRequest<E>>().await?; + xml.close().await?; + Ok(PropName(acc)) + } +} + +impl<E: Extension> QRead<PropertyRequest<E>> for PropertyRequest<E> { + async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> { + let maybe = if xml.maybe_open(DAV_URN, "creationdate").await?.is_some() { + Some(PropertyRequest::CreationDate) + } else if xml.maybe_open(DAV_URN, "displayname").await?.is_some() { + Some(PropertyRequest::DisplayName) + } else if xml.maybe_open(DAV_URN, "getcontentlanguage").await?.is_some() { + Some(PropertyRequest::GetContentLanguage) + } else if xml.maybe_open(DAV_URN, "getcontentlength").await?.is_some() { + Some(PropertyRequest::GetContentLength) + } else if xml.maybe_open(DAV_URN, "getcontenttype").await?.is_some() { + Some(PropertyRequest::GetContentType) + } else if xml.maybe_open(DAV_URN, "getetag").await?.is_some() { + Some(PropertyRequest::GetEtag) + } else if xml.maybe_open(DAV_URN, "getlastmodified").await?.is_some() { + Some(PropertyRequest::GetLastModified) + } else if xml.maybe_open(DAV_URN, "lockdiscovery").await?.is_some() { + Some(PropertyRequest::LockDiscovery) + } else if xml.maybe_open(DAV_URN, "resourcetype").await?.is_some() { + Some(PropertyRequest::ResourceType) + } else if xml.maybe_open(DAV_URN, "supportedlock").await?.is_some() { + Some(PropertyRequest::SupportedLock) + } else { + None + }; + + match maybe { + Some(pr) => { + xml.close().await?; + Ok(pr) + }, + None => E::PropertyRequest::qread(xml).await.map(PropertyRequest::Extension), + } + } +} + +impl<E: Extension> QRead<Property<E>> for Property<E> { + async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> { + use chrono::{DateTime, FixedOffset, TimeZone}; + + // Core WebDAV properties + if xml.maybe_open(DAV_URN, "creationdate").await?.is_some() { + let datestr = xml.tag_string().await?; + xml.close().await?; + return Ok(Property::CreationDate(DateTime::parse_from_rfc3339(datestr.as_str())?)) + } else if xml.maybe_open(DAV_URN, "displayname").await?.is_some() { + let name = xml.tag_string().await?; + xml.close().await?; + return Ok(Property::DisplayName(name)) + } else if xml.maybe_open(DAV_URN, "getcontentlanguage").await?.is_some() { + let lang = xml.tag_string().await?; + xml.close().await?; + return Ok(Property::GetContentLanguage(lang)) + } else if xml.maybe_open(DAV_URN, "getcontentlength").await?.is_some() { + let cl = xml.tag_string().await?.parse::<u64>()?; + xml.close().await?; + return Ok(Property::GetContentLength(cl)) + } else if xml.maybe_open(DAV_URN, "getcontenttype").await?.is_some() { + let ct = xml.tag_string().await?; + xml.close().await?; + return Ok(Property::GetContentType(ct)) + } else if xml.maybe_open(DAV_URN, "getetag").await?.is_some() { + let etag = xml.tag_string().await?; + xml.close().await?; + return Ok(Property::GetEtag(etag)) + } else if xml.maybe_open(DAV_URN, "getlastmodified").await?.is_some() { + let datestr = xml.tag_string().await?; + xml.close().await?; + return Ok(Property::GetLastModified(DateTime::parse_from_rfc2822(datestr.as_str())?)) + } else if xml.maybe_open(DAV_URN, "lockdiscovery").await?.is_some() { + let acc = xml.collect::<ActiveLock>().await?; + xml.close().await?; + return Ok(Property::LockDiscovery(acc)) + } else if xml.maybe_open(DAV_URN, "resourcetype").await?.is_some() { + let acc = xml.collect::<ResourceType<E>>().await?; + xml.close().await?; + return Ok(Property::ResourceType(acc)) + } else if xml.maybe_open(DAV_URN, "supportedlock").await?.is_some() { + let acc = xml.collect::<LockEntry>().await?; + xml.close().await?; + return Ok(Property::SupportedLock(acc)) + } + + // Option 2: an extension property, delegating + E::Property::qread(xml).await.map(Property::Extension) + } +} + +impl QRead<ActiveLock> for ActiveLock { + async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> { + xml.open(DAV_URN, "activelock").await?; + let (mut m_scope, mut m_type, mut m_depth, mut owner, mut timeout, mut locktoken, mut m_root) = + (None, None, None, None, None, None, None); + + loop { + let mut dirty = false; + xml.maybe_read::<LockScope>(&mut m_scope, &mut dirty).await?; + xml.maybe_read::<LockType>(&mut m_type, &mut dirty).await?; + xml.maybe_read::<Depth>(&mut m_depth, &mut dirty).await?; + xml.maybe_read::<Owner>(&mut owner, &mut dirty).await?; + xml.maybe_read::<Timeout>(&mut timeout, &mut dirty).await?; + xml.maybe_read::<LockToken>(&mut locktoken, &mut dirty).await?; + xml.maybe_read::<LockRoot>(&mut m_root, &mut dirty).await?; + + if !dirty { + match xml.peek() { + Event::End(_) => break, + _ => { xml.skip().await?; }, + } + } + } + + xml.close().await?; + match (m_scope, m_type, m_depth, m_root) { + (Some(lockscope), Some(locktype), Some(depth), Some(lockroot)) => + Ok(ActiveLock { lockscope, locktype, depth, owner, timeout, locktoken, lockroot }), + _ => Err(ParsingError::MissingChild), + } + } +} + +impl QRead<Depth> for Depth { + async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> { + xml.open(DAV_URN, "depth").await?; + let depth_str = xml.tag_string().await?; + xml.close().await?; + match depth_str.as_str() { + "0" => Ok(Depth::Zero), + "1" => Ok(Depth::One), + "infinity" => Ok(Depth::Infinity), + _ => Err(ParsingError::WrongToken), + } + } +} + +impl QRead<Owner> for Owner { + async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> { + xml.open(DAV_URN, "owner").await?; + + let mut owner = Owner::Unknown; + loop { + match xml.peek() { + Event::Text(_) | Event::CData(_) => { + let txt = xml.tag_string().await?; + if matches!(owner, Owner::Unknown) { + owner = Owner::Txt(txt); + } + } + Event::Start(_) | Event::Empty(_) => { + match Href::qread(xml).await { + Ok(href) => { owner = Owner::Href(href); }, + Err(ParsingError::Recoverable) => { xml.skip().await?; }, + Err(e) => return Err(e), + } + } + Event::End(_) => break, + _ => { xml.skip().await?; }, + } + }; + xml.close().await?; + Ok(owner) + } +} + +impl QRead<Timeout> for Timeout { + async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> { + const SEC_PFX: &str = "SEC_PFX"; + xml.open(DAV_URN, "timeout").await?; + + let timeout = match xml.tag_string().await?.as_str() { + "Infinite" => Timeout::Infinite, + seconds => match seconds.strip_prefix(SEC_PFX) { + Some(secs) => Timeout::Seconds(secs.parse::<u32>()?), + None => return Err(ParsingError::InvalidValue), + }, + }; + + xml.close().await?; + Ok(timeout) + } +} + +impl QRead<LockToken> for LockToken { + async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> { + xml.open(DAV_URN, "locktoken").await?; + let href = Href::qread(xml).await?; + xml.close().await?; + Ok(LockToken(href)) + } +} + +impl QRead<LockRoot> for LockRoot { + async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> { + xml.open(DAV_URN, "lockroot").await?; + let href = Href::qread(xml).await?; + xml.close().await?; + Ok(LockRoot(href)) + } +} + +impl<E: Extension> QRead<ResourceType<E>> for ResourceType<E> { + async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> { + if xml.maybe_open(DAV_URN, "collection").await?.is_some() { + xml.close().await?; + return Ok(ResourceType::Collection) + } + + E::ResourceType::qread(xml).await.map(ResourceType::Extension) + } +} + +impl QRead<LockEntry> for LockEntry { + async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> { + xml.open(DAV_URN, "lockentry").await?; + let (mut maybe_scope, mut maybe_type) = (None, None); + + loop { + let mut dirty = false; + xml.maybe_read::<LockScope>(&mut maybe_scope, &mut dirty).await?; + xml.maybe_read::<LockType>(&mut maybe_type, &mut dirty).await?; + if !dirty { + match xml.peek() { + Event::End(_) => break, + _ => xml.skip().await?, + }; + } + } + + xml.close().await?; + match (maybe_scope, maybe_type) { + (Some(lockscope), Some(locktype)) => Ok(LockEntry { lockscope, locktype }), + _ => Err(ParsingError::MissingChild), + } + } +} + +impl QRead<LockScope> for LockScope { + async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> { + xml.open(DAV_URN, "lockscope").await?; + + let lockscope = loop { + if xml.maybe_open(DAV_URN, "exclusive").await?.is_some() { + xml.close().await?; + break LockScope::Exclusive + } else if xml.maybe_open(DAV_URN, "shared").await?.is_some() { + xml.close().await?; + break LockScope::Shared + } + + xml.skip().await?; + }; + + xml.close().await?; + Ok(lockscope) + } +} + +impl QRead<LockType> for LockType { + async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> { + xml.open(DAV_URN, "locktype").await?; + + let locktype = loop { + if xml.maybe_open(DAV_URN, "write").await?.is_some() { + xml.close().await?; + break LockType::Write + } + + xml.skip().await?; + }; + + xml.close().await?; + Ok(locktype) + } +} + +impl QRead<Href> for Href { + async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> { + xml.open(DAV_URN, "href").await?; + let mut url = xml.tag_string().await?; + xml.close().await?; + Ok(Href(url)) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use chrono::{FixedOffset, DateTime, TimeZone, Utc}; + use crate::realization::Core; + + #[tokio::test] + async fn basic_propfind_propname() { + let src = r#"<?xml version="1.0" encoding="utf-8" ?> +<rando/> +<garbage><old/></garbage> +<D:propfind xmlns:D="DAV:"> + <D:propname/> +</D:propfind> +"#; + + let mut rdr = Reader::new(NsReader::from_reader(src.as_bytes())).await.unwrap(); + let got = rdr.find::<PropFind::<Core>>().await.unwrap(); + + assert_eq!(got, PropFind::<Core>::PropName); + } + + #[tokio::test] + async fn basic_propfind_prop() { + let src = r#"<?xml version="1.0" encoding="utf-8" ?> +<rando/> +<garbage><old/></garbage> +<D:propfind xmlns:D="DAV:"> + <D:prop> + <D:displayname/> + <D:getcontentlength/> + <D:getcontenttype/> + <D:getetag/> + <D:getlastmodified/> + <D:resourcetype/> + <D:supportedlock/> + </D:prop> +</D:propfind> +"#; + + let mut rdr = Reader::new(NsReader::from_reader(src.as_bytes())).await.unwrap(); + let got = rdr.find::<PropFind::<Core>>().await.unwrap(); + + assert_eq!(got, PropFind::Prop(PropName(vec![ + PropertyRequest::DisplayName, + PropertyRequest::GetContentLength, + PropertyRequest::GetContentType, + PropertyRequest::GetEtag, + PropertyRequest::GetLastModified, + PropertyRequest::ResourceType, + PropertyRequest::SupportedLock, + ]))); + } + + #[tokio::test] + async fn rfc_lock_error() { + let src = r#"<?xml version="1.0" encoding="utf-8" ?> + <D:error xmlns:D="DAV:"> + <D:lock-token-submitted> + <D:href>/locked/</D:href> + </D:lock-token-submitted> + </D:error>"#; + + let mut rdr = Reader::new(NsReader::from_reader(src.as_bytes())).await.unwrap(); + let got = rdr.find::<Error::<Core>>().await.unwrap(); + + assert_eq!(got, Error(vec![ + Violation::LockTokenSubmitted(vec![ + Href("/locked/".into()) + ]) + ])); + } + + + #[tokio::test] + async fn rfc_propertyupdate() { + let src = r#"<?xml version="1.0" encoding="utf-8" ?> + <D:propertyupdate xmlns:D="DAV:" + xmlns:Z="http://ns.example.com/standards/z39.50/"> + <D:set> + <D:prop> + <Z:Authors> + <Z:Author>Jim Whitehead</Z:Author> + <Z:Author>Roy Fielding</Z:Author> + </Z:Authors> + </D:prop> + </D:set> + <D:remove> + <D:prop><Z:Copyright-Owner/></D:prop> + </D:remove> + </D:propertyupdate>"#; + + let mut rdr = Reader::new(NsReader::from_reader(src.as_bytes())).await.unwrap(); + let got = rdr.find::<PropertyUpdate::<Core>>().await.unwrap(); + + assert_eq!(got, PropertyUpdate(vec![ + PropertyUpdateItem::Set(Set(PropValue(vec![]))), + PropertyUpdateItem::Remove(Remove(PropName(vec![]))), + ])); + } + + #[tokio::test] + async fn rfc_lockinfo() { + let src = r#" +<?xml version="1.0" encoding="utf-8" ?> +<D:lockinfo xmlns:D='DAV:'> + <D:lockscope><D:exclusive/></D:lockscope> + <D:locktype><D:write/></D:locktype> + <D:owner> + <D:href>http://example.org/~ejw/contact.html</D:href> + </D:owner> +</D:lockinfo> +"#; + + let mut rdr = Reader::new(NsReader::from_reader(src.as_bytes())).await.unwrap(); + let got = rdr.find::<LockInfo>().await.unwrap(); + + assert_eq!(got, LockInfo { + lockscope: LockScope::Exclusive, + locktype: LockType::Write, + owner: Some(Owner::Href(Href("http://example.org/~ejw/contact.html".into()))), + }); + } + + #[tokio::test] + async fn rfc_multistatus_name() { + let src = r#" +<?xml version="1.0" encoding="utf-8" ?> + <multistatus xmlns="DAV:"> + <response> + <href>http://www.example.com/container/</href> + <propstat> + <prop xmlns:R="http://ns.example.com/boxschema/"> + <R:bigbox/> + <R:author/> + <creationdate/> + <displayname/> + <resourcetype/> + <supportedlock/> + </prop> + <status>HTTP/1.1 200 OK</status> + </propstat> + </response> + <response> + <href>http://www.example.com/container/front.html</href> + <propstat> + <prop xmlns:R="http://ns.example.com/boxschema/"> + <R:bigbox/> + <creationdate/> + <displayname/> + <getcontentlength/> + <getcontenttype/> + <getetag/> + <getlastmodified/> + <resourcetype/> + <supportedlock/> + </prop> + <status>HTTP/1.1 200 OK</status> + </propstat> + </response> + </multistatus> +"#; + + let mut rdr = Reader::new(NsReader::from_reader(src.as_bytes())).await.unwrap(); + let got = rdr.find::<Multistatus::<Core, PropName<Core>>>().await.unwrap(); + + assert_eq!(got, Multistatus { + responses: vec![ + Response { + status_or_propstat: StatusOrPropstat::PropStat( + Href("http://www.example.com/container/".into()), + vec![PropStat { + prop: PropName(vec![ + PropertyRequest::CreationDate, + PropertyRequest::DisplayName, + PropertyRequest::ResourceType, + PropertyRequest::SupportedLock, + ]), + status: Status(http::status::StatusCode::OK), + error: None, + responsedescription: None, + }], + ), + error: None, + responsedescription: None, + location: None, + }, + Response { + status_or_propstat: StatusOrPropstat::PropStat( + Href("http://www.example.com/container/front.html".into()), + vec![PropStat { + prop: PropName(vec![ + PropertyRequest::CreationDate, + PropertyRequest::DisplayName, + PropertyRequest::GetContentLength, + PropertyRequest::GetContentType, + PropertyRequest::GetEtag, + PropertyRequest::GetLastModified, + PropertyRequest::ResourceType, + PropertyRequest::SupportedLock, + ]), + status: Status(http::status::StatusCode::OK), + error: None, + responsedescription: None, + }], + ), + error: None, + responsedescription: None, + location: None, + }, + ], + responsedescription: None, + }); + } + + + #[tokio::test] + async fn rfc_multistatus_value() { + let src = r#" + <?xml version="1.0" encoding="utf-8" ?> + <D:multistatus xmlns:D="DAV:"> + <D:response> + <D:href>/container/</D:href> + <D:propstat> + <D:prop xmlns:R="http://ns.example.com/boxschema/"> + <R:bigbox><R:BoxType>Box type A</R:BoxType></R:bigbox> + <R:author><R:Name>Hadrian</R:Name></R:author> + <D:creationdate>1997-12-01T17:42:21-08:00</D:creationdate> + <D:displayname>Example collection</D:displayname> + <D:resourcetype><D:collection/></D:resourcetype> + <D:supportedlock> + <D:lockentry> + <D:lockscope><D:exclusive/></D:lockscope> + <D:locktype><D:write/></D:locktype> + </D:lockentry> + <D:lockentry> + <D:lockscope><D:shared/></D:lockscope> + <D:locktype><D:write/></D:locktype> + </D:lockentry> + </D:supportedlock> + </D:prop> + <D:status>HTTP/1.1 200 OK</D:status> + </D:propstat> + </D:response> + <D:response> + <D:href>/container/front.html</D:href> + <D:propstat> + <D:prop xmlns:R="http://ns.example.com/boxschema/"> + <R:bigbox><R:BoxType>Box type B</R:BoxType> + </R:bigbox> + <D:creationdate>1997-12-01T18:27:21-08:00</D:creationdate> + <D:displayname>Example HTML resource</D:displayname> + <D:getcontentlength>4525</D:getcontentlength> + <D:getcontenttype>text/html</D:getcontenttype> + <D:getetag>"zzyzx"</D:getetag> + <D:getlastmodified + >Mon, 12 Jan 1998 09:25:56 GMT</D:getlastmodified> + <D:resourcetype/> + <D:supportedlock> + <D:lockentry> + <D:lockscope><D:exclusive/></D:lockscope> + <D:locktype><D:write/></D:locktype> + </D:lockentry> + <D:lockentry> + <D:lockscope><D:shared/></D:lockscope> + <D:locktype><D:write/></D:locktype> + </D:lockentry> + </D:supportedlock> + </D:prop> + <D:status>HTTP/1.1 200 OK</D:status> + </D:propstat> + </D:response> + </D:multistatus>"#; + + let mut rdr = Reader::new(NsReader::from_reader(src.as_bytes())).await.unwrap(); + let got = rdr.find::<Multistatus::<Core, PropValue<Core>>>().await.unwrap(); + + assert_eq!(got, Multistatus { + responses: vec![ + Response { + status_or_propstat: StatusOrPropstat::PropStat( + Href("/container/".into()), + vec![PropStat { + prop: PropValue(vec![ + Property::CreationDate(FixedOffset::west_opt(8 * 3600).unwrap().with_ymd_and_hms(1997, 12, 01, 17, 42, 21).unwrap()), + Property::DisplayName("Example collection".into()), + Property::ResourceType(vec![ResourceType::Collection]), + Property::SupportedLock(vec![ + LockEntry { + lockscope: LockScope::Exclusive, + locktype: LockType::Write, + }, + LockEntry { + lockscope: LockScope::Shared, + locktype: LockType::Write, + }, + ]), + ]), + status: Status(http::status::StatusCode::OK), + error: None, + responsedescription: None, + }], + ), + error: None, + responsedescription: None, + location: None, + + }, + Response { + status_or_propstat: StatusOrPropstat::PropStat( + Href("/container/front.html".into()), + vec![PropStat { + prop: PropValue(vec![ + Property::CreationDate(FixedOffset::west_opt(8 * 3600).unwrap().with_ymd_and_hms(1997, 12, 01, 18, 27, 21).unwrap()), + Property::DisplayName("Example HTML resource".into()), + Property::GetContentLength(4525), + Property::GetContentType("text/html".into()), + Property::GetEtag(r#""zzyzx""#.into()), + Property::GetLastModified(FixedOffset::west_opt(0).unwrap().with_ymd_and_hms(1998, 01, 12, 09, 25, 56).unwrap()), + //Property::ResourceType(vec![]), + Property::SupportedLock(vec![ + LockEntry { + lockscope: LockScope::Exclusive, + locktype: LockType::Write, + }, + LockEntry { + lockscope: LockScope::Shared, + locktype: LockType::Write, + }, + ]), + ]), + status: Status(http::status::StatusCode::OK), + error: None, + responsedescription: None, + }], + ), + error: None, + responsedescription: None, + location: None, + + }, + ], + responsedescription: None, + }); + } + +} diff --git a/aero-dav/src/encoder.rs b/aero-dav/src/encoder.rs new file mode 100644 index 0000000..fd2f9ca --- /dev/null +++ b/aero-dav/src/encoder.rs @@ -0,0 +1,1112 @@ +use quick_xml::Error as QError; +use quick_xml::events::{Event, BytesText}; +use super::types::*; +use super::xml::{Node, Writer,QWrite,IWrite}; + + +// --- XML ROOTS + +/// PROPFIND REQUEST +impl<E: Extension> QWrite for PropFind<E> { + async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> { + let start = xml.create_dav_element("propfind"); + let end = start.to_end(); + + xml.q.write_event_async(Event::Start(start.clone())).await?; + match self { + Self::PropName => { + let empty_propname = xml.create_dav_element("propname"); + xml.q.write_event_async(Event::Empty(empty_propname)).await? + }, + Self::AllProp(maybe_include) => { + let empty_allprop = xml.create_dav_element("allprop"); + xml.q.write_event_async(Event::Empty(empty_allprop)).await?; + if let Some(include) = maybe_include { + include.qwrite(xml).await?; + } + }, + Self::Prop(propname) => propname.qwrite(xml).await?, + } + xml.q.write_event_async(Event::End(end)).await + } +} + +/// PROPPATCH REQUEST +impl<E: Extension> QWrite for PropertyUpdate<E> { + async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> { + let start = xml.create_dav_element("propertyupdate"); + let end = start.to_end(); + + xml.q.write_event_async(Event::Start(start.clone())).await?; + for update in self.0.iter() { + update.qwrite(xml).await?; + } + xml.q.write_event_async(Event::End(end)).await + } +} + + +/// PROPFIND RESPONSE, PROPPATCH RESPONSE, COPY RESPONSE, MOVE RESPONSE +/// DELETE RESPONSE, +impl<E: Extension, N: Node<N>> QWrite for Multistatus<E,N> { + async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> { + let start = xml.create_dav_element("multistatus"); + let end = start.to_end(); + + xml.q.write_event_async(Event::Start(start.clone())).await?; + for response in self.responses.iter() { + response.qwrite(xml).await?; + } + if let Some(description) = &self.responsedescription { + description.qwrite(xml).await?; + } + + xml.q.write_event_async(Event::End(end)).await?; + Ok(()) + } +} + +/// LOCK REQUEST +impl QWrite for LockInfo { + async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> { + let start = xml.create_dav_element("lockinfo"); + let end = start.to_end(); + + xml.q.write_event_async(Event::Start(start.clone())).await?; + self.lockscope.qwrite(xml).await?; + self.locktype.qwrite(xml).await?; + if let Some(owner) = &self.owner { + owner.qwrite(xml).await?; + } + xml.q.write_event_async(Event::End(end)).await + } +} + +/// SOME LOCK RESPONSES +impl<E: Extension> QWrite for PropValue<E> { + async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> { + let start = xml.create_dav_element("prop"); + let end = start.to_end(); + + xml.q.write_event_async(Event::Start(start.clone())).await?; + for propval in &self.0 { + propval.qwrite(xml).await?; + } + xml.q.write_event_async(Event::End(end)).await + } +} + +/// Error response +impl<E: Extension> QWrite for Error<E> { + async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> { + let start = xml.create_dav_element("error"); + let end = start.to_end(); + + xml.q.write_event_async(Event::Start(start.clone())).await?; + for violation in &self.0 { + violation.qwrite(xml).await?; + } + xml.q.write_event_async(Event::End(end)).await + } +} + +// --- XML inner elements +impl<E: Extension> QWrite for PropertyUpdateItem<E> { + async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> { + match self { + Self::Set(set) => set.qwrite(xml).await, + Self::Remove(rm) => rm.qwrite(xml).await, + } + } +} + +impl<E: Extension> QWrite for Set<E> { + async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> { + let start = xml.create_dav_element("set"); + let end = start.to_end(); + xml.q.write_event_async(Event::Start(start.clone())).await?; + self.0.qwrite(xml).await?; + xml.q.write_event_async(Event::End(end)).await + } +} + +impl<E: Extension> QWrite for Remove<E> { + async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> { + let start = xml.create_dav_element("remove"); + let end = start.to_end(); + xml.q.write_event_async(Event::Start(start.clone())).await?; + self.0.qwrite(xml).await?; + xml.q.write_event_async(Event::End(end)).await + } +} + + +impl<E: Extension> QWrite for PropName<E> { + async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> { + let start = xml.create_dav_element("prop"); + let end = start.to_end(); + + xml.q.write_event_async(Event::Start(start.clone())).await?; + for propname in &self.0 { + propname.qwrite(xml).await?; + } + xml.q.write_event_async(Event::End(end)).await + } +} + + +impl QWrite for Href { + async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> { + let start = xml.create_dav_element("href"); + let end = start.to_end(); + + xml.q.write_event_async(Event::Start(start.clone())).await?; + xml.q.write_event_async(Event::Text(BytesText::new(&self.0))).await?; + xml.q.write_event_async(Event::End(end)).await + } +} + +impl<E: Extension, N: Node<N>> QWrite for Response<E,N> { + async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> { + let start = xml.create_dav_element("response"); + let end = start.to_end(); + + xml.q.write_event_async(Event::Start(start.clone())).await?; + self.status_or_propstat.qwrite(xml).await?; + if let Some(error) = &self.error { + error.qwrite(xml).await?; + } + if let Some(responsedescription) = &self.responsedescription { + responsedescription.qwrite(xml).await?; + } + if let Some(location) = &self.location { + location.qwrite(xml).await?; + } + xml.q.write_event_async(Event::End(end)).await + } +} + +impl<E: Extension, N: Node<N>> QWrite for StatusOrPropstat<E,N> { + async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> { + match self { + Self::Status(many_href, status) => { + for href in many_href.iter() { + href.qwrite(xml).await?; + } + status.qwrite(xml).await + }, + Self::PropStat(href, propstat_list) => { + href.qwrite(xml).await?; + for propstat in propstat_list.iter() { + propstat.qwrite(xml).await?; + } + Ok(()) + } + } + } +} + +impl QWrite for Status { + async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> { + let start = xml.create_dav_element("status"); + let end = start.to_end(); + + xml.q.write_event_async(Event::Start(start.clone())).await?; + + let txt = format!("HTTP/1.1 {} {}", self.0.as_str(), self.0.canonical_reason().unwrap_or("No reason")); + xml.q.write_event_async(Event::Text(BytesText::new(&txt))).await?; + + xml.q.write_event_async(Event::End(end)).await?; + + Ok(()) + } +} + +impl QWrite for ResponseDescription { + async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> { + let start = xml.create_dav_element("responsedescription"); + let end = start.to_end(); + + xml.q.write_event_async(Event::Start(start.clone())).await?; + xml.q.write_event_async(Event::Text(BytesText::new(&self.0))).await?; + xml.q.write_event_async(Event::End(end)).await + } +} + +impl QWrite for Location { + async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> { + let start = xml.create_dav_element("location"); + let end = start.to_end(); + + xml.q.write_event_async(Event::Start(start.clone())).await?; + self.0.qwrite(xml).await?; + xml.q.write_event_async(Event::End(end)).await + } +} + +impl<E: Extension, N: Node<N>> QWrite for PropStat<E,N> { + async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> { + let start = xml.create_dav_element("propstat"); + let end = start.to_end(); + + xml.q.write_event_async(Event::Start(start.clone())).await?; + self.prop.qwrite(xml).await?; + self.status.qwrite(xml).await?; + if let Some(error) = &self.error { + error.qwrite(xml).await?; + } + if let Some(description) = &self.responsedescription { + description.qwrite(xml).await?; + } + xml.q.write_event_async(Event::End(end)).await?; + + Ok(()) + } +} + +impl<E: Extension> QWrite for Property<E> { + async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> { + use Property::*; + match self { + CreationDate(date) => { + // <D:creationdate>1997-12-01T17:42:21-08:00</D:creationdate> + let start = xml.create_dav_element("creationdate"); + let end = start.to_end(); + + xml.q.write_event_async(Event::Start(start.clone())).await?; + xml.q.write_event_async(Event::Text(BytesText::new(&date.to_rfc3339()))).await?; + xml.q.write_event_async(Event::End(end)).await?; + }, + DisplayName(name) => { + // <D:displayname>Example collection</D:displayname> + let start = xml.create_dav_element("displayname"); + let end = start.to_end(); + + xml.q.write_event_async(Event::Start(start.clone())).await?; + xml.q.write_event_async(Event::Text(BytesText::new(name))).await?; + xml.q.write_event_async(Event::End(end)).await?; + }, + GetContentLanguage(lang) => { + let start = xml.create_dav_element("getcontentlanguage"); + let end = start.to_end(); + + xml.q.write_event_async(Event::Start(start.clone())).await?; + xml.q.write_event_async(Event::Text(BytesText::new(lang))).await?; + xml.q.write_event_async(Event::End(end)).await?; + }, + GetContentLength(len) => { + // <D:getcontentlength>4525</D:getcontentlength> + let start = xml.create_dav_element("getcontentlength"); + let end = start.to_end(); + + xml.q.write_event_async(Event::Start(start.clone())).await?; + xml.q.write_event_async(Event::Text(BytesText::new(&len.to_string()))).await?; + xml.q.write_event_async(Event::End(end)).await?; + }, + GetContentType(ct) => { + // <D:getcontenttype>text/html</D:getcontenttype> + let start = xml.create_dav_element("getcontenttype"); + let end = start.to_end(); + + xml.q.write_event_async(Event::Start(start.clone())).await?; + xml.q.write_event_async(Event::Text(BytesText::new(&ct))).await?; + xml.q.write_event_async(Event::End(end)).await?; + }, + GetEtag(et) => { + // <D:getetag>"zzyzx"</D:getetag> + let start = xml.create_dav_element("getetag"); + let end = start.to_end(); + + xml.q.write_event_async(Event::Start(start.clone())).await?; + xml.q.write_event_async(Event::Text(BytesText::new(et))).await?; + xml.q.write_event_async(Event::End(end)).await?; + }, + GetLastModified(date) => { + // <D:getlastmodified>Mon, 12 Jan 1998 09:25:56 GMT</D:getlastmodified> + let start = xml.create_dav_element("getlastmodified"); + let end = start.to_end(); + + xml.q.write_event_async(Event::Start(start.clone())).await?; + xml.q.write_event_async(Event::Text(BytesText::new(&date.to_rfc2822()))).await?; + xml.q.write_event_async(Event::End(end)).await?; + }, + LockDiscovery(many_locks) => { + // <D:lockdiscovery><D:activelock> ... </D:activelock></D:lockdiscovery> + let start = xml.create_dav_element("lockdiscovery"); + let end = start.to_end(); + + xml.q.write_event_async(Event::Start(start.clone())).await?; + for lock in many_locks.iter() { + lock.qwrite(xml).await?; + } + xml.q.write_event_async(Event::End(end)).await?; + }, + ResourceType(many_types) => { + // <D:resourcetype><D:collection/></D:resourcetype> + + // <D:resourcetype/> + + // <x:resourcetype xmlns:x="DAV:"> + // <x:collection/> + // <f:search-results xmlns:f="http://www.example.com/ns"/> + // </x:resourcetype> + + let start = xml.create_dav_element("resourcetype"); + if many_types.is_empty() { + xml.q.write_event_async(Event::Empty(start)).await?; + } else { + let end = start.to_end(); + xml.q.write_event_async(Event::Start(start.clone())).await?; + for restype in many_types.iter() { + restype.qwrite(xml).await?; + } + xml.q.write_event_async(Event::End(end)).await?; + } + }, + SupportedLock(many_entries) => { + // <D:supportedlock/> + + // <D:supportedlock> <D:lockentry> ... </D:lockentry> </D:supportedlock> + + let start = xml.create_dav_element("supportedlock"); + if many_entries.is_empty() { + xml.q.write_event_async(Event::Empty(start)).await?; + } else { + let end = start.to_end(); + xml.q.write_event_async(Event::Start(start.clone())).await?; + for entry in many_entries.iter() { + entry.qwrite(xml).await?; + } + xml.q.write_event_async(Event::End(end)).await?; + } + }, + Extension(inner) => inner.qwrite(xml).await?, + }; + Ok(()) + } +} + +impl<E: Extension> QWrite for ResourceType<E> { + async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> { + match self { + Self::Collection => { + let empty_collection = xml.create_dav_element("collection"); + xml.q.write_event_async(Event::Empty(empty_collection)).await + }, + Self::Extension(inner) => inner.qwrite(xml).await, + } + } +} + +impl<E: Extension> QWrite for Include<E> { + async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> { + let start = xml.create_dav_element("include"); + let end = start.to_end(); + + xml.q.write_event_async(Event::Start(start.clone())).await?; + for prop in self.0.iter() { + prop.qwrite(xml).await?; + } + xml.q.write_event_async(Event::End(end)).await + } +} + +impl<E: Extension> QWrite for PropertyRequest<E> { + async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> { + use PropertyRequest::*; + let mut atom = async |c| { + let empty_tag = xml.create_dav_element(c); + xml.q.write_event_async(Event::Empty(empty_tag)).await + }; + + match self { + CreationDate => atom("creationdate").await, + DisplayName => atom("displayname").await, + GetContentLanguage => atom("getcontentlanguage").await, + GetContentLength => atom("getcontentlength").await, + GetContentType => atom("getcontenttype").await, + GetEtag => atom("getetag").await, + GetLastModified => atom("getlastmodified").await, + LockDiscovery => atom("lockdiscovery").await, + ResourceType => atom("resourcetype").await, + SupportedLock => atom("supportedlock").await, + Extension(inner) => inner.qwrite(xml).await, + } + } +} + +impl QWrite for ActiveLock { + async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> { + // <D:activelock> + // <D:locktype><D:write/></D:locktype> + // <D:lockscope><D:exclusive/></D:lockscope> + // <D:depth>infinity</D:depth> + // <D:owner> + // <D:href>http://example.org/~ejw/contact.html</D:href> + // </D:owner> + // <D:timeout>Second-604800</D:timeout> + // <D:locktoken> + // <D:href>urn:uuid:e71d4fae-5dec-22d6-fea5-00a0c91e6be4</D:href> + // </D:locktoken> + // <D:lockroot> + // <D:href>http://example.com/workspace/webdav/proposal.doc</D:href> + // </D:lockroot> + // </D:activelock> + let start = xml.create_dav_element("activelock"); + let end = start.to_end(); + + xml.q.write_event_async(Event::Start(start.clone())).await?; + self.locktype.qwrite(xml).await?; + self.lockscope.qwrite(xml).await?; + self.depth.qwrite(xml).await?; + if let Some(owner) = &self.owner { + owner.qwrite(xml).await?; + } + if let Some(timeout) = &self.timeout { + timeout.qwrite(xml).await?; + } + if let Some(locktoken) = &self.locktoken { + locktoken.qwrite(xml).await?; + } + self.lockroot.qwrite(xml).await?; + xml.q.write_event_async(Event::End(end)).await + } +} + +impl QWrite for LockType { + async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> { + let start = xml.create_dav_element("locktype"); + let end = start.to_end(); + + xml.q.write_event_async(Event::Start(start.clone())).await?; + match self { + Self::Write => { + let empty_write = xml.create_dav_element("write"); + xml.q.write_event_async(Event::Empty(empty_write)).await? + }, + }; + xml.q.write_event_async(Event::End(end)).await + } +} + +impl QWrite for LockScope { + async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> { + let start = xml.create_dav_element("lockscope"); + let end = start.to_end(); + + xml.q.write_event_async(Event::Start(start.clone())).await?; + match self { + Self::Exclusive => { + let empty_tag = xml.create_dav_element("exclusive"); + xml.q.write_event_async(Event::Empty(empty_tag)).await? + }, + Self::Shared => { + let empty_tag = xml.create_dav_element("shared"); + xml.q.write_event_async(Event::Empty(empty_tag)).await? + }, + }; + xml.q.write_event_async(Event::End(end)).await + } +} + +impl QWrite for Owner { + async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> { + let start = xml.create_dav_element("owner"); + let end = start.to_end(); + + xml.q.write_event_async(Event::Start(start.clone())).await?; + match self { + Self::Txt(txt) => xml.q.write_event_async(Event::Text(BytesText::new(&txt))).await?, + Self::Href(href) => href.qwrite(xml).await?, + Self::Unknown => (), + } + xml.q.write_event_async(Event::End(end)).await + } +} + +impl QWrite for Depth { + async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> { + let start = xml.create_dav_element("depth"); + let end = start.to_end(); + + xml.q.write_event_async(Event::Start(start.clone())).await?; + match self { + Self::Zero => xml.q.write_event_async(Event::Text(BytesText::new("0"))).await?, + Self::One => xml.q.write_event_async(Event::Text(BytesText::new("1"))).await?, + Self::Infinity => xml.q.write_event_async(Event::Text(BytesText::new("infinity"))).await?, + }; + xml.q.write_event_async(Event::End(end)).await + } +} + +impl QWrite for Timeout { + async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> { + let start = xml.create_dav_element("timeout"); + let end = start.to_end(); + + xml.q.write_event_async(Event::Start(start.clone())).await?; + match self { + Self::Seconds(count) => { + let txt = format!("Second-{}", count); + xml.q.write_event_async(Event::Text(BytesText::new(&txt))).await? + }, + Self::Infinite => xml.q.write_event_async(Event::Text(BytesText::new("Infinite"))).await? + }; + xml.q.write_event_async(Event::End(end)).await + } +} + +impl QWrite for LockToken { + async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> { + let start = xml.create_dav_element("locktoken"); + let end = start.to_end(); + + xml.q.write_event_async(Event::Start(start.clone())).await?; + self.0.qwrite(xml).await?; + xml.q.write_event_async(Event::End(end)).await + } +} + +impl QWrite for LockRoot { + async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> { + let start = xml.create_dav_element("lockroot"); + let end = start.to_end(); + + xml.q.write_event_async(Event::Start(start.clone())).await?; + self.0.qwrite(xml).await?; + xml.q.write_event_async(Event::End(end)).await + } +} + +impl QWrite for LockEntry { + async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> { + let start = xml.create_dav_element("lockentry"); + let end = start.to_end(); + + xml.q.write_event_async(Event::Start(start.clone())).await?; + self.lockscope.qwrite(xml).await?; + self.locktype.qwrite(xml).await?; + xml.q.write_event_async(Event::End(end)).await + } +} + +impl<E: Extension> QWrite for Violation<E> { + async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> { + let mut atom = async |c| { + let empty_tag = xml.create_dav_element(c); + xml.q.write_event_async(Event::Empty(empty_tag)).await + }; + + match self { + Violation::LockTokenMatchesRequestUri => atom("lock-token-matches-request-uri").await, + Violation::LockTokenSubmitted(hrefs) if hrefs.is_empty() => atom("lock-token-submitted").await, + Violation::LockTokenSubmitted(hrefs) => { + let start = xml.create_dav_element("lock-token-submitted"); + let end = start.to_end(); + + xml.q.write_event_async(Event::Start(start.clone())).await?; + for href in hrefs { + href.qwrite(xml).await?; + } + xml.q.write_event_async(Event::End(end)).await + }, + Violation::NoConflictingLock(hrefs) if hrefs.is_empty() => atom("no-conflicting-lock").await, + Violation::NoConflictingLock(hrefs) => { + let start = xml.create_dav_element("no-conflicting-lock"); + let end = start.to_end(); + + xml.q.write_event_async(Event::Start(start.clone())).await?; + for href in hrefs { + href.qwrite(xml).await?; + } + xml.q.write_event_async(Event::End(end)).await + }, + Violation::NoExternalEntities => atom("no-external-entities").await, + Violation::PreservedLiveProperties => atom("preserved-live-properties").await, + Violation::PropfindFiniteDepth => atom("propfind-finite-depth").await, + Violation::CannotModifyProtectedProperty => atom("cannot-modify-protected-property").await, + Violation::Extension(inner) => inner.qwrite(xml).await, + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::realization::Core; + use tokio::io::AsyncWriteExt; + + /// To run only the unit tests and avoid the behavior ones: + /// cargo test --bin aerogramme + + async fn serialize(elem: &impl QWrite) -> String { + let mut buffer = Vec::new(); + let mut tokio_buffer = tokio::io::BufWriter::new(&mut buffer); + let q = quick_xml::writer::Writer::new_with_indent(&mut tokio_buffer, b' ', 4); + let ns_to_apply = vec![ ("xmlns:D".into(), "DAV:".into()) ]; + let mut writer = Writer { q, ns_to_apply }; + + elem.qwrite(&mut writer).await.expect("xml serialization"); + tokio_buffer.flush().await.expect("tokio buffer flush"); + let got = std::str::from_utf8(buffer.as_slice()).unwrap(); + + return got.into() + } + + #[tokio::test] + async fn basic_href() { + + let got = serialize( + &Href("/SOGo/dav/so/".into()) + ).await; + let expected = r#"<D:href xmlns:D="DAV:">/SOGo/dav/so/</D:href>"#; + + assert_eq!(&got, expected, "\n---GOT---\n{got}\n---EXP---\n{expected}\n"); + } + + #[tokio::test] + async fn basic_multistatus() { + let got = serialize( + &Multistatus::<Core, PropName<Core>> { + responses: vec![], + responsedescription: Some(ResponseDescription("Hello world".into())) + }, + ).await; + + let expected = r#"<D:multistatus xmlns:D="DAV:"> + <D:responsedescription>Hello world</D:responsedescription> +</D:multistatus>"#; + + assert_eq!(&got, expected, "\n---GOT---\n{got}\n---EXP---\n{expected}\n"); + } + + + #[tokio::test] + async fn rfc_error_delete_locked() { + let got = serialize( + &Error::<Core>(vec![ + Violation::LockTokenSubmitted(vec![ + Href("/locked/".into()) + ]) + ]), + ).await; + + let expected = r#"<D:error xmlns:D="DAV:"> + <D:lock-token-submitted> + <D:href>/locked/</D:href> + </D:lock-token-submitted> +</D:error>"#; + + assert_eq!(&got, expected, "\n---GOT---\n{got}\n---EXP---\n{expected}\n"); + } + + #[tokio::test] + async fn rfc_propname_req() { + let got = serialize( + &PropFind::<Core>::PropName, + ).await; + + let expected = r#"<D:propfind xmlns:D="DAV:"> + <D:propname/> +</D:propfind>"#; + + assert_eq!(&got, expected, "\n---GOT---\n{got}\n---EXP---\n{expected}\n"); + } + + #[tokio::test] + async fn rfc_propname_res() { + let got = serialize( + &Multistatus::<Core, PropName<Core>> { + responses: vec![ + Response { + status_or_propstat: StatusOrPropstat::PropStat( + Href("http://www.example.com/container/".into()), + vec![PropStat { + prop: PropName(vec![ + PropertyRequest::CreationDate, + PropertyRequest::DisplayName, + PropertyRequest::ResourceType, + PropertyRequest::SupportedLock, + ]), + status: Status(http::status::StatusCode::OK), + error: None, + responsedescription: None, + }] + ), + error: None, + responsedescription: None, + location: None, + }, + Response { + status_or_propstat: StatusOrPropstat::PropStat( + Href("http://www.example.com/container/front.html".into()), + vec![PropStat { + prop: PropName(vec![ + PropertyRequest::CreationDate, + PropertyRequest::DisplayName, + PropertyRequest::GetContentLength, + PropertyRequest::GetContentType, + PropertyRequest::GetEtag, + PropertyRequest::GetLastModified, + PropertyRequest::ResourceType, + PropertyRequest::SupportedLock, + ]), + status: Status(http::status::StatusCode::OK), + error: None, + responsedescription: None, + } + ]), + error: None, + responsedescription: None, + location: None, + }, + ], + responsedescription: None, + }, + ).await; + + let expected = r#"<D:multistatus xmlns:D="DAV:"> + <D:response> + <D:href>http://www.example.com/container/</D:href> + <D:propstat> + <D:prop> + <D:creationdate/> + <D:displayname/> + <D:resourcetype/> + <D:supportedlock/> + </D:prop> + <D:status>HTTP/1.1 200 OK</D:status> + </D:propstat> + </D:response> + <D:response> + <D:href>http://www.example.com/container/front.html</D:href> + <D:propstat> + <D:prop> + <D:creationdate/> + <D:displayname/> + <D:getcontentlength/> + <D:getcontenttype/> + <D:getetag/> + <D:getlastmodified/> + <D:resourcetype/> + <D:supportedlock/> + </D:prop> + <D:status>HTTP/1.1 200 OK</D:status> + </D:propstat> + </D:response> +</D:multistatus>"#; + + + assert_eq!(&got, expected, "\n---GOT---\n{got}\n---EXP---\n{expected}\n"); + } + + #[tokio::test] + async fn rfc_allprop_req() { + let got = serialize( + &PropFind::<Core>::AllProp(None), + ).await; + + let expected = r#"<D:propfind xmlns:D="DAV:"> + <D:allprop/> +</D:propfind>"#; + + assert_eq!(&got, expected, "\n---GOT---\n{got}\n---EXP---\n{expected}\n"); + } + + #[tokio::test] + async fn rfc_allprop_res() { + use chrono::{DateTime,FixedOffset,TimeZone}; + let got = serialize( + &Multistatus::<Core, PropValue<Core>> { + responses: vec![ + Response { + status_or_propstat: StatusOrPropstat::PropStat( + Href("/container/".into()), + vec![PropStat { + prop: PropValue(vec![ + Property::CreationDate(FixedOffset::west_opt(8 * 3600) + .unwrap() + .with_ymd_and_hms(1997, 12, 1, 17, 42, 21) + .unwrap()), + Property::DisplayName("Example collection".into()), + Property::ResourceType(vec![ResourceType::Collection]), + Property::SupportedLock(vec![ + LockEntry { + lockscope: LockScope::Exclusive, + locktype: LockType::Write, + }, + LockEntry { + lockscope: LockScope::Shared, + locktype: LockType::Write, + }, + ]), + ]), + status: Status(http::status::StatusCode::OK), + error: None, + responsedescription: None, + }] + ), + error: None, + responsedescription: None, + location: None, + }, + Response { + status_or_propstat: StatusOrPropstat::PropStat( + Href("/container/front.html".into()), + vec![PropStat { + prop: PropValue(vec![ + Property::CreationDate(FixedOffset::west_opt(8 * 3600) + .unwrap() + .with_ymd_and_hms(1997, 12, 1, 18, 27, 21) + .unwrap()), + Property::DisplayName("Example HTML resource".into()), + Property::GetContentLength(4525), + Property::GetContentType("text/html".into()), + Property::GetEtag(r#""zzyzx""#.into()), + Property::GetLastModified(FixedOffset::east_opt(0) + .unwrap() + .with_ymd_and_hms(1998, 1, 12, 9, 25, 56) + .unwrap()), + Property::ResourceType(vec![]), + Property::SupportedLock(vec![ + LockEntry { + lockscope: LockScope::Exclusive, + locktype: LockType::Write, + }, + LockEntry { + lockscope: LockScope::Shared, + locktype: LockType::Write, + }, + ]), + ]), + status: Status(http::status::StatusCode::OK), + error: None, + responsedescription: None, + }] + ), + error: None, + responsedescription: None, + location: None, + }, + ], + responsedescription: None, + } + ).await; + + let expected = r#"<D:multistatus xmlns:D="DAV:"> + <D:response> + <D:href>/container/</D:href> + <D:propstat> + <D:prop> + <D:creationdate>1997-12-01T17:42:21-08:00</D:creationdate> + <D:displayname>Example collection</D:displayname> + <D:resourcetype> + <D:collection/> + </D:resourcetype> + <D:supportedlock> + <D:lockentry> + <D:lockscope> + <D:exclusive/> + </D:lockscope> + <D:locktype> + <D:write/> + </D:locktype> + </D:lockentry> + <D:lockentry> + <D:lockscope> + <D:shared/> + </D:lockscope> + <D:locktype> + <D:write/> + </D:locktype> + </D:lockentry> + </D:supportedlock> + </D:prop> + <D:status>HTTP/1.1 200 OK</D:status> + </D:propstat> + </D:response> + <D:response> + <D:href>/container/front.html</D:href> + <D:propstat> + <D:prop> + <D:creationdate>1997-12-01T18:27:21-08:00</D:creationdate> + <D:displayname>Example HTML resource</D:displayname> + <D:getcontentlength>4525</D:getcontentlength> + <D:getcontenttype>text/html</D:getcontenttype> + <D:getetag>"zzyzx"</D:getetag> + <D:getlastmodified>Mon, 12 Jan 1998 09:25:56 +0000</D:getlastmodified> + <D:resourcetype/> + <D:supportedlock> + <D:lockentry> + <D:lockscope> + <D:exclusive/> + </D:lockscope> + <D:locktype> + <D:write/> + </D:locktype> + </D:lockentry> + <D:lockentry> + <D:lockscope> + <D:shared/> + </D:lockscope> + <D:locktype> + <D:write/> + </D:locktype> + </D:lockentry> + </D:supportedlock> + </D:prop> + <D:status>HTTP/1.1 200 OK</D:status> + </D:propstat> + </D:response> +</D:multistatus>"#; + + assert_eq!(&got, expected, "\n---GOT---\n{got}\n---EXP---\n{expected}\n"); + } + + #[tokio::test] + async fn rfc_allprop_include() { + let got = serialize( + &PropFind::<Core>::AllProp(Some(Include(vec![ + PropertyRequest::DisplayName, + PropertyRequest::ResourceType, + ]))), + ).await; + + let expected = r#"<D:propfind xmlns:D="DAV:"> + <D:allprop/> + <D:include> + <D:displayname/> + <D:resourcetype/> + </D:include> +</D:propfind>"#; + + assert_eq!(&got, expected, "\n---GOT---\n{got}\n---EXP---\n{expected}\n"); + } + + #[tokio::test] + async fn rfc_propertyupdate() { + let got = serialize( + &PropertyUpdate::<Core>(vec![ + PropertyUpdateItem::Set(Set(PropValue(vec![ + Property::GetContentLanguage("fr-FR".into()), + ]))), + PropertyUpdateItem::Remove(Remove(PropName(vec![ + PropertyRequest::DisplayName, + ]))), + ]), + ).await; + + let expected = r#"<D:propertyupdate xmlns:D="DAV:"> + <D:set> + <D:prop> + <D:getcontentlanguage>fr-FR</D:getcontentlanguage> + </D:prop> + </D:set> + <D:remove> + <D:prop> + <D:displayname/> + </D:prop> + </D:remove> +</D:propertyupdate>"#; + + assert_eq!(&got, expected, "\n---GOT---\n{got}\n---EXP---\n{expected}\n"); + } + + #[tokio::test] + async fn rfc_delete_locked2() { + let got = serialize( + &Multistatus::<Core, PropValue<Core>> { + responses: vec![Response { + status_or_propstat: StatusOrPropstat::Status( + vec![Href("http://www.example.com/container/resource3".into())], + Status(http::status::StatusCode::from_u16(423).unwrap()) + ), + error: Some(Error(vec![Violation::LockTokenSubmitted(vec![])])), + responsedescription: None, + location: None, + }], + responsedescription: None, + }, + ).await; + + let expected = r#"<D:multistatus xmlns:D="DAV:"> + <D:response> + <D:href>http://www.example.com/container/resource3</D:href> + <D:status>HTTP/1.1 423 Locked</D:status> + <D:error> + <D:lock-token-submitted/> + </D:error> + </D:response> +</D:multistatus>"#; + + assert_eq!(&got, expected, "\n---GOT---\n{got}\n---EXP---\n{expected}\n"); + } + + #[tokio::test] + async fn rfc_simple_lock_request() { + let got = serialize( + &LockInfo { + lockscope: LockScope::Exclusive, + locktype: LockType::Write, + owner: Some(Owner::Href(Href("http://example.org/~ejw/contact.html".into()))), + }, + ).await; + + let expected = r#"<D:lockinfo xmlns:D="DAV:"> + <D:lockscope> + <D:exclusive/> + </D:lockscope> + <D:locktype> + <D:write/> + </D:locktype> + <D:owner> + <D:href>http://example.org/~ejw/contact.html</D:href> + </D:owner> +</D:lockinfo>"#; + + assert_eq!(&got, expected, "\n---GOT---\n{got}\n---EXP---\n{expected}\n"); + } + + #[tokio::test] + async fn rfc_simple_lock_response() { + let got = serialize( + &PropValue::<Core>(vec![ + Property::LockDiscovery(vec![ActiveLock { + lockscope: LockScope::Exclusive, + locktype: LockType::Write, + depth: Depth::Infinity, + owner: Some(Owner::Href(Href("http://example.org/~ejw/contact.html".into()))), + timeout: Some(Timeout::Seconds(604800)), + locktoken: Some(LockToken(Href("urn:uuid:e71d4fae-5dec-22d6-fea5-00a0c91e6be4".into()))), + lockroot: LockRoot(Href("http://example.com/workspace/webdav/proposal.doc".into())), + }]), + ]), + ).await; + + let expected = r#"<D:prop xmlns:D="DAV:"> + <D:lockdiscovery> + <D:activelock> + <D:locktype> + <D:write/> + </D:locktype> + <D:lockscope> + <D:exclusive/> + </D:lockscope> + <D:depth>infinity</D:depth> + <D:owner> + <D:href>http://example.org/~ejw/contact.html</D:href> + </D:owner> + <D:timeout>Second-604800</D:timeout> + <D:locktoken> + <D:href>urn:uuid:e71d4fae-5dec-22d6-fea5-00a0c91e6be4</D:href> + </D:locktoken> + <D:lockroot> + <D:href>http://example.com/workspace/webdav/proposal.doc</D:href> + </D:lockroot> + </D:activelock> + </D:lockdiscovery> +</D:prop>"#; + + assert_eq!(&got, expected, "\n---GOT---\n{got}\n---EXP---\n{expected}\n"); + } +} diff --git a/aero-dav/src/error.rs b/aero-dav/src/error.rs new file mode 100644 index 0000000..78c6d6b --- /dev/null +++ b/aero-dav/src/error.rs @@ -0,0 +1,42 @@ +use quick_xml::events::attributes::AttrError; + +#[derive(Debug)] +pub enum ParsingError { + Recoverable, + MissingChild, + NamespacePrefixAlreadyUsed, + WrongToken, + TagNotFound, + InvalidValue, + Utf8Error(std::str::Utf8Error), + QuickXml(quick_xml::Error), + Chrono(chrono::format::ParseError), + Int(std::num::ParseIntError), + Eof +} +impl From<AttrError> for ParsingError { + fn from(value: AttrError) -> Self { + Self::QuickXml(value.into()) + } +} +impl From<quick_xml::Error> for ParsingError { + fn from(value: quick_xml::Error) -> Self { + Self::QuickXml(value) + } +} +impl From<std::str::Utf8Error> for ParsingError { + fn from(value: std::str::Utf8Error) -> Self { + Self::Utf8Error(value) + } +} +impl From<chrono::format::ParseError> for ParsingError { + fn from(value: chrono::format::ParseError) -> Self { + Self::Chrono(value) + } +} + +impl From<std::num::ParseIntError> for ParsingError { + fn from(value: std::num::ParseIntError) -> Self { + Self::Int(value) + } +} diff --git a/aero-dav/src/lib.rs b/aero-dav/src/lib.rs new file mode 100644 index 0000000..6bfbf62 --- /dev/null +++ b/aero-dav/src/lib.rs @@ -0,0 +1,25 @@ +#![feature(type_alias_impl_trait)] +#![feature(async_fn_in_trait)] +#![feature(async_closure)] +#![feature(trait_alias)] + +// utils +pub mod error; +pub mod xml; + +// webdav +pub mod types; +pub mod encoder; +pub mod decoder; + +// calendar +pub mod caltypes; +pub mod calencoder; +pub mod caldecoder; + +// wip +mod acltypes; +mod versioningtypes; + +// final type +pub mod realization; diff --git a/aero-dav/src/realization.rs b/aero-dav/src/realization.rs new file mode 100644 index 0000000..33a556e --- /dev/null +++ b/aero-dav/src/realization.rs @@ -0,0 +1,42 @@ +use super::types as dav; +use super::caltypes as cal; +use super::xml; +use super::error; + +#[derive(Debug, PartialEq)] +pub struct Disabled(()); +impl xml::QRead<Disabled> for Disabled { + async fn qread(xml: &mut xml::Reader<impl xml::IRead>) -> Result<Self, error::ParsingError> { + Err(error::ParsingError::Recoverable) + } +} +impl xml::QWrite for Disabled { + async fn qwrite(&self, xml: &mut xml::Writer<impl xml::IWrite>) -> Result<(), quick_xml::Error> { + unreachable!(); + } +} + +/// The base WebDAV +/// +/// Any extension is kooh is disabled through an object we can't build +/// due to a private inner element. +#[derive(Debug, PartialEq)] +pub struct Core {} +impl dav::Extension for Core { + type Error = Disabled; + type Property = Disabled; + type PropertyRequest = Disabled; + type ResourceType = Disabled; +} + +// WebDAV with the base Calendar implementation (RFC4791) +#[derive(Debug, PartialEq)] +pub struct Calendar {} +impl dav::Extension for Calendar +{ + type Error = cal::Violation; + type Property = cal::Property; + type PropertyRequest = cal::PropertyRequest; + type ResourceType = cal::ResourceType; +} + diff --git a/aero-dav/src/types.rs b/aero-dav/src/types.rs new file mode 100644 index 0000000..2489c0a --- /dev/null +++ b/aero-dav/src/types.rs @@ -0,0 +1,949 @@ +#![allow(dead_code)] +use std::fmt::Debug; + +use chrono::{DateTime,FixedOffset}; +use super::xml; + +/// It's how we implement a DAV extension +/// (That's the dark magic part...) +pub trait Extension: std::fmt::Debug + PartialEq { + type Error: xml::Node<Self::Error>; + type Property: xml::Node<Self::Property>; + type PropertyRequest: xml::Node<Self::PropertyRequest>; + type ResourceType: xml::Node<Self::ResourceType>; +} + +/// 14.1. activelock XML Element +/// +/// Name: activelock +/// +/// Purpose: Describes a lock on a resource. +/// <!ELEMENT activelock (lockscope, locktype, depth, owner?, timeout?, +/// locktoken?, lockroot)> +#[derive(Debug, PartialEq)] +pub struct ActiveLock { + pub lockscope: LockScope, + pub locktype: LockType, + pub depth: Depth, + pub owner: Option<Owner>, + pub timeout: Option<Timeout>, + pub locktoken: Option<LockToken>, + pub lockroot: LockRoot, +} + +/// 14.3 collection XML Element +/// +/// Name: collection +/// +/// Purpose: Identifies the associated resource as a collection. The +/// DAV:resourcetype property of a collection resource MUST contain +/// this element. It is normally empty but extensions may add sub- +/// elements. +/// +/// <!ELEMENT collection EMPTY > +#[derive(Debug, PartialEq)] +pub struct Collection{} + +/// 14.4 depth XML Element +/// +/// Name: depth +/// +/// Purpose: Used for representing depth values in XML content (e.g., +/// in lock information). +/// +/// Value: "0" | "1" | "infinity" +/// +/// <!ELEMENT depth (#PCDATA) > +#[derive(Debug, PartialEq)] +pub enum Depth { + Zero, + One, + Infinity +} + +/// 14.5 error XML Element +/// +/// Name: error +/// +/// Purpose: Error responses, particularly 403 Forbidden and 409 +/// Conflict, sometimes need more information to indicate what went +/// wrong. In these cases, servers MAY return an XML response body +/// with a document element of 'error', containing child elements +/// identifying particular condition codes. +/// +/// Description: Contains at least one XML element, and MUST NOT +/// contain text or mixed content. Any element that is a child of the +/// 'error' element is considered to be a precondition or +/// postcondition code. Unrecognized elements MUST be ignored. +/// +/// <!ELEMENT error ANY > +#[derive(Debug, PartialEq)] +pub struct Error<E: Extension>(pub Vec<Violation<E>>); +#[derive(Debug, PartialEq)] +pub enum Violation<E: Extension> { + /// Name: lock-token-matches-request-uri + /// + /// Use with: 409 Conflict + /// + /// Purpose: (precondition) -- A request may include a Lock-Token header + /// to identify a lock for the UNLOCK method. However, if the + /// Request-URI does not fall within the scope of the lock identified + /// by the token, the server SHOULD use this error. The lock may have + /// a scope that does not include the Request-URI, or the lock could + /// have disappeared, or the token may be invalid. + LockTokenMatchesRequestUri, + + /// Name: lock-token-submitted (precondition) + /// + /// Use with: 423 Locked + /// + /// Purpose: The request could not succeed because a lock token should + /// have been submitted. This element, if present, MUST contain at + /// least one URL of a locked resource that prevented the request. In + /// cases of MOVE, COPY, and DELETE where collection locks are + /// involved, it can be difficult for the client to find out which + /// locked resource made the request fail -- but the server is only + /// responsible for returning one such locked resource. The server + /// MAY return every locked resource that prevented the request from + /// succeeding if it knows them all. + /// + /// <!ELEMENT lock-token-submitted (href+) > + LockTokenSubmitted(Vec<Href>), + + /// Name: no-conflicting-lock (precondition) + /// + /// Use with: Typically 423 Locked + /// + /// Purpose: A LOCK request failed due the presence of an already + /// existing conflicting lock. Note that a lock can be in conflict + /// although the resource to which the request was directed is only + /// indirectly locked. In this case, the precondition code can be + /// used to inform the client about the resource that is the root of + /// the conflicting lock, avoiding a separate lookup of the + /// "lockdiscovery" property. + /// + /// <!ELEMENT no-conflicting-lock (href)* > + NoConflictingLock(Vec<Href>), + + /// Name: no-external-entities + /// + /// Use with: 403 Forbidden + /// + /// Purpose: (precondition) -- If the server rejects a client request + /// because the request body contains an external entity, the server + /// SHOULD use this error. + NoExternalEntities, + + /// Name: preserved-live-properties + /// + /// Use with: 409 Conflict + /// + /// Purpose: (postcondition) -- The server received an otherwise-valid + /// MOVE or COPY request, but cannot maintain the live properties with + /// the same behavior at the destination. It may be that the server + /// only supports some live properties in some parts of the + /// repository, or simply has an internal error. + PreservedLiveProperties, + + /// Name: propfind-finite-depth + /// + /// Use with: 403 Forbidden + /// + /// Purpose: (precondition) -- This server does not allow infinite-depth + /// PROPFIND requests on collections. + PropfindFiniteDepth, + + + /// Name: cannot-modify-protected-property + /// + /// Use with: 403 Forbidden + /// + /// Purpose: (precondition) -- The client attempted to set a protected + /// property in a PROPPATCH (such as DAV:getetag). See also + /// [RFC3253], Section 3.12. + CannotModifyProtectedProperty, + + /// Specific errors + Extension(E::Error), +} + +/// 14.6. exclusive XML Element +/// +/// Name: exclusive +/// +/// Purpose: Specifies an exclusive lock. +/// +/// <!ELEMENT exclusive EMPTY > +#[derive(Debug, PartialEq)] +pub struct Exclusive {} + +/// 14.7. href XML Element +/// +/// Name: href +/// +/// Purpose: MUST contain a URI or a relative reference. +/// +/// Description: There may be limits on the value of 'href' depending +/// on the context of its use. Refer to the specification text where +/// 'href' is used to see what limitations apply in each case. +/// +/// Value: Simple-ref +/// +/// <!ELEMENT href (#PCDATA)> +#[derive(Debug, PartialEq)] +pub struct Href(pub String); + + +/// 14.8. include XML Element +/// +/// Name: include +/// +/// Purpose: Any child element represents the name of a property to be +/// included in the PROPFIND response. All elements inside an +/// 'include' XML element MUST define properties related to the +/// resource, although possible property names are in no way limited +/// to those property names defined in this document or other +/// standards. This element MUST NOT contain text or mixed content. +/// +/// <!ELEMENT include ANY > +#[derive(Debug, PartialEq)] +pub struct Include<E: Extension>(pub Vec<PropertyRequest<E>>); + +/// 14.9. location XML Element +/// +/// Name: location +/// +/// Purpose: HTTP defines the "Location" header (see [RFC2616], Section +/// 14.30) for use with some status codes (such as 201 and the 300 +/// series codes). When these codes are used inside a 'multistatus' +/// element, the 'location' element can be used to provide the +/// accompanying Location header value. +/// +/// Description: Contains a single href element with the same value +/// that would be used in a Location header. +/// +/// <!ELEMENT location (href)> +#[derive(Debug, PartialEq)] +pub struct Location(pub Href); + +/// 14.10. lockentry XML Element +/// +/// Name: lockentry +/// +/// Purpose: Defines the types of locks that can be used with the +/// resource. +/// +/// <!ELEMENT lockentry (lockscope, locktype) > +#[derive(Debug, PartialEq)] +pub struct LockEntry { + pub lockscope: LockScope, + pub locktype: LockType, +} + +/// 14.11. lockinfo XML Element +/// +/// Name: lockinfo +/// +/// Purpose: The 'lockinfo' XML element is used with a LOCK method to +/// specify the type of lock the client wishes to have created. +/// +/// <!ELEMENT lockinfo (lockscope, locktype, owner?) > +#[derive(Debug, PartialEq)] +pub struct LockInfo { + pub lockscope: LockScope, + pub locktype: LockType, + pub owner: Option<Owner>, +} + +/// 14.12. lockroot XML Element +/// +/// Name: lockroot +/// +/// Purpose: Contains the root URL of the lock, which is the URL +/// through which the resource was addressed in the LOCK request. +/// +/// Description: The href element contains the root of the lock. The +/// server SHOULD include this in all DAV:lockdiscovery property +/// values and the response to LOCK requests. +/// +/// <!ELEMENT lockroot (href) > +#[derive(Debug, PartialEq)] +pub struct LockRoot(pub Href); + +/// 14.13. lockscope XML Element +/// +/// Name: lockscope +/// +/// Purpose: Specifies whether a lock is an exclusive lock, or a shared +/// lock. +/// <!ELEMENT lockscope (exclusive | shared) > +#[derive(Debug, PartialEq)] +pub enum LockScope { + Exclusive, + Shared +} + +/// 14.14. locktoken XML Element +/// +/// Name: locktoken +/// +/// Purpose: The lock token associated with a lock. +/// +/// Description: The href contains a single lock token URI, which +/// refers to the lock. +/// +/// <!ELEMENT locktoken (href) > +#[derive(Debug, PartialEq)] +pub struct LockToken(pub Href); + +/// 14.15. locktype XML Element +/// +/// Name: locktype +/// +/// Purpose: Specifies the access type of a lock. At present, this +/// specification only defines one lock type, the write lock. +/// +/// <!ELEMENT locktype (write) > +#[derive(Debug, PartialEq)] +pub enum LockType { + /// 14.30. write XML Element + /// + /// Name: write + /// + /// Purpose: Specifies a write lock. + /// + /// + /// <!ELEMENT write EMPTY > + Write +} + +/// 14.16. multistatus XML Element +/// +/// Name: multistatus +/// +/// Purpose: Contains multiple response messages. +/// +/// Description: The 'responsedescription' element at the top level is +/// used to provide a general message describing the overarching +/// nature of the response. If this value is available, an +/// application may use it instead of presenting the individual +/// response descriptions contained within the responses. +/// +/// <!ELEMENT multistatus (response*, responsedescription?) > +#[derive(Debug, PartialEq)] +pub struct Multistatus<E: Extension, N: xml::Node<N>> { + pub responses: Vec<Response<E, N>>, + pub responsedescription: Option<ResponseDescription>, +} + +/// 14.17. owner XML Element +/// +/// Name: owner +/// +/// Purpose: Holds client-supplied information about the creator of a +/// lock. +/// +/// Description: Allows a client to provide information sufficient for +/// either directly contacting a principal (such as a telephone number +/// or Email URI), or for discovering the principal (such as the URL +/// of a homepage) who created a lock. The value provided MUST be +/// treated as a dead property in terms of XML Information Item +/// preservation. The server MUST NOT alter the value unless the +/// owner value provided by the client is empty. For a certain amount +/// of interoperability between different client implementations, if +/// clients have URI-formatted contact information for the lock +/// creator suitable for user display, then clients SHOULD put those +/// URIs in 'href' child elements of the 'owner' element. +/// +/// Extensibility: MAY be extended with child elements, mixed content, +/// text content or attributes. +/// +/// <!ELEMENT owner ANY > +//@FIXME might need support for an extension +#[derive(Debug, PartialEq)] +pub enum Owner { + Txt(String), + Href(Href), + Unknown, +} + +/// 14.18. prop XML Element +/// +/// Name: prop +/// +/// Purpose: Contains properties related to a resource. +/// +/// Description: A generic container for properties defined on +/// resources. All elements inside a 'prop' XML element MUST define +/// properties related to the resource, although possible property +/// names are in no way limited to those property names defined in +/// this document or other standards. This element MUST NOT contain +/// text or mixed content. +/// +/// <!ELEMENT prop ANY > +#[derive(Debug, PartialEq)] +pub struct PropName<E: Extension>(pub Vec<PropertyRequest<E>>); + +#[derive(Debug, PartialEq)] +pub struct PropValue<E: Extension>(pub Vec<Property<E>>); + +/// 14.19. propertyupdate XML Element +/// +/// Name: propertyupdate +/// +/// Purpose: Contains a request to alter the properties on a resource. +/// +/// Description: This XML element is a container for the information +/// required to modify the properties on the resource. +/// +/// <!ELEMENT propertyupdate (remove | set)+ > +#[derive(Debug, PartialEq)] +pub struct PropertyUpdate<E: Extension>(pub Vec<PropertyUpdateItem<E>>); + +#[derive(Debug, PartialEq)] +pub enum PropertyUpdateItem<E: Extension> { + Remove(Remove<E>), + Set(Set<E>), +} + +/// 14.2 allprop XML Element +/// +/// Name: allprop +/// +/// Purpose: Specifies that all names and values of dead properties and +/// the live properties defined by this document existing on the +/// resource are to be returned. +/// +/// <!ELEMENT allprop EMPTY > +/// +/// --- +/// +/// 14.21. propname XML Element +/// +/// Name: propname +/// +/// Purpose: Specifies that only a list of property names on the +/// resource is to be returned. +/// +/// <!ELEMENT propname EMPTY > +/// +/// --- +/// +/// 14.20. propfind XML Element +/// +/// Name: propfind +/// +/// Purpose: Specifies the properties to be returned from a PROPFIND +/// method. Four special elements are specified for use with +/// 'propfind': 'prop', 'allprop', 'include', and 'propname'. If +/// 'prop' is used inside 'propfind', it MUST NOT contain property +/// values. +/// +/// <!ELEMENT propfind ( propname | (allprop, include?) | prop ) > +#[derive(Debug, PartialEq)] +pub enum PropFind<E: Extension> { + PropName, + AllProp(Option<Include<E>>), + Prop(PropName<E>), +} + +/// 14.22 propstat XML Element +/// +/// Name: propstat +/// +/// Purpose: Groups together a prop and status element that is +/// associated with a particular 'href' element. +/// +/// Description: The propstat XML element MUST contain one prop XML +/// element and one status XML element. The contents of the prop XML +/// element MUST only list the names of properties to which the result +/// in the status element applies. The optional precondition/ +/// postcondition element and 'responsedescription' text also apply to +/// the properties named in 'prop'. +/// +/// <!ELEMENT propstat (prop, status, error?, responsedescription?) > +#[derive(Debug, PartialEq)] +pub struct PropStat<E: Extension, N: xml::Node<N>> { + pub prop: N, + pub status: Status, + pub error: Option<Error<E>>, + pub responsedescription: Option<ResponseDescription>, +} + +/// 14.23. remove XML Element +/// +/// Name: remove +/// +/// Purpose: Lists the properties to be removed from a resource. +/// +/// Description: Remove instructs that the properties specified in prop +/// should be removed. Specifying the removal of a property that does +/// not exist is not an error. All the XML elements in a 'prop' XML +/// element inside of a 'remove' XML element MUST be empty, as only +/// the names of properties to be removed are required. +/// +/// <!ELEMENT remove (prop) > +#[derive(Debug, PartialEq)] +pub struct Remove<E: Extension>(pub PropName<E>); + +/// 14.24. response XML Element +/// +/// Name: response +/// +/// Purpose: Holds a single response describing the effect of a method +/// on resource and/or its properties. +/// +/// Description: The 'href' element contains an HTTP URL pointing to a +/// WebDAV resource when used in the 'response' container. A +/// particular 'href' value MUST NOT appear more than once as the +/// child of a 'response' XML element under a 'multistatus' XML +/// element. This requirement is necessary in order to keep +/// processing costs for a response to linear time. Essentially, this +/// prevents having to search in order to group together all the +/// responses by 'href'. There are, however, no requirements +/// regarding ordering based on 'href' values. The optional +/// precondition/postcondition element and 'responsedescription' text +/// can provide additional information about this resource relative to +/// the request or result. +/// +/// <!ELEMENT response (href, ((href*, status)|(propstat+)), +/// error?, responsedescription? , location?) > +/// +/// --- rewritten as --- +/// <!ELEMENT response ((href+, status)|(href, propstat+), error?, responsedescription?, location?> +#[derive(Debug, PartialEq)] +pub enum StatusOrPropstat<E: Extension, N: xml::Node<N>> { + // One status, multiple hrefs... + Status(Vec<Href>, Status), + // A single href, multiple properties... + PropStat(Href, Vec<PropStat<E, N>>), +} + +#[derive(Debug, PartialEq)] +pub struct Response<E: Extension, N: xml::Node<N>> { + pub status_or_propstat: StatusOrPropstat<E, N>, + pub error: Option<Error<E>>, + pub responsedescription: Option<ResponseDescription>, + pub location: Option<Location>, +} + +/// 14.25. responsedescription XML Element +/// +/// Name: responsedescription +/// +/// Purpose: Contains information about a status response within a +/// Multi-Status. +/// +/// Description: Provides information suitable to be presented to a +/// user. +/// +/// <!ELEMENT responsedescription (#PCDATA) > +#[derive(Debug, PartialEq)] +pub struct ResponseDescription(pub String); + +/// 14.26. set XML Element +/// +/// Name: set +/// +/// Purpose: Lists the property values to be set for a resource. +/// +/// Description: The 'set' element MUST contain only a 'prop' element. +/// The elements contained by the 'prop' element inside the 'set' +/// element MUST specify the name and value of properties that are set +/// on the resource identified by Request-URI. If a property already +/// exists, then its value is replaced. Language tagging information +/// appearing in the scope of the 'prop' element (in the "xml:lang" +/// attribute, if present) MUST be persistently stored along with the +/// property, and MUST be subsequently retrievable using PROPFIND. +/// +/// <!ELEMENT set (prop) > +#[derive(Debug, PartialEq)] +pub struct Set<E: Extension>(pub PropValue<E>); + +/// 14.27. shared XML Element +/// +/// Name: shared +/// +/// Purpose: Specifies a shared lock. +/// +/// +/// <!ELEMENT shared EMPTY > +#[derive(Debug, PartialEq)] +pub struct Shared {} + + +/// 14.28. status XML Element +/// +/// Name: status +/// +/// Purpose: Holds a single HTTP status-line. +/// +/// Value: status-line (defined in Section 6.1 of [RFC2616]) +/// +/// <!ELEMENT status (#PCDATA) > +//@FIXME: Better typing is possible with an enum for example +#[derive(Debug, PartialEq)] +pub struct Status(pub http::status::StatusCode); + +/// 14.29. timeout XML Element +/// +/// Name: timeout +/// +/// Purpose: The number of seconds remaining before a lock expires. +/// +/// Value: TimeType (defined in Section 10.7) +/// +/// +/// <!ELEMENT timeout (#PCDATA) > +/// +/// TimeOut = "Timeout" ":" 1#TimeType +/// TimeType = ("Second-" DAVTimeOutVal | "Infinite") +/// ; No LWS allowed within TimeType +/// DAVTimeOutVal = 1*DIGIT +/// +/// Clients MAY include Timeout request headers in their LOCK requests. +/// However, the server is not required to honor or even consider these +/// requests. Clients MUST NOT submit a Timeout request header with any +/// method other than a LOCK method. +/// +/// The "Second" TimeType specifies the number of seconds that will +/// elapse between granting of the lock at the server, and the automatic +/// removal of the lock. The timeout value for TimeType "Second" MUST +/// NOT be greater than 2^32-1. +#[derive(Debug, PartialEq)] +pub enum Timeout { + Seconds(u32), + Infinite, +} + + +/// 15. DAV Properties +/// +/// For DAV properties, the name of the property is also the same as the +/// name of the XML element that contains its value. In the section +/// below, the final line of each section gives the element type +/// declaration using the format defined in [REC-XML]. The "Value" +/// field, where present, specifies further restrictions on the allowable +/// contents of the XML element using BNF (i.e., to further restrict the +/// values of a PCDATA element). +/// +/// A protected property is one that cannot be changed with a PROPPATCH +/// request. There may be other requests that would result in a change +/// to a protected property (as when a LOCK request affects the value of +/// DAV:lockdiscovery). Note that a given property could be protected on +/// one type of resource, but not protected on another type of resource. +/// +/// A computed property is one with a value defined in terms of a +/// computation (based on the content and other properties of that +/// resource, or even of some other resource). A computed property is +/// always a protected property. +/// +/// COPY and MOVE behavior refers to local COPY and MOVE operations. +/// +/// For properties defined based on HTTP GET response headers (DAV:get*), +/// the header value could include LWS as defined in [RFC2616], Section +/// 4.2. Server implementors SHOULD strip LWS from these values before +/// using as WebDAV property values. +#[derive(Debug, PartialEq)] +pub enum PropertyRequest<E: Extension> { + CreationDate, + DisplayName, + GetContentLanguage, + GetContentLength, + GetContentType, + GetEtag, + GetLastModified, + LockDiscovery, + ResourceType, + SupportedLock, + Extension(E::PropertyRequest), +} + +#[derive(Debug, PartialEq)] +pub enum Property<E: Extension> { + /// 15.1. creationdate Property + /// + /// Name: creationdate + /// + /// Purpose: Records the time and date the resource was created. + /// + /// Value: date-time (defined in [RFC3339], see the ABNF in Section + /// 5.6.) + /// + /// Protected: MAY be protected. Some servers allow DAV:creationdate + /// to be changed to reflect the time the document was created if that + /// is more meaningful to the user (rather than the time it was + /// uploaded). Thus, clients SHOULD NOT use this property in + /// synchronization logic (use DAV:getetag instead). + /// + /// COPY/MOVE behavior: This property value SHOULD be kept during a + /// MOVE operation, but is normally re-initialized when a resource is + /// created with a COPY. It should not be set in a COPY. + /// + /// Description: The DAV:creationdate property SHOULD be defined on all + /// DAV compliant resources. If present, it contains a timestamp of + /// the moment when the resource was created. Servers that are + /// incapable of persistently recording the creation date SHOULD + /// instead leave it undefined (i.e. report "Not Found"). + /// + /// <!ELEMENT creationdate (#PCDATA) > + CreationDate(DateTime<FixedOffset>), + + /// 15.2. displayname Property + /// + /// Name: displayname + /// + /// Purpose: Provides a name for the resource that is suitable for + /// presentation to a user. + /// + /// Value: Any text. + /// + /// Protected: SHOULD NOT be protected. Note that servers implementing + /// [RFC2518] might have made this a protected property as this is a + /// new requirement. + /// + /// COPY/MOVE behavior: This property value SHOULD be preserved in COPY + /// and MOVE operations. + /// + /// Description: Contains a description of the resource that is + /// suitable for presentation to a user. This property is defined on + /// the resource, and hence SHOULD have the same value independent of + /// the Request-URI used to retrieve it (thus, computing this property + /// based on the Request-URI is deprecated). While generic clients + /// might display the property value to end users, client UI designers + /// must understand that the method for identifying resources is still + /// the URL. Changes to DAV:displayname do not issue moves or copies + /// to the server, but simply change a piece of meta-data on the + /// individual resource. Two resources can have the same DAV: + /// displayname value even within the same collection. + /// + /// <!ELEMENT displayname (#PCDATA) > + DisplayName(String), + + + /// 15.3. getcontentlanguage Property + /// + /// Name: getcontentlanguage + /// + /// Purpose: Contains the Content-Language header value (from Section + /// 14.12 of [RFC2616]) as it would be returned by a GET without + /// accept headers. + /// + /// Value: language-tag (language-tag is defined in Section 3.10 of + /// [RFC2616]) + /// + /// Protected: SHOULD NOT be protected, so that clients can reset the + /// language. Note that servers implementing [RFC2518] might have + /// made this a protected property as this is a new requirement. + /// + /// COPY/MOVE behavior: This property value SHOULD be preserved in COPY + /// and MOVE operations. + /// + /// Description: The DAV:getcontentlanguage property MUST be defined on + /// any DAV-compliant resource that returns the Content-Language + /// header on a GET. + /// + /// <!ELEMENT getcontentlanguage (#PCDATA) > + GetContentLanguage(String), + + /// 15.4. getcontentlength Property + /// + /// Name: getcontentlength + /// + /// Purpose: Contains the Content-Length header returned by a GET + /// without accept headers. + /// + /// Value: See Section 14.13 of [RFC2616]. + /// + /// Protected: This property is computed, therefore protected. + /// + /// Description: The DAV:getcontentlength property MUST be defined on + /// any DAV-compliant resource that returns the Content-Length header + /// in response to a GET. + /// + /// COPY/MOVE behavior: This property value is dependent on the size of + /// the destination resource, not the value of the property on the + /// source resource. + /// + /// <!ELEMENT getcontentlength (#PCDATA) > + GetContentLength(u64), + + /// 15.5. getcontenttype Property + /// + /// Name: getcontenttype + /// + /// Purpose: Contains the Content-Type header value (from Section 14.17 + /// of [RFC2616]) as it would be returned by a GET without accept + /// headers. + /// + /// Value: media-type (defined in Section 3.7 of [RFC2616]) + /// + /// Protected: Potentially protected if the server prefers to assign + /// content types on its own (see also discussion in Section 9.7.1). + /// + /// COPY/MOVE behavior: This property value SHOULD be preserved in COPY + /// and MOVE operations. + /// + /// Description: This property MUST be defined on any DAV-compliant + /// resource that returns the Content-Type header in response to a + /// GET. + /// + /// <!ELEMENT getcontenttype (#PCDATA) > + GetContentType(String), + + /// 15.6. getetag Property + /// + /// Name: getetag + /// + /// Purpose: Contains the ETag header value (from Section 14.19 of + /// [RFC2616]) as it would be returned by a GET without accept + /// headers. + /// + /// Value: entity-tag (defined in Section 3.11 of [RFC2616]) + /// + /// Protected: MUST be protected because this value is created and + /// controlled by the server. + /// + /// COPY/MOVE behavior: This property value is dependent on the final + /// state of the destination resource, not the value of the property + /// on the source resource. Also note the considerations in + /// Section 8.8. + /// + /// Description: The getetag property MUST be defined on any DAV- + /// compliant resource that returns the Etag header. Refer to Section + /// 3.11 of RFC 2616 for a complete definition of the semantics of an + /// ETag, and to Section 8.6 for a discussion of ETags in WebDAV. + /// + /// <!ELEMENT getetag (#PCDATA) > + GetEtag(String), + + /// 15.7. getlastmodified Property + /// + /// Name: getlastmodified + /// + /// Purpose: Contains the Last-Modified header value (from Section + /// 14.29 of [RFC2616]) as it would be returned by a GET method + /// without accept headers. + /// + /// Value: rfc1123-date (defined in Section 3.3.1 of [RFC2616]) + /// + /// Protected: SHOULD be protected because some clients may rely on the + /// value for appropriate caching behavior, or on the value of the + /// Last-Modified header to which this property is linked. + /// + /// COPY/MOVE behavior: This property value is dependent on the last + /// modified date of the destination resource, not the value of the + /// property on the source resource. Note that some server + /// implementations use the file system date modified value for the + /// DAV:getlastmodified value, and this can be preserved in a MOVE + /// even when the HTTP Last-Modified value SHOULD change. Note that + /// since [RFC2616] requires clients to use ETags where provided, a + /// server implementing ETags can count on clients using a much better + /// mechanism than modification dates for offline synchronization or + /// cache control. Also note the considerations in Section 8.8. + /// + /// Description: The last-modified date on a resource SHOULD only + /// reflect changes in the body (the GET responses) of the resource. + /// A change in a property only SHOULD NOT cause the last-modified + /// date to change, because clients MAY rely on the last-modified date + /// to know when to overwrite the existing body. The DAV: + /// getlastmodified property MUST be defined on any DAV-compliant + /// resource that returns the Last-Modified header in response to a + /// GET. + /// + /// <!ELEMENT getlastmodified (#PCDATA) > + GetLastModified(DateTime<FixedOffset>), + + /// 15.8. lockdiscovery Property + /// + /// Name: lockdiscovery + /// + /// Purpose: Describes the active locks on a resource + /// + /// Protected: MUST be protected. Clients change the list of locks + /// through LOCK and UNLOCK, not through PROPPATCH. + /// + /// COPY/MOVE behavior: The value of this property depends on the lock + /// state of the destination, not on the locks of the source resource. + /// Recall that locks are not moved in a MOVE operation. + /// + /// Description: Returns a listing of who has a lock, what type of lock + /// he has, the timeout type and the time remaining on the timeout, + /// and the associated lock token. Owner information MAY be omitted + /// if it is considered sensitive. If there are no locks, but the + /// server supports locks, the property will be present but contain + /// zero 'activelock' elements. If there are one or more locks, an + /// 'activelock' element appears for each lock on the resource. This + /// property is NOT lockable with respect to write locks (Section 7). + /// + /// <!ELEMENT lockdiscovery (activelock)* > + LockDiscovery(Vec<ActiveLock>), + + + /// 15.9. resourcetype Property + /// + /// Name: resourcetype + /// + /// Purpose: Specifies the nature of the resource. + /// + /// Protected: SHOULD be protected. Resource type is generally decided + /// through the operation creating the resource (MKCOL vs PUT), not by + /// PROPPATCH. + /// + /// COPY/MOVE behavior: Generally a COPY/MOVE of a resource results in + /// the same type of resource at the destination. + /// + /// Description: MUST be defined on all DAV-compliant resources. Each + /// child element identifies a specific type the resource belongs to, + /// such as 'collection', which is the only resource type defined by + /// this specification (see Section 14.3). If the element contains + /// the 'collection' child element plus additional unrecognized + /// elements, it should generally be treated as a collection. If the + /// element contains no recognized child elements, it should be + /// treated as a non-collection resource. The default value is empty. + /// This element MUST NOT contain text or mixed content. Any custom + /// child element is considered to be an identifier for a resource + /// type. + /// + /// Example: (fictional example to show extensibility) + /// + /// <x:resourcetype xmlns:x="DAV:"> + /// <x:collection/> + /// <f:search-results xmlns:f="http://www.example.com/ns"/> + /// </x:resourcetype> + ResourceType(Vec<ResourceType<E>>), + + /// 15.10. supportedlock Property + /// + /// Name: supportedlock + /// + /// Purpose: To provide a listing of the lock capabilities supported by + /// the resource. + /// + /// Protected: MUST be protected. Servers, not clients, determine what + /// lock mechanisms are supported. + /// COPY/MOVE behavior: This property value is dependent on the kind of + /// locks supported at the destination, not on the value of the + /// property at the source resource. Servers attempting to COPY to a + /// destination should not attempt to set this property at the + /// destination. + /// + /// Description: Returns a listing of the combinations of scope and + /// access types that may be specified in a lock request on the + /// resource. Note that the actual contents are themselves controlled + /// by access controls, so a server is not required to provide + /// information the client is not authorized to see. This property is + /// NOT lockable with respect to write locks (Section 7). + /// + /// <!ELEMENT supportedlock (lockentry)* > + SupportedLock(Vec<LockEntry>), + + /// Any extension + Extension(E::Property), +} + +#[derive(Debug, PartialEq)] +pub enum ResourceType<E: Extension> { + Collection, + Extension(E::ResourceType), +} diff --git a/aero-dav/src/versioningtypes.rs b/aero-dav/src/versioningtypes.rs new file mode 100644 index 0000000..6c1c204 --- /dev/null +++ b/aero-dav/src/versioningtypes.rs @@ -0,0 +1,3 @@ +//@FIXME required for a full DAV implementation +// See section 7.1 of the CalDAV RFC +// It seems it's mainly due to the fact that the REPORT method is re-used. diff --git a/aero-dav/src/xml.rs b/aero-dav/src/xml.rs new file mode 100644 index 0000000..98037ac --- /dev/null +++ b/aero-dav/src/xml.rs @@ -0,0 +1,274 @@ +use futures::Future; +use quick_xml::events::{Event, BytesStart}; +use quick_xml::name::ResolveResult; +use quick_xml::reader::NsReader; +use tokio::io::{AsyncWrite, AsyncBufRead}; + +use super::error::ParsingError; + +// Constants +pub const DAV_URN: &[u8] = b"DAV:"; +pub const CAL_URN: &[u8] = b"urn:ietf:params:xml:ns:caldav"; +pub const CARD_URN: &[u8] = b"urn:ietf:params:xml:ns:carddav"; + +// Async traits +pub trait IWrite = AsyncWrite + Unpin; +pub trait IRead = AsyncBufRead + Unpin; + +// Serialization/Deserialization traits +pub trait QWrite { + fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> impl Future<Output = Result<(), quick_xml::Error>>; +} +pub trait QRead<T> { + fn qread(xml: &mut Reader<impl IRead>) -> impl Future<Output = Result<T, ParsingError>>; +} + +// The representation of an XML node in Rust +pub trait Node<T> = QRead<T> + QWrite + std::fmt::Debug + PartialEq; + +// --------------- + +/// Transform a Rust object into an XML stream of characters +pub struct Writer<T: IWrite> { + pub q: quick_xml::writer::Writer<T>, + pub ns_to_apply: Vec<(String, String)>, +} +impl<T: IWrite> Writer<T> { + pub fn create_dav_element(&mut self, name: &str) -> BytesStart<'static> { + self.create_ns_element("D", name) + } + pub fn create_cal_element(&mut self, name: &str) -> BytesStart<'static> { + self.create_ns_element("C", name) + } + + fn create_ns_element(&mut self, ns: &str, name: &str) -> BytesStart<'static> { + let mut start = BytesStart::new(format!("{}:{}", ns, name)); + if !self.ns_to_apply.is_empty() { + start.extend_attributes(self.ns_to_apply.iter().map(|(k, n)| (k.as_str(), n.as_str()))); + self.ns_to_apply.clear() + } + start + } +} + +/// Transform an XML stream of characters into a Rust object +pub struct Reader<T: IRead> { + pub rdr: NsReader<T>, + cur: Event<'static>, + parents: Vec<Event<'static>>, + buf: Vec<u8>, +} +impl<T: IRead> Reader<T> { + pub async fn new(mut rdr: NsReader<T>) -> Result<Self, ParsingError> { + let mut buf: Vec<u8> = vec![]; + let cur = rdr.read_event_into_async(&mut buf).await?.into_owned(); + let parents = vec![]; + buf.clear(); + Ok(Self { cur, parents, rdr, buf }) + } + + /// read one more tag + /// do not expose it publicly + async fn next(&mut self) -> Result<Event<'static>, ParsingError> { + let evt = self.rdr.read_event_into_async(&mut self.buf).await?.into_owned(); + self.buf.clear(); + let old_evt = std::mem::replace(&mut self.cur, evt); + Ok(old_evt) + } + + /// skip a node at current level + /// I would like to make this one private but not ready + pub async fn skip(&mut self) -> Result<Event<'static>, ParsingError> { + //println!("skipping inside node {:?}", self.parents.last()); + match &self.cur { + Event::Start(b) => { + let _span = self.rdr.read_to_end_into_async(b.to_end().name(), &mut self.buf).await?; + self.next().await + }, + Event::End(_) => Err(ParsingError::WrongToken), + Event::Eof => Err(ParsingError::Eof), + _ => self.next().await, + } + } + + /// check if this is the desired tag + fn is_tag(&self, ns: &[u8], key: &str) -> bool { + let qname = match self.peek() { + Event::Start(bs) | Event::Empty(bs) => bs.name(), + Event::End(be) => be.name(), + _ => return false, + }; + + let (extr_ns, local) = self.rdr.resolve_element(qname); + + if local.into_inner() != key.as_bytes() { + return false + } + + match extr_ns { + ResolveResult::Bound(v) => v.into_inner() == ns, + _ => false, + } + } + + fn parent_has_child(&self) -> bool { + matches!(self.parents.last(), Some(Event::Start(_)) | None) + } + + fn ensure_parent_has_child(&self) -> Result<(), ParsingError> { + match self.parent_has_child() { + true => Ok(()), + false => Err(ParsingError::Recoverable), + } + } + + pub fn peek(&self) -> &Event<'static> { + &self.cur + } + + // NEW API + pub async fn tag_string(&mut self) -> Result<String, ParsingError> { + self.ensure_parent_has_child()?; + + let mut acc = String::new(); + loop { + match self.peek() { + Event::CData(unescaped) => { + acc.push_str(std::str::from_utf8(unescaped.as_ref())?); + self.next().await? + }, + Event::Text(escaped) => { + acc.push_str(escaped.unescape()?.as_ref()); + self.next().await? + } + Event::End(_) | Event::Start(_) | Event::Empty(_) => return Ok(acc), + _ => self.next().await?, + }; + } + } + + pub async fn maybe_read<N: Node<N>>(&mut self, t: &mut Option<N>, dirty: &mut bool) -> Result<(), ParsingError> { + if !self.parent_has_child() { + return Ok(()) + } + + match N::qread(self).await { + Ok(v) => { + *t = Some(v); + *dirty = true; + Ok(()) + }, + Err(ParsingError::Recoverable) => Ok(()), + Err(e) => Err(e), + } + } + + pub async fn maybe_push<N: Node<N>>(&mut self, t: &mut Vec<N>, dirty: &mut bool) -> Result<(), ParsingError> { + if !self.parent_has_child() { + return Ok(()) + } + + match N::qread(self).await { + Ok(v) => { + t.push(v); + *dirty = true; + Ok(()) + }, + Err(ParsingError::Recoverable) => Ok(()), + Err(e) => Err(e), + } + } + + pub async fn find<N: Node<N>>(&mut self) -> Result<N, ParsingError> { + self.ensure_parent_has_child()?; + + loop { + // Try parse + match N::qread(self).await { + Err(ParsingError::Recoverable) => (), + otherwise => return otherwise, + } + + // If recovered, skip the element + self.skip().await?; + } + } + + pub async fn maybe_find<N: Node<N>>(&mut self) -> Result<Option<N>, ParsingError> { + self.ensure_parent_has_child()?; + + loop { + // Try parse + match N::qread(self).await { + Err(ParsingError::Recoverable) => (), + otherwise => return otherwise.map(Some), + } + + match self.peek() { + Event::End(_) => return Ok(None), + _ => self.skip().await?, + }; + } + } + + pub async fn collect<N: Node<N>>(&mut self) -> Result<Vec<N>, ParsingError> { + self.ensure_parent_has_child()?; + let mut acc = Vec::new(); + + loop { + match N::qread(self).await { + Err(ParsingError::Recoverable) => match self.peek() { + Event::End(_) => return Ok(acc), + _ => { + self.skip().await?; + }, + }, + Ok(v) => acc.push(v), + Err(e) => return Err(e), + } + } + } + + pub async fn open(&mut self, ns: &[u8], key: &str) -> Result<Event<'static>, ParsingError> { + let evt = match self.peek() { + Event::Empty(_) if self.is_tag(ns, key) => self.cur.clone(), + Event::Start(_) if self.is_tag(ns, key) => self.next().await?, + _ => return Err(ParsingError::Recoverable), + }; + + //println!("open tag {:?}", evt); + self.parents.push(evt.clone()); + Ok(evt) + } + + pub async fn maybe_open(&mut self, ns: &[u8], key: &str) -> Result<Option<Event<'static>>, ParsingError> { + match self.open(ns, key).await { + Ok(v) => Ok(Some(v)), + Err(ParsingError::Recoverable) => Ok(None), + Err(e) => Err(e), + } + } + + // find stop tag + pub async fn close(&mut self) -> Result<Event<'static>, ParsingError> { + //println!("close tag {:?}", self.parents.last()); + + // Handle the empty case + if !self.parent_has_child() { + self.parents.pop(); + return self.next().await + } + + // Handle the start/end case + loop { + match self.peek() { + Event::End(_) => { + self.parents.pop(); + return self.next().await + }, + _ => self.skip().await?, + }; + } + } +} + |