Skip to main content

rust_bash/vfs/
mod.rs

1mod memory;
2mod mountable;
3
4#[cfg(feature = "native-fs")]
5mod overlay;
6#[cfg(feature = "native-fs")]
7mod readwrite;
8
9#[cfg(test)]
10mod tests;
11
12#[cfg(all(test, feature = "native-fs"))]
13mod readwrite_tests;
14
15#[cfg(all(test, feature = "native-fs"))]
16mod overlay_tests;
17
18#[cfg(test)]
19mod mountable_tests;
20
21pub use memory::InMemoryFs;
22pub use mountable::MountableFs;
23
24#[cfg(feature = "native-fs")]
25pub use overlay::OverlayFs;
26#[cfg(feature = "native-fs")]
27pub use readwrite::ReadWriteFs;
28
29use crate::error::VfsError;
30use crate::platform::SystemTime;
31use std::path::{Path, PathBuf};
32use std::sync::Arc;
33
34/// VFS paths always use Unix-style `/` separators. `std::path::Path::is_absolute()`
35/// is platform-dependent and returns `false` on `wasm32-unknown-unknown` even for
36/// `/home/user`, so we roll our own check.
37pub(crate) fn vfs_path_is_absolute(path: &Path) -> bool {
38    path.to_str().is_some_and(|s| s.starts_with('/'))
39}
40
41/// Metadata for a filesystem node.
42#[derive(Debug, Clone, PartialEq, Eq)]
43pub struct Metadata {
44    pub node_type: NodeType,
45    pub size: u64,
46    pub mode: u32,
47    pub mtime: SystemTime,
48}
49
50/// The type of a filesystem node (without content).
51#[derive(Debug, Clone, Copy, PartialEq, Eq)]
52pub enum NodeType {
53    File,
54    Directory,
55    Symlink,
56}
57
58/// An entry returned by `readdir`.
59#[derive(Debug, Clone, PartialEq, Eq)]
60pub struct DirEntry {
61    pub name: String,
62    pub node_type: NodeType,
63}
64
65/// In-memory representation of a filesystem node.
66#[derive(Debug, Clone)]
67pub enum FsNode {
68    File {
69        content: Vec<u8>,
70        mode: u32,
71        mtime: SystemTime,
72    },
73    Directory {
74        children: std::collections::BTreeMap<String, FsNode>,
75        mode: u32,
76        mtime: SystemTime,
77    },
78    Symlink {
79        target: PathBuf,
80        mtime: SystemTime,
81    },
82}
83
84/// Options that modify glob expansion behavior.
85#[derive(Debug, Clone, Default)]
86pub struct GlobOptions {
87    /// Include dot-files even when the pattern doesn't start with `.`.
88    pub dotglob: bool,
89    /// Use case-insensitive matching for filenames.
90    pub nocaseglob: bool,
91    /// Treat `**` as recursive directory match (globstar).
92    /// When false, `**` is treated as `*`.
93    pub globstar: bool,
94    /// Enable extended glob patterns: `@(...)`, `+(...)`, `*(...)`, `?(...)`, `!(...)`.
95    pub extglob: bool,
96}
97
98/// Trait abstracting all filesystem operations.
99///
100/// All methods take `&self` — implementations use interior mutability.
101/// All paths are expected to be absolute.
102pub trait VirtualFs: Send + Sync {
103    // File CRUD
104    fn read_file(&self, path: &Path) -> Result<Vec<u8>, VfsError>;
105    fn write_file(&self, path: &Path, content: &[u8]) -> Result<(), VfsError>;
106    fn append_file(&self, path: &Path, content: &[u8]) -> Result<(), VfsError>;
107    fn remove_file(&self, path: &Path) -> Result<(), VfsError>;
108
109    // Directory operations
110    fn mkdir(&self, path: &Path) -> Result<(), VfsError>;
111    fn mkdir_p(&self, path: &Path) -> Result<(), VfsError>;
112    fn readdir(&self, path: &Path) -> Result<Vec<DirEntry>, VfsError>;
113    fn remove_dir(&self, path: &Path) -> Result<(), VfsError>;
114    fn remove_dir_all(&self, path: &Path) -> Result<(), VfsError>;
115
116    // Metadata and permissions
117    fn exists(&self, path: &Path) -> bool;
118    fn stat(&self, path: &Path) -> Result<Metadata, VfsError>;
119    fn lstat(&self, path: &Path) -> Result<Metadata, VfsError>;
120    fn chmod(&self, path: &Path, mode: u32) -> Result<(), VfsError>;
121    fn utimes(&self, path: &Path, mtime: SystemTime) -> Result<(), VfsError>;
122
123    // Links
124    fn symlink(&self, target: &Path, link: &Path) -> Result<(), VfsError>;
125    fn hardlink(&self, src: &Path, dst: &Path) -> Result<(), VfsError>;
126    fn readlink(&self, path: &Path) -> Result<PathBuf, VfsError>;
127
128    // Path resolution
129    fn canonicalize(&self, path: &Path) -> Result<PathBuf, VfsError>;
130
131    // File operations
132    fn copy(&self, src: &Path, dst: &Path) -> Result<(), VfsError>;
133    fn rename(&self, src: &Path, dst: &Path) -> Result<(), VfsError>;
134
135    // Glob expansion (stub for now)
136    fn glob(&self, pattern: &str, cwd: &Path) -> Result<Vec<PathBuf>, VfsError>;
137
138    /// Glob expansion with shopt-controlled options (dotglob, nocaseglob, globstar).
139    ///
140    /// The default implementation ignores options and delegates to `glob()`.
141    /// Override in backends that can honor the options.
142    fn glob_with_opts(
143        &self,
144        pattern: &str,
145        cwd: &Path,
146        _opts: &GlobOptions,
147    ) -> Result<Vec<PathBuf>, VfsError> {
148        self.glob(pattern, cwd)
149    }
150
151    /// Create an independent deep copy for subshell isolation.
152    ///
153    /// Subshells `( ... )` and command substitutions `$(...)` need an isolated
154    /// filesystem so their mutations don't leak back to the parent. Each backend
155    /// decides what "independent copy" means:
156    /// - InMemoryFs: clones the entire tree
157    /// - OverlayFs: clones the upper layer and whiteouts; lower is shared
158    /// - ReadWriteFs: no isolation (returns Arc::clone — writes hit real FS)
159    /// - MountableFs: recursively deep-clones each mount
160    fn deep_clone(&self) -> Arc<dyn VirtualFs>;
161}