diff --git a/Cargo.lock b/Cargo.lock index 58c2dbb..b910749 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2510,6 +2510,7 @@ dependencies = [ "quick-xml", "rustical_dav", "rustical_store", + "rustical_xml", "serde", "sha2", "strum", @@ -2535,6 +2536,7 @@ dependencies = [ "quick-xml", "rustical_dav", "rustical_store", + "rustical_xml", "serde", "strum", "thiserror 2.0.8", @@ -2602,6 +2604,7 @@ dependencies = [ "regex", "rstest", "rstest_reuse", + "rustical_xml", "serde", "sha2", "thiserror 2.0.8", diff --git a/crates/caldav/Cargo.toml b/crates/caldav/Cargo.toml index c433cf3..5a0c01a 100644 --- a/crates/caldav/Cargo.toml +++ b/crates/caldav/Cargo.toml @@ -26,3 +26,4 @@ rustical_dav = { workspace = true } rustical_store = { workspace = true } chrono = { workspace = true } sha2 = { workspace = true } +rustical_xml.workspace = true diff --git a/crates/caldav/src/calendar/methods/report/calendar_multiget.rs b/crates/caldav/src/calendar/methods/report/calendar_multiget.rs index 2a2b2d8..a5b3fd4 100644 --- a/crates/caldav/src/calendar/methods/report/calendar_multiget.rs +++ b/crates/caldav/src/calendar/methods/report/calendar_multiget.rs @@ -13,16 +13,16 @@ use rustical_dav::{ xml::{multistatus::ResponseElement, MultistatusElement, PropElement, PropfindType}, }; use rustical_store::{auth::User, CalendarObject, CalendarStore}; -use serde::Deserialize; +use rustical_xml::XmlDeserialize; -#[derive(Deserialize, Clone, Debug)] -#[serde(rename_all = "kebab-case")] +#[derive(XmlDeserialize, Clone, Debug, PartialEq)] #[allow(dead_code)] // -pub struct CalendarMultigetRequest { - #[serde(flatten)] - prop: PropfindType, - href: Vec, +pub(crate) struct CalendarMultigetRequest { + #[xml(ty = "untagged")] + pub(crate) prop: PropfindType, + #[xml(flatten)] + pub(crate) href: Vec, } pub async fn get_objects_calendar_multiget( @@ -78,7 +78,10 @@ pub async fn handle_calendar_multiget( PropfindType::Propname => { vec!["propname".to_owned()] } - PropfindType::Prop(PropElement { prop: prop_tags }) => prop_tags.into_inner(), + PropfindType::Prop(PropElement { prop: prop_tags }) => prop_tags + .into_iter() + .map(|propname| propname.name) + .collect(), }; let props: Vec<&str> = props.iter().map(String::as_str).collect(); diff --git a/crates/caldav/src/calendar/methods/report/calendar_query.rs b/crates/caldav/src/calendar/methods/report/calendar_query.rs index a55137e..aba9c8e 100644 --- a/crates/caldav/src/calendar/methods/report/calendar_query.rs +++ b/crates/caldav/src/calendar/methods/report/calendar_query.rs @@ -1,11 +1,12 @@ +use std::ops::Deref; + use actix_web::HttpRequest; -use chrono::{DateTime, Utc}; use rustical_dav::{ resource::{CommonPropertiesProp, EitherProp, Resource}, xml::{MultistatusElement, PropElement, PropfindType}, }; -use rustical_store::{auth::User, CalendarObject, CalendarStore}; -use serde::Deserialize; +use rustical_store::{auth::User, calendar::UtcDateTime, CalendarObject, CalendarStore}; +use rustical_xml::{Unit, XmlDeserialize}; use crate::{ calendar_object::resource::{CalendarObjectProp, CalendarObjectResource}, @@ -14,70 +15,57 @@ use crate::{ // TODO: Implement all the other filters -#[derive(Deserialize, Clone, Debug)] -#[serde(rename_all = "kebab-case")] +#[derive(XmlDeserialize, Clone, Debug, PartialEq)] #[allow(dead_code)] -struct TimeRangeElement { - #[serde( - rename = "@start", - deserialize_with = "rustical_store::calendar::deserialize_utc_datetime", - default - )] - start: Option>, - #[serde( - rename = "@end", - deserialize_with = "rustical_store::calendar::deserialize_utc_datetime", - default - )] - end: Option>, +pub(crate) struct TimeRangeElement { + #[xml(ty = "attr")] + pub(crate) start: Option, + #[xml(ty = "attr")] + pub(crate) end: Option, } -#[derive(Deserialize, Clone, Debug)] -#[serde(rename_all = "kebab-case")] +#[derive(XmlDeserialize, Clone, Debug, PartialEq)] #[allow(dead_code)] struct ParamFilterElement { - is_not_defined: Option<()>, + is_not_defined: Option, text_match: Option, - #[serde(rename = "@name")] + #[xml(ty = "attr")] name: String, } -#[derive(Deserialize, Clone, Debug)] -#[serde(rename_all = "kebab-case")] +#[derive(XmlDeserialize, Clone, Debug, PartialEq)] #[allow(dead_code)] struct TextMatchElement { - #[serde(rename = "@collation")] + #[xml(ty = "attr")] collation: String, - #[serde(rename = "@negate-collation")] + #[xml(ty = "attr")] negate_collation: String, } -#[derive(Deserialize, Clone, Debug)] -#[serde(rename_all = "kebab-case")] +#[derive(XmlDeserialize, Clone, Debug, PartialEq)] #[allow(dead_code)] -struct PropFilterElement { - is_not_defined: Option<()>, +pub(crate) struct PropFilterElement { + is_not_defined: Option, time_range: Option, text_match: Option, - #[serde(default)] + #[xml(flatten)] param_filter: Vec, } -#[derive(Deserialize, Clone, Debug)] -#[serde(rename_all = "kebab-case")] +#[derive(XmlDeserialize, Clone, Debug, PartialEq)] #[allow(dead_code)] // https://datatracker.ietf.org/doc/html/rfc4791#section-9.7.1 -struct CompFilterElement { - is_not_defined: Option<()>, - time_range: Option, - #[serde(default)] - prop_filter: Vec, - #[serde(default)] - comp_filter: Vec, +pub(crate) struct CompFilterElement { + pub(crate) is_not_defined: Option, + pub(crate) time_range: Option, + #[xml(flatten)] + pub(crate) prop_filter: Vec, + #[xml(flatten)] + pub(crate) comp_filter: Vec, - #[serde(rename = "@name")] - name: String, + #[xml(ty = "attr")] + pub(crate) name: String, } impl CompFilterElement { @@ -146,14 +134,14 @@ impl CompFilterElement { if let Some(time_range) = &self.time_range { if let Some(start) = &time_range.start { if let Some(first_occurence) = cal_object.get_first_occurence().unwrap_or(None) { - if start > &first_occurence.utc() { + if start.deref() > &first_occurence.utc() { return false; } }; } if let Some(end) = &time_range.end { if let Some(last_occurence) = cal_object.get_last_occurence().unwrap_or(None) { - if end < &last_occurence.utc() { + if end.deref() < &last_occurence.utc() { return false; } }; @@ -164,12 +152,11 @@ impl CompFilterElement { } } -#[derive(Deserialize, Clone, Debug)] -#[serde(rename_all = "kebab-case")] +#[derive(XmlDeserialize, Clone, Debug, PartialEq)] #[allow(dead_code)] // https://datatracker.ietf.org/doc/html/rfc4791#section-9.7 -struct FilterElement { - comp_filter: CompFilterElement, +pub(crate) struct FilterElement { + pub(crate) comp_filter: CompFilterElement, } impl FilterElement { @@ -178,15 +165,14 @@ impl FilterElement { } } -#[derive(Deserialize, Clone, Debug)] -#[serde(rename_all = "kebab-case")] +#[derive(XmlDeserialize, Clone, Debug, PartialEq)] #[allow(dead_code)] // pub struct CalendarQueryRequest { - #[serde(flatten)] + #[xml(ty = "untagged")] pub prop: PropfindType, - filter: Option, - timezone: Option, + pub(crate) filter: Option, + pub(crate) timezone: Option, } pub async fn get_objects_calendar_query( @@ -220,7 +206,10 @@ pub async fn handle_calendar_query( PropfindType::Propname => { vec!["propname".to_owned()] } - PropfindType::Prop(PropElement { prop: prop_tags }) => prop_tags.into_inner(), + PropfindType::Prop(PropElement { prop: prop_tags }) => prop_tags + .into_iter() + .map(|propname| propname.name) + .collect(), }; let props: Vec<&str> = props.iter().map(String::as_str).collect(); diff --git a/crates/caldav/src/calendar/methods/report/mod.rs b/crates/caldav/src/calendar/methods/report/mod.rs index 9c9188f..c46d4b9 100644 --- a/crates/caldav/src/calendar/methods/report/mod.rs +++ b/crates/caldav/src/calendar/methods/report/mod.rs @@ -6,7 +6,7 @@ use actix_web::{ use calendar_multiget::{handle_calendar_multiget, CalendarMultigetRequest}; use calendar_query::{handle_calendar_query, CalendarQueryRequest}; use rustical_store::{auth::User, CalendarStore}; -use serde::{Deserialize, Serialize}; +use rustical_xml::{XmlDeserialize, XmlDocument}; use sync_collection::{handle_sync_collection, SyncCollectionRequest}; use tracing::instrument; @@ -14,17 +14,8 @@ mod calendar_multiget; mod calendar_query; mod sync_collection; -#[derive(Deserialize, Serialize, Clone, Debug)] -#[serde(rename_all = "kebab-case")] -pub enum PropQuery { - Allprop, - Prop, - Propname, -} - -#[derive(Deserialize, Clone, Debug)] -#[serde(rename_all = "kebab-case")] -pub enum ReportRequest { +#[derive(XmlDeserialize, XmlDocument, Clone, Debug, PartialEq)] +pub(crate) enum ReportRequest { CalendarMultiget(CalendarMultigetRequest), CalendarQuery(CalendarQueryRequest), SyncCollection(SyncCollectionRequest), @@ -43,7 +34,7 @@ pub async fn route_report_calendar( return Err(Error::Unauthorized); } - let request: ReportRequest = quick_xml::de::from_str(&body)?; + let request = ReportRequest::parse_str(&body)?; Ok(match request.clone() { ReportRequest::CalendarQuery(cal_query) => { @@ -81,3 +72,66 @@ pub async fn route_report_calendar( } }) } + +#[cfg(test)] +mod tests { + use calendar_query::{CompFilterElement, FilterElement, TimeRangeElement}; + use rustical_dav::xml::{PropElement, PropfindType, Propname}; + use rustical_store::calendar::UtcDateTime; + use rustical_xml::Value; + + use super::*; + + #[test] + fn test_xml_sync_collection() { + let report_request = ReportRequest::parse_str( + r#" + + + + + + + + + + + + + "#, + ) + .unwrap(); + assert_eq!( + report_request, + ReportRequest::CalendarQuery(CalendarQueryRequest { + prop: PropfindType::Prop(PropElement { + prop: vec![Propname { + name: "getetag".to_owned() + }] + }), + filter: Some(FilterElement { + comp_filter: CompFilterElement { + is_not_defined: None, + time_range: None, + prop_filter: vec![], + comp_filter: vec![CompFilterElement { + is_not_defined: None, + time_range: Some(TimeRangeElement { + start: Some( + ::deserialize("20240924T143437Z") + .unwrap() + ), + end: None + }), + prop_filter: vec![], + comp_filter: vec![], + name: "VEVENT".to_owned() + }], + name: "VCALENDAR".to_owned() + } + }), + timezone: None, + }) + ) + } +} diff --git a/crates/caldav/src/calendar/methods/report/sync_collection.rs b/crates/caldav/src/calendar/methods/report/sync_collection.rs index aaf5b8f..155e09a 100644 --- a/crates/caldav/src/calendar/methods/report/sync_collection.rs +++ b/crates/caldav/src/calendar/methods/report/sync_collection.rs @@ -9,34 +9,49 @@ use rustical_store::{ synctoken::{format_synctoken, parse_synctoken}, CalendarStore, }; -use serde::Deserialize; +use rustical_xml::{Value, XmlDeserialize}; use crate::{ calendar_object::resource::{CalendarObjectProp, CalendarObjectResource}, Error, }; -#[derive(Deserialize, Clone, Debug)] -#[serde(rename_all = "kebab-case")] -enum SyncLevel { - #[serde(rename = "1")] +#[derive(Clone, Debug, PartialEq)] +pub(crate) enum SyncLevel { One, Infinity, } -#[derive(Deserialize, Clone, Debug)] -#[serde(rename_all = "kebab-case")] +impl Value for SyncLevel { + fn deserialize(val: &str) -> Result { + Ok(match val { + "1" => Self::One, + "Infinity" => Self::Infinity, + // TODO: proper error + _ => return Err(rustical_xml::XmlDeError::UnknownError), + }) + } + fn serialize(&self) -> String { + match self { + SyncLevel::One => "1", + SyncLevel::Infinity => "Infinity", + } + .to_owned() + } +} + +#[derive(XmlDeserialize, Clone, Debug, PartialEq)] #[allow(dead_code)] // // // -pub struct SyncCollectionRequest { - sync_token: String, - sync_level: SyncLevel, - timezone: Option, - #[serde(flatten)] +pub(crate) struct SyncCollectionRequest { + pub(crate) sync_token: String, + pub(crate) sync_level: SyncLevel, + pub(crate) timezone: Option, + #[xml(ty = "untagged")] pub prop: PropfindType, - limit: Option, + pub(crate) limit: Option, } pub async fn handle_sync_collection( @@ -55,7 +70,10 @@ pub async fn handle_sync_collection( PropfindType::Propname => { vec!["propname".to_owned()] } - PropfindType::Prop(PropElement { prop: prop_tags }) => prop_tags.into_inner(), + PropfindType::Prop(PropElement { prop: prop_tags }) => prop_tags + .into_iter() + .map(|propname| propname.name) + .collect(), }; let props: Vec<&str> = props.iter().map(String::as_str).collect(); diff --git a/crates/caldav/src/error.rs b/crates/caldav/src/error.rs index 05fb51b..19802ad 100644 --- a/crates/caldav/src/error.rs +++ b/crates/caldav/src/error.rs @@ -18,6 +18,9 @@ pub enum Error { #[error(transparent)] DavError(#[from] rustical_dav::Error), + #[error(transparent)] + NewXmlDecodeError(#[from] rustical_xml::XmlDeError), + #[error(transparent)] XmlDecodeError(#[from] quick_xml::DeError), @@ -35,6 +38,7 @@ impl actix_web::ResponseError for Error { }, Error::DavError(err) => err.status_code(), Error::Unauthorized => StatusCode::UNAUTHORIZED, + Error::NewXmlDecodeError(_) => StatusCode::BAD_REQUEST, Error::XmlDecodeError(_) => StatusCode::BAD_REQUEST, Error::NotImplemented => StatusCode::INTERNAL_SERVER_ERROR, Error::Other(_) => StatusCode::INTERNAL_SERVER_ERROR, diff --git a/crates/carddav/Cargo.toml b/crates/carddav/Cargo.toml index 44b9db5..3fd4393 100644 --- a/crates/carddav/Cargo.toml +++ b/crates/carddav/Cargo.toml @@ -25,3 +25,4 @@ url = { workspace = true } rustical_dav = { workspace = true } rustical_store = { workspace = true } chrono = { workspace = true } +rustical_xml.workspace = true diff --git a/crates/carddav/src/addressbook/methods/report/addressbook_multiget.rs b/crates/carddav/src/addressbook/methods/report/addressbook_multiget.rs index 78c6416..668db01 100644 --- a/crates/carddav/src/addressbook/methods/report/addressbook_multiget.rs +++ b/crates/carddav/src/addressbook/methods/report/addressbook_multiget.rs @@ -14,14 +14,14 @@ use rustical_dav::{ xml::{PropElement, PropfindType}, }; use rustical_store::{auth::User, AddressObject, AddressbookStore}; -use serde::Deserialize; +use rustical_xml::XmlDeserialize; -#[derive(Deserialize, Clone, Debug)] -#[serde(rename_all = "kebab-case")] +#[derive(XmlDeserialize, Clone, Debug, PartialEq)] #[allow(dead_code)] pub struct AddressbookMultigetRequest { - #[serde(flatten)] + #[xml(ty = "untagged")] prop: PropfindType, + #[xml(flatten)] href: Vec, } @@ -84,7 +84,10 @@ pub async fn handle_addressbook_multiget( PropfindType::Propname => { vec!["propname".to_owned()] } - PropfindType::Prop(PropElement { prop: prop_tags }) => prop_tags.into_inner(), + PropfindType::Prop(PropElement { prop: prop_tags }) => prop_tags + .into_iter() + .map(|propname| propname.name) + .collect(), }; let props: Vec<&str> = props.iter().map(String::as_str).collect(); diff --git a/crates/carddav/src/addressbook/methods/report/mod.rs b/crates/carddav/src/addressbook/methods/report/mod.rs index be1d956..2155dde 100644 --- a/crates/carddav/src/addressbook/methods/report/mod.rs +++ b/crates/carddav/src/addressbook/methods/report/mod.rs @@ -5,24 +5,15 @@ use actix_web::{ }; use addressbook_multiget::{handle_addressbook_multiget, AddressbookMultigetRequest}; use rustical_store::{auth::User, AddressbookStore}; -use serde::{Deserialize, Serialize}; +use rustical_xml::{XmlDeserialize, XmlDocument}; use sync_collection::{handle_sync_collection, SyncCollectionRequest}; use tracing::instrument; mod addressbook_multiget; mod sync_collection; -#[derive(Deserialize, Serialize, Clone, Debug)] -#[serde(rename_all = "kebab-case")] -pub enum PropQuery { - Allprop, - Prop, - Propname, -} - -#[derive(Deserialize, Clone, Debug)] -#[serde(rename_all = "kebab-case")] -pub enum ReportRequest { +#[derive(XmlDeserialize, XmlDocument, Clone, Debug, PartialEq)] +pub(crate) enum ReportRequest { AddressbookMultiget(AddressbookMultigetRequest), SyncCollection(SyncCollectionRequest), } @@ -40,7 +31,7 @@ pub async fn route_report_addressbook( return Err(Error::Unauthorized); } - let request: ReportRequest = quick_xml::de::from_str(&body)?; + let request = ReportRequest::parse_str(&body).map_err(crate::Error::NewXmlDecodeError)?; Ok(match request.clone() { ReportRequest::AddressbookMultiget(addr_multiget) => { @@ -67,3 +58,40 @@ pub async fn route_report_addressbook( } }) } + +#[cfg(test)] +mod tests { + use rustical_dav::xml::{PropElement, Propname}; + use sync_collection::SyncLevel; + + use super::*; + + #[test] + fn test_xml_sync_collection() { + let report_request = ReportRequest::parse_str( + r#" + + + + 1 + + + + "#, + ) + .unwrap(); + assert_eq!( + report_request, + ReportRequest::SyncCollection(SyncCollectionRequest { + sync_token: "".to_owned(), + sync_level: SyncLevel::One, + prop: rustical_dav::xml::PropfindType::Prop(PropElement { + prop: vec![Propname { + name: "getetag".to_owned() + }] + }), + limit: None + }) + ) + } +} diff --git a/crates/carddav/src/addressbook/methods/report/sync_collection.rs b/crates/carddav/src/addressbook/methods/report/sync_collection.rs index e43c53c..ef1809b 100644 --- a/crates/carddav/src/addressbook/methods/report/sync_collection.rs +++ b/crates/carddav/src/addressbook/methods/report/sync_collection.rs @@ -13,28 +13,42 @@ use rustical_store::{ synctoken::{format_synctoken, parse_synctoken}, AddressbookStore, }; -use serde::Deserialize; +use rustical_xml::{Value, XmlDeserialize}; -#[derive(Deserialize, Clone, Debug)] -#[serde(rename_all = "kebab-case")] -enum SyncLevel { - #[serde(rename = "1")] +#[derive(Clone, Debug, PartialEq)] +pub(crate) enum SyncLevel { One, Infinity, } -#[derive(Deserialize, Clone, Debug)] -#[serde(rename_all = "kebab-case")] -#[allow(dead_code)] +impl Value for SyncLevel { + fn deserialize(val: &str) -> Result { + Ok(match val { + "1" => Self::One, + "Infinity" => Self::Infinity, + // TODO: proper error + _ => return Err(rustical_xml::XmlDeError::UnknownError), + }) + } + fn serialize(&self) -> String { + match self { + SyncLevel::One => "1", + SyncLevel::Infinity => "Infinity", + } + .to_owned() + } +} + +#[derive(XmlDeserialize, Clone, Debug, PartialEq)] // // // -pub struct SyncCollectionRequest { - sync_token: String, - sync_level: SyncLevel, - #[serde(flatten)] +pub(crate) struct SyncCollectionRequest { + pub(crate) sync_token: String, + pub(crate) sync_level: SyncLevel, + #[xml(ty = "untagged")] pub prop: PropfindType, - limit: Option, + pub(crate) limit: Option, } pub async fn handle_sync_collection( @@ -53,7 +67,10 @@ pub async fn handle_sync_collection( PropfindType::Propname => { vec!["propname".to_owned()] } - PropfindType::Prop(PropElement { prop: prop_tags }) => prop_tags.into_inner(), + PropfindType::Prop(PropElement { prop: prop_tags }) => prop_tags + .into_iter() + .map(|propname| propname.name) + .collect(), }; let props: Vec<&str> = props.iter().map(String::as_str).collect(); diff --git a/crates/carddav/src/addressbook/resource.rs b/crates/carddav/src/addressbook/resource.rs index 39b9ce7..1a42735 100644 --- a/crates/carddav/src/addressbook/resource.rs +++ b/crates/carddav/src/addressbook/resource.rs @@ -1,5 +1,5 @@ use super::methods::mkcol::route_mkcol; -// use super::methods::report::route_report_addressbook; +use super::methods::report::route_report_addressbook; use super::prop::{SupportedAddressData, SupportedReportSet}; use crate::address_object::resource::AddressObjectResource; use crate::principal::PrincipalResource; @@ -242,9 +242,8 @@ impl ResourceService for AddressbookResourceServi #[inline] fn actix_additional_routes(res: actix_web::Resource) -> actix_web::Resource { let mkcol_method = web::method(Method::from_str("MKCOL").unwrap()); - // TODO: Re-enable REPORT - // let report_method = web::method(Method::from_str("REPORT").unwrap()); + let report_method = web::method(Method::from_str("REPORT").unwrap()); res.route(mkcol_method.to(route_mkcol::)) - // .route(report_method.to(route_report_addressbook::)) + .route(report_method.to(route_report_addressbook::)) } } diff --git a/crates/carddav/src/error.rs b/crates/carddav/src/error.rs index 05fb51b..19802ad 100644 --- a/crates/carddav/src/error.rs +++ b/crates/carddav/src/error.rs @@ -18,6 +18,9 @@ pub enum Error { #[error(transparent)] DavError(#[from] rustical_dav::Error), + #[error(transparent)] + NewXmlDecodeError(#[from] rustical_xml::XmlDeError), + #[error(transparent)] XmlDecodeError(#[from] quick_xml::DeError), @@ -35,6 +38,7 @@ impl actix_web::ResponseError for Error { }, Error::DavError(err) => err.status_code(), Error::Unauthorized => StatusCode::UNAUTHORIZED, + Error::NewXmlDecodeError(_) => StatusCode::BAD_REQUEST, Error::XmlDecodeError(_) => StatusCode::BAD_REQUEST, Error::NotImplemented => StatusCode::INTERNAL_SERVER_ERROR, Error::Other(_) => StatusCode::INTERNAL_SERVER_ERROR, diff --git a/crates/dav/src/error.rs b/crates/dav/src/error.rs index cda9005..e8abcb4 100644 --- a/crates/dav/src/error.rs +++ b/crates/dav/src/error.rs @@ -19,6 +19,9 @@ pub enum Error { #[error("prop is read-only")] PropReadOnly, + #[error(transparent)] + NewXmlDeserializationError(#[from] rustical_xml::XmlDeError), + #[error(transparent)] XmlDeserializationError(#[from] quick_xml::DeError), @@ -33,6 +36,7 @@ impl actix_web::error::ResponseError for Error { Self::NotFound => StatusCode::NOT_FOUND, Self::BadRequest(_) => StatusCode::BAD_REQUEST, Self::Unauthorized => StatusCode::UNAUTHORIZED, + Self::NewXmlDeserializationError(_) => StatusCode::BAD_REQUEST, Self::XmlDeserializationError(_) => StatusCode::BAD_REQUEST, Self::XmlSerializationError(_) => StatusCode::BAD_REQUEST, Error::PropReadOnly => StatusCode::CONFLICT, diff --git a/crates/dav/src/resource/methods/propfind.rs b/crates/dav/src/resource/methods/propfind.rs index 1b49b31..8a8dd73 100644 --- a/crates/dav/src/resource/methods/propfind.rs +++ b/crates/dav/src/resource/methods/propfind.rs @@ -6,22 +6,16 @@ use crate::resource::Resource; use crate::resource::ResourceService; use crate::xml::MultistatusElement; use crate::xml::PropElement; +use crate::xml::PropfindElement; use crate::xml::PropfindType; use crate::Error; use actix_web::web::Path; use actix_web::HttpRequest; use rustical_store::auth::User; -use serde::Deserialize; +use rustical_xml::de::XmlDocument; use tracing::instrument; use tracing_actix_web::RootSpan; -#[derive(Deserialize, Clone, Debug)] -#[serde(rename_all = "kebab-case")] -struct PropfindElement { - #[serde(rename = "$value")] - prop: PropfindType, -} - #[instrument(parent = root_span.id(), skip(path_components, req, root_span))] #[allow(clippy::type_complexity)] pub(crate) async fn route_propfind( @@ -48,7 +42,7 @@ pub(crate) async fn route_propfind( // A request body is optional. If empty we MUST return all props let propfind: PropfindElement = if !body.is_empty() { - quick_xml::de::from_str(&body).map_err(Error::XmlDeserializationError)? + PropfindElement::parse_str(&body).map_err(Error::NewXmlDeserializationError)? } else { PropfindElement { prop: PropfindType::Allprop, @@ -58,7 +52,10 @@ pub(crate) async fn route_propfind( let props = match propfind.prop { PropfindType::Allprop => vec!["allprop".to_owned()], PropfindType::Propname => vec!["propname".to_owned()], - PropfindType::Prop(PropElement { prop: prop_tags }) => prop_tags.into_inner(), + PropfindType::Prop(PropElement { prop: prop_tags }) => prop_tags + .into_iter() + .map(|propname| propname.name) + .collect(), }; let props: Vec<&str> = props.iter().map(String::as_str).collect(); diff --git a/crates/dav/src/xml/mod.rs b/crates/dav/src/xml/mod.rs index 46dbbe6..88d28aa 100644 --- a/crates/dav/src/xml/mod.rs +++ b/crates/dav/src/xml/mod.rs @@ -4,7 +4,7 @@ mod resourcetype; pub mod tag_list; pub mod tag_name; -pub use propfind::{PropElement, PropfindType}; +pub use propfind::{PropElement, PropfindElement, PropfindType, Propname}; use derive_more::derive::From; pub use multistatus::MultistatusElement; diff --git a/crates/dav/src/xml/propfind.rs b/crates/dav/src/xml/propfind.rs index 0f9441b..a401b85 100644 --- a/crates/dav/src/xml/propfind.rs +++ b/crates/dav/src/xml/propfind.rs @@ -1,15 +1,26 @@ -use super::TagList; -use serde::Deserialize; +use rustical_xml::XmlDeserialize; +use rustical_xml::XmlRootTag; -#[derive(Deserialize, Clone, Debug)] -#[serde(rename_all = "kebab-case")] -pub struct PropElement { - #[serde(flatten)] - pub prop: TagList, +#[derive(Debug, Clone, XmlDeserialize, XmlRootTag, PartialEq)] +#[xml(root = b"propfind", ns = b"DAV:")] +pub struct PropfindElement { + #[xml(ty = "untagged")] + pub prop: PropfindType, } -#[derive(Deserialize, Clone, Debug)] -#[serde(rename_all = "kebab-case")] +#[derive(Debug, Clone, XmlDeserialize, PartialEq)] +pub struct PropElement { + #[xml(ty = "untagged", flatten)] + pub prop: Vec, +} + +#[derive(Debug, Clone, XmlDeserialize, PartialEq)] +pub struct Propname { + #[xml(ty = "tag_name")] + pub name: String, +} + +#[derive(Debug, Clone, XmlDeserialize, PartialEq)] pub enum PropfindType { Propname, Allprop, diff --git a/crates/dav/src/xml/tag_list.rs b/crates/dav/src/xml/tag_list.rs index 6e64e66..ecfd06a 100644 --- a/crates/dav/src/xml/tag_list.rs +++ b/crates/dav/src/xml/tag_list.rs @@ -1,44 +1,10 @@ use derive_more::derive::From; use serde::ser::SerializeMap; - -use serde::{ - de::{MapAccess, Visitor}, - Deserialize, Serialize, -}; +use serde::Serialize; #[derive(Clone, Debug, PartialEq, From)] pub struct TagList(Vec); -struct TagListVisitor; - -impl<'de> Visitor<'de> for TagListVisitor { - type Value = TagList; - - fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { - formatter.write_str("TagList") - } - - fn visit_map(self, mut map: A) -> Result - where - A: MapAccess<'de>, - { - let mut tags = Vec::new(); - while let Some(key) = map.next_key::()? { - tags.push(key); - } - Ok(TagList(tags)) - } -} - -impl<'de> Deserialize<'de> for TagList { - fn deserialize(deserializer: D) -> Result - where - D: serde::Deserializer<'de>, - { - deserializer.deserialize_map(TagListVisitor) - } -} - impl Serialize for TagList { fn serialize(&self, serializer: S) -> Result where diff --git a/crates/dav/tests/propfind.rs b/crates/dav/tests/propfind.rs new file mode 100644 index 0000000..7479858 --- /dev/null +++ b/crates/dav/tests/propfind.rs @@ -0,0 +1,87 @@ +use rustical_dav::xml::{PropElement, PropfindElement, PropfindType, Propname}; +use rustical_xml::de::XmlDocument; + +#[test] +fn propfind_allprop() { + let propfind = PropfindElement::parse_str( + r#" + + + + "#, + ) + .unwrap(); + assert_eq!( + propfind, + PropfindElement { + prop: PropfindType::Allprop + } + ); +} + +#[test] +fn propfind_propname() { + let propfind = PropfindElement::parse_str( + r#" + + + + "#, + ) + .unwrap(); + assert_eq!( + propfind, + PropfindElement { + prop: PropfindType::Propname + } + ); +} + +#[test] +fn propfind_prop() { + let propfind = PropfindElement::parse_str( + r#" + + + + + + + "#, + ) + .unwrap(); + assert_eq!( + propfind, + PropfindElement { + prop: PropfindType::Prop(PropElement { + prop: vec![ + Propname { + name: "displayname".to_owned() + }, + Propname { + name: "color".to_owned() + }, + ] + }) + } + ); +} + +/// Example taken from DAVx5 +#[test] +fn propfind_decl() { + let propfind = PropfindElement::parse_str( + r#" + + + + + + + + + + + "# + ).unwrap(); +} diff --git a/crates/dav/tests/tag_list.rs b/crates/dav/tests/tag_list.rs deleted file mode 100644 index 4bb3a4a..0000000 --- a/crates/dav/tests/tag_list.rs +++ /dev/null @@ -1,48 +0,0 @@ -use rustical_dav::xml::TagList; -use serde::{Deserialize, Serialize}; - -const INPUT: &str = r#" - - - - -"#; - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] -#[serde(rename_all = "kebab-case")] -struct PropElement { - #[serde(flatten)] - tags: TagList, -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] -#[serde(rename_all = "kebab-case")] -struct Document { - prop: PropElement, -} - -fn expected_output() -> Document { - Document { - prop: PropElement { - tags: vec!["nicename".to_owned(), "anotherprop".to_owned()].into(), - }, - } -} - -#[test] -fn test_tagname_deserialize() { - let result: Document = quick_xml::de::from_str(INPUT).unwrap(); - assert_eq!(result, expected_output()); -} - -#[test] -fn test_tagname_serialize() { - let mut result = String::new(); - let mut ser = quick_xml::se::Serializer::new(&mut result); - ser.indent(' ', 4); - - let to_serialize = &expected_output(); - to_serialize.serialize(ser).unwrap(); - - assert_eq!(result, INPUT); -} diff --git a/crates/store/Cargo.toml b/crates/store/Cargo.toml index 7805306..015c9cf 100644 --- a/crates/store/Cargo.toml +++ b/crates/store/Cargo.toml @@ -24,6 +24,7 @@ tracing = { workspace = true } pbkdf2 = { workspace = true } chrono-tz = { workspace = true } derive_more = { workspace = true } +rustical_xml.workspace = true [dev-dependencies] rstest = { workspace = true } diff --git a/crates/store/src/calendar/timestamp.rs b/crates/store/src/calendar/timestamp.rs index 596ae04..481c092 100644 --- a/crates/store/src/calendar/timestamp.rs +++ b/crates/store/src/calendar/timestamp.rs @@ -1,12 +1,13 @@ use crate::Error; use chrono::{DateTime, Duration, NaiveDate, NaiveDateTime, NaiveTime, Utc}; use chrono_tz::Tz; +use derive_more::derive::Deref; use ical::{ parser::{ical::component::IcalTimeZone, Component}, property::Property, }; use lazy_static::lazy_static; -use serde::{Deserialize, Deserializer}; +use rustical_xml::Value; use std::{collections::HashMap, ops::Add}; lazy_static! { @@ -17,6 +18,24 @@ const LOCAL_DATE_TIME: &str = "%Y%m%dT%H%M%S"; const UTC_DATE_TIME: &str = "%Y%m%dT%H%M%SZ"; const LOCAL_DATE: &str = "%Y%m%d"; +#[derive(Debug, Clone, Deref, PartialEq)] +pub struct UtcDateTime(DateTime); + +impl Value for UtcDateTime { + fn deserialize(val: &str) -> Result { + let input = ::deserialize(val)?; + Ok(Self( + NaiveDateTime::parse_from_str(&input, UTC_DATE_TIME) + // TODO: proper error + .map_err(|_| rustical_xml::XmlDeError::UnknownError)? + .and_utc(), + )) + } + fn serialize(&self) -> String { + format!("{}", self.0.format(UTC_DATE_TIME)) + } +} + #[derive(Debug, Clone)] pub enum CalDateTime { // Form 1, example: 19980118T230000 @@ -29,22 +48,6 @@ pub enum CalDateTime { Date(NaiveDate), } -pub fn deserialize_utc_datetime<'de, D>(deserializer: D) -> Result>, D::Error> -where - D: Deserializer<'de>, -{ - type Inner = Option; - Ok(if let Some(input) = Inner::deserialize(deserializer)? { - Some( - NaiveDateTime::parse_from_str(&input, UTC_DATE_TIME) - .map_err(|err| serde::de::Error::custom(err.to_string()))? - .and_utc(), - ) - } else { - None - }) -} - impl Add for CalDateTime { type Output = Self; diff --git a/crates/xml/derive/src/de/de_enum.rs b/crates/xml/derive/src/de/de_enum.rs index fc548e9..aa595ab 100644 --- a/crates/xml/derive/src/de/de_enum.rs +++ b/crates/xml/derive/src/de/de_enum.rs @@ -238,16 +238,19 @@ impl Enum { use ::quick_xml::events::Event; let mut buf = Vec::new(); - let event = reader.read_event_into(&mut buf)?; - let empty = matches!(event, Event::Empty(_)); + loop { + let event = reader.read_event_into(&mut buf)?; + let empty = matches!(event, Event::Empty(_)); - match event { - Event::Start(start) | Event::Empty(start) => { - return Self::deserialize(&mut reader, &start, empty); - } - _ => {} - }; - Err(::rustical_xml::XmlDeError::UnknownError) + match event { + Event::Decl(_) => { /* ignore this */ } + Event::Comment(_) => { /* ignore this */ } + Event::Start(start) | Event::Empty(start) => { + return Self::deserialize(&mut reader, &start, empty); + } + _ => return Err(::rustical_xml::XmlDeError::UnknownError), + }; + } } } } diff --git a/crates/xml/derive/src/de/field.rs b/crates/xml/derive/src/de/field.rs index 6eeacde..334d606 100644 --- a/crates/xml/derive/src/de/field.rs +++ b/crates/xml/derive/src/de/field.rs @@ -71,23 +71,47 @@ impl Field { .expect("tuple structs not supported") } - /// Field type - pub fn ty(&self) -> &syn::Type { + fn is_optional(&self) -> bool { + if let syn::Type::Path(syn::TypePath { path, .. }) = &self.field.ty { + if path.segments.len() != 1 { + return false; + } + let type_ident = &path.segments.first().unwrap().ident; + let option: syn::Ident = syn::parse_str("Option").unwrap(); + return type_ident == &option; + } + false + } + + /// The type to deserialize to + /// - type Option => optional: deserialize with T + /// - flatten Vec: deserialize with T + /// - deserialize with T + pub fn deserializer_type(&self) -> &syn::Type { + if self.is_optional() { + return get_generic_type(&self.field.ty).unwrap(); + } + if self.attrs.flatten.is_present() { + return get_generic_type(&self.field.ty).expect("flatten attribute only implemented for explicit generics (rustical_xml will assume the first generic as the inner type)"); + } &self.field.ty } /// Field in the builder struct for the deserializer pub fn builder_field(&self) -> proc_macro2::TokenStream { let field_ident = self.field_ident(); - let ty = self.ty(); + let ty = self.deserializer_type(); - let builder_field_type = match (self.attrs.flatten.is_present(), &self.attrs.default) { - (_, Some(_default)) => quote! { #ty }, - (true, None) => { - let generic_type = get_generic_type(ty).expect("flatten attribute only implemented for explicit generics (rustical_xml will assume the first generic as the inner type)"); - quote! { Vec<#generic_type> } - } - (false, None) => quote! { Option<#ty> }, + let builder_field_type = match ( + self.attrs.flatten.is_present(), + &self.attrs.default, + self.is_optional(), + ) { + (_, Some(_default), true) => panic!("default value for Option doesn't make sense"), + (_, Some(_default), false) => quote! { #ty }, + (true, None, true) => panic!("cannot flatten Option"), + (true, None, false) => quote! { Vec<#ty> }, + (false, None, _) => quote! { Option<#ty> }, }; quote! { #field_ident: #builder_field_type } @@ -96,11 +120,16 @@ impl Field { /// Field initialiser in the builder struct for the deserializer pub fn builder_field_init(&self) -> proc_macro2::TokenStream { let field_ident = self.field_ident(); - let builder_field_initialiser = match (self.attrs.flatten.is_present(), &self.attrs.default) - { - (_, Some(default)) => quote! { #default() }, - (true, None) => quote! { vec![] }, - (false, None) => quote! { None }, + let builder_field_initialiser = match ( + self.attrs.flatten.is_present(), + &self.attrs.default, + self.is_optional(), + ) { + (_, Some(_), true) => unreachable!(), + (_, Some(default), false) => quote! { #default() }, + (true, None, true) => unreachable!(), + (true, None, false) => quote! { vec![] }, + (false, None, _) => quote! { None }, }; quote! { #field_ident: #builder_field_initialiser } } @@ -111,10 +140,18 @@ impl Field { let builder_value = match ( self.attrs.flatten.is_present(), self.attrs.default.is_some(), + self.is_optional(), ) { - (true, _) => quote! { FromIterator::from_iter(builder.#field_ident.into_iter()) }, - (false, true) => quote! { builder.#field_ident }, - (false, false) => quote! { builder.#field_ident.expect("todo: handle missing field") }, + (true, _, true) => unreachable!(), + (true, _, false) => { + quote! { FromIterator::from_iter(builder.#field_ident.into_iter()) } + } + (false, true, true) => unreachable!(), + (false, true, false) => quote! { builder.#field_ident }, + (false, false, true) => quote! { builder.#field_ident }, + (false, false, false) => { + quote! { builder.#field_ident.expect("todo: handle missing field") } + } }; quote! { #field_ident: #builder_value } } @@ -136,22 +173,14 @@ impl Field { let field_name = self.xml_name(); let field_ident = self.field_ident(); - let deserializer = self.ty(); + let deserializer = self.deserializer_type(); let value = quote! { <#deserializer as rustical_xml::XmlDeserialize>::deserialize(reader, &start, empty)? }; let assignment = match (self.attrs.flatten.is_present(), &self.attrs.default) { (true, _) => { - // TODO: Make nicer, watch out with deserializer typing - let deserializer = get_generic_type(self.ty()).expect("flatten attribute only implemented for explicit generics (rustical_xml will assume the first generic as the inner type)"); - quote! { - builder.#field_ident.push(<#deserializer as rustical_xml::XmlDeserialize>::deserialize(reader, &start, empty)?); - } + quote! { builder.#field_ident.push(#value); } } - (false, Some(_default)) => quote! { - builder.#field_ident = #value; - }, - (false, None) => quote! { - builder.#field_ident = Some(#value); - }, + (false, Some(_default)) => quote! { builder.#field_ident = #value; }, + (false, None) => quote! { builder.#field_ident = Some(#value); }, }; Some(quote! { @@ -164,20 +193,16 @@ impl Field { return None; } let field_ident = self.field_ident(); - let deserializer = self.ty(); + let deserializer = self.deserializer_type(); + let value = quote! { <#deserializer as rustical_xml::XmlDeserialize>::deserialize(reader, &start, empty)? }; Some(if self.attrs.flatten.is_present() { - let deserializer = get_generic_type(self.ty()).expect("flatten attribute only implemented for explicit generics (rustical_xml will assume the first generic as the inner type)"); quote! { - _ => { - builder.#field_ident.push(<#deserializer as rustical_xml::XmlDeserialize>::deserialize(reader, &start, empty)?); - } + _ => { builder.#field_ident.push(#value); } } } else { quote! { - _ => { - builder.#field_ident = Some(<#deserializer as rustical_xml::XmlDeserialize>::deserialize(reader, &start, empty)?); - } + _ => { builder.#field_ident = Some(#value); } } }) } diff --git a/crates/xml/src/de.rs b/crates/xml/src/de.rs index 513d179..62e2e04 100644 --- a/crates/xml/src/de.rs +++ b/crates/xml/src/de.rs @@ -74,34 +74,39 @@ impl XmlDocument for T { Self: XmlDeserialize, { let mut buf = Vec::new(); - let event = reader.read_event_into(&mut buf)?; - let empty = matches!(event, Event::Empty(_)); - match event { - Event::Start(start) | Event::Empty(start) => { - let (ns, name) = reader.resolve_element(start.name()); - let matches = match (Self::root_ns(), &ns, name) { - // Wrong tag - (_, _, name) if name.as_ref() != Self::root_tag() => false, - // Wrong namespace - (Some(root_ns), ns, _) if &ResolveResult::Bound(Namespace(root_ns)) != ns => { - false - } - _ => true, - }; - if !matches { - let root_ns = Self::root_ns(); - return Err(XmlDeError::InvalidTag( - format!("{ns:?}"), - String::from_utf8_lossy(name.as_ref()).to_string(), - format!("{root_ns:?}"), - String::from_utf8_lossy(Self::root_tag()).to_string(), - )); - }; + loop { + let event = reader.read_event_into(&mut buf)?; + let empty = matches!(event, Event::Empty(_)); + match event { + Event::Decl(_) => { /* ignore this */ } + Event::Comment(_) => { /* ignore this */ } + Event::Start(start) | Event::Empty(start) => { + let (ns, name) = reader.resolve_element(start.name()); + let matches = match (Self::root_ns(), &ns, name) { + // Wrong tag + (_, _, name) if name.as_ref() != Self::root_tag() => false, + // Wrong namespace + (Some(root_ns), ns, _) + if &ResolveResult::Bound(Namespace(root_ns)) != ns => + { + false + } + _ => true, + }; + if !matches { + let root_ns = Self::root_ns(); + return Err(XmlDeError::InvalidTag( + format!("{ns:?}"), + String::from_utf8_lossy(name.as_ref()).to_string(), + format!("{root_ns:?}"), + String::from_utf8_lossy(Self::root_tag()).to_string(), + )); + }; - return Self::deserialize(&mut reader, &start, empty); - } - _ => {} - }; - Err(XmlDeError::UnknownError) + return Self::deserialize(&mut reader, &start, empty); + } + _ => return Err(XmlDeError::UnknownError), + }; + } } } diff --git a/crates/xml/src/lib.rs b/crates/xml/src/lib.rs index 62997df..5b57e55 100644 --- a/crates/xml/src/lib.rs +++ b/crates/xml/src/lib.rs @@ -12,15 +12,15 @@ pub use de::XmlRootTag; pub use se::XmlSerialize; pub use value::Value; -impl XmlDeserialize for Option { - fn deserialize( - reader: &mut quick_xml::NsReader, - start: &BytesStart, - empty: bool, - ) -> Result { - Ok(Some(T::deserialize(reader, start, empty)?)) - } -} +// impl XmlDeserialize for Option { +// fn deserialize( +// reader: &mut quick_xml::NsReader, +// start: &BytesStart, +// empty: bool, +// ) -> Result { +// Ok(Some(T::deserialize(reader, start, empty)?)) +// } +// } #[derive(Debug, Clone, PartialEq)] pub struct Unit; diff --git a/crates/xml/src/value.rs b/crates/xml/src/value.rs index 49a6c6a..a264477 100644 --- a/crates/xml/src/value.rs +++ b/crates/xml/src/value.rs @@ -1,6 +1,5 @@ use quick_xml::events::{BytesStart, Event}; use std::num::{ParseFloatError, ParseIntError}; -use std::str::FromStr; use std::{convert::Infallible, io::BufRead}; use thiserror::Error; @@ -21,6 +20,21 @@ pub trait Value: Sized { fn deserialize(val: &str) -> Result; } +// impl Value for Option { +// fn serialize(&self) -> String { +// match self { +// Some(inner) => inner.serialize(), +// None => "".to_owned(), +// } +// } +// fn deserialize(val: &str) -> Result { +// match val { +// "" => Ok(None), +// val => Ok(Some(T::deserialize(val)?)), +// } +// } +// } + macro_rules! impl_value_parse { ($t:ty) => { impl Value for $t { @@ -54,7 +68,7 @@ impl_value_parse!(usize); impl XmlDeserialize for T { fn deserialize( reader: &mut quick_xml::NsReader, - start: &BytesStart, + _start: &BytesStart, empty: bool, ) -> Result { let mut string = String::new(); @@ -64,17 +78,19 @@ impl XmlDeserialize for T { loop { match reader.read_event_into(&mut buf)? { Event::Text(text) => { - if !start.is_empty() { + if !string.is_empty() { // Content already written - return Err(XmlDeError::UnsupportedEvent("todo")); + return Err(XmlDeError::UnsupportedEvent("content already written")); } string = String::from_utf8_lossy(text.as_ref()).to_string(); } + Event::End(_) => break, + Event::Eof => return Err(XmlDeError::Eof), _ => return Err(XmlDeError::UnsupportedEvent("todo")), }; } } - ::deserialize(&string) + Value::deserialize(&string) } } diff --git a/crates/xml/tests/de_struct.rs b/crates/xml/tests/de_struct.rs index 6f79b94..1f10ca0 100644 --- a/crates/xml/tests/de_struct.rs +++ b/crates/xml/tests/de_struct.rs @@ -80,7 +80,6 @@ fn test_struct_optional_field() { #[derive(Debug, XmlDeserialize, XmlRootTag, PartialEq)] #[xml(root = b"document")] struct Document { - #[xml(default = "Default::default")] child: Option, }