1use core::{fmt, str};
59
60use alloc::collections::{BTreeMap, BTreeSet};
61
62use log::trace;
63use thiserror::Error;
64
65use crate::{coroutine::*, flag::M2dirFlags, m2dir::M2dir, path::M2dirPath};
66
67#[derive(Clone, Debug, Error)]
69pub enum M2dirFlagRemoveError {
70 #[error("M2DIR REMOVE FLAGS failed: unexpected coroutine arg")]
72 UnexpectedArg,
73 #[error("M2DIR REMOVE FLAGS failed: missing coroutine arg")]
75 MissingArg,
76}
77
78#[derive(Clone, Debug, Default, Eq, PartialEq)]
80pub struct M2dirFlagRemoveOptions {}
81
82pub struct M2dirFlagRemove {
84 flags_path: M2dirPath,
85 flags: M2dirFlags,
86 state: State,
87 #[allow(dead_code)]
88 opts: M2dirFlagRemoveOptions,
89}
90
91impl M2dirFlagRemove {
92 pub fn new(
95 m2dir: &M2dir,
96 id: impl AsRef<str>,
97 flags: M2dirFlags,
98 opts: M2dirFlagRemoveOptions,
99 ) -> Self {
100 Self {
101 flags_path: m2dir.flags_path(id.as_ref()),
102 flags,
103 state: State::Start,
104 opts,
105 }
106 }
107}
108
109impl M2dirCoroutine for M2dirFlagRemove {
110 type Yield = M2dirYield;
111 type Return = Result<(), M2dirFlagRemoveError>;
112
113 fn resume(&mut self, arg: Option<M2dirArg>) -> M2dirCoroutineState<Self::Yield, Self::Return> {
114 match (&self.state, arg) {
115 (State::Start, None) => {
116 trace!("wants existing flags read at {}", self.flags_path);
117 let paths = BTreeSet::from_iter([self.flags_path.clone()]);
118 self.state = State::Read;
119 M2dirCoroutineState::Yielded(M2dirYield::WantsFileRead(paths))
120 }
121 (State::Read, Some(M2dirArg::FileRead(contents))) => {
122 let bytes = contents.into_values().next().unwrap_or_default();
123 let existing = str::from_utf8(&bytes).unwrap_or("");
124
125 let mut remaining = M2dirFlags::from_meta(existing);
126 remaining.difference(&self.flags);
127
128 self.state = State::Done;
129
130 if remaining.is_empty() {
131 trace!("wants flags remove at {}", self.flags_path);
132 let paths = BTreeSet::from_iter([self.flags_path.clone()]);
133 M2dirCoroutineState::Yielded(M2dirYield::WantsFileRemove(paths))
134 } else {
135 trace!(
136 "wants flags write at {} ({} flags)",
137 self.flags_path,
138 remaining.len(),
139 );
140 let serialized = remaining.to_meta().into_bytes();
141 let files = BTreeMap::from_iter([(self.flags_path.clone(), serialized)]);
142 M2dirCoroutineState::Yielded(M2dirYield::WantsFileCreate(files))
143 }
144 }
145 (State::Done, Some(M2dirArg::FileCreate | M2dirArg::FileRemove)) => {
146 trace!("flags removed from {}", self.flags_path);
147 M2dirCoroutineState::Complete(Ok(()))
148 }
149 (_, Some(_)) => {
150 let err = M2dirFlagRemoveError::UnexpectedArg;
151 M2dirCoroutineState::Complete(Err(err))
152 }
153 (_, None) => {
154 let err = M2dirFlagRemoveError::MissingArg;
155 M2dirCoroutineState::Complete(Err(err))
156 }
157 }
158 }
159}
160
161enum State {
162 Start,
163 Read,
164 Done,
165}
166
167impl fmt::Display for State {
168 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
169 match self {
170 Self::Start => f.write_str("start"),
171 Self::Read => f.write_str("read existing flags"),
172 Self::Done => f.write_str("done"),
173 }
174 }
175}
176
177#[cfg(test)]
178mod tests {
179 use alloc::vec::Vec;
180
181 use super::*;
182
183 #[test]
184 fn subtracts_flags_and_writes_remainder() {
185 let m2dir = M2dir::from_path("/tmp/inbox");
186 let mut to_remove = M2dirFlags::default();
187 to_remove.insert("$seen");
188
189 let mut rm = M2dirFlagRemove::new(
190 &m2dir,
191 "entry",
192 to_remove,
193 M2dirFlagRemoveOptions::default(),
194 );
195
196 let probes = expect_wants_file_read(&mut rm, None);
197 let path = probes.into_iter().next().unwrap();
198 let reply = BTreeMap::from_iter([(path, b"$seen\n$forwarded\n".to_vec())]);
199
200 let files = expect_wants_file_create(&mut rm, Some(M2dirArg::FileRead(reply)));
201 let (_, bytes) = files.into_iter().next().unwrap();
202 let serialized = str::from_utf8(&bytes).unwrap();
203 assert!(!serialized.contains("$seen"));
204 assert!(serialized.contains("$forwarded"));
205
206 expect_complete_ok(&mut rm, Some(M2dirArg::FileCreate));
207 }
208
209 #[test]
210 fn empty_remainder_removes_the_flags_file() {
211 let m2dir = M2dir::from_path("/tmp/inbox");
212 let mut to_remove = M2dirFlags::default();
213 to_remove.insert("$seen");
214
215 let mut rm = M2dirFlagRemove::new(
216 &m2dir,
217 "entry",
218 to_remove,
219 M2dirFlagRemoveOptions::default(),
220 );
221
222 let probes = expect_wants_file_read(&mut rm, None);
223 let path = probes.into_iter().next().unwrap();
224 let reply = BTreeMap::from_iter([(path, b"$seen\n".to_vec())]);
225
226 let _ = expect_wants_file_remove(&mut rm, Some(M2dirArg::FileRead(reply)));
227
228 expect_complete_ok(&mut rm, Some(M2dirArg::FileRemove));
229 }
230
231 #[test]
232 fn unexpected_arg_at_start_returns_unexpected_arg_error() {
233 let m2dir = M2dir::from_path("/tmp/inbox");
234 let mut rm = M2dirFlagRemove::new(
235 &m2dir,
236 "entry",
237 M2dirFlags::default(),
238 M2dirFlagRemoveOptions::default(),
239 );
240
241 let err = expect_complete_err(&mut rm, Some(M2dirArg::FileCreate));
242 assert!(matches!(err, M2dirFlagRemoveError::UnexpectedArg));
243 }
244
245 #[test]
246 fn missing_arg_at_read_returns_missing_arg_error() {
247 let m2dir = M2dir::from_path("/tmp/inbox");
248 let mut rm = M2dirFlagRemove::new(
249 &m2dir,
250 "entry",
251 M2dirFlags::default(),
252 M2dirFlagRemoveOptions::default(),
253 );
254 let _ = expect_wants_file_read(&mut rm, None);
255
256 let err = expect_complete_err(&mut rm, None);
257 assert!(matches!(err, M2dirFlagRemoveError::MissingArg));
258 }
259
260 #[test]
261 fn unexpected_arg_kind_at_done_returns_unexpected_arg_error() {
262 let m2dir = M2dir::from_path("/tmp/inbox");
263 let mut to_remove = M2dirFlags::default();
264 to_remove.insert("$seen");
265
266 let mut rm = M2dirFlagRemove::new(
267 &m2dir,
268 "entry",
269 to_remove,
270 M2dirFlagRemoveOptions::default(),
271 );
272
273 let probes = expect_wants_file_read(&mut rm, None);
274 let path = probes.into_iter().next().unwrap();
275 let reply = BTreeMap::from_iter([(path, b"$seen\n".to_vec())]);
276 let _ = expect_wants_file_remove(&mut rm, Some(M2dirArg::FileRead(reply)));
277
278 let err = expect_complete_err(&mut rm, Some(M2dirArg::DirRemove));
279 assert!(matches!(err, M2dirFlagRemoveError::UnexpectedArg));
280 }
281
282 fn expect_wants_file_read(
283 cor: &mut M2dirFlagRemove,
284 arg: Option<M2dirArg>,
285 ) -> BTreeSet<M2dirPath> {
286 match cor.resume(arg) {
287 M2dirCoroutineState::Yielded(M2dirYield::WantsFileRead(paths)) => paths,
288 state => panic!("expected WantsFileRead, got {state:?}"),
289 }
290 }
291
292 fn expect_wants_file_create(
293 cor: &mut M2dirFlagRemove,
294 arg: Option<M2dirArg>,
295 ) -> BTreeMap<M2dirPath, Vec<u8>> {
296 match cor.resume(arg) {
297 M2dirCoroutineState::Yielded(M2dirYield::WantsFileCreate(files)) => files,
298 state => panic!("expected WantsFileCreate, got {state:?}"),
299 }
300 }
301
302 fn expect_wants_file_remove(
303 cor: &mut M2dirFlagRemove,
304 arg: Option<M2dirArg>,
305 ) -> BTreeSet<M2dirPath> {
306 match cor.resume(arg) {
307 M2dirCoroutineState::Yielded(M2dirYield::WantsFileRemove(paths)) => paths,
308 state => panic!("expected WantsFileRemove, got {state:?}"),
309 }
310 }
311
312 fn expect_complete_ok(cor: &mut M2dirFlagRemove, arg: Option<M2dirArg>) {
313 match cor.resume(arg) {
314 M2dirCoroutineState::Complete(Ok(())) => {}
315 state => panic!("expected Complete(Ok), got {state:?}"),
316 }
317 }
318
319 fn expect_complete_err(
320 cor: &mut M2dirFlagRemove,
321 arg: Option<M2dirArg>,
322 ) -> M2dirFlagRemoveError {
323 match cor.resume(arg) {
324 M2dirCoroutineState::Complete(Err(err)) => err,
325 state => panic!("expected Complete(Err), got {state:?}"),
326 }
327 }
328}