use std::cell::RefCell;
use std::collections::VecDeque;
use std::path::{self, Path, PathBuf};
use std::fs;
use std::fmt::{self, Debug};
use std::io::{self, Read, Seek, Write};
use zip;
use {GameError, GameResult};
fn convenient_path_to_str(path: &path::Path) -> GameResult<&str> {
let errmessage = format!("Invalid path format for resource: {:?}", path);
let error = GameError::FilesystemError(errmessage);
path.to_str().ok_or(error)
}
pub trait VFile: Read + Write + Seek + Debug {}
impl<T> VFile for T where T: Read + Write + Seek + Debug {}
#[derive(Debug, Default, Copy, Clone, PartialEq)]
pub struct OpenOptions {
read: bool,
write: bool,
create: bool,
append: bool,
truncate: bool,
}
impl OpenOptions {
pub fn new() -> OpenOptions {
Default::default()
}
pub fn read(&mut self, read: bool) -> &mut OpenOptions {
self.read = read;
self
}
pub fn write(&mut self, write: bool) -> &mut OpenOptions {
self.write = write;
self
}
pub fn create(&mut self, create: bool) -> &mut OpenOptions {
self.create = create;
self
}
pub fn append(&mut self, append: bool) -> &mut OpenOptions {
self.append = append;
self
}
pub fn truncate(&mut self, truncate: bool) -> &mut OpenOptions {
self.truncate = truncate;
self
}
fn to_fs_openoptions(&self) -> fs::OpenOptions {
let mut opt = fs::OpenOptions::new();
opt.read(self.read)
.write(self.write)
.create(self.create)
.append(self.append)
.truncate(self.truncate)
.create(self.create);
opt
}
}
pub trait VFS: Debug {
fn open_options(&self, path: &Path, open_options: &OpenOptions) -> GameResult<Box<VFile>>;
fn open(&self, path: &Path) -> GameResult<Box<VFile>> {
self.open_options(path, OpenOptions::new().read(true))
}
fn create(&self, path: &Path) -> GameResult<Box<VFile>> {
self.open_options(path,
OpenOptions::new()
.write(true)
.create(true)
.truncate(true))
}
fn append(&self, path: &Path) -> GameResult<Box<VFile>> {
self.open_options(path,
OpenOptions::new().write(true).create(true).append(true))
}
fn mkdir(&self, path: &Path) -> GameResult<()>;
fn rm(&self, path: &Path) -> GameResult<()>;
fn rmrf(&self, path: &Path) -> GameResult<()>;
fn exists(&self, path: &Path) -> bool;
fn metadata(&self, path: &Path) -> GameResult<Box<VMetadata>>;
fn read_dir(&self, path: &Path) -> GameResult<Box<Iterator<Item = GameResult<PathBuf>>>>;
fn to_path_buf(&self) -> Option<PathBuf>;
}
pub trait VMetadata {
fn is_dir(&self) -> bool;
fn is_file(&self) -> bool;
fn len(&self) -> u64;
}
#[derive(Clone)]
pub struct PhysicalFS {
root: PathBuf,
readonly: bool,
}
#[derive(Debug, Clone)]
pub struct PhysicalMetadata(fs::Metadata);
impl VMetadata for PhysicalMetadata {
fn is_dir(&self) -> bool {
self.0.is_dir()
}
fn is_file(&self) -> bool {
self.0.is_file()
}
fn len(&self) -> u64 {
self.0.len()
}
}
fn sanitize_path(path: &path::Path) -> Option<PathBuf> {
let mut c = path.components();
match c.next() {
Some(path::Component::RootDir) => (),
_ => return None,
}
fn is_normal_component(comp: path::Component) -> Option<&str> {
match comp {
path::Component::Normal(s) => s.to_str(),
_ => None,
}
}
let mut accm = PathBuf::new();
for component in c {
if let Some(s) = is_normal_component(component) {
accm.push(s)
} else {
return None;
}
}
Some(accm)
}
impl PhysicalFS {
pub fn new(root: &Path, readonly: bool) -> Self {
PhysicalFS {
root: root.into(),
readonly: readonly,
}
}
fn get_absolute(&self, p: &Path) -> GameResult<PathBuf> {
if let Some(safe_path) = sanitize_path(p) {
let mut root_path = self.root.clone();
root_path.push(safe_path);
Ok(root_path)
} else {
let msg = format!("Path {:?} is not valid: must be an absolute path with no \
references to parent directories",
p);
Err(GameError::FilesystemError(msg))
}
}
}
impl Debug for PhysicalFS {
fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> {
write!(f, "<PhysicalFS root: {}>", self.root.display())
}
}
impl VFS for PhysicalFS {
fn open_options(&self, path: &Path, open_options: &OpenOptions) -> GameResult<Box<VFile>> {
if self.readonly &&
(open_options.write || open_options.create || open_options.append ||
open_options.truncate) {
let msg = format!("Cannot alter file {:?} in root {:?}, filesystem read-only",
path,
self);
return Err(GameError::FilesystemError(msg));
}
let p = self.get_absolute(path)?;
open_options
.to_fs_openoptions()
.open(p)
.map(|x| Box::new(x) as Box<VFile>)
.map_err(GameError::from)
}
fn mkdir(&self, path: &Path) -> GameResult<()> {
if self.readonly {
return Err(GameError::FilesystemError("Tried to make directory {} but FS is \
read-only"
.to_string()));
}
let p = self.get_absolute(path)?;
fs::DirBuilder::new()
.recursive(true)
.create(p)
.map_err(GameError::from)
}
fn rm(&self, path: &Path) -> GameResult<()> {
if self.readonly {
return Err(GameError::FilesystemError("Tried to remove file {} but FS is read-only"
.to_string()));
}
let p = self.get_absolute(path)?;
if p.is_dir() {
fs::remove_dir(p).map_err(GameError::from)
} else {
fs::remove_file(p).map_err(GameError::from)
}
}
fn rmrf(&self, path: &Path) -> GameResult<()> {
if self.readonly {
return Err(GameError::FilesystemError("Tried to remove file/dir {} but FS is \
read-only"
.to_string()));
}
let p = self.get_absolute(path)?;
if p.is_dir() {
fs::remove_dir_all(p).map_err(GameError::from)
} else {
fs::remove_file(p).map_err(GameError::from)
}
}
fn exists(&self, path: &Path) -> bool {
match self.get_absolute(path) {
Ok(p) => p.exists(),
_ => false,
}
}
fn metadata(&self, path: &Path) -> GameResult<Box<VMetadata>> {
let p = self.get_absolute(path)?;
p.metadata()
.map(|m| Box::new(PhysicalMetadata(m)) as Box<VMetadata>)
.map_err(GameError::from)
}
fn read_dir(&self, path: &Path) -> GameResult<Box<Iterator<Item = GameResult<PathBuf>>>> {
let p = self.get_absolute(path)?;
let direntry_to_path = |entry: &fs::DirEntry| -> GameResult<PathBuf> {
let fname = entry.file_name().into_string().unwrap();
let mut pathbuf = PathBuf::from(path);
pathbuf.push(fname);
Ok(pathbuf)
};
let itr = fs::read_dir(p)?
.map(|entry| direntry_to_path(&entry?))
.collect::<Vec<_>>()
.into_iter();
Ok(Box::new(itr))
}
fn to_path_buf(&self) -> Option<PathBuf> {
Some(self.root.clone())
}
}
#[derive(Debug)]
pub struct OverlayFS {
roots: VecDeque<Box<VFS>>,
}
impl OverlayFS {
pub fn new() -> Self {
Self { roots: VecDeque::new() }
}
#[allow(dead_code)]
pub fn push_front(&mut self, fs: Box<VFS>) {
self.roots.push_front(fs);
}
pub fn push_back(&mut self, fs: Box<VFS>) {
self.roots.push_back(fs);
}
pub fn roots(&self) -> &VecDeque<Box<VFS>> {
&self.roots
}
}
impl VFS for OverlayFS {
fn open_options(&self, path: &Path, open_options: &OpenOptions) -> GameResult<Box<VFile>> {
let mut tried: Vec<(PathBuf, GameError)> = vec![];
for vfs in &self.roots {
match vfs.open_options(path, open_options) {
Err(e) => {
if let Some(vfs_path) = vfs.to_path_buf() {
tried.push((vfs_path, e));
} else {
tried.push((PathBuf::from("<invalid path>"), e));
}
}
f => return f,
}
}
let errmessage = String::from(convenient_path_to_str(path)?);
Err(GameError::ResourceNotFound(errmessage, tried))
}
fn mkdir(&self, path: &Path) -> GameResult<()> {
for vfs in &self.roots {
match vfs.mkdir(path) {
Err(_) => (),
f => return f,
}
}
Err(GameError::FilesystemError(format!("Could not find anywhere writeable to make dir {:?}",
path)))
}
fn rm(&self, path: &Path) -> GameResult<()> {
for vfs in &self.roots {
match vfs.rm(path) {
Err(_) => (),
f => return f,
}
}
Err(GameError::FilesystemError(format!("Could not remove file {:?}", path)))
}
fn rmrf(&self, path: &Path) -> GameResult<()> {
for vfs in &self.roots {
match vfs.rmrf(path) {
Err(_) => (),
f => return f,
}
}
Err(GameError::FilesystemError(format!("Could not remove file/dir {:?}", path)))
}
fn exists(&self, path: &Path) -> bool {
for vfs in &self.roots {
if vfs.exists(path) {
return true;
}
}
false
}
fn metadata(&self, path: &Path) -> GameResult<Box<VMetadata>> {
for vfs in &self.roots {
match vfs.metadata(path) {
Err(_) => (),
f => return f,
}
}
Err(GameError::FilesystemError(format!("Could not get metadata for file/dir {:?}", path)))
}
fn read_dir(&self, path: &Path) -> GameResult<Box<Iterator<Item = GameResult<PathBuf>>>> {
let mut v = Vec::new();
for fs in &self.roots {
if let Ok(rddir) = fs.read_dir(path) {
v.extend(rddir)
}
}
Ok(Box::new(v.into_iter()))
}
fn to_path_buf(&self) -> Option<PathBuf> {
None
}
}
#[derive(Debug)]
pub struct ZipFS {
source: PathBuf,
archive: RefCell<zip::ZipArchive<fs::File>>,
index: Vec<String>,
}
impl ZipFS {
pub fn new(filename: &Path) -> GameResult<Self> {
let f = fs::File::open(filename)?;
let mut archive = zip::ZipArchive::new(f)?;
let idx = (0..archive.len())
.map(|i| archive.by_index(i).unwrap().name().to_string())
.collect();
Ok(Self {
source: filename.into(),
archive: RefCell::new(archive),
index: idx,
})
}
}
#[derive(Clone)]
pub struct ZipFileWrapper {
buffer: io::Cursor<Vec<u8>>,
}
impl ZipFileWrapper {
fn new(z: &mut zip::read::ZipFile) -> GameResult<Self> {
let mut b = Vec::new();
z.read_to_end(&mut b)?;
Ok(Self { buffer: io::Cursor::new(b) })
}
}
impl io::Read for ZipFileWrapper {
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
self.buffer.read(buf)
}
}
impl io::Write for ZipFileWrapper {
fn write(&mut self, _buf: &[u8]) -> io::Result<usize> {
panic!("Cannot write to a zip file!")
}
fn flush(&mut self) -> io::Result<()> {
Ok(())
}
}
impl io::Seek for ZipFileWrapper {
fn seek(&mut self, pos: io::SeekFrom) -> io::Result<u64> {
self.buffer.seek(pos)
}
}
impl Debug for ZipFileWrapper {
fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> {
write!(f, "<Zipfile>")
}
}
#[derive(Debug, Copy, Clone, PartialEq)]
struct ZipMetadata {
len: u64,
is_dir: bool,
is_file: bool,
}
impl ZipMetadata {
fn new<T>(name: &str, archive: &mut zip::ZipArchive<T>) -> Option<Self>
where T: io::Read + io::Seek
{
match archive.by_name(name) {
Err(_) => None,
Ok(zipfile) => {
let len = zipfile.size();
Some(ZipMetadata {
len: len,
is_file: true,
is_dir: false, })
}
}
}
}
impl VMetadata for ZipMetadata {
fn is_dir(&self) -> bool {
self.is_dir
}
fn is_file(&self) -> bool {
self.is_file
}
fn len(&self) -> u64 {
self.len
}
}
impl VFS for ZipFS {
fn open_options(&self, path: &Path, open_options: &OpenOptions) -> GameResult<Box<VFile>> {
let path = convenient_path_to_str(path)?;
if open_options.write || open_options.create || open_options.append ||
open_options.truncate {
let msg = format!("Cannot alter file {:?} in zipfile {:?}, filesystem read-only",
path,
self);
return Err(GameError::FilesystemError(msg));
}
let mut stupid_archive_borrow = self.archive
.try_borrow_mut()
.expect("Couldn't borrow ZipArchive in ZipFS::open_options(); should never happen! Report a bug at https://github.com/ggez/ggez/");
let mut f = stupid_archive_borrow.by_name(path)?;
let zipfile = ZipFileWrapper::new(&mut f)?;
Ok(Box::new(zipfile) as Box<VFile>)
}
fn mkdir(&self, path: &Path) -> GameResult<()> {
let msg = format!("Cannot mkdir {:?} in zipfile {:?}, filesystem read-only",
path,
self);
Err(GameError::FilesystemError(msg))
}
fn rm(&self, path: &Path) -> GameResult<()> {
let msg = format!("Cannot rm {:?} in zipfile {:?}, filesystem read-only",
path,
self);
Err(GameError::FilesystemError(msg))
}
fn rmrf(&self, path: &Path) -> GameResult<()> {
let msg = format!("Cannot rmrf {:?} in zipfile {:?}, filesystem read-only",
path,
self);
Err(GameError::FilesystemError(msg))
}
fn exists(&self, path: &Path) -> bool {
let mut stupid_archive_borrow = self.archive
.try_borrow_mut()
.expect("Couldn't borrow ZipArchive in ZipFS::exists(); should never happen! Report a bug at https://github.com/ggez/ggez/");
if let Ok(path) = convenient_path_to_str(path) {
stupid_archive_borrow.by_name(path).is_ok()
} else {
false
}
}
fn metadata(&self, path: &Path) -> GameResult<Box<VMetadata>> {
let path = convenient_path_to_str(path)?;
let mut stupid_archive_borrow =
self.archive
.try_borrow_mut()
.expect("Couldn't borrow ZipArchive in ZipFS::metadata(); should never happen! Report a bug at https://github.com/ggez/ggez/");
match ZipMetadata::new(path, &mut stupid_archive_borrow) {
None => {
Err(GameError::FilesystemError(format!("Metadata not found in zip file for {}",
path)))
}
Some(md) => Ok(Box::new(md) as Box<VMetadata>),
}
}
fn read_dir(&self, path: &Path) -> GameResult<Box<Iterator<Item = GameResult<PathBuf>>>> {
let path = convenient_path_to_str(path)?;
let itr = self.index
.iter()
.filter(|s| s.starts_with(path))
.map(|s| Ok(PathBuf::from(s)))
.collect::<Vec<_>>();
Ok(Box::new(itr.into_iter()))
}
fn to_path_buf(&self) -> Option<PathBuf> {
Some(self.source.clone())
}
}
#[cfg(test)]
mod tests {
use std::io::{self, BufRead};
use super::*;
#[test]
fn test_path_filtering() {
let p = path::Path::new("/foo");
sanitize_path(p).unwrap();
let p = path::Path::new("/foo/");
sanitize_path(p).unwrap();
let p = path::Path::new("/foo/bar.txt");
sanitize_path(p).unwrap();
let p = path::Path::new("/");
sanitize_path(p).unwrap();
let p = path::Path::new("../foo");
assert!(sanitize_path(p).is_none());
let p = path::Path::new("foo");
assert!(sanitize_path(p).is_none());
let p = path::Path::new("/foo/../../");
assert!(sanitize_path(p).is_none());
let p = path::Path::new("/foo/../bop");
assert!(sanitize_path(p).is_none());
let p = path::Path::new("/../bar");
assert!(sanitize_path(p).is_none());
let p = path::Path::new("");
assert!(sanitize_path(p).is_none());
}
#[test]
fn test_read() {
let cargo_path = Path::new(env!("CARGO_MANIFEST_DIR"));
let fs = PhysicalFS::new(cargo_path, true);
let f = fs.open(Path::new("/Cargo.toml")).unwrap();
let mut bf = io::BufReader::new(f);
let mut s = String::new();
bf.read_line(&mut s).unwrap();
assert_eq!(&s, "[package]\n");
}
#[test]
fn test_read_overlay() {
let cargo_path = Path::new(env!("CARGO_MANIFEST_DIR"));
let fs1 = PhysicalFS::new(cargo_path, true);
let mut f2path = PathBuf::from(cargo_path);
f2path.push("src");
let fs2 = PhysicalFS::new(&f2path, true);
let mut ofs = OverlayFS::new();
ofs.push_back(Box::new(fs1));
ofs.push_back(Box::new(fs2));
assert!(ofs.exists(Path::new("/Cargo.toml")));
assert!(ofs.exists(Path::new("/lib.rs")));
assert!(!ofs.exists(Path::new("/foobaz.rs")));
}
#[test]
fn test_physical_all() {
let cargo_path = Path::new(env!("CARGO_MANIFEST_DIR"));
let fs = PhysicalFS::new(cargo_path, false);
let testdir = Path::new("/testdir");
let f1 = Path::new("/testdir/file1.txt");
if fs.exists(testdir) {
fs.rmrf(testdir).unwrap();
}
assert!(!fs.exists(testdir));
fs.mkdir(testdir).unwrap();
assert!(fs.exists(testdir));
fs.rm(testdir).unwrap();
assert!(!fs.exists(testdir));
let test_string = "Foo!";
fs.mkdir(testdir).unwrap();
{
let mut f = fs.append(f1).unwrap();
f.write(test_string.as_bytes()).unwrap();
}
{
let mut buf = Vec::new();
let mut f = fs.open(f1).unwrap();
f.read_to_end(&mut buf).unwrap();
assert_eq!(&buf[..], test_string.as_bytes());
}
{
let m = fs.metadata(f1).unwrap();
assert!(m.is_file());
assert!(!m.is_dir());
assert_eq!(m.len(), 4);
let m = fs.metadata(testdir).unwrap();
assert!(!m.is_file());
assert!(m.is_dir());
}
{
let r = fs.read_dir(testdir).unwrap();
assert_eq!(r.count(), 1);
let r = fs.read_dir(testdir).unwrap();
for f in r {
let fname = f.unwrap();
assert!(fs.exists(&fname));
}
}
{
assert!(fs.exists(f1));
fs.rm(f1).unwrap();
assert!(!fs.exists(f1));
}
fs.rmrf(testdir).unwrap();
assert!(!fs.exists(testdir));
}
}