libdav/
lib.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
#![deny(clippy::pedantic)]
#![deny(clippy::unwrap_used)]
#![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 documentation for the [`encoding`] module for
//! more details on conventions on encoding different hrefs and URLs.
//!
//! # 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 dav::WebDavError;
use http::HeaderValue;
use http::StatusCode;

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

pub use caldav::service_for_url as caldav_service_for_url;
pub use caldav::CalDavClient;
pub use carddav::service_for_url as carddav_service_for_url;
pub use carddav::CardDavClient;
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)
    }
}

/// Error type for [`CalDavClient::find_calendar_home_set`] and [`CardDavClient::find_address_book_home_set`].
#[derive(thiserror::Error, Debug)]
#[error("error finding home set collection: {0}")]
pub struct FindHomeSetError(#[source] pub WebDavError);

/// 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 [`CalDavClient::check_support`] and [`CardDavClient::check_support`].
///
/// Returned when checking support for a feature encounters an error.
#[derive(thiserror::Error, Debug)]
pub enum CheckSupportError {
    /// The `DAV` header was missing from the response received.
    #[error("the DAV header was missing from the response")]
    MissingHeader,

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

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

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

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

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

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

/// 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, 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,
        }
    }
}