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::{BTreeMap, BTreeSet};
14use std::io::{Read, Write};
15use std::path::{Component, Path, PathBuf};
16
17use anyhow::anyhow;
18
19use crate::core::{FsBackend, Result};
20use crate::{DirEntry, DirEntryType};
21
22/// A virtual filesystem (VFS) implementation that maps to a real directory on the host system.
23///
24/// `DirFS` provides an isolated, path‑normalized view of a portion of the filesystem, rooted at a
25/// designated absolute path (`root`). It maintains an internal state of valid paths and supports
26/// standard operations:
27/// - Navigate via `cd()` (change working directory).
28/// - Create directories (`mkdir()`) and files (`mkfile()`).
29/// - Remove entries (`rm()`).
30/// - Check existence (`exists()`).
31/// - Read and write content (`read()` / `write()` / `append()`).
32///
33/// ### Usage notes:
34/// - `DirFS` does not follow symlinks; `rm()` removes the link, not the target.
35/// - Permissions are not automatically adjusted; ensure `root` is writable.
36/// - Not thread‑safe in current version (wrap in `Mutex` if needed).
37/// - Errors are returned via `anyhow::Result` with descriptive messages.
38///
39/// ### Example:
40/// ```
41/// use vfs_kit::{DirFS, FsBackend};
42///
43/// let tmp = std::env::temp_dir();
44/// let root = tmp.join("my_vfs");
45///
46/// let mut fs = DirFS::new(root).unwrap();
47/// fs.mkdir("/docs").unwrap();
48/// fs.mkfile("/docs/note.txt", Some(b"Hello")).unwrap();
49/// assert!(fs.exists("/docs/note.txt"));
50///
51/// fs.rm("/docs/note.txt").unwrap();
52/// ```
53pub struct DirFS {
54    root: PathBuf,                        // host-related absolute normalized path
55    cwd: PathBuf,                         // inner absolute normalized path
56    entries: BTreeMap<PathBuf, DirEntry>, // inner absolute normalized paths
57    created_root_parents: Vec<PathBuf>,   // host-related absolute normalized paths
58    is_auto_clean: bool,
59}
60
61impl DirFS {
62    /// Creates a new DirFs instance with the root directory at `path`.
63    /// Checks permissions to create and write into `path`.
64    /// * `path` is an absolute host path.
65    /// If `path` is not absolute, error returns.
66    pub fn new<P: AsRef<Path>>(root: P) -> Result<Self> {
67        let root = root.as_ref();
68
69        if root.as_os_str().is_empty() {
70            return Err(anyhow!("invalid root path: empty"));
71        }
72        if root.is_relative() {
73            return Err(anyhow!("the root path must be absolute"));
74        }
75        if root.exists() && !root.is_dir() {
76            return Err(anyhow!("{:?} is not a directory", root));
77        }
78
79        let root = Self::normalize(root);
80
81        let mut created_root_parents = Vec::new();
82        if !std::fs::exists(&root)? {
83            created_root_parents.extend(Self::mkdir_all(&root)?);
84        }
85
86        // check permissions
87        if !Self::check_permissions(&root) {
88            return Err(anyhow!("Access denied: {:?}", root));
89        }
90
91        let inner_root = PathBuf::from("/");
92        let mut entries = BTreeMap::new();
93        entries.insert(
94            inner_root.clone(),
95            DirEntry::new(&inner_root, DirEntryType::Directory),
96        );
97
98        Ok(Self {
99            root,
100            cwd: inner_root,
101            entries,
102            created_root_parents,
103            is_auto_clean: true,
104        })
105    }
106
107    /// Changes auto-clean flag.
108    /// If auto-clean flag is true all created in vfs artifacts
109    /// will be removed on drop.
110    pub fn set_auto_clean(&mut self, clean: bool) {
111        self.is_auto_clean = clean;
112    }
113
114    /// Adds an existing artifact (file or directory) to the VFS.
115    /// The artifact must exist and be located in the VFS root directory.
116    /// If artifact is directory - all its childs will be added recursively.
117    /// Once added, it will be managed by the VFS (e.g., deleted upon destruction).
118    /// * `path` is an inner VFS path.
119    pub fn add<P: AsRef<Path>>(&mut self, path: P) -> Result<()> {
120        let inner = self.to_inner(&path);
121        let host = self.to_host(&inner);
122        if !host.exists() {
123            return Err(anyhow!(
124                "No such file or directory: {}",
125                path.as_ref().display()
126            ));
127        }
128        self.add_recursive(&inner, &host)
129    }
130
131    /// Removes a file or directory from the VFS and recursively untracks all its contents.
132    ///
133    /// This method "forgets" the specified path — it is permanently removed from the VFS tracking.
134    /// If the path is a directory, all its children (files and subdirectories) are also untracked
135    /// recursively.
136    ///
137    /// # Arguments
138    ///
139    /// * `path` - The path to remove from the VFS. Can be a file or a directory.
140    ///
141    /// # Returns
142    ///
143    /// * `Ok(())` - If the path was successfully removed (or was not tracked in the first place).
144    /// * `Err(anyhow::Error)` - If:
145    ///   * The path is not tracked by the VFS.
146    ///   * The path is the root directory (`/`), which cannot be forgotten.
147    ///
148    /// # Behavior
149    ///
150    /// 1. **Existence check**: Returns an error if the resolved path is not currently tracked.
151    /// 2. **Root protection**: Blocks attempts to forget the root directory (`/`).
152    /// 3. **Removal**:
153    ///    * If the path is a file: removes only that file.
154    ///    * If the path is a directory: removes the directory and all its descendants (recursively).
155    ///
156    ///
157    /// # Examples
158    ///
159    /// ```
160    /// use vfs_kit::{DirFS, FsBackend};
161    ///
162    /// let temp_dir = tempdir::TempDir::new("vfs_example").unwrap();
163    /// let mut vfs = DirFS::new(temp_dir.path()).unwrap();
164    ///
165    /// vfs.mkdir("/docs/backup");
166    /// vfs.mkfile("/docs/readme.txt", None);
167    ///
168    /// // Forget the entire /docs directory (and all its contents)
169    /// vfs.forget("/docs").unwrap();
170    ///
171    /// assert!(!vfs.exists("/docs/readme.txt"));
172    /// assert!(!vfs.exists("/docs/backup"));
173    /// ```
174    ///
175    /// ```
176    /// use vfs_kit::{DirFS, FsBackend};
177    ///
178    /// let temp_dir = tempdir::TempDir::new("vfs_example").unwrap();
179    /// let mut vfs = DirFS::new(temp_dir.path()).unwrap();
180    ///
181    /// // Error: trying to forget a non-existent path
182    /// assert!(vfs.forget("/nonexistent").is_err());
183    ///
184    /// // Error: trying to forget the root
185    /// assert!(vfs.forget("/").is_err());
186    /// ```
187    ///
188    /// # Notes
189    ///
190    /// * The method does **not** interact with the real filesystem — it only affects the VFS's
191    ///   internal tracking.
192    /// * If the path does not exist in the VFS, the method returns an error
193    ///   (unlike `remove` in some systems that may silently succeed).
194    pub fn forget<P: AsRef<Path>>(&mut self, path: P) -> Result<()> {
195        let inner = self.to_inner(&path);
196        if !self.exists(&inner) {
197            return Err(anyhow!("{:?} path is not tracked by VFS", path.as_ref()));
198        }
199        if Self::is_inner_root(&inner) {
200            return Err(anyhow!("cannot forget root directory"));
201        }
202
203        if let Some(entry) = self.entries.remove(&inner) {
204            if entry.is_dir() {
205                let childs: Vec<_> = self
206                    .entries
207                    .iter()
208                    .map(|(path, _)| path)
209                    .filter(|&path| path.starts_with(&inner))
210                    .cloned()
211                    .collect();
212
213                for child in childs {
214                    self.entries.remove(&child);
215                }
216            }
217        }
218
219        Ok(())
220    }
221
222    /// Normalizes an arbitrary `path` by processing all occurrences
223    /// of '.' and '..' elements. Also, removes final `/`.
224    fn normalize<P: AsRef<Path>>(path: P) -> PathBuf {
225        let mut result = PathBuf::new();
226        for component in path.as_ref().components() {
227            match component {
228                Component::CurDir => {}
229                Component::ParentDir => {
230                    result.pop();
231                }
232                _ => {
233                    result.push(component);
234                }
235            }
236        }
237        // remove final /
238        if result != PathBuf::from("/") && result.ends_with("/") {
239            result.pop();
240        }
241        result
242    }
243
244    fn to_host<P: AsRef<Path>>(&self, inner_path: P) -> PathBuf {
245        let inner = self.to_inner(inner_path);
246        self.root.join(inner.strip_prefix("/").unwrap())
247    }
248
249    fn to_inner<P: AsRef<Path>>(&self, inner_path: P) -> PathBuf {
250        Self::normalize(self.cwd.join(inner_path))
251    }
252
253    fn is_inner_root<P: AsRef<Path>>(path: P) -> bool {
254        let components: Vec<_> = path.as_ref().components().collect();
255        components.len() == 1 && components[0] == Component::RootDir
256    }
257
258    /// Make directories recursively.
259    /// * `path` is an absolute host path.
260    /// Returns vector of created directories.
261    fn mkdir_all<P: AsRef<Path>>(path: P) -> Result<Vec<PathBuf>> {
262        let host_path = path.as_ref().to_path_buf();
263
264        // Looking for the first existing parent
265        let mut existed_part = host_path.clone();
266        while let Some(parent) = existed_part.parent() {
267            let parent_buf = parent.to_path_buf();
268            if std::fs::exists(parent)? {
269                existed_part = parent_buf;
270                break;
271            }
272            existed_part = parent_buf;
273        }
274
275        // Create from the closest existing parent to the target path
276        let need_to_create: Vec<_> = host_path
277            .strip_prefix(&existed_part)?
278            .components()
279            .collect();
280
281        let mut created = Vec::new();
282
283        let mut built = PathBuf::from(&existed_part);
284        for component in need_to_create {
285            built.push(component);
286            if !std::fs::exists(&built)? {
287                std::fs::create_dir(&built)?;
288                created.push(built.clone());
289            }
290        }
291
292        Ok(created)
293    }
294
295    fn rm_host_artifact<P: AsRef<Path>>(host_path: P) -> Result<()> {
296        let host_path = host_path.as_ref();
297        if host_path.is_dir() {
298            std::fs::remove_dir_all(host_path)?
299        } else {
300            std::fs::remove_file(host_path)?
301        }
302        Ok(())
303    }
304
305    fn check_permissions<P: AsRef<Path>>(path: P) -> bool {
306        let path = path.as_ref();
307        let filename = path.join(".access");
308        if let Err(_) = std::fs::write(&filename, b"check") {
309            return false;
310        }
311        if let Err(_) = std::fs::remove_file(filename) {
312            return false;
313        }
314        true
315    }
316
317    /// Recursively adds a directory and all its entries to the VFS.
318    fn add_recursive(&mut self, inner_path: &Path, host_path: &Path) -> Result<()> {
319        let entry_type = if host_path.is_dir() {
320            DirEntryType::Directory
321        } else {
322            DirEntryType::File
323        };
324        let entry = DirEntry::new(inner_path, entry_type);
325        self.entries.insert(inner_path.to_path_buf(), entry);
326
327        if host_path.is_dir() {
328            for entry in std::fs::read_dir(host_path)? {
329                let entry = entry?;
330                let host_child = entry.path();
331                let inner_child = inner_path.join(entry.file_name());
332
333                self.add_recursive(&inner_child, &host_child)?;
334            }
335        }
336
337        Ok(())
338    }
339
340    fn inner_or_cwd<P: AsRef<Path>>(&self, path: Option<P>) -> Result<PathBuf> {
341        Ok(match path {
342            Some(p) => {
343                let inner = self.to_inner(&p);
344                if !self.exists(&inner) {
345                    return Err(anyhow!("{} does not exist", p.as_ref().display()));
346                }
347                inner
348            }
349            None => self.cwd().to_path_buf(),
350        })
351    }
352}
353
354impl FsBackend for DirFS {
355    /// Returns root path related to the host file system.
356    fn root(&self) -> &Path {
357        self.root.as_path()
358    }
359
360    /// Returns current working directory related to the vfs root.
361    fn cwd(&self) -> &Path {
362        self.cwd.as_path()
363    }
364
365    /// Changes the current working directory.
366    /// * `path` can be in relative or absolute form, but in both cases it must exist.
367    /// An error is returned if the specified `path` does not exist.
368    fn cd<P: AsRef<Path>>(&mut self, path: P) -> Result<()> {
369        let target = self.to_inner(path);
370        if !self.exists(&target) {
371            return Err(anyhow!("{} does not exist", target.display()));
372        }
373        self.cwd = target;
374        Ok(())
375    }
376
377    /// Checks if a `path` exists in the vfs.
378    /// The `path` can be:
379    /// - absolute (starting with '/'),
380    /// - relative (relative to the vfs cwd),
381    /// - contain '..' or '.'.
382    fn exists<P: AsRef<Path>>(&self, path: P) -> bool {
383        let inner = self.to_inner(path);
384        self.entries.contains_key(&inner)
385    }
386
387    /// Returns an iterator over directory entries at a specific depth (shallow listing).
388    ///
389    /// This method lists only the **immediate children** of the given directory (or CWD if `None`),
390    /// i.e., entries that are exactly one level below the specified path.
391    /// It does *not* recurse into subdirectories (see `tree()` if you need recurse).
392    ///
393    /// # Arguments
394    /// * `path` - Optional path to the directory to list:
395    ///   - `Some(p)`: list contents of directory `p` (must exist in VFS).
396    ///   - `None`: list contents of the current working directory (`cwd`).
397    ///
398    /// # Returns
399    /// * `Ok(impl Iterator<Item = DirEntry>)` - Iterator over entries of immediate children
400    ///   (relative to VFS root). The yielded paths are *inside* the target directory
401    ///   but do not include deeper nesting.
402    /// * `Err(anyhow::Error)` - If the specified path does not exist in VFS.
403    ///
404    /// # Example:
405    /// ```
406    /// use std::path::Path;
407    /// use vfs_kit::{DirEntry, DirFS, FsBackend};
408    ///
409    /// let temp_dir = tempdir::TempDir::new("vfs_example").unwrap();
410    /// let mut fs = DirFS::new(temp_dir.path()).unwrap();
411    ///
412    /// fs.mkdir("/docs/subdir");
413    /// fs.mkfile("/docs/document.txt", None);
414    ///
415    /// // List current directory contents
416    /// for entry in fs.ls::<&Path>(None).unwrap() {
417    ///     println!("{:?}", entry);
418    /// }
419    ///
420    /// // List contents of "/docs"
421    /// for entry in fs.ls(Some("/docs")).unwrap() {
422    ///     if entry.is_file() {
423    ///         println!("File: {:?}", entry);
424    ///     } else {
425    ///         println!("Dir:  {:?}", entry);
426    ///     }
427    /// }
428    /// ```
429    ///
430    /// # Notes
431    /// - **No recursion:** Unlike `tree()`, this method does *not* traverse subdirectories.
432    /// - **Path ownership:** The returned iterator borrows from the VFS's internal state.
433    ///   It is valid as long as `self` lives.
434    /// - **Excludes root:** The input directory itself is not included in the output.
435    /// - **Error handling:** If `path` does not exist, an error is returned before iteration.
436    /// - **Performance:** The filtering is done in‑memory; no additional filesystem I/O occurs
437    ///   during iteration.
438    fn ls<P: AsRef<Path>>(&self, path: Option<P>) -> Result<impl Iterator<Item = DirEntry>> {
439        let inner_path = self.inner_or_cwd(path)?;
440        let component_count = inner_path.components().count() + 1;
441        Ok(self
442            .entries
443            .iter()
444            .map(|(_, entry)| entry.clone())
445            .filter(move |entry| {
446                entry.path().starts_with(&inner_path)
447                    && entry.path() != inner_path
448                    && entry.path().components().count() == component_count
449            }))
450    }
451
452    /// Returns a recursive iterator over the directory tree starting from a given path.
453    ///
454    /// The iterator yields all entries (files and directories) that are *inside* the specified
455    /// directory (i.e., the starting directory itself is **not** included).
456    ///
457    /// # Arguments
458    /// * `path` - Optional path to the directory to traverse:
459    ///   - `Some(p)`: start from directory `p` (must exist in VFS).
460    ///   - `None`: start from the current working directory (`cwd`).
461    ///
462    /// # Returns
463    /// * `Ok(impl Iterator<Item = DirEntry>)` - Iterator over all entries *within* the tree
464    ///   (relative to VFS root), excluding the root of the traversal.
465    /// * `Err(anyhow::Error)` - If:
466    ///   - The specified path does not exist in VFS.
467    ///   - The path is not a directory (implicitly checked via `exists` and tree structure).
468    ///
469    /// # Behavior
470    /// - **Recursive traversal**: Includes all nested files and directories.
471    /// - **Excludes root**: The starting directory path is not yielded (only its contents).
472    /// - **Path normalization**: Input path is normalized.
473    /// - **VFS-only**: Only returns paths tracked in VFS.
474    /// - **Performance:** The filtering is done in‑memory; no additional filesystem I/O occurs
475    ///   during iteration.
476    ///
477    /// # Example:
478    /// ```
479    /// use std::path::Path;
480    /// use vfs_kit::{DirFS, FsBackend};
481    ///
482    /// let temp_dir = tempdir::TempDir::new("vfs_example").unwrap();
483    /// let mut fs = DirFS::new(temp_dir.path()).unwrap();
484    ///
485    /// fs.mkdir("/docs/subdir");
486    /// fs.mkfile("/docs/document.txt", None);
487    ///
488    /// // Iterate over current working directory
489    /// for entry in fs.tree::<&Path>(None).unwrap() {
490    ///     println!("{:?}", entry);
491    /// }
492    ///
493    /// // Iterate over a specific directory
494    /// for entry in fs.tree(Some("/docs")).unwrap() {
495    ///     if entry.is_file() {
496    ///         println!("File: {:?}", entry);
497    ///     }
498    /// }
499    /// ```
500    ///
501    /// # Notes
502    /// - The iterator borrows data from VFS. The returned iterator is valid as long
503    ///   as `self` is alive.
504    /// - Symbolic links are treated as regular entries (no follow/resolve).
505    /// - Use `Path` methods (e.g., `is_file()`, `is_dir()`) on yielded items for type checks.
506    fn tree<P: AsRef<Path>>(&self, path: Option<P>) -> Result<impl Iterator<Item = DirEntry>> {
507        let path = self.inner_or_cwd(path)?;
508        Ok(self
509            .entries
510            .iter()
511            .map(|(_, entry)| entry.clone())
512            .filter(move |entry| entry.path().starts_with(&path) && entry.path() != path))
513    }
514
515    /// Creates directory and all it parents (if needed).
516    /// * `path` - inner vfs path.
517    fn mkdir<P: AsRef<Path>>(&mut self, path: P) -> Result<()> {
518        if path.as_ref().as_os_str().is_empty() {
519            return Err(anyhow!("invalid path: empty"));
520        }
521
522        let inner_path = self.to_inner(path);
523
524        if self.exists(&inner_path) {
525            return Err(anyhow!("path already exists: {}", inner_path.display()));
526        }
527
528        // Looking for the first existing parent
529        let mut existed_parent = inner_path.clone();
530        while let Some(parent) = existed_parent.parent() {
531            let parent_buf = parent.to_path_buf();
532            if self.exists(parent) {
533                existed_parent = parent_buf;
534                break;
535            }
536            existed_parent = parent_buf;
537        }
538
539        // Create from the closest existing parent to the target path
540        let need_to_create: Vec<_> = inner_path
541            .strip_prefix(&existed_parent)?
542            .components()
543            .collect();
544
545        let mut built = PathBuf::from(&existed_parent);
546        for component in need_to_create {
547            built.push(component);
548            if !self.exists(&built) {
549                let host = self.to_host(&built);
550                std::fs::create_dir(&host)?;
551                self.entries.insert(
552                    built.clone(),
553                    DirEntry::new(&built, DirEntryType::Directory),
554                );
555            }
556        }
557
558        Ok(())
559    }
560
561    /// Creates new file in vfs.
562    /// * `file_path` must be inner vfs path. It must contain the name of the file,
563    /// optionally preceded by existing parent directory.
564    /// If the parent directory does not exist, an error is returned.
565    fn mkfile<P: AsRef<Path>>(&mut self, file_path: P, content: Option<&[u8]>) -> Result<()> {
566        let file_path = self.to_inner(file_path);
567        if let Some(parent) = file_path.parent() {
568            if let Err(e) = std::fs::exists(parent) {
569                return Err(anyhow!("{:?}: {}", parent, e));
570            }
571        }
572        let host = self.to_host(&file_path);
573        let mut fd = std::fs::File::create(host)?;
574        self.entries.insert(
575            file_path.clone(),
576            DirEntry::new(&file_path, DirEntryType::File),
577        );
578        if let Some(content) = content {
579            fd.write_all(content)?;
580        }
581        Ok(())
582    }
583
584    /// Reads the entire contents of a file into a byte vector.
585    /// * `path` is the inner VFS path.
586    ///
587    /// # Returns
588    /// * `Ok(Vec<u8>)` - File content as a byte vector if successful.
589    /// * `Err(anyhow::Error)` - If any of the following occurs:
590    ///   - File does not exist in VFS (`file does not exist: ...`)
591    ///   - Path points to a directory (`... is a directory`)
592    ///   - Permission issues when accessing the host file
593    ///   - I/O errors during reading
594    ///
595    /// # Notes
596    /// - Does **not** follow symbolic links on the host filesystem (reads the link itself).
597    /// - Returns an empty vector for empty files.
598    fn read<P: AsRef<Path>>(&self, path: P) -> Result<Vec<u8>> {
599        let inner = self.to_inner(&path);
600        if !self.exists(&inner) {
601            return Err(anyhow!("file does not exist: {}", path.as_ref().display()));
602        }
603        if let Some(entry) = self.entries.get(&inner) {
604            if entry.is_dir() {
605                return Err(anyhow!("{} is a directory", inner.display()));
606            }
607        }
608
609        let mut content = Vec::new();
610        let host = self.to_host(&inner);
611        std::fs::File::open(&host)?.read_to_end(&mut content)?;
612
613        Ok(content)
614    }
615
616    /// Writes bytes to an existing file, replacing its entire contents.
617    /// * `path` - Path to the file.
618    /// * `content` - Byte slice (`&[u8]`) to write to the file.
619    ///
620    /// # Returns
621    /// * `Ok(())` - If the write operation succeeded.
622    /// * `Err(anyhow::Error)` - If any of the following occurs:
623    ///   - File does not exist in VFS (`file does not exist: ...`)
624    ///   - Path points to a directory (`... is a directory`)
625    ///   - Permission issues when accessing the host file
626    ///   - I/O errors during writing (e.g., disk full, invalid path)
627    ///
628    /// # Behavior
629    /// - **Overwrites completely**: The entire existing content is replaced.
630    /// - **No file creation**: File must exist (use `mkfile()` first).
631    /// - **Atomic operation**: Uses `std::fs::write()` which replaces the file in one step.
632    /// - **Permissions**: The file retains its original permissions (no chmod is performed).
633    fn write<P: AsRef<Path>>(&self, path: P, content: &[u8]) -> Result<()> {
634        let inner = self.to_inner(&path);
635        if !self.exists(&inner) {
636            return Err(anyhow!("file does not exist: {}", path.as_ref().display()));
637        }
638        if let Some(entry) = self.entries.get(&inner) {
639            if entry.is_dir() {
640                return Err(anyhow!("{} is a directory", inner.display()));
641            }
642        }
643
644        let host = self.to_host(&inner);
645        std::fs::write(&host, content)?;
646
647        Ok(())
648    }
649
650    /// Appends bytes to the end of an existing file, preserving its old contents.
651    ///
652    /// # Arguments
653    /// * `path` - Path to the existing file.
654    /// * `content` - Byte slice (`&[u8]`) to append to the file.
655    ///
656    /// # Returns
657    /// * `Ok(())` - If the append operation succeeded.
658    /// * `Err(anyhow::Error)` - If any of the following occurs:
659    ///   - File does not exist in VFS (`file does not exist: ...`)
660    ///   - Path points to a directory (`... is a directory`)
661    ///   - Permission issues when accessing the host file
662    ///   - I/O errors during writing (e.g., disk full, invalid path)
663    ///
664    /// # Behavior
665    /// - **Appends only**: Existing content is preserved; new bytes are added at the end.
666    /// - **No parent creation**: Parent directories must exist (use `mkdir()` first if needed).
667    /// - **File creation**: Does NOT create the file if it doesn't exist (returns error).
668    /// - **Permissions**: The file retains its original permissions.
669    fn append<P: AsRef<Path>>(&self, path: P, content: &[u8]) -> Result<()> {
670        let inner = self.to_inner(&path);
671        if !self.exists(&inner) {
672            return Err(anyhow!("file does not exist: {}", path.as_ref().display()));
673        }
674        if let Some(entry) = self.entries.get(&inner) {
675            if entry.is_dir() {
676                return Err(anyhow!("{} is a directory", inner.display()));
677            }
678        }
679
680        // Open file in append mode and write content
681        use std::fs::OpenOptions;
682        let host = self.to_host(&inner);
683        let mut file = OpenOptions::new().write(true).append(true).open(&host)?;
684
685        file.write_all(content)?;
686
687        Ok(())
688    }
689
690    /// Removes a file or directory at the specified path.
691    ///
692    /// - `path`: can be absolute (starting with '/') or relative to the current working
693    /// directory (cwd).
694    /// - If the path is a directory, all its contents are removed recursively.
695    ///
696    /// Returns:
697    /// - `Ok(())` on successful removal.
698    /// - `Err(_)` if:
699    ///   - the path does not exist in the VFS;
700    ///   - there are insufficient permissions;
701    ///   - a filesystem error occurs.
702    fn rm<P: AsRef<Path>>(&mut self, path: P) -> Result<()> {
703        if path.as_ref().as_os_str().is_empty() {
704            return Err(anyhow!("invalid path: empty"));
705        }
706        if Self::is_inner_root(&path) {
707            return Err(anyhow!("invalid path: the root cannot be removed"));
708        }
709
710        let inner_path = self.to_inner(path); // Convert to VFS-internal normalized path
711        let host_path = self.to_host(&inner_path); // Map to real filesystem path
712
713        // Check if the path exists in the virtual filesystem
714        if !self.exists(&inner_path) {
715            return Err(anyhow!("{} does not exist", inner_path.display()));
716        }
717
718        // Remove from the real filesystem
719        Self::rm_host_artifact(host_path)?;
720
721        // Update internal state: collect all entries that start with `inner_path`
722        let removed: Vec<PathBuf> = self
723            .entries
724            .iter()
725            .map(|(entry_path, _)| entry_path)
726            .filter(|&p| p.starts_with(&inner_path)) // Match prefix (includes subpaths)
727            .cloned()
728            .collect();
729
730        // Remove all matched entries from the set
731        for p in &removed {
732            self.entries.remove(p);
733        }
734
735        Ok(())
736    }
737
738    /// Removes all artifacts (dirs and files) in vfs, but preserve its root.
739    fn cleanup(&mut self) -> bool {
740        let mut is_ok = true;
741
742        // Collect all paths to delete (except the root "/")
743        let mut sorted_paths_to_remove: BTreeSet<PathBuf> = BTreeSet::new();
744        for (path, entry) in &self.entries {
745            if !entry.is_root() {
746                sorted_paths_to_remove.insert(path.clone());
747            }
748        }
749
750        for entry in sorted_paths_to_remove.iter().rev() {
751            let host = self.to_host(entry);
752            let result = Self::rm_host_artifact(&host);
753            if result.is_ok() {
754                self.entries.remove(entry);
755            } else {
756                is_ok = false;
757                eprintln!("Unable to remove: {}", host.display());
758            }
759        }
760
761        is_ok
762    }
763}
764
765impl Drop for DirFS {
766    fn drop(&mut self) {
767        if !self.is_auto_clean {
768            return;
769        }
770
771        if self.cleanup() {
772            self.entries.clear();
773        }
774
775        let errors: Vec<_> = self
776            .created_root_parents
777            .iter()
778            .rev()
779            .filter_map(|p| Self::rm_host_artifact(p).err())
780            .collect();
781        if !errors.is_empty() {
782            eprintln!("Failed to remove parents: {:?}", errors);
783        }
784
785        self.created_root_parents.clear();
786    }
787}
788
789#[cfg(test)]
790mod tests {
791    use super::*;
792    use tempdir::TempDir;
793
794    mod creations {
795        use super::*;
796
797        #[test]
798        fn test_new_absolute_path_existing() {
799            let temp_dir = setup_test_env();
800            let root = temp_dir.path().to_path_buf();
801
802            let fs = DirFS::new(&root).unwrap();
803
804            assert_eq!(fs.root, root);
805            assert_eq!(fs.cwd, PathBuf::from("/"));
806            assert!(fs.entries.contains_key(&PathBuf::from("/")));
807            assert!(fs.created_root_parents.is_empty());
808            assert!(fs.is_auto_clean);
809        }
810
811        #[test]
812        fn test_new_nonexistent_path_created() {
813            let temp_dir = setup_test_env();
814            let nonexistent = temp_dir.path().join("new_root");
815
816            let fs = DirFS::new(&nonexistent).unwrap();
817
818            assert_eq!(fs.root, nonexistent);
819            assert!(!fs.created_root_parents.is_empty()); // parents must be created
820            assert!(nonexistent.exists()); // The catalog has been created
821        }
822
823        #[test]
824        fn test_new_nested_nonexistent_path() {
825            let temp_dir = setup_test_env();
826            let nested = temp_dir.path().join("a/b/c");
827
828            let fs = DirFS::new(&nested).unwrap();
829
830            assert_eq!(fs.root, nested);
831            assert_eq!(fs.created_root_parents.len(), 3); // a, a/b, a/b/c
832            assert!(nested.exists());
833        }
834
835        #[test]
836        fn test_new_permission_denied() {
837            // This test requires a specific environment (e.g. readonly FS)
838            #[cfg(unix)]
839            {
840                use std::os::unix::fs::PermissionsExt;
841
842                let temp_dir = setup_test_env();
843                let protected = temp_dir.path().join("protected");
844                let protected_root = protected.join("root");
845                std::fs::create_dir_all(&protected_root).unwrap();
846                std::fs::set_permissions(&protected, PermissionsExt::from_mode(0o000)).unwrap(); // No access
847
848                let result = DirFS::new(&protected_root);
849                assert!(result.is_err());
850
851                std::fs::set_permissions(&protected, PermissionsExt::from_mode(0o755)).unwrap(); // Grant access
852            }
853        }
854
855        #[test]
856        fn test_new_normalize_path() {
857            let temp_dir = setup_test_env();
858            let messy_path = temp_dir.path().join("././subdir/../subdir");
859
860            let fs = DirFS::new(&messy_path).unwrap();
861            let canonical = DirFS::normalize(temp_dir.path().join("subdir"));
862
863            assert_eq!(fs.root, canonical);
864        }
865
866        #[test]
867        fn test_new_root_is_file() {
868            let temp_dir = setup_test_env();
869            let file_path = temp_dir.path().join("file.txt");
870            std::fs::write(&file_path, "content").unwrap();
871
872            let result = DirFS::new(&file_path);
873            assert!(result.is_err()); // Cannot create DirFs on file
874        }
875
876        #[test]
877        fn test_new_empty_path() {
878            let result = DirFS::new("");
879            assert!(result.is_err());
880        }
881
882        #[test]
883        fn test_new_special_characters() {
884            let temp_dir = setup_test_env();
885            let special = temp_dir.path().join("папка с пробелами и юникод!");
886
887            let fs = DirFS::new(&special).unwrap();
888
889            assert_eq!(fs.root, special);
890            assert!(special.exists());
891        }
892
893        #[test]
894        fn test_new_is_auto_clean_default() {
895            let temp_dir = setup_test_env();
896            let fs = DirFS::new(temp_dir.path()).unwrap();
897            assert!(fs.is_auto_clean); // True by default
898        }
899
900        #[test]
901        fn test_root_returns_correct_path() {
902            let temp_dir = setup_test_env();
903
904            let vfs_root = temp_dir.path().join("vfs-root");
905            let fs = DirFS::new(&vfs_root).unwrap();
906            assert_eq!(fs.root(), vfs_root);
907        }
908
909        #[test]
910        fn test_cwd_defaults_to_root() {
911            let temp_dir = setup_test_env();
912            let fs = DirFS::new(temp_dir).unwrap();
913            assert_eq!(fs.cwd(), Path::new("/"));
914        }
915    }
916
917    mod normalize {
918        use super::*;
919
920        #[test]
921        fn test_normalize_path() {
922            assert_eq!(DirFS::normalize("/a/b/c/"), PathBuf::from("/a/b/c"));
923            assert_eq!(DirFS::normalize("/a/b/./c"), PathBuf::from("/a/b/c"));
924            assert_eq!(DirFS::normalize("/a/b/../c"), PathBuf::from("/a/c"));
925            assert_eq!(DirFS::normalize("/"), PathBuf::from("/"));
926            assert_eq!(DirFS::normalize("/.."), PathBuf::from("/"));
927            assert_eq!(DirFS::normalize(".."), PathBuf::from(""));
928            assert_eq!(DirFS::normalize(""), PathBuf::from(""));
929            assert_eq!(DirFS::normalize("../a"), PathBuf::from("a"));
930            assert_eq!(DirFS::normalize("./a"), PathBuf::from("a"));
931        }
932    }
933
934    mod cd {
935        use super::*;
936
937        #[test]
938        fn test_cd_to_absolute_path() {
939            let temp_dir = setup_test_env();
940            let mut fs = DirFS::new(&temp_dir).unwrap();
941            fs.mkdir("/projects").unwrap();
942            fs.cd("/projects").unwrap();
943            assert_eq!(fs.cwd(), Path::new("/projects"));
944        }
945
946        #[test]
947        fn test_cd_with_relative_path() {
948            let temp_dir = setup_test_env();
949            let mut fs = DirFS::new(&temp_dir).unwrap();
950            fs.mkdir("/home/user").unwrap();
951            fs.cwd = PathBuf::from("/home");
952            fs.cd("user").unwrap();
953            assert_eq!(fs.cwd(), Path::new("/home/user"));
954        }
955
956        #[test]
957        fn test_cd_extreme_cases() {
958            let temp_dir = setup_test_env();
959            let mut fs = DirFS::new(&temp_dir).unwrap();
960
961            fs.cd("..").unwrap(); // where cwd == "/"
962            assert_eq!(fs.cwd(), Path::new("/"));
963
964            fs.cd(".").unwrap(); // where cwd == "/"
965            assert_eq!(fs.cwd(), Path::new("/"));
966
967            fs.cwd = PathBuf::from("/home");
968            assert_eq!(fs.cwd(), Path::new("/home"));
969            fs.mkdir("/other").unwrap();
970            fs.cd("../other").unwrap();
971            assert_eq!(fs.cwd(), Path::new("/other"));
972
973            fs.cwd = PathBuf::from("/home");
974            assert_eq!(fs.cwd(), Path::new("/home"));
975            fs.mkdir("/home/other").unwrap();
976            fs.cd("./other").unwrap();
977            assert_eq!(fs.cwd(), Path::new("/home/other"));
978        }
979    }
980
981    mod mkdir {
982        use super::*;
983
984        #[test]
985        fn test_mkdir_create_single_dir() {
986            let temp_dir = setup_test_env();
987            let mut fs = DirFS::new(&temp_dir).unwrap();
988            fs.mkdir("/projects").unwrap();
989            assert!(fs.exists("/projects"));
990        }
991
992        #[test]
993        fn test_mkdir_relative_path() {
994            let temp_dir = setup_test_env();
995            let mut fs = DirFS::new(&temp_dir).unwrap();
996            fs.mkdir("home").unwrap();
997            fs.cd("/home").unwrap();
998            fs.mkdir("user").unwrap();
999            assert!(fs.exists("/home/user"));
1000        }
1001
1002        #[test]
1003        fn test_mkdir_nested_path() {
1004            let temp_dir = setup_test_env();
1005            let mut fs = DirFS::new(&temp_dir).unwrap();
1006            fs.mkdir("/a/b/c").unwrap();
1007            assert!(fs.exists("/a"));
1008            assert!(fs.exists("/a/b"));
1009            assert!(fs.exists("/a/b/c"));
1010        }
1011
1012        #[test]
1013        fn test_mkdir_already_exists() {
1014            let temp_dir = setup_test_env();
1015            let mut fs = DirFS::new(&temp_dir).unwrap();
1016            fs.mkdir("/data").unwrap();
1017            let result = fs.mkdir("/data");
1018            assert!(result.is_err());
1019        }
1020
1021        #[test]
1022        fn test_mkdir_invalid_path() {
1023            let temp_dir = setup_test_env();
1024            let mut fs = DirFS::new(&temp_dir).unwrap();
1025            let result = fs.mkdir("");
1026            assert!(result.is_err());
1027        }
1028    }
1029
1030    mod exists {
1031        use super::*;
1032
1033        #[test]
1034        fn test_exists_root() {
1035            let temp_dir = setup_test_env();
1036            let fs = DirFS::new(&temp_dir).unwrap();
1037            assert!(fs.exists("/"));
1038        }
1039
1040        #[test]
1041        fn test_exists_cwd() {
1042            let temp_dir = setup_test_env();
1043            let mut fs = DirFS::new(&temp_dir).unwrap();
1044            fs.mkdir("/projects").unwrap();
1045            fs.cd("/projects").unwrap();
1046            assert!(fs.exists("."));
1047            assert!(fs.exists("./"));
1048            assert!(fs.exists("/projects"));
1049        }
1050    }
1051
1052    mod ls {
1053        use super::*;
1054
1055        #[test]
1056        fn test_ls_empty_cwd() -> Result<()> {
1057            let temp_dir = setup_test_env();
1058            let fs = DirFS::new(temp_dir.path())?;
1059
1060            let entries: Vec<_> = fs.ls::<&Path>(None)?.collect();
1061            assert!(entries.is_empty(), "CWD should have no entries");
1062
1063            Ok(())
1064        }
1065
1066        #[test]
1067        fn test_ls_single_file_in_cwd() -> Result<()> {
1068            let temp_dir = setup_test_env();
1069            let mut fs = DirFS::new(temp_dir.path())?;
1070
1071            fs.mkfile("/file.txt", Some(b"Hello"))?;
1072
1073            let entries: Vec<_> = fs.ls::<&Path>(None)?.collect();
1074            assert_eq!(entries.len(), 1, "Should return exactly one file");
1075            assert_eq!(
1076                entries[0].path(),
1077                Path::new("/file.txt"),
1078                "File path should match"
1079            );
1080
1081            Ok(())
1082        }
1083
1084        #[test]
1085        fn test_ls_multiple_items_in_directory() -> Result<()> {
1086            let temp_dir = setup_test_env();
1087            let mut fs = DirFS::new(temp_dir.path())?;
1088
1089            fs.mkdir("/docs")?;
1090            fs.mkfile("/docs/readme.txt", None)?;
1091            fs.mkfile("/docs/todo.txt", None)?;
1092
1093            let entries: Vec<_> = fs.ls(Some("/docs"))?.collect();
1094
1095            assert_eq!(entries.len(), 2, "Should list both files in directory");
1096            assert!(entries.contains(&DirEntry::new("/docs/readme.txt", DirEntryType::File)));
1097            assert!(entries.contains(&DirEntry::new("/docs/todo.txt", DirEntryType::File)));
1098
1099            Ok(())
1100        }
1101
1102        #[test]
1103        fn test_ls_nested_files_excluded() -> Result<()> {
1104            let temp_dir = setup_test_env();
1105            let mut fs = DirFS::new(temp_dir.path())?;
1106
1107            fs.mkdir("/project/src")?;
1108            fs.mkfile("/project/main.rs", None)?;
1109            fs.mkfile("/project/src/lib.rs", None)?; // nested - should be excluded
1110
1111            let entries: Vec<_> = fs.ls(Some("/project"))?.collect();
1112
1113            assert_eq!(entries.len(), 2, "Only immediate children should be listed");
1114            assert!(entries.contains(&DirEntry::new("/project/main.rs", DirEntryType::File)));
1115            assert!(
1116                !entries
1117                    .iter()
1118                    .any(|p| p == &DirEntry::new("/project/src/lib.rs", DirEntryType::File)),
1119                "Nested file should not be included"
1120            );
1121
1122            Ok(())
1123        }
1124
1125        #[test]
1126        fn test_ls_directories_and_files_mixed() -> Result<()> {
1127            let temp_dir = setup_test_env();
1128            let mut fs = DirFS::new(temp_dir.path())?;
1129
1130            fs.mkdir("/mix")?;
1131            fs.mkfile("/mix/file1.txt", None)?;
1132            fs.mkdir("/mix/subdir")?; // subdirectory - should be included
1133            fs.mkfile("/mix/subdir/deep.txt", None)?; // deeper - should be excluded
1134
1135            let entries: Vec<_> = fs.ls(Some("/mix"))?.collect();
1136
1137            assert_eq!(
1138                entries.len(),
1139                2,
1140                "Both file and subdirectory should be listed"
1141            );
1142            assert!(entries.contains(&DirEntry::new("/mix/file1.txt", DirEntryType::File)));
1143            assert!(entries.contains(&DirEntry::new("/mix/subdir", DirEntryType::Directory)));
1144            assert!(
1145                !entries
1146                    .iter()
1147                    .any(|p| p.path().to_str().unwrap().contains("deep.txt")),
1148                "Deeper nested file should be excluded"
1149            );
1150
1151            Ok(())
1152        }
1153
1154        #[test]
1155        fn test_ls_nonexistent_path_returns_error() -> Result<()> {
1156            let temp_dir = setup_test_env();
1157            let fs = DirFS::new(temp_dir.path())?;
1158
1159            let result: Result<Vec<_>> = fs
1160                .tree(Some("/nonexistent/path"))
1161                .map(|iter| iter.collect());
1162
1163            assert!(result.is_err(), "Should return error for nonexistent path");
1164            assert!(
1165                result.unwrap_err().to_string().contains("does not exist"),
1166                "Error message should indicate path does not exist"
1167            );
1168
1169            Ok(())
1170        }
1171
1172        #[test]
1173        fn test_ls_relative_path_resolution() -> Result<()> {
1174            let temp_dir = setup_test_env();
1175            let mut fs = DirFS::new(temp_dir.path())?;
1176
1177            fs.mkdir("/base")?;
1178            fs.cd("/base")?;
1179            fs.mkdir("sub")?;
1180            fs.mkfile("sub/file.txt", None)?;
1181            fs.mkfile("note.txt", None)?;
1182
1183            // List contents of relative path "sub"
1184            let sub_entries: Vec<_> = fs.ls(Some("sub"))?.collect();
1185            assert_eq!(
1186                sub_entries.len(),
1187                1,
1188                "Current directory should list one item"
1189            );
1190
1191            // List current directory (base)
1192            let base_entries: Vec<_> = fs.ls(Some("."))?.collect();
1193            assert_eq!(
1194                base_entries.len(),
1195                2,
1196                "Current directory should list two items"
1197            );
1198            assert!(base_entries.contains(&DirEntry::new("/base/sub", DirEntryType::Directory)));
1199            assert!(base_entries.contains(&DirEntry::new("/base/note.txt", DirEntryType::File)));
1200
1201            Ok(())
1202        }
1203
1204        #[test]
1205        fn test_ls_unicode_path_support() -> Result<()> {
1206            let temp_dir = setup_test_env();
1207            let mut fs = DirFS::new(temp_dir.path())?;
1208
1209            fs.mkdir("/проект")?;
1210            fs.mkfile("/проект/документ.txt", Some(b"Content"))?;
1211            fs.mkdir("/проект/подпапка")?;
1212            fs.mkfile("/проект/подпапка/файл.txt", Some(b"Nested"))?; // should be excluded
1213
1214            let entries: Vec<_> = fs.ls(Some("/проект"))?.collect();
1215
1216            assert_eq!(
1217                entries.len(),
1218                2,
1219                "Should include both file and subdir at level"
1220            );
1221            assert!(entries.contains(&DirEntry::new("/проект/документ.txt", DirEntryType::File)));
1222            assert!(entries.contains(&DirEntry::new("/проект/подпапка", DirEntryType::Directory)));
1223            assert!(
1224                !entries
1225                    .iter()
1226                    .any(|p| p.path().to_str().unwrap().contains("файл.txt")),
1227                "Nested unicode file should be excluded"
1228            );
1229
1230            Ok(())
1231        }
1232
1233        #[test]
1234        fn test_ls_root_directory_listing() -> Result<()> {
1235            let temp_dir = setup_test_env();
1236            let mut fs = DirFS::new(temp_dir.path())?;
1237
1238            fs.mkfile("/a.txt", None)?;
1239            fs.mkdir("/sub")?;
1240            fs.mkfile("/sub/inner.txt", None)?; // should be excluded (nested)
1241
1242            let entries: Vec<_> = fs.ls(Some("/"))?.collect();
1243
1244            assert_eq!(
1245                entries.len(),
1246                2,
1247                "Root should list immediate files and dirs"
1248            );
1249            assert!(entries.contains(&DirEntry::new("/a.txt", DirEntryType::File)));
1250            assert!(entries.contains(&DirEntry::new("/sub", DirEntryType::Directory)));
1251            assert!(
1252                !entries
1253                    .iter()
1254                    .any(|p| p.path().to_str().unwrap().contains("inner.txt")),
1255                "Nested file in sub should be excluded"
1256            );
1257
1258            Ok(())
1259        }
1260
1261        #[test]
1262        fn test_ls_empty_directory_returns_empty() -> Result<()> {
1263            let temp_dir = setup_test_env();
1264            let mut fs = DirFS::new(temp_dir.path())?;
1265
1266            fs.mkdir("/empty")?;
1267
1268            let entries: Vec<_> = fs.ls(Some("/empty"))?.collect();
1269            assert!(
1270                entries.is_empty(),
1271                "Empty directory should return no entries"
1272            );
1273
1274            Ok(())
1275        }
1276    }
1277
1278    mod tree {
1279        use super::*;
1280
1281        #[test]
1282        fn test_tree_current_directory_empty() -> Result<()> {
1283            let temp_dir = setup_test_env();
1284            let fs = DirFS::new(temp_dir.path())?;
1285
1286            let entries: Vec<_> = fs.tree::<&Path>(None)?.collect();
1287            assert!(entries.is_empty());
1288
1289            Ok(())
1290        }
1291
1292        #[test]
1293        fn test_tree_specific_directory_empty() -> Result<()> {
1294            let temp_dir = setup_test_env();
1295            let mut fs = DirFS::new(temp_dir.path())?;
1296
1297            fs.mkdir("/empty_dir")?;
1298
1299            let entries: Vec<_> = fs.tree(Some("/empty_dir"))?.collect();
1300            assert!(entries.is_empty());
1301
1302            Ok(())
1303        }
1304
1305        #[test]
1306        fn test_tree_single_file_in_cwd() -> Result<()> {
1307            let temp_dir = setup_test_env();
1308            let mut fs = DirFS::new(temp_dir.path())?;
1309
1310            fs.mkfile("/file.txt", Some(b"Content"))?;
1311
1312            let entries: Vec<_> = fs.tree::<&Path>(None)?.collect();
1313            assert_eq!(entries.len(), 1);
1314            assert_eq!(entries[0], DirEntry::new("/file.txt", DirEntryType::File));
1315
1316            Ok(())
1317        }
1318
1319        #[test]
1320        fn test_tree_file_in_subdirectory() -> Result<()> {
1321            let temp_dir = setup_test_env();
1322            let mut fs = DirFS::new(temp_dir.path())?;
1323
1324            fs.mkdir("/docs")?;
1325            fs.mkfile("/docs/readme.txt", Some(b"Docs"))?;
1326
1327            let entries: Vec<_> = fs.tree(Some("/docs"))?.collect();
1328            assert_eq!(entries.len(), 1);
1329            assert_eq!(
1330                entries[0],
1331                DirEntry::new("/docs/readme.txt", DirEntryType::File)
1332            );
1333
1334            Ok(())
1335        }
1336
1337        #[test]
1338        fn test_tree_nested_structure() -> Result<()> {
1339            let temp_dir = setup_test_env();
1340            let mut fs = DirFS::new(temp_dir.path())?;
1341
1342            // Create nested structure
1343            fs.mkdir("/project")?;
1344            fs.mkdir("/project/src")?;
1345            fs.mkdir("/project/tests")?;
1346            fs.mkfile("/project/main.rs", Some(b"fn main() {}"))?;
1347            fs.mkfile("/project/src/lib.rs", Some(b"mod utils;"))?;
1348            fs.mkfile("/project/tests/test.rs", Some(b"#[test] fn it_works() {}"))?;
1349
1350            // Test tree from root
1351            let root_entries: Vec<_> = fs.tree::<&Path>(None)?.collect();
1352            assert_eq!(root_entries.len(), 6); // /project, /project/src, /project/tests, /project/main.rs, /project/src/lib.rs, /project/tests/test.rs
1353
1354            // Test tree from /project
1355            let project_entries: Vec<_> = fs.tree(Some("/project"))?.collect();
1356            assert_eq!(project_entries.len(), 5); // /project/src, /project/tests, /project/main.rs, /project/src/lib.rs, /project/tests/test.rs
1357
1358            Ok(())
1359        }
1360
1361        #[test]
1362        fn test_tree_nonexistent_path_error() -> Result<()> {
1363            let temp_dir = setup_test_env();
1364            let fs = DirFS::new(temp_dir.path())?;
1365
1366            let result: Result<Vec<_>> = fs.tree(Some("/nonexistent")).map(|iter| iter.collect());
1367            assert!(result.is_err());
1368            assert!(result.unwrap_err().to_string().contains("does not exist"));
1369
1370            Ok(())
1371        }
1372
1373        #[test]
1374        fn test_tree_relative_path() -> Result<()> {
1375            let temp_dir = setup_test_env();
1376            let mut fs = DirFS::new(temp_dir.path())?;
1377
1378            fs.mkdir("/docs")?;
1379            fs.cd("/docs")?;
1380            fs.mkdir("sub")?;
1381            fs.mkfile("sub/file.txt", Some(b"Relative"))?;
1382
1383            let entries: Vec<_> = fs.tree(Some("sub"))?.collect();
1384            assert_eq!(entries.len(), 1);
1385            assert_eq!(
1386                entries[0],
1387                DirEntry::new("/docs/sub/file.txt", DirEntryType::File)
1388            );
1389
1390            Ok(())
1391        }
1392
1393        #[test]
1394        fn test_tree_unicode_paths() -> Result<()> {
1395            let temp_dir = setup_test_env();
1396            let mut fs = DirFS::new(temp_dir.path())?;
1397
1398            fs.mkdir("/проект")?;
1399            fs.mkfile("/проект/документ.txt", Some(b"Unicode"))?;
1400            fs.mkdir("/проект/подпапка")?;
1401            fs.mkfile("/проект/подпапка/файл.txt", Some(b"Nested unicode"))?;
1402
1403            let entries: Vec<_> = fs.tree(Some("/проект"))?.collect();
1404
1405            assert_eq!(entries.len(), 3);
1406            assert!(entries.contains(&DirEntry::new("/проект/документ.txt", DirEntryType::File)));
1407            assert!(entries.contains(&DirEntry::new("/проект/подпапка", DirEntryType::Directory)));
1408            assert!(entries.contains(&DirEntry::new(
1409                "/проект/подпапка/файл.txt",
1410                DirEntryType::File
1411            )));
1412
1413            Ok(())
1414        }
1415
1416        #[test]
1417        fn test_tree_no_root_inclusion() -> Result<()> {
1418            let temp_dir = setup_test_env();
1419            let mut fs = DirFS::new(temp_dir.path())?;
1420
1421            fs.mkdir("/parent")?;
1422            fs.mkfile("/parent/child.txt", Some(b"Child"))?;
1423
1424            let entries: Vec<_> = fs.tree(Some("/parent"))?.collect();
1425
1426            // Should not include /parent itself, only its contents
1427            assert!(
1428                !entries
1429                    .iter()
1430                    .any(|p| p == &DirEntry::new("/parent", DirEntryType::Directory))
1431            );
1432            assert!(
1433                entries
1434                    .iter()
1435                    .any(|p| p == &DirEntry::new("/parent/child.txt", DirEntryType::File))
1436            );
1437
1438            Ok(())
1439        }
1440
1441        #[test]
1442        fn test_tree_order_independence() -> Result<()> {
1443            let temp_dir = setup_test_env();
1444            let mut fs = DirFS::new(temp_dir.path())?;
1445
1446            fs.mkdir("/order_test")?;
1447            fs.mkfile("/order_test/a.txt", None)?;
1448            fs.mkfile("/order_test/b.txt", None)?;
1449            fs.mkfile("/order_test/c.txt", None)?;
1450
1451            let entries: Vec<_> = fs.tree(Some("/order_test"))?.collect();
1452
1453            assert_eq!(entries.len(), 3);
1454
1455            Ok(())
1456        }
1457    }
1458
1459    mod mkdir_all {
1460        use super::*;
1461        use std::fs;
1462        use std::path::PathBuf;
1463
1464        #[test]
1465        fn test_mkdir_all_simple_creation() {
1466            let temp_dir = setup_test_env();
1467            let target = temp_dir.path().join("a/b/c");
1468
1469            let created = DirFS::mkdir_all(&target).unwrap();
1470
1471            assert_eq!(created.len(), 3);
1472            assert!(created.contains(&temp_dir.path().join("a")));
1473            assert!(created.contains(&temp_dir.path().join("a/b")));
1474            assert!(created.contains(&temp_dir.path().join("a/b/c")));
1475
1476            // Проверяем, что каталоги реально созданы
1477            assert!(temp_dir.path().join("a").is_dir());
1478            assert!(temp_dir.path().join("a/b").is_dir());
1479            assert!(temp_dir.path().join("a/b/c").is_dir());
1480        }
1481
1482        #[test]
1483        fn test_mkdir_all_existing_parent() {
1484            let temp_dir = setup_test_env();
1485            fs::create_dir_all(temp_dir.path().join("a")).unwrap(); // It already exists
1486
1487            let target = temp_dir.path().join("a/b/c");
1488            let created = DirFS::mkdir_all(&target).unwrap();
1489
1490            assert_eq!(created.len(), 2); // Только b и c
1491            assert!(created.contains(&temp_dir.path().join("a/b")));
1492            assert!(created.contains(&temp_dir.path().join("a/b/c")));
1493        }
1494
1495        #[test]
1496        fn test_mkdir_all_target_exists() {
1497            let temp_dir = setup_test_env();
1498            fs::create_dir_all(temp_dir.path().join("x/y")).unwrap();
1499
1500            let target = temp_dir.path().join("x/y");
1501            let created = DirFS::mkdir_all(&target).unwrap();
1502
1503            assert!(created.is_empty()); // Nothing was created
1504        }
1505
1506        #[test]
1507        fn test_mkdir_all_root_path() {
1508            // FS root (usually "/")
1509            let result = DirFS::mkdir_all("/");
1510            assert!(result.is_ok());
1511            assert!(result.unwrap().is_empty());
1512        }
1513
1514        #[test]
1515        fn test_mkdir_all_single_dir() {
1516            let temp_dir = setup_test_env();
1517            let target = temp_dir.path().join("single");
1518
1519            let created = DirFS::mkdir_all(&target).unwrap();
1520
1521            assert_eq!(created.len(), 1);
1522            assert!(created.contains(&target));
1523            assert!(target.is_dir());
1524        }
1525
1526        #[test]
1527        fn test_mkdir_all_absolute_vs_relative() {
1528            let temp_dir = setup_test_env();
1529
1530            // The absolute path
1531            let abs_target = temp_dir.path().join("abs/a/b");
1532            let abs_created = DirFS::mkdir_all(&abs_target).unwrap();
1533
1534            assert!(!abs_created.is_empty());
1535        }
1536
1537        #[test]
1538        fn test_mkdir_all_nested_existing() {
1539            let temp_dir = setup_test_env();
1540            fs::create_dir_all(temp_dir.path().join("deep/a")).unwrap();
1541
1542            let target = temp_dir.path().join("deep/a/b/c/d");
1543            let created = DirFS::mkdir_all(&target).unwrap();
1544
1545            assert_eq!(created.len(), 3); // b, c, d
1546        }
1547
1548        #[test]
1549        fn test_mkdir_all_invalid_path() {
1550            // Attempt to create in a non-existent location (without rights)
1551            #[cfg(unix)]
1552            {
1553                let invalid_path = PathBuf::from("/nonexistent/parent/child");
1554
1555                // Expecting an error (e.g. PermissionDenied or NoSuchFile)
1556                let result = DirFS::mkdir_all(&invalid_path);
1557                assert!(result.is_err());
1558            }
1559        }
1560
1561        #[test]
1562        fn test_mkdir_all_file_in_path() {
1563            let temp_dir = setup_test_env();
1564            let file_path = temp_dir.path().join("file.txt");
1565            fs::write(&file_path, "content").unwrap(); // Create a file
1566
1567            let target = file_path.join("subdir"); // Trying to create inside the file
1568
1569            let result = DirFS::mkdir_all(&target);
1570            assert!(result.is_err()); // Must be an error
1571        }
1572
1573        #[test]
1574        fn test_mkdir_all_trailing_slash() {
1575            let temp_dir = setup_test_env();
1576            let target = temp_dir.path().join("trailing/");
1577
1578            let created = DirFS::mkdir_all(&target).unwrap();
1579            assert!(!created.is_empty());
1580            assert!(temp_dir.path().join("trailing").is_dir());
1581        }
1582
1583        #[test]
1584        fn test_mkdir_all_unicode_paths() {
1585            let temp_dir = setup_test_env();
1586            let target = temp_dir.path().join("папка/файл");
1587
1588            let created = DirFS::mkdir_all(&target).unwrap();
1589
1590            assert_eq!(created.len(), 2);
1591            assert!(temp_dir.path().join("папка").is_dir());
1592            assert!(temp_dir.path().join("папка/файл").is_dir());
1593        }
1594
1595        #[test]
1596        fn test_mkdir_all_permissions_error() {
1597            // This test requires a specific environment (e.g. readonly FS).
1598            // Skip it in general tests, but leave it for manual launch.
1599            #[cfg(unix)]
1600            {
1601                use std::os::unix::fs::PermissionsExt;
1602
1603                let temp_dir = setup_test_env();
1604                fs::set_permissions(&temp_dir, PermissionsExt::from_mode(0o444)).unwrap(); // readonly
1605
1606                let target = temp_dir.path().join("protected/dir");
1607                let result = DirFS::mkdir_all(&target);
1608
1609                assert!(result.is_err());
1610            }
1611        }
1612    }
1613
1614    mod drop {
1615        use super::*;
1616
1617        #[test]
1618        fn test_drop_removes_created_directories() {
1619            let temp_dir = setup_test_env();
1620            let root = temp_dir.path().join("to_remove");
1621
1622            // Create DirFs, which will create new directories.
1623            let fs = DirFS::new(&root).unwrap();
1624            assert!(root.exists());
1625
1626            // Destroy fs (Drop should work)
1627            drop(fs);
1628
1629            // Check that the root has been removed.
1630            assert!(!root.exists());
1631        }
1632
1633        #[test]
1634        fn test_drop_only_removes_created_parents() {
1635            let temp_dir = setup_test_env();
1636            let parent = temp_dir.path().join("parent");
1637            let child = parent.join("child");
1638
1639            std::fs::create_dir_all(&parent).unwrap(); // The parent already exists
1640            let fs = DirFS::new(&child).unwrap();
1641
1642            assert!(parent.exists()); // The parent must remain.
1643            assert!(child.exists());
1644
1645            drop(fs);
1646
1647            assert!(parent.exists()); // The parent is not deleted
1648            assert!(!child.exists()); // The child has been removed
1649        }
1650
1651        #[test]
1652        fn test_drop_with_is_auto_clean_false() {
1653            let temp_dir = setup_test_env();
1654            let root = temp_dir.path().join("keep");
1655
1656            let mut fs = DirFS::new(&root).unwrap();
1657            fs.is_auto_clean = false; // Disable auto-cleaning
1658
1659            drop(fs);
1660
1661            assert!(root.exists()); // The catalog must remain
1662        }
1663
1664        #[test]
1665        fn test_drop_empty_created_root_parents() {
1666            let temp_dir = setup_test_env();
1667            let existing = temp_dir.path().join("existing");
1668            std::fs::create_dir(&existing).unwrap();
1669
1670            let fs = DirFS::new(&existing).unwrap(); // Already exists → created_root_parents is empty
1671
1672            drop(fs);
1673
1674            assert!(existing.exists()); // It should remain (we didn't create it)
1675        }
1676
1677        #[test]
1678        fn test_drop_nested_directories_removed() {
1679            let temp_dir = setup_test_env();
1680            let nested = temp_dir.path().join("a/b/c");
1681
1682            let fs = DirFS::new(&nested).unwrap();
1683            assert!(nested.exists());
1684
1685            drop(fs);
1686
1687            // Все уровни должны быть удалены
1688            assert!(!temp_dir.path().join("a").exists());
1689            assert!(!temp_dir.path().join("a/b").exists());
1690            assert!(!nested.exists());
1691        }
1692
1693        //-----------------------------
1694
1695        #[test]
1696        fn test_drop_removes_entries_created_by_mkdir() {
1697            let temp_dir = setup_test_env();
1698            let root = temp_dir.path().join("test_root");
1699
1700            let mut fs = DirFS::new(&root).unwrap();
1701            fs.mkdir("/subdir").unwrap();
1702            assert!(root.join("subdir").exists());
1703
1704            drop(fs);
1705
1706            assert!(!root.exists()); // Корень удалён
1707            assert!(!root.join("subdir").exists()); // The subdirectory has also been deleted.
1708        }
1709
1710        #[test]
1711        fn test_drop_removes_entries_created_by_mkfile() {
1712            let temp_dir = setup_test_env();
1713            let root = temp_dir.path().join("test_root");
1714
1715            let mut fs = DirFS::new(&root).unwrap();
1716            fs.mkfile("/file.txt", None).unwrap();
1717            assert!(root.join("file.txt").exists());
1718
1719            drop(fs);
1720
1721            assert!(!root.exists());
1722            assert!(!root.join("file.txt").exists());
1723        }
1724
1725        #[test]
1726        fn test_drop_handles_nested_entries() {
1727            let temp_dir = setup_test_env();
1728            let root = temp_dir.path().join("test_root");
1729
1730            let mut fs = DirFS::new(&root).unwrap();
1731            fs.mkdir("/a/b/c").unwrap();
1732            fs.mkfile("/a/file.txt", None).unwrap();
1733
1734            assert!(root.join("a/b/c").exists());
1735            assert!(root.join("a/file.txt").exists());
1736
1737            drop(fs);
1738
1739            assert!(!root.exists());
1740        }
1741
1742        #[test]
1743        fn test_drop_ignores_non_entries() {
1744            let temp_dir = setup_test_env();
1745            let root = temp_dir.path().join("test_root");
1746            let external = temp_dir.path().join("external_file.txt");
1747
1748            std::fs::write(&external, "content").unwrap(); // File outside VFS
1749
1750            let fs = DirFS::new(&root).unwrap();
1751            drop(fs);
1752
1753            assert!(!root.exists());
1754            assert!(external.exists()); // The external file remains
1755        }
1756
1757        #[test]
1758        fn test_drop_with_empty_entries() {
1759            let temp_dir = setup_test_env();
1760            let root = temp_dir.path().join("empty_root");
1761
1762            let fs = DirFS::new(&root).unwrap();
1763            // entries contains only "/" (root)
1764
1765            drop(fs);
1766
1767            assert!(!root.exists());
1768        }
1769    }
1770
1771    mod mkfile {
1772        use super::*;
1773
1774        #[test]
1775        fn test_mkfile_simple_creation() {
1776            let temp_dir = setup_test_env();
1777            let root = temp_dir.path();
1778
1779            let mut fs = DirFS::new(root).unwrap();
1780            fs.mkfile("/file.txt", None).unwrap();
1781
1782            assert!(fs.exists("/file.txt"));
1783            assert!(root.join("file.txt").exists());
1784            assert_eq!(fs.entries.contains_key(&PathBuf::from("/file.txt")), true);
1785        }
1786
1787        #[test]
1788        fn test_mkfile_with_content() {
1789            let temp_dir = setup_test_env();
1790            let root = temp_dir.path();
1791
1792            let mut fs = DirFS::new(root).unwrap();
1793            let content = b"Hello, VFS!";
1794            fs.mkfile("/data.bin", Some(content)).unwrap();
1795
1796            assert!(fs.exists("/data.bin"));
1797            let file_content = std::fs::read(root.join("data.bin")).unwrap();
1798            assert_eq!(&file_content, content);
1799        }
1800
1801        #[test]
1802        fn test_mkfile_in_subdirectory() {
1803            let temp_dir = setup_test_env();
1804            let root = temp_dir.path();
1805
1806            let mut fs = DirFS::new(root).unwrap();
1807            fs.mkdir("/subdir").unwrap();
1808            fs.mkfile("/subdir/file.txt", None).unwrap();
1809
1810            assert!(fs.exists("/subdir/file.txt"));
1811            assert!(root.join("subdir/file.txt").exists());
1812        }
1813
1814        #[test]
1815        fn test_mkfile_parent_does_not_exist() {
1816            let temp_dir = setup_test_env();
1817            let root = temp_dir.path();
1818
1819            let mut fs = DirFS::new(root).unwrap();
1820
1821            let result = fs.mkfile("/nonexistent/file.txt", None);
1822            assert!(result.is_err());
1823        }
1824
1825        #[test]
1826        fn test_mkfile_file_already_exists() {
1827            let temp_dir = setup_test_env();
1828            let root = temp_dir.path();
1829
1830            let mut fs = DirFS::new(root).unwrap();
1831            fs.mkfile("/existing.txt", None).unwrap();
1832
1833            // Trying to create the same file again
1834            let result = fs.mkfile("/existing.txt", None);
1835            assert!(result.is_ok()); // Should overwrite (File::create truncates the file)
1836            assert!(fs.exists("/existing.txt"));
1837        }
1838
1839        #[test]
1840        fn test_mkfile_empty_content() {
1841            let temp_dir = setup_test_env();
1842            let root = temp_dir.path();
1843
1844            let mut fs = DirFS::new(root).unwrap();
1845            fs.mkfile("/empty.txt", Some(&[])).unwrap(); // An empty array
1846
1847            assert!(fs.exists("/empty.txt"));
1848            let file_size = std::fs::metadata(root.join("empty.txt")).unwrap().len();
1849            assert_eq!(file_size, 0);
1850        }
1851
1852        #[test]
1853        fn test_mkfile_relative_path() {
1854            let temp_dir = setup_test_env();
1855            let root = temp_dir.path();
1856
1857            let mut fs = DirFS::new(root).unwrap();
1858            fs.mkdir("/sub").unwrap();
1859            fs.cd("/sub").unwrap(); // Changes the current directory
1860
1861            fs.mkfile("relative.txt", None).unwrap(); // A relative path
1862
1863            assert!(fs.exists("/sub/relative.txt"));
1864            assert!(root.join("sub/relative.txt").exists());
1865        }
1866
1867        #[test]
1868        fn test_mkfile_normalize_path() {
1869            let temp_dir = setup_test_env();
1870            let root = temp_dir.path();
1871
1872            let mut fs = DirFS::new(root).unwrap();
1873            fs.mkdir("/normalized").unwrap();
1874
1875            fs.mkfile("/./normalized/../normalized/file.txt", None)
1876                .unwrap();
1877
1878            assert!(fs.exists("/normalized/file.txt"));
1879            assert!(root.join("normalized/file.txt").exists());
1880        }
1881
1882        #[test]
1883        fn test_mkfile_invalid_path_components() {
1884            let temp_dir = setup_test_env();
1885            let root = temp_dir.path();
1886
1887            let mut fs = DirFS::new(root).unwrap();
1888
1889            // Attempt to create a file with an invalid name (depending on the file system)
1890            #[cfg(unix)]
1891            {
1892                let result = fs.mkfile("/invalid\0name.txt", None);
1893                assert!(result.is_err()); // NUL in filenames is prohibited in Unix.
1894            }
1895        }
1896
1897        #[test]
1898        fn test_mkfile_permission_denied() {
1899            #[cfg(unix)]
1900            {
1901                use std::os::unix::fs::PermissionsExt;
1902
1903                let temp_dir = setup_test_env();
1904                let root = temp_dir.path();
1905                let protected = root.join("protected");
1906                std::fs::create_dir(&protected).unwrap();
1907                std::fs::set_permissions(&protected, PermissionsExt::from_mode(0o000)).unwrap(); // No access
1908
1909                let mut fs = DirFS::new(root).unwrap();
1910                let result = fs.mkfile("/protected/file.txt", None);
1911
1912                std::fs::set_permissions(&protected, PermissionsExt::from_mode(0o755)).unwrap(); // Grant access
1913
1914                assert!(result.is_err());
1915                assert!(
1916                    result
1917                        .unwrap_err()
1918                        .to_string()
1919                        .contains("Permission denied")
1920                );
1921            }
1922        }
1923
1924        #[test]
1925        fn test_mkfile_root_directory() {
1926            let temp_dir = setup_test_env();
1927            let root = temp_dir.path();
1928
1929            let mut fs = DirFS::new(root).unwrap();
1930
1931            // Cannot create a file named "/" (it is a directory)
1932            let result = fs.mkfile("/", None);
1933            assert!(result.is_err());
1934        }
1935
1936        #[test]
1937        fn test_mkfile_unicode_filename() {
1938            let temp_dir = setup_test_env();
1939            let root = temp_dir.path();
1940
1941            let mut fs = DirFS::new(root).unwrap();
1942            fs.mkfile("/тест.txt", Some(b"Content")).unwrap();
1943
1944            assert!(fs.exists("/тест.txt"));
1945            assert!(root.join("тест.txt").exists());
1946            let content = std::fs::read_to_string(root.join("тест.txt")).unwrap();
1947            assert_eq!(content, "Content");
1948        }
1949    }
1950
1951    mod read {
1952        use super::*;
1953
1954        #[test]
1955        fn test_read_existing_file() -> Result<()> {
1956            let temp_dir = setup_test_env();
1957            let mut fs = DirFS::new(&temp_dir)?;
1958
1959            // Create and write a file
1960            fs.mkfile("/test.txt", Some(b"Hello, VFS!"))?;
1961
1962            // Read it back
1963            let content = fs.read("/test.txt")?;
1964            assert_eq!(content, b"Hello, VFS!");
1965
1966            Ok(())
1967        }
1968
1969        #[test]
1970        fn test_read_nonexistent_file() -> Result<()> {
1971            let temp_dir = setup_test_env();
1972            let fs = DirFS::new(temp_dir.path())?;
1973
1974            let result = fs.read("/not/found.txt");
1975            assert!(result.is_err());
1976            assert!(
1977                result
1978                    .unwrap_err()
1979                    .to_string()
1980                    .contains("file does not exist: /not/found.txt")
1981            );
1982
1983            Ok(())
1984        }
1985
1986        #[test]
1987        fn test_read_directory_as_file() -> Result<()> {
1988            let temp_dir = setup_test_env();
1989            let mut fs = DirFS::new(temp_dir.path())?;
1990
1991            fs.mkdir("/empty_dir")?;
1992
1993            let result = fs.read("/empty_dir");
1994            assert!(result.is_err());
1995            // Note: error comes from std::fs::File::open (not a file), not our exists check
1996            assert!(result.unwrap_err().to_string().contains("is a directory"));
1997
1998            Ok(())
1999        }
2000
2001        #[test]
2002        fn test_read_empty_file() -> Result<()> {
2003            let temp_dir = setup_test_env();
2004            let mut fs = DirFS::new(temp_dir.path())?;
2005
2006            fs.mkfile("/empty.txt", None)?; // Create empty file
2007
2008            let content = fs.read("/empty.txt")?;
2009            assert_eq!(content.len(), 0);
2010
2011            Ok(())
2012        }
2013
2014        #[test]
2015        fn test_read_relative_path() -> Result<()> {
2016            let temp_dir = setup_test_env();
2017            let mut fs = DirFS::new(temp_dir.path())?;
2018
2019            fs.cd("/")?;
2020            fs.mkdir("/parent")?;
2021            fs.cd("/parent")?;
2022            fs.mkfile("child.txt", Some(b"Content"))?;
2023
2024            // Read using relative path from cwd
2025            let content = fs.read("child.txt")?;
2026            assert_eq!(content, b"Content");
2027
2028            Ok(())
2029        }
2030
2031        #[test]
2032        fn test_read_unicode_path() -> Result<()> {
2033            let temp_dir = setup_test_env();
2034            let mut fs = DirFS::new(temp_dir.path())?;
2035
2036            fs.mkdir("/папка")?;
2037            fs.mkfile("/папка/файл.txt", Some(b"Unicode content"))?;
2038
2039            let content = fs.read("/папка/файл.txt")?;
2040            assert_eq!(content, b"Unicode content");
2041
2042            Ok(())
2043        }
2044
2045        #[test]
2046        fn test_read_permission_denied() -> Result<()> {
2047            #[cfg(unix)]
2048            {
2049                use std::os::unix::fs::PermissionsExt;
2050
2051                let temp_dir = setup_test_env();
2052                let mut fs = DirFS::new(temp_dir.path())?;
2053
2054                // Create file and restrict permissions
2055                fs.mkfile("/protected.txt", Some(b"Secret"))?;
2056                let host_path = temp_dir.path().join("protected.txt");
2057                std::fs::set_permissions(&host_path, PermissionsExt::from_mode(0o000))?;
2058
2059                // Try to read (should fail due to permissions)
2060                let result = fs.read("/protected.txt");
2061                assert!(result.is_err());
2062                assert!(
2063                    result
2064                        .unwrap_err()
2065                        .to_string()
2066                        .contains("Permission denied")
2067                );
2068
2069                // Clean up: restore permissions
2070                std::fs::set_permissions(&host_path, PermissionsExt::from_mode(0o644))?;
2071            }
2072            Ok(())
2073        }
2074
2075        #[test]
2076        fn test_read_root_file() -> Result<()> {
2077            let temp_dir = setup_test_env();
2078            let mut fs = DirFS::new(temp_dir.path())?;
2079
2080            fs.mkfile("/root_file.txt", Some(b"At root"))?;
2081            let content = fs.read("/root_file.txt")?;
2082            assert_eq!(content, b"At root");
2083
2084            Ok(())
2085        }
2086    }
2087
2088    mod write {
2089        use super::*;
2090
2091        #[test]
2092        fn test_write_new_file() -> Result<()> {
2093            let temp_dir = setup_test_env();
2094            let mut fs = DirFS::new(temp_dir.path())?;
2095
2096            fs.mkfile("/new.txt", None)?;
2097            let content = b"Hello, VFS!";
2098            fs.write("/new.txt", content)?;
2099
2100            // Check file exists and has correct content
2101            assert!(fs.exists("/new.txt"));
2102            let read_back = fs.read("/new.txt")?;
2103            assert_eq!(read_back, content);
2104
2105            Ok(())
2106        }
2107
2108        #[test]
2109        fn test_write_existing_file_overwrite() -> Result<()> {
2110            let temp_dir = setup_test_env();
2111            let mut fs = DirFS::new(temp_dir.path())?;
2112
2113            fs.mkfile("/exist.txt", Some(b"Old content"))?;
2114
2115            let new_content = b"New content";
2116            fs.write("/exist.txt", new_content)?;
2117
2118            let read_back = fs.read("/exist.txt")?;
2119            assert_eq!(read_back, new_content);
2120
2121            Ok(())
2122        }
2123
2124        #[test]
2125        fn test_write_to_directory_path() -> Result<()> {
2126            let temp_dir = setup_test_env();
2127            let mut fs = DirFS::new(temp_dir.path())?;
2128
2129            fs.mkdir("/dir")?;
2130
2131            let result = fs.write("/dir", b"Content");
2132            assert!(result.is_err());
2133            assert!(result.unwrap_err().to_string().contains("is a directory"));
2134
2135            Ok(())
2136        }
2137
2138        #[test]
2139        fn test_write_to_nonexistent_file() -> Result<()> {
2140            let temp_dir = setup_test_env();
2141            let fs = DirFS::new(temp_dir.path())?;
2142
2143            let result = fs.write("/parent/child.txt", b"Content");
2144            assert!(result.is_err());
2145            assert!(
2146                result
2147                    .unwrap_err()
2148                    .to_string()
2149                    .contains("file does not exist")
2150            );
2151
2152            Ok(())
2153        }
2154
2155        #[test]
2156        fn test_write_empty_content() -> Result<()> {
2157            let temp_dir = setup_test_env();
2158            let mut fs = DirFS::new(temp_dir.path())?;
2159
2160            fs.mkfile("/empty.txt", None)?;
2161            fs.write("/empty.txt", &[])?;
2162
2163            let read_back = fs.read("/empty.txt")?;
2164            assert!(read_back.is_empty());
2165
2166            Ok(())
2167        }
2168
2169        #[test]
2170        fn test_write_relative_path() -> Result<()> {
2171            let temp_dir = setup_test_env();
2172            let mut fs = DirFS::new(temp_dir.path())?;
2173
2174            fs.mkdir("/docs")?;
2175            fs.cd("docs")?;
2176
2177            fs.mkfile("file.txt", None)?;
2178            let content = b"Relative write";
2179            fs.write("file.txt", content)?;
2180
2181            let read_back = fs.read("/docs/file.txt")?;
2182            assert_eq!(read_back, content);
2183
2184            Ok(())
2185        }
2186    }
2187
2188    mod append {
2189        use super::*;
2190
2191        #[test]
2192        fn test_append_to_existing_file() -> Result<()> {
2193            let temp_dir = setup_test_env();
2194            let mut fs = DirFS::new(temp_dir.path())?;
2195
2196            // Create initial file
2197            fs.mkfile("/log.txt", Some(b"Initial content\n"))?;
2198
2199            // Append new content
2200            fs.append("/log.txt", b"Appended line 1\n")?;
2201            fs.append("/log.txt", b"Appended line 2\n")?;
2202
2203            // Verify full content
2204            let content = fs.read("/log.txt")?;
2205            assert_eq!(
2206                content,
2207                b"Initial content\nAppended line 1\nAppended line 2\n"
2208            );
2209
2210            Ok(())
2211        }
2212
2213        #[test]
2214        fn test_append_to_empty_file() -> Result<()> {
2215            let temp_dir = setup_test_env();
2216            let mut fs = DirFS::new(temp_dir.path())?;
2217
2218            // Create empty file
2219            fs.mkfile("/empty.txt", Some(&[]))?;
2220
2221            // Append content
2222            fs.append("/empty.txt", b"First append\n")?;
2223            fs.append("/empty.txt", b"Second append\n")?;
2224
2225            let content = fs.read("/empty.txt")?;
2226            assert_eq!(content, b"First append\nSecond append\n");
2227
2228            Ok(())
2229        }
2230
2231        #[test]
2232        fn test_append_nonexistent_file() -> Result<()> {
2233            let temp_dir = setup_test_env();
2234            let fs = DirFS::new(temp_dir.path())?;
2235
2236            let result = fs.append("/not_found.txt", b"Content");
2237            assert!(result.is_err());
2238            assert!(
2239                result
2240                    .unwrap_err()
2241                    .to_string()
2242                    .contains("file does not exist: /not_found.txt")
2243            );
2244
2245            Ok(())
2246        }
2247
2248        #[test]
2249        fn test_append_to_directory() -> Result<()> {
2250            let temp_dir = setup_test_env();
2251            let mut fs = DirFS::new(temp_dir.path())?;
2252
2253            fs.mkdir("/mydir")?;
2254
2255            let result = fs.append("/mydir", b"Content");
2256            assert!(result.is_err());
2257            assert!(result.unwrap_err().to_string().contains("is a directory"));
2258
2259            Ok(())
2260        }
2261
2262        #[test]
2263        fn test_append_empty_content() -> Result<()> {
2264            let temp_dir = setup_test_env();
2265            let mut fs = DirFS::new(temp_dir.path())?;
2266
2267            fs.mkfile("/test.txt", Some(b"Existing\n"))?;
2268
2269            // Append empty slice
2270            fs.append("/test.txt", &[])?;
2271
2272            // Content should remain unchanged
2273            let content = fs.read("/test.txt")?;
2274            assert_eq!(content, b"Existing\n");
2275
2276            Ok(())
2277        }
2278
2279        #[test]
2280        fn test_append_relative_path() -> Result<()> {
2281            let temp_dir = setup_test_env();
2282            let mut fs = DirFS::new(temp_dir.path())?;
2283
2284            fs.mkdir("/docs")?;
2285            fs.cd("/docs")?;
2286            fs.mkfile("log.txt", Some(b"Start\n"))?; // Relative path
2287
2288            fs.append("log.txt", b"Added\n")?;
2289
2290            let content = fs.read("/docs/log.txt")?;
2291            assert_eq!(content, b"Start\nAdded\n");
2292
2293            Ok(())
2294        }
2295
2296        #[test]
2297        fn test_append_unicode_path() -> Result<()> {
2298            let temp_dir = setup_test_env();
2299            let mut fs = DirFS::new(temp_dir.path())?;
2300
2301            let first = Vec::from("Начало\n");
2302            let second = Vec::from("Продолжение\n");
2303
2304            fs.mkdir("/папка")?;
2305            fs.mkfile("/папка/файл.txt", Some(first.as_slice()))?;
2306            fs.append("/папка/файл.txt", second.as_slice())?;
2307
2308            let content = fs.read("/папка/файл.txt")?;
2309
2310            let mut expected = Vec::from(first);
2311            expected.extend(second);
2312
2313            assert_eq!(content, expected);
2314
2315            Ok(())
2316        }
2317
2318        #[test]
2319        fn test_concurrent_append_safety() -> Result<()> {
2320            let temp_dir = setup_test_env();
2321            let mut fs = DirFS::new(temp_dir.path())?;
2322
2323            fs.mkfile("/concurrent.txt", Some(b""))?;
2324
2325            // Simulate multiple appends
2326            for i in 1..=3 {
2327                fs.append("/concurrent.txt", format!("Line {}\n", i).as_bytes())?;
2328            }
2329
2330            let content = fs.read("/concurrent.txt")?;
2331            assert_eq!(content, b"Line 1\nLine 2\nLine 3\n");
2332
2333            Ok(())
2334        }
2335
2336        #[test]
2337        fn test_append_permission_denied() -> Result<()> {
2338            #[cfg(unix)]
2339            {
2340                use std::os::unix::fs::PermissionsExt;
2341
2342                let temp_dir = setup_test_env();
2343                let mut fs = DirFS::new(temp_dir.path())?;
2344
2345                // Create file and restrict permissions
2346                fs.mkfile("/protected.txt", Some(b"Content"))?;
2347                let host_path = temp_dir.path().join("protected.txt");
2348                std::fs::set_permissions(&host_path, PermissionsExt::from_mode(0o000))?;
2349
2350                // Try to append (should fail)
2351                let result = fs.append("/protected.txt", b"New content");
2352                assert!(result.is_err());
2353                assert!(
2354                    result
2355                        .unwrap_err()
2356                        .to_string()
2357                        .contains("Permission denied")
2358                );
2359
2360                // Clean up: restore permissions
2361                std::fs::set_permissions(&host_path, PermissionsExt::from_mode(0o644))?;
2362            }
2363            Ok(())
2364        }
2365    }
2366
2367    mod add {
2368        use super::*;
2369
2370        #[test]
2371        fn test_add_existing_file() -> Result<()> {
2372            let temp_dir = setup_test_env();
2373            let mut fs = DirFS::new(temp_dir.path())?;
2374
2375            // Create a file outside VFS that we'll add
2376            let host_file = temp_dir.path().join("external.txt");
2377            std::fs::write(&host_file, b"Content from host")?;
2378
2379            // Add it to VFS
2380            fs.add("external.txt")?;
2381
2382            // Verify it's now tracked by VFS
2383            assert!(fs.exists("/external.txt"));
2384            let content = fs.read("/external.txt")?;
2385            assert_eq!(content, b"Content from host");
2386
2387            Ok(())
2388        }
2389
2390        #[test]
2391        fn test_add_existing_directory() -> Result<()> {
2392            let temp_dir = setup_test_env();
2393            let mut fs = DirFS::new(temp_dir.path())?;
2394
2395            // Create directory outside VFS
2396            let host_dir = temp_dir.path().join("external_dir");
2397            std::fs::create_dir_all(&host_dir)?;
2398
2399            // Add directory to VFS
2400            fs.add("external_dir")?;
2401
2402            // Verify directory and its contents are accessible
2403            assert!(fs.exists("/external_dir"));
2404
2405            Ok(())
2406        }
2407
2408        #[test]
2409        fn test_add_nonexistent_path() -> Result<()> {
2410            let temp_dir = setup_test_env();
2411            let mut fs = DirFS::new(temp_dir.path())?;
2412
2413            let result = fs.add("/nonexistent.txt");
2414            assert!(result.is_err());
2415            assert!(
2416                result
2417                    .unwrap_err()
2418                    .to_string()
2419                    .contains("No such file or directory")
2420            );
2421
2422            Ok(())
2423        }
2424
2425        #[test]
2426        fn test_add_relative_path() -> Result<()> {
2427            let temp_dir = setup_test_env();
2428            let mut fs = DirFS::new(temp_dir.path())?;
2429
2430            // Create file in subdirectory
2431            let subdir = temp_dir.path().join("sub");
2432            std::fs::create_dir_all(&subdir)?;
2433            std::fs::write(subdir.join("file.txt"), b"Relative content")?;
2434
2435            fs.add("/sub")?;
2436            fs.cd("/sub")?;
2437
2438            // Change cwd and add using relative path
2439            fs.add("file.txt")?;
2440
2441            assert!(fs.exists("/sub/file.txt"));
2442            let content = fs.read("/sub/file.txt")?;
2443            assert_eq!(content, b"Relative content");
2444
2445            Ok(())
2446        }
2447
2448        #[test]
2449        fn test_add_already_tracked_path() -> Result<()> {
2450            let temp_dir = setup_test_env();
2451            let mut fs = DirFS::new(temp_dir.path())?;
2452
2453            // First add a file
2454            let host_file = temp_dir.path().join("duplicate.txt");
2455            std::fs::write(&host_file, b"Original")?;
2456            fs.add("duplicate.txt")?;
2457
2458            // Then try to add it again
2459            let result = fs.add("duplicate.txt");
2460            // Should succeed (no harm in re-adding)
2461            assert!(result.is_ok());
2462
2463            // Content should remain unchanged
2464            let content = fs.read("/duplicate.txt")?;
2465            assert_eq!(content, b"Original");
2466
2467            Ok(())
2468        }
2469
2470        #[test]
2471        fn test_add_unicode_path() -> Result<()> {
2472            let temp_dir = setup_test_env();
2473            let mut fs = DirFS::new(temp_dir.path())?;
2474
2475            // Create file with Unicode name
2476            let unicode_file = temp_dir.path().join("файл.txt");
2477            std::fs::write(&unicode_file, b"Unicode content")?;
2478
2479            fs.add("файл.txt")?;
2480
2481            assert!(fs.exists("/файл.txt"));
2482            let content = fs.read("/файл.txt")?;
2483            assert_eq!(content, b"Unicode content");
2484
2485            Ok(())
2486        }
2487
2488        #[test]
2489        fn test_add_and_auto_cleanup() -> Result<()> {
2490            let temp_dir = setup_test_env();
2491            let mut fs = DirFS::new(temp_dir.path())?;
2492
2493            // Create and add a file
2494            let host_file = temp_dir.path().join("cleanup.txt");
2495            std::fs::write(&host_file, b"To be cleaned up")?;
2496            fs.add("cleanup.txt")?;
2497
2498            assert!(host_file.exists());
2499
2500            // Drop fs - should auto-cleanup if configured
2501            drop(fs);
2502
2503            // Depending on auto_cleanup setting, file may or may not exist
2504            // This test assumes auto_cleanup=true
2505            assert!(!host_file.exists());
2506
2507            Ok(())
2508        }
2509
2510        #[test]
2511        fn test_add_single_file_no_recursion() -> Result<()> {
2512            let temp_dir = setup_test_env();
2513            let mut fs = DirFS::new(temp_dir.path())?;
2514
2515            let host_file = temp_dir.path().join("file.txt");
2516            std::fs::write(&host_file, b"Content")?;
2517
2518            fs.add("file.txt")?;
2519
2520            assert!(fs.exists("/file.txt"));
2521            assert_eq!(fs.read("/file.txt")?, b"Content");
2522
2523            Ok(())
2524        }
2525
2526        #[test]
2527        fn test_add_empty_directory() -> Result<()> {
2528            let temp_dir = setup_test_env();
2529            let mut fs = DirFS::new(temp_dir.path())?;
2530
2531            let host_dir = temp_dir.path().join("empty_dir");
2532            std::fs::create_dir_all(&host_dir)?;
2533
2534            fs.add("empty_dir")?;
2535
2536            assert!(fs.exists("/empty_dir"));
2537
2538            Ok(())
2539        }
2540
2541        #[test]
2542        fn test_add_directory_with_files() -> Result<()> {
2543            let temp_dir = setup_test_env();
2544            let mut fs = DirFS::new(temp_dir.path())?;
2545
2546            let data_dir = temp_dir.path().join("data");
2547            std::fs::create_dir_all(&data_dir)?;
2548            std::fs::write(data_dir.join("file1.txt"), b"First")?;
2549            std::fs::write(data_dir.join("file2.txt"), b"Second")?;
2550
2551            fs.add("data")?;
2552
2553            assert!(fs.exists("/data"));
2554            assert!(fs.exists("/data/file1.txt"));
2555            assert!(fs.exists("/data/file2.txt"));
2556            assert_eq!(fs.read("/data/file1.txt")?, b"First");
2557            assert_eq!(fs.read("/data/file2.txt")?, b"Second");
2558
2559            Ok(())
2560        }
2561
2562        #[test]
2563        fn test_add_nested_directories() -> Result<()> {
2564            let temp_dir = setup_test_env();
2565            let mut fs = DirFS::new(temp_dir.path())?;
2566
2567            let project = temp_dir.path().join("project");
2568            std::fs::create_dir_all(project.join("src"))?;
2569            std::fs::create_dir_all(project.join("docs"))?;
2570
2571            std::fs::write(project.join("src").join("main.rs"), b"fn main() {}")?;
2572            std::fs::write(project.join("docs").join("README.md"), b"Project docs")?;
2573
2574            std::fs::write(project.join("config.toml"), b"[config]")?;
2575
2576            fs.add("project")?;
2577
2578            assert!(fs.exists("/project"));
2579            assert!(fs.exists("/project/src"));
2580            assert!(fs.exists("/project/docs"));
2581            assert!(fs.exists("/project/src/main.rs"));
2582            assert!(fs.exists("/project/docs/README.md"));
2583            assert!(fs.exists("/project/config.toml"));
2584
2585            assert_eq!(fs.read("/project/src/main.rs")?, b"fn main() {}");
2586            assert_eq!(fs.read("/project/docs/README.md")?, b"Project docs");
2587            assert_eq!(fs.read("/project/config.toml")?, b"[config]");
2588
2589            Ok(())
2590        }
2591    }
2592
2593    mod forget {
2594        use super::*;
2595
2596        #[test]
2597        fn test_forget_existing_file() -> Result<()> {
2598            let temp_dir = setup_test_env();
2599            let mut fs = DirFS::new(temp_dir.path())?;
2600
2601            fs.mkfile("/note.txt", Some(b"Hello"))?;
2602            assert!(fs.exists("/note.txt"));
2603
2604            fs.forget("/note.txt")?;
2605
2606            assert!(!fs.exists("/note.txt"));
2607            assert!(std::fs::exists(fs.root().join("note.txt")).unwrap());
2608
2609            Ok(())
2610        }
2611
2612        #[test]
2613        fn test_forget_existing_directory() -> Result<()> {
2614            let temp_dir = setup_test_env();
2615            let mut fs = DirFS::new(temp_dir.path())?;
2616
2617            fs.mkdir("/temp")?;
2618            assert!(fs.exists("/temp"));
2619
2620            fs.forget("/temp")?;
2621
2622            assert!(!fs.exists("/temp"));
2623            assert!(std::fs::exists(fs.root().join("temp")).unwrap());
2624
2625            Ok(())
2626        }
2627
2628        #[test]
2629        fn test_forget_nested_path() -> Result<()> {
2630            let temp_dir = setup_test_env();
2631            let mut fs = DirFS::new(temp_dir.path())?;
2632
2633            fs.mkdir("/a")?;
2634            fs.mkdir("/a/b")?;
2635            fs.mkfile("/a/b/file.txt", Some(b"Data"))?;
2636
2637            assert!(fs.exists("/a/b/file.txt"));
2638
2639            fs.forget("/a/b")?;
2640
2641            assert!(!fs.exists("/a/b"));
2642            assert!(!fs.exists("/a/b/file.txt"));
2643            assert!(fs.exists("/a"));
2644
2645            Ok(())
2646        }
2647
2648        #[test]
2649        fn test_forget_nonexistent_path() -> Result<()> {
2650            let temp_dir = setup_test_env();
2651            let mut fs = DirFS::new(temp_dir.path())?;
2652
2653            let result = fs.forget("/not/found.txt");
2654            assert!(result.is_err());
2655            assert!(
2656                result
2657                    .unwrap_err()
2658                    .to_string()
2659                    .contains("path is not tracked by VFS")
2660            );
2661
2662            Ok(())
2663        }
2664
2665        #[test]
2666        fn test_forget_relative_path() -> Result<()> {
2667            let temp_dir = setup_test_env();
2668            let mut fs = DirFS::new(temp_dir.path())?;
2669
2670            fs.mkdir("/docs")?;
2671            fs.cd("/docs")?;
2672            fs.mkdir("sub")?;
2673            fs.mkfile("sub/file.txt", Some(b"Content"))?;
2674
2675            assert!(fs.exists("/docs/sub/file.txt"));
2676
2677            fs.forget("sub/file.txt")?;
2678
2679            assert!(!fs.exists("/docs/sub/file.txt"));
2680            assert!(fs.exists("/docs/sub"));
2681
2682            Ok(())
2683        }
2684
2685        #[test]
2686        fn test_forget_root_directory() -> Result<()> {
2687            let temp_dir = setup_test_env();
2688            let mut fs = DirFS::new(temp_dir.path())?;
2689
2690            let result = fs.forget("/");
2691            assert!(result.is_err());
2692            assert!(
2693                result
2694                    .unwrap_err()
2695                    .to_string()
2696                    .contains("cannot forget root directory")
2697            );
2698
2699            assert!(fs.exists("/"));
2700
2701            Ok(())
2702        }
2703
2704        #[test]
2705        fn test_forget_parent_after_child() -> Result<()> {
2706            let temp_dir = setup_test_env();
2707            let mut fs = DirFS::new(temp_dir.path())?;
2708
2709            fs.mkdir("/parent")?;
2710            fs.mkfile("/parent/child.txt", Some(b"Child content"))?;
2711
2712            fs.forget("/parent/child.txt")?;
2713            assert!(!fs.exists("/parent/child.txt"));
2714
2715            fs.forget("/parent")?;
2716            assert!(!fs.exists("/parent"));
2717
2718            Ok(())
2719        }
2720
2721        #[test]
2722        fn test_forget_unicode_path() -> Result<()> {
2723            let temp_dir = setup_test_env();
2724            let mut fs = DirFS::new(temp_dir.path())?;
2725
2726            fs.mkdir("/папка")?;
2727            fs.mkfile("/папка/файл.txt", Some(b"Unicode"))?;
2728            assert!(fs.exists("/папка/файл.txt"));
2729
2730            fs.forget("/папка/файл.txt")?;
2731
2732            assert!(!fs.exists("/папка/файл.txt"));
2733            assert!(fs.exists("/папка"));
2734
2735            Ok(())
2736        }
2737
2738        #[test]
2739        fn test_forget_case_sensitivity_unix() -> Result<()> {
2740            #[cfg(unix)]
2741            {
2742                let temp_dir = setup_test_env();
2743                let mut fs = DirFS::new(temp_dir.path())?;
2744
2745                fs.mkfile("/File.TXT", Some(b"Case test"))?;
2746                assert!(fs.exists("/File.TXT"));
2747
2748                let result = fs.forget("/file.txt");
2749                assert!(result.is_err());
2750                assert!(fs.exists("/File.TXT"));
2751
2752                fs.forget("/File.TXT")?;
2753                assert!(!fs.exists("/File.TXT"));
2754            }
2755            Ok(())
2756        }
2757
2758        #[test]
2759        fn test_forget_after_add_and_remove() -> Result<()> {
2760            let temp_dir = setup_test_env();
2761            let mut fs = DirFS::new(temp_dir.path())?;
2762
2763            let host_file = temp_dir.path().join("external.txt");
2764            std::fs::write(&host_file, b"External")?;
2765
2766            fs.add("external.txt")?;
2767            assert!(fs.exists("/external.txt"));
2768
2769            std::fs::remove_file(&host_file)?;
2770            assert!(!host_file.exists());
2771
2772            fs.forget("external.txt")?;
2773            assert!(!fs.exists("/external.txt"));
2774
2775            Ok(())
2776        }
2777    }
2778
2779    mod rm {
2780        use super::*;
2781
2782        #[test]
2783        fn test_rm_file_success() {
2784            let temp_dir = setup_test_env();
2785            let mut fs = DirFS::new(temp_dir.path()).unwrap();
2786
2787            // Create a file in VFS
2788            fs.mkfile("/test.txt", Some(b"hello")).unwrap();
2789            assert!(fs.exists("/test.txt"));
2790            assert!(temp_dir.path().join("test.txt").exists());
2791
2792            // Remove it
2793            fs.rm("/test.txt").unwrap();
2794
2795            // Verify: VFS and filesystem are updated
2796            assert!(!fs.exists("/test.txt"));
2797            assert!(!temp_dir.path().join("test.txt").exists());
2798        }
2799
2800        #[test]
2801        fn test_rm_directory_recursive() {
2802            let temp_dir = setup_test_env();
2803            let mut fs = DirFS::new(temp_dir.path()).unwrap();
2804
2805            // Create nested structure
2806            fs.mkdir("/a/b/c").unwrap();
2807            fs.mkfile("/a/file1.txt", None).unwrap();
2808            fs.mkfile("/a/b/file2.txt", None).unwrap();
2809
2810            assert!(fs.exists("/a/b/c"));
2811            assert!(fs.exists("/a/file1.txt"));
2812            assert!(fs.exists("/a/b/file2.txt"));
2813
2814            // Remove top-level directory
2815            fs.rm("/a").unwrap();
2816
2817            // Verify everything is gone
2818            assert!(!fs.exists("/a"));
2819            assert!(!fs.exists("/a/b"));
2820            assert!(!fs.exists("/a/b/c"));
2821            assert!(!fs.exists("/a/file1.txt"));
2822            assert!(!fs.exists("/a/b/file2.txt"));
2823
2824            assert!(!temp_dir.path().join("a").exists());
2825        }
2826
2827        #[test]
2828        fn test_rm_nonexistent_path() {
2829            #[cfg(unix)]
2830            {
2831                let temp_dir = setup_test_env();
2832                let mut fs = DirFS::new(temp_dir.path()).unwrap();
2833
2834                let result = fs.rm("/not/found");
2835                assert!(result.is_err());
2836                assert_eq!(result.unwrap_err().to_string(), "/not/found does not exist");
2837            }
2838        }
2839
2840        #[test]
2841        fn test_rm_relative_path() {
2842            let temp_dir = setup_test_env();
2843            let mut fs = DirFS::new(temp_dir.path()).unwrap();
2844
2845            fs.mkdir("/parent").unwrap();
2846            fs.cd("/parent").unwrap();
2847            fs.mkfile("child.txt", None).unwrap();
2848
2849            assert!(fs.exists("/parent/child.txt"));
2850
2851            // Remove using relative path
2852            fs.rm("child.txt").unwrap();
2853
2854            assert!(!fs.exists("/parent/child.txt"));
2855            assert!(!temp_dir.path().join("parent/child.txt").exists());
2856        }
2857
2858        #[test]
2859        fn test_rm_empty_string_path() {
2860            let temp_dir = setup_test_env();
2861            let mut fs = DirFS::new(temp_dir.path()).unwrap();
2862
2863            let result = fs.rm("");
2864            assert!(result.is_err());
2865            assert_eq!(result.unwrap_err().to_string(), "invalid path: empty");
2866        }
2867
2868        #[test]
2869        fn test_rm_root_directory() {
2870            let temp_dir = setup_test_env();
2871            let mut fs = DirFS::new(temp_dir.path()).unwrap();
2872
2873            // Attempt to remove root '/'
2874            let result = fs.rm("/");
2875            assert!(result.is_err());
2876            assert_eq!(
2877                result.unwrap_err().to_string(),
2878                "invalid path: the root cannot be removed"
2879            );
2880
2881            // Root should still exist
2882            assert!(fs.exists("/"));
2883            assert!(temp_dir.path().exists());
2884        }
2885
2886        #[test]
2887        fn test_rm_trailing_slash() {
2888            let temp_dir = setup_test_env();
2889            let mut fs = DirFS::new(temp_dir.path()).unwrap();
2890
2891            fs.mkdir("/dir/").unwrap(); // With trailing slash
2892            fs.mkfile("/dir/file.txt", None).unwrap();
2893
2894            // Remove with trailing slash
2895            fs.rm("/dir/").unwrap();
2896
2897            assert!(!fs.exists("/dir"));
2898            assert!(!temp_dir.path().join("dir").exists());
2899        }
2900
2901        #[test]
2902        fn test_rm_unicode_path() {
2903            let temp_dir = setup_test_env();
2904            let mut fs = DirFS::new(temp_dir.path()).unwrap();
2905
2906            let unicode_path = "/папка/файл.txt";
2907            fs.mkdir("/папка").unwrap();
2908            fs.mkfile(unicode_path, None).unwrap();
2909
2910            assert!(fs.exists(unicode_path));
2911
2912            fs.rm(unicode_path).unwrap();
2913
2914            assert!(!fs.exists(unicode_path));
2915            assert!(!temp_dir.path().join("папка/файл.txt").exists());
2916        }
2917
2918        #[test]
2919        fn test_rm_permission_denied() {
2920            #[cfg(unix)]
2921            {
2922                use std::os::unix::fs::PermissionsExt;
2923
2924                let temp_dir = setup_test_env();
2925                let mut fs = DirFS::new(temp_dir.path()).unwrap();
2926                fs.mkdir("/protected").unwrap();
2927
2928                // Create a directory and restrict permissions
2929                let protected = fs.root().join("protected");
2930                std::fs::set_permissions(&protected, PermissionsExt::from_mode(0o000)).unwrap();
2931
2932                // Try to remove via VFS (should fail)
2933                let result = fs.rm("/protected");
2934                assert!(result.is_err());
2935                assert!(
2936                    result
2937                        .unwrap_err()
2938                        .to_string()
2939                        .contains("Permission denied")
2940                );
2941
2942                // Clean up: restore permissions
2943                std::fs::set_permissions(&protected, PermissionsExt::from_mode(0o755)).unwrap();
2944            }
2945        }
2946
2947        #[test]
2948        fn test_rm_symlink_file() {
2949            #[cfg(unix)]
2950            {
2951                use std::os::unix::fs::symlink;
2952
2953                let temp_dir = setup_test_env();
2954                let mut fs = DirFS::new(temp_dir.path()).unwrap();
2955
2956                // Create real file and symlink
2957                std::fs::write(temp_dir.path().join("real.txt"), "content").unwrap();
2958                symlink("real.txt", temp_dir.path().join("link.txt")).unwrap();
2959
2960                fs.mkfile("/link.txt", None).unwrap(); // Add symlink to VFS
2961                assert!(fs.exists("/link.txt"));
2962
2963                // Remove symlink (not the target)
2964                fs.rm("/link.txt").unwrap();
2965
2966                assert!(!fs.exists("/link.txt"));
2967                assert!(!temp_dir.path().join("link.txt").exists()); // Symlink gone
2968                assert!(temp_dir.path().join("real.txt").exists()); // Target still there
2969            }
2970        }
2971
2972        #[test]
2973        fn test_rm_after_cd() {
2974            let temp_dir = setup_test_env();
2975            let mut fs = DirFS::new(temp_dir.path()).unwrap();
2976
2977            fs.mkdir("/projects").unwrap();
2978            fs.cd("/projects").unwrap();
2979            fs.mkfile("notes.txt", None).unwrap();
2980
2981            assert!(fs.exists("/projects/notes.txt"));
2982
2983            // Remove from cwd using relative path
2984            fs.rm("notes.txt").unwrap();
2985
2986            assert!(!fs.exists("/projects/notes.txt"));
2987            assert!(!temp_dir.path().join("projects/notes.txt").exists());
2988        }
2989    }
2990
2991    mod cleanup {
2992        use super::*;
2993
2994        #[test]
2995        fn test_cleanup_ignores_is_auto_clean() {
2996            let temp_dir = setup_test_env();
2997            let root = temp_dir.path();
2998
2999            let mut fs = DirFS::new(root).unwrap();
3000            fs.is_auto_clean = false; // Clearly disabled
3001            fs.mkfile("/temp.txt", None).unwrap();
3002
3003            fs.cleanup(); // Must be removed despite is_auto_clean=false
3004
3005            assert!(!fs.exists("/temp.txt"));
3006            assert!(!root.join("temp.txt").exists());
3007        }
3008
3009        #[test]
3010        fn test_cleanup_preserves_root_and_parents() {
3011            let temp_dir = setup_test_env();
3012            let root = temp_dir.path().join("preserve_root");
3013
3014            let mut fs = DirFS::new(&root).unwrap();
3015            fs.mkdir("/subdir").unwrap();
3016            fs.mkfile("/subdir/file.txt", None).unwrap();
3017
3018            // created_root_parents is populated at initialization
3019            assert!(!fs.created_root_parents.is_empty());
3020
3021            fs.cleanup();
3022
3023            // Root and his parents remained
3024            assert!(root.exists());
3025            for parent in &fs.created_root_parents {
3026                assert!(parent.exists());
3027            }
3028
3029            // Only entries (except "/") were removed
3030            assert_eq!(fs.entries.len(), 1);
3031            assert!(fs.entries.contains_key(&PathBuf::from("/")));
3032        }
3033
3034        #[test]
3035        fn test_cleanup_empty_entries() {
3036            let temp_dir = setup_test_env();
3037            let root = temp_dir.path();
3038
3039            let mut fs = DirFS::new(root).unwrap();
3040            // entries contains only "/"
3041            assert_eq!(fs.entries.len(), 1);
3042
3043            fs.cleanup();
3044
3045            assert_eq!(fs.entries.len(), 1); // "/" remained
3046            assert!(fs.entries.contains_key(&PathBuf::from("/")));
3047            assert!(root.exists()); // The root is not removed
3048        }
3049    }
3050
3051    // Helper function: Creates a temporary directory for tests
3052    fn setup_test_env() -> TempDir {
3053        TempDir::new("dirfs_test").unwrap()
3054    }
3055}