diff --git a/crates/dav-proto/resources/requests/lockinfo-001.json b/crates/dav-proto/resources/requests/lockinfo-001.json index ae2b2587..34cb0cdd 100644 --- a/crates/dav-proto/resources/requests/lockinfo-001.json +++ b/crates/dav-proto/resources/requests/lockinfo-001.json @@ -5,8 +5,8 @@ { "type": "ElementStart", "data": { - "name": "D:href", - "attrs": null + "name": "href", + "attrs": "xmlns=\"DAV:\"" } }, { diff --git a/crates/dav-proto/resources/requests/lockinfo-002.json b/crates/dav-proto/resources/requests/lockinfo-002.json index e405d67d..e6138681 100644 --- a/crates/dav-proto/resources/requests/lockinfo-002.json +++ b/crates/dav-proto/resources/requests/lockinfo-002.json @@ -5,8 +5,8 @@ { "type": "ElementStart", "data": { - "name": "D:href", - "attrs": null + "name": "href", + "attrs": "xmlns=\"DAV:\"" } }, { diff --git a/crates/dav-proto/resources/requests/propfind-001.json b/crates/dav-proto/resources/requests/propfind-001.json index 0cb6e986..769ce837 100644 --- a/crates/dav-proto/resources/requests/propfind-001.json +++ b/crates/dav-proto/resources/requests/propfind-001.json @@ -4,29 +4,29 @@ { "type": "DeadProperty", "data": { - "name": "R:bigbox", - "attrs": null + "name": "bigbox", + "attrs": "xmlns=\"http://ns.example.com/boxschema/\"" } }, { "type": "DeadProperty", "data": { - "name": "R:author", - "attrs": null + "name": "author", + "attrs": "xmlns=\"http://ns.example.com/boxschema/\"" } }, { "type": "DeadProperty", "data": { - "name": "R:DingALing", - "attrs": null + "name": "DingALing", + "attrs": "xmlns=\"http://ns.example.com/boxschema/\"" } }, { "type": "DeadProperty", "data": { - "name": "R:Random", - "attrs": null + "name": "Random", + "attrs": "xmlns=\"http://ns.example.com/boxschema/\"" } } ] diff --git a/crates/dav-proto/resources/requests/propfind-007.json b/crates/dav-proto/resources/requests/propfind-007.json index 8abfd1d7..3963b0e4 100644 --- a/crates/dav-proto/resources/requests/propfind-007.json +++ b/crates/dav-proto/resources/requests/propfind-007.json @@ -22,8 +22,8 @@ { "type": "DeadProperty", "data": { - "name": "apple:calendar-color", - "attrs": null + "name": "calendar-color", + "attrs": "xmlns=\"http://apple.com/ns/ical/\"" } }, { diff --git a/crates/dav-proto/resources/requests/propfind-010.json b/crates/dav-proto/resources/requests/propfind-010.json new file mode 100644 index 00000000..42e94363 --- /dev/null +++ b/crates/dav-proto/resources/requests/propfind-010.json @@ -0,0 +1,36 @@ +{ + "type": "Prop", + "data": [ + { + "type": "WebDav", + "data": { + "type": "ResourceType" + } + }, + { + "type": "WebDav", + "data": { + "type": "DisplayName" + } + }, + { + "type": "WebDav", + "data": { + "type": "SyncToken" + } + }, + { + "type": "WebDav", + "data": { + "type": "GetCTag" + } + }, + { + "type": "DeadProperty", + "data": { + "name": "me-card", + "attrs": "xmlns=\"http://calendarserver.org/ns/\" hello=\"world & test\"" + } + } + ] +} \ No newline at end of file diff --git a/crates/dav-proto/resources/requests/propfind-010.xml b/crates/dav-proto/resources/requests/propfind-010.xml new file mode 100644 index 00000000..b9fb7388 --- /dev/null +++ b/crates/dav-proto/resources/requests/propfind-010.xml @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/crates/dav-proto/resources/requests/report-022.json b/crates/dav-proto/resources/requests/report-022.json index 238f186d..76eb5a5d 100644 --- a/crates/dav-proto/resources/requests/report-022.json +++ b/crates/dav-proto/resources/requests/report-022.json @@ -14,8 +14,8 @@ "property": { "type": "DeadProperty", "data": { - "name": "B:title", - "attrs": null + "name": "title", + "attrs": "xmlns=\"http://www.example.com/ns/\"" } }, "match_": "Sales" @@ -31,29 +31,29 @@ { "type": "DeadProperty", "data": { - "name": "B:department", - "attrs": null + "name": "department", + "attrs": "xmlns=\"http://www.example.com/ns/\"" } }, { "type": "DeadProperty", "data": { - "name": "B:phone", - "attrs": null + "name": "phone", + "attrs": "xmlns=\"http://www.example.com/ns/\"" } }, { "type": "DeadProperty", "data": { - "name": "B:office", - "attrs": null + "name": "office", + "attrs": "xmlns=\"http://www.example.com/ns/\"" } }, { "type": "DeadProperty", "data": { - "name": "B:salary", - "attrs": null + "name": "salary", + "attrs": "xmlns=\"http://www.example.com/ns/\"" } } ], diff --git a/crates/dav-proto/src/parser/mod.rs b/crates/dav-proto/src/parser/mod.rs index aa79a521..fa94b08a 100644 --- a/crates/dav-proto/src/parser/mod.rs +++ b/crates/dav-proto/src/parser/mod.rs @@ -20,11 +20,14 @@ pub mod tokenizer; #[derive(Debug, Clone)] pub enum Error { - Xml(quick_xml::Error), - UnexpectedToken { - expected: Option>, - found: Token<'static>, - }, + Xml(Box), + UnexpectedToken(Box), +} + +#[derive(Debug, Clone)] +pub struct UnexpectedToken { + pub expected: Option>, + pub found: Token<'static>, } pub type Result = std::result::Result; @@ -43,7 +46,10 @@ pub enum Token<'x> { } #[derive(Debug, Clone)] -pub struct RawElement<'x>(pub BytesStart<'x>); +pub struct RawElement<'x> { + pub element: BytesStart<'x>, + pub namespace: Option>, +} pub trait DavParser: Sized { fn parse(stream: &mut Tokenizer<'_>) -> Result; @@ -89,20 +95,50 @@ impl Token<'_> { match self { Token::ElementStart { name, raw } => Token::ElementStart { name, - raw: RawElement(raw.0.into_owned()), + raw: raw.into_owned(), }, Token::ElementEnd => Token::ElementEnd, Token::Bytes(bytes) => Token::Bytes(bytes.into_owned().into()), Token::Text(text) => Token::Text(text.into_owned().into()), - Token::UnknownElement(raw) => Token::UnknownElement(RawElement(raw.0.into_owned())), + Token::UnknownElement(raw) => Token::UnknownElement(raw.into_owned()), Token::Eof => Token::Eof, } } pub fn into_unexpected(self) -> Error { - Error::UnexpectedToken { + Error::UnexpectedToken(Box::new(UnexpectedToken { expected: None, found: self.into_owned(), + })) + } +} + +impl<'x> RawElement<'x> { + pub fn new(element: BytesStart<'x>) -> Self { + RawElement { + element, + namespace: None, + } + } + + pub fn with_namespace(self, namespace: quick_xml::name::Namespace<'_>) -> Self { + RawElement { + element: self.element, + namespace: Some(Cow::Owned(namespace.into_inner().to_vec())), + } + } + + pub fn with_namespace_static(self, namespace: &'static [u8]) -> Self { + RawElement { + element: self.element, + namespace: Some(Cow::Borrowed(namespace)), + } + } + + pub fn into_owned(self) -> RawElement<'static> { + RawElement { + element: self.element.into_owned(), + namespace: self.namespace, } } } @@ -123,16 +159,17 @@ impl PartialEq for Token<'_> { ) => { l_name == r_name && l_raw - .0 + .element .attributes_raw() .trim_ascii() - .eq_ignore_ascii_case(r_raw.0.attributes_raw().trim_ascii()) + .eq_ignore_ascii_case(r_raw.element.attributes_raw().trim_ascii()) } (Self::Bytes(l0), Self::Bytes(r0)) => l0 == r0, (Self::Text(l0), Self::Text(r0)) => l0 == r0, - (Self::UnknownElement(l0), Self::UnknownElement(r0)) => { - l0.0.as_ref().eq_ignore_ascii_case(r0.0.as_ref()) - } + (Self::UnknownElement(l0), Self::UnknownElement(r0)) => l0 + .element + .as_ref() + .eq_ignore_ascii_case(r0.element.as_ref()), _ => core::mem::discriminant(self) == core::mem::discriminant(other), } } @@ -140,19 +177,19 @@ impl PartialEq for Token<'_> { impl NamedElement { pub fn into_unexpected(self) -> Error { - Error::UnexpectedToken { + Error::UnexpectedToken(Box::new(UnexpectedToken { expected: None, found: Token::ElementStart { name: self, - raw: RawElement(BytesStart::new("")), + raw: RawElement::new(BytesStart::new("")), }, - } + })) } } impl Default for RawElement<'_> { fn default() -> Self { - RawElement(BytesStart::new("")) + RawElement::new(BytesStart::new("")) } } @@ -160,9 +197,9 @@ impl Display for Error { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { match self { Error::Xml(err) => write!(f, "XML error: {}", err), - Error::UnexpectedToken { expected, found } => { - write!(f, "Unexpected token: {found:?}")?; - if let Some(expected) = expected { + Error::UnexpectedToken(err) => { + write!(f, "Unexpected token: {:?}", err.found)?; + if let Some(expected) = &err.expected { write!(f, ", expected: {expected:?}")?; } Ok(()) diff --git a/crates/dav-proto/src/parser/property.rs b/crates/dav-proto/src/parser/property.rs index 8c19b472..41ea17f2 100644 --- a/crates/dav-proto/src/parser/property.rs +++ b/crates/dav-proto/src/parser/property.rs @@ -65,7 +65,7 @@ impl Tokenizer<'_> { break; } Token::UnknownElement(name) => { - elements.push(DavProperty::DeadProperty(name.into())); + elements.push(DavProperty::DeadProperty((&name).into())); self.expect_element_end()?; } token => return Err(token.into_unexpected()), @@ -369,7 +369,7 @@ impl Tokenizer<'_> { } Token::UnknownElement(raw) => { elements.push(DavPropertyValue { - property: DavProperty::DeadProperty(raw.into()), + property: DavProperty::DeadProperty((&raw).into()), value: DavValue::DeadProperty(DeadProperty::parse(self)?), }); } diff --git a/crates/dav-proto/src/parser/tokenizer.rs b/crates/dav-proto/src/parser/tokenizer.rs index c78aba41..0a66a388 100644 --- a/crates/dav-proto/src/parser/tokenizer.rs +++ b/crates/dav-proto/src/parser/tokenizer.rs @@ -12,7 +12,7 @@ use quick_xml::{ use crate::schema::{Attribute, AttributeValue, Element, NamedElement, Namespace}; -use super::{Error, RawElement, Token, XmlValueParser}; +use super::{Error, RawElement, Token, UnexpectedToken, XmlValueParser}; pub struct Tokenizer<'x> { xml: NsReader<&'x [u8]>, @@ -47,7 +47,10 @@ impl<'x> Tokenizer<'x> { return Ok(Token::ElementEnd); } Event::Text(text) if text.iter().any(|ch| !ch.is_ascii_whitespace()) => { - return text.unescape().map(Token::Text).map_err(Error::Xml); + return text + .unescape() + .map(Token::Text) + .map_err(|err| Error::Xml(Box::new(err))); } Event::CData(bytes) => return Ok(Token::Bytes(bytes.into_inner())), Event::Eof => return Ok(Token::Eof), @@ -59,26 +62,29 @@ impl<'x> Tokenizer<'x> { // Parse element let name = tag.name(); match resolve_result { - ResolveResult::Bound(ns) if !ns.as_ref().is_empty() => { + ResolveResult::Bound(raw_ns) if !raw_ns.as_ref().is_empty() => { if let (Some(ns), Some(element)) = ( - Namespace::try_parse(ns.as_ref()), + Namespace::try_parse(raw_ns.as_ref()), Element::try_parse(name.local_name().as_ref()).copied(), ) { return Ok(Token::ElementStart { name: NamedElement { ns, element }, - raw: RawElement(tag), + raw: RawElement::new(tag) + .with_namespace_static(ns.namespace().as_bytes()), }); } else { - return Ok(Token::UnknownElement(RawElement(tag))); + return Ok(Token::UnknownElement( + RawElement::new(tag).with_namespace(raw_ns), + )); } } ResolveResult::Unknown(p) => { - return Err(Error::Xml(quick_xml::Error::Namespace( + return Err(Error::Xml(Box::new(quick_xml::Error::Namespace( quick_xml::name::NamespaceError::UnknownPrefix(p), - ))) + )))) } _ => { - return Ok(Token::UnknownElement(RawElement(tag))); + return Ok(Token::UnknownElement(RawElement::new(tag))); } } } @@ -87,24 +93,24 @@ impl<'x> Tokenizer<'x> { pub fn unwrap_named_element(&mut self) -> super::Result { match self.token()? { Token::ElementStart { name, .. } => Ok(name), - found => Err(Error::UnexpectedToken { + found => Err(Error::UnexpectedToken(Box::new(UnexpectedToken { expected: None, found: found.into_owned(), - }), + }))), } } pub fn expect_named_element(&mut self, expected: NamedElement) -> super::Result<()> { match self.token()? { Token::ElementStart { name, .. } if name == expected => Ok(()), - found => Err(Error::UnexpectedToken { + found => Err(Error::UnexpectedToken(Box::new(UnexpectedToken { expected: Token::ElementStart { name: expected, raw: RawElement::default(), } .into(), found: found.into_owned(), - }), + }))), } } @@ -112,24 +118,24 @@ impl<'x> Tokenizer<'x> { match self.token()? { Token::ElementStart { name, .. } if name == expected => Ok(true), Token::Eof => Ok(false), - found => Err(Error::UnexpectedToken { + found => Err(Error::UnexpectedToken(Box::new(UnexpectedToken { expected: Token::ElementStart { name: expected, raw: RawElement::default(), } .into(), found: found.into_owned(), - }), + }))), } } pub fn expect_element_end(&mut self) -> super::Result<()> { match self.token()? { Token::ElementEnd => Ok(()), - found => Err(Error::UnexpectedToken { + found => Err(Error::UnexpectedToken(Box::new(UnexpectedToken { expected: Token::ElementEnd.into(), found: found.into_owned(), - }), + }))), } } @@ -249,7 +255,7 @@ impl RawElement<'_> { pub fn attributes( &self, ) -> impl Iterator>> + '_ { - self.0.attributes().filter_map(|attr| match attr { + self.element.attributes().filter_map(|attr| match attr { Ok(attr) => match attr.unescape_value() { Ok(value) => Attribute::from_param(attr.key.as_ref(), value).map(Ok), Err(err) => Some(Err(err.into())), @@ -261,13 +267,13 @@ impl RawElement<'_> { impl From for Error { fn from(err: quick_xml::Error) -> Self { - Error::Xml(err) + Error::Xml(Box::new(err)) } } impl From for Error { fn from(err: AttrError) -> Self { - Error::Xml(err.into()) + Error::Xml(Box::new(err.into())) } } diff --git a/crates/dav-proto/src/requests/mod.rs b/crates/dav-proto/src/requests/mod.rs index 906f768a..0b902f2f 100644 --- a/crates/dav-proto/src/requests/mod.rs +++ b/crates/dav-proto/src/requests/mod.rs @@ -27,7 +27,7 @@ impl DavParser for DeadProperty { loop { match stream.token()? { Token::ElementStart { raw, .. } | Token::UnknownElement(raw) => { - items.0.push(DeadPropertyTag::ElementStart(raw.into())); + items.0.push(DeadPropertyTag::ElementStart((&raw).into())); depth += 1; } Token::ElementEnd => { @@ -142,14 +142,42 @@ impl ArchivedDeadElementTag { } } -impl From> for DeadElementTag { - fn from(raw: RawElement<'_>) -> Self { - let name = String::from_utf8_lossy(raw.0.name().as_ref().trim_ascii()).into_owned(); - let attr = raw.0.attributes_raw().trim_ascii(); +impl From<&RawElement<'_>> for DeadElementTag { + fn from(raw: &RawElement<'_>) -> Self { + let name = std::str::from_utf8(raw.element.local_name().as_ref()) + .unwrap_or("invalid-utf8") + .trim_ascii() + .to_string(); + let mut attrs = String::with_capacity(raw.element.attributes_raw().len()); + if let Some(namespace) = &raw.namespace { + attrs.push_str("xmlns=\""); + attrs.push_str(std::str::from_utf8(namespace).unwrap_or("invalid-utf8")); + attrs.push('"'); + } + + for attr in raw.element.attributes().flatten() { + if attr.key.as_ref() == b"xmlns" || attr.key.as_ref().starts_with(b"xmlns:") { + // Skip namespace attributes + continue; + } + if let (Ok(key), Ok(value)) = ( + std::str::from_utf8(attr.key.as_ref()), + std::str::from_utf8(attr.value.as_ref()), + ) { + if !attrs.is_empty() { + attrs.push(' '); + } + attrs.push_str(key); + attrs.push('='); + attrs.push('"'); + attrs.push_str(value); + attrs.push('"'); + } + } DeadElementTag { name, - attrs: (!attr.is_empty()).then(|| String::from_utf8_lossy(attr).into_owned()), + attrs: (!attrs.is_empty()).then_some(attrs), } } } diff --git a/crates/dav-proto/src/requests/report.rs b/crates/dav-proto/src/requests/report.rs index a867fb50..fd1dcd67 100644 --- a/crates/dav-proto/src/requests/report.rs +++ b/crates/dav-proto/src/requests/report.rs @@ -528,7 +528,7 @@ impl DavParser for ExpandProperty { depth: depth - 1, }); } else { - let attrs = raw.0.attributes_raw().trim_ascii(); + let attrs = raw.element.attributes_raw().trim_ascii(); ep.properties.push(ExpandPropertyItem { property: DavProperty::DeadProperty(DeadElementTag { name, diff --git a/crates/dav-proto/src/schema/mod.rs b/crates/dav-proto/src/schema/mod.rs index 4a3e2b9e..62d9830c 100644 --- a/crates/dav-proto/src/schema/mod.rs +++ b/crates/dav-proto/src/schema/mod.rs @@ -66,10 +66,8 @@ impl Namespace { Namespace::CalendarServer => "C", } } -} -impl AsRef for Namespace { - fn as_ref(&self) -> &str { + pub fn namespace(&self) -> &'static str { match self { Namespace::Dav => "DAV:", Namespace::CalDav => "urn:ietf:params:xml:ns:caldav", @@ -79,6 +77,12 @@ impl AsRef for Namespace { } } +impl AsRef for Namespace { + fn as_ref(&self) -> &str { + self.namespace() + } +} + #[derive(Debug, Clone, Copy, PartialEq, Eq)] #[cfg_attr(test, derive(serde::Serialize, serde::Deserialize))] pub enum Element { diff --git a/tests/src/webdav/lock.rs b/tests/src/webdav/lock.rs index b6a973a5..43316cc1 100644 --- a/tests/src/webdav/lock.rs +++ b/tests/src/webdav/lock.rs @@ -32,7 +32,7 @@ pub async fn test(test: &WebDavTest) { .with_status(StatusCode::CREATED); let lock_token = response .with_value( - "D:prop.D:lockdiscovery.D:activelock.D:owner.D:href", + "D:prop.D:lockdiscovery.D:activelock.D:owner.href", "super-owner", ) .with_value("D:prop.D:lockdiscovery.D:activelock.D:depth", "infinity") @@ -55,7 +55,7 @@ pub async fn test(test: &WebDavTest) { .await .with_status(StatusCode::OK) .with_value( - "D:prop.D:lockdiscovery.D:activelock.D:owner.D:href", + "D:prop.D:lockdiscovery.D:activelock.D:owner.href", "super-owner", ) .with_any_value( @@ -117,7 +117,7 @@ pub async fn test(test: &WebDavTest) { props .get(DavProperty::WebDav(WebDavProperty::LockDiscovery)) .with_some_values([ - "D:activelock.D:owner.D:href:super-owner", + "D:activelock.D:owner.href:super-owner", "D:activelock.D:depth:infinity", format!("D:activelock.D:locktoken.D:href:{lock_token}").as_str(), format!("D:activelock.D:lockroot.D:href:{path}").as_str(), diff --git a/tests/src/webdav/prop.rs b/tests/src/webdav/prop.rs index e1c89a66..ffedb231 100644 --- a/tests/src/webdav/prop.rs +++ b/tests/src/webdav/prop.rs @@ -501,8 +501,8 @@ pub async fn test(test: &WebDavTest) { ), ( DavProperty::DeadProperty(DeadElementTag::new( - "C:my-dead-element".to_string(), - None, + "my-dead-element".to_string(), + Some("xmlns=\"http://example.com/ns/\"".to_string()), )), "this is a dead but exciting element", ), @@ -514,8 +514,8 @@ pub async fn test(test: &WebDavTest) { let mut props = vec![ ( DavProperty::DeadProperty(DeadElementTag::new( - "C:my-dead-element".to_string(), - None, + "my-dead-element".to_string(), + Some("xmlns=\"http://example.com/ns/\"".to_string()), )), "", ), @@ -604,8 +604,8 @@ pub async fn test(test: &WebDavTest) { let mut chunky_props = vec![ DavProperty::WebDav(WebDavProperty::DisplayName), DavProperty::DeadProperty(DeadElementTag::new( - "C:my-chunky-dead-element".to_string(), - None, + "my-chunky-dead-element".to_string(), + Some("xmlns=\"http://example.com/ns/\"".to_string()), )), ]; if !is_file {