use std::{collections::HashMap, path::Path};
use cowfile::CowFile;
pub struct VirtualFs {
entries: HashMap<String, CowFile>,
}
impl VirtualFs {
#[must_use]
pub fn new() -> Self {
Self {
entries: HashMap::new(),
}
}
pub fn map_cow(&mut self, vfs_path: &str, cow: CowFile) {
self.entries.insert(Self::normalize_path(vfs_path), cow);
}
pub fn map_disk_file(
&mut self,
vfs_path: &str,
disk_path: &Path,
) -> Result<(), cowfile::Error> {
let cow = CowFile::open(disk_path)?;
self.entries.insert(Self::normalize_path(vfs_path), cow);
Ok(())
}
pub fn map_data(&mut self, vfs_path: &str, data: Vec<u8>) {
self.entries
.insert(Self::normalize_path(vfs_path), CowFile::from_vec(data));
}
pub fn get(&self, vfs_path: &str) -> Option<&CowFile> {
let normalized = Self::normalize_path(vfs_path);
self.entries.get(&normalized).or_else(|| {
let filename = Self::extract_filename(&normalized);
self.entries
.iter()
.find(|(k, _)| Self::extract_filename(k) == filename)
.map(|(_, v)| v)
})
}
pub fn exists(&self, vfs_path: &str) -> bool {
self.get(vfs_path).is_some()
}
pub fn is_empty(&self) -> bool {
self.entries.is_empty()
}
pub fn len(&self) -> usize {
self.entries.len()
}
pub fn paths(&self) -> Vec<&str> {
self.entries.keys().map(|s| s.as_str()).collect()
}
pub fn fork(&self) -> Result<VirtualFs, cowfile::Error> {
let entries = self
.entries
.iter()
.map(|(path, cow)| Ok((path.clone(), cow.fork()?)))
.collect::<Result<_, cowfile::Error>>()?;
Ok(VirtualFs { entries })
}
fn normalize_path(path: &str) -> String {
let mut normalized = path.to_lowercase().replace('\\', "/");
if normalized.len() >= 3
&& normalized.as_bytes()[0].is_ascii_alphabetic()
&& &normalized[1..3] == ":/"
{
normalized = normalized[2..].to_string();
}
normalized.trim_start_matches('/').to_string()
}
fn extract_filename(path: &str) -> &str {
path.rsplit('/').next().unwrap_or(path)
}
}
impl Default for VirtualFs {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use cowfile::CowFile;
use crate::emulation::filesystem::VirtualFs;
#[test]
fn test_normalize_path() {
assert_eq!(
VirtualFs::normalize_path(r"C:\Users\test.exe"),
"users/test.exe"
);
assert_eq!(VirtualFs::normalize_path("/usr/bin/test"), "usr/bin/test");
assert_eq!(VirtualFs::normalize_path("test.exe"), "test.exe");
}
#[test]
fn test_map_and_get() {
let mut vfs = VirtualFs::new();
vfs.map_data("test.exe", vec![0x4D, 0x5A]);
assert!(vfs.exists("test.exe"));
assert!(vfs.exists("TEST.EXE"));
let data = vfs.get("test.exe").unwrap();
assert_eq!(data.data(), &[0x4D, 0x5A]);
}
#[test]
fn test_path_matching() {
let mut vfs = VirtualFs::new();
vfs.map_data("test.exe", vec![0x4D, 0x5A]);
assert!(vfs.exists(r"C:\Program Files\test.exe"));
}
#[test]
fn test_fork() {
let mut vfs = VirtualFs::new();
vfs.map_data("test.exe", vec![1, 2, 3]);
let forked = vfs.fork().unwrap();
assert!(forked.exists("test.exe"));
assert_eq!(forked.get("test.exe").unwrap().data(), &[1, 2, 3]);
}
#[test]
fn test_fork_from_disk() {
use std::io::Write;
let mut tmpfile = tempfile::NamedTempFile::new().unwrap();
tmpfile.write_all(&[0xDE, 0xAD]).unwrap();
tmpfile.flush().unwrap();
let cow = CowFile::open(tmpfile.path()).unwrap();
let mut vfs = VirtualFs::new();
vfs.map_cow("sample.exe", cow);
let forked = vfs.fork().unwrap();
assert_eq!(forked.get("sample.exe").unwrap().data(), &[0xDE, 0xAD]);
}
}