use std::{str::FromStr, sync::Arc, time::Duration};
use async_trait::async_trait;
use libdav::PropertyName;
use sha2::{Digest as _, Sha256};
use vparser::Parser;
use crate::{
CollectionId, ErrorKind, Etag, Href, ItemKind, Result,
addressbook::AddressBookProperty,
calendar::CalendarProperty,
disco::Discovery,
hash::hash_normalized,
watch::{IntervalMonitor, StorageMonitor},
};
#[async_trait]
pub trait Storage: Sync + Send {
fn item_kind(&self) -> ItemKind;
async fn check(&self) -> Result<()>;
async fn discover_collections(&self) -> Result<Discovery>;
async fn create_collection(&self, href: &str) -> Result<Collection>;
async fn delete_collection(&self, href: &str) -> Result<()>;
async fn list_properties(&self, collection_href: &str) -> Result<Vec<FetchedProperty>> {
let properties = Property::known_properties(self.item_kind());
let mut result = Vec::with_capacity(properties.len());
for &property in properties {
if let Some(value) = self.get_property(collection_href, property).await? {
result.push(FetchedProperty { property, value });
}
}
Ok(result)
}
async fn get_property(&self, href: &str, property: Property) -> Result<Option<String>>;
async fn set_property(&self, href: &str, property: Property, value: &str) -> Result<()>;
async fn unset_property(&self, href: &str, property: Property) -> Result<()>;
async fn list_items(&self, collection_href: &str) -> Result<Vec<ItemVersion>>;
async fn get_item(&self, href: &str) -> Result<(Item, Etag)>;
async fn get_many_items(&self, hrefs: &[&str]) -> Result<Vec<FetchedItem>> {
let mut items = Vec::with_capacity(hrefs.len());
for href in hrefs {
let item = self.get_item(href).await?;
items.push(FetchedItem {
href: (*href).to_owned(),
item: item.0,
etag: item.1,
});
}
Ok(items)
}
async fn get_all_items(&self, collection: &str) -> Result<Vec<FetchedItem>> {
let item_vers = self.list_items(collection).await?;
let mut items = Vec::with_capacity(item_vers.len());
for item_ver in item_vers {
let item = self.get_item(&item_ver.href).await?;
items.push(FetchedItem {
href: item_ver.href,
item: item.0,
etag: item.1,
});
}
Ok(items)
}
async fn create_item(
&self,
collection: &str,
item: &Item,
opts: CreateItemOptions,
) -> Result<ItemVersion>;
async fn update_item(&self, href: &str, etag: &Etag, item: &Item) -> Result<Etag>;
async fn delete_item(&self, href: &str, etag: &Etag) -> Result<()>;
fn href_for_collection_id(&self, id: &CollectionId) -> Result<Href>;
async fn monitor(&self, interval: Duration) -> Result<Box<dyn StorageMonitor>> {
Ok(Box::new(IntervalMonitor::new(interval)) as Box<dyn StorageMonitor>)
}
async fn changed_since(
&self,
_collection: &str,
_since_state: Option<&str>,
) -> Result<CollectionChanges> {
Err(ErrorKind::Unsupported.into())
}
}
#[derive(Default, Clone)]
pub struct CreateItemOptions {
pub href: Option<Href>,
}
#[derive(Debug)]
pub struct Collection {
href: Href,
}
impl Collection {
#[must_use]
pub fn href(&self) -> &Href {
&self.href
}
#[must_use]
pub fn into_href(self) -> Href {
self.href
}
pub(crate) fn new(href: String) -> Collection {
Collection { href }
}
}
#[derive(PartialEq, Debug, Clone)]
pub struct ItemVersion {
pub href: Href,
pub etag: Etag,
}
impl ItemVersion {
#[must_use]
pub fn new(href: Href, etag: Etag) -> ItemVersion {
ItemVersion { href, etag }
}
}
#[derive(Debug, Clone)]
pub struct CollectionChanges {
pub new_state: Option<String>,
pub changed: Vec<Href>,
pub deleted: Vec<Href>,
}
#[derive(Clone, Copy, std::fmt::Debug, std::hash::Hash, PartialEq, Eq)]
pub enum Property {
AddressBook(AddressBookProperty),
Calendar(CalendarProperty),
}
impl Property {
#[must_use]
pub fn name(&self) -> &str {
match self {
Property::AddressBook(p) => p.name(),
Property::Calendar(p) => p.name(),
}
}
#[must_use]
pub fn known_properties(item_kind: ItemKind) -> &'static [Self]
where
Self: Sized,
{
match item_kind {
ItemKind::AddressBook => AddressBookProperty::known_properties(),
ItemKind::Calendar => CalendarProperty::known_properties(),
}
}
#[must_use]
pub fn dav_propname(&self) -> &PropertyName<'_, '_> {
match self {
Property::AddressBook(p) => p.dav_propname(),
Property::Calendar(p) => p.dav_propname(),
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct Item {
raw: String,
}
impl Item {
#[must_use]
pub fn uid(&self) -> Option<String> {
let mut lines = self.as_str().split_terminator("\r\n");
let mut uid = lines
.find_map(|line| line.strip_prefix("UID:"))
.map(String::from)?;
lines
.map_while(|line| line.strip_prefix(' ').or_else(|| line.strip_prefix('\t')))
.for_each(|part| uid.push_str(part));
Some(uid)
}
#[must_use]
pub fn hash(&self) -> ItemHash {
let mut hasher = Sha256::new();
hash_normalized(&self.raw, &mut hasher);
ItemHash(Arc::from(<[u8; 32]>::from(hasher.finalize())))
}
#[must_use]
pub fn ident(&self) -> String {
self.uid().unwrap_or_else(|| self.hash().to_string())
}
#[must_use]
pub fn with_uid(&self, new_uid: &str) -> Self {
let mut inside_component = false;
let mut new = String::new();
for line in Parser::new(self.as_str()) {
if line.name() == "BEGIN"
&& ["VEVENT", "VTODO", "VJOURNAL", "VCARD"].contains(&line.value().as_ref())
{
inside_component = true;
}
if line.name() == "END"
&& ["VEVENT", "VTODO", "VJOURNAL", "VCARD"].contains(&line.value().as_ref())
{
inside_component = false;
}
if inside_component && line.name() == "UID" {
new.push_str("UID:");
new.push_str(new_uid);
new.push_str("\r\n");
} else {
new.push_str(line.raw());
new.push_str("\r\n");
}
}
Self::from(new)
}
#[must_use]
pub fn as_str(&self) -> &str {
&self.raw
}
}
#[derive(Default, PartialEq, Clone)]
pub struct ItemHash(Arc<[u8; 32]>);
impl std::fmt::Display for ItemHash {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
for byte in self.0.iter() {
write!(f, "{byte:02X}")?;
}
Ok(())
}
}
impl std::fmt::Debug for ItemHash {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "ItemHash(")?;
for byte in self.0.iter() {
write!(f, "{byte:02X}")?;
}
write!(f, ")")
}
}
#[derive(Debug, thiserror::Error)]
pub enum ItemHashError {
#[error("Hash must be exactly 64 characters long")]
InvalidLength,
#[error("Invalid character in hash representation")]
InvalidCharacter,
}
impl FromStr for ItemHash {
type Err = ItemHashError;
fn from_str(value: &str) -> Result<Self, Self::Err> {
if value.len() != 64 {
return Err(ItemHashError::InvalidLength);
}
let mut bytes = [0u8; 32];
for (byte, chunk) in bytes.iter_mut().zip(value.as_bytes().chunks(2)) {
let hex = std::str::from_utf8(chunk).map_err(|_| ItemHashError::InvalidCharacter)?;
*byte = u8::from_str_radix(hex, 16).map_err(|_| ItemHashError::InvalidCharacter)?;
}
Ok(ItemHash(Arc::new(bytes)))
}
}
impl From<String> for Item {
fn from(value: String) -> Self {
Item { raw: value }
}
}
#[derive(Debug)]
pub struct FetchedItem {
pub href: Href,
pub item: Item,
pub etag: Etag,
}
pub struct FetchedProperty {
pub property: Property,
pub value: String,
}
#[cfg(test)]
mod test {
use crate::base::Item;
#[test]
fn test_single_line_uid() {
let raw = ["BEGIN:VCARD", "UID:hello", "END:VCARD"].join("\r\n");
let item = Item::from(raw);
assert_eq!(item.uid(), Some(String::from("hello")));
assert_eq!(item.ident(), String::from("hello"));
let raw = ["BEGIN:VCARD", "UID:hel", "lo", "END:VCARD"].join("\r\n");
let item = Item::from(raw);
assert_eq!(item.uid(), Some(String::from("hel")));
assert_eq!(item.ident(), String::from("hel"));
let raw = [
"BEGIN:VCARD",
"UID:hello",
"REV:20210307T195614Z\tthere",
"END:VCARD",
]
.join("\r\n");
let item = Item::from(raw);
assert_eq!(item.uid(), Some(String::from("hello")));
assert_eq!(item.ident(), String::from("hello"));
}
#[test]
fn test_multi_line_uid() {
let raw = ["BEGIN:VCARD", "UID:hello", "\tthere", "END:VCARD"].join("\r\n");
let item = Item::from(raw);
assert_eq!(item.uid(), Some(String::from("hellothere")));
assert_eq!(item.ident(), String::from("hellothere"));
let raw = [
"BEGIN:VCARD",
"UID:hello",
"\tthere",
"REV:20210307T195614Z",
"\tnope",
"END:VCARD",
]
.join("\r\n");
let item = Item::from(raw);
assert_eq!(item.uid(), Some(String::from("hellothere")));
assert_eq!(item.ident(), String::from("hellothere"));
}
#[test]
fn test_missing_uid() {
let raw = [
"BEGIN:VCARD",
"UIDX:hello",
"REV:20210307T195614Z\tthere",
"END:VCARD",
]
.join("\r\n");
let item = Item::from(raw);
assert_eq!(item.uid(), None);
assert_eq!(item.ident(), item.hash().to_string());
}
#[test]
fn test_with_uid() {
let raw = ["BEGIN:VCARD", "UID:hello", "END:VCARD"].join("\r\n");
let item = Item::from(raw);
let item2 = item.with_uid("goodbye");
assert_eq!(item2.uid(), Some(String::from("goodbye")));
assert_eq!(item2.ident(), String::from("goodbye"));
}
#[test]
fn test_with_uid_without_uid() {
let raw = ["BEGIN:VCARD", "SUMMARY:hello", "END:VCARD"].join("\r\n");
let item = Item::from(raw);
let item2 = item.with_uid("goodbye");
assert_eq!(item2.uid(), None);
}
}