1use std::collections::{HashMap, HashSet};
10use std::io::{self, Error, ErrorKind};
11use std::path::{Component, Path, PathBuf};
12use std::sync::{Arc, RwLock};
13
14use prov_graph::fs::{DirEntry, FileType, Metadata, ReadStorage};
15
16use super::{Capabilities, Storage};
17
18#[derive(Debug, Clone, Default)]
46pub struct InMemoryFs {
47 files: Arc<RwLock<HashMap<PathBuf, String>>>,
49 binary_files: Arc<RwLock<HashMap<PathBuf, Vec<u8>>>>,
51 directories: Arc<RwLock<HashSet<PathBuf>>>,
54 symlinks: Arc<RwLock<HashMap<PathBuf, PathBuf>>>,
58}
59
60impl InMemoryFs {
61 pub fn new() -> Self {
63 Self::default()
64 }
65
66 pub fn with_files(entries: Vec<(PathBuf, String)>) -> Self {
69 let fs = Self::new();
70 {
71 let mut files = fs.files.write().unwrap();
72 let mut dirs = fs.directories.write().unwrap();
73 for (path, content) in entries {
74 insert_ancestor_dirs(&mut dirs, &path);
75 files.insert(path, content);
76 }
77 }
78 fs
79 }
80
81 pub fn load_from_entries(entries: Vec<(String, String)>) -> Self {
84 Self::with_files(
85 entries
86 .into_iter()
87 .map(|(path, content)| (PathBuf::from(path), content))
88 .collect(),
89 )
90 }
91
92 pub fn export_entries(&self) -> Vec<(String, String)> {
96 self.files
97 .read()
98 .unwrap()
99 .iter()
100 .map(|(path, content)| (path.to_string_lossy().into_owned(), content.clone()))
101 .collect()
102 }
103
104 pub fn export_binary_entries(&self) -> Vec<(String, Vec<u8>)> {
106 self.binary_files
107 .read()
108 .unwrap()
109 .iter()
110 .map(|(path, content)| (path.to_string_lossy().into_owned(), content.clone()))
111 .collect()
112 }
113
114 pub fn load_binary_entries(&self, entries: Vec<(String, Vec<u8>)>) {
116 let mut binary_files = self.binary_files.write().unwrap();
117 let mut dirs = self.directories.write().unwrap();
118 for (path_str, content) in entries {
119 let path = PathBuf::from(path_str);
120 insert_ancestor_dirs(&mut dirs, &path);
121 binary_files.insert(path, content);
122 }
123 }
124
125 pub fn list_all_files(&self) -> Vec<PathBuf> {
127 self.files.read().unwrap().keys().cloned().collect()
128 }
129
130 pub fn clear(&self) {
134 self.files.write().unwrap().clear();
135 self.binary_files.write().unwrap().clear();
136 self.directories.write().unwrap().clear();
137 self.symlinks.write().unwrap().clear();
138 }
139
140 pub fn add_symlink(&self, link: &Path, target: &Path) {
147 let link = normalize_path(link);
148 let target = normalize_path(target);
149 insert_ancestor_dirs(&mut self.directories.write().unwrap(), &link);
150 self.symlinks.write().unwrap().insert(link, target);
151 }
152
153 fn resolve(&self, normalized: &Path) -> PathBuf {
157 self.symlinks
158 .read()
159 .unwrap()
160 .get(normalized)
161 .cloned()
162 .unwrap_or_else(|| normalized.to_path_buf())
163 }
164}
165
166fn normalize_path(path: &Path) -> PathBuf {
171 let mut components: Vec<Component> = Vec::new();
172 for component in path.components() {
173 match component {
174 Component::CurDir => {}
175 Component::ParentDir => {
176 if !matches!(components.last(), None | Some(Component::RootDir)) {
177 components.pop();
178 }
179 }
180 c => components.push(c),
181 }
182 }
183 components.iter().collect()
184}
185
186fn insert_ancestor_dirs(dirs: &mut HashSet<PathBuf>, path: &Path) {
190 let mut current = path;
191 while let Some(parent) = current.parent() {
192 if parent.as_os_str().is_empty() {
193 break;
194 }
195 dirs.insert(parent.to_path_buf());
196 current = parent;
197 }
198}
199
200fn not_found(path: &Path) -> Error {
201 Error::new(
202 ErrorKind::NotFound,
203 format!("not found: {}", path.display()),
204 )
205}
206
207impl ReadStorage for InMemoryFs {
208 async fn read(&self, path: &Path) -> io::Result<Vec<u8>> {
209 let normalized = normalize_path(path);
210 let resolved = self.resolve(&normalized);
211 if let Some(data) = self.binary_files.read().unwrap().get(&resolved) {
212 return Ok(data.clone());
213 }
214 if let Some(text) = self.files.read().unwrap().get(&resolved) {
215 return Ok(text.as_bytes().to_vec());
216 }
217 Err(not_found(path))
218 }
219
220 async fn read_to_string(&self, path: &Path) -> io::Result<String> {
221 let bytes = self.read(path).await?;
227 String::from_utf8(bytes).map_err(|e| Error::new(ErrorKind::InvalidData, e))
228 }
229
230 async fn read_dir(&self, path: &Path) -> io::Result<Vec<DirEntry>> {
231 let normalized = normalize_path(path);
232 if !normalized.as_os_str().is_empty()
233 && !self.directories.read().unwrap().contains(&normalized)
234 {
235 return Err(not_found(path));
236 }
237
238 let mut result = Vec::new();
239 for entry in self.files.read().unwrap().keys() {
240 if entry.parent() == Some(normalized.as_path()) {
241 result.push(DirEntry::new(entry.clone(), FileType::FILE));
242 }
243 }
244 for entry in self.binary_files.read().unwrap().keys() {
245 if entry.parent() == Some(normalized.as_path()) {
246 result.push(DirEntry::new(entry.clone(), FileType::FILE));
247 }
248 }
249 for entry in self.symlinks.read().unwrap().keys() {
253 if entry.parent() == Some(normalized.as_path()) {
254 result.push(DirEntry::new(entry.clone(), FileType::SYMLINK));
255 }
256 }
257 for entry in self.directories.read().unwrap().iter() {
258 if entry.parent() == Some(normalized.as_path()) && entry != &normalized {
259 result.push(DirEntry::new(entry.clone(), FileType::DIR));
260 }
261 }
262 Ok(result)
263 }
264
265 async fn metadata(&self, path: &Path) -> io::Result<Metadata> {
266 let normalized = normalize_path(path);
267 let resolved = self.resolve(&normalized);
268
269 if let Some(data) = self.binary_files.read().unwrap().get(&resolved) {
270 return Ok(Metadata::new(FileType::FILE, data.len() as u64, None));
271 }
272 if let Some(text) = self.files.read().unwrap().get(&resolved) {
273 return Ok(Metadata::new(FileType::FILE, text.len() as u64, None));
274 }
275 if self.directories.read().unwrap().contains(&resolved) {
276 return Ok(Metadata::new(FileType::DIR, 0, None));
277 }
278 Err(not_found(path))
279 }
280
281 }
288
289impl Storage for InMemoryFs {
290 async fn write(&self, path: &Path, contents: &[u8]) -> io::Result<()> {
291 let normalized = normalize_path(path);
292 insert_ancestor_dirs(&mut self.directories.write().unwrap(), &normalized);
293
294 match std::str::from_utf8(contents) {
300 Ok(s) => {
301 self.files
302 .write()
303 .unwrap()
304 .insert(normalized.clone(), s.to_string());
305 self.binary_files.write().unwrap().remove(&normalized);
306 }
307 Err(_) => {
308 self.binary_files
309 .write()
310 .unwrap()
311 .insert(normalized.clone(), contents.to_vec());
312 self.files.write().unwrap().remove(&normalized);
313 }
314 }
315 Ok(())
316 }
317
318 async fn create_dir_all(&self, path: &Path) -> io::Result<()> {
319 let normalized = normalize_path(path);
320 let mut dirs = self.directories.write().unwrap();
321 if !normalized.as_os_str().is_empty() {
322 dirs.insert(normalized.clone());
323 }
324 insert_ancestor_dirs(&mut dirs, &normalized);
325 Ok(())
326 }
327
328 async fn remove_file(&self, path: &Path) -> io::Result<()> {
329 let normalized = normalize_path(path);
330 if self.files.write().unwrap().remove(&normalized).is_some() {
331 return Ok(());
332 }
333 if self
334 .binary_files
335 .write()
336 .unwrap()
337 .remove(&normalized)
338 .is_some()
339 {
340 return Ok(());
341 }
342 if self.symlinks.write().unwrap().remove(&normalized).is_some() {
343 return Ok(());
344 }
345 Err(not_found(path))
346 }
347
348 async fn remove_dir_all(&self, path: &Path) -> io::Result<()> {
349 let normalized = normalize_path(path);
350 self.files
351 .write()
352 .unwrap()
353 .retain(|p, _| !p.starts_with(&normalized));
354 self.binary_files
355 .write()
356 .unwrap()
357 .retain(|p, _| !p.starts_with(&normalized));
358 self.symlinks
359 .write()
360 .unwrap()
361 .retain(|p, _| !p.starts_with(&normalized));
362 self.directories
363 .write()
364 .unwrap()
365 .retain(|p| p != &normalized && !p.starts_with(&normalized));
366 Ok(())
367 }
368
369 async fn rename(&self, from: &Path, to: &Path) -> io::Result<()> {
370 let from_norm = normalize_path(from);
371 let to_norm = normalize_path(to);
372 if from_norm == to_norm {
373 return Ok(());
374 }
375
376 let is_dir = self.directories.read().unwrap().contains(&from_norm);
377 if is_dir {
378 self.rename_dir(&from_norm, &to_norm, to)
379 } else {
380 self.rename_file(&from_norm, &to_norm, from, to).await
381 }
382 }
383
384 fn capabilities(&self) -> Capabilities {
385 Capabilities::IN_MEMORY
386 }
387
388 async fn write_atomic(&self, path: &Path, contents: &[u8]) -> io::Result<()> {
389 self.write(path, contents).await
398 }
399}
400
401impl InMemoryFs {
402 fn rename_dir(&self, from_norm: &Path, to_norm: &Path, to: &Path) -> io::Result<()> {
403 {
404 let files = self.files.read().unwrap();
405 let bin = self.binary_files.read().unwrap();
406 let dirs = self.directories.read().unwrap();
407 if files.contains_key(to_norm) || bin.contains_key(to_norm) || dirs.contains(to_norm) {
408 return Err(Error::new(
409 ErrorKind::AlreadyExists,
410 format!("destination already exists: {}", to.display()),
411 ));
412 }
413 }
414
415 let files_to_move: Vec<(PathBuf, String)> = self
416 .files
417 .read()
418 .unwrap()
419 .iter()
420 .filter(|(p, _)| p.starts_with(from_norm))
421 .map(|(p, c)| (p.clone(), c.clone()))
422 .collect();
423 let binaries_to_move: Vec<(PathBuf, Vec<u8>)> = self
424 .binary_files
425 .read()
426 .unwrap()
427 .iter()
428 .filter(|(p, _)| p.starts_with(from_norm))
429 .map(|(p, c)| (p.clone(), c.clone()))
430 .collect();
431
432 {
433 let mut files = self.files.write().unwrap();
434 for (old_path, content) in files_to_move {
435 files.remove(&old_path);
436 let relative = old_path.strip_prefix(from_norm).unwrap();
437 files.insert(to_norm.join(relative), content);
438 }
439 }
440 {
441 let mut binary = self.binary_files.write().unwrap();
442 for (old_path, content) in binaries_to_move {
443 binary.remove(&old_path);
444 let relative = old_path.strip_prefix(from_norm).unwrap();
445 binary.insert(to_norm.join(relative), content);
446 }
447 }
448 {
449 let mut dirs = self.directories.write().unwrap();
450 let old_dirs: Vec<PathBuf> = dirs
451 .iter()
452 .filter(|d| d.starts_with(from_norm))
453 .cloned()
454 .collect();
455 for old_dir in old_dirs {
456 dirs.remove(&old_dir);
457 let relative = old_dir.strip_prefix(from_norm).unwrap();
458 dirs.insert(to_norm.join(relative));
459 }
460 insert_ancestor_dirs(&mut dirs, to_norm);
461 }
462
463 Ok(())
464 }
465
466 async fn rename_file(
467 &self,
468 from_norm: &Path,
469 to_norm: &Path,
470 from: &Path,
471 to: &Path,
472 ) -> io::Result<()> {
473 {
474 let files = self.files.read().unwrap();
475 let bin = self.binary_files.read().unwrap();
476 if !files.contains_key(from_norm) && !bin.contains_key(from_norm) {
477 return Err(not_found(from));
478 }
479 if files.contains_key(to_norm) || bin.contains_key(to_norm) {
480 return Err(Error::new(
481 ErrorKind::AlreadyExists,
482 format!("destination already exists: {}", to.display()),
483 ));
484 }
485 }
486
487 if let Some(parent) = to_norm.parent() {
488 self.create_dir_all(parent).await?;
489 }
490
491 let removed_text = self.files.write().unwrap().remove(from_norm);
500 if let Some(content) = removed_text {
501 self.files
502 .write()
503 .unwrap()
504 .insert(to_norm.to_path_buf(), content);
505 return Ok(());
506 }
507 let removed_binary = self.binary_files.write().unwrap().remove(from_norm);
508 if let Some(content) = removed_binary {
509 self.binary_files
510 .write()
511 .unwrap()
512 .insert(to_norm.to_path_buf(), content);
513 return Ok(());
514 }
515 Err(not_found(from))
516 }
517}
518
519#[cfg(test)]
520mod tests {
521 use super::*;
522 use prov_graph::exec::block_on;
523
524 #[test]
525 fn read_write_roundtrip() {
526 let fs = InMemoryFs::new();
527 block_on(fs.write(Path::new("test.md"), b"Hello, World!")).unwrap();
528 assert_eq!(
529 block_on(fs.read_to_string(Path::new("test.md"))).unwrap(),
530 "Hello, World!"
531 );
532 assert!(block_on(fs.try_exists(Path::new("test.md"))).unwrap());
533 block_on(fs.remove_file(Path::new("test.md"))).unwrap();
534 assert!(!block_on(fs.try_exists(Path::new("test.md"))).unwrap());
535 }
536
537 #[test]
538 fn binary_content_round_trips_through_read_but_not_read_to_string() {
539 let fs = InMemoryFs::new();
540 let invalid_utf8 = vec![0xff, 0xfe, 0xfd];
541 block_on(fs.write(Path::new("bin.dat"), &invalid_utf8)).unwrap();
542 assert_eq!(
543 block_on(fs.read(Path::new("bin.dat"))).unwrap(),
544 invalid_utf8
545 );
546 let err = block_on(fs.read_to_string(Path::new("bin.dat"))).unwrap_err();
547 assert_eq!(err.kind(), io::ErrorKind::InvalidData);
548 }
549
550 #[test]
551 fn create_dir_all_creates_parents_implicitly_via_write() {
552 let fs = InMemoryFs::new();
553 block_on(fs.write(Path::new("a/b/c/file.md"), b"Content")).unwrap();
554 assert!(block_on(fs.metadata(Path::new("a"))).unwrap().is_dir());
555 assert!(block_on(fs.metadata(Path::new("a/b"))).unwrap().is_dir());
556 assert!(block_on(fs.metadata(Path::new("a/b/c"))).unwrap().is_dir());
557 assert!(block_on(fs.try_exists(Path::new("a/b/c/file.md"))).unwrap());
558 }
559
560 #[test]
561 fn read_dir_returns_immediate_children_only() {
562 let fs = InMemoryFs::new();
563 block_on(fs.write(Path::new("dir/file1.md"), b"1")).unwrap();
564 block_on(fs.write(Path::new("dir/file2.md"), b"2")).unwrap();
565 block_on(fs.write(Path::new("dir/subdir/file3.md"), b"3")).unwrap();
566
567 let entries = block_on(fs.read_dir(Path::new("dir"))).unwrap();
568 let paths: Vec<PathBuf> = entries.iter().map(|e| e.path().to_path_buf()).collect();
569 assert!(paths.contains(&PathBuf::from("dir/file1.md")));
570 assert!(paths.contains(&PathBuf::from("dir/file2.md")));
571 assert!(paths.contains(&PathBuf::from("dir/subdir")));
572 assert!(!paths.contains(&PathBuf::from("dir/subdir/file3.md")));
573 }
574
575 #[test]
576 fn read_dir_of_an_untracked_directory_is_not_found() {
577 let fs = InMemoryFs::new();
580 let err = block_on(fs.read_dir(Path::new("never/created"))).unwrap_err();
581 assert_eq!(err.kind(), io::ErrorKind::NotFound);
582 }
583
584 #[test]
585 fn read_dir_of_the_root_never_errors() {
586 let fs = InMemoryFs::new();
590 assert!(block_on(fs.read_dir(Path::new(""))).unwrap().is_empty());
591 }
592
593 #[test]
594 fn export_then_import_roundtrip() {
595 let fs = InMemoryFs::new();
596 block_on(fs.write(Path::new("file1.md"), b"Content 1")).unwrap();
597 block_on(fs.write(Path::new("dir/file2.md"), b"Content 2")).unwrap();
598
599 let entries = fs.export_entries();
600 let fs2 = InMemoryFs::load_from_entries(entries);
601
602 assert_eq!(
603 block_on(fs2.read_to_string(Path::new("file1.md"))).unwrap(),
604 "Content 1"
605 );
606 assert_eq!(
607 block_on(fs2.read_to_string(Path::new("dir/file2.md"))).unwrap(),
608 "Content 2"
609 );
610 }
611
612 #[test]
613 fn path_normalization() {
614 let fs = InMemoryFs::new();
615 block_on(fs.write(Path::new("dir/file.md"), b"Content")).unwrap();
616 assert!(block_on(fs.try_exists(Path::new("dir/file.md"))).unwrap());
617 assert!(block_on(fs.try_exists(Path::new("dir/./file.md"))).unwrap());
618 assert!(block_on(fs.try_exists(Path::new("dir/subdir/../file.md"))).unwrap());
619 }
620
621 #[test]
622 fn rename_moves_a_single_file() {
623 let fs = InMemoryFs::new();
624 block_on(fs.write(Path::new("old.md"), b"content")).unwrap();
625 block_on(fs.rename(Path::new("old.md"), Path::new("new.md"))).unwrap();
626 assert!(!block_on(fs.try_exists(Path::new("old.md"))).unwrap());
627 assert_eq!(
628 block_on(fs.read_to_string(Path::new("new.md"))).unwrap(),
629 "content"
630 );
631 }
632
633 #[test]
634 fn rename_moves_a_directory_and_its_contents() {
635 let fs = InMemoryFs::new();
636 block_on(fs.write(Path::new("dir/a.md"), b"a")).unwrap();
637 block_on(fs.write(Path::new("dir/sub/b.md"), b"b")).unwrap();
638
639 block_on(fs.rename(Path::new("dir"), Path::new("moved"))).unwrap();
640
641 assert!(!block_on(fs.try_exists(Path::new("dir/a.md"))).unwrap());
642 assert_eq!(
643 block_on(fs.read_to_string(Path::new("moved/a.md"))).unwrap(),
644 "a"
645 );
646 assert_eq!(
647 block_on(fs.read_to_string(Path::new("moved/sub/b.md"))).unwrap(),
648 "b"
649 );
650 assert!(
651 block_on(fs.metadata(Path::new("moved/sub")))
652 .unwrap()
653 .is_dir()
654 );
655 }
656
657 #[test]
658 fn rename_refuses_to_clobber_an_existing_destination() {
659 let fs = InMemoryFs::new();
660 block_on(fs.write(Path::new("a.md"), b"a")).unwrap();
661 block_on(fs.write(Path::new("b.md"), b"b")).unwrap();
662 let err = block_on(fs.rename(Path::new("a.md"), Path::new("b.md"))).unwrap_err();
663 assert_eq!(err.kind(), io::ErrorKind::AlreadyExists);
664 }
665
666 #[test]
672 fn metadata_and_read_follow_a_symlink_to_its_target() {
673 let fs = InMemoryFs::new();
674 block_on(fs.write(Path::new("real.md"), b"hello")).unwrap();
675 fs.add_symlink(Path::new("link.md"), Path::new("real.md"));
676
677 let m = block_on(fs.metadata(Path::new("link.md"))).unwrap();
678 assert!(m.is_file());
679 assert!(!m.is_dir());
680
681 assert_eq!(
682 block_on(fs.read_to_string(Path::new("link.md"))).unwrap(),
683 "hello"
684 );
685 }
686
687 #[test]
688 fn read_dir_reports_a_symlink_by_its_own_unfollowed_type() {
689 let fs = InMemoryFs::new();
692 block_on(fs.write(Path::new("real.md"), b"hello")).unwrap();
693 fs.add_symlink(Path::new("link.md"), Path::new("real.md"));
694
695 let entries = block_on(fs.read_dir(Path::new(""))).unwrap();
696 let link_entry = entries
697 .iter()
698 .find(|e| e.path() == Path::new("link.md"))
699 .expect("symlink should appear in its parent's listing");
700 assert!(link_entry.file_type().is_symlink());
701
702 let real_entry = entries
703 .iter()
704 .find(|e| e.path() == Path::new("real.md"))
705 .expect("the real file should also be listed");
706 assert!(!real_entry.file_type().is_symlink());
707 }
708
709 #[test]
710 fn a_symlink_to_a_missing_target_is_not_found_by_metadata() {
711 let fs = InMemoryFs::new();
712 fs.add_symlink(Path::new("dangling.md"), Path::new("nowhere.md"));
713 let err = block_on(fs.metadata(Path::new("dangling.md"))).unwrap_err();
714 assert_eq!(err.kind(), io::ErrorKind::NotFound);
715 }
716
717 #[test]
718 fn removing_a_symlink_leaves_its_target_untouched() {
719 let fs = InMemoryFs::new();
720 block_on(fs.write(Path::new("real.md"), b"hello")).unwrap();
721 fs.add_symlink(Path::new("link.md"), Path::new("real.md"));
722
723 block_on(fs.remove_file(Path::new("link.md"))).unwrap();
724
725 assert!(!block_on(fs.try_exists(Path::new("link.md"))).unwrap());
726 assert_eq!(
727 block_on(fs.read_to_string(Path::new("real.md"))).unwrap(),
728 "hello"
729 );
730 }
731
732 #[test]
735 fn in_memory_declares_atomic_replace_but_no_durability_across_a_restart() {
736 let fs = InMemoryFs::new();
737 let caps = fs.capabilities();
738 assert!(
739 caps.atomic_replace,
740 "a single locked write is already atomic"
741 );
742 assert_eq!(
743 caps.sync_guarantee,
744 super::super::SyncGuarantee::None,
745 "nothing here survives the process exiting, so there is not even an \
746 ordering worth promising against a crash"
747 );
748 assert!(
749 !caps.native_transactions,
750 "the lock covers one call, not a batch of several committed together"
751 );
752 }
753
754 #[test]
755 fn write_atomic_lands_the_new_contents_without_a_temp_sibling() {
756 let fs = InMemoryFs::new();
757 block_on(fs.write(Path::new("doc.md"), b"old")).unwrap();
758 block_on(fs.write_atomic(Path::new("doc.md"), b"new")).unwrap();
759
760 assert_eq!(
761 block_on(fs.read_to_string(Path::new("doc.md"))).unwrap(),
762 "new"
763 );
764 let entries = block_on(fs.read_dir(Path::new(""))).unwrap();
767 assert_eq!(entries.len(), 1, "no stray temp-sibling entry: {entries:?}");
768 }
769
770 #[test]
773 fn clones_share_the_same_backing_store() {
774 let fs = InMemoryFs::new();
775 let clone = fs.clone();
776 block_on(fs.write(Path::new("shared.md"), b"visible everywhere")).unwrap();
777 assert_eq!(
778 block_on(clone.read_to_string(Path::new("shared.md"))).unwrap(),
779 "visible everywhere"
780 );
781 }
782
783 #[test]
784 fn clear_empties_every_store() {
785 let fs = InMemoryFs::new();
786 block_on(fs.write(Path::new("a.md"), b"a")).unwrap();
787 fs.add_symlink(Path::new("link.md"), Path::new("a.md"));
788
789 fs.clear();
790
791 assert!(!block_on(fs.try_exists(Path::new("a.md"))).unwrap());
792 assert!(block_on(fs.metadata(Path::new("link.md"))).is_err());
793 }
794}