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