use std::{
path::{Component, Display, Path, PathBuf},
str::FromStr,
};
use crate::Runtime;
use serde::{de::Error as DeError, Deserialize, Deserializer, Serialize};
use serde_repr::{Deserialize_repr, Serialize_repr};
pub(crate) mod plugin;
use crate::error::*;
#[cfg(target_os = "android")]
mod android;
#[cfg(not(target_os = "android"))]
mod desktop;
#[cfg(target_os = "android")]
pub use android::PathResolver;
#[cfg(not(target_os = "android"))]
pub use desktop::PathResolver;
#[derive(Clone, Debug, Serialize)]
pub struct SafePathBuf(PathBuf);
impl SafePathBuf {
pub fn new(path: PathBuf) -> std::result::Result<Self, &'static str> {
if path.components().any(|x| matches!(x, Component::ParentDir)) {
Err("cannot traverse directory, rewrite the path without the use of `../`")
} else {
Ok(Self(path))
}
}
pub fn display(&self) -> Display<'_> {
self.0.display()
}
}
impl AsRef<Path> for SafePathBuf {
fn as_ref(&self) -> &Path {
self.0.as_ref()
}
}
impl FromStr for SafePathBuf {
type Err = &'static str;
fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
Self::new(s.into())
}
}
impl<'de> Deserialize<'de> for SafePathBuf {
fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let path = PathBuf::deserialize(deserializer)?;
SafePathBuf::new(path).map_err(DeError::custom)
}
}
#[derive(Serialize_repr, Deserialize_repr, Clone, Copy, Debug)]
#[repr(u16)]
#[non_exhaustive]
pub enum BaseDirectory {
Audio = 1,
Cache = 2,
Config = 3,
Data = 4,
LocalData = 5,
Document = 6,
Download = 7,
Picture = 8,
Public = 9,
Video = 10,
Resource = 11,
Temp = 12,
AppConfig = 13,
AppData = 14,
AppLocalData = 15,
AppCache = 16,
AppLog = 17,
#[cfg(not(target_os = "android"))]
Desktop = 18,
#[cfg(not(target_os = "android"))]
Executable = 19,
#[cfg(not(target_os = "android"))]
Font = 20,
Home = 21,
#[cfg(not(target_os = "android"))]
Runtime = 22,
#[cfg(not(target_os = "android"))]
Template = 23,
}
impl BaseDirectory {
pub fn variable(self) -> &'static str {
match self {
Self::Audio => "$AUDIO",
Self::Cache => "$CACHE",
Self::Config => "$CONFIG",
Self::Data => "$DATA",
Self::LocalData => "$LOCALDATA",
Self::Document => "$DOCUMENT",
Self::Download => "$DOWNLOAD",
Self::Picture => "$PICTURE",
Self::Public => "$PUBLIC",
Self::Video => "$VIDEO",
Self::Resource => "$RESOURCE",
Self::Temp => "$TEMP",
Self::AppConfig => "$APPCONFIG",
Self::AppData => "$APPDATA",
Self::AppLocalData => "$APPLOCALDATA",
Self::AppCache => "$APPCACHE",
Self::AppLog => "$APPLOG",
Self::Home => "$HOME",
#[cfg(not(target_os = "android"))]
Self::Desktop => "$DESKTOP",
#[cfg(not(target_os = "android"))]
Self::Executable => "$EXE",
#[cfg(not(target_os = "android"))]
Self::Font => "$FONT",
#[cfg(not(target_os = "android"))]
Self::Runtime => "$RUNTIME",
#[cfg(not(target_os = "android"))]
Self::Template => "$TEMPLATE",
}
}
pub fn from_variable(variable: &str) -> Option<Self> {
let res = match variable {
"$AUDIO" => Self::Audio,
"$CACHE" => Self::Cache,
"$CONFIG" => Self::Config,
"$DATA" => Self::Data,
"$LOCALDATA" => Self::LocalData,
"$DOCUMENT" => Self::Document,
"$DOWNLOAD" => Self::Download,
"$PICTURE" => Self::Picture,
"$PUBLIC" => Self::Public,
"$VIDEO" => Self::Video,
"$RESOURCE" => Self::Resource,
"$TEMP" => Self::Temp,
"$APPCONFIG" => Self::AppConfig,
"$APPDATA" => Self::AppData,
"$APPLOCALDATA" => Self::AppLocalData,
"$APPCACHE" => Self::AppCache,
"$APPLOG" => Self::AppLog,
"$HOME" => Self::Home,
#[cfg(not(target_os = "android"))]
"$DESKTOP" => Self::Desktop,
#[cfg(not(target_os = "android"))]
"$EXE" => Self::Executable,
#[cfg(not(target_os = "android"))]
"$FONT" => Self::Font,
#[cfg(not(target_os = "android"))]
"$RUNTIME" => Self::Runtime,
#[cfg(not(target_os = "android"))]
"$TEMPLATE" => Self::Template,
_ => return None,
};
Some(res)
}
}
impl<R: Runtime> PathResolver<R> {
pub fn resolve<P: AsRef<Path>>(&self, path: P, base_directory: BaseDirectory) -> Result<PathBuf> {
resolve_path::<R>(self, base_directory, Some(path.as_ref().to_path_buf()))
}
pub fn parse<P: AsRef<Path>>(&self, path: P) -> Result<PathBuf> {
let mut p = PathBuf::new();
let mut components = path.as_ref().components();
match components.next() {
Some(Component::Normal(str)) => {
if let Some(base_directory) = BaseDirectory::from_variable(&str.to_string_lossy()) {
p.push(resolve_path::<R>(self, base_directory, None)?);
} else {
p.push(str);
}
}
Some(component) => p.push(component),
None => (),
}
for component in components {
if let Component::ParentDir = component {
continue;
}
p.push(component);
}
Ok(p)
}
}
fn resolve_path<R: Runtime>(
resolver: &PathResolver<R>,
directory: BaseDirectory,
path: Option<PathBuf>,
) -> Result<PathBuf> {
let resolve_resource = matches!(directory, BaseDirectory::Resource);
let mut base_dir_path = match directory {
BaseDirectory::Audio => resolver.audio_dir(),
BaseDirectory::Cache => resolver.cache_dir(),
BaseDirectory::Config => resolver.config_dir(),
BaseDirectory::Data => resolver.data_dir(),
BaseDirectory::LocalData => resolver.local_data_dir(),
BaseDirectory::Document => resolver.document_dir(),
BaseDirectory::Download => resolver.download_dir(),
BaseDirectory::Picture => resolver.picture_dir(),
BaseDirectory::Public => resolver.public_dir(),
BaseDirectory::Video => resolver.video_dir(),
BaseDirectory::Resource => resolver.resource_dir(),
BaseDirectory::Temp => resolver.temp_dir(),
BaseDirectory::AppConfig => resolver.app_config_dir(),
BaseDirectory::AppData => resolver.app_data_dir(),
BaseDirectory::AppLocalData => resolver.app_local_data_dir(),
BaseDirectory::AppCache => resolver.app_cache_dir(),
BaseDirectory::AppLog => resolver.app_log_dir(),
BaseDirectory::Home => resolver.home_dir(),
#[cfg(not(target_os = "android"))]
BaseDirectory::Desktop => resolver.desktop_dir(),
#[cfg(not(target_os = "android"))]
BaseDirectory::Executable => resolver.executable_dir(),
#[cfg(not(target_os = "android"))]
BaseDirectory::Font => resolver.font_dir(),
#[cfg(not(target_os = "android"))]
BaseDirectory::Runtime => resolver.runtime_dir(),
#[cfg(not(target_os = "android"))]
BaseDirectory::Template => resolver.template_dir(),
}?;
if let Some(path) = path {
if resolve_resource {
let mut resource_path = PathBuf::new();
for component in path.components() {
match component {
Component::Prefix(_) => {}
Component::RootDir => resource_path.push("_root_"),
Component::CurDir => {}
Component::ParentDir => resource_path.push("_up_"),
Component::Normal(p) => resource_path.push(p),
}
}
base_dir_path.push(resource_path);
} else {
base_dir_path.push(path);
}
}
Ok(base_dir_path)
}
#[cfg(test)]
mod test {
use super::SafePathBuf;
use quickcheck::{Arbitrary, Gen};
use std::path::PathBuf;
impl Arbitrary for SafePathBuf {
fn arbitrary(g: &mut Gen) -> Self {
Self(PathBuf::arbitrary(g))
}
fn shrink(&self) -> Box<dyn Iterator<Item = Self>> {
Box::new(self.0.shrink().map(SafePathBuf))
}
}
}