mod directories;
use std::fs;
use std::io;
use std::path::Component;
use std::path::Path;
pub use directories::{cached_config_directory, cached_data_directory, Error};
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub enum Version {
DDNet06,
Teeworlds07,
}
pub fn exists<P: AsRef<Path>>(path: P, version: Version) -> Result<bool, io::Error> {
exists_(path.as_ref(), version)
}
fn exists_(path: &Path, version: Version) -> Result<bool, io::Error> {
check_path(path)?;
Ok(
cached_config_directory(version).is_ok_and(|cfg| cfg.join(path).exists())
|| cached_data_directory(version).is_ok_and(|data| data.join(path).exists()),
)
}
pub fn rename<P: AsRef<Path>>(from: P, to: P, version: Version) -> Result<(), io::Error> {
rename_(from.as_ref(), to.as_ref(), version)
}
fn rename_(from: &Path, to: &Path, version: Version) -> Result<(), io::Error> {
check_path(from)?;
check_path(to)?;
let cfg_dir = cached_config_directory(version)?;
fs::rename(cfg_dir.join(from), cfg_dir.join(to))
}
pub fn read<P: AsRef<Path>>(path: P, version: Version) -> Result<Vec<u8>, io::Error> {
read_(path.as_ref(), version)
}
fn read_(path: &Path, version: Version) -> Result<Vec<u8>, io::Error> {
check_path(path)?;
let cfg_dir = cached_config_directory(version)?;
fs::read(cfg_dir.join(path))
}
pub fn write<P: AsRef<Path>, C: AsRef<[u8]>>(
path: P,
contents: C,
version: Version,
) -> Result<(), io::Error> {
write_(path.as_ref(), contents.as_ref(), version)
}
fn write_(path: &Path, contents: &[u8], version: Version) -> Result<(), io::Error> {
check_path(path)?;
let cfg_dir = cached_config_directory(version)?;
fs::write(cfg_dir.join(path), contents)
}
pub fn open_file<P: AsRef<Path>>(path: P, version: Version) -> Result<fs::File, io::Error> {
open_file_(path.as_ref(), version)
}
fn open_file_(path: &Path, version: Version) -> Result<fs::File, io::Error> {
check_path(path)?;
if let Ok(cfg_dir) = cached_config_directory(version) {
if let Ok(file) = fs::File::open(cfg_dir.join(path)) {
return Ok(file);
}
}
match cached_data_directory(version) {
Ok(data_dir) => fs::File::open(data_dir.join(path)),
Err(err) => Err(err.into()),
}
}
#[deprecated = "Use open_file instead"]
pub fn read_file(path: &str, version: Version) -> Result<fs::File, io::Error> {
open_file(path, version)
}
pub fn create_file<P: AsRef<Path>>(path: P, version: Version) -> Result<fs::File, io::Error> {
create_file_(path.as_ref(), version)
}
fn create_file_(path: &Path, version: Version) -> Result<fs::File, io::Error> {
check_path(path)?;
let cfg_dir = cached_config_directory(version)?;
fs::File::create(cfg_dir.join(path))
}
pub fn remove_file<P: AsRef<Path>>(path: P, version: Version) -> Result<(), io::Error> {
remove_file_(path.as_ref(), version)
}
fn remove_file_(path: &Path, version: Version) -> Result<(), io::Error> {
check_path(path)?;
let cfg_dir = cached_config_directory(version)?;
fs::remove_file(cfg_dir.join(path))
}
pub fn read_dir<P: AsRef<Path>>(path: P, version: Version) -> Result<ReadDir, io::Error> {
read_dir_(path.as_ref(), version)
}
fn read_dir_(path: &Path, version: Version) -> Result<ReadDir, io::Error> {
check_path(path)?;
let cfg = cached_config_directory(version)
.map_err(io::Error::from)
.and_then(|cfg| fs::read_dir(cfg.join(path)));
let data_dir = cached_data_directory(version)?;
let data = fs::read_dir(data_dir.join(path));
Ok(ReadDir {
cfg,
data,
cfg_file_names: Default::default(),
})
}
pub fn create_dir_all<P: AsRef<Path>>(path: P, version: Version) -> Result<(), io::Error> {
create_dir_all_(path.as_ref(), version)
}
fn create_dir_all_(path: &Path, version: Version) -> Result<(), io::Error> {
check_path(path)?;
let cfg_dir = cached_config_directory(version)?;
fs::create_dir_all(cfg_dir.join(path))
}
pub fn remove_dir<P: AsRef<Path>>(path: P, version: Version) -> Result<(), io::Error> {
remove_dir_(path.as_ref(), version)
}
fn remove_dir_(path: &Path, version: Version) -> Result<(), io::Error> {
check_path(path)?;
let cfg_dir = cached_config_directory(version)?;
fs::remove_dir(cfg_dir.join(path))
}
fn check_path(path: &Path) -> Result<(), io::Error> {
check_no_parent_dir(path)?;
check_no_absoulte_path(path)?;
Ok(())
}
fn check_no_parent_dir(path: &Path) -> Result<(), io::Error> {
if path
.components()
.any(|component| component == Component::ParentDir)
{
Err(io::Error::new(
io::ErrorKind::InvalidInput,
"Attempt to access parent directory in Teeworlds/DDNet storage path",
))
} else {
Ok(())
}
}
fn check_no_absoulte_path(path: &Path) -> Result<(), io::Error> {
if path.is_absolute() {
Err(io::Error::new(
io::ErrorKind::InvalidInput,
"Attempt to access an absolute path from Teeworlds/DDNet storage",
))
} else {
Ok(())
}
}
pub struct ReadDir {
cfg: io::Result<fs::ReadDir>,
data: io::Result<fs::ReadDir>,
cfg_file_names: std::collections::HashSet<std::ffi::OsString>,
}
impl std::iter::Iterator for ReadDir {
type Item = io::Result<fs::DirEntry>;
fn next(&mut self) -> Option<Self::Item> {
if let Ok(cfg) = &mut self.cfg {
if let Some(next_cfg) = cfg.next() {
if let Ok(entry) = &next_cfg {
self.cfg_file_names.insert(entry.file_name());
}
return Some(next_cfg);
}
}
if let Ok(data) = &mut self.data {
if let Some(next_data) = data.next() {
if next_data
.as_ref()
.is_ok_and(|e| !self.cfg_file_names.contains(&e.file_name()))
{
return Some(next_data);
}
}
}
None
}
}