vstorage/
lib.rs

1// Copyright 2023-2024 Hugo Osvaldo Barrera
2//
3// SPDX-License-Identifier: EUPL-1.2
4#![deny(clippy::pedantic)]
5#![deny(clippy::unwrap_used)]
6#![allow(clippy::module_name_repetitions)]
7#![forbid(unsafe_code)]
8#![forbid(clippy::print_stdout)]
9
10//! Interact with and synchronise with storages with different underlying implementations.
11//!
12//! Storages contain collections which can themselves contain `icalendar` components, `vcard`
13//! entries, or other similar content types where items have an internal unique ids.
14//!
15//! This crates contains the underlying logic for [pimsync][pimsync]. pimsync is a command line
16//! tool to synchronise storages with calendars and contacts. This crate implements the actual
17//! logic for comparing and synchronising storages, and can be used to write alternative interfaces
18//! for the same synchronisation implementation.
19//!
20//! [pimsync]: https://pimsync.whynothugo.nl/
21//!
22//! # Storage types
23//!
24//! A [`Storage`] contains a set of collections, where each collection can contain many items, but
25//! not other collections. This restriction matches the semantics of CalDAV/CardDAV and also object
26//! stores like S3.
27//!
28//! This crate currently includes the following implementations:
29//!
30//! - [`CalDavStorage`]: a CalDAV server, where each collection is an individual calendar, and
31//!   each item is an individual event or todo in a calendar.
32//! - [`CardDavStorage`]: a CardDAV server, where each collection is an individual address book, and
33//!   each item is an individual contact card.
34//! - `JmapStorage`: *(optional, requires `jmap` feature)* a JMAP server, where each collection is
35//!   an individual address book, and each item is an individual contact card.
36//! - [`ReadOnlyStorage`]: wraps around another `Storage` instance, returning an error of kind
37//!   [`ErrorKind::ReadOnly`] for any write operation.
38//! - [`VdirStorage`] a local directory, where each collection is a directory and each
39//!   item is a file.
40//! - [`WebCal`]: An icalendar file loaded via HTTP(s). This storage is implicitly read-only.
41//!
42//! The `Storage` type and the logic for synchronisation of storages is agnostic to the content
43//! type inside collections, and can synchronise collections with any type of content. When
44//! synchronising two storages, items with the same UID on both sides are synchronised with each
45//! other. Interpreting content of items in order to extract these UIDs is done via a minimal and
46//! permissive parser.
47//!
48//! [`Storage`]: crate::base::Storage
49//! [`CalDavStorage`]: crate::caldav::CalDavStorage
50//! [`CardDavStorage`]: crate::carddav::CardDavStorage
51//! [`ReadOnlyStorage`]: crate::readonly::ReadOnlyStorage
52//! [`VdirStorage`]: crate::vdir::VdirStorage
53//! [`WebCal`]: crate::webcal::WebCalStorage
54//!
55//! ## Collections, Hrefs and Collections Ids
56//!
57//! As mentioned above, collections cannot be nested (note for IMAP: having an `INBOX` collection
58//! and an `INBOX/Feeds` collection is perfectly valid).
59//!
60//! A collection has an `href` and usually has an `id`.
61//!
62//! The `href` attribute is the path to an item inside a storage instance. Its value is storage
63//! dependant, meaning that when a collection is synchronised to another storage, it may have a
64//! different `href` on each side.
65//!
66//! The `id` for a collection is not storage-specific. When synchronising two storages, the default
67//! approach is to synchronise items across collections with the same `id`. The `id` of a
68//! collection is entirely dependant on its `href`, and should never change.
69//!
70//! The [`Href`] alias is used to refer to `href`s to avoid ambiguity. [`Href`] instances should be
71//! treated as an opaque value and not given any special meaning outside of this crate.
72//!
73//! See also: [`CollectionId`].
74//!
75//! ## Items
76//!
77//! See [`Item`](crate::base::Item).
78//!
79//! ## Properties
80//!
81//! Storages expose properties for collections. Property types vary depending on a Storage's items,
82//! although items themselves cannot have properties.
83//!
84//! Calendars have a `Colour`, `Description`, `DisplayName` and `Order`
85//!
86//! Address Books have `DisplayName` and `Description`.
87//!
88//! ## Entity tags
89//!
90//! An `Etag` is a value that changes whenever an item has changed in a collection. It is inspired
91//! on the HTTP header with the same name (used extensively in WebDAV). See [`Etag`].
92//!
93//! # Storage builders
94//!
95//! Storages all use a `Builder` type for instantiation. The builder takes the minimal set of
96//! mandatory arguments, while other constructor argument may be passed via associated functions.
97//! This pattern allows extending builders with new initialisation parameters without introducing
98//! backwards-incompatible changes. It also allows keeping a reference to a would-be Storage
99//! without performing the I/O which might be necessary to initialise it.
100//!
101//! # Re-exports
102//!
103//! This library re-exports `libdav` and `libjmap` (if JMAP is enabled). Prefer using the
104//! re-exported reference instead of using libdav directly to avoid having to track matching
105//! releases.
106
107use std::{backtrace::Backtrace, str::FromStr, sync::Arc};
108
109pub use libdav;
110#[cfg(feature = "jmap")]
111pub use libjmap;
112
113pub mod addressbook;
114mod atomic;
115pub mod base;
116pub mod caldav;
117pub mod calendar;
118pub mod carddav;
119mod dav;
120pub mod disco;
121pub mod hash;
122#[cfg(feature = "jmap")]
123pub mod jmap;
124pub mod readonly;
125mod simple_component;
126pub mod sync;
127pub mod vdir;
128pub mod watch;
129pub mod webcal;
130
131type Result<T, E = crate::Error> = std::result::Result<T, E>;
132
133/// Variants used to categorise [`Error`] instances.
134#[derive(Debug, PartialEq)]
135pub enum ErrorKind {
136    /// A storage, collection or resource does not exist.
137    DoesNotExist,
138    /// Referenced resource is not a collection.
139    NotACollection,
140    /// Referenced resource is not a storage.
141    NotAStorage,
142    /// Access was denied by the underlying storage.
143    AccessDenied,
144    /// Generic input / output error.
145    Io,
146    /// Storage returned invalid data.
147    InvalidData,
148    /// Input provided is invalid.
149    InvalidInput,
150    /// Resources is read-only.
151    ReadOnly,
152    /// Collection is not empty.
153    CollectionNotEmpty,
154    /// A precondition has failed.
155    ///
156    /// Typically, this error is returned when attempting to operate on an item using a stale
157    /// [`Etag`].
158    PreconditionFailed,
159    /// The requested operation is not possible on this specific instance.
160    Unavailable,
161    /// This storage implementation does not support a required feature.
162    Unsupported,
163    /// Uncategorised error.
164    ///
165    /// This variant is deprecated and should not be used for any new error paths.
166    // #[deprecated]
167    Uncategorised,
168}
169
170impl ErrorKind {
171    /// Create a new error of this kind.
172    ///
173    /// This is merely a convenience shortcut to [`Error::new`].
174    fn error<E>(self, source: E) -> Error
175    where
176        E: Into<Box<dyn std::error::Error + Send + Sync>>,
177    {
178        Error::new(self, source)
179    }
180
181    #[must_use]
182    const fn as_str(&self) -> &'static str {
183        match self {
184            ErrorKind::DoesNotExist => "resource does not exist",
185            ErrorKind::NotACollection => "resource exists, but is not a collection",
186            ErrorKind::NotAStorage => "resource exists, but is not a storage",
187            ErrorKind::AccessDenied => "access to the resource was denied",
188            ErrorKind::Io => "input/output error",
189            ErrorKind::InvalidData => "operation returned data, but it is not valid",
190            ErrorKind::InvalidInput => "input data is invalid",
191            ErrorKind::ReadOnly => "the resource is read-only",
192            ErrorKind::CollectionNotEmpty => "the collection is not empty",
193            ErrorKind::PreconditionFailed => "a required condition was not met",
194            ErrorKind::Unavailable => "the operation is not possible on this instance",
195            ErrorKind::Unsupported => "the operation is not supported",
196            ErrorKind::Uncategorised => "uncategorised error",
197        }
198    }
199}
200
201/// Common error type used by all Storage implementations.
202///
203/// See also [`ErrorKind`].
204#[derive(Debug)]
205pub struct Error {
206    kind: ErrorKind,
207    source: Option<Box<dyn std::error::Error + Send + Sync>>,
208    backtrace: Backtrace,
209}
210
211impl Error {
212    fn new<E>(kind: ErrorKind, source: E) -> Error
213    where
214        E: Into<Box<dyn std::error::Error + Send + Sync>>,
215    {
216        Error {
217            kind,
218            source: Some(source.into()),
219            backtrace: Backtrace::capture(),
220        }
221    }
222
223    /// Return the backtrace for this error.
224    ///
225    /// A backtrace is not always available, see documentation for the [`::std::backtrace`] module.
226    pub fn backtrace(&self) -> &Backtrace {
227        &self.backtrace
228    }
229}
230
231impl From<ErrorKind> for Error {
232    fn from(kind: ErrorKind) -> Self {
233        Error {
234            kind,
235            source: None,
236            backtrace: Backtrace::capture(),
237        }
238    }
239}
240
241impl From<std::io::Error> for Error {
242    fn from(value: std::io::Error) -> Self {
243        let kind = match value.kind() {
244            std::io::ErrorKind::NotFound => ErrorKind::DoesNotExist,
245            std::io::ErrorKind::PermissionDenied => ErrorKind::AccessDenied,
246            std::io::ErrorKind::InvalidInput => ErrorKind::InvalidInput,
247            std::io::ErrorKind::InvalidData => ErrorKind::InvalidData,
248            _ => ErrorKind::Io,
249        };
250        Error {
251            kind,
252            source: Some(value.into()),
253            backtrace: Backtrace::capture(),
254        }
255    }
256}
257
258impl std::fmt::Display for ErrorKind {
259    fn fmt(&self, fmt: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
260        fmt.write_str(self.as_str())
261    }
262}
263
264impl std::fmt::Display for Error {
265    fn fmt(&self, fmt: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
266        match self.source {
267            Some(ref s) => write!(fmt, "{}: {}", self.kind, s),
268            None => self.kind.fmt(fmt),
269        }
270    }
271}
272
273impl std::error::Error for Error {
274    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
275        match &self.source {
276            Some(e) => Some(e.as_ref()),
277            None => None,
278        }
279    }
280}
281
282/// Identifier for a specific version of a resource.
283///
284/// Each time that a resource is read, it will return its current `Etag`. The `Etag` is a unique
285/// identifier for the current version. An `Etag` value is specific to a specific storage
286/// implementation and instance. E.g.: they are opaque values that have no meaning across storages.
287///
288/// This is strongly inspired on the [HTTP header of the same name][MDN].
289///
290/// It is assumed that all `Etag` values are valid UTF-8 strings. As of HTTP 1.1, all header values
291/// are restricted to visible characters in the ASCII range, so this is not a problem for CalDAV or
292/// CardDAV storages. Other storages with no native `Etag` concept should attempt to use the most
293/// suitable approximation.
294///
295/// [MDN]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/ETag
296#[derive(Debug, PartialEq, Clone)]
297pub struct Etag(String);
298
299impl Etag {
300    /// Return a reference to the underlying string.
301    #[must_use]
302    pub fn as_str(&self) -> &str {
303        self.0.as_str()
304    }
305}
306
307impl<T> From<T> for Etag
308where
309    String: From<T>,
310{
311    fn from(value: T) -> Self {
312        Etag(value.into())
313    }
314}
315
316impl AsRef<str> for Etag {
317    fn as_ref(&self) -> &str {
318        &self.0
319    }
320}
321
322impl std::fmt::Display for Etag {
323    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
324        f.write_str(&self.0)
325    }
326}
327
328/// Path to the item inside the collection.
329///
330/// For example, for CardDAV collections this is the path of the entry inside the collection. For
331/// [`vdir::VdirStorage`], this the file's relative path, etc. `Href`s MUST be valid UTF-8 sequences.
332/// Implementations MUST define their `Href` in a way that it is possible to infer:
333///
334/// - Whether an Href belongs to a collection or an item.
335/// - For an item, to which collection it belongs.
336///
337/// Whether an `href` is relative to a collection or absolute is storage dependant. As such, this
338/// should be treated as an opaque string by consumers of this library.
339pub type Href = String;
340
341/// Identifier for a collection.
342///
343/// Collection identifiers are a short string that uniquely identify a collection inside a storage.
344/// They are based on the `href` of a collection, which never changes. A `CollectionId` is
345/// intended as a more human-friendly substitute for collection `href`s, and an attribute which is
346/// more storage-agnostic.
347///
348/// The following limitations exist, given that such values would produce ambiguous results with
349/// the implementation of [`VdirStorage`], [`CalDavStorage`], and [`CardDavStorage`]:
350///
351/// - A `CollectionId` cannot contain a `/` (slash)
352/// - A `CollectionId` cannot be exactly `..` (double period).
353/// - A `CollectionId` cannot be exactly `.` (a single period).
354///
355/// [`VdirStorage`]: crate::vdir::VdirStorage
356/// [`CalDavStorage`]: crate::caldav::CalDavStorage
357/// [`CardDavStorage`]: crate::carddav::CardDavStorage
358///
359/// Instances of `CollectionId` always contain previously validated data. Instances are internally
360/// reference counted and cheap to [`clone`][`Clone::clone`].
361///
362/// # Example
363///
364/// ```
365/// # use vstorage::CollectionId;
366/// let collection_id: CollectionId = "personal".parse().unwrap();
367/// ```
368#[derive(PartialEq, Debug, Clone, Eq, Hash)]
369// INVARIANT: matches rules in documentation above.
370pub struct CollectionId(Arc<str>);
371
372impl AsRef<str> for CollectionId {
373    fn as_ref(&self) -> &str {
374        &self.0
375    }
376}
377
378impl std::fmt::Display for CollectionId {
379    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
380        self.0.fmt(f)
381    }
382}
383
384/// Error type when creating a new [`CollectionId`].
385#[derive(Debug, thiserror::Error)]
386pub enum CollectionIdError {
387    /// Found a slash in the collection id.
388    #[error("collection id must not contain a slash")]
389    Slash,
390    /// Collection id was `..`, which is an invalid id.
391    #[error("collection id must not be '..'")]
392    DoublePeriod,
393    /// Collection id was `.`, which is an invalid id.
394    #[error("collection id must not be '.'")]
395    SinglePeriod,
396}
397
398impl FromStr for CollectionId {
399    type Err = CollectionIdError;
400
401    /// Allocates a new [`CollectionId`] with the input data.
402    fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
403        match s {
404            s if s.chars().any(|c| c == '/') => Err(CollectionIdError::Slash),
405            ".." => Err(CollectionIdError::DoublePeriod),
406            "." => Err(CollectionIdError::SinglePeriod),
407            s => Ok(CollectionId(Arc::from(s))),
408        }
409    }
410}
411
412/// Kind of items allowed inside this vdir.
413#[derive(Clone, Copy, Debug, PartialEq)]
414pub enum ItemKind {
415    AddressBook,
416    Calendar,
417}