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}