use core::{fmt, mem};
use alloc::{collections::BTreeMap, string::String, vec::Vec};
use thiserror::Error;
use crate::{
coroutine::*,
item::{TMP, VdirItemKind},
path::VdirPath,
};
const UUID_LEN: usize = 16;
#[derive(Clone, Debug, Error)]
pub enum VdirItemStoreError {
#[error("Vdir item store failed: unexpected arg {0:?}")]
UnexpectedArg(Option<VdirReply>),
}
#[derive(Clone, Debug)]
pub struct VdirItemStoreOutput {
pub id: String,
pub path: VdirPath,
}
#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub struct VdirItemStoreOptions {}
#[derive(Debug)]
pub struct VdirItemStore {
state: State,
#[allow(dead_code)]
opts: VdirItemStoreOptions,
}
impl VdirItemStore {
pub fn new(
collection: impl Into<VdirPath>,
id: Option<String>,
kind: VdirItemKind,
contents: Vec<u8>,
opts: VdirItemStoreOptions,
) -> Self {
Self {
opts,
state: State::Start {
collection: collection.into(),
id,
kind,
contents,
},
}
}
}
impl VdirCoroutine for VdirItemStore {
type Yield = VdirYield;
type Return = Result<VdirItemStoreOutput, VdirItemStoreError>;
fn resume(&mut self, arg: Option<VdirReply>) -> VdirCoroutineState<Self::Yield, Self::Return> {
match (&mut self.state, arg) {
(
State::Start {
collection,
id,
kind,
contents,
},
None,
) => {
let collection = mem::take(collection);
let kind = *kind;
let contents = mem::take(contents);
match id.take() {
Some(id) => {
let (tmp_path, final_path) = build_paths(&collection, &id, kind);
let files = BTreeMap::from_iter([(tmp_path.clone(), contents)]);
self.state = State::AwaitFileCreate {
id,
tmp_path,
final_path,
};
VdirCoroutineState::Yielded(VdirYield::WantsFileCreate(files))
}
None => {
self.state = State::AwaitRandom {
collection,
kind,
contents,
};
VdirCoroutineState::Yielded(VdirYield::WantsRandom { len: UUID_LEN })
}
}
}
(
State::AwaitRandom {
collection,
kind,
contents,
},
Some(VdirReply::Random(bytes)),
) => {
let collection = mem::take(collection);
let kind = *kind;
let contents = mem::take(contents);
let id = uuid_v4(&bytes);
let (tmp_path, final_path) = build_paths(&collection, &id, kind);
let files = BTreeMap::from_iter([(tmp_path.clone(), contents)]);
self.state = State::AwaitFileCreate {
id,
tmp_path,
final_path,
};
VdirCoroutineState::Yielded(VdirYield::WantsFileCreate(files))
}
(
State::AwaitFileCreate {
id,
tmp_path,
final_path,
},
Some(VdirReply::FileCreate),
) => {
let id = mem::take(id);
let tmp_path = mem::take(tmp_path);
let final_path = mem::take(final_path);
let pairs = vec![(tmp_path, final_path.clone())];
self.state = State::AwaitRename { id, final_path };
VdirCoroutineState::Yielded(VdirYield::WantsRename(pairs))
}
(State::AwaitRename { id, final_path }, Some(VdirReply::Rename)) => {
let out = VdirItemStoreOutput {
id: mem::take(id),
path: mem::take(final_path),
};
VdirCoroutineState::Complete(Ok(out))
}
(_, arg) => {
let err = VdirItemStoreError::UnexpectedArg(arg);
VdirCoroutineState::Complete(Err(err))
}
}
}
}
#[derive(Debug)]
enum State {
Start {
collection: VdirPath,
id: Option<String>,
kind: VdirItemKind,
contents: Vec<u8>,
},
AwaitRandom {
collection: VdirPath,
kind: VdirItemKind,
contents: Vec<u8>,
},
AwaitFileCreate {
id: String,
tmp_path: VdirPath,
final_path: VdirPath,
},
AwaitRename {
id: String,
final_path: VdirPath,
},
}
impl fmt::Display for State {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Start { .. } => f.write_str("start"),
Self::AwaitRandom { .. } => f.write_str("await random reply"),
Self::AwaitFileCreate { .. } => f.write_str("await file create reply"),
Self::AwaitRename { .. } => f.write_str("await rename reply"),
}
}
}
fn build_paths(collection: &VdirPath, id: &str, kind: VdirItemKind) -> (VdirPath, VdirPath) {
let ext = kind.extension();
let final_path = collection.join(&format!("{id}.{ext}"));
let tmp_path = collection.join(&format!("{id}.{ext}.{TMP}"));
(tmp_path, final_path)
}
fn uuid_v4(bytes: &[u8]) -> String {
let mut bytes: [u8; UUID_LEN] = bytes[..UUID_LEN].try_into().unwrap_or([0u8; UUID_LEN]);
bytes[6] = (bytes[6] & 0x0f) | 0x40;
bytes[8] = (bytes[8] & 0x3f) | 0x80;
let mut id = String::with_capacity(36);
let groups: [&[u8]; 5] = [
&bytes[0..4],
&bytes[4..6],
&bytes[6..8],
&bytes[8..10],
&bytes[10..16],
];
for (i, group) in groups.iter().enumerate() {
if i > 0 {
id.push('-');
}
for byte in *group {
id.push(hex_nibble(byte >> 4));
id.push(hex_nibble(byte & 0x0f));
}
}
id
}
fn hex_nibble(n: u8) -> char {
match n {
0..=9 => (b'0' + n) as char,
10..=15 => (b'a' + (n - 10)) as char,
_ => '0',
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn reuses_supplied_id() {
let mut cor = VdirItemStore::new(
"root/contacts",
Some("alice".into()),
VdirItemKind::Vcard,
b"BEGIN:VCARD".to_vec(),
VdirItemStoreOptions::default(),
);
let files = match cor.resume(None) {
VdirCoroutineState::Yielded(VdirYield::WantsFileCreate(files)) => files,
state => panic!("expected WantsFileCreate, got {state:?}"),
};
let tmp = VdirPath::from("root/contacts/alice.vcf.tmp");
assert!(files.contains_key(&tmp));
let pairs = match cor.resume(Some(VdirReply::FileCreate)) {
VdirCoroutineState::Yielded(VdirYield::WantsRename(pairs)) => pairs,
state => panic!("expected WantsRename, got {state:?}"),
};
assert_eq!(
pairs,
vec![(tmp, VdirPath::from("root/contacts/alice.vcf"))]
);
let out = match cor.resume(Some(VdirReply::Rename)) {
VdirCoroutineState::Complete(Ok(out)) => out,
state => panic!("expected Complete(Ok), got {state:?}"),
};
assert_eq!(out.id, "alice");
assert_eq!(out.path, VdirPath::from("root/contacts/alice.vcf"));
}
#[test]
fn generates_uuid_when_id_missing() {
let mut cor = VdirItemStore::new(
"root/contacts",
None,
VdirItemKind::Ical,
b"BEGIN:VCAL".to_vec(),
VdirItemStoreOptions::default(),
);
match cor.resume(None) {
VdirCoroutineState::Yielded(VdirYield::WantsRandom { len }) => {
assert_eq!(len, UUID_LEN)
}
state => panic!("expected WantsRandom, got {state:?}"),
}
let bytes = vec![0xabu8; UUID_LEN];
let files = match cor.resume(Some(VdirReply::Random(bytes))) {
VdirCoroutineState::Yielded(VdirYield::WantsFileCreate(files)) => files,
state => panic!("expected WantsFileCreate, got {state:?}"),
};
let tmp = files.keys().next().unwrap();
assert!(tmp.as_str().ends_with(".ics.tmp"));
let id = tmp.file_name().unwrap();
assert_eq!(id.chars().nth(14), Some('4'));
assert!(matches!(id.chars().nth(19), Some('8'..='b')));
}
#[test]
fn unexpected_reply_returns_error() {
let mut cor = VdirItemStore::new(
"root/contacts",
Some("alice".into()),
VdirItemKind::Vcard,
b"x".to_vec(),
VdirItemStoreOptions::default(),
);
let _ = cor.resume(None);
let err = match cor.resume(Some(VdirReply::DirCreate)) {
VdirCoroutineState::Complete(Err(err)) => err,
state => panic!("expected Complete(Err), got {state:?}"),
};
assert!(matches!(err, VdirItemStoreError::UnexpectedArg(_)));
}
}