Files
Stalwart/crates/dav/src/lib.rs
2025-08-18 11:44:58 +01:00

260 lines
7.3 KiB
Rust

/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <hello@stalw.art>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
#![warn(clippy::large_futures)]
pub mod calendar;
pub mod card;
pub mod common;
pub mod file;
pub mod principal;
pub mod request;
use dav_proto::schema::{
request::DavPropertyValue,
response::{Condition, List, Prop, PropStat, ResponseDescription, Status},
};
use groupware::{DavResourceName, RFC_3986};
use hyper::{Method, StatusCode};
use std::borrow::Cow;
use store::ahash::AHashMap;
pub(crate) type Result<T> = std::result::Result<T, DavError>;
#[derive(Debug, Clone, Copy)]
pub enum DavMethod {
GET,
PUT,
POST,
DELETE,
HEAD,
PATCH,
PROPFIND,
PROPPATCH,
REPORT,
MKCOL,
MKCALENDAR,
COPY,
MOVE,
LOCK,
UNLOCK,
OPTIONS,
ACL,
}
impl From<DavMethod> for trc::WebDavEvent {
fn from(value: DavMethod) -> Self {
match value {
DavMethod::GET => trc::WebDavEvent::Get,
DavMethod::PUT => trc::WebDavEvent::Put,
DavMethod::POST => trc::WebDavEvent::Post,
DavMethod::DELETE => trc::WebDavEvent::Delete,
DavMethod::HEAD => trc::WebDavEvent::Head,
DavMethod::PATCH => trc::WebDavEvent::Patch,
DavMethod::PROPFIND => trc::WebDavEvent::Propfind,
DavMethod::PROPPATCH => trc::WebDavEvent::Proppatch,
DavMethod::REPORT => trc::WebDavEvent::Report,
DavMethod::MKCOL => trc::WebDavEvent::Mkcol,
DavMethod::MKCALENDAR => trc::WebDavEvent::Mkcalendar,
DavMethod::COPY => trc::WebDavEvent::Copy,
DavMethod::MOVE => trc::WebDavEvent::Move,
DavMethod::LOCK => trc::WebDavEvent::Lock,
DavMethod::UNLOCK => trc::WebDavEvent::Unlock,
DavMethod::OPTIONS => trc::WebDavEvent::Options,
DavMethod::ACL => trc::WebDavEvent::Acl,
}
}
}
pub(crate) enum DavError {
Parse(dav_proto::parser::Error),
Internal(trc::Error),
Condition(DavErrorCondition),
Code(StatusCode),
}
struct DavErrorCondition {
pub code: StatusCode,
pub condition: Condition,
pub details: Option<String>,
}
impl From<DavErrorCondition> for DavError {
fn from(value: DavErrorCondition) -> Self {
DavError::Condition(value)
}
}
impl From<Condition> for DavErrorCondition {
fn from(value: Condition) -> Self {
DavErrorCondition {
code: StatusCode::CONFLICT,
condition: value,
details: None,
}
}
}
impl DavErrorCondition {
pub fn new(code: StatusCode, condition: impl Into<Condition>) -> Self {
DavErrorCondition {
code,
condition: condition.into(),
details: None,
}
}
pub fn with_details(mut self, details: impl Into<String>) -> Self {
self.details = Some(details.into());
self
}
}
impl DavMethod {
pub fn parse(method: &Method) -> Option<Self> {
match *method {
Method::GET => Some(DavMethod::GET),
Method::PUT => Some(DavMethod::PUT),
Method::DELETE => Some(DavMethod::DELETE),
Method::OPTIONS => Some(DavMethod::OPTIONS),
Method::POST => Some(DavMethod::POST),
Method::PATCH => Some(DavMethod::PATCH),
Method::HEAD => Some(DavMethod::HEAD),
_ => {
hashify::tiny_map!(method.as_str().as_bytes(),
"PROPFIND" => DavMethod::PROPFIND,
"PROPPATCH" => DavMethod::PROPPATCH,
"REPORT" => DavMethod::REPORT,
"MKCOL" => DavMethod::MKCOL,
"MKCALENDAR" => DavMethod::MKCALENDAR,
"COPY" => DavMethod::COPY,
"MOVE" => DavMethod::MOVE,
"LOCK" => DavMethod::LOCK,
"UNLOCK" => DavMethod::UNLOCK,
"ACL" => DavMethod::ACL
)
}
}
}
#[inline]
pub fn has_body(self) -> bool {
matches!(
self,
DavMethod::PUT
| DavMethod::POST
| DavMethod::PATCH
| DavMethod::PROPPATCH
| DavMethod::PROPFIND
| DavMethod::REPORT
| DavMethod::LOCK
| DavMethod::ACL
| DavMethod::MKCALENDAR
)
}
}
#[derive(Debug, Default)]
pub struct PropStatBuilder {
propstats: AHashMap<(StatusCode, Option<Condition>, Option<String>), Vec<DavPropertyValue>>,
}
impl PropStatBuilder {
pub fn insert_ok(&mut self, prop: impl Into<DavPropertyValue>) -> &mut Self {
self.propstats
.entry((StatusCode::OK, None, None))
.or_default()
.push(prop.into());
self
}
pub fn insert_with_status(
&mut self,
prop: impl Into<DavPropertyValue>,
status: StatusCode,
) -> &mut Self {
self.propstats
.entry((status, None, None))
.or_default()
.push(prop.into());
self
}
pub fn insert_error_with_description(
&mut self,
prop: impl Into<DavPropertyValue>,
status: StatusCode,
description: impl Into<String>,
) -> &mut Self {
self.propstats
.entry((status, None, Some(description.into())))
.or_default()
.push(prop.into());
self
}
pub fn insert_precondition_failed(
&mut self,
prop: impl Into<DavPropertyValue>,
status: StatusCode,
condition: impl Into<Condition>,
) -> &mut Self {
self.propstats
.entry((status, Some(condition.into()), None))
.or_default()
.push(prop.into());
self
}
pub fn insert_precondition_failed_with_description(
&mut self,
prop: impl Into<DavPropertyValue>,
status: StatusCode,
condition: impl Into<Condition>,
description: impl Into<String>,
) -> &mut Self {
self.propstats
.entry((status, Some(condition.into()), Some(description.into())))
.or_default()
.push(prop.into());
self
}
pub fn build(self) -> Vec<PropStat> {
self.propstats
.into_iter()
.map(|((status, condition, description), props)| PropStat {
prop: Prop(List(props)),
status: Status(status),
error: condition,
response_description: description.map(ResponseDescription),
})
.collect()
}
}
// Workaround for Apple bug with missing percent encoding in paths
pub(crate) fn fix_percent_encoding(path: &'_ str) -> Cow<'_, str> {
let (parent, name) = if let Some((parent, name)) = path.rsplit_once('/') {
(Some(parent), name)
} else {
(None, path)
};
for &ch in name.as_bytes() {
if !matches!(ch, b'0'..=b'9' | b'A'..=b'Z' | b'a'..=b'z' | b'-' | b'.' | b'_' | b'~' | b'%')
{
let name = percent_encoding::percent_encode(name.as_bytes(), RFC_3986);
return if let Some(parent) = parent {
Cow::Owned(format!("{parent}/{name}"))
} else {
Cow::Owned(name.to_string())
};
}
}
path.into()
}