From b540362791437092c4857b27ba16fb99c9afbb5b Mon Sep 17 00:00:00 2001 From: Lennart <18233294+lennart-k@users.noreply.github.com> Date: Thu, 14 Mar 2024 19:24:23 +0100 Subject: [PATCH] Migrate DAV props to serde. Big clusterfuck right now but it'll hopefully pay off --- crates/caldav/src/lib.rs | 1 - crates/caldav/src/resources/calendar.rs | 237 ++++++++++++++--------- crates/caldav/src/resources/event.rs | 49 +++-- crates/caldav/src/resources/principal.rs | 45 +++-- crates/caldav/src/resources/root.rs | 48 +++-- crates/dav/Cargo.toml | 1 + crates/dav/src/lib.rs | 1 + crates/dav/src/resource.rs | 29 +-- crates/{caldav => dav}/src/tagname.rs | 0 crates/dav/src/xml_snippets.rs | 25 ++- 10 files changed, 246 insertions(+), 190 deletions(-) rename crates/{caldav => dav}/src/tagname.rs (100%) diff --git a/crates/caldav/src/lib.rs b/crates/caldav/src/lib.rs index 6b8f775..f008a9b 100644 --- a/crates/caldav/src/lib.rs +++ b/crates/caldav/src/lib.rs @@ -17,7 +17,6 @@ use tokio::sync::RwLock; pub mod error; pub mod resources; pub mod routes; -pub mod tagname; pub struct CalDavContext { pub prefix: String, diff --git a/crates/caldav/src/resources/calendar.rs b/crates/caldav/src/resources/calendar.rs index b8ef03a..0f4f4a7 100644 --- a/crates/caldav/src/resources/calendar.rs +++ b/crates/caldav/src/resources/calendar.rs @@ -1,19 +1,16 @@ -use std::{io::Write, sync::Arc}; - use actix_web::{web::Data, HttpRequest}; use anyhow::{anyhow, Result}; use async_trait::async_trait; -use quick_xml::Writer; use rustical_auth::AuthInfo; -use rustical_store::calendar::{Calendar, CalendarStore}; -use strum::{EnumProperty, EnumString, IntoStaticStr, VariantNames}; -use tokio::sync::RwLock; - -use crate::tagname::TagName; use rustical_dav::{ resource::Resource, - xml_snippets::{write_resourcetype, HrefElement, TextElement}, + xml_snippets::{HrefElement, TextNode}, }; +use rustical_store::calendar::{Calendar, CalendarStore}; +use serde::Serialize; +use std::sync::Arc; +use strum::{EnumProperty, EnumString, IntoStaticStr, VariantNames}; +use tokio::sync::RwLock; pub struct CalendarResource { pub cal_store: Arc>, @@ -23,7 +20,98 @@ pub struct CalendarResource { pub principal: String, } -#[derive(EnumString, Debug, VariantNames, IntoStaticStr, EnumProperty)] +#[derive(Serialize)] +#[serde(rename_all = "kebab-case")] +pub struct SupportedCalendarComponent(#[serde(rename = "@name")] pub &'static str); + +#[derive(Serialize)] +#[serde(rename_all = "kebab-case")] +pub struct SupportedCalendarComponentSet { + #[serde(rename = "C:comp")] + pub comp: Vec, +} + +#[derive(Serialize)] +#[serde(rename_all = "kebab-case")] +pub struct CalendarData { + content_type: &'static str, + version: &'static str, +} + +impl Default for CalendarData { + fn default() -> Self { + Self { + content_type: "text/calendar;charset=utf-8", + version: "2.0", + } + } +} + +#[derive(Serialize, Default)] +#[serde(rename_all = "kebab-case")] +pub struct SupportedCalendarData { + #[serde(rename = "C:calendar-data")] + calendar_data: CalendarData, +} + +#[derive(Serialize, Default)] +#[serde(rename_all = "kebab-case")] +pub struct Resourcetype { + #[serde(rename = "C:calendar")] + calendar: (), + collection: (), +} + +#[derive(Serialize)] +#[serde(rename_all = "kebab-case")] +pub enum UserPrivilege { + Read, + ReadAcl, + Write, + WriteAcl, + WriteContent, + ReadCurrentUserPrivilegeSet, + Bind, + Unbind, +} + +#[derive(Serialize)] +#[serde(rename_all = "kebab-case")] +pub struct UserPrivilegeWrapper { + #[serde(rename = "$value")] + privilege: UserPrivilege, +} + +impl From for UserPrivilegeWrapper { + fn from(value: UserPrivilege) -> Self { + Self { privilege: value } + } +} + +#[derive(Serialize)] +#[serde(rename_all = "kebab-case")] +pub struct UserPrivilegeSet { + privilege: Vec, +} + +impl Default for UserPrivilegeSet { + fn default() -> Self { + Self { + privilege: vec![ + UserPrivilege::Read.into(), + UserPrivilege::ReadAcl.into(), + UserPrivilege::Write.into(), + UserPrivilege::WriteAcl.into(), + UserPrivilege::WriteContent.into(), + UserPrivilege::ReadCurrentUserPrivilegeSet.into(), + UserPrivilege::Bind.into(), + UserPrivilege::Unbind.into(), + ], + } + } +} + +#[derive(EnumString, Debug, VariantNames, IntoStaticStr, EnumProperty, Clone)] #[strum(serialize_all = "kebab-case")] pub enum CalendarProp { Resourcetype, @@ -43,11 +131,27 @@ pub enum CalendarProp { MaxResourceSize, } +#[derive(Serialize)] +#[serde(untagged)] +pub enum CalendarPropResponse { + Resourcetype(Resourcetype), + CurrentUser(HrefElement), + Displayname(TextNode), + CalendarColor(TextNode), + CalendarDescription(TextNode), + SupportedCalendarComponentSet(SupportedCalendarComponentSet), + SupportedCalendarData(SupportedCalendarData), + Getcontenttype(TextNode), + MaxResourceSize(TextNode), + CurrentUserPrivilegeSet(UserPrivilegeSet), +} + #[async_trait(?Send)] impl Resource for CalendarResource { type MemberType = Self; type UriComponents = (String, String); // principal, calendar_id type PropType = CalendarProp; + type PropResponse = CalendarPropResponse; async fn acquire_from_request( req: HttpRequest, @@ -81,93 +185,44 @@ impl Resource for CalendarResource { Ok(vec![]) } - fn write_prop(&self, writer: &mut Writer, prop: Self::PropType) -> Result<()> { + fn get_prop(&self, prop: Self::PropType) -> Result { match prop { CalendarProp::Resourcetype => { - write_resourcetype(writer, vec!["C:calendar", "collection"])? + Ok(CalendarPropResponse::Resourcetype(Resourcetype::default())) } CalendarProp::CurrentUserPrincipal | CalendarProp::Owner => { - writer.write_serializable( - prop.tagname(), - &HrefElement::new(format!("{}/{}/", self.prefix, self.principal)), - )?; - } - CalendarProp::Displayname => { - let name = self.calendar.name.clone(); - writer.write_serializable(prop.tagname(), &TextElement(name))?; - } - CalendarProp::CalendarColor => { - let color = self.calendar.color.clone(); - writer.write_serializable(prop.tagname(), &TextElement(color))?; - } - CalendarProp::CalendarDescription => { - let description = self.calendar.description.clone(); - writer.write_serializable(prop.tagname(), &TextElement(description))?; + Ok(CalendarPropResponse::CurrentUser(HrefElement::new( + format!("{}/{}/", self.prefix, self.principal), + ))) } + CalendarProp::Displayname => Ok(CalendarPropResponse::Displayname(TextNode( + self.calendar.name.clone(), + ))), + CalendarProp::CalendarColor => Ok(CalendarPropResponse::CalendarColor(TextNode( + self.calendar.color.clone(), + ))), + CalendarProp::CalendarDescription => Ok(CalendarPropResponse::CalendarDescription( + TextNode(self.calendar.description.clone()), + )), CalendarProp::SupportedCalendarComponentSet => { - writer - .create_element(prop.tagname()) - .write_inner_content(|writer| { - writer - .create_element("C:comp") - .with_attribute(("name", "VEVENT")) - .write_empty()?; - Ok::<(), quick_xml::Error>(()) - })?; + Ok(CalendarPropResponse::SupportedCalendarComponentSet( + SupportedCalendarComponentSet { + comp: vec![SupportedCalendarComponent("VEVENT")], + }, + )) } - CalendarProp::SupportedCalendarData => { - writer - .create_element(prop.tagname()) - .write_inner_content(|writer| { - // - writer - .create_element("C:calendar-data") - .with_attributes(vec![ - ("content-type", "text/calendar"), - ("version", "2.0"), - ]) - .write_empty()?; - Ok::<(), quick_xml::Error>(()) - })?; - } - CalendarProp::Getcontenttype => { - writer.write_serializable( - prop.tagname(), - &TextElement(Some("text/calendar".to_owned())), - )?; - } - CalendarProp::MaxResourceSize => { - writer.write_serializable( - prop.tagname(), - &TextElement(Some("10000000".to_owned())), - )?; - } - CalendarProp::CurrentUserPrivilegeSet => { - writer - .create_element(prop.tagname()) - // These are just hard-coded for now and will possibly change in the future - .write_inner_content(|writer| { - for privilege in [ - "read", - "read-acl", - "write", - "write-acl", - "write-content", - "read-current-user-privilege-set", - "bind", - "unbind", - ] { - writer - .create_element("privilege") - .write_inner_content(|writer| { - writer.create_element(privilege).write_empty()?; - Ok::<(), quick_xml::Error>(()) - })?; - } - Ok::<(), quick_xml::Error>(()) - })?; - } - }; - Ok(()) + CalendarProp::SupportedCalendarData => Ok(CalendarPropResponse::SupportedCalendarData( + SupportedCalendarData::default(), + )), + CalendarProp::Getcontenttype => Ok(CalendarPropResponse::Getcontenttype(TextNode( + Some("text/calendar;charset=utf-8".to_owned()), + ))), + CalendarProp::MaxResourceSize => Ok(CalendarPropResponse::MaxResourceSize(TextNode( + Some("10000000".to_owned()), + ))), + CalendarProp::CurrentUserPrivilegeSet => Ok( + CalendarPropResponse::CurrentUserPrivilegeSet(UserPrivilegeSet::default()), + ), + } } } diff --git a/crates/caldav/src/resources/event.rs b/crates/caldav/src/resources/event.rs index 29cd0ca..f0a149c 100644 --- a/crates/caldav/src/resources/event.rs +++ b/crates/caldav/src/resources/event.rs @@ -1,11 +1,11 @@ -use crate::tagname::TagName; use actix_web::{web::Data, HttpRequest}; use anyhow::{anyhow, Result}; use async_trait::async_trait; use rustical_auth::AuthInfo; -use rustical_dav::{resource::Resource, xml_snippets::TextElement}; +use rustical_dav::{resource::Resource, xml_snippets::TextNode}; use rustical_store::calendar::CalendarStore; use rustical_store::event::Event; +use serde::Serialize; use std::sync::Arc; use strum::{EnumProperty, EnumString, IntoStaticStr, VariantNames}; use tokio::sync::RwLock; @@ -16,7 +16,7 @@ pub struct EventResource { pub event: Event, } -#[derive(EnumString, Debug, VariantNames, IntoStaticStr, EnumProperty)] +#[derive(EnumString, Debug, VariantNames, IntoStaticStr, EnumProperty, Clone)] #[strum(serialize_all = "kebab-case")] pub enum EventProp { Getetag, @@ -25,11 +25,20 @@ pub enum EventProp { Getcontenttype, } +#[derive(Serialize)] +#[serde(untagged)] +pub enum PrincipalPropResponse { + Getetag(TextNode), + CalendarData(TextNode), + Getcontenttype(TextNode), +} + #[async_trait(?Send)] impl Resource for EventResource { type UriComponents = (String, String, String); // principal, calendar, event type MemberType = Self; type PropType = EventProp; + type PropResponse = PrincipalPropResponse; fn get_path(&self) -> &str { &self.path @@ -62,29 +71,17 @@ impl Resource for EventResource { }) } - fn write_prop( - &self, - writer: &mut quick_xml::Writer, - prop: Self::PropType, - ) -> Result<()> { + fn get_prop(&self, prop: Self::PropType) -> Result { match prop { - EventProp::Getetag => { - writer.write_serializable( - prop.tagname(), - &TextElement(Some(self.event.get_etag())), - )?; - } - EventProp::CalendarData => { - writer - .write_serializable(prop.tagname(), &TextElement(Some(self.event.get_ics())))?; - } - EventProp::Getcontenttype => { - writer.write_serializable( - prop.tagname(), - &TextElement(Some("text/calendar;charset=utf-8".to_owned())), - )?; - } - }; - Ok(()) + EventProp::Getetag => Ok(PrincipalPropResponse::Getetag(TextNode(Some( + self.event.get_etag(), + )))), + EventProp::CalendarData => Ok(PrincipalPropResponse::CalendarData(TextNode(Some( + self.event.get_ics(), + )))), + EventProp::Getcontenttype => Ok(PrincipalPropResponse::Getcontenttype(TextNode(Some( + "text/calendar;charset=utf-8".to_owned(), + )))), + } } } diff --git a/crates/caldav/src/resources/principal.rs b/crates/caldav/src/resources/principal.rs index 36ddb57..6b74870 100644 --- a/crates/caldav/src/resources/principal.rs +++ b/crates/caldav/src/resources/principal.rs @@ -1,13 +1,10 @@ -use crate::tagname::TagName; use actix_web::{web::Data, HttpRequest}; use anyhow::{anyhow, Result}; use async_trait::async_trait; use rustical_auth::AuthInfo; -use rustical_dav::{ - resource::Resource, - xml_snippets::{write_resourcetype, HrefElement}, -}; +use rustical_dav::{resource::Resource, xml_snippets::HrefElement}; use rustical_store::calendar::CalendarStore; +use serde::Serialize; use std::sync::Arc; use strum::{EnumProperty, EnumString, IntoStaticStr, VariantNames}; use tokio::sync::RwLock; @@ -21,7 +18,21 @@ pub struct PrincipalCalendarsResource { cal_store: Arc>, } -#[derive(EnumString, Debug, VariantNames, IntoStaticStr, EnumProperty)] +#[derive(Serialize, Default)] +#[serde(rename_all = "kebab-case")] +pub struct Resourcetype { + principal: (), + collection: (), +} + +#[derive(Serialize)] +#[serde(untagged)] +pub enum PrincipalPropResponse { + Resourcetype(Resourcetype), + CurrentUser(HrefElement), +} + +#[derive(EnumString, Debug, VariantNames, IntoStaticStr, EnumProperty, Clone)] #[strum(serialize_all = "kebab-case")] pub enum PrincipalProp { Resourcetype, @@ -39,6 +50,7 @@ impl Resource for PrincipalCalendarsResource { type UriComponents = (); type MemberType = CalendarResource; type PropType = PrincipalProp; + type PropResponse = PrincipalPropResponse; fn get_path(&self) -> &str { &self.path @@ -83,26 +95,17 @@ impl Resource for PrincipalCalendarsResource { path: req.path().to_string(), }) } - - fn write_prop( - &self, - writer: &mut quick_xml::Writer, - prop: Self::PropType, - ) -> Result<()> { + fn get_prop(&self, prop: Self::PropType) -> Result { match prop { PrincipalProp::Resourcetype => { - write_resourcetype(writer, vec!["principal", "collection"])? + Ok(PrincipalPropResponse::Resourcetype(Resourcetype::default())) } PrincipalProp::CurrentUserPrincipal | PrincipalProp::PrincipalUrl | PrincipalProp::CalendarHomeSet - | PrincipalProp::CalendarUserAddressSet => { - writer.write_serializable( - prop.tagname(), - &HrefElement::new(format!("{}/{}/", self.prefix, self.principal)), - )?; - } - }; - Ok(()) + | PrincipalProp::CalendarUserAddressSet => Ok(PrincipalPropResponse::CurrentUser( + HrefElement::new(format!("{}/{}/", self.prefix, self.principal)), + )), + } } } diff --git a/crates/caldav/src/resources/root.rs b/crates/caldav/src/resources/root.rs index 2d50007..bdd910e 100644 --- a/crates/caldav/src/resources/root.rs +++ b/crates/caldav/src/resources/root.rs @@ -1,10 +1,9 @@ -use crate::tagname::TagName; use actix_web::HttpRequest; use anyhow::Result; use async_trait::async_trait; -use quick_xml::events::BytesText; use rustical_auth::AuthInfo; -use rustical_dav::{resource::Resource, xml_snippets::write_resourcetype}; +use rustical_dav::{resource::Resource, xml_snippets::HrefElement}; +use serde::Serialize; use strum::{EnumProperty, EnumString, IntoStaticStr, VariantNames}; pub struct RootResource { @@ -13,18 +12,32 @@ pub struct RootResource { path: String, } -#[derive(EnumString, Debug, VariantNames, EnumProperty, IntoStaticStr)] +#[derive(EnumString, Debug, VariantNames, EnumProperty, IntoStaticStr, Clone)] #[strum(serialize_all = "kebab-case")] pub enum RootProp { Resourcetype, CurrentUserPrincipal, } +#[derive(Serialize, Default)] +#[serde(rename_all = "kebab-case")] +pub struct Resourcetype { + collection: (), +} + +#[derive(Serialize)] +#[serde(untagged)] +pub enum RootPropResponse { + Resourcetype(Resourcetype), + CurrentUser(HrefElement), +} + #[async_trait(?Send)] impl Resource for RootResource { type UriComponents = (); type MemberType = Self; type PropType = RootProp; + type PropResponse = RootPropResponse; fn get_path(&self) -> &str { &self.path @@ -47,27 +60,12 @@ impl Resource for RootResource { }) } - fn write_prop( - &self, - writer: &mut quick_xml::Writer, - prop: Self::PropType, - ) -> Result<()> { + fn get_prop(&self, prop: Self::PropType) -> Result { match prop { - RootProp::Resourcetype => write_resourcetype(writer, vec!["collection"])?, - RootProp::CurrentUserPrincipal => { - writer - .create_element(prop.tagname()) - .write_inner_content(|writer| { - writer - .create_element("href") - .write_text_content(BytesText::new(&format!( - "{}/{}", - self.prefix, self.principal - )))?; - Ok::<(), quick_xml::Error>(()) - })?; - } - }; - Ok(()) + RootProp::Resourcetype => Ok(RootPropResponse::Resourcetype(Resourcetype::default())), + RootProp::CurrentUserPrincipal => Ok(RootPropResponse::CurrentUser(HrefElement::new( + format!("{}/{}/", self.prefix, self.principal), + ))), + } } } diff --git a/crates/dav/Cargo.toml b/crates/dav/Cargo.toml index e66e24e..e6355be 100644 --- a/crates/dav/Cargo.toml +++ b/crates/dav/Cargo.toml @@ -11,4 +11,5 @@ derive_more = "0.99" futures-util = "0.3" quick-xml = "0.31" rustical_auth = { path = "../auth/" } +serde = { version = "1.0.197", features = ["derive"] } strum = "0.26.2" diff --git a/crates/dav/src/lib.rs b/crates/dav/src/lib.rs index 374d1d1..ddad68e 100644 --- a/crates/dav/src/lib.rs +++ b/crates/dav/src/lib.rs @@ -1,4 +1,5 @@ pub mod depth_extractor; pub mod namespace; pub mod resource; +pub mod tagname; pub mod xml_snippets; diff --git a/crates/dav/src/resource.rs b/crates/dav/src/resource.rs index 4052195..22d2685 100644 --- a/crates/dav/src/resource.rs +++ b/crates/dav/src/resource.rs @@ -1,13 +1,16 @@ -use std::{io::Write, str::FromStr}; - use actix_web::{http::StatusCode, HttpRequest}; use anyhow::{anyhow, Result}; use async_trait::async_trait; use quick_xml::Writer; use rustical_auth::AuthInfo; -use strum::VariantNames; +use serde::Serialize; +use std::str::FromStr; +use strum::{EnumProperty, VariantNames}; -use crate::xml_snippets::{write_invalid_props_response, write_propstat_response}; +use crate::{ + tagname::TagName, + xml_snippets::{write_invalid_props_response, write_propstat_response}, +}; // A resource is identified by a URI and has properties // A resource can also be a collection @@ -17,7 +20,8 @@ use crate::xml_snippets::{write_invalid_props_response, write_propstat_response} pub trait Resource: Sized { type MemberType: Resource; type UriComponents: Sized; // defines how the resource URI maps to parameters, i.e. /{principal}/{calendar} -> (String, String) - type PropType: FromStr + VariantNames; + type PropType: FromStr + VariantNames + Into<&'static str> + EnumProperty + Clone; + type PropResponse: Serialize; async fn acquire_from_request( req: HttpRequest, @@ -32,7 +36,7 @@ pub trait Resource: Sized { fn list_dead_props() -> &'static [&'static str] { Self::PropType::VARIANTS } - fn write_prop(&self, writer: &mut Writer, prop: Self::PropType) -> Result<()>; + fn get_prop(&self, prop: Self::PropType) -> Result; } pub trait HandlePropfind { @@ -59,13 +63,12 @@ impl HandlePropfind for R { for prop in props { if let Ok(valid_prop) = R::PropType::from_str(prop) { // TODO: Fix error types - match self - .write_prop(writer, valid_prop) - .map_err(|_e| quick_xml::Error::TextNotFound) - { - // TODO: clean this mess up - Ok(_) => {} - // not really an invalid prop, but some error happened + match self.get_prop(valid_prop.clone()) { + Ok(response) => { + writer + .write_serializable(valid_prop.tagname(), &response) + .map_err(|_e| quick_xml::Error::TextNotFound)?; + } Err(_) => invalid_props.push(prop), }; } else { diff --git a/crates/caldav/src/tagname.rs b/crates/dav/src/tagname.rs similarity index 100% rename from crates/caldav/src/tagname.rs rename to crates/dav/src/tagname.rs diff --git a/crates/dav/src/xml_snippets.rs b/crates/dav/src/xml_snippets.rs index f1b25d5..f6485f8 100644 --- a/crates/dav/src/xml_snippets.rs +++ b/crates/dav/src/xml_snippets.rs @@ -6,21 +6,20 @@ use quick_xml::{ events::{attributes::Attribute, BytesText}, Writer, }; +use serde::Serialize; -pub fn write_resourcetype( - writer: &mut Writer, - types: Vec<&str>, -) -> Result<(), quick_xml::Error> { - writer - .create_element("resourcetype") - .write_inner_content(|writer| { - for resourcetype in types { - writer.create_element(resourcetype).write_empty()?; - } - Ok::<(), quick_xml::Error>(()) - })?; - Ok(()) +#[derive(Serialize)] +pub struct HrefElement { + pub href: String, } +impl HrefElement { + pub fn new(href: String) -> Self { + Self { href } + } +} + +#[derive(Serialize)] +pub struct TextNode(pub Option); pub fn write_invalid_props_response( writer: &mut Writer,