1use std::collections::HashMap;
8use std::ffi::{OsStr, OsString};
9use std::path::PathBuf;
10
11pub trait EnvAccess {
22 fn var_os(&self, name: &str) -> Option<OsString>;
24
25 fn config_dir(&self) -> Option<PathBuf>;
27
28 fn data_dir(&self) -> Option<PathBuf>;
30
31 fn cache_dir(&self) -> Option<PathBuf>;
33
34 fn home_dir(&self) -> Option<PathBuf>;
36
37 #[cfg(windows)]
39 fn program_data_dir(&self) -> Option<PathBuf>;
40
41 fn var(&self, name: &str) -> Option<String> {
43 self.var_os(name).and_then(|value| value.into_string().ok())
44 }
45}
46
47pub struct SystemEnv;
49
50impl EnvAccess for SystemEnv {
51 fn var_os(&self, name: &str) -> Option<OsString> {
52 std::env::var_os(name)
53 }
54
55 fn config_dir(&self) -> Option<PathBuf> {
56 dirs::config_dir()
57 }
58
59 fn data_dir(&self) -> Option<PathBuf> {
60 dirs::data_dir()
61 }
62
63 fn cache_dir(&self) -> Option<PathBuf> {
64 dirs::cache_dir()
65 }
66
67 fn home_dir(&self) -> Option<PathBuf> {
68 dirs::home_dir()
69 }
70
71 #[cfg(windows)]
72 fn program_data_dir(&self) -> Option<PathBuf> {
73 known_folder_program_data().or_else(|| std::env::var_os("ProgramData").map(PathBuf::from))
78 }
79}
80
81#[cfg(windows)]
83fn known_folder_program_data() -> Option<PathBuf> {
84 use std::ffi::OsString;
85 use std::os::windows::ffi::OsStringExt;
86 use std::slice;
87 use windows_sys::Win32::Globalization::lstrlenW;
88 use windows_sys::Win32::System::Com::CoTaskMemFree;
89 use windows_sys::Win32::UI::Shell::{FOLDERID_ProgramData, SHGetKnownFolderPath};
90 use windows_sys::core::PWSTR;
91
92 unsafe {
96 let mut path_ptr: PWSTR = std::ptr::null_mut();
97 let result = SHGetKnownFolderPath(
98 &FOLDERID_ProgramData,
99 0,
100 std::ptr::null_mut(),
101 &mut path_ptr,
102 );
103 if result == 0 && !path_ptr.is_null() {
104 let len = lstrlenW(path_ptr) as usize;
105 let path = slice::from_raw_parts(path_ptr, len);
106 let ostr = OsString::from_wide(path);
107 CoTaskMemFree(path_ptr.cast());
108 Some(PathBuf::from(ostr))
109 } else {
110 if !path_ptr.is_null() {
111 CoTaskMemFree(path_ptr.cast());
112 }
113 None
114 }
115 }
116}
117
118#[derive(derive_setters::Setters)]
123#[setters(prefix = "with_", strip_option, into)]
124pub struct TestEnv {
125 #[setters(skip)]
126 vars: HashMap<String, OsString>,
127 config_dir: Option<PathBuf>,
128 data_dir: Option<PathBuf>,
129 cache_dir: Option<PathBuf>,
130 home_dir: Option<PathBuf>,
131 #[cfg(windows)]
132 program_data_dir: Option<PathBuf>,
133}
134
135impl TestEnv {
136 pub fn new(vars: HashMap<String, String>) -> Self {
138 Self {
139 vars: vars
140 .into_iter()
141 .map(|(k, v)| (k, OsString::from(v)))
142 .collect(),
143 config_dir: None,
144 data_dir: None,
145 cache_dir: None,
146 home_dir: None,
147 #[cfg(windows)]
148 program_data_dir: None,
149 }
150 }
151
152 pub fn with_os_vars(vars: HashMap<String, OsString>) -> Self {
154 Self {
155 vars,
156 config_dir: None,
157 data_dir: None,
158 cache_dir: None,
159 home_dir: None,
160 #[cfg(windows)]
161 program_data_dir: None,
162 }
163 }
164
165 pub fn insert_os(&mut self, name: impl Into<String>, value: impl AsRef<OsStr>) {
167 self.vars.insert(name.into(), value.as_ref().to_os_string());
168 }
169}
170
171impl EnvAccess for TestEnv {
172 fn var_os(&self, name: &str) -> Option<OsString> {
173 self.vars.get(name).cloned()
174 }
175
176 fn config_dir(&self) -> Option<PathBuf> {
177 self.config_dir.clone()
178 }
179
180 fn data_dir(&self) -> Option<PathBuf> {
181 self.data_dir.clone()
182 }
183
184 fn cache_dir(&self) -> Option<PathBuf> {
185 self.cache_dir.clone()
186 }
187
188 fn home_dir(&self) -> Option<PathBuf> {
189 self.home_dir.clone()
190 }
191
192 #[cfg(windows)]
193 fn program_data_dir(&self) -> Option<PathBuf> {
194 self.program_data_dir
195 .clone()
196 .or_else(|| self.var_os("ProgramData").map(PathBuf::from))
197 }
198}
199
200#[cfg(all(test, windows))]
201mod tests {
202 use super::*;
203 use std::path::{Component, Path};
204
205 fn path_key(path: &Path) -> String {
207 path.components()
208 .map(|c| match c {
209 Component::Prefix(p) => p.as_os_str().to_string_lossy().to_ascii_lowercase(),
210 Component::RootDir => String::new(),
211 Component::Normal(s) => s.to_string_lossy().to_ascii_lowercase(),
212 Component::CurDir => ".".into(),
213 Component::ParentDir => "..".into(),
214 })
215 .filter(|s| !s.is_empty())
216 .collect::<Vec<_>>()
217 .join("/")
218 }
219
220 #[test]
221 fn known_folder_program_data_returns_absolute_existing_directory() {
222 let path = known_folder_program_data().expect(
223 "SHGetKnownFolderPath(FOLDERID_ProgramData) should succeed on Windows CI/hosts",
224 );
225
226 assert!(
227 path.is_absolute(),
228 "ProgramData known folder must be absolute, got {path:?}"
229 );
230 assert!(
231 path.is_dir(),
232 "ProgramData known folder must exist as a directory, got {path:?}"
233 );
234 assert_eq!(
235 path.file_name().and_then(|n| n.to_str()),
236 Some("ProgramData"),
237 "expected final component to be ProgramData, got {path:?}"
238 );
239 }
240
241 #[test]
242 fn system_env_program_data_dir_uses_known_folder_when_available() {
243 let from_api = known_folder_program_data()
244 .expect("known folder lookup should succeed on Windows CI/hosts");
245 let from_system = SystemEnv
246 .program_data_dir()
247 .expect("SystemEnv::program_data_dir should resolve ProgramData");
248
249 assert_eq!(
250 path_key(&from_system),
251 path_key(&from_api),
252 "SystemEnv should prefer the known-folder path over env fallback; \
253 system={from_system:?} api={from_api:?}"
254 );
255
256 if let Some(from_env) = std::env::var_os("ProgramData").map(PathBuf::from) {
259 assert_eq!(
260 path_key(&from_api),
261 path_key(&from_env),
262 "known folder and %ProgramData% should resolve to the same location; \
263 api={from_api:?} env={from_env:?}"
264 );
265 }
266 }
267}