use http::{StatusCode, Uri};
use libdav::{
PropertyName,
dav::{ListedResource, WebDavError},
names,
sd::BootstrapError,
};
use log::warn;
use crate::{
CollectionId, CollectionIdError, Error, ErrorKind, ItemKind, Result,
base::{CreateItemOptions, Item, ItemVersion},
property::Property,
};
pub(crate) fn dav_propname(
property: Property,
item_kind: ItemKind,
) -> Result<&'static PropertyName<'static, 'static>> {
match (property, item_kind) {
(Property::DisplayName, _) => Ok(&names::DISPLAY_NAME),
(Property::Description, ItemKind::Calendar) => Ok(&names::CALENDAR_DESCRIPTION),
(Property::Description, ItemKind::AddressBook) => Ok(&names::ADDRESSBOOK_DESCRIPTION),
(Property::Colour, ItemKind::Calendar) => Ok(&names::CALENDAR_COLOUR),
(Property::Order, ItemKind::Calendar) => Ok(&names::CALENDAR_ORDER),
(Property::Colour | Property::Order, ItemKind::AddressBook) => {
Err(ErrorKind::InvalidInput.error("property not valid for address books"))
}
}
}
#[derive(Clone, Copy, Debug, Default)]
pub enum CollectionIdSegment {
#[default]
Last,
SecondLast,
}
pub(crate) fn path_for_collection_in_home_set(home_set: &Uri, id: &str) -> String {
let mut path = match home_set.clone().into_parts().path_and_query {
Some(ref pq) => pq.path(),
None => "/",
}
.to_owned();
if let Some(index) = path.find('?') {
path.truncate(index + 1);
}
if !path.ends_with('/') {
path.push('/');
}
path.push_str(id);
path.push('/');
path
}
pub(crate) fn collection_href_for_item(item_href: &str) -> Result<&str> {
let mut parts = item_href.rsplitn(2, '/');
let _resource = parts.next();
let collection_href = parts
.next()
.ok_or_else(|| Error::from(ErrorKind::InvalidInput))?;
Ok(collection_href)
}
#[derive(Debug, thiserror::Error)]
pub(crate) enum CollectionIdForHrefError {
#[error(transparent)]
CollectionIdError(CollectionIdError),
#[error("missing path segment in href")]
MissingSegment,
}
#[inline]
pub(crate) fn collection_id_for_href(
href: &str,
collection_id_segment: CollectionIdSegment,
) -> Result<CollectionId, CollectionIdForHrefError> {
let mut rsplit = href
.trim_matches('/') .rsplit('/');
match collection_id_segment {
CollectionIdSegment::Last => rsplit
.next()
.expect("rsplit always returns at least one item")
.parse(),
CollectionIdSegment::SecondLast => rsplit
.nth(1)
.ok_or(CollectionIdForHrefError::MissingSegment)?
.parse(),
}
.map_err(CollectionIdForHrefError::CollectionIdError)
}
pub(crate) fn parse_list_items(response: Vec<ListedResource>) -> Result<Vec<ItemVersion>> {
match response.as_slice() {
[one] if one.status == Some(StatusCode::NOT_FOUND) => {
return Err(ErrorKind::DoesNotExist.into());
}
_ => {}
}
response
.into_iter()
.filter_map(|r| match (r.status, r.etag) {
(Some(StatusCode::OK) | None, Some(etag)) => Some(Ok(ItemVersion {
href: r.href,
etag: etag.into(),
})),
(Some(status), _) => {
warn!("Got status code {status} for item: {}.", r.href);
None
}
(_, None) => Some(Err(ErrorKind::InvalidData.error("missing Etag"))),
})
.collect()
}
impl From<BootstrapError> for Error {
fn from(value: BootstrapError) -> Self {
ErrorKind::Uncategorised.error(value)
}
}
impl<E: std::fmt::Debug + std::fmt::Display + Send + Sync + 'static>
From<libdav::dav::WebDavError<E>> for Error
{
fn from(value: libdav::dav::WebDavError<E>) -> Self {
match value {
WebDavError::BadStatusCode(StatusCode::NOT_FOUND) => {
ErrorKind::DoesNotExist.error(value)
}
WebDavError::BadStatusCode(StatusCode::FORBIDDEN) => {
ErrorKind::AccessDenied.error(value)
}
WebDavError::PreconditionFailed(precondition) => {
ErrorKind::PreconditionFailed.error(precondition)
}
err => ErrorKind::Uncategorised.error(err), }
}
}
pub(crate) fn join_hrefs(collection_href: &str, item_href: &str) -> String {
if item_href.starts_with('/') {
return item_href.to_string();
}
let mut href = collection_href
.strip_suffix('/')
.unwrap_or(collection_href)
.to_string();
href.push('/');
href.push_str(item_href);
href
}
fn is_valid_resource_name(name: &str) -> bool {
if name.is_empty() || name == ".." {
return false;
}
name.chars().all(|c| {
!c.is_control()
&& !matches!(
c,
':' | '/' | '?' | '#' | '[' | ']' | '@' | '!' | '$' | '&' | '\'' | '(' | ')'
)
})
}
pub(crate) fn name_for_creation(collection: &str, item: &Item, opts: CreateItemOptions) -> String {
if let Some(name) = opts.resource_name
&& is_valid_resource_name(&name)
{
return join_hrefs(collection, &name);
}
if let Some(name) = item.uid()
&& is_valid_resource_name(&name)
{
return join_hrefs(collection, &name);
}
join_hrefs(collection, &item.hash().to_string())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_valid_names() {
assert!(is_valid_resource_name("example"));
assert!(is_valid_resource_name("example.ics"));
assert!(is_valid_resource_name("resource-1"));
assert!(is_valid_resource_name("resource_1"));
assert!(is_valid_resource_name("contact.vcf"));
assert!(is_valid_resource_name("doc.pdf"));
assert!(is_valid_resource_name(".hidden"));
assert!(is_valid_resource_name("-party.ics"));
assert!(is_valid_resource_name("my event.ics"));
assert!(is_valid_resource_name("file\\name")); assert!(is_valid_resource_name("file*")); }
#[test]
fn test_invalid_names() {
assert!(!is_valid_resource_name(""));
assert!(!is_valid_resource_name("folder/file.txt"));
assert!(!is_valid_resource_name("path/../file"));
assert!(!is_valid_resource_name("path//file"));
assert!(!is_valid_resource_name("file\tname"));
assert!(!is_valid_resource_name("file\nname"));
assert!(!is_valid_resource_name("file?query"));
assert!(!is_valid_resource_name("file#fragment"));
}
}