Store dead element's namespaces (#1545)

This commit is contained in:
mdecimus 2025-05-25 13:04:19 +02:00
parent 1b1f85a156
commit 85bc434fbf
15 changed files with 198 additions and 86 deletions

View file

@ -5,8 +5,8 @@
{
"type": "ElementStart",
"data": {
"name": "D:href",
"attrs": null
"name": "href",
"attrs": "xmlns=\"DAV:\""
}
},
{

View file

@ -5,8 +5,8 @@
{
"type": "ElementStart",
"data": {
"name": "D:href",
"attrs": null
"name": "href",
"attrs": "xmlns=\"DAV:\""
}
},
{

View file

@ -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/\""
}
}
]

View file

@ -22,8 +22,8 @@
{
"type": "DeadProperty",
"data": {
"name": "apple:calendar-color",
"attrs": null
"name": "calendar-color",
"attrs": "xmlns=\"http://apple.com/ns/ical/\""
}
},
{

View file

@ -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\""
}
}
]
}

View file

@ -0,0 +1 @@
<d:propfind xmlns:d="DAV:" xmlns:cs="http://calendarserver.org/ns/" xmlns:c="urn:ietf:params:xml:ns:carddav"><d:prop><d:resourcetype /><d:displayname/><d:sync-token></d:sync-token><cs:getctag /><cs:me-card hello="world &amp; test"/></d:prop></d:propfind>

View file

@ -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/\""
}
}
],

View file

@ -20,11 +20,14 @@ pub mod tokenizer;
#[derive(Debug, Clone)]
pub enum Error {
Xml(quick_xml::Error),
UnexpectedToken {
expected: Option<Token<'static>>,
found: Token<'static>,
},
Xml(Box<quick_xml::Error>),
UnexpectedToken(Box<UnexpectedToken>),
}
#[derive(Debug, Clone)]
pub struct UnexpectedToken {
pub expected: Option<Token<'static>>,
pub found: Token<'static>,
}
pub type Result<T> = std::result::Result<T, Error>;
@ -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<Cow<'static, [u8]>>,
}
pub trait DavParser: Sized {
fn parse(stream: &mut Tokenizer<'_>) -> Result<Self>;
@ -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(())

View file

@ -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)?),
});
}

View file

@ -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<NamedElement> {
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<T: AttributeValue>(
&self,
) -> impl Iterator<Item = super::Result<Attribute<T>>> + '_ {
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<quick_xml::Error> for Error {
fn from(err: quick_xml::Error) -> Self {
Error::Xml(err)
Error::Xml(Box::new(err))
}
}
impl From<AttrError> for Error {
fn from(err: AttrError) -> Self {
Error::Xml(err.into())
Error::Xml(Box::new(err.into()))
}
}

View file

@ -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<RawElement<'_>> 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),
}
}
}

View file

@ -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,

View file

@ -66,10 +66,8 @@ impl Namespace {
Namespace::CalendarServer => "C",
}
}
}
impl AsRef<str> 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<str> for Namespace {
}
}
impl AsRef<str> 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 {

View file

@ -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(),

View file

@ -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 {