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 {
22 root: PathBuf, cwd: PathBuf, entries: HashSet<PathBuf>, created_root_parents: Vec<PathBuf>, is_auto_clean: bool,
27}
28
29impl DirFS {
71 pub fn new<P: AsRef<Path>>(root: P) -> Result<Self> {
76 let root = root.as_ref();
77
78 if root.as_os_str().is_empty() {
79 return Err(anyhow!("invalid root path: empty"));
80 }
81 if root.is_relative() {
82 return Err(anyhow!("the root path must be absolute"));
83 }
84 if root.exists() && !root.is_dir() {
85 return Err(anyhow!("{:?} is not a directory", root));
86 }
87
88 let root = Self::normalize(root);
89
90 let mut created_root_parents = Vec::new();
91 if !std::fs::exists(&root)? {
92 created_root_parents.extend(Self::mkdir_all(&root)?);
93 }
94
95 if !Self::check_permissions(&root) {
97 return Err(anyhow!("Access denied: {:?}", root));
98 }
99
100 Ok(Self {
101 root,
102 cwd: PathBuf::from("/"),
103 entries: HashSet::from([PathBuf::from("/")]),
104 created_root_parents,
105 is_auto_clean: true,
106 })
107 }
108
109 pub fn set_auto_clean(&mut self, clean: bool) {
113 self.is_auto_clean = clean;
114 }
115
116 fn normalize<P: AsRef<Path>>(path: P) -> PathBuf {
119 let mut result = PathBuf::new();
120 for component in path.as_ref().components() {
121 match component {
122 Component::CurDir => {}
123 Component::ParentDir => {
124 result.pop();
125 }
126 _ => {
127 result.push(component);
128 }
129 }
130 }
131 if result != PathBuf::from("/") && result.ends_with("/") {
133 result.pop();
134 }
135 result
136 }
137
138 fn to_host<P: AsRef<Path>>(&self, path: P) -> PathBuf {
139 let inner = self.to_inner(path);
140 self.root.join(inner.strip_prefix("/").unwrap())
141 }
142
143 fn to_inner<P: AsRef<Path>>(&self, path: P) -> PathBuf {
144 Self::normalize(self.cwd.join(path))
145 }
146
147 fn mkdir_all<P: AsRef<Path>>(path: P) -> Result<Vec<PathBuf>> {
151 let host_path = path.as_ref().to_path_buf();
152
153 let mut existed_part = host_path.clone();
155 while let Some(parent) = existed_part.parent() {
156 let parent_buf = parent.to_path_buf();
157 if std::fs::exists(parent)? {
158 existed_part = parent_buf;
159 break;
160 }
161 existed_part = parent_buf;
162 }
163
164 let need_to_create: Vec<_> = host_path
166 .strip_prefix(&existed_part)?
167 .components()
168 .collect();
169
170 let mut created = Vec::new();
171
172 let mut built = PathBuf::from(&existed_part);
173 for component in need_to_create {
174 built.push(component);
175 if !std::fs::exists(&built)? {
176 std::fs::create_dir(&built)?;
177 created.push(built.clone());
178 }
179 }
180
181 Ok(created)
182 }
183
184 fn rm_host_artifact<P: AsRef<Path>>(host_path: P) -> Result<()> {
185 let host_path = host_path.as_ref();
186 if host_path.is_dir() {
187 std::fs::remove_dir_all(host_path)?
188 } else {
189 std::fs::remove_file(host_path)?
190 }
191 Ok(())
192 }
193
194 fn check_permissions<P: AsRef<Path>>(path: P) -> bool {
195 let path = path.as_ref();
196 let filename = path.join(".access");
197 if let Err(_) = std::fs::write(&filename, b"check") {
198 return false;
199 }
200 if let Err(_) = std::fs::remove_file(filename) {
201 return false;
202 }
203 true
204 }
205}
206
207impl FsBackend for DirFS {
208 fn root(&self) -> &Path {
210 self.root.as_path()
211 }
212
213 fn cwd(&self) -> &Path {
215 self.cwd.as_path()
216 }
217
218 fn cd<P: AsRef<Path>>(&mut self, path: P) -> Result<()> {
222 let target = self.to_inner(path);
223 if !self.exists(&target) {
224 return Err(anyhow!("{} does not exist", target.display()));
225 }
226 self.cwd = target;
227 Ok(())
228 }
229
230 fn exists<P: AsRef<Path>>(&self, path: P) -> bool {
236 let inner_path = self.to_inner(path);
237 self.entries.contains(&inner_path)
238 }
239
240 fn mkdir<P: AsRef<Path>>(&mut self, path: P) -> Result<()> {
243 if path.as_ref().as_os_str().is_empty() {
244 return Err(anyhow!("invalid path: empty"));
245 }
246
247 let inner_path = self.to_inner(path);
248
249 if self.exists(&inner_path) {
250 return Err(anyhow!("path already exists: {}", inner_path.display()));
251 }
252
253 let mut existed_parent = inner_path.clone();
255 while let Some(parent) = existed_parent.parent() {
256 let parent_buf = parent.to_path_buf();
257 if self.entries.contains(parent) {
258 existed_parent = parent_buf;
259 break;
260 }
261 existed_parent = parent_buf;
262 }
263
264 let need_to_create: Vec<_> = inner_path
266 .strip_prefix(&existed_parent)?
267 .components()
268 .collect();
269
270 let mut built = PathBuf::from(&existed_parent);
271 for component in need_to_create {
272 built.push(component);
273 if !self.entries.contains(&built) {
274 let host = self.to_host(&built);
275 std::fs::create_dir(&host)?;
276 self.entries.insert(built.clone());
277 }
278 }
279
280 Ok(())
281 }
282
283 fn mkfile<P: AsRef<Path>>(&mut self, file_path: P, content: Option<&[u8]>) -> Result<()> {
288 let file_path = self.to_inner(file_path);
289 if let Some(parent) = file_path.parent() {
290 if let Err(e) = std::fs::exists(parent) {
291 return Err(anyhow!("{:?}: {}", parent, e));
292 }
293 }
294 let host = self.to_host(&file_path);
295 let mut fd = std::fs::File::create(host)?;
296 self.entries.insert(file_path);
297 if let Some(content) = content {
298 fd.write_all(content)?;
299 }
300 Ok(())
301 }
302
303 fn read<P: AsRef<Path>>(&self, path: P) -> Result<Vec<u8>> {
318 let inner = self.to_inner(&path);
319 if !self.exists(&inner) {
320 return Err(anyhow!("file does not exist: {}", path.as_ref().display()));
321 }
322 let host = self.to_host(&inner);
323 if host.is_dir() {
324 return Err(anyhow!("{} is a directory", host.display()));
325 }
326
327 let mut content = Vec::new();
328 std::fs::File::open(&host)?.read_to_end(&mut content)?;
329
330 Ok(content)
331 }
332
333 fn write<P: AsRef<Path>>(&self, path: P, content: &[u8]) -> Result<()> {
351 let inner = self.to_inner(&path);
352 let host = self.to_host(&inner);
353
354 if !self.exists(&inner) {
355 return Err(anyhow!("file does not exist: {}", path.as_ref().display()));
356 }
357 if host.is_dir() {
358 return Err(anyhow!("{} is a directory", host.display()));
359 }
360
361 std::fs::write(&host, content)?;
362
363 Ok(())
364 }
365
366 fn append<P: AsRef<Path>>(&self, path: P, content: &[u8]) -> Result<()> {
386 let inner = self.to_inner(&path);
387 let host = self.to_host(&inner);
388
389 if !self.exists(&inner) {
390 return Err(anyhow!("file does not exist: {}", path.as_ref().display()));
391 }
392 if host.is_dir() {
393 return Err(anyhow!("{} is a directory", host.display()));
394 }
395
396 use std::fs::OpenOptions;
398 let mut file = OpenOptions::new()
399 .write(true)
400 .append(true)
401 .open(&host)?;
402
403 file.write_all(content)?;
404
405 Ok(())
406 }
407
408 fn rm<P: AsRef<Path>>(&mut self, path: P) -> Result<()> {
421 if path.as_ref().as_os_str().is_empty() {
422 return Err(anyhow!("invalid path: empty"));
423 }
424 if path.as_ref().as_os_str() == "/" {
425 return Err(anyhow!("invalid path: the root cannot be removed"));
426 }
427
428 let inner_path = self.to_inner(path); let host_path = self.to_host(&inner_path); if !self.exists(&inner_path) {
433 return Err(anyhow!("{} does not exist", inner_path.display()));
434 }
435
436 Self::rm_host_artifact(host_path)?;
438
439 let removed: Vec<PathBuf> = self
441 .entries
442 .iter()
443 .filter(|p| p.starts_with(&inner_path)) .cloned()
445 .collect();
446
447 for p in removed {
449 self.entries.remove(&p);
450 }
451
452 Ok(())
453 }
454
455 fn cleanup(&mut self) -> bool {
457 let mut is_ok = true;
458
459 let mut sorted_paths_to_remove: BTreeSet<PathBuf> = BTreeSet::new();
461 for entry in &self.entries {
462 if entry != &PathBuf::from("/") {
463 sorted_paths_to_remove.insert(entry.clone());
464 }
465 }
466
467 for entry in sorted_paths_to_remove.iter().rev() {
468 let host = self.to_host(entry);
469 let result = Self::rm_host_artifact(&host);
470 if result.is_ok() {
471 self.entries.remove(entry);
472 } else {
473 is_ok = false;
474 eprintln!("Unable to remove: {}", host.display());
475 }
476 }
477
478 is_ok
479 }
480}
481
482impl Drop for DirFS {
483 fn drop(&mut self) {
484 if !self.is_auto_clean {
485 return;
486 }
487
488 if self.cleanup() {
489 self.entries.clear();
490 }
491
492 let errors: Vec<_> = self
493 .created_root_parents
494 .iter()
495 .rev()
496 .filter_map(|p| Self::rm_host_artifact(p).err())
497 .collect();
498 if !errors.is_empty() {
499 eprintln!("Failed to remove parents: {:?}", errors);
500 }
501
502 self.created_root_parents.clear();
503 }
504}
505
506#[cfg(test)]
507mod tests {
508 use super::*;
509 use tempdir::TempDir;
510
511 mod creations {
512 use super::*;
513
514 #[test]
515 fn test_new_absolute_path_existing() {
516 let temp_dir = setup_test_env();
517 let root = temp_dir.path().to_path_buf();
518
519 let fs = DirFS::new(&root).unwrap();
520
521 assert_eq!(fs.root, root);
522 assert_eq!(fs.cwd, PathBuf::from("/"));
523 assert!(fs.entries.contains(&PathBuf::from("/")));
524 assert!(fs.created_root_parents.is_empty());
525 assert!(fs.is_auto_clean);
526 }
527
528 #[test]
529 fn test_new_nonexistent_path_created() {
530 let temp_dir = setup_test_env();
531 let nonexistent = temp_dir.path().join("new_root");
532
533 let fs = DirFS::new(&nonexistent).unwrap();
534
535 assert_eq!(fs.root, nonexistent);
536 assert!(!fs.created_root_parents.is_empty()); assert!(nonexistent.exists()); }
539
540 #[test]
541 fn test_new_nested_nonexistent_path() {
542 let temp_dir = setup_test_env();
543 let nested = temp_dir.path().join("a/b/c");
544
545 let fs = DirFS::new(&nested).unwrap();
546
547 assert_eq!(fs.root, nested);
548 assert_eq!(fs.created_root_parents.len(), 3); assert!(nested.exists());
550 }
551
552 #[test]
553 fn test_new_permission_denied() {
554 #[cfg(unix)]
556 {
557 use std::os::unix::fs::PermissionsExt;
558
559 let temp_dir = setup_test_env();
560 let protected = temp_dir.path().join("protected");
561 let protected_root = protected.join("root");
562 std::fs::create_dir_all(&protected_root).unwrap();
563 std::fs::set_permissions(&protected, PermissionsExt::from_mode(0o000)).unwrap(); let result = DirFS::new(&protected_root);
566 assert!(result.is_err());
567
568 std::fs::set_permissions(&protected, PermissionsExt::from_mode(0o755)).unwrap(); }
570 }
571
572 #[test]
573 fn test_new_normalize_path() {
574 let temp_dir = setup_test_env();
575 let messy_path = temp_dir.path().join("././subdir/../subdir");
576
577 let fs = DirFS::new(&messy_path).unwrap();
578 let canonical = DirFS::normalize(temp_dir.path().join("subdir"));
579
580 assert_eq!(fs.root, canonical);
581 }
582
583 #[test]
584 fn test_new_root_is_file() {
585 let temp_dir = setup_test_env();
586 let file_path = temp_dir.path().join("file.txt");
587 std::fs::write(&file_path, "content").unwrap();
588
589 let result = DirFS::new(&file_path);
590 assert!(result.is_err()); }
592
593 #[test]
594 fn test_new_empty_path() {
595 let result = DirFS::new("");
596 assert!(result.is_err());
597 }
598
599 #[test]
600 fn test_new_special_characters() {
601 let temp_dir = setup_test_env();
602 let special = temp_dir.path().join("папка с пробелами и юникод!");
603
604 let fs = DirFS::new(&special).unwrap();
605
606 assert_eq!(fs.root, special);
607 assert!(special.exists());
608 }
609
610 #[test]
611 fn test_new_is_auto_clean_default() {
612 let temp_dir = setup_test_env();
613 let fs = DirFS::new(temp_dir.path()).unwrap();
614 assert!(fs.is_auto_clean); }
616
617 #[test]
618 fn test_root_returns_correct_path() {
619 let temp_dir = setup_test_env();
620
621 let vfs_root = temp_dir.path().join("vfs-root");
622 let fs = DirFS::new(&vfs_root).unwrap();
623 assert_eq!(fs.root(), vfs_root);
624 }
625
626 #[test]
627 fn test_cwd_defaults_to_root() {
628 let temp_dir = setup_test_env();
629 let fs = DirFS::new(temp_dir).unwrap();
630 assert_eq!(fs.cwd(), Path::new("/"));
631 }
632 }
633
634 mod normalize {
635 use super::*;
636
637 #[test]
638 fn test_normalize_path() {
639 assert_eq!(DirFS::normalize("/a/b/c/"), PathBuf::from("/a/b/c"));
640 assert_eq!(DirFS::normalize("/a/b/./c"), PathBuf::from("/a/b/c"));
641 assert_eq!(DirFS::normalize("/a/b/../c"), PathBuf::from("/a/c"));
642 assert_eq!(DirFS::normalize("/"), PathBuf::from("/"));
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("../a"), PathBuf::from("a"));
647 assert_eq!(DirFS::normalize("./a"), PathBuf::from("a"));
648 }
649 }
650
651 mod cd {
652 use super::*;
653
654 #[test]
655 fn test_cd_to_absolute_path() {
656 let temp_dir = setup_test_env();
657 let mut fs = DirFS::new(&temp_dir).unwrap();
658 fs.mkdir("/projects").unwrap();
659 fs.cd("/projects").unwrap();
660 assert_eq!(fs.cwd(), Path::new("/projects"));
661 }
662
663 #[test]
664 fn test_cd_with_relative_path() {
665 let temp_dir = setup_test_env();
666 let mut fs = DirFS::new(&temp_dir).unwrap();
667 fs.mkdir("/home/user").unwrap();
668 fs.cwd = PathBuf::from("/home");
669 fs.cd("user").unwrap();
670 assert_eq!(fs.cwd(), Path::new("/home/user"));
671 }
672
673 #[test]
674 fn test_cd_extreme_cases() {
675 let temp_dir = setup_test_env();
676 let mut fs = DirFS::new(&temp_dir).unwrap();
677
678 fs.cd("..").unwrap(); assert_eq!(fs.cwd(), Path::new("/"));
680
681 fs.cd(".").unwrap(); assert_eq!(fs.cwd(), Path::new("/"));
683
684 fs.cwd = PathBuf::from("/home");
685 assert_eq!(fs.cwd(), Path::new("/home"));
686 fs.mkdir("/other").unwrap();
687 fs.cd("../other").unwrap();
688 assert_eq!(fs.cwd(), Path::new("/other"));
689
690 fs.cwd = PathBuf::from("/home");
691 assert_eq!(fs.cwd(), Path::new("/home"));
692 fs.mkdir("/home/other").unwrap();
693 fs.cd("./other").unwrap();
694 assert_eq!(fs.cwd(), Path::new("/home/other"));
695 }
696 }
697
698 mod mkdir {
699 use super::*;
700
701 #[test]
702 fn test_mkdir_create_single_dir() {
703 let temp_dir = setup_test_env();
704 let mut fs = DirFS::new(&temp_dir).unwrap();
705 fs.mkdir("/projects").unwrap();
706 assert!(fs.exists("/projects"));
707 }
708
709 #[test]
710 fn test_mkdir_relative_path() {
711 let temp_dir = setup_test_env();
712 let mut fs = DirFS::new(&temp_dir).unwrap();
713 fs.mkdir("home").unwrap();
714 fs.cd("/home").unwrap();
715 fs.mkdir("user").unwrap();
716 assert!(fs.exists("/home/user"));
717 }
718
719 #[test]
720 fn test_mkdir_nested_path() {
721 let temp_dir = setup_test_env();
722 let mut fs = DirFS::new(&temp_dir).unwrap();
723 fs.mkdir("/a/b/c").unwrap();
724 assert!(fs.exists("/a"));
725 assert!(fs.exists("/a/b"));
726 assert!(fs.exists("/a/b/c"));
727 }
728
729 #[test]
730 fn test_mkdir_already_exists() {
731 let temp_dir = setup_test_env();
732 let mut fs = DirFS::new(&temp_dir).unwrap();
733 fs.mkdir("/data").unwrap();
734 let result = fs.mkdir("/data");
735 assert!(result.is_err());
736 }
737
738 #[test]
739 fn test_mkdir_invalid_path() {
740 let temp_dir = setup_test_env();
741 let mut fs = DirFS::new(&temp_dir).unwrap();
742 let result = fs.mkdir("");
743 assert!(result.is_err());
744 }
745 }
746
747 mod exists {
748 use super::*;
749
750 #[test]
751 fn test_exists_root() {
752 let temp_dir = setup_test_env();
753 let fs = DirFS::new(&temp_dir).unwrap();
754 assert!(fs.exists("/"));
755 }
756
757 #[test]
758 fn test_exists_cwd() {
759 let temp_dir = setup_test_env();
760 let mut fs = DirFS::new(&temp_dir).unwrap();
761 fs.mkdir("/projects").unwrap();
762 fs.cd("/projects").unwrap();
763 assert!(fs.exists("."));
764 assert!(fs.exists("./"));
765 assert!(fs.exists("/projects"));
766 }
767 }
768
769 mod mkdir_all {
770 use super::*;
771 use std::fs;
772 use std::path::PathBuf;
773
774 #[test]
775 fn test_mkdir_all_simple_creation() {
776 let temp_dir = setup_test_env();
777 let target = temp_dir.path().join("a/b/c");
778
779 let created = DirFS::mkdir_all(&target).unwrap();
780
781 assert_eq!(created.len(), 3);
782 assert!(created.contains(&temp_dir.path().join("a")));
783 assert!(created.contains(&temp_dir.path().join("a/b")));
784 assert!(created.contains(&temp_dir.path().join("a/b/c")));
785
786 assert!(temp_dir.path().join("a").is_dir());
788 assert!(temp_dir.path().join("a/b").is_dir());
789 assert!(temp_dir.path().join("a/b/c").is_dir());
790 }
791
792 #[test]
793 fn test_mkdir_all_existing_parent() {
794 let temp_dir = setup_test_env();
795 fs::create_dir_all(temp_dir.path().join("a")).unwrap(); let target = temp_dir.path().join("a/b/c");
798 let created = DirFS::mkdir_all(&target).unwrap();
799
800 assert_eq!(created.len(), 2); assert!(created.contains(&temp_dir.path().join("a/b")));
802 assert!(created.contains(&temp_dir.path().join("a/b/c")));
803 }
804
805 #[test]
806 fn test_mkdir_all_target_exists() {
807 let temp_dir = setup_test_env();
808 fs::create_dir_all(temp_dir.path().join("x/y")).unwrap();
809
810 let target = temp_dir.path().join("x/y");
811 let created = DirFS::mkdir_all(&target).unwrap();
812
813 assert!(created.is_empty()); }
815
816 #[test]
817 fn test_mkdir_all_root_path() {
818 let result = DirFS::mkdir_all("/");
820 assert!(result.is_ok());
821 assert!(result.unwrap().is_empty());
822 }
823
824 #[test]
825 fn test_mkdir_all_single_dir() {
826 let temp_dir = setup_test_env();
827 let target = temp_dir.path().join("single");
828
829 let created = DirFS::mkdir_all(&target).unwrap();
830
831 assert_eq!(created.len(), 1);
832 assert!(created.contains(&target));
833 assert!(target.is_dir());
834 }
835
836 #[test]
837 fn test_mkdir_all_absolute_vs_relative() {
838 let temp_dir = setup_test_env();
839
840 let abs_target = temp_dir.path().join("abs/a/b");
842 let abs_created = DirFS::mkdir_all(&abs_target).unwrap();
843
844 assert!(!abs_created.is_empty());
845 }
846
847 #[test]
848 fn test_mkdir_all_nested_existing() {
849 let temp_dir = setup_test_env();
850 fs::create_dir_all(temp_dir.path().join("deep/a")).unwrap();
851
852 let target = temp_dir.path().join("deep/a/b/c/d");
853 let created = DirFS::mkdir_all(&target).unwrap();
854
855 assert_eq!(created.len(), 3); }
857
858 #[test]
859 fn test_mkdir_all_invalid_path() {
860 #[cfg(unix)]
862 {
863 let invalid_path = PathBuf::from("/nonexistent/parent/child");
864
865 let result = DirFS::mkdir_all(&invalid_path);
867 assert!(result.is_err());
868 }
869 }
870
871 #[test]
872 fn test_mkdir_all_file_in_path() {
873 let temp_dir = setup_test_env();
874 let file_path = temp_dir.path().join("file.txt");
875 fs::write(&file_path, "content").unwrap(); let target = file_path.join("subdir"); let result = DirFS::mkdir_all(&target);
880 assert!(result.is_err()); }
882
883 #[test]
884 fn test_mkdir_all_trailing_slash() {
885 let temp_dir = setup_test_env();
886 let target = temp_dir.path().join("trailing/");
887
888 let created = DirFS::mkdir_all(&target).unwrap();
889 assert!(!created.is_empty());
890 assert!(temp_dir.path().join("trailing").is_dir());
891 }
892
893 #[test]
894 fn test_mkdir_all_unicode_paths() {
895 let temp_dir = setup_test_env();
896 let target = temp_dir.path().join("папка/файл");
897
898 let created = DirFS::mkdir_all(&target).unwrap();
899
900 assert_eq!(created.len(), 2);
901 assert!(temp_dir.path().join("папка").is_dir());
902 assert!(temp_dir.path().join("папка/файл").is_dir());
903 }
904
905 #[test]
906 fn test_mkdir_all_permissions_error() {
907 #[cfg(unix)]
910 {
911 use std::os::unix::fs::PermissionsExt;
912
913 let temp_dir = setup_test_env();
914 fs::set_permissions(&temp_dir, PermissionsExt::from_mode(0o444)).unwrap(); let target = temp_dir.path().join("protected/dir");
917 let result = DirFS::mkdir_all(&target);
918
919 assert!(result.is_err());
920 }
921 }
922 }
923
924 mod drop {
925 use super::*;
926
927 #[test]
928 fn test_drop_removes_created_directories() {
929 let temp_dir = setup_test_env();
930 let root = temp_dir.path().join("to_remove");
931
932 let fs = DirFS::new(&root).unwrap();
934 assert!(root.exists());
935
936 drop(fs);
938
939 assert!(!root.exists());
941 }
942
943 #[test]
944 fn test_drop_only_removes_created_parents() {
945 let temp_dir = setup_test_env();
946 let parent = temp_dir.path().join("parent");
947 let child = parent.join("child");
948
949 std::fs::create_dir_all(&parent).unwrap(); let fs = DirFS::new(&child).unwrap();
951
952 assert!(parent.exists()); assert!(child.exists());
954
955 drop(fs);
956
957 assert!(parent.exists()); assert!(!child.exists()); }
960
961 #[test]
962 fn test_drop_with_is_auto_clean_false() {
963 let temp_dir = setup_test_env();
964 let root = temp_dir.path().join("keep");
965
966 let mut fs = DirFS::new(&root).unwrap();
967 fs.is_auto_clean = false; drop(fs);
970
971 assert!(root.exists()); }
973
974 #[test]
975 fn test_drop_empty_created_root_parents() {
976 let temp_dir = setup_test_env();
977 let existing = temp_dir.path().join("existing");
978 std::fs::create_dir(&existing).unwrap();
979
980 let fs = DirFS::new(&existing).unwrap(); drop(fs);
983
984 assert!(existing.exists()); }
986
987 #[test]
988 fn test_drop_nested_directories_removed() {
989 let temp_dir = setup_test_env();
990 let nested = temp_dir.path().join("a/b/c");
991
992 let fs = DirFS::new(&nested).unwrap();
993 assert!(nested.exists());
994
995 drop(fs);
996
997 assert!(!temp_dir.path().join("a").exists());
999 assert!(!temp_dir.path().join("a/b").exists());
1000 assert!(!nested.exists());
1001 }
1002
1003 #[test]
1006 fn test_drop_removes_entries_created_by_mkdir() {
1007 let temp_dir = setup_test_env();
1008 let root = temp_dir.path().join("test_root");
1009
1010 let mut fs = DirFS::new(&root).unwrap();
1011 fs.mkdir("/subdir").unwrap();
1012 assert!(root.join("subdir").exists());
1013
1014 drop(fs);
1015
1016 assert!(!root.exists()); assert!(!root.join("subdir").exists()); }
1019
1020 #[test]
1021 fn test_drop_removes_entries_created_by_mkfile() {
1022 let temp_dir = setup_test_env();
1023 let root = temp_dir.path().join("test_root");
1024
1025 let mut fs = DirFS::new(&root).unwrap();
1026 fs.mkfile("/file.txt", None).unwrap();
1027 assert!(root.join("file.txt").exists());
1028
1029 drop(fs);
1030
1031 assert!(!root.exists());
1032 assert!(!root.join("file.txt").exists());
1033 }
1034
1035 #[test]
1036 fn test_drop_handles_nested_entries() {
1037 let temp_dir = setup_test_env();
1038 let root = temp_dir.path().join("test_root");
1039
1040 let mut fs = DirFS::new(&root).unwrap();
1041 fs.mkdir("/a/b/c").unwrap();
1042 fs.mkfile("/a/file.txt", None).unwrap();
1043
1044 assert!(root.join("a/b/c").exists());
1045 assert!(root.join("a/file.txt").exists());
1046
1047 drop(fs);
1048
1049 assert!(!root.exists());
1050 }
1051
1052 #[test]
1053 fn test_drop_ignores_non_entries() {
1054 let temp_dir = setup_test_env();
1055 let root = temp_dir.path().join("test_root");
1056 let external = temp_dir.path().join("external_file.txt");
1057
1058 std::fs::write(&external, "content").unwrap(); let fs = DirFS::new(&root).unwrap();
1061 drop(fs);
1062
1063 assert!(!root.exists());
1064 assert!(external.exists()); }
1066
1067 #[test]
1068 fn test_drop_with_empty_entries() {
1069 let temp_dir = setup_test_env();
1070 let root = temp_dir.path().join("empty_root");
1071
1072 let fs = DirFS::new(&root).unwrap();
1073 drop(fs);
1076
1077 assert!(!root.exists());
1078 }
1079 }
1080
1081 mod mkfile {
1082 use super::*;
1083
1084 #[test]
1085 fn test_mkfile_simple_creation() {
1086 let temp_dir = setup_test_env();
1087 let root = temp_dir.path();
1088
1089 let mut fs = DirFS::new(root).unwrap();
1090 fs.mkfile("/file.txt", None).unwrap();
1091
1092 assert!(fs.exists("/file.txt"));
1093 assert!(root.join("file.txt").exists());
1094 assert_eq!(fs.entries.contains(&PathBuf::from("/file.txt")), true);
1095 }
1096
1097 #[test]
1098 fn test_mkfile_with_content() {
1099 let temp_dir = setup_test_env();
1100 let root = temp_dir.path();
1101
1102 let mut fs = DirFS::new(root).unwrap();
1103 let content = b"Hello, VFS!";
1104 fs.mkfile("/data.bin", Some(content)).unwrap();
1105
1106 assert!(fs.exists("/data.bin"));
1107 let file_content = std::fs::read(root.join("data.bin")).unwrap();
1108 assert_eq!(&file_content, content);
1109 }
1110
1111 #[test]
1112 fn test_mkfile_in_subdirectory() {
1113 let temp_dir = setup_test_env();
1114 let root = temp_dir.path();
1115
1116 let mut fs = DirFS::new(root).unwrap();
1117 fs.mkdir("/subdir").unwrap();
1118 fs.mkfile("/subdir/file.txt", None).unwrap();
1119
1120 assert!(fs.exists("/subdir/file.txt"));
1121 assert!(root.join("subdir/file.txt").exists());
1122 }
1123
1124 #[test]
1125 fn test_mkfile_parent_does_not_exist() {
1126 let temp_dir = setup_test_env();
1127 let root = temp_dir.path();
1128
1129 let mut fs = DirFS::new(root).unwrap();
1130
1131 let result = fs.mkfile("/nonexistent/file.txt", None);
1132 assert!(result.is_err());
1133 }
1134
1135 #[test]
1136 fn test_mkfile_file_already_exists() {
1137 let temp_dir = setup_test_env();
1138 let root = temp_dir.path();
1139
1140 let mut fs = DirFS::new(root).unwrap();
1141 fs.mkfile("/existing.txt", None).unwrap();
1142
1143 let result = fs.mkfile("/existing.txt", None);
1145 assert!(result.is_ok()); assert!(fs.exists("/existing.txt"));
1147 }
1148
1149 #[test]
1150 fn test_mkfile_empty_content() {
1151 let temp_dir = setup_test_env();
1152 let root = temp_dir.path();
1153
1154 let mut fs = DirFS::new(root).unwrap();
1155 fs.mkfile("/empty.txt", Some(&[])).unwrap(); assert!(fs.exists("/empty.txt"));
1158 let file_size = std::fs::metadata(root.join("empty.txt")).unwrap().len();
1159 assert_eq!(file_size, 0);
1160 }
1161
1162 #[test]
1163 fn test_mkfile_relative_path() {
1164 let temp_dir = setup_test_env();
1165 let root = temp_dir.path();
1166
1167 let mut fs = DirFS::new(root).unwrap();
1168 fs.mkdir("/sub").unwrap();
1169 fs.cd("/sub").unwrap(); fs.mkfile("relative.txt", None).unwrap(); assert!(fs.exists("/sub/relative.txt"));
1174 assert!(root.join("sub/relative.txt").exists());
1175 }
1176
1177 #[test]
1178 fn test_mkfile_normalize_path() {
1179 let temp_dir = setup_test_env();
1180 let root = temp_dir.path();
1181
1182 let mut fs = DirFS::new(root).unwrap();
1183 fs.mkdir("/normalized").unwrap();
1184
1185 fs.mkfile("/./normalized/../normalized/file.txt", None)
1186 .unwrap();
1187
1188 assert!(fs.exists("/normalized/file.txt"));
1189 assert!(root.join("normalized/file.txt").exists());
1190 }
1191
1192 #[test]
1193 fn test_mkfile_invalid_path_components() {
1194 let temp_dir = setup_test_env();
1195 let root = temp_dir.path();
1196
1197 let mut fs = DirFS::new(root).unwrap();
1198
1199 #[cfg(unix)]
1201 {
1202 let result = fs.mkfile("/invalid\0name.txt", None);
1203 assert!(result.is_err()); }
1205 }
1206
1207 #[test]
1208 fn test_mkfile_permission_denied() {
1209 #[cfg(unix)]
1210 {
1211 use std::os::unix::fs::PermissionsExt;
1212
1213 let temp_dir = setup_test_env();
1214 let root = temp_dir.path();
1215 let protected = root.join("protected");
1216 std::fs::create_dir(&protected).unwrap();
1217 std::fs::set_permissions(&protected, PermissionsExt::from_mode(0o000)).unwrap(); let mut fs = DirFS::new(root).unwrap();
1220 let result = fs.mkfile("/protected/file.txt", None);
1221
1222 std::fs::set_permissions(&protected, PermissionsExt::from_mode(0o755)).unwrap(); assert!(result.is_err());
1225 assert!(
1226 result
1227 .unwrap_err()
1228 .to_string()
1229 .contains("Permission denied")
1230 );
1231 }
1232 }
1233
1234 #[test]
1235 fn test_mkfile_root_directory() {
1236 let temp_dir = setup_test_env();
1237 let root = temp_dir.path();
1238
1239 let mut fs = DirFS::new(root).unwrap();
1240
1241 let result = fs.mkfile("/", None);
1243 assert!(result.is_err());
1244 }
1245
1246 #[test]
1247 fn test_mkfile_unicode_filename() {
1248 let temp_dir = setup_test_env();
1249 let root = temp_dir.path();
1250
1251 let mut fs = DirFS::new(root).unwrap();
1252 fs.mkfile("/тест.txt", Some(b"Content")).unwrap();
1253
1254 assert!(fs.exists("/тест.txt"));
1255 assert!(root.join("тест.txt").exists());
1256 let content = std::fs::read_to_string(root.join("тест.txt")).unwrap();
1257 assert_eq!(content, "Content");
1258 }
1259 }
1260
1261 mod read {
1262 use super::*;
1263
1264 #[test]
1265 fn test_read_existing_file() -> Result<()> {
1266 let temp_dir = setup_test_env();
1267 let mut fs = DirFS::new(&temp_dir)?;
1268
1269 fs.mkfile("/test.txt", Some(b"Hello, VFS!"))?;
1271
1272 let content = fs.read("/test.txt")?;
1274 assert_eq!(content, b"Hello, VFS!");
1275
1276 Ok(())
1277 }
1278
1279 #[test]
1280 fn test_read_nonexistent_file() -> Result<()> {
1281 let temp_dir = setup_test_env();
1282 let fs = DirFS::new(temp_dir.path())?;
1283
1284 let result = fs.read("/not/found.txt");
1285 assert!(result.is_err());
1286 assert!(
1287 result
1288 .unwrap_err()
1289 .to_string()
1290 .contains("file does not exist: /not/found.txt")
1291 );
1292
1293 Ok(())
1294 }
1295
1296 #[test]
1297 fn test_read_directory_as_file() -> Result<()> {
1298 let temp_dir = setup_test_env();
1299 let mut fs = DirFS::new(temp_dir.path())?;
1300
1301 fs.mkdir("/empty_dir")?;
1302
1303 let result = fs.read("/empty_dir");
1304 assert!(result.is_err());
1305 assert!(result.unwrap_err().to_string().contains("is a directory"));
1307
1308 Ok(())
1309 }
1310
1311 #[test]
1312 fn test_read_empty_file() -> Result<()> {
1313 let temp_dir = setup_test_env();
1314 let mut fs = DirFS::new(temp_dir.path())?;
1315
1316 fs.mkfile("/empty.txt", None)?; let content = fs.read("/empty.txt")?;
1319 assert_eq!(content.len(), 0);
1320
1321 Ok(())
1322 }
1323
1324 #[test]
1325 fn test_read_relative_path() -> Result<()> {
1326 let temp_dir = setup_test_env();
1327 let mut fs = DirFS::new(temp_dir.path())?;
1328
1329 fs.cd("/")?;
1330 fs.mkdir("/parent")?;
1331 fs.cd("/parent")?;
1332 fs.mkfile("child.txt", Some(b"Content"))?;
1333
1334 let content = fs.read("child.txt")?;
1336 assert_eq!(content, b"Content");
1337
1338 Ok(())
1339 }
1340
1341 #[test]
1342 fn test_read_unicode_path() -> Result<()> {
1343 let temp_dir = setup_test_env();
1344 let mut fs = DirFS::new(temp_dir.path())?;
1345
1346 fs.mkdir("/папка")?;
1347 fs.mkfile("/папка/файл.txt", Some(b"Unicode content"))?;
1348
1349 let content = fs.read("/папка/файл.txt")?;
1350 assert_eq!(content, b"Unicode content");
1351
1352 Ok(())
1353 }
1354
1355 #[test]
1356 fn test_read_permission_denied() -> Result<()> {
1357 #[cfg(unix)]
1358 {
1359 use std::os::unix::fs::PermissionsExt;
1360
1361 let temp_dir = setup_test_env();
1362 let mut fs = DirFS::new(temp_dir.path())?;
1363
1364 fs.mkfile("/protected.txt", Some(b"Secret"))?;
1366 let host_path = temp_dir.path().join("protected.txt");
1367 std::fs::set_permissions(&host_path, PermissionsExt::from_mode(0o000))?;
1368
1369 let result = fs.read("/protected.txt");
1371 assert!(result.is_err());
1372 assert!(
1373 result
1374 .unwrap_err()
1375 .to_string()
1376 .contains("Permission denied")
1377 );
1378
1379 std::fs::set_permissions(&host_path, PermissionsExt::from_mode(0o644))?;
1381 }
1382 Ok(())
1383 }
1384
1385 #[test]
1386 fn test_read_root_file() -> Result<()> {
1387 let temp_dir = setup_test_env();
1388 let mut fs = DirFS::new(temp_dir.path())?;
1389
1390 fs.mkfile("/root_file.txt", Some(b"At root"))?;
1391 let content = fs.read("/root_file.txt")?;
1392 assert_eq!(content, b"At root");
1393
1394 Ok(())
1395 }
1396 }
1397
1398 mod write {
1399 use super::*;
1400
1401 #[test]
1402 fn test_write_new_file() -> Result<()> {
1403 let temp_dir = setup_test_env();
1404 let mut fs = DirFS::new(temp_dir.path())?;
1405
1406 fs.mkfile("/new.txt", None)?;
1407 let content = b"Hello, VFS!";
1408 fs.write("/new.txt", content)?;
1409
1410 assert!(fs.exists("/new.txt"));
1412 let read_back = fs.read("/new.txt")?;
1413 assert_eq!(read_back, content);
1414
1415 Ok(())
1416 }
1417
1418 #[test]
1419 fn test_write_existing_file_overwrite() -> Result<()> {
1420 let temp_dir = setup_test_env();
1421 let mut fs = DirFS::new(temp_dir.path())?;
1422
1423 fs.mkfile("/exist.txt", Some(b"Old content"))?;
1424
1425 let new_content = b"New content";
1426 fs.write("/exist.txt", new_content)?;
1427
1428 let read_back = fs.read("/exist.txt")?;
1429 assert_eq!(read_back, new_content);
1430
1431 Ok(())
1432 }
1433
1434 #[test]
1435 fn test_write_to_directory_path() -> Result<()> {
1436 let temp_dir = setup_test_env();
1437 let mut fs = DirFS::new(temp_dir.path())?;
1438
1439 fs.mkdir("/dir")?;
1440
1441 let result = fs.write("/dir", b"Content");
1442 assert!(result.is_err());
1443 assert!(result.unwrap_err().to_string().contains("is a directory"));
1444
1445 Ok(())
1446 }
1447
1448 #[test]
1449 fn test_write_to_nonexistent_file() -> Result<()> {
1450 let temp_dir = setup_test_env();
1451 let fs = DirFS::new(temp_dir.path())?;
1452
1453 let result = fs.write("/parent/child.txt", b"Content");
1454 assert!(result.is_err());
1455 assert!(
1456 result
1457 .unwrap_err()
1458 .to_string()
1459 .contains("file does not exist")
1460 );
1461
1462 Ok(())
1463 }
1464
1465 #[test]
1466 fn test_write_empty_content() -> Result<()> {
1467 let temp_dir = setup_test_env();
1468 let mut fs = DirFS::new(temp_dir.path())?;
1469
1470 fs.mkfile("/empty.txt", None)?;
1471 fs.write("/empty.txt", &[])?;
1472
1473 let read_back = fs.read("/empty.txt")?;
1474 assert!(read_back.is_empty());
1475
1476 Ok(())
1477 }
1478
1479 #[test]
1480 fn test_write_relative_path() -> Result<()> {
1481 let temp_dir = setup_test_env();
1482 let mut fs = DirFS::new(temp_dir.path())?;
1483
1484 fs.mkdir("/docs")?;
1485 fs.cd("docs")?;
1486
1487 fs.mkfile("file.txt", None)?;
1488 let content = b"Relative write";
1489 fs.write("file.txt", content)?;
1490
1491 let read_back = fs.read("/docs/file.txt")?;
1492 assert_eq!(read_back, content);
1493
1494 Ok(())
1495 }
1496 }
1497
1498 mod append {
1499 use super::*;
1500
1501 #[test]
1502 fn test_append_to_existing_file() -> Result<()> {
1503 let temp_dir = setup_test_env();
1504 let mut fs = DirFS::new(temp_dir.path())?;
1505
1506 fs.mkfile("/log.txt", Some(b"Initial content\n"))?;
1508
1509 fs.append("/log.txt", b"Appended line 1\n")?;
1511 fs.append("/log.txt", b"Appended line 2\n")?;
1512
1513 let content = fs.read("/log.txt")?;
1515 assert_eq!(
1516 content,
1517 b"Initial content\nAppended line 1\nAppended line 2\n"
1518 );
1519
1520 Ok(())
1521 }
1522
1523 #[test]
1524 fn test_append_to_empty_file() -> Result<()> {
1525 let temp_dir = setup_test_env();
1526 let mut fs = DirFS::new(temp_dir.path())?;
1527
1528 fs.mkfile("/empty.txt", Some(&[]))?;
1530
1531 fs.append("/empty.txt", b"First append\n")?;
1533 fs.append("/empty.txt", b"Second append\n")?;
1534
1535 let content = fs.read("/empty.txt")?;
1536 assert_eq!(content, b"First append\nSecond append\n");
1537
1538 Ok(())
1539 }
1540
1541 #[test]
1542 fn test_append_nonexistent_file() -> Result<()> {
1543 let temp_dir = setup_test_env();
1544 let fs = DirFS::new(temp_dir.path())?;
1545
1546 let result = fs.append("/not_found.txt", b"Content");
1547 assert!(result.is_err());
1548 assert!(result
1549 .unwrap_err()
1550 .to_string()
1551 .contains("file does not exist: /not_found.txt"));
1552
1553 Ok(())
1554 }
1555
1556 #[test]
1557 fn test_append_to_directory() -> Result<()> {
1558 let temp_dir = setup_test_env();
1559 let mut fs = DirFS::new(temp_dir.path())?;
1560
1561 fs.mkdir("/mydir")?;
1562
1563 let result = fs.append("/mydir", b"Content");
1564 assert!(result.is_err());
1565 assert!(result
1566 .unwrap_err()
1567 .to_string()
1568 .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!(result.unwrap_err().to_string().contains("Permission denied"));
1665
1666 std::fs::set_permissions(&host_path, PermissionsExt::from_mode(0o644))?;
1668 }
1669 Ok(())
1670 }
1671 }
1672
1673 mod rm {
1674 use super::*;
1675
1676 #[test]
1677 fn test_rm_file_success() {
1678 let temp_dir = setup_test_env();
1679 let mut fs = DirFS::new(temp_dir.path()).unwrap();
1680
1681 fs.mkfile("/test.txt", Some(b"hello")).unwrap();
1683 assert!(fs.exists("/test.txt"));
1684 assert!(temp_dir.path().join("test.txt").exists());
1685
1686 fs.rm("/test.txt").unwrap();
1688
1689 assert!(!fs.exists("/test.txt"));
1691 assert!(!temp_dir.path().join("test.txt").exists());
1692 }
1693
1694 #[test]
1695 fn test_rm_directory_recursive() {
1696 let temp_dir = setup_test_env();
1697 let mut fs = DirFS::new(temp_dir.path()).unwrap();
1698
1699 fs.mkdir("/a/b/c").unwrap();
1701 fs.mkfile("/a/file1.txt", None).unwrap();
1702 fs.mkfile("/a/b/file2.txt", None).unwrap();
1703
1704 assert!(fs.exists("/a/b/c"));
1705 assert!(fs.exists("/a/file1.txt"));
1706 assert!(fs.exists("/a/b/file2.txt"));
1707
1708 fs.rm("/a").unwrap();
1710
1711 assert!(!fs.exists("/a"));
1713 assert!(!fs.exists("/a/b"));
1714 assert!(!fs.exists("/a/b/c"));
1715 assert!(!fs.exists("/a/file1.txt"));
1716 assert!(!fs.exists("/a/b/file2.txt"));
1717
1718 assert!(!temp_dir.path().join("a").exists());
1719 }
1720
1721 #[test]
1722 fn test_rm_nonexistent_path() {
1723 #[cfg(unix)]
1724 {
1725 let temp_dir = setup_test_env();
1726 let mut fs = DirFS::new(temp_dir.path()).unwrap();
1727
1728 let result = fs.rm("/not/found");
1729 assert!(result.is_err());
1730 assert_eq!(result.unwrap_err().to_string(), "/not/found does not exist");
1731 }
1732 }
1733
1734 #[test]
1735 fn test_rm_relative_path() {
1736 let temp_dir = setup_test_env();
1737 let mut fs = DirFS::new(temp_dir.path()).unwrap();
1738
1739 fs.mkdir("/parent").unwrap();
1740 fs.cd("/parent").unwrap();
1741 fs.mkfile("child.txt", None).unwrap();
1742
1743 assert!(fs.exists("/parent/child.txt"));
1744
1745 fs.rm("child.txt").unwrap();
1747
1748 assert!(!fs.exists("/parent/child.txt"));
1749 assert!(!temp_dir.path().join("parent/child.txt").exists());
1750 }
1751
1752 #[test]
1753 fn test_rm_empty_string_path() {
1754 let temp_dir = setup_test_env();
1755 let mut fs = DirFS::new(temp_dir.path()).unwrap();
1756
1757 let result = fs.rm("");
1758 assert!(result.is_err());
1759 assert_eq!(result.unwrap_err().to_string(), "invalid path: empty");
1760 }
1761
1762 #[test]
1763 fn test_rm_root_directory() {
1764 let temp_dir = setup_test_env();
1765 let mut fs = DirFS::new(temp_dir.path()).unwrap();
1766
1767 let result = fs.rm("/");
1769 assert!(result.is_err());
1770 assert_eq!(
1771 result.unwrap_err().to_string(),
1772 "invalid path: the root cannot be removed"
1773 );
1774
1775 assert!(fs.exists("/"));
1777 assert!(temp_dir.path().exists());
1778 }
1779
1780 #[test]
1781 fn test_rm_trailing_slash() {
1782 let temp_dir = setup_test_env();
1783 let mut fs = DirFS::new(temp_dir.path()).unwrap();
1784
1785 fs.mkdir("/dir/").unwrap(); fs.mkfile("/dir/file.txt", None).unwrap();
1787
1788 fs.rm("/dir/").unwrap();
1790
1791 assert!(!fs.exists("/dir"));
1792 assert!(!temp_dir.path().join("dir").exists());
1793 }
1794
1795 #[test]
1796 fn test_rm_unicode_path() {
1797 let temp_dir = setup_test_env();
1798 let mut fs = DirFS::new(temp_dir.path()).unwrap();
1799
1800 let unicode_path = "/папка/файл.txt";
1801 fs.mkdir("/папка").unwrap();
1802 fs.mkfile(unicode_path, None).unwrap();
1803
1804 assert!(fs.exists(unicode_path));
1805
1806 fs.rm(unicode_path).unwrap();
1807
1808 assert!(!fs.exists(unicode_path));
1809 assert!(!temp_dir.path().join("папка/файл.txt").exists());
1810 }
1811
1812 #[test]
1813 fn test_rm_permission_denied() {
1814 #[cfg(unix)]
1815 {
1816 use std::os::unix::fs::PermissionsExt;
1817
1818 let temp_dir = setup_test_env();
1819 let mut fs = DirFS::new(temp_dir.path()).unwrap();
1820 fs.mkdir("/protected").unwrap();
1821
1822 let protected = fs.root().join("protected");
1824 std::fs::set_permissions(&protected, PermissionsExt::from_mode(0o000)).unwrap();
1825
1826 let result = fs.rm("/protected");
1828 assert!(result.is_err());
1829 assert!(
1830 result
1831 .unwrap_err()
1832 .to_string()
1833 .contains("Permission denied")
1834 );
1835
1836 std::fs::set_permissions(&protected, PermissionsExt::from_mode(0o755)).unwrap();
1838 }
1839 }
1840
1841 #[test]
1842 fn test_rm_symlink_file() {
1843 #[cfg(unix)]
1844 {
1845 use std::os::unix::fs::symlink;
1846
1847 let temp_dir = setup_test_env();
1848 let mut fs = DirFS::new(temp_dir.path()).unwrap();
1849
1850 std::fs::write(temp_dir.path().join("real.txt"), "content").unwrap();
1852 symlink("real.txt", temp_dir.path().join("link.txt")).unwrap();
1853
1854 fs.mkfile("/link.txt", None).unwrap(); assert!(fs.exists("/link.txt"));
1856
1857 fs.rm("/link.txt").unwrap();
1859
1860 assert!(!fs.exists("/link.txt"));
1861 assert!(!temp_dir.path().join("link.txt").exists()); assert!(temp_dir.path().join("real.txt").exists()); }
1864 }
1865
1866 #[test]
1867 fn test_rm_after_cd() {
1868 let temp_dir = setup_test_env();
1869 let mut fs = DirFS::new(temp_dir.path()).unwrap();
1870
1871 fs.mkdir("/projects").unwrap();
1872 fs.cd("/projects").unwrap();
1873 fs.mkfile("notes.txt", None).unwrap();
1874
1875 assert!(fs.exists("/projects/notes.txt"));
1876
1877 fs.rm("notes.txt").unwrap();
1879
1880 assert!(!fs.exists("/projects/notes.txt"));
1881 assert!(!temp_dir.path().join("projects/notes.txt").exists());
1882 }
1883 }
1884
1885 mod cleanup {
1886 use super::*;
1887
1888 #[test]
1889 fn test_cleanup_ignores_is_auto_clean() {
1890 let temp_dir = setup_test_env();
1891 let root = temp_dir.path();
1892
1893 let mut fs = DirFS::new(root).unwrap();
1894 fs.is_auto_clean = false; fs.mkfile("/temp.txt", None).unwrap();
1896
1897 fs.cleanup(); assert!(!fs.exists("/temp.txt"));
1900 assert!(!root.join("temp.txt").exists());
1901 }
1902
1903 #[test]
1904 fn test_cleanup_preserves_root_and_parents() {
1905 let temp_dir = setup_test_env();
1906 let root = temp_dir.path().join("preserve_root");
1907
1908 let mut fs = DirFS::new(&root).unwrap();
1909 fs.mkdir("/subdir").unwrap();
1910 fs.mkfile("/subdir/file.txt", None).unwrap();
1911
1912 assert!(!fs.created_root_parents.is_empty());
1914
1915 fs.cleanup();
1916
1917 assert!(root.exists());
1919 for parent in &fs.created_root_parents {
1920 assert!(parent.exists());
1921 }
1922
1923 assert_eq!(fs.entries.len(), 1);
1925 assert!(fs.entries.contains(&PathBuf::from("/")));
1926 }
1927
1928 #[test]
1929 fn test_cleanup_empty_entries() {
1930 let temp_dir = setup_test_env();
1931 let root = temp_dir.path();
1932
1933 let mut fs = DirFS::new(root).unwrap();
1934 assert_eq!(fs.entries.len(), 1);
1936
1937 fs.cleanup();
1938
1939 assert_eq!(fs.entries.len(), 1); assert!(fs.entries.contains(&PathBuf::from("/")));
1941 assert!(root.exists()); }
1943 }
1944
1945 fn setup_test_env() -> TempDir {
1947 TempDir::new("dirfs_test").unwrap()
1948 }
1949}