aboutsummaryrefslogtreecommitdiff
path: root/aero-dav/src
diff options
context:
space:
mode:
authorQuentin Dufour <quentin@deuxfleurs.fr>2024-05-27 18:16:53 +0200
committerQuentin Dufour <quentin@deuxfleurs.fr>2024-05-27 18:16:53 +0200
commit5b1da2a33b265b674a130a90377c289faea7a210 (patch)
treee3c5ec8bc0745d978fd49ddc60d7e9170055dc9f /aero-dav/src
parent418adf92be86ea83008a145180837f1e0ad3018a (diff)
downloadaerogramme-5b1da2a33b265b674a130a90377c289faea7a210.tar.gz
aerogramme-5b1da2a33b265b674a130a90377c289faea7a210.zip
webdav sync core codec
Diffstat (limited to 'aero-dav/src')
-rw-r--r--aero-dav/src/caldecoder.rs2
-rw-r--r--aero-dav/src/calencoder.rs2
-rw-r--r--aero-dav/src/caltypes.rs2
-rw-r--r--aero-dav/src/lib.rs13
-rw-r--r--aero-dav/src/realization.rs33
-rw-r--r--aero-dav/src/syncdecoder.rs175
-rw-r--r--aero-dav/src/syncencoder.rs144
-rw-r--r--aero-dav/src/synctypes.rs68
-rw-r--r--aero-dav/src/types.rs5
-rw-r--r--aero-dav/src/versioningdecoder.rs62
-rw-r--r--aero-dav/src/versioningencoder.rs81
-rw-r--r--aero-dav/src/versioningtypes.rs36
12 files changed, 617 insertions, 6 deletions
diff --git a/aero-dav/src/caldecoder.rs b/aero-dav/src/caldecoder.rs
index b6a843f..ff79845 100644
--- a/aero-dav/src/caldecoder.rs
+++ b/aero-dav/src/caldecoder.rs
@@ -25,7 +25,7 @@ impl<E: dav::Extension> QRead<MkCalendarResponse<E>> for MkCalendarResponse<E> {
}
}
-impl<E: dav::Extension> QRead<Report<E>> for Report<E> {
+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) => (),
diff --git a/aero-dav/src/calencoder.rs b/aero-dav/src/calencoder.rs
index 723d95d..48c93d0 100644
--- a/aero-dav/src/calencoder.rs
+++ b/aero-dav/src/calencoder.rs
@@ -33,7 +33,7 @@ impl<E: Extension> QWrite for MkCalendarResponse<E> {
}
// ----------------------- REPORT METHOD -------------------------------------
-impl<E: Extension> QWrite for Report<E> {
+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,
diff --git a/aero-dav/src/caltypes.rs b/aero-dav/src/caltypes.rs
index 50cdb92..a763653 100644
--- a/aero-dav/src/caltypes.rs
+++ b/aero-dav/src/caltypes.rs
@@ -51,7 +51,7 @@ pub struct MkCalendarResponse<E: dav::Extension>(pub Vec<dav::PropStat<E>>);
// --- (REPORT PART) ---
#[derive(Debug, PartialEq, Clone)]
-pub enum Report<E: dav::Extension> {
+pub enum ReportType<E: dav::Extension> {
Query(CalendarQuery<E>),
Multiget(CalendarMultiget<E>),
FreeBusy(FreeBusyQuery),
diff --git a/aero-dav/src/lib.rs b/aero-dav/src/lib.rs
index 7507ddc..64be929 100644
--- a/aero-dav/src/lib.rs
+++ b/aero-dav/src/lib.rs
@@ -16,13 +16,20 @@ pub mod caldecoder;
pub mod calencoder;
pub mod caltypes;
-// acl (wip)
+// acl (partial)
pub mod acldecoder;
pub mod aclencoder;
pub mod acltypes;
-// versioning (wip)
-mod versioningtypes;
+// 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
index 7283e68..0f3aec4 100644
--- a/aero-dav/src/realization.rs
+++ b/aero-dav/src/realization.rs
@@ -1,6 +1,7 @@
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::xml;
@@ -31,6 +32,7 @@ impl dav::Extension for Core {
type Property = Disabled;
type PropertyRequest = Disabled;
type ResourceType = Disabled;
+ type ReportType = Disabled;
}
// WebDAV with the base Calendar implementation (RFC4791)
@@ -41,6 +43,7 @@ impl dav::Extension for Calendar {
type Property = cal::Property;
type PropertyRequest = cal::PropertyRequest;
type ResourceType = cal::ResourceType;
+ type ReportType = cal::ReportType<Calendar>;
}
// ACL
@@ -51,6 +54,7 @@ impl dav::Extension for Acl {
type Property = acl::Property;
type PropertyRequest = acl::PropertyRequest;
type ResourceType = acl::ResourceType;
+ type ReportType = Disabled;
}
// All merged
@@ -61,6 +65,7 @@ impl dav::Extension for All {
type Property = Property;
type PropertyRequest = PropertyRequest;
type ResourceType = ResourceType;
+ type ReportType = ReportType<All>;
}
#[derive(Debug, PartialEq, Clone)]
@@ -142,3 +147,31 @@ impl xml::QWrite for ResourceType {
}
}
}
+
+#[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,
+ }
+ }
+}
diff --git a/aero-dav/src/syncdecoder.rs b/aero-dav/src/syncdecoder.rs
new file mode 100644
index 0000000..8e035ab
--- /dev/null
+++ b/aero-dav/src/syncdecoder.rs
@@ -0,0 +1,175 @@
+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<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::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);
+ }
+ }
+}
diff --git a/aero-dav/src/syncencoder.rs b/aero-dav/src/syncencoder.rs
new file mode 100644
index 0000000..22b288b
--- /dev/null
+++ b/aero-dav/src/syncencoder.rs
@@ -0,0 +1,144 @@
+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<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::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;
+ }
+}
diff --git a/aero-dav/src/synctypes.rs b/aero-dav/src/synctypes.rs
new file mode 100644
index 0000000..a2f40bd
--- /dev/null
+++ b/aero-dav/src/synctypes.rs
@@ -0,0 +1,68 @@
+use super::types as dav;
+use super::versioningtypes as vers;
+
+// RFC 6578
+// https://datatracker.ietf.org/doc/html/rfc6578
+
+//@FIXME add SyncTokenRequest to PropertyRequest
+//@FIXME add SyncToken to Property
+//@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
index d5466da..6039a26 100644
--- a/aero-dav/src/types.rs
+++ b/aero-dav/src/types.rs
@@ -11,6 +11,7 @@ pub trait Extension: std::fmt::Debug + PartialEq + Clone {
type Property: xml::Node<Self::Property>;
type PropertyRequest: xml::Node<Self::PropertyRequest>;
type ResourceType: xml::Node<Self::ResourceType>;
+ type ReportType: xml::Node<Self::ReportType>;
}
/// 14.1. activelock XML Element
@@ -328,6 +329,10 @@ pub enum LockType {
/// 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>>,
diff --git a/aero-dav/src/versioningdecoder.rs b/aero-dav/src/versioningdecoder.rs
new file mode 100644
index 0000000..4816cf1
--- /dev/null
+++ b/aero-dav/src/versioningdecoder.rs
@@ -0,0 +1,62 @@
+use super::error::ParsingError;
+use super::types as dav;
+use super::versioningtypes::*;
+use super::xml::{IRead, QRead, Reader, DAV_URN};
+
+impl<E: dav::Extension> QRead<Report<E>> for Report<E> {
+ async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> {
+ //@FIXME VersionTree not implemented
+ //@FIXME ExpandTree not implemented
+
+ 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..bd40f1b
--- /dev/null
+++ b/aero-dav/src/versioningencoder.rs
@@ -0,0 +1,81 @@
+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};
+
+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,
+ }
+ }
+}
+
+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
index 6c1c204..ba64b05 100644
--- a/aero-dav/src/versioningtypes.rs
+++ b/aero-dav/src/versioningtypes.rs
@@ -1,3 +1,39 @@
+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 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);