1use core::{fmt, mem};
57
58use alloc::collections::{BTreeMap, BTreeSet};
59
60use log::trace;
61use thiserror::Error;
62
63use crate::{
64 coroutine::*,
65 m2dir::types::{DOT_M2DIR, M2dir},
66 path::M2dirPath,
67 store::M2dirStore,
68};
69
70#[derive(Clone, Debug, Error)]
72pub enum M2dirListError {
73 #[error("M2DIR LIST failed: unexpected coroutine arg")]
74 UnexpectedArg,
75 #[error("M2DIR LIST failed: missing coroutine arg")]
76 MissingArg,
77}
78
79#[derive(Clone, Debug, Default, Eq, PartialEq)]
81pub struct M2dirListOptions {}
82
83pub struct M2dirList {
85 state: State,
86 #[allow(dead_code)]
87 opts: M2dirListOptions,
88}
89
90impl M2dirList {
91 pub fn new(store: &M2dirStore, opts: M2dirListOptions) -> Self {
94 let pending = BTreeSet::from_iter([store.path().clone()]);
95 Self {
96 state: State::Scanning {
97 pending,
98 found: BTreeSet::new(),
99 },
100 opts,
101 }
102 }
103}
104
105impl M2dirCoroutine for M2dirList {
106 type Yield = M2dirYield;
107 type Return = Result<BTreeSet<M2dir>, M2dirListError>;
108
109 fn resume(&mut self, arg: Option<M2dirArg>) -> M2dirCoroutineState<Self::Yield, Self::Return> {
110 trace!("list m2dirs: {}", self.state);
111
112 match (&mut self.state, arg) {
113 (State::Scanning { pending, .. }, None) => {
114 let batch = mem::take(pending);
115 trace!("wants read of {} directories", batch.len());
116 M2dirCoroutineState::Yielded(M2dirYield::WantsDirRead(batch))
117 }
118 (State::Scanning { pending, found }, Some(M2dirArg::DirRead(entries))) => {
119 trace!("scanned {} directories", entries.len());
120
121 let mut markers = BTreeMap::new();
122 let mut next_pending = mem::take(pending);
123 let found = mem::take(found);
124
125 for (_dir, names) in entries {
126 for path in names {
127 let Some(name) = path.file_name() else {
128 continue;
129 };
130
131 if name.starts_with('.') {
132 continue;
133 }
134
135 let marker = path.join(DOT_M2DIR);
136 markers.insert(marker, path.clone());
137 next_pending.insert(path);
138 }
139 }
140
141 if markers.is_empty() {
142 if next_pending.is_empty() {
143 trace!("found {} m2dirs", found.len());
144 return M2dirCoroutineState::Complete(Ok(found));
145 }
146
147 let batch = next_pending;
148 self.state = State::Scanning {
149 pending: BTreeSet::new(),
150 found,
151 };
152 return M2dirCoroutineState::Yielded(M2dirYield::WantsDirRead(batch));
153 }
154
155 let probes: BTreeSet<M2dirPath> = markers.keys().cloned().collect();
156 trace!("wants existence check for {} markers", probes.len());
157
158 self.state = State::CheckingMarkers {
159 next_pending,
160 markers,
161 found,
162 };
163 M2dirCoroutineState::Yielded(M2dirYield::WantsFileExists(probes))
164 }
165 (
166 State::CheckingMarkers {
167 next_pending,
168 markers,
169 found,
170 },
171 Some(M2dirArg::FileExists(probes)),
172 ) => {
173 let next_pending = mem::take(next_pending);
174 let markers = mem::take(markers);
175 let mut found = mem::take(found);
176
177 for (marker, dir) in markers {
178 if probes.get(&marker).copied().unwrap_or(false) {
179 found.insert(M2dir::from_path(dir));
180 }
181 }
182
183 if next_pending.is_empty() {
184 trace!("found {} m2dirs", found.len());
185 return M2dirCoroutineState::Complete(Ok(found));
186 }
187
188 let batch = next_pending;
189 trace!("wants read of {} directories", batch.len());
190
191 self.state = State::Scanning {
192 pending: BTreeSet::new(),
193 found,
194 };
195 M2dirCoroutineState::Yielded(M2dirYield::WantsDirRead(batch))
196 }
197 (_, Some(_)) => {
198 let err = M2dirListError::UnexpectedArg;
199 M2dirCoroutineState::Complete(Err(err))
200 }
201 (_, None) => {
202 let err = M2dirListError::MissingArg;
203 M2dirCoroutineState::Complete(Err(err))
204 }
205 }
206 }
207}
208
209enum State {
210 Scanning {
211 pending: BTreeSet<M2dirPath>,
212 found: BTreeSet<M2dir>,
213 },
214 CheckingMarkers {
215 next_pending: BTreeSet<M2dirPath>,
216 markers: BTreeMap<M2dirPath, M2dirPath>,
217 found: BTreeSet<M2dir>,
218 },
219}
220
221impl fmt::Display for State {
222 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
223 match self {
224 Self::Scanning { .. } => f.write_str("scanning"),
225 Self::CheckingMarkers { .. } => f.write_str("checking markers"),
226 }
227 }
228}
229
230#[cfg(test)]
231mod tests {
232 use super::*;
233
234 #[test]
235 fn empty_store_returns_no_mailboxes() {
236 let store = M2dirStore::from_path("/tmp/store");
237 let mut list = M2dirList::new(&store, M2dirListOptions::default());
238
239 let batch = expect_wants_dir_read(&mut list, None);
240 let dir = batch.into_iter().next().unwrap();
241 let mut reply = BTreeMap::new();
242 reply.insert(dir, BTreeSet::new());
243
244 let mboxes = match list.resume(Some(M2dirArg::DirRead(reply))) {
245 M2dirCoroutineState::Complete(Ok(mboxes)) => mboxes,
246 state => panic!("expected Complete(Ok), got {state:?}"),
247 };
248 assert!(mboxes.is_empty());
249 }
250
251 #[test]
252 fn directory_without_marker_is_skipped() {
253 let store = M2dirStore::from_path("/tmp/store");
254 let mut list = M2dirList::new(&store, M2dirListOptions::default());
255
256 let _ = expect_wants_dir_read(&mut list, None);
257
258 let mut names = BTreeSet::new();
259 names.insert(M2dirPath::from("/tmp/store/maybe"));
260 let mut reply = BTreeMap::new();
261 reply.insert(M2dirPath::from("/tmp/store"), names);
262
263 let probes = expect_wants_file_exists(&mut list, Some(M2dirArg::DirRead(reply)));
264 let exists: BTreeMap<M2dirPath, bool> = probes.into_iter().map(|p| (p, false)).collect();
265
266 let _ = expect_wants_dir_read(&mut list, Some(M2dirArg::FileExists(exists)));
267 }
268
269 #[test]
270 fn directory_with_marker_is_reported_as_mailbox() {
271 let store = M2dirStore::from_path("/tmp/store");
272 let mut list = M2dirList::new(&store, M2dirListOptions::default());
273
274 let _ = expect_wants_dir_read(&mut list, None);
275
276 let mut names = BTreeSet::new();
277 names.insert(M2dirPath::from("/tmp/store/inbox"));
278 let mut reply = BTreeMap::new();
279 reply.insert(M2dirPath::from("/tmp/store"), names);
280
281 let probes = expect_wants_file_exists(&mut list, Some(M2dirArg::DirRead(reply)));
282 let exists: BTreeMap<M2dirPath, bool> = probes.into_iter().map(|p| (p, true)).collect();
283
284 let next_batch = expect_wants_dir_read(&mut list, Some(M2dirArg::FileExists(exists)));
285 let mut reply = BTreeMap::new();
286 for dir in next_batch {
287 reply.insert(dir, BTreeSet::new());
288 }
289 let mboxes = match list.resume(Some(M2dirArg::DirRead(reply))) {
290 M2dirCoroutineState::Complete(Ok(mboxes)) => mboxes,
291 state => panic!("expected Complete(Ok), got {state:?}"),
292 };
293 assert!(
294 mboxes
295 .iter()
296 .any(|m| m.path().as_str() == "/tmp/store/inbox")
297 );
298 }
299
300 #[test]
301 fn unexpected_arg_returns_unexpected_arg_error() {
302 let store = M2dirStore::from_path("/tmp/store");
303 let mut list = M2dirList::new(&store, M2dirListOptions::default());
304
305 let err = expect_complete_err(&mut list, Some(M2dirArg::FileCreate));
306 assert!(matches!(err, M2dirListError::UnexpectedArg));
307 }
308
309 #[test]
310 fn missing_arg_at_checking_markers_returns_missing_arg_error() {
311 let store = M2dirStore::from_path("/tmp/store");
312 let mut list = M2dirList::new(&store, M2dirListOptions::default());
313
314 let _ = expect_wants_dir_read(&mut list, None);
315
316 let mut names = BTreeSet::new();
317 names.insert(M2dirPath::from("/tmp/store/maybe"));
318 let mut reply = BTreeMap::new();
319 reply.insert(M2dirPath::from("/tmp/store"), names);
320
321 let _ = expect_wants_file_exists(&mut list, Some(M2dirArg::DirRead(reply)));
322
323 let err = expect_complete_err(&mut list, None);
324 assert!(matches!(err, M2dirListError::MissingArg));
325 }
326
327 fn expect_wants_dir_read(cor: &mut M2dirList, arg: Option<M2dirArg>) -> BTreeSet<M2dirPath> {
330 match cor.resume(arg) {
331 M2dirCoroutineState::Yielded(M2dirYield::WantsDirRead(paths)) => paths,
332 state => panic!("expected WantsDirRead, got {state:?}"),
333 }
334 }
335
336 fn expect_wants_file_exists(cor: &mut M2dirList, arg: Option<M2dirArg>) -> BTreeSet<M2dirPath> {
337 match cor.resume(arg) {
338 M2dirCoroutineState::Yielded(M2dirYield::WantsFileExists(probes)) => probes,
339 state => panic!("expected WantsFileExists, got {state:?}"),
340 }
341 }
342
343 fn expect_complete_err(cor: &mut M2dirList, arg: Option<M2dirArg>) -> M2dirListError {
344 match cor.resume(arg) {
345 M2dirCoroutineState::Complete(Err(err)) => err,
346 state => panic!("expected Complete(Err), got {state:?}"),
347 }
348 }
349}