use std::collections::HashMap;
use std::ffi::{OsStr, OsString};
use std::path::PathBuf;
pub trait EnvAccess {
fn var_os(&self, name: &str) -> Option<OsString>;
fn config_dir(&self) -> Option<PathBuf>;
fn data_dir(&self) -> Option<PathBuf>;
fn cache_dir(&self) -> Option<PathBuf>;
fn home_dir(&self) -> Option<PathBuf>;
#[cfg(windows)]
fn program_data_dir(&self) -> Option<PathBuf>;
fn var(&self, name: &str) -> Option<String> {
self.var_os(name).and_then(|value| value.into_string().ok())
}
}
pub struct SystemEnv;
impl EnvAccess for SystemEnv {
fn var_os(&self, name: &str) -> Option<OsString> {
std::env::var_os(name)
}
fn config_dir(&self) -> Option<PathBuf> {
dirs::config_dir()
}
fn data_dir(&self) -> Option<PathBuf> {
dirs::data_dir()
}
fn cache_dir(&self) -> Option<PathBuf> {
dirs::cache_dir()
}
fn home_dir(&self) -> Option<PathBuf> {
dirs::home_dir()
}
#[cfg(windows)]
fn program_data_dir(&self) -> Option<PathBuf> {
known_folder_program_data().or_else(|| std::env::var_os("ProgramData").map(PathBuf::from))
}
}
#[cfg(windows)]
fn known_folder_program_data() -> Option<PathBuf> {
use std::ffi::OsString;
use std::os::windows::ffi::OsStringExt;
use std::slice;
use windows_sys::Win32::Globalization::lstrlenW;
use windows_sys::Win32::System::Com::CoTaskMemFree;
use windows_sys::Win32::UI::Shell::{FOLDERID_ProgramData, SHGetKnownFolderPath};
use windows_sys::core::PWSTR;
unsafe {
let mut path_ptr: PWSTR = std::ptr::null_mut();
let result = SHGetKnownFolderPath(
&FOLDERID_ProgramData,
0,
std::ptr::null_mut(),
&mut path_ptr,
);
if result == 0 && !path_ptr.is_null() {
let len = lstrlenW(path_ptr) as usize;
let path = slice::from_raw_parts(path_ptr, len);
let ostr = OsString::from_wide(path);
CoTaskMemFree(path_ptr.cast());
Some(PathBuf::from(ostr))
} else {
if !path_ptr.is_null() {
CoTaskMemFree(path_ptr.cast());
}
None
}
}
}
#[derive(derive_setters::Setters)]
#[setters(prefix = "with_", strip_option, into)]
pub struct TestEnv {
#[setters(skip)]
vars: HashMap<String, OsString>,
config_dir: Option<PathBuf>,
data_dir: Option<PathBuf>,
cache_dir: Option<PathBuf>,
home_dir: Option<PathBuf>,
#[cfg(windows)]
program_data_dir: Option<PathBuf>,
}
impl TestEnv {
pub fn new(vars: HashMap<String, String>) -> Self {
Self {
vars: vars
.into_iter()
.map(|(k, v)| (k, OsString::from(v)))
.collect(),
config_dir: None,
data_dir: None,
cache_dir: None,
home_dir: None,
#[cfg(windows)]
program_data_dir: None,
}
}
pub fn with_os_vars(vars: HashMap<String, OsString>) -> Self {
Self {
vars,
config_dir: None,
data_dir: None,
cache_dir: None,
home_dir: None,
#[cfg(windows)]
program_data_dir: None,
}
}
pub fn insert_os(&mut self, name: impl Into<String>, value: impl AsRef<OsStr>) {
self.vars.insert(name.into(), value.as_ref().to_os_string());
}
}
impl EnvAccess for TestEnv {
fn var_os(&self, name: &str) -> Option<OsString> {
self.vars.get(name).cloned()
}
fn config_dir(&self) -> Option<PathBuf> {
self.config_dir.clone()
}
fn data_dir(&self) -> Option<PathBuf> {
self.data_dir.clone()
}
fn cache_dir(&self) -> Option<PathBuf> {
self.cache_dir.clone()
}
fn home_dir(&self) -> Option<PathBuf> {
self.home_dir.clone()
}
#[cfg(windows)]
fn program_data_dir(&self) -> Option<PathBuf> {
self.program_data_dir
.clone()
.or_else(|| self.var_os("ProgramData").map(PathBuf::from))
}
}
#[cfg(all(test, windows))]
mod tests {
use super::*;
use std::path::{Component, Path};
fn path_key(path: &Path) -> String {
path.components()
.map(|c| match c {
Component::Prefix(p) => p.as_os_str().to_string_lossy().to_ascii_lowercase(),
Component::RootDir => String::new(),
Component::Normal(s) => s.to_string_lossy().to_ascii_lowercase(),
Component::CurDir => ".".into(),
Component::ParentDir => "..".into(),
})
.filter(|s| !s.is_empty())
.collect::<Vec<_>>()
.join("/")
}
#[test]
fn known_folder_program_data_returns_absolute_existing_directory() {
let path = known_folder_program_data().expect(
"SHGetKnownFolderPath(FOLDERID_ProgramData) should succeed on Windows CI/hosts",
);
assert!(
path.is_absolute(),
"ProgramData known folder must be absolute, got {path:?}"
);
assert!(
path.is_dir(),
"ProgramData known folder must exist as a directory, got {path:?}"
);
assert_eq!(
path.file_name().and_then(|n| n.to_str()),
Some("ProgramData"),
"expected final component to be ProgramData, got {path:?}"
);
}
#[test]
fn system_env_program_data_dir_uses_known_folder_when_available() {
let from_api = known_folder_program_data()
.expect("known folder lookup should succeed on Windows CI/hosts");
let from_system = SystemEnv
.program_data_dir()
.expect("SystemEnv::program_data_dir should resolve ProgramData");
assert_eq!(
path_key(&from_system),
path_key(&from_api),
"SystemEnv should prefer the known-folder path over env fallback; \
system={from_system:?} api={from_api:?}"
);
if let Some(from_env) = std::env::var_os("ProgramData").map(PathBuf::from) {
assert_eq!(
path_key(&from_api),
path_key(&from_env),
"known folder and %ProgramData% should resolve to the same location; \
api={from_api:?} env={from_env:?}"
);
}
}
}