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::types::M2dirEntry, m2dir::types::M2dir, path::M2dirPath};
71
72#[derive(Clone, Debug, Error)]
74pub enum M2dirEntryListError {
75 #[error("M2DIR LIST failed: unexpected coroutine arg")]
76 UnexpectedArg,
77 #[error("M2DIR LIST failed: missing coroutine arg")]
78 MissingArg,
79}
80
81#[derive(Clone, Debug, Default, Eq, PartialEq)]
83pub struct M2dirEntryListOptions {}
84
85pub struct M2dirEntryList {
87 m2dir: M2dir,
88 state: State,
89 #[allow(dead_code)]
90 opts: M2dirEntryListOptions,
91}
92
93impl M2dirEntryList {
94 pub fn new(m2dir: M2dir, opts: M2dirEntryListOptions) -> Self {
97 Self {
98 m2dir,
99 state: State::Start,
100 opts,
101 }
102 }
103}
104
105impl M2dirCoroutine for M2dirEntryList {
106 type Yield = M2dirYield;
107 type Return = Result<Vec<M2dirEntry>, M2dirEntryListError>;
108
109 fn resume(&mut self, arg: Option<M2dirArg>) -> M2dirCoroutineState<Self::Yield, Self::Return> {
110 trace!("list entries: {}", self.state);
111
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(
270 cor: &mut M2dirEntryList,
271 arg: Option<M2dirArg>,
272 ) -> BTreeSet<M2dirPath> {
273 match cor.resume(arg) {
274 M2dirCoroutineState::Yielded(M2dirYield::WantsDirRead(paths)) => paths,
275 state => panic!("expected WantsDirRead, got {state:?}"),
276 }
277 }
278
279 fn expect_complete_err(cor: &mut M2dirEntryList, arg: Option<M2dirArg>) -> M2dirEntryListError {
280 match cor.resume(arg) {
281 M2dirCoroutineState::Complete(Err(err)) => err,
282 state => panic!("expected Complete(Err), got {state:?}"),
283 }
284 }
285}