1use std::collections::{BTreeMap, BTreeSet};
17use std::io;
18use std::path::{Path, PathBuf};
19use std::sync::Arc;
20use std::time::SystemTime;
21
22use parking_lot::RwLock;
23
24use super::{Availability, Entry, EntryKind, Vault, VaultError, VaultPath};
25
26#[derive(Debug, Clone)]
27struct MemFile {
28 bytes: Arc<[u8]>,
29 modified: SystemTime,
30 created: SystemTime,
31 availability: Availability,
32}
33
34#[derive(Debug, Default)]
35struct MemState {
36 files: BTreeMap<VaultPath, MemFile>,
37 dirs: BTreeSet<VaultPath>,
39 unlisted_dirs: BTreeSet<VaultPath>,
42 downloads: Vec<VaultPath>,
43 downloads_held: bool,
46}
47
48impl MemState {
49 fn add_ancestors(&mut self, path: &VaultPath) {
50 let mut parent = path.parent();
51 while let Some(dir) = parent {
52 parent = dir.parent();
53 if !self.dirs.insert(dir) {
54 break;
55 }
56 }
57 }
58}
59
60#[derive(Debug)]
63pub struct MemVault {
64 root: PathBuf,
65 state: RwLock<MemState>,
66}
67
68impl Default for MemVault {
69 fn default() -> Self {
70 Self::new()
71 }
72}
73
74fn fixture_path(path: &str) -> VaultPath {
77 VaultPath::new(path).unwrap_or_else(|e| panic!("invalid MemVault path {path:?}: {e}"))
78}
79
80impl MemVault {
81 pub const DEFAULT_ROOT: &'static str = "/mbr-mem-vault";
85
86 pub fn new() -> Self {
88 Self::with_root(Self::DEFAULT_ROOT)
89 }
90
91 pub fn with_root(root: impl Into<PathBuf>) -> Self {
93 let state = MemState {
94 dirs: BTreeSet::from([VaultPath::root()]),
95 ..MemState::default()
96 };
97 Self {
98 root: root.into(),
99 state: RwLock::new(state),
100 }
101 }
102
103 #[must_use]
105 pub fn with_file(self, path: &str, contents: impl AsRef<[u8]>) -> Self {
106 self.insert_file(path, contents);
107 self
108 }
109
110 #[must_use]
112 pub fn with_dir(self, path: &str) -> Self {
113 self.insert_dir(path);
114 self
115 }
116
117 pub fn insert_file(&self, path: &str, contents: impl AsRef<[u8]>) {
123 let path = fixture_path(path);
124 let now = SystemTime::now();
125 let mut state = self.state.write();
126 state.add_ancestors(&path);
127 let created = state.files.get(&path).map_or(now, |f| f.created);
128 state.files.insert(
129 path,
130 MemFile {
131 bytes: Arc::from(contents.as_ref()),
132 modified: now,
133 created,
134 availability: Availability::Local,
135 },
136 );
137 }
138
139 pub fn insert_dir(&self, path: &str) {
145 let path = fixture_path(path);
146 let mut state = self.state.write();
147 state.add_ancestors(&path);
148 state.dirs.insert(path);
149 }
150
151 pub fn set_availability(&self, path: &str, availability: Availability) {
157 let path = fixture_path(path);
158 let mut state = self.state.write();
159 let file = state
160 .files
161 .get_mut(&path)
162 .unwrap_or_else(|| panic!("no MemVault file {path}"));
163 file.availability = availability;
164 }
165
166 pub fn set_dir_availability(&self, path: &str, availability: Availability) {
177 let path = fixture_path(path);
178 let mut state = self.state.write();
179 assert!(state.dirs.contains(&path), "no MemVault directory {path}");
180 match availability {
181 Availability::NotDownloaded => state.unlisted_dirs.insert(path),
182 Availability::Local | Availability::Unknown => state.unlisted_dirs.remove(&path),
183 };
184 }
185
186 pub fn set_modified(&self, path: &str, modified: SystemTime) {
192 let path = fixture_path(path);
193 let mut state = self.state.write();
194 let file = state
195 .files
196 .get_mut(&path)
197 .unwrap_or_else(|| panic!("no MemVault file {path}"));
198 file.modified = modified;
199 }
200
201 pub fn contents(&self, path: &str) -> Option<Vec<u8>> {
203 let path = VaultPath::new(path).ok()?;
204 self.state.read().files.get(&path).map(|f| f.bytes.to_vec())
205 }
206
207 pub fn hold_downloads(&self, held: bool) {
210 self.state.write().downloads_held = held;
211 }
212
213 pub fn download_requests(&self) -> Vec<VaultPath> {
215 self.state.read().downloads.clone()
216 }
217
218 fn file_entry(path: &VaultPath, file: &MemFile) -> Entry {
219 Entry {
220 path: path.clone(),
221 kind: EntryKind::File,
222 size: file.bytes.len() as u64,
223 modified: file.modified,
224 created: Some(file.created),
225 availability: file.availability,
226 }
227 }
228
229 fn dir_entry(path: &VaultPath) -> Entry {
230 Entry {
231 path: path.clone(),
232 kind: EntryKind::Dir,
233 size: 0,
234 modified: SystemTime::UNIX_EPOCH,
235 created: None,
236 availability: Availability::Local,
237 }
238 }
239
240 fn readable(&self, path: &VaultPath) -> Result<Arc<[u8]>, VaultError> {
241 let state = self.state.read();
242 match state.files.get(path) {
243 Some(file) if file.availability == Availability::NotDownloaded => {
244 Err(VaultError::NotDownloaded {
245 path: path.to_string(),
246 })
247 }
248 Some(file) => Ok(Arc::clone(&file.bytes)),
249 None if state.dirs.contains(path) => Err(VaultError::Io {
250 path: path.to_string(),
251 source: io::Error::new(io::ErrorKind::IsADirectory, "is a directory"),
252 }),
253 None => Err(VaultError::not_found(path)),
254 }
255 }
256}
257
258impl Vault for MemVault {
259 fn root(&self) -> &Path {
260 &self.root
261 }
262
263 fn stat(&self, path: &VaultPath) -> Result<Option<Entry>, VaultError> {
264 let state = self.state.read();
265 if let Some(file) = state.files.get(path) {
266 return Ok(Some(Self::file_entry(path, file)));
267 }
268 Ok(state.dirs.contains(path).then(|| Self::dir_entry(path)))
269 }
270
271 fn list_dir(&self, dir: &VaultPath) -> Result<Vec<Entry>, VaultError> {
272 let state = self.state.read();
273 if !state.dirs.contains(dir) {
274 return Err(if state.files.contains_key(dir) {
275 VaultError::Io {
276 path: dir.to_string(),
277 source: io::Error::new(io::ErrorKind::NotADirectory, "not a directory"),
278 }
279 } else {
280 VaultError::not_found(dir)
281 });
282 }
283 if state.unlisted_dirs.contains(dir) {
284 return Err(VaultError::NotDownloaded {
285 path: dir.to_string(),
286 });
287 }
288 let is_child = |p: &VaultPath| p.parent().as_ref() == Some(dir);
289 let dirs = state
290 .dirs
291 .iter()
292 .filter(|p| !p.is_root() && is_child(p))
293 .map(Self::dir_entry);
294 let files = state
295 .files
296 .iter()
297 .filter(|(p, _)| is_child(p))
298 .map(|(p, f)| Self::file_entry(p, f));
299 Ok(dirs.chain(files).collect())
300 }
301
302 fn canonicalize(&self, path: &VaultPath) -> Result<VaultPath, VaultError> {
303 let state = self.state.read();
304 if state.files.contains_key(path) || state.dirs.contains(path) {
305 Ok(path.clone())
306 } else {
307 Err(VaultError::not_found(path))
308 }
309 }
310
311 fn read(&self, path: &VaultPath) -> Result<Vec<u8>, VaultError> {
312 self.readable(path).map(|bytes| bytes.to_vec())
313 }
314
315 fn read_prefix(&self, path: &VaultPath, max_len: usize) -> Result<Vec<u8>, VaultError> {
316 self.readable(path)
317 .map(|bytes| bytes[..bytes.len().min(max_len)].to_vec())
318 }
319
320 fn availability(&self, path: &VaultPath) -> Availability {
321 let state = self.state.read();
322 match state.files.get(path) {
323 Some(file) => file.availability,
324 None if state.dirs.contains(path) => Availability::Local,
325 None => Availability::Unknown,
326 }
327 }
328
329 fn write_atomic(&self, path: &VaultPath, bytes: &[u8]) -> Result<(), VaultError> {
330 let mut state = self.state.write();
331 let parent = path.parent().ok_or_else(|| VaultError::Io {
332 path: path.to_string(),
333 source: io::Error::new(io::ErrorKind::IsADirectory, "is a directory"),
334 })?;
335 if !state.dirs.contains(&parent) {
336 return Err(VaultError::not_found(&parent));
337 }
338 if state.dirs.contains(path) {
339 return Err(VaultError::Io {
340 path: path.to_string(),
341 source: io::Error::new(io::ErrorKind::IsADirectory, "is a directory"),
342 });
343 }
344 let now = SystemTime::now();
345 let created = state.files.get(path).map_or(now, |f| f.created);
346 state.files.insert(
347 path.clone(),
348 MemFile {
349 bytes: Arc::from(bytes),
350 modified: now,
351 created,
352 availability: Availability::Local,
353 },
354 );
355 Ok(())
356 }
357
358 fn create_dir_all(&self, path: &VaultPath) -> Result<(), VaultError> {
359 let mut state = self.state.write();
360 if let Some(blocker) = path.ancestors().find(|a| state.files.contains_key(a)) {
361 return Err(VaultError::Io {
362 path: blocker.to_string(),
363 source: io::Error::new(io::ErrorKind::NotADirectory, "not a directory"),
364 });
365 }
366 state.add_ancestors(path);
367 state.dirs.insert(path.clone());
368 Ok(())
369 }
370
371 fn remove_file(&self, path: &VaultPath) -> Result<(), VaultError> {
372 self.state
373 .write()
374 .files
375 .remove(path)
376 .map(|_| ())
377 .ok_or_else(|| VaultError::not_found(path))
378 }
379
380 fn rename(&self, from: &VaultPath, to: &VaultPath) -> Result<(), VaultError> {
381 let mut state = self.state.write();
382 let parent = to.parent().ok_or_else(|| VaultError::not_found(to))?;
383 if !state.dirs.contains(&parent) {
384 return Err(VaultError::not_found(&parent));
385 }
386 let file = state
387 .files
388 .remove(from)
389 .ok_or_else(|| VaultError::not_found(from))?;
390 state.files.insert(to.clone(), file);
391 Ok(())
392 }
393
394 fn request_download(&self, paths: &[VaultPath]) {
395 let mut state = self.state.write();
396 let deliver = !state.downloads_held;
397 for path in paths {
398 if let Some(file) = state.files.get_mut(path).filter(|_| deliver) {
399 file.availability = Availability::Local;
400 }
401 state.downloads.push(path.clone());
402 }
403 }
404}
405
406#[cfg(test)]
407mod tests {
408 use super::*;
409
410 fn vp(s: &str) -> VaultPath {
411 VaultPath::new(s).unwrap()
412 }
413
414 #[test]
415 fn files_imply_their_directories() {
416 let vault = MemVault::new().with_file("a/b/c.md", "# C");
417 assert!(vault.is_dir(&vp("a")));
418 assert!(vault.is_dir(&vp("a/b")));
419 assert!(vault.is_file(&vp("a/b/c.md")));
420 assert!(vault.is_dir(&VaultPath::root()));
421 assert_eq!(vault.stat(&vp("a/x")).unwrap(), None);
422 }
423
424 #[test]
425 fn list_dir_lists_immediate_children_only() {
426 let vault = MemVault::new()
427 .with_file("a/b/c.md", "c")
428 .with_file("a/d.md", "d")
429 .with_dir("a/empty");
430 let mut names: Vec<String> = vault
431 .list_dir(&vp("a"))
432 .unwrap()
433 .into_iter()
434 .map(|e| e.path.to_string())
435 .collect();
436 names.sort();
437 assert_eq!(names, vec!["a/b", "a/d.md", "a/empty"]);
438 let top: Vec<String> = vault
439 .list_dir(&VaultPath::root())
440 .unwrap()
441 .into_iter()
442 .map(|e| e.path.to_string())
443 .collect();
444 assert_eq!(top, vec!["a"]);
445 assert!(vault.list_dir(&vp("missing")).unwrap_err().is_not_found());
446 assert!(vault.list_dir(&vp("a/d.md")).is_err());
447 }
448
449 #[test]
450 fn reads_prefixes_and_utf8() {
451 let vault = MemVault::new().with_file("x.md", "hello");
452 assert_eq!(vault.read(&vp("x.md")).unwrap(), b"hello");
453 assert_eq!(vault.read_prefix(&vp("x.md"), 2).unwrap(), b"he");
454 assert_eq!(vault.read_prefix(&vp("x.md"), 99).unwrap(), b"hello");
455 assert_eq!(vault.read_to_string(&vp("x.md")).unwrap(), "hello");
456 assert!(vault.read(&vp("y.md")).unwrap_err().is_not_found());
457 assert_eq!(vault.local_path(&vp("x.md")), None);
458 }
459
460 #[test]
461 fn not_downloaded_files_are_listed_but_unreadable_until_requested() {
462 let vault = MemVault::new().with_file("cloud.md", "# Cloud");
463 vault.set_availability("cloud.md", Availability::NotDownloaded);
464 let entry = vault.stat(&vp("cloud.md")).unwrap().unwrap();
465 assert_eq!(entry.availability, Availability::NotDownloaded);
466 assert_eq!(entry.size, 7);
467 assert!(matches!(
468 vault.read(&vp("cloud.md")),
469 Err(VaultError::NotDownloaded { .. })
470 ));
471 vault.request_download(&[vp("cloud.md")]);
472 assert_eq!(vault.download_requests(), vec![vp("cloud.md")]);
473 assert_eq!(vault.read(&vp("cloud.md")).unwrap(), b"# Cloud");
474 }
475
476 #[test]
477 fn held_downloads_are_recorded_but_never_delivered() {
478 let vault = MemVault::new().with_file("cloud.md", "# Cloud");
479 vault.set_availability("cloud.md", Availability::NotDownloaded);
480 vault.hold_downloads(true);
481 vault.request_download(&[vp("cloud.md")]);
482 assert_eq!(vault.download_requests(), vec![vp("cloud.md")]);
483 assert!(matches!(
484 vault.read(&vp("cloud.md")),
485 Err(VaultError::NotDownloaded { .. })
486 ));
487 vault.hold_downloads(false);
488 vault.request_download(&[vp("cloud.md")]);
489 assert_eq!(vault.read(&vp("cloud.md")).unwrap(), b"# Cloud");
490 }
491
492 #[test]
493 fn write_atomic_semantics_match_the_local_vault() {
494 let vault = MemVault::new().with_file("docs/a.md", "old");
495 vault.write_atomic(&vp("docs/a.md"), b"new").unwrap();
496 assert_eq!(vault.contents("docs/a.md").unwrap(), b"new");
497 vault.write_atomic(&vp("docs/b.md"), b"b").unwrap();
498 assert!(vault.is_file(&vp("docs/b.md")));
499 assert!(
501 vault
502 .write_atomic(&vp("nope/x.md"), b"x")
503 .unwrap_err()
504 .is_not_found()
505 );
506 assert!(vault.write_atomic(&vp("docs"), b"x").is_err());
508 vault.create_dir_all(&vp("nope/deeper")).unwrap();
509 vault.write_atomic(&vp("nope/deeper/x.md"), b"x").unwrap();
510 assert!(vault.create_dir_all(&vp("docs/a.md/sub")).is_err());
512 }
513
514 #[test]
515 fn rename_and_remove() {
516 let vault = MemVault::new().with_file("a.md", "a").with_dir("d");
517 vault.rename(&vp("a.md"), &vp("d/a.md")).unwrap();
518 assert!(vault.is_file(&vp("d/a.md")));
519 assert!(!vault.is_file(&vp("a.md")));
520 assert!(vault.rename(&vp("a.md"), &vp("d/b.md")).is_err());
521 assert!(vault.rename(&vp("d/a.md"), &vp("nope/a.md")).is_err());
522 vault.remove_file(&vp("d/a.md")).unwrap();
523 assert!(vault.remove_file(&vp("d/a.md")).is_err());
524 }
525
526 #[test]
527 fn canonicalize_is_identity_for_existing_paths() {
528 let vault = MemVault::new().with_file("a/b.md", "b");
529 assert_eq!(vault.canonicalize(&vp("a/b.md")).unwrap(), vp("a/b.md"));
530 assert_eq!(vault.canonicalize(&vp("a")).unwrap(), vp("a"));
531 assert!(
532 vault
533 .canonicalize(&vp("a/c.md"))
534 .unwrap_err()
535 .is_not_found()
536 );
537 }
538
539 #[test]
540 fn keys_live_under_the_virtual_root() {
541 let vault = MemVault::new();
542 let key = vault.key(&vp("a/b.md"));
543 assert!(key.starts_with(MemVault::DEFAULT_ROOT));
544 assert_eq!(vault.vault_path(&key).unwrap(), vp("a/b.md"));
545 }
546}