504 lines
18 KiB
Rust
504 lines
18 KiB
Rust
/*
|
|
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <hello@stalw.art>
|
|
*
|
|
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
|
*/
|
|
|
|
use super::{Error, RawElement, Token, UnexpectedToken, XmlValueParser};
|
|
use crate::schema::{Attribute, AttributeValue, Element, NamedElement, Namespace};
|
|
use quick_xml::{
|
|
NsReader, XmlVersion,
|
|
events::{Event, attributes::AttrError},
|
|
name::ResolveResult,
|
|
};
|
|
|
|
pub struct Tokenizer<'x> {
|
|
xml: NsReader<&'x [u8]>,
|
|
last_is_end: bool,
|
|
}
|
|
|
|
impl<'x> Tokenizer<'x> {
|
|
pub fn new(input: &'x [u8]) -> Self {
|
|
let mut xml = NsReader::from_reader(input);
|
|
xml.config_mut();
|
|
Self {
|
|
xml,
|
|
last_is_end: false,
|
|
}
|
|
}
|
|
|
|
pub fn token(&'_ mut self) -> super::Result<Token<'_>> {
|
|
loop {
|
|
if self.last_is_end {
|
|
self.last_is_end = false;
|
|
return Ok(Token::ElementEnd);
|
|
}
|
|
|
|
let (resolve_result, event) = self.xml.read_resolved_event()?;
|
|
let tag = match event {
|
|
Event::Start(tag) => tag,
|
|
Event::Empty(tag) => {
|
|
self.last_is_end = true;
|
|
tag
|
|
}
|
|
Event::End(_) => {
|
|
return Ok(Token::ElementEnd);
|
|
}
|
|
Event::Text(text) if text.iter().any(|ch| !ch.is_ascii_whitespace()) => {
|
|
return text
|
|
.xml_content(XmlVersion::Implicit1_0)
|
|
.map(Token::Text)
|
|
.map_err(|err| Error::Xml(Box::new(err.into())));
|
|
}
|
|
Event::GeneralRef(entity) => {
|
|
let entity_ref: &[u8] = entity.as_ref();
|
|
hashify::fnc_map!(entity_ref,
|
|
b"lt" => { return Ok(Token::Text("<".into())); },
|
|
b"gt" => { return Ok(Token::Text(">".into())); },
|
|
b"amp" => { return Ok(Token::Text("&".into())); },
|
|
b"apos" => { return Ok(Token::Text("'".into())); },
|
|
b"quot" => { return Ok(Token::Text("\"".into())); },
|
|
_ => {
|
|
if let Ok(Some(gr)) = entity.resolve_char_ref() {
|
|
return Ok(Token::Text(gr.to_string().into()));
|
|
}
|
|
}
|
|
);
|
|
|
|
return entity
|
|
.xml_content(XmlVersion::Implicit1_0)
|
|
.map(Token::Text)
|
|
.map_err(|err| Error::Xml(Box::new(err.into())));
|
|
}
|
|
Event::CData(bytes) => return Ok(Token::Bytes(bytes.into_inner())),
|
|
Event::Eof => return Ok(Token::Eof),
|
|
_ => {
|
|
continue;
|
|
}
|
|
};
|
|
|
|
// Parse element
|
|
let name = tag.name();
|
|
match resolve_result {
|
|
ResolveResult::Bound(raw_ns) if !raw_ns.as_ref().is_empty() => {
|
|
if let (Some(ns), Some(element)) = (
|
|
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::new(tag)
|
|
.with_namespace_static(ns.namespace().as_bytes()),
|
|
});
|
|
} else {
|
|
return Ok(Token::UnknownElement(
|
|
RawElement::new(tag).with_namespace(raw_ns),
|
|
));
|
|
}
|
|
}
|
|
ResolveResult::Unknown(p) => {
|
|
return Err(Error::Xml(Box::new(quick_xml::Error::Namespace(
|
|
quick_xml::name::NamespaceError::UnknownPrefix(p),
|
|
))));
|
|
}
|
|
_ => {
|
|
return Ok(Token::UnknownElement(RawElement::new(tag)));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
pub fn unwrap_named_element(&mut self) -> super::Result<NamedElement> {
|
|
match self.token()? {
|
|
Token::ElementStart { name, .. } => Ok(name),
|
|
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(Box::new(UnexpectedToken {
|
|
expected: Token::ElementStart {
|
|
name: expected,
|
|
raw: RawElement::default(),
|
|
}
|
|
.into(),
|
|
found: found.into_owned(),
|
|
}))),
|
|
}
|
|
}
|
|
|
|
pub fn expect_named_element_or_eof(&mut self, expected: NamedElement) -> super::Result<bool> {
|
|
match self.token()? {
|
|
Token::ElementStart { name, .. } if name == expected => Ok(true),
|
|
Token::Eof => Ok(false),
|
|
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(Box::new(UnexpectedToken {
|
|
expected: Token::ElementEnd.into(),
|
|
found: found.into_owned(),
|
|
}))),
|
|
}
|
|
}
|
|
|
|
pub fn seek_element_end(&mut self) -> super::Result<()> {
|
|
let mut depth = 1;
|
|
loop {
|
|
match self.token()? {
|
|
Token::ElementStart { .. } | Token::UnknownElement(_) => depth += 1,
|
|
Token::ElementEnd => {
|
|
depth -= 1;
|
|
if depth == 0 {
|
|
return Ok(());
|
|
}
|
|
}
|
|
Token::Eof => return Err(Token::Eof.into_unexpected()),
|
|
_ => {}
|
|
}
|
|
}
|
|
}
|
|
|
|
pub fn collect_string_value(&mut self) -> super::Result<Option<String>> {
|
|
let mut depth = 1;
|
|
let mut value: Option<String> = None;
|
|
|
|
loop {
|
|
match self.token()? {
|
|
Token::ElementStart { .. } | Token::UnknownElement(_) => depth += 1,
|
|
Token::ElementEnd => {
|
|
depth -= 1;
|
|
if depth == 0 {
|
|
break;
|
|
}
|
|
}
|
|
Token::Text(text) => {
|
|
if let Some(ref mut v) = value {
|
|
v.push_str(&text);
|
|
} else {
|
|
value = Some(text.into_owned());
|
|
}
|
|
}
|
|
Token::Bytes(bytes) => {
|
|
if let Some(ref mut v) = value {
|
|
v.push_str(&String::from_utf8_lossy(&bytes));
|
|
} else {
|
|
value = Some(String::from_utf8_lossy(&bytes).into_owned());
|
|
}
|
|
}
|
|
Token::Eof => return Err(Token::Eof.into_unexpected()),
|
|
}
|
|
}
|
|
|
|
Ok(value)
|
|
}
|
|
|
|
pub fn parse_value<T: XmlValueParser>(&mut self) -> super::Result<Option<Result<T, String>>> {
|
|
let mut depth = 1;
|
|
let mut result: Option<Result<T, String>> = None;
|
|
|
|
loop {
|
|
match self.token()? {
|
|
Token::ElementStart { .. } | Token::UnknownElement(_) => depth += 1,
|
|
Token::ElementEnd => {
|
|
depth -= 1;
|
|
if depth == 0 {
|
|
break;
|
|
}
|
|
}
|
|
Token::Text(text) => {
|
|
if let Some(value) = T::parse_str(&text) {
|
|
result = Some(Ok(value));
|
|
} else {
|
|
result = Some(Err(text.into_owned()));
|
|
}
|
|
}
|
|
Token::Bytes(bytes) => {
|
|
if let Some(value) = T::parse_bytes(&bytes) {
|
|
result = Some(Ok(value));
|
|
} else {
|
|
result = Some(Err(String::from_utf8_lossy(&bytes).into_owned()));
|
|
}
|
|
}
|
|
Token::Eof => return Err(Token::Eof.into_unexpected()),
|
|
}
|
|
}
|
|
|
|
Ok(result)
|
|
}
|
|
|
|
pub fn collect_elements<T>(&mut self) -> super::Result<Vec<T>>
|
|
where
|
|
T: TryFrom<NamedElement>,
|
|
{
|
|
let mut elements = Vec::with_capacity(2);
|
|
let mut depth = 1;
|
|
|
|
loop {
|
|
match self.token()? {
|
|
Token::ElementStart { name, .. } => {
|
|
if depth == 1
|
|
&& let Ok(element) = T::try_from(name)
|
|
{
|
|
elements.push(element);
|
|
}
|
|
|
|
depth += 1;
|
|
}
|
|
Token::UnknownElement(_) => {
|
|
depth += 1;
|
|
}
|
|
Token::ElementEnd => {
|
|
depth -= 1;
|
|
if depth == 0 {
|
|
break;
|
|
}
|
|
}
|
|
Token::Eof => break,
|
|
_ => {}
|
|
}
|
|
}
|
|
Ok(elements)
|
|
}
|
|
}
|
|
|
|
impl RawElement<'_> {
|
|
pub fn attributes<T: AttributeValue>(
|
|
&self,
|
|
) -> impl Iterator<Item = super::Result<Attribute<T>>> + '_ {
|
|
self.element.attributes().filter_map(|attr| match attr {
|
|
Ok(attr) => match attr.normalized_value(XmlVersion::Implicit1_0) {
|
|
Ok(value) => Attribute::from_param(attr.key.as_ref(), value).map(Ok),
|
|
Err(err) => Some(Err(err.into())),
|
|
},
|
|
Err(err) => Some(Err(err.into())),
|
|
})
|
|
}
|
|
}
|
|
|
|
impl From<quick_xml::Error> for Error {
|
|
fn from(err: quick_xml::Error) -> Self {
|
|
Error::Xml(Box::new(err))
|
|
}
|
|
}
|
|
|
|
impl From<AttrError> for Error {
|
|
fn from(err: AttrError) -> Self {
|
|
Error::Xml(Box::new(err.into()))
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
|
|
use std::borrow::Cow;
|
|
|
|
use crate::schema::{Collation, MatchType};
|
|
|
|
use super::*;
|
|
|
|
#[derive(Debug, PartialEq, Eq)]
|
|
pub enum TestToken<'x> {
|
|
ElementStart(NamedElement),
|
|
ElementEnd,
|
|
Attribute(Attribute<String>),
|
|
Bytes(Cow<'x, [u8]>),
|
|
Text(Cow<'x, str>),
|
|
}
|
|
|
|
#[test]
|
|
fn test_tokenizer() {
|
|
for (input, expected) in [
|
|
(
|
|
r#"<?xml version="1.0" encoding="utf-8" ?>
|
|
<C:calendar-query xmlns:D="DAV:"
|
|
xmlns:C="urn:ietf:params:xml:ns:caldav">
|
|
<D:prop>
|
|
<D:getetag/>
|
|
<C:calendar-data/>
|
|
</D:prop>
|
|
<C:filter>
|
|
<C:comp-filter name="VCALENDAR"/>
|
|
</C:filter>
|
|
</C:calendar-query>"#,
|
|
vec![
|
|
TestToken::ElementStart(NamedElement {
|
|
ns: Namespace::CalDav,
|
|
element: Element::CalendarQuery,
|
|
}),
|
|
TestToken::ElementStart(NamedElement {
|
|
ns: Namespace::Dav,
|
|
element: Element::Prop,
|
|
}),
|
|
TestToken::ElementStart(NamedElement {
|
|
ns: Namespace::Dav,
|
|
element: Element::Getetag,
|
|
}),
|
|
TestToken::ElementEnd,
|
|
TestToken::ElementStart(NamedElement {
|
|
ns: Namespace::CalDav,
|
|
element: Element::CalendarData,
|
|
}),
|
|
TestToken::ElementEnd,
|
|
TestToken::ElementEnd,
|
|
TestToken::ElementStart(NamedElement {
|
|
ns: Namespace::CalDav,
|
|
element: Element::Filter,
|
|
}),
|
|
TestToken::ElementStart(NamedElement {
|
|
ns: Namespace::CalDav,
|
|
element: Element::CompFilter,
|
|
}),
|
|
TestToken::Attribute(Attribute::Name("VCALENDAR".to_string())),
|
|
TestToken::ElementEnd,
|
|
TestToken::ElementEnd,
|
|
TestToken::ElementEnd,
|
|
],
|
|
),
|
|
(
|
|
r#" <?xml version="1.0" encoding="utf-8" ?>
|
|
<C:addressbook-query xmlns:D="DAV:"
|
|
xmlns:C="urn:ietf:params:xml:ns:carddav">
|
|
<D:prop>
|
|
<D:getetag/>
|
|
<C:address-data>
|
|
<C:prop name="VERSION"/>
|
|
<C:prop name="UID"/>
|
|
<C:prop name="NICKNAME"/>
|
|
<C:prop name="EMAIL"/>
|
|
<C:prop name="FN"/>
|
|
</C:address-data>
|
|
</D:prop>
|
|
<C:filter>
|
|
<C:prop-filter name="NICKNAME">
|
|
<C:text-match collation="i;unicode-casemap"
|
|
match-type="equals"
|
|
>me</C:text-match>
|
|
</C:prop-filter>
|
|
</C:filter>
|
|
</C:addressbook-query>"#,
|
|
vec![
|
|
TestToken::ElementStart(NamedElement {
|
|
ns: Namespace::CardDav,
|
|
element: Element::AddressbookQuery,
|
|
}),
|
|
TestToken::ElementStart(NamedElement {
|
|
ns: Namespace::Dav,
|
|
element: Element::Prop,
|
|
}),
|
|
TestToken::ElementStart(NamedElement {
|
|
ns: Namespace::Dav,
|
|
element: Element::Getetag,
|
|
}),
|
|
TestToken::ElementEnd,
|
|
TestToken::ElementStart(NamedElement {
|
|
ns: Namespace::CardDav,
|
|
element: Element::AddressData,
|
|
}),
|
|
TestToken::ElementStart(NamedElement {
|
|
ns: Namespace::CardDav,
|
|
element: Element::Prop,
|
|
}),
|
|
TestToken::Attribute(Attribute::Name("VERSION".to_string())),
|
|
TestToken::ElementEnd,
|
|
TestToken::ElementStart(NamedElement {
|
|
ns: Namespace::CardDav,
|
|
element: Element::Prop,
|
|
}),
|
|
TestToken::Attribute(Attribute::Name("UID".to_string())),
|
|
TestToken::ElementEnd,
|
|
TestToken::ElementStart(NamedElement {
|
|
ns: Namespace::CardDav,
|
|
element: Element::Prop,
|
|
}),
|
|
TestToken::Attribute(Attribute::Name("NICKNAME".to_string())),
|
|
TestToken::ElementEnd,
|
|
TestToken::ElementStart(NamedElement {
|
|
ns: Namespace::CardDav,
|
|
element: Element::Prop,
|
|
}),
|
|
TestToken::Attribute(Attribute::Name("EMAIL".to_string())),
|
|
TestToken::ElementEnd,
|
|
TestToken::ElementStart(NamedElement {
|
|
ns: Namespace::CardDav,
|
|
element: Element::Prop,
|
|
}),
|
|
TestToken::Attribute(Attribute::Name("FN".to_string())),
|
|
TestToken::ElementEnd,
|
|
TestToken::ElementEnd,
|
|
TestToken::ElementEnd,
|
|
TestToken::ElementStart(NamedElement {
|
|
ns: Namespace::CardDav,
|
|
element: Element::Filter,
|
|
}),
|
|
TestToken::ElementStart(NamedElement {
|
|
ns: Namespace::CardDav,
|
|
element: Element::PropFilter,
|
|
}),
|
|
TestToken::Attribute(Attribute::Name("NICKNAME".to_string())),
|
|
TestToken::ElementStart(NamedElement {
|
|
ns: Namespace::CardDav,
|
|
element: Element::TextMatch,
|
|
}),
|
|
TestToken::Attribute(Attribute::Collation(Collation::UnicodeCasemap)),
|
|
TestToken::Attribute(Attribute::MatchType(MatchType::Equals)),
|
|
TestToken::Text("me".into()),
|
|
TestToken::ElementEnd,
|
|
TestToken::ElementEnd,
|
|
TestToken::ElementEnd,
|
|
TestToken::ElementEnd,
|
|
],
|
|
),
|
|
] {
|
|
let mut tokenizer = Tokenizer::new(input.as_bytes());
|
|
let mut result = vec![];
|
|
|
|
loop {
|
|
match tokenizer.token() {
|
|
Ok(token) => match token {
|
|
Token::ElementStart { name, raw } => {
|
|
result.push(TestToken::ElementStart(name));
|
|
for attr in raw.attributes::<String>() {
|
|
result.push(TestToken::Attribute(attr.unwrap()));
|
|
}
|
|
}
|
|
Token::ElementEnd => {
|
|
result.push(TestToken::ElementEnd);
|
|
}
|
|
Token::Bytes(cow) => {
|
|
result.push(TestToken::Bytes(cow.into_owned().into()));
|
|
}
|
|
Token::Text(cow) => {
|
|
result.push(TestToken::Text(cow.into_owned().into()));
|
|
}
|
|
Token::UnknownElement(_) => {
|
|
//result.push(TestToken::UnknownElement(unknown_element));
|
|
}
|
|
Token::Eof => break,
|
|
},
|
|
Err(err) => {
|
|
panic!("Error: {:?}", err);
|
|
}
|
|
}
|
|
}
|
|
|
|
assert_eq!(result, expected);
|
|
}
|
|
}
|
|
}
|