Skip to main content

io_vdir/
item.rs

1//! Vdir items: the [`VdirItem`] handle and its [`VdirItemKind`], plus
2//! the I/O-free coroutines for the item lifecycle (store, get, list,
3//! locate, copy, move, delete).
4//!
5//! An item is a single vCard ([RFC 6350]) or iCalendar ([RFC 5545])
6//! file, as laid out by the [vdir specification]. The coroutine
7//! submodules own one operation each; this module owns the shared
8//! handle and the file extensions they key off.
9//!
10//! [RFC 6350]: https://www.rfc-editor.org/rfc/rfc6350
11//! [RFC 5545]: https://www.rfc-editor.org/rfc/rfc5545
12//! [vdir specification]: https://vdirsyncer.pimutils.org/en/stable/vdir.html
13
14use core::hash::{Hash, Hasher};
15
16use alloc::vec::Vec;
17
18use crate::path::VdirPath;
19
20pub mod copy;
21pub mod delete;
22pub mod get;
23pub mod list;
24pub mod locate;
25pub mod r#move;
26pub mod store;
27
28/// File extension of vCard items.
29pub(crate) const VCF: &str = "vcf";
30
31/// File extension of iCalendar items.
32pub(crate) const ICS: &str = "ics";
33
34/// Temporary file extension used while atomically replacing an item
35/// or a metadata file.
36pub(crate) const TMP: &str = "tmp";
37
38/// Kind of a Vdir item.
39///
40/// Either an iCalendar component (`.ics`) or a vCard (`.vcf`).
41#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
42#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
43pub enum VdirItemKind {
44    /// iCalendar variant: event, task, alarm, or any valid iCalendar
45    /// component.
46    Ical,
47    /// vCard variant: a contact.
48    Vcard,
49}
50
51impl VdirItemKind {
52    /// Returns the file extension associated with the item kind.
53    pub fn extension(&self) -> &'static str {
54        match self {
55            Self::Ical => ICS,
56            Self::Vcard => VCF,
57        }
58    }
59
60    /// Parses an extension string into a [`VdirItemKind`].
61    pub fn from_extension(ext: &str) -> Option<Self> {
62        match ext {
63            ICS => Some(Self::Ical),
64            VCF => Some(Self::Vcard),
65            _ => None,
66        }
67    }
68}
69
70/// Builds the `(tmp, final)` paths for item `id` of `kind` under
71/// `collection`. Bytes land on the first and are renamed onto the
72/// second, so the item is only ever enumerated whole.
73pub(crate) fn build_paths(
74    collection: &VdirPath,
75    id: &str,
76    kind: VdirItemKind,
77) -> (VdirPath, VdirPath) {
78    let ext = kind.extension();
79    let final_path = collection.join(&format!("{id}.{ext}"));
80    let tmp_path = collection.join(&format!("{id}.{ext}.{TMP}"));
81    (tmp_path, final_path)
82}
83
84/// A Vdir collection's item.
85///
86/// Carries the on-disk path, the parsed kind (from the file
87/// extension) and the raw file bytes. The bytes stay raw: io-vdir
88/// never decodes them, leaving the choice of vCard or iCalendar
89/// parser to the caller.
90#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd)]
91#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
92pub struct VdirItem {
93    /// On-disk path of the item file.
94    pub path: VdirPath,
95    /// Kind of the item, derived from the file extension.
96    pub kind: VdirItemKind,
97    /// Raw file bytes.
98    pub contents: Vec<u8>,
99}
100
101impl VdirItem {
102    /// Returns the item id: the file stem (final path component
103    /// without the extension), or the whole file name when no
104    /// extension is present.
105    pub fn id(&self) -> Option<&str> {
106        let name = self.path.file_name()?;
107        Some(match name.rsplit_once('.') {
108            Some((stem, _)) if !stem.is_empty() => stem,
109            _ => name,
110        })
111    }
112
113    /// Returns the item bytes.
114    pub fn contents(&self) -> &[u8] {
115        &self.contents
116    }
117}
118
119impl Hash for VdirItem {
120    fn hash<H: Hasher>(&self, state: &mut H) {
121        self.path.hash(state);
122    }
123}
124
125impl AsRef<VdirPath> for VdirItem {
126    fn as_ref(&self) -> &VdirPath {
127        &self.path
128    }
129}
130
131impl From<(VdirPath, VdirItemKind, Vec<u8>)> for VdirItem {
132    fn from((path, kind, contents): (VdirPath, VdirItemKind, Vec<u8>)) -> Self {
133        Self {
134            path,
135            kind,
136            contents,
137        }
138    }
139}