[go: up one dir, main page]

twstorage 0.2.1

Access the data and config directories of Teeworlds and DDNet
Documentation
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,
}

/// Like [std::fs::exists].
/// Returns `true`, if this file exists in either the data or config dir.
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()),
    )
}

/// Like [std::fs::rename]. Moves a file within the config dir.
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))
}

/// Like [std::fs::read]. Reads a file into a `Vec`, prioritising the cfg dir.
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))
}

/// Like [std::fs::write]. Writes data into a file in the config dir.
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)
}

/// Like [std::fs::File::open].
/// Opens a file from the config or data directory in read-only mode.
/// `path` is a relative file path, e.g. `"mapres/grass_main.png"`.
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)
}

/// Like [std::fs::File::create].
/// Creates the specified file in the config dir, potentially overwriting a file.
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))
}

/// Like [std::fs::remove_file].
/// Remove the specified file in the config dir.
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))
}

/// Like [std::fs::read_dir].
/// Returns an iterator over the entries of a directory.
/// The order is unspecified, but config dir entries overwrite data dir entries.
/// So any data dir file will not be yielded if a config dir file has the same name.
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(),
    })
}

/// Like [std::fs::create_dir_all].
/// Recursively creates the specified directories in the config dir.
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))
}

/// Like [std::fs::remove_dir].
/// Removes the specified directory in the config dir.
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(())
}

/// Verify that the path does not access some parent directory with `..`.
/// TODO: Might need to exclude mapres/../skins in the future.
/// Although twmap currently won't even permit those file paths.
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(())
    }
}

/// Iterator over the files in the Teeworlds/DDNet storage.
/// Matching files in the cfg directory overwrite files from the data dir.
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
    }
}