Skip to main content

io_m2dir/flag/
set.rs

1//! I/O-free coroutine to replace an m2dir entry's flags metadata
2//! file with a new flag set.
3//!
4//! If the new set is empty the metadata file is removed; otherwise
5//! it is overwritten.
6//!
7//! # Example
8//!
9//! ```rust,no_run
10//! use std::fs;
11//!
12//! use io_m2dir::{
13//!     coroutine::{M2dirArg, M2dirCoroutine, M2dirCoroutineState, M2dirYield},
14//!     flag::{
15//!         set::{M2dirFlagSet, M2dirFlagSetOptions},
16//!         M2dirFlags,
17//!     },
18//!     m2dir::M2dir,
19//! };
20//!
21//! let m2dir = M2dir::from_path("/tmp/inbox");
22//! let opts = M2dirFlagSetOptions::default();
23//! let mut flags = M2dirFlags::default();
24//! flags.insert("$seen");
25//! let mut coroutine = M2dirFlagSet::new(&m2dir, "entry-id", flags, opts);
26//! let mut arg = None;
27//!
28//! loop {
29//!     match coroutine.resume(arg.take()) {
30//!         M2dirCoroutineState::Yielded(M2dirYield::WantsFileCreate(files)) => {
31//!             for (path, bytes) in files {
32//!                 fs::write(path.as_str(), bytes).unwrap();
33//!             }
34//!             arg = Some(M2dirArg::FileCreate);
35//!         }
36//!         M2dirCoroutineState::Yielded(M2dirYield::WantsFileRemove(paths)) => {
37//!             for path in paths {
38//!                 let _ = fs::remove_file(path.as_str());
39//!             }
40//!             arg = Some(M2dirArg::FileRemove);
41//!         }
42//!         M2dirCoroutineState::Complete(Ok(())) => break,
43//!         M2dirCoroutineState::Complete(Err(err)) => panic!("{err}"),
44//!         state => panic!("unexpected state {state:?}"),
45//!     }
46//! }
47//! ```
48
49use core::{fmt, str};
50
51use alloc::collections::{BTreeMap, BTreeSet};
52
53use log::trace;
54use thiserror::Error;
55
56use crate::{coroutine::*, flag::M2dirFlags, m2dir::M2dir, path::M2dirPath};
57
58/// Failure causes during the m2dir flag SET flow.
59#[derive(Clone, Debug, Error)]
60pub enum M2dirFlagSetError {
61    /// The resume arg did not match the pending filesystem request.
62    #[error("M2DIR SET FLAGS failed: unexpected coroutine arg")]
63    UnexpectedArg,
64    /// The coroutine was resumed without the arg its request needs.
65    #[error("M2DIR SET FLAGS failed: missing coroutine arg")]
66    MissingArg,
67}
68
69/// Options for [`M2dirFlagSet::new`].
70#[derive(Clone, Debug, Default, Eq, PartialEq)]
71pub struct M2dirFlagSetOptions {}
72
73/// I/O-free m2dir flag SET coroutine.
74pub struct M2dirFlagSet {
75    flags_path: M2dirPath,
76    flags: M2dirFlags,
77    state: State,
78    #[allow(dead_code)]
79    opts: M2dirFlagSetOptions,
80}
81
82impl M2dirFlagSet {
83    /// Creates a new coroutine that will replace the flags metadata
84    /// file for entry `id` inside `m2dir` with `flags`.
85    pub fn new(
86        m2dir: &M2dir,
87        id: impl AsRef<str>,
88        flags: M2dirFlags,
89        opts: M2dirFlagSetOptions,
90    ) -> Self {
91        Self {
92            flags_path: m2dir.flags_path(id.as_ref()),
93            flags,
94            state: State::Start,
95            opts,
96        }
97    }
98}
99
100impl M2dirCoroutine for M2dirFlagSet {
101    type Yield = M2dirYield;
102    type Return = Result<(), M2dirFlagSetError>;
103
104    fn resume(&mut self, arg: Option<M2dirArg>) -> M2dirCoroutineState<Self::Yield, Self::Return> {
105        match (&self.state, arg) {
106            (State::Start, None) => {
107                self.state = State::Done;
108
109                if self.flags.is_empty() {
110                    trace!("wants flags remove at {}", self.flags_path);
111                    let paths = BTreeSet::from_iter([self.flags_path.clone()]);
112                    M2dirCoroutineState::Yielded(M2dirYield::WantsFileRemove(paths))
113                } else {
114                    trace!(
115                        "wants flags write at {} ({} flags)",
116                        self.flags_path,
117                        self.flags.len(),
118                    );
119                    let serialized = self.flags.to_meta().into_bytes();
120                    let files = BTreeMap::from_iter([(self.flags_path.clone(), serialized)]);
121                    M2dirCoroutineState::Yielded(M2dirYield::WantsFileCreate(files))
122                }
123            }
124            (State::Done, Some(M2dirArg::FileCreate | M2dirArg::FileRemove)) => {
125                trace!("flags set at {}", self.flags_path);
126                M2dirCoroutineState::Complete(Ok(()))
127            }
128            (_, Some(_)) => {
129                let err = M2dirFlagSetError::UnexpectedArg;
130                M2dirCoroutineState::Complete(Err(err))
131            }
132            (_, None) => {
133                let err = M2dirFlagSetError::MissingArg;
134                M2dirCoroutineState::Complete(Err(err))
135            }
136        }
137    }
138}
139
140enum State {
141    Start,
142    Done,
143}
144
145impl fmt::Display for State {
146    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
147        match self {
148            Self::Start => f.write_str("start"),
149            Self::Done => f.write_str("done"),
150        }
151    }
152}
153
154#[cfg(test)]
155mod tests {
156    use alloc::vec::Vec;
157
158    use super::*;
159
160    #[test]
161    fn non_empty_flags_yields_file_create_then_completes_ok() {
162        let m2dir = M2dir::from_path("/tmp/inbox");
163        let mut flags = M2dirFlags::default();
164        flags.insert("$seen");
165
166        let mut set = M2dirFlagSet::new(&m2dir, "entry", flags, M2dirFlagSetOptions::default());
167
168        let files = expect_wants_file_create(&mut set, None);
169        assert_eq!(files.len(), 1);
170        let (path, bytes) = files.into_iter().next().unwrap();
171        assert!(path.as_str().contains("entry.flags"));
172        assert_eq!(str::from_utf8(&bytes).unwrap(), "$seen\n");
173
174        expect_complete_ok(&mut set, Some(M2dirArg::FileCreate));
175    }
176
177    #[test]
178    fn empty_flags_yields_file_remove_then_completes_ok() {
179        let m2dir = M2dir::from_path("/tmp/inbox");
180        let mut set = M2dirFlagSet::new(
181            &m2dir,
182            "entry",
183            M2dirFlags::default(),
184            M2dirFlagSetOptions::default(),
185        );
186
187        let paths = expect_wants_file_remove(&mut set, None);
188        assert_eq!(paths.len(), 1);
189
190        expect_complete_ok(&mut set, Some(M2dirArg::FileRemove));
191    }
192
193    #[test]
194    fn missing_arg_at_done_returns_missing_arg_error() {
195        let m2dir = M2dir::from_path("/tmp/inbox");
196        let mut flags = M2dirFlags::default();
197        flags.insert("$seen");
198
199        let mut set = M2dirFlagSet::new(&m2dir, "entry", flags, M2dirFlagSetOptions::default());
200        let _ = expect_wants_file_create(&mut set, None);
201
202        let err = expect_complete_err(&mut set, None);
203        assert!(matches!(err, M2dirFlagSetError::MissingArg));
204    }
205
206    #[test]
207    fn unexpected_arg_at_start_returns_unexpected_arg_error() {
208        let m2dir = M2dir::from_path("/tmp/inbox");
209        let mut set = M2dirFlagSet::new(
210            &m2dir,
211            "entry",
212            M2dirFlags::default(),
213            M2dirFlagSetOptions::default(),
214        );
215
216        let err = expect_complete_err(&mut set, Some(M2dirArg::FileCreate));
217        assert!(matches!(err, M2dirFlagSetError::UnexpectedArg));
218    }
219
220    #[test]
221    fn unexpected_arg_kind_at_done_returns_unexpected_arg_error() {
222        let m2dir = M2dir::from_path("/tmp/inbox");
223        let mut flags = M2dirFlags::default();
224        flags.insert("$seen");
225
226        let mut set = M2dirFlagSet::new(&m2dir, "entry", flags, M2dirFlagSetOptions::default());
227        let _ = expect_wants_file_create(&mut set, None);
228
229        let err = expect_complete_err(&mut set, Some(M2dirArg::DirRemove));
230        assert!(matches!(err, M2dirFlagSetError::UnexpectedArg));
231    }
232
233    fn expect_wants_file_create(
234        cor: &mut M2dirFlagSet,
235        arg: Option<M2dirArg>,
236    ) -> BTreeMap<M2dirPath, Vec<u8>> {
237        match cor.resume(arg) {
238            M2dirCoroutineState::Yielded(M2dirYield::WantsFileCreate(files)) => files,
239            state => panic!("expected WantsFileCreate, got {state:?}"),
240        }
241    }
242
243    fn expect_wants_file_remove(
244        cor: &mut M2dirFlagSet,
245        arg: Option<M2dirArg>,
246    ) -> BTreeSet<M2dirPath> {
247        match cor.resume(arg) {
248            M2dirCoroutineState::Yielded(M2dirYield::WantsFileRemove(paths)) => paths,
249            state => panic!("expected WantsFileRemove, got {state:?}"),
250        }
251    }
252
253    fn expect_complete_ok(cor: &mut M2dirFlagSet, arg: Option<M2dirArg>) {
254        match cor.resume(arg) {
255            M2dirCoroutineState::Complete(Ok(())) => {}
256            state => panic!("expected Complete(Ok), got {state:?}"),
257        }
258    }
259
260    fn expect_complete_err(cor: &mut M2dirFlagSet, arg: Option<M2dirArg>) -> M2dirFlagSetError {
261        match cor.resume(arg) {
262            M2dirCoroutineState::Complete(Err(err)) => err,
263            state => panic!("expected Complete(Err), got {state:?}"),
264        }
265    }
266}