libdav 0.10.3

CalDAV and CardDAV client implementations.
Documentation
#![deny(clippy::pedantic)]
#![deny(clippy::unwrap_used)]
#![cfg_attr(not(test), deny(clippy::indexing_slicing))]
#![deny(rustdoc::broken_intra_doc_links)]
#![warn(missing_docs)]
// Copyright 2023-2024 Hugo Osvaldo Barrera
//
// SPDX-License-Identifier: ISC

//! CalDAV and CardDAV client implementations.
//!
//! See [`CalDavClient`] and [`CardDavClient`] as useful entry points.
//!
//! Both clients wrap a [`dav::WebDavClient`], and implement `Deref<Target = WebDavClient>`, so all
//! of `WebDavClient`'s associated functions for  are usable directly.
//!
//! # Bootstrapping and service discovery
//!
//! Clients support bootstrapping themselves using the service discovery, which is implemented in
//! the [`sd`] module. The [`CalDavClient::bootstrap_via_service_discovery`] and
//! [`CardDavClient::bootstrap_via_service_discovery`] functions are available as shortcuts to creating a new
//! client instance.
//!
//! The implementation does not validate DNSSEC signatures. Because of this, discovery must only be
//! used with a validating DNS resolver (as defined in [rfc4033][rfc4033]), or with domains served
//! from a local, trusted networks.
//!
//! [rfc4033]: https://www.rfc-editor.org/rfc/rfc4033
//!
//! # Uris and Hrefs
//!
//! An href is a path to a collection or resource in a WebDAV server. It is the path component of
//! the corresponding Url. Hrefs returned by this library are always encoded with
//! [`encoding::normalise_percent_encoded`].
//!
//! See the [`encoding`] module for more details on conventions on encoding different hrefs and
//! URLs.
//!
//! # Requests
//!
//! `libdav` uses a Requests API, where different common requests is modelled as a separate type.
//! Each type can prepare a request and parse a response, but is I/O agnostic (and therefore usable
//! with other I/O interfaces).
//!
//! See implementers of the [`requests::DavRequest`] trait for available requests.
//!
//! # Errors
//!
//! Errors returned by this crate expose clear details of the cause of the error, but also reflect
//! the internal implementation in great detail too. It is somewhat of an anti-pattern, and while
//! this crate is in a relatively mature state, the error types are subject to change.
//!
//! # Thanks
//!
//! Special thanks to the [NLnet foundation][nlnet] and the [NGI Zero Entrust program][ngi0] of the
//! European Commission, which helped secure funding for the work on [pimsync] and related projects
//! such a this one.
//!
//! [nlnet]: https://nlnet.nl/project/vdirsyncer/
//! [ngi0]: https://www.ngi.eu/ngi-projects/ngi-zero-entrust/
//! [pimsync]: https://git.sr.ht/~whynothugo/pimsync
//!
//! # See also
//!
//! The source code is currently hosted at <https://git.sr.ht/~whynothugo/libdav>.
//!
//! The [davcli](https://git.sr.ht/~whynothugo/davcli) command line tool provides a minimal
//! interface to CalDAV and CardDAV servers, and can serve as an example of a simple application
//! using this library.

use dav::RequestError;
use http::HeaderValue;
use http::StatusCode;

pub mod caldav;
pub mod carddav;
mod common;
pub mod dav;
pub mod encoding;
pub mod names;
pub mod requests;
pub mod sd;
pub mod xmlutils;

pub use caldav::CalDavClient;
pub use caldav::service_for_url as caldav_service_for_url;
pub use carddav::CardDavClient;
pub use carddav::service_for_url as carddav_service_for_url;
use roxmltree::ExpandedName;

/// A WebDAV property with a `namespace` and `name`.
///
/// See the [names] module for a variety of constants often used in CalDAV and CardDAV.
#[derive(Debug, PartialEq)]
pub struct PropertyName<'ns, 'name> {
    namespace: &'ns str,
    name: &'name str,
}

impl<'ns, 'name> PropertyName<'ns, 'name> {
    /// Create an property instance.
    #[must_use]
    pub const fn new(namespace: &'ns str, name: &'name str) -> PropertyName<'ns, 'name> {
        PropertyName { namespace, name }
    }
}

impl<'ns, 'name> PropertyName<'ns, 'name> {
    /// Returns the name of this property.
    #[must_use]
    pub fn name(&self) -> &'name str {
        self.name
    }

    /// Returns the namespace of this property.
    #[must_use]
    pub fn namespace(&self) -> &'ns str {
        self.namespace
    }
}

impl PartialEq<ExpandedName<'_, '_>> for PropertyName<'_, '_> {
    fn eq(&self, other: &ExpandedName<'_, '_>) -> bool {
        other.name() == self.name && other.namespace() == Some(self.namespace)
    }
}

impl PartialEq<PropertyName<'_, '_>> for ExpandedName<'_, '_> {
    fn eq(&self, other: &PropertyName<'_, '_>) -> bool {
        self.name() == other.name && self.namespace() == Some(other.namespace)
    }
}

/// WebDAV  precondition.
#[derive(thiserror::Error, Debug)]
#[error("ns={}, name={}", .0.namespace, .0.name)]
pub struct Precondition<'p>(PropertyName<'p, 'p>);

impl<'p> From<PropertyName<'p, 'p>> for Precondition<'p> {
    fn from(value: PropertyName<'p, 'p>) -> Precondition<'p> {
        Precondition(value)
    }
}

impl<'p> From<Precondition<'p>> for PropertyName<'p, 'p> {
    fn from(value: Precondition<'p>) -> PropertyName<'p, 'p> {
        value.0
    }
}

/// See [`FetchedResource`]
#[derive(Debug, PartialEq, Eq)]
pub struct FetchedResourceContent {
    /// Raw resource data, with lines separated by `\r\n`.
    pub data: String,
    /// The entity tag reflecting the version of the fetched resource.
    ///
    /// See: <https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/ETag>
    pub etag: String,
}

/// Parsed resource fetched from a server.
#[derive(Debug, PartialEq, Eq)]
pub struct FetchedResource {
    /// Absolute path to the resource in the server.
    ///
    /// Should be treated as an opaque string. Only reserved characters are percent-encoded.
    pub href: String,
    /// Contents of the resource if available, or the status code if unavailable.
    pub content: Result<FetchedResourceContent, StatusCode>,
}

/// Error type for capability checking requests.
///
/// Used by [`dav::CheckSupport`].
#[derive(thiserror::Error, Debug)]
pub enum CheckSupportError<E> {
    /// The `DAV` header was missing from the response received.
    #[error("DAV header missing from the response")]
    MissingHeader,

    /// The server does not advertise the queried capability.
    #[error("requested support not advertised by the server")]
    NotAdvertised,

    /// Failed to parse the `DAV` header as a UTF-8 string.
    #[error("DAV header is not a valid string: {0}")]
    HeaderNotAscii(#[from] http::header::ToStrError),

    /// Error sending HTTP request.
    #[error(transparent)]
    Request(#[from] RequestError<E>),

    /// The provided URL is not acceptable.
    #[error("invalid input URL: {0}")]
    InvalidInput(#[from] http::Error),

    /// Server returned a non-success status code.
    #[error("http request returned {0}")]
    BadStatusCode(http::StatusCode),
}

impl<E> From<StatusCode> for CheckSupportError<E> {
    fn from(status: StatusCode) -> Self {
        CheckSupportError::BadStatusCode(status)
    }
}

impl<E> From<dav::CheckSupportParseError> for CheckSupportError<E> {
    fn from(error: dav::CheckSupportParseError) -> Self {
        match error {
            dav::CheckSupportParseError::MissingHeader => CheckSupportError::MissingHeader,
            dav::CheckSupportParseError::NotAdvertised => CheckSupportError::NotAdvertised,
            dav::CheckSupportParseError::HeaderNotAscii(e) => CheckSupportError::HeaderNotAscii(e),
            dav::CheckSupportParseError::BadStatusCode(s) => CheckSupportError::BadStatusCode(s),
        }
    }
}

/// Resource type for an item.
///
/// This type requires further work and will likely change in future versions.
// TODO: Support unknown types too.
//       Keeping all the `String` instances can be inefficient when listing thousands of resources.
//       Perhaps de-duplicated strings?
// TODO: Maybe use an enum with common values as variants and an associated String for unknown types?
#[derive(Default, Debug, Clone, PartialEq, Eq)]
pub struct ResourceType {
    /// Resource is a WebDAV collection.
    pub is_collection: bool,
    /// Resource is a CalDAV calendar collection.
    pub is_calendar: bool,
    /// Resource is a CardDAV address book collection.
    pub is_address_book: bool,
}

/// Value for the `Depth` request header.
///
/// Defined in: <https://www.rfc-editor.org/rfc/rfc4918#section-10.2>
#[derive(Clone, Copy, PartialEq, Eq)]
pub enum Depth {
    /// Indicates that a method should be applied only to a resource.
    Zero,
    /// Indicates that a method should be applied to a resource and its internal members.
    One,
    /// Indicates that a method should be applied to a resource and all its members.
    Infinity,
}

const DEPTH_ZERO: HeaderValue = HeaderValue::from_static("0");
const DEPTH_ONE: HeaderValue = HeaderValue::from_static("1");
const DEPTH_INFINITY: HeaderValue = HeaderValue::from_static("infinity");

impl From<Depth> for HeaderValue {
    fn from(value: Depth) -> Self {
        match value {
            Depth::Zero => DEPTH_ZERO,
            Depth::One => DEPTH_ONE,
            Depth::Infinity => DEPTH_INFINITY,
        }
    }
}

impl std::fmt::Display for Depth {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.write_str(match self {
            Depth::Zero => "0",
            Depth::One => "1",
            Depth::Infinity => "infinity",
        })
    }
}