Skip to main content

vfs_kit/vfs/
dir_fs.rs

1/// This module provides a virtual filesystem (VFS) implementation that maps to a real directory
2/// on the host system. It allows file and directory operations (create, read, remove, navigate)
3/// within a controlled root path while maintaining internal state consistency.
4///
5/// Key Features:
6/// - **Isolated root**: All operations are confined to a designated root directory (self.root).
7/// - **Path normalization**: Automatically resolves . and .. components and removes trailing slashes.
8/// - **State tracking**: Maintains an internal set of valid paths (self.entries) to reflect VFS
9///   structure.
10/// - **Auto‑cleanup**: Optionally removes created artifacts on Drop (when is_auto_clean = true).
11/// - **Cross‑platform**: Uses std::path::Path and PathBuf for portable path handling.
12
13use std::collections::{BTreeSet, HashSet};
14use std::io::{Read, Write};
15use std::path::{Component, Path, PathBuf};
16
17use anyhow::anyhow;
18
19use crate::core::{FsBackend, Result};
20
21pub struct DirFS {
22    root: PathBuf,                      // host-related absolute normalized path
23    cwd: PathBuf,                       // inner absolute normalized path
24    entries: HashSet<PathBuf>,          // inner absolute normalized paths
25    created_root_parents: Vec<PathBuf>, // host-related absolute normalized paths
26    is_auto_clean: bool,
27}
28
29/// A virtual filesystem (VFS) implementation that maps to a real directory on the host system.
30///
31/// `DirFS` provides an isolated, path‑normalized view of a portion of the filesystem, rooted at a
32/// designated absolute path (`root`). It maintains an internal state of valid paths and supports
33/// standard operations:
34/// - Navigate via `cd()` (change working directory).
35/// - Create directories (`mkdir()`) and files (`mkfile()`).
36/// - Remove entries (`rm()`).
37/// - Check existence (`exists()`).
38/// - Read and write content (`read()` / `write()` / `append()`).
39///
40/// Key features:
41/// - **Path normalization**: Automatically resolves `.`, `..`, and trailing slashes.
42/// - **State consistency**: Tracks all valid VFS paths to ensure operations reflect
43///   the actual VFS structure.
44/// - **Isolated root**: All operations are confined to the `root` directory;
45///   no access to parent paths.
46/// - **Auto‑cleanup**: Optionally removes created parent directories on drop
47///   (when `is_auto_clean = true`).
48/// - **Cross‑platform**: Uses `std::path::Path` for portable path handling.
49///
50/// Usage notes:
51/// - `DirFS` does not follow symlinks; `rm()` removes the link, not the target.
52/// - Permissions are not automatically adjusted; ensure `root` is writable.
53/// - Not thread‑safe in current version (wrap in `Mutex` if needed).
54/// - Errors are returned via `anyhow::Result` with descriptive messages.
55///
56/// Example:
57/// ```
58/// use vfs_kit::{DirFS, FsBackend};
59///
60/// let tmp = std::env::temp_dir();
61/// let root = tmp.join("my_vfs");
62///
63/// let mut fs = DirFS::new(root).unwrap();
64/// fs.mkdir("/docs").unwrap();
65/// fs.mkfile("/docs/note.txt", Some(b"Hello")).unwrap();
66/// assert!(fs.exists("/docs/note.txt"));
67///
68/// fs.rm("/docs/note.txt").unwrap();
69/// ```
70impl DirFS {
71    /// Creates a new DirFs instance with the root directory at `path`.
72    /// Checks permissions to create and write into `path`.
73    /// * `path` is an absolute host path.
74    /// If `path` is not absolute, error returns.
75    pub fn new<P: AsRef<Path>>(root: P) -> Result<Self> {
76        let root = root.as_ref();
77
78        if root.as_os_str().is_empty() {
79            return Err(anyhow!("invalid root path: empty"));
80        }
81        if root.is_relative() {
82            return Err(anyhow!("the root path must be absolute"));
83        }
84        if root.exists() && !root.is_dir() {
85            return Err(anyhow!("{:?} is not a directory", root));
86        }
87
88        let root = Self::normalize(root);
89
90        let mut created_root_parents = Vec::new();
91        if !std::fs::exists(&root)? {
92            created_root_parents.extend(Self::mkdir_all(&root)?);
93        }
94
95        // check permissions
96        if !Self::check_permissions(&root) {
97            return Err(anyhow!("Access denied: {:?}", root));
98        }
99
100        Ok(Self {
101            root,
102            cwd: PathBuf::from("/"),
103            entries: HashSet::from([PathBuf::from("/")]),
104            created_root_parents,
105            is_auto_clean: true,
106        })
107    }
108
109    /// Changes auto-clean flag.
110    /// If auto-clean flag is true all created in vfs artifacts
111    /// will be removed on drop.
112    pub fn set_auto_clean(&mut self, clean: bool) {
113        self.is_auto_clean = clean;
114    }
115
116    /// Normalizes an arbitrary `path` by processing all occurrences
117    /// of '.' and '..' elements. Also, removes final `/`.
118    fn normalize<P: AsRef<Path>>(path: P) -> PathBuf {
119        let mut result = PathBuf::new();
120        for component in path.as_ref().components() {
121            match component {
122                Component::CurDir => {}
123                Component::ParentDir => {
124                    result.pop();
125                }
126                _ => {
127                    result.push(component);
128                }
129            }
130        }
131        // remove final /
132        if result != PathBuf::from("/") && result.ends_with("/") {
133            result.pop();
134        }
135        result
136    }
137
138    fn to_host<P: AsRef<Path>>(&self, path: P) -> PathBuf {
139        let inner = self.to_inner(path);
140        self.root.join(inner.strip_prefix("/").unwrap())
141    }
142
143    fn to_inner<P: AsRef<Path>>(&self, path: P) -> PathBuf {
144        Self::normalize(self.cwd.join(path))
145    }
146
147    /// Make directories recursively.
148    /// * `path` is an absolute host path.
149    /// Returns vector of created directories.
150    fn mkdir_all<P: AsRef<Path>>(path: P) -> Result<Vec<PathBuf>> {
151        let host_path = path.as_ref().to_path_buf();
152
153        // Looking for the first existing parent
154        let mut existed_part = host_path.clone();
155        while let Some(parent) = existed_part.parent() {
156            let parent_buf = parent.to_path_buf();
157            if std::fs::exists(parent)? {
158                existed_part = parent_buf;
159                break;
160            }
161            existed_part = parent_buf;
162        }
163
164        // Create from the closest existing parent to the target path
165        let need_to_create: Vec<_> = host_path
166            .strip_prefix(&existed_part)?
167            .components()
168            .collect();
169
170        let mut created = Vec::new();
171
172        let mut built = PathBuf::from(&existed_part);
173        for component in need_to_create {
174            built.push(component);
175            if !std::fs::exists(&built)? {
176                std::fs::create_dir(&built)?;
177                created.push(built.clone());
178            }
179        }
180
181        Ok(created)
182    }
183
184    fn rm_host_artifact<P: AsRef<Path>>(host_path: P) -> Result<()> {
185        let host_path = host_path.as_ref();
186        if host_path.is_dir() {
187            std::fs::remove_dir_all(host_path)?
188        } else {
189            std::fs::remove_file(host_path)?
190        }
191        Ok(())
192    }
193
194    fn check_permissions<P: AsRef<Path>>(path: P) -> bool {
195        let path = path.as_ref();
196        let filename = path.join(".access");
197        if let Err(_) = std::fs::write(&filename, b"check") {
198            return false;
199        }
200        if let Err(_) = std::fs::remove_file(filename) {
201            return false;
202        }
203        true
204    }
205}
206
207impl FsBackend for DirFS {
208    /// Returns root path related to the host file system.
209    fn root(&self) -> &Path {
210        self.root.as_path()
211    }
212
213    /// Returns current working directory related to the vfs root.
214    fn cwd(&self) -> &Path {
215        self.cwd.as_path()
216    }
217
218    /// Changes the current working directory.
219    /// * `path` can be in relative or absolute form, but in both cases it must exist.
220    /// An error is returned if the specified `path` does not exist.
221    fn cd<P: AsRef<Path>>(&mut self, path: P) -> Result<()> {
222        let target = self.to_inner(path);
223        if !self.exists(&target) {
224            return Err(anyhow!("{} does not exist", target.display()));
225        }
226        self.cwd = target;
227        Ok(())
228    }
229
230    /// Checks if a `path` exists in the vfs.
231    /// The `path` can be:
232    /// - absolute (starting with '/'),
233    /// - relative (relative to the vfs cwd),
234    /// - contain '..' or '.'.
235    fn exists<P: AsRef<Path>>(&self, path: P) -> bool {
236        let inner_path = self.to_inner(path);
237        self.entries.contains(&inner_path)
238    }
239
240    /// Creates directory and all it parents (if needed).
241    /// * `path` - inner vfs path.
242    fn mkdir<P: AsRef<Path>>(&mut self, path: P) -> Result<()> {
243        if path.as_ref().as_os_str().is_empty() {
244            return Err(anyhow!("invalid path: empty"));
245        }
246
247        let inner_path = self.to_inner(path);
248
249        if self.exists(&inner_path) {
250            return Err(anyhow!("path already exists: {}", inner_path.display()));
251        }
252
253        // Looking for the first existing parent
254        let mut existed_parent = inner_path.clone();
255        while let Some(parent) = existed_parent.parent() {
256            let parent_buf = parent.to_path_buf();
257            if self.entries.contains(parent) {
258                existed_parent = parent_buf;
259                break;
260            }
261            existed_parent = parent_buf;
262        }
263
264        // Create from the closest existing parent to the target path
265        let need_to_create: Vec<_> = inner_path
266            .strip_prefix(&existed_parent)?
267            .components()
268            .collect();
269
270        let mut built = PathBuf::from(&existed_parent);
271        for component in need_to_create {
272            built.push(component);
273            if !self.entries.contains(&built) {
274                let host = self.to_host(&built);
275                std::fs::create_dir(&host)?;
276                self.entries.insert(built.clone());
277            }
278        }
279
280        Ok(())
281    }
282
283    /// Creates new file in vfs.
284    /// * `file_path` must be inner vfs path. It must contain the name of the file,
285    /// optionally preceded by existing parent directory.
286    /// If the parent directory does not exist, an error is returned.
287    fn mkfile<P: AsRef<Path>>(&mut self, file_path: P, content: Option<&[u8]>) -> Result<()> {
288        let file_path = self.to_inner(file_path);
289        if let Some(parent) = file_path.parent() {
290            if let Err(e) = std::fs::exists(parent) {
291                return Err(anyhow!("{:?}: {}", parent, e));
292            }
293        }
294        let host = self.to_host(&file_path);
295        let mut fd = std::fs::File::create(host)?;
296        self.entries.insert(file_path);
297        if let Some(content) = content {
298            fd.write_all(content)?;
299        }
300        Ok(())
301    }
302
303    /// Reads the entire contents of a file into a byte vector.
304    /// * `path` is the inner VFS path.
305    ///
306    /// # Returns
307    /// * `Ok(Vec<u8>)` - File content as a byte vector if successful.
308    /// * `Err(anyhow::Error)` - If any of the following occurs:
309    ///   - File does not exist in VFS (`file does not exist: ...`)
310    ///   - Path points to a directory (`... is a directory`)
311    ///   - Permission issues when accessing the host file
312    ///   - I/O errors during reading
313    ///
314    /// # Notes
315    /// - Does **not** follow symbolic links on the host filesystem (reads the link itself).
316    /// - Returns an empty vector for empty files.
317    fn read<P: AsRef<Path>>(&self, path: P) -> Result<Vec<u8>> {
318        let inner = self.to_inner(&path);
319        if !self.exists(&inner) {
320            return Err(anyhow!("file does not exist: {}", path.as_ref().display()));
321        }
322        let host = self.to_host(&inner);
323        if host.is_dir() {
324            return Err(anyhow!("{} is a directory", host.display()));
325        }
326
327        let mut content = Vec::new();
328        std::fs::File::open(&host)?.read_to_end(&mut content)?;
329
330        Ok(content)
331    }
332
333    /// Writes bytes to an existing file, replacing its entire contents.
334    /// * `path` - Path to the file.
335    /// * `content` - Byte slice (`&[u8]`) to write to the file.
336    ///
337    /// # Returns
338    /// * `Ok(())` - If the write operation succeeded.
339    /// * `Err(anyhow::Error)` - If any of the following occurs:
340    ///   - File does not exist in VFS (`file does not exist: ...`)
341    ///   - Path points to a directory (`... is a directory`)
342    ///   - Permission issues when accessing the host file
343    ///   - I/O errors during writing (e.g., disk full, invalid path)
344    ///
345    /// # Behavior
346    /// - **Overwrites completely**: The entire existing content is replaced.
347    /// - **No file creation**: File must exist (use `mkfile()` first).
348    /// - **Atomic operation**: Uses `std::fs::write()` which replaces the file in one step.
349    /// - **Permissions**: The file retains its original permissions (no chmod is performed).
350    fn write<P: AsRef<Path>>(&self, path: P, content: &[u8]) -> Result<()> {
351        let inner = self.to_inner(&path);
352        let host = self.to_host(&inner);
353
354        if !self.exists(&inner) {
355            return Err(anyhow!("file does not exist: {}", path.as_ref().display()));
356        }
357        if host.is_dir() {
358            return Err(anyhow!("{} is a directory", host.display()));
359        }
360
361        std::fs::write(&host, content)?;
362
363        Ok(())
364    }
365
366    /// Appends bytes to the end of an existing file, preserving its old contents.
367    ///
368    /// # Arguments
369    /// * `path` - Path to the existing file.
370    /// * `content` - Byte slice (`&[u8]`) to append to the file.
371    ///
372    /// # Returns
373    /// * `Ok(())` - If the append operation succeeded.
374    /// * `Err(anyhow::Error)` - If any of the following occurs:
375    ///   - File does not exist in VFS (`file does not exist: ...`)
376    ///   - Path points to a directory (`... is a directory`)
377    ///   - Permission issues when accessing the host file
378    ///   - I/O errors during writing (e.g., disk full, invalid path)
379    ///
380    /// # Behavior
381    /// - **Appends only**: Existing content is preserved; new bytes are added at the end.
382    /// - **No parent creation**: Parent directories must exist (use `mkdir()` first if needed).
383    /// - **File creation**: Does NOT create the file if it doesn't exist (returns error).
384    /// - **Permissions**: The file retains its original permissions.
385    fn append<P: AsRef<Path>>(&self, path: P, content: &[u8]) -> Result<()> {
386        let inner = self.to_inner(&path);
387        let host = self.to_host(&inner);
388
389        if !self.exists(&inner) {
390            return Err(anyhow!("file does not exist: {}", path.as_ref().display()));
391        }
392        if host.is_dir() {
393            return Err(anyhow!("{} is a directory", host.display()));
394        }
395
396        // Open file in append mode and write content
397        use std::fs::OpenOptions;
398        let mut file = OpenOptions::new()
399            .write(true)
400            .append(true)
401            .open(&host)?;
402
403        file.write_all(content)?;
404
405        Ok(())
406    }
407
408    /// Removes a file or directory at the specified path.
409    ///
410    /// - `path`: can be absolute (starting with '/') or relative to the current working
411    /// directory (cwd).
412    /// - If the path is a directory, all its contents are removed recursively.
413    ///
414    /// Returns:
415    /// - `Ok(())` on successful removal.
416    /// - `Err(_)` if:
417    ///   - the path does not exist in the VFS;
418    ///   - there are insufficient permissions;
419    ///   - a filesystem error occurs.
420    fn rm<P: AsRef<Path>>(&mut self, path: P) -> Result<()> {
421        if path.as_ref().as_os_str().is_empty() {
422            return Err(anyhow!("invalid path: empty"));
423        }
424        if path.as_ref().as_os_str() == "/" {
425            return Err(anyhow!("invalid path: the root cannot be removed"));
426        }
427
428        let inner_path = self.to_inner(path); // Convert to VFS-internal normalized path
429        let host_path = self.to_host(&inner_path); // Map to real filesystem path
430
431        // Check if the path exists in the virtual filesystem
432        if !self.exists(&inner_path) {
433            return Err(anyhow!("{} does not exist", inner_path.display()));
434        }
435
436        // Remove from the real filesystem
437        Self::rm_host_artifact(host_path)?;
438
439        // Update internal state: collect all entries that start with `inner_path`
440        let removed: Vec<PathBuf> = self
441            .entries
442            .iter()
443            .filter(|p| p.starts_with(&inner_path)) // Match prefix (includes subpaths)
444            .cloned()
445            .collect();
446
447        // Remove all matched entries from the set
448        for p in removed {
449            self.entries.remove(&p);
450        }
451
452        Ok(())
453    }
454
455    /// Removes all artifacts (dirs and files) in vfs, but preserve its root.
456    fn cleanup(&mut self) -> bool {
457        let mut is_ok = true;
458
459        // Collect all paths to delete (except the root "/")
460        let mut sorted_paths_to_remove: BTreeSet<PathBuf> = BTreeSet::new();
461        for entry in &self.entries {
462            if entry != &PathBuf::from("/") {
463                sorted_paths_to_remove.insert(entry.clone());
464            }
465        }
466
467        for entry in sorted_paths_to_remove.iter().rev() {
468            let host = self.to_host(entry);
469            let result = Self::rm_host_artifact(&host);
470            if result.is_ok() {
471                self.entries.remove(entry);
472            } else {
473                is_ok = false;
474                eprintln!("Unable to remove: {}", host.display());
475            }
476        }
477
478        is_ok
479    }
480}
481
482impl Drop for DirFS {
483    fn drop(&mut self) {
484        if !self.is_auto_clean {
485            return;
486        }
487
488        if self.cleanup() {
489            self.entries.clear();
490        }
491
492        let errors: Vec<_> = self
493            .created_root_parents
494            .iter()
495            .rev()
496            .filter_map(|p| Self::rm_host_artifact(p).err())
497            .collect();
498        if !errors.is_empty() {
499            eprintln!("Failed to remove parents: {:?}", errors);
500        }
501
502        self.created_root_parents.clear();
503    }
504}
505
506#[cfg(test)]
507mod tests {
508    use super::*;
509    use tempdir::TempDir;
510
511    mod creations {
512        use super::*;
513
514        #[test]
515        fn test_new_absolute_path_existing() {
516            let temp_dir = setup_test_env();
517            let root = temp_dir.path().to_path_buf();
518
519            let fs = DirFS::new(&root).unwrap();
520
521            assert_eq!(fs.root, root);
522            assert_eq!(fs.cwd, PathBuf::from("/"));
523            assert!(fs.entries.contains(&PathBuf::from("/")));
524            assert!(fs.created_root_parents.is_empty());
525            assert!(fs.is_auto_clean);
526        }
527
528        #[test]
529        fn test_new_nonexistent_path_created() {
530            let temp_dir = setup_test_env();
531            let nonexistent = temp_dir.path().join("new_root");
532
533            let fs = DirFS::new(&nonexistent).unwrap();
534
535            assert_eq!(fs.root, nonexistent);
536            assert!(!fs.created_root_parents.is_empty()); // parents must be created
537            assert!(nonexistent.exists()); // The catalog has been created
538        }
539
540        #[test]
541        fn test_new_nested_nonexistent_path() {
542            let temp_dir = setup_test_env();
543            let nested = temp_dir.path().join("a/b/c");
544
545            let fs = DirFS::new(&nested).unwrap();
546
547            assert_eq!(fs.root, nested);
548            assert_eq!(fs.created_root_parents.len(), 3); // a, a/b, a/b/c
549            assert!(nested.exists());
550        }
551
552        #[test]
553        fn test_new_permission_denied() {
554            // This test requires a specific environment (e.g. readonly FS)
555            #[cfg(unix)]
556            {
557                use std::os::unix::fs::PermissionsExt;
558
559                let temp_dir = setup_test_env();
560                let protected = temp_dir.path().join("protected");
561                let protected_root = protected.join("root");
562                std::fs::create_dir_all(&protected_root).unwrap();
563                std::fs::set_permissions(&protected, PermissionsExt::from_mode(0o000)).unwrap(); // No access
564
565                let result = DirFS::new(&protected_root);
566                assert!(result.is_err());
567
568                std::fs::set_permissions(&protected, PermissionsExt::from_mode(0o755)).unwrap(); // Grant access
569            }
570        }
571
572        #[test]
573        fn test_new_normalize_path() {
574            let temp_dir = setup_test_env();
575            let messy_path = temp_dir.path().join("././subdir/../subdir");
576
577            let fs = DirFS::new(&messy_path).unwrap();
578            let canonical = DirFS::normalize(temp_dir.path().join("subdir"));
579
580            assert_eq!(fs.root, canonical);
581        }
582
583        #[test]
584        fn test_new_root_is_file() {
585            let temp_dir = setup_test_env();
586            let file_path = temp_dir.path().join("file.txt");
587            std::fs::write(&file_path, "content").unwrap();
588
589            let result = DirFS::new(&file_path);
590            assert!(result.is_err()); // Cannot create DirFs on file
591        }
592
593        #[test]
594        fn test_new_empty_path() {
595            let result = DirFS::new("");
596            assert!(result.is_err());
597        }
598
599        #[test]
600        fn test_new_special_characters() {
601            let temp_dir = setup_test_env();
602            let special = temp_dir.path().join("папка с пробелами и юникод!");
603
604            let fs = DirFS::new(&special).unwrap();
605
606            assert_eq!(fs.root, special);
607            assert!(special.exists());
608        }
609
610        #[test]
611        fn test_new_is_auto_clean_default() {
612            let temp_dir = setup_test_env();
613            let fs = DirFS::new(temp_dir.path()).unwrap();
614            assert!(fs.is_auto_clean); // True by default
615        }
616
617        #[test]
618        fn test_root_returns_correct_path() {
619            let temp_dir = setup_test_env();
620
621            let vfs_root = temp_dir.path().join("vfs-root");
622            let fs = DirFS::new(&vfs_root).unwrap();
623            assert_eq!(fs.root(), vfs_root);
624        }
625
626        #[test]
627        fn test_cwd_defaults_to_root() {
628            let temp_dir = setup_test_env();
629            let fs = DirFS::new(temp_dir).unwrap();
630            assert_eq!(fs.cwd(), Path::new("/"));
631        }
632    }
633
634    mod normalize {
635        use super::*;
636
637        #[test]
638        fn test_normalize_path() {
639            assert_eq!(DirFS::normalize("/a/b/c/"), PathBuf::from("/a/b/c"));
640            assert_eq!(DirFS::normalize("/a/b/./c"), PathBuf::from("/a/b/c"));
641            assert_eq!(DirFS::normalize("/a/b/../c"), PathBuf::from("/a/c"));
642            assert_eq!(DirFS::normalize("/"), PathBuf::from("/"));
643            assert_eq!(DirFS::normalize("/.."), PathBuf::from("/"));
644            assert_eq!(DirFS::normalize(".."), PathBuf::from(""));
645            assert_eq!(DirFS::normalize(""), PathBuf::from(""));
646            assert_eq!(DirFS::normalize("../a"), PathBuf::from("a"));
647            assert_eq!(DirFS::normalize("./a"), PathBuf::from("a"));
648        }
649    }
650
651    mod cd {
652        use super::*;
653
654        #[test]
655        fn test_cd_to_absolute_path() {
656            let temp_dir = setup_test_env();
657            let mut fs = DirFS::new(&temp_dir).unwrap();
658            fs.mkdir("/projects").unwrap();
659            fs.cd("/projects").unwrap();
660            assert_eq!(fs.cwd(), Path::new("/projects"));
661        }
662
663        #[test]
664        fn test_cd_with_relative_path() {
665            let temp_dir = setup_test_env();
666            let mut fs = DirFS::new(&temp_dir).unwrap();
667            fs.mkdir("/home/user").unwrap();
668            fs.cwd = PathBuf::from("/home");
669            fs.cd("user").unwrap();
670            assert_eq!(fs.cwd(), Path::new("/home/user"));
671        }
672
673        #[test]
674        fn test_cd_extreme_cases() {
675            let temp_dir = setup_test_env();
676            let mut fs = DirFS::new(&temp_dir).unwrap();
677
678            fs.cd("..").unwrap(); // where cwd == "/"
679            assert_eq!(fs.cwd(), Path::new("/"));
680
681            fs.cd(".").unwrap(); // where cwd == "/"
682            assert_eq!(fs.cwd(), Path::new("/"));
683
684            fs.cwd = PathBuf::from("/home");
685            assert_eq!(fs.cwd(), Path::new("/home"));
686            fs.mkdir("/other").unwrap();
687            fs.cd("../other").unwrap();
688            assert_eq!(fs.cwd(), Path::new("/other"));
689
690            fs.cwd = PathBuf::from("/home");
691            assert_eq!(fs.cwd(), Path::new("/home"));
692            fs.mkdir("/home/other").unwrap();
693            fs.cd("./other").unwrap();
694            assert_eq!(fs.cwd(), Path::new("/home/other"));
695        }
696    }
697
698    mod mkdir {
699        use super::*;
700
701        #[test]
702        fn test_mkdir_create_single_dir() {
703            let temp_dir = setup_test_env();
704            let mut fs = DirFS::new(&temp_dir).unwrap();
705            fs.mkdir("/projects").unwrap();
706            assert!(fs.exists("/projects"));
707        }
708
709        #[test]
710        fn test_mkdir_relative_path() {
711            let temp_dir = setup_test_env();
712            let mut fs = DirFS::new(&temp_dir).unwrap();
713            fs.mkdir("home").unwrap();
714            fs.cd("/home").unwrap();
715            fs.mkdir("user").unwrap();
716            assert!(fs.exists("/home/user"));
717        }
718
719        #[test]
720        fn test_mkdir_nested_path() {
721            let temp_dir = setup_test_env();
722            let mut fs = DirFS::new(&temp_dir).unwrap();
723            fs.mkdir("/a/b/c").unwrap();
724            assert!(fs.exists("/a"));
725            assert!(fs.exists("/a/b"));
726            assert!(fs.exists("/a/b/c"));
727        }
728
729        #[test]
730        fn test_mkdir_already_exists() {
731            let temp_dir = setup_test_env();
732            let mut fs = DirFS::new(&temp_dir).unwrap();
733            fs.mkdir("/data").unwrap();
734            let result = fs.mkdir("/data");
735            assert!(result.is_err());
736        }
737
738        #[test]
739        fn test_mkdir_invalid_path() {
740            let temp_dir = setup_test_env();
741            let mut fs = DirFS::new(&temp_dir).unwrap();
742            let result = fs.mkdir("");
743            assert!(result.is_err());
744        }
745    }
746
747    mod exists {
748        use super::*;
749
750        #[test]
751        fn test_exists_root() {
752            let temp_dir = setup_test_env();
753            let fs = DirFS::new(&temp_dir).unwrap();
754            assert!(fs.exists("/"));
755        }
756
757        #[test]
758        fn test_exists_cwd() {
759            let temp_dir = setup_test_env();
760            let mut fs = DirFS::new(&temp_dir).unwrap();
761            fs.mkdir("/projects").unwrap();
762            fs.cd("/projects").unwrap();
763            assert!(fs.exists("."));
764            assert!(fs.exists("./"));
765            assert!(fs.exists("/projects"));
766        }
767    }
768
769    mod mkdir_all {
770        use super::*;
771        use std::fs;
772        use std::path::PathBuf;
773
774        #[test]
775        fn test_mkdir_all_simple_creation() {
776            let temp_dir = setup_test_env();
777            let target = temp_dir.path().join("a/b/c");
778
779            let created = DirFS::mkdir_all(&target).unwrap();
780
781            assert_eq!(created.len(), 3);
782            assert!(created.contains(&temp_dir.path().join("a")));
783            assert!(created.contains(&temp_dir.path().join("a/b")));
784            assert!(created.contains(&temp_dir.path().join("a/b/c")));
785
786            // Проверяем, что каталоги реально созданы
787            assert!(temp_dir.path().join("a").is_dir());
788            assert!(temp_dir.path().join("a/b").is_dir());
789            assert!(temp_dir.path().join("a/b/c").is_dir());
790        }
791
792        #[test]
793        fn test_mkdir_all_existing_parent() {
794            let temp_dir = setup_test_env();
795            fs::create_dir_all(temp_dir.path().join("a")).unwrap(); // It already exists
796
797            let target = temp_dir.path().join("a/b/c");
798            let created = DirFS::mkdir_all(&target).unwrap();
799
800            assert_eq!(created.len(), 2); // Только b и c
801            assert!(created.contains(&temp_dir.path().join("a/b")));
802            assert!(created.contains(&temp_dir.path().join("a/b/c")));
803        }
804
805        #[test]
806        fn test_mkdir_all_target_exists() {
807            let temp_dir = setup_test_env();
808            fs::create_dir_all(temp_dir.path().join("x/y")).unwrap();
809
810            let target = temp_dir.path().join("x/y");
811            let created = DirFS::mkdir_all(&target).unwrap();
812
813            assert!(created.is_empty()); // Nothing was created
814        }
815
816        #[test]
817        fn test_mkdir_all_root_path() {
818            // FS root (usually "/")
819            let result = DirFS::mkdir_all("/");
820            assert!(result.is_ok());
821            assert!(result.unwrap().is_empty());
822        }
823
824        #[test]
825        fn test_mkdir_all_single_dir() {
826            let temp_dir = setup_test_env();
827            let target = temp_dir.path().join("single");
828
829            let created = DirFS::mkdir_all(&target).unwrap();
830
831            assert_eq!(created.len(), 1);
832            assert!(created.contains(&target));
833            assert!(target.is_dir());
834        }
835
836        #[test]
837        fn test_mkdir_all_absolute_vs_relative() {
838            let temp_dir = setup_test_env();
839
840            // The absolute path
841            let abs_target = temp_dir.path().join("abs/a/b");
842            let abs_created = DirFS::mkdir_all(&abs_target).unwrap();
843
844            assert!(!abs_created.is_empty());
845        }
846
847        #[test]
848        fn test_mkdir_all_nested_existing() {
849            let temp_dir = setup_test_env();
850            fs::create_dir_all(temp_dir.path().join("deep/a")).unwrap();
851
852            let target = temp_dir.path().join("deep/a/b/c/d");
853            let created = DirFS::mkdir_all(&target).unwrap();
854
855            assert_eq!(created.len(), 3); // b, c, d
856        }
857
858        #[test]
859        fn test_mkdir_all_invalid_path() {
860            // Attempt to create in a non-existent location (without rights)
861            #[cfg(unix)]
862            {
863                let invalid_path = PathBuf::from("/nonexistent/parent/child");
864
865                // Expecting an error (e.g. PermissionDenied or NoSuchFile)
866                let result = DirFS::mkdir_all(&invalid_path);
867                assert!(result.is_err());
868            }
869        }
870
871        #[test]
872        fn test_mkdir_all_file_in_path() {
873            let temp_dir = setup_test_env();
874            let file_path = temp_dir.path().join("file.txt");
875            fs::write(&file_path, "content").unwrap(); // Create a file
876
877            let target = file_path.join("subdir"); // Trying to create inside the file
878
879            let result = DirFS::mkdir_all(&target);
880            assert!(result.is_err()); // Must be an error
881        }
882
883        #[test]
884        fn test_mkdir_all_trailing_slash() {
885            let temp_dir = setup_test_env();
886            let target = temp_dir.path().join("trailing/");
887
888            let created = DirFS::mkdir_all(&target).unwrap();
889            assert!(!created.is_empty());
890            assert!(temp_dir.path().join("trailing").is_dir());
891        }
892
893        #[test]
894        fn test_mkdir_all_unicode_paths() {
895            let temp_dir = setup_test_env();
896            let target = temp_dir.path().join("папка/файл");
897
898            let created = DirFS::mkdir_all(&target).unwrap();
899
900            assert_eq!(created.len(), 2);
901            assert!(temp_dir.path().join("папка").is_dir());
902            assert!(temp_dir.path().join("папка/файл").is_dir());
903        }
904
905        #[test]
906        fn test_mkdir_all_permissions_error() {
907            // This test requires a specific environment (e.g. readonly FS).
908            // Skip it in general tests, but leave it for manual launch.
909            #[cfg(unix)]
910            {
911                use std::os::unix::fs::PermissionsExt;
912
913                let temp_dir = setup_test_env();
914                fs::set_permissions(&temp_dir, PermissionsExt::from_mode(0o444)).unwrap(); // readonly
915
916                let target = temp_dir.path().join("protected/dir");
917                let result = DirFS::mkdir_all(&target);
918
919                assert!(result.is_err());
920            }
921        }
922    }
923
924    mod drop {
925        use super::*;
926
927        #[test]
928        fn test_drop_removes_created_directories() {
929            let temp_dir = setup_test_env();
930            let root = temp_dir.path().join("to_remove");
931
932            // Create DirFs, which will create new directories.
933            let fs = DirFS::new(&root).unwrap();
934            assert!(root.exists());
935
936            // Destroy fs (Drop should work)
937            drop(fs);
938
939            // Check that the root has been removed.
940            assert!(!root.exists());
941        }
942
943        #[test]
944        fn test_drop_only_removes_created_parents() {
945            let temp_dir = setup_test_env();
946            let parent = temp_dir.path().join("parent");
947            let child = parent.join("child");
948
949            std::fs::create_dir_all(&parent).unwrap(); // The parent already exists
950            let fs = DirFS::new(&child).unwrap();
951
952            assert!(parent.exists()); // The parent must remain.
953            assert!(child.exists());
954
955            drop(fs);
956
957            assert!(parent.exists()); // The parent is not deleted
958            assert!(!child.exists()); // The child has been removed
959        }
960
961        #[test]
962        fn test_drop_with_is_auto_clean_false() {
963            let temp_dir = setup_test_env();
964            let root = temp_dir.path().join("keep");
965
966            let mut fs = DirFS::new(&root).unwrap();
967            fs.is_auto_clean = false; // Disable auto-cleaning
968
969            drop(fs);
970
971            assert!(root.exists()); // The catalog must remain
972        }
973
974        #[test]
975        fn test_drop_empty_created_root_parents() {
976            let temp_dir = setup_test_env();
977            let existing = temp_dir.path().join("existing");
978            std::fs::create_dir(&existing).unwrap();
979
980            let fs = DirFS::new(&existing).unwrap(); // Already exists → created_root_parents is empty
981
982            drop(fs);
983
984            assert!(existing.exists()); // It should remain (we didn't create it)
985        }
986
987        #[test]
988        fn test_drop_nested_directories_removed() {
989            let temp_dir = setup_test_env();
990            let nested = temp_dir.path().join("a/b/c");
991
992            let fs = DirFS::new(&nested).unwrap();
993            assert!(nested.exists());
994
995            drop(fs);
996
997            // Все уровни должны быть удалены
998            assert!(!temp_dir.path().join("a").exists());
999            assert!(!temp_dir.path().join("a/b").exists());
1000            assert!(!nested.exists());
1001        }
1002
1003        //-----------------------------
1004
1005        #[test]
1006        fn test_drop_removes_entries_created_by_mkdir() {
1007            let temp_dir = setup_test_env();
1008            let root = temp_dir.path().join("test_root");
1009
1010            let mut fs = DirFS::new(&root).unwrap();
1011            fs.mkdir("/subdir").unwrap();
1012            assert!(root.join("subdir").exists());
1013
1014            drop(fs);
1015
1016            assert!(!root.exists()); // Корень удалён
1017            assert!(!root.join("subdir").exists()); // The subdirectory has also been deleted.
1018        }
1019
1020        #[test]
1021        fn test_drop_removes_entries_created_by_mkfile() {
1022            let temp_dir = setup_test_env();
1023            let root = temp_dir.path().join("test_root");
1024
1025            let mut fs = DirFS::new(&root).unwrap();
1026            fs.mkfile("/file.txt", None).unwrap();
1027            assert!(root.join("file.txt").exists());
1028
1029            drop(fs);
1030
1031            assert!(!root.exists());
1032            assert!(!root.join("file.txt").exists());
1033        }
1034
1035        #[test]
1036        fn test_drop_handles_nested_entries() {
1037            let temp_dir = setup_test_env();
1038            let root = temp_dir.path().join("test_root");
1039
1040            let mut fs = DirFS::new(&root).unwrap();
1041            fs.mkdir("/a/b/c").unwrap();
1042            fs.mkfile("/a/file.txt", None).unwrap();
1043
1044            assert!(root.join("a/b/c").exists());
1045            assert!(root.join("a/file.txt").exists());
1046
1047            drop(fs);
1048
1049            assert!(!root.exists());
1050        }
1051
1052        #[test]
1053        fn test_drop_ignores_non_entries() {
1054            let temp_dir = setup_test_env();
1055            let root = temp_dir.path().join("test_root");
1056            let external = temp_dir.path().join("external_file.txt");
1057
1058            std::fs::write(&external, "content").unwrap(); // File outside VFS
1059
1060            let fs = DirFS::new(&root).unwrap();
1061            drop(fs);
1062
1063            assert!(!root.exists());
1064            assert!(external.exists()); // The external file remains
1065        }
1066
1067        #[test]
1068        fn test_drop_with_empty_entries() {
1069            let temp_dir = setup_test_env();
1070            let root = temp_dir.path().join("empty_root");
1071
1072            let fs = DirFS::new(&root).unwrap();
1073            // entries contains only "/" (root)
1074
1075            drop(fs);
1076
1077            assert!(!root.exists());
1078        }
1079    }
1080
1081    mod mkfile {
1082        use super::*;
1083
1084        #[test]
1085        fn test_mkfile_simple_creation() {
1086            let temp_dir = setup_test_env();
1087            let root = temp_dir.path();
1088
1089            let mut fs = DirFS::new(root).unwrap();
1090            fs.mkfile("/file.txt", None).unwrap();
1091
1092            assert!(fs.exists("/file.txt"));
1093            assert!(root.join("file.txt").exists());
1094            assert_eq!(fs.entries.contains(&PathBuf::from("/file.txt")), true);
1095        }
1096
1097        #[test]
1098        fn test_mkfile_with_content() {
1099            let temp_dir = setup_test_env();
1100            let root = temp_dir.path();
1101
1102            let mut fs = DirFS::new(root).unwrap();
1103            let content = b"Hello, VFS!";
1104            fs.mkfile("/data.bin", Some(content)).unwrap();
1105
1106            assert!(fs.exists("/data.bin"));
1107            let file_content = std::fs::read(root.join("data.bin")).unwrap();
1108            assert_eq!(&file_content, content);
1109        }
1110
1111        #[test]
1112        fn test_mkfile_in_subdirectory() {
1113            let temp_dir = setup_test_env();
1114            let root = temp_dir.path();
1115
1116            let mut fs = DirFS::new(root).unwrap();
1117            fs.mkdir("/subdir").unwrap();
1118            fs.mkfile("/subdir/file.txt", None).unwrap();
1119
1120            assert!(fs.exists("/subdir/file.txt"));
1121            assert!(root.join("subdir/file.txt").exists());
1122        }
1123
1124        #[test]
1125        fn test_mkfile_parent_does_not_exist() {
1126            let temp_dir = setup_test_env();
1127            let root = temp_dir.path();
1128
1129            let mut fs = DirFS::new(root).unwrap();
1130
1131            let result = fs.mkfile("/nonexistent/file.txt", None);
1132            assert!(result.is_err());
1133        }
1134
1135        #[test]
1136        fn test_mkfile_file_already_exists() {
1137            let temp_dir = setup_test_env();
1138            let root = temp_dir.path();
1139
1140            let mut fs = DirFS::new(root).unwrap();
1141            fs.mkfile("/existing.txt", None).unwrap();
1142
1143            // Trying to create the same file again
1144            let result = fs.mkfile("/existing.txt", None);
1145            assert!(result.is_ok()); // Should overwrite (File::create truncates the file)
1146            assert!(fs.exists("/existing.txt"));
1147        }
1148
1149        #[test]
1150        fn test_mkfile_empty_content() {
1151            let temp_dir = setup_test_env();
1152            let root = temp_dir.path();
1153
1154            let mut fs = DirFS::new(root).unwrap();
1155            fs.mkfile("/empty.txt", Some(&[])).unwrap(); // An empty array
1156
1157            assert!(fs.exists("/empty.txt"));
1158            let file_size = std::fs::metadata(root.join("empty.txt")).unwrap().len();
1159            assert_eq!(file_size, 0);
1160        }
1161
1162        #[test]
1163        fn test_mkfile_relative_path() {
1164            let temp_dir = setup_test_env();
1165            let root = temp_dir.path();
1166
1167            let mut fs = DirFS::new(root).unwrap();
1168            fs.mkdir("/sub").unwrap();
1169            fs.cd("/sub").unwrap(); // Changes the current directory
1170
1171            fs.mkfile("relative.txt", None).unwrap(); // A relative path
1172
1173            assert!(fs.exists("/sub/relative.txt"));
1174            assert!(root.join("sub/relative.txt").exists());
1175        }
1176
1177        #[test]
1178        fn test_mkfile_normalize_path() {
1179            let temp_dir = setup_test_env();
1180            let root = temp_dir.path();
1181
1182            let mut fs = DirFS::new(root).unwrap();
1183            fs.mkdir("/normalized").unwrap();
1184
1185            fs.mkfile("/./normalized/../normalized/file.txt", None)
1186                .unwrap();
1187
1188            assert!(fs.exists("/normalized/file.txt"));
1189            assert!(root.join("normalized/file.txt").exists());
1190        }
1191
1192        #[test]
1193        fn test_mkfile_invalid_path_components() {
1194            let temp_dir = setup_test_env();
1195            let root = temp_dir.path();
1196
1197            let mut fs = DirFS::new(root).unwrap();
1198
1199            // Attempt to create a file with an invalid name (depending on the file system)
1200            #[cfg(unix)]
1201            {
1202                let result = fs.mkfile("/invalid\0name.txt", None);
1203                assert!(result.is_err()); // NUL in filenames is prohibited in Unix.
1204            }
1205        }
1206
1207        #[test]
1208        fn test_mkfile_permission_denied() {
1209            #[cfg(unix)]
1210            {
1211                use std::os::unix::fs::PermissionsExt;
1212
1213                let temp_dir = setup_test_env();
1214                let root = temp_dir.path();
1215                let protected = root.join("protected");
1216                std::fs::create_dir(&protected).unwrap();
1217                std::fs::set_permissions(&protected, PermissionsExt::from_mode(0o000)).unwrap(); // No access
1218
1219                let mut fs = DirFS::new(root).unwrap();
1220                let result = fs.mkfile("/protected/file.txt", None);
1221
1222                std::fs::set_permissions(&protected, PermissionsExt::from_mode(0o755)).unwrap(); // Grant access
1223
1224                assert!(result.is_err());
1225                assert!(
1226                    result
1227                        .unwrap_err()
1228                        .to_string()
1229                        .contains("Permission denied")
1230                );
1231            }
1232        }
1233
1234        #[test]
1235        fn test_mkfile_root_directory() {
1236            let temp_dir = setup_test_env();
1237            let root = temp_dir.path();
1238
1239            let mut fs = DirFS::new(root).unwrap();
1240
1241            // Cannot create a file named "/" (it is a directory)
1242            let result = fs.mkfile("/", None);
1243            assert!(result.is_err());
1244        }
1245
1246        #[test]
1247        fn test_mkfile_unicode_filename() {
1248            let temp_dir = setup_test_env();
1249            let root = temp_dir.path();
1250
1251            let mut fs = DirFS::new(root).unwrap();
1252            fs.mkfile("/тест.txt", Some(b"Content")).unwrap();
1253
1254            assert!(fs.exists("/тест.txt"));
1255            assert!(root.join("тест.txt").exists());
1256            let content = std::fs::read_to_string(root.join("тест.txt")).unwrap();
1257            assert_eq!(content, "Content");
1258        }
1259    }
1260
1261    mod read {
1262        use super::*;
1263
1264        #[test]
1265        fn test_read_existing_file() -> Result<()> {
1266            let temp_dir = setup_test_env();
1267            let mut fs = DirFS::new(&temp_dir)?;
1268
1269            // Create and write a file
1270            fs.mkfile("/test.txt", Some(b"Hello, VFS!"))?;
1271
1272            // Read it back
1273            let content = fs.read("/test.txt")?;
1274            assert_eq!(content, b"Hello, VFS!");
1275
1276            Ok(())
1277        }
1278
1279        #[test]
1280        fn test_read_nonexistent_file() -> Result<()> {
1281            let temp_dir = setup_test_env();
1282            let fs = DirFS::new(temp_dir.path())?;
1283
1284            let result = fs.read("/not/found.txt");
1285            assert!(result.is_err());
1286            assert!(
1287                result
1288                    .unwrap_err()
1289                    .to_string()
1290                    .contains("file does not exist: /not/found.txt")
1291            );
1292
1293            Ok(())
1294        }
1295
1296        #[test]
1297        fn test_read_directory_as_file() -> Result<()> {
1298            let temp_dir = setup_test_env();
1299            let mut fs = DirFS::new(temp_dir.path())?;
1300
1301            fs.mkdir("/empty_dir")?;
1302
1303            let result = fs.read("/empty_dir");
1304            assert!(result.is_err());
1305            // Note: error comes from std::fs::File::open (not a file), not our exists check
1306            assert!(result.unwrap_err().to_string().contains("is a directory"));
1307
1308            Ok(())
1309        }
1310
1311        #[test]
1312        fn test_read_empty_file() -> Result<()> {
1313            let temp_dir = setup_test_env();
1314            let mut fs = DirFS::new(temp_dir.path())?;
1315
1316            fs.mkfile("/empty.txt", None)?; // Create empty file
1317
1318            let content = fs.read("/empty.txt")?;
1319            assert_eq!(content.len(), 0);
1320
1321            Ok(())
1322        }
1323
1324        #[test]
1325        fn test_read_relative_path() -> Result<()> {
1326            let temp_dir = setup_test_env();
1327            let mut fs = DirFS::new(temp_dir.path())?;
1328
1329            fs.cd("/")?;
1330            fs.mkdir("/parent")?;
1331            fs.cd("/parent")?;
1332            fs.mkfile("child.txt", Some(b"Content"))?;
1333
1334            // Read using relative path from cwd
1335            let content = fs.read("child.txt")?;
1336            assert_eq!(content, b"Content");
1337
1338            Ok(())
1339        }
1340
1341        #[test]
1342        fn test_read_unicode_path() -> Result<()> {
1343            let temp_dir = setup_test_env();
1344            let mut fs = DirFS::new(temp_dir.path())?;
1345
1346            fs.mkdir("/папка")?;
1347            fs.mkfile("/папка/файл.txt", Some(b"Unicode content"))?;
1348
1349            let content = fs.read("/папка/файл.txt")?;
1350            assert_eq!(content, b"Unicode content");
1351
1352            Ok(())
1353        }
1354
1355        #[test]
1356        fn test_read_permission_denied() -> Result<()> {
1357            #[cfg(unix)]
1358            {
1359                use std::os::unix::fs::PermissionsExt;
1360
1361                let temp_dir = setup_test_env();
1362                let mut fs = DirFS::new(temp_dir.path())?;
1363
1364                // Create file and restrict permissions
1365                fs.mkfile("/protected.txt", Some(b"Secret"))?;
1366                let host_path = temp_dir.path().join("protected.txt");
1367                std::fs::set_permissions(&host_path, PermissionsExt::from_mode(0o000))?;
1368
1369                // Try to read (should fail due to permissions)
1370                let result = fs.read("/protected.txt");
1371                assert!(result.is_err());
1372                assert!(
1373                    result
1374                        .unwrap_err()
1375                        .to_string()
1376                        .contains("Permission denied")
1377                );
1378
1379                // Clean up: restore permissions
1380                std::fs::set_permissions(&host_path, PermissionsExt::from_mode(0o644))?;
1381            }
1382            Ok(())
1383        }
1384
1385        #[test]
1386        fn test_read_root_file() -> Result<()> {
1387            let temp_dir = setup_test_env();
1388            let mut fs = DirFS::new(temp_dir.path())?;
1389
1390            fs.mkfile("/root_file.txt", Some(b"At root"))?;
1391            let content = fs.read("/root_file.txt")?;
1392            assert_eq!(content, b"At root");
1393
1394            Ok(())
1395        }
1396    }
1397
1398    mod write {
1399        use super::*;
1400
1401        #[test]
1402        fn test_write_new_file() -> Result<()> {
1403            let temp_dir = setup_test_env();
1404            let mut fs = DirFS::new(temp_dir.path())?;
1405
1406            fs.mkfile("/new.txt", None)?;
1407            let content = b"Hello, VFS!";
1408            fs.write("/new.txt", content)?;
1409
1410            // Check file exists and has correct content
1411            assert!(fs.exists("/new.txt"));
1412            let read_back = fs.read("/new.txt")?;
1413            assert_eq!(read_back, content);
1414
1415            Ok(())
1416        }
1417
1418        #[test]
1419        fn test_write_existing_file_overwrite() -> Result<()> {
1420            let temp_dir = setup_test_env();
1421            let mut fs = DirFS::new(temp_dir.path())?;
1422
1423            fs.mkfile("/exist.txt", Some(b"Old content"))?;
1424
1425            let new_content = b"New content";
1426            fs.write("/exist.txt", new_content)?;
1427
1428            let read_back = fs.read("/exist.txt")?;
1429            assert_eq!(read_back, new_content);
1430
1431            Ok(())
1432        }
1433
1434        #[test]
1435        fn test_write_to_directory_path() -> Result<()> {
1436            let temp_dir = setup_test_env();
1437            let mut fs = DirFS::new(temp_dir.path())?;
1438
1439            fs.mkdir("/dir")?;
1440
1441            let result = fs.write("/dir", b"Content");
1442            assert!(result.is_err());
1443            assert!(result.unwrap_err().to_string().contains("is a directory"));
1444
1445            Ok(())
1446        }
1447
1448        #[test]
1449        fn test_write_to_nonexistent_file() -> Result<()> {
1450            let temp_dir = setup_test_env();
1451            let fs = DirFS::new(temp_dir.path())?;
1452
1453            let result = fs.write("/parent/child.txt", b"Content");
1454            assert!(result.is_err());
1455            assert!(
1456                result
1457                    .unwrap_err()
1458                    .to_string()
1459                    .contains("file does not exist")
1460            );
1461
1462            Ok(())
1463        }
1464
1465        #[test]
1466        fn test_write_empty_content() -> Result<()> {
1467            let temp_dir = setup_test_env();
1468            let mut fs = DirFS::new(temp_dir.path())?;
1469
1470            fs.mkfile("/empty.txt", None)?;
1471            fs.write("/empty.txt", &[])?;
1472
1473            let read_back = fs.read("/empty.txt")?;
1474            assert!(read_back.is_empty());
1475
1476            Ok(())
1477        }
1478
1479        #[test]
1480        fn test_write_relative_path() -> Result<()> {
1481            let temp_dir = setup_test_env();
1482            let mut fs = DirFS::new(temp_dir.path())?;
1483
1484            fs.mkdir("/docs")?;
1485            fs.cd("docs")?;
1486
1487            fs.mkfile("file.txt", None)?;
1488            let content = b"Relative write";
1489            fs.write("file.txt", content)?;
1490
1491            let read_back = fs.read("/docs/file.txt")?;
1492            assert_eq!(read_back, content);
1493
1494            Ok(())
1495        }
1496    }
1497
1498    mod append {
1499        use super::*;
1500
1501        #[test]
1502        fn test_append_to_existing_file() -> Result<()> {
1503            let temp_dir = setup_test_env();
1504            let mut fs = DirFS::new(temp_dir.path())?;
1505
1506            // Create initial file
1507            fs.mkfile("/log.txt", Some(b"Initial content\n"))?;
1508
1509            // Append new content
1510            fs.append("/log.txt", b"Appended line 1\n")?;
1511            fs.append("/log.txt", b"Appended line 2\n")?;
1512
1513            // Verify full content
1514            let content = fs.read("/log.txt")?;
1515            assert_eq!(
1516                content,
1517                b"Initial content\nAppended line 1\nAppended line 2\n"
1518            );
1519
1520            Ok(())
1521        }
1522
1523        #[test]
1524        fn test_append_to_empty_file() -> Result<()> {
1525            let temp_dir = setup_test_env();
1526            let mut fs = DirFS::new(temp_dir.path())?;
1527
1528            // Create empty file
1529            fs.mkfile("/empty.txt", Some(&[]))?;
1530
1531            // Append content
1532            fs.append("/empty.txt", b"First append\n")?;
1533            fs.append("/empty.txt", b"Second append\n")?;
1534
1535            let content = fs.read("/empty.txt")?;
1536            assert_eq!(content, b"First append\nSecond append\n");
1537
1538            Ok(())
1539        }
1540
1541        #[test]
1542        fn test_append_nonexistent_file() -> Result<()> {
1543            let temp_dir = setup_test_env();
1544            let fs = DirFS::new(temp_dir.path())?;
1545
1546            let result = fs.append("/not_found.txt", b"Content");
1547            assert!(result.is_err());
1548            assert!(result
1549                .unwrap_err()
1550                .to_string()
1551                .contains("file does not exist: /not_found.txt"));
1552
1553            Ok(())
1554        }
1555
1556        #[test]
1557        fn test_append_to_directory() -> Result<()> {
1558            let temp_dir = setup_test_env();
1559            let mut fs = DirFS::new(temp_dir.path())?;
1560
1561            fs.mkdir("/mydir")?;
1562
1563            let result = fs.append("/mydir", b"Content");
1564            assert!(result.is_err());
1565            assert!(result
1566                .unwrap_err()
1567                .to_string()
1568                .contains("is a directory"));
1569
1570            Ok(())
1571        }
1572
1573        #[test]
1574        fn test_append_empty_content() -> Result<()> {
1575            let temp_dir = setup_test_env();
1576            let mut fs = DirFS::new(temp_dir.path())?;
1577
1578            fs.mkfile("/test.txt", Some(b"Existing\n"))?;
1579
1580            // Append empty slice
1581            fs.append("/test.txt", &[])?;
1582
1583            // Content should remain unchanged
1584            let content = fs.read("/test.txt")?;
1585            assert_eq!(content, b"Existing\n");
1586
1587            Ok(())
1588        }
1589
1590        #[test]
1591        fn test_append_relative_path() -> Result<()> {
1592            let temp_dir = setup_test_env();
1593            let mut fs = DirFS::new(temp_dir.path())?;
1594
1595            fs.mkdir("/docs")?;
1596            fs.cd("/docs")?;
1597            fs.mkfile("log.txt", Some(b"Start\n"))?; // Relative path
1598
1599            fs.append("log.txt", b"Added\n")?;
1600
1601            let content = fs.read("/docs/log.txt")?;
1602            assert_eq!(content, b"Start\nAdded\n");
1603
1604            Ok(())
1605        }
1606
1607        #[test]
1608        fn test_append_unicode_path() -> Result<()> {
1609            let temp_dir = setup_test_env();
1610            let mut fs = DirFS::new(temp_dir.path())?;
1611
1612            let first = Vec::from("Начало\n");
1613            let second = Vec::from("Продолжение\n");
1614
1615            fs.mkdir("/папка")?;
1616            fs.mkfile("/папка/файл.txt", Some(first.as_slice()))?;
1617            fs.append("/папка/файл.txt", second.as_slice())?;
1618
1619            let content = fs.read("/папка/файл.txt")?;
1620
1621            let mut expected = Vec::from(first);
1622            expected.extend(second);
1623
1624            assert_eq!(content, expected);
1625
1626            Ok(())
1627        }
1628
1629        #[test]
1630        fn test_concurrent_append_safety() -> Result<()> {
1631            let temp_dir = setup_test_env();
1632            let mut fs = DirFS::new(temp_dir.path())?;
1633
1634            fs.mkfile("/concurrent.txt", Some(b""))?;
1635
1636            // Simulate multiple appends
1637            for i in 1..=3 {
1638                fs.append("/concurrent.txt", format!("Line {}\n", i).as_bytes())?;
1639            }
1640
1641            let content = fs.read("/concurrent.txt")?;
1642            assert_eq!(content, b"Line 1\nLine 2\nLine 3\n");
1643
1644            Ok(())
1645        }
1646
1647        #[test]
1648        fn test_append_permission_denied() -> Result<()> {
1649            #[cfg(unix)]
1650            {
1651                use std::os::unix::fs::PermissionsExt;
1652
1653                let temp_dir = setup_test_env();
1654                let mut fs = DirFS::new(temp_dir.path())?;
1655
1656                // Create file and restrict permissions
1657                fs.mkfile("/protected.txt", Some(b"Content"))?;
1658                let host_path = temp_dir.path().join("protected.txt");
1659                std::fs::set_permissions(&host_path, PermissionsExt::from_mode(0o000))?;
1660
1661                // Try to append (should fail)
1662                let result = fs.append("/protected.txt", b"New content");
1663                assert!(result.is_err());
1664                assert!(result.unwrap_err().to_string().contains("Permission denied"));
1665
1666                // Clean up: restore permissions
1667                std::fs::set_permissions(&host_path, PermissionsExt::from_mode(0o644))?;
1668            }
1669            Ok(())
1670        }
1671    }
1672
1673    mod rm {
1674        use super::*;
1675
1676        #[test]
1677        fn test_rm_file_success() {
1678            let temp_dir = setup_test_env();
1679            let mut fs = DirFS::new(temp_dir.path()).unwrap();
1680
1681            // Create a file in VFS
1682            fs.mkfile("/test.txt", Some(b"hello")).unwrap();
1683            assert!(fs.exists("/test.txt"));
1684            assert!(temp_dir.path().join("test.txt").exists());
1685
1686            // Remove it
1687            fs.rm("/test.txt").unwrap();
1688
1689            // Verify: VFS and filesystem are updated
1690            assert!(!fs.exists("/test.txt"));
1691            assert!(!temp_dir.path().join("test.txt").exists());
1692        }
1693
1694        #[test]
1695        fn test_rm_directory_recursive() {
1696            let temp_dir = setup_test_env();
1697            let mut fs = DirFS::new(temp_dir.path()).unwrap();
1698
1699            // Create nested structure
1700            fs.mkdir("/a/b/c").unwrap();
1701            fs.mkfile("/a/file1.txt", None).unwrap();
1702            fs.mkfile("/a/b/file2.txt", None).unwrap();
1703
1704            assert!(fs.exists("/a/b/c"));
1705            assert!(fs.exists("/a/file1.txt"));
1706            assert!(fs.exists("/a/b/file2.txt"));
1707
1708            // Remove top-level directory
1709            fs.rm("/a").unwrap();
1710
1711            // Verify everything is gone
1712            assert!(!fs.exists("/a"));
1713            assert!(!fs.exists("/a/b"));
1714            assert!(!fs.exists("/a/b/c"));
1715            assert!(!fs.exists("/a/file1.txt"));
1716            assert!(!fs.exists("/a/b/file2.txt"));
1717
1718            assert!(!temp_dir.path().join("a").exists());
1719        }
1720
1721        #[test]
1722        fn test_rm_nonexistent_path() {
1723            #[cfg(unix)]
1724            {
1725                let temp_dir = setup_test_env();
1726                let mut fs = DirFS::new(temp_dir.path()).unwrap();
1727
1728                let result = fs.rm("/not/found");
1729                assert!(result.is_err());
1730                assert_eq!(result.unwrap_err().to_string(), "/not/found does not exist");
1731            }
1732        }
1733
1734        #[test]
1735        fn test_rm_relative_path() {
1736            let temp_dir = setup_test_env();
1737            let mut fs = DirFS::new(temp_dir.path()).unwrap();
1738
1739            fs.mkdir("/parent").unwrap();
1740            fs.cd("/parent").unwrap();
1741            fs.mkfile("child.txt", None).unwrap();
1742
1743            assert!(fs.exists("/parent/child.txt"));
1744
1745            // Remove using relative path
1746            fs.rm("child.txt").unwrap();
1747
1748            assert!(!fs.exists("/parent/child.txt"));
1749            assert!(!temp_dir.path().join("parent/child.txt").exists());
1750        }
1751
1752        #[test]
1753        fn test_rm_empty_string_path() {
1754            let temp_dir = setup_test_env();
1755            let mut fs = DirFS::new(temp_dir.path()).unwrap();
1756
1757            let result = fs.rm("");
1758            assert!(result.is_err());
1759            assert_eq!(result.unwrap_err().to_string(), "invalid path: empty");
1760        }
1761
1762        #[test]
1763        fn test_rm_root_directory() {
1764            let temp_dir = setup_test_env();
1765            let mut fs = DirFS::new(temp_dir.path()).unwrap();
1766
1767            // Attempt to remove root '/'
1768            let result = fs.rm("/");
1769            assert!(result.is_err());
1770            assert_eq!(
1771                result.unwrap_err().to_string(),
1772                "invalid path: the root cannot be removed"
1773            );
1774
1775            // Root should still exist
1776            assert!(fs.exists("/"));
1777            assert!(temp_dir.path().exists());
1778        }
1779
1780        #[test]
1781        fn test_rm_trailing_slash() {
1782            let temp_dir = setup_test_env();
1783            let mut fs = DirFS::new(temp_dir.path()).unwrap();
1784
1785            fs.mkdir("/dir/").unwrap(); // With trailing slash
1786            fs.mkfile("/dir/file.txt", None).unwrap();
1787
1788            // Remove with trailing slash
1789            fs.rm("/dir/").unwrap();
1790
1791            assert!(!fs.exists("/dir"));
1792            assert!(!temp_dir.path().join("dir").exists());
1793        }
1794
1795        #[test]
1796        fn test_rm_unicode_path() {
1797            let temp_dir = setup_test_env();
1798            let mut fs = DirFS::new(temp_dir.path()).unwrap();
1799
1800            let unicode_path = "/папка/файл.txt";
1801            fs.mkdir("/папка").unwrap();
1802            fs.mkfile(unicode_path, None).unwrap();
1803
1804            assert!(fs.exists(unicode_path));
1805
1806            fs.rm(unicode_path).unwrap();
1807
1808            assert!(!fs.exists(unicode_path));
1809            assert!(!temp_dir.path().join("папка/файл.txt").exists());
1810        }
1811
1812        #[test]
1813        fn test_rm_permission_denied() {
1814            #[cfg(unix)]
1815            {
1816                use std::os::unix::fs::PermissionsExt;
1817
1818                let temp_dir = setup_test_env();
1819                let mut fs = DirFS::new(temp_dir.path()).unwrap();
1820                fs.mkdir("/protected").unwrap();
1821
1822                // Create a directory and restrict permissions
1823                let protected = fs.root().join("protected");
1824                std::fs::set_permissions(&protected, PermissionsExt::from_mode(0o000)).unwrap();
1825
1826                // Try to remove via VFS (should fail)
1827                let result = fs.rm("/protected");
1828                assert!(result.is_err());
1829                assert!(
1830                    result
1831                        .unwrap_err()
1832                        .to_string()
1833                        .contains("Permission denied")
1834                );
1835
1836                // Clean up: restore permissions
1837                std::fs::set_permissions(&protected, PermissionsExt::from_mode(0o755)).unwrap();
1838            }
1839        }
1840
1841        #[test]
1842        fn test_rm_symlink_file() {
1843            #[cfg(unix)]
1844            {
1845                use std::os::unix::fs::symlink;
1846
1847                let temp_dir = setup_test_env();
1848                let mut fs = DirFS::new(temp_dir.path()).unwrap();
1849
1850                // Create real file and symlink
1851                std::fs::write(temp_dir.path().join("real.txt"), "content").unwrap();
1852                symlink("real.txt", temp_dir.path().join("link.txt")).unwrap();
1853
1854                fs.mkfile("/link.txt", None).unwrap(); // Add symlink to VFS
1855                assert!(fs.exists("/link.txt"));
1856
1857                // Remove symlink (not the target)
1858                fs.rm("/link.txt").unwrap();
1859
1860                assert!(!fs.exists("/link.txt"));
1861                assert!(!temp_dir.path().join("link.txt").exists()); // Symlink gone
1862                assert!(temp_dir.path().join("real.txt").exists()); // Target still there
1863            }
1864        }
1865
1866        #[test]
1867        fn test_rm_after_cd() {
1868            let temp_dir = setup_test_env();
1869            let mut fs = DirFS::new(temp_dir.path()).unwrap();
1870
1871            fs.mkdir("/projects").unwrap();
1872            fs.cd("/projects").unwrap();
1873            fs.mkfile("notes.txt", None).unwrap();
1874
1875            assert!(fs.exists("/projects/notes.txt"));
1876
1877            // Remove from cwd using relative path
1878            fs.rm("notes.txt").unwrap();
1879
1880            assert!(!fs.exists("/projects/notes.txt"));
1881            assert!(!temp_dir.path().join("projects/notes.txt").exists());
1882        }
1883    }
1884
1885    mod cleanup {
1886        use super::*;
1887
1888        #[test]
1889        fn test_cleanup_ignores_is_auto_clean() {
1890            let temp_dir = setup_test_env();
1891            let root = temp_dir.path();
1892
1893            let mut fs = DirFS::new(root).unwrap();
1894            fs.is_auto_clean = false; // Clearly disabled
1895            fs.mkfile("/temp.txt", None).unwrap();
1896
1897            fs.cleanup(); // Must be removed despite is_auto_clean=false
1898
1899            assert!(!fs.exists("/temp.txt"));
1900            assert!(!root.join("temp.txt").exists());
1901        }
1902
1903        #[test]
1904        fn test_cleanup_preserves_root_and_parents() {
1905            let temp_dir = setup_test_env();
1906            let root = temp_dir.path().join("preserve_root");
1907
1908            let mut fs = DirFS::new(&root).unwrap();
1909            fs.mkdir("/subdir").unwrap();
1910            fs.mkfile("/subdir/file.txt", None).unwrap();
1911
1912            // created_root_parents is populated at initialization
1913            assert!(!fs.created_root_parents.is_empty());
1914
1915            fs.cleanup();
1916
1917            // Root and his parents remained
1918            assert!(root.exists());
1919            for parent in &fs.created_root_parents {
1920                assert!(parent.exists());
1921            }
1922
1923            // Only entries (except "/") were removed
1924            assert_eq!(fs.entries.len(), 1);
1925            assert!(fs.entries.contains(&PathBuf::from("/")));
1926        }
1927
1928        #[test]
1929        fn test_cleanup_empty_entries() {
1930            let temp_dir = setup_test_env();
1931            let root = temp_dir.path();
1932
1933            let mut fs = DirFS::new(root).unwrap();
1934            // entries contains only "/"
1935            assert_eq!(fs.entries.len(), 1);
1936
1937            fs.cleanup();
1938
1939            assert_eq!(fs.entries.len(), 1); // "/" remained
1940            assert!(fs.entries.contains(&PathBuf::from("/")));
1941            assert!(root.exists()); // The root is not removed
1942        }
1943    }
1944
1945    // Helper function: Creates a temporary directory for tests
1946    fn setup_test_env() -> TempDir {
1947        TempDir::new("dirfs_test").unwrap()
1948    }
1949}