1use core::{fmt, mem};
53
54use alloc::collections::{BTreeMap, BTreeSet};
55
56use log::trace;
57use thiserror::Error;
58
59use crate::{
60 coroutine::*,
61 item::{VdirItem, VdirItemKind},
62 path::VdirPath,
63};
64
65#[derive(Clone, Debug, Error)]
67pub enum VdirItemListError {
68 #[error("Vdir item list failed: unexpected arg {0:?}")]
71 UnexpectedArg(Option<VdirReply>),
72}
73
74#[derive(Clone, Debug, Default, Eq, PartialEq)]
76pub struct VdirItemListOptions {}
77
78#[derive(Debug)]
80pub struct VdirItemList {
81 state: State,
82 #[allow(dead_code)]
83 opts: VdirItemListOptions,
84}
85
86impl VdirItemList {
87 pub fn new(collection: impl Into<VdirPath>, opts: VdirItemListOptions) -> Self {
90 Self {
91 opts,
92 state: State::Start(collection.into()),
93 }
94 }
95}
96
97impl VdirCoroutine for VdirItemList {
98 type Yield = VdirYield;
99 type Return = Result<BTreeSet<VdirItem>, VdirItemListError>;
100
101 fn resume(&mut self, arg: Option<VdirReply>) -> VdirCoroutineState<Self::Yield, Self::Return> {
102 match (&mut self.state, arg) {
103 (State::Start(collection), None) => {
104 let paths = BTreeSet::from_iter([mem::take(collection)]);
105 self.state = State::AwaitDirRead;
106 VdirCoroutineState::Yielded(VdirYield::WantsDirRead(paths))
107 }
108 (State::AwaitDirRead, Some(VdirReply::DirRead(entries))) => {
109 let mut kinds: BTreeMap<VdirPath, VdirItemKind> = BTreeMap::new();
110
111 for paths in entries.into_values() {
112 for path in paths {
113 let Some(name) = path.file_name() else {
114 continue;
115 };
116
117 let Some((_, ext)) = name.rsplit_once('.') else {
118 continue;
119 };
120
121 let Some(kind) = VdirItemKind::from_extension(ext) else {
122 continue;
123 };
124
125 kinds.insert(path, kind);
126 }
127 }
128
129 if kinds.is_empty() {
130 return VdirCoroutineState::Complete(Ok(BTreeSet::new()));
131 }
132
133 let probes: BTreeSet<VdirPath> = kinds.keys().cloned().collect();
134 self.state = State::AwaitFileRead { kinds };
135 VdirCoroutineState::Yielded(VdirYield::WantsFileRead(probes))
136 }
137 (State::AwaitFileRead { kinds }, Some(VdirReply::FileRead(mut contents))) => {
138 let mut items = BTreeSet::new();
139
140 for (path, kind) in mem::take(kinds) {
141 let bytes = contents.remove(&path).unwrap_or_default();
142 items.insert(VdirItem {
143 path,
144 kind,
145 contents: bytes,
146 });
147 }
148
149 trace!("listed {} items", items.len());
150 VdirCoroutineState::Complete(Ok(items))
151 }
152 (_, arg) => {
153 let err = VdirItemListError::UnexpectedArg(arg);
154 VdirCoroutineState::Complete(Err(err))
155 }
156 }
157 }
158}
159
160#[derive(Debug)]
161enum State {
162 Start(VdirPath),
163 AwaitDirRead,
164 AwaitFileRead {
165 kinds: BTreeMap<VdirPath, VdirItemKind>,
166 },
167}
168
169impl fmt::Display for State {
170 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
171 match self {
172 Self::Start(_) => f.write_str("start"),
173 Self::AwaitDirRead => f.write_str("await dir read reply"),
174 Self::AwaitFileRead { .. } => f.write_str("await file read reply"),
175 }
176 }
177}
178
179#[cfg(test)]
180mod tests {
181 use super::*;
182
183 #[test]
184 fn reads_only_vcf_and_ics_entries() {
185 let mut cor = VdirItemList::new("root/contacts", VdirItemListOptions::default());
186
187 let paths = expect_wants_dir_read(&mut cor);
188 assert!(paths.contains(&VdirPath::from("root/contacts")));
189
190 let vcf = VdirPath::from("root/contacts/alice.vcf");
191 let mut entries = BTreeMap::new();
192 entries.insert(
193 VdirPath::from("root/contacts"),
194 BTreeSet::from_iter([
195 vcf.clone(),
196 VdirPath::from("root/contacts/displayname"),
197 VdirPath::from("root/contacts/alice.vcf.tmp"),
198 ]),
199 );
200
201 let probes = match cor.resume(Some(VdirReply::DirRead(entries))) {
202 VdirCoroutineState::Yielded(VdirYield::WantsFileRead(probes)) => probes,
203 state => panic!("expected WantsFileRead, got {state:?}"),
204 };
205 assert_eq!(probes.len(), 1);
206 assert!(probes.contains(&vcf));
207
208 let mut contents = BTreeMap::new();
209 contents.insert(vcf.clone(), b"BEGIN:VCARD".to_vec());
210 let items = match cor.resume(Some(VdirReply::FileRead(contents))) {
211 VdirCoroutineState::Complete(Ok(items)) => items,
212 state => panic!("expected Complete(Ok), got {state:?}"),
213 };
214 assert_eq!(items.len(), 1);
215 assert_eq!(items.iter().next().unwrap().kind, VdirItemKind::Vcard);
216 }
217
218 #[test]
219 fn empty_collection_yields_no_items() {
220 let mut cor = VdirItemList::new("root/contacts", VdirItemListOptions::default());
221 let _ = expect_wants_dir_read(&mut cor);
222
223 let mut entries = BTreeMap::new();
224 entries.insert(VdirPath::from("root/contacts"), BTreeSet::new());
225 let items = match cor.resume(Some(VdirReply::DirRead(entries))) {
226 VdirCoroutineState::Complete(Ok(items)) => items,
227 state => panic!("expected Complete(Ok), got {state:?}"),
228 };
229 assert!(items.is_empty());
230 }
231
232 #[test]
233 fn unexpected_reply_returns_error() {
234 let mut cor = VdirItemList::new("root/contacts", VdirItemListOptions::default());
235 let _ = expect_wants_dir_read(&mut cor);
236
237 let err = match cor.resume(Some(VdirReply::DirCreate)) {
238 VdirCoroutineState::Complete(Err(err)) => err,
239 state => panic!("expected Complete(Err), got {state:?}"),
240 };
241 assert!(matches!(err, VdirItemListError::UnexpectedArg(_)));
242 }
243
244 fn expect_wants_dir_read(cor: &mut VdirItemList) -> BTreeSet<VdirPath> {
245 match cor.resume(None) {
246 VdirCoroutineState::Yielded(VdirYield::WantsDirRead(paths)) => paths,
247 state => panic!("expected WantsDirRead, got {state:?}"),
248 }
249 }
250}