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,235 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use calcard::vcard::VCardVersion;
|
||||
use compact_str::{CompactString, ToCompactString};
|
||||
use trc::Value;
|
||||
|
||||
pub mod parser;
|
||||
pub mod requests;
|
||||
pub mod responses;
|
||||
pub mod schema;
|
||||
|
||||
pub fn xml_pretty_print(xml_string: &str) -> String {
|
||||
// Create a reader
|
||||
let mut reader = quick_xml::Reader::from_str(xml_string);
|
||||
let mut writer = quick_xml::Writer::new_with_indent(std::io::Cursor::new(Vec::new()), b' ', 2);
|
||||
let mut buf = Vec::new();
|
||||
loop {
|
||||
match reader.read_event_into(&mut buf) {
|
||||
Ok(quick_xml::events::Event::Eof) => break,
|
||||
Ok(event) => {
|
||||
writer.write_event(event).unwrap();
|
||||
}
|
||||
Err(e) => panic!("Error at position {}: {:?}", reader.buffer_position(), e),
|
||||
}
|
||||
buf.clear();
|
||||
}
|
||||
|
||||
let result = writer.into_inner().into_inner();
|
||||
String::from_utf8(result).unwrap()
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, PartialEq, Eq)]
|
||||
pub struct RequestHeaders<'x> {
|
||||
pub uri: &'x str,
|
||||
pub depth: Depth,
|
||||
pub timeout: Timeout,
|
||||
pub content_type: Option<&'x str>,
|
||||
pub destination: Option<&'x str>,
|
||||
pub lock_token: Option<&'x str>,
|
||||
pub vcard_version: Option<VCardVersion>,
|
||||
pub no_schedule_reply: bool,
|
||||
pub if_schedule_tag: Option<u32>,
|
||||
pub overwrite_fail: bool,
|
||||
pub no_timezones: bool,
|
||||
pub ret: Return,
|
||||
pub depth_no_root: bool,
|
||||
pub if_: Vec<If<'x>>,
|
||||
pub range: Option<ByteRange>,
|
||||
pub if_range: Option<&'x str>,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
|
||||
pub enum ByteRange {
|
||||
Offset { start: u64, end: Option<u64> },
|
||||
Suffix(u64),
|
||||
}
|
||||
|
||||
pub struct ResourceState<T: AsRef<str>> {
|
||||
pub resource: Option<T>,
|
||||
pub etag: T,
|
||||
pub state_token: T,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, PartialEq, Eq, Clone, Copy)]
|
||||
pub enum Return {
|
||||
Minimal,
|
||||
Representation,
|
||||
#[default]
|
||||
Default,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Clone)]
|
||||
pub struct If<'x> {
|
||||
pub resource: Option<&'x str>,
|
||||
pub list: Vec<Condition<'x>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Clone)]
|
||||
pub enum Condition<'x> {
|
||||
StateToken { is_not: bool, token: &'x str },
|
||||
ETag { is_not: bool, tag: &'x str },
|
||||
Exists { is_not: bool },
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, PartialEq, Eq, Clone, Copy)]
|
||||
#[cfg_attr(test, derive(serde::Serialize, serde::Deserialize))]
|
||||
#[cfg_attr(test, serde(tag = "type", content = "data"))]
|
||||
pub enum Timeout {
|
||||
Infinite,
|
||||
Second(u64),
|
||||
#[default]
|
||||
None,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, PartialEq, Eq, Clone, Copy)]
|
||||
#[cfg_attr(test, derive(serde::Serialize, serde::Deserialize))]
|
||||
pub enum Depth {
|
||||
Zero,
|
||||
One,
|
||||
Infinity,
|
||||
#[default]
|
||||
None,
|
||||
}
|
||||
|
||||
impl From<&RequestHeaders<'_>> for Value {
|
||||
fn from(headers: &RequestHeaders<'_>) -> Self {
|
||||
let mut values = Vec::with_capacity(4);
|
||||
if headers.depth != Depth::None {
|
||||
values.push(Value::String(CompactString::const_new("Depth")));
|
||||
values.push(match headers.depth {
|
||||
Depth::Zero => Value::Int(0),
|
||||
Depth::One => Value::Int(1),
|
||||
Depth::Infinity => Value::String(CompactString::const_new("infinity")),
|
||||
Depth::None => Value::None,
|
||||
});
|
||||
}
|
||||
if headers.timeout != Timeout::None {
|
||||
values.push(Value::String(CompactString::const_new("Timeout")));
|
||||
values.push(match headers.timeout {
|
||||
Timeout::Infinite => Value::String(CompactString::const_new("infinite")),
|
||||
Timeout::Second(n) => Value::Int(n as i64),
|
||||
Timeout::None => Value::None,
|
||||
});
|
||||
}
|
||||
for (name, header_value) in [
|
||||
("Content-Type", headers.content_type),
|
||||
("Destination", headers.destination),
|
||||
("Lock-Token", headers.lock_token),
|
||||
("If-Range", headers.if_range),
|
||||
] {
|
||||
if let Some(value) = header_value {
|
||||
values.push(CompactString::const_new(name).into());
|
||||
values.push(value.to_compact_string().into());
|
||||
}
|
||||
}
|
||||
if let Some(range) = headers.range {
|
||||
values.push(CompactString::const_new("Range").into());
|
||||
values.push(
|
||||
match range {
|
||||
ByteRange::Offset {
|
||||
start,
|
||||
end: Some(end),
|
||||
} => format!("{start}-{end}"),
|
||||
ByteRange::Offset { start, end: None } => format!("{start}-"),
|
||||
ByteRange::Suffix(length) => format!("-{length}"),
|
||||
}
|
||||
.to_compact_string()
|
||||
.into(),
|
||||
);
|
||||
}
|
||||
for (name, is_set) in [
|
||||
("Overwrite", headers.overwrite_fail),
|
||||
("No-Timezones", headers.no_timezones),
|
||||
("Depth-No-Root", headers.depth_no_root),
|
||||
] {
|
||||
if is_set {
|
||||
values.push(CompactString::const_new(name).into());
|
||||
}
|
||||
}
|
||||
for if_ in &headers.if_ {
|
||||
values.push(CompactString::const_new("If").into());
|
||||
let mut if_values = Vec::with_capacity(if_.list.len() * 2 + 1);
|
||||
if let Some(resource) = if_.resource {
|
||||
if_values.push(Value::String(resource.to_compact_string()));
|
||||
}
|
||||
for condition in &if_.list {
|
||||
match condition {
|
||||
Condition::StateToken { is_not, token } => {
|
||||
if *is_not {
|
||||
if_values.push(Value::String(CompactString::const_new("!State-Token")));
|
||||
} else {
|
||||
if_values.push(Value::String(CompactString::const_new("State-Token")));
|
||||
}
|
||||
if_values.push(Value::String(token.to_compact_string()));
|
||||
}
|
||||
Condition::ETag { is_not, tag } => {
|
||||
if *is_not {
|
||||
if_values.push(Value::String(CompactString::const_new("!ETag")));
|
||||
} else {
|
||||
if_values.push(Value::String(CompactString::const_new("ETag")));
|
||||
}
|
||||
if_values.push(Value::String(tag.to_compact_string()));
|
||||
}
|
||||
Condition::Exists { is_not } => {
|
||||
if *is_not {
|
||||
if_values.push(Value::String(CompactString::const_new("!Exists")));
|
||||
} else {
|
||||
if_values.push(Value::String(CompactString::const_new("Exists")));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
values.push(Value::Array(if_values));
|
||||
}
|
||||
|
||||
Value::Array(values)
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
|
||||
|
||||
Implemented:
|
||||
|
||||
RFC4918 - HTTP Extensions for Web Distributed Authoring and Versioning (WebDAV)
|
||||
RFC5689 - Extended MKCOL for Web Distributed Authoring and Versioning (WebDAV)
|
||||
RFC6578 - Collection Synchronization for Web Distributed Authoring and Versioning (WebDAV)
|
||||
RFC3744 - Web Distributed Authoring and Versioning (WebDAV) Access Control Protocol
|
||||
RFC4331 - Quota and Size Properties for Distributed Authoring and Versioning (DAV) Collections
|
||||
RFC5397 - WebDAV Current Principal Extension
|
||||
RFC8144 - Use of the Prefer Header Field in Web Distributed Authoring and Versioning (WebDAV)
|
||||
RFC4791 - Calendaring Extensions to WebDAV (CalDAV)
|
||||
RFC7809 - Calendaring Extensions to WebDAV (CalDAV) Time Zones by Reference
|
||||
RFC6638 - Scheduling Extensions to CalDAV
|
||||
RFC6352 - CardDAV vCard Extensions to Web Distributed Authoring and Versioning (WebDAV)
|
||||
RFC6764 - Locating Services for Calendaring Extensions to WebDAV (CalDAV) and vCard Extensions to WebDAV (CardDAV)
|
||||
|
||||
Out of scope:
|
||||
|
||||
RFC5842 - Binding Extensions to Web Distributed Authoring and Versioning (WebDAV)
|
||||
RFC4316 - Datatypes for Web Distributed Authoring and Versioning (WebDAV) Properties
|
||||
RFC4709 - Mounting Web Distributed Authoring and Versioning (WebDAV) Servers
|
||||
RFC3648 - Web Distributed Authoring and Versioning (WebDAV) Ordered Collections Protocol
|
||||
RFC4437 - Web Distributed Authoring and Versioning (WebDAV) Redirect Reference Resources
|
||||
RFC8607 - Calendaring Extensions to WebDAV (CalDAV) Managed Attachments
|
||||
RFC5995 - Using POST to Add Members to Web Distributed Authoring and Versioning (WebDAV) Collections
|
||||
RFC3253 - Versioning Extensions to WebDAV (Web Distributed Authoring and Versioning)
|
||||
RFC5323 - Web Distributed Authoring and Versioning (WebDAV) SEARCH
|
||||
|
||||
|
||||
*/
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,469 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use crate::{
|
||||
parser::{DavParser, Token, tokenizer::Tokenizer},
|
||||
schema::{
|
||||
Element, NamedElement, Namespace,
|
||||
property::{DavValue, Privilege},
|
||||
request::{
|
||||
Acl, AclPrincipalPropSet, DavPropertyValue, PrincipalMatch, PrincipalMatchProperties,
|
||||
PrincipalPropertySearch, PropertySearch,
|
||||
},
|
||||
response::{Ace, GrantDeny, Href, List, Principal},
|
||||
},
|
||||
};
|
||||
|
||||
impl DavParser for Acl {
|
||||
fn parse(stream: &mut Tokenizer<'_>) -> crate::parser::Result<Self> {
|
||||
stream.expect_named_element(NamedElement::dav(Element::Acl))?;
|
||||
|
||||
let mut acl = Acl { aces: vec![] };
|
||||
|
||||
loop {
|
||||
match stream.token()? {
|
||||
Token::ElementStart {
|
||||
name:
|
||||
NamedElement {
|
||||
ns: Namespace::Dav,
|
||||
element: Element::Ace,
|
||||
},
|
||||
..
|
||||
} => {
|
||||
acl.aces.push(Ace::parse(stream)?);
|
||||
}
|
||||
Token::ElementEnd => {
|
||||
break;
|
||||
}
|
||||
Token::UnknownElement(_) => {
|
||||
stream.seek_element_end()?;
|
||||
}
|
||||
other => {
|
||||
return Err(other.into_unexpected());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(acl)
|
||||
}
|
||||
}
|
||||
|
||||
impl DavParser for Ace {
|
||||
fn parse(stream: &mut Tokenizer<'_>) -> crate::parser::Result<Self> {
|
||||
let mut ace = Ace {
|
||||
principal: Principal::All,
|
||||
invert: false,
|
||||
grant_deny: GrantDeny::Grant(List(vec![])),
|
||||
protected: false,
|
||||
inherited: None,
|
||||
};
|
||||
let mut depth = 1;
|
||||
|
||||
loop {
|
||||
match stream.token()? {
|
||||
Token::ElementStart {
|
||||
name:
|
||||
NamedElement {
|
||||
ns: Namespace::Dav,
|
||||
element: Element::Principal,
|
||||
},
|
||||
..
|
||||
} => {
|
||||
ace.principal = Principal::parse(stream)?;
|
||||
}
|
||||
Token::ElementStart {
|
||||
name:
|
||||
NamedElement {
|
||||
ns: Namespace::Dav,
|
||||
element: Element::Invert,
|
||||
},
|
||||
..
|
||||
} if depth == 1 => {
|
||||
ace.invert = true;
|
||||
depth += 1;
|
||||
}
|
||||
Token::ElementStart {
|
||||
name:
|
||||
NamedElement {
|
||||
ns: Namespace::Dav,
|
||||
element: Element::Protected,
|
||||
},
|
||||
..
|
||||
} if depth == 1 => {
|
||||
ace.protected = true;
|
||||
stream.expect_element_end()?;
|
||||
}
|
||||
Token::ElementStart {
|
||||
name:
|
||||
NamedElement {
|
||||
ns: Namespace::Dav,
|
||||
element: Element::Inherited,
|
||||
},
|
||||
..
|
||||
} if depth == 1 => {
|
||||
stream.expect_named_element(NamedElement::dav(Element::Href))?;
|
||||
ace.inherited = stream.collect_string_value()?.map(Href);
|
||||
stream.expect_element_end()?;
|
||||
}
|
||||
Token::ElementStart {
|
||||
name:
|
||||
NamedElement {
|
||||
ns: Namespace::Dav,
|
||||
element: Element::Grant,
|
||||
},
|
||||
..
|
||||
} if depth == 1 => {
|
||||
ace.grant_deny = GrantDeny::Grant(List(stream.collect_privileges()?));
|
||||
}
|
||||
Token::ElementStart {
|
||||
name:
|
||||
NamedElement {
|
||||
ns: Namespace::Dav,
|
||||
element: Element::Deny,
|
||||
},
|
||||
..
|
||||
} if depth == 1 => {
|
||||
ace.grant_deny = GrantDeny::Deny(List(stream.collect_privileges()?));
|
||||
}
|
||||
Token::ElementEnd => {
|
||||
depth -= 1;
|
||||
if depth == 0 {
|
||||
break;
|
||||
}
|
||||
}
|
||||
Token::UnknownElement(_) => {
|
||||
stream.seek_element_end()?;
|
||||
}
|
||||
other => {
|
||||
return Err(other.into_unexpected());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(ace)
|
||||
}
|
||||
}
|
||||
|
||||
impl DavParser for Principal {
|
||||
fn parse(stream: &mut Tokenizer<'_>) -> crate::parser::Result<Self> {
|
||||
let result = match stream.unwrap_named_element()? {
|
||||
NamedElement {
|
||||
ns: Namespace::Dav,
|
||||
element: Element::Href,
|
||||
} => Principal::Href(Href(stream.collect_string_value()?.unwrap_or_default())),
|
||||
NamedElement {
|
||||
ns: Namespace::Dav,
|
||||
element: Element::All,
|
||||
} => {
|
||||
stream.expect_element_end()?;
|
||||
Principal::All
|
||||
}
|
||||
NamedElement {
|
||||
ns: Namespace::Dav,
|
||||
element: Element::Authenticated,
|
||||
} => {
|
||||
stream.expect_element_end()?;
|
||||
Principal::Authenticated
|
||||
}
|
||||
NamedElement {
|
||||
ns: Namespace::Dav,
|
||||
element: Element::Unauthenticated,
|
||||
} => {
|
||||
stream.expect_element_end()?;
|
||||
Principal::Unauthenticated
|
||||
}
|
||||
NamedElement {
|
||||
ns: Namespace::Dav,
|
||||
element: Element::Property,
|
||||
} => {
|
||||
let property = stream.collect_properties(Vec::new())?;
|
||||
Principal::Property(List(
|
||||
property
|
||||
.into_iter()
|
||||
.map(|prop| DavPropertyValue::new(prop, DavValue::Null))
|
||||
.collect(),
|
||||
))
|
||||
}
|
||||
NamedElement {
|
||||
ns: Namespace::Dav,
|
||||
element: Element::Self_,
|
||||
} => {
|
||||
stream.expect_element_end()?;
|
||||
Principal::Self_
|
||||
}
|
||||
other => return Err(other.into_unexpected()),
|
||||
};
|
||||
stream.expect_element_end()?;
|
||||
Ok(result)
|
||||
}
|
||||
}
|
||||
|
||||
impl Tokenizer<'_> {
|
||||
pub fn collect_privileges(&mut self) -> crate::parser::Result<Vec<Privilege>> {
|
||||
let mut privileges = Vec::new();
|
||||
let mut depth = 1;
|
||||
|
||||
loop {
|
||||
match self.token()? {
|
||||
Token::ElementStart { name, .. } => {
|
||||
if let Some(privilege) = Privilege::from_element(name) {
|
||||
privileges.push(privilege);
|
||||
self.expect_element_end()?;
|
||||
} else {
|
||||
depth += 1;
|
||||
}
|
||||
}
|
||||
Token::ElementEnd => {
|
||||
depth -= 1;
|
||||
if depth == 0 {
|
||||
break;
|
||||
}
|
||||
}
|
||||
Token::UnknownElement(_) => {
|
||||
self.seek_element_end()?;
|
||||
}
|
||||
other => {
|
||||
return Err(other.into_unexpected());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(privileges)
|
||||
}
|
||||
}
|
||||
|
||||
impl Privilege {
|
||||
pub fn from_element(element: NamedElement) -> Option<Self> {
|
||||
match (element.ns, element.element) {
|
||||
(Namespace::Dav, Element::Read) => Some(Privilege::Read),
|
||||
(Namespace::Dav, Element::Write) => Some(Privilege::Write),
|
||||
(Namespace::Dav, Element::WriteProperties) => Some(Privilege::WriteProperties),
|
||||
(Namespace::Dav, Element::WriteContent) => Some(Privilege::WriteContent),
|
||||
(Namespace::Dav, Element::Unlock) => Some(Privilege::Unlock),
|
||||
(Namespace::Dav, Element::ReadAcl) => Some(Privilege::ReadAcl),
|
||||
(Namespace::Dav, Element::ReadCurrentUserPrivilegeSet) => {
|
||||
Some(Privilege::ReadCurrentUserPrivilegeSet)
|
||||
}
|
||||
(Namespace::Dav, Element::WriteAcl) => Some(Privilege::WriteAcl),
|
||||
(Namespace::Dav, Element::Bind) => Some(Privilege::Bind),
|
||||
(Namespace::Dav, Element::Unbind) => Some(Privilege::Unbind),
|
||||
(Namespace::Dav, Element::All) => Some(Privilege::All),
|
||||
(Namespace::CalDav, Element::ReadFreeBusy) => Some(Privilege::ReadFreeBusy),
|
||||
(Namespace::CalDav, Element::ScheduleDeliver) => Some(Privilege::ScheduleDeliver),
|
||||
(Namespace::CalDav, Element::ScheduleDeliverInvite) => {
|
||||
Some(Privilege::ScheduleDeliverInvite)
|
||||
}
|
||||
(Namespace::CalDav, Element::ScheduleDeliverReply) => {
|
||||
Some(Privilege::ScheduleDeliverReply)
|
||||
}
|
||||
(Namespace::CalDav, Element::ScheduleQueryFreebusy) => {
|
||||
Some(Privilege::ScheduleQueryFreeBusy)
|
||||
}
|
||||
(Namespace::CalDav, Element::ScheduleSend) => Some(Privilege::ScheduleSend),
|
||||
(Namespace::CalDav, Element::ScheduleSendInvite) => Some(Privilege::ScheduleSendInvite),
|
||||
(Namespace::CalDav, Element::ScheduleSendReply) => Some(Privilege::ScheduleSendReply),
|
||||
(Namespace::CalDav, Element::ScheduleSendFreebusy) => {
|
||||
Some(Privilege::ScheduleSendFreeBusy)
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl DavParser for AclPrincipalPropSet {
|
||||
fn parse(stream: &mut Tokenizer<'_>) -> crate::parser::Result<Self> {
|
||||
let mut acps = AclPrincipalPropSet { properties: vec![] };
|
||||
|
||||
loop {
|
||||
match stream.token()? {
|
||||
Token::ElementStart {
|
||||
name:
|
||||
NamedElement {
|
||||
ns: Namespace::Dav,
|
||||
element: Element::Prop,
|
||||
},
|
||||
..
|
||||
} => {
|
||||
acps.properties = stream.collect_properties(acps.properties)?;
|
||||
}
|
||||
Token::ElementEnd => {
|
||||
break;
|
||||
}
|
||||
Token::UnknownElement(_) => {
|
||||
stream.seek_element_end()?;
|
||||
}
|
||||
other => {
|
||||
return Err(other.into_unexpected());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(acps)
|
||||
}
|
||||
}
|
||||
|
||||
impl DavParser for PrincipalMatch {
|
||||
fn parse(stream: &mut Tokenizer<'_>) -> crate::parser::Result<Self> {
|
||||
let mut pm = PrincipalMatch {
|
||||
principal_properties: PrincipalMatchProperties::Self_,
|
||||
properties: vec![],
|
||||
};
|
||||
|
||||
loop {
|
||||
match stream.token()? {
|
||||
Token::ElementStart {
|
||||
name:
|
||||
NamedElement {
|
||||
ns: Namespace::Dav,
|
||||
element: Element::PrincipalProperty,
|
||||
},
|
||||
..
|
||||
} => {
|
||||
pm.principal_properties = PrincipalMatchProperties::Properties(
|
||||
stream.collect_properties(Vec::new())?,
|
||||
);
|
||||
}
|
||||
Token::ElementStart {
|
||||
name:
|
||||
NamedElement {
|
||||
ns: Namespace::Dav,
|
||||
element: Element::Self_,
|
||||
},
|
||||
..
|
||||
} => {
|
||||
pm.principal_properties = PrincipalMatchProperties::Self_;
|
||||
stream.expect_element_end()?;
|
||||
}
|
||||
Token::ElementStart {
|
||||
name:
|
||||
NamedElement {
|
||||
ns: Namespace::Dav,
|
||||
element: Element::Prop,
|
||||
},
|
||||
..
|
||||
} => {
|
||||
pm.properties = stream.collect_properties(pm.properties)?;
|
||||
}
|
||||
Token::ElementEnd => {
|
||||
break;
|
||||
}
|
||||
Token::UnknownElement(_) => {
|
||||
stream.seek_element_end()?;
|
||||
}
|
||||
other => {
|
||||
return Err(other.into_unexpected());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(pm)
|
||||
}
|
||||
}
|
||||
|
||||
impl DavParser for PrincipalPropertySearch {
|
||||
fn parse(stream: &mut Tokenizer<'_>) -> crate::parser::Result<Self> {
|
||||
let mut pps = PrincipalPropertySearch {
|
||||
property_search: vec![],
|
||||
properties: vec![],
|
||||
apply_to_principal_collection_set: false,
|
||||
};
|
||||
|
||||
loop {
|
||||
match stream.token()? {
|
||||
Token::ElementStart {
|
||||
name:
|
||||
NamedElement {
|
||||
ns: Namespace::Dav,
|
||||
element: Element::PropertySearch,
|
||||
},
|
||||
..
|
||||
} => {
|
||||
if let Some(prop) = PropertySearch::parse(stream)? {
|
||||
pps.property_search.push(prop);
|
||||
}
|
||||
}
|
||||
Token::ElementStart {
|
||||
name:
|
||||
NamedElement {
|
||||
ns: Namespace::Dav,
|
||||
element: Element::Prop,
|
||||
},
|
||||
..
|
||||
} => {
|
||||
pps.properties = stream.collect_properties(pps.properties)?;
|
||||
}
|
||||
Token::ElementStart {
|
||||
name:
|
||||
NamedElement {
|
||||
ns: Namespace::Dav,
|
||||
element: Element::ApplyToPrincipalCollectionSet,
|
||||
},
|
||||
..
|
||||
} => {
|
||||
stream.expect_element_end()?;
|
||||
pps.apply_to_principal_collection_set = true;
|
||||
}
|
||||
Token::ElementEnd => {
|
||||
break;
|
||||
}
|
||||
Token::UnknownElement(_) => {
|
||||
stream.seek_element_end()?;
|
||||
}
|
||||
other => {
|
||||
return Err(other.into_unexpected());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(pps)
|
||||
}
|
||||
}
|
||||
|
||||
impl PropertySearch {
|
||||
fn parse(stream: &mut Tokenizer<'_>) -> crate::parser::Result<Option<Self>> {
|
||||
let mut property = None;
|
||||
let mut match_ = None;
|
||||
|
||||
loop {
|
||||
match stream.token()? {
|
||||
Token::ElementStart {
|
||||
name:
|
||||
NamedElement {
|
||||
ns: Namespace::Dav,
|
||||
element: Element::Prop,
|
||||
},
|
||||
..
|
||||
} => {
|
||||
property = stream.collect_properties(Vec::new())?.into_iter().next();
|
||||
}
|
||||
Token::ElementStart {
|
||||
name:
|
||||
NamedElement {
|
||||
ns: Namespace::Dav,
|
||||
element: Element::Match,
|
||||
},
|
||||
..
|
||||
} => {
|
||||
match_ = stream.collect_string_value()?;
|
||||
}
|
||||
Token::ElementEnd => {
|
||||
break;
|
||||
}
|
||||
Token::UnknownElement(_) => {
|
||||
stream.seek_element_end()?;
|
||||
}
|
||||
other => {
|
||||
return Err(other.into_unexpected());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(property.map(|property| PropertySearch {
|
||||
property,
|
||||
match_: match_.unwrap_or_default(),
|
||||
}))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use crate::{
|
||||
parser::{DavParser, Token, tokenizer::Tokenizer},
|
||||
schema::{
|
||||
Element, NamedElement, Namespace,
|
||||
property::{LockScope, LockType},
|
||||
request::LockInfo,
|
||||
},
|
||||
};
|
||||
use types::dead_property::DeadProperty;
|
||||
|
||||
impl DavParser for LockInfo {
|
||||
fn parse(stream: &mut Tokenizer<'_>) -> crate::parser::Result<Self> {
|
||||
let mut lockinfo = LockInfo {
|
||||
lock_scope: LockScope::Exclusive,
|
||||
lock_type: LockType::Write,
|
||||
owner: None,
|
||||
};
|
||||
|
||||
if stream.expect_named_element_or_eof(NamedElement::dav(Element::Lockinfo))? {
|
||||
loop {
|
||||
match stream.token()? {
|
||||
Token::ElementStart {
|
||||
name:
|
||||
NamedElement {
|
||||
ns: Namespace::Dav,
|
||||
element: Element::Lockscope,
|
||||
},
|
||||
..
|
||||
} => {
|
||||
lockinfo.lock_scope = LockScope::parse(stream)?;
|
||||
}
|
||||
Token::ElementStart {
|
||||
name:
|
||||
NamedElement {
|
||||
ns: Namespace::Dav,
|
||||
element: Element::Locktype,
|
||||
},
|
||||
..
|
||||
} => {
|
||||
lockinfo.lock_type = LockType::parse(stream)?;
|
||||
}
|
||||
Token::ElementStart {
|
||||
name:
|
||||
NamedElement {
|
||||
ns: Namespace::Dav,
|
||||
element: Element::Owner,
|
||||
},
|
||||
..
|
||||
} => {
|
||||
lockinfo.owner = Some(DeadProperty::parse(stream)?);
|
||||
}
|
||||
Token::ElementEnd | Token::Eof => {
|
||||
break;
|
||||
}
|
||||
other => {
|
||||
return Err(other.into_unexpected());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(lockinfo)
|
||||
}
|
||||
}
|
||||
|
||||
impl DavParser for LockScope {
|
||||
fn parse(stream: &mut Tokenizer<'_>) -> crate::parser::Result<Self> {
|
||||
match stream.unwrap_named_element()? {
|
||||
NamedElement {
|
||||
ns: Namespace::Dav,
|
||||
element: Element::Exclusive,
|
||||
} => {
|
||||
stream.expect_element_end()?;
|
||||
stream.expect_element_end()?;
|
||||
Ok(LockScope::Exclusive)
|
||||
}
|
||||
NamedElement {
|
||||
ns: Namespace::Dav,
|
||||
element: Element::Shared,
|
||||
} => {
|
||||
stream.expect_element_end()?;
|
||||
stream.expect_element_end()?;
|
||||
Ok(LockScope::Shared)
|
||||
}
|
||||
other => Err(other.into_unexpected()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl DavParser for LockType {
|
||||
fn parse(stream: &mut Tokenizer<'_>) -> crate::parser::Result<Self> {
|
||||
match stream.unwrap_named_element()? {
|
||||
NamedElement {
|
||||
ns: Namespace::Dav,
|
||||
element: Element::Write,
|
||||
} => {
|
||||
stream.expect_element_end()?;
|
||||
stream.expect_element_end()?;
|
||||
Ok(LockType::Write)
|
||||
}
|
||||
other => Err(other.into_unexpected()),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use crate::{
|
||||
parser::{DavParser, Token, tokenizer::Tokenizer},
|
||||
schema::{Element, NamedElement, Namespace, request::MkCol},
|
||||
};
|
||||
|
||||
impl DavParser for MkCol {
|
||||
fn parse(stream: &mut Tokenizer<'_>) -> crate::parser::Result<Self> {
|
||||
let mut mkcol = MkCol {
|
||||
is_mkcalendar: false,
|
||||
props: Vec::new(),
|
||||
};
|
||||
match stream.token()? {
|
||||
Token::ElementStart {
|
||||
name:
|
||||
NamedElement {
|
||||
ns: Namespace::Dav,
|
||||
element: Element::Mkcol,
|
||||
},
|
||||
..
|
||||
} => {}
|
||||
Token::ElementStart {
|
||||
name:
|
||||
NamedElement {
|
||||
ns: Namespace::CalDav,
|
||||
element: Element::Mkcalendar,
|
||||
},
|
||||
..
|
||||
} => {
|
||||
mkcol.is_mkcalendar = true;
|
||||
}
|
||||
Token::Eof => {
|
||||
return Ok(mkcol);
|
||||
}
|
||||
other => return Err(other.into_unexpected()),
|
||||
};
|
||||
|
||||
loop {
|
||||
match stream.token()? {
|
||||
Token::ElementStart {
|
||||
name:
|
||||
NamedElement {
|
||||
ns: Namespace::Dav,
|
||||
element: Element::Set,
|
||||
},
|
||||
..
|
||||
} => {
|
||||
stream.expect_named_element(NamedElement::dav(Element::Prop))?;
|
||||
stream.collect_property_values(&mut mkcol.props)?;
|
||||
stream.expect_element_end()?;
|
||||
}
|
||||
Token::ElementEnd | Token::Eof => {
|
||||
break;
|
||||
}
|
||||
token => return Err(token.into_unexpected()),
|
||||
}
|
||||
}
|
||||
|
||||
Ok(mkcol)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,201 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use crate::{
|
||||
parser::{DavParser, RawElement, Token, tokenizer::Tokenizer},
|
||||
schema::Namespace,
|
||||
};
|
||||
use types::dead_property::{DeadElementTag, DeadProperty, DeadPropertyTag};
|
||||
|
||||
pub mod acl;
|
||||
pub mod lockinfo;
|
||||
pub mod mkcol;
|
||||
pub mod propertyupdate;
|
||||
pub mod propfind;
|
||||
pub mod report;
|
||||
|
||||
impl DavParser for DeadProperty {
|
||||
fn parse(stream: &mut Tokenizer<'_>) -> crate::parser::Result<Self> {
|
||||
let mut depth = 1;
|
||||
let mut items = DeadProperty::default();
|
||||
|
||||
loop {
|
||||
match stream.token()? {
|
||||
Token::ElementStart { raw, .. } | Token::UnknownElement(raw) => {
|
||||
items.0.push(DeadPropertyTag::ElementStart((&raw).into()));
|
||||
depth += 1;
|
||||
}
|
||||
Token::ElementEnd => {
|
||||
depth -= 1;
|
||||
if depth == 0 {
|
||||
break;
|
||||
}
|
||||
items.0.push(DeadPropertyTag::ElementEnd);
|
||||
}
|
||||
Token::Text(text) => {
|
||||
items.0.push(DeadPropertyTag::Text(text.into_owned()));
|
||||
}
|
||||
Token::Bytes(bytes) => {
|
||||
items.0.push(DeadPropertyTag::Text(
|
||||
String::from_utf8_lossy(&bytes).into_owned(),
|
||||
));
|
||||
}
|
||||
Token::Eof => {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(items)
|
||||
}
|
||||
}
|
||||
|
||||
pub trait NsDeadProperty {
|
||||
fn single_with_ns(namespace: Namespace, name: &str) -> Self;
|
||||
}
|
||||
|
||||
impl NsDeadProperty for DeadProperty {
|
||||
fn single_with_ns(namespace: Namespace, name: &str) -> Self {
|
||||
DeadProperty(vec![
|
||||
DeadPropertyTag::ElementStart(DeadElementTag {
|
||||
name: format!("{}:{name}", namespace.prefix()),
|
||||
attrs: None,
|
||||
}),
|
||||
DeadPropertyTag::ElementEnd,
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&RawElement<'_>> for DeadElementTag {
|
||||
fn from(raw: &RawElement<'_>) -> Self {
|
||||
let name = std::str::from_utf8(raw.element.local_name().as_ref())
|
||||
.unwrap_or("invalid-utf8")
|
||||
.trim_ascii()
|
||||
.to_string();
|
||||
let mut attrs = String::with_capacity(raw.element.attributes_raw().len());
|
||||
if let Some(namespace) = &raw.namespace {
|
||||
attrs.push_str("xmlns=\"");
|
||||
attrs.push_str(std::str::from_utf8(namespace).unwrap_or("invalid-utf8"));
|
||||
attrs.push('"');
|
||||
}
|
||||
|
||||
for attr in raw.element.attributes().flatten() {
|
||||
if attr.key.as_ref() == b"xmlns" || attr.key.as_ref().starts_with(b"xmlns:") {
|
||||
// Skip namespace attributes
|
||||
continue;
|
||||
}
|
||||
if let (Ok(key), Ok(value)) = (
|
||||
std::str::from_utf8(attr.key.as_ref()),
|
||||
std::str::from_utf8(attr.value.as_ref()),
|
||||
) {
|
||||
if !attrs.is_empty() {
|
||||
attrs.push(' ');
|
||||
}
|
||||
attrs.push_str(key);
|
||||
attrs.push('=');
|
||||
attrs.push('"');
|
||||
attrs.push_str(value);
|
||||
attrs.push('"');
|
||||
}
|
||||
}
|
||||
|
||||
DeadElementTag {
|
||||
name,
|
||||
attrs: (!attrs.is_empty()).then_some(attrs),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use calcard::vcard::VCardVersion;
|
||||
|
||||
use crate::{
|
||||
parser::{DavParser, tokenizer::Tokenizer},
|
||||
schema::{
|
||||
property::{CardDavProperty, DavProperty},
|
||||
request::{Acl, LockInfo, MkCol, PropFind, PropertyUpdate, Report},
|
||||
},
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn parse_address_data_version() {
|
||||
let xml = r#"<?xml version="1.0" encoding="utf-8" ?>
|
||||
<C:addressbook-query xmlns:D="DAV:" xmlns:C="urn:ietf:params:xml:ns:carddav">
|
||||
<D:prop>
|
||||
<C:address-data content-type="text/vcard" version="3.0">
|
||||
<C:prop name="FN"/>
|
||||
</C:address-data>
|
||||
</D:prop>
|
||||
</C:addressbook-query>"#;
|
||||
|
||||
let mut tokenizer = Tokenizer::new(xml.as_bytes());
|
||||
let report = Report::parse(&mut tokenizer).unwrap();
|
||||
let Report::AddressbookQuery(query) = report else {
|
||||
panic!("expected addressbook-query, got {report:?}");
|
||||
};
|
||||
let PropFind::Prop(properties) = query.properties else {
|
||||
panic!("expected prop, got {:?}", query.properties);
|
||||
};
|
||||
|
||||
let version = properties.iter().find_map(|property| match property {
|
||||
DavProperty::CardDav(CardDavProperty::AddressData { version, .. }) => Some(*version),
|
||||
_ => None,
|
||||
});
|
||||
|
||||
assert_eq!(version, Some(Some(VCardVersion::V3_0)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_requests() {
|
||||
for entry in std::fs::read_dir("resources/requests").unwrap() {
|
||||
let entry = entry.unwrap();
|
||||
let path = entry.path();
|
||||
|
||||
if path.extension().map(|ext| ext == "xml").unwrap_or(false) {
|
||||
println!("Parsing: {:?}", path);
|
||||
let filename = path.file_name().unwrap().to_str().unwrap();
|
||||
let xml = std::fs::read_to_string(&path).unwrap();
|
||||
let mut tokenizer = Tokenizer::new(xml.as_bytes());
|
||||
|
||||
let json_path = path.with_extension("json");
|
||||
let json_output = match filename.split_once('-').unwrap().0 {
|
||||
"propfind" => match PropFind::parse(&mut tokenizer) {
|
||||
Ok(propfind) => serde_json::to_string_pretty(&propfind).unwrap(),
|
||||
Err(_) => String::new(),
|
||||
},
|
||||
"propertyupdate" => serde_json::to_string_pretty(
|
||||
&PropertyUpdate::parse(&mut tokenizer).unwrap(),
|
||||
)
|
||||
.unwrap(),
|
||||
"mkcol" => serde_json::to_string_pretty(&MkCol::parse(&mut tokenizer).unwrap())
|
||||
.unwrap(),
|
||||
"lockinfo" => {
|
||||
serde_json::to_string_pretty(&LockInfo::parse(&mut tokenizer).unwrap())
|
||||
.unwrap()
|
||||
}
|
||||
"report" => {
|
||||
serde_json::to_string_pretty(&Report::parse(&mut tokenizer).unwrap())
|
||||
.unwrap()
|
||||
}
|
||||
"acl" => {
|
||||
serde_json::to_string_pretty(&Acl::parse(&mut tokenizer).unwrap()).unwrap()
|
||||
}
|
||||
_ => {
|
||||
panic!("Unknown method: {}", filename);
|
||||
}
|
||||
};
|
||||
|
||||
/*if json_path.exists() {
|
||||
let expected = std::fs::read_to_string(json_path).unwrap();
|
||||
assert_eq!(json_output, expected);
|
||||
} else {*/
|
||||
std::fs::write(json_path, json_output).unwrap();
|
||||
//}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use crate::{
|
||||
parser::{DavParser, Token, tokenizer::Tokenizer},
|
||||
schema::{Element, NamedElement, Namespace, request::PropertyUpdate},
|
||||
};
|
||||
|
||||
impl DavParser for PropertyUpdate {
|
||||
fn parse(stream: &mut Tokenizer<'_>) -> crate::parser::Result<Self> {
|
||||
stream.expect_named_element(NamedElement::dav(Element::Propertyupdate))?;
|
||||
let mut update = PropertyUpdate {
|
||||
set: Vec::with_capacity(4),
|
||||
remove: Vec::with_capacity(4),
|
||||
set_first: true,
|
||||
};
|
||||
|
||||
loop {
|
||||
match stream.token()? {
|
||||
Token::ElementStart {
|
||||
name:
|
||||
NamedElement {
|
||||
ns: Namespace::Dav,
|
||||
element: Element::Set,
|
||||
},
|
||||
..
|
||||
} => {
|
||||
stream.expect_named_element(NamedElement::dav(Element::Prop))?;
|
||||
stream.collect_property_values(&mut update.set)?;
|
||||
stream.expect_element_end()?;
|
||||
update.set_first = update.remove.is_empty();
|
||||
}
|
||||
Token::ElementStart {
|
||||
name:
|
||||
NamedElement {
|
||||
ns: Namespace::Dav,
|
||||
element: Element::Remove,
|
||||
},
|
||||
..
|
||||
} => {
|
||||
stream.expect_named_element(NamedElement::dav(Element::Prop))?;
|
||||
update.remove = stream.collect_properties(update.remove)?;
|
||||
stream.expect_element_end()?;
|
||||
}
|
||||
Token::ElementEnd | Token::Eof => {
|
||||
break;
|
||||
}
|
||||
Token::UnknownElement(_) => {
|
||||
// Ignore unknown elements
|
||||
stream.seek_element_end()?;
|
||||
}
|
||||
token => return Err(token.into_unexpected()),
|
||||
}
|
||||
}
|
||||
|
||||
Ok(update)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use crate::{
|
||||
parser::{DavParser, Token, tokenizer::Tokenizer},
|
||||
schema::{Element, NamedElement, Namespace, request::PropFind},
|
||||
};
|
||||
|
||||
impl DavParser for PropFind {
|
||||
fn parse(stream: &mut Tokenizer<'_>) -> crate::parser::Result<Self> {
|
||||
if stream.expect_named_element_or_eof(NamedElement::dav(Element::Propfind))? {
|
||||
match stream.unwrap_named_element()? {
|
||||
NamedElement {
|
||||
ns: Namespace::Dav,
|
||||
element: Element::Propname,
|
||||
} => Ok(PropFind::PropName),
|
||||
NamedElement {
|
||||
ns: Namespace::Dav,
|
||||
element: Element::Allprop,
|
||||
} => {
|
||||
stream.expect_element_end()?;
|
||||
if matches!(
|
||||
stream.token()?,
|
||||
Token::ElementStart {
|
||||
name: NamedElement {
|
||||
ns: Namespace::Dav,
|
||||
element: Element::Include
|
||||
},
|
||||
..
|
||||
}
|
||||
) {
|
||||
stream.collect_properties(Vec::new()).map(PropFind::AllProp)
|
||||
} else {
|
||||
Ok(PropFind::AllProp(vec![]))
|
||||
}
|
||||
}
|
||||
NamedElement {
|
||||
ns: Namespace::Dav,
|
||||
element: Element::Prop,
|
||||
} => stream.collect_properties(Vec::new()).map(PropFind::Prop),
|
||||
element => Err(element.into_unexpected()),
|
||||
}
|
||||
} else {
|
||||
Ok(PropFind::AllProp(vec![]))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,641 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use crate::{
|
||||
Depth,
|
||||
parser::{
|
||||
DavParser, RawElement, Token, XmlValueParser, property::TimeRangeFromRaw,
|
||||
tokenizer::Tokenizer,
|
||||
},
|
||||
schema::{
|
||||
Attribute, Collation, Element, MatchType, NamedElement, Namespace,
|
||||
property::DavProperty,
|
||||
request::{
|
||||
AclPrincipalPropSet, AddressbookQuery, CalendarQuery, ExpandProperty,
|
||||
ExpandPropertyItem, Filter, FilterOp, FreeBusyQuery, MultiGet, PrincipalMatch,
|
||||
PrincipalPropertySearch, PropFind, Report, SyncCollection, TextMatch, Timezone,
|
||||
VCardPropertyWithGroup,
|
||||
},
|
||||
},
|
||||
};
|
||||
use calcard::{
|
||||
icalendar::{ICalendarComponentType, ICalendarParameterName, ICalendarProperty},
|
||||
vcard::VCardParameterName,
|
||||
};
|
||||
use types::{TimeRange, dead_property::DeadElementTag};
|
||||
|
||||
impl DavParser for Report {
|
||||
fn parse(stream: &mut Tokenizer<'_>) -> crate::parser::Result<Self> {
|
||||
match stream.unwrap_named_element()? {
|
||||
NamedElement {
|
||||
ns: Namespace::CalDav,
|
||||
element: Element::CalendarQuery,
|
||||
} => CalendarQuery::parse(stream).map(Report::CalendarQuery),
|
||||
NamedElement {
|
||||
ns: Namespace::CalDav,
|
||||
element: Element::FreeBusyQuery,
|
||||
} => FreeBusyQuery::parse(stream).map(Report::FreeBusyQuery),
|
||||
NamedElement {
|
||||
ns: Namespace::CalDav,
|
||||
element: Element::CalendarMultiget,
|
||||
} => MultiGet::parse(stream).map(Report::CalendarMultiGet),
|
||||
NamedElement {
|
||||
ns: Namespace::CardDav,
|
||||
element: Element::AddressbookQuery,
|
||||
} => AddressbookQuery::parse(stream).map(Report::AddressbookQuery),
|
||||
NamedElement {
|
||||
ns: Namespace::CardDav,
|
||||
element: Element::AddressbookMultiget,
|
||||
} => MultiGet::parse(stream).map(Report::AddressbookMultiGet),
|
||||
NamedElement {
|
||||
ns: Namespace::Dav,
|
||||
element: Element::SyncCollection,
|
||||
} => SyncCollection::parse(stream).map(Report::SyncCollection),
|
||||
NamedElement {
|
||||
ns: Namespace::Dav,
|
||||
element: Element::AclPrincipalPropSet,
|
||||
} => AclPrincipalPropSet::parse(stream).map(Report::AclPrincipalPropSet),
|
||||
NamedElement {
|
||||
ns: Namespace::Dav,
|
||||
element: Element::PrincipalMatch,
|
||||
} => PrincipalMatch::parse(stream).map(Report::PrincipalMatch),
|
||||
NamedElement {
|
||||
ns: Namespace::Dav,
|
||||
element: Element::PrincipalPropertySearch,
|
||||
} => PrincipalPropertySearch::parse(stream).map(Report::PrincipalPropertySearch),
|
||||
NamedElement {
|
||||
ns: Namespace::Dav,
|
||||
element: Element::PrincipalSearchPropertySet,
|
||||
} => stream
|
||||
.expect_element_end()
|
||||
.map(|_| Report::PrincipalSearchPropertySet),
|
||||
NamedElement {
|
||||
ns: Namespace::Dav,
|
||||
element: Element::ExpandProperty,
|
||||
} => ExpandProperty::parse(stream).map(Report::ExpandProperty),
|
||||
other => Err(other.into_unexpected()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl DavParser for CalendarQuery {
|
||||
fn parse(stream: &mut Tokenizer<'_>) -> crate::parser::Result<Self> {
|
||||
let mut cq = CalendarQuery {
|
||||
properties: PropFind::AllProp(vec![]),
|
||||
filters: vec![],
|
||||
timezone: Timezone::None,
|
||||
};
|
||||
let mut depth = 1;
|
||||
let mut components = Vec::with_capacity(3);
|
||||
let mut property = None;
|
||||
let mut parameter = None;
|
||||
|
||||
loop {
|
||||
match stream.token()? {
|
||||
Token::ElementStart { name, raw } => match name {
|
||||
NamedElement {
|
||||
ns: Namespace::Dav,
|
||||
element: Element::Propname,
|
||||
} if depth == 1 => {
|
||||
cq.properties = PropFind::PropName;
|
||||
stream.expect_element_end()?;
|
||||
}
|
||||
NamedElement {
|
||||
ns: Namespace::Dav,
|
||||
element: Element::Allprop,
|
||||
} if depth == 1 => {
|
||||
stream.expect_element_end()?;
|
||||
}
|
||||
NamedElement {
|
||||
ns: Namespace::Dav,
|
||||
element: Element::Prop,
|
||||
} if depth == 1 => {
|
||||
cq.properties = PropFind::Prop(stream.collect_properties(Vec::new())?);
|
||||
}
|
||||
NamedElement {
|
||||
ns: Namespace::CalDav,
|
||||
element: Element::Filter,
|
||||
} if depth == 1 => {
|
||||
depth += 1;
|
||||
}
|
||||
NamedElement {
|
||||
ns: Namespace::CalDav,
|
||||
element: Element::Timezone,
|
||||
} if depth == 1 => {
|
||||
cq.timezone =
|
||||
Timezone::Name(stream.collect_string_value()?.unwrap_or_default());
|
||||
}
|
||||
NamedElement {
|
||||
ns: Namespace::CalDav,
|
||||
element: Element::TimezoneId,
|
||||
} if depth == 1 => {
|
||||
cq.timezone =
|
||||
Timezone::Id(stream.collect_string_value()?.unwrap_or_default());
|
||||
}
|
||||
NamedElement {
|
||||
ns: Namespace::CalDav,
|
||||
element: Element::CompFilter,
|
||||
} if depth >= 2 => {
|
||||
for attribute in raw.attributes::<ICalendarComponentType>() {
|
||||
if let Attribute::Name(name) = attribute? {
|
||||
components.push((name, depth));
|
||||
}
|
||||
}
|
||||
depth += 1;
|
||||
}
|
||||
|
||||
NamedElement {
|
||||
ns: Namespace::CalDav,
|
||||
element: Element::PropFilter,
|
||||
} if depth >= 3 => {
|
||||
for attribute in raw.attributes::<ICalendarProperty>() {
|
||||
if let Attribute::Name(name) = attribute? {
|
||||
property = Some(name);
|
||||
}
|
||||
}
|
||||
depth += 1;
|
||||
}
|
||||
NamedElement {
|
||||
ns: Namespace::CalDav,
|
||||
element: Element::ParamFilter,
|
||||
} if depth >= 4 => {
|
||||
for attribute in raw.attributes::<ICalendarParameterName>() {
|
||||
if let Attribute::Name(name) = attribute? {
|
||||
parameter = Some(name);
|
||||
}
|
||||
}
|
||||
depth += 1;
|
||||
}
|
||||
NamedElement {
|
||||
ns: Namespace::CalDav,
|
||||
element: Element::IsNotDefined,
|
||||
} => {
|
||||
stream.expect_element_end()?;
|
||||
if let Some(filter) = Filter::from_parts(
|
||||
components.iter().map(|(c, _)| c.clone()).collect(),
|
||||
property.clone(),
|
||||
parameter.clone(),
|
||||
FilterOp::Undefined,
|
||||
) {
|
||||
cq.filters.push(filter);
|
||||
}
|
||||
}
|
||||
NamedElement {
|
||||
ns: Namespace::CalDav,
|
||||
element: Element::TextMatch,
|
||||
} => {
|
||||
let mut tm = TextMatch::parse(raw)?;
|
||||
tm.value = stream.collect_string_value()?.unwrap_or_default();
|
||||
if let Some(filter) = Filter::from_parts(
|
||||
components.iter().map(|(c, _)| c.clone()).collect(),
|
||||
property.clone(),
|
||||
parameter.clone(),
|
||||
FilterOp::TextMatch(tm),
|
||||
) {
|
||||
cq.filters.push(filter);
|
||||
}
|
||||
}
|
||||
NamedElement {
|
||||
ns: Namespace::CalDav,
|
||||
element: Element::TimeRange,
|
||||
} => {
|
||||
let range = TimeRange::from_raw(&raw)?;
|
||||
stream.expect_element_end()?;
|
||||
if let Some(filter) = range.and_then(|range| {
|
||||
Filter::from_parts(
|
||||
components.iter().map(|(c, _)| c.clone()).collect(),
|
||||
property.clone(),
|
||||
parameter.clone(),
|
||||
FilterOp::TimeRange(range),
|
||||
)
|
||||
}) {
|
||||
cq.filters.push(filter);
|
||||
}
|
||||
}
|
||||
name => return Err(name.into_unexpected()),
|
||||
},
|
||||
Token::ElementEnd => {
|
||||
depth -= 1;
|
||||
if depth == 0 {
|
||||
break;
|
||||
}
|
||||
if matches!(components.last(), Some((_, d)) if *d == depth) {
|
||||
if components.len() > 1
|
||||
&& cq
|
||||
.filters
|
||||
.last()
|
||||
.and_then(|c| c.components())
|
||||
.is_none_or(|c| c.len() < components.len())
|
||||
{
|
||||
cq.filters.push(Filter::Component {
|
||||
comp: components.iter().map(|(c, _)| c.clone()).collect(),
|
||||
op: FilterOp::Exists,
|
||||
});
|
||||
}
|
||||
components.pop();
|
||||
}
|
||||
}
|
||||
Token::UnknownElement(_) => {
|
||||
stream.seek_element_end()?;
|
||||
}
|
||||
element => return Err(element.into_unexpected()),
|
||||
}
|
||||
}
|
||||
|
||||
Ok(cq)
|
||||
}
|
||||
}
|
||||
|
||||
impl DavParser for AddressbookQuery {
|
||||
fn parse(stream: &mut Tokenizer<'_>) -> crate::parser::Result<Self> {
|
||||
let mut aq = AddressbookQuery {
|
||||
properties: PropFind::AllProp(vec![]),
|
||||
filters: vec![],
|
||||
limit: None,
|
||||
};
|
||||
let mut depth = 1;
|
||||
let mut property = None;
|
||||
let mut parameter = None;
|
||||
|
||||
loop {
|
||||
match stream.token()? {
|
||||
Token::ElementStart { name, raw } => match name {
|
||||
NamedElement {
|
||||
ns: Namespace::Dav,
|
||||
element: Element::Propname,
|
||||
} if depth == 1 => {
|
||||
aq.properties = PropFind::PropName;
|
||||
stream.expect_element_end()?;
|
||||
}
|
||||
NamedElement {
|
||||
ns: Namespace::Dav,
|
||||
element: Element::Allprop,
|
||||
} if depth == 1 => {
|
||||
stream.expect_element_end()?;
|
||||
}
|
||||
NamedElement {
|
||||
ns: Namespace::Dav,
|
||||
element: Element::Prop,
|
||||
} if depth == 1 => {
|
||||
aq.properties = PropFind::Prop(stream.collect_properties(Vec::new())?);
|
||||
}
|
||||
NamedElement {
|
||||
ns: Namespace::CardDav,
|
||||
element: Element::Filter,
|
||||
} if depth == 1 => {
|
||||
if let Some(filter) = Filter::parse(raw)? {
|
||||
aq.filters.push(filter);
|
||||
}
|
||||
depth += 1;
|
||||
}
|
||||
NamedElement {
|
||||
ns: Namespace::CardDav,
|
||||
element: Element::Limit,
|
||||
} if depth == 1 => {
|
||||
stream.expect_named_element(NamedElement::carddav(Element::Nresults))?;
|
||||
if let Some(Ok(limit)) = stream.parse_value::<u32>()? {
|
||||
aq.limit = limit.into();
|
||||
}
|
||||
stream.expect_element_end()?;
|
||||
}
|
||||
NamedElement {
|
||||
ns: Namespace::CardDav,
|
||||
element: Element::PropFilter,
|
||||
} if depth == 2 => {
|
||||
let mut filter = None;
|
||||
for attribute in raw.attributes::<VCardPropertyWithGroup>() {
|
||||
match attribute? {
|
||||
Attribute::Name(name) => {
|
||||
property = Some(name);
|
||||
}
|
||||
Attribute::TestAllOf(all_of) => {
|
||||
filter =
|
||||
(if all_of { Filter::AllOf } else { Filter::AnyOf }).into();
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
if let Some(filter) = filter {
|
||||
aq.filters.push(filter);
|
||||
}
|
||||
depth += 1;
|
||||
}
|
||||
NamedElement {
|
||||
ns: Namespace::CardDav,
|
||||
element: Element::ParamFilter,
|
||||
} if depth == 3 => {
|
||||
for attribute in raw.attributes::<VCardParameterName>() {
|
||||
if let Attribute::Name(name) = attribute? {
|
||||
parameter = Some(name);
|
||||
}
|
||||
}
|
||||
depth += 1;
|
||||
}
|
||||
NamedElement {
|
||||
ns: Namespace::CardDav,
|
||||
element: Element::IsNotDefined,
|
||||
} => {
|
||||
stream.expect_element_end()?;
|
||||
if let Some(filter) = Filter::from_parts(
|
||||
(),
|
||||
property.clone(),
|
||||
parameter.clone(),
|
||||
FilterOp::Undefined,
|
||||
) {
|
||||
aq.filters.push(filter);
|
||||
}
|
||||
}
|
||||
NamedElement {
|
||||
ns: Namespace::CardDav,
|
||||
element: Element::TextMatch,
|
||||
} => {
|
||||
let mut tm = TextMatch::parse(raw)?;
|
||||
tm.value = stream.collect_string_value()?.unwrap_or_default();
|
||||
if let Some(filter) = Filter::from_parts(
|
||||
(),
|
||||
property.clone(),
|
||||
parameter.clone(),
|
||||
FilterOp::TextMatch(tm),
|
||||
) {
|
||||
aq.filters.push(filter);
|
||||
}
|
||||
}
|
||||
name => return Err(name.into_unexpected()),
|
||||
},
|
||||
Token::ElementEnd => {
|
||||
depth -= 1;
|
||||
if depth == 0 {
|
||||
break;
|
||||
}
|
||||
}
|
||||
Token::UnknownElement(_) => {
|
||||
stream.seek_element_end()?;
|
||||
}
|
||||
element => return Err(element.into_unexpected()),
|
||||
}
|
||||
}
|
||||
|
||||
Ok(aq)
|
||||
}
|
||||
}
|
||||
|
||||
impl DavParser for FreeBusyQuery {
|
||||
fn parse(stream: &mut Tokenizer<'_>) -> crate::parser::Result<Self> {
|
||||
match stream.token()? {
|
||||
Token::ElementStart {
|
||||
name:
|
||||
NamedElement {
|
||||
ns: Namespace::CalDav,
|
||||
element: Element::TimeRange,
|
||||
},
|
||||
raw,
|
||||
} => TimeRange::from_raw(&raw).map(|range| FreeBusyQuery { range }),
|
||||
other => Err(other.into_unexpected()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl DavParser for MultiGet {
|
||||
fn parse(stream: &mut Tokenizer<'_>) -> crate::parser::Result<Self> {
|
||||
let mut mg = MultiGet {
|
||||
properties: PropFind::AllProp(vec![]),
|
||||
hrefs: vec![],
|
||||
};
|
||||
|
||||
loop {
|
||||
match stream.token()? {
|
||||
Token::ElementStart { name, .. } => match name {
|
||||
NamedElement {
|
||||
ns: Namespace::Dav,
|
||||
element: Element::Propname,
|
||||
} => {
|
||||
mg.properties = PropFind::PropName;
|
||||
stream.expect_element_end()?;
|
||||
}
|
||||
NamedElement {
|
||||
ns: Namespace::Dav,
|
||||
element: Element::Allprop,
|
||||
} => {
|
||||
stream.expect_element_end()?;
|
||||
}
|
||||
NamedElement {
|
||||
ns: Namespace::Dav,
|
||||
element: Element::Prop,
|
||||
} => {
|
||||
mg.properties = PropFind::Prop(stream.collect_properties(Vec::new())?);
|
||||
}
|
||||
NamedElement {
|
||||
ns: Namespace::Dav,
|
||||
element: Element::Href,
|
||||
} => {
|
||||
if let Some(href) = stream.collect_string_value()? {
|
||||
mg.hrefs.push(href);
|
||||
}
|
||||
}
|
||||
name => return Err(name.into_unexpected()),
|
||||
},
|
||||
Token::ElementEnd => {
|
||||
break;
|
||||
}
|
||||
element => return Err(element.into_unexpected()),
|
||||
}
|
||||
}
|
||||
|
||||
Ok(mg)
|
||||
}
|
||||
}
|
||||
|
||||
impl DavParser for SyncCollection {
|
||||
fn parse(stream: &mut Tokenizer<'_>) -> crate::parser::Result<Self> {
|
||||
let mut sc = SyncCollection {
|
||||
properties: PropFind::AllProp(vec![]),
|
||||
limit: None,
|
||||
sync_token: None,
|
||||
depth: Depth::None,
|
||||
};
|
||||
|
||||
loop {
|
||||
match stream.token()? {
|
||||
Token::ElementStart { name, .. } => match name {
|
||||
NamedElement {
|
||||
ns: Namespace::Dav,
|
||||
element: Element::Prop,
|
||||
} => {
|
||||
sc.properties = PropFind::Prop(stream.collect_properties(Vec::new())?);
|
||||
}
|
||||
NamedElement {
|
||||
ns: Namespace::Dav,
|
||||
element: Element::Limit,
|
||||
} => {
|
||||
stream.expect_named_element(NamedElement::dav(Element::Nresults))?;
|
||||
if let Some(Ok(limit)) = stream.parse_value::<u32>()? {
|
||||
sc.limit = limit.into();
|
||||
}
|
||||
stream.expect_element_end()?;
|
||||
}
|
||||
NamedElement {
|
||||
ns: Namespace::Dav,
|
||||
element: Element::SyncToken,
|
||||
} => {
|
||||
sc.sync_token = stream.collect_string_value()?;
|
||||
}
|
||||
NamedElement {
|
||||
ns: Namespace::Dav,
|
||||
element: Element::SyncLevel,
|
||||
} => {
|
||||
if let Some(Ok(depth)) = stream.parse_value::<Depth>()? {
|
||||
sc.depth = depth;
|
||||
}
|
||||
}
|
||||
name => return Err(name.into_unexpected()),
|
||||
},
|
||||
Token::ElementEnd => {
|
||||
break;
|
||||
}
|
||||
Token::UnknownElement(_) => {
|
||||
stream.seek_element_end()?;
|
||||
}
|
||||
element => return Err(element.into_unexpected()),
|
||||
}
|
||||
}
|
||||
|
||||
Ok(sc)
|
||||
}
|
||||
}
|
||||
|
||||
impl DavParser for ExpandProperty {
|
||||
fn parse(stream: &mut Tokenizer<'_>) -> crate::parser::Result<Self> {
|
||||
let mut ep = ExpandProperty { properties: vec![] };
|
||||
let mut depth = 1;
|
||||
|
||||
loop {
|
||||
match stream.token()? {
|
||||
Token::ElementStart { name, raw } => match name {
|
||||
NamedElement {
|
||||
ns,
|
||||
element: Element::Property,
|
||||
} => {
|
||||
for attribute in raw.attributes::<String>() {
|
||||
if let Attribute::Name(name) = attribute? {
|
||||
if let Some(property) = Element::try_parse(name.as_bytes())
|
||||
.copied()
|
||||
.and_then(|element| {
|
||||
DavProperty::from_element(NamedElement { ns, element })
|
||||
})
|
||||
{
|
||||
ep.properties.push(ExpandPropertyItem {
|
||||
property,
|
||||
depth: depth - 1,
|
||||
});
|
||||
} else {
|
||||
let attrs = raw.element.attributes_raw().trim_ascii();
|
||||
ep.properties.push(ExpandPropertyItem {
|
||||
property: DavProperty::DeadProperty(DeadElementTag {
|
||||
name,
|
||||
attrs: (!attrs.is_empty()).then(|| {
|
||||
String::from_utf8_lossy(attrs).into_owned()
|
||||
}),
|
||||
}),
|
||||
depth: depth - 1,
|
||||
});
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
depth += 1;
|
||||
}
|
||||
name => return Err(name.into_unexpected()),
|
||||
},
|
||||
Token::ElementEnd => {
|
||||
depth -= 1;
|
||||
|
||||
if depth == 0 {
|
||||
break;
|
||||
}
|
||||
}
|
||||
Token::UnknownElement(_) => {
|
||||
stream.seek_element_end()?;
|
||||
}
|
||||
element => return Err(element.into_unexpected()),
|
||||
}
|
||||
}
|
||||
|
||||
Ok(ep)
|
||||
}
|
||||
}
|
||||
|
||||
impl TextMatch {
|
||||
fn parse(raw: RawElement<'_>) -> crate::parser::Result<Self> {
|
||||
let mut tm = TextMatch {
|
||||
match_type: MatchType::Contains,
|
||||
value: String::new(),
|
||||
collation: Collation::AsciiCasemap,
|
||||
negate: false,
|
||||
};
|
||||
|
||||
for attribute in raw.attributes::<String>() {
|
||||
match attribute? {
|
||||
Attribute::MatchType(match_type) => {
|
||||
tm.match_type = match_type;
|
||||
}
|
||||
Attribute::NegateCondition(negate) => {
|
||||
tm.negate = negate;
|
||||
}
|
||||
Attribute::Collation(collation) => {
|
||||
tm.collation = collation;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(tm)
|
||||
}
|
||||
}
|
||||
|
||||
impl<A, B, C> Filter<A, B, C> {
|
||||
fn from_parts(comp: A, prop: Option<B>, param: Option<C>, op: FilterOp) -> Option<Self> {
|
||||
match (prop, param) {
|
||||
(Some(prop), Some(param)) => Some(Filter::Parameter {
|
||||
comp,
|
||||
prop,
|
||||
param,
|
||||
op,
|
||||
}),
|
||||
(Some(prop), None) => Some(Filter::Property { comp, prop, op }),
|
||||
(None, None) => Some(Filter::Component { comp, op }),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn components(&self) -> Option<&A> {
|
||||
match self {
|
||||
Filter::Component { comp, .. } => Some(comp),
|
||||
Filter::Property { comp, .. } => Some(comp),
|
||||
Filter::Parameter { comp, .. } => Some(comp),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn parse(raw: RawElement<'_>) -> crate::parser::Result<Option<Self>> {
|
||||
for attribute in raw.attributes::<String>() {
|
||||
if let Attribute::TestAllOf(all_of) = attribute? {
|
||||
return Ok(Some(if all_of { Filter::AllOf } else { Filter::AnyOf }));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
impl XmlValueParser for Depth {
|
||||
fn parse_bytes(bytes: &[u8]) -> Option<Self> {
|
||||
Depth::parse(bytes)
|
||||
}
|
||||
|
||||
fn parse_str(text: &str) -> Option<Self> {
|
||||
Depth::parse(text.as_bytes())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,426 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use crate::{
|
||||
responses::XmlEscape,
|
||||
schema::{
|
||||
Namespace, Namespaces,
|
||||
property::{DavProperty, Privilege},
|
||||
response::{
|
||||
Ace, AclRestrictions, GrantDeny, Href, List, Principal, PrincipalSearchProperty,
|
||||
PrincipalSearchPropertySet, RequiredPrincipal, Resource, SupportedPrivilege,
|
||||
},
|
||||
},
|
||||
};
|
||||
use std::fmt::Display;
|
||||
|
||||
impl Display for SupportedPrivilege {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "<D:supported-privilege>{}", self.privilege)?;
|
||||
if self.abstract_ {
|
||||
write!(f, "<D:abstract/>")?;
|
||||
}
|
||||
write!(f, "<D:description>")?;
|
||||
self.description.write_escaped_to(f)?;
|
||||
write!(
|
||||
f,
|
||||
"</D:description>{}</D:supported-privilege>",
|
||||
self.supported_privilege
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl SupportedPrivilege {
|
||||
pub fn new(privilege: Privilege, description: impl Into<String>) -> Self {
|
||||
SupportedPrivilege {
|
||||
privilege,
|
||||
abstract_: false,
|
||||
description: description.into(),
|
||||
supported_privilege: List(vec![]),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_abstract(mut self) -> Self {
|
||||
self.abstract_ = true;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_supported_privilege(mut self, supported_privilege: SupportedPrivilege) -> Self {
|
||||
self.supported_privilege.0.push(supported_privilege);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_opt_supported_privilege(
|
||||
mut self,
|
||||
supported_privilege: Option<SupportedPrivilege>,
|
||||
) -> Self {
|
||||
if let Some(supported_privilege) = supported_privilege {
|
||||
self.supported_privilege.0.push(supported_privilege);
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
pub fn all_privileges(is_calendar: bool) -> SupportedPrivilege {
|
||||
SupportedPrivilege::new(Privilege::All, "Any operation")
|
||||
.with_abstract()
|
||||
.with_supported_privilege(
|
||||
SupportedPrivilege::new(Privilege::Read, "Read objects").with_supported_privilege(
|
||||
SupportedPrivilege::new(
|
||||
Privilege::ReadCurrentUserPrivilegeSet,
|
||||
"Read current user privileges",
|
||||
),
|
||||
),
|
||||
)
|
||||
.with_supported_privilege(
|
||||
SupportedPrivilege::new(Privilege::Write, "Write objects")
|
||||
.with_supported_privilege(SupportedPrivilege::new(
|
||||
Privilege::WriteProperties,
|
||||
"Write properties",
|
||||
))
|
||||
.with_supported_privilege(SupportedPrivilege::new(
|
||||
Privilege::WriteContent,
|
||||
"Write object contents",
|
||||
))
|
||||
.with_supported_privilege(SupportedPrivilege::new(
|
||||
Privilege::Bind,
|
||||
"Add resources to a collection",
|
||||
))
|
||||
.with_supported_privilege(SupportedPrivilege::new(
|
||||
Privilege::Unbind,
|
||||
"Remove resources from a collection",
|
||||
))
|
||||
.with_supported_privilege(SupportedPrivilege::new(
|
||||
Privilege::Unlock,
|
||||
"Unlock resources",
|
||||
)),
|
||||
)
|
||||
.with_supported_privilege(SupportedPrivilege::new(Privilege::ReadAcl, "Read ACL"))
|
||||
.with_supported_privilege(SupportedPrivilege::new(Privilege::WriteAcl, "Write ACL"))
|
||||
.with_opt_supported_privilege((is_calendar).then(|| {
|
||||
SupportedPrivilege::new(Privilege::ReadFreeBusy, "Read free/busy information")
|
||||
}))
|
||||
}
|
||||
|
||||
pub fn all_scheduling_privileges(is_inbox: bool) -> SupportedPrivilege {
|
||||
let privilege = SupportedPrivilege::new(Privilege::All, "Any operation")
|
||||
.with_abstract()
|
||||
.with_supported_privilege(
|
||||
SupportedPrivilege::new(Privilege::Read, "Read objects").with_supported_privilege(
|
||||
SupportedPrivilege::new(
|
||||
Privilege::ReadCurrentUserPrivilegeSet,
|
||||
"Read current user privileges",
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
if is_inbox {
|
||||
privilege.with_supported_privilege(
|
||||
SupportedPrivilege::new(
|
||||
Privilege::ScheduleDeliver,
|
||||
"Deliver calendar scheduling messages",
|
||||
)
|
||||
.with_supported_privilege(SupportedPrivilege::new(
|
||||
Privilege::ScheduleDeliverInvite,
|
||||
"Deliver calendar scheduling invites",
|
||||
))
|
||||
.with_supported_privilege(SupportedPrivilege::new(
|
||||
Privilege::ScheduleDeliverReply,
|
||||
"Deliver calendar scheduling replies",
|
||||
))
|
||||
.with_supported_privilege(SupportedPrivilege::new(
|
||||
Privilege::ScheduleQueryFreeBusy,
|
||||
"Query free/busy information",
|
||||
)),
|
||||
)
|
||||
} else {
|
||||
privilege.with_supported_privilege(
|
||||
SupportedPrivilege::new(
|
||||
Privilege::ScheduleSend,
|
||||
"Send calendar scheduling messages",
|
||||
)
|
||||
.with_supported_privilege(SupportedPrivilege::new(
|
||||
Privilege::ScheduleSendInvite,
|
||||
"Send calendar scheduling invites",
|
||||
))
|
||||
.with_supported_privilege(SupportedPrivilege::new(
|
||||
Privilege::ScheduleSendReply,
|
||||
"Send calendar scheduling replies",
|
||||
))
|
||||
.with_supported_privilege(SupportedPrivilege::new(
|
||||
Privilege::ScheduleSendFreeBusy,
|
||||
"Send free/busy information",
|
||||
)),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for Ace {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "<D:ace>")?;
|
||||
if self.invert {
|
||||
write!(f, "<D:invert>")?;
|
||||
}
|
||||
self.principal.fmt(f)?;
|
||||
if self.invert {
|
||||
write!(f, "</D:invert>")?;
|
||||
}
|
||||
self.grant_deny.fmt(f)?;
|
||||
if self.protected {
|
||||
write!(f, "<D:protected/>")?;
|
||||
}
|
||||
if let Some(inherited) = &self.inherited {
|
||||
write!(f, "<D:inherited>")?;
|
||||
inherited.fmt(f)?;
|
||||
write!(f, "</D:inherited>")?;
|
||||
}
|
||||
write!(f, "</D:ace>")
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for Principal {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "<D:principal>")?;
|
||||
match self {
|
||||
Principal::Href(href) => href.fmt(f),
|
||||
Principal::Response(response) => response.fmt(f),
|
||||
Principal::All => "<D:all/>".fmt(f),
|
||||
Principal::Authenticated => "<D:authenticated/>".fmt(f),
|
||||
Principal::Unauthenticated => "<D:unauthenticated/>".fmt(f),
|
||||
Principal::Property(property) => {
|
||||
write!(f, "<D:property>{}</D:property>", property)
|
||||
}
|
||||
Principal::Self_ => "<D:self/>".fmt(f),
|
||||
}?;
|
||||
write!(f, "</D:principal>")
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for GrantDeny {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
GrantDeny::Grant(privileges) => {
|
||||
write!(f, "<D:grant>")?;
|
||||
privileges.fmt(f)?;
|
||||
write!(f, "</D:grant>")
|
||||
}
|
||||
GrantDeny::Deny(privileges) => {
|
||||
write!(f, "<D:deny>")?;
|
||||
privileges.fmt(f)?;
|
||||
write!(f, "</D:deny>")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for AclRestrictions {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
if self.grant_only {
|
||||
write!(f, "<D:grant-only/>")?;
|
||||
}
|
||||
if self.no_invert {
|
||||
write!(f, "<D:no-invert/>")?;
|
||||
}
|
||||
if self.deny_before_grant {
|
||||
write!(f, "<D:deny-before-grant/>")?;
|
||||
}
|
||||
if let Some(required_principal) = &self.required_principal {
|
||||
required_principal.fmt(f)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for RequiredPrincipal {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "<D:required-principal>")?;
|
||||
match self {
|
||||
RequiredPrincipal::All => "<D:all/>".fmt(f)?,
|
||||
RequiredPrincipal::Authenticated => "<D:authenticated/>".fmt(f)?,
|
||||
RequiredPrincipal::Unauthenticated => "<D:unauthenticated/>".fmt(f)?,
|
||||
RequiredPrincipal::Self_ => "<D:self/>".fmt(f)?,
|
||||
RequiredPrincipal::Href(hrefs) => hrefs.fmt(f)?,
|
||||
RequiredPrincipal::Property(properties) => {
|
||||
for property in properties {
|
||||
write!(f, "<D:property>{}</D:property>", property)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
write!(f, "</D:required-principal>")
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for Privilege {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Privilege::Read => "<D:privilege><D:read/></D:privilege>".fmt(f),
|
||||
Privilege::Write => "<D:privilege><D:write/></D:privilege>".fmt(f),
|
||||
Privilege::WriteProperties => "<D:privilege><D:write-properties/></D:privilege>".fmt(f),
|
||||
Privilege::WriteContent => "<D:privilege><D:write-content/></D:privilege>".fmt(f),
|
||||
Privilege::Unlock => "<D:privilege><D:unlock/></D:privilege>".fmt(f),
|
||||
Privilege::ReadAcl => "<D:privilege><D:read-acl/></D:privilege>".fmt(f),
|
||||
Privilege::ReadCurrentUserPrivilegeSet => {
|
||||
"<D:privilege><D:read-current-user-privilege-set/></D:privilege>".fmt(f)
|
||||
}
|
||||
Privilege::WriteAcl => "<D:privilege><D:write-acl/></D:privilege>".fmt(f),
|
||||
Privilege::Bind => "<D:privilege><D:bind/></D:privilege>".fmt(f),
|
||||
Privilege::Unbind => "<D:privilege><D:unbind/></D:privilege>".fmt(f),
|
||||
Privilege::All => "<D:privilege><D:all/></D:privilege>".fmt(f),
|
||||
Privilege::ReadFreeBusy => "<D:privilege><A:read-free-busy/></D:privilege>".fmt(f),
|
||||
Privilege::ScheduleDeliver => "<D:privilege><A:schedule-deliver/></D:privilege>".fmt(f),
|
||||
Privilege::ScheduleDeliverInvite => {
|
||||
"<D:privilege><A:schedule-deliver-invite/></D:privilege>".fmt(f)
|
||||
}
|
||||
Privilege::ScheduleDeliverReply => {
|
||||
"<D:privilege><A:schedule-deliver-reply/></D:privilege>".fmt(f)
|
||||
}
|
||||
Privilege::ScheduleQueryFreeBusy => {
|
||||
"<D:privilege><A:schedule-query-freebusy/></D:privilege>".fmt(f)
|
||||
}
|
||||
Privilege::ScheduleSend => "<D:privilege><A:schedule-send/></D:privilege>".fmt(f),
|
||||
Privilege::ScheduleSendInvite => {
|
||||
"<D:privilege><A:schedule-send-invite/></D:privilege>".fmt(f)
|
||||
}
|
||||
Privilege::ScheduleSendReply => {
|
||||
"<D:privilege><A:schedule-send-reply/></D:privilege>".fmt(f)
|
||||
}
|
||||
Privilege::ScheduleSendFreeBusy => {
|
||||
"<D:privilege><A:schedule-send-freebusy/></D:privilege>".fmt(f)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for Resource {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"<D:resource>{}{}</D:resource>",
|
||||
self.href, self.privilege
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for PrincipalSearchPropertySet {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "<?xml version=\"1.0\" encoding=\"UTF-8\"?>")?;
|
||||
write!(
|
||||
f,
|
||||
"<D:principal-search-property-set {}>{}</D:principal-search-property-set>",
|
||||
self.namespaces, self.properties
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for PrincipalSearchProperty {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"<D:principal-search-property><D:prop>{}</D:prop>",
|
||||
self.name
|
||||
)?;
|
||||
write!(
|
||||
f,
|
||||
"<D:description>{}</D:description></D:principal-search-property>",
|
||||
self.description
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl Resource {
|
||||
pub fn new(href: impl Into<String>, privilege: Privilege) -> Self {
|
||||
Resource {
|
||||
href: Href(href.into()),
|
||||
privilege,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PrincipalSearchPropertySet {
|
||||
pub fn new(properties: Vec<PrincipalSearchProperty>) -> Self {
|
||||
PrincipalSearchPropertySet {
|
||||
namespaces: Namespaces::default(),
|
||||
properties: List(properties),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_namespace(mut self, namespace: Namespace) -> Self {
|
||||
self.namespaces.set(namespace);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl PrincipalSearchProperty {
|
||||
pub fn new(name: impl Into<DavProperty>, description: impl Into<String>) -> Self {
|
||||
PrincipalSearchProperty {
|
||||
name: name.into(),
|
||||
description: description.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Ace {
|
||||
pub fn new(principal: Principal, grant_deny: GrantDeny) -> Self {
|
||||
Ace {
|
||||
principal,
|
||||
invert: false,
|
||||
grant_deny,
|
||||
protected: false,
|
||||
inherited: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_invert(mut self) -> Self {
|
||||
self.invert = true;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_protected(mut self) -> Self {
|
||||
self.protected = true;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_inherited(mut self, inherited: impl Into<String>) -> Self {
|
||||
self.inherited = Some(Href(inherited.into()));
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl GrantDeny {
|
||||
pub fn grant(privileges: Vec<Privilege>) -> Self {
|
||||
GrantDeny::Grant(List(privileges))
|
||||
}
|
||||
|
||||
pub fn deny(privileges: Vec<Privilege>) -> Self {
|
||||
GrantDeny::Deny(List(privileges))
|
||||
}
|
||||
}
|
||||
|
||||
impl AclRestrictions {
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
pub fn with_grant_only(mut self) -> Self {
|
||||
self.grant_only = true;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_no_invert(mut self) -> Self {
|
||||
self.no_invert = true;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_deny_before_grant(mut self) -> Self {
|
||||
self.deny_before_grant = true;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_required_principal(mut self, required_principal: RequiredPrincipal) -> Self {
|
||||
self.required_principal = Some(required_principal);
|
||||
self
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,201 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use crate::schema::{
|
||||
Namespace, Namespaces,
|
||||
response::{BaseCondition, CalCondition, CardCondition, Condition, ErrorResponse},
|
||||
};
|
||||
use std::fmt::Display;
|
||||
|
||||
impl Display for ErrorResponse {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"<?xml version=\"1.0\" encoding=\"UTF-8\"?><D:error {}>",
|
||||
self.namespaces
|
||||
)?;
|
||||
|
||||
match &self.error {
|
||||
Condition::Base(e) => e.fmt(f)?,
|
||||
Condition::Cal(e) => e.fmt(f)?,
|
||||
Condition::Card(e) => e.fmt(f)?,
|
||||
}
|
||||
|
||||
write!(f, "</D:error>")
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for Condition {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "<D:error>")?;
|
||||
|
||||
match self {
|
||||
Condition::Base(e) => e.fmt(f)?,
|
||||
Condition::Cal(e) => e.fmt(f)?,
|
||||
Condition::Card(e) => e.fmt(f)?,
|
||||
}
|
||||
|
||||
write!(f, "</D:error>")
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for BaseCondition {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
BaseCondition::NoConflictingLock(items) => {
|
||||
write!(f, "<D:no-conflicting-lock>{items}</D:no-conflicting-lock>")
|
||||
}
|
||||
BaseCondition::LockTokenSubmitted(items) => write!(
|
||||
f,
|
||||
"<D:lock-token-submitted>{items}</D:lock-token-submitted>"
|
||||
),
|
||||
BaseCondition::LockTokenMatchesRequestUri => {
|
||||
write!(f, "<D:lock-token-matches-request-uri/>")
|
||||
}
|
||||
BaseCondition::CannotModifyProtectedProperty => {
|
||||
write!(f, "<D:cannot-modify-protected-property/>")
|
||||
}
|
||||
BaseCondition::NoExternalEntities => write!(f, "<D:no-external-entities/>"),
|
||||
BaseCondition::PreservedLiveProperties => write!(f, "<D:preserved-live-properties/>"),
|
||||
BaseCondition::PropFindFiniteDepth => write!(f, "<D:propfind-finite-depth/>"),
|
||||
BaseCondition::ResourceMustBeNull => write!(f, "<D:resource-must-be-null/>"),
|
||||
BaseCondition::NeedPrivileges(resources) => {
|
||||
write!(f, "<D:need-privileges>{resources}</D:need-privileges>")
|
||||
}
|
||||
BaseCondition::NumberOfMatchesWithinLimit => {
|
||||
write!(f, "<D:number-of-matches-within-limits/>")
|
||||
}
|
||||
BaseCondition::QuotaNotExceeded => write!(f, "<D:quota-not-exceeded/>"),
|
||||
BaseCondition::ValidResourceType => write!(f, "<D:valid-resourcetype/>"),
|
||||
BaseCondition::ValidSyncToken => write!(f, "<D:valid-sync-token/>"),
|
||||
BaseCondition::NoAceConflict => write!(f, "<D:no-ace-conflict/>"),
|
||||
BaseCondition::NoProtectedAceConflict => write!(f, "<D:no-protected-ace-conflict/>"),
|
||||
BaseCondition::NoInheritedAceConflict => write!(f, "<D:no-inherited-ace-conflict/>"),
|
||||
BaseCondition::LimitedNumberOfAces => write!(f, "<D:limited-number-of-aces/>"),
|
||||
BaseCondition::DenyBeforeGrant => write!(f, "<D:deny-before-grant/>"),
|
||||
BaseCondition::GrantOnly => write!(f, "<D:grant-only/>"),
|
||||
BaseCondition::NoInvert => write!(f, "<D:no-invert/>"),
|
||||
BaseCondition::NoAbstract => write!(f, "<D:no-abstract/>"),
|
||||
BaseCondition::NotSupportedPrivilege => write!(f, "<D:not-supported-privilege/>"),
|
||||
BaseCondition::MissingRequiredPrincipal => write!(f, "<D:missing-required-principal/>"),
|
||||
BaseCondition::RecognizedPrincipal => write!(f, "<D:recognized-principal/>"),
|
||||
BaseCondition::AllowedPrincipal => write!(f, "<D:allowed-principal/>"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for CalCondition {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
CalCondition::CalendarCollectionLocationOk => {
|
||||
write!(f, "<A:calendar-collection-location-ok/>")
|
||||
}
|
||||
CalCondition::ValidCalendarData => write!(f, "<A:valid-calendar-data/>"),
|
||||
CalCondition::ValidFilter => write!(f, "<A:valid-filter/>"),
|
||||
CalCondition::ValidTimezone => write!(f, "<A:valid-timezone/>"),
|
||||
CalCondition::ValidCalendarObjectResource => {
|
||||
write!(f, "<A:valid-calendar-object-resource/>")
|
||||
}
|
||||
CalCondition::NoUidConflict(uid) => {
|
||||
write!(f, "<A:no-uid-conflict>{uid}</A:no-uid-conflict>")
|
||||
}
|
||||
CalCondition::InitializeCalendarCollection => {
|
||||
write!(f, "<A:initialize-calendar-collection/>")
|
||||
}
|
||||
CalCondition::SupportedCalendarData => write!(f, "<A:supported-calendar-data/>"),
|
||||
CalCondition::SupportedFilter(_) => write!(f, "<A:supported-filter/>"),
|
||||
CalCondition::SupportedCollation(c) => {
|
||||
write!(f, "<A:supported-collation>{c}</A:supported-collation>")
|
||||
}
|
||||
CalCondition::MinDateTime => write!(f, "<A:min-date-time/>"),
|
||||
CalCondition::MaxDateTime => write!(f, "<A:max-date-time/>"),
|
||||
CalCondition::MaxResourceSize(l) => {
|
||||
write!(f, "<A:max-resource-size>{l}</A:max-resource-size>")
|
||||
}
|
||||
CalCondition::MaxInstances => write!(f, "<A:max-instances/>"),
|
||||
CalCondition::MaxAttendeesPerInstance => write!(f, "<A:max-attendees-per-instance/>"),
|
||||
CalCondition::UniqueSchedulingObjectResource(href) => write!(
|
||||
f,
|
||||
"<A:unique-scheduling-object-resource>{href}</A:unique-scheduling-object-resource>"
|
||||
),
|
||||
CalCondition::SameOrganizerInAllComponents => {
|
||||
write!(f, "<A:same-organizer-in-all-components/>")
|
||||
}
|
||||
CalCondition::AllowedOrganizerObjectChange => {
|
||||
write!(f, "<A:allowed-organizer-scheduling-object-change/>")
|
||||
}
|
||||
CalCondition::AllowedAttendeeObjectChange => {
|
||||
write!(f, "<A:allowed-attendee-scheduling-object-change/>")
|
||||
}
|
||||
CalCondition::DefaultCalendarNeeded => write!(f, "<A:default-calendar-needed/>"),
|
||||
CalCondition::ValidScheduleDefaultCalendarUrl => {
|
||||
write!(f, "<A:valid-schedule-default-calendar-URL/>")
|
||||
}
|
||||
CalCondition::ValidSchedulingMessage => write!(f, "<A:valid-scheduling-message/>"),
|
||||
CalCondition::ValidOrganizer => write!(f, "<A:valid-organizer/>"),
|
||||
CalCondition::SupportedCalendarComponent => {
|
||||
write!(f, "<A:supported-calendar-component/>")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for CardCondition {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
CardCondition::SupportedAddressData => write!(f, "<B:supported-address-data/>"),
|
||||
CardCondition::SupportedAddressDataConversion => {
|
||||
write!(f, "<B:supported-address-data-conversion/>")
|
||||
}
|
||||
CardCondition::SupportedFilter(_) => write!(f, "<B:supported-filter/>"),
|
||||
CardCondition::SupportedCollation(c) => {
|
||||
write!(f, "<B:supported-collation>{c}</B:supported-collation>")
|
||||
}
|
||||
CardCondition::ValidAddressData => write!(f, "<B:valid-address-data/>"),
|
||||
CardCondition::NoUidConflict(uid) => {
|
||||
write!(f, "<B:no-uid-conflict>{uid}</B:no-uid-conflict>")
|
||||
}
|
||||
CardCondition::MaxResourceSize(l) => {
|
||||
write!(f, "<B:max-resource-size>{l}</B:max-resource-size>")
|
||||
}
|
||||
CardCondition::AddressBookCollectionLocationOk => {
|
||||
write!(f, "<B:addressbook-collection-location-ok/>")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<CalCondition> for Condition {
|
||||
fn from(error: CalCondition) -> Self {
|
||||
Condition::Cal(error)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<CardCondition> for Condition {
|
||||
fn from(error: CardCondition) -> Self {
|
||||
Condition::Card(error)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<BaseCondition> for Condition {
|
||||
fn from(error: BaseCondition) -> Self {
|
||||
Condition::Base(error)
|
||||
}
|
||||
}
|
||||
|
||||
impl ErrorResponse {
|
||||
pub fn new(error: impl Into<Condition>) -> Self {
|
||||
ErrorResponse {
|
||||
namespaces: Namespaces::default(),
|
||||
error: error.into(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_namespace(mut self, namespace: impl Into<Namespace>) -> Self {
|
||||
self.namespaces.set(namespace.into());
|
||||
self
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,174 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use crate::{
|
||||
Depth, Timeout,
|
||||
responses::DeadPropertyFormat,
|
||||
schema::{
|
||||
property::{ActiveLock, LockDiscovery, LockEntry, LockScope, LockType, SupportedLock},
|
||||
request::LockInfo,
|
||||
response::{Href, List},
|
||||
},
|
||||
};
|
||||
use std::fmt::Display;
|
||||
use types::dead_property::DeadProperty;
|
||||
|
||||
impl Display for SupportedLock {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "<D:supportedlock>{}</D:supportedlock>", self.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for LockDiscovery {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "<D:lockdiscovery>{}</D:lockdiscovery>", self.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for ActiveLock {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"<D:activelock>{}{}{}",
|
||||
self.lock_scope, self.lock_type, self.depth
|
||||
)?;
|
||||
|
||||
if let Some(owner) = &self.owner {
|
||||
f.write_str("<D:owner>")?;
|
||||
owner.fmt(f)?;
|
||||
f.write_str("</D:owner>")?;
|
||||
}
|
||||
|
||||
write!(f, "{}", self.timeout)?;
|
||||
|
||||
if let Some(lock_token) = &self.lock_token {
|
||||
write!(f, "<D:locktoken>{}</D:locktoken>", lock_token)?;
|
||||
}
|
||||
|
||||
write!(
|
||||
f,
|
||||
"<D:lockroot>{}</D:lockroot></D:activelock>",
|
||||
self.lock_root
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for Depth {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Depth::Zero => write!(f, "<D:depth>0</D:depth>"),
|
||||
Depth::One => write!(f, "<D:depth>1</D:depth>"),
|
||||
Depth::Infinity => write!(f, "<D:depth>infinity</D:depth>"),
|
||||
Depth::None => write!(f, "<D:depth/>"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for Timeout {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Timeout::Infinite => write!(f, "<D:timeout>Infinite</D:timeout>"),
|
||||
Timeout::Second(s) => write!(f, "<D:timeout>Second-{}</D:timeout>", s),
|
||||
Timeout::None => Ok(()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for LockInfo {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "<D:lockinfo>{}{}", self.lock_scope, self.lock_type)?;
|
||||
|
||||
if let Some(owner) = &self.owner {
|
||||
f.write_str("<D:owner>")?;
|
||||
owner.fmt(f)?;
|
||||
f.write_str("</D:owner>")?;
|
||||
}
|
||||
|
||||
write!(f, "</D:lockinfo>",)
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for LockEntry {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"<D:lockentry>{}{}</D:lockentry>",
|
||||
self.lock_scope, self.lock_type
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for LockScope {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
LockScope::Exclusive => write!(f, "<D:lockscope><D:exclusive/></D:lockscope>"),
|
||||
LockScope::Shared => write!(f, "<D:lockscope><D:shared/></D:lockscope>"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for LockType {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
LockType::Write => write!(f, "<D:locktype><D:write/></D:locktype>"),
|
||||
LockType::Other => write!(f, "<D:locktype><D:other/></D:locktype>"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ActiveLock {
|
||||
pub fn new(href: impl Into<String>, lock_scope: LockScope) -> Self {
|
||||
Self {
|
||||
lock_scope,
|
||||
lock_type: LockType::Write,
|
||||
depth: Depth::Infinity,
|
||||
owner: None,
|
||||
timeout: Timeout::Infinite,
|
||||
lock_token: None,
|
||||
lock_root: Href(href.into()),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_depth(mut self, depth: Depth) -> Self {
|
||||
self.depth = depth;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_timeout(mut self, timeout: u64) -> Self {
|
||||
self.timeout = Timeout::Second(timeout);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_owner_opt(mut self, owner: Option<DeadProperty>) -> Self {
|
||||
self.owner = owner;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_owner(mut self, owner: DeadProperty) -> Self {
|
||||
self.owner = Some(owner);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_lock_token(mut self, token: impl Into<String>) -> Self {
|
||||
self.lock_token = Some(Href(token.into()));
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for SupportedLock {
|
||||
fn default() -> Self {
|
||||
Self(List(vec![
|
||||
LockEntry {
|
||||
lock_scope: LockScope::Exclusive,
|
||||
lock_type: LockType::Write,
|
||||
},
|
||||
LockEntry {
|
||||
lock_scope: LockScope::Shared,
|
||||
lock_type: LockType::Write,
|
||||
},
|
||||
]))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use crate::schema::{
|
||||
Namespace, Namespaces,
|
||||
response::{List, MkColResponse, PropStat},
|
||||
};
|
||||
use std::fmt::Display;
|
||||
|
||||
impl Display for MkColResponse {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "<?xml version=\"1.0\" encoding=\"UTF-8\"?>")?;
|
||||
if !self.mkcalendar {
|
||||
write!(
|
||||
f,
|
||||
"<D:mkcol-response {}>{}</D:mkcol-response>",
|
||||
self.namespaces, self.propstat
|
||||
)
|
||||
} else {
|
||||
write!(
|
||||
f,
|
||||
"<A:mkcalendar-response {}>{}</A:mkcalendar-response>",
|
||||
self.namespaces, self.propstat
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl MkColResponse {
|
||||
pub fn new(propstat: Vec<PropStat>) -> Self {
|
||||
Self {
|
||||
namespaces: Namespaces::default(),
|
||||
propstat: List(propstat),
|
||||
mkcalendar: false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_mkcalendar(mut self, mkcalendar: bool) -> Self {
|
||||
self.mkcalendar = mkcalendar;
|
||||
if mkcalendar {
|
||||
self.namespaces.set(Namespace::CalDav);
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_namespace(mut self, namespace: Namespace) -> Self {
|
||||
self.namespaces.set(namespace);
|
||||
self
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,750 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
pub mod acl;
|
||||
pub mod error;
|
||||
pub mod lock;
|
||||
pub mod mkcol;
|
||||
pub mod multistatus;
|
||||
pub mod property;
|
||||
pub mod propstat;
|
||||
pub mod schedule;
|
||||
|
||||
use crate::schema::{
|
||||
Namespaces,
|
||||
property::{Comp, ResourceType, SupportedCollation},
|
||||
response::{Href, List, Location, ResponseDescription, Status, SyncToken},
|
||||
};
|
||||
use std::fmt::{Display, Write};
|
||||
use types::dead_property::{DeadProperty, DeadPropertyTag};
|
||||
|
||||
trait XmlEscape {
|
||||
fn write_escaped_to(&self, out: &mut impl Write) -> std::fmt::Result;
|
||||
}
|
||||
|
||||
trait XmlCdataEscape {
|
||||
fn write_cdata_escaped_to(&self, out: &mut impl Write) -> std::fmt::Result;
|
||||
}
|
||||
|
||||
impl<T: AsRef<str>> XmlEscape for T {
|
||||
fn write_escaped_to(&self, out: &mut impl Write) -> std::fmt::Result {
|
||||
let str = self.as_ref();
|
||||
|
||||
for c in str.chars() {
|
||||
match c {
|
||||
'<' => out.write_str("<")?,
|
||||
'>' => out.write_str(">")?,
|
||||
'&' => out.write_str("&")?,
|
||||
'"' => out.write_str(""")?,
|
||||
'\'' => out.write_str("'")?,
|
||||
_ => out.write_char(c)?,
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: AsRef<str>> XmlCdataEscape for T {
|
||||
fn write_cdata_escaped_to(&self, out: &mut impl Write) -> std::fmt::Result {
|
||||
let str = self.as_ref();
|
||||
let mut last_ch = '\0';
|
||||
let mut last_ch2 = '\0';
|
||||
|
||||
out.write_str("<![CDATA[")?;
|
||||
|
||||
for ch in str.chars() {
|
||||
match ch {
|
||||
'>' if last_ch == ']' && last_ch2 == ']' => {
|
||||
out.write_str("]]><![CDATA[>")?;
|
||||
}
|
||||
_ => out.write_char(ch)?,
|
||||
}
|
||||
|
||||
last_ch2 = last_ch;
|
||||
last_ch = ch;
|
||||
}
|
||||
|
||||
out.write_str("]]>")
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for Namespaces {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.write_str("xmlns:D=\"DAV:\"")?;
|
||||
if self.cal {
|
||||
f.write_str(" xmlns:A=\"urn:ietf:params:xml:ns:caldav\"")?;
|
||||
}
|
||||
if self.card {
|
||||
f.write_str(" xmlns:B=\"urn:ietf:params:xml:ns:carddav\"")?;
|
||||
}
|
||||
if self.cs {
|
||||
f.write_str(" xmlns:C=\"http://calendarserver.org/ns/\"")?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for Href {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "<D:href>")?;
|
||||
self.0.write_escaped_to(f)?;
|
||||
write!(f, "</D:href>")
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Display> Display for List<T> {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
for item in &self.0 {
|
||||
item.fmt(f)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for Status {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "<D:status>")?;
|
||||
write!(f, "HTTP/1.1 {}", self.0)?;
|
||||
write!(f, "</D:status>")
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for ResponseDescription {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "<D:responsedescription>")?;
|
||||
self.0.write_escaped_to(f)?;
|
||||
write!(f, "</D:responsedescription>")
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for Location {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "<D:location>")?;
|
||||
self.0.fmt(f)?;
|
||||
write!(f, "</D:location>")
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for SyncToken {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "<D:sync-token>")?;
|
||||
self.0.write_escaped_to(f)?;
|
||||
write!(f, "</D:sync-token>")
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for Comp {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "<A:comp name=\"{}\"/>", self.0.as_str())
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for ResourceType {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
ResourceType::Collection => write!(f, "<D:collection/>"),
|
||||
ResourceType::Principal => write!(f, "<D:principal/>"),
|
||||
ResourceType::AddressBook => write!(f, "<B:addressbook/>"),
|
||||
ResourceType::Calendar => write!(f, "<A:calendar/>"),
|
||||
ResourceType::ScheduleInbox => write!(f, "<A:schedule-inbox/>"),
|
||||
ResourceType::ScheduleOutbox => write!(f, "<A:schedule-outbox/>"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for SupportedCollation {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
let ns = self.namespace.prefix();
|
||||
write!(
|
||||
f,
|
||||
"<{ns}:supported-collation>{}</{ns}:supported-collation>",
|
||||
self.collation.as_str()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
pub trait DeadPropertyFormat {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result;
|
||||
}
|
||||
|
||||
impl DeadPropertyFormat for DeadProperty {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
let mut last_tag = "";
|
||||
|
||||
for item in &self.0 {
|
||||
match item {
|
||||
DeadPropertyTag::ElementStart(tag) => {
|
||||
let name = &tag.name;
|
||||
if let Some(attrs) = &tag.attrs {
|
||||
write!(f, "<{name} {attrs}>")?;
|
||||
} else {
|
||||
write!(f, "<{name}>")?;
|
||||
}
|
||||
last_tag = name;
|
||||
}
|
||||
DeadPropertyTag::ElementEnd => {
|
||||
write!(f, "</{}>", last_tag)?;
|
||||
}
|
||||
DeadPropertyTag::Text(text) => {
|
||||
text.write_escaped_to(f)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::fmt::Display;
|
||||
|
||||
use calcard::{icalendar::ICalendar, vcard::VCard};
|
||||
use hyper::StatusCode;
|
||||
use mail_parser::DateTime;
|
||||
use types::dead_property::{DeadElementTag, DeadProperty, DeadPropertyTag};
|
||||
|
||||
use crate::{
|
||||
Depth,
|
||||
parser::{Token, tokenizer::Tokenizer},
|
||||
responses::XmlCdataEscape,
|
||||
schema::{
|
||||
Namespace,
|
||||
property::{
|
||||
ActiveLock, CalDavProperty, CardDavProperty, DavValue, LockScope, Privilege,
|
||||
ResourceType, Rfc1123DateTime, SupportedLock, WebDavProperty,
|
||||
},
|
||||
request::DavPropertyValue,
|
||||
response::{
|
||||
Ace, AclRestrictions, BaseCondition, ErrorResponse, GrantDeny, Href, List,
|
||||
MkColResponse, MultiStatus, Principal, PrincipalSearchProperty,
|
||||
PrincipalSearchPropertySet, PropResponse, PropStat, RequiredPrincipal, Resource,
|
||||
Response, ScheduleResponse, ScheduleResponseItem, SupportedPrivilege,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
impl<T: Display> List<T> {
|
||||
pub fn new(vec: impl IntoIterator<Item = T>) -> Self {
|
||||
List(vec.into_iter().collect())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ICalendar> for DavValue {
|
||||
fn from(v: ICalendar) -> Self {
|
||||
DavValue::ICalendar(v)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<VCard> for DavValue {
|
||||
fn from(v: VCard) -> Self {
|
||||
DavValue::VCard(v)
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_responses() {
|
||||
for (num, test) in [
|
||||
// 001.xml
|
||||
ErrorResponse::new(BaseCondition::LockTokenSubmitted(List::new([Href(
|
||||
"/locked/".to_string(),
|
||||
)])))
|
||||
.to_string(),
|
||||
// 002.xml
|
||||
MultiStatus::new(vec![Response::new_propstat(
|
||||
"http://www.example.com/file",
|
||||
vec![
|
||||
PropStat::new(DavPropertyValue::new(
|
||||
WebDavProperty::DisplayName,
|
||||
"Box type A",
|
||||
)),
|
||||
PropStat::new(DavPropertyValue::new(
|
||||
WebDavProperty::DisplayName,
|
||||
"Box type B",
|
||||
))
|
||||
.with_status(StatusCode::FORBIDDEN)
|
||||
.with_response_description(
|
||||
"The user does not have access to the DingALing property.",
|
||||
),
|
||||
],
|
||||
)])
|
||||
.with_response_description("There has been an access violation error.")
|
||||
.to_string(),
|
||||
// 003.xml
|
||||
MultiStatus::new(vec![
|
||||
Response::new_propstat(
|
||||
"/container/",
|
||||
vec![PropStat::new_list(vec![
|
||||
DavPropertyValue::new(
|
||||
WebDavProperty::CreationDate,
|
||||
DateTime::parse_rfc3339("1997-12-01T17:42:21-08:00Z").unwrap(),
|
||||
),
|
||||
DavPropertyValue::new(WebDavProperty::DisplayName, "Example collection"),
|
||||
DavPropertyValue::new(
|
||||
WebDavProperty::ResourceType,
|
||||
vec![ResourceType::Collection],
|
||||
),
|
||||
DavPropertyValue::new(
|
||||
WebDavProperty::SupportedLock,
|
||||
SupportedLock::default(),
|
||||
),
|
||||
])],
|
||||
),
|
||||
Response::new_propstat(
|
||||
"/container/front.html",
|
||||
vec![PropStat::new_list(vec![
|
||||
DavPropertyValue::new(
|
||||
WebDavProperty::CreationDate,
|
||||
DateTime::parse_rfc3339("1997-12-01T18:27:21-08:00").unwrap(),
|
||||
),
|
||||
DavPropertyValue::new(WebDavProperty::DisplayName, "Example HTML resource"),
|
||||
DavPropertyValue::new(WebDavProperty::GetContentLength, 4525u64),
|
||||
DavPropertyValue::new(WebDavProperty::GetContentType, "text/html"),
|
||||
DavPropertyValue::new(WebDavProperty::GetETag, "\"zzyzx\""),
|
||||
DavPropertyValue::new(
|
||||
WebDavProperty::GetLastModified,
|
||||
DavValue::Rfc1123Date(Rfc1123DateTime::new(
|
||||
DateTime::parse_rfc822("Mon, 12 Jan 1998 09:25:56 GMT")
|
||||
.unwrap()
|
||||
.to_timestamp(),
|
||||
)),
|
||||
),
|
||||
DavPropertyValue::new(WebDavProperty::ResourceType, DavValue::Null),
|
||||
DavPropertyValue::new(
|
||||
WebDavProperty::SupportedLock,
|
||||
SupportedLock::default(),
|
||||
),
|
||||
])],
|
||||
),
|
||||
])
|
||||
.to_string(),
|
||||
// 004.xml
|
||||
MultiStatus::new(vec![
|
||||
Response::new_status(
|
||||
["http://www.example.com/container/resource3"],
|
||||
StatusCode::LOCKED,
|
||||
)
|
||||
.with_error(BaseCondition::LockTokenSubmitted(List(vec![]))),
|
||||
])
|
||||
.to_string(),
|
||||
// 005.xml
|
||||
PropResponse::new(vec![DavPropertyValue::new(
|
||||
WebDavProperty::LockDiscovery,
|
||||
vec![
|
||||
ActiveLock::new(
|
||||
"http://example.com/workspace/webdav/proposal.doc",
|
||||
LockScope::Exclusive,
|
||||
)
|
||||
.with_owner(DeadProperty(vec![
|
||||
DeadPropertyTag::ElementStart(DeadElementTag {
|
||||
name: "D:href".to_string(),
|
||||
attrs: None,
|
||||
}),
|
||||
DeadPropertyTag::Text("http://example.org/~ejw/contact.html".to_string()),
|
||||
DeadPropertyTag::ElementEnd,
|
||||
]))
|
||||
.with_timeout(604800)
|
||||
.with_lock_token("urn:uuid:e71d4fae-5dec-22d6-fea5-00a0c91e6be4"),
|
||||
],
|
||||
)])
|
||||
.to_string(),
|
||||
// 006.xml
|
||||
MultiStatus::new(vec![Response::new_propstat(
|
||||
"http://www.example.com/container/",
|
||||
vec![PropStat::new_list(vec![DavPropertyValue::new(
|
||||
WebDavProperty::LockDiscovery,
|
||||
vec![
|
||||
ActiveLock::new("http://www.example.com/container/", LockScope::Shared)
|
||||
.with_owner(DeadProperty(vec![DeadPropertyTag::Text(
|
||||
"Jane Smith".to_string(),
|
||||
)]))
|
||||
.with_depth(Depth::Zero)
|
||||
.with_lock_token("urn:uuid:f81de2ad-7f3d-a1b2-4f3c-00a0c91a9d76"),
|
||||
],
|
||||
)])],
|
||||
)])
|
||||
.to_string(),
|
||||
// 007.xml
|
||||
ErrorResponse::new(BaseCondition::LockTokenSubmitted(List(vec![Href(
|
||||
"/workspace/webdav/".to_string(),
|
||||
)])))
|
||||
.to_string(),
|
||||
// 008.xml
|
||||
MultiStatus::new(vec![
|
||||
Response::new_propstat(
|
||||
"http://cal.example.com/bernard/work/abcd2.ics",
|
||||
vec![PropStat::new_list(vec![
|
||||
DavPropertyValue::new(WebDavProperty::GetETag, "\"fffff-abcd2\""),
|
||||
DavPropertyValue::new(
|
||||
CalDavProperty::CalendarData(Default::default()),
|
||||
DavValue::CData(
|
||||
r#"BEGIN:VCALENDAR
|
||||
VERSION:2.0
|
||||
BEGIN:VEVENT
|
||||
DTSTART;TZID=US/Eastern:20060106T140000
|
||||
DURATION:PT1H
|
||||
RECURRENCE-ID;TZID=US/Eastern:20060106T120000
|
||||
SUMMARY:Event #2 bis bis
|
||||
UID:[email protected]
|
||||
END:VEVENT
|
||||
END:VCALENDAR
|
||||
"#
|
||||
.to_string(),
|
||||
),
|
||||
),
|
||||
])],
|
||||
),
|
||||
Response::new_propstat(
|
||||
"http://cal.example.com/bernard/work/abcd3.ics",
|
||||
vec![PropStat::new_list(vec![
|
||||
DavPropertyValue::new(WebDavProperty::GetETag, "\"fffff-abcd3\""),
|
||||
DavPropertyValue::new(
|
||||
CalDavProperty::CalendarData(Default::default()),
|
||||
DavValue::CData(
|
||||
r#"BEGIN:VCALENDAR
|
||||
VERSION:2.0
|
||||
PRODID:-//Example Corp.//CalDAV Client//EN
|
||||
BEGIN:VEVENT
|
||||
DTSTART;TZID=US/Eastern:20060104T100000
|
||||
DURATION:PT1H
|
||||
SUMMARY:Event #3
|
||||
UID:[email protected]
|
||||
END:VEVENT
|
||||
END:VCALENDAR
|
||||
"#
|
||||
.to_string(),
|
||||
),
|
||||
),
|
||||
])],
|
||||
),
|
||||
])
|
||||
.with_namespace(Namespace::CalDav)
|
||||
.to_string(),
|
||||
// 009.xml
|
||||
MkColResponse::new(vec![PropStat::new_list(vec![
|
||||
DavPropertyValue::new(WebDavProperty::ResourceType, DavValue::Null),
|
||||
DavPropertyValue::new(WebDavProperty::DisplayName, DavValue::Null),
|
||||
DavPropertyValue::new(CardDavProperty::AddressbookDescription, DavValue::Null),
|
||||
])])
|
||||
.with_namespace(Namespace::CardDav)
|
||||
.to_string(),
|
||||
// 010.xml
|
||||
MultiStatus::new(vec![Response::new_propstat(
|
||||
"/home/bernard/addressbook/v102.vcf",
|
||||
vec![PropStat::new_list(vec![
|
||||
DavPropertyValue::new(WebDavProperty::GetETag, "\"23ba4d-ff11fb\""),
|
||||
DavPropertyValue::new(
|
||||
CardDavProperty::AddressData {
|
||||
properties: Default::default(),
|
||||
version: None,
|
||||
},
|
||||
DavValue::CData(
|
||||
r#"BEGIN:VCARD
|
||||
VERSION:3.0
|
||||
NICKNAME:me
|
||||
UID:[email protected]
|
||||
FN:Cyrus Daboo
|
||||
EMAIL:[email protected]
|
||||
END:VCARD
|
||||
"#
|
||||
.to_string(),
|
||||
),
|
||||
),
|
||||
])],
|
||||
)])
|
||||
.with_namespace(Namespace::CardDav)
|
||||
.to_string(),
|
||||
// 011.xml
|
||||
MultiStatus::new(vec![
|
||||
Response::new_status(
|
||||
["/home/bernard/addressbook/"],
|
||||
StatusCode::INSUFFICIENT_STORAGE,
|
||||
)
|
||||
.with_error(BaseCondition::NumberOfMatchesWithinLimit)
|
||||
.with_response_description("Only two matching records were returned"),
|
||||
Response::new_propstat(
|
||||
"/home/bernard/addressbook/v102.vcf",
|
||||
vec![PropStat::new_list(vec![DavPropertyValue::new(
|
||||
WebDavProperty::GetETag,
|
||||
"\"23ba4d-ff11fb\"",
|
||||
)])],
|
||||
),
|
||||
Response::new_propstat(
|
||||
"/home/bernard/addressbook/v104.vcf",
|
||||
vec![PropStat::new_list(vec![DavPropertyValue::new(
|
||||
WebDavProperty::GetETag,
|
||||
"\"23ba4d-ff11fc\"",
|
||||
)])],
|
||||
),
|
||||
])
|
||||
.with_namespace(Namespace::CardDav)
|
||||
.to_string(),
|
||||
// 012.xml
|
||||
ErrorResponse::new(BaseCondition::NeedPrivileges(List(vec![
|
||||
Resource::new("/a", Privilege::Unbind),
|
||||
Resource::new("/c", Privilege::Bind),
|
||||
])))
|
||||
.to_string(),
|
||||
// 013.xml
|
||||
PrincipalSearchPropertySet::new(vec![
|
||||
PrincipalSearchProperty::new(WebDavProperty::DisplayName, "Full name"),
|
||||
PrincipalSearchProperty::new(WebDavProperty::DisplayName, "Job title"),
|
||||
])
|
||||
.to_string(),
|
||||
// 014.xml
|
||||
MultiStatus::new(vec![Response::new_propstat(
|
||||
"http://www.example.com/papers/",
|
||||
vec![PropStat::new_list(vec![DavPropertyValue::new(
|
||||
WebDavProperty::SupportedPrivilegeSet,
|
||||
vec![
|
||||
SupportedPrivilege::new(Privilege::All, "Any operation")
|
||||
.with_abstract()
|
||||
.with_supported_privilege(
|
||||
SupportedPrivilege::new(Privilege::Read, "Read any object")
|
||||
.with_supported_privilege(
|
||||
SupportedPrivilege::new(Privilege::ReadAcl, "Read ACL")
|
||||
.with_abstract(),
|
||||
)
|
||||
.with_supported_privilege(
|
||||
SupportedPrivilege::new(
|
||||
Privilege::ReadCurrentUserPrivilegeSet,
|
||||
"Read current user privilege set property",
|
||||
)
|
||||
.with_abstract(),
|
||||
),
|
||||
)
|
||||
.with_supported_privilege(
|
||||
SupportedPrivilege::new(Privilege::Write, "Write any object")
|
||||
.with_supported_privilege(
|
||||
SupportedPrivilege::new(Privilege::WriteAcl, "Write ACL")
|
||||
.with_abstract(),
|
||||
)
|
||||
.with_supported_privilege(SupportedPrivilege::new(
|
||||
Privilege::WriteProperties,
|
||||
"Write properties",
|
||||
))
|
||||
.with_supported_privilege(SupportedPrivilege::new(
|
||||
Privilege::WriteContent,
|
||||
"Write resource content",
|
||||
)),
|
||||
)
|
||||
.with_supported_privilege(SupportedPrivilege::new(
|
||||
Privilege::Unlock,
|
||||
"Unlock resource",
|
||||
)),
|
||||
],
|
||||
)])],
|
||||
)])
|
||||
.to_string(),
|
||||
// 015.xml
|
||||
MultiStatus::new(vec![Response::new_propstat(
|
||||
"http://www.example.com/papers/",
|
||||
vec![PropStat::new_list(vec![DavPropertyValue::new(
|
||||
WebDavProperty::CurrentUserPrivilegeSet,
|
||||
vec![Privilege::Read],
|
||||
)])],
|
||||
)])
|
||||
.to_string(),
|
||||
// 016.xml
|
||||
MultiStatus::new(vec![Response::new_propstat(
|
||||
"http://www.example.com/papers/",
|
||||
vec![PropStat::new_list(vec![DavPropertyValue::new(
|
||||
WebDavProperty::Acl,
|
||||
vec![
|
||||
Ace::new(
|
||||
Principal::Href(Href(
|
||||
"http://www.example.com/acl/groups/maintainers".to_string(),
|
||||
)),
|
||||
GrantDeny::grant(vec![Privilege::Write]),
|
||||
),
|
||||
Ace::new(Principal::All, GrantDeny::grant(vec![Privilege::Read])),
|
||||
],
|
||||
)])],
|
||||
)])
|
||||
.to_string(),
|
||||
// 017.xml
|
||||
MultiStatus::new(vec![Response::new_propstat(
|
||||
"http://www.example.com/papers/",
|
||||
vec![PropStat::new_list(vec![DavPropertyValue::new(
|
||||
WebDavProperty::AclRestrictions,
|
||||
AclRestrictions::new()
|
||||
.with_grant_only()
|
||||
.with_required_principal(RequiredPrincipal::All),
|
||||
)])],
|
||||
)])
|
||||
.to_string(),
|
||||
// 018.xml
|
||||
MultiStatus::new(vec![Response::new_propstat(
|
||||
"http://www.example.com/papers/",
|
||||
vec![PropStat::new_list(vec![DavPropertyValue::new(
|
||||
WebDavProperty::PrincipalCollectionSet,
|
||||
vec![
|
||||
Href("http://www.example.com/acl/users/".to_string()),
|
||||
Href("http://www.example.com/acl/groups/".to_string()),
|
||||
],
|
||||
)])],
|
||||
)])
|
||||
.to_string(),
|
||||
// 019.xml
|
||||
MultiStatus::new(vec![Response::new_propstat(
|
||||
"http://www.example.com/top/container/",
|
||||
vec![PropStat::new_list(vec![
|
||||
DavPropertyValue::new(
|
||||
WebDavProperty::Owner,
|
||||
vec![Href("http://www.example.com/users/gclemm".to_string())],
|
||||
),
|
||||
DavPropertyValue::new(
|
||||
WebDavProperty::SupportedPrivilegeSet,
|
||||
vec![
|
||||
SupportedPrivilege::new(Privilege::All, "Any operation")
|
||||
.with_abstract()
|
||||
.with_supported_privilege(SupportedPrivilege::new(
|
||||
Privilege::Read,
|
||||
"Read any object",
|
||||
))
|
||||
.with_supported_privilege(
|
||||
SupportedPrivilege::new(Privilege::Write, "Write any object")
|
||||
.with_abstract(),
|
||||
)
|
||||
.with_supported_privilege(SupportedPrivilege::new(
|
||||
Privilege::ReadAcl,
|
||||
"Read the ACL",
|
||||
))
|
||||
.with_supported_privilege(SupportedPrivilege::new(
|
||||
Privilege::WriteAcl,
|
||||
"Write the ACL",
|
||||
)),
|
||||
],
|
||||
),
|
||||
DavPropertyValue::new(
|
||||
WebDavProperty::CurrentUserPrivilegeSet,
|
||||
vec![Privilege::Read, Privilege::ReadAcl],
|
||||
),
|
||||
DavPropertyValue::new(
|
||||
WebDavProperty::Acl,
|
||||
vec![
|
||||
Ace::new(
|
||||
Principal::Href(Href(
|
||||
"http://www.example.com/users/esedlar".to_string(),
|
||||
)),
|
||||
GrantDeny::grant(vec![
|
||||
Privilege::Read,
|
||||
Privilege::Write,
|
||||
Privilege::ReadAcl,
|
||||
]),
|
||||
),
|
||||
Ace::new(
|
||||
Principal::Href(Href(
|
||||
"http://www.example.com/groups/mrktng".to_string(),
|
||||
)),
|
||||
GrantDeny::deny(vec![Privilege::Read]),
|
||||
),
|
||||
Ace::new(
|
||||
Principal::Property(List(vec![DavPropertyValue::new(
|
||||
WebDavProperty::Owner,
|
||||
DavValue::Null,
|
||||
)])),
|
||||
GrantDeny::grant(vec![Privilege::ReadAcl, Privilege::WriteAcl]),
|
||||
),
|
||||
Ace::new(Principal::All, GrantDeny::grant(vec![Privilege::Read]))
|
||||
.with_inherited("http://www.example.com/top"),
|
||||
],
|
||||
),
|
||||
])],
|
||||
)])
|
||||
.to_string(),
|
||||
// 020.xml
|
||||
ScheduleResponse {
|
||||
items: List(vec![
|
||||
ScheduleResponseItem {
|
||||
recipient: Href("mailto:[email protected]".to_string()),
|
||||
request_status: "2.0;Success".into(),
|
||||
calendar_data: Some("BEGIN:VCALENDAR".to_string()),
|
||||
},
|
||||
ScheduleResponseItem {
|
||||
recipient: Href("mailto:[email protected]".to_string()),
|
||||
request_status: "2.0;Success".into(),
|
||||
calendar_data: Some("END:VCALENDAR".to_string()),
|
||||
},
|
||||
ScheduleResponseItem {
|
||||
recipient: Href("mailto:[email protected]".to_string()),
|
||||
request_status: "3.7;Invalid calendar user".into(),
|
||||
calendar_data: None,
|
||||
},
|
||||
]),
|
||||
}
|
||||
.to_string(),
|
||||
]
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
{
|
||||
let xml =
|
||||
std::fs::read_to_string(format!("resources/responses/{:03}.xml", num + 1)).unwrap();
|
||||
let mut output_token = Tokenizer::new(test.as_bytes());
|
||||
let mut expected_token = Tokenizer::new(xml.as_bytes());
|
||||
let mut output_tokens = Vec::new();
|
||||
let mut expected_tokens = Vec::new();
|
||||
|
||||
for (tokens, tokenizer) in [
|
||||
(&mut output_tokens, &mut output_token),
|
||||
(&mut expected_tokens, &mut expected_token),
|
||||
] {
|
||||
while let Ok(token) = tokenizer.token() {
|
||||
if token == Token::Eof {
|
||||
break;
|
||||
}
|
||||
match (tokens.last_mut(), token) {
|
||||
(Some(Token::Text(text)), Token::Text(new_text)) => {
|
||||
*text = format!("{}{}", text, new_text).into();
|
||||
}
|
||||
(_, element) => {
|
||||
tokens.push(element.into_owned());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
assert!(!output_tokens.is_empty());
|
||||
assert!(!expected_tokens.is_empty());
|
||||
assert_eq!(output_tokens.len(), expected_tokens.len());
|
||||
|
||||
for (output, expected) in output_tokens.iter().zip(expected_tokens.iter()) {
|
||||
if output != expected {
|
||||
eprintln!("{test}");
|
||||
}
|
||||
assert_eq!(output, expected, "failed for {:03}.xml", num + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn escape_cdata() {
|
||||
for (test, expected) in [
|
||||
("", "<![CDATA[]]>"),
|
||||
("hello", "<![CDATA[hello]]>"),
|
||||
("hello world", "<![CDATA[hello world]]>"),
|
||||
("<hello>", "<![CDATA[<hello>]]>"),
|
||||
("&hello;", "<![CDATA[&hello;]]>"),
|
||||
("'hello'", "<![CDATA['hello']]>"),
|
||||
("\"hello\"", "<![CDATA[\"hello\"]]>"),
|
||||
("<>&'\"", "<![CDATA[<>&'\"]]>"),
|
||||
(">", "<![CDATA[>]]>"),
|
||||
("]]>]", "<![CDATA[]]]]><![CDATA[>]]]>"),
|
||||
("]]>", "<![CDATA[]]]]><![CDATA[>]]>"),
|
||||
("hello]]>world", "<![CDATA[hello]]]]><![CDATA[>world]]>"),
|
||||
(
|
||||
"hello]]><nasty-xml>pure-evil</nasty-xml>",
|
||||
"<![CDATA[hello]]]]><![CDATA[><nasty-xml>pure-evil</nasty-xml>]]>",
|
||||
),
|
||||
] {
|
||||
let mut output = String::new();
|
||||
test.write_cdata_escaped_to(&mut output).unwrap();
|
||||
assert_eq!(output, expected, "failed for input: {test:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,161 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use crate::schema::{
|
||||
Namespace, Namespaces,
|
||||
response::{
|
||||
Condition, Href, List, Location, MultiStatus, PropStat, Response, ResponseDescription,
|
||||
ResponseType, Status, SyncToken,
|
||||
},
|
||||
};
|
||||
use hyper::StatusCode;
|
||||
use std::fmt::Display;
|
||||
|
||||
impl Display for MultiStatus {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"<?xml version=\"1.0\" encoding=\"UTF-8\"?><D:multistatus {}>{}",
|
||||
self.namespaces, self.response
|
||||
)?;
|
||||
if let Some(response_description) = &self.response_description {
|
||||
write!(f, "{response_description}")?;
|
||||
}
|
||||
|
||||
if let Some(sync_token) = &self.sync_token {
|
||||
write!(f, "{sync_token}")?;
|
||||
}
|
||||
|
||||
write!(f, "</D:multistatus>")
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for Response {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "<D:response>")?;
|
||||
self.href.fmt(f)?;
|
||||
self.typ.fmt(f)?;
|
||||
if let Some(error) = &self.error {
|
||||
error.fmt(f)?;
|
||||
}
|
||||
if let Some(response_description) = &self.response_description {
|
||||
response_description.fmt(f)?;
|
||||
}
|
||||
if let Some(location) = &self.location {
|
||||
location.fmt(f)?;
|
||||
}
|
||||
write!(f, "</D:response>")
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for ResponseType {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
ResponseType::PropStat(list) => list.fmt(f),
|
||||
ResponseType::Status { href, status } => {
|
||||
href.fmt(f)?;
|
||||
status.fmt(f)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl MultiStatus {
|
||||
pub fn new(response: Vec<Response>) -> Self {
|
||||
MultiStatus {
|
||||
namespaces: Namespaces::default(),
|
||||
response: List(response),
|
||||
response_description: None,
|
||||
sync_token: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_response(mut self, response: Response) -> Self {
|
||||
self.response.0.push(response);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn not_found(href: impl Into<String>) -> Self {
|
||||
let mut response = Self::new(Vec::with_capacity(1));
|
||||
response.response.0.push(
|
||||
Response::new_status([href], StatusCode::NOT_FOUND)
|
||||
.with_response_description("No resources found"),
|
||||
);
|
||||
response
|
||||
}
|
||||
|
||||
pub fn add_response(&mut self, response: Response) {
|
||||
self.response.0.push(response);
|
||||
}
|
||||
|
||||
pub fn with_response_description(mut self, response_description: impl Into<String>) -> Self {
|
||||
self.response_description = Some(ResponseDescription(response_description.into()));
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_namespace(mut self, namespace: Namespace) -> Self {
|
||||
self.namespaces.set(namespace);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn set_namespace(&mut self, namespace: Namespace) {
|
||||
self.namespaces.set(namespace);
|
||||
}
|
||||
|
||||
pub fn with_sync_token(mut self, sync_token: impl Into<String>) -> Self {
|
||||
self.sync_token = Some(SyncToken(sync_token.into()));
|
||||
self
|
||||
}
|
||||
|
||||
pub fn set_sync_token(&mut self, sync_token: impl Into<String>) {
|
||||
self.sync_token = Some(SyncToken(sync_token.into()));
|
||||
}
|
||||
}
|
||||
|
||||
impl Response {
|
||||
pub fn new_propstat(href: impl Into<Href>, propstat: Vec<PropStat>) -> Self {
|
||||
Response {
|
||||
href: href.into(),
|
||||
typ: ResponseType::PropStat(List(propstat)),
|
||||
error: None,
|
||||
response_description: None,
|
||||
location: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new_status<T, H>(href: T, status: StatusCode) -> Self
|
||||
where
|
||||
T: IntoIterator<Item = H>,
|
||||
H: Into<String>,
|
||||
{
|
||||
let mut href = href.into_iter().map(|h| Href(h.into()));
|
||||
Response {
|
||||
href: href.next().unwrap(),
|
||||
typ: ResponseType::Status {
|
||||
href: List(href.collect()),
|
||||
status: Status(status),
|
||||
},
|
||||
error: None,
|
||||
response_description: None,
|
||||
location: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_error(mut self, error: impl Into<Condition>) -> Self {
|
||||
self.error = Some(error.into());
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_response_description(mut self, response_description: impl Into<String>) -> Self {
|
||||
self.response_description = Some(ResponseDescription(response_description.into()));
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_location(mut self, location: impl Into<String>) -> Self {
|
||||
self.location = Some(Location(Href(location.into())));
|
||||
self
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,440 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use super::{XmlCdataEscape, XmlEscape};
|
||||
use crate::{
|
||||
responses::DeadPropertyFormat,
|
||||
schema::{
|
||||
Namespace, Namespaces,
|
||||
property::{
|
||||
ActiveLock, CalDavProperty, CardDavProperty, Comp, DavProperty, DavValue,
|
||||
LockDiscovery, LockEntry, PrincipalProperty, Privilege, ReportSet, ResourceType,
|
||||
Rfc1123DateTime, SupportedCollation, SupportedLock, WebDavProperty,
|
||||
},
|
||||
request::DavPropertyValue,
|
||||
response::{Ace, AclRestrictions, Href, List, PropResponse, SupportedPrivilege},
|
||||
},
|
||||
};
|
||||
use calcard::icalendar::ICalendarComponentType;
|
||||
use mail_parser::{
|
||||
DateTime,
|
||||
parsers::fields::date::{DOW, MONTH},
|
||||
};
|
||||
use std::fmt::Display;
|
||||
use types::dead_property::DeadProperty;
|
||||
|
||||
impl Display for PropResponse {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"<?xml version=\"1.0\" encoding=\"UTF-8\"?><D:prop {}>{}</D:prop>",
|
||||
self.namespaces, self.properties
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for DavPropertyValue {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
let (name, attrs) = self.property.tag_name();
|
||||
|
||||
write!(f, "<{}", name)?;
|
||||
|
||||
if let Some(attrs) = attrs {
|
||||
write!(f, " {attrs}")?;
|
||||
}
|
||||
|
||||
if !matches!(self.value, DavValue::Null) {
|
||||
write!(f, ">{}</{}>", self.value, name)
|
||||
} else {
|
||||
write!(f, "/>")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for Rfc1123DateTime {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
let dt = DateTime::from_timestamp(self.0);
|
||||
write!(
|
||||
f,
|
||||
"{}, {:02} {} {:04} {:02}:{:02}:{:02} GMT",
|
||||
DOW[dt.day_of_week() as usize],
|
||||
dt.day,
|
||||
MONTH
|
||||
.get(dt.month.saturating_sub(1) as usize)
|
||||
.copied()
|
||||
.unwrap_or_default(),
|
||||
dt.year,
|
||||
dt.hour,
|
||||
dt.minute,
|
||||
dt.second,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for DavValue {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
DavValue::Timestamp(v) => {
|
||||
let dt = DateTime::from_timestamp(*v);
|
||||
write!(
|
||||
f,
|
||||
"{:04}-{:02}-{:02}T{:02}:{:02}:{:02}Z",
|
||||
dt.year, dt.month, dt.day, dt.hour, dt.minute, dt.second,
|
||||
)
|
||||
}
|
||||
DavValue::Rfc1123Date(v) => v.fmt(f),
|
||||
DavValue::Uint64(v) => v.fmt(f),
|
||||
DavValue::String(v) => v.write_escaped_to(f),
|
||||
DavValue::ResourceTypes(v) => v.fmt(f),
|
||||
DavValue::ActiveLocks(v) => v.fmt(f),
|
||||
DavValue::LockEntries(v) => v.fmt(f),
|
||||
DavValue::ReportSets(v) => v.fmt(f),
|
||||
DavValue::CData(v) => v.write_cdata_escaped_to(f),
|
||||
DavValue::Components(v) => v.fmt(f),
|
||||
DavValue::Collations(v) => v.fmt(f),
|
||||
DavValue::Href(v) => v.fmt(f),
|
||||
DavValue::PrivilegeSet(v) => v.fmt(f),
|
||||
DavValue::Privileges(v) => v.fmt(f),
|
||||
DavValue::Acl(v) => v.fmt(f),
|
||||
DavValue::AclRestrictions(v) => v.fmt(f),
|
||||
DavValue::DeadProperty(v) => v.fmt(f),
|
||||
DavValue::SupportedAddressData => {
|
||||
write!(
|
||||
f,
|
||||
concat!(
|
||||
"<B:address-data-type content-type=\"text/vcard\" version=\"4.0\"/>",
|
||||
"<B:address-data-type content-type=\"text/vcard\" version=\"3.0\"/>",
|
||||
"<B:address-data-type content-type=\"text/vcard\" version=\"2.1\"/>",
|
||||
)
|
||||
)
|
||||
}
|
||||
DavValue::SupportedCalendarData => {
|
||||
write!(
|
||||
f,
|
||||
concat!(
|
||||
"<A:calendar-data-type content-type=\"text/calendar\" version=\"2.0\"/>",
|
||||
"<A:calendar-data-type content-type=\"text/calendar\" version=\"1.0\"/>",
|
||||
)
|
||||
)
|
||||
}
|
||||
DavValue::Response(v) => v.fmt(f),
|
||||
DavValue::VCard(_) | DavValue::ICalendar(_) | DavValue::Null => Ok(()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl DavValue {
|
||||
pub fn all_calendar_components() -> Self {
|
||||
DavValue::Components(List(vec![
|
||||
Comp(ICalendarComponentType::VEvent),
|
||||
Comp(ICalendarComponentType::VTodo),
|
||||
Comp(ICalendarComponentType::VJournal),
|
||||
Comp(ICalendarComponentType::VFreebusy),
|
||||
Comp(ICalendarComponentType::VTimezone),
|
||||
Comp(ICalendarComponentType::VAlarm),
|
||||
Comp(ICalendarComponentType::Standard),
|
||||
Comp(ICalendarComponentType::Daylight),
|
||||
Comp(ICalendarComponentType::VAvailability),
|
||||
Comp(ICalendarComponentType::Available),
|
||||
Comp(ICalendarComponentType::Participant),
|
||||
Comp(ICalendarComponentType::VLocation),
|
||||
Comp(ICalendarComponentType::VResource),
|
||||
]))
|
||||
}
|
||||
}
|
||||
|
||||
impl DavProperty {
|
||||
fn tag_name(&self) -> (&str, Option<&str>) {
|
||||
(
|
||||
match self {
|
||||
DavProperty::WebDav(prop) => match prop {
|
||||
WebDavProperty::CreationDate => "D:creationdate",
|
||||
WebDavProperty::DisplayName => "D:displayname",
|
||||
WebDavProperty::GetContentLanguage => "D:getcontentlanguage",
|
||||
WebDavProperty::GetContentLength => "D:getcontentlength",
|
||||
WebDavProperty::GetContentType => "D:getcontenttype",
|
||||
WebDavProperty::GetETag => "D:getetag",
|
||||
WebDavProperty::GetLastModified => "D:getlastmodified",
|
||||
WebDavProperty::ResourceType => "D:resourcetype",
|
||||
WebDavProperty::LockDiscovery => "D:lockdiscovery",
|
||||
WebDavProperty::SupportedLock => "D:supportedlock",
|
||||
WebDavProperty::CurrentUserPrincipal => "D:current-user-principal",
|
||||
WebDavProperty::QuotaAvailableBytes => "D:quota-available-bytes",
|
||||
WebDavProperty::QuotaUsedBytes => "D:quota-used-bytes",
|
||||
WebDavProperty::SupportedReportSet => "D:supported-report-set",
|
||||
WebDavProperty::SyncToken => "D:sync-token",
|
||||
WebDavProperty::Owner => "D:owner",
|
||||
WebDavProperty::Group => "D:group",
|
||||
WebDavProperty::SupportedPrivilegeSet => "D:supported-privilege-set",
|
||||
WebDavProperty::CurrentUserPrivilegeSet => "D:current-user-privilege-set",
|
||||
WebDavProperty::Acl => "D:acl",
|
||||
WebDavProperty::AclRestrictions => "D:acl-restrictions",
|
||||
WebDavProperty::InheritedAclSet => "D:inherited-acl-set",
|
||||
WebDavProperty::PrincipalCollectionSet => "D:principal-collection-set",
|
||||
WebDavProperty::GetCTag => "C:getctag",
|
||||
},
|
||||
DavProperty::CardDav(prop) => match prop {
|
||||
CardDavProperty::AddressbookDescription => "B:addressbook-description",
|
||||
CardDavProperty::SupportedAddressData => "B:supported-address-data",
|
||||
CardDavProperty::SupportedCollationSet => "B:supported-collation-set",
|
||||
CardDavProperty::MaxResourceSize => "B:max-resource-size",
|
||||
CardDavProperty::AddressData { .. } => "B:address-data",
|
||||
},
|
||||
DavProperty::CalDav(prop) => match prop {
|
||||
CalDavProperty::CalendarDescription => "A:calendar-description",
|
||||
CalDavProperty::CalendarTimezone => "A:calendar-timezone",
|
||||
CalDavProperty::SupportedCalendarComponentSet => {
|
||||
"A:supported-calendar-component-set"
|
||||
}
|
||||
CalDavProperty::SupportedCalendarData => "A:supported-calendar-data",
|
||||
CalDavProperty::SupportedCollationSet => "A:supported-collation-set",
|
||||
CalDavProperty::MaxResourceSize => "A:max-resource-size",
|
||||
CalDavProperty::MinDateTime => "A:min-date-time",
|
||||
CalDavProperty::MaxDateTime => "A:max-date-time",
|
||||
CalDavProperty::MaxInstances => "A:max-instances",
|
||||
CalDavProperty::MaxAttendeesPerInstance => "A:max-attendees-per-instance",
|
||||
CalDavProperty::CalendarData(_) => "A:calendar-data",
|
||||
CalDavProperty::TimezoneServiceSet => "A:timezone-service-set",
|
||||
CalDavProperty::TimezoneId => "A:calendar-timezone-id",
|
||||
CalDavProperty::ScheduleDefaultCalendarURL => "A:schedule-default-calendar-URL",
|
||||
CalDavProperty::ScheduleTag => "A:schedule-tag",
|
||||
CalDavProperty::ScheduleCalendarTransp => "A:schedule-calendar-transp",
|
||||
},
|
||||
DavProperty::Principal(prop) => match prop {
|
||||
PrincipalProperty::AlternateURISet => "D:alternate-URI-set",
|
||||
PrincipalProperty::PrincipalURL => "D:principal-URL",
|
||||
PrincipalProperty::GroupMemberSet => "D:group-member-set",
|
||||
PrincipalProperty::GroupMembership => "D:group-membership",
|
||||
PrincipalProperty::CalendarHomeSet => "A:calendar-home-set",
|
||||
PrincipalProperty::AddressbookHomeSet => "B:addressbook-home-set",
|
||||
PrincipalProperty::PrincipalAddress => "B:principal-address",
|
||||
PrincipalProperty::CalendarUserAddressSet => "A:calendar-user-address-set",
|
||||
PrincipalProperty::CalendarUserType => "A:calendar-user-type",
|
||||
PrincipalProperty::ScheduleInboxURL => "A:schedule-inbox-URL",
|
||||
PrincipalProperty::ScheduleOutboxURL => "A:schedule-outbox-URL",
|
||||
},
|
||||
DavProperty::DeadProperty(dead) => {
|
||||
return (dead.name.as_str(), dead.attrs.as_deref());
|
||||
}
|
||||
},
|
||||
None,
|
||||
)
|
||||
}
|
||||
|
||||
pub fn namespace(&self) -> Namespace {
|
||||
match self {
|
||||
DavProperty::WebDav(WebDavProperty::GetCTag) => Namespace::CalendarServer,
|
||||
DavProperty::CardDav(_)
|
||||
| DavProperty::Principal(
|
||||
PrincipalProperty::AddressbookHomeSet | PrincipalProperty::PrincipalAddress,
|
||||
) => Namespace::CardDav,
|
||||
DavProperty::CalDav(_)
|
||||
| DavProperty::Principal(
|
||||
PrincipalProperty::CalendarHomeSet
|
||||
| PrincipalProperty::CalendarUserAddressSet
|
||||
| PrincipalProperty::CalendarUserType
|
||||
| PrincipalProperty::ScheduleInboxURL
|
||||
| PrincipalProperty::ScheduleOutboxURL,
|
||||
) => Namespace::CalDav,
|
||||
_ => Namespace::Dav,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<str> for DavProperty {
|
||||
fn as_ref(&self) -> &str {
|
||||
self.tag_name().0
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for ReportSet {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.write_str("<D:supported-report><D:report>")?;
|
||||
match self {
|
||||
ReportSet::SyncCollection => write!(f, "<D:sync-collection/>"),
|
||||
ReportSet::ExpandProperty => write!(f, "<D:expand-property/>"),
|
||||
ReportSet::AddressbookQuery => write!(f, "<B:addressbook-query/>"),
|
||||
ReportSet::AddressbookMultiGet => write!(f, "<B:addressbook-multiget/>"),
|
||||
ReportSet::CalendarQuery => write!(f, "<A:calendar-query/>"),
|
||||
ReportSet::CalendarMultiGet => write!(f, "<A:calendar-multiget/>"),
|
||||
ReportSet::FreeBusyQuery => write!(f, "<A:free-busy-query/>"),
|
||||
ReportSet::AclPrincipalPropSet => write!(f, "<D:acl-principal-prop-set/>"),
|
||||
ReportSet::PrincipalMatch => write!(f, "<D:principal-match/>"),
|
||||
ReportSet::PrincipalPropertySearch => write!(f, "<D:principal-property-search/>"),
|
||||
ReportSet::PrincipalSearchPropertySet => {
|
||||
write!(f, "<D:principal-search-property-set/>")
|
||||
}
|
||||
}?;
|
||||
f.write_str("</D:report></D:supported-report>")
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for DavProperty {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
let (name, attrs) = self.tag_name();
|
||||
if let Some(attrs) = attrs {
|
||||
write!(f, "<{name} {attrs}/>")
|
||||
} else {
|
||||
write!(f, "<{name}/>")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PropResponse {
|
||||
pub fn new(properties: Vec<DavPropertyValue>) -> Self {
|
||||
PropResponse {
|
||||
namespaces: Namespaces::default(),
|
||||
properties: List(properties),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_namespace(mut self, namespace: Namespace) -> Self {
|
||||
self.namespaces.set(namespace);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl From<WebDavProperty> for DavProperty {
|
||||
fn from(prop: WebDavProperty) -> Self {
|
||||
DavProperty::WebDav(prop)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<CardDavProperty> for DavProperty {
|
||||
fn from(prop: CardDavProperty) -> Self {
|
||||
DavProperty::CardDav(prop)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<CalDavProperty> for DavProperty {
|
||||
fn from(prop: CalDavProperty) -> Self {
|
||||
DavProperty::CalDav(prop)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<String> for DavValue {
|
||||
fn from(v: String) -> Self {
|
||||
DavValue::String(v)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&str> for DavValue {
|
||||
fn from(v: &str) -> Self {
|
||||
DavValue::String(v.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<u64> for DavValue {
|
||||
fn from(v: u64) -> Self {
|
||||
DavValue::Uint64(v)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<DateTime> for DavValue {
|
||||
fn from(v: DateTime) -> Self {
|
||||
DavValue::Timestamp(v.to_timestamp())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Vec<ResourceType>> for DavValue {
|
||||
fn from(v: Vec<ResourceType>) -> Self {
|
||||
DavValue::ResourceTypes(List(v))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Vec<ReportSet>> for DavValue {
|
||||
fn from(v: Vec<ReportSet>) -> Self {
|
||||
DavValue::ReportSets(List(v))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Vec<Comp>> for DavValue {
|
||||
fn from(v: Vec<Comp>) -> Self {
|
||||
DavValue::Components(List(v))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Vec<SupportedCollation>> for DavValue {
|
||||
fn from(v: Vec<SupportedCollation>) -> Self {
|
||||
DavValue::Collations(List(v))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<SupportedLock> for DavValue {
|
||||
fn from(v: SupportedLock) -> Self {
|
||||
DavValue::LockEntries(v.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Vec<LockEntry>> for DavValue {
|
||||
fn from(v: Vec<LockEntry>) -> Self {
|
||||
DavValue::LockEntries(List(v))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Vec<ActiveLock>> for DavValue {
|
||||
fn from(v: Vec<ActiveLock>) -> Self {
|
||||
DavValue::ActiveLocks(List(v))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<LockDiscovery> for DavValue {
|
||||
fn from(v: LockDiscovery) -> Self {
|
||||
DavValue::ActiveLocks(v.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Vec<SupportedPrivilege>> for DavValue {
|
||||
fn from(v: Vec<SupportedPrivilege>) -> Self {
|
||||
DavValue::PrivilegeSet(List(v))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Vec<Privilege>> for DavValue {
|
||||
fn from(v: Vec<Privilege>) -> Self {
|
||||
DavValue::Privileges(List(v))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Vec<Href>> for DavValue {
|
||||
fn from(v: Vec<Href>) -> Self {
|
||||
DavValue::Href(List(v))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Vec<Ace>> for DavValue {
|
||||
fn from(v: Vec<Ace>) -> Self {
|
||||
DavValue::Acl(List(v))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<AclRestrictions> for DavValue {
|
||||
fn from(v: AclRestrictions) -> Self {
|
||||
DavValue::AclRestrictions(v)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<DeadProperty> for DavValue {
|
||||
fn from(v: DeadProperty) -> Self {
|
||||
DavValue::DeadProperty(v)
|
||||
}
|
||||
}
|
||||
|
||||
impl DavPropertyValue {
|
||||
pub fn new(property: impl Into<DavProperty>, value: impl Into<DavValue>) -> Self {
|
||||
DavPropertyValue {
|
||||
property: property.into(),
|
||||
value: value.into(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn empty(property: impl Into<DavProperty>) -> Self {
|
||||
DavPropertyValue {
|
||||
property: property.into(),
|
||||
value: DavValue::Null,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use crate::schema::{
|
||||
request::DavPropertyValue,
|
||||
response::{Condition, List, Prop, PropStat, ResponseDescription, Status},
|
||||
};
|
||||
use hyper::StatusCode;
|
||||
use std::fmt::Display;
|
||||
|
||||
impl Display for PropStat {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "<D:propstat>")?;
|
||||
self.prop.fmt(f)?;
|
||||
self.status.fmt(f)?;
|
||||
if let Some(error) = &self.error {
|
||||
error.fmt(f)?;
|
||||
}
|
||||
if let Some(response_description) = &self.response_description {
|
||||
response_description.fmt(f)?;
|
||||
}
|
||||
write!(f, "</D:propstat>")
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for Prop {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "<D:prop>{}</D:prop>", self.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl PropStat {
|
||||
#[cfg(test)]
|
||||
pub(crate) fn new(prop: impl Into<DavPropertyValue>) -> Self {
|
||||
PropStat {
|
||||
prop: Prop(List(vec![prop.into()])),
|
||||
status: Status(StatusCode::OK),
|
||||
error: None,
|
||||
response_description: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new_list(props: Vec<DavPropertyValue>) -> Self {
|
||||
PropStat {
|
||||
prop: Prop(List(props)),
|
||||
status: Status(StatusCode::OK),
|
||||
error: None,
|
||||
response_description: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_prop(mut self, prop: impl Into<DavPropertyValue>) -> Self {
|
||||
self.prop.0.0.push(prop.into());
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_status(mut self, status: StatusCode) -> Self {
|
||||
self.status = Status(status);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_error(mut self, error: impl Into<Condition>) -> Self {
|
||||
self.error = Some(error.into());
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_response_description(mut self, response_description: impl Into<String>) -> Self {
|
||||
self.response_description = Some(ResponseDescription(response_description.into()));
|
||||
self
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use crate::{
|
||||
responses::{XmlCdataEscape, XmlEscape},
|
||||
schema::{
|
||||
Namespaces,
|
||||
response::{ScheduleResponse, ScheduleResponseItem},
|
||||
},
|
||||
};
|
||||
use std::fmt::Display;
|
||||
|
||||
const NAMESPACE: Namespaces = Namespaces {
|
||||
cal: true,
|
||||
card: false,
|
||||
cs: false,
|
||||
};
|
||||
|
||||
impl Display for ScheduleResponse {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "<?xml version=\"1.0\" encoding=\"UTF-8\"?>")?;
|
||||
write!(
|
||||
f,
|
||||
"<A:schedule-response {NAMESPACE}>{}</A:schedule-response>",
|
||||
self.items
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for ScheduleResponseItem {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "<A:response>")?;
|
||||
write!(f, "<A:recipient>{}</A:recipient>", self.recipient)?;
|
||||
|
||||
write!(f, "<A:request-status>")?;
|
||||
self.request_status.write_escaped_to(f)?;
|
||||
write!(f, "</A:request-status>")?;
|
||||
|
||||
if let Some(calendar_data) = &self.calendar_data {
|
||||
write!(f, "<A:calendar-data>")?;
|
||||
calendar_data.write_cdata_escaped_to(f)?;
|
||||
write!(f, "</A:calendar-data>")?;
|
||||
}
|
||||
write!(f, "</A:response>")
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,441 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use super::{
|
||||
Collation, Namespace,
|
||||
request::DavPropertyValue,
|
||||
response::{Ace, AclRestrictions, Href, List, Response, SupportedPrivilege},
|
||||
};
|
||||
use crate::{Depth, Timeout};
|
||||
use calcard::{
|
||||
icalendar::{ICalendar, ICalendarComponentType, ICalendarProperty},
|
||||
vcard::{VCard, VCardProperty, VCardVersion},
|
||||
};
|
||||
use types::{
|
||||
TimeRange,
|
||||
dead_property::{DeadElementTag, DeadProperty},
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
#[cfg_attr(test, derive(serde::Serialize, serde::Deserialize))]
|
||||
#[cfg_attr(test, serde(tag = "type", content = "data"))]
|
||||
pub enum DavProperty {
|
||||
WebDav(WebDavProperty),
|
||||
CardDav(CardDavProperty),
|
||||
CalDav(CalDavProperty),
|
||||
Principal(PrincipalProperty),
|
||||
DeadProperty(DeadElementTag),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
#[cfg_attr(test, derive(serde::Serialize, serde::Deserialize))]
|
||||
#[cfg_attr(test, serde(tag = "type", content = "data"))]
|
||||
pub enum WebDavProperty {
|
||||
CreationDate,
|
||||
DisplayName,
|
||||
GetContentLanguage,
|
||||
GetContentLength,
|
||||
GetContentType,
|
||||
GetETag,
|
||||
GetLastModified,
|
||||
ResourceType,
|
||||
LockDiscovery,
|
||||
SupportedLock,
|
||||
SupportedReportSet,
|
||||
CurrentUserPrincipal,
|
||||
// Quota properties
|
||||
QuotaAvailableBytes,
|
||||
QuotaUsedBytes,
|
||||
// Sync properties
|
||||
SyncToken,
|
||||
// ACL properties (all protected)
|
||||
Owner,
|
||||
Group,
|
||||
SupportedPrivilegeSet,
|
||||
CurrentUserPrivilegeSet,
|
||||
Acl,
|
||||
AclRestrictions,
|
||||
InheritedAclSet,
|
||||
PrincipalCollectionSet,
|
||||
// Apple proprietary properties
|
||||
GetCTag,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
#[cfg_attr(test, derive(serde::Serialize, serde::Deserialize))]
|
||||
#[cfg_attr(test, serde(tag = "type", content = "data"))]
|
||||
pub enum CardDavProperty {
|
||||
AddressbookDescription,
|
||||
SupportedAddressData,
|
||||
SupportedCollationSet,
|
||||
MaxResourceSize,
|
||||
AddressData {
|
||||
properties: Vec<CardDavPropertyName>,
|
||||
#[cfg_attr(test, serde(skip))]
|
||||
version: Option<VCardVersion>,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
#[cfg_attr(test, derive(serde::Serialize, serde::Deserialize))]
|
||||
pub struct CardDavPropertyName {
|
||||
pub group: Option<String>,
|
||||
pub name: VCardProperty,
|
||||
pub no_value: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
#[cfg_attr(test, derive(serde::Serialize, serde::Deserialize))]
|
||||
#[cfg_attr(test, serde(tag = "type", content = "data"))]
|
||||
pub enum CalDavProperty {
|
||||
CalendarDescription,
|
||||
CalendarTimezone,
|
||||
SupportedCalendarComponentSet,
|
||||
SupportedCalendarData,
|
||||
SupportedCollationSet,
|
||||
MaxResourceSize,
|
||||
MinDateTime,
|
||||
MaxDateTime,
|
||||
MaxInstances,
|
||||
MaxAttendeesPerInstance,
|
||||
CalendarData(CalendarData),
|
||||
TimezoneServiceSet,
|
||||
TimezoneId,
|
||||
ScheduleDefaultCalendarURL,
|
||||
ScheduleTag,
|
||||
ScheduleCalendarTransp,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
#[cfg_attr(test, derive(serde::Serialize, serde::Deserialize))]
|
||||
#[cfg_attr(test, serde(tag = "type", content = "data"))]
|
||||
pub enum PrincipalProperty {
|
||||
AlternateURISet,
|
||||
PrincipalURL,
|
||||
GroupMemberSet,
|
||||
GroupMembership,
|
||||
CalendarHomeSet,
|
||||
AddressbookHomeSet,
|
||||
PrincipalAddress,
|
||||
CalendarUserAddressSet,
|
||||
CalendarUserType,
|
||||
ScheduleInboxURL,
|
||||
ScheduleOutboxURL,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Clone, PartialEq, Eq)]
|
||||
#[cfg_attr(test, derive(serde::Serialize, serde::Deserialize))]
|
||||
pub struct CalendarData {
|
||||
pub properties: Vec<CalDavPropertyName>,
|
||||
pub expand: Option<TimeRange>,
|
||||
pub limit_recurrence: Option<TimeRange>,
|
||||
pub limit_freebusy: Option<TimeRange>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
#[cfg_attr(test, derive(serde::Serialize, serde::Deserialize))]
|
||||
pub struct CalDavPropertyName {
|
||||
pub component: Option<ICalendarComponentType>,
|
||||
pub name: Option<ICalendarProperty>,
|
||||
pub no_value: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
#[cfg_attr(test, derive(serde::Serialize, serde::Deserialize))]
|
||||
#[repr(transparent)]
|
||||
pub struct Rfc1123DateTime(pub(crate) i64);
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
#[cfg_attr(test, derive(serde::Serialize, serde::Deserialize))]
|
||||
pub enum DavValue {
|
||||
Timestamp(i64),
|
||||
Rfc1123Date(Rfc1123DateTime),
|
||||
Uint64(u64),
|
||||
String(String),
|
||||
CData(String),
|
||||
ResourceTypes(List<ResourceType>),
|
||||
ActiveLocks(List<ActiveLock>),
|
||||
LockEntries(List<LockEntry>),
|
||||
ReportSets(List<ReportSet>),
|
||||
ICalendar(ICalendar),
|
||||
VCard(VCard),
|
||||
Components(List<Comp>),
|
||||
Collations(List<SupportedCollation>),
|
||||
PrivilegeSet(List<SupportedPrivilege>),
|
||||
Privileges(List<Privilege>),
|
||||
Href(List<Href>),
|
||||
Acl(List<Ace>),
|
||||
AclRestrictions(AclRestrictions),
|
||||
Response(Box<Response>),
|
||||
DeadProperty(DeadProperty),
|
||||
SupportedAddressData,
|
||||
SupportedCalendarData,
|
||||
Null,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
#[cfg_attr(test, derive(serde::Serialize, serde::Deserialize))]
|
||||
pub enum ReportSet {
|
||||
SyncCollection,
|
||||
ExpandProperty,
|
||||
AddressbookQuery,
|
||||
AddressbookMultiGet,
|
||||
CalendarQuery,
|
||||
CalendarMultiGet,
|
||||
FreeBusyQuery,
|
||||
AclPrincipalPropSet,
|
||||
PrincipalMatch,
|
||||
PrincipalPropertySearch,
|
||||
PrincipalSearchPropertySet,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
#[cfg_attr(test, derive(serde::Serialize, serde::Deserialize))]
|
||||
pub struct Comp(pub ICalendarComponentType);
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
#[cfg_attr(test, derive(serde::Serialize, serde::Deserialize))]
|
||||
pub struct SupportedCollation {
|
||||
pub collation: Collation,
|
||||
pub namespace: Namespace,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
#[cfg_attr(test, derive(serde::Serialize, serde::Deserialize))]
|
||||
pub enum ResourceType {
|
||||
Collection,
|
||||
Principal,
|
||||
AddressBook,
|
||||
Calendar,
|
||||
ScheduleInbox,
|
||||
ScheduleOutbox,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
#[cfg_attr(test, derive(serde::Serialize, serde::Deserialize))]
|
||||
pub struct LockDiscovery(pub List<ActiveLock>);
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
#[cfg_attr(test, derive(serde::Serialize, serde::Deserialize))]
|
||||
pub struct ActiveLock {
|
||||
pub lock_scope: LockScope,
|
||||
pub lock_type: LockType,
|
||||
pub depth: Depth,
|
||||
pub owner: Option<DeadProperty>,
|
||||
pub timeout: Timeout,
|
||||
pub lock_token: Option<Href>,
|
||||
pub lock_root: Href,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
#[cfg_attr(test, derive(serde::Serialize, serde::Deserialize))]
|
||||
pub struct SupportedLock(pub List<LockEntry>);
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
#[cfg_attr(test, derive(serde::Serialize, serde::Deserialize))]
|
||||
pub struct LockEntry {
|
||||
pub lock_scope: LockScope,
|
||||
pub lock_type: LockType,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
#[cfg_attr(test, derive(serde::Serialize, serde::Deserialize))]
|
||||
pub enum LockType {
|
||||
Write,
|
||||
Other,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
#[cfg_attr(test, derive(serde::Serialize, serde::Deserialize))]
|
||||
pub enum LockScope {
|
||||
Exclusive,
|
||||
Shared,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||
#[cfg_attr(test, derive(serde::Serialize, serde::Deserialize))]
|
||||
pub enum Privilege {
|
||||
Read,
|
||||
Write,
|
||||
WriteProperties,
|
||||
WriteContent,
|
||||
Unlock,
|
||||
ReadAcl,
|
||||
ReadCurrentUserPrivilegeSet,
|
||||
WriteAcl,
|
||||
Bind,
|
||||
Unbind,
|
||||
All,
|
||||
ReadFreeBusy,
|
||||
ScheduleDeliver,
|
||||
ScheduleDeliverInvite,
|
||||
ScheduleDeliverReply,
|
||||
ScheduleQueryFreeBusy,
|
||||
ScheduleSend,
|
||||
ScheduleSendInvite,
|
||||
ScheduleSendReply,
|
||||
ScheduleSendFreeBusy,
|
||||
}
|
||||
|
||||
impl Privilege {
|
||||
pub fn all(is_calendar: bool) -> Vec<Privilege> {
|
||||
if is_calendar {
|
||||
vec![
|
||||
Privilege::All,
|
||||
Privilege::Read,
|
||||
Privilege::Write,
|
||||
Privilege::WriteProperties,
|
||||
Privilege::WriteContent,
|
||||
Privilege::Unlock,
|
||||
Privilege::ReadAcl,
|
||||
Privilege::ReadCurrentUserPrivilegeSet,
|
||||
Privilege::WriteAcl,
|
||||
Privilege::Bind,
|
||||
Privilege::Unbind,
|
||||
Privilege::ReadFreeBusy,
|
||||
]
|
||||
} else {
|
||||
vec![
|
||||
Privilege::All,
|
||||
Privilege::Read,
|
||||
Privilege::Write,
|
||||
Privilege::WriteProperties,
|
||||
Privilege::WriteContent,
|
||||
Privilege::Unlock,
|
||||
Privilege::ReadAcl,
|
||||
Privilege::ReadCurrentUserPrivilegeSet,
|
||||
Privilege::WriteAcl,
|
||||
Privilege::Bind,
|
||||
Privilege::Unbind,
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
pub fn scheduling(is_inbox: bool, is_owner: bool) -> Vec<Privilege> {
|
||||
let mut privileges = if is_inbox {
|
||||
vec![
|
||||
Privilege::Read,
|
||||
Privilege::ReadCurrentUserPrivilegeSet,
|
||||
Privilege::ScheduleDeliver,
|
||||
Privilege::ScheduleDeliverInvite,
|
||||
Privilege::ScheduleDeliverReply,
|
||||
Privilege::ScheduleQueryFreeBusy,
|
||||
]
|
||||
} else {
|
||||
vec![
|
||||
Privilege::Read,
|
||||
Privilege::ReadCurrentUserPrivilegeSet,
|
||||
Privilege::ScheduleSend,
|
||||
Privilege::ScheduleSendInvite,
|
||||
Privilege::ScheduleSendReply,
|
||||
Privilege::ScheduleSendFreeBusy,
|
||||
]
|
||||
};
|
||||
|
||||
if is_owner {
|
||||
privileges.extend([
|
||||
Privilege::All,
|
||||
Privilege::Write,
|
||||
Privilege::WriteProperties,
|
||||
Privilege::WriteContent,
|
||||
Privilege::ReadAcl,
|
||||
Privilege::WriteAcl,
|
||||
]);
|
||||
}
|
||||
|
||||
privileges
|
||||
}
|
||||
}
|
||||
|
||||
impl From<DavProperty> for DavPropertyValue {
|
||||
fn from(value: DavProperty) -> Self {
|
||||
DavPropertyValue {
|
||||
property: value,
|
||||
value: DavValue::Null,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Rfc1123DateTime {
|
||||
pub fn new(timestamp: i64) -> Self {
|
||||
Self(timestamp)
|
||||
}
|
||||
}
|
||||
|
||||
impl DavProperty {
|
||||
pub const ALL_PROPS: [DavProperty; 11] = [
|
||||
DavProperty::WebDav(WebDavProperty::CreationDate),
|
||||
DavProperty::WebDav(WebDavProperty::DisplayName),
|
||||
DavProperty::WebDav(WebDavProperty::GetETag),
|
||||
DavProperty::WebDav(WebDavProperty::GetLastModified),
|
||||
DavProperty::WebDav(WebDavProperty::ResourceType),
|
||||
DavProperty::WebDav(WebDavProperty::LockDiscovery),
|
||||
DavProperty::WebDav(WebDavProperty::SupportedLock),
|
||||
DavProperty::WebDav(WebDavProperty::CurrentUserPrincipal),
|
||||
DavProperty::WebDav(WebDavProperty::GetContentLanguage),
|
||||
DavProperty::WebDav(WebDavProperty::GetContentLength),
|
||||
DavProperty::WebDav(WebDavProperty::GetContentType),
|
||||
];
|
||||
|
||||
pub fn is_all_prop(&self) -> bool {
|
||||
matches!(
|
||||
self,
|
||||
DavProperty::WebDav(WebDavProperty::CreationDate)
|
||||
| DavProperty::WebDav(WebDavProperty::DisplayName)
|
||||
| DavProperty::WebDav(WebDavProperty::GetETag)
|
||||
| DavProperty::WebDav(WebDavProperty::GetLastModified)
|
||||
| DavProperty::WebDav(WebDavProperty::ResourceType)
|
||||
| DavProperty::WebDav(WebDavProperty::LockDiscovery)
|
||||
| DavProperty::WebDav(WebDavProperty::SupportedLock)
|
||||
| DavProperty::WebDav(WebDavProperty::CurrentUserPrincipal)
|
||||
| DavProperty::WebDav(WebDavProperty::GetContentLanguage)
|
||||
| DavProperty::WebDav(WebDavProperty::GetContentLength)
|
||||
| DavProperty::WebDav(WebDavProperty::GetContentType)
|
||||
| DavProperty::DeadProperty(_)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl ReportSet {
|
||||
pub fn calendar() -> Vec<ReportSet> {
|
||||
vec![
|
||||
ReportSet::SyncCollection,
|
||||
ReportSet::AclPrincipalPropSet,
|
||||
ReportSet::PrincipalMatch,
|
||||
ReportSet::ExpandProperty,
|
||||
ReportSet::CalendarQuery,
|
||||
ReportSet::CalendarMultiGet,
|
||||
ReportSet::FreeBusyQuery,
|
||||
]
|
||||
}
|
||||
|
||||
pub fn addressbook() -> Vec<ReportSet> {
|
||||
vec![
|
||||
ReportSet::SyncCollection,
|
||||
ReportSet::AclPrincipalPropSet,
|
||||
ReportSet::PrincipalMatch,
|
||||
ReportSet::ExpandProperty,
|
||||
ReportSet::AddressbookQuery,
|
||||
ReportSet::AddressbookMultiGet,
|
||||
]
|
||||
}
|
||||
|
||||
pub fn file() -> Vec<ReportSet> {
|
||||
vec![
|
||||
ReportSet::SyncCollection,
|
||||
ReportSet::AclPrincipalPropSet,
|
||||
ReportSet::PrincipalMatch,
|
||||
]
|
||||
}
|
||||
|
||||
pub fn principal() -> Vec<ReportSet> {
|
||||
vec![
|
||||
ReportSet::PrincipalPropertySearch,
|
||||
ReportSet::PrincipalSearchPropertySet,
|
||||
ReportSet::PrincipalMatch,
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,295 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use super::{
|
||||
Collation, MatchType,
|
||||
property::{DavProperty, DavValue, LockScope, LockType},
|
||||
response::Ace,
|
||||
};
|
||||
use crate::{Condition, Depth};
|
||||
use calcard::{
|
||||
icalendar::{ICalendarComponentType, ICalendarParameterName, ICalendarProperty},
|
||||
vcard::{VCardParameterName, VCardProperty},
|
||||
};
|
||||
use types::{
|
||||
TimeRange,
|
||||
dead_property::{ArchivedDeadProperty, ArchivedDeadPropertyTag, DeadElementTag, DeadProperty},
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Default)]
|
||||
#[cfg_attr(test, derive(serde::Serialize, serde::Deserialize))]
|
||||
#[cfg_attr(test, serde(tag = "type", content = "data"))]
|
||||
pub enum PropFind {
|
||||
#[default]
|
||||
PropName,
|
||||
AllProp(Vec<DavProperty>),
|
||||
Prop(Vec<DavProperty>),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
#[cfg_attr(test, derive(serde::Serialize, serde::Deserialize))]
|
||||
pub struct PropertyUpdate {
|
||||
pub set: Vec<DavPropertyValue>,
|
||||
pub remove: Vec<DavProperty>,
|
||||
pub set_first: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
#[cfg_attr(test, derive(serde::Serialize, serde::Deserialize))]
|
||||
pub struct DavPropertyValue {
|
||||
pub property: DavProperty,
|
||||
pub value: DavValue,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
#[cfg_attr(test, derive(serde::Serialize, serde::Deserialize))]
|
||||
pub struct MkCol {
|
||||
pub is_mkcalendar: bool,
|
||||
pub props: Vec<DavPropertyValue>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
#[cfg_attr(test, derive(serde::Serialize, serde::Deserialize))]
|
||||
pub struct LockInfo {
|
||||
pub lock_scope: LockScope,
|
||||
pub lock_type: LockType,
|
||||
pub owner: Option<DeadProperty>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
#[cfg_attr(test, derive(serde::Serialize, serde::Deserialize))]
|
||||
#[cfg_attr(test, serde(tag = "type"))]
|
||||
pub enum Report {
|
||||
AddressbookQuery(AddressbookQuery),
|
||||
AddressbookMultiGet(MultiGet),
|
||||
CalendarQuery(CalendarQuery),
|
||||
CalendarMultiGet(MultiGet),
|
||||
FreeBusyQuery(FreeBusyQuery),
|
||||
SyncCollection(SyncCollection),
|
||||
ExpandProperty(ExpandProperty),
|
||||
AclPrincipalPropSet(AclPrincipalPropSet),
|
||||
PrincipalMatch(PrincipalMatch),
|
||||
PrincipalPropertySearch(PrincipalPropertySearch),
|
||||
PrincipalSearchPropertySet,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
#[cfg_attr(test, derive(serde::Serialize, serde::Deserialize))]
|
||||
pub struct ExpandProperty {
|
||||
pub properties: Vec<ExpandPropertyItem>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
#[cfg_attr(test, derive(serde::Serialize, serde::Deserialize))]
|
||||
pub struct ExpandPropertyItem {
|
||||
pub property: DavProperty,
|
||||
pub depth: u32,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
#[cfg_attr(test, derive(serde::Serialize, serde::Deserialize))]
|
||||
pub struct AddressbookQuery {
|
||||
pub properties: PropFind,
|
||||
pub filters: Vec<Filter<(), VCardPropertyWithGroup, VCardParameterName>>,
|
||||
pub limit: Option<u32>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
#[cfg_attr(test, derive(serde::Serialize, serde::Deserialize))]
|
||||
pub struct VCardPropertyWithGroup {
|
||||
pub name: VCardProperty,
|
||||
pub group: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
#[cfg_attr(test, derive(serde::Serialize, serde::Deserialize))]
|
||||
pub struct CalendarQuery {
|
||||
pub properties: PropFind,
|
||||
pub filters:
|
||||
Vec<Filter<Vec<ICalendarComponentType>, ICalendarProperty, ICalendarParameterName>>,
|
||||
pub timezone: Timezone,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
#[cfg_attr(test, derive(serde::Serialize, serde::Deserialize))]
|
||||
#[cfg_attr(test, serde(tag = "type"))]
|
||||
pub enum Timezone {
|
||||
Name(String),
|
||||
Id(String),
|
||||
None,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
#[cfg_attr(test, derive(serde::Serialize, serde::Deserialize))]
|
||||
pub struct FreeBusyQuery {
|
||||
pub range: Option<TimeRange>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
#[cfg_attr(test, derive(serde::Serialize, serde::Deserialize))]
|
||||
pub struct MultiGet {
|
||||
pub properties: PropFind,
|
||||
pub hrefs: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
#[cfg_attr(test, derive(serde::Serialize, serde::Deserialize))]
|
||||
pub struct SyncCollection {
|
||||
pub sync_token: Option<String>,
|
||||
pub properties: PropFind,
|
||||
pub depth: Depth,
|
||||
pub limit: Option<u32>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||
#[cfg_attr(test, derive(serde::Serialize, serde::Deserialize))]
|
||||
#[cfg_attr(test, serde(tag = "type"))]
|
||||
pub enum Filter<A, B, C> {
|
||||
AnyOf,
|
||||
AllOf,
|
||||
Component {
|
||||
comp: A,
|
||||
op: FilterOp,
|
||||
},
|
||||
Property {
|
||||
comp: A,
|
||||
prop: B,
|
||||
op: FilterOp,
|
||||
},
|
||||
Parameter {
|
||||
comp: A,
|
||||
prop: B,
|
||||
param: C,
|
||||
op: FilterOp,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||
#[cfg_attr(test, derive(serde::Serialize, serde::Deserialize))]
|
||||
#[cfg_attr(test, serde(tag = "type", content = "data"))]
|
||||
pub enum FilterOp {
|
||||
Exists,
|
||||
Undefined,
|
||||
TimeRange(TimeRange),
|
||||
TextMatch(TextMatch),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||
#[cfg_attr(test, derive(serde::Serialize, serde::Deserialize))]
|
||||
#[cfg_attr(test, serde(tag = "type"))]
|
||||
pub struct TextMatch {
|
||||
pub match_type: MatchType,
|
||||
pub value: String,
|
||||
pub collation: Collation,
|
||||
pub negate: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
#[cfg_attr(test, derive(serde::Serialize, serde::Deserialize))]
|
||||
pub struct Acl {
|
||||
pub aces: Vec<Ace>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
#[cfg_attr(test, derive(serde::Serialize, serde::Deserialize))]
|
||||
pub struct AclPrincipalPropSet {
|
||||
pub properties: Vec<DavProperty>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
#[cfg_attr(test, derive(serde::Serialize, serde::Deserialize))]
|
||||
pub struct PrincipalMatch {
|
||||
pub principal_properties: PrincipalMatchProperties,
|
||||
pub properties: Vec<DavProperty>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
#[cfg_attr(test, derive(serde::Serialize, serde::Deserialize))]
|
||||
pub enum PrincipalMatchProperties {
|
||||
Properties(Vec<DavProperty>),
|
||||
Self_,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
#[cfg_attr(test, derive(serde::Serialize, serde::Deserialize))]
|
||||
pub struct PrincipalPropertySearch {
|
||||
pub property_search: Vec<PropertySearch>,
|
||||
pub properties: Vec<DavProperty>,
|
||||
pub apply_to_principal_collection_set: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
#[cfg_attr(test, derive(serde::Serialize, serde::Deserialize))]
|
||||
pub struct PropertySearch {
|
||||
pub property: DavProperty,
|
||||
pub match_: String,
|
||||
}
|
||||
|
||||
impl PropertyUpdate {
|
||||
pub fn has_changes(&self) -> bool {
|
||||
!self.set.is_empty() || !self.remove.is_empty()
|
||||
}
|
||||
}
|
||||
|
||||
impl FreeBusyQuery {
|
||||
pub fn new(start: i64, end: i64) -> Self {
|
||||
FreeBusyQuery {
|
||||
range: Some(TimeRange { start, end }),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub trait DavDeadProperty {
|
||||
fn to_dav_values(&self, output: &mut Vec<DavPropertyValue>);
|
||||
}
|
||||
|
||||
impl DavDeadProperty for ArchivedDeadProperty {
|
||||
fn to_dav_values(&self, output: &mut Vec<DavPropertyValue>) {
|
||||
let mut depth: u32 = 0;
|
||||
let mut tags = Vec::new();
|
||||
let mut tag_start = None;
|
||||
|
||||
for tag in self.0.iter() {
|
||||
match tag {
|
||||
ArchivedDeadPropertyTag::ElementStart(start) => {
|
||||
if depth == 0 {
|
||||
tag_start = Some(DeadElementTag::from(start));
|
||||
} else {
|
||||
tags.push(tag.into());
|
||||
}
|
||||
|
||||
depth += 1;
|
||||
}
|
||||
ArchivedDeadPropertyTag::ElementEnd => {
|
||||
depth = depth.saturating_sub(1);
|
||||
|
||||
if depth > 0 {
|
||||
tags.push(tag.into());
|
||||
} else if let Some(tag_start) = tag_start.take() {
|
||||
output.push(DavPropertyValue::new(
|
||||
DavProperty::DeadProperty(tag_start),
|
||||
DavValue::DeadProperty(DeadProperty(std::mem::take(&mut tags))),
|
||||
));
|
||||
}
|
||||
}
|
||||
ArchivedDeadPropertyTag::Text(_) => {
|
||||
if tag_start.is_some() {
|
||||
tags.push(tag.into());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Condition<'_> {
|
||||
pub fn is_none_match(&self) -> bool {
|
||||
match self {
|
||||
Condition::ETag { is_not, .. } | Condition::Exists { is_not } => *is_not,
|
||||
Condition::StateToken { .. } => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,430 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use super::{
|
||||
Namespaces,
|
||||
property::{DavProperty, Privilege},
|
||||
request::{DavPropertyValue, Filter},
|
||||
};
|
||||
use calcard::{
|
||||
icalendar::{ICalendarComponentType, ICalendarParameterName, ICalendarProperty},
|
||||
vcard::{VCardParameterName, VCardProperty},
|
||||
};
|
||||
use hyper::StatusCode;
|
||||
use std::{borrow::Cow, fmt::Display};
|
||||
|
||||
pub struct MultiStatus {
|
||||
pub namespaces: Namespaces,
|
||||
pub response: List<Response>,
|
||||
pub response_description: Option<ResponseDescription>,
|
||||
pub sync_token: Option<SyncToken>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
#[cfg_attr(test, derive(serde::Serialize, serde::Deserialize))]
|
||||
pub struct Response {
|
||||
pub href: Href,
|
||||
pub typ: ResponseType,
|
||||
pub error: Option<Condition>,
|
||||
pub response_description: Option<ResponseDescription>,
|
||||
pub location: Option<Location>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
#[cfg_attr(test, derive(serde::Serialize, serde::Deserialize))]
|
||||
pub enum ResponseType {
|
||||
PropStat(List<PropStat>),
|
||||
Status { href: List<Href>, status: Status },
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
#[repr(transparent)]
|
||||
pub struct Status(pub StatusCode);
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
#[cfg_attr(test, derive(serde::Serialize, serde::Deserialize))]
|
||||
#[repr(transparent)]
|
||||
pub struct Location(pub Href);
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
#[cfg_attr(test, derive(serde::Serialize, serde::Deserialize))]
|
||||
#[repr(transparent)]
|
||||
pub struct ResponseDescription(pub String);
|
||||
|
||||
#[repr(transparent)]
|
||||
pub struct SyncToken(pub String);
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash, Default)]
|
||||
#[cfg_attr(test, derive(serde::Serialize, serde::Deserialize))]
|
||||
#[repr(transparent)]
|
||||
pub struct Href(pub String);
|
||||
|
||||
#[derive(Debug, Default, Clone, PartialEq, Eq, Hash)]
|
||||
#[cfg_attr(test, derive(serde::Serialize, serde::Deserialize))]
|
||||
#[repr(transparent)]
|
||||
pub struct List<T: Display>(pub Vec<T>);
|
||||
|
||||
pub struct MkColResponse {
|
||||
pub namespaces: Namespaces,
|
||||
pub propstat: List<PropStat>,
|
||||
pub mkcalendar: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
#[cfg_attr(test, derive(serde::Serialize, serde::Deserialize))]
|
||||
pub struct PropStat {
|
||||
pub prop: Prop,
|
||||
pub status: Status,
|
||||
pub error: Option<Condition>,
|
||||
pub response_description: Option<ResponseDescription>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
#[cfg_attr(test, derive(serde::Serialize, serde::Deserialize))]
|
||||
#[repr(transparent)]
|
||||
pub struct Prop(pub List<DavPropertyValue>);
|
||||
|
||||
pub struct PropResponse {
|
||||
pub namespaces: Namespaces,
|
||||
pub properties: List<DavPropertyValue>,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct ScheduleResponse {
|
||||
pub items: List<ScheduleResponseItem>,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct ScheduleResponseItem {
|
||||
pub recipient: Href,
|
||||
pub request_status: Cow<'static, str>,
|
||||
pub calendar_data: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
#[cfg_attr(test, derive(serde::Serialize, serde::Deserialize))]
|
||||
pub struct SupportedPrivilege {
|
||||
pub privilege: Privilege,
|
||||
pub abstract_: bool,
|
||||
pub description: String,
|
||||
pub supported_privilege: List<SupportedPrivilege>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
#[cfg_attr(test, derive(serde::Serialize, serde::Deserialize))]
|
||||
pub struct Ace {
|
||||
pub principal: Principal,
|
||||
pub invert: bool,
|
||||
pub grant_deny: GrantDeny,
|
||||
pub protected: bool,
|
||||
pub inherited: Option<Href>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
#[cfg_attr(test, derive(serde::Serialize, serde::Deserialize))]
|
||||
pub enum GrantDeny {
|
||||
Grant(List<Privilege>),
|
||||
Deny(List<Privilege>),
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Clone, PartialEq, Eq)]
|
||||
#[cfg_attr(test, derive(serde::Serialize, serde::Deserialize))]
|
||||
pub enum Principal {
|
||||
Href(Href),
|
||||
Response(Response),
|
||||
All,
|
||||
#[default]
|
||||
Authenticated,
|
||||
Unauthenticated,
|
||||
Property(List<DavPropertyValue>),
|
||||
Self_,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Clone, PartialEq, Eq)]
|
||||
#[cfg_attr(test, derive(serde::Serialize, serde::Deserialize))]
|
||||
pub struct AclRestrictions {
|
||||
pub grant_only: bool,
|
||||
pub no_invert: bool,
|
||||
pub deny_before_grant: bool,
|
||||
pub required_principal: Option<RequiredPrincipal>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
#[cfg_attr(test, derive(serde::Serialize, serde::Deserialize))]
|
||||
pub enum RequiredPrincipal {
|
||||
All,
|
||||
Authenticated,
|
||||
Unauthenticated,
|
||||
Self_,
|
||||
Href(List<Href>),
|
||||
Property(Vec<DavPropertyValue>),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
#[cfg_attr(test, derive(serde::Serialize, serde::Deserialize))]
|
||||
pub struct PrincipalSearchPropertySet {
|
||||
pub namespaces: Namespaces,
|
||||
pub properties: List<PrincipalSearchProperty>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
#[cfg_attr(test, derive(serde::Serialize, serde::Deserialize))]
|
||||
pub struct PrincipalSearchProperty {
|
||||
pub name: DavProperty,
|
||||
pub description: String,
|
||||
}
|
||||
|
||||
pub struct ErrorResponse {
|
||||
pub namespaces: Namespaces,
|
||||
pub error: Condition,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||
#[cfg_attr(test, derive(serde::Serialize, serde::Deserialize))]
|
||||
pub enum Condition {
|
||||
Base(BaseCondition),
|
||||
Cal(CalCondition),
|
||||
Card(CardCondition),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||
#[cfg_attr(test, derive(serde::Serialize, serde::Deserialize))]
|
||||
pub enum BaseCondition {
|
||||
NoConflictingLock(List<Href>),
|
||||
LockTokenSubmitted(List<Href>),
|
||||
LockTokenMatchesRequestUri,
|
||||
CannotModifyProtectedProperty,
|
||||
NoExternalEntities,
|
||||
PreservedLiveProperties,
|
||||
PropFindFiniteDepth,
|
||||
ResourceMustBeNull,
|
||||
NeedPrivileges(List<Resource>),
|
||||
NoAceConflict,
|
||||
NoProtectedAceConflict,
|
||||
NoInheritedAceConflict,
|
||||
LimitedNumberOfAces,
|
||||
DenyBeforeGrant,
|
||||
GrantOnly,
|
||||
NoInvert,
|
||||
NoAbstract,
|
||||
NotSupportedPrivilege,
|
||||
MissingRequiredPrincipal,
|
||||
RecognizedPrincipal,
|
||||
AllowedPrincipal,
|
||||
NumberOfMatchesWithinLimit,
|
||||
QuotaNotExceeded,
|
||||
ValidResourceType,
|
||||
ValidSyncToken,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||
#[cfg_attr(test, derive(serde::Serialize, serde::Deserialize))]
|
||||
pub struct Resource {
|
||||
pub href: Href,
|
||||
pub privilege: Privilege,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||
#[cfg_attr(test, derive(serde::Serialize, serde::Deserialize))]
|
||||
pub enum CalCondition {
|
||||
CalendarCollectionLocationOk,
|
||||
ValidCalendarData,
|
||||
ValidFilter,
|
||||
ValidCalendarObjectResource,
|
||||
ValidTimezone,
|
||||
NoUidConflict(Href),
|
||||
InitializeCalendarCollection,
|
||||
SupportedCalendarData,
|
||||
SupportedFilter(
|
||||
Vec<Filter<Vec<ICalendarComponentType>, ICalendarProperty, ICalendarParameterName>>,
|
||||
),
|
||||
SupportedCollation(String),
|
||||
SupportedCalendarComponent,
|
||||
MinDateTime,
|
||||
MaxDateTime,
|
||||
MaxResourceSize(u32),
|
||||
MaxInstances,
|
||||
MaxAttendeesPerInstance,
|
||||
UniqueSchedulingObjectResource(Href),
|
||||
SameOrganizerInAllComponents,
|
||||
AllowedOrganizerObjectChange,
|
||||
AllowedAttendeeObjectChange,
|
||||
DefaultCalendarNeeded,
|
||||
ValidScheduleDefaultCalendarUrl,
|
||||
ValidSchedulingMessage,
|
||||
ValidOrganizer,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||
#[cfg_attr(test, derive(serde::Serialize, serde::Deserialize))]
|
||||
pub enum CardCondition {
|
||||
SupportedAddressData,
|
||||
SupportedAddressDataConversion,
|
||||
SupportedFilter(Vec<Filter<(), VCardProperty, VCardParameterName>>),
|
||||
SupportedCollation(String),
|
||||
ValidAddressData,
|
||||
NoUidConflict(Href),
|
||||
MaxResourceSize(u32),
|
||||
AddressBookCollectionLocationOk,
|
||||
}
|
||||
|
||||
impl BaseCondition {
|
||||
pub fn status(&self) -> StatusCode {
|
||||
match self {
|
||||
BaseCondition::NoConflictingLock(_) => StatusCode::LOCKED,
|
||||
BaseCondition::CannotModifyProtectedProperty => StatusCode::FORBIDDEN,
|
||||
BaseCondition::LockTokenSubmitted(_) => StatusCode::LOCKED,
|
||||
BaseCondition::LockTokenMatchesRequestUri => StatusCode::CONFLICT,
|
||||
BaseCondition::NoExternalEntities => StatusCode::FORBIDDEN,
|
||||
BaseCondition::PreservedLiveProperties => StatusCode::CONFLICT,
|
||||
BaseCondition::PropFindFiniteDepth => StatusCode::FORBIDDEN,
|
||||
BaseCondition::ResourceMustBeNull => StatusCode::CONFLICT,
|
||||
BaseCondition::NeedPrivileges(_) => StatusCode::FORBIDDEN,
|
||||
BaseCondition::NumberOfMatchesWithinLimit => StatusCode::FORBIDDEN,
|
||||
_ => StatusCode::FORBIDDEN,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<String> for Href {
|
||||
fn from(value: String) -> Self {
|
||||
Self(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&str> for Href {
|
||||
fn from(value: &str) -> Self {
|
||||
Self(value.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl MultiStatus {
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.response.0.is_empty()
|
||||
}
|
||||
}
|
||||
|
||||
impl BaseCondition {
|
||||
pub fn display_name(&self) -> &'static str {
|
||||
match self {
|
||||
BaseCondition::NoConflictingLock(_) => "NoConflictingLock",
|
||||
BaseCondition::CannotModifyProtectedProperty => "CannotModifyProtectedProperty",
|
||||
BaseCondition::LockTokenSubmitted(_) => "LockTokenSubmitted",
|
||||
BaseCondition::LockTokenMatchesRequestUri => "LockTokenMatchesRequestUri",
|
||||
BaseCondition::NoExternalEntities => "NoExternalEntities",
|
||||
BaseCondition::PreservedLiveProperties => "PreservedLiveProperties",
|
||||
BaseCondition::PropFindFiniteDepth => "PropFindFiniteDepth",
|
||||
BaseCondition::ResourceMustBeNull => "ResourceMustBeNull",
|
||||
BaseCondition::NeedPrivileges(_) => "NeedPrivileges",
|
||||
BaseCondition::NoAceConflict => "NoAceConflict",
|
||||
BaseCondition::NoProtectedAceConflict => "NoProtectedAceConflict",
|
||||
BaseCondition::NoInheritedAceConflict => "NoInheritedAceConflict",
|
||||
BaseCondition::LimitedNumberOfAces => "LimitedNumberOfAces",
|
||||
BaseCondition::DenyBeforeGrant => "DenyBeforeGrant",
|
||||
BaseCondition::GrantOnly => "GrantOnly",
|
||||
BaseCondition::NoInvert => "NoInvert",
|
||||
BaseCondition::NoAbstract => "NoAbstract",
|
||||
BaseCondition::NotSupportedPrivilege => "NotSupportedPrivilege",
|
||||
BaseCondition::MissingRequiredPrincipal => "MissingRequiredPrincipal",
|
||||
BaseCondition::RecognizedPrincipal => "RecognizedPrincipal",
|
||||
BaseCondition::AllowedPrincipal => "AllowedPrincipal",
|
||||
BaseCondition::NumberOfMatchesWithinLimit => "NumberOfMatchesWithinLimit",
|
||||
BaseCondition::QuotaNotExceeded => "QuotaNotExceeded",
|
||||
BaseCondition::ValidResourceType => "ValidResourceType",
|
||||
BaseCondition::ValidSyncToken => "ValidSyncToken",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl CalCondition {
|
||||
pub fn display_name(&self) -> &'static str {
|
||||
match self {
|
||||
CalCondition::CalendarCollectionLocationOk => "CalendarCollectionLocationOk",
|
||||
CalCondition::ValidCalendarData => "ValidCalendarData",
|
||||
CalCondition::ValidFilter => "ValidFilter",
|
||||
CalCondition::ValidCalendarObjectResource => "ValidCalendarObjectResource",
|
||||
CalCondition::ValidTimezone => "ValidTimezone",
|
||||
CalCondition::NoUidConflict(_) => "NoUidConflict",
|
||||
CalCondition::InitializeCalendarCollection => "InitializeCalendarCollection",
|
||||
CalCondition::SupportedCalendarData => "SupportedCalendarData",
|
||||
CalCondition::SupportedFilter(_) => "SupportedFilter",
|
||||
CalCondition::SupportedCollation(_) => "SupportedCollation",
|
||||
CalCondition::MinDateTime => "MinDateTime",
|
||||
CalCondition::MaxDateTime => "MaxDateTime",
|
||||
CalCondition::MaxResourceSize(_) => "MaxResourceSize",
|
||||
CalCondition::MaxInstances => "MaxInstances",
|
||||
CalCondition::MaxAttendeesPerInstance => "MaxAttendeesPerInstance",
|
||||
CalCondition::UniqueSchedulingObjectResource(_) => "UniqueSchedulingObjectResource",
|
||||
CalCondition::SameOrganizerInAllComponents => "SameOrganizerInAllComponents",
|
||||
CalCondition::AllowedOrganizerObjectChange => "AllowedOrganizerObjectChange",
|
||||
CalCondition::AllowedAttendeeObjectChange => "AllowedAttendeeObjectChange",
|
||||
CalCondition::DefaultCalendarNeeded => "DefaultCalendarNeeded",
|
||||
CalCondition::ValidScheduleDefaultCalendarUrl => "ValidScheduleDefaultCalendarUrl",
|
||||
CalCondition::ValidSchedulingMessage => "ValidSchedulingMessage",
|
||||
CalCondition::ValidOrganizer => "ValidOrganizer",
|
||||
CalCondition::SupportedCalendarComponent => "SupportedCalendarComponent",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl CardCondition {
|
||||
pub fn display_name(&self) -> &'static str {
|
||||
match self {
|
||||
CardCondition::SupportedAddressData => "SupportedAddressData",
|
||||
CardCondition::SupportedAddressDataConversion => "SupportedAddressDataConversion",
|
||||
CardCondition::SupportedFilter(_) => "SupportedFilter",
|
||||
CardCondition::SupportedCollation(_) => "SupportedCollation",
|
||||
CardCondition::ValidAddressData => "ValidAddressData",
|
||||
CardCondition::NoUidConflict(_) => "NoUidConflict",
|
||||
CardCondition::MaxResourceSize(_) => "MaxResourceSize",
|
||||
CardCondition::AddressBookCollectionLocationOk => "AddressBookCollectionLocationOk",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Condition {
|
||||
pub fn display_name(&self) -> &'static str {
|
||||
match self {
|
||||
Condition::Base(base) => base.display_name(),
|
||||
Condition::Cal(cal) => cal.display_name(),
|
||||
Condition::Card(card) => card.display_name(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod serde_impl {
|
||||
use super::Status;
|
||||
use hyper::StatusCode;
|
||||
use serde::{Deserialize, Deserializer, Serialize, Serializer};
|
||||
|
||||
impl Serialize for Status {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
// Serialize the status code as a u16
|
||||
serializer.serialize_u16(self.0.as_u16())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for Status {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
// Deserialize as u16
|
||||
let status_value = u16::deserialize(deserializer)?;
|
||||
|
||||
// Convert u16 to StatusCode
|
||||
let status_code = StatusCode::try_from(status_value).map_err(|_| {
|
||||
serde::de::Error::custom(format!("Invalid status code: {}", status_value))
|
||||
})?;
|
||||
|
||||
Ok(Status(status_code))
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user