1use core::{fmt, mem};
57
58use alloc::collections::{BTreeMap, BTreeSet};
59
60use log::trace;
61use thiserror::Error;
62
63use crate::{
64 coroutine::*,
65 m2dir::{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")]
75 UnexpectedArg,
76 #[error("M2DIR LIST failed: missing coroutine arg")]
78 MissingArg,
79}
80
81#[derive(Clone, Debug, Default, Eq, PartialEq)]
83pub struct M2dirListOptions {}
84
85pub struct M2dirList {
87 state: State,
88 #[allow(dead_code)]
89 opts: M2dirListOptions,
90}
91
92impl M2dirList {
93 pub fn new(store: &M2dirStore, opts: M2dirListOptions) -> Self {
96 let pending = BTreeSet::from_iter([store.path().clone()]);
97 Self {
98 state: State::Scanning {
99 pending,
100 found: BTreeSet::new(),
101 },
102 opts,
103 }
104 }
105}
106
107impl M2dirCoroutine for M2dirList {
108 type Yield = M2dirYield;
109 type Return = Result<BTreeSet<M2dir>, M2dirListError>;
110
111 fn resume(&mut self, arg: Option<M2dirArg>) -> M2dirCoroutineState<Self::Yield, Self::Return> {
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> {
328 match cor.resume(arg) {
329 M2dirCoroutineState::Yielded(M2dirYield::WantsDirRead(paths)) => paths,
330 state => panic!("expected WantsDirRead, got {state:?}"),
331 }
332 }
333
334 fn expect_wants_file_exists(cor: &mut M2dirList, arg: Option<M2dirArg>) -> BTreeSet<M2dirPath> {
335 match cor.resume(arg) {
336 M2dirCoroutineState::Yielded(M2dirYield::WantsFileExists(probes)) => probes,
337 state => panic!("expected WantsFileExists, got {state:?}"),
338 }
339 }
340
341 fn expect_complete_err(cor: &mut M2dirList, arg: Option<M2dirArg>) -> M2dirListError {
342 match cor.resume(arg) {
343 M2dirCoroutineState::Complete(Err(err)) => err,
344 state => panic!("expected Complete(Err), got {state:?}"),
345 }
346 }
347}