use core::{
fmt, mem,
sync::atomic::{AtomicU32, Ordering},
};
use alloc::{
collections::BTreeMap,
string::{String, ToString},
vec::Vec,
};
use log::trace;
use thiserror::Error;
use crate::{
coroutine::*,
entry::types::INFORMATIONAL_SUFFIX_SEPARATOR,
flag::types::MaildirFlags,
maildir::types::{Maildir, MaildirSubdir},
path::FsPath,
};
static COUNTER: AtomicU32 = AtomicU32::new(0);
#[derive(Clone, Debug, Error)]
pub enum MaildirEntryStoreError {
#[error("Maildir message store failed: unexpected arg {0:?}")]
UnexpectedArg(Option<MaildirReply>),
}
#[derive(Clone, Debug)]
pub struct MaildirEntryStoreOutput {
pub id: String,
pub path: FsPath,
}
#[derive(Debug)]
pub struct MaildirEntryStore {
maildir: Maildir,
subdir: MaildirSubdir,
flags: MaildirFlags,
contents: Vec<u8>,
state: State,
}
impl MaildirEntryStore {
pub fn new(
maildir: Maildir,
subdir: MaildirSubdir,
flags: MaildirFlags,
contents: Vec<u8>,
) -> Self {
Self {
maildir,
subdir,
flags,
contents,
state: State::Start,
}
}
}
impl MaildirCoroutine for MaildirEntryStore {
type Yield = MaildirYield;
type Return = Result<MaildirEntryStoreOutput, MaildirEntryStoreError>;
fn resume(
&mut self,
arg: Option<MaildirReply>,
) -> MaildirCoroutineState<Self::Yield, Self::Return> {
trace!("entry store: {}", self.state);
match (&mut self.state, arg) {
(State::Start, None) => {
self.state = State::AwaitTime;
MaildirCoroutineState::Yielded(MaildirYield::WantsTime)
}
(State::AwaitTime, Some(MaildirReply::Time { secs, nanos })) => {
self.state = State::AwaitPid { secs, nanos };
MaildirCoroutineState::Yielded(MaildirYield::WantsPid)
}
(State::AwaitPid { secs, nanos }, Some(MaildirReply::Pid(pid))) => {
let secs = *secs;
let nanos = *nanos;
self.state = State::AwaitHostname { secs, nanos, pid };
MaildirCoroutineState::Yielded(MaildirYield::WantsHostname)
}
(State::AwaitHostname { secs, nanos, pid }, Some(MaildirReply::Hostname(hostname))) => {
let secs = *secs;
let nanos = *nanos;
let pid = *pid;
let counter = COUNTER.fetch_add(1, Ordering::AcqRel);
let id = format!("{secs}.#{counter:x}M{nanos}P{pid}.{hostname}");
let mut final_name = id.clone();
if matches!(self.subdir, MaildirSubdir::Cur) {
final_name.push(INFORMATIONAL_SUFFIX_SEPARATOR);
final_name.push_str("2,");
final_name.push_str(&self.flags.to_string());
}
let tmp_path = self.maildir.tmp().join(&id);
let final_path = self.maildir.subdir(&self.subdir).join(&final_name);
let contents = mem::take(&mut self.contents);
let files = BTreeMap::from_iter([(tmp_path.clone(), contents)]);
self.state = State::AwaitCreateTmp {
tmp_path,
final_path,
id,
};
MaildirCoroutineState::Yielded(MaildirYield::WantsFileCreate(files))
}
(
State::AwaitCreateTmp {
tmp_path,
final_path,
id,
},
Some(MaildirReply::FileCreate),
) => {
let tmp_path = mem::take(tmp_path);
let final_path = mem::take(final_path);
let id = mem::take(id);
let pairs = vec![(tmp_path, final_path.clone())];
self.state = State::AwaitRename { final_path, id };
MaildirCoroutineState::Yielded(MaildirYield::WantsRename(pairs))
}
(State::AwaitRename { final_path, id }, Some(MaildirReply::Rename)) => {
let final_path = mem::take(final_path);
let id = mem::take(id);
MaildirCoroutineState::Complete(Ok(MaildirEntryStoreOutput {
id,
path: final_path,
}))
}
(_, arg) => {
let err = MaildirEntryStoreError::UnexpectedArg(arg);
MaildirCoroutineState::Complete(Err(err))
}
}
}
}
#[derive(Debug)]
enum State {
Start,
AwaitTime,
AwaitPid {
secs: u64,
nanos: u32,
},
AwaitHostname {
secs: u64,
nanos: u32,
pid: u32,
},
AwaitCreateTmp {
tmp_path: FsPath,
final_path: FsPath,
id: String,
},
AwaitRename {
final_path: FsPath,
id: String,
},
}
impl fmt::Display for State {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Start => f.write_str("start"),
Self::AwaitTime => f.write_str("await time reply"),
Self::AwaitPid { .. } => f.write_str("await pid reply"),
Self::AwaitHostname { .. } => f.write_str("await hostname reply"),
Self::AwaitCreateTmp { .. } => f.write_str("await tmp create reply"),
Self::AwaitRename { .. } => f.write_str("await rename reply"),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn maildir() -> Maildir {
Maildir::from_path("root")
}
#[test]
fn full_flow_returns_ok() {
let mut cor = MaildirEntryStore::new(
maildir(),
MaildirSubdir::New,
MaildirFlags::default(),
b"hello".to_vec(),
);
match cor.resume(None) {
MaildirCoroutineState::Yielded(MaildirYield::WantsTime) => {}
state => panic!("expected WantsTime, got {state:?}"),
}
match cor.resume(Some(MaildirReply::Time { secs: 1, nanos: 2 })) {
MaildirCoroutineState::Yielded(MaildirYield::WantsPid) => {}
state => panic!("expected WantsPid, got {state:?}"),
}
match cor.resume(Some(MaildirReply::Pid(3))) {
MaildirCoroutineState::Yielded(MaildirYield::WantsHostname) => {}
state => panic!("expected WantsHostname, got {state:?}"),
}
match cor.resume(Some(MaildirReply::Hostname(String::from("host")))) {
MaildirCoroutineState::Yielded(MaildirYield::WantsFileCreate(_)) => {}
state => panic!("expected WantsFileCreate, got {state:?}"),
}
match cor.resume(Some(MaildirReply::FileCreate)) {
MaildirCoroutineState::Yielded(MaildirYield::WantsRename(_)) => {}
state => panic!("expected WantsRename, got {state:?}"),
}
match cor.resume(Some(MaildirReply::Rename)) {
MaildirCoroutineState::Complete(Ok(out)) => {
assert!(out.id.starts_with("1."));
assert!(out.path.as_str().contains("/new/"));
}
state => panic!("expected Complete(Ok), got {state:?}"),
}
}
#[test]
fn unexpected_reply_returns_error() {
let mut cor = MaildirEntryStore::new(
maildir(),
MaildirSubdir::New,
MaildirFlags::default(),
b"hello".to_vec(),
);
match cor.resume(None) {
MaildirCoroutineState::Yielded(MaildirYield::WantsTime) => {}
state => panic!("expected WantsTime, got {state:?}"),
}
match cor.resume(Some(MaildirReply::DirCreate)) {
MaildirCoroutineState::Complete(Err(MaildirEntryStoreError::UnexpectedArg(_))) => {}
state => panic!("expected Complete(Err), got {state:?}"),
}
}
}