WIP: Complete work of propfind parsing

This commit is contained in:
Lennart
2025-06-04 18:11:25 +02:00
parent 5ad6ee2e99
commit e57a14cad1
43 changed files with 875 additions and 1036 deletions

View File

@@ -4,9 +4,9 @@ use crate::{
resource::{PrincipalUri, Resource},
xml::{HrefElement, Resourcetype},
};
use rustical_xml::{EnumUnitVariants, EnumVariants, XmlDeserialize, XmlSerialize};
use rustical_xml::{EnumVariants, PropName, XmlDeserialize, XmlSerialize};
#[derive(XmlDeserialize, XmlSerialize, PartialEq, Clone, EnumUnitVariants, EnumVariants)]
#[derive(XmlDeserialize, XmlSerialize, PartialEq, Clone, PropName, EnumVariants)]
#[xml(unit_variants_ident = "CommonPropertiesPropName")]
pub enum CommonPropertiesProp {
// WebDAV (RFC 2518)

View File

@@ -1,6 +1,6 @@
use rustical_xml::{EnumUnitVariants, EnumVariants, XmlDeserialize, XmlSerialize};
use rustical_xml::{EnumVariants, PropName, XmlDeserialize, XmlSerialize};
#[derive(XmlDeserialize, XmlSerialize, PartialEq, Clone, EnumUnitVariants, EnumVariants)]
#[derive(XmlDeserialize, XmlSerialize, PartialEq, Clone, PropName, EnumVariants)]
#[xml(unit_variants_ident = "SyncTokenExtensionPropName")]
pub enum SyncTokenExtensionProp {
// Collection Synchronization (RFC 6578)

View File

@@ -5,9 +5,9 @@ use crate::resource::PrincipalUri;
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 rustical_xml::PropName;
use rustical_xml::XmlDocument;
use tracing::instrument;
@@ -58,37 +58,36 @@ pub(crate) async fn route_propfind<R: ResourceService>(
}
// A request body is optional. If empty we MUST return all props
let propfind: PropfindElement = if !body.is_empty() {
PropfindElement::parse_str(&body).map_err(Error::XmlError)?
} else {
PropfindElement {
prop: PropfindType::Allprop,
}
};
// TODO: respect namespaces?
let props = match &propfind.prop {
PropfindType::Allprop => vec!["allprop"],
PropfindType::Propname => vec!["propname"],
PropfindType::Prop(PropElement(prop_tags)) => prop_tags
.iter()
.map(|propname| propname.name.as_str())
.collect(),
};
let propfind_self: PropfindElement<<<R::Resource as Resource>::Prop as PropName>::Names> =
if !body.is_empty() {
PropfindElement::parse_str(&body).map_err(Error::XmlError)?
} else {
PropfindElement {
prop: PropfindType::Allprop,
}
};
let propfind_member: PropfindElement<<<R::MemberType as Resource>::Prop as PropName>::Names> =
if !body.is_empty() {
PropfindElement::parse_str(&body).map_err(Error::XmlError)?
} else {
PropfindElement {
prop: PropfindType::Allprop,
}
};
let mut member_responses = Vec::new();
if depth != Depth::Zero {
for (subpath, member) in resource_service.get_members(path_components).await? {
member_responses.push(member.propfind(
member_responses.push(member.propfind_typed(
&format!("{}/{}", path.trim_end_matches('/'), subpath),
&props,
&propfind_member.prop,
puri,
&user,
)?);
}
}
let response = resource.propfind(path, &props, puri, &user)?;
let response = resource.propfind_typed(path, &propfind_self.prop, puri, &user)?;
Ok(MultistatusElement {
responses: vec![response],

View File

@@ -7,7 +7,8 @@ use crate::xml::TagList;
use crate::xml::multistatus::{PropstatElement, PropstatWrapper, ResponseElement};
use http::StatusCode;
use quick_xml::name::Namespace;
use rustical_xml::EnumUnitVariants;
use rustical_xml::NamespaceOwned;
use rustical_xml::PropName;
use rustical_xml::Unparsed;
use rustical_xml::XmlDeserialize;
use rustical_xml::XmlDocument;
@@ -111,13 +112,15 @@ pub(crate) async fn route_proppatch<R: ResourceService>(
}) => {
match property {
SetPropertyPropWrapper::Valid(prop) => {
let propname: <<R::Resource as Resource>::Prop as EnumUnitVariants>::UnitVariants = prop.clone().into();
let propname: <<R::Resource as Resource>::Prop as PropName>::Names =
prop.clone().into();
let (ns, propname): (Option<Namespace>, &str) = propname.into();
match resource.set_prop(prop) {
Ok(()) => props_ok.push((ns, propname.to_owned())),
Err(Error::PropReadOnly) => {
props_conflict.push((ns, propname.to_owned()))
Ok(()) => {
props_ok.push((ns.map(NamespaceOwned::from), propname.to_owned()))
}
Err(Error::PropReadOnly) => props_conflict
.push((ns.map(NamespaceOwned::from), propname.to_owned())),
Err(err) => return Err(err.into()),
};
}
@@ -128,7 +131,7 @@ pub(crate) async fn route_proppatch<R: ResourceService>(
.into_iter()
.find_map(|(ns, tag)| {
if tag == propname.as_str() {
Some((ns, tag.to_owned()))
Some((ns.map(NamespaceOwned::from), tag.to_owned()))
} else {
None
}
@@ -146,14 +149,12 @@ pub(crate) async fn route_proppatch<R: ResourceService>(
}
Operation::Remove(remove_el) => {
let propname = remove_el.prop.0.0;
match <<R::Resource as Resource>::Prop as EnumUnitVariants>::UnitVariants::from_str(
&propname,
) {
match <<R::Resource as Resource>::Prop as PropName>::Names::from_str(&propname) {
Ok(prop) => match resource.remove_prop(&prop) {
Ok(()) => props_ok.push((None, propname)),
Err(Error::PropReadOnly) => props_conflict.push({
let (ns, tag) = prop.into();
(ns, tag.to_owned())
(ns.map(NamespaceOwned::from), tag.to_owned())
}),
Err(err) => return Err(err.into()),
},

View File

@@ -1,14 +1,14 @@
use crate::Principal;
use crate::privileges::UserPrivilegeSet;
use crate::xml::Resourcetype;
use crate::xml::multistatus::{PropTagWrapper, PropstatElement, PropstatWrapper};
use crate::xml::{PropElement, PropfindType, Resourcetype};
use crate::xml::{TagList, multistatus::ResponseElement};
use crate::{Error, Principal};
use headers::{ETag, IfMatch, IfNoneMatch};
use http::StatusCode;
use itertools::Itertools;
use quick_xml::name::Namespace;
pub use resource_service::ResourceService;
use rustical_xml::{EnumUnitVariants, EnumVariants, XmlDeserialize, XmlSerialize};
use rustical_xml::{EnumVariants, NamespaceOwned, PropName, XmlDeserialize, XmlSerialize};
use std::collections::HashSet;
use std::str::FromStr;
@@ -26,7 +26,7 @@ pub trait ResourcePropName: FromStr {}
impl<T: FromStr> ResourcePropName for T {}
pub trait Resource: Clone + 'static {
type Prop: ResourceProp + PartialEq + Clone + EnumVariants + EnumUnitVariants;
type Prop: ResourceProp + PartialEq + Clone + EnumVariants + PropName;
type Error: From<crate::Error>;
type Principal: Principal;
@@ -40,17 +40,14 @@ pub trait Resource: Clone + 'static {
&self,
principal_uri: &impl PrincipalUri,
principal: &Self::Principal,
prop: &<Self::Prop as EnumUnitVariants>::UnitVariants,
prop: &<Self::Prop as PropName>::Names,
) -> Result<Self::Prop, Self::Error>;
fn set_prop(&mut self, _prop: Self::Prop) -> Result<(), crate::Error> {
Err(crate::Error::PropReadOnly)
}
fn remove_prop(
&mut self,
_prop: &<Self::Prop as EnumUnitVariants>::UnitVariants,
) -> Result<(), crate::Error> {
fn remove_prop(&mut self, _prop: &<Self::Prop as PropName>::Names) -> Result<(), crate::Error> {
Err(crate::Error::PropReadOnly)
}
@@ -91,62 +88,45 @@ pub trait Resource: Clone + 'static {
principal: &Self::Principal,
) -> Result<UserPrivilegeSet, Self::Error>;
fn propfind(
fn propfind_typed(
&self,
path: &str,
props: &[&str],
prop: &PropfindType<<Self::Prop as PropName>::Names>,
principal_uri: &impl PrincipalUri,
principal: &Self::Principal,
) -> Result<ResponseElement<Self::Prop>, Self::Error> {
let mut props: HashSet<&str> = props.iter().cloned().collect();
// TODO: Support include element
let (props, invalid_props): (HashSet<<Self::Prop as PropName>::Names>, Vec<_>) = match prop
{
PropfindType::Propname => {
let props = Self::list_props()
.into_iter()
.map(|(ns, tag)| (ns.map(NamespaceOwned::from), tag.to_string()))
.collect_vec();
if props.contains(&"propname") {
if props.len() != 1 {
// propname MUST be the only queried prop per spec
return Err(
Error::BadRequest("propname MUST be the only queried prop".to_owned()).into(),
);
return Ok(ResponseElement {
href: path.to_owned(),
propstat: vec![PropstatWrapper::TagList(PropstatElement {
prop: TagList::from(props),
status: StatusCode::OK,
})],
..Default::default()
});
}
PropfindType::Allprop => (
Self::list_props()
.iter()
.map(|(_ns, name)| <Self::Prop as PropName>::Names::from_str(name).unwrap())
.collect(),
vec![],
),
PropfindType::Prop(PropElement(valid_tags, invalid_tags)) => (
valid_tags.iter().cloned().collect(),
invalid_tags.to_owned(),
),
};
let props = Self::list_props()
.into_iter()
.map(|(ns, tag)| (ns.to_owned(), tag.to_string()))
.collect_vec();
return Ok(ResponseElement {
href: path.to_owned(),
propstat: vec![PropstatWrapper::TagList(PropstatElement {
prop: TagList::from(props),
status: StatusCode::OK,
})],
..Default::default()
});
}
if props.contains(&"allprop") {
if props.len() != 1 {
// allprop MUST be the only queried prop per spec
return Err(
Error::BadRequest("allprop MUST be the only queried prop".to_owned()).into(),
);
}
props = Self::list_props()
.into_iter()
.map(|(_ns, tag)| tag)
.collect();
}
let mut valid_props = vec![];
let mut invalid_props = vec![];
for prop in props {
if let Ok(valid_prop) = <Self::Prop as EnumUnitVariants>::UnitVariants::from_str(prop) {
valid_props.push(valid_prop);
} else {
invalid_props.push(prop.to_string())
}
}
let prop_responses = valid_props
let prop_responses = props
.into_iter()
.map(|prop| self.get_prop(principal_uri, principal, &prop))
.collect::<Result<Vec<_>, Self::Error>>()?;
@@ -158,11 +138,7 @@ pub trait Resource: Clone + 'static {
if !invalid_props.is_empty() {
propstats.push(PropstatWrapper::TagList(PropstatElement {
status: StatusCode::NOT_FOUND,
prop: invalid_props
.into_iter()
.map(|tag| (None, tag))
.collect_vec()
.into(),
prop: invalid_props.into(),
}));
}
Ok(ResponseElement {

View File

@@ -4,7 +4,7 @@ mod resourcetype;
pub mod tag_list;
use derive_more::derive::From;
pub use multistatus::MultistatusElement;
pub use propfind::{PropElement, PropfindElement, PropfindType, Propname};
pub use propfind::{PropElement, PropfindElement, PropfindType};
pub use resourcetype::{Resourcetype, ResourcetypeInner};
use rustical_xml::{XmlDeserialize, XmlSerialize};
pub use tag_list::TagList;

View File

@@ -1,27 +1,85 @@
use quick_xml::events::Event;
use quick_xml::name::ResolveResult;
use rustical_xml::NamespaceOwned;
use rustical_xml::Unparsed;
use rustical_xml::XmlDeserialize;
use rustical_xml::XmlError;
use rustical_xml::XmlRootTag;
#[derive(Debug, Clone, XmlDeserialize, XmlRootTag, PartialEq)]
#[xml(root = b"propfind", ns = "crate::namespace::NS_DAV")]
pub struct PropfindElement {
pub struct PropfindElement<PN: XmlDeserialize> {
#[xml(ty = "untagged")]
pub prop: PropfindType,
pub prop: PropfindType<PN>,
}
#[derive(Debug, Clone, PartialEq)]
// pub struct PropElement<PN: XmlDeserialize = Propname>(#[xml(ty = "untagged", flatten)] pub Vec<PN>);
pub struct PropElement<PN: XmlDeserialize>(
// valid
pub Vec<PN>,
// invalid
pub Vec<(Option<NamespaceOwned>, String)>,
);
impl<PN: XmlDeserialize> XmlDeserialize for PropElement<PN> {
fn deserialize<R: std::io::BufRead>(
reader: &mut quick_xml::NsReader<R>,
start: &quick_xml::events::BytesStart,
empty: bool,
) -> Result<Self, XmlError> {
if empty {
return Ok(Self(vec![], vec![]));
}
let mut buf = Vec::new();
let mut valid_props = vec![];
let mut invalid_props = vec![];
loop {
let event = reader.read_event_into(&mut buf)?;
match &event {
Event::End(e) if e.name() == start.name() => {
break;
}
Event::Eof => return Err(XmlError::Eof),
// start of a child element
Event::Start(start) | Event::Empty(start) => {
let empty = matches!(event, Event::Empty(_));
let (ns, name) = reader.resolve_element(start.name());
let ns = match ns {
ResolveResult::Bound(ns) => Some(NamespaceOwned::from(ns)),
ResolveResult::Unknown(_ns) => todo!("handle error"),
ResolveResult::Unbound => None,
};
match PN::deserialize(reader, start, empty) {
Ok(propname) => valid_props.push(propname),
Err(XmlError::InvalidVariant(_)) => {
invalid_props
.push((ns, String::from_utf8_lossy(name.as_ref()).to_string()));
// Consume content
Unparsed::deserialize(reader, start, empty)?;
}
Err(err) => return Err(err),
}
}
Event::Text(_) | Event::CData(_) => {
return Err(XmlError::UnsupportedEvent("Not expecting text here"));
}
Event::Decl(_) | Event::Comment(_) | Event::DocType(_) | Event::PI(_) => { /* ignore */
}
Event::End(_end) => {
unreachable!(
"Unexpected closing tag for wrong element, should be handled by quick_xml"
);
}
}
}
Ok(Self(valid_props, invalid_props))
}
}
#[derive(Debug, Clone, XmlDeserialize, PartialEq)]
pub struct PropElement<PN: XmlDeserialize = Propname>(#[xml(ty = "untagged", flatten)] pub Vec<PN>);
#[derive(Debug, Clone, XmlDeserialize, PartialEq)]
pub struct Propname {
#[xml(ty = "namespace")]
pub ns: Option<NamespaceOwned>,
#[xml(ty = "tag_name")]
pub name: String,
}
#[derive(Debug, Clone, XmlDeserialize, PartialEq)]
pub enum PropfindType<PN: XmlDeserialize = Propname> {
pub enum PropfindType<PN: XmlDeserialize> {
#[xml(ns = "crate::namespace::NS_DAV")]
Propname,
#[xml(ns = "crate::namespace::NS_DAV")]

View File

@@ -1,6 +1,6 @@
use rustical_xml::{ValueDeserialize, ValueSerialize, XmlDeserialize};
use super::{PropfindType, Propname};
use super::PropfindType;
#[derive(Clone, Debug, PartialEq)]
pub enum SyncLevel {
@@ -37,7 +37,7 @@ impl ValueSerialize for SyncLevel {
// <!-- DAV:limit defined in RFC 5323, Section 5.17 -->
// <!-- DAV:prop defined in RFC 4918, Section 14.18 -->
#[xml(ns = "crate::namespace::NS_DAV")]
pub struct SyncCollectionRequest<PN: XmlDeserialize = Propname> {
pub struct SyncCollectionRequest<PN: XmlDeserialize> {
#[xml(ns = "crate::namespace::NS_DAV")]
pub sync_token: String,
#[xml(ns = "crate::namespace::NS_DAV")]

View File

@@ -1,10 +1,13 @@
use derive_more::derive::From;
use quick_xml::name::Namespace;
use rustical_xml::XmlSerialize;
use quick_xml::{
events::{BytesStart, Event},
name::Namespace,
};
use rustical_xml::{NamespaceOwned, XmlSerialize};
use std::collections::HashMap;
#[derive(Clone, Debug, PartialEq, From)]
pub struct TagList(Vec<(Option<Namespace<'static>>, String)>);
pub struct TagList(Vec<(Option<NamespaceOwned>, String)>);
impl XmlSerialize for TagList {
fn serialize<W: std::io::Write>(
@@ -14,22 +17,10 @@ impl XmlSerialize for TagList {
namespaces: &HashMap<Namespace, &[u8]>,
writer: &mut quick_xml::Writer<W>,
) -> std::io::Result<()> {
#[derive(Debug, XmlSerialize, PartialEq)]
struct Inner(#[xml(ty = "untagged", flatten)] Vec<Tag>);
#[derive(Debug, XmlSerialize, PartialEq)]
struct Tag(
#[xml(ty = "namespace")] Option<Namespace<'static>>,
#[xml(ty = "tag_name")] String,
);
Inner(
self.0
.iter()
.map(|(ns, tag)| Tag(ns.to_owned(), tag.to_owned()))
.collect(),
)
.serialize(ns, tag, namespaces, writer)
for (_ns, tag) in &self.0 {
writer.write_event(Event::Empty(BytesStart::new(tag)))?;
}
Ok(())
}
#[allow(refining_impl_trait)]

View File

@@ -1,87 +0,0 @@
use rustical_dav::xml::{PropElement, PropfindElement, PropfindType, Propname};
use rustical_xml::de::XmlDocument;
#[test]
fn propfind_allprop() {
let propfind = PropfindElement::parse_str(
r#"
<propfind xmlns="DAV:">
<allprop />
</propfind>
"#,
)
.unwrap();
assert_eq!(
propfind,
PropfindElement {
prop: PropfindType::Allprop
}
);
}
#[test]
fn propfind_propname() {
let propfind = PropfindElement::parse_str(
r#"
<propfind xmlns="DAV:">
<propname />
</propfind>
"#,
)
.unwrap();
assert_eq!(
propfind,
PropfindElement {
prop: PropfindType::Propname
}
);
}
#[test]
fn propfind_prop() {
let propfind = PropfindElement::parse_str(
r#"
<propfind xmlns="DAV:">
<prop>
<displayname />
<color />
</prop>
</propfind>
"#,
)
.unwrap();
assert_eq!(
propfind,
PropfindElement {
prop: PropfindType::Prop(PropElement(vec![
Propname {
name: "displayname".to_owned(),
ns: Some("DAV:".to_owned().into())
},
Propname {
name: "color".to_owned(),
ns: Some("DAV:".to_owned().into())
},
]))
}
);
}
/// Example taken from DAVx5
#[test]
fn propfind_decl() {
let propfind = PropfindElement::parse_str(
r#"
<?xml version='1.0' encoding='UTF-8' ?>
<propfind xmlns="DAV:" xmlns:CAL="urn:ietf:params:xml:ns:caldav" xmlns:CARD="urn:ietf:params:xml:ns:carddav">
<prop>
<CARD:max-resource-size />
<CARD:supported-address-data />
<supported-report-set />
<n0:getctag xmlns:n0="http://calendarserver.org/ns/" />
<sync-token />
</prop>
</propfind>
"#
).unwrap();
}