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 coroutine::*,
79 entry::{
80 list::*,
81 types::{M2dirEntry, ParseFilenameError},
82 utils::validate_checksum,
83 },
84 m2dir::types::M2dir,
85 path::M2dirPath,
86};
87
88#[derive(Clone, Debug, Error)]
90pub enum M2dirEntryGetError {
91 #[error("M2DIR GET failed: unexpected coroutine arg")]
92 UnexpectedArg,
93 #[error("M2DIR GET failed: missing coroutine arg")]
94 MissingArg,
95 #[error("M2DIR GET failed: entry {0} not found")]
96 NotFound(String),
97 #[error("M2DIR GET failed: {0}")]
98 List(#[from] M2dirEntryListError),
99 #[error("M2DIR GET failed: {0}")]
100 Parse(#[from] ParseFilenameError),
101}
102
103#[derive(Clone, Debug, Default, Eq, PartialEq)]
105pub struct M2dirEntryGetOptions {}
106
107#[derive(Clone, Debug)]
109pub struct M2dirEntryGetOutput {
110 pub entry: M2dirEntry,
112 pub contents: Vec<u8>,
114}
115
116pub struct M2dirEntryGet {
118 id: String,
119 state: State,
120 #[allow(dead_code)]
121 opts: M2dirEntryGetOptions,
122}
123
124impl M2dirEntryGet {
125 pub fn new(m2dir: M2dir, id: impl ToString, opts: M2dirEntryGetOptions) -> Self {
128 Self {
129 id: id.to_string(),
130 state: State::List(M2dirEntryList::new(m2dir, M2dirEntryListOptions::default())),
131 opts,
132 }
133 }
134}
135
136impl M2dirCoroutine for M2dirEntryGet {
137 type Yield = M2dirYield;
138 type Return = Result<M2dirEntryGetOutput, M2dirEntryGetError>;
139
140 fn resume(&mut self, arg: Option<M2dirArg>) -> M2dirCoroutineState<Self::Yield, Self::Return> {
141 trace!("get entry: {}", self.state);
142
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 = ParseFilenameError::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}