use std::io::{self, Read};
#[cfg(test)]
use std::path::Path;
#[derive(Debug, Clone)]
pub(super) struct DirEntry {
pub name: String,
pub path: String,
pub is_dir: bool,
}
pub(super) trait VfsFileSystem: Send + Sync + 'static {
fn open_file(&self, path: &str) -> io::Result<Box<dyn Read + Send>>;
fn read_file(&self, path: &str) -> io::Result<Vec<u8>> {
let mut reader = self.open_file(path)?;
let mut buffer = Vec::new();
reader.read_to_end(&mut buffer)?;
Ok(buffer)
}
fn read_dir(&self, path: &str) -> io::Result<Vec<DirEntry>>;
fn exists(&self, path: &str) -> bool;
fn is_dir(&self, path: &str) -> bool;
}
#[cfg(not(target_arch = "wasm32"))]
#[derive(Debug)]
pub(super) struct PhysicalVfs {
root: vfs::VfsPath,
}
#[cfg(not(target_arch = "wasm32"))]
impl PhysicalVfs {
pub(super) fn new() -> Self {
let root = vfs::PhysicalFS::new("/").into();
Self { root }
}
fn get_path(&self, path: &str) -> io::Result<vfs::VfsPath> {
self.root
.join(path)
.map_err(|e| io::Error::other(format!("Invalid path '{path}': {e}")))
}
}
#[cfg(not(target_arch = "wasm32"))]
impl Default for PhysicalVfs {
fn default() -> Self {
Self::new()
}
}
#[cfg(not(target_arch = "wasm32"))]
impl VfsFileSystem for PhysicalVfs {
fn open_file(&self, path: &str) -> io::Result<Box<dyn Read + Send>> {
let vfs_path = self.get_path(path)?;
let file = vfs_path
.open_file()
.map_err(|e| io::Error::new(io::ErrorKind::NotFound, e))?;
Ok(Box::new(file))
}
fn read_dir(&self, path: &str) -> io::Result<Vec<DirEntry>> {
let vfs_path = self.get_path(path)?;
let entries = vfs_path
.read_dir()
.map_err(|e| io::Error::new(io::ErrorKind::NotFound, e))?;
let mut result = Vec::new();
for entry in entries {
let metadata = entry.metadata().map_err(io::Error::other)?;
let filename = entry.filename();
let full_path = entry.as_str().to_string();
result.push(DirEntry {
name: filename,
path: full_path,
is_dir: metadata.file_type == vfs::VfsFileType::Directory,
});
}
Ok(result)
}
fn exists(&self, path: &str) -> bool {
self.get_path(path)
.is_ok_and(|p| p.exists().unwrap_or(false))
}
fn is_dir(&self, path: &str) -> bool {
self.get_path(path)
.ok()
.and_then(|p| p.metadata().ok())
.is_some_and(|m| m.file_type == vfs::VfsFileType::Directory)
}
}
#[cfg(all(not(target_arch = "wasm32"), test))]
impl PhysicalVfs {
pub(super) fn is_file(&self, path: &str) -> bool {
self.get_path(path)
.ok()
.and_then(|p| p.metadata().ok())
.is_some_and(|m| m.file_type == vfs::VfsFileType::File)
}
}
#[cfg(test)]
#[derive(Debug)]
pub(super) struct MemoryVfs {
root: vfs::VfsPath,
}
#[cfg(test)]
impl MemoryVfs {
pub(super) fn new() -> Self {
let root = vfs::MemoryFS::new().into();
Self { root }
}
fn get_path(&self, path: &str) -> io::Result<vfs::VfsPath> {
self.root
.join(path)
.map_err(|e| io::Error::other(format!("Invalid path '{path}': {e}")))
}
pub(super) fn create_file(&self, path: &str, content: &[u8]) -> io::Result<()> {
let vfs_path = self.get_path(path)?;
if let Some(parent) = Path::new(path).parent()
&& !parent.as_os_str().is_empty()
{
let parent_str = parent.to_str().ok_or_else(|| {
io::Error::new(io::ErrorKind::InvalidInput, "Invalid parent path")
})?;
self.create_dir_all(parent_str)?;
}
let mut file = vfs_path.create_file().map_err(io::Error::other)?;
std::io::Write::write_all(&mut file, content)?;
Ok(())
}
pub(super) fn create_dir_all(&self, path: &str) -> io::Result<()> {
let vfs_path = self.get_path(path)?;
vfs_path.create_dir_all().map_err(io::Error::other)
}
}
#[cfg(test)]
impl Default for MemoryVfs {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
impl VfsFileSystem for MemoryVfs {
fn open_file(&self, path: &str) -> io::Result<Box<dyn Read + Send>> {
let vfs_path = self.get_path(path)?;
let file = vfs_path
.open_file()
.map_err(|e| io::Error::new(io::ErrorKind::NotFound, e))?;
Ok(Box::new(file))
}
fn read_dir(&self, path: &str) -> io::Result<Vec<DirEntry>> {
let vfs_path = self.get_path(path)?;
let entries = vfs_path
.read_dir()
.map_err(|e| io::Error::new(io::ErrorKind::NotFound, e))?;
let mut result = Vec::new();
for entry in entries {
let metadata = entry.metadata().map_err(io::Error::other)?;
let filename = entry.filename();
let full_path = entry.as_str().to_string();
result.push(DirEntry {
name: filename,
path: full_path,
is_dir: metadata.file_type == vfs::VfsFileType::Directory,
});
}
Ok(result)
}
fn exists(&self, path: &str) -> bool {
self.get_path(path)
.is_ok_and(|p| p.exists().unwrap_or(false))
}
fn is_dir(&self, path: &str) -> bool {
self.get_path(path)
.ok()
.and_then(|p| p.metadata().ok())
.is_some_and(|m| m.file_type == vfs::VfsFileType::Directory)
}
}
#[cfg(test)]
impl MemoryVfs {
pub(super) fn is_file(&self, path: &str) -> bool {
self.get_path(path)
.ok()
.and_then(|p| p.metadata().ok())
.is_some_and(|m| m.file_type == vfs::VfsFileType::File)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_memory_vfs_create_and_read_file() {
let vfs = MemoryVfs::new();
let content = b"test content";
vfs.create_file("/test.txt", content).unwrap();
assert!(vfs.exists("/test.txt"));
assert!(vfs.is_file("/test.txt"));
assert!(!vfs.is_dir("/test.txt"));
let read_content = vfs.read_file("/test.txt").unwrap();
assert_eq!(read_content, content);
}
#[test]
fn test_memory_vfs_create_dir() {
let vfs = MemoryVfs::new();
vfs.create_dir_all("/test/nested/dir").unwrap();
assert!(vfs.exists("/test"));
assert!(vfs.is_dir("/test"));
assert!(!vfs.is_file("/test"));
assert!(vfs.exists("/test/nested/dir"));
assert!(vfs.is_dir("/test/nested/dir"));
}
#[test]
fn test_memory_vfs_read_dir() {
let vfs = MemoryVfs::new();
vfs.create_file("/test/file1.txt", b"content1").unwrap();
vfs.create_file("/test/file2.txt", b"content2").unwrap();
vfs.create_dir_all("/test/subdir").unwrap();
let entries = vfs.read_dir("/test").unwrap();
assert_eq!(entries.len(), 3);
let names: Vec<String> = entries.iter().map(|e| e.name.clone()).collect();
assert!(names.contains(&"file1.txt".to_string()));
assert!(names.contains(&"file2.txt".to_string()));
assert!(names.contains(&"subdir".to_string()));
}
#[test]
fn test_memory_vfs_nonexistent_file() {
let vfs = MemoryVfs::new();
assert!(!vfs.exists("/nonexistent.txt"));
let result = vfs.read_file("/nonexistent.txt");
result.expect_err("Expected error when reading nonexistent file");
}
#[cfg(not(target_arch = "wasm32"))]
#[test]
fn test_physical_vfs_exists() {
use std::fs;
use tempfile::TempDir;
let temp_dir = TempDir::new().unwrap();
let test_file = temp_dir.path().join("test.txt");
fs::write(&test_file, b"test").unwrap();
let vfs = PhysicalVfs::new();
let path_str = test_file.to_str().unwrap();
assert!(vfs.exists(path_str));
assert!(vfs.is_file(path_str));
assert!(!vfs.is_dir(path_str));
}
#[cfg(not(target_arch = "wasm32"))]
#[test]
fn test_physical_vfs_read_file() {
use std::fs;
use tempfile::TempDir;
let temp_dir = TempDir::new().unwrap();
let test_file = temp_dir.path().join("test.txt");
let content = b"test content";
fs::write(&test_file, content).unwrap();
let vfs = PhysicalVfs::new();
let path_str = test_file.to_str().unwrap();
let read_content = vfs.read_file(path_str).unwrap();
assert_eq!(read_content, content);
}
#[cfg(not(target_arch = "wasm32"))]
#[test]
fn test_physical_vfs_read_dir() {
use std::fs;
use tempfile::TempDir;
let temp_dir = TempDir::new().unwrap();
let dir_path = temp_dir.path();
fs::write(dir_path.join("file1.txt"), b"content1").unwrap();
fs::write(dir_path.join("file2.txt"), b"content2").unwrap();
fs::create_dir(dir_path.join("subdir")).unwrap();
let vfs = PhysicalVfs::new();
let path_str = dir_path.to_str().unwrap();
let entries = vfs.read_dir(path_str).unwrap();
assert_eq!(entries.len(), 3);
let names: Vec<String> = entries.iter().map(|e| e.name.clone()).collect();
assert!(names.contains(&"file1.txt".to_string()));
assert!(names.contains(&"file2.txt".to_string()));
assert!(names.contains(&"subdir".to_string()));
}
}