aboutsummaryrefslogtreecommitdiff
path: root/aero-dav/src
diff options
context:
space:
mode:
authorQuentin Dufour <quentin@deuxfleurs.fr>2024-05-29 10:14:51 +0200
committerQuentin Dufour <quentin@deuxfleurs.fr>2024-05-29 10:14:51 +0200
commitb9ce5886033677f6c65a4b873e17574fdb8df31d (patch)
tree9ed1d721361027d7d6fef0ecad65d7e1b74a7ddb /aero-dav/src
parent0dcf69f180f5a7b71b6ad2ac67e4cdd81e5154f1 (diff)
parent5954de6efbb040b8b47daf0c7663a60f3db1da6e (diff)
downloadaerogramme-b9ce5886033677f6c65a4b873e17574fdb8df31d.tar.gz
aerogramme-b9ce5886033677f6c65a4b873e17574fdb8df31d.zip
Merge branch 'caldav'
Diffstat (limited to 'aero-dav/src')
-rw-r--r--aero-dav/src/acldecoder.rs84
-rw-r--r--aero-dav/src/aclencoder.rs71
-rw-r--r--aero-dav/src/acltypes.rs38
-rw-r--r--aero-dav/src/caldecoder.rs1421
-rw-r--r--aero-dav/src/calencoder.rs1036
-rw-r--r--aero-dav/src/caltypes.rs1500
-rw-r--r--aero-dav/src/decoder.rs1152
-rw-r--r--aero-dav/src/encoder.rs1262
-rw-r--r--aero-dav/src/error.rs62
-rw-r--r--aero-dav/src/lib.rs35
-rw-r--r--aero-dav/src/realization.rs260
-rw-r--r--aero-dav/src/syncdecoder.rs248
-rw-r--r--aero-dav/src/syncencoder.rs227
-rw-r--r--aero-dav/src/synctypes.rs86
-rw-r--r--aero-dav/src/types.rs964
-rw-r--r--aero-dav/src/versioningdecoder.rs132
-rw-r--r--aero-dav/src/versioningencoder.rs143
-rw-r--r--aero-dav/src/versioningtypes.rs59
-rw-r--r--aero-dav/src/xml.rs367
19 files changed, 9147 insertions, 0 deletions
diff --git a/aero-dav/src/acldecoder.rs b/aero-dav/src/acldecoder.rs
new file mode 100644
index 0000000..405286e
--- /dev/null
+++ b/aero-dav/src/acldecoder.rs
@@ -0,0 +1,84 @@
+use super::acltypes::*;
+use super::error::ParsingError;
+use super::types as dav;
+use super::xml::{IRead, QRead, Reader, DAV_URN};
+
+impl QRead<Property> for Property {
+ async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> {
+ if xml.maybe_open_start(DAV_URN, "owner").await?.is_some() {
+ let href = xml.find().await?;
+ xml.close().await?;
+ return Ok(Self::Owner(href));
+ }
+ if xml
+ .maybe_open_start(DAV_URN, "current-user-principal")
+ .await?
+ .is_some()
+ {
+ let user = xml.find().await?;
+ xml.close().await?;
+ return Ok(Self::CurrentUserPrincipal(user));
+ }
+ if xml
+ .maybe_open_start(DAV_URN, "current-user-privilege-set")
+ .await?
+ .is_some()
+ {
+ xml.close().await?;
+ return Ok(Self::CurrentUserPrivilegeSet(vec![]));
+ }
+
+ Err(ParsingError::Recoverable)
+ }
+}
+
+impl QRead<PropertyRequest> for PropertyRequest {
+ async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> {
+ if xml.maybe_open(DAV_URN, "owner").await?.is_some() {
+ xml.close().await?;
+ return Ok(Self::Owner);
+ }
+
+ if xml
+ .maybe_open(DAV_URN, "current-user-principal")
+ .await?
+ .is_some()
+ {
+ xml.close().await?;
+ return Ok(Self::CurrentUserPrincipal);
+ }
+
+ if xml
+ .maybe_open(DAV_URN, "current-user-privilege-set")
+ .await?
+ .is_some()
+ {
+ xml.close().await?;
+ return Ok(Self::CurrentUserPrivilegeSet);
+ }
+
+ Err(ParsingError::Recoverable)
+ }
+}
+
+impl QRead<ResourceType> for ResourceType {
+ async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> {
+ if xml.maybe_open(DAV_URN, "principal").await?.is_some() {
+ xml.close().await?;
+ return Ok(Self::Principal);
+ }
+ Err(ParsingError::Recoverable)
+ }
+}
+
+// -----
+impl QRead<User> for User {
+ async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> {
+ if xml.maybe_open(DAV_URN, "unauthenticated").await?.is_some() {
+ xml.close().await?;
+ return Ok(Self::Unauthenticated);
+ }
+
+ dav::Href::qread(xml).await.map(Self::Authenticated)
+ }
+}
diff --git a/aero-dav/src/aclencoder.rs b/aero-dav/src/aclencoder.rs
new file mode 100644
index 0000000..28c01a7
--- /dev/null
+++ b/aero-dav/src/aclencoder.rs
@@ -0,0 +1,71 @@
+use quick_xml::events::Event;
+use quick_xml::Error as QError;
+
+use super::acltypes::*;
+use super::error::ParsingError;
+use super::xml::{IWrite, QWrite, Writer};
+
+impl QWrite for Property {
+ async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> {
+ match self {
+ Self::Owner(href) => {
+ let start = xml.create_dav_element("owner");
+ 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::CurrentUserPrincipal(user) => {
+ let start = xml.create_dav_element("current-user-principal");
+ let end = start.to_end();
+ xml.q.write_event_async(Event::Start(start.clone())).await?;
+ user.qwrite(xml).await?;
+ xml.q.write_event_async(Event::End(end)).await
+ }
+ Self::CurrentUserPrivilegeSet(_) => {
+ let empty_tag = xml.create_dav_element("current-user-privilege-set");
+ xml.q.write_event_async(Event::Empty(empty_tag)).await
+ }
+ }
+ }
+}
+
+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_dav_element(c);
+ xml.q.write_event_async(Event::Empty(empty_tag)).await
+ };
+
+ match self {
+ Self::Owner => atom("owner").await,
+ Self::CurrentUserPrincipal => atom("current-user-principal").await,
+ Self::CurrentUserPrivilegeSet => atom("current-user-privilege-set").await,
+ }
+ }
+}
+
+impl QWrite for ResourceType {
+ async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> {
+ match self {
+ Self::Principal => {
+ let empty_tag = xml.create_dav_element("principal");
+ xml.q.write_event_async(Event::Empty(empty_tag)).await
+ }
+ }
+ }
+}
+
+// -----
+
+impl QWrite for User {
+ async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> {
+ match self {
+ Self::Unauthenticated => {
+ let tag = xml.create_dav_element("unauthenticated");
+ xml.q.write_event_async(Event::Empty(tag)).await
+ }
+ Self::Authenticated(href) => href.qwrite(xml).await,
+ }
+ }
+}
diff --git a/aero-dav/src/acltypes.rs b/aero-dav/src/acltypes.rs
new file mode 100644
index 0000000..0af3c8a
--- /dev/null
+++ b/aero-dav/src/acltypes.rs
@@ -0,0 +1,38 @@
+use super::types as dav;
+
+//RFC covered: RFC3744 (ACL core) + RFC5397 (ACL Current Principal Extension)
+
+//@FIXME required for a full CalDAV 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.
+// Note: it is used by Thunderbird
+
+#[derive(Debug, PartialEq, Clone)]
+pub enum PropertyRequest {
+ Owner,
+ CurrentUserPrincipal,
+ CurrentUserPrivilegeSet,
+}
+
+#[derive(Debug, PartialEq, Clone)]
+pub enum Property {
+ Owner(dav::Href),
+ CurrentUserPrincipal(User),
+ CurrentUserPrivilegeSet(Vec<Privilege>),
+}
+
+#[derive(Debug, PartialEq, Clone)]
+pub enum ResourceType {
+ Principal,
+}
+
+/// Not implemented, it's a placeholder
+#[derive(Debug, PartialEq, Clone)]
+pub struct Privilege(());
+
+#[derive(Debug, PartialEq, Clone)]
+pub enum User {
+ Unauthenticated,
+ Authenticated(dav::Href),
+}
diff --git a/aero-dav/src/caldecoder.rs b/aero-dav/src/caldecoder.rs
new file mode 100644
index 0000000..9ed783a
--- /dev/null
+++ b/aero-dav/src/caldecoder.rs
@@ -0,0 +1,1421 @@
+use chrono::NaiveDateTime;
+use quick_xml::events::Event;
+
+use super::caltypes::*;
+use super::error::ParsingError;
+use super::types as dav;
+use super::xml::{IRead, QRead, Reader, CAL_URN, DAV_URN};
+
+// ---- ROOT ELEMENTS ---
+impl<E: dav::Extension> QRead<MkCalendar<E>> for MkCalendar<E> {
+ async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> {
+ xml.open(CAL_URN, "mkcalendar").await?;
+ let set = xml.find().await?;
+ xml.close().await?;
+ Ok(MkCalendar(set))
+ }
+}
+
+impl<E: dav::Extension> QRead<MkCalendarResponse<E>> for MkCalendarResponse<E> {
+ async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> {
+ xml.open(CAL_URN, "mkcalendar-response").await?;
+ let propstats = xml.collect().await?;
+ xml.close().await?;
+ Ok(MkCalendarResponse(propstats))
+ }
+}
+
+impl<E: dav::Extension> QRead<ReportType<E>> for ReportType<E> {
+ async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> {
+ match CalendarQuery::<E>::qread(xml).await {
+ Err(ParsingError::Recoverable) => (),
+ otherwise => return otherwise.map(Self::Query),
+ }
+
+ match CalendarMultiget::<E>::qread(xml).await {
+ Err(ParsingError::Recoverable) => (),
+ otherwise => return otherwise.map(Self::Multiget),
+ }
+
+ FreeBusyQuery::qread(xml).await.map(Self::FreeBusy)
+ }
+}
+
+impl<E: dav::Extension> QRead<CalendarQuery<E>> for CalendarQuery<E> {
+ async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> {
+ xml.open(CAL_URN, "calendar-query").await?;
+ let (mut selector, mut filter, mut timezone) = (None, None, None);
+ loop {
+ let mut dirty = false;
+ xml.maybe_read(&mut selector, &mut dirty).await?;
+ xml.maybe_read(&mut filter, &mut dirty).await?;
+ xml.maybe_read(&mut timezone, &mut dirty).await?;
+
+ if !dirty {
+ match xml.peek() {
+ Event::End(_) => break,
+ _ => xml.skip().await?,
+ };
+ }
+ }
+ xml.close().await?;
+
+ match filter {
+ Some(filter) => Ok(CalendarQuery {
+ selector,
+ filter,
+ timezone,
+ }),
+ _ => Err(ParsingError::MissingChild),
+ }
+ }
+}
+
+impl<E: dav::Extension> QRead<CalendarMultiget<E>> for CalendarMultiget<E> {
+ async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> {
+ xml.open(CAL_URN, "calendar-multiget").await?;
+ let mut selector = None;
+ let mut href = Vec::new();
+
+ loop {
+ let mut dirty = false;
+ xml.maybe_read(&mut selector, &mut dirty).await?;
+ xml.maybe_push(&mut href, &mut dirty).await?;
+
+ if !dirty {
+ match xml.peek() {
+ Event::End(_) => break,
+ _ => xml.skip().await?,
+ };
+ }
+ }
+
+ xml.close().await?;
+ Ok(CalendarMultiget { selector, href })
+ }
+}
+
+impl QRead<FreeBusyQuery> for FreeBusyQuery {
+ async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> {
+ xml.open(CAL_URN, "free-busy-query").await?;
+ let range = xml.find().await?;
+ xml.close().await?;
+ Ok(FreeBusyQuery(range))
+ }
+}
+
+impl QRead<ReportTypeName> for ReportTypeName {
+ async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> {
+ if xml.maybe_open(DAV_URN, "calendar-query").await?.is_some() {
+ xml.close().await?;
+ return Ok(Self::Query);
+ }
+ if xml
+ .maybe_open(DAV_URN, "calendar-multiget")
+ .await?
+ .is_some()
+ {
+ xml.close().await?;
+ return Ok(Self::Multiget);
+ }
+ if xml.maybe_open(DAV_URN, "free-busy-query").await?.is_some() {
+ xml.close().await?;
+ return Ok(Self::FreeBusy);
+ }
+ Err(ParsingError::Recoverable)
+ }
+}
+
+// ---- EXTENSIONS ---
+impl QRead<Violation> for Violation {
+ async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> {
+ if xml
+ .maybe_open(DAV_URN, "resource-must-be-null")
+ .await?
+ .is_some()
+ {
+ xml.close().await?;
+ Ok(Self::ResourceMustBeNull)
+ } else if xml.maybe_open(DAV_URN, "need-privileges").await?.is_some() {
+ xml.close().await?;
+ Ok(Self::NeedPrivileges)
+ } else if xml
+ .maybe_open(CAL_URN, "calendar-collection-location-ok")
+ .await?
+ .is_some()
+ {
+ xml.close().await?;
+ Ok(Self::CalendarCollectionLocationOk)
+ } else if xml
+ .maybe_open(CAL_URN, "valid-calendar-data")
+ .await?
+ .is_some()
+ {
+ xml.close().await?;
+ Ok(Self::ValidCalendarData)
+ } else if xml
+ .maybe_open(CAL_URN, "initialize-calendar-collection")
+ .await?
+ .is_some()
+ {
+ xml.close().await?;
+ Ok(Self::InitializeCalendarCollection)
+ } else if xml
+ .maybe_open(CAL_URN, "supported-calendar-data")
+ .await?
+ .is_some()
+ {
+ xml.close().await?;
+ Ok(Self::SupportedCalendarData)
+ } else if xml
+ .maybe_open(CAL_URN, "valid-calendar-object-resource")
+ .await?
+ .is_some()
+ {
+ xml.close().await?;
+ Ok(Self::ValidCalendarObjectResource)
+ } else if xml
+ .maybe_open(CAL_URN, "supported-calendar-component")
+ .await?
+ .is_some()
+ {
+ xml.close().await?;
+ Ok(Self::SupportedCalendarComponent)
+ } else if xml.maybe_open(CAL_URN, "no-uid-conflict").await?.is_some() {
+ let href = xml.find().await?;
+ xml.close().await?;
+ Ok(Self::NoUidConflict(href))
+ } else if xml
+ .maybe_open(CAL_URN, "max-resource-size")
+ .await?
+ .is_some()
+ {
+ xml.close().await?;
+ Ok(Self::MaxResourceSize)
+ } else if xml.maybe_open(CAL_URN, "min-date-time").await?.is_some() {
+ xml.close().await?;
+ Ok(Self::MinDateTime)
+ } else if xml.maybe_open(CAL_URN, "max-date-time").await?.is_some() {
+ xml.close().await?;
+ Ok(Self::MaxDateTime)
+ } else if xml.maybe_open(CAL_URN, "max-instances").await?.is_some() {
+ xml.close().await?;
+ Ok(Self::MaxInstances)
+ } else if xml
+ .maybe_open(CAL_URN, "max-attendees-per-instance")
+ .await?
+ .is_some()
+ {
+ xml.close().await?;
+ Ok(Self::MaxAttendeesPerInstance)
+ } else if xml.maybe_open(CAL_URN, "valid-filter").await?.is_some() {
+ xml.close().await?;
+ Ok(Self::ValidFilter)
+ } else if xml.maybe_open(CAL_URN, "supported-filter").await?.is_some() {
+ let (mut comp, mut prop, mut param) = (Vec::new(), Vec::new(), Vec::new());
+ loop {
+ let mut dirty = false;
+ xml.maybe_push(&mut comp, &mut dirty).await?;
+ xml.maybe_push(&mut prop, &mut dirty).await?;
+ xml.maybe_push(&mut param, &mut dirty).await?;
+
+ if !dirty {
+ match xml.peek() {
+ Event::End(_) => break,
+ _ => xml.skip().await?,
+ };
+ }
+ }
+ xml.close().await?;
+ Ok(Self::SupportedFilter { comp, prop, param })
+ } else if xml
+ .maybe_open(CAL_URN, "number-of-matches-within-limits")
+ .await?
+ .is_some()
+ {
+ xml.close().await?;
+ Ok(Self::NumberOfMatchesWithinLimits)
+ } else {
+ Err(ParsingError::Recoverable)
+ }
+ }
+}
+
+impl QRead<Property> for Property {
+ async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> {
+ if xml
+ .maybe_open_start(CAL_URN, "calendar-home-set")
+ .await?
+ .is_some()
+ {
+ let href = xml.find().await?;
+ xml.close().await?;
+ return Ok(Property::CalendarHomeSet(href));
+ }
+ if xml
+ .maybe_open_start(CAL_URN, "calendar-description")
+ .await?
+ .is_some()
+ {
+ let lang = xml.prev_attr("xml:lang");
+ let text = xml.tag_string().await?;
+ xml.close().await?;
+ return Ok(Property::CalendarDescription { lang, text });
+ }
+
+ if xml
+ .maybe_open_start(CAL_URN, "calendar-timezone")
+ .await?
+ .is_some()
+ {
+ let tz = xml.tag_string().await?;
+ xml.close().await?;
+ return Ok(Property::CalendarTimezone(tz));
+ }
+
+ if xml
+ .maybe_open_start(CAL_URN, "supported-calendar-component-set")
+ .await?
+ .is_some()
+ {
+ let comp = xml.collect().await?;
+ xml.close().await?;
+ return Ok(Property::SupportedCalendarComponentSet(comp));
+ }
+
+ if xml
+ .maybe_open_start(CAL_URN, "supported-calendar-data")
+ .await?
+ .is_some()
+ {
+ let mime = xml.collect().await?;
+ xml.close().await?;
+ return Ok(Property::SupportedCalendarData(mime));
+ }
+
+ if xml
+ .maybe_open_start(CAL_URN, "max-resource-size")
+ .await?
+ .is_some()
+ {
+ let sz = xml.tag_string().await?.parse::<u64>()?;
+ xml.close().await?;
+ return Ok(Property::MaxResourceSize(sz));
+ }
+
+ if xml
+ .maybe_open_start(CAL_URN, "max-date-time")
+ .await?
+ .is_some()
+ {
+ let dtstr = xml.tag_string().await?;
+ let dt = NaiveDateTime::parse_from_str(dtstr.as_str(), UTC_DATETIME_FMT)?.and_utc();
+ xml.close().await?;
+ return Ok(Property::MaxDateTime(dt));
+ }
+
+ if xml
+ .maybe_open_start(CAL_URN, "max-instances")
+ .await?
+ .is_some()
+ {
+ let sz = xml.tag_string().await?.parse::<u64>()?;
+ xml.close().await?;
+ return Ok(Property::MaxInstances(sz));
+ }
+
+ if xml
+ .maybe_open_start(CAL_URN, "max-attendees-per-instance")
+ .await?
+ .is_some()
+ {
+ let sz = xml.tag_string().await?.parse::<u64>()?;
+ xml.close().await?;
+ return Ok(Property::MaxAttendeesPerInstance(sz));
+ }
+
+ if xml
+ .maybe_open_start(CAL_URN, "supported-collation-set")
+ .await?
+ .is_some()
+ {
+ let cols = xml.collect().await?;
+ xml.close().await?;
+ return Ok(Property::SupportedCollationSet(cols));
+ }
+
+ let mut dirty = false;
+ let mut caldata: Option<CalendarDataPayload> = None;
+ xml.maybe_read(&mut caldata, &mut dirty).await?;
+ if let Some(cal) = caldata {
+ return Ok(Property::CalendarData(cal));
+ }
+
+ Err(ParsingError::Recoverable)
+ }
+}
+
+impl QRead<PropertyRequest> for PropertyRequest {
+ async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> {
+ if xml
+ .maybe_open(CAL_URN, "calendar-home-set")
+ .await?
+ .is_some()
+ {
+ xml.close().await?;
+ return Ok(Self::CalendarHomeSet);
+ }
+ if xml
+ .maybe_open(CAL_URN, "calendar-description")
+ .await?
+ .is_some()
+ {
+ xml.close().await?;
+ return Ok(Self::CalendarDescription);
+ }
+ if xml
+ .maybe_open(CAL_URN, "calendar-timezone")
+ .await?
+ .is_some()
+ {
+ xml.close().await?;
+ return Ok(Self::CalendarTimezone);
+ }
+ if xml
+ .maybe_open(CAL_URN, "supported-calendar-component-set")
+ .await?
+ .is_some()
+ {
+ xml.close().await?;
+ return Ok(Self::SupportedCalendarComponentSet);
+ }
+ if xml
+ .maybe_open(CAL_URN, "supported-calendar-data")
+ .await?
+ .is_some()
+ {
+ xml.close().await?;
+ return Ok(Self::SupportedCalendarData);
+ }
+ if xml
+ .maybe_open(CAL_URN, "max-resource-size")
+ .await?
+ .is_some()
+ {
+ xml.close().await?;
+ return Ok(Self::MaxResourceSize);
+ }
+ if xml.maybe_open(CAL_URN, "min-date-time").await?.is_some() {
+ xml.close().await?;
+ return Ok(Self::MinDateTime);
+ }
+ if xml.maybe_open(CAL_URN, "max-date-time").await?.is_some() {
+ xml.close().await?;
+ return Ok(Self::MaxDateTime);
+ }
+ if xml.maybe_open(CAL_URN, "max-instances").await?.is_some() {
+ xml.close().await?;
+ return Ok(Self::MaxInstances);
+ }
+ if xml
+ .maybe_open(CAL_URN, "max-attendees-per-instance")
+ .await?
+ .is_some()
+ {
+ xml.close().await?;
+ return Ok(Self::MaxAttendeesPerInstance);
+ }
+ if xml
+ .maybe_open(CAL_URN, "supported-collation-set")
+ .await?
+ .is_some()
+ {
+ xml.close().await?;
+ return Ok(Self::SupportedCollationSet);
+ }
+ let mut dirty = false;
+ let mut m_cdr = None;
+ xml.maybe_read(&mut m_cdr, &mut dirty).await?;
+ m_cdr
+ .ok_or(ParsingError::Recoverable)
+ .map(Self::CalendarData)
+ }
+}
+
+impl QRead<ResourceType> for ResourceType {
+ async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> {
+ if xml.maybe_open(CAL_URN, "calendar").await?.is_some() {
+ xml.close().await?;
+ return Ok(Self::Calendar);
+ }
+ Err(ParsingError::Recoverable)
+ }
+}
+
+// ---- INNER XML ----
+impl QRead<SupportedCollation> for SupportedCollation {
+ async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> {
+ xml.open(CAL_URN, "supported-collation").await?;
+ let col = Collation::new(xml.tag_string().await?);
+ xml.close().await?;
+ Ok(SupportedCollation(col))
+ }
+}
+
+impl QRead<CalendarDataPayload> for CalendarDataPayload {
+ async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> {
+ xml.open(CAL_URN, "calendar-data").await?;
+ let mime = CalendarDataSupport::qread(xml).await.ok();
+ let payload = xml.tag_string().await?;
+ xml.close().await?;
+ Ok(CalendarDataPayload { mime, payload })
+ }
+}
+
+impl QRead<CalendarDataSupport> for CalendarDataSupport {
+ async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> {
+ let ct = xml.prev_attr("content-type");
+ let vs = xml.prev_attr("version");
+ match (ct, vs) {
+ (Some(content_type), Some(version)) => Ok(Self {
+ content_type,
+ version,
+ }),
+ _ => Err(ParsingError::Recoverable),
+ }
+ }
+}
+
+impl QRead<CalendarDataRequest> for CalendarDataRequest {
+ async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> {
+ xml.open(CAL_URN, "calendar-data").await?;
+ let mime = CalendarDataSupport::qread(xml).await.ok();
+ let (mut comp, mut recurrence, mut limit_freebusy_set) = (None, None, None);
+
+ if !xml.parent_has_child() {
+ return Ok(Self {
+ mime,
+ comp,
+ recurrence,
+ limit_freebusy_set,
+ });
+ }
+
+ loop {
+ let mut dirty = false;
+ xml.maybe_read(&mut comp, &mut dirty).await?;
+ xml.maybe_read(&mut recurrence, &mut dirty).await?;
+ xml.maybe_read(&mut limit_freebusy_set, &mut dirty).await?;
+
+ if !dirty {
+ match xml.peek() {
+ Event::End(_) => break,
+ _ => xml.skip().await?,
+ };
+ }
+ }
+
+ xml.close().await?;
+ Ok(Self {
+ mime,
+ comp,
+ recurrence,
+ limit_freebusy_set,
+ })
+ }
+}
+
+impl QRead<CalendarDataEmpty> for CalendarDataEmpty {
+ async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> {
+ xml.open(CAL_URN, "calendar-data").await?;
+ let mime = CalendarDataSupport::qread(xml).await.ok();
+ xml.close().await?;
+ Ok(Self(mime))
+ }
+}
+
+impl QRead<Comp> for Comp {
+ async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> {
+ let (mut prop_kind, mut comp_kind) = (None, None);
+
+ let bs = xml.open(CAL_URN, "comp").await?;
+ let name = Component::new(
+ xml.prev_attr("name")
+ .ok_or(ParsingError::MissingAttribute)?,
+ );
+
+ // Return early if it's an empty tag
+ if matches!(bs, Event::Empty(_)) {
+ xml.close().await?;
+ return Ok(Self {
+ name,
+ prop_kind,
+ comp_kind,
+ });
+ }
+
+ loop {
+ let mut dirty = false;
+ let (mut tmp_prop_kind, mut tmp_comp_kind): (Option<PropKind>, Option<CompKind>) =
+ (None, None);
+
+ xml.maybe_read(&mut tmp_prop_kind, &mut dirty).await?;
+ Box::pin(xml.maybe_read(&mut tmp_comp_kind, &mut dirty)).await?;
+
+ //@FIXME hack
+ // Merge
+ match (tmp_prop_kind, &mut prop_kind) {
+ (Some(PropKind::Prop(mut a)), Some(PropKind::Prop(ref mut b))) => b.append(&mut a),
+ (Some(PropKind::AllProp), v) => *v = Some(PropKind::AllProp),
+ (Some(x), b) => *b = Some(x),
+ (None, _) => (),
+ };
+ match (tmp_comp_kind, &mut comp_kind) {
+ (Some(CompKind::Comp(mut a)), Some(CompKind::Comp(ref mut b))) => b.append(&mut a),
+ (Some(CompKind::AllComp), v) => *v = Some(CompKind::AllComp),
+ (Some(a), b) => *b = Some(a),
+ (None, _) => (),
+ };
+
+ if !dirty {
+ match xml.peek() {
+ Event::End(_) => break,
+ _ => xml.skip().await?,
+ };
+ }
+ }
+
+ xml.close().await?;
+ Ok(Self {
+ name,
+ prop_kind,
+ comp_kind,
+ })
+ }
+}
+
+impl QRead<CompSupport> for CompSupport {
+ async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> {
+ xml.open(CAL_URN, "comp").await?;
+ let inner = Component::new(
+ xml.prev_attr("name")
+ .ok_or(ParsingError::MissingAttribute)?,
+ );
+ xml.close().await?;
+ Ok(Self(inner))
+ }
+}
+
+impl QRead<CompKind> for CompKind {
+ async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> {
+ let mut comp = Vec::new();
+ loop {
+ let mut dirty = false;
+
+ if xml.maybe_open(CAL_URN, "allcomp").await?.is_some() {
+ xml.close().await?;
+ return Ok(CompKind::AllComp);
+ }
+
+ xml.maybe_push(&mut comp, &mut dirty).await?;
+
+ if !dirty {
+ break;
+ }
+ }
+ match &comp[..] {
+ [] => Err(ParsingError::Recoverable),
+ _ => Ok(CompKind::Comp(comp)),
+ }
+ }
+}
+
+impl QRead<PropKind> for PropKind {
+ async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> {
+ let mut prop = Vec::new();
+ loop {
+ let mut dirty = false;
+
+ if xml.maybe_open(CAL_URN, "allprop").await?.is_some() {
+ xml.close().await?;
+ return Ok(PropKind::AllProp);
+ }
+
+ xml.maybe_push(&mut prop, &mut dirty).await?;
+
+ if !dirty {
+ break;
+ }
+ }
+
+ match &prop[..] {
+ [] => Err(ParsingError::Recoverable),
+ _ => Ok(PropKind::Prop(prop)),
+ }
+ }
+}
+
+impl QRead<RecurrenceModifier> for RecurrenceModifier {
+ async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> {
+ match Expand::qread(xml).await {
+ Err(ParsingError::Recoverable) => (),
+ otherwise => return otherwise.map(RecurrenceModifier::Expand),
+ }
+ LimitRecurrenceSet::qread(xml)
+ .await
+ .map(RecurrenceModifier::LimitRecurrenceSet)
+ }
+}
+
+impl QRead<Expand> for Expand {
+ async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> {
+ xml.open(CAL_URN, "expand").await?;
+ let (rstart, rend) = match (xml.prev_attr("start"), xml.prev_attr("end")) {
+ (Some(start), Some(end)) => (start, end),
+ _ => return Err(ParsingError::MissingAttribute),
+ };
+
+ let start = NaiveDateTime::parse_from_str(rstart.as_str(), UTC_DATETIME_FMT)?.and_utc();
+ let end = NaiveDateTime::parse_from_str(rend.as_str(), UTC_DATETIME_FMT)?.and_utc();
+ if start > end {
+ return Err(ParsingError::InvalidValue);
+ }
+
+ xml.close().await?;
+ Ok(Expand(start, end))
+ }
+}
+
+impl QRead<LimitRecurrenceSet> for LimitRecurrenceSet {
+ async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> {
+ xml.open(CAL_URN, "limit-recurrence-set").await?;
+ let (rstart, rend) = match (xml.prev_attr("start"), xml.prev_attr("end")) {
+ (Some(start), Some(end)) => (start, end),
+ _ => return Err(ParsingError::MissingAttribute),
+ };
+
+ let start = NaiveDateTime::parse_from_str(rstart.as_str(), UTC_DATETIME_FMT)?.and_utc();
+ let end = NaiveDateTime::parse_from_str(rend.as_str(), UTC_DATETIME_FMT)?.and_utc();
+ if start > end {
+ return Err(ParsingError::InvalidValue);
+ }
+
+ xml.close().await?;
+ Ok(LimitRecurrenceSet(start, end))
+ }
+}
+
+impl QRead<LimitFreebusySet> for LimitFreebusySet {
+ async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> {
+ xml.open(CAL_URN, "limit-freebusy-set").await?;
+ let (rstart, rend) = match (xml.prev_attr("start"), xml.prev_attr("end")) {
+ (Some(start), Some(end)) => (start, end),
+ _ => return Err(ParsingError::MissingAttribute),
+ };
+
+ let start = NaiveDateTime::parse_from_str(rstart.as_str(), UTC_DATETIME_FMT)?.and_utc();
+ let end = NaiveDateTime::parse_from_str(rend.as_str(), UTC_DATETIME_FMT)?.and_utc();
+ if start > end {
+ return Err(ParsingError::InvalidValue);
+ }
+
+ xml.close().await?;
+ Ok(LimitFreebusySet(start, end))
+ }
+}
+
+impl<E: dav::Extension> QRead<CalendarSelector<E>> for CalendarSelector<E> {
+ async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> {
+ // allprop
+ if let Some(_) = xml.maybe_open(DAV_URN, "allprop").await? {
+ xml.close().await?;
+ return Ok(Self::AllProp);
+ }
+
+ // propname
+ if let Some(_) = xml.maybe_open(DAV_URN, "propname").await? {
+ xml.close().await?;
+ return Ok(Self::PropName);
+ }
+
+ // prop
+ let (mut maybe_prop, mut dirty) = (None, false);
+ xml.maybe_read::<dav::PropName<E>>(&mut maybe_prop, &mut dirty)
+ .await?;
+ if let Some(prop) = maybe_prop {
+ return Ok(Self::Prop(prop));
+ }
+
+ Err(ParsingError::Recoverable)
+ }
+}
+
+impl QRead<CompFilter> for CompFilter {
+ async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> {
+ xml.open(CAL_URN, "comp-filter").await?;
+ let name = Component::new(
+ xml.prev_attr("name")
+ .ok_or(ParsingError::MissingAttribute)?,
+ );
+ let additional_rules = Box::pin(xml.maybe_find()).await?;
+ xml.close().await?;
+ Ok(Self {
+ name,
+ additional_rules,
+ })
+ }
+}
+
+impl QRead<CompFilterRules> for CompFilterRules {
+ async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> {
+ let mut time_range = None;
+ let mut prop_filter = Vec::new();
+ let mut comp_filter = Vec::new();
+
+ loop {
+ let mut dirty = false;
+
+ if xml.maybe_open(CAL_URN, "is-not-defined").await?.is_some() {
+ xml.close().await?;
+ return Ok(Self::IsNotDefined);
+ }
+
+ xml.maybe_read(&mut time_range, &mut dirty).await?;
+ xml.maybe_push(&mut prop_filter, &mut dirty).await?;
+ xml.maybe_push(&mut comp_filter, &mut dirty).await?;
+
+ if !dirty {
+ match xml.peek() {
+ Event::End(_) => break,
+ _ => xml.skip().await?,
+ };
+ }
+ }
+
+ match (&time_range, &prop_filter[..], &comp_filter[..]) {
+ (None, [], []) => Err(ParsingError::Recoverable),
+ _ => Ok(Self::Matches(CompFilterMatch {
+ time_range,
+ prop_filter,
+ comp_filter,
+ })),
+ }
+ }
+}
+
+impl QRead<CompFilterMatch> for CompFilterMatch {
+ async fn qread(_xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> {
+ unreachable!();
+ }
+}
+
+impl QRead<PropFilter> for PropFilter {
+ async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> {
+ xml.open(CAL_URN, "prop-filter").await?;
+ let name = ComponentProperty(
+ xml.prev_attr("name")
+ .ok_or(ParsingError::MissingAttribute)?,
+ );
+ let additional_rules = xml.maybe_find().await?;
+ xml.close().await?;
+ Ok(Self {
+ name,
+ additional_rules,
+ })
+ }
+}
+
+impl QRead<PropFilterRules> for PropFilterRules {
+ async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> {
+ let mut time_or_text = None;
+ let mut param_filter = Vec::new();
+
+ loop {
+ let mut dirty = false;
+
+ if xml.maybe_open(CAL_URN, "is-not-defined").await?.is_some() {
+ xml.close().await?;
+ return Ok(Self::IsNotDefined);
+ }
+
+ xml.maybe_read(&mut time_or_text, &mut dirty).await?;
+ xml.maybe_push(&mut param_filter, &mut dirty).await?;
+
+ if !dirty {
+ match xml.peek() {
+ Event::End(_) => break,
+ _ => xml.skip().await?,
+ };
+ }
+ }
+
+ match (&time_or_text, &param_filter[..]) {
+ (None, []) => Err(ParsingError::Recoverable),
+ _ => Ok(PropFilterRules::Match(PropFilterMatch {
+ time_or_text,
+ param_filter,
+ })),
+ }
+ }
+}
+
+impl QRead<PropFilterMatch> for PropFilterMatch {
+ async fn qread(_xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> {
+ unreachable!();
+ }
+}
+
+impl QRead<ParamFilter> for ParamFilter {
+ async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> {
+ xml.open(CAL_URN, "param-filter").await?;
+ let name = PropertyParameter(
+ xml.prev_attr("name")
+ .ok_or(ParsingError::MissingAttribute)?,
+ );
+ let additional_rules = xml.maybe_find().await?;
+ xml.close().await?;
+ Ok(Self {
+ name,
+ additional_rules,
+ })
+ }
+}
+
+impl QRead<TimeOrText> for TimeOrText {
+ async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> {
+ match TimeRange::qread(xml).await {
+ Err(ParsingError::Recoverable) => (),
+ otherwise => return otherwise.map(Self::Time),
+ }
+ TextMatch::qread(xml).await.map(Self::Text)
+ }
+}
+
+impl QRead<TextMatch> for TextMatch {
+ async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> {
+ xml.open(CAL_URN, "text-match").await?;
+ let collation = xml.prev_attr("collation").map(Collation::new);
+ let negate_condition = xml.prev_attr("negate-condition").map(|v| v == "yes");
+ let text = xml.tag_string().await?;
+ xml.close().await?;
+ Ok(Self {
+ collation,
+ negate_condition,
+ text,
+ })
+ }
+}
+
+impl QRead<ParamFilterMatch> for ParamFilterMatch {
+ async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> {
+ if xml.maybe_open(CAL_URN, "is-not-defined").await?.is_some() {
+ xml.close().await?;
+ return Ok(Self::IsNotDefined);
+ }
+ TextMatch::qread(xml).await.map(Self::Match)
+ }
+}
+
+impl QRead<TimeZone> for TimeZone {
+ async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> {
+ xml.open(CAL_URN, "timezone").await?;
+ let inner = xml.tag_string().await?;
+ xml.close().await?;
+ Ok(Self(inner))
+ }
+}
+
+impl QRead<Filter> for Filter {
+ async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> {
+ xml.open(CAL_URN, "filter").await?;
+ let comp_filter = xml.find().await?;
+ xml.close().await?;
+ Ok(Self(comp_filter))
+ }
+}
+
+impl QRead<TimeRange> for TimeRange {
+ async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> {
+ xml.open(CAL_URN, "time-range").await?;
+
+ let start = match xml.prev_attr("start") {
+ Some(r) => Some(NaiveDateTime::parse_from_str(r.as_str(), UTC_DATETIME_FMT)?.and_utc()),
+ _ => None,
+ };
+ let end = match xml.prev_attr("end") {
+ Some(r) => Some(NaiveDateTime::parse_from_str(r.as_str(), UTC_DATETIME_FMT)?.and_utc()),
+ _ => None,
+ };
+
+ xml.close().await?;
+
+ match (start, end) {
+ (Some(start), Some(end)) => {
+ if start > end {
+ return Err(ParsingError::InvalidValue);
+ }
+ Ok(TimeRange::FullRange(start, end))
+ }
+ (Some(start), None) => Ok(TimeRange::OnlyStart(start)),
+ (None, Some(end)) => Ok(TimeRange::OnlyEnd(end)),
+ (None, None) => Err(ParsingError::MissingAttribute),
+ }
+ }
+}
+
+impl QRead<CalProp> for CalProp {
+ async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> {
+ xml.open(CAL_URN, "prop").await?;
+ let name = ComponentProperty(
+ xml.prev_attr("name")
+ .ok_or(ParsingError::MissingAttribute)?,
+ );
+ let novalue = xml.prev_attr("novalue").map(|v| v == "yes");
+ xml.close().await?;
+ Ok(Self { name, novalue })
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use crate::realization::Calendar;
+ use crate::xml::Node;
+ use chrono::{TimeZone, Utc};
+ //use quick_reader::NsReader;
+
+ async fn deserialize<T: Node<T>>(src: &str) -> T {
+ let mut rdr = Reader::new(quick_xml::NsReader::from_reader(src.as_bytes()))
+ .await
+ .unwrap();
+ rdr.find().await.unwrap()
+ }
+
+ #[tokio::test]
+ async fn simple_comp_filter() {
+ let expected = CompFilter {
+ name: Component::VEvent,
+ additional_rules: None,
+ };
+ let src = r#"<C:comp-filter name="VEVENT" xmlns:C="urn:ietf:params:xml:ns:caldav" />"#;
+ let got = deserialize::<CompFilter>(src).await;
+ assert_eq!(got, expected);
+ }
+
+ #[tokio::test]
+ async fn basic_mkcalendar() {
+ let expected = MkCalendar(dav::Set(dav::PropValue(vec![dav::Property::DisplayName(
+ "Lisa's Events".into(),
+ )])));
+
+ let src = r#"
+<?xml version="1.0" encoding="utf-8" ?>
+<C:mkcalendar xmlns:D="DAV:"
+ xmlns:C="urn:ietf:params:xml:ns:caldav">
+ <D:set>
+ <D:prop>
+ <D:displayname>Lisa's Events</D:displayname>
+ </D:prop>
+ </D:set>
+ </C:mkcalendar>
+"#;
+ let got = deserialize::<MkCalendar<Calendar>>(src).await;
+ assert_eq!(got, expected)
+ }
+
+ #[tokio::test]
+ async fn rfc_mkcalendar() {
+ let expected = MkCalendar(dav::Set(dav::PropValue(vec![
+ dav::Property::DisplayName("Lisa's Events".into()),
+ dav::Property::Extension(Property::CalendarDescription {
+ lang: Some("en".into()),
+ text: "Calendar restricted to events.".into(),
+ }),
+ dav::Property::Extension(Property::SupportedCalendarComponentSet(vec![
+ CompSupport(Component::VEvent)
+ ])),
+ dav::Property::Extension(Property::CalendarTimezone("BEGIN:VCALENDAR\nPRODID:-//Example Corp.//CalDAV Client//EN\nVERSION:2.0\nEND:VCALENDAR".into())),
+ ])));
+
+ let src = r#"
+ <?xml version="1.0" encoding="utf-8" ?>
+ <C:mkcalendar xmlns:D="DAV:"
+ xmlns:C="urn:ietf:params:xml:ns:caldav">
+ <D:set>
+ <D:prop>
+ <D:displayname>Lisa's Events</D:displayname>
+ <C:calendar-description xml:lang="en"
+ >Calendar restricted to events.</C:calendar-description>
+ <C:supported-calendar-component-set>
+ <C:comp name="VEVENT"/>
+ </C:supported-calendar-component-set>
+ <C:calendar-timezone><![CDATA[BEGIN:VCALENDAR
+PRODID:-//Example Corp.//CalDAV Client//EN
+VERSION:2.0
+END:VCALENDAR]]></C:calendar-timezone>
+ </D:prop>
+ </D:set>
+ </C:mkcalendar>"#;
+
+ let got = deserialize::<MkCalendar<Calendar>>(src).await;
+ assert_eq!(got, expected)
+ }
+
+ #[tokio::test]
+ async fn rfc_calendar_query() {
+ let expected = CalendarQuery {
+ selector: Some(CalendarSelector::Prop(dav::PropName(vec![
+ dav::PropertyRequest::GetEtag,
+ dav::PropertyRequest::Extension(PropertyRequest::CalendarData(
+ CalendarDataRequest {
+ mime: None,
+ comp: Some(Comp {
+ name: Component::VCalendar,
+ prop_kind: Some(PropKind::Prop(vec![CalProp {
+ name: ComponentProperty("VERSION".into()),
+ novalue: None,
+ }])),
+ comp_kind: Some(CompKind::Comp(vec![
+ Comp {
+ name: Component::VEvent,
+ prop_kind: Some(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: None,
+ },
+ Comp {
+ name: Component::VTimeZone,
+ prop_kind: None,
+ comp_kind: None,
+ },
+ ])),
+ }),
+ recurrence: None,
+ limit_freebusy_set: None,
+ },
+ )),
+ ]))),
+ filter: Filter(CompFilter {
+ name: Component::VCalendar,
+ additional_rules: Some(CompFilterRules::Matches(CompFilterMatch {
+ prop_filter: vec![],
+ comp_filter: vec![CompFilter {
+ name: Component::VEvent,
+ additional_rules: Some(CompFilterRules::Matches(CompFilterMatch {
+ prop_filter: vec![],
+ comp_filter: vec![],
+ 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(),
+ )),
+ })),
+ }],
+ time_range: None,
+ })),
+ }),
+ timezone: None,
+ };
+
+ let src = r#"
+<?xml version="1.0" encoding="utf-8" ?>
+<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>
+"#;
+
+ let got = deserialize::<CalendarQuery<Calendar>>(src).await;
+ assert_eq!(got, expected)
+ }
+
+ #[tokio::test]
+ async fn rfc_calendar_query_res() {
+ let expected = dav::Multistatus::<Calendar> {
+ extension: None,
+ 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::AnyProp(vec![
+ dav::AnyProperty::Value(dav::Property::GetEtag(
+ "\"fffff-abcd2\"".into(),
+ )),
+ dav::AnyProperty::Value(dav::Property::Extension(
+ Property::CalendarData(CalendarDataPayload {
+ mime: None,
+ payload: "BEGIN:VCALENDAR".into(),
+ }),
+ )),
+ ]),
+ status: dav::Status(http::status::StatusCode::OK),
+ error: None,
+ responsedescription: None,
+ }],
+ ),
+ error: None,
+ location: 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::AnyProp(vec![
+ dav::AnyProperty::Value(dav::Property::GetEtag(
+ "\"fffff-abcd3\"".into(),
+ )),
+ dav::AnyProperty::Value(dav::Property::Extension(
+ Property::CalendarData(CalendarDataPayload {
+ mime: None,
+ payload: "BEGIN:VCALENDAR".into(),
+ }),
+ )),
+ ]),
+ status: dav::Status(http::status::StatusCode::OK),
+ error: None,
+ responsedescription: None,
+ }],
+ ),
+ error: None,
+ location: None,
+ responsedescription: None,
+ },
+ ],
+ responsedescription: None,
+ };
+
+ let src = r#"<D:multistatus xmlns:D="DAV:"
+ xmlns:C="urn:ietf:params:xml:ns:caldav">
+ <D:response>
+ <D:href>http://cal.example.com/bernard/work/abcd2.ics</D:href>
+ <D:propstat>
+ <D:prop>
+ <D:getetag>"fffff-abcd2"</D:getetag>
+ <C:calendar-data>BEGIN:VCALENDAR</C:calendar-data>
+ </D:prop>
+ <D:status>HTTP/1.1 200 OK</D:status>
+ </D:propstat>
+ </D:response>
+ <D:response>
+ <D:href>http://cal.example.com/bernard/work/abcd3.ics</D:href>
+ <D:propstat>
+ <D:prop>
+ <D:getetag>"fffff-abcd3"</D:getetag>
+ <C:calendar-data>BEGIN:VCALENDAR</C:calendar-data>
+ </D:prop>
+ <D:status>HTTP/1.1 200 OK</D:status>
+ </D:propstat>
+ </D:response>
+ </D:multistatus>
+"#;
+
+ let got = deserialize::<dav::Multistatus<Calendar>>(src).await;
+ assert_eq!(got, expected)
+ }
+
+ #[tokio::test]
+ async fn rfc_recurring_evt() {
+ let expected = CalendarQuery::<Calendar> {
+ selector: Some(CalendarSelector::Prop(dav::PropName(vec![
+ dav::PropertyRequest::Extension(PropertyRequest::CalendarData(
+ CalendarDataRequest {
+ mime: None,
+ comp: None,
+ recurrence: Some(RecurrenceModifier::LimitRecurrenceSet(
+ LimitRecurrenceSet(
+ Utc.with_ymd_and_hms(2006, 1, 3, 0, 0, 0).unwrap(),
+ Utc.with_ymd_and_hms(2006, 1, 5, 0, 0, 0).unwrap(),
+ ),
+ )),
+ limit_freebusy_set: None,
+ },
+ )),
+ ]))),
+ filter: Filter(CompFilter {
+ name: Component::VCalendar,
+ additional_rules: Some(CompFilterRules::Matches(CompFilterMatch {
+ prop_filter: vec![],
+ comp_filter: vec![CompFilter {
+ name: Component::VEvent,
+ additional_rules: Some(CompFilterRules::Matches(CompFilterMatch {
+ prop_filter: vec![],
+ comp_filter: vec![],
+ time_range: Some(TimeRange::FullRange(
+ Utc.with_ymd_and_hms(2006, 1, 3, 0, 0, 0).unwrap(),
+ Utc.with_ymd_and_hms(2006, 1, 5, 0, 0, 0).unwrap(),
+ )),
+ })),
+ }],
+ time_range: None,
+ })),
+ }),
+ timezone: None,
+ };
+
+ let src = r#"
+ <?xml version="1.0" encoding="utf-8" ?>
+ <C:calendar-query xmlns:D="DAV:"
+ xmlns:C="urn:ietf:params:xml:ns:caldav">
+ <D:prop>
+ <C:calendar-data>
+ <C:limit-recurrence-set start="20060103T000000Z"
+ end="20060105T000000Z"/>
+ </C:calendar-data>
+ </D:prop>
+ <C:filter>
+ <C:comp-filter name="VCALENDAR">
+ <C:comp-filter name="VEVENT">
+ <C:time-range start="20060103T000000Z"
+ end="20060105T000000Z"/>
+ </C:comp-filter>
+ </C:comp-filter>
+ </C:filter>
+ </C:calendar-query>"#;
+
+ let got = deserialize::<CalendarQuery<Calendar>>(src).await;
+ assert_eq!(got, expected)
+ }
+
+ #[tokio::test]
+ async fn rfc_pending_todos() {
+ let expected = CalendarQuery::<Calendar> {
+ selector: Some(CalendarSelector::Prop(dav::PropName(vec![
+ dav::PropertyRequest::GetEtag,
+ dav::PropertyRequest::Extension(PropertyRequest::CalendarData(
+ CalendarDataRequest {
+ mime: None,
+ comp: 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::VTodo,
+ additional_rules: Some(CompFilterRules::Matches(CompFilterMatch {
+ time_range: None,
+ comp_filter: vec![],
+ prop_filter: vec![
+ PropFilter {
+ name: ComponentProperty("COMPLETED".into()),
+ additional_rules: Some(PropFilterRules::IsNotDefined),
+ },
+ PropFilter {
+ name: ComponentProperty("STATUS".into()),
+ additional_rules: Some(PropFilterRules::Match(
+ PropFilterMatch {
+ param_filter: vec![],
+ time_or_text: Some(TimeOrText::Text(TextMatch {
+ collation: None,
+ negate_condition: Some(true),
+ text: "CANCELLED".into(),
+ })),
+ },
+ )),
+ },
+ ],
+ })),
+ }],
+ })),
+ }),
+ timezone: None,
+ };
+
+ let src = r#"<?xml version="1.0" encoding="utf-8" ?>
+ <C:calendar-query xmlns:C="urn:ietf:params:xml:ns:caldav">
+ <D:prop xmlns:D="DAV:">
+ <D:getetag/>
+ <C:calendar-data/>
+ </D:prop>
+ <C:filter>
+ <C:comp-filter name="VCALENDAR">
+ <C:comp-filter name="VTODO">
+ <C:prop-filter name="COMPLETED">
+ <C:is-not-defined/>
+ </C:prop-filter>
+ <C:prop-filter name="STATUS">
+ <C:text-match
+ negate-condition="yes">CANCELLED</C:text-match>
+ </C:prop-filter>
+ </C:comp-filter>
+ </C:comp-filter>
+ </C:filter>
+ </C:calendar-query>"#;
+
+ let got = deserialize::<CalendarQuery<Calendar>>(src).await;
+ assert_eq!(got, expected)
+ }
+}
diff --git a/aero-dav/src/calencoder.rs b/aero-dav/src/calencoder.rs
new file mode 100644
index 0000000..15df965
--- /dev/null
+++ b/aero-dav/src/calencoder.rs
@@ -0,0 +1,1036 @@
+use quick_xml::events::{BytesText, Event};
+use quick_xml::Error as QError;
+
+use super::caltypes::*;
+use super::types::Extension;
+use super::xml::{IWrite, Node, QWrite, Writer};
+
+// ==================== 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> QWrite for MkCalendarResponse<E> {
+ 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 QWrite for ReportTypeName {
+ async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> {
+ match self {
+ Self::Query => {
+ let start = xml.create_dav_element("calendar-query");
+ xml.q.write_event_async(Event::Empty(start)).await
+ }
+ Self::Multiget => {
+ let start = xml.create_dav_element("calendar-multiget");
+ xml.q.write_event_async(Event::Empty(start)).await
+ }
+ Self::FreeBusy => {
+ let start = xml.create_dav_element("free-busy-query");
+ xml.q.write_event_async(Event::Empty(start)).await
+ }
+ }
+ }
+}
+
+impl<E: Extension> QWrite for ReportType<E> {
+ async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> {
+ match self {
+ Self::Query(v) => v.qwrite(xml).await,
+ Self::Multiget(v) => v.qwrite(xml).await,
+ Self::FreeBusy(v) => v.qwrite(xml).await,
+ }
+ }
+}
+
+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::CalendarHomeSet => atom("calendar-home-set").await,
+ 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::CalendarHomeSet(href) => {
+ let start = xml.create_cal_element("calendar-home-set");
+ 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::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(UTC_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(UTC_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_cal_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 CalendarDataSupport {
+ async fn qwrite(&self, _xml: &mut Writer<impl IWrite>) -> Result<(), QError> {
+ unreachable!();
+ }
+}
+
+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()));
+ }
+
+ // Empty tag
+ if self.comp.is_none() && self.recurrence.is_none() && self.limit_freebusy_set.is_none() {
+ return xml.q.write_event_async(Event::Empty(start.clone())).await;
+ }
+
+ 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.prop_kind, &self.comp_kind) {
+ (None, None) => xml.q.write_event_async(Event::Empty(start)).await,
+ _ => {
+ let end = start.to_end();
+ xml.q.write_event_async(Event::Start(start.clone())).await?;
+ if let Some(prop_kind) = &self.prop_kind {
+ prop_kind.qwrite(xml).await?;
+ }
+ if let Some(comp_kind) = &self.comp_kind {
+ 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
+ // Cycle detected when computing type of ...
+ // For more information about this error, try `rustc --explain E0391`.
+ // https://github.com/rust-lang/rust/issues/78649
+ #[inline(always)]
+ fn recurse<'a>(
+ comp: &'a Comp,
+ xml: &'a mut Writer<impl IWrite>,
+ ) -> futures::future::BoxFuture<'a, Result<(), QError>> {
+ Box::pin(comp.qwrite(xml))
+ }
+ recurse(comp, 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(UTC_DATETIME_FMT)).as_str(),
+ ));
+ empty.push_attribute((
+ "end",
+ format!("{}", self.1.format(UTC_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(UTC_DATETIME_FMT)).as_str(),
+ ));
+ empty.push_attribute((
+ "end",
+ format!("{}", self.1.format(UTC_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(UTC_DATETIME_FMT)).as_str(),
+ ));
+ empty.push_attribute((
+ "end",
+ format!("{}", self.1.format(UTC_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
+ // Cycle detected when computing type of ...
+ // For more information about this error, try `rustc --explain E0391`.
+ // https://github.com/rust-lang/rust/issues/78649
+ #[inline(always)]
+ fn recurse<'a>(
+ comp: &'a CompFilter,
+ xml: &'a mut Writer<impl IWrite>,
+ ) -> futures::future::BoxFuture<'a, Result<(), QError>> {
+ Box::pin(comp.qwrite(xml))
+ }
+ recurse(comp_item, 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.0.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_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 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 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(UTC_DATETIME_FMT)).as_str(),
+ )),
+ Self::OnlyEnd(end) => {
+ empty.push_attribute(("end", format!("{}", end.format(UTC_DATETIME_FMT)).as_str()))
+ }
+ Self::FullRange(start, end) => {
+ empty.push_attribute((
+ "start",
+ format!("{}", start.format(UTC_DATETIME_FMT)).as_str(),
+ ));
+ empty.push_attribute(("end", format!("{}", end.format(UTC_DATETIME_FMT)).as_str()));
+ }
+ }
+ xml.q.write_event_async(Event::Empty(empty)).await
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use crate::realization::Calendar;
+ use crate::types as dav;
+ use chrono::{TimeZone, Utc};
+ use tokio::io::AsyncWriteExt;
+
+ 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,
+ prop_kind: Some(PropKind::Prop(vec![CalProp {
+ name: ComponentProperty("VERSION".into()),
+ novalue: None,
+ }])),
+ comp_kind: Some(CompKind::Comp(vec![
+ Comp {
+ name: Component::VEvent,
+ prop_kind: Some(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: None,
+ },
+ Comp {
+ name: Component::VTimeZone,
+ prop_kind: None,
+ comp_kind: 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> {
+ extension: None,
+ 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::AnyProp(vec![
+ dav::AnyProperty::Value(dav::Property::GetEtag(
+ "\"fffff-abcd2\"".into(),
+ )),
+ dav::AnyProperty::Value(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::AnyProp(vec![
+ dav::AnyProperty::Value(dav::Property::GetEtag(
+ "\"fffff-abcd3\"".into(),
+ )),
+ dav::AnyProperty::Value(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..a4f6fef
--- /dev/null
+++ b/aero-dav/src/caltypes.rs
@@ -0,0 +1,1500 @@
+#![allow(dead_code)]
+
+use super::types as dav;
+use chrono::{DateTime, Utc};
+
+pub const FLOATING_DATETIME_FMT: &str = "%Y%m%dT%H%M%S";
+pub const UTC_DATETIME_FMT: &str = "%Y%m%dT%H%M%SZ";
+
+//@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, Clone)]
+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, Clone)]
+pub struct MkCalendarResponse<E: dav::Extension>(pub Vec<dav::PropStat<E>>);
+
+// --- (REPORT PART) ---
+#[derive(Debug, PartialEq, Clone)]
+pub enum ReportTypeName {
+ Query,
+ Multiget,
+ FreeBusy,
+}
+
+#[derive(Debug, PartialEq, Clone)]
+pub enum ReportType<E: dav::Extension> {
+ Query(CalendarQuery<E>),
+ Multiget(CalendarMultiget<E>),
+ FreeBusy(FreeBusyQuery),
+}
+
+/// 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, Clone)]
+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, Clone)]
+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, Clone)]
+pub struct FreeBusyQuery(pub TimeRange);
+
+// ----- Hooks -----
+#[derive(Debug, PartialEq, Clone)]
+pub enum ResourceType {
+ Calendar,
+}
+
+/// Check the matching Property object for documentation
+#[derive(Debug, PartialEq, Clone)]
+pub enum PropertyRequest {
+ CalendarHomeSet,
+ CalendarDescription,
+ CalendarTimezone,
+ SupportedCalendarComponentSet,
+ SupportedCalendarData,
+ MaxResourceSize,
+ MinDateTime,
+ MaxDateTime,
+ MaxInstances,
+ MaxAttendeesPerInstance,
+ SupportedCollationSet,
+ CalendarData(CalendarDataRequest),
+}
+
+#[derive(Debug, PartialEq, Clone)]
+pub enum Property {
+ /// Name: calendar-home-set
+ ///
+ /// Namespace: urn:ietf:params:xml:ns:caldav
+ ///
+ /// Purpose: Identifies the URL of any WebDAV collections that contain
+ /// calendar collections owned by the associated principal resource.
+ ///
+ /// Conformance: This property SHOULD be defined on a principal
+ /// resource. 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]).
+ ///
+ /// Description: The CALDAV:calendar-home-set property is meant to allow
+ /// users to easily find the calendar collections owned by the
+ /// principal. Typically, users will group all the calendar
+ /// collections that they own under a common collection. This
+ /// property specifies the URL of collections that are either calendar
+ /// collections or ordinary collections that have child or descendant
+ /// calendar collections owned by the principal.
+ ///
+ /// Definition:
+ ///
+ /// <!ELEMENT calendar-home-set (DAV:href*)>
+ CalendarHomeSet(dav::Href),
+
+ /// 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, Clone)]
+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, Clone)]
+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, Clone)]
+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, Clone, Default)]
+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, Clone)]
+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, Clone)]
+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, Clone)]
+pub struct Comp {
+ pub name: Component,
+ pub prop_kind: Option<PropKind>,
+ pub comp_kind: Option<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, Clone)]
+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, Clone)]
+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, Clone)]
+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, Clone)]
+pub struct CalProp {
+ pub name: ComponentProperty,
+ pub novalue: Option<bool>,
+}
+
+#[derive(Debug, PartialEq, Clone)]
+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, Clone)]
+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, Clone)]
+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, Clone)]
+pub struct LimitFreebusySet(pub DateTime<Utc>, pub DateTime<Utc>);
+
+/// Used by CalendarQuery & CalendarMultiget
+#[derive(Debug, PartialEq, Clone)]
+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, Clone)]
+pub struct CompFilter {
+ pub name: Component,
+ // Option 1 = None, Option 2, 3, 4 = Some
+ pub additional_rules: Option<CompFilterRules>,
+}
+#[derive(Debug, PartialEq, Clone)]
+pub enum CompFilterRules {
+ // Option 2
+ IsNotDefined,
+ // Options 3 & 4
+ Matches(CompFilterMatch),
+}
+#[derive(Debug, PartialEq, Clone)]
+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, Clone)]
+pub struct PropFilter {
+ pub name: ComponentProperty,
+ // None = Option 1, Some() = Option 2, 3 & 4
+ pub additional_rules: Option<PropFilterRules>,
+}
+#[derive(Debug, PartialEq, Clone)]
+pub enum PropFilterRules {
+ // Option 2
+ IsNotDefined,
+ // Options 3 & 4
+ Match(PropFilterMatch),
+}
+#[derive(Debug, PartialEq, Clone)]
+pub struct PropFilterMatch {
+ pub time_or_text: Option<TimeOrText>,
+ pub param_filter: Vec<ParamFilter>,
+}
+#[derive(Debug, PartialEq, Clone)]
+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, Clone)]
+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, Clone)]
+pub struct ParamFilter {
+ pub name: PropertyParameter,
+ pub additional_rules: Option<ParamFilterMatch>,
+}
+#[derive(Debug, PartialEq, Clone)]
+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, Clone)]
+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, Clone)]
+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, Clone)]
+pub enum TimeRange {
+ OnlyStart(DateTime<Utc>),
+ OnlyEnd(DateTime<Utc>),
+ FullRange(DateTime<Utc>, DateTime<Utc>),
+}
+
+// ----------------------- ENUM ATTRIBUTES ---------------------
+
+/// Known components
+#[derive(Debug, PartialEq, Clone)]
+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,
+ }
+ }
+ pub fn new(v: String) -> Self {
+ match v.as_str() {
+ "VCALENDAR" => Self::VCalendar,
+ "VJOURNAL" => Self::VJournal,
+ "VFREEBUSY" => Self::VFreeBusy,
+ "VEVENT" => Self::VEvent,
+ "VTODO" => Self::VTodo,
+ "VALARM" => Self::VAlarm,
+ "VTIMEZONE" => Self::VTimeZone,
+ _ => Self::Unknown(v),
+ }
+ }
+}
+
+/// name="VERSION", name="SUMMARY", etc.
+/// Can be set on different objects: VCalendar, VEvent, etc.
+/// Might be replaced by an enum later
+#[derive(Debug, PartialEq, Clone)]
+pub struct ComponentProperty(pub String);
+
+/// like PARSTAT
+#[derive(Debug, PartialEq, Clone)]
+pub struct PropertyParameter(pub String);
+impl PropertyParameter {
+ pub fn as_str<'a>(&'a self) -> &'a str {
+ self.0.as_str()
+ }
+}
+
+#[derive(Default, Debug, PartialEq, Clone)]
+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(),
+ }
+ }
+ pub fn new(v: String) -> Self {
+ match v.as_str() {
+ "i;ascii-casemap" => Self::AsciiCaseMap,
+ "i;octet" => Self::Octet,
+ _ => Self::Unknown(v),
+ }
+ }
+}
diff --git a/aero-dav/src/decoder.rs b/aero-dav/src/decoder.rs
new file mode 100644
index 0000000..bb64455
--- /dev/null
+++ b/aero-dav/src/decoder.rs
@@ -0,0 +1,1152 @@
+use chrono::DateTime;
+use quick_xml::events::Event;
+
+use super::error::ParsingError;
+use super::types::*;
+use super::xml::{IRead, Node, QRead, Reader, DAV_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 like xml::choices on some lib 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? {
+ xml.close().await?;
+ let includ = xml.maybe_find::<Include<E>>().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> QRead<Multistatus<E>> for Multistatus<E> {
+ 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;
+ let mut extension = None;
+
+ loop {
+ let mut dirty = false;
+ xml.maybe_push(&mut responses, &mut dirty).await?;
+ xml.maybe_read(&mut responsedescription, &mut dirty).await?;
+ xml.maybe_read(&mut extension, &mut dirty).await?;
+ if !dirty {
+ match xml.peek() {
+ Event::End(_) => break,
+ _ => xml.skip().await?,
+ };
+ }
+ }
+
+ xml.close().await?;
+ Ok(Multistatus {
+ responses,
+ responsedescription,
+ extension,
+ })
+ }
+}
+
+// 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> {
+ println!("---- propvalue");
+ xml.open(DAV_URN, "prop").await?;
+ let 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> QRead<Response<E>> for Response<E> {
+ 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>>(&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> QRead<PropStat<E>> for PropStat<E> {
+ async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> {
+ xml.open(DAV_URN, "propstat").await?;
+
+ let (mut m_any_prop, mut m_status, mut error, mut responsedescription) =
+ (None, None, None, None);
+
+ loop {
+ let mut dirty = false;
+ xml.maybe_read::<AnyProp<E>>(&mut m_any_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_any_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<AnyProp<E>> for AnyProp<E> {
+ async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> {
+ xml.open(DAV_URN, "prop").await?;
+ let acc = xml.collect::<AnyProperty<E>>().await?;
+ xml.close().await?;
+ Ok(AnyProp(acc))
+ }
+}
+
+impl<E: Extension> QRead<AnyProperty<E>> for AnyProperty<E> {
+ async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> {
+ match Property::qread(xml).await {
+ Err(ParsingError::Recoverable) => (),
+ otherwise => return otherwise.map(Self::Value),
+ }
+ PropertyRequest::qread(xml).await.map(Self::Request)
+ }
+}
+
+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> {
+ // Core WebDAV properties
+ if xml
+ .maybe_open_start(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_start(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_start(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_start(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_start(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_start(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_start(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_start(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_start(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_start(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 = "Second-";
+ 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 = xml.find::<Href>().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 = xml.find::<Href>().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;
+ }
+
+ 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 url = xml.tag_string().await?;
+ xml.close().await?;
+ Ok(Href(url))
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use crate::realization::Core;
+ use chrono::{FixedOffset, TimeZone};
+ use quick_xml::reader::NsReader;
+
+ #[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>>().await.unwrap();
+
+ assert_eq!(
+ got,
+ Multistatus {
+ responses: vec![
+ Response {
+ status_or_propstat: StatusOrPropstat::PropStat(
+ Href("http://www.example.com/container/".into()),
+ vec![PropStat {
+ prop: AnyProp(vec![
+ AnyProperty::Request(PropertyRequest::CreationDate),
+ AnyProperty::Request(PropertyRequest::DisplayName),
+ AnyProperty::Request(PropertyRequest::ResourceType),
+ AnyProperty::Request(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: AnyProp(vec![
+ AnyProperty::Request(PropertyRequest::CreationDate),
+ AnyProperty::Request(PropertyRequest::DisplayName),
+ AnyProperty::Request(PropertyRequest::GetContentLength),
+ AnyProperty::Request(PropertyRequest::GetContentType),
+ AnyProperty::Request(PropertyRequest::GetEtag),
+ AnyProperty::Request(PropertyRequest::GetLastModified),
+ AnyProperty::Request(PropertyRequest::ResourceType),
+ AnyProperty::Request(PropertyRequest::SupportedLock),
+ ]),
+ status: Status(http::status::StatusCode::OK),
+ error: None,
+ responsedescription: None,
+ }],
+ ),
+ error: None,
+ responsedescription: None,
+ location: None,
+ },
+ ],
+ responsedescription: None,
+ extension: 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>>().await.unwrap();
+
+ assert_eq!(
+ got,
+ Multistatus {
+ extension: None,
+ responses: vec![
+ Response {
+ status_or_propstat: StatusOrPropstat::PropStat(
+ Href("/container/".into()),
+ vec![PropStat {
+ prop: AnyProp(vec![
+ AnyProperty::Value(Property::CreationDate(
+ FixedOffset::west_opt(8 * 3600)
+ .unwrap()
+ .with_ymd_and_hms(1997, 12, 01, 17, 42, 21)
+ .unwrap()
+ )),
+ AnyProperty::Value(Property::DisplayName(
+ "Example collection".into()
+ )),
+ AnyProperty::Value(Property::ResourceType(vec![
+ ResourceType::Collection
+ ])),
+ AnyProperty::Value(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: AnyProp(vec![
+ AnyProperty::Value(Property::CreationDate(
+ FixedOffset::west_opt(8 * 3600)
+ .unwrap()
+ .with_ymd_and_hms(1997, 12, 01, 18, 27, 21)
+ .unwrap()
+ )),
+ AnyProperty::Value(Property::DisplayName(
+ "Example HTML resource".into()
+ )),
+ AnyProperty::Value(Property::GetContentLength(4525)),
+ AnyProperty::Value(Property::GetContentType(
+ "text/html".into()
+ )),
+ AnyProperty::Value(Property::GetEtag(r#""zzyzx""#.into())),
+ AnyProperty::Value(Property::GetLastModified(
+ FixedOffset::west_opt(0)
+ .unwrap()
+ .with_ymd_and_hms(1998, 01, 12, 09, 25, 56)
+ .unwrap()
+ )),
+ //@FIXME know bug, can't disambiguate between an empty resource
+ //type value and a request resource type
+ AnyProperty::Request(PropertyRequest::ResourceType),
+ AnyProperty::Value(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..6c77aa6
--- /dev/null
+++ b/aero-dav/src/encoder.rs
@@ -0,0 +1,1262 @@
+use super::types::*;
+use super::xml::{IWrite, Node, QWrite, Writer};
+use quick_xml::events::{BytesText, Event};
+use quick_xml::Error as QError;
+
+// --- 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> QWrite for Multistatus<E> {
+ 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?;
+ }
+ if let Some(extension) = &self.extension {
+ extension.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<E: Extension> QWrite for AnyProp<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<E: Extension> QWrite for AnyProperty<E> {
+ async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> {
+ match self {
+ Self::Request(v) => v.qwrite(xml).await,
+ Self::Value(v) => v.qwrite(xml).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> QWrite for Response<E> {
+ 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> QWrite for StatusOrPropstat<E> {
+ 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> QWrite for PropStat<E> {
+ 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::super::xml;
+ 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();
+ }
+
+ async fn deserialize<T: xml::Node<T>>(src: &str) -> T {
+ let mut rdr = xml::Reader::new(quick_xml::reader::NsReader::from_reader(src.as_bytes()))
+ .await
+ .unwrap();
+ rdr.find().await.unwrap()
+ }
+
+ #[tokio::test]
+ async fn basic_href() {
+ let orig = Href("/SOGo/dav/so/".into());
+
+ let got = serialize(&orig).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"
+ );
+ assert_eq!(deserialize::<Href>(got.as_str()).await, orig)
+ }
+
+ #[tokio::test]
+ async fn basic_multistatus() {
+ let orig = Multistatus::<Core> {
+ extension: None,
+ responses: vec![],
+ responsedescription: Some(ResponseDescription("Hello world".into())),
+ };
+ let got = serialize(&orig).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"
+ );
+ assert_eq!(deserialize::<Multistatus::<Core>>(got.as_str()).await, orig)
+ }
+
+ #[tokio::test]
+ async fn rfc_error_delete_locked() {
+ let orig = Error::<Core>(vec![Violation::LockTokenSubmitted(vec![Href(
+ "/locked/".into(),
+ )])]);
+ let got = serialize(&orig).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"
+ );
+ assert_eq!(deserialize::<Error<Core>>(got.as_str()).await, orig)
+ }
+
+ #[tokio::test]
+ async fn rfc_propname_req() {
+ let orig = PropFind::<Core>::PropName;
+
+ let got = serialize(&orig).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"
+ );
+ assert_eq!(deserialize::<PropFind::<Core>>(got.as_str()).await, orig)
+ }
+
+ #[tokio::test]
+ async fn rfc_propname_res() {
+ let orig = Multistatus::<Core> {
+ extension: None,
+ responses: vec![
+ Response {
+ status_or_propstat: StatusOrPropstat::PropStat(
+ Href("http://www.example.com/container/".into()),
+ vec![PropStat {
+ prop: AnyProp(vec![
+ AnyProperty::Request(PropertyRequest::CreationDate),
+ AnyProperty::Request(PropertyRequest::DisplayName),
+ AnyProperty::Request(PropertyRequest::ResourceType),
+ AnyProperty::Request(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: AnyProp(vec![
+ AnyProperty::Request(PropertyRequest::CreationDate),
+ AnyProperty::Request(PropertyRequest::DisplayName),
+ AnyProperty::Request(PropertyRequest::GetContentLength),
+ AnyProperty::Request(PropertyRequest::GetContentType),
+ AnyProperty::Request(PropertyRequest::GetEtag),
+ AnyProperty::Request(PropertyRequest::GetLastModified),
+ AnyProperty::Request(PropertyRequest::ResourceType),
+ AnyProperty::Request(PropertyRequest::SupportedLock),
+ ]),
+ status: Status(http::status::StatusCode::OK),
+ error: None,
+ responsedescription: None,
+ }],
+ ),
+ error: None,
+ responsedescription: None,
+ location: None,
+ },
+ ],
+ responsedescription: None,
+ };
+
+ let got = serialize(&orig).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"
+ );
+ assert_eq!(deserialize::<Multistatus::<Core>>(got.as_str()).await, orig)
+ }
+
+ #[tokio::test]
+ async fn rfc_allprop_req() {
+ let orig = PropFind::<Core>::AllProp(None);
+ let got = serialize(&orig).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"
+ );
+ assert_eq!(deserialize::<PropFind::<Core>>(got.as_str()).await, orig)
+ }
+
+ #[tokio::test]
+ async fn rfc_allprop_res() {
+ use chrono::{FixedOffset, TimeZone};
+
+ let orig = Multistatus::<Core> {
+ extension: None,
+ responses: vec![
+ Response {
+ status_or_propstat: StatusOrPropstat::PropStat(
+ Href("/container/".into()),
+ vec![PropStat {
+ prop: AnyProp(vec![
+ AnyProperty::Value(Property::CreationDate(
+ FixedOffset::west_opt(8 * 3600)
+ .unwrap()
+ .with_ymd_and_hms(1997, 12, 1, 17, 42, 21)
+ .unwrap(),
+ )),
+ AnyProperty::Value(Property::DisplayName(
+ "Example collection".into(),
+ )),
+ AnyProperty::Value(Property::ResourceType(vec![
+ ResourceType::Collection,
+ ])),
+ AnyProperty::Value(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: AnyProp(vec![
+ AnyProperty::Value(Property::CreationDate(
+ FixedOffset::west_opt(8 * 3600)
+ .unwrap()
+ .with_ymd_and_hms(1997, 12, 1, 18, 27, 21)
+ .unwrap(),
+ )),
+ AnyProperty::Value(Property::DisplayName(
+ "Example HTML resource".into(),
+ )),
+ AnyProperty::Value(Property::GetContentLength(4525)),
+ AnyProperty::Value(Property::GetContentType("text/html".into())),
+ AnyProperty::Value(Property::GetEtag(r#""zzyzx""#.into())),
+ AnyProperty::Value(Property::GetLastModified(
+ FixedOffset::east_opt(0)
+ .unwrap()
+ .with_ymd_and_hms(1998, 1, 12, 9, 25, 56)
+ .unwrap(),
+ )),
+ //@FIXME know bug, can't disambiguate between an empty resource
+ //type value and a request resource type
+ AnyProperty::Request(PropertyRequest::ResourceType),
+ AnyProperty::Value(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,
+ };
+
+ let got = serialize(&orig).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"
+ );
+ assert_eq!(deserialize::<Multistatus::<Core>>(got.as_str()).await, orig)
+ }
+
+ #[tokio::test]
+ async fn rfc_allprop_include() {
+ let orig = PropFind::<Core>::AllProp(Some(Include(vec![
+ PropertyRequest::DisplayName,
+ PropertyRequest::ResourceType,
+ ])));
+
+ let got = serialize(&orig).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"
+ );
+ assert_eq!(deserialize::<PropFind::<Core>>(got.as_str()).await, orig)
+ }
+
+ #[tokio::test]
+ async fn rfc_propertyupdate() {
+ let orig = PropertyUpdate::<Core>(vec![
+ PropertyUpdateItem::Set(Set(PropValue(vec![Property::GetContentLanguage(
+ "fr-FR".into(),
+ )]))),
+ PropertyUpdateItem::Remove(Remove(PropName(vec![PropertyRequest::DisplayName]))),
+ ]);
+ let got = serialize(&orig).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"
+ );
+ assert_eq!(
+ deserialize::<PropertyUpdate::<Core>>(got.as_str()).await,
+ orig
+ )
+ }
+
+ #[tokio::test]
+ async fn rfc_delete_locked2() {
+ let orig = Multistatus::<Core> {
+ extension: None,
+ 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,
+ };
+
+ let got = serialize(&orig).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"
+ );
+ assert_eq!(deserialize::<Multistatus::<Core>>(got.as_str()).await, orig)
+ }
+
+ #[tokio::test]
+ async fn rfc_simple_lock_request() {
+ let orig = LockInfo {
+ lockscope: LockScope::Exclusive,
+ locktype: LockType::Write,
+ owner: Some(Owner::Href(Href(
+ "http://example.org/~ejw/contact.html".into(),
+ ))),
+ };
+
+ let got = serialize(&orig).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"
+ );
+ assert_eq!(deserialize::<LockInfo>(got.as_str()).await, orig)
+ }
+
+ #[tokio::test]
+ async fn rfc_simple_lock_response() {
+ let orig = 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(),
+ )),
+ }])]);
+
+ let got = serialize(&orig).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"
+ );
+ assert_eq!(deserialize::<PropValue::<Core>>(got.as_str()).await, orig)
+ }
+}
diff --git a/aero-dav/src/error.rs b/aero-dav/src/error.rs
new file mode 100644
index 0000000..c8f1de1
--- /dev/null
+++ b/aero-dav/src/error.rs
@@ -0,0 +1,62 @@
+use quick_xml::events::attributes::AttrError;
+
+#[derive(Debug)]
+pub enum ParsingError {
+ Recoverable,
+ MissingChild,
+ MissingAttribute,
+ NamespacePrefixAlreadyUsed,
+ WrongToken,
+ TagNotFound,
+ InvalidValue,
+ Utf8Error(std::str::Utf8Error),
+ QuickXml(quick_xml::Error),
+ Chrono(chrono::format::ParseError),
+ Int(std::num::ParseIntError),
+ Eof,
+}
+impl std::fmt::Display for ParsingError {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ match self {
+ Self::Recoverable => write!(f, "Recoverable"),
+ Self::MissingChild => write!(f, "Missing child"),
+ Self::MissingAttribute => write!(f, "Missing attribute"),
+ Self::NamespacePrefixAlreadyUsed => write!(f, "Namespace prefix already used"),
+ Self::WrongToken => write!(f, "Wrong token"),
+ Self::TagNotFound => write!(f, "Tag not found"),
+ Self::InvalidValue => write!(f, "Invalid value"),
+ Self::Utf8Error(_) => write!(f, "Utf8 Error"),
+ Self::QuickXml(_) => write!(f, "Quick XML error"),
+ Self::Chrono(_) => write!(f, "Chrono error"),
+ Self::Int(_) => write!(f, "Number parsing error"),
+ Self::Eof => write!(f, "Found EOF while expecting data"),
+ }
+ }
+}
+impl std::error::Error for ParsingError {}
+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..64be929
--- /dev/null
+++ b/aero-dav/src/lib.rs
@@ -0,0 +1,35 @@
+#![feature(type_alias_impl_trait)]
+#![feature(async_closure)]
+#![feature(trait_alias)]
+
+// utils
+pub mod error;
+pub mod xml;
+
+// webdav
+pub mod decoder;
+pub mod encoder;
+pub mod types;
+
+// calendar
+pub mod caldecoder;
+pub mod calencoder;
+pub mod caltypes;
+
+// acl (partial)
+pub mod acldecoder;
+pub mod aclencoder;
+pub mod acltypes;
+
+// versioning (partial)
+pub mod versioningdecoder;
+pub mod versioningencoder;
+pub mod versioningtypes;
+
+// sync
+pub mod syncdecoder;
+pub mod syncencoder;
+pub mod synctypes;
+
+// 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..76170f8
--- /dev/null
+++ b/aero-dav/src/realization.rs
@@ -0,0 +1,260 @@
+use super::acltypes as acl;
+use super::caltypes as cal;
+use super::error;
+use super::synctypes as sync;
+use super::types as dav;
+use super::versioningtypes as vers;
+use super::xml;
+
+#[derive(Debug, PartialEq, Clone)]
+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 disabled through an object we can't build
+/// due to a private inner element.
+#[derive(Debug, PartialEq, Clone)]
+pub struct Core {}
+impl dav::Extension for Core {
+ type Error = Disabled;
+ type Property = Disabled;
+ type PropertyRequest = Disabled;
+ type ResourceType = Disabled;
+ type ReportType = Disabled;
+ type ReportTypeName = Disabled;
+ type Multistatus = Disabled;
+}
+
+// WebDAV with the base Calendar implementation (RFC4791)
+#[derive(Debug, PartialEq, Clone)]
+pub struct Calendar {}
+impl dav::Extension for Calendar {
+ type Error = cal::Violation;
+ type Property = cal::Property;
+ type PropertyRequest = cal::PropertyRequest;
+ type ResourceType = cal::ResourceType;
+ type ReportType = cal::ReportType<Calendar>;
+ type ReportTypeName = cal::ReportTypeName;
+ type Multistatus = Disabled;
+}
+
+// ACL
+#[derive(Debug, PartialEq, Clone)]
+pub struct Acl {}
+impl dav::Extension for Acl {
+ type Error = Disabled;
+ type Property = acl::Property;
+ type PropertyRequest = acl::PropertyRequest;
+ type ResourceType = acl::ResourceType;
+ type ReportType = Disabled;
+ type ReportTypeName = Disabled;
+ type Multistatus = Disabled;
+}
+
+// All merged
+#[derive(Debug, PartialEq, Clone)]
+pub struct All {}
+impl dav::Extension for All {
+ type Error = cal::Violation;
+ type Property = Property<All>;
+ type PropertyRequest = PropertyRequest;
+ type ResourceType = ResourceType;
+ type ReportType = ReportType<All>;
+ type ReportTypeName = ReportTypeName;
+ type Multistatus = Multistatus;
+}
+
+#[derive(Debug, PartialEq, Clone)]
+pub enum Property<E: dav::Extension> {
+ Cal(cal::Property),
+ Acl(acl::Property),
+ Sync(sync::Property),
+ Vers(vers::Property<E>),
+}
+impl<E: dav::Extension> xml::QRead<Property<E>> for Property<E> {
+ async fn qread(xml: &mut xml::Reader<impl xml::IRead>) -> Result<Self, error::ParsingError> {
+ match cal::Property::qread(xml).await {
+ Err(error::ParsingError::Recoverable) => (),
+ otherwise => return otherwise.map(Property::<E>::Cal),
+ }
+ match acl::Property::qread(xml).await {
+ Err(error::ParsingError::Recoverable) => (),
+ otherwise => return otherwise.map(Property::Acl),
+ }
+ match sync::Property::qread(xml).await {
+ Err(error::ParsingError::Recoverable) => (),
+ otherwise => return otherwise.map(Property::Sync),
+ }
+ vers::Property::qread(xml).await.map(Property::Vers)
+ }
+}
+impl<E: dav::Extension> xml::QWrite for Property<E> {
+ async fn qwrite(
+ &self,
+ xml: &mut xml::Writer<impl xml::IWrite>,
+ ) -> Result<(), quick_xml::Error> {
+ match self {
+ Self::Cal(c) => c.qwrite(xml).await,
+ Self::Acl(a) => a.qwrite(xml).await,
+ Self::Sync(s) => s.qwrite(xml).await,
+ Self::Vers(v) => v.qwrite(xml).await,
+ }
+ }
+}
+
+#[derive(Debug, PartialEq, Clone)]
+pub enum PropertyRequest {
+ Cal(cal::PropertyRequest),
+ Acl(acl::PropertyRequest),
+ Sync(sync::PropertyRequest),
+ Vers(vers::PropertyRequest),
+}
+impl xml::QRead<PropertyRequest> for PropertyRequest {
+ async fn qread(xml: &mut xml::Reader<impl xml::IRead>) -> Result<Self, error::ParsingError> {
+ match cal::PropertyRequest::qread(xml).await {
+ Err(error::ParsingError::Recoverable) => (),
+ otherwise => return otherwise.map(PropertyRequest::Cal),
+ }
+ match acl::PropertyRequest::qread(xml).await {
+ Err(error::ParsingError::Recoverable) => (),
+ otherwise => return otherwise.map(PropertyRequest::Acl),
+ }
+ match sync::PropertyRequest::qread(xml).await {
+ Err(error::ParsingError::Recoverable) => (),
+ otherwise => return otherwise.map(PropertyRequest::Sync),
+ }
+ vers::PropertyRequest::qread(xml)
+ .await
+ .map(PropertyRequest::Vers)
+ }
+}
+impl xml::QWrite for PropertyRequest {
+ async fn qwrite(
+ &self,
+ xml: &mut xml::Writer<impl xml::IWrite>,
+ ) -> Result<(), quick_xml::Error> {
+ match self {
+ Self::Cal(c) => c.qwrite(xml).await,
+ Self::Acl(a) => a.qwrite(xml).await,
+ Self::Sync(s) => s.qwrite(xml).await,
+ Self::Vers(v) => v.qwrite(xml).await,
+ }
+ }
+}
+
+#[derive(Debug, PartialEq, Clone)]
+pub enum ResourceType {
+ Cal(cal::ResourceType),
+ Acl(acl::ResourceType),
+}
+impl xml::QRead<ResourceType> for ResourceType {
+ async fn qread(xml: &mut xml::Reader<impl xml::IRead>) -> Result<Self, error::ParsingError> {
+ match cal::ResourceType::qread(xml).await {
+ Err(error::ParsingError::Recoverable) => (),
+ otherwise => return otherwise.map(ResourceType::Cal),
+ }
+ acl::ResourceType::qread(xml).await.map(ResourceType::Acl)
+ }
+}
+impl xml::QWrite for ResourceType {
+ async fn qwrite(
+ &self,
+ xml: &mut xml::Writer<impl xml::IWrite>,
+ ) -> Result<(), quick_xml::Error> {
+ match self {
+ Self::Cal(c) => c.qwrite(xml).await,
+ Self::Acl(a) => a.qwrite(xml).await,
+ }
+ }
+}
+
+#[derive(Debug, PartialEq, Clone)]
+pub enum ReportType<E: dav::Extension> {
+ Cal(cal::ReportType<E>),
+ Sync(sync::SyncCollection<E>),
+}
+impl<E: dav::Extension> xml::QRead<ReportType<E>> for ReportType<E> {
+ async fn qread(
+ xml: &mut xml::Reader<impl xml::IRead>,
+ ) -> Result<ReportType<E>, error::ParsingError> {
+ match cal::ReportType::qread(xml).await {
+ Err(error::ParsingError::Recoverable) => (),
+ otherwise => return otherwise.map(ReportType::Cal),
+ }
+ sync::SyncCollection::qread(xml).await.map(ReportType::Sync)
+ }
+}
+impl<E: dav::Extension> xml::QWrite for ReportType<E> {
+ async fn qwrite(
+ &self,
+ xml: &mut xml::Writer<impl xml::IWrite>,
+ ) -> Result<(), quick_xml::Error> {
+ match self {
+ Self::Cal(c) => c.qwrite(xml).await,
+ Self::Sync(s) => s.qwrite(xml).await,
+ }
+ }
+}
+
+#[derive(Debug, PartialEq, Clone)]
+pub enum ReportTypeName {
+ Cal(cal::ReportTypeName),
+ Sync(sync::ReportTypeName),
+}
+impl xml::QRead<ReportTypeName> for ReportTypeName {
+ async fn qread(xml: &mut xml::Reader<impl xml::IRead>) -> Result<Self, error::ParsingError> {
+ match cal::ReportTypeName::qread(xml).await {
+ Err(error::ParsingError::Recoverable) => (),
+ otherwise => return otherwise.map(ReportTypeName::Cal),
+ }
+ sync::ReportTypeName::qread(xml)
+ .await
+ .map(ReportTypeName::Sync)
+ }
+}
+impl xml::QWrite for ReportTypeName {
+ async fn qwrite(
+ &self,
+ xml: &mut xml::Writer<impl xml::IWrite>,
+ ) -> Result<(), quick_xml::Error> {
+ match self {
+ Self::Cal(c) => c.qwrite(xml).await,
+ Self::Sync(s) => s.qwrite(xml).await,
+ }
+ }
+}
+
+#[derive(Debug, PartialEq, Clone)]
+pub enum Multistatus {
+ Sync(sync::Multistatus),
+}
+
+impl xml::QWrite for Multistatus {
+ async fn qwrite(
+ &self,
+ xml: &mut xml::Writer<impl xml::IWrite>,
+ ) -> Result<(), quick_xml::Error> {
+ match self {
+ Self::Sync(s) => s.qwrite(xml).await,
+ }
+ }
+}
+
+impl xml::QRead<Multistatus> for Multistatus {
+ async fn qread(xml: &mut xml::Reader<impl xml::IRead>) -> Result<Self, error::ParsingError> {
+ sync::Multistatus::qread(xml).await.map(Self::Sync)
+ }
+}
diff --git a/aero-dav/src/syncdecoder.rs b/aero-dav/src/syncdecoder.rs
new file mode 100644
index 0000000..2a61dea
--- /dev/null
+++ b/aero-dav/src/syncdecoder.rs
@@ -0,0 +1,248 @@
+use quick_xml::events::Event;
+
+use super::error::ParsingError;
+use super::synctypes::*;
+use super::types as dav;
+use super::xml::{IRead, QRead, Reader, DAV_URN};
+
+impl QRead<PropertyRequest> for PropertyRequest {
+ async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> {
+ if xml.maybe_open(DAV_URN, "sync-token").await?.is_some() {
+ xml.close().await?;
+ return Ok(Self::SyncToken);
+ }
+ return Err(ParsingError::Recoverable);
+ }
+}
+
+impl QRead<Property> for Property {
+ async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> {
+ let mut dirty = false;
+ let mut m_cdr = None;
+ xml.maybe_read(&mut m_cdr, &mut dirty).await?;
+ m_cdr.ok_or(ParsingError::Recoverable).map(Self::SyncToken)
+ }
+}
+
+impl QRead<ReportTypeName> for ReportTypeName {
+ async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> {
+ if xml.maybe_open(DAV_URN, "sync-collection").await?.is_some() {
+ xml.close().await?;
+ return Ok(Self::SyncCollection);
+ }
+ Err(ParsingError::Recoverable)
+ }
+}
+
+impl QRead<Multistatus> for Multistatus {
+ async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> {
+ SyncToken::qread(xml)
+ .await
+ .map(|sync_token| Multistatus { sync_token })
+ }
+}
+
+impl<E: dav::Extension> QRead<SyncCollection<E>> for SyncCollection<E> {
+ async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> {
+ xml.open(DAV_URN, "sync-collection").await?;
+ let (mut sync_token, mut sync_level, mut limit, mut prop) = (None, None, None, None);
+ loop {
+ let mut dirty = false;
+ xml.maybe_read(&mut sync_token, &mut dirty).await?;
+ xml.maybe_read(&mut sync_level, &mut dirty).await?;
+ xml.maybe_read(&mut limit, &mut dirty).await?;
+ xml.maybe_read(&mut prop, &mut dirty).await?;
+
+ if !dirty {
+ match xml.peek() {
+ Event::End(_) => break,
+ _ => xml.skip().await?,
+ };
+ }
+ }
+
+ xml.close().await?;
+ match (sync_token, sync_level, prop) {
+ (Some(sync_token), Some(sync_level), Some(prop)) => Ok(SyncCollection {
+ sync_token,
+ sync_level,
+ limit,
+ prop,
+ }),
+ _ => Err(ParsingError::MissingChild),
+ }
+ }
+}
+
+impl QRead<SyncTokenRequest> for SyncTokenRequest {
+ async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> {
+ xml.open(DAV_URN, "sync-token").await?;
+ let token = match xml.tag_string().await {
+ Ok(v) => SyncTokenRequest::IncrementalSync(v),
+ Err(ParsingError::Recoverable) => SyncTokenRequest::InitialSync,
+ Err(e) => return Err(e),
+ };
+ xml.close().await?;
+ Ok(token)
+ }
+}
+
+impl QRead<SyncToken> for SyncToken {
+ async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> {
+ xml.open(DAV_URN, "sync-token").await?;
+ let token = xml.tag_string().await?;
+ xml.close().await?;
+ Ok(SyncToken(token))
+ }
+}
+
+impl QRead<SyncLevel> for SyncLevel {
+ async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> {
+ xml.open(DAV_URN, "sync-level").await?;
+ let lvl = match xml.tag_string().await?.to_lowercase().as_str() {
+ "1" => SyncLevel::One,
+ "infinite" => SyncLevel::Infinite,
+ _ => return Err(ParsingError::InvalidValue),
+ };
+ xml.close().await?;
+ Ok(lvl)
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use crate::realization::{self, All};
+ use crate::types as dav;
+ use crate::versioningtypes as vers;
+ use crate::xml::Node;
+
+ async fn deserialize<T: Node<T>>(src: &str) -> T {
+ let mut rdr = Reader::new(quick_xml::NsReader::from_reader(src.as_bytes()))
+ .await
+ .unwrap();
+ rdr.find().await.unwrap()
+ }
+
+ #[tokio::test]
+ async fn sync_level() {
+ {
+ let expected = SyncLevel::One;
+ let src = r#"<D:sync-level xmlns:D="DAV:">1</D:sync-level>"#;
+ let got = deserialize::<SyncLevel>(src).await;
+ assert_eq!(got, expected);
+ }
+ {
+ let expected = SyncLevel::Infinite;
+ let src = r#"<D:sync-level xmlns:D="DAV:">infinite</D:sync-level>"#;
+ let got = deserialize::<SyncLevel>(src).await;
+ assert_eq!(got, expected);
+ }
+ }
+
+ #[tokio::test]
+ async fn sync_token_request() {
+ {
+ let expected = SyncTokenRequest::InitialSync;
+ let src = r#"<D:sync-token xmlns:D="DAV:"/>"#;
+ let got = deserialize::<SyncTokenRequest>(src).await;
+ assert_eq!(got, expected);
+ }
+ {
+ let expected =
+ SyncTokenRequest::IncrementalSync("http://example.com/ns/sync/1232".into());
+ let src =
+ r#"<D:sync-token xmlns:D="DAV:">http://example.com/ns/sync/1232</D:sync-token>"#;
+ let got = deserialize::<SyncTokenRequest>(src).await;
+ assert_eq!(got, expected);
+ }
+ }
+
+ #[tokio::test]
+ async fn sync_token() {
+ let expected = SyncToken("http://example.com/ns/sync/1232".into());
+ let src = r#"<D:sync-token xmlns:D="DAV:">http://example.com/ns/sync/1232</D:sync-token>"#;
+ let got = deserialize::<SyncToken>(src).await;
+ assert_eq!(got, expected);
+ }
+
+ #[tokio::test]
+ async fn sync_collection() {
+ {
+ let expected = SyncCollection::<All> {
+ sync_token: SyncTokenRequest::IncrementalSync(
+ "http://example.com/ns/sync/1232".into(),
+ ),
+ sync_level: SyncLevel::One,
+ limit: Some(vers::Limit(vers::NResults(100))),
+ prop: dav::PropName(vec![dav::PropertyRequest::GetEtag]),
+ };
+ let src = r#"<D:sync-collection xmlns:D="DAV:">
+ <D:sync-token>http://example.com/ns/sync/1232</D:sync-token>
+ <D:sync-level>1</D:sync-level>
+ <D:limit>
+ <D:nresults>100</D:nresults>
+ </D:limit>
+ <D:prop>
+ <D:getetag/>
+ </D:prop>
+ </D:sync-collection>"#;
+ let got = deserialize::<SyncCollection<All>>(src).await;
+ assert_eq!(got, expected);
+ }
+
+ {
+ let expected = SyncCollection::<All> {
+ sync_token: SyncTokenRequest::InitialSync,
+ sync_level: SyncLevel::Infinite,
+ limit: None,
+ prop: dav::PropName(vec![dav::PropertyRequest::GetEtag]),
+ };
+ let src = r#"<D:sync-collection xmlns:D="DAV:">
+ <D:sync-token/>
+ <D:sync-level>infinite</D:sync-level>
+ <D:prop>
+ <D:getetag/>
+ </D:prop>
+ </D:sync-collection>"#;
+ let got = deserialize::<SyncCollection<All>>(src).await;
+ assert_eq!(got, expected);
+ }
+ }
+
+ #[tokio::test]
+ async fn prop_req() {
+ let expected = dav::PropName::<All>(vec![dav::PropertyRequest::Extension(
+ realization::PropertyRequest::Sync(PropertyRequest::SyncToken),
+ )]);
+ let src = r#"<prop xmlns="DAV:"><sync-token/></prop>"#;
+ let got = deserialize::<dav::PropName<All>>(src).await;
+ assert_eq!(got, expected);
+ }
+
+ #[tokio::test]
+ async fn prop_val() {
+ let expected = dav::PropValue::<All>(vec![
+ dav::Property::Extension(realization::Property::Sync(Property::SyncToken(SyncToken(
+ "http://example.com/ns/sync/1232".into(),
+ )))),
+ dav::Property::Extension(realization::Property::Vers(
+ vers::Property::SupportedReportSet(vec![vers::SupportedReport(
+ vers::ReportName::Extension(realization::ReportTypeName::Sync(
+ ReportTypeName::SyncCollection,
+ )),
+ )]),
+ )),
+ ]);
+ let src = r#"<prop xmlns="DAV:">
+ <sync-token>http://example.com/ns/sync/1232</sync-token>
+ <supported-report-set>
+ <supported-report>
+ <report><sync-collection/></report>
+ </supported-report>
+ </supported-report-set>
+ </prop>"#;
+ let got = deserialize::<dav::PropValue<All>>(src).await;
+ assert_eq!(got, expected);
+ }
+}
diff --git a/aero-dav/src/syncencoder.rs b/aero-dav/src/syncencoder.rs
new file mode 100644
index 0000000..55f7ad6
--- /dev/null
+++ b/aero-dav/src/syncencoder.rs
@@ -0,0 +1,227 @@
+use quick_xml::events::{BytesText, Event};
+use quick_xml::Error as QError;
+
+use super::synctypes::*;
+use super::types::Extension;
+use super::xml::{IWrite, QWrite, Writer};
+
+impl QWrite for Property {
+ async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> {
+ match self {
+ Self::SyncToken(token) => token.qwrite(xml).await,
+ }
+ }
+}
+
+impl QWrite for PropertyRequest {
+ async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> {
+ match self {
+ Self::SyncToken => {
+ let start = xml.create_dav_element("sync-token");
+ xml.q.write_event_async(Event::Empty(start)).await
+ }
+ }
+ }
+}
+
+impl QWrite for ReportTypeName {
+ async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> {
+ match self {
+ Self::SyncCollection => {
+ let start = xml.create_dav_element("sync-collection");
+ xml.q.write_event_async(Event::Empty(start)).await
+ }
+ }
+ }
+}
+
+impl QWrite for Multistatus {
+ async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> {
+ self.sync_token.qwrite(xml).await
+ }
+}
+
+impl<E: Extension> QWrite for SyncCollection<E> {
+ async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> {
+ let start = xml.create_dav_element("sync-collection");
+ let end = start.to_end();
+
+ xml.q.write_event_async(Event::Start(start.clone())).await?;
+ self.sync_token.qwrite(xml).await?;
+ self.sync_level.qwrite(xml).await?;
+ if let Some(limit) = &self.limit {
+ limit.qwrite(xml).await?;
+ }
+ self.prop.qwrite(xml).await?;
+ xml.q.write_event_async(Event::End(end)).await
+ }
+}
+
+impl QWrite for SyncTokenRequest {
+ async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> {
+ let start = xml.create_dav_element("sync-token");
+
+ match self {
+ Self::InitialSync => xml.q.write_event_async(Event::Empty(start)).await,
+ Self::IncrementalSync(uri) => {
+ let end = start.to_end();
+ xml.q.write_event_async(Event::Start(start.clone())).await?;
+ xml.q
+ .write_event_async(Event::Text(BytesText::new(uri.as_str())))
+ .await?;
+ xml.q.write_event_async(Event::End(end)).await
+ }
+ }
+ }
+}
+
+impl QWrite for SyncToken {
+ async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> {
+ let start = xml.create_dav_element("sync-token");
+ 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 SyncLevel {
+ async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> {
+ let start = xml.create_dav_element("sync-level");
+ let end = start.to_end();
+ let text = match self {
+ Self::One => "1",
+ Self::Infinite => "infinite",
+ };
+
+ 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
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use crate::realization::{self, All};
+ use crate::types as dav;
+ use crate::versioningtypes as vers;
+ use crate::xml::Node;
+ use crate::xml::Reader;
+ use tokio::io::AsyncWriteExt;
+
+ async fn serialize_deserialize<T: Node<T>>(src: &T) {
+ 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 };
+
+ src.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();
+
+ // deserialize
+ let mut rdr = Reader::new(quick_xml::NsReader::from_reader(got.as_bytes()))
+ .await
+ .unwrap();
+ let res = rdr.find().await.unwrap();
+
+ // check
+ assert_eq!(src, &res);
+ }
+
+ #[tokio::test]
+ async fn sync_level() {
+ serialize_deserialize(&SyncLevel::One).await;
+ serialize_deserialize(&SyncLevel::Infinite).await;
+ }
+
+ #[tokio::test]
+ async fn sync_token_request() {
+ serialize_deserialize(&SyncTokenRequest::InitialSync).await;
+ serialize_deserialize(&SyncTokenRequest::IncrementalSync(
+ "http://example.com/ns/sync/1232".into(),
+ ))
+ .await;
+ }
+
+ #[tokio::test]
+ async fn sync_token() {
+ serialize_deserialize(&SyncToken("http://example.com/ns/sync/1232".into())).await;
+ }
+
+ #[tokio::test]
+ async fn sync_collection() {
+ serialize_deserialize(&SyncCollection::<All> {
+ sync_token: SyncTokenRequest::IncrementalSync("http://example.com/ns/sync/1232".into()),
+ sync_level: SyncLevel::One,
+ limit: Some(vers::Limit(vers::NResults(100))),
+ prop: dav::PropName(vec![dav::PropertyRequest::GetEtag]),
+ })
+ .await;
+
+ serialize_deserialize(&SyncCollection::<All> {
+ sync_token: SyncTokenRequest::InitialSync,
+ sync_level: SyncLevel::Infinite,
+ limit: None,
+ prop: dav::PropName(vec![dav::PropertyRequest::GetEtag]),
+ })
+ .await;
+ }
+
+ #[tokio::test]
+ async fn prop_req() {
+ serialize_deserialize(&dav::PropName::<All>(vec![
+ dav::PropertyRequest::Extension(realization::PropertyRequest::Sync(
+ PropertyRequest::SyncToken,
+ )),
+ ]))
+ .await;
+ }
+
+ #[tokio::test]
+ async fn prop_val() {
+ serialize_deserialize(&dav::PropValue::<All>(vec![
+ dav::Property::Extension(realization::Property::Sync(Property::SyncToken(SyncToken(
+ "http://example.com/ns/sync/1232".into(),
+ )))),
+ dav::Property::Extension(realization::Property::Vers(
+ vers::Property::SupportedReportSet(vec![vers::SupportedReport(
+ vers::ReportName::Extension(realization::ReportTypeName::Sync(
+ ReportTypeName::SyncCollection,
+ )),
+ )]),
+ )),
+ ]))
+ .await;
+ }
+
+ #[tokio::test]
+ async fn multistatus_ext() {
+ serialize_deserialize(&dav::Multistatus::<All> {
+ responses: vec![dav::Response {
+ status_or_propstat: dav::StatusOrPropstat::Status(
+ vec![dav::Href("/".into())],
+ dav::Status(http::status::StatusCode::OK),
+ ),
+ error: None,
+ location: None,
+ responsedescription: None,
+ }],
+ responsedescription: None,
+ extension: Some(realization::Multistatus::Sync(Multistatus {
+ sync_token: SyncToken("http://example.com/ns/sync/1232".into()),
+ })),
+ })
+ .await;
+ }
+}
diff --git a/aero-dav/src/synctypes.rs b/aero-dav/src/synctypes.rs
new file mode 100644
index 0000000..2a14221
--- /dev/null
+++ b/aero-dav/src/synctypes.rs
@@ -0,0 +1,86 @@
+use super::types as dav;
+use super::versioningtypes as vers;
+
+// RFC 6578
+// https://datatracker.ietf.org/doc/html/rfc6578
+
+#[derive(Debug, PartialEq, Clone)]
+pub enum PropertyRequest {
+ SyncToken,
+}
+
+#[derive(Debug, PartialEq, Clone)]
+pub enum Property {
+ SyncToken(SyncToken),
+}
+
+#[derive(Debug, PartialEq, Clone)]
+pub enum ReportTypeName {
+ SyncCollection,
+}
+
+#[derive(Debug, PartialEq, Clone)]
+pub struct Multistatus {
+ pub sync_token: SyncToken,
+}
+
+//@FIXME add SyncToken to Multistatus
+
+/// Name: sync-collection
+///
+/// Namespace: DAV:
+///
+/// Purpose: WebDAV report used to synchronize data between client and
+/// server.
+///
+/// Description: See Section 3.
+///
+/// <!ELEMENT sync-collection (sync-token, sync-level, limit?, prop)>
+///
+/// <!-- DAV:limit defined in RFC 5323, Section 5.17 -->
+/// <!-- DAV:prop defined in RFC 4918, Section 14.18 -->
+
+#[derive(Debug, PartialEq, Clone)]
+pub struct SyncCollection<E: dav::Extension> {
+ pub sync_token: SyncTokenRequest,
+ pub sync_level: SyncLevel,
+ pub limit: Option<vers::Limit>,
+ pub prop: dav::PropName<E>,
+}
+
+/// Name: sync-token
+///
+/// Namespace: DAV:
+///
+/// Purpose: The synchronization token provided by the server and
+/// returned by the client.
+///
+/// Description: See Section 3.
+///
+/// <!ELEMENT sync-token CDATA>
+///
+/// <!-- Text MUST be a URI -->
+/// Used by multistatus
+#[derive(Debug, PartialEq, Clone)]
+pub struct SyncToken(pub String);
+
+/// Used by propfind and report sync-collection
+#[derive(Debug, PartialEq, Clone)]
+pub enum SyncTokenRequest {
+ InitialSync,
+ IncrementalSync(String),
+}
+
+/// Name: sync-level
+///
+/// Namespace: DAV:
+///
+/// Purpose: Indicates the "scope" of the synchronization report
+/// request.
+///
+/// Description: See Section 3.3.
+#[derive(Debug, PartialEq, Clone)]
+pub enum SyncLevel {
+ One,
+ Infinite,
+}
diff --git a/aero-dav/src/types.rs b/aero-dav/src/types.rs
new file mode 100644
index 0000000..61a6fe9
--- /dev/null
+++ b/aero-dav/src/types.rs
@@ -0,0 +1,964 @@
+#![allow(dead_code)]
+use std::fmt::Debug;
+
+use super::xml;
+use chrono::{DateTime, FixedOffset};
+
+/// It's how we implement a DAV extension
+/// (That's the dark magic part...)
+pub trait Extension: std::fmt::Debug + PartialEq + Clone {
+ type Error: xml::Node<Self::Error>;
+ type Property: xml::Node<Self::Property>;
+ type PropertyRequest: xml::Node<Self::PropertyRequest>;
+ type ResourceType: xml::Node<Self::ResourceType>;
+ type ReportType: xml::Node<Self::ReportType>;
+ type ReportTypeName: xml::Node<Self::ReportTypeName>;
+ type Multistatus: xml::Node<Self::Multistatus>;
+}
+
+/// 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, Clone)]
+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, Clone)]
+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, Clone)]
+pub struct Error<E: Extension>(pub Vec<Violation<E>>);
+#[derive(Debug, PartialEq, Clone)]
+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, Clone)]
+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, Clone)]
+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, Clone)]
+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, Clone)]
+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, Clone)]
+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, Clone)]
+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, Clone)]
+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, Clone)]
+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, Clone)]
+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?) >
+///
+/// In WebDAV sync (rfc6578), multistatus is extended:
+///
+/// <!ELEMENT multistatus (response*, responsedescription?, sync-token?) >
+#[derive(Debug, PartialEq, Clone)]
+pub struct Multistatus<E: Extension> {
+ pub responses: Vec<Response<E>>,
+ pub responsedescription: Option<ResponseDescription>,
+ pub extension: Option<E::Multistatus>,
+}
+
+/// 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, Clone)]
+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, Clone)]
+pub struct PropName<E: Extension>(pub Vec<PropertyRequest<E>>);
+
+#[derive(Debug, PartialEq, Clone)]
+pub struct PropValue<E: Extension>(pub Vec<Property<E>>);
+
+#[derive(Debug, PartialEq, Clone)]
+pub struct AnyProp<E: Extension>(pub Vec<AnyProperty<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, Clone)]
+pub struct PropertyUpdate<E: Extension>(pub Vec<PropertyUpdateItem<E>>);
+
+#[derive(Debug, PartialEq, Clone)]
+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, Clone)]
+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, Clone)]
+pub struct PropStat<E: Extension> {
+ pub prop: AnyProp<E>,
+ 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, Clone)]
+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, Clone)]
+pub enum StatusOrPropstat<E: Extension> {
+ // One status, multiple hrefs...
+ Status(Vec<Href>, Status),
+ // A single href, multiple properties...
+ PropStat(Href, Vec<PropStat<E>>),
+}
+
+#[derive(Debug, PartialEq, Clone)]
+pub struct Response<E: Extension> {
+ pub status_or_propstat: StatusOrPropstat<E>,
+ 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, Clone)]
+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, Clone)]
+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, Clone)]
+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, Clone)]
+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, Clone)]
+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, Clone)]
+pub enum AnyProperty<E: Extension> {
+ Request(PropertyRequest<E>),
+ Value(Property<E>),
+}
+
+#[derive(Debug, PartialEq, Clone)]
+pub enum PropertyRequest<E: Extension> {
+ CreationDate,
+ DisplayName,
+ GetContentLanguage,
+ GetContentLength,
+ GetContentType,
+ GetEtag,
+ GetLastModified,
+ LockDiscovery,
+ ResourceType,
+ SupportedLock,
+ Extension(E::PropertyRequest),
+}
+
+#[derive(Debug, PartialEq, Clone)]
+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, Clone)]
+pub enum ResourceType<E: Extension> {
+ Collection,
+ Extension(E::ResourceType),
+}
diff --git a/aero-dav/src/versioningdecoder.rs b/aero-dav/src/versioningdecoder.rs
new file mode 100644
index 0000000..a0a3ddf
--- /dev/null
+++ b/aero-dav/src/versioningdecoder.rs
@@ -0,0 +1,132 @@
+use super::error::ParsingError;
+use super::types as dav;
+use super::versioningtypes::*;
+use super::xml::{IRead, QRead, Reader, DAV_URN};
+
+// -- extensions ---
+impl QRead<PropertyRequest> for PropertyRequest {
+ async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> {
+ if xml
+ .maybe_open(DAV_URN, "supported-report-set")
+ .await?
+ .is_some()
+ {
+ xml.close().await?;
+ return Ok(Self::SupportedReportSet);
+ }
+ return Err(ParsingError::Recoverable);
+ }
+}
+
+impl<E: dav::Extension> QRead<Property<E>> for Property<E> {
+ async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> {
+ if xml
+ .maybe_open_start(DAV_URN, "supported-report-set")
+ .await?
+ .is_some()
+ {
+ let supported_reports = xml.collect().await?;
+ xml.close().await?;
+ return Ok(Property::SupportedReportSet(supported_reports));
+ }
+ Err(ParsingError::Recoverable)
+ }
+}
+
+impl<E: dav::Extension> QRead<SupportedReport<E>> for SupportedReport<E> {
+ async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> {
+ xml.open(DAV_URN, "supported-report").await?;
+ let r = xml.find().await?;
+ xml.close().await?;
+ Ok(SupportedReport(r))
+ }
+}
+
+impl<E: dav::Extension> QRead<ReportName<E>> for ReportName<E> {
+ async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> {
+ xml.open(DAV_URN, "report").await?;
+
+ let final_result = if xml.maybe_open(DAV_URN, "version-tree").await?.is_some() {
+ xml.close().await?;
+ Ok(ReportName::VersionTree)
+ } else if xml.maybe_open(DAV_URN, "expand-property").await?.is_some() {
+ xml.close().await?;
+ Ok(ReportName::ExpandProperty)
+ } else {
+ let x = match xml.maybe_find().await? {
+ Some(v) => v,
+ None => return Err(ParsingError::MissingChild),
+ };
+ Ok(ReportName::Extension(x))
+ //E::ReportTypeName::qread(xml).await.map(ReportName::Extension)
+ };
+
+ xml.close().await?;
+ final_result
+ }
+}
+
+impl<E: dav::Extension> QRead<Report<E>> for Report<E> {
+ async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> {
+ if xml.maybe_open(DAV_URN, "version-tree").await?.is_some() {
+ xml.close().await?;
+ tracing::warn!("version-tree is not implemented, skipping");
+ Ok(Report::VersionTree)
+ } else if xml.maybe_open(DAV_URN, "expand-property").await?.is_some() {
+ xml.close().await?;
+ tracing::warn!("expand-property is not implemented, skipping");
+ Ok(Report::ExpandProperty)
+ } else {
+ E::ReportType::qread(xml).await.map(Report::Extension)
+ }
+ }
+}
+
+impl QRead<Limit> for Limit {
+ async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> {
+ xml.open(DAV_URN, "limit").await?;
+ let nres = xml.find().await?;
+ xml.close().await?;
+ Ok(Limit(nres))
+ }
+}
+
+impl QRead<NResults> for NResults {
+ async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> {
+ xml.open(DAV_URN, "nresults").await?;
+ let sz = xml.tag_string().await?.parse::<u64>()?;
+ xml.close().await?;
+ Ok(NResults(sz))
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use crate::xml::Node;
+
+ async fn deserialize<T: Node<T>>(src: &str) -> T {
+ let mut rdr = Reader::new(quick_xml::NsReader::from_reader(src.as_bytes()))
+ .await
+ .unwrap();
+ rdr.find().await.unwrap()
+ }
+
+ #[tokio::test]
+ async fn nresults() {
+ let expected = NResults(100);
+ let src = r#"<D:nresults xmlns:D="DAV:">100</D:nresults>"#;
+ let got = deserialize::<NResults>(src).await;
+ assert_eq!(got, expected);
+ }
+
+ #[tokio::test]
+ async fn limit() {
+ let expected = Limit(NResults(1024));
+ let src = r#"<D:limit xmlns:D="DAV:">
+ <D:nresults>1024</D:nresults>
+ </D:limit>"#;
+ let got = deserialize::<Limit>(src).await;
+ assert_eq!(got, expected);
+ }
+}
diff --git a/aero-dav/src/versioningencoder.rs b/aero-dav/src/versioningencoder.rs
new file mode 100644
index 0000000..c061f07
--- /dev/null
+++ b/aero-dav/src/versioningencoder.rs
@@ -0,0 +1,143 @@
+use quick_xml::events::{BytesText, Event};
+use quick_xml::Error as QError;
+
+use super::types::Extension;
+use super::versioningtypes::*;
+use super::xml::{IWrite, QWrite, Writer};
+
+// --- extensions to PROP
+impl QWrite for PropertyRequest {
+ async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> {
+ match self {
+ Self::SupportedReportSet => {
+ let start = xml.create_dav_element("supported-report-set");
+ xml.q.write_event_async(Event::Empty(start)).await
+ }
+ }
+ }
+}
+
+impl<E: Extension> QWrite for Property<E> {
+ async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> {
+ match self {
+ Self::SupportedReportSet(set) => {
+ let start = xml.create_dav_element("supported-report-set");
+ let end = start.to_end();
+
+ xml.q.write_event_async(Event::Start(start.clone())).await?;
+ for v in set.iter() {
+ v.qwrite(xml).await?;
+ }
+ xml.q.write_event_async(Event::End(end)).await
+ }
+ }
+ }
+}
+
+impl<E: Extension> QWrite for SupportedReport<E> {
+ async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> {
+ let start = xml.create_dav_element("supported-report");
+ 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 ReportName<E> {
+ async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> {
+ let start = xml.create_dav_element("report");
+ let end = start.to_end();
+
+ xml.q.write_event_async(Event::Start(start.clone())).await?;
+ match self {
+ Self::VersionTree => {
+ let start = xml.create_dav_element("version-tree");
+ xml.q.write_event_async(Event::Empty(start)).await?;
+ }
+ Self::ExpandProperty => {
+ let start = xml.create_dav_element("expand-property");
+ xml.q.write_event_async(Event::Empty(start)).await?;
+ }
+ Self::Extension(ext) => ext.qwrite(xml).await?,
+ };
+ xml.q.write_event_async(Event::End(end)).await
+ }
+}
+
+// --- root REPORT object ---
+impl<E: Extension> QWrite for Report<E> {
+ async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> {
+ match self {
+ Report::VersionTree => unimplemented!(),
+ Report::ExpandProperty => unimplemented!(),
+ Report::Extension(inner) => inner.qwrite(xml).await,
+ }
+ }
+}
+
+// --- limit REPORT parameter ---
+impl QWrite for Limit {
+ async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> {
+ let start = xml.create_dav_element("limit");
+ 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 NResults {
+ async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> {
+ let start = xml.create_dav_element("nresults");
+ let end = start.to_end();
+
+ xml.q.write_event_async(Event::Start(start.clone())).await?;
+ xml.q
+ .write_event_async(Event::Text(BytesText::new(&format!("{}", self.0))))
+ .await?;
+ xml.q.write_event_async(Event::End(end)).await
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use crate::xml::Node;
+ use crate::xml::Reader;
+ use tokio::io::AsyncWriteExt;
+
+ async fn serialize_deserialize<T: Node<T>>(src: &T) -> T {
+ 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 };
+
+ src.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();
+
+ // deserialize
+ let mut rdr = Reader::new(quick_xml::NsReader::from_reader(got.as_bytes()))
+ .await
+ .unwrap();
+ rdr.find().await.unwrap()
+ }
+
+ #[tokio::test]
+ async fn nresults() {
+ let orig = NResults(100);
+ assert_eq!(orig, serialize_deserialize(&orig).await);
+ }
+
+ #[tokio::test]
+ async fn limit() {
+ let orig = Limit(NResults(1024));
+ assert_eq!(orig, serialize_deserialize(&orig).await);
+ }
+}
diff --git a/aero-dav/src/versioningtypes.rs b/aero-dav/src/versioningtypes.rs
new file mode 100644
index 0000000..1f8d1cf
--- /dev/null
+++ b/aero-dav/src/versioningtypes.rs
@@ -0,0 +1,59 @@
+use super::types as dav;
+
+//@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.
+// https://datatracker.ietf.org/doc/html/rfc4791#section-7.1
+//
+// Defines (required by CalDAV):
+// - REPORT method
+// - expand-property root report method
+//
+// Defines (required by Sync):
+// - limit, nresults
+// - supported-report-set
+
+// This property identifies the reports that are supported by the
+// resource.
+//
+// <!ELEMENT supported-report-set (supported-report*)>
+// <!ELEMENT supported-report report>
+// <!ELEMENT report ANY>
+// ANY value: a report element type
+
+#[derive(Debug, PartialEq, Clone)]
+pub enum PropertyRequest {
+ SupportedReportSet,
+}
+
+#[derive(Debug, PartialEq, Clone)]
+pub enum Property<E: dav::Extension> {
+ SupportedReportSet(Vec<SupportedReport<E>>),
+}
+
+#[derive(Debug, PartialEq, Clone)]
+pub struct SupportedReport<E: dav::Extension>(pub ReportName<E>);
+
+#[derive(Debug, PartialEq, Clone)]
+pub enum ReportName<E: dav::Extension> {
+ VersionTree,
+ ExpandProperty,
+ Extension(E::ReportTypeName),
+}
+
+#[derive(Debug, PartialEq, Clone)]
+pub enum Report<E: dav::Extension> {
+ VersionTree, // Not yet implemented
+ ExpandProperty, // Not yet implemented
+ Extension(E::ReportType),
+}
+
+/// Limit
+/// <!ELEMENT limit (nresults) >
+#[derive(Debug, PartialEq, Clone)]
+pub struct Limit(pub NResults);
+
+/// NResults
+/// <!ELEMENT nresults (#PCDATA) >
+#[derive(Debug, PartialEq, Clone)]
+pub struct NResults(pub u64);
diff --git a/aero-dav/src/xml.rs b/aero-dav/src/xml.rs
new file mode 100644
index 0000000..e59f136
--- /dev/null
+++ b/aero-dav/src/xml.rs
@@ -0,0 +1,367 @@
+use futures::Future;
+use quick_xml::events::{BytesStart, Event};
+use quick_xml::name::ResolveResult;
+use quick_xml::reader::NsReader;
+use tokio::io::{AsyncBufRead, AsyncWrite};
+
+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 + Send;
+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>> + Send;
+}
+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 + Clone + Sync;
+
+// ---------------
+
+/// 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>,
+ prev: 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![];
+ let prev = Event::Eof;
+ buf.clear();
+ Ok(Self {
+ cur,
+ prev,
+ 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();
+ self.prev = std::mem::replace(&mut self.cur, evt);
+ Ok(self.prev.clone())
+ }
+
+ /// 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 {:?} value {:?}", self.parents.last(), self.cur);
+ 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,
+ }
+ }
+
+ pub 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
+ }
+
+ pub fn previous(&self) -> &Event<'static> {
+ &self.prev
+ }
+
+ // 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> {
+ // We can't find anything inside a self-closed tag
+ if !self.parent_has_child() {
+ return Ok(None);
+ }
+
+ loop {
+ // Try parse
+ match N::qread(self).await {
+ Err(ParsingError::Recoverable) => (),
+ otherwise => return otherwise.map(Some),
+ }
+
+ // Skip or stop
+ match self.peek() {
+ Event::End(_) => return Ok(None),
+ _ => self.skip().await?,
+ };
+ }
+ }
+
+ pub async fn collect<N: Node<N>>(&mut self) -> Result<Vec<N>, ParsingError> {
+ let mut acc = Vec::new();
+ if !self.parent_has_child() {
+ return Ok(acc);
+ }
+
+ 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> {
+ //println!("try open tag {:?}, on {:?}", key, self.peek());
+ let evt = match self.peek() {
+ Event::Empty(_) if self.is_tag(ns, key) => {
+ // hack to make `prev_attr` works
+ // here we duplicate the current tag
+ // as in other words, we virtually moved one token
+ // which is useful for prev_attr and any logic based on
+ // self.prev + self.open() on empty nodes
+ self.prev = self.cur.clone();
+ 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 open_start(
+ &mut self,
+ ns: &[u8],
+ key: &str,
+ ) -> Result<Event<'static>, ParsingError> {
+ //println!("try open start tag {:?}, on {:?}", key, self.peek());
+ let evt = match self.peek() {
+ Event::Start(_) if self.is_tag(ns, key) => self.next().await?,
+ _ => return Err(ParsingError::Recoverable),
+ };
+
+ //println!("open start 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),
+ }
+ }
+
+ pub async fn maybe_open_start(
+ &mut self,
+ ns: &[u8],
+ key: &str,
+ ) -> Result<Option<Event<'static>>, ParsingError> {
+ match self.open_start(ns, key).await {
+ Ok(v) => Ok(Some(v)),
+ Err(ParsingError::Recoverable) => Ok(None),
+ Err(e) => Err(e),
+ }
+ }
+
+ pub fn prev_attr(&self, attr: &str) -> Option<String> {
+ match &self.prev {
+ Event::Start(bs) | Event::Empty(bs) => match bs.try_get_attribute(attr) {
+ Ok(Some(attr)) => attr
+ .decode_and_unescape_value(&self.rdr)
+ .ok()
+ .map(|v| v.into_owned()),
+ _ => None,
+ },
+ _ => None,
+ }
+ }
+
+ // 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?,
+ };
+ }
+ }
+}