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:
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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(())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user