use std::{
collections::HashMap,
ffi::{OsStr, OsString},
path::PathBuf,
};
use serde::{Deserialize, Serialize};
use crate::{
error::{InstallerError, InstallerErrorKind},
os::AccessScope,
};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum AppPathPrefix {
User,
System,
SingleDir(PathBuf),
CustomUnix(PathBuf),
}
impl Default for AppPathPrefix {
fn default() -> Self {
Self::User
}
}
impl From<AccessScope> for AppPathPrefix {
fn from(value: AccessScope) -> Self {
match value {
AccessScope::User => Self::User,
AccessScope::System => Self::System,
}
}
}
#[derive(Debug, Clone)]
enum ResolvedPrefix {
SingleDir(PathBuf),
Unix(PathBuf),
}
impl Default for ResolvedPrefix {
fn default() -> Self {
Self::SingleDir(PathBuf::default())
}
}
#[derive(Debug, Clone, Default)]
pub struct PathResolver {
app_id: String,
prefix: ResolvedPrefix,
env_map: Option<HashMap<OsString, OsString>>,
}
impl PathResolver {
pub fn new<I: AsRef<str>>(app_id: I, prefix: &AppPathPrefix) -> Result<Self, InstallerError> {
let app_id = app_id.as_ref();
Self::new_impl(app_id, prefix, None)
}
fn new_impl(
app_id: &str,
prefix: &AppPathPrefix,
env_map: Option<HashMap<OsString, OsString>>,
) -> Result<Self, InstallerError> {
let mut self_ = Self {
prefix: ResolvedPrefix::default(),
app_id: app_id.to_string(),
env_map,
};
self_.prefix = self_.resolve_prefix(prefix)?;
Ok(self_)
}
fn resolve_prefix(&mut self, prefix: &AppPathPrefix) -> Result<ResolvedPrefix, InstallerError> {
match prefix {
AppPathPrefix::User => self.resolve_user_prefix(),
AppPathPrefix::System => self.resolve_system_prefix(),
AppPathPrefix::SingleDir(path) => Ok(ResolvedPrefix::SingleDir(path.to_path_buf())),
AppPathPrefix::CustomUnix(path) => Ok(ResolvedPrefix::Unix(path.to_path_buf())),
}
}
fn resolve_user_prefix(&mut self) -> Result<ResolvedPrefix, InstallerError> {
match std::env::consts::FAMILY {
"windows" => {
let dir = self.get_env_var("LOCALAPPDATA")?;
let mut dir = PathBuf::from(dir);
dir.push("Programs");
dir.push(&self.app_id);
Ok(ResolvedPrefix::SingleDir(dir))
}
"unix" => {
let dir = self.get_env_var("HOME")?;
let mut dir = PathBuf::from(dir);
dir.push(".local");
Ok(ResolvedPrefix::Unix(dir))
}
_ => Err(InstallerErrorKind::UnsupportedOsFamily.into()),
}
}
fn resolve_system_prefix(&mut self) -> Result<ResolvedPrefix, InstallerError> {
match std::env::consts::FAMILY {
"windows" => {
let dir = self.get_env_var("PROGRAMFILES")?;
let mut dir = PathBuf::from(dir);
dir.push(&self.app_id);
Ok(ResolvedPrefix::SingleDir(dir))
}
"unix" => Ok(ResolvedPrefix::Unix(PathBuf::from("/usr/local"))),
_ => Err(InstallerErrorKind::UnsupportedOsFamily.into()),
}
}
pub fn bin_dir(&self) -> PathBuf {
match &self.prefix {
ResolvedPrefix::SingleDir(path) => path.join("bin"),
ResolvedPrefix::Unix(path) => path.join("bin"),
}
}
pub fn data_dir(&self) -> PathBuf {
match &self.prefix {
ResolvedPrefix::SingleDir(path) => path.to_path_buf(),
ResolvedPrefix::Unix(path) => path.join("share").join(&self.app_id),
}
}
fn get_env_var<K: AsRef<OsStr>>(&self, key: K) -> Result<OsString, InstallerError> {
if let Some(map) = &self.env_map {
map.get(key.as_ref())
.cloned()
.ok_or_else(|| InstallerErrorKind::InvalidEnvironmentVariable.into())
} else {
crate::os::env_var(key)
}
}
}
#[cfg(test)]
mod tests {
use std::path::Path;
use super::*;
fn get_env_map() -> HashMap<OsString, OsString> {
HashMap::from_iter(
[
("LOCALAPPDATA", "c:/users/rust/appdata/local"),
("PROGRAMFILES", "c:/program files"),
("XDG_DATA_HOME", "/home/rust/.local/share"),
("HOME", "/home/rust"),
]
.into_iter()
.map(|(k, v)| (k.into(), v.into())),
)
}
#[cfg(windows)]
#[test]
fn test_user_windows() {
let resolver =
PathResolver::new_impl("my_app", &AppPathPrefix::User, Some(get_env_map())).unwrap();
let bin_dir = resolver.bin_dir();
assert!(bin_dir.is_absolute());
assert_eq!(
bin_dir,
Path::new("c:/users/rust/appdata/local/Programs/my_app/bin")
);
let data_dir = resolver.data_dir();
assert!(data_dir.is_absolute());
assert_eq!(
data_dir,
Path::new("c:/users/rust/appdata/local/Programs/my_app")
);
}
#[cfg(unix)]
#[test]
fn test_user_unix() {
let resolver =
PathResolver::new_impl("my_app", &AppPathPrefix::User, Some(get_env_map())).unwrap();
let bin_dir = resolver.bin_dir();
assert!(bin_dir.is_absolute());
assert_eq!(bin_dir, Path::new("/home/rust/.local/bin"));
let data_dir = resolver.data_dir();
assert!(data_dir.is_absolute());
assert_eq!(data_dir, Path::new("/home/rust/.local/share/my_app"));
}
#[cfg(windows)]
#[test]
fn test_system_windows() {
let resolver =
PathResolver::new_impl("my_app", &AppPathPrefix::System, Some(get_env_map())).unwrap();
let bin_dir = resolver.bin_dir();
assert_eq!(bin_dir, Path::new("c:/program files/my_app/bin"));
let data_dir = resolver.data_dir();
assert_eq!(data_dir, Path::new("c:/program files/my_app"));
}
#[cfg(unix)]
#[test]
fn test_system_unix() {
let resolver =
PathResolver::new_impl("my_app", &AppPathPrefix::System, Some(get_env_map())).unwrap();
let bin_dir = resolver.bin_dir();
assert_eq!(bin_dir, Path::new("/usr/local/bin"));
let data_dir = resolver.data_dir();
assert_eq!(data_dir, Path::new("/usr/local/share/my_app"));
}
#[test]
fn test_single_dir() {
let resolver = PathResolver::new_impl(
"my_app",
&AppPathPrefix::SingleDir(PathBuf::from("/opt/my_app")),
Some(get_env_map()),
)
.unwrap();
let bin_dir = resolver.bin_dir();
assert_eq!(bin_dir, Path::new("/opt/my_app/bin"));
let data_dir = resolver.data_dir();
assert_eq!(data_dir, Path::new("/opt/my_app"));
}
#[test]
fn test_custom_unix() {
let resolver = PathResolver::new_impl(
"my_app",
&AppPathPrefix::CustomUnix(PathBuf::from("/usr2")),
Some(get_env_map()),
)
.unwrap();
let bin_dir = resolver.bin_dir();
assert_eq!(bin_dir, Path::new("/usr2/bin"));
let data_dir = resolver.data_dir();
assert_eq!(data_dir, Path::new("/usr2/share/my_app"));
}
}