use bashkit::{Bash, DirEntry, FileType, FsBackend, Metadata, PosixFs, Result, async_trait};
use std::collections::HashMap;
use std::io::{Error as IoError, ErrorKind};
use std::path::{Path, PathBuf};
use std::sync::RwLock;
use std::time::SystemTime;
#[derive(Clone)]
enum StorageEntry {
File(Vec<u8>),
Directory,
}
pub struct SimpleStorage {
entries: RwLock<HashMap<PathBuf, StorageEntry>>,
}
impl SimpleStorage {
pub fn new() -> Self {
let mut entries = HashMap::new();
entries.insert(PathBuf::from("/"), StorageEntry::Directory);
entries.insert(PathBuf::from("/tmp"), StorageEntry::Directory);
entries.insert(PathBuf::from("/home"), StorageEntry::Directory);
entries.insert(PathBuf::from("/home/user"), StorageEntry::Directory);
Self {
entries: RwLock::new(entries),
}
}
fn normalize(path: &Path) -> PathBuf {
let mut result = PathBuf::from("/");
for component in path.components() {
match component {
std::path::Component::Normal(name) => result.push(name),
std::path::Component::ParentDir => {
result.pop();
}
_ => {}
}
}
if result.as_os_str().is_empty() {
result.push("/");
}
result
}
}
impl Default for SimpleStorage {
fn default() -> Self {
Self::new()
}
}
#[async_trait]
impl FsBackend for SimpleStorage {
async fn read(&self, path: &Path) -> Result<Vec<u8>> {
let path = Self::normalize(path);
let entries = self.entries.read().unwrap();
match entries.get(&path) {
Some(StorageEntry::File(content)) => Ok(content.clone()),
Some(StorageEntry::Directory) => Err(IoError::other("is a directory").into()),
None => Err(IoError::new(ErrorKind::NotFound, "file not found").into()),
}
}
async fn write(&self, path: &Path, content: &[u8]) -> Result<()> {
let path = Self::normalize(path);
let mut entries = self.entries.write().unwrap();
entries.insert(path, StorageEntry::File(content.to_vec()));
Ok(())
}
async fn append(&self, path: &Path, content: &[u8]) -> Result<()> {
let path = Self::normalize(path);
let mut entries = self.entries.write().unwrap();
match entries.get_mut(&path) {
Some(StorageEntry::File(existing)) => {
existing.extend_from_slice(content);
Ok(())
}
Some(StorageEntry::Directory) => Err(IoError::other("is a directory").into()),
None => {
entries.insert(path, StorageEntry::File(content.to_vec()));
Ok(())
}
}
}
async fn mkdir(&self, path: &Path, recursive: bool) -> Result<()> {
let path = Self::normalize(path);
if recursive {
let mut current = PathBuf::from("/");
let mut entries = self.entries.write().unwrap();
for component in path.components().skip(1) {
current.push(component);
entries
.entry(current.clone())
.or_insert(StorageEntry::Directory);
}
} else {
let mut entries = self.entries.write().unwrap();
if entries.contains_key(&path) {
return Err(IoError::new(ErrorKind::AlreadyExists, "already exists").into());
}
entries.insert(path, StorageEntry::Directory);
}
Ok(())
}
async fn remove(&self, path: &Path, recursive: bool) -> Result<()> {
let path = Self::normalize(path);
let mut entries = self.entries.write().unwrap();
if !entries.contains_key(&path) {
return Err(IoError::new(ErrorKind::NotFound, "not found").into());
}
if recursive {
let path_str = path.to_string_lossy().to_string();
let prefix = if path_str == "/" {
"/".to_string()
} else {
format!("{}/", path_str)
};
let to_remove: Vec<_> = entries
.keys()
.filter(|k| {
let k_str = k.to_string_lossy();
k_str.starts_with(&prefix) || *k == &path
})
.cloned()
.collect();
for key in to_remove {
entries.remove(&key);
}
} else {
entries.remove(&path);
}
Ok(())
}
async fn stat(&self, path: &Path) -> Result<Metadata> {
let path = Self::normalize(path);
let entries = self.entries.read().unwrap();
match entries.get(&path) {
Some(StorageEntry::File(content)) => Ok(Metadata {
file_type: FileType::File,
size: content.len() as u64,
mode: 0o644,
modified: SystemTime::now(),
created: SystemTime::now(),
}),
Some(StorageEntry::Directory) => Ok(Metadata {
file_type: FileType::Directory,
size: 0,
mode: 0o755,
modified: SystemTime::now(),
created: SystemTime::now(),
}),
None => Err(IoError::new(ErrorKind::NotFound, "not found").into()),
}
}
async fn read_dir(&self, path: &Path) -> Result<Vec<DirEntry>> {
let path = Self::normalize(path);
let entries = self.entries.read().unwrap();
if !entries.contains_key(&path) {
return Err(IoError::new(ErrorKind::NotFound, "not found").into());
}
let path_str = path.to_string_lossy().to_string();
let prefix = if path_str == "/" {
"/".to_string()
} else {
format!("{}/", path_str)
};
let mut result = Vec::new();
let mut seen = std::collections::HashSet::new();
for (entry_path, entry) in entries.iter() {
let entry_str = entry_path.to_string_lossy();
if entry_str.starts_with(&prefix) && entry_path != &path {
let remainder = &entry_str[prefix.len()..];
let name = remainder.split('/').next().unwrap_or("");
if !name.is_empty() && seen.insert(name.to_string()) {
let (file_type, size) = match entry {
StorageEntry::File(content) => (FileType::File, content.len() as u64),
StorageEntry::Directory => (FileType::Directory, 0),
};
result.push(DirEntry {
name: name.to_string(),
metadata: Metadata {
file_type,
size,
mode: 0o644,
modified: SystemTime::now(),
created: SystemTime::now(),
},
});
}
}
}
result.sort_by(|a, b| a.name.cmp(&b.name));
Ok(result)
}
async fn exists(&self, path: &Path) -> Result<bool> {
let path = Self::normalize(path);
let entries = self.entries.read().unwrap();
Ok(entries.contains_key(&path))
}
async fn rename(&self, from: &Path, to: &Path) -> Result<()> {
let from = Self::normalize(from);
let to = Self::normalize(to);
let mut entries = self.entries.write().unwrap();
if let Some(entry) = entries.remove(&from) {
entries.insert(to, entry);
Ok(())
} else {
Err(IoError::new(ErrorKind::NotFound, "source not found").into())
}
}
async fn copy(&self, from: &Path, to: &Path) -> Result<()> {
let from = Self::normalize(from);
let to = Self::normalize(to);
let entries = self.entries.read().unwrap();
if let Some(entry) = entries.get(&from) {
let entry = entry.clone();
drop(entries);
let mut entries = self.entries.write().unwrap();
entries.insert(to, entry);
Ok(())
} else {
Err(IoError::new(ErrorKind::NotFound, "source not found").into())
}
}
async fn symlink(&self, target: &Path, link: &Path) -> Result<()> {
let link = Self::normalize(link);
let content = format!("SYMLINK:{}", target.display()).into_bytes();
let mut entries = self.entries.write().unwrap();
entries.insert(link, StorageEntry::File(content));
Ok(())
}
async fn read_link(&self, path: &Path) -> Result<PathBuf> {
let path = Self::normalize(path);
let entries = self.entries.read().unwrap();
if let Some(StorageEntry::File(content)) = entries.get(&path) {
let content_str = String::from_utf8_lossy(content);
if let Some(target) = content_str.strip_prefix("SYMLINK:") {
return Ok(PathBuf::from(target));
}
}
Err(IoError::other("not a symbolic link").into())
}
async fn chmod(&self, path: &Path, _mode: u32) -> Result<()> {
let path = Self::normalize(path);
let entries = self.entries.read().unwrap();
if entries.contains_key(&path) {
Ok(())
} else {
Err(IoError::new(ErrorKind::NotFound, "not found").into())
}
}
}
#[tokio::main]
async fn main() -> anyhow::Result<()> {
println!("=== FsBackend + PosixFs Example ===\n");
let backend = SimpleStorage::new();
let fs = std::sync::Arc::new(PosixFs::new(backend));
let mut bash = Bash::builder().fs(fs.clone()).build();
println!("1. Basic file operations:");
bash.exec("echo 'Hello, World!' > /tmp/hello.txt").await?;
let result = bash.exec("cat /tmp/hello.txt").await?;
println!(" {}", result.stdout.trim());
println!("\n2. POSIX semantics are enforced by PosixFs:");
bash.exec("mkdir -p /tmp/mydir").await?;
let result = bash
.exec("echo test > /tmp/mydir 2>/dev/null; echo $?")
.await?;
println!(" Write to directory: exit code {}", result.stdout.trim());
bash.exec("echo test > /tmp/myfile").await?;
let result = bash.exec("mkdir /tmp/myfile 2>/dev/null; echo $?").await?;
println!(" mkdir on file: exit code {}", result.stdout.trim());
println!("\n3. The backend doesn't need POSIX checks - PosixFs handles them!");
println!(" This makes implementing custom backends much simpler.");
println!("\n=== Complete ===");
println!("\nCompare this to custom_filesystem_impl.rs which implements");
println!("FileSystem directly with all POSIX checks inline.");
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use bashkit::FileSystem;
use std::sync::Arc;
#[tokio::test]
async fn test_posix_wrapper_type_checks() {
let backend = SimpleStorage::new();
let fs = PosixFs::new(backend);
fs.mkdir(Path::new("/tmp/testdir"), false).await.unwrap();
let result = fs.write_file(Path::new("/tmp/testdir"), b"test").await;
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("directory"));
}
#[tokio::test]
async fn test_posix_wrapper_mkdir_on_file() {
let backend = SimpleStorage::new();
let fs = PosixFs::new(backend);
fs.write_file(Path::new("/tmp/testfile"), b"test")
.await
.unwrap();
let result = fs.mkdir(Path::new("/tmp/testfile"), false).await;
assert!(result.is_err());
}
#[tokio::test]
async fn test_with_bash() {
let backend = SimpleStorage::new();
let fs = Arc::new(PosixFs::new(backend));
let mut bash = Bash::builder().fs(fs).build();
let result = bash
.exec("echo hello > /tmp/test.txt && cat /tmp/test.txt")
.await
.unwrap();
assert_eq!(result.stdout, "hello\n");
}
}