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