1use core::{fmt, mem};
60
61use alloc::{
62 collections::{BTreeMap, BTreeSet},
63 string::{String, ToString},
64 vec::Vec,
65};
66
67use log::trace;
68use thiserror::Error;
69
70use crate::{coroutine::*, entry::M2dirEntry, m2dir::M2dir, path::M2dirPath};
71
72#[derive(Clone, Debug, Error)]
74pub enum M2dirEntryListError {
75 #[error("M2DIR LIST failed: unexpected coroutine arg")]
77 UnexpectedArg,
78 #[error("M2DIR LIST failed: missing coroutine arg")]
80 MissingArg,
81}
82
83#[derive(Clone, Debug, Default, Eq, PartialEq)]
85pub struct M2dirEntryListOptions {}
86
87pub struct M2dirEntryList {
89 m2dir: M2dir,
90 state: State,
91 #[allow(dead_code)]
92 opts: M2dirEntryListOptions,
93}
94
95impl M2dirEntryList {
96 pub fn new(m2dir: M2dir, opts: M2dirEntryListOptions) -> Self {
99 Self {
100 m2dir,
101 state: State::Start,
102 opts,
103 }
104 }
105}
106
107impl M2dirCoroutine for M2dirEntryList {
108 type Yield = M2dirYield;
109 type Return = Result<Vec<M2dirEntry>, M2dirEntryListError>;
110
111 fn resume(&mut self, arg: Option<M2dirArg>) -> M2dirCoroutineState<Self::Yield, Self::Return> {
112 match (&mut self.state, arg) {
113 (State::Start, None) => {
114 trace!("wants directory read of {}", self.m2dir.path());
115 let paths = BTreeSet::from_iter([self.m2dir.path().clone()]);
116 self.state = State::Reading;
117 M2dirCoroutineState::Yielded(M2dirYield::WantsDirRead(paths))
118 }
119 (State::Reading, Some(M2dirArg::DirRead(entries))) => {
120 let mut candidates = BTreeMap::new();
121
122 for (_dir, names) in entries {
123 for path in names {
124 let Some(name) = path.file_name() else {
125 continue;
126 };
127
128 if name.starts_with('.') {
129 continue;
130 }
131
132 let Some(id) = M2dir::parse_filename_id(name) else {
133 trace!("skipping unparseable entry filename: {name}");
134 continue;
135 };
136
137 candidates.insert(path.clone(), id.to_string());
138 }
139 }
140
141 if candidates.is_empty() {
142 trace!("no candidate entries");
143 return M2dirCoroutineState::Complete(Ok(Vec::new()));
144 }
145
146 let probes: BTreeSet<M2dirPath> = candidates.keys().cloned().collect();
147 trace!("wants existence check for {} candidates", probes.len());
148
149 self.state = State::Checking { candidates };
150 M2dirCoroutineState::Yielded(M2dirYield::WantsFileExists(probes))
151 }
152 (State::Checking { candidates }, Some(M2dirArg::FileExists(probes))) => {
153 let candidates = mem::take(candidates);
154 let mut found = Vec::new();
155
156 for (path, id) in candidates {
157 if probes.get(&path).copied().unwrap_or(false) {
158 found.push(M2dirEntry::from_parts(id, path));
159 }
160 }
161
162 trace!("found {} entries", found.len());
163 M2dirCoroutineState::Complete(Ok(found))
164 }
165 (_, Some(_)) => {
166 let err = M2dirEntryListError::UnexpectedArg;
167 M2dirCoroutineState::Complete(Err(err))
168 }
169 (_, None) => {
170 let err = M2dirEntryListError::MissingArg;
171 M2dirCoroutineState::Complete(Err(err))
172 }
173 }
174 }
175}
176
177enum State {
178 Start,
179 Reading,
180 Checking {
181 candidates: BTreeMap<M2dirPath, String>,
182 },
183}
184
185impl fmt::Display for State {
186 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
187 match self {
188 Self::Start => f.write_str("start"),
189 Self::Reading => f.write_str("reading directory"),
190 Self::Checking { .. } => f.write_str("checking candidates"),
191 }
192 }
193}
194
195#[cfg(test)]
196mod tests {
197 use super::*;
198
199 #[test]
200 fn empty_directory_returns_empty_list() {
201 let m2dir = M2dir::from_path("/tmp/inbox");
202 let mut list = M2dirEntryList::new(m2dir, M2dirEntryListOptions::default());
203
204 let probes = expect_wants_dir_read(&mut list, None);
205 let dir = probes.into_iter().next().unwrap();
206 let mut reply = BTreeMap::new();
207 reply.insert(dir, BTreeSet::new());
208
209 let entries = match list.resume(Some(M2dirArg::DirRead(reply))) {
210 M2dirCoroutineState::Complete(Ok(entries)) => entries,
211 state => panic!("expected Complete(Ok), got {state:?}"),
212 };
213 assert!(entries.is_empty());
214 }
215
216 #[test]
217 fn skips_unparseable_filenames_and_dotfiles() {
218 let m2dir = M2dir::from_path("/tmp/inbox");
219 let mut list = M2dirEntryList::new(m2dir, M2dirEntryListOptions::default());
220
221 let probes = expect_wants_dir_read(&mut list, None);
222 let dir = probes.into_iter().next().unwrap();
223
224 let mut names = BTreeSet::new();
225 names.insert(M2dirPath::from("/tmp/inbox/.meta"));
226 names.insert(M2dirPath::from("/tmp/inbox/garbage"));
227
228 let mut reply = BTreeMap::new();
229 reply.insert(dir, names);
230
231 let entries = match list.resume(Some(M2dirArg::DirRead(reply))) {
232 M2dirCoroutineState::Complete(Ok(entries)) => entries,
233 state => panic!("expected Complete(Ok), got {state:?}"),
234 };
235 assert!(entries.is_empty());
236 }
237
238 #[test]
239 fn missing_arg_at_reading_returns_missing_arg_error() {
240 let m2dir = M2dir::from_path("/tmp/inbox");
241 let mut list = M2dirEntryList::new(m2dir, M2dirEntryListOptions::default());
242 let _ = expect_wants_dir_read(&mut list, None);
243
244 let err = expect_complete_err(&mut list, None);
245 assert!(matches!(err, M2dirEntryListError::MissingArg));
246 }
247
248 #[test]
249 fn unexpected_arg_at_start_returns_unexpected_arg_error() {
250 let m2dir = M2dir::from_path("/tmp/inbox");
251 let mut list = M2dirEntryList::new(m2dir, M2dirEntryListOptions::default());
252
253 let err = expect_complete_err(&mut list, Some(M2dirArg::DirCreate));
254 assert!(matches!(err, M2dirEntryListError::UnexpectedArg));
255 }
256
257 #[test]
258 fn unexpected_arg_kind_at_reading_returns_unexpected_arg_error() {
259 let m2dir = M2dir::from_path("/tmp/inbox");
260 let mut list = M2dirEntryList::new(m2dir, M2dirEntryListOptions::default());
261 let _ = expect_wants_dir_read(&mut list, None);
262
263 let err = expect_complete_err(&mut list, Some(M2dirArg::FileCreate));
264 assert!(matches!(err, M2dirEntryListError::UnexpectedArg));
265 }
266
267 fn expect_wants_dir_read(
268 cor: &mut M2dirEntryList,
269 arg: Option<M2dirArg>,
270 ) -> BTreeSet<M2dirPath> {
271 match cor.resume(arg) {
272 M2dirCoroutineState::Yielded(M2dirYield::WantsDirRead(paths)) => paths,
273 state => panic!("expected WantsDirRead, got {state:?}"),
274 }
275 }
276
277 fn expect_complete_err(cor: &mut M2dirEntryList, arg: Option<M2dirArg>) -> M2dirEntryListError {
278 match cor.resume(arg) {
279 M2dirCoroutineState::Complete(Err(err)) => err,
280 state => panic!("expected Complete(Err), got {state:?}"),
281 }
282 }
283}