Import upstream v0.16.22, stripped

Upstream commit: 474dd0229cb20cf513036619781ed97bd8073c3f
Enterprise-only files removed or emptied: 63
Enterprise-only snippets removed: 117 in 50 files
Dangling module declarations removed: 5
Cargo edits turning enterprise off: 14
Verification: clean
Enterprise feature gates left for rebuilt features: 19 in 18 files

Produced by tools/fork/strip.py. The full report is in docs/fork/strip-reports/ on main.
This commit is contained in:
2026-09-18 10:21:56 -07:00
commit 7dae9b29fd
1650 changed files with 485521 additions and 0 deletions
+950
View File
@@ -0,0 +1,950 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use crate::{ByteRange, Condition, Depth, If, RequestHeaders, ResourceState, Return, Timeout};
use calcard::vcard::VCardVersion;
use std::ops::Range;
impl<'x> RequestHeaders<'x> {
pub fn new(uri: &'x str) -> Self {
RequestHeaders {
uri,
..Default::default()
}
}
pub fn parse(&mut self, key: &str, value: &'x str) -> bool {
hashify::fnc_map_ignore_case!(key.as_bytes(),
"Depth" => {
if let Some(depth) = Depth::parse(value.as_bytes()) {
self.depth = depth;
return true;
}
},
"Destination" => {
self.destination = Some(value);
return true;
},
"Lock-Token" => {
self.lock_token = Some(try_unwrap_coded_url(value));
return true;
},
"If" => {
let num = self.if_.len();
self.parse_if(value);
return self.if_.len() != num;
},
"If-Match" => {
let num = self.if_.len();
self.parse_if_match(value, false);
return self.if_.len() != num;
},
"If-None-Match" => {
let num = self.if_.len();
self.parse_if_match(value, true);
return self.if_.len() != num;
},
"Timeout" => {
let value = value.split_once(',').map(|(first, _)| first).unwrap_or(value).trim();
if let Some(seconds) = value.strip_prefix("Second-") {
if let Ok(seconds) = seconds.parse() {
self.timeout = Timeout::Second(seconds);
return true;
}
} else if value == "Infinite" {
self.timeout = Timeout::Infinite;
return true;
}
},
"Overwrite" => {
self.overwrite_fail = value == "F";
return true;
},
"CalDAV-Timezones" => {
self.no_timezones = value == "F";
return true;
},
"Prefer" => {
for value in value.split(&[',', ';']) {
match value.trim() {
"return=minimal" => self.ret = Return::Minimal,
"return=representation" => self.ret = Return::Representation,
"depth-noroot" => self.depth_no_root = true,
_ => {}
}
}
},
"Content-Type" => {
let value = value.trim();
if (2..=127).contains(&value.len()) {
self.content_type = Some(value);
}
return true;
},
"Accept" => {
let mut preferred: Option<(f32, VCardVersion)> = None;
for entry in value.split(',') {
let mut parts = entry.split(';');
if !parts.next().is_some_and(|media_type| media_type.trim().eq_ignore_ascii_case("text/vcard")) {
continue;
}
let mut version = None;
let mut quality = 1.0;
for param in parts {
let Some((name, param_value)) = param.split_once('=') else {
continue;
};
let param_value = param_value.trim().trim_matches('"');
if name.trim().eq_ignore_ascii_case("version") {
version = VCardVersion::try_parse(param_value);
} else if name.trim().eq_ignore_ascii_case("q") {
quality = param_value.parse().unwrap_or(1.0);
}
}
if let Some(version) = version
&& quality > 0.0
&& preferred.is_none_or(|(preferred_quality, _)| quality > preferred_quality) {
preferred = Some((quality, version));
}
}
if let Some((_, version)) = preferred {
self.vcard_version = Some(version);
}
return true;
},
"If-Schedule-Tag-Match" => {
self.if_schedule_tag = value.trim().trim_matches('"').parse().ok();
return true;
},
"Schedule-Reply" => {
self.no_schedule_reply = value == "F";
return true;
},
"Range" => {
self.range = ByteRange::parse(value);
return self.range.is_some();
},
"If-Range" => {
self.if_range = Some(value.trim());
return true;
},
_ => {}
);
false
}
pub fn has_if(&self) -> bool {
!self.if_.is_empty()
}
pub fn eval_if_resources(&self) -> impl Iterator<Item = &str> {
self.if_.iter().filter_map(|if_| if_.resource)
}
pub fn eval_if<T>(&self, resources: &[ResourceState<T>]) -> bool
where
T: AsRef<str>,
{
if self.if_.is_empty() {
return true;
}
'outer: for if_ in &self.if_ {
if if_.list.is_empty() {
continue;
}
let (current_token, current_etag) = resources
.iter()
.find_map(|r| {
if if_.resource == r.resource.as_ref().map(|v| v.as_ref()) {
Some((r.state_token.as_ref(), r.etag.as_ref()))
} else {
None
}
})
.unwrap_or_default();
for cond in if_.list.iter() {
match cond {
Condition::StateToken { is_not, token } => {
if !((current_token == *token) ^ is_not) {
continue 'outer;
}
}
Condition::ETag { is_not, tag } => {
if !((current_etag == *tag) ^ is_not) {
continue 'outer;
}
}
Condition::Exists { is_not } => {
if !((current_etag.is_empty()) ^ is_not) {
continue 'outer;
}
}
}
}
return true;
}
false
}
fn parse_if(&mut self, value: &'x str) {
let value = value.as_bytes();
let mut iter = value.iter().enumerate();
let mut resource = None;
while let Some((idx, ch)) = iter.next() {
match ch {
b'<' if resource.is_none() => {
for (to_idx, ch) in iter.by_ref() {
if *ch == b'>' {
resource = Some(std::str::from_utf8(&value[idx + 1..to_idx]).unwrap());
break;
}
}
}
b'(' => {
let mut is_not = false;
let mut conditions = Vec::new();
while let Some((idx, ch)) = iter.next() {
match ch {
b'N' => {
if matches!(iter.next(), Some((_, b'o')))
&& matches!(iter.next(), Some((_, b't')))
{
is_not = true;
} else {
return;
}
}
b'<' | b'[' => {
let (stop_char, is_etag) = match ch {
b'<' => (b'>', false),
b'[' => (b']', true),
_ => unreachable!(),
};
for (to_idx, ch) in iter.by_ref() {
if *ch == stop_char {
let value =
std::str::from_utf8(&value[idx + 1..to_idx]).unwrap();
let condition = if is_etag {
Condition::ETag { is_not, tag: value }
} else {
Condition::StateToken {
is_not,
token: value,
}
};
conditions.push(condition);
is_not = false;
break;
}
}
}
b')' => {
self.if_.push(If {
resource: resource.take(),
list: conditions,
});
break;
}
_ => {
if !ch.is_ascii_whitespace() {
return;
}
}
}
}
}
_ => {
if !ch.is_ascii_whitespace() {
return;
}
}
}
}
}
pub fn parse_if_match(&mut self, value: &'x str, is_not: bool) {
if value == "*" {
self.if_.push(If {
resource: None,
list: vec![Condition::Exists { is_not }],
});
} else if !is_not {
for etag in value.split(',') {
self.if_.push(If {
resource: None,
list: vec![Condition::ETag {
is_not,
tag: etag.trim(),
}],
});
}
} else {
let mut etags = Vec::new();
for etag in value.split(',') {
etags.push(Condition::ETag {
is_not,
tag: etag.trim(),
});
}
self.if_.push(If {
resource: None,
list: etags,
});
}
}
pub fn eval_if_range(&self, etag: &str, last_modified: Option<&str>) -> bool {
match self.if_range {
Some(validator) => {
!validator.starts_with("W/")
&& (validator == etag
|| last_modified.is_some_and(|last_modified| validator == last_modified))
}
None => true,
}
}
pub fn base_uri(&self) -> Option<&str> {
dav_base_uri(self.uri)
}
}
impl ByteRange {
pub fn parse(value: &str) -> Option<Self> {
let (unit, spec) = value.split_once('=')?;
if !unit.trim().eq_ignore_ascii_case("bytes") || spec.contains(',') {
return None;
}
let (start, end) = spec.split_once('-')?;
let (start, end) = (start.trim(), end.trim());
if !start.is_empty() {
let start = start.parse::<u64>().ok()?;
let end = if !end.is_empty() {
let end = end.parse::<u64>().ok()?;
if end < start {
return None;
}
Some(end)
} else {
None
};
Some(ByteRange::Offset { start, end })
} else {
end.parse::<u64>().ok().map(ByteRange::Suffix)
}
}
pub fn resolve(&self, size: u64) -> Option<Range<u64>> {
match self {
ByteRange::Offset { start, end } if *start < size => {
Some(*start..end.map_or(size, |end| std::cmp::min(end.saturating_add(1), size)))
}
ByteRange::Suffix(length) if *length > 0 && size > 0 => {
Some(size.saturating_sub(*length)..size)
}
_ => None,
}
}
}
pub fn dav_base_uri(uri: &str) -> Option<&str> {
// From a path ../dav/collection/account/..
// returns ../dav/collection/account without the trailing slash
let uri = uri.as_bytes();
let mut found_dav = false;
let mut last_idx = 0;
let mut sep_count = 0;
for (idx, ch) in uri.iter().enumerate() {
if *ch == b'/' {
if !found_dav {
found_dav = uri.get(idx + 1..idx + 5).is_some_and(|s| s == b"dav/");
} else if found_dav {
if sep_count == 2 {
break;
}
sep_count += 1;
}
}
last_idx = idx;
}
if sep_count == 2 {
uri.get(..last_idx + 1)
.map(|uri| std::str::from_utf8(uri).unwrap())
} else {
None
}
}
impl Depth {
pub fn parse(value: &[u8]) -> Option<Self> {
hashify::tiny_map!(value,
"0" => Depth::Zero,
"1" => Depth::One,
"infinity" => Depth::Infinity,
"infinite" => Depth::Infinity,
)
}
}
fn try_unwrap_coded_url(url: &str) -> &str {
url.strip_prefix("<")
.and_then(|url| url.strip_suffix(">"))
.unwrap_or(url)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_accept_vcard_version() {
for (header, expected) in [
("text/vcard", None),
("application/json", None),
("text/vcard; version=3.0", Some(VCardVersion::V3_0)),
("text/vcard;version=\"3.0\"", Some(VCardVersion::V3_0)),
("text/vcard; VERSION=3.0", Some(VCardVersion::V3_0)),
("text/vcard; version=3.0; q=1.0", Some(VCardVersion::V3_0)),
(
"text/vcard; version=3.0; charset=utf-8",
Some(VCardVersion::V3_0),
),
(
"text/vcard; charset=utf-8; version=3.0",
Some(VCardVersion::V3_0),
),
(
"text/vcard; version=4.0, text/vcard; version=3.0",
Some(VCardVersion::V4_0),
),
(
"text/vcard; version=4.0; q=0.5, text/vcard; version=3.0",
Some(VCardVersion::V3_0),
),
(
"text/vcard; q=0.5; version=4.0, text/vcard; q=1.0; version=3.0",
Some(VCardVersion::V3_0),
),
(
"text/vcard; version=4.0; q=0, text/vcard; version=3.0; q=0.1",
Some(VCardVersion::V3_0),
),
("*/*, text/vcard; version=3.0", Some(VCardVersion::V3_0)),
("text/vcard-custom; version=3.0", None),
] {
let mut headers = RequestHeaders::new("/dav/card/test/default/");
assert!(headers.parse("Accept", header));
assert_eq!(headers.vcard_version, expected, "failed for {header:?}");
}
}
#[test]
fn parse_range() {
for (header, expected) in [
(
"bytes=0-499",
Some(ByteRange::Offset {
start: 0,
end: Some(499),
}),
),
(
"bytes=500-",
Some(ByteRange::Offset {
start: 500,
end: None,
}),
),
(
"bytes = 500 - 999 ",
Some(ByteRange::Offset {
start: 500,
end: Some(999),
}),
),
(
"BYTES=0-0",
Some(ByteRange::Offset {
start: 0,
end: Some(0),
}),
),
("bytes=-500", Some(ByteRange::Suffix(500))),
("bytes=-0", Some(ByteRange::Suffix(0))),
("bytes=0-499,600-999", None),
("bytes=499-100", None),
("bytes=abc-def", None),
("bytes=-", None),
("bytes=0", None),
("items=0-499", None),
("0-499", None),
] {
let mut headers = RequestHeaders::new("/dav/file/test/file.txt");
assert_eq!(headers.parse("Range", header), expected.is_some());
assert_eq!(headers.range, expected, "failed for {header:?}");
}
for (range, size, expected) in [
(
ByteRange::Offset {
start: 0,
end: Some(499),
},
1000,
Some(0..500),
),
(
ByteRange::Offset {
start: 0,
end: Some(499),
},
100,
Some(0..100),
),
(
ByteRange::Offset {
start: 500,
end: None,
},
1000,
Some(500..1000),
),
(
ByteRange::Offset {
start: 999,
end: Some(u64::MAX),
},
1000,
Some(999..1000),
),
(
ByteRange::Offset {
start: 1000,
end: None,
},
1000,
None,
),
(
ByteRange::Offset {
start: 0,
end: None,
},
0,
None,
),
(ByteRange::Suffix(500), 1000, Some(500..1000)),
(ByteRange::Suffix(5000), 1000, Some(0..1000)),
(ByteRange::Suffix(0), 1000, None),
(ByteRange::Suffix(500), 0, None),
] {
assert_eq!(
range.resolve(size),
expected,
"failed for {range:?} of {size}"
);
}
}
#[test]
fn eval_if_range_header() {
const LAST_MODIFIED: &str = "Mon, 10 Aug 2026 12:00:00 GMT";
let mut headers = RequestHeaders::new("/dav/file/test/file.txt");
assert!(headers.eval_if_range("\"etag\"", Some(LAST_MODIFIED)));
for (validator, expected) in [
("\"etag\"", true),
(LAST_MODIFIED, true),
("W/\"etag\"", false),
("\"other\"", false),
("Sun, 09 Aug 2026 12:00:00 GMT", false),
] {
assert!(headers.parse("If-Range", validator));
assert_eq!(
headers.eval_if_range("\"etag\"", Some(LAST_MODIFIED)),
expected,
"failed for {validator:?}"
);
}
for (validator, expected) in [("\"etag\"", true), (LAST_MODIFIED, false)] {
assert!(headers.parse("If-Range", validator));
assert_eq!(
headers.eval_if_range("\"etag\"", None),
expected,
"failed for {validator:?}"
);
}
}
#[test]
fn base_uri() {
for (uri, expected_base) in [
(
"http://host/dav/collection/account/test/",
Some("http://host/dav/collection/account"),
),
(
"http://host/dav/collection/account/test",
Some("http://host/dav/collection/account"),
),
(
"http://host/dav/collection/account/",
Some("http://host/dav/collection/account"),
),
(
"http://host/dav/collection/account",
Some("http://host/dav/collection/account"),
),
(
"http://host/dev/dav/collection/account/test/",
Some("http://host/dev/dav/collection/account"),
),
(
"http://host/dev/dav/collection/account/test",
Some("http://host/dev/dav/collection/account"),
),
(
"http://host/dev/dav/collection/account/",
Some("http://host/dev/dav/collection/account"),
),
(
"http://host/dev/dav/collection/account",
Some("http://host/dev/dav/collection/account"),
),
(
"/dav/collection/account/test/",
Some("/dav/collection/account"),
),
(
"/dav/collection/account/test",
Some("/dav/collection/account"),
),
("/dav/collection/account/", Some("/dav/collection/account")),
("/dav/collection/account", Some("/dav/collection/account")),
] {
assert_eq!(RequestHeaders::new(uri).base_uri(), expected_base);
}
}
#[test]
fn eval_if_header() {
let mut headers = RequestHeaders::default();
assert!(headers.parse(
"If",
r#"(<urn:uuid:181d4fae-7d8c-11d0-a765-00a0c91e6bf2>
["I am an ETag"])
(["I am another ETag"])"#,
));
assert!(headers.eval_if(&[ResourceState {
resource: None,
state_token: "urn:uuid:181d4fae-7d8c-11d0-a765-00a0c91e6bf2",
etag: "\"I am an ETag\""
}]));
assert!(headers.eval_if(&[ResourceState {
resource: None,
state_token: "",
etag: "\"I am another ETag\""
}]));
assert!(!headers.eval_if(&[ResourceState {
resource: None,
state_token: "",
etag: "\"Unknown ETag\""
}]));
assert!(!headers.eval_if(&[ResourceState {
resource: None,
state_token: "urn:uuid:181d4fae-7d8c-11d0-a765-00a0c91e6bf2",
etag: ""
}]));
assert!(!headers.eval_if(&[ResourceState {
resource: None,
state_token: "urn:uuid:181d4fae-7d8c-11d0-a765-00a0c91e6bf2",
etag: "\"Other ETag\""
}]));
assert!(!headers.eval_if(&[ResourceState {
resource: None,
state_token: "",
etag: "\"I am an ETag\""
}]));
assert!(!headers.eval_if(&[ResourceState {
resource: None,
state_token: "urn:blah",
etag: "\"I am an ETag\""
}]));
assert!(headers.parse(
"If",
r#"(Not <urn:uuid:181d4fae-7d8c-11d0-a765-00a0c91e6bf2>
<urn:uuid:58f202ac-22cf-11d1-b12d-002035b29092>)"#,
));
assert!(headers.eval_if(&[ResourceState {
resource: None,
state_token: "urn:uuid:58f202ac-22cf-11d1-b12d-002035b29092",
etag: ""
}]));
assert!(!headers.eval_if(&[ResourceState {
resource: None,
state_token: "urn:uuid:181d4fae-7d8c-11d0-a765-00a0c91e6bf2",
etag: ""
}]));
assert!(headers.parse(
"If",
r#"(<urn:uuid:181d4fae-7d8c-11d0-a765-00a0c91e6bf2>)
(Not <DAV:no-lock>)"#
));
assert!(headers.eval_if(&[ResourceState {
resource: None,
state_token: "urn:uuid:181d4fae-7d8c-11d0-a765-00a0c91e6bf2",
etag: ""
}]));
assert!(headers.eval_if(&[ResourceState {
resource: None,
state_token: "urn:other-token",
etag: ""
}]));
}
#[test]
fn parse_headers() {
let mut headers = RequestHeaders::default();
assert!(headers.parse("Depth", "0"));
assert_eq!(headers.depth, Depth::Zero);
assert!(headers.parse("Destination", "/path/to/destination"));
assert_eq!(headers.destination, Some("/path/to/destination"));
assert!(headers.parse("Lock-Token", "<urn:uuid:1234>"));
assert_eq!(headers.lock_token, Some("urn:uuid:1234"));
for (input, expected) in [
(
"<urn:uuid:1234>(<urn:uuid:1234>)",
vec![If {
resource: "urn:uuid:1234".into(),
list: vec![Condition::StateToken {
is_not: false,
token: "urn:uuid:1234",
}],
}],
),
(
"<>(<>)",
vec![If {
resource: "".into(),
list: vec![Condition::StateToken {
is_not: false,
token: "",
}],
}],
),
(
r#"(<urn:uuid:181d4fae-7d8c-11d0-a765-00a0c91e6bf2>
["I am an ETag"])
(["I am another ETag"])"#,
vec![
If {
resource: None,
list: vec![
Condition::StateToken {
is_not: false,
token: "urn:uuid:181d4fae-7d8c-11d0-a765-00a0c91e6bf2",
},
Condition::ETag {
is_not: false,
tag: "\"I am an ETag\"",
},
],
},
If {
resource: None,
list: vec![Condition::ETag {
is_not: false,
tag: "\"I am another ETag\"",
}],
},
],
),
(
r#"(Not <urn:uuid:181d4fae-7d8c-11d0-a765-00a0c91e6bf2>
<urn:uuid:58f202ac-22cf-11d1-b12d-002035b29092>)"#,
vec![If {
resource: None,
list: vec![
Condition::StateToken {
is_not: true,
token: "urn:uuid:181d4fae-7d8c-11d0-a765-00a0c91e6bf2",
},
Condition::StateToken {
is_not: false,
token: "urn:uuid:58f202ac-22cf-11d1-b12d-002035b29092",
},
],
}],
),
(
r#"(<urn:uuid:181d4fae-7d8c-11d0-a765-00a0c91e6bf2>)
(Not <DAV:no-lock>)"#,
vec![
If {
resource: None,
list: vec![Condition::StateToken {
is_not: false,
token: "urn:uuid:181d4fae-7d8c-11d0-a765-00a0c91e6bf2",
}],
},
If {
resource: None,
list: vec![Condition::StateToken {
is_not: true,
token: "DAV:no-lock",
}],
},
],
),
(
r#"</resource1>
(<urn:uuid:181d4fae-7d8c-11d0-a765-00a0c91e6bf2>
[W/"A weak ETag"]) (["strong ETag"])"#,
vec![
If {
resource: "/resource1".into(),
list: vec![
Condition::StateToken {
is_not: false,
token: "urn:uuid:181d4fae-7d8c-11d0-a765-00a0c91e6bf2",
},
Condition::ETag {
is_not: false,
tag: "W/\"A weak ETag\"",
},
],
},
If {
resource: None,
list: vec![Condition::ETag {
is_not: false,
tag: "\"strong ETag\"",
}],
},
],
),
(
r#"<http://www.example.com/specs/>
(<urn:uuid:181d4fae-7d8c-11d0-a765-00a0c91e6bf2>)"#,
vec![If {
resource: "http://www.example.com/specs/".into(),
list: vec![Condition::StateToken {
is_not: false,
token: "urn:uuid:181d4fae-7d8c-11d0-a765-00a0c91e6bf2",
}],
}],
),
(
r#"</specs/rfc2518.doc> (["4217"])"#,
vec![If {
resource: "/specs/rfc2518.doc".into(),
list: vec![Condition::ETag {
is_not: false,
tag: "\"4217\"",
}],
}],
),
(
r#"</specs/rfc2518.doc> (Not ["4217"])"#,
vec![If {
resource: "/specs/rfc2518.doc".into(),
list: vec![Condition::ETag {
is_not: true,
tag: "\"4217\"",
}],
}],
),
(
r#"</test/file.txt> (["1234"]) </specs/rfc2518.doc> (Not ["4217"])"#,
vec![
If {
resource: "/test/file.txt".into(),
list: vec![Condition::ETag {
is_not: false,
tag: "\"1234\"",
}],
},
If {
resource: "/specs/rfc2518.doc".into(),
list: vec![Condition::ETag {
is_not: true,
tag: "\"4217\"",
}],
},
],
),
] {
assert!(headers.parse("If", input));
assert_eq!(headers.if_, expected, "Failed for input: {}", input);
headers.if_.clear();
}
assert!(headers.parse("If-Match", "*"));
assert_eq!(
headers.if_,
vec![If {
resource: None,
list: vec![Condition::Exists { is_not: false }],
}]
);
headers.if_.clear();
assert!(headers.parse("If-None-Match", "etag1, etag2"));
assert_eq!(
headers.if_,
vec![If {
resource: None,
list: vec![
Condition::ETag {
is_not: true,
tag: "etag1",
},
Condition::ETag {
is_not: true,
tag: "etag2",
}
],
},]
);
assert!(headers.parse("Timeout", "Second-10"));
assert_eq!(headers.timeout, Timeout::Second(10));
assert!(headers.parse("Timeout", "Infinite, Second-4100000000"));
assert_eq!(headers.timeout, Timeout::Infinite);
assert!(headers.parse("Overwrite", "F"));
assert!(headers.overwrite_fail);
}
}
+210
View File
@@ -0,0 +1,210 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use std::{
borrow::Cow,
fmt::{Display, Formatter},
};
use quick_xml::events::BytesStart;
use tokenizer::Tokenizer;
use crate::schema::{Element, NamedElement, Namespace};
pub mod header;
pub mod property;
pub mod tokenizer;
#[derive(Debug, Clone)]
pub enum Error {
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>;
#[derive(Debug, Clone)]
pub enum Token<'x> {
ElementStart {
name: NamedElement,
raw: RawElement<'x>,
},
ElementEnd,
Bytes(Cow<'x, [u8]>),
Text(Cow<'x, str>),
UnknownElement(RawElement<'x>),
Eof,
}
#[derive(Debug, Clone)]
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>;
}
pub trait XmlValueParser: Sized {
fn parse_bytes(bytes: &[u8]) -> Option<Self>;
fn parse_str(text: &str) -> Option<Self>;
}
impl NamedElement {
pub fn dav(element: Element) -> NamedElement {
NamedElement {
ns: Namespace::Dav,
element,
}
}
pub fn caldav(element: Element) -> NamedElement {
NamedElement {
ns: Namespace::CalDav,
element,
}
}
pub fn carddav(element: Element) -> NamedElement {
NamedElement {
ns: Namespace::CardDav,
element,
}
}
pub fn calendarserver(element: Element) -> NamedElement {
NamedElement {
ns: Namespace::CalendarServer,
element,
}
}
}
impl Token<'_> {
pub fn into_owned(self) -> Token<'static> {
match self {
Token::ElementStart { name, raw } => Token::ElementStart {
name,
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(raw.into_owned()),
Token::Eof => Token::Eof,
}
}
pub fn into_unexpected(self) -> Error {
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,
}
}
}
#[cfg(test)]
impl PartialEq for Token<'_> {
fn eq(&self, other: &Self) -> bool {
match (self, other) {
(
Self::ElementStart {
name: l_name,
raw: l_raw,
},
Self::ElementStart {
name: r_name,
raw: r_raw,
},
) => {
l_name == r_name
&& l_raw
.element
.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)) => {
let l0: &[u8] = l0.element.as_ref();
let r0: &[u8] = r0.element.as_ref();
l0.eq_ignore_ascii_case(r0)
}
_ => core::mem::discriminant(self) == core::mem::discriminant(other),
}
}
}
impl NamedElement {
pub fn into_unexpected(self) -> Error {
Error::UnexpectedToken(Box::new(UnexpectedToken {
expected: None,
found: Token::ElementStart {
name: self,
raw: RawElement::new(BytesStart::new("")),
},
}))
}
}
impl Default for RawElement<'_> {
fn default() -> Self {
RawElement::new(BytesStart::new(""))
}
}
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(err) => {
write!(f, "Unexpected token: {:?}", err.found)?;
if let Some(expected) = &err.expected {
write!(f, ", expected: {expected:?}")?;
}
Ok(())
}
}
}
}
+741
View File
@@ -0,0 +1,741 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use super::{DavParser, RawElement, Token, XmlValueParser, tokenizer::Tokenizer};
use crate::schema::{
Attribute, AttributeValue, Element, NamedElement, Namespace,
property::{
CalDavProperty, CalDavPropertyName, CalendarData, CardDavProperty, CardDavPropertyName,
Comp, DavProperty, DavValue, PrincipalProperty, ResourceType, WebDavProperty,
},
request::{DavPropertyValue, VCardPropertyWithGroup},
response::List,
};
use calcard::{
Entry, Parser,
common::{IanaParse, PartialDateTime},
icalendar::{ICalendar, ICalendarComponentType, ICalendarParameterName, ICalendarProperty},
vcard::{VCardParameterName, VCardProperty, VCardVersion},
};
use mail_parser::DateTime;
use types::{TimeRange, dead_property::DeadProperty};
impl Tokenizer<'_> {
pub(crate) fn collect_properties(
&mut self,
mut elements: Vec<DavProperty>,
) -> crate::parser::Result<Vec<DavProperty>> {
loop {
match self.token()? {
Token::ElementStart {
name:
NamedElement {
ns: Namespace::CalDav,
element: Element::CalendarData,
},
..
} => {
elements.push(DavProperty::CalDav(CalDavProperty::CalendarData(
self.collect_calendar_data()?,
)));
}
Token::ElementStart {
name:
NamedElement {
ns: Namespace::CardDav,
element: Element::AddressData,
},
raw,
} => {
let mut version = None;
for attribute in raw.attributes::<VCardPropertyWithGroup>() {
if let Attribute::Version(value) = attribute? {
version = VCardVersion::try_parse(value.trim().trim_matches('"'));
}
}
elements.push(DavProperty::CardDav(CardDavProperty::AddressData {
properties: self.collect_address_data()?,
version,
}));
}
Token::ElementStart { name, .. } => {
if let Some(property) = DavProperty::from_element(name) {
elements.push(property);
}
self.expect_element_end()?;
}
Token::ElementEnd => {
break;
}
Token::UnknownElement(name) => {
elements.push(DavProperty::DeadProperty((&name).into()));
self.expect_element_end()?;
}
token => return Err(token.into_unexpected()),
}
}
Ok(elements)
}
pub(crate) fn collect_calendar_data(&mut self) -> crate::parser::Result<CalendarData> {
let mut depth = 1;
let mut data = CalendarData {
properties: Vec::with_capacity(4),
expand: None,
limit_recurrence: None,
limit_freebusy: None,
};
let mut components: Vec<ICalendarComponentType> = Vec::new();
loop {
match self.token()? {
Token::ElementStart {
name:
NamedElement {
ns: Namespace::CalDav,
element: Element::Allcomp,
},
..
} => {
self.expect_element_end()?;
}
Token::ElementStart {
name:
NamedElement {
ns: Namespace::CalDav,
element: Element::Allprop,
},
..
} => {
if let Some(component) = components.last().cloned() {
data.properties.push(CalDavPropertyName {
component: Some(component),
name: None,
no_value: false,
});
}
self.expect_element_end()?;
}
Token::ElementStart {
name:
NamedElement {
ns: Namespace::CalDav,
element: Element::Comp,
},
raw,
} => {
depth += 1;
for attribute in raw.attributes::<ICalendarComponentType>() {
if let Attribute::Name(name) = attribute? {
components.push(name);
}
}
}
Token::ElementStart {
name:
NamedElement {
ns: Namespace::CalDav,
element: Element::Prop,
},
raw,
} => {
let mut name = None;
let mut no_value = false;
for attribute in raw.attributes::<ICalendarProperty>() {
match attribute? {
Attribute::Name(name_) => {
name = Some(name_);
}
Attribute::NoValue(no_value_) => {
no_value = no_value_;
}
_ => {}
}
}
if let Some(name) = name {
data.properties.push(CalDavPropertyName {
component: components.last().cloned(),
name: Some(name),
no_value,
});
}
self.expect_element_end()?;
}
Token::ElementStart {
name:
NamedElement {
ns: Namespace::CalDav,
element: Element::Expand,
},
raw,
} => {
data.expand = TimeRange::from_raw(&raw)?;
self.expect_element_end()?;
}
Token::ElementStart {
name:
NamedElement {
ns: Namespace::CalDav,
element: Element::LimitRecurrenceSet,
},
raw,
} => {
data.limit_recurrence = TimeRange::from_raw(&raw)?;
self.expect_element_end()?;
}
Token::ElementStart {
name:
NamedElement {
ns: Namespace::CalDav,
element: Element::LimitFreebusySet,
},
raw,
} => {
data.limit_freebusy = TimeRange::from_raw(&raw)?;
self.expect_element_end()?;
}
Token::ElementEnd => {
depth -= 1;
if depth == 0 {
break;
}
if let Some(last_component) = components.pop()
&& last_component != ICalendarComponentType::VCalendar
&& !matches!(data.properties.last(), Some(CalDavPropertyName { component: Some(component), .. }) if component == &last_component)
{
data.properties.push(CalDavPropertyName {
component: Some(last_component),
name: None,
no_value: false,
});
}
}
Token::Eof => {
break;
}
token => return Err(token.into_unexpected()),
}
}
Ok(data)
}
pub(crate) fn collect_address_data(
&mut self,
) -> crate::parser::Result<Vec<CardDavPropertyName>> {
let mut items = Vec::with_capacity(4);
loop {
match self.token()? {
Token::ElementStart {
name:
NamedElement {
ns: Namespace::CardDav,
element: Element::Allprop,
},
..
} => {
self.expect_element_end()?;
}
Token::ElementStart {
name:
NamedElement {
ns: Namespace::CardDav,
element: Element::Prop,
},
raw,
} => {
let mut name = None;
let mut group = None;
let mut no_value = false;
for attribute in raw.attributes::<VCardPropertyWithGroup>() {
match attribute? {
Attribute::Name(name_) => {
name = Some(name_.name);
group = name_.group;
}
Attribute::NoValue(no_value_) => {
no_value = no_value_;
}
_ => {}
}
}
if let Some(name) = name {
items.push(CardDavPropertyName {
name,
group,
no_value,
});
}
self.expect_element_end()?;
}
Token::ElementEnd | Token::Eof => {
break;
}
token => return Err(token.into_unexpected()),
}
}
Ok(items)
}
}
impl Tokenizer<'_> {
pub(crate) fn collect_property_values(
&mut self,
elements: &mut Vec<DavPropertyValue>,
) -> crate::parser::Result<()> {
loop {
match self.token()? {
Token::ElementStart { name, .. } => {
if let Some(property) = DavProperty::from_element(name) {
let value = match property {
DavProperty::WebDav(WebDavProperty::ResourceType) => {
DavValue::ResourceTypes(List(self.collect_elements()?))
}
DavProperty::WebDav(WebDavProperty::CreationDate) => {
match self.parse_value::<DateTime>()? {
Some(Ok(value)) => DavValue::Timestamp(value.to_timestamp()),
Some(Err(value)) => DavValue::String(value),
None => DavValue::Null,
}
}
DavProperty::CalDav(CalDavProperty::CalendarTimezone) => {
match self
.collect_string_value()?
.map(|v| ICalendar::parse(&v).map_err(|_| v))
{
Some(Ok(value)) => DavValue::ICalendar(value),
Some(Err(value)) => DavValue::String(value),
None => DavValue::Null,
}
}
DavProperty::CalDav(CalDavProperty::SupportedCalendarComponentSet) => {
let mut components = Vec::new();
loop {
match self.token()? {
Token::ElementStart { name, raw } => {
if name.ns == Namespace::CalDav
&& name.element == Element::Comp
{
for component in
raw.attributes::<ICalendarComponentType>()
{
if let Attribute::Name(name) = component? {
components.push(Comp(name));
}
}
}
self.seek_element_end()?;
}
Token::UnknownElement(_) => {
// Ignore unknown elements
self.seek_element_end()?;
}
Token::ElementEnd | Token::Eof => {
break;
}
_ => {}
}
}
DavValue::Components(List(components))
}
DavProperty::CalDav(
CalDavProperty::MaxInstances
| CalDavProperty::MaxAttendeesPerInstance,
) => match self.parse_value()? {
Some(Ok(value)) => DavValue::Uint64(value),
Some(Err(value)) => DavValue::String(value),
None => DavValue::Null,
},
_ => self
.collect_string_value()?
.map(DavValue::String)
.unwrap_or(DavValue::Null),
};
elements.push(DavPropertyValue { property, value });
} else {
// Ignore unknown elements
self.seek_element_end()?;
}
}
Token::ElementEnd | Token::Eof => {
break;
}
Token::UnknownElement(raw) => {
elements.push(DavPropertyValue {
property: DavProperty::DeadProperty((&raw).into()),
value: DavValue::DeadProperty(DeadProperty::parse(self)?),
});
}
token => return Err(token.into_unexpected()),
}
}
Ok(())
}
}
pub(crate) trait TimeRangeFromRaw {
fn from_raw(raw: &RawElement<'_>) -> super::Result<Option<TimeRange>>;
}
impl TimeRangeFromRaw for TimeRange {
fn from_raw(raw: &RawElement<'_>) -> super::Result<Option<Self>> {
let mut range = TimeRange {
start: i64::MIN,
end: i64::MAX,
};
for attribute in raw.attributes::<ICalendarDateTime>() {
match attribute? {
Attribute::Start(start) => {
range.start = start.0;
}
Attribute::End(end) => {
range.end = end.0;
}
_ => {}
}
}
if range.end < range.start {
range.end = i64::MAX;
}
if range.start != i64::MIN || range.end != i64::MAX {
Ok(Some(range))
} else {
Ok(None)
}
}
}
impl DavProperty {
pub(crate) fn from_element(element: NamedElement) -> Option<Self> {
match (element.ns, element.element) {
(Namespace::Dav, Element::Creationdate) => {
Some(DavProperty::WebDav(WebDavProperty::CreationDate))
}
(Namespace::Dav, Element::Displayname) => {
Some(DavProperty::WebDav(WebDavProperty::DisplayName))
}
(Namespace::Dav, Element::Getcontentlanguage) => {
Some(DavProperty::WebDav(WebDavProperty::GetContentLanguage))
}
(Namespace::Dav, Element::Getcontentlength) => {
Some(DavProperty::WebDav(WebDavProperty::GetContentLength))
}
(Namespace::Dav, Element::Getcontenttype) => {
Some(DavProperty::WebDav(WebDavProperty::GetContentType))
}
(Namespace::Dav, Element::Getetag) => {
Some(DavProperty::WebDav(WebDavProperty::GetETag))
}
(Namespace::Dav, Element::Getlastmodified) => {
Some(DavProperty::WebDav(WebDavProperty::GetLastModified))
}
(Namespace::Dav, Element::Resourcetype) => {
Some(DavProperty::WebDav(WebDavProperty::ResourceType))
}
(Namespace::Dav, Element::Lockdiscovery) => {
Some(DavProperty::WebDav(WebDavProperty::LockDiscovery))
}
(Namespace::Dav, Element::Supportedlock) => {
Some(DavProperty::WebDav(WebDavProperty::SupportedLock))
}
(Namespace::Dav, Element::CurrentUserPrincipal) => {
Some(DavProperty::WebDav(WebDavProperty::CurrentUserPrincipal))
}
(Namespace::Dav, Element::QuotaAvailableBytes) => {
Some(DavProperty::WebDav(WebDavProperty::QuotaAvailableBytes))
}
(Namespace::Dav, Element::QuotaUsedBytes) => {
Some(DavProperty::WebDav(WebDavProperty::QuotaUsedBytes))
}
(Namespace::Dav, Element::SupportedReportSet) => {
Some(DavProperty::WebDav(WebDavProperty::SupportedReportSet))
}
(Namespace::Dav, Element::SyncToken) => {
Some(DavProperty::WebDav(WebDavProperty::SyncToken))
}
(Namespace::Dav, Element::AlternateUriSet) => {
Some(DavProperty::Principal(PrincipalProperty::AlternateURISet))
}
(Namespace::Dav, Element::PrincipalUrl) => {
Some(DavProperty::Principal(PrincipalProperty::PrincipalURL))
}
(Namespace::Dav, Element::GroupMemberSet) => {
Some(DavProperty::Principal(PrincipalProperty::GroupMemberSet))
}
(Namespace::Dav, Element::GroupMembership) => {
Some(DavProperty::Principal(PrincipalProperty::GroupMembership))
}
(Namespace::Dav, Element::Owner) => Some(DavProperty::WebDav(WebDavProperty::Owner)),
(Namespace::Dav, Element::Group) => Some(DavProperty::WebDav(WebDavProperty::Group)),
(Namespace::Dav, Element::SupportedPrivilegeSet) => {
Some(DavProperty::WebDav(WebDavProperty::SupportedPrivilegeSet))
}
(Namespace::Dav, Element::CurrentUserPrivilegeSet) => {
Some(DavProperty::WebDav(WebDavProperty::CurrentUserPrivilegeSet))
}
(Namespace::Dav, Element::Acl) => Some(DavProperty::WebDav(WebDavProperty::Acl)),
(Namespace::Dav, Element::AclRestrictions) => {
Some(DavProperty::WebDav(WebDavProperty::AclRestrictions))
}
(Namespace::Dav, Element::InheritedAclSet) => {
Some(DavProperty::WebDav(WebDavProperty::InheritedAclSet))
}
(Namespace::Dav, Element::PrincipalCollectionSet) => {
Some(DavProperty::WebDav(WebDavProperty::PrincipalCollectionSet))
}
(Namespace::CardDav, Element::AddressbookDescription) => Some(DavProperty::CardDav(
CardDavProperty::AddressbookDescription,
)),
(Namespace::CardDav, Element::SupportedAddressData) => {
Some(DavProperty::CardDav(CardDavProperty::SupportedAddressData))
}
(Namespace::CardDav, Element::SupportedCollationSet) => {
Some(DavProperty::CardDav(CardDavProperty::SupportedCollationSet))
}
(Namespace::CardDav, Element::AddressbookHomeSet) => Some(DavProperty::Principal(
PrincipalProperty::AddressbookHomeSet,
)),
(Namespace::CardDav, Element::PrincipalAddress) => {
Some(DavProperty::Principal(PrincipalProperty::PrincipalAddress))
}
(Namespace::CardDav, Element::AddressData) => {
Some(DavProperty::CardDav(CardDavProperty::AddressData {
properties: Default::default(),
version: None,
}))
}
(Namespace::CardDav, Element::MaxResourceSize) => {
Some(DavProperty::CardDav(CardDavProperty::MaxResourceSize))
}
(Namespace::CalDav, Element::CalendarDescription) => {
Some(DavProperty::CalDav(CalDavProperty::CalendarDescription))
}
(Namespace::CalDav, Element::CalendarTimezone) => {
Some(DavProperty::CalDav(CalDavProperty::CalendarTimezone))
}
(Namespace::CalDav, Element::SupportedCalendarComponentSet) => Some(
DavProperty::CalDav(CalDavProperty::SupportedCalendarComponentSet),
),
(Namespace::CalDav, Element::SupportedCollationSet) => {
Some(DavProperty::CalDav(CalDavProperty::SupportedCollationSet))
}
(Namespace::CalDav, Element::SupportedCalendarData) => {
Some(DavProperty::CalDav(CalDavProperty::SupportedCalendarData))
}
(Namespace::CalDav, Element::MaxResourceSize) => {
Some(DavProperty::CalDav(CalDavProperty::MaxResourceSize))
}
(Namespace::CalDav, Element::MinDateTime) => {
Some(DavProperty::CalDav(CalDavProperty::MinDateTime))
}
(Namespace::CalDav, Element::MaxDateTime) => {
Some(DavProperty::CalDav(CalDavProperty::MaxDateTime))
}
(Namespace::CalDav, Element::MaxInstances) => {
Some(DavProperty::CalDav(CalDavProperty::MaxInstances))
}
(Namespace::CalDav, Element::MaxAttendeesPerInstance) => {
Some(DavProperty::CalDav(CalDavProperty::MaxAttendeesPerInstance))
}
(Namespace::CalDav, Element::ScheduleDefaultCalendarUrl) => Some(DavProperty::CalDav(
CalDavProperty::ScheduleDefaultCalendarURL,
)),
(Namespace::CalDav, Element::ScheduleTag) => {
Some(DavProperty::CalDav(CalDavProperty::ScheduleTag))
}
(Namespace::CalDav, Element::ScheduleCalendarTransp) => {
Some(DavProperty::CalDav(CalDavProperty::ScheduleCalendarTransp))
}
(Namespace::CalDav, Element::CalendarHomeSet) => {
Some(DavProperty::Principal(PrincipalProperty::CalendarHomeSet))
}
(Namespace::CalDav, Element::CalendarUserAddressSet) => Some(DavProperty::Principal(
PrincipalProperty::CalendarUserAddressSet,
)),
(Namespace::CalDav, Element::CalendarUserType) => {
Some(DavProperty::Principal(PrincipalProperty::CalendarUserType))
}
(Namespace::CalDav, Element::ScheduleInboxUrl) => {
Some(DavProperty::Principal(PrincipalProperty::ScheduleInboxURL))
}
(Namespace::CalDav, Element::ScheduleOutboxUrl) => {
Some(DavProperty::Principal(PrincipalProperty::ScheduleOutboxURL))
}
(Namespace::CalDav, Element::CalendarData) => Some(DavProperty::CalDav(
CalDavProperty::CalendarData(Default::default()),
)),
(Namespace::CalDav, Element::TimezoneServiceSet) => {
Some(DavProperty::CalDav(CalDavProperty::TimezoneServiceSet))
}
(Namespace::CalDav, Element::CalendarTimezoneId) => {
Some(DavProperty::CalDav(CalDavProperty::TimezoneId))
}
(Namespace::CalendarServer, Element::Getctag) => {
Some(DavProperty::WebDav(WebDavProperty::GetCTag))
}
_ => None,
}
}
}
impl TryFrom<NamedElement> for ResourceType {
type Error = ();
fn try_from(value: NamedElement) -> Result<Self, Self::Error> {
match (value.ns, value.element) {
(Namespace::Dav, Element::Collection) => Ok(ResourceType::Collection),
(Namespace::Dav, Element::Principal) => Ok(ResourceType::Principal),
(Namespace::CardDav, Element::Addressbook) => Ok(ResourceType::AddressBook),
(Namespace::CalDav, Element::Calendar) => Ok(ResourceType::Calendar),
(Namespace::CalDav, Element::ScheduleInbox) => Ok(ResourceType::ScheduleInbox),
(Namespace::CalDav, Element::ScheduleOutbox) => Ok(ResourceType::ScheduleOutbox),
_ => Err(()),
}
}
}
struct ICalendarDateTime(i64);
impl AttributeValue for ICalendarDateTime {
fn from_str(s: &str) -> Option<Self>
where
Self: Sized,
{
let mut dt = PartialDateTime::default();
dt.parse_timestamp(&mut s.as_bytes().iter().peekable(), true);
dt.to_timestamp().map(ICalendarDateTime)
}
}
impl AttributeValue for ICalendarComponentType {
fn from_str(s: &str) -> Option<Self>
where
Self: Sized,
{
ICalendarComponentType::parse(s.as_bytes())
}
}
impl AttributeValue for ICalendarProperty {
fn from_str(s: &str) -> Option<Self>
where
Self: Sized,
{
ICalendarProperty::parse(s.as_bytes())
.unwrap_or_else(|| ICalendarProperty::Other(s.to_string()))
.into()
}
}
impl AttributeValue for ICalendarParameterName {
fn from_str(s: &str) -> Option<Self>
where
Self: Sized,
{
ICalendarParameterName::parse(s).into()
}
}
impl AttributeValue for VCardPropertyWithGroup {
fn from_str(s: &str) -> Option<Self>
where
Self: Sized,
{
if let Some((group, s)) = s.split_once('.') {
VCardPropertyWithGroup {
name: VCardProperty::parse(s.as_bytes())
.unwrap_or_else(|| VCardProperty::Other(s.to_string())),
group: group.to_string().into(),
}
.into()
} else {
VCardPropertyWithGroup {
name: VCardProperty::parse(s.as_bytes())
.unwrap_or_else(|| VCardProperty::Other(s.to_string())),
group: None,
}
.into()
}
}
}
impl AttributeValue for VCardParameterName {
fn from_str(s: &str) -> Option<Self>
where
Self: Sized,
{
VCardParameterName::parse(s).into()
}
}
impl XmlValueParser for ICalendar {
fn parse_bytes(bytes: &[u8]) -> Option<Self> {
let text = String::from_utf8_lossy(bytes);
let mut parser = Parser::new(&text);
if let Entry::ICalendar(ical) = parser.entry() {
Some(ical)
} else {
None
}
}
fn parse_str(text: &str) -> Option<Self> {
let mut parser = Parser::new(text);
if let Entry::ICalendar(ical) = parser.entry() {
Some(ical)
} else {
None
}
}
}
impl XmlValueParser for u64 {
fn parse_bytes(bytes: &[u8]) -> Option<Self> {
std::str::from_utf8(bytes).ok().and_then(|s| s.parse().ok())
}
fn parse_str(text: &str) -> Option<Self> {
text.parse().ok()
}
}
impl XmlValueParser for u32 {
fn parse_bytes(bytes: &[u8]) -> Option<Self> {
std::str::from_utf8(bytes).ok().and_then(|s| s.parse().ok())
}
fn parse_str(text: &str) -> Option<Self> {
text.parse().ok()
}
}
impl XmlValueParser for DateTime {
fn parse_bytes(bytes: &[u8]) -> Option<Self> {
std::str::from_utf8(bytes)
.ok()
.and_then(DateTime::parse_rfc3339)
}
fn parse_str(text: &str) -> Option<Self> {
DateTime::parse_rfc3339(text)
}
}
+503
View File
@@ -0,0 +1,503 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* 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);
}
}
}