1use std::collections::{BTreeSet, HashSet};
14use std::io::{Read, Write};
15use std::path::{Component, Path, PathBuf};
16
17use anyhow::anyhow;
18
19use crate::core::{FsBackend, Result};
20
21pub struct DirFS {
53 root: PathBuf, cwd: PathBuf, entries: HashSet<PathBuf>, created_root_parents: Vec<PathBuf>, is_auto_clean: bool,
58}
59
60impl DirFS {
61 pub fn new<P: AsRef<Path>>(root: P) -> Result<Self> {
66 let root = root.as_ref();
67
68 if root.as_os_str().is_empty() {
69 return Err(anyhow!("invalid root path: empty"));
70 }
71 if root.is_relative() {
72 return Err(anyhow!("the root path must be absolute"));
73 }
74 if root.exists() && !root.is_dir() {
75 return Err(anyhow!("{:?} is not a directory", root));
76 }
77
78 let root = Self::normalize(root);
79
80 let mut created_root_parents = Vec::new();
81 if !std::fs::exists(&root)? {
82 created_root_parents.extend(Self::mkdir_all(&root)?);
83 }
84
85 if !Self::check_permissions(&root) {
87 return Err(anyhow!("Access denied: {:?}", root));
88 }
89
90 Ok(Self {
91 root,
92 cwd: PathBuf::from("/"),
93 entries: HashSet::from([PathBuf::from("/")]),
94 created_root_parents,
95 is_auto_clean: true,
96 })
97 }
98
99 pub fn set_auto_clean(&mut self, clean: bool) {
103 self.is_auto_clean = clean;
104 }
105
106 pub fn add<P: AsRef<Path>>(&mut self, path: P) -> Result<()> {
111 let inner = self.to_inner(&path);
112 let host = self.to_host(&inner);
113 if !host.exists() {
114 return Err(anyhow!("No such file or directory: {}", path.as_ref().display()));
115 }
116 self.entries.insert(inner);
117 Ok(())
118 }
119
120 fn normalize<P: AsRef<Path>>(path: P) -> PathBuf {
123 let mut result = PathBuf::new();
124 for component in path.as_ref().components() {
125 match component {
126 Component::CurDir => {}
127 Component::ParentDir => {
128 result.pop();
129 }
130 _ => {
131 result.push(component);
132 }
133 }
134 }
135 if result != PathBuf::from("/") && result.ends_with("/") {
137 result.pop();
138 }
139 result
140 }
141
142 fn to_host<P: AsRef<Path>>(&self, path: P) -> PathBuf {
143 let inner = self.to_inner(path);
144 self.root.join(inner.strip_prefix("/").unwrap())
145 }
146
147 fn to_inner<P: AsRef<Path>>(&self, path: P) -> PathBuf {
148 Self::normalize(self.cwd.join(path))
149 }
150
151 fn mkdir_all<P: AsRef<Path>>(path: P) -> Result<Vec<PathBuf>> {
155 let host_path = path.as_ref().to_path_buf();
156
157 let mut existed_part = host_path.clone();
159 while let Some(parent) = existed_part.parent() {
160 let parent_buf = parent.to_path_buf();
161 if std::fs::exists(parent)? {
162 existed_part = parent_buf;
163 break;
164 }
165 existed_part = parent_buf;
166 }
167
168 let need_to_create: Vec<_> = host_path
170 .strip_prefix(&existed_part)?
171 .components()
172 .collect();
173
174 let mut created = Vec::new();
175
176 let mut built = PathBuf::from(&existed_part);
177 for component in need_to_create {
178 built.push(component);
179 if !std::fs::exists(&built)? {
180 std::fs::create_dir(&built)?;
181 created.push(built.clone());
182 }
183 }
184
185 Ok(created)
186 }
187
188 fn rm_host_artifact<P: AsRef<Path>>(host_path: P) -> Result<()> {
189 let host_path = host_path.as_ref();
190 if host_path.is_dir() {
191 std::fs::remove_dir_all(host_path)?
192 } else {
193 std::fs::remove_file(host_path)?
194 }
195 Ok(())
196 }
197
198 fn check_permissions<P: AsRef<Path>>(path: P) -> bool {
199 let path = path.as_ref();
200 let filename = path.join(".access");
201 if let Err(_) = std::fs::write(&filename, b"check") {
202 return false;
203 }
204 if let Err(_) = std::fs::remove_file(filename) {
205 return false;
206 }
207 true
208 }
209}
210
211impl FsBackend for DirFS {
212 fn root(&self) -> &Path {
214 self.root.as_path()
215 }
216
217 fn cwd(&self) -> &Path {
219 self.cwd.as_path()
220 }
221
222 fn cd<P: AsRef<Path>>(&mut self, path: P) -> Result<()> {
226 let target = self.to_inner(path);
227 if !self.exists(&target) {
228 return Err(anyhow!("{} does not exist", target.display()));
229 }
230 self.cwd = target;
231 Ok(())
232 }
233
234 fn exists<P: AsRef<Path>>(&self, path: P) -> bool {
240 let inner_path = self.to_inner(path);
241 self.entries.contains(&inner_path)
242 }
243
244 fn mkdir<P: AsRef<Path>>(&mut self, path: P) -> Result<()> {
247 if path.as_ref().as_os_str().is_empty() {
248 return Err(anyhow!("invalid path: empty"));
249 }
250
251 let inner_path = self.to_inner(path);
252
253 if self.exists(&inner_path) {
254 return Err(anyhow!("path already exists: {}", inner_path.display()));
255 }
256
257 let mut existed_parent = inner_path.clone();
259 while let Some(parent) = existed_parent.parent() {
260 let parent_buf = parent.to_path_buf();
261 if self.entries.contains(parent) {
262 existed_parent = parent_buf;
263 break;
264 }
265 existed_parent = parent_buf;
266 }
267
268 let need_to_create: Vec<_> = inner_path
270 .strip_prefix(&existed_parent)?
271 .components()
272 .collect();
273
274 let mut built = PathBuf::from(&existed_parent);
275 for component in need_to_create {
276 built.push(component);
277 if !self.entries.contains(&built) {
278 let host = self.to_host(&built);
279 std::fs::create_dir(&host)?;
280 self.entries.insert(built.clone());
281 }
282 }
283
284 Ok(())
285 }
286
287 fn mkfile<P: AsRef<Path>>(&mut self, file_path: P, content: Option<&[u8]>) -> Result<()> {
292 let file_path = self.to_inner(file_path);
293 if let Some(parent) = file_path.parent() {
294 if let Err(e) = std::fs::exists(parent) {
295 return Err(anyhow!("{:?}: {}", parent, e));
296 }
297 }
298 let host = self.to_host(&file_path);
299 let mut fd = std::fs::File::create(host)?;
300 self.entries.insert(file_path);
301 if let Some(content) = content {
302 fd.write_all(content)?;
303 }
304 Ok(())
305 }
306
307 fn read<P: AsRef<Path>>(&self, path: P) -> Result<Vec<u8>> {
322 let inner = self.to_inner(&path);
323 if !self.exists(&inner) {
324 return Err(anyhow!("file does not exist: {}", path.as_ref().display()));
325 }
326 let host = self.to_host(&inner);
327 if host.is_dir() {
328 return Err(anyhow!("{} is a directory", host.display()));
329 }
330
331 let mut content = Vec::new();
332 std::fs::File::open(&host)?.read_to_end(&mut content)?;
333
334 Ok(content)
335 }
336
337 fn write<P: AsRef<Path>>(&self, path: P, content: &[u8]) -> Result<()> {
355 let inner = self.to_inner(&path);
356 let host = self.to_host(&inner);
357
358 if !self.exists(&inner) {
359 return Err(anyhow!("file does not exist: {}", path.as_ref().display()));
360 }
361 if host.is_dir() {
362 return Err(anyhow!("{} is a directory", host.display()));
363 }
364
365 std::fs::write(&host, content)?;
366
367 Ok(())
368 }
369
370 fn append<P: AsRef<Path>>(&self, path: P, content: &[u8]) -> Result<()> {
390 let inner = self.to_inner(&path);
391 let host = self.to_host(&inner);
392
393 if !self.exists(&inner) {
394 return Err(anyhow!("file does not exist: {}", path.as_ref().display()));
395 }
396 if host.is_dir() {
397 return Err(anyhow!("{} is a directory", host.display()));
398 }
399
400 use std::fs::OpenOptions;
402 let mut file = OpenOptions::new().write(true).append(true).open(&host)?;
403
404 file.write_all(content)?;
405
406 Ok(())
407 }
408
409 fn rm<P: AsRef<Path>>(&mut self, path: P) -> Result<()> {
422 if path.as_ref().as_os_str().is_empty() {
423 return Err(anyhow!("invalid path: empty"));
424 }
425 if path.as_ref().as_os_str() == "/" {
426 return Err(anyhow!("invalid path: the root cannot be removed"));
427 }
428
429 let inner_path = self.to_inner(path); let host_path = self.to_host(&inner_path); if !self.exists(&inner_path) {
434 return Err(anyhow!("{} does not exist", inner_path.display()));
435 }
436
437 Self::rm_host_artifact(host_path)?;
439
440 let removed: Vec<PathBuf> = self
442 .entries
443 .iter()
444 .filter(|p| p.starts_with(&inner_path)) .cloned()
446 .collect();
447
448 for p in removed {
450 self.entries.remove(&p);
451 }
452
453 Ok(())
454 }
455
456 fn cleanup(&mut self) -> bool {
458 let mut is_ok = true;
459
460 let mut sorted_paths_to_remove: BTreeSet<PathBuf> = BTreeSet::new();
462 for entry in &self.entries {
463 if entry != &PathBuf::from("/") {
464 sorted_paths_to_remove.insert(entry.clone());
465 }
466 }
467
468 for entry in sorted_paths_to_remove.iter().rev() {
469 let host = self.to_host(entry);
470 let result = Self::rm_host_artifact(&host);
471 if result.is_ok() {
472 self.entries.remove(entry);
473 } else {
474 is_ok = false;
475 eprintln!("Unable to remove: {}", host.display());
476 }
477 }
478
479 is_ok
480 }
481}
482
483impl Drop for DirFS {
484 fn drop(&mut self) {
485 if !self.is_auto_clean {
486 return;
487 }
488
489 if self.cleanup() {
490 self.entries.clear();
491 }
492
493 let errors: Vec<_> = self
494 .created_root_parents
495 .iter()
496 .rev()
497 .filter_map(|p| Self::rm_host_artifact(p).err())
498 .collect();
499 if !errors.is_empty() {
500 eprintln!("Failed to remove parents: {:?}", errors);
501 }
502
503 self.created_root_parents.clear();
504 }
505}
506
507#[cfg(test)]
508mod tests {
509 use super::*;
510 use tempdir::TempDir;
511
512 mod creations {
513 use super::*;
514
515 #[test]
516 fn test_new_absolute_path_existing() {
517 let temp_dir = setup_test_env();
518 let root = temp_dir.path().to_path_buf();
519
520 let fs = DirFS::new(&root).unwrap();
521
522 assert_eq!(fs.root, root);
523 assert_eq!(fs.cwd, PathBuf::from("/"));
524 assert!(fs.entries.contains(&PathBuf::from("/")));
525 assert!(fs.created_root_parents.is_empty());
526 assert!(fs.is_auto_clean);
527 }
528
529 #[test]
530 fn test_new_nonexistent_path_created() {
531 let temp_dir = setup_test_env();
532 let nonexistent = temp_dir.path().join("new_root");
533
534 let fs = DirFS::new(&nonexistent).unwrap();
535
536 assert_eq!(fs.root, nonexistent);
537 assert!(!fs.created_root_parents.is_empty()); assert!(nonexistent.exists()); }
540
541 #[test]
542 fn test_new_nested_nonexistent_path() {
543 let temp_dir = setup_test_env();
544 let nested = temp_dir.path().join("a/b/c");
545
546 let fs = DirFS::new(&nested).unwrap();
547
548 assert_eq!(fs.root, nested);
549 assert_eq!(fs.created_root_parents.len(), 3); assert!(nested.exists());
551 }
552
553 #[test]
554 fn test_new_permission_denied() {
555 #[cfg(unix)]
557 {
558 use std::os::unix::fs::PermissionsExt;
559
560 let temp_dir = setup_test_env();
561 let protected = temp_dir.path().join("protected");
562 let protected_root = protected.join("root");
563 std::fs::create_dir_all(&protected_root).unwrap();
564 std::fs::set_permissions(&protected, PermissionsExt::from_mode(0o000)).unwrap(); let result = DirFS::new(&protected_root);
567 assert!(result.is_err());
568
569 std::fs::set_permissions(&protected, PermissionsExt::from_mode(0o755)).unwrap(); }
571 }
572
573 #[test]
574 fn test_new_normalize_path() {
575 let temp_dir = setup_test_env();
576 let messy_path = temp_dir.path().join("././subdir/../subdir");
577
578 let fs = DirFS::new(&messy_path).unwrap();
579 let canonical = DirFS::normalize(temp_dir.path().join("subdir"));
580
581 assert_eq!(fs.root, canonical);
582 }
583
584 #[test]
585 fn test_new_root_is_file() {
586 let temp_dir = setup_test_env();
587 let file_path = temp_dir.path().join("file.txt");
588 std::fs::write(&file_path, "content").unwrap();
589
590 let result = DirFS::new(&file_path);
591 assert!(result.is_err()); }
593
594 #[test]
595 fn test_new_empty_path() {
596 let result = DirFS::new("");
597 assert!(result.is_err());
598 }
599
600 #[test]
601 fn test_new_special_characters() {
602 let temp_dir = setup_test_env();
603 let special = temp_dir.path().join("папка с пробелами и юникод!");
604
605 let fs = DirFS::new(&special).unwrap();
606
607 assert_eq!(fs.root, special);
608 assert!(special.exists());
609 }
610
611 #[test]
612 fn test_new_is_auto_clean_default() {
613 let temp_dir = setup_test_env();
614 let fs = DirFS::new(temp_dir.path()).unwrap();
615 assert!(fs.is_auto_clean); }
617
618 #[test]
619 fn test_root_returns_correct_path() {
620 let temp_dir = setup_test_env();
621
622 let vfs_root = temp_dir.path().join("vfs-root");
623 let fs = DirFS::new(&vfs_root).unwrap();
624 assert_eq!(fs.root(), vfs_root);
625 }
626
627 #[test]
628 fn test_cwd_defaults_to_root() {
629 let temp_dir = setup_test_env();
630 let fs = DirFS::new(temp_dir).unwrap();
631 assert_eq!(fs.cwd(), Path::new("/"));
632 }
633 }
634
635 mod normalize {
636 use super::*;
637
638 #[test]
639 fn test_normalize_path() {
640 assert_eq!(DirFS::normalize("/a/b/c/"), PathBuf::from("/a/b/c"));
641 assert_eq!(DirFS::normalize("/a/b/./c"), PathBuf::from("/a/b/c"));
642 assert_eq!(DirFS::normalize("/a/b/../c"), PathBuf::from("/a/c"));
643 assert_eq!(DirFS::normalize("/"), PathBuf::from("/"));
644 assert_eq!(DirFS::normalize("/.."), PathBuf::from("/"));
645 assert_eq!(DirFS::normalize(".."), PathBuf::from(""));
646 assert_eq!(DirFS::normalize(""), PathBuf::from(""));
647 assert_eq!(DirFS::normalize("../a"), PathBuf::from("a"));
648 assert_eq!(DirFS::normalize("./a"), PathBuf::from("a"));
649 }
650 }
651
652 mod cd {
653 use super::*;
654
655 #[test]
656 fn test_cd_to_absolute_path() {
657 let temp_dir = setup_test_env();
658 let mut fs = DirFS::new(&temp_dir).unwrap();
659 fs.mkdir("/projects").unwrap();
660 fs.cd("/projects").unwrap();
661 assert_eq!(fs.cwd(), Path::new("/projects"));
662 }
663
664 #[test]
665 fn test_cd_with_relative_path() {
666 let temp_dir = setup_test_env();
667 let mut fs = DirFS::new(&temp_dir).unwrap();
668 fs.mkdir("/home/user").unwrap();
669 fs.cwd = PathBuf::from("/home");
670 fs.cd("user").unwrap();
671 assert_eq!(fs.cwd(), Path::new("/home/user"));
672 }
673
674 #[test]
675 fn test_cd_extreme_cases() {
676 let temp_dir = setup_test_env();
677 let mut fs = DirFS::new(&temp_dir).unwrap();
678
679 fs.cd("..").unwrap(); assert_eq!(fs.cwd(), Path::new("/"));
681
682 fs.cd(".").unwrap(); assert_eq!(fs.cwd(), Path::new("/"));
684
685 fs.cwd = PathBuf::from("/home");
686 assert_eq!(fs.cwd(), Path::new("/home"));
687 fs.mkdir("/other").unwrap();
688 fs.cd("../other").unwrap();
689 assert_eq!(fs.cwd(), Path::new("/other"));
690
691 fs.cwd = PathBuf::from("/home");
692 assert_eq!(fs.cwd(), Path::new("/home"));
693 fs.mkdir("/home/other").unwrap();
694 fs.cd("./other").unwrap();
695 assert_eq!(fs.cwd(), Path::new("/home/other"));
696 }
697 }
698
699 mod mkdir {
700 use super::*;
701
702 #[test]
703 fn test_mkdir_create_single_dir() {
704 let temp_dir = setup_test_env();
705 let mut fs = DirFS::new(&temp_dir).unwrap();
706 fs.mkdir("/projects").unwrap();
707 assert!(fs.exists("/projects"));
708 }
709
710 #[test]
711 fn test_mkdir_relative_path() {
712 let temp_dir = setup_test_env();
713 let mut fs = DirFS::new(&temp_dir).unwrap();
714 fs.mkdir("home").unwrap();
715 fs.cd("/home").unwrap();
716 fs.mkdir("user").unwrap();
717 assert!(fs.exists("/home/user"));
718 }
719
720 #[test]
721 fn test_mkdir_nested_path() {
722 let temp_dir = setup_test_env();
723 let mut fs = DirFS::new(&temp_dir).unwrap();
724 fs.mkdir("/a/b/c").unwrap();
725 assert!(fs.exists("/a"));
726 assert!(fs.exists("/a/b"));
727 assert!(fs.exists("/a/b/c"));
728 }
729
730 #[test]
731 fn test_mkdir_already_exists() {
732 let temp_dir = setup_test_env();
733 let mut fs = DirFS::new(&temp_dir).unwrap();
734 fs.mkdir("/data").unwrap();
735 let result = fs.mkdir("/data");
736 assert!(result.is_err());
737 }
738
739 #[test]
740 fn test_mkdir_invalid_path() {
741 let temp_dir = setup_test_env();
742 let mut fs = DirFS::new(&temp_dir).unwrap();
743 let result = fs.mkdir("");
744 assert!(result.is_err());
745 }
746 }
747
748 mod exists {
749 use super::*;
750
751 #[test]
752 fn test_exists_root() {
753 let temp_dir = setup_test_env();
754 let fs = DirFS::new(&temp_dir).unwrap();
755 assert!(fs.exists("/"));
756 }
757
758 #[test]
759 fn test_exists_cwd() {
760 let temp_dir = setup_test_env();
761 let mut fs = DirFS::new(&temp_dir).unwrap();
762 fs.mkdir("/projects").unwrap();
763 fs.cd("/projects").unwrap();
764 assert!(fs.exists("."));
765 assert!(fs.exists("./"));
766 assert!(fs.exists("/projects"));
767 }
768 }
769
770 mod mkdir_all {
771 use super::*;
772 use std::fs;
773 use std::path::PathBuf;
774
775 #[test]
776 fn test_mkdir_all_simple_creation() {
777 let temp_dir = setup_test_env();
778 let target = temp_dir.path().join("a/b/c");
779
780 let created = DirFS::mkdir_all(&target).unwrap();
781
782 assert_eq!(created.len(), 3);
783 assert!(created.contains(&temp_dir.path().join("a")));
784 assert!(created.contains(&temp_dir.path().join("a/b")));
785 assert!(created.contains(&temp_dir.path().join("a/b/c")));
786
787 assert!(temp_dir.path().join("a").is_dir());
789 assert!(temp_dir.path().join("a/b").is_dir());
790 assert!(temp_dir.path().join("a/b/c").is_dir());
791 }
792
793 #[test]
794 fn test_mkdir_all_existing_parent() {
795 let temp_dir = setup_test_env();
796 fs::create_dir_all(temp_dir.path().join("a")).unwrap(); let target = temp_dir.path().join("a/b/c");
799 let created = DirFS::mkdir_all(&target).unwrap();
800
801 assert_eq!(created.len(), 2); assert!(created.contains(&temp_dir.path().join("a/b")));
803 assert!(created.contains(&temp_dir.path().join("a/b/c")));
804 }
805
806 #[test]
807 fn test_mkdir_all_target_exists() {
808 let temp_dir = setup_test_env();
809 fs::create_dir_all(temp_dir.path().join("x/y")).unwrap();
810
811 let target = temp_dir.path().join("x/y");
812 let created = DirFS::mkdir_all(&target).unwrap();
813
814 assert!(created.is_empty()); }
816
817 #[test]
818 fn test_mkdir_all_root_path() {
819 let result = DirFS::mkdir_all("/");
821 assert!(result.is_ok());
822 assert!(result.unwrap().is_empty());
823 }
824
825 #[test]
826 fn test_mkdir_all_single_dir() {
827 let temp_dir = setup_test_env();
828 let target = temp_dir.path().join("single");
829
830 let created = DirFS::mkdir_all(&target).unwrap();
831
832 assert_eq!(created.len(), 1);
833 assert!(created.contains(&target));
834 assert!(target.is_dir());
835 }
836
837 #[test]
838 fn test_mkdir_all_absolute_vs_relative() {
839 let temp_dir = setup_test_env();
840
841 let abs_target = temp_dir.path().join("abs/a/b");
843 let abs_created = DirFS::mkdir_all(&abs_target).unwrap();
844
845 assert!(!abs_created.is_empty());
846 }
847
848 #[test]
849 fn test_mkdir_all_nested_existing() {
850 let temp_dir = setup_test_env();
851 fs::create_dir_all(temp_dir.path().join("deep/a")).unwrap();
852
853 let target = temp_dir.path().join("deep/a/b/c/d");
854 let created = DirFS::mkdir_all(&target).unwrap();
855
856 assert_eq!(created.len(), 3); }
858
859 #[test]
860 fn test_mkdir_all_invalid_path() {
861 #[cfg(unix)]
863 {
864 let invalid_path = PathBuf::from("/nonexistent/parent/child");
865
866 let result = DirFS::mkdir_all(&invalid_path);
868 assert!(result.is_err());
869 }
870 }
871
872 #[test]
873 fn test_mkdir_all_file_in_path() {
874 let temp_dir = setup_test_env();
875 let file_path = temp_dir.path().join("file.txt");
876 fs::write(&file_path, "content").unwrap(); let target = file_path.join("subdir"); let result = DirFS::mkdir_all(&target);
881 assert!(result.is_err()); }
883
884 #[test]
885 fn test_mkdir_all_trailing_slash() {
886 let temp_dir = setup_test_env();
887 let target = temp_dir.path().join("trailing/");
888
889 let created = DirFS::mkdir_all(&target).unwrap();
890 assert!(!created.is_empty());
891 assert!(temp_dir.path().join("trailing").is_dir());
892 }
893
894 #[test]
895 fn test_mkdir_all_unicode_paths() {
896 let temp_dir = setup_test_env();
897 let target = temp_dir.path().join("папка/файл");
898
899 let created = DirFS::mkdir_all(&target).unwrap();
900
901 assert_eq!(created.len(), 2);
902 assert!(temp_dir.path().join("папка").is_dir());
903 assert!(temp_dir.path().join("папка/файл").is_dir());
904 }
905
906 #[test]
907 fn test_mkdir_all_permissions_error() {
908 #[cfg(unix)]
911 {
912 use std::os::unix::fs::PermissionsExt;
913
914 let temp_dir = setup_test_env();
915 fs::set_permissions(&temp_dir, PermissionsExt::from_mode(0o444)).unwrap(); let target = temp_dir.path().join("protected/dir");
918 let result = DirFS::mkdir_all(&target);
919
920 assert!(result.is_err());
921 }
922 }
923 }
924
925 mod drop {
926 use super::*;
927
928 #[test]
929 fn test_drop_removes_created_directories() {
930 let temp_dir = setup_test_env();
931 let root = temp_dir.path().join("to_remove");
932
933 let fs = DirFS::new(&root).unwrap();
935 assert!(root.exists());
936
937 drop(fs);
939
940 assert!(!root.exists());
942 }
943
944 #[test]
945 fn test_drop_only_removes_created_parents() {
946 let temp_dir = setup_test_env();
947 let parent = temp_dir.path().join("parent");
948 let child = parent.join("child");
949
950 std::fs::create_dir_all(&parent).unwrap(); let fs = DirFS::new(&child).unwrap();
952
953 assert!(parent.exists()); assert!(child.exists());
955
956 drop(fs);
957
958 assert!(parent.exists()); assert!(!child.exists()); }
961
962 #[test]
963 fn test_drop_with_is_auto_clean_false() {
964 let temp_dir = setup_test_env();
965 let root = temp_dir.path().join("keep");
966
967 let mut fs = DirFS::new(&root).unwrap();
968 fs.is_auto_clean = false; drop(fs);
971
972 assert!(root.exists()); }
974
975 #[test]
976 fn test_drop_empty_created_root_parents() {
977 let temp_dir = setup_test_env();
978 let existing = temp_dir.path().join("existing");
979 std::fs::create_dir(&existing).unwrap();
980
981 let fs = DirFS::new(&existing).unwrap(); drop(fs);
984
985 assert!(existing.exists()); }
987
988 #[test]
989 fn test_drop_nested_directories_removed() {
990 let temp_dir = setup_test_env();
991 let nested = temp_dir.path().join("a/b/c");
992
993 let fs = DirFS::new(&nested).unwrap();
994 assert!(nested.exists());
995
996 drop(fs);
997
998 assert!(!temp_dir.path().join("a").exists());
1000 assert!(!temp_dir.path().join("a/b").exists());
1001 assert!(!nested.exists());
1002 }
1003
1004 #[test]
1007 fn test_drop_removes_entries_created_by_mkdir() {
1008 let temp_dir = setup_test_env();
1009 let root = temp_dir.path().join("test_root");
1010
1011 let mut fs = DirFS::new(&root).unwrap();
1012 fs.mkdir("/subdir").unwrap();
1013 assert!(root.join("subdir").exists());
1014
1015 drop(fs);
1016
1017 assert!(!root.exists()); assert!(!root.join("subdir").exists()); }
1020
1021 #[test]
1022 fn test_drop_removes_entries_created_by_mkfile() {
1023 let temp_dir = setup_test_env();
1024 let root = temp_dir.path().join("test_root");
1025
1026 let mut fs = DirFS::new(&root).unwrap();
1027 fs.mkfile("/file.txt", None).unwrap();
1028 assert!(root.join("file.txt").exists());
1029
1030 drop(fs);
1031
1032 assert!(!root.exists());
1033 assert!(!root.join("file.txt").exists());
1034 }
1035
1036 #[test]
1037 fn test_drop_handles_nested_entries() {
1038 let temp_dir = setup_test_env();
1039 let root = temp_dir.path().join("test_root");
1040
1041 let mut fs = DirFS::new(&root).unwrap();
1042 fs.mkdir("/a/b/c").unwrap();
1043 fs.mkfile("/a/file.txt", None).unwrap();
1044
1045 assert!(root.join("a/b/c").exists());
1046 assert!(root.join("a/file.txt").exists());
1047
1048 drop(fs);
1049
1050 assert!(!root.exists());
1051 }
1052
1053 #[test]
1054 fn test_drop_ignores_non_entries() {
1055 let temp_dir = setup_test_env();
1056 let root = temp_dir.path().join("test_root");
1057 let external = temp_dir.path().join("external_file.txt");
1058
1059 std::fs::write(&external, "content").unwrap(); let fs = DirFS::new(&root).unwrap();
1062 drop(fs);
1063
1064 assert!(!root.exists());
1065 assert!(external.exists()); }
1067
1068 #[test]
1069 fn test_drop_with_empty_entries() {
1070 let temp_dir = setup_test_env();
1071 let root = temp_dir.path().join("empty_root");
1072
1073 let fs = DirFS::new(&root).unwrap();
1074 drop(fs);
1077
1078 assert!(!root.exists());
1079 }
1080 }
1081
1082 mod mkfile {
1083 use super::*;
1084
1085 #[test]
1086 fn test_mkfile_simple_creation() {
1087 let temp_dir = setup_test_env();
1088 let root = temp_dir.path();
1089
1090 let mut fs = DirFS::new(root).unwrap();
1091 fs.mkfile("/file.txt", None).unwrap();
1092
1093 assert!(fs.exists("/file.txt"));
1094 assert!(root.join("file.txt").exists());
1095 assert_eq!(fs.entries.contains(&PathBuf::from("/file.txt")), true);
1096 }
1097
1098 #[test]
1099 fn test_mkfile_with_content() {
1100 let temp_dir = setup_test_env();
1101 let root = temp_dir.path();
1102
1103 let mut fs = DirFS::new(root).unwrap();
1104 let content = b"Hello, VFS!";
1105 fs.mkfile("/data.bin", Some(content)).unwrap();
1106
1107 assert!(fs.exists("/data.bin"));
1108 let file_content = std::fs::read(root.join("data.bin")).unwrap();
1109 assert_eq!(&file_content, content);
1110 }
1111
1112 #[test]
1113 fn test_mkfile_in_subdirectory() {
1114 let temp_dir = setup_test_env();
1115 let root = temp_dir.path();
1116
1117 let mut fs = DirFS::new(root).unwrap();
1118 fs.mkdir("/subdir").unwrap();
1119 fs.mkfile("/subdir/file.txt", None).unwrap();
1120
1121 assert!(fs.exists("/subdir/file.txt"));
1122 assert!(root.join("subdir/file.txt").exists());
1123 }
1124
1125 #[test]
1126 fn test_mkfile_parent_does_not_exist() {
1127 let temp_dir = setup_test_env();
1128 let root = temp_dir.path();
1129
1130 let mut fs = DirFS::new(root).unwrap();
1131
1132 let result = fs.mkfile("/nonexistent/file.txt", None);
1133 assert!(result.is_err());
1134 }
1135
1136 #[test]
1137 fn test_mkfile_file_already_exists() {
1138 let temp_dir = setup_test_env();
1139 let root = temp_dir.path();
1140
1141 let mut fs = DirFS::new(root).unwrap();
1142 fs.mkfile("/existing.txt", None).unwrap();
1143
1144 let result = fs.mkfile("/existing.txt", None);
1146 assert!(result.is_ok()); assert!(fs.exists("/existing.txt"));
1148 }
1149
1150 #[test]
1151 fn test_mkfile_empty_content() {
1152 let temp_dir = setup_test_env();
1153 let root = temp_dir.path();
1154
1155 let mut fs = DirFS::new(root).unwrap();
1156 fs.mkfile("/empty.txt", Some(&[])).unwrap(); assert!(fs.exists("/empty.txt"));
1159 let file_size = std::fs::metadata(root.join("empty.txt")).unwrap().len();
1160 assert_eq!(file_size, 0);
1161 }
1162
1163 #[test]
1164 fn test_mkfile_relative_path() {
1165 let temp_dir = setup_test_env();
1166 let root = temp_dir.path();
1167
1168 let mut fs = DirFS::new(root).unwrap();
1169 fs.mkdir("/sub").unwrap();
1170 fs.cd("/sub").unwrap(); fs.mkfile("relative.txt", None).unwrap(); assert!(fs.exists("/sub/relative.txt"));
1175 assert!(root.join("sub/relative.txt").exists());
1176 }
1177
1178 #[test]
1179 fn test_mkfile_normalize_path() {
1180 let temp_dir = setup_test_env();
1181 let root = temp_dir.path();
1182
1183 let mut fs = DirFS::new(root).unwrap();
1184 fs.mkdir("/normalized").unwrap();
1185
1186 fs.mkfile("/./normalized/../normalized/file.txt", None)
1187 .unwrap();
1188
1189 assert!(fs.exists("/normalized/file.txt"));
1190 assert!(root.join("normalized/file.txt").exists());
1191 }
1192
1193 #[test]
1194 fn test_mkfile_invalid_path_components() {
1195 let temp_dir = setup_test_env();
1196 let root = temp_dir.path();
1197
1198 let mut fs = DirFS::new(root).unwrap();
1199
1200 #[cfg(unix)]
1202 {
1203 let result = fs.mkfile("/invalid\0name.txt", None);
1204 assert!(result.is_err()); }
1206 }
1207
1208 #[test]
1209 fn test_mkfile_permission_denied() {
1210 #[cfg(unix)]
1211 {
1212 use std::os::unix::fs::PermissionsExt;
1213
1214 let temp_dir = setup_test_env();
1215 let root = temp_dir.path();
1216 let protected = root.join("protected");
1217 std::fs::create_dir(&protected).unwrap();
1218 std::fs::set_permissions(&protected, PermissionsExt::from_mode(0o000)).unwrap(); let mut fs = DirFS::new(root).unwrap();
1221 let result = fs.mkfile("/protected/file.txt", None);
1222
1223 std::fs::set_permissions(&protected, PermissionsExt::from_mode(0o755)).unwrap(); assert!(result.is_err());
1226 assert!(
1227 result
1228 .unwrap_err()
1229 .to_string()
1230 .contains("Permission denied")
1231 );
1232 }
1233 }
1234
1235 #[test]
1236 fn test_mkfile_root_directory() {
1237 let temp_dir = setup_test_env();
1238 let root = temp_dir.path();
1239
1240 let mut fs = DirFS::new(root).unwrap();
1241
1242 let result = fs.mkfile("/", None);
1244 assert!(result.is_err());
1245 }
1246
1247 #[test]
1248 fn test_mkfile_unicode_filename() {
1249 let temp_dir = setup_test_env();
1250 let root = temp_dir.path();
1251
1252 let mut fs = DirFS::new(root).unwrap();
1253 fs.mkfile("/тест.txt", Some(b"Content")).unwrap();
1254
1255 assert!(fs.exists("/тест.txt"));
1256 assert!(root.join("тест.txt").exists());
1257 let content = std::fs::read_to_string(root.join("тест.txt")).unwrap();
1258 assert_eq!(content, "Content");
1259 }
1260 }
1261
1262 mod read {
1263 use super::*;
1264
1265 #[test]
1266 fn test_read_existing_file() -> Result<()> {
1267 let temp_dir = setup_test_env();
1268 let mut fs = DirFS::new(&temp_dir)?;
1269
1270 fs.mkfile("/test.txt", Some(b"Hello, VFS!"))?;
1272
1273 let content = fs.read("/test.txt")?;
1275 assert_eq!(content, b"Hello, VFS!");
1276
1277 Ok(())
1278 }
1279
1280 #[test]
1281 fn test_read_nonexistent_file() -> Result<()> {
1282 let temp_dir = setup_test_env();
1283 let fs = DirFS::new(temp_dir.path())?;
1284
1285 let result = fs.read("/not/found.txt");
1286 assert!(result.is_err());
1287 assert!(
1288 result
1289 .unwrap_err()
1290 .to_string()
1291 .contains("file does not exist: /not/found.txt")
1292 );
1293
1294 Ok(())
1295 }
1296
1297 #[test]
1298 fn test_read_directory_as_file() -> Result<()> {
1299 let temp_dir = setup_test_env();
1300 let mut fs = DirFS::new(temp_dir.path())?;
1301
1302 fs.mkdir("/empty_dir")?;
1303
1304 let result = fs.read("/empty_dir");
1305 assert!(result.is_err());
1306 assert!(result.unwrap_err().to_string().contains("is a directory"));
1308
1309 Ok(())
1310 }
1311
1312 #[test]
1313 fn test_read_empty_file() -> Result<()> {
1314 let temp_dir = setup_test_env();
1315 let mut fs = DirFS::new(temp_dir.path())?;
1316
1317 fs.mkfile("/empty.txt", None)?; let content = fs.read("/empty.txt")?;
1320 assert_eq!(content.len(), 0);
1321
1322 Ok(())
1323 }
1324
1325 #[test]
1326 fn test_read_relative_path() -> Result<()> {
1327 let temp_dir = setup_test_env();
1328 let mut fs = DirFS::new(temp_dir.path())?;
1329
1330 fs.cd("/")?;
1331 fs.mkdir("/parent")?;
1332 fs.cd("/parent")?;
1333 fs.mkfile("child.txt", Some(b"Content"))?;
1334
1335 let content = fs.read("child.txt")?;
1337 assert_eq!(content, b"Content");
1338
1339 Ok(())
1340 }
1341
1342 #[test]
1343 fn test_read_unicode_path() -> Result<()> {
1344 let temp_dir = setup_test_env();
1345 let mut fs = DirFS::new(temp_dir.path())?;
1346
1347 fs.mkdir("/папка")?;
1348 fs.mkfile("/папка/файл.txt", Some(b"Unicode content"))?;
1349
1350 let content = fs.read("/папка/файл.txt")?;
1351 assert_eq!(content, b"Unicode content");
1352
1353 Ok(())
1354 }
1355
1356 #[test]
1357 fn test_read_permission_denied() -> Result<()> {
1358 #[cfg(unix)]
1359 {
1360 use std::os::unix::fs::PermissionsExt;
1361
1362 let temp_dir = setup_test_env();
1363 let mut fs = DirFS::new(temp_dir.path())?;
1364
1365 fs.mkfile("/protected.txt", Some(b"Secret"))?;
1367 let host_path = temp_dir.path().join("protected.txt");
1368 std::fs::set_permissions(&host_path, PermissionsExt::from_mode(0o000))?;
1369
1370 let result = fs.read("/protected.txt");
1372 assert!(result.is_err());
1373 assert!(
1374 result
1375 .unwrap_err()
1376 .to_string()
1377 .contains("Permission denied")
1378 );
1379
1380 std::fs::set_permissions(&host_path, PermissionsExt::from_mode(0o644))?;
1382 }
1383 Ok(())
1384 }
1385
1386 #[test]
1387 fn test_read_root_file() -> Result<()> {
1388 let temp_dir = setup_test_env();
1389 let mut fs = DirFS::new(temp_dir.path())?;
1390
1391 fs.mkfile("/root_file.txt", Some(b"At root"))?;
1392 let content = fs.read("/root_file.txt")?;
1393 assert_eq!(content, b"At root");
1394
1395 Ok(())
1396 }
1397 }
1398
1399 mod write {
1400 use super::*;
1401
1402 #[test]
1403 fn test_write_new_file() -> Result<()> {
1404 let temp_dir = setup_test_env();
1405 let mut fs = DirFS::new(temp_dir.path())?;
1406
1407 fs.mkfile("/new.txt", None)?;
1408 let content = b"Hello, VFS!";
1409 fs.write("/new.txt", content)?;
1410
1411 assert!(fs.exists("/new.txt"));
1413 let read_back = fs.read("/new.txt")?;
1414 assert_eq!(read_back, content);
1415
1416 Ok(())
1417 }
1418
1419 #[test]
1420 fn test_write_existing_file_overwrite() -> Result<()> {
1421 let temp_dir = setup_test_env();
1422 let mut fs = DirFS::new(temp_dir.path())?;
1423
1424 fs.mkfile("/exist.txt", Some(b"Old content"))?;
1425
1426 let new_content = b"New content";
1427 fs.write("/exist.txt", new_content)?;
1428
1429 let read_back = fs.read("/exist.txt")?;
1430 assert_eq!(read_back, new_content);
1431
1432 Ok(())
1433 }
1434
1435 #[test]
1436 fn test_write_to_directory_path() -> Result<()> {
1437 let temp_dir = setup_test_env();
1438 let mut fs = DirFS::new(temp_dir.path())?;
1439
1440 fs.mkdir("/dir")?;
1441
1442 let result = fs.write("/dir", b"Content");
1443 assert!(result.is_err());
1444 assert!(result.unwrap_err().to_string().contains("is a directory"));
1445
1446 Ok(())
1447 }
1448
1449 #[test]
1450 fn test_write_to_nonexistent_file() -> Result<()> {
1451 let temp_dir = setup_test_env();
1452 let fs = DirFS::new(temp_dir.path())?;
1453
1454 let result = fs.write("/parent/child.txt", b"Content");
1455 assert!(result.is_err());
1456 assert!(
1457 result
1458 .unwrap_err()
1459 .to_string()
1460 .contains("file does not exist")
1461 );
1462
1463 Ok(())
1464 }
1465
1466 #[test]
1467 fn test_write_empty_content() -> Result<()> {
1468 let temp_dir = setup_test_env();
1469 let mut fs = DirFS::new(temp_dir.path())?;
1470
1471 fs.mkfile("/empty.txt", None)?;
1472 fs.write("/empty.txt", &[])?;
1473
1474 let read_back = fs.read("/empty.txt")?;
1475 assert!(read_back.is_empty());
1476
1477 Ok(())
1478 }
1479
1480 #[test]
1481 fn test_write_relative_path() -> Result<()> {
1482 let temp_dir = setup_test_env();
1483 let mut fs = DirFS::new(temp_dir.path())?;
1484
1485 fs.mkdir("/docs")?;
1486 fs.cd("docs")?;
1487
1488 fs.mkfile("file.txt", None)?;
1489 let content = b"Relative write";
1490 fs.write("file.txt", content)?;
1491
1492 let read_back = fs.read("/docs/file.txt")?;
1493 assert_eq!(read_back, content);
1494
1495 Ok(())
1496 }
1497 }
1498
1499 mod append {
1500 use super::*;
1501
1502 #[test]
1503 fn test_append_to_existing_file() -> Result<()> {
1504 let temp_dir = setup_test_env();
1505 let mut fs = DirFS::new(temp_dir.path())?;
1506
1507 fs.mkfile("/log.txt", Some(b"Initial content\n"))?;
1509
1510 fs.append("/log.txt", b"Appended line 1\n")?;
1512 fs.append("/log.txt", b"Appended line 2\n")?;
1513
1514 let content = fs.read("/log.txt")?;
1516 assert_eq!(
1517 content,
1518 b"Initial content\nAppended line 1\nAppended line 2\n"
1519 );
1520
1521 Ok(())
1522 }
1523
1524 #[test]
1525 fn test_append_to_empty_file() -> Result<()> {
1526 let temp_dir = setup_test_env();
1527 let mut fs = DirFS::new(temp_dir.path())?;
1528
1529 fs.mkfile("/empty.txt", Some(&[]))?;
1531
1532 fs.append("/empty.txt", b"First append\n")?;
1534 fs.append("/empty.txt", b"Second append\n")?;
1535
1536 let content = fs.read("/empty.txt")?;
1537 assert_eq!(content, b"First append\nSecond append\n");
1538
1539 Ok(())
1540 }
1541
1542 #[test]
1543 fn test_append_nonexistent_file() -> Result<()> {
1544 let temp_dir = setup_test_env();
1545 let fs = DirFS::new(temp_dir.path())?;
1546
1547 let result = fs.append("/not_found.txt", b"Content");
1548 assert!(result.is_err());
1549 assert!(
1550 result
1551 .unwrap_err()
1552 .to_string()
1553 .contains("file does not exist: /not_found.txt")
1554 );
1555
1556 Ok(())
1557 }
1558
1559 #[test]
1560 fn test_append_to_directory() -> Result<()> {
1561 let temp_dir = setup_test_env();
1562 let mut fs = DirFS::new(temp_dir.path())?;
1563
1564 fs.mkdir("/mydir")?;
1565
1566 let result = fs.append("/mydir", b"Content");
1567 assert!(result.is_err());
1568 assert!(result.unwrap_err().to_string().contains("is a directory"));
1569
1570 Ok(())
1571 }
1572
1573 #[test]
1574 fn test_append_empty_content() -> Result<()> {
1575 let temp_dir = setup_test_env();
1576 let mut fs = DirFS::new(temp_dir.path())?;
1577
1578 fs.mkfile("/test.txt", Some(b"Existing\n"))?;
1579
1580 fs.append("/test.txt", &[])?;
1582
1583 let content = fs.read("/test.txt")?;
1585 assert_eq!(content, b"Existing\n");
1586
1587 Ok(())
1588 }
1589
1590 #[test]
1591 fn test_append_relative_path() -> Result<()> {
1592 let temp_dir = setup_test_env();
1593 let mut fs = DirFS::new(temp_dir.path())?;
1594
1595 fs.mkdir("/docs")?;
1596 fs.cd("/docs")?;
1597 fs.mkfile("log.txt", Some(b"Start\n"))?; fs.append("log.txt", b"Added\n")?;
1600
1601 let content = fs.read("/docs/log.txt")?;
1602 assert_eq!(content, b"Start\nAdded\n");
1603
1604 Ok(())
1605 }
1606
1607 #[test]
1608 fn test_append_unicode_path() -> Result<()> {
1609 let temp_dir = setup_test_env();
1610 let mut fs = DirFS::new(temp_dir.path())?;
1611
1612 let first = Vec::from("Начало\n");
1613 let second = Vec::from("Продолжение\n");
1614
1615 fs.mkdir("/папка")?;
1616 fs.mkfile("/папка/файл.txt", Some(first.as_slice()))?;
1617 fs.append("/папка/файл.txt", second.as_slice())?;
1618
1619 let content = fs.read("/папка/файл.txt")?;
1620
1621 let mut expected = Vec::from(first);
1622 expected.extend(second);
1623
1624 assert_eq!(content, expected);
1625
1626 Ok(())
1627 }
1628
1629 #[test]
1630 fn test_concurrent_append_safety() -> Result<()> {
1631 let temp_dir = setup_test_env();
1632 let mut fs = DirFS::new(temp_dir.path())?;
1633
1634 fs.mkfile("/concurrent.txt", Some(b""))?;
1635
1636 for i in 1..=3 {
1638 fs.append("/concurrent.txt", format!("Line {}\n", i).as_bytes())?;
1639 }
1640
1641 let content = fs.read("/concurrent.txt")?;
1642 assert_eq!(content, b"Line 1\nLine 2\nLine 3\n");
1643
1644 Ok(())
1645 }
1646
1647 #[test]
1648 fn test_append_permission_denied() -> Result<()> {
1649 #[cfg(unix)]
1650 {
1651 use std::os::unix::fs::PermissionsExt;
1652
1653 let temp_dir = setup_test_env();
1654 let mut fs = DirFS::new(temp_dir.path())?;
1655
1656 fs.mkfile("/protected.txt", Some(b"Content"))?;
1658 let host_path = temp_dir.path().join("protected.txt");
1659 std::fs::set_permissions(&host_path, PermissionsExt::from_mode(0o000))?;
1660
1661 let result = fs.append("/protected.txt", b"New content");
1663 assert!(result.is_err());
1664 assert!(
1665 result
1666 .unwrap_err()
1667 .to_string()
1668 .contains("Permission denied")
1669 );
1670
1671 std::fs::set_permissions(&host_path, PermissionsExt::from_mode(0o644))?;
1673 }
1674 Ok(())
1675 }
1676 }
1677
1678 mod add {
1679 use super::*;
1680
1681 #[test]
1682 fn test_add_existing_file() -> Result<()> {
1683 let temp_dir = setup_test_env();
1684 let mut fs = DirFS::new(temp_dir.path())?;
1685
1686 let host_file = temp_dir.path().join("external.txt");
1688 std::fs::write(&host_file, b"Content from host")?;
1689
1690 fs.add("external.txt")?;
1692
1693 assert!(fs.exists("/external.txt"));
1695 let content = fs.read("/external.txt")?;
1696 assert_eq!(content, b"Content from host");
1697
1698 Ok(())
1699 }
1700
1701 #[test]
1702 fn test_add_existing_directory() -> Result<()> {
1703 let temp_dir = setup_test_env();
1704 let mut fs = DirFS::new(temp_dir.path())?;
1705
1706 let host_dir = temp_dir.path().join("external_dir");
1708 std::fs::create_dir_all(&host_dir)?;
1709
1710 std::fs::write(host_dir.join("file.txt"), b"Inside dir")?;
1711
1712 fs.add("external_dir")?;
1714
1715 assert!(fs.exists("/external_dir"));
1717 assert!(!fs.exists("/external_dir/file.txt"));
1718
1719 Ok(())
1720 }
1721
1722 #[test]
1723 fn test_add_nonexistent_path() -> Result<()> {
1724 let temp_dir = setup_test_env();
1725 let mut fs = DirFS::new(temp_dir.path())?;
1726
1727 let result = fs.add("/nonexistent.txt");
1728 assert!(result.is_err());
1729 assert!(
1730 result
1731 .unwrap_err()
1732 .to_string()
1733 .contains("No such file or directory")
1734 );
1735
1736 Ok(())
1737 }
1738
1739 #[test]
1740 fn test_add_relative_path() -> Result<()> {
1741 let temp_dir = setup_test_env();
1742 let mut fs = DirFS::new(temp_dir.path())?;
1743
1744 let subdir = temp_dir.path().join("sub");
1746 std::fs::create_dir_all(&subdir)?;
1747 std::fs::write(subdir.join("file.txt"), b"Relative content")?;
1748
1749 fs.add("/sub")?;
1750 fs.cd("/sub")?;
1751
1752 fs.add("file.txt")?;
1754
1755 assert!(fs.exists("/sub/file.txt"));
1756 let content = fs.read("/sub/file.txt")?;
1757 assert_eq!(content, b"Relative content");
1758
1759 Ok(())
1760 }
1761
1762 #[test]
1763 fn test_add_already_tracked_path() -> Result<()> {
1764 let temp_dir = setup_test_env();
1765 let mut fs = DirFS::new(temp_dir.path())?;
1766
1767 let host_file = temp_dir.path().join("duplicate.txt");
1769 std::fs::write(&host_file, b"Original")?;
1770 fs.add("duplicate.txt")?;
1771
1772 let result = fs.add("duplicate.txt");
1774 assert!(result.is_ok());
1776
1777 let content = fs.read("/duplicate.txt")?;
1779 assert_eq!(content, b"Original");
1780
1781 Ok(())
1782 }
1783
1784 #[test]
1785 fn test_add_unicode_path() -> Result<()> {
1786 let temp_dir = setup_test_env();
1787 let mut fs = DirFS::new(temp_dir.path())?;
1788
1789 let unicode_file = temp_dir.path().join("файл.txt");
1791 std::fs::write(&unicode_file, b"Unicode content")?;
1792
1793 fs.add("файл.txt")?;
1794
1795 assert!(fs.exists("/файл.txt"));
1796 let content = fs.read("/файл.txt")?;
1797 assert_eq!(content, b"Unicode content");
1798
1799 Ok(())
1800 }
1801
1802 #[test]
1803 fn test_add_and_auto_cleanup() -> Result<()> {
1804 let temp_dir = setup_test_env();
1805 let mut fs = DirFS::new(temp_dir.path())?;
1806
1807 let host_file = temp_dir.path().join("cleanup.txt");
1809 std::fs::write(&host_file, b"To be cleaned up")?;
1810 fs.add("cleanup.txt")?;
1811
1812 assert!(host_file.exists());
1813
1814 drop(fs);
1816
1817 assert!(!host_file.exists());
1820
1821 Ok(())
1822 }
1823 }
1824
1825 mod rm {
1826 use super::*;
1827
1828 #[test]
1829 fn test_rm_file_success() {
1830 let temp_dir = setup_test_env();
1831 let mut fs = DirFS::new(temp_dir.path()).unwrap();
1832
1833 fs.mkfile("/test.txt", Some(b"hello")).unwrap();
1835 assert!(fs.exists("/test.txt"));
1836 assert!(temp_dir.path().join("test.txt").exists());
1837
1838 fs.rm("/test.txt").unwrap();
1840
1841 assert!(!fs.exists("/test.txt"));
1843 assert!(!temp_dir.path().join("test.txt").exists());
1844 }
1845
1846 #[test]
1847 fn test_rm_directory_recursive() {
1848 let temp_dir = setup_test_env();
1849 let mut fs = DirFS::new(temp_dir.path()).unwrap();
1850
1851 fs.mkdir("/a/b/c").unwrap();
1853 fs.mkfile("/a/file1.txt", None).unwrap();
1854 fs.mkfile("/a/b/file2.txt", None).unwrap();
1855
1856 assert!(fs.exists("/a/b/c"));
1857 assert!(fs.exists("/a/file1.txt"));
1858 assert!(fs.exists("/a/b/file2.txt"));
1859
1860 fs.rm("/a").unwrap();
1862
1863 assert!(!fs.exists("/a"));
1865 assert!(!fs.exists("/a/b"));
1866 assert!(!fs.exists("/a/b/c"));
1867 assert!(!fs.exists("/a/file1.txt"));
1868 assert!(!fs.exists("/a/b/file2.txt"));
1869
1870 assert!(!temp_dir.path().join("a").exists());
1871 }
1872
1873 #[test]
1874 fn test_rm_nonexistent_path() {
1875 #[cfg(unix)]
1876 {
1877 let temp_dir = setup_test_env();
1878 let mut fs = DirFS::new(temp_dir.path()).unwrap();
1879
1880 let result = fs.rm("/not/found");
1881 assert!(result.is_err());
1882 assert_eq!(result.unwrap_err().to_string(), "/not/found does not exist");
1883 }
1884 }
1885
1886 #[test]
1887 fn test_rm_relative_path() {
1888 let temp_dir = setup_test_env();
1889 let mut fs = DirFS::new(temp_dir.path()).unwrap();
1890
1891 fs.mkdir("/parent").unwrap();
1892 fs.cd("/parent").unwrap();
1893 fs.mkfile("child.txt", None).unwrap();
1894
1895 assert!(fs.exists("/parent/child.txt"));
1896
1897 fs.rm("child.txt").unwrap();
1899
1900 assert!(!fs.exists("/parent/child.txt"));
1901 assert!(!temp_dir.path().join("parent/child.txt").exists());
1902 }
1903
1904 #[test]
1905 fn test_rm_empty_string_path() {
1906 let temp_dir = setup_test_env();
1907 let mut fs = DirFS::new(temp_dir.path()).unwrap();
1908
1909 let result = fs.rm("");
1910 assert!(result.is_err());
1911 assert_eq!(result.unwrap_err().to_string(), "invalid path: empty");
1912 }
1913
1914 #[test]
1915 fn test_rm_root_directory() {
1916 let temp_dir = setup_test_env();
1917 let mut fs = DirFS::new(temp_dir.path()).unwrap();
1918
1919 let result = fs.rm("/");
1921 assert!(result.is_err());
1922 assert_eq!(
1923 result.unwrap_err().to_string(),
1924 "invalid path: the root cannot be removed"
1925 );
1926
1927 assert!(fs.exists("/"));
1929 assert!(temp_dir.path().exists());
1930 }
1931
1932 #[test]
1933 fn test_rm_trailing_slash() {
1934 let temp_dir = setup_test_env();
1935 let mut fs = DirFS::new(temp_dir.path()).unwrap();
1936
1937 fs.mkdir("/dir/").unwrap(); fs.mkfile("/dir/file.txt", None).unwrap();
1939
1940 fs.rm("/dir/").unwrap();
1942
1943 assert!(!fs.exists("/dir"));
1944 assert!(!temp_dir.path().join("dir").exists());
1945 }
1946
1947 #[test]
1948 fn test_rm_unicode_path() {
1949 let temp_dir = setup_test_env();
1950 let mut fs = DirFS::new(temp_dir.path()).unwrap();
1951
1952 let unicode_path = "/папка/файл.txt";
1953 fs.mkdir("/папка").unwrap();
1954 fs.mkfile(unicode_path, None).unwrap();
1955
1956 assert!(fs.exists(unicode_path));
1957
1958 fs.rm(unicode_path).unwrap();
1959
1960 assert!(!fs.exists(unicode_path));
1961 assert!(!temp_dir.path().join("папка/файл.txt").exists());
1962 }
1963
1964 #[test]
1965 fn test_rm_permission_denied() {
1966 #[cfg(unix)]
1967 {
1968 use std::os::unix::fs::PermissionsExt;
1969
1970 let temp_dir = setup_test_env();
1971 let mut fs = DirFS::new(temp_dir.path()).unwrap();
1972 fs.mkdir("/protected").unwrap();
1973
1974 let protected = fs.root().join("protected");
1976 std::fs::set_permissions(&protected, PermissionsExt::from_mode(0o000)).unwrap();
1977
1978 let result = fs.rm("/protected");
1980 assert!(result.is_err());
1981 assert!(
1982 result
1983 .unwrap_err()
1984 .to_string()
1985 .contains("Permission denied")
1986 );
1987
1988 std::fs::set_permissions(&protected, PermissionsExt::from_mode(0o755)).unwrap();
1990 }
1991 }
1992
1993 #[test]
1994 fn test_rm_symlink_file() {
1995 #[cfg(unix)]
1996 {
1997 use std::os::unix::fs::symlink;
1998
1999 let temp_dir = setup_test_env();
2000 let mut fs = DirFS::new(temp_dir.path()).unwrap();
2001
2002 std::fs::write(temp_dir.path().join("real.txt"), "content").unwrap();
2004 symlink("real.txt", temp_dir.path().join("link.txt")).unwrap();
2005
2006 fs.mkfile("/link.txt", None).unwrap(); assert!(fs.exists("/link.txt"));
2008
2009 fs.rm("/link.txt").unwrap();
2011
2012 assert!(!fs.exists("/link.txt"));
2013 assert!(!temp_dir.path().join("link.txt").exists()); assert!(temp_dir.path().join("real.txt").exists()); }
2016 }
2017
2018 #[test]
2019 fn test_rm_after_cd() {
2020 let temp_dir = setup_test_env();
2021 let mut fs = DirFS::new(temp_dir.path()).unwrap();
2022
2023 fs.mkdir("/projects").unwrap();
2024 fs.cd("/projects").unwrap();
2025 fs.mkfile("notes.txt", None).unwrap();
2026
2027 assert!(fs.exists("/projects/notes.txt"));
2028
2029 fs.rm("notes.txt").unwrap();
2031
2032 assert!(!fs.exists("/projects/notes.txt"));
2033 assert!(!temp_dir.path().join("projects/notes.txt").exists());
2034 }
2035 }
2036
2037 mod cleanup {
2038 use super::*;
2039
2040 #[test]
2041 fn test_cleanup_ignores_is_auto_clean() {
2042 let temp_dir = setup_test_env();
2043 let root = temp_dir.path();
2044
2045 let mut fs = DirFS::new(root).unwrap();
2046 fs.is_auto_clean = false; fs.mkfile("/temp.txt", None).unwrap();
2048
2049 fs.cleanup(); assert!(!fs.exists("/temp.txt"));
2052 assert!(!root.join("temp.txt").exists());
2053 }
2054
2055 #[test]
2056 fn test_cleanup_preserves_root_and_parents() {
2057 let temp_dir = setup_test_env();
2058 let root = temp_dir.path().join("preserve_root");
2059
2060 let mut fs = DirFS::new(&root).unwrap();
2061 fs.mkdir("/subdir").unwrap();
2062 fs.mkfile("/subdir/file.txt", None).unwrap();
2063
2064 assert!(!fs.created_root_parents.is_empty());
2066
2067 fs.cleanup();
2068
2069 assert!(root.exists());
2071 for parent in &fs.created_root_parents {
2072 assert!(parent.exists());
2073 }
2074
2075 assert_eq!(fs.entries.len(), 1);
2077 assert!(fs.entries.contains(&PathBuf::from("/")));
2078 }
2079
2080 #[test]
2081 fn test_cleanup_empty_entries() {
2082 let temp_dir = setup_test_env();
2083 let root = temp_dir.path();
2084
2085 let mut fs = DirFS::new(root).unwrap();
2086 assert_eq!(fs.entries.len(), 1);
2088
2089 fs.cleanup();
2090
2091 assert_eq!(fs.entries.len(), 1); assert!(fs.entries.contains(&PathBuf::from("/")));
2093 assert!(root.exists()); }
2095 }
2096
2097 fn setup_test_env() -> TempDir {
2099 TempDir::new("dirfs_test").unwrap()
2100 }
2101}