aboutsummaryrefslogtreecommitdiff
path: root/aero-dav/src
diff options
context:
space:
mode:
authorQuentin Dufour <quentin@deuxfleurs.fr>2024-03-08 08:17:03 +0100
committerQuentin Dufour <quentin@deuxfleurs.fr>2024-03-08 08:17:03 +0100
commit1a43ce5ac7033c148f64a033f2b1d335e95e11d5 (patch)
tree60b234604170fe207248458a9c4cdd3f4b7c36f2 /aero-dav/src
parentbb9cb386b65834c44cae86bd100f800883022062 (diff)
downloadaerogramme-1a43ce5ac7033c148f64a033f2b1d335e95e11d5.tar.gz
aerogramme-1a43ce5ac7033c148f64a033f2b1d335e95e11d5.zip
WIP refactor
Diffstat (limited to 'aero-dav/src')
-rw-r--r--aero-dav/src/acltypes.rs4
-rw-r--r--aero-dav/src/caldecoder.rs33
-rw-r--r--aero-dav/src/calencoder.rs886
-rw-r--r--aero-dav/src/caltypes.rs1453
-rw-r--r--aero-dav/src/decoder.rs947
-rw-r--r--aero-dav/src/encoder.rs1112
-rw-r--r--aero-dav/src/error.rs42
-rw-r--r--aero-dav/src/lib.rs25
-rw-r--r--aero-dav/src/realization.rs42
-rw-r--r--aero-dav/src/types.rs949
-rw-r--r--aero-dav/src/versioningtypes.rs3
-rw-r--r--aero-dav/src/xml.rs274
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>&quot;fffff-abcd2&quot;</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>&quot;fffff-abcd3&quot;</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 &lt;, &gt;, &amp; 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 &amp; Canada)
+ /// END:STANDARD
+ /// BEGIN:DAYLIGHT
+ /// DTSTART:19870405T020000
+ /// RRULE:FREQ=YEARLY;BYDAY=1SU;BYMONTH=4
+ /// TZOFFSETFROM:-0500
+ /// TZOFFSETTO:-0400
+ /// TZNAME:Eastern Daylight Time (US &amp; 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 &lt;, &gt;, &amp; 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 &lt;, &gt;, &amp; 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>&quot;zzyzx&quot;</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?,
+ };
+ }
+ }
+}
+