use crate::{
conf,
vfs::{self, OverlayFS, VFS},
GameError, GameResult,
};
use directories::ProjectDirs;
use std::{
env,
io::{self, Read},
path,
sync::{Arc, RwLock},
};
pub use crate::vfs::OpenOptions;
const CONFIG_NAME: &str = "/conf.toml";
#[derive(Clone, Debug)]
pub struct Filesystem {
vfs: Arc<RwLock<vfs::OverlayFS>>,
resources_dir: path::PathBuf,
zip_dir: path::PathBuf,
user_config_dir: path::PathBuf,
user_data_dir: path::PathBuf,
}
#[derive(Debug)]
pub struct File(Box<dyn vfs::VFile>);
impl io::Read for File {
#[inline]
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
self.0.read(buf)
}
#[inline]
fn read_exact(&mut self, buf: &mut [u8]) -> io::Result<()> {
self.0.read_exact(buf)
}
#[inline]
fn read_to_end(&mut self, buf: &mut Vec<u8>) -> io::Result<usize> {
self.0.read_to_end(buf)
}
#[inline]
fn read_to_string(&mut self, buf: &mut String) -> io::Result<usize> {
self.0.read_to_string(buf)
}
}
impl io::Write for File {
#[inline]
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
self.0.write(buf)
}
#[inline]
fn write_all(&mut self, buf: &[u8]) -> io::Result<()> {
self.0.write_all(buf)
}
#[inline]
fn flush(&mut self) -> io::Result<()> {
self.0.flush()
}
}
impl io::Seek for File {
#[inline]
fn seek(&mut self, pos: io::SeekFrom) -> io::Result<u64> {
self.0.seek(pos)
}
#[inline]
fn stream_position(&mut self) -> io::Result<u64> {
self.0.stream_position()
}
}
impl Filesystem {
pub fn new<P: AsRef<path::Path>, Q: AsRef<path::Path>>(
id: &str,
author: &str,
resources_dir_name: P,
resources_zip_name: Q,
) -> GameResult<Filesystem> {
Self::_new(
id,
author,
resources_dir_name.as_ref(),
resources_zip_name.as_ref(),
)
}
fn _new(
id: &str,
author: &str,
resources_dir_name: &path::Path,
resources_zip_name: &path::Path,
) -> GameResult<Filesystem> {
let mut root_path = env::current_exe()?;
if root_path.file_name().is_some() {
let _ = root_path.pop();
}
let mut overlay = vfs::OverlayFS::new();
let mut resources_path;
let mut resources_zip_path;
let user_data_path;
let user_config_path;
let project_dirs = match ProjectDirs::from("", author, id) {
Some(dirs) => dirs,
None => {
return Err(GameError::FilesystemError(String::from(
"No valid home directory path could be retrieved.",
)));
}
};
{
resources_path = root_path.clone();
resources_path.push(resources_dir_name);
trace!("Resources path: {resources_path:?}");
let physfs = vfs::PhysicalFS::new(&resources_path, true);
overlay.push_back(Box::new(physfs));
}
{
resources_zip_path = root_path;
resources_zip_path.push(resources_zip_name);
if resources_zip_path.exists() {
trace!("Resources zip file: {resources_zip_path:?}");
let zipfs = vfs::ZipFS::new(&resources_zip_path)?;
overlay.push_back(Box::new(zipfs));
} else {
trace!("No resources zip file found");
}
}
{
user_data_path = project_dirs.data_local_dir();
trace!("User-local data path: {user_data_path:?}");
let physfs = vfs::PhysicalFS::new(user_data_path, true);
overlay.push_back(Box::new(physfs));
}
{
user_config_path = project_dirs.config_dir();
trace!("User-local configuration path: {user_config_path:?}");
let physfs = vfs::PhysicalFS::new(user_config_path, false);
overlay.push_back(Box::new(physfs));
}
let fs = Filesystem {
vfs: Arc::new(RwLock::new(overlay)),
resources_dir: resources_path,
zip_dir: resources_zip_path,
user_config_dir: user_config_path.to_path_buf(),
user_data_dir: user_data_path.to_path_buf(),
};
Ok(fs)
}
fn vfs(&self) -> impl std::ops::Deref<Target = OverlayFS> + '_ {
self.vfs.read().unwrap()
}
fn vfs_mut(&self) -> impl std::ops::DerefMut<Target = OverlayFS> + '_ {
self.vfs.write().unwrap()
}
pub fn open<P: AsRef<path::Path>>(&self, path: P) -> GameResult<File> {
self.vfs().open(path.as_ref()).map(File)
}
pub fn open_options<P: AsRef<path::Path>>(
&self,
path: P,
options: OpenOptions,
) -> GameResult<File> {
self.vfs()
.open_options(path.as_ref(), options)
.map(File)
.map_err(|e| {
GameError::ResourceLoadError(format!(
"Tried to open {:?} but got error: {:?}",
path.as_ref(),
e
))
})
}
pub fn create<P: AsRef<path::Path>>(&self, path: P) -> GameResult<File> {
self.vfs().create(path.as_ref()).map(File)
}
pub fn read<P: AsRef<path::Path>>(&self, path: P) -> GameResult<Vec<u8>> {
let mut file = self.open(path)?;
let mut buf = Vec::new();
let _ = file.read_to_end(&mut buf)?;
Ok(buf)
}
pub fn read_to_string<P: AsRef<path::Path>>(&self, path: P) -> GameResult<String> {
let mut file = self.open(path)?;
let mut buf = String::new();
let _ = file.read_to_string(&mut buf)?;
Ok(buf)
}
pub fn create_dir<P: AsRef<path::Path>>(&self, path: P) -> GameResult {
self.vfs().mkdir(path.as_ref())
}
pub fn delete<P: AsRef<path::Path>>(&self, path: P) -> GameResult {
self.vfs().rm(path.as_ref())
}
pub fn delete_dir<P: AsRef<path::Path>>(&self, path: P) -> GameResult {
self.vfs().rmrf(path.as_ref())
}
pub fn exists<P: AsRef<path::Path>>(&self, path: P) -> bool {
self.vfs().exists(path.as_ref())
}
pub fn is_file<P: AsRef<path::Path>>(&self, path: P) -> bool {
self.vfs()
.metadata(path.as_ref())
.map(|m| m.is_file())
.unwrap_or(false)
}
pub fn is_dir<P: AsRef<path::Path>>(&self, path: P) -> bool {
self.vfs()
.metadata(path.as_ref())
.map(|m| m.is_dir())
.unwrap_or(false)
}
pub fn read_dir<P: AsRef<path::Path>>(&self, path: P) -> GameResult<Vec<path::PathBuf>> {
let mut paths = Vec::new();
self.vfs().read_dir(path.as_ref(), &mut paths)?;
Ok(paths)
}
fn write_to_string(&self) -> String {
use std::fmt::Write;
let mut s = String::new();
for vfs in self.vfs().roots() {
write!(s, "Source {vfs:?}").expect("Could not write to string; should never happen?");
match self.read_dir("/") {
Ok(files) => {
for itm in files {
write!(s, " {itm:?}")
.expect("Could not write to string; should never happen?");
}
}
Err(e) => write!(s, " Could not read source: {e:?}")
.expect("Could not write to string; should never happen?"),
}
}
s
}
pub fn print_all(&self) {
println!("{}", self.write_to_string());
}
pub fn log_all(&self) {
info!("{}", self.write_to_string());
}
pub fn mount(&self, path: &path::Path, readonly: bool) {
let physfs = vfs::PhysicalFS::new(path, readonly);
trace!("Mounting new path: {physfs:?}");
self.vfs_mut().push_back(Box::new(physfs));
}
pub fn add_zip_file<R: io::Read + io::Seek + Send + 'static>(&self, reader: R) -> GameResult {
let zipfs = vfs::ZipFS::from_read(reader)?;
trace!("Adding zip file from reader");
self.vfs_mut().push_back(Box::new(zipfs));
Ok(())
}
pub fn read_config(&self) -> GameResult<conf::Conf> {
let conf_path = path::Path::new(CONFIG_NAME);
if self.is_file(conf_path) {
let mut file = self.open(conf_path)?;
let c = conf::Conf::from_toml_file(&mut file)?;
Ok(c)
} else {
Err(GameError::ConfigError(String::from(
"Config file not found",
)))
}
}
pub fn write_config(&self, conf: &conf::Conf) -> GameResult {
let conf_path = path::Path::new(CONFIG_NAME);
let mut file = self.create(conf_path)?;
conf.to_toml_file(&mut file)?;
if self.is_file(conf_path) {
Ok(())
} else {
Err(GameError::ConfigError(format!(
"Failed to write config file at {}",
conf_path.to_string_lossy()
)))
}
}
pub fn resources_dir(&self) -> &path::Path {
&self.resources_dir
}
pub fn zip_dir(&self) -> &path::Path {
&self.zip_dir
}
pub fn user_config_dir(&self) -> &path::Path {
&self.user_config_dir
}
pub fn user_data_dir(&self) -> &path::Path {
&self.user_data_dir
}
}
#[cfg(test)]
mod tests {
use crate::conf;
use crate::error::GameError;
use crate::filesystem::{env, vfs, Arc, Filesystem, RwLock, CONFIG_NAME};
use std::io::{Read, Write};
use std::path;
fn dummy_fs_for_tests() -> Filesystem {
let mut path = path::PathBuf::from(env!("CARGO_MANIFEST_DIR"));
path.push("resources");
let physfs = vfs::PhysicalFS::new(&path, false);
let mut ofs = vfs::OverlayFS::new();
ofs.push_front(Box::new(physfs));
Filesystem {
vfs: Arc::new(RwLock::new(ofs)),
resources_dir: "".into(),
zip_dir: "".into(),
user_config_dir: "".into(),
user_data_dir: "".into(),
}
}
#[test]
fn headless_test_file_exists() {
let f = dummy_fs_for_tests();
let tile_file = path::Path::new("/tile.png");
assert!(f.exists(tile_file));
assert!(f.is_file(tile_file));
let tile_file = path::Path::new("/oglebog.png");
assert!(!f.exists(tile_file));
assert!(!f.is_file(tile_file));
assert!(!f.is_dir(tile_file));
}
#[test]
fn headless_test_read_dir() {
let f = dummy_fs_for_tests();
let dir_contents_size = f.read_dir("/").unwrap().len();
assert!(dir_contents_size > 0);
}
#[test]
fn headless_test_create_delete_file() {
let fs = dummy_fs_for_tests();
let test_file = path::Path::new("/testfile.txt");
let bytes = b"test";
{
let mut file = fs.create(test_file).unwrap();
let _ = file.write(bytes).unwrap();
}
{
let mut buffer = Vec::new();
let mut file = fs.open(test_file).unwrap();
let _ = file.read_to_end(&mut buffer).unwrap();
assert_eq!(bytes, buffer.as_slice());
}
fs.delete(test_file).unwrap();
}
#[test]
fn headless_test_file_not_found() {
let fs = dummy_fs_for_tests();
{
let rel_file = "testfile.txt";
match fs.open(rel_file) {
Err(GameError::ResourceNotFound(_, _)) => (),
Err(e) => panic!("Invalid error for opening file with relative path: {e:?}"),
Ok(f) => panic!("Should have gotten an error but instead got {f:?}!"),
}
}
{
match fs.open("/ooglebooglebarg.txt") {
Err(GameError::ResourceNotFound(_, _)) => (),
Err(e) => panic!("Invalid error for opening nonexistent file: {e}"),
Ok(f) => panic!("Should have gotten an error but instead got {f:?}"),
}
}
}
#[test]
fn headless_test_write_config() {
let f = dummy_fs_for_tests();
let conf = conf::Conf::new();
match f.write_config(&conf) {
Ok(_) => (),
Err(e) => panic!("{e:?}"),
}
f.delete(CONFIG_NAME).unwrap();
}
}