1use core::{fmt, str};
59
60use alloc::collections::{BTreeMap, BTreeSet};
61
62use log::trace;
63use thiserror::Error;
64
65use crate::{coroutine::*, flag::types::M2dirFlags, m2dir::types::M2dir, path::M2dirPath};
66
67#[derive(Clone, Debug, Error)]
69pub enum M2dirFlagRemoveError {
70 #[error("M2DIR REMOVE FLAGS failed: unexpected coroutine arg")]
71 UnexpectedArg,
72 #[error("M2DIR REMOVE FLAGS failed: missing coroutine arg")]
73 MissingArg,
74}
75
76#[derive(Clone, Debug, Default, Eq, PartialEq)]
78pub struct M2dirFlagRemoveOptions {}
79
80pub struct M2dirFlagRemove {
82 flags_path: M2dirPath,
83 flags: M2dirFlags,
84 state: State,
85 #[allow(dead_code)]
86 opts: M2dirFlagRemoveOptions,
87}
88
89impl M2dirFlagRemove {
90 pub fn new(
93 m2dir: &M2dir,
94 id: impl AsRef<str>,
95 flags: M2dirFlags,
96 opts: M2dirFlagRemoveOptions,
97 ) -> Self {
98 Self {
99 flags_path: m2dir.flags_path(id.as_ref()),
100 flags,
101 state: State::Start,
102 opts,
103 }
104 }
105}
106
107impl M2dirCoroutine for M2dirFlagRemove {
108 type Yield = M2dirYield;
109 type Return = Result<(), M2dirFlagRemoveError>;
110
111 fn resume(&mut self, arg: Option<M2dirArg>) -> M2dirCoroutineState<Self::Yield, Self::Return> {
112 trace!("remove flags: {}", self.state);
113
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(
285 cor: &mut M2dirFlagRemove,
286 arg: Option<M2dirArg>,
287 ) -> BTreeSet<M2dirPath> {
288 match cor.resume(arg) {
289 M2dirCoroutineState::Yielded(M2dirYield::WantsFileRead(paths)) => paths,
290 state => panic!("expected WantsFileRead, got {state:?}"),
291 }
292 }
293
294 fn expect_wants_file_create(
295 cor: &mut M2dirFlagRemove,
296 arg: Option<M2dirArg>,
297 ) -> BTreeMap<M2dirPath, Vec<u8>> {
298 match cor.resume(arg) {
299 M2dirCoroutineState::Yielded(M2dirYield::WantsFileCreate(files)) => files,
300 state => panic!("expected WantsFileCreate, got {state:?}"),
301 }
302 }
303
304 fn expect_wants_file_remove(
305 cor: &mut M2dirFlagRemove,
306 arg: Option<M2dirArg>,
307 ) -> BTreeSet<M2dirPath> {
308 match cor.resume(arg) {
309 M2dirCoroutineState::Yielded(M2dirYield::WantsFileRemove(paths)) => paths,
310 state => panic!("expected WantsFileRemove, got {state:?}"),
311 }
312 }
313
314 fn expect_complete_ok(cor: &mut M2dirFlagRemove, arg: Option<M2dirArg>) {
315 match cor.resume(arg) {
316 M2dirCoroutineState::Complete(Ok(())) => {}
317 state => panic!("expected Complete(Ok), got {state:?}"),
318 }
319 }
320
321 fn expect_complete_err(
322 cor: &mut M2dirFlagRemove,
323 arg: Option<M2dirArg>,
324 ) -> M2dirFlagRemoveError {
325 match cor.resume(arg) {
326 M2dirCoroutineState::Complete(Err(err)) => err,
327 state => panic!("expected Complete(Err), got {state:?}"),
328 }
329 }
330}