Skip to main content

bashkit/fs/
mod.rs

1//! Virtual filesystem for Bashkit.
2//!
3//! This module provides a virtual filesystem abstraction that allows Bashkit to
4//! operate in a virtual environment without accessing the real filesystem.
5//!
6//! # Which Trait/Type Should I Use?
7//!
8//! ```text
9//! Do you need a custom filesystem?
10//!     │
11//!     ├─ NO → Use InMemoryFs (default with Bash::new())
12//!     │
13//!     └─ YES → Is your storage simple (key-value, database, cloud)?
14//!               │
15//!               ├─ YES → Implement FsBackend + wrap with PosixFs
16//!               │        (POSIX checks are automatic)
17//!               │
18//!               └─ NO → Implement FileSystem directly
19//!                       (full control, you handle all checks)
20//! ```
21//!
22//! # Architecture
23//!
24//! The filesystem abstraction has two layers:
25//!
26//! | Layer | Trait/Type | What You Implement |
27//! |-------|------------|-------------------|
28//! | Backend | [`FsBackend`] | Raw storage only (read/write/list) |
29//! | POSIX | [`FileSystem`] | Full POSIX semantics (type checks, parent dirs) |
30//!
31//! **[`PosixFs`]** bridges these: it wraps any `FsBackend` and provides `FileSystem`.
32//!
33//! # Implementing Custom Filesystems
34//!
35//! ## Option 1: `FsBackend` + `PosixFs` (Recommended)
36//!
37//! Best for: databases, cloud storage, simple key-value stores.
38//!
39//! ```rust,ignore
40//! use bashkit::{async_trait, FsBackend, PosixFs, Bash, Result, Metadata, DirEntry};
41//! use std::sync::Arc;
42//!
43//! // Implement raw storage operations
44//! struct MyStorage { /* ... */ }
45//!
46//! #[async_trait]
47//! impl FsBackend for MyStorage {
48//!     async fn read(&self, path: &Path) -> Result<Vec<u8>> { /* ... */ }
49//!     async fn write(&self, path: &Path, content: &[u8]) -> Result<()> { /* ... */ }
50//!     // ... other methods
51//! }
52//!
53//! // Wrap with PosixFs - POSIX semantics are automatic!
54//! let fs = Arc::new(PosixFs::new(MyStorage::new()));
55//! let mut bash = Bash::builder().fs(fs).build();
56//! ```
57//!
58//! ## Option 2: `FileSystem` Directly
59//!
60//! Best for: complex behavior, custom caching, specialized semantics.
61//!
62//! ```rust,ignore
63//! use bashkit::{async_trait, FileSystem, Bash};
64//!
65//! struct MyFs { /* ... */ }
66//!
67//! #[async_trait]
68//! impl FileSystem for MyFs {
69//!     async fn write_file(&self, path: &Path, content: &[u8]) -> Result<()> {
70//!         // You MUST check: is path a directory?
71//!         if self.is_directory(path) {
72//!             return Err(fs_errors::is_a_directory());
73//!         }
74//!         // ... write logic
75//!     }
76//!     // ... other methods with POSIX checks
77//! }
78//! ```
79//!
80//! See `examples/custom_backend.rs` and `examples/custom_filesystem_impl.rs`.
81//!
82//! # Built-in Implementations
83//!
84//! | Type | Description | Use Case |
85//! |------|-------------|----------|
86//! | [`InMemoryFs`] | HashMap-based storage with POSIX checks | Default, isolated execution |
87//! | [`OverlayFs`] | Copy-on-write layered filesystem | Templates, immutable bases |
88//! | [`MountableFs`] | Multiple filesystems at mount points | Complex multi-source setups |
89//!
90//! All implementations are thread-safe (`Send + Sync`) and fully async.
91//!
92//! # Quick Start
93//!
94//! ## Using InMemoryFs (Default)
95//!
96//! [`InMemoryFs`] is the default filesystem when using [`Bash::new()`](crate::Bash::new):
97//!
98//! ```rust
99//! use bashkit::Bash;
100//!
101//! # #[tokio::main]
102//! # async fn main() -> bashkit::Result<()> {
103//! let mut bash = Bash::new();
104//!
105//! // Files are stored entirely in memory
106//! bash.exec("echo 'hello' > /tmp/test.txt").await?;
107//! let result = bash.exec("cat /tmp/test.txt").await?;
108//! assert_eq!(result.stdout, "hello\n");
109//! # Ok(())
110//! # }
111//! ```
112//!
113//! ## Using OverlayFs
114//!
115//! [`OverlayFs`] provides copy-on-write semantics - reads fall through to a base
116//! layer while writes go to an overlay layer:
117//!
118//! ```rust
119//! use bashkit::{Bash, FileSystem, InMemoryFs, OverlayFs};
120//! use std::path::Path;
121//! use std::sync::Arc;
122//!
123//! # #[tokio::main]
124//! # async fn main() -> bashkit::Result<()> {
125//! // Create a base filesystem with template files
126//! let base = Arc::new(InMemoryFs::new());
127//! base.mkdir(Path::new("/templates"), false).await?;
128//! base.write_file(Path::new("/templates/config.txt"), b"default=true").await?;
129//!
130//! // Create overlay - base is read-only, changes go to overlay
131//! let overlay = Arc::new(OverlayFs::new(base.clone()));
132//!
133//! let mut bash = Bash::builder().fs(overlay).build();
134//!
135//! // Read from base layer
136//! let result = bash.exec("cat /templates/config.txt").await?;
137//! assert_eq!(result.stdout, "default=true");
138//!
139//! // Modify - changes go to overlay, base is unchanged
140//! bash.exec("echo 'modified=true' > /templates/config.txt").await?;
141//!
142//! // Base still has original content
143//! let original = base.read_file(Path::new("/templates/config.txt")).await?;
144//! assert_eq!(original, b"default=true");
145//! # Ok(())
146//! # }
147//! ```
148//!
149//! ## Using MountableFs
150//!
151//! [`MountableFs`] allows mounting different filesystems at specific paths:
152//!
153//! ```rust
154//! use bashkit::{Bash, FileSystem, InMemoryFs, MountableFs};
155//! use std::path::Path;
156//! use std::sync::Arc;
157//!
158//! # #[tokio::main]
159//! # async fn main() -> bashkit::Result<()> {
160//! // Create root and a separate data filesystem
161//! let root = Arc::new(InMemoryFs::new());
162//! let data_fs = Arc::new(InMemoryFs::new());
163//!
164//! // Pre-populate the data filesystem
165//! data_fs.write_file(Path::new("/users.json"), br#"["alice", "bob"]"#).await?;
166//!
167//! // Create mountable filesystem and mount data_fs at /mnt/data
168//! let mountable = MountableFs::new(root);
169//! mountable.mount("/mnt/data", data_fs)?;
170//!
171//! let mut bash = Bash::builder().fs(Arc::new(mountable)).build();
172//!
173//! // Access the mounted filesystem
174//! let result = bash.exec("cat /mnt/data/users.json").await?;
175//! assert!(result.stdout.contains("alice"));
176//! # Ok(())
177//! # }
178//! ```
179//!
180//! # Direct Filesystem Access
181//!
182//! Access the filesystem directly for pre-populating files or reading output:
183//!
184//! ```rust
185//! use bashkit::{Bash, FileSystem};
186//! use std::path::Path;
187//!
188//! # #[tokio::main]
189//! # async fn main() -> bashkit::Result<()> {
190//! let mut bash = Bash::new();
191//! let fs = bash.fs();
192//!
193//! // Create directories
194//! fs.mkdir(Path::new("/data"), false).await?;
195//! fs.mkdir(Path::new("/data/input"), false).await?;
196//! fs.mkdir(Path::new("/data/output"), false).await?;
197//!
198//! // Pre-populate input files
199//! fs.write_file(Path::new("/data/input/data.csv"), b"name,value\nalice,100").await?;
200//!
201//! // Run a script that processes the data
202//! bash.exec("cat /data/input/data.csv | grep alice > /data/output/result.txt").await?;
203//!
204//! // Read the output directly
205//! let output = fs.read_file(Path::new("/data/output/result.txt")).await?;
206//! assert_eq!(output, b"alice,100\n");
207//!
208//! // Check file exists
209//! assert!(fs.exists(Path::new("/data/output/result.txt")).await?);
210//!
211//! // Get file metadata
212//! let stat = fs.stat(Path::new("/data/output/result.txt")).await?;
213//! assert!(stat.file_type.is_file());
214//!
215//! // List directory contents
216//! let entries = fs.read_dir(Path::new("/data")).await?;
217//! let names: Vec<_> = entries.iter().map(|e| e.name.as_str()).collect();
218//! assert!(names.contains(&"input"));
219//! assert!(names.contains(&"output"));
220//! # Ok(())
221//! # }
222//! ```
223//!
224//! # Binary File Support
225//!
226//! The filesystem fully supports binary data including null bytes:
227//!
228//! ```rust
229//! use bashkit::{Bash, FileSystem};
230//! use std::path::Path;
231//!
232//! # #[tokio::main]
233//! # async fn main() -> bashkit::Result<()> {
234//! let bash = Bash::new();
235//! let fs = bash.fs();
236//!
237//! // Write binary data (e.g., PNG header)
238//! let binary_data = vec![0x89, 0x50, 0x4E, 0x47, 0x00, 0xFF];
239//! fs.write_file(Path::new("/tmp/image.bin"), &binary_data).await?;
240//!
241//! // Read it back
242//! let content = fs.read_file(Path::new("/tmp/image.bin")).await?;
243//! assert_eq!(content, binary_data);
244//! # Ok(())
245//! # }
246//! ```
247//!
248//! # Implementing Custom Filesystems
249//!
250//! Implement the [`FileSystem`] trait to create custom storage backends:
251//!
252//! ```rust
253//! use bashkit::{async_trait, FileSystem, DirEntry, Metadata, FileType, Result, Error};
254//! use std::path::{Path, PathBuf};
255//! use std::collections::HashMap;
256//! use std::sync::RwLock;
257//! use std::time::SystemTime;
258//!
259//! /// A simple custom filesystem example
260//! pub struct SimpleFs {
261//!     files: RwLock<HashMap<PathBuf, Vec<u8>>>,
262//! }
263//!
264//! impl SimpleFs {
265//!     pub fn new() -> Self {
266//!         let mut files = HashMap::new();
267//!         // Initialize with root and common directories
268//!         files.insert(PathBuf::from("/"), Vec::new());
269//!         files.insert(PathBuf::from("/tmp"), Vec::new());
270//!         Self { files: RwLock::new(files) }
271//!     }
272//! }
273//!
274//! #[async_trait]
275//! impl FileSystem for SimpleFs {
276//!     async fn read_file(&self, path: &Path) -> Result<Vec<u8>> {
277//!         let files = self.files.read().unwrap();
278//!         files.get(path)
279//!             .cloned()
280//!             .ok_or_else(|| Error::Io(std::io::Error::new(
281//!                 std::io::ErrorKind::NotFound,
282//!                 "file not found"
283//!             )))
284//!     }
285//!
286//!     async fn write_file(&self, path: &Path, content: &[u8]) -> Result<()> {
287//!         let mut files = self.files.write().unwrap();
288//!         files.insert(path.to_path_buf(), content.to_vec());
289//!         Ok(())
290//!     }
291//!
292//!     // ... implement remaining methods
293//! #   async fn append_file(&self, _path: &Path, _content: &[u8]) -> Result<()> { Ok(()) }
294//! #   async fn mkdir(&self, _path: &Path, _recursive: bool) -> Result<()> { Ok(()) }
295//! #   async fn remove(&self, _path: &Path, _recursive: bool) -> Result<()> { Ok(()) }
296//! #   async fn stat(&self, _path: &Path) -> Result<Metadata> {
297//! #       Ok(Metadata::default())
298//! #   }
299//! #   async fn read_dir(&self, _path: &Path) -> Result<Vec<DirEntry>> { Ok(vec![]) }
300//! #   async fn exists(&self, _path: &Path) -> Result<bool> { Ok(false) }
301//! #   async fn rename(&self, _from: &Path, _to: &Path) -> Result<()> { Ok(()) }
302//! #   async fn copy(&self, _from: &Path, _to: &Path) -> Result<()> { Ok(()) }
303//! #   async fn symlink(&self, _target: &Path, _link: &Path) -> Result<()> { Ok(()) }
304//! #   async fn read_link(&self, _path: &Path) -> Result<PathBuf> { Ok(PathBuf::new()) }
305//! #   async fn chmod(&self, _path: &Path, _mode: u32) -> Result<()> { Ok(()) }
306//! }
307//! ```
308//!
309//! For a complete custom filesystem implementation example, see
310//! `examples/custom_filesystem_impl.rs`.
311//!
312//! # Default Directory Structure
313//!
314//! [`InMemoryFs::new()`] creates these directories by default:
315//!
316//! - `/` - Root directory
317//! - `/tmp` - Temporary files
318//! - `/home` - Home directories
319//! - `/home/user` - Default user home
320//! - `/dev` - Device files
321//! - `/dev/null` - Null device (discards writes, returns empty on read)
322//!
323//! # Requirements for Custom FileSystem Implementations
324//!
325//! When implementing [`FileSystem`] for custom storage backends, your implementation
326//! **must** ensure:
327//!
328//! 1. **Root directory exists**: `exists("/")` must return `true`
329//! 2. **Path normalization**: Paths like `/.`, `/tmp/..`, etc. must resolve correctly
330//! 3. **Root is listable**: `read_dir("/")` must return the root's contents
331//!
332//! Without these, commands like `cd /` and `ls /` will fail with "No such file or directory".
333//!
334//! Use [`verify_filesystem_requirements`] to test your implementation:
335//!
336//! ```rust
337//! use bashkit::{verify_filesystem_requirements, InMemoryFs};
338//! use std::sync::Arc;
339//!
340//! # #[tokio::main]
341//! # async fn main() -> bashkit::Result<()> {
342//! let fs = Arc::new(InMemoryFs::new());
343//! verify_filesystem_requirements(&*fs).await?;
344//! # Ok(())
345//! # }
346//! ```
347
348mod backend;
349mod limits;
350mod memory;
351mod mountable;
352mod overlay;
353mod posix;
354mod traits;
355
356pub use backend::FsBackend;
357pub use limits::{FsLimitExceeded, FsLimits, FsUsage};
358pub use memory::InMemoryFs;
359pub use mountable::MountableFs;
360pub use overlay::OverlayFs;
361pub use posix::PosixFs;
362#[allow(unused_imports)]
363pub use traits::{fs_errors, DirEntry, FileSystem, FileType, Metadata};
364
365use crate::error::Result;
366use std::io::{Error as IoError, ErrorKind};
367use std::path::Path;
368
369/// Verify that a filesystem implementation meets minimum requirements for Bashkit.
370///
371/// This function checks that your custom [`FileSystem`] implementation:
372/// - Has root directory `/` that exists
373/// - Can stat the root directory
374/// - Can list the root directory contents
375/// - Handles path normalization (e.g., `/.` resolves to `/`)
376///
377/// # Errors
378///
379/// Returns an error describing what requirement is not met.
380///
381/// # Example
382///
383/// ```rust
384/// use bashkit::{verify_filesystem_requirements, InMemoryFs};
385/// use std::sync::Arc;
386///
387/// # #[tokio::main]
388/// # async fn main() -> bashkit::Result<()> {
389/// let fs = Arc::new(InMemoryFs::new());
390/// verify_filesystem_requirements(&*fs).await?;
391/// println!("Filesystem meets all requirements!");
392/// # Ok(())
393/// # }
394/// ```
395pub async fn verify_filesystem_requirements(fs: &dyn FileSystem) -> Result<()> {
396    // Check 1: Root directory must exist
397    if !fs.exists(Path::new("/")).await? {
398        return Err(IoError::new(
399            ErrorKind::NotFound,
400            "FileSystem requirement not met: root directory '/' does not exist. \
401             Custom FileSystem implementations must ensure '/' exists on creation.",
402        )
403        .into());
404    }
405
406    // Check 2: Root must be a directory
407    let stat = fs.stat(Path::new("/")).await.map_err(|_| {
408        IoError::new(
409            ErrorKind::NotFound,
410            "FileSystem requirement not met: cannot stat root directory '/'. \
411             Ensure stat() works for the root path.",
412        )
413    })?;
414
415    if !stat.file_type.is_dir() {
416        return Err(IoError::new(
417            ErrorKind::InvalidData,
418            "FileSystem requirement not met: root '/' is not a directory.",
419        )
420        .into());
421    }
422
423    // Check 3: Root must be listable
424    fs.read_dir(Path::new("/")).await.map_err(|_| {
425        IoError::new(
426            ErrorKind::NotFound,
427            "FileSystem requirement not met: cannot list root directory '/'. \
428             Ensure read_dir() works for the root path.",
429        )
430    })?;
431
432    // Check 4: Path normalization - "/." should resolve to "/"
433    if !fs.exists(Path::new("/.")).await? {
434        return Err(IoError::new(
435            ErrorKind::NotFound,
436            "FileSystem requirement not met: path '/.' does not resolve to root. \
437             Ensure your implementation normalizes paths (removes '.' components).",
438        )
439        .into());
440    }
441
442    Ok(())
443}