use std::env;
use std::ffi::OsString;
use std::path::{Path, PathBuf};
const APP_DIR: &str = "franken_snowflake";
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum PortableDirKind {
Config,
Cache,
Artifacts,
}
impl PortableDirKind {
#[must_use]
pub const fn env_var(self) -> &'static str {
match self {
Self::Config => "FSNOW_CONFIG_DIR",
Self::Cache => "FSNOW_CACHE_DIR",
Self::Artifacts => "FSNOW_ARTIFACTS_DIR",
}
}
fn leaf(self) -> &'static str {
match self {
Self::Config => "config",
Self::Cache => "cache",
Self::Artifacts => "artifacts",
}
}
}
#[must_use]
pub fn resolve_project_dir(kind: PortableDirKind, explicit: Option<&Path>) -> PathBuf {
if let Some(path) = explicit {
return path.to_path_buf();
}
if let Some(path) = env::var_os(kind.env_var()) {
return PathBuf::from(path);
}
platform_default(kind)
}
#[must_use]
pub fn config_dir(explicit: Option<&Path>) -> PathBuf {
resolve_project_dir(PortableDirKind::Config, explicit)
}
#[must_use]
pub fn cache_dir(explicit: Option<&Path>) -> PathBuf {
resolve_project_dir(PortableDirKind::Cache, explicit)
}
#[must_use]
pub fn artifacts_dir(explicit: Option<&Path>) -> PathBuf {
resolve_project_dir(PortableDirKind::Artifacts, explicit)
}
fn platform_default(kind: PortableDirKind) -> PathBuf {
platform_default_with(kind, |name| env::var_os(name))
}
fn platform_default_with<F>(kind: PortableDirKind, mut env_get: F) -> PathBuf
where
F: FnMut(&str) -> Option<OsString>,
{
#[cfg(target_os = "windows")]
{
let base_var = match kind {
PortableDirKind::Config => "APPDATA",
PortableDirKind::Cache | PortableDirKind::Artifacts => "LOCALAPPDATA",
};
if let Some(base) = env_get(base_var) {
return PathBuf::from(base)
.join("FrankenSuite")
.join(APP_DIR)
.join(kind.leaf());
}
if let Some(home) = env_get("USERPROFILE") {
return PathBuf::from(home)
.join("AppData")
.join(if matches!(kind, PortableDirKind::Config) {
"Roaming"
} else {
"Local"
})
.join("FrankenSuite")
.join(APP_DIR)
.join(kind.leaf());
}
}
#[cfg(target_os = "macos")]
{
if let Some(home) = env_get("HOME") {
let base = PathBuf::from(home).join("Library").join(match kind {
PortableDirKind::Config | PortableDirKind::Artifacts => "Application Support",
PortableDirKind::Cache => "Caches",
});
return base.join(APP_DIR).join(kind.leaf());
}
}
#[cfg(all(unix, not(target_os = "macos")))]
{
let override_var = match kind {
PortableDirKind::Config => "XDG_CONFIG_HOME",
PortableDirKind::Cache => "XDG_CACHE_HOME",
PortableDirKind::Artifacts => "XDG_STATE_HOME",
};
if let Some(base) = env_get(override_var) {
return PathBuf::from(base).join(APP_DIR).join(kind.leaf());
}
if let Some(home) = env_get("HOME") {
let base = PathBuf::from(home).join(match kind {
PortableDirKind::Config => ".config",
PortableDirKind::Cache => ".cache",
PortableDirKind::Artifacts => ".local/state",
});
return base.join(APP_DIR).join(kind.leaf());
}
}
env::temp_dir().join(APP_DIR).join(kind.leaf())
}
#[cfg(test)]
mod tests {
use super::*;
use std::collections::BTreeMap;
fn fake_env(entries: &[(&str, &str)]) -> impl FnMut(&str) -> Option<OsString> {
let map = entries
.iter()
.map(|(key, value)| ((*key).to_owned(), OsString::from(value)))
.collect::<BTreeMap<_, _>>();
move |key| map.get(key).cloned()
}
#[test]
fn explicit_path_has_highest_precedence() {
let explicit = Path::new("/explicit/fsnow");
assert_eq!(
resolve_project_dir(PortableDirKind::Config, Some(explicit)),
explicit
);
}
#[test]
fn directory_kinds_expose_stable_env_overrides() {
assert_eq!(PortableDirKind::Config.env_var(), "FSNOW_CONFIG_DIR");
assert_eq!(PortableDirKind::Cache.env_var(), "FSNOW_CACHE_DIR");
assert_eq!(PortableDirKind::Artifacts.env_var(), "FSNOW_ARTIFACTS_DIR");
}
#[cfg(all(unix, not(target_os = "macos")))]
#[test]
fn linux_defaults_follow_xdg_then_home() {
let xdg = platform_default_with(
PortableDirKind::Artifacts,
fake_env(&[("XDG_STATE_HOME", "/xdg/state"), ("HOME", "/home/alice")]),
);
assert_eq!(
xdg,
PathBuf::from("/xdg/state").join(APP_DIR).join("artifacts")
);
let home =
platform_default_with(PortableDirKind::Cache, fake_env(&[("HOME", "/home/alice")]));
assert_eq!(
home,
PathBuf::from("/home/alice")
.join(".cache")
.join(APP_DIR)
.join("cache")
);
}
#[cfg(target_os = "macos")]
#[test]
fn macos_defaults_follow_library_locations() {
let config =
platform_default_with(PortableDirKind::Config, fake_env(&[("HOME", "/Users/a")]));
assert_eq!(
config,
PathBuf::from("/Users/a")
.join("Library")
.join("Application Support")
.join(APP_DIR)
.join("config")
);
}
#[cfg(target_os = "windows")]
#[test]
fn windows_defaults_follow_appdata_locations() {
let config = platform_default_with(
PortableDirKind::Config,
fake_env(&[("APPDATA", r"C:\Users\a\AppData\Roaming")]),
);
assert_eq!(
config,
PathBuf::from(r"C:\Users\a\AppData\Roaming")
.join("FrankenSuite")
.join(APP_DIR)
.join("config")
);
}
}