diff --git a/crates/caldav/src/calendar/resource.rs b/crates/caldav/src/calendar/resource.rs index 880aec8..6f9dee8 100644 --- a/crates/caldav/src/calendar/resource.rs +++ b/crates/caldav/src/calendar/resource.rs @@ -24,7 +24,7 @@ use serde::Serialize; use sha2::{Digest, Sha256}; use std::str::FromStr; use std::sync::Arc; -use strum::{EnumDiscriminants, EnumString, VariantNames}; +use strum::{EnumDiscriminants, EnumString, IntoStaticStr, VariantNames}; pub struct CalendarResourceService { cal_store: Arc, @@ -32,10 +32,10 @@ pub struct CalendarResourceService { calendar_id: String, } -#[derive(Default, XmlDeserialize, Serialize, PartialEq, EnumDiscriminants)] +#[derive(XmlDeserialize, Serialize, PartialEq, EnumDiscriminants, Clone)] #[strum_discriminants( name(CalendarPropName), - derive(EnumString, VariantNames), + derive(EnumString, VariantNames, IntoStaticStr), strum(serialize_all = "kebab-case") )] #[serde(rename_all = "kebab-case")] @@ -90,11 +90,6 @@ pub enum CalendarProp { Getctag(String), #[serde(rename = "CS:source", alias = "source")] Source(Option), - - #[serde(other)] - #[strum_discriminants(strum(disabled))] - #[default] - Invalid, } #[derive(Clone, Debug, From, Into)] @@ -168,9 +163,6 @@ impl Resource for CalendarResource { CalendarPropName::Source => { CalendarProp::Source(self.0.subscription_url.to_owned().map(HrefElement::from)) } - CalendarPropName::Invalid => { - return Err(rustical_dav::Error::BadRequest("invalid prop name".to_owned()).into()) - } }) } @@ -209,7 +201,6 @@ impl Resource for CalendarResource { CalendarProp::Getctag(_) => Err(rustical_dav::Error::PropReadOnly), // Converting between a calendar subscription calendar and a normal one would be weird CalendarProp::Source(_) => Err(rustical_dav::Error::PropReadOnly), - CalendarProp::Invalid => Err(rustical_dav::Error::PropReadOnly), } } @@ -248,7 +239,6 @@ impl Resource for CalendarResource { CalendarPropName::Getctag => Err(rustical_dav::Error::PropReadOnly), // Converting a calendar subscription calendar into a normal one would be weird CalendarPropName::Source => Err(rustical_dav::Error::PropReadOnly), - CalendarPropName::Invalid => Err(rustical_dav::Error::PropReadOnly), } } diff --git a/crates/caldav/src/calendar_object/resource.rs b/crates/caldav/src/calendar_object/resource.rs index eba2d27..50d8cfc 100644 --- a/crates/caldav/src/calendar_object/resource.rs +++ b/crates/caldav/src/calendar_object/resource.rs @@ -11,7 +11,7 @@ use rustical_store::{auth::User, CalendarObject, CalendarStore}; use rustical_xml::XmlDeserialize; use serde::{Deserialize, Serialize}; use std::sync::Arc; -use strum::{EnumDiscriminants, EnumString, VariantNames}; +use strum::{EnumDiscriminants, EnumString, IntoStaticStr, VariantNames}; pub struct CalendarObjectResourceService { cal_store: Arc, @@ -20,10 +20,10 @@ pub struct CalendarObjectResourceService { object_id: String, } -#[derive(Default, XmlDeserialize, Serialize, PartialEq, EnumDiscriminants)] +#[derive(XmlDeserialize, Serialize, PartialEq, EnumDiscriminants, Clone)] #[strum_discriminants( name(CalendarObjectPropName), - derive(EnumString, VariantNames), + derive(EnumString, VariantNames, IntoStaticStr), strum(serialize_all = "kebab-case") )] #[serde(rename_all = "kebab-case")] @@ -35,12 +35,6 @@ pub enum CalendarObjectProp { // CalDAV (RFC 4791) #[serde(rename = "C:calendar-data")] CalendarData(String), - - #[serde(other)] - #[xml(other)] - #[strum_discriminants(strum(disabled))] - #[default] - Invalid, } #[derive(Clone, From, Into)] @@ -73,14 +67,12 @@ impl Resource for CalendarObjectResource { CalendarObjectPropName::Getcontenttype => { CalendarObjectProp::Getcontenttype("text/calendar;charset=utf-8".to_owned()) } - CalendarObjectPropName::Invalid => { - return Err(rustical_dav::Error::BadRequest("invalid prop name".to_owned()).into()) - } }) } #[inline] fn resource_name() -> &'static str { + let a: CalendarObjectPropName = CalendarObjectProp::Getetag("".to_owned()).into(); "caldav_calendar_object" } diff --git a/crates/caldav/src/principal/mod.rs b/crates/caldav/src/principal/mod.rs index 5de2be4..014fc2d 100644 --- a/crates/caldav/src/principal/mod.rs +++ b/crates/caldav/src/principal/mod.rs @@ -12,7 +12,7 @@ use rustical_store::CalendarStore; use rustical_xml::XmlDeserialize; use serde::Serialize; use std::sync::Arc; -use strum::{EnumDiscriminants, EnumString, VariantNames}; +use strum::{EnumDiscriminants, EnumString, IntoStaticStr, VariantNames}; pub struct PrincipalResourceService { principal: String, @@ -24,10 +24,10 @@ pub struct PrincipalResource { principal: String, } -#[derive(Default, XmlDeserialize, Serialize, PartialEq, EnumDiscriminants)] +#[derive(XmlDeserialize, Serialize, PartialEq, EnumDiscriminants, Clone)] #[strum_discriminants( name(PrincipalPropName), - derive(EnumString, VariantNames), + derive(EnumString, VariantNames, IntoStaticStr), strum(serialize_all = "kebab-case") )] #[serde(rename_all = "kebab-case")] @@ -42,12 +42,6 @@ pub enum PrincipalProp { CalendarHomeSet(HrefElement), #[serde(rename = "C:calendar-user-address-set")] CalendarUserAddressSet(HrefElement), - - #[serde(other)] - #[xml(other)] - #[strum_discriminants(strum(disabled))] - #[default] - Invalid, } impl PrincipalResource { @@ -80,9 +74,6 @@ impl Resource for PrincipalResource { PrincipalPropName::CalendarUserAddressSet => { PrincipalProp::CalendarUserAddressSet(principal_href) } - PrincipalPropName::Invalid => { - return Err(rustical_dav::Error::BadRequest("invalid prop name".to_owned()).into()) - } }) } diff --git a/crates/carddav/src/address_object/resource.rs b/crates/carddav/src/address_object/resource.rs index 7050167..ad95b91 100644 --- a/crates/carddav/src/address_object/resource.rs +++ b/crates/carddav/src/address_object/resource.rs @@ -10,7 +10,7 @@ use rustical_store::{auth::User, AddressObject, AddressbookStore}; use rustical_xml::XmlDeserialize; use serde::{Deserialize, Serialize}; use std::sync::Arc; -use strum::{EnumDiscriminants, EnumString, VariantNames}; +use strum::{EnumDiscriminants, EnumString, IntoStaticStr, VariantNames}; use super::methods::{get_object, put_object}; @@ -21,10 +21,10 @@ pub struct AddressObjectResourceService { object_id: String, } -#[derive(Default, XmlDeserialize, Serialize, PartialEq, EnumDiscriminants)] +#[derive(XmlDeserialize, Serialize, PartialEq, EnumDiscriminants, Clone)] #[strum_discriminants( name(AddressObjectPropName), - derive(EnumString, VariantNames), + derive(EnumString, VariantNames, IntoStaticStr), strum(serialize_all = "kebab-case") )] #[serde(rename_all = "kebab-case")] @@ -36,12 +36,6 @@ pub enum AddressObjectProp { // CalDAV (RFC 4791) #[serde(rename = "CARD:address-data")] AddressData(String), - - #[serde(other)] - #[xml(other)] - #[strum_discriminants(strum(disabled))] - #[default] - Invalid, } #[derive(Clone, From, Into)] @@ -74,9 +68,6 @@ impl Resource for AddressObjectResource { AddressObjectPropName::Getcontenttype => { AddressObjectProp::Getcontenttype("text/vcard;charset=utf-8".to_owned()) } - AddressObjectPropName::Invalid => { - return Err(rustical_dav::Error::BadRequest("invalid prop name".to_owned()).into()) - } }) } diff --git a/crates/carddav/src/addressbook/resource.rs b/crates/carddav/src/addressbook/resource.rs index a7a15b3..be50ddd 100644 --- a/crates/carddav/src/addressbook/resource.rs +++ b/crates/carddav/src/addressbook/resource.rs @@ -18,7 +18,7 @@ use rustical_xml::XmlDeserialize; use serde::Serialize; use std::str::FromStr; use std::sync::Arc; -use strum::{EnumDiscriminants, EnumString, VariantNames}; +use strum::{EnumDiscriminants, EnumString, IntoStaticStr, VariantNames}; pub struct AddressbookResourceService { addr_store: Arc, @@ -26,11 +26,11 @@ pub struct AddressbookResourceService { addressbook_id: String, } -#[derive(Default, XmlDeserialize, Serialize, PartialEq, EnumDiscriminants)] +#[derive(XmlDeserialize, Serialize, PartialEq, EnumDiscriminants, Clone)] #[serde(rename_all = "kebab-case")] #[strum_discriminants( name(AddressbookPropName), - derive(EnumString, VariantNames), + derive(EnumString, VariantNames, IntoStaticStr), strum(serialize_all = "kebab-case") )] pub enum AddressbookProp { @@ -61,12 +61,6 @@ pub enum AddressbookProp { // Didn't find the spec Getctag(String), - - #[serde(other)] - #[xml(other)] - #[strum_discriminants(strum(disabled))] - #[default] - Invalid, } #[derive(Clone, Debug, From, Into)] @@ -107,9 +101,6 @@ impl Resource for AddressbookResource { } AddressbookPropName::SyncToken => AddressbookProp::SyncToken(self.0.format_synctoken()), AddressbookPropName::Getctag => AddressbookProp::Getctag(self.0.format_synctoken()), - AddressbookPropName::Invalid => { - return Err(rustical_dav::Error::BadRequest("invalid prop name".to_owned()).into()) - } }) } @@ -129,7 +120,6 @@ impl Resource for AddressbookResource { AddressbookProp::SupportedAddressData(_) => Err(rustical_dav::Error::PropReadOnly), AddressbookProp::SyncToken(_) => Err(rustical_dav::Error::PropReadOnly), AddressbookProp::Getctag(_) => Err(rustical_dav::Error::PropReadOnly), - AddressbookProp::Invalid => Err(rustical_dav::Error::PropReadOnly), } } @@ -149,7 +139,6 @@ impl Resource for AddressbookResource { AddressbookPropName::SupportedAddressData => Err(rustical_dav::Error::PropReadOnly), AddressbookPropName::SyncToken => Err(rustical_dav::Error::PropReadOnly), AddressbookPropName::Getctag => Err(rustical_dav::Error::PropReadOnly), - AddressbookPropName::Invalid => Err(rustical_dav::Error::PropReadOnly), } } diff --git a/crates/carddav/src/principal/mod.rs b/crates/carddav/src/principal/mod.rs index 9db6f14..f0fef53 100644 --- a/crates/carddav/src/principal/mod.rs +++ b/crates/carddav/src/principal/mod.rs @@ -12,7 +12,7 @@ use rustical_store::AddressbookStore; use rustical_xml::XmlDeserialize; use serde::Serialize; use std::sync::Arc; -use strum::{EnumDiscriminants, EnumString, VariantNames}; +use strum::{EnumDiscriminants, EnumString, IntoStaticStr, VariantNames}; pub struct PrincipalResourceService { principal: String, @@ -24,10 +24,10 @@ pub struct PrincipalResource { principal: String, } -#[derive(Default, XmlDeserialize, Serialize, PartialEq, EnumDiscriminants)] +#[derive(XmlDeserialize, Serialize, PartialEq, EnumDiscriminants, Clone)] #[strum_discriminants( name(PrincipalPropName), - derive(EnumString, VariantNames), + derive(EnumString, VariantNames, IntoStaticStr), strum(serialize_all = "kebab-case") )] #[serde(rename_all = "kebab-case")] @@ -42,12 +42,6 @@ pub enum PrincipalProp { AddressbookHomeSet(HrefElement), #[serde(rename = "CARD:principal-address")] PrincipalAddress(Option), - - #[serde(other)] - #[xml(other)] - #[strum_discriminants(strum(disabled))] - #[default] - Invalid, } impl PrincipalResource { @@ -80,9 +74,6 @@ impl Resource for PrincipalResource { PrincipalProp::AddressbookHomeSet(principal_href) } PrincipalPropName::PrincipalAddress => PrincipalProp::PrincipalAddress(None), - PrincipalPropName::Invalid => { - return Err(rustical_dav::Error::BadRequest("invalid prop name".to_owned()).into()) - } }) } diff --git a/crates/dav/src/resource/invalid_property.rs b/crates/dav/src/resource/invalid_property.rs deleted file mode 100644 index b6216a8..0000000 --- a/crates/dav/src/resource/invalid_property.rs +++ /dev/null @@ -1,9 +0,0 @@ -pub trait InvalidProperty { - fn invalid_property(&self) -> bool; -} - -impl InvalidProperty for T { - fn invalid_property(&self) -> bool { - self == &T::default() - } -} diff --git a/crates/dav/src/resource/methods/proppatch.rs b/crates/dav/src/resource/methods/proppatch.rs index ab5de26..48ef887 100644 --- a/crates/dav/src/resource/methods/proppatch.rs +++ b/crates/dav/src/resource/methods/proppatch.rs @@ -1,5 +1,4 @@ use crate::privileges::UserPrivilege; -use crate::resource::InvalidProperty; use crate::resource::Resource; use crate::resource::ResourceService; use crate::xml::multistatus::{PropstatElement, PropstatWrapper, ResponseElement}; @@ -9,6 +8,7 @@ use crate::Error; use actix_web::http::StatusCode; use actix_web::{web::Path, HttpRequest}; use rustical_store::auth::User; +use rustical_xml::Unparsed; use rustical_xml::XmlDeserialize; use rustical_xml::XmlDocument; use rustical_xml::XmlRootTag; @@ -16,6 +16,21 @@ use std::str::FromStr; use tracing::instrument; use tracing_actix_web::RootSpan; +#[derive(XmlDeserialize, Clone, Debug)] +#[xml(untagged)] +enum SetPropertyPropWrapper { + Valid(T), + Invalid(Unparsed), +} + +// We are +#[derive(XmlDeserialize, Clone, Debug)] +struct SetPropertyPropWrapperWrapper { + #[xml(ty = "untagged")] + property: SetPropertyPropWrapper, +} + +// We are #[derive(XmlDeserialize, Clone, Debug)] struct SetPropertyElement { prop: T, @@ -47,10 +62,6 @@ enum Operation { #[derive(XmlDeserialize, XmlRootTag, Clone, Debug)] #[xml(root = b"propertyupdate")] struct PropertyupdateElement { - // #[xml(flatten)] - // set: Vec, - // #[xml(flatten)] - // remove: Vec, #[xml(ty = "untagged", flatten)] operations: Vec>, } @@ -68,21 +79,9 @@ pub(crate) async fn route_proppatch( let resource_service = R::new(&req, path_components.clone()).await?; // Extract operations - let PropertyupdateElement::<::Prop> { operations } = - XmlDocument::parse_str(&body).map_err(Error::XmlDeserializationError)?; - - // Extract all set property names without verification - // Weird workaround because quick_xml doesn't allow untagged enums - let propnames: Vec = PropertyupdateElement::::parse_str(&body) - .map_err(Error::XmlDeserializationError)? - .operations - .into_iter() - .map(|op_el| match op_el { - Operation::Set(set_el) => set_el.prop.name, - // If we can't remove a nonexisting property then that's no big deal - Operation::Remove(remove_el) => remove_el.prop.property.name, - }) - .collect(); + let PropertyupdateElement::::Prop>> { + operations, + } = XmlDocument::parse_str(&body).map_err(Error::XmlDeserializationError)?; let mut resource = resource_service.get_resource().await?; let privileges = resource.get_user_privileges(&user)?; @@ -94,27 +93,34 @@ pub(crate) async fn route_proppatch( let mut props_conflict = Vec::new(); let mut props_not_found = Vec::new(); - for (operation, propname) in operations.into_iter().zip(propnames) { + for operation in operations.into_iter() { match operation { Operation::Set(SetPropertyElement { prop }) => { - if prop.invalid_property() { - if ::list_props().contains(&propname.as_str()) { - // This happens in following cases: - // - read-only properties with #[serde(skip_deserializing)] - // - internal properties - props_conflict.push(propname) - } else { - props_not_found.push(propname); + match prop.property { + SetPropertyPropWrapper::Valid(prop) => { + let propname: ::PropName = prop.clone().into(); + let propname: &str = propname.into(); + match resource.set_prop(prop) { + Ok(()) => props_ok.push(propname.to_owned()), + Err(Error::PropReadOnly) => props_conflict.push(propname.to_owned()), + Err(err) => return Err(err.into()), + }; + } + SetPropertyPropWrapper::Invalid(invalid) => { + let propname = invalid.tag_name(); + if ::list_props().contains(&propname.as_str()) { + // This happens in following cases: + // - read-only properties with #[serde(skip_deserializing)] + // - internal properties + props_conflict.push(propname) + } else { + props_not_found.push(propname); + } } - continue; } - match resource.set_prop(prop) { - Ok(()) => props_ok.push(propname), - Err(Error::PropReadOnly) => props_conflict.push(propname), - Err(err) => return Err(err.into()), - }; } - Operation::Remove(_remove_el) => { + Operation::Remove(remove_el) => { + let propname = remove_el.prop.property.name; match <::PropName as FromStr>::from_str(&propname) { Ok(prop) => match resource.remove_prop(&prop) { Ok(()) => props_ok.push(propname), diff --git a/crates/dav/src/resource/mod.rs b/crates/dav/src/resource/mod.rs index 47b4307..f635481 100644 --- a/crates/dav/src/resource/mod.rs +++ b/crates/dav/src/resource/mod.rs @@ -7,7 +7,6 @@ use actix_web::dev::ResourceMap; use actix_web::error::UrlGenerationError; use actix_web::test::TestRequest; use actix_web::{http::StatusCode, ResponseError}; -pub use invalid_property::InvalidProperty; use itertools::Itertools; pub use resource_service::ResourceService; use rustical_store::auth::User; @@ -16,12 +15,11 @@ use serde::Serialize; use std::str::FromStr; use strum::{EnumString, VariantNames}; -mod invalid_property; mod methods; mod resource_service; -pub trait ResourceProp: InvalidProperty + Serialize + XmlDeserialize {} -impl ResourceProp for T {} +pub trait ResourceProp: Serialize + XmlDeserialize {} +impl ResourceProp for T {} pub trait ResourcePropName: FromStr + VariantNames {} impl ResourcePropName for T {} @@ -29,7 +27,7 @@ impl ResourcePropName for T {} pub trait ResourceType: Serialize + XmlDeserialize {} impl ResourceType for T {} -#[derive(XmlDeserialize, Serialize, PartialEq, Default)] +#[derive(XmlDeserialize, Serialize, PartialEq)] #[serde(rename_all = "kebab-case")] pub enum CommonPropertiesProp { // WebDAV (RFC 2518) @@ -43,10 +41,6 @@ pub enum CommonPropertiesProp { // WebDAV Access Control Protocol (RFC 3477) CurrentUserPrivilegeSet(UserPrivilegeSet), Owner(HrefElement), - - #[serde(other)] - #[default] - Invalid, } #[derive(Serialize)] @@ -67,8 +61,8 @@ pub enum CommonPropertiesPropName { } pub trait Resource: Clone + 'static { - type PropName: ResourcePropName; - type Prop: ResourceProp + PartialEq; + type PropName: ResourcePropName + From + Into<&'static str>; + type Prop: ResourceProp + PartialEq + Clone; type Error: ResponseError + From; type PrincipalResource: Resource; diff --git a/crates/dav/src/resources/root.rs b/crates/dav/src/resources/root.rs index 5be0853..ec369ed 100644 --- a/crates/dav/src/resources/root.rs +++ b/crates/dav/src/resources/root.rs @@ -8,7 +8,7 @@ use rustical_xml::XmlDeserialize; use serde::Serialize; use std::any::type_name; use std::marker::PhantomData; -use strum::{EnumString, VariantNames}; +use strum::{EnumString, IntoStaticStr, VariantNames}; #[derive(Clone)] pub struct RootResource(PhantomData); @@ -19,15 +19,17 @@ impl Default for RootResource { } } -#[derive(EnumString, VariantNames, Clone)] +#[derive(EnumString, VariantNames, Clone, IntoStaticStr)] #[strum(serialize_all = "kebab-case")] pub enum RootResourcePropName {} -#[derive(XmlDeserialize, Serialize, Default, Clone, PartialEq)] -pub enum RootResourceProp { - #[serde(other)] - #[default] - Invalid, +#[derive(XmlDeserialize, Serialize, Clone, PartialEq)] +pub enum RootResourceProp {} + +impl From for RootResourcePropName { + fn from(_value: RootResourceProp) -> Self { + unreachable!() + } } impl Resource for RootResource { diff --git a/crates/xml/derive/src/xml_struct.rs b/crates/xml/derive/src/xml_struct.rs index 33b1dcf..0e14889 100644 --- a/crates/xml/derive/src/xml_struct.rs +++ b/crates/xml/derive/src/xml_struct.rs @@ -5,11 +5,13 @@ use darling::FromDeriveInput; use quote::quote; use syn::{DataStruct, DeriveInput}; -fn invalid_field_branch(allow: bool) -> proc_macro2::TokenStream { +fn invalid_field_branch(ident: &syn::Ident, allow: bool) -> proc_macro2::TokenStream { + let ident = ident.to_string(); if allow { quote! {} } else { - quote! { return Err(XmlDeError::InvalidFieldName(format!("[{ns:?}]{tag}", tag = String::from_utf8_lossy(tag)))) } + quote! { + return Err(XmlDeError::InvalidFieldName(#ident, format!("[{ns:?}]{tag}", tag = String::from_utf8_lossy(tag)))) } } } @@ -85,7 +87,8 @@ impl NamedStruct { let builder_field_builds = self.fields.iter().map(Field::builder_field_build); - let invalid_field_branch = invalid_field_branch(self.attrs.allow_invalid.is_present()); + let invalid_field_branch = + invalid_field_branch(ident, self.attrs.allow_invalid.is_present()); quote! { impl #impl_generics ::rustical_xml::XmlDeserialize for #ident #type_generics #where_clause { diff --git a/crates/xml/src/de.rs b/crates/xml/src/de.rs index 1baa409..527ead0 100644 --- a/crates/xml/src/de.rs +++ b/crates/xml/src/de.rs @@ -28,8 +28,8 @@ pub enum XmlDeError { Other(String), #[error("Invalid variant: {0}")] InvalidVariant(String), - #[error("Invalid field name: {0}")] - InvalidFieldName(String), + #[error("Invalid field name in {0}: {1}")] + InvalidFieldName(&'static str, String), #[error(transparent)] InvalidValue(#[from] crate::value::ParseValueError), } diff --git a/crates/xml/src/lib.rs b/crates/xml/src/lib.rs index 87b854f..12d65ae 100644 --- a/crates/xml/src/lib.rs +++ b/crates/xml/src/lib.rs @@ -36,6 +36,13 @@ impl XmlDeserialize for () { #[derive(Debug, Clone, PartialEq)] pub struct Unparsed(BytesStart<'static>); +impl Unparsed { + pub fn tag_name(&self) -> String { + // TODO: respect namespace? + String::from_utf8_lossy(self.0.local_name().as_ref()).to_string() + } +} + impl XmlDeserialize for Unparsed { fn deserialize( reader: &mut quick_xml::NsReader,