Skip to main content

vfs_kit/vfs/
dir_fs.rs

1//! This module provides a virtual filesystem (VFS) implementation that maps to a real directory
2//! on the host system. It allows file and directory operations (create, read, remove, navigate)
3//! within a controlled root path while maintaining internal state consistency.
4//!
5//! ### Key Features:
6//! - **Isolated root**: All operations are confined to a designated root directory (self.root).
7//! - **Path normalization**: Automatically resolves . and .. components and removes trailing slashes.
8//! - **State tracking**: Maintains an internal set of valid paths (self.entries) to reflect VFS
9//!   structure.
10//! - **Auto‑cleanup**: Optionally removes created artifacts on Drop (when is_auto_clean = true).
11//! - **Cross‑platform**: Uses std::path::Path and PathBuf for portable path handling.
12
13use 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
21/// A virtual filesystem (VFS) implementation that maps to a real directory on the host system.
22///
23/// `DirFS` provides an isolated, path‑normalized view of a portion of the filesystem, rooted at a
24/// designated absolute path (`root`). It maintains an internal state of valid paths and supports
25/// standard operations:
26/// - Navigate via `cd()` (change working directory).
27/// - Create directories (`mkdir()`) and files (`mkfile()`).
28/// - Remove entries (`rm()`).
29/// - Check existence (`exists()`).
30/// - Read and write content (`read()` / `write()` / `append()`).
31///
32/// ### Usage notes:
33/// - `DirFS` does not follow symlinks; `rm()` removes the link, not the target.
34/// - Permissions are not automatically adjusted; ensure `root` is writable.
35/// - Not thread‑safe in current version (wrap in `Mutex` if needed).
36/// - Errors are returned via `anyhow::Result` with descriptive messages.
37///
38/// ### Example:
39/// ```
40/// use vfs_kit::{DirFS, FsBackend};
41///
42/// let tmp = std::env::temp_dir();
43/// let root = tmp.join("my_vfs");
44///
45/// let mut fs = DirFS::new(root).unwrap();
46/// fs.mkdir("/docs").unwrap();
47/// fs.mkfile("/docs/note.txt", Some(b"Hello")).unwrap();
48/// assert!(fs.exists("/docs/note.txt"));
49///
50/// fs.rm("/docs/note.txt").unwrap();
51/// ```
52pub struct DirFS {
53    root: PathBuf,                      // host-related absolute normalized path
54    cwd: PathBuf,                       // inner absolute normalized path
55    entries: HashSet<PathBuf>,          // inner absolute normalized paths
56    created_root_parents: Vec<PathBuf>, // host-related absolute normalized paths
57    is_auto_clean: bool,
58}
59
60impl DirFS {
61    /// Creates a new DirFs instance with the root directory at `path`.
62    /// Checks permissions to create and write into `path`.
63    /// * `path` is an absolute host path.
64    /// If `path` is not absolute, error returns.
65    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        // check permissions
86        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    /// Changes auto-clean flag.
100    /// If auto-clean flag is true all created in vfs artifacts
101    /// will be removed on drop.
102    pub fn set_auto_clean(&mut self, clean: bool) {
103        self.is_auto_clean = clean;
104    }
105
106    /// Adds an existing artifact (file or directory) to the VFS.
107    /// The artifact must exist and be located in the VFS root directory.
108    /// Once added, it will be managed by the VFS (e.g., deleted upon destruction).
109    /// * `path` is an inner VFS path.
110    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    /// Normalizes an arbitrary `path` by processing all occurrences
121    /// of '.' and '..' elements. Also, removes final `/`.
122    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        // remove final /
136        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    /// Make directories recursively.
152    /// * `path` is an absolute host path.
153    /// Returns vector of created directories.
154    fn mkdir_all<P: AsRef<Path>>(path: P) -> Result<Vec<PathBuf>> {
155        let host_path = path.as_ref().to_path_buf();
156
157        // Looking for the first existing parent
158        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        // Create from the closest existing parent to the target path
169        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    /// Returns root path related to the host file system.
213    fn root(&self) -> &Path {
214        self.root.as_path()
215    }
216
217    /// Returns current working directory related to the vfs root.
218    fn cwd(&self) -> &Path {
219        self.cwd.as_path()
220    }
221
222    /// Changes the current working directory.
223    /// * `path` can be in relative or absolute form, but in both cases it must exist.
224    /// An error is returned if the specified `path` does not exist.
225    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    /// Checks if a `path` exists in the vfs.
235    /// The `path` can be:
236    /// - absolute (starting with '/'),
237    /// - relative (relative to the vfs cwd),
238    /// - contain '..' or '.'.
239    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    /// Creates directory and all it parents (if needed).
245    /// * `path` - inner vfs path.
246    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        // Looking for the first existing parent
258        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        // Create from the closest existing parent to the target path
269        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    /// Creates new file in vfs.
288    /// * `file_path` must be inner vfs path. It must contain the name of the file,
289    /// optionally preceded by existing parent directory.
290    /// If the parent directory does not exist, an error is returned.
291    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    /// Reads the entire contents of a file into a byte vector.
308    /// * `path` is the inner VFS path.
309    ///
310    /// # Returns
311    /// * `Ok(Vec<u8>)` - File content as a byte vector if successful.
312    /// * `Err(anyhow::Error)` - If any of the following occurs:
313    ///   - File does not exist in VFS (`file does not exist: ...`)
314    ///   - Path points to a directory (`... is a directory`)
315    ///   - Permission issues when accessing the host file
316    ///   - I/O errors during reading
317    ///
318    /// # Notes
319    /// - Does **not** follow symbolic links on the host filesystem (reads the link itself).
320    /// - Returns an empty vector for empty files.
321    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    /// Writes bytes to an existing file, replacing its entire contents.
338    /// * `path` - Path to the file.
339    /// * `content` - Byte slice (`&[u8]`) to write to the file.
340    ///
341    /// # Returns
342    /// * `Ok(())` - If the write operation succeeded.
343    /// * `Err(anyhow::Error)` - If any of the following occurs:
344    ///   - File does not exist in VFS (`file does not exist: ...`)
345    ///   - Path points to a directory (`... is a directory`)
346    ///   - Permission issues when accessing the host file
347    ///   - I/O errors during writing (e.g., disk full, invalid path)
348    ///
349    /// # Behavior
350    /// - **Overwrites completely**: The entire existing content is replaced.
351    /// - **No file creation**: File must exist (use `mkfile()` first).
352    /// - **Atomic operation**: Uses `std::fs::write()` which replaces the file in one step.
353    /// - **Permissions**: The file retains its original permissions (no chmod is performed).
354    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    /// Appends bytes to the end of an existing file, preserving its old contents.
371    ///
372    /// # Arguments
373    /// * `path` - Path to the existing file.
374    /// * `content` - Byte slice (`&[u8]`) to append to the file.
375    ///
376    /// # Returns
377    /// * `Ok(())` - If the append operation succeeded.
378    /// * `Err(anyhow::Error)` - If any of the following occurs:
379    ///   - File does not exist in VFS (`file does not exist: ...`)
380    ///   - Path points to a directory (`... is a directory`)
381    ///   - Permission issues when accessing the host file
382    ///   - I/O errors during writing (e.g., disk full, invalid path)
383    ///
384    /// # Behavior
385    /// - **Appends only**: Existing content is preserved; new bytes are added at the end.
386    /// - **No parent creation**: Parent directories must exist (use `mkdir()` first if needed).
387    /// - **File creation**: Does NOT create the file if it doesn't exist (returns error).
388    /// - **Permissions**: The file retains its original permissions.
389    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        // Open file in append mode and write content
401        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    /// Removes a file or directory at the specified path.
410    ///
411    /// - `path`: can be absolute (starting with '/') or relative to the current working
412    /// directory (cwd).
413    /// - If the path is a directory, all its contents are removed recursively.
414    ///
415    /// Returns:
416    /// - `Ok(())` on successful removal.
417    /// - `Err(_)` if:
418    ///   - the path does not exist in the VFS;
419    ///   - there are insufficient permissions;
420    ///   - a filesystem error occurs.
421    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); // Convert to VFS-internal normalized path
430        let host_path = self.to_host(&inner_path); // Map to real filesystem path
431
432        // Check if the path exists in the virtual filesystem
433        if !self.exists(&inner_path) {
434            return Err(anyhow!("{} does not exist", inner_path.display()));
435        }
436
437        // Remove from the real filesystem
438        Self::rm_host_artifact(host_path)?;
439
440        // Update internal state: collect all entries that start with `inner_path`
441        let removed: Vec<PathBuf> = self
442            .entries
443            .iter()
444            .filter(|p| p.starts_with(&inner_path)) // Match prefix (includes subpaths)
445            .cloned()
446            .collect();
447
448        // Remove all matched entries from the set
449        for p in removed {
450            self.entries.remove(&p);
451        }
452
453        Ok(())
454    }
455
456    /// Removes all artifacts (dirs and files) in vfs, but preserve its root.
457    fn cleanup(&mut self) -> bool {
458        let mut is_ok = true;
459
460        // Collect all paths to delete (except the root "/")
461        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()); // parents must be created
538            assert!(nonexistent.exists()); // The catalog has been created
539        }
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); // a, a/b, a/b/c
550            assert!(nested.exists());
551        }
552
553        #[test]
554        fn test_new_permission_denied() {
555            // This test requires a specific environment (e.g. readonly FS)
556            #[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(); // No access
565
566                let result = DirFS::new(&protected_root);
567                assert!(result.is_err());
568
569                std::fs::set_permissions(&protected, PermissionsExt::from_mode(0o755)).unwrap(); // Grant access
570            }
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()); // Cannot create DirFs on file
592        }
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); // True by default
616        }
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(); // where cwd == "/"
680            assert_eq!(fs.cwd(), Path::new("/"));
681
682            fs.cd(".").unwrap(); // where cwd == "/"
683            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            // Проверяем, что каталоги реально созданы
788            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(); // It already exists
797
798            let target = temp_dir.path().join("a/b/c");
799            let created = DirFS::mkdir_all(&target).unwrap();
800
801            assert_eq!(created.len(), 2); // Только b и c
802            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()); // Nothing was created
815        }
816
817        #[test]
818        fn test_mkdir_all_root_path() {
819            // FS root (usually "/")
820            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            // The absolute path
842            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); // b, c, d
857        }
858
859        #[test]
860        fn test_mkdir_all_invalid_path() {
861            // Attempt to create in a non-existent location (without rights)
862            #[cfg(unix)]
863            {
864                let invalid_path = PathBuf::from("/nonexistent/parent/child");
865
866                // Expecting an error (e.g. PermissionDenied or NoSuchFile)
867                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(); // Create a file
877
878            let target = file_path.join("subdir"); // Trying to create inside the file
879
880            let result = DirFS::mkdir_all(&target);
881            assert!(result.is_err()); // Must be an error
882        }
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            // This test requires a specific environment (e.g. readonly FS).
909            // Skip it in general tests, but leave it for manual launch.
910            #[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(); // readonly
916
917                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            // Create DirFs, which will create new directories.
934            let fs = DirFS::new(&root).unwrap();
935            assert!(root.exists());
936
937            // Destroy fs (Drop should work)
938            drop(fs);
939
940            // Check that the root has been removed.
941            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(); // The parent already exists
951            let fs = DirFS::new(&child).unwrap();
952
953            assert!(parent.exists()); // The parent must remain.
954            assert!(child.exists());
955
956            drop(fs);
957
958            assert!(parent.exists()); // The parent is not deleted
959            assert!(!child.exists()); // The child has been removed
960        }
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; // Disable auto-cleaning
969
970            drop(fs);
971
972            assert!(root.exists()); // The catalog must remain
973        }
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(); // Already exists → created_root_parents is empty
982
983            drop(fs);
984
985            assert!(existing.exists()); // It should remain (we didn't create it)
986        }
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            // Все уровни должны быть удалены
999            assert!(!temp_dir.path().join("a").exists());
1000            assert!(!temp_dir.path().join("a/b").exists());
1001            assert!(!nested.exists());
1002        }
1003
1004        //-----------------------------
1005
1006        #[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()); // Корень удалён
1018            assert!(!root.join("subdir").exists()); // The subdirectory has also been deleted.
1019        }
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(); // File outside VFS
1060
1061            let fs = DirFS::new(&root).unwrap();
1062            drop(fs);
1063
1064            assert!(!root.exists());
1065            assert!(external.exists()); // The external file remains
1066        }
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            // entries contains only "/" (root)
1075
1076            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            // Trying to create the same file again
1145            let result = fs.mkfile("/existing.txt", None);
1146            assert!(result.is_ok()); // Should overwrite (File::create truncates the file)
1147            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(); // An empty array
1157
1158            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(); // Changes the current directory
1171
1172            fs.mkfile("relative.txt", None).unwrap(); // A relative path
1173
1174            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            // Attempt to create a file with an invalid name (depending on the file system)
1201            #[cfg(unix)]
1202            {
1203                let result = fs.mkfile("/invalid\0name.txt", None);
1204                assert!(result.is_err()); // NUL in filenames is prohibited in Unix.
1205            }
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(); // No access
1219
1220                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(); // Grant access
1224
1225                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            // Cannot create a file named "/" (it is a directory)
1243            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            // Create and write a file
1271            fs.mkfile("/test.txt", Some(b"Hello, VFS!"))?;
1272
1273            // Read it back
1274            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            // Note: error comes from std::fs::File::open (not a file), not our exists check
1307            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)?; // Create empty file
1318
1319            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            // Read using relative path from cwd
1336            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                // Create file and restrict permissions
1366                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                // Try to read (should fail due to permissions)
1371                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                // Clean up: restore permissions
1381                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            // Check file exists and has correct content
1412            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            // Create initial file
1508            fs.mkfile("/log.txt", Some(b"Initial content\n"))?;
1509
1510            // Append new content
1511            fs.append("/log.txt", b"Appended line 1\n")?;
1512            fs.append("/log.txt", b"Appended line 2\n")?;
1513
1514            // Verify full content
1515            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            // Create empty file
1530            fs.mkfile("/empty.txt", Some(&[]))?;
1531
1532            // Append content
1533            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            // Append empty slice
1581            fs.append("/test.txt", &[])?;
1582
1583            // Content should remain unchanged
1584            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"))?; // Relative path
1598
1599            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            // Simulate multiple appends
1637            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                // Create file and restrict permissions
1657                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                // Try to append (should fail)
1662                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                // Clean up: restore permissions
1672                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            // Create a file outside VFS that we'll add
1687            let host_file = temp_dir.path().join("external.txt");
1688            std::fs::write(&host_file, b"Content from host")?;
1689
1690            // Add it to VFS
1691            fs.add("external.txt")?;
1692
1693            // Verify it's now tracked by VFS
1694            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            // Create directory outside VFS
1707            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            // Add directory to VFS
1713            fs.add("external_dir")?;
1714
1715            // Verify directory and its contents are accessible
1716            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            // Create file in subdirectory
1745            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            // Change cwd and add using relative path
1753            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            // First add a file
1768            let host_file = temp_dir.path().join("duplicate.txt");
1769            std::fs::write(&host_file, b"Original")?;
1770            fs.add("duplicate.txt")?;
1771
1772            // Then try to add it again
1773            let result = fs.add("duplicate.txt");
1774            // Should succeed (no harm in re-adding)
1775            assert!(result.is_ok());
1776
1777            // Content should remain unchanged
1778            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            // Create file with Unicode name
1790            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            // Create and add a file
1808            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 - should auto-cleanup if configured
1815            drop(fs);
1816
1817            // Depending on auto_cleanup setting, file may or may not exist
1818            // This test assumes auto_cleanup=true
1819            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            // Create a file in VFS
1834            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            // Remove it
1839            fs.rm("/test.txt").unwrap();
1840
1841            // Verify: VFS and filesystem are updated
1842            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            // Create nested structure
1852            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            // Remove top-level directory
1861            fs.rm("/a").unwrap();
1862
1863            // Verify everything is gone
1864            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            // Remove using relative path
1898            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            // Attempt to remove root '/'
1920            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            // Root should still exist
1928            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(); // With trailing slash
1938            fs.mkfile("/dir/file.txt", None).unwrap();
1939
1940            // Remove with trailing slash
1941            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                // Create a directory and restrict permissions
1975                let protected = fs.root().join("protected");
1976                std::fs::set_permissions(&protected, PermissionsExt::from_mode(0o000)).unwrap();
1977
1978                // Try to remove via VFS (should fail)
1979                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                // Clean up: restore permissions
1989                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                // Create real file and symlink
2003                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(); // Add symlink to VFS
2007                assert!(fs.exists("/link.txt"));
2008
2009                // Remove symlink (not the target)
2010                fs.rm("/link.txt").unwrap();
2011
2012                assert!(!fs.exists("/link.txt"));
2013                assert!(!temp_dir.path().join("link.txt").exists()); // Symlink gone
2014                assert!(temp_dir.path().join("real.txt").exists()); // Target still there
2015            }
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            // Remove from cwd using relative path
2030            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; // Clearly disabled
2047            fs.mkfile("/temp.txt", None).unwrap();
2048
2049            fs.cleanup(); // Must be removed despite is_auto_clean=false
2050
2051            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            // created_root_parents is populated at initialization
2065            assert!(!fs.created_root_parents.is_empty());
2066
2067            fs.cleanup();
2068
2069            // Root and his parents remained
2070            assert!(root.exists());
2071            for parent in &fs.created_root_parents {
2072                assert!(parent.exists());
2073            }
2074
2075            // Only entries (except "/") were removed
2076            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            // entries contains only "/"
2087            assert_eq!(fs.entries.len(), 1);
2088
2089            fs.cleanup();
2090
2091            assert_eq!(fs.entries.len(), 1); // "/" remained
2092            assert!(fs.entries.contains(&PathBuf::from("/")));
2093            assert!(root.exists()); // The root is not removed
2094        }
2095    }
2096
2097    // Helper function: Creates a temporary directory for tests
2098    fn setup_test_env() -> TempDir {
2099        TempDir::new("dirfs_test").unwrap()
2100    }
2101}