1#![allow(clippy::unwrap_used)]
40
41use async_trait::async_trait;
42use std::collections::HashMap;
43use std::io::{Error as IoError, ErrorKind};
44use std::path::{Path, PathBuf};
45use std::sync::RwLock;
46use std::time::SystemTime;
47
48use super::limits::{FsLimits, FsUsage};
49use super::traits::{DirEntry, FileSystem, FileType, Metadata};
50use crate::error::Result;
51
52#[cfg(feature = "failpoints")]
53use fail::fail_point;
54
55pub struct InMemoryFs {
169 entries: RwLock<HashMap<PathBuf, FsEntry>>,
170 limits: FsLimits,
171}
172
173#[derive(Debug, Clone)]
174enum FsEntry {
175 File {
176 content: Vec<u8>,
177 metadata: Metadata,
178 },
179 Directory {
180 metadata: Metadata,
181 },
182 Symlink {
183 target: PathBuf,
184 metadata: Metadata,
185 },
186}
187
188impl Default for InMemoryFs {
189 fn default() -> Self {
190 Self::new()
191 }
192}
193
194impl InMemoryFs {
195 pub fn new() -> Self {
231 Self::with_limits(FsLimits::default())
232 }
233
234 pub fn with_limits(limits: FsLimits) -> Self {
253 let mut entries = HashMap::new();
254
255 entries.insert(
257 PathBuf::from("/"),
258 FsEntry::Directory {
259 metadata: Metadata {
260 file_type: FileType::Directory,
261 size: 0,
262 mode: 0o755,
263 modified: SystemTime::now(),
264 created: SystemTime::now(),
265 },
266 },
267 );
268
269 for dir in &["/tmp", "/home", "/home/user", "/dev"] {
271 entries.insert(
272 PathBuf::from(dir),
273 FsEntry::Directory {
274 metadata: Metadata {
275 file_type: FileType::Directory,
276 size: 0,
277 mode: 0o755,
278 modified: SystemTime::now(),
279 created: SystemTime::now(),
280 },
281 },
282 );
283 }
284
285 entries.insert(
288 PathBuf::from("/dev/null"),
289 FsEntry::File {
290 content: Vec::new(),
291 metadata: Metadata {
292 file_type: FileType::File,
293 size: 0,
294 mode: 0o666,
295 modified: SystemTime::now(),
296 created: SystemTime::now(),
297 },
298 },
299 );
300
301 entries.insert(
303 PathBuf::from("/dev/fd"),
304 FsEntry::Directory {
305 metadata: Metadata {
306 file_type: FileType::Directory,
307 size: 0,
308 mode: 0o755,
309 modified: SystemTime::now(),
310 created: SystemTime::now(),
311 },
312 },
313 );
314
315 Self {
316 entries: RwLock::new(entries),
317 limits,
318 }
319 }
320
321 fn compute_usage(&self) -> FsUsage {
323 let entries = self.entries.read().unwrap();
324 let mut total_bytes = 0u64;
325 let mut file_count = 0u64;
326 let mut dir_count = 0u64;
327
328 for entry in entries.values() {
329 match entry {
330 FsEntry::File { content, .. } => {
331 total_bytes += content.len() as u64;
332 file_count += 1;
333 }
334 FsEntry::Directory { .. } => {
335 dir_count += 1;
336 }
337 FsEntry::Symlink { .. } => {
338 }
340 }
341 }
342
343 FsUsage::new(total_bytes, file_count, dir_count)
344 }
345
346 fn check_write_limits(
348 &self,
349 entries: &HashMap<PathBuf, FsEntry>,
350 path: &Path,
351 new_size: usize,
352 ) -> Result<()> {
353 self.limits
355 .check_file_size(new_size as u64)
356 .map_err(|e| IoError::other(e.to_string()))?;
357
358 let mut current_total = 0u64;
360 let mut current_file_count = 0u64;
361 let mut old_file_size = 0u64;
362 let mut is_new_file = true;
363
364 for (entry_path, entry) in entries.iter() {
365 if let FsEntry::File { content, .. } = entry {
366 current_total += content.len() as u64;
367 current_file_count += 1;
368 if entry_path == path {
369 old_file_size = content.len() as u64;
370 is_new_file = false;
371 }
372 }
373 }
374
375 if is_new_file {
377 self.limits
378 .check_file_count(current_file_count)
379 .map_err(|e| IoError::other(e.to_string()))?;
380 }
381
382 let new_total = current_total - old_file_size + new_size as u64;
385 if new_total > self.limits.max_total_bytes {
386 return Err(IoError::other(format!(
387 "filesystem full: {} bytes would exceed {} byte limit",
388 new_total, self.limits.max_total_bytes
389 ))
390 .into());
391 }
392
393 Ok(())
394 }
395
396 fn normalize_path(path: &Path) -> PathBuf {
397 let mut result = PathBuf::new();
398
399 for component in path.components() {
400 match component {
401 std::path::Component::RootDir => {
402 result.push("/");
403 }
404 std::path::Component::Normal(name) => {
405 result.push(name);
406 }
407 std::path::Component::ParentDir => {
408 result.pop();
409 }
410 std::path::Component::CurDir => {}
411 std::path::Component::Prefix(_) => {}
412 }
413 }
414
415 if result.as_os_str().is_empty() {
416 result.push("/");
417 }
418
419 result
420 }
421
422 pub fn add_file(&self, path: impl AsRef<Path>, content: impl AsRef<[u8]>, mode: u32) {
450 let path = Self::normalize_path(path.as_ref());
451 let content = content.as_ref();
452 let mut entries = self.entries.write().unwrap();
453
454 if let Some(parent) = path.parent() {
456 let mut current = PathBuf::from("/");
457 for component in parent.components().skip(1) {
458 current.push(component);
459 if !entries.contains_key(¤t) {
460 entries.insert(
461 current.clone(),
462 FsEntry::Directory {
463 metadata: Metadata {
464 file_type: FileType::Directory,
465 size: 0,
466 mode: 0o755,
467 modified: SystemTime::now(),
468 created: SystemTime::now(),
469 },
470 },
471 );
472 }
473 }
474 }
475
476 entries.insert(
477 path,
478 FsEntry::File {
479 content: content.to_vec(),
480 metadata: Metadata {
481 file_type: FileType::File,
482 size: content.len() as u64,
483 mode,
484 modified: SystemTime::now(),
485 created: SystemTime::now(),
486 },
487 },
488 );
489 }
490}
491
492#[async_trait]
493impl FileSystem for InMemoryFs {
494 async fn read_file(&self, path: &Path) -> Result<Vec<u8>> {
495 self.limits
497 .validate_path(path)
498 .map_err(|e| IoError::other(e.to_string()))?;
499
500 #[cfg(feature = "failpoints")]
502 fail_point!("fs::read_file", |action| {
503 match action.as_deref() {
504 Some("io_error") => {
505 return Err(IoError::other("injected I/O error").into());
506 }
507 Some("permission_denied") => {
508 return Err(
509 IoError::new(ErrorKind::PermissionDenied, "permission denied").into(),
510 );
511 }
512 Some("corrupt_data") => {
513 return Ok(vec![0xFF, 0xFE, 0x00, 0x01]);
515 }
516 _ => {}
517 }
518 Err(IoError::other("fail point triggered").into())
519 });
520
521 let path = Self::normalize_path(path);
522 let entries = self.entries.read().unwrap();
523
524 match entries.get(&path) {
525 Some(FsEntry::File { content, .. }) => Ok(content.clone()),
526 Some(FsEntry::Directory { .. }) => Err(IoError::other("is a directory").into()),
527 Some(FsEntry::Symlink { .. }) => {
528 Err(IoError::new(ErrorKind::NotFound, "file not found").into())
530 }
531 None => Err(IoError::new(ErrorKind::NotFound, "file not found").into()),
532 }
533 }
534
535 async fn write_file(&self, path: &Path, content: &[u8]) -> Result<()> {
536 self.limits
538 .validate_path(path)
539 .map_err(|e| IoError::other(e.to_string()))?;
540
541 #[cfg(feature = "failpoints")]
543 fail_point!("fs::write_file", |action| {
544 match action.as_deref() {
545 Some("io_error") => {
546 return Err(IoError::other("injected I/O error").into());
547 }
548 Some("disk_full") => {
549 return Err(IoError::other("no space left on device").into());
550 }
551 Some("permission_denied") => {
552 return Err(
553 IoError::new(ErrorKind::PermissionDenied, "permission denied").into(),
554 );
555 }
556 Some("partial_write") => {
557 return Err(IoError::new(ErrorKind::Interrupted, "partial write").into());
560 }
561 _ => {}
562 }
563 Err(IoError::other("fail point triggered").into())
564 });
565
566 let path = Self::normalize_path(path);
567
568 if path == Path::new("/dev/null") {
570 return Ok(());
571 }
572
573 let mut entries = self.entries.write().unwrap();
574
575 if let Some(parent) = path.parent() {
577 if !entries.contains_key(parent) && parent != Path::new("/") {
578 return Err(IoError::new(ErrorKind::NotFound, "parent directory not found").into());
579 }
580 }
581
582 if let Some(FsEntry::Directory { .. }) = entries.get(&path) {
584 return Err(IoError::other("is a directory").into());
585 }
586
587 self.check_write_limits(&entries, &path, content.len())?;
589
590 entries.insert(
591 path,
592 FsEntry::File {
593 content: content.to_vec(),
594 metadata: Metadata {
595 file_type: FileType::File,
596 size: content.len() as u64,
597 mode: 0o644,
598 modified: SystemTime::now(),
599 created: SystemTime::now(),
600 },
601 },
602 );
603
604 Ok(())
605 }
606
607 async fn append_file(&self, path: &Path, content: &[u8]) -> Result<()> {
608 self.limits
610 .validate_path(path)
611 .map_err(|e| IoError::other(e.to_string()))?;
612
613 let path = Self::normalize_path(path);
614
615 if path == Path::new("/dev/null") {
617 return Ok(());
618 }
619
620 let (should_create, current_size) = {
622 let entries = self.entries.read().unwrap();
623 match entries.get(&path) {
624 Some(FsEntry::File {
625 content: existing, ..
626 }) => (false, Some(existing.len())),
627 Some(FsEntry::Directory { .. }) => {
628 return Err(IoError::other("is a directory").into());
629 }
630 Some(FsEntry::Symlink { .. }) => {
631 return Err(IoError::new(ErrorKind::NotFound, "file not found").into());
632 }
633 None => (true, None),
634 }
635 };
636
637 if should_create {
638 return self.write_file(&path, content).await;
639 }
640
641 let current_file_size = current_size.unwrap();
643 let new_size = current_file_size + content.len();
644
645 self.limits
647 .check_file_size(new_size as u64)
648 .map_err(|e| IoError::other(e.to_string()))?;
649
650 let mut entries = self.entries.write().unwrap();
652
653 let mut current_total = 0u64;
655 for entry in entries.values() {
656 if let FsEntry::File {
657 content: file_content,
658 ..
659 } = entry
660 {
661 current_total += file_content.len() as u64;
662 }
663 }
664
665 let new_total = current_total + content.len() as u64;
667 if new_total > self.limits.max_total_bytes {
668 return Err(IoError::other(format!(
669 "filesystem full: {} bytes would exceed {} byte limit",
670 new_total, self.limits.max_total_bytes
671 ))
672 .into());
673 }
674
675 if let Some(FsEntry::File {
677 content: existing,
678 metadata,
679 }) = entries.get_mut(&path)
680 {
681 existing.extend_from_slice(content);
682 metadata.size = existing.len() as u64;
683 metadata.modified = SystemTime::now();
684 }
685
686 Ok(())
687 }
688
689 async fn mkdir(&self, path: &Path, recursive: bool) -> Result<()> {
690 self.limits
692 .validate_path(path)
693 .map_err(|e| IoError::other(e.to_string()))?;
694
695 let path = Self::normalize_path(path);
696 let mut entries = self.entries.write().unwrap();
697
698 if recursive {
699 let mut current = PathBuf::from("/");
700 for component in path.components().skip(1) {
701 current.push(component);
702 match entries.get(¤t) {
703 Some(FsEntry::Directory { .. }) => {
704 }
706 Some(FsEntry::File { .. } | FsEntry::Symlink { .. }) => {
707 return Err(IoError::new(ErrorKind::AlreadyExists, "file exists").into());
709 }
710 None => {
711 entries.insert(
713 current.clone(),
714 FsEntry::Directory {
715 metadata: Metadata {
716 file_type: FileType::Directory,
717 size: 0,
718 mode: 0o755,
719 modified: SystemTime::now(),
720 created: SystemTime::now(),
721 },
722 },
723 );
724 }
725 }
726 }
727 } else {
728 if let Some(parent) = path.parent() {
730 if !entries.contains_key(parent) && parent != Path::new("/") {
731 return Err(
732 IoError::new(ErrorKind::NotFound, "parent directory not found").into(),
733 );
734 }
735 }
736
737 if entries.contains_key(&path) {
738 return Err(IoError::new(ErrorKind::AlreadyExists, "directory exists").into());
739 }
740
741 entries.insert(
742 path,
743 FsEntry::Directory {
744 metadata: Metadata {
745 file_type: FileType::Directory,
746 size: 0,
747 mode: 0o755,
748 modified: SystemTime::now(),
749 created: SystemTime::now(),
750 },
751 },
752 );
753 }
754
755 Ok(())
756 }
757
758 async fn remove(&self, path: &Path, recursive: bool) -> Result<()> {
759 let path = Self::normalize_path(path);
760 let mut entries = self.entries.write().unwrap();
761
762 match entries.get(&path) {
763 Some(FsEntry::Directory { .. }) => {
764 if recursive {
765 let to_remove: Vec<PathBuf> = entries
767 .keys()
768 .filter(|p| p.starts_with(&path))
769 .cloned()
770 .collect();
771
772 for p in to_remove {
773 entries.remove(&p);
774 }
775 } else {
776 let has_children = entries
778 .keys()
779 .any(|p| p != &path && p.parent() == Some(&path));
780
781 if has_children {
782 return Err(IoError::other("directory not empty").into());
783 }
784
785 entries.remove(&path);
786 }
787 }
788 Some(FsEntry::File { .. }) | Some(FsEntry::Symlink { .. }) => {
789 entries.remove(&path);
790 }
791 None => {
792 return Err(IoError::new(ErrorKind::NotFound, "not found").into());
793 }
794 }
795
796 Ok(())
797 }
798
799 async fn stat(&self, path: &Path) -> Result<Metadata> {
800 let path = Self::normalize_path(path);
801 let entries = self.entries.read().unwrap();
802
803 match entries.get(&path) {
804 Some(FsEntry::File { metadata, .. })
805 | Some(FsEntry::Directory { metadata })
806 | Some(FsEntry::Symlink { metadata, .. }) => Ok(metadata.clone()),
807 None => Err(IoError::new(ErrorKind::NotFound, "not found").into()),
808 }
809 }
810
811 async fn read_dir(&self, path: &Path) -> Result<Vec<DirEntry>> {
812 let path = Self::normalize_path(path);
813 let entries = self.entries.read().unwrap();
814
815 match entries.get(&path) {
816 Some(FsEntry::Directory { .. }) => {
817 let mut result = Vec::new();
818
819 for (entry_path, entry) in entries.iter() {
820 if entry_path.parent() == Some(&path) && entry_path != &path {
821 let name = entry_path
822 .file_name()
823 .map(|n| n.to_string_lossy().to_string())
824 .unwrap_or_default();
825
826 let metadata = match entry {
827 FsEntry::File { metadata, .. }
828 | FsEntry::Directory { metadata }
829 | FsEntry::Symlink { metadata, .. } => metadata.clone(),
830 };
831
832 result.push(DirEntry { name, metadata });
833 }
834 }
835
836 Ok(result)
837 }
838 Some(_) => Err(IoError::other("not a directory").into()),
839 None => Err(IoError::new(ErrorKind::NotFound, "not found").into()),
840 }
841 }
842
843 async fn exists(&self, path: &Path) -> Result<bool> {
844 let path = Self::normalize_path(path);
845 let entries = self.entries.read().unwrap();
846 Ok(entries.contains_key(&path))
847 }
848
849 async fn rename(&self, from: &Path, to: &Path) -> Result<()> {
850 let from = Self::normalize_path(from);
851 let to = Self::normalize_path(to);
852 let mut entries = self.entries.write().unwrap();
853
854 let entry = entries
855 .remove(&from)
856 .ok_or_else(|| IoError::new(ErrorKind::NotFound, "not found"))?;
857
858 entries.insert(to, entry);
859 Ok(())
860 }
861
862 async fn copy(&self, from: &Path, to: &Path) -> Result<()> {
863 let from = Self::normalize_path(from);
864 let to = Self::normalize_path(to);
865 let mut entries = self.entries.write().unwrap();
866
867 let entry = entries
868 .get(&from)
869 .cloned()
870 .ok_or_else(|| IoError::new(ErrorKind::NotFound, "not found"))?;
871
872 entries.insert(to, entry);
873 Ok(())
874 }
875
876 async fn symlink(&self, target: &Path, link: &Path) -> Result<()> {
877 let link = Self::normalize_path(link);
878 let mut entries = self.entries.write().unwrap();
879
880 entries.insert(
881 link,
882 FsEntry::Symlink {
883 target: target.to_path_buf(),
884 metadata: Metadata {
885 file_type: FileType::Symlink,
886 size: 0,
887 mode: 0o777,
888 modified: SystemTime::now(),
889 created: SystemTime::now(),
890 },
891 },
892 );
893
894 Ok(())
895 }
896
897 async fn read_link(&self, path: &Path) -> Result<PathBuf> {
898 let path = Self::normalize_path(path);
899 let entries = self.entries.read().unwrap();
900
901 match entries.get(&path) {
902 Some(FsEntry::Symlink { target, .. }) => Ok(target.clone()),
903 Some(_) => Err(IoError::other("not a symlink").into()),
904 None => Err(IoError::new(ErrorKind::NotFound, "not found").into()),
905 }
906 }
907
908 async fn chmod(&self, path: &Path, mode: u32) -> Result<()> {
909 let path = Self::normalize_path(path);
910 let mut entries = self.entries.write().unwrap();
911
912 match entries.get_mut(&path) {
913 Some(FsEntry::File { metadata, .. })
914 | Some(FsEntry::Directory { metadata })
915 | Some(FsEntry::Symlink { metadata, .. }) => {
916 metadata.mode = mode;
917 Ok(())
918 }
919 None => Err(IoError::new(ErrorKind::NotFound, "not found").into()),
920 }
921 }
922
923 fn usage(&self) -> FsUsage {
924 self.compute_usage()
925 }
926
927 fn limits(&self) -> FsLimits {
928 self.limits.clone()
929 }
930}
931
932#[cfg(test)]
933#[allow(clippy::unwrap_used)]
934mod tests {
935 use super::*;
936
937 #[tokio::test]
938 async fn test_write_and_read_file() {
939 let fs = InMemoryFs::new();
940
941 fs.write_file(Path::new("/tmp/test.txt"), b"hello world")
942 .await
943 .unwrap();
944
945 let content = fs.read_file(Path::new("/tmp/test.txt")).await.unwrap();
946 assert_eq!(content, b"hello world");
947 }
948
949 #[tokio::test]
950 async fn test_mkdir_and_read_dir() {
951 let fs = InMemoryFs::new();
952
953 fs.mkdir(Path::new("/tmp/mydir"), false).await.unwrap();
954 fs.write_file(Path::new("/tmp/mydir/file.txt"), b"test")
955 .await
956 .unwrap();
957
958 let entries = fs.read_dir(Path::new("/tmp/mydir")).await.unwrap();
959 assert_eq!(entries.len(), 1);
960 assert_eq!(entries[0].name, "file.txt");
961 }
962
963 #[tokio::test]
964 async fn test_exists() {
965 let fs = InMemoryFs::new();
966
967 assert!(fs.exists(Path::new("/tmp")).await.unwrap());
968 assert!(!fs.exists(Path::new("/tmp/nonexistent")).await.unwrap());
969 }
970
971 #[tokio::test]
972 async fn test_add_file_basic() {
973 let fs = InMemoryFs::new();
974 fs.add_file("/tmp/added.txt", "hello from add_file", 0o644);
975
976 let content = fs.read_file(Path::new("/tmp/added.txt")).await.unwrap();
977 assert_eq!(content, b"hello from add_file");
978 }
979
980 #[tokio::test]
981 async fn test_add_file_with_mode() {
982 let fs = InMemoryFs::new();
983 fs.add_file("/etc/readonly.conf", "secret", 0o444);
984
985 let stat = fs.stat(Path::new("/etc/readonly.conf")).await.unwrap();
986 assert_eq!(stat.mode, 0o444);
987 }
988
989 #[tokio::test]
990 async fn test_add_file_creates_parent_directories() {
991 let fs = InMemoryFs::new();
992 fs.add_file("/a/b/c/d/nested.txt", "deep content", 0o644);
993
994 assert!(fs.exists(Path::new("/a/b/c/d/nested.txt")).await.unwrap());
996
997 assert!(fs.exists(Path::new("/a")).await.unwrap());
999 assert!(fs.exists(Path::new("/a/b")).await.unwrap());
1000 assert!(fs.exists(Path::new("/a/b/c")).await.unwrap());
1001 assert!(fs.exists(Path::new("/a/b/c/d")).await.unwrap());
1002
1003 let content = fs
1005 .read_file(Path::new("/a/b/c/d/nested.txt"))
1006 .await
1007 .unwrap();
1008 assert_eq!(content, b"deep content");
1009 }
1010
1011 #[tokio::test]
1012 async fn test_add_file_binary() {
1013 let fs = InMemoryFs::new();
1014 let binary_data = vec![0x00, 0xFF, 0x89, 0x50, 0x4E, 0x47];
1015 fs.add_file("/data/binary.bin", &binary_data, 0o644);
1016
1017 let content = fs.read_file(Path::new("/data/binary.bin")).await.unwrap();
1018 assert_eq!(content, binary_data);
1019 }
1020 #[tokio::test]
1023 async fn test_file_size_limit() {
1024 let limits = FsLimits::new().max_file_size(100);
1025 let fs = InMemoryFs::with_limits(limits);
1026
1027 fs.write_file(Path::new("/tmp/small.txt"), &[0u8; 50])
1029 .await
1030 .unwrap();
1031
1032 fs.write_file(Path::new("/tmp/exact.txt"), &[0u8; 100])
1034 .await
1035 .unwrap();
1036
1037 let result = fs
1039 .write_file(Path::new("/tmp/large.txt"), &[0u8; 101])
1040 .await;
1041 assert!(result.is_err());
1042 let err = result.unwrap_err().to_string();
1043 assert!(err.contains("file too large") || err.contains("exceeds"));
1044 }
1045
1046 #[tokio::test]
1047 async fn test_total_bytes_limit() {
1048 let limits = FsLimits::new().max_total_bytes(200);
1049 let fs = InMemoryFs::with_limits(limits);
1050
1051 fs.write_file(Path::new("/tmp/file1.txt"), &[0u8; 100])
1053 .await
1054 .unwrap();
1055
1056 fs.write_file(Path::new("/tmp/file2.txt"), &[0u8; 50])
1058 .await
1059 .unwrap();
1060
1061 let result = fs
1063 .write_file(Path::new("/tmp/file3.txt"), &[0u8; 100])
1064 .await;
1065 assert!(result.is_err());
1066 let err = result.unwrap_err().to_string();
1067 assert!(err.contains("filesystem full") || err.contains("exceeds"));
1068 }
1069
1070 #[tokio::test]
1071 async fn test_file_count_limit() {
1072 let limits = FsLimits::new().max_file_count(4); let fs = InMemoryFs::with_limits(limits);
1075
1076 fs.write_file(Path::new("/tmp/file1.txt"), b"1")
1078 .await
1079 .unwrap();
1080 fs.write_file(Path::new("/tmp/file2.txt"), b"2")
1081 .await
1082 .unwrap();
1083 fs.write_file(Path::new("/tmp/file3.txt"), b"3")
1084 .await
1085 .unwrap();
1086
1087 let result = fs.write_file(Path::new("/tmp/file4.txt"), b"4").await;
1089 assert!(result.is_err());
1090 let err = result.unwrap_err().to_string();
1091 assert!(err.contains("too many files") || err.contains("limit"));
1092 }
1093
1094 #[tokio::test]
1095 async fn test_overwrite_does_not_increase_count() {
1096 let limits = FsLimits::new().max_file_count(3); let fs = InMemoryFs::with_limits(limits);
1099
1100 fs.write_file(Path::new("/tmp/file1.txt"), b"original")
1102 .await
1103 .unwrap();
1104 fs.write_file(Path::new("/tmp/file2.txt"), b"original")
1105 .await
1106 .unwrap();
1107
1108 fs.write_file(Path::new("/tmp/file1.txt"), b"updated")
1110 .await
1111 .unwrap();
1112
1113 let result = fs.write_file(Path::new("/tmp/file3.txt"), b"new").await;
1115 assert!(result.is_err());
1116 }
1117
1118 #[tokio::test]
1119 async fn test_append_respects_limits() {
1120 let limits = FsLimits::new().max_file_size(100);
1121 let fs = InMemoryFs::with_limits(limits);
1122
1123 fs.write_file(Path::new("/tmp/append.txt"), &[0u8; 50])
1125 .await
1126 .unwrap();
1127
1128 fs.append_file(Path::new("/tmp/append.txt"), &[0u8; 30])
1130 .await
1131 .unwrap();
1132
1133 let result = fs
1135 .append_file(Path::new("/tmp/append.txt"), &[0u8; 50])
1136 .await;
1137 assert!(result.is_err());
1138 }
1139
1140 #[tokio::test]
1141 async fn test_usage_tracking() {
1142 let fs = InMemoryFs::new();
1143
1144 let usage = fs.usage();
1146 assert_eq!(usage.total_bytes, 0); assert_eq!(usage.file_count, 1); fs.write_file(Path::new("/tmp/test.txt"), b"hello")
1151 .await
1152 .unwrap();
1153
1154 let usage = fs.usage();
1155 assert_eq!(usage.total_bytes, 5);
1156 assert_eq!(usage.file_count, 2); }
1158
1159 #[tokio::test]
1160 async fn test_limits_method() {
1161 let limits = FsLimits::new()
1162 .max_total_bytes(1000)
1163 .max_file_size(500)
1164 .max_file_count(10);
1165 let fs = InMemoryFs::with_limits(limits.clone());
1166
1167 let returned = fs.limits();
1168 assert_eq!(returned.max_total_bytes, 1000);
1169 assert_eq!(returned.max_file_size, 500);
1170 assert_eq!(returned.max_file_count, 10);
1171 }
1172
1173 #[tokio::test]
1174 async fn test_unlimited_fs() {
1175 let fs = InMemoryFs::with_limits(FsLimits::unlimited());
1176
1177 fs.write_file(Path::new("/tmp/large.txt"), &[0u8; 10_000_000])
1179 .await
1180 .unwrap();
1181
1182 let limits = fs.limits();
1183 assert_eq!(limits.max_total_bytes, u64::MAX);
1184 }
1185
1186 #[tokio::test]
1187 async fn test_delete_frees_space() {
1188 let limits = FsLimits::new().max_total_bytes(100);
1189 let fs = InMemoryFs::with_limits(limits);
1190
1191 fs.write_file(Path::new("/tmp/file.txt"), &[0u8; 80])
1193 .await
1194 .unwrap();
1195
1196 let result = fs.write_file(Path::new("/tmp/more.txt"), &[0u8; 80]).await;
1198 assert!(result.is_err());
1199
1200 fs.remove(Path::new("/tmp/file.txt"), false).await.unwrap();
1202
1203 fs.write_file(Path::new("/tmp/more.txt"), &[0u8; 80])
1205 .await
1206 .unwrap();
1207 }
1208
1209 #[tokio::test]
1212 async fn test_write_file_to_directory_fails() {
1213 let fs = InMemoryFs::new();
1214
1215 fs.mkdir(Path::new("/tmp/mydir"), false).await.unwrap();
1217
1218 let result = fs.write_file(Path::new("/tmp/mydir"), b"content").await;
1220 assert!(result.is_err());
1221 let err = result.unwrap_err();
1222 assert!(
1223 err.to_string().contains("directory"),
1224 "Error should mention directory: {}",
1225 err
1226 );
1227 }
1228
1229 #[tokio::test]
1230 async fn test_append_file_to_directory_fails() {
1231 let fs = InMemoryFs::new();
1232
1233 fs.mkdir(Path::new("/tmp/appenddir"), false).await.unwrap();
1235
1236 let result = fs
1238 .append_file(Path::new("/tmp/appenddir"), b"content")
1239 .await;
1240 assert!(result.is_err());
1241 let err = result.unwrap_err();
1242 assert!(
1243 err.to_string().contains("directory"),
1244 "Error should mention directory: {}",
1245 err
1246 );
1247 }
1248
1249 #[tokio::test]
1250 async fn test_mkdir_on_existing_file_fails() {
1251 let fs = InMemoryFs::new();
1252
1253 fs.write_file(Path::new("/tmp/myfile"), b"content")
1255 .await
1256 .unwrap();
1257
1258 let result = fs.mkdir(Path::new("/tmp/myfile"), false).await;
1260 assert!(result.is_err());
1261 }
1262
1263 #[tokio::test]
1264 async fn test_mkdir_recursive_on_existing_file_fails() {
1265 let fs = InMemoryFs::new();
1266
1267 fs.write_file(Path::new("/tmp/myfile"), b"content")
1269 .await
1270 .unwrap();
1271
1272 let result = fs.mkdir(Path::new("/tmp/myfile"), true).await;
1274 assert!(result.is_err());
1275 }
1276
1277 #[tokio::test]
1278 async fn test_mkdir_on_existing_directory_fails() {
1279 let fs = InMemoryFs::new();
1280
1281 let result = fs.mkdir(Path::new("/tmp"), false).await;
1283 assert!(result.is_err());
1284 }
1285
1286 #[tokio::test]
1287 async fn test_mkdir_recursive_on_existing_directory_succeeds() {
1288 let fs = InMemoryFs::new();
1289
1290 let result = fs.mkdir(Path::new("/tmp"), true).await;
1292 assert!(result.is_ok());
1293 }
1294
1295 #[tokio::test]
1296 async fn test_write_file_overwrites_existing_file() {
1297 let fs = InMemoryFs::new();
1298
1299 fs.write_file(Path::new("/tmp/file.txt"), b"original")
1301 .await
1302 .unwrap();
1303
1304 fs.write_file(Path::new("/tmp/file.txt"), b"updated")
1306 .await
1307 .unwrap();
1308
1309 let content = fs.read_file(Path::new("/tmp/file.txt")).await.unwrap();
1310 assert_eq!(content, b"updated");
1311 }
1312}