1use core::{fmt, mem};
67
68use alloc::{
69 collections::BTreeSet,
70 string::{String, ToString},
71 vec::Vec,
72};
73
74use log::trace;
75use thiserror::Error;
76
77use crate::{
78 checksum::validate_checksum,
79 coroutine::*,
80 entry::{M2dirEntry, M2dirEntryParseError, list::*},
81 m2dir::M2dir,
82 path::M2dirPath,
83};
84
85#[derive(Clone, Debug, Error)]
87pub enum M2dirEntryGetError {
88 #[error("M2DIR GET failed: unexpected coroutine arg")]
90 UnexpectedArg,
91 #[error("M2DIR GET failed: missing coroutine arg")]
93 MissingArg,
94 #[error("M2DIR GET failed: entry {0} not found")]
96 NotFound(String),
97 #[error("M2DIR GET failed: {0}")]
99 List(#[from] M2dirEntryListError),
100 #[error("M2DIR GET failed: {0}")]
102 Parse(#[from] M2dirEntryParseError),
103}
104
105#[derive(Clone, Debug, Default, Eq, PartialEq)]
107pub struct M2dirEntryGetOptions {}
108
109#[derive(Clone, Debug)]
111pub struct M2dirEntryGetOutput {
112 pub entry: M2dirEntry,
114 pub contents: Vec<u8>,
116}
117
118pub struct M2dirEntryGet {
120 id: String,
121 state: State,
122 #[allow(dead_code)]
123 opts: M2dirEntryGetOptions,
124}
125
126impl M2dirEntryGet {
127 pub fn new(m2dir: M2dir, id: impl ToString, opts: M2dirEntryGetOptions) -> Self {
130 Self {
131 id: id.to_string(),
132 state: State::List(M2dirEntryList::new(m2dir, M2dirEntryListOptions::default())),
133 opts,
134 }
135 }
136}
137
138impl M2dirCoroutine for M2dirEntryGet {
139 type Yield = M2dirYield;
140 type Return = Result<M2dirEntryGetOutput, M2dirEntryGetError>;
141
142 fn resume(&mut self, arg: Option<M2dirArg>) -> M2dirCoroutineState<Self::Yield, Self::Return> {
143 match (&mut self.state, arg) {
144 (State::List(list), arg) => match list.resume(arg) {
145 M2dirCoroutineState::Yielded(yld) => M2dirCoroutineState::Yielded(yld),
146 M2dirCoroutineState::Complete(Ok(entries)) => {
147 let Some(entry) = entries.into_iter().find(|e| e.id() == self.id) else {
148 let err = M2dirEntryGetError::NotFound(self.id.clone());
149 return M2dirCoroutineState::Complete(Err(err));
150 };
151
152 trace!("located entry at {}", entry.path());
153
154 let paths = BTreeSet::from_iter([entry.path().clone()]);
155 self.state = State::Read(entry);
156 M2dirCoroutineState::Yielded(M2dirYield::WantsFileRead(paths))
157 }
158 M2dirCoroutineState::Complete(Err(err)) => {
159 M2dirCoroutineState::Complete(Err(err.into()))
160 }
161 },
162 (State::Read(entry), Some(M2dirArg::FileRead(contents))) => {
163 let bytes = contents.into_values().next().unwrap_or_default();
164 let entry = mem::replace(
165 entry,
166 M2dirEntry::from_parts(String::new(), M2dirPath::default()),
167 );
168 let checksum = entry.checksum();
169
170 if !validate_checksum(checksum, &bytes) {
171 let err = M2dirEntryParseError::InvalidChecksum {
172 path: entry.path().clone(),
173 expected: checksum.to_string(),
174 got: entry.id().to_string(),
175 };
176 return M2dirCoroutineState::Complete(Err(err.into()));
177 }
178
179 M2dirCoroutineState::Complete(Ok(M2dirEntryGetOutput {
180 entry,
181 contents: bytes,
182 }))
183 }
184 (_, Some(_)) => {
185 let err = M2dirEntryGetError::UnexpectedArg;
186 M2dirCoroutineState::Complete(Err(err))
187 }
188 (_, None) => {
189 let err = M2dirEntryGetError::MissingArg;
190 M2dirCoroutineState::Complete(Err(err))
191 }
192 }
193 }
194}
195
196enum State {
197 List(M2dirEntryList),
198 Read(M2dirEntry),
199}
200
201impl fmt::Display for State {
202 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
203 match self {
204 Self::List(_) => f.write_str("locate entry"),
205 Self::Read(_) => f.write_str("read entry"),
206 }
207 }
208}
209
210#[cfg(test)]
211mod tests {
212 use alloc::collections::BTreeMap;
213
214 use super::*;
215
216 #[test]
217 fn missing_entry_returns_not_found_error() {
218 let m2dir = M2dir::from_path("/tmp/inbox");
219 let mut get = M2dirEntryGet::new(m2dir, "missing", M2dirEntryGetOptions::default());
220
221 let probes = match get.resume(None) {
222 M2dirCoroutineState::Yielded(M2dirYield::WantsDirRead(paths)) => paths,
223 state => panic!("expected WantsDirRead, got {state:?}"),
224 };
225 let dir = probes.into_iter().next().unwrap();
226 let mut reply = BTreeMap::new();
227 reply.insert(dir, BTreeSet::new());
228
229 let err = match get.resume(Some(M2dirArg::DirRead(reply))) {
230 M2dirCoroutineState::Complete(Err(err)) => err,
231 state => panic!("expected Complete(Err), got {state:?}"),
232 };
233 assert!(matches!(err, M2dirEntryGetError::NotFound(id) if id == "missing"));
234 }
235
236 #[test]
237 fn list_error_propagates_via_from() {
238 let m2dir = M2dir::from_path("/tmp/inbox");
239 let mut get = M2dirEntryGet::new(m2dir, "missing", M2dirEntryGetOptions::default());
240
241 let _ = get.resume(None);
242 let err = match get.resume(Some(M2dirArg::FileCreate)) {
243 M2dirCoroutineState::Complete(Err(err)) => err,
244 state => panic!("expected Complete(Err), got {state:?}"),
245 };
246 assert!(matches!(err, M2dirEntryGetError::List(_)));
247 }
248
249 #[test]
250 fn unexpected_arg_at_read_returns_unexpected_arg_error() {
251 let m2dir = M2dir::from_path("/tmp/inbox");
252 let mut get = M2dirEntryGet::new(m2dir, "foo", M2dirEntryGetOptions::default());
253
254 let err = match get.resume(Some(M2dirArg::FileRead(BTreeMap::new()))) {
255 M2dirCoroutineState::Complete(Err(err)) => err,
256 state => panic!("expected Complete(Err), got {state:?}"),
257 };
258 assert!(matches!(err, M2dirEntryGetError::List(_)));
259 }
260
261 #[test]
262 fn invalid_checksum_returns_parse_error() {
263 let m2dir = M2dir::from_path("/tmp/inbox");
264 let mut get = M2dirEntryGet::new(m2dir, "checksum.nonce", M2dirEntryGetOptions::default());
265
266 let probes = match get.resume(None) {
267 M2dirCoroutineState::Yielded(M2dirYield::WantsDirRead(paths)) => paths,
268 state => panic!("expected WantsDirRead, got {state:?}"),
269 };
270 let dir = probes.into_iter().next().unwrap();
271
272 let mut names = BTreeSet::new();
273 let entry_path = M2dirPath::from("/tmp/inbox/123,checksum.nonce");
274 names.insert(entry_path.clone());
275
276 let mut reply = BTreeMap::new();
277 reply.insert(dir, names);
278
279 let probes = match get.resume(Some(M2dirArg::DirRead(reply))) {
280 M2dirCoroutineState::Yielded(M2dirYield::WantsFileExists(probes)) => probes,
281 state => panic!("expected WantsFileExists, got {state:?}"),
282 };
283 let exists: BTreeMap<M2dirPath, bool> = probes.into_iter().map(|p| (p, true)).collect();
284
285 let read_paths = match get.resume(Some(M2dirArg::FileExists(exists))) {
286 M2dirCoroutineState::Yielded(M2dirYield::WantsFileRead(paths)) => paths,
287 state => panic!("expected WantsFileRead, got {state:?}"),
288 };
289 let read_reply: BTreeMap<M2dirPath, Vec<u8>> = read_paths
290 .into_iter()
291 .map(|p| (p, b"wrong content".to_vec()))
292 .collect();
293
294 let err = match get.resume(Some(M2dirArg::FileRead(read_reply))) {
295 M2dirCoroutineState::Complete(Err(err)) => err,
296 state => panic!("expected Complete(Err), got {state:?}"),
297 };
298 assert!(matches!(err, M2dirEntryGetError::Parse(_)));
299 }
300
301 #[test]
302 fn missing_arg_at_list_propagates_via_list_error() {
303 let m2dir = M2dir::from_path("/tmp/inbox");
304 let mut get = M2dirEntryGet::new(m2dir, "foo", M2dirEntryGetOptions::default());
305 let _ = get.resume(None);
306
307 let err = match get.resume(None) {
308 M2dirCoroutineState::Complete(Err(err)) => err,
309 state => panic!("expected Complete(Err), got {state:?}"),
310 };
311 assert!(matches!(err, M2dirEntryGetError::List(_)));
312 }
313}