1use std::{env, path::PathBuf};
2
3use log::{info, warn};
4use orion_conf::{TomlIO, YamlIO};
5use orion_error::prelude::*;
6use orion_variate::vars::UpperKey;
7use orion_variate::vars::{EnvDict, ValueDict};
8
9use crate::{
10 error::SecResult,
11 sec::{NoSecConv, SecFrom, SecValueObj, SecValueType},
12};
13
14const SEC_PREFIX: &str = "SEC_";
15const SEC_VALUE_FILE_NAME: &str = "sec_value.yml";
16const GALAXY_DOT_DIR: &str = ".galaxy";
17const DEFAULT_FALLBACK_DIR: &str = "./";
18
19fn normalize_sec_key(key: &str) -> UpperKey {
20 let upper = key.to_uppercase();
21 if upper.starts_with(SEC_PREFIX) {
22 UpperKey::from(upper)
23 } else {
24 UpperKey::from(format!("{SEC_PREFIX}{upper}"))
25 }
26}
27
28pub fn load_sec_dict() -> SecResult<EnvDict> {
29 let space = load_secfile()?;
30 let mut dict = EnvDict::new();
31 for (k, v) in space.no_sec() {
32 dict.insert(k, v);
33 }
34 Ok(dict)
35}
36
37pub fn load_sec_dict_by(dot_name: &str, file_name: &str, fmt: SecFileFmt) -> SecResult<EnvDict> {
38 let sec_file = dot_path(dot_name).join(file_name);
39 let space = load_secfile_by(sec_file, fmt)?;
40 let mut dict = EnvDict::new();
41 for (k, v) in space.no_sec() {
42 dict.insert(k, v);
43 }
44 Ok(dict)
45}
46
47pub fn load_secfile() -> SecResult<SecValueObj> {
48 let default = sec_value_galaxy_path();
49 load_secfile_by(default, SecFileFmt::Yaml)
50}
51
52pub fn load_galaxy_secfile() -> SecResult<SecValueObj> {
53 let default = sec_value_galaxy_path();
54 load_secfile_by(default, SecFileFmt::Yaml)
55}
56pub enum SecFileFmt {
57 Yaml,
58 Toml,
59}
60
61pub fn load_secfile_by(sec_file: PathBuf, fmt: SecFileFmt) -> SecResult<SecValueObj> {
62 let mut vars_dict = SecValueObj::new();
63 if sec_file.exists() {
64 let dict = match fmt {
65 SecFileFmt::Yaml => ValueDict::load_yaml(&sec_file)
66 .conv_err()
67 .with_context(&sec_file)?,
68 SecFileFmt::Toml => ValueDict::load_toml(&sec_file)
69 .conv_err()
70 .with_context(&sec_file)?,
71 };
72 info!(target: "exec"," load {}", sec_file.display());
73 for (k, v) in dict.iter() {
74 vars_dict.insert(
75 normalize_sec_key(k.as_str()),
76 SecValueType::sec_from(v.clone()),
77 );
78 }
79 }
80 Ok(vars_dict)
81}
82
83pub fn sec_value_galaxy_path() -> PathBuf {
84 dot_path(GALAXY_DOT_DIR).join(SEC_VALUE_FILE_NAME)
85}
86
87pub fn dot_path(name: &str) -> PathBuf {
88 let current_dir = env::current_dir().unwrap_or_else(|_| PathBuf::from(DEFAULT_FALLBACK_DIR));
89 let local_candidate = current_dir.join(name);
90
91 if local_candidate.exists() {
92 return local_candidate;
93 }
94
95 match resolve_home_dir() {
96 Some(home) => home.join(name),
97 None => {
98 warn!(target: "exec", " HOME not set; defaulting to current directory for {}", name);
99 local_candidate
100 }
101 }
102}
103
104fn resolve_home_dir() -> Option<PathBuf> {
105 env::var_os("HOME")
106 .map(PathBuf::from)
107 .or_else(|| env::var_os("USERPROFILE").map(PathBuf::from))
108 .or_else(|| {
109 let drive = env::var_os("HOMEDRIVE")?;
110 let path = env::var_os("HOMEPATH")?;
111 let mut buf = PathBuf::from(drive);
112 buf.push(path);
113 Some(buf)
114 })
115}
116
117#[cfg(test)]
118mod tests {
119 use super::*;
120 use std::ffi::OsString;
121 use std::fs;
122 use std::io::Write;
123 use std::path::{Path, PathBuf};
124 use std::sync::{Mutex, MutexGuard, OnceLock};
125 use tempfile::{NamedTempFile, TempDir};
126
127 #[test]
128 fn test_load_secfile_by_nonexistent_file() {
129 let path = PathBuf::from("/nonexistent/path/to/file.yml");
130 let result = load_secfile_by(path, SecFileFmt::Yaml);
131 assert!(result.is_ok());
132 assert!(result.unwrap().is_empty());
133 }
134
135 #[test]
136 fn test_load_secfile_by_yaml() {
137 let mut file = NamedTempFile::with_suffix(".yml").unwrap();
138 writeln!(file, "username: admin").unwrap();
139 writeln!(file, "password: secret123").unwrap();
140 writeln!(file, "port: 8080").unwrap();
141
142 let result = load_secfile_by(file.path().to_path_buf(), SecFileFmt::Yaml);
143 assert!(result.is_ok());
144
145 let obj = result.unwrap();
146 assert_eq!(obj.len(), 3);
147 assert!(obj.contains_key(&UpperKey::from("SEC_USERNAME".to_string())));
148 assert!(obj.contains_key(&UpperKey::from("SEC_PASSWORD".to_string())));
149 assert!(obj.contains_key(&UpperKey::from("SEC_PORT".to_string())));
150 }
151
152 #[test]
153 fn test_load_secfile_by_toml() {
154 let mut file = NamedTempFile::with_suffix(".toml").unwrap();
155 writeln!(file, "api_key = \"abc123\"").unwrap();
156 writeln!(file, "debug = true").unwrap();
157
158 let result = load_secfile_by(file.path().to_path_buf(), SecFileFmt::Toml);
159 assert!(result.is_ok());
160
161 let obj = result.unwrap();
162 assert_eq!(obj.len(), 2);
163 assert!(obj.contains_key(&UpperKey::from("SEC_API_KEY".to_string())));
164 assert!(obj.contains_key(&UpperKey::from("SEC_DEBUG".to_string())));
165 }
166
167 #[test]
168 fn test_load_secfile_by_key_uppercase() {
169 let mut file = NamedTempFile::with_suffix(".yml").unwrap();
170 writeln!(file, "mixedCase: value1").unwrap();
171 writeln!(file, "lower_case: value2").unwrap();
172
173 let result = load_secfile_by(file.path().to_path_buf(), SecFileFmt::Yaml);
174 assert!(result.is_ok());
175
176 let obj = result.unwrap();
177 assert!(obj.contains_key(&UpperKey::from("SEC_MIXEDCASE".to_string())));
178 assert!(obj.contains_key(&UpperKey::from("SEC_LOWER_CASE".to_string())));
179 }
180
181 #[test]
182 fn test_load_secfile_by_keeps_existing_sec_prefix() {
183 let mut file = NamedTempFile::with_suffix(".toml").unwrap();
184 writeln!(file, "SEC_data_1 = \"value1\"").unwrap();
185
186 let result = load_secfile_by(file.path().to_path_buf(), SecFileFmt::Toml);
187 assert!(result.is_ok());
188
189 let obj = result.unwrap();
190 assert!(obj.contains_key(&UpperKey::from("SEC_DATA_1".to_string())));
191 assert!(!obj.contains_key(&UpperKey::from("SEC_SEC_DATA_1".to_string())));
192 }
193
194 #[test]
195 fn test_load_secfile_by_values_are_secret() {
196 let mut file = NamedTempFile::with_suffix(".yml").unwrap();
197 writeln!(file, "token: my_secret_token").unwrap();
198
199 let result = load_secfile_by(file.path().to_path_buf(), SecFileFmt::Yaml);
200 assert!(result.is_ok());
201
202 let obj = result.unwrap();
203 let value = obj.get(&UpperKey::from("SEC_TOKEN".to_string())).unwrap();
204 assert!(matches!(value, SecValueType::String(s) if s.is_secret()));
205 }
206
207 #[test]
208 fn test_load_secfile_by_empty_file() {
209 let file = NamedTempFile::with_suffix(".yml").unwrap();
210
211 let result = load_secfile_by(file.path().to_path_buf(), SecFileFmt::Yaml);
212 assert!(result.is_ok());
213 assert!(result.unwrap().is_empty());
214 }
215
216 #[test]
217 fn test_load_sec_dict_by_yaml() {
218 with_temp_home(|home_path| {
219 let dot_dir = home_path.join(".myapp");
220 fs::create_dir_all(&dot_dir).unwrap();
221
222 let sec_file = dot_dir.join("secrets.yml");
223 let mut file = fs::File::create(&sec_file).unwrap();
224 writeln!(file, "db_user: root").unwrap();
225 writeln!(file, "db_pass: password123").unwrap();
226
227 let result = load_sec_dict_by(".myapp", "secrets.yml", SecFileFmt::Yaml);
228 assert!(result.is_ok());
229
230 let dict = result.unwrap();
231 assert_eq!(dict.len(), 2);
232 assert!(dict.contains_key("SEC_DB_USER"));
233 assert!(dict.contains_key("SEC_DB_PASS"));
234 });
235 }
236
237 #[test]
238 fn test_load_sec_dict_by_toml() {
239 with_temp_home(|home_path| {
240 let dot_dir = home_path.join(".config");
241 fs::create_dir_all(&dot_dir).unwrap();
242
243 let sec_file = dot_dir.join("app.toml");
244 let mut file = fs::File::create(&sec_file).unwrap();
245 writeln!(file, "secret_key = \"abc123\"").unwrap();
246 writeln!(file, "enabled = true").unwrap();
247
248 let result = load_sec_dict_by(".config", "app.toml", SecFileFmt::Toml);
249 assert!(result.is_ok());
250
251 let dict = result.unwrap();
252 assert_eq!(dict.len(), 2);
253 assert!(dict.contains_key("SEC_SECRET_KEY"));
254 assert!(dict.contains_key("SEC_ENABLED"));
255 });
256 }
257
258 #[test]
259 fn test_load_sec_dict_by_nonexistent_dir() {
260 with_temp_home(|_| {
261 let result = load_sec_dict_by(".nonexistent", "file.yml", SecFileFmt::Yaml);
262 assert!(result.is_ok());
263 assert!(result.unwrap().is_empty());
264 });
265 }
266
267 #[test]
268 fn test_load_sec_dict_by_values_not_secret() {
269 with_temp_home(|home_path| {
270 let dot_dir = home_path.join(".test");
271 fs::create_dir_all(&dot_dir).unwrap();
272
273 let sec_file = dot_dir.join("data.yml");
274 let mut file = fs::File::create(&sec_file).unwrap();
275 writeln!(file, "value: test_data").unwrap();
276
277 let result = load_sec_dict_by(".test", "data.yml", SecFileFmt::Yaml);
278 assert!(result.is_ok());
279
280 let dict = result.unwrap();
281 assert!(dict.contains_key("SEC_VALUE"));
283 });
284 }
285
286 #[test]
287 fn test_dot_path_prefers_current_dir_before_home() {
288 let workspace = TempDir::new().unwrap();
289 let workspace_path = fs::canonicalize(workspace.path()).unwrap();
290 let local_dot_dir = workspace_path.join(GALAXY_DOT_DIR);
291 fs::create_dir_all(&local_dot_dir).unwrap();
292
293 with_temp_home(|home_path| {
294 let home_dot_dir = home_path.join(GALAXY_DOT_DIR);
295 fs::create_dir_all(&home_dot_dir).unwrap();
296
297 let _cwd_guard = CurrentDirGuard::set(&workspace_path);
298 let resolved = dot_path(GALAXY_DOT_DIR);
299 assert_eq!(resolved, local_dot_dir);
300 });
301 }
302
303 #[test]
304 fn test_dot_path_falls_back_to_home_when_local_missing() {
305 let workspace = TempDir::new().unwrap();
306 let workspace_path = fs::canonicalize(workspace.path()).unwrap();
307
308 with_temp_home(|home_path| {
309 let home_dot_dir = home_path.join(GALAXY_DOT_DIR);
310 fs::create_dir_all(&home_dot_dir).unwrap();
311
312 let _cwd_guard = CurrentDirGuard::set(&workspace_path);
313 let resolved = dot_path(GALAXY_DOT_DIR);
314 assert_eq!(resolved, home_dot_dir);
315 });
316 }
317
318 #[test]
319 fn test_load_sec_dict_by_prefers_current_dir() {
320 let workspace = TempDir::new().unwrap();
321 let workspace_path = fs::canonicalize(workspace.path()).unwrap();
322 let dot_name = ".pref";
323 let local_dot_dir = workspace_path.join(dot_name);
324 fs::create_dir_all(&local_dot_dir).unwrap();
325 let local_file = local_dot_dir.join("data.yml");
326 let mut local_writer = fs::File::create(&local_file).unwrap();
327 writeln!(local_writer, "local_only: true").unwrap();
328
329 with_temp_home(|home_path| {
330 let home_dot_dir = home_path.join(dot_name);
331 fs::create_dir_all(&home_dot_dir).unwrap();
332 let home_file = home_dot_dir.join("data.yml");
333 let mut home_writer = fs::File::create(&home_file).unwrap();
334 writeln!(home_writer, "home_only: true").unwrap();
335
336 let _cwd_guard = CurrentDirGuard::set(&workspace_path);
337 let dict = load_sec_dict_by(dot_name, "data.yml", SecFileFmt::Yaml).unwrap();
338 assert!(dict.contains_key("SEC_LOCAL_ONLY"));
339 assert!(!dict.contains_key("SEC_HOME_ONLY"));
340 });
341 }
342
343 fn with_temp_home<F>(test: F)
344 where
345 F: FnOnce(&Path),
346 {
347 let temp_dir = TempDir::new().unwrap();
348 let _guard = HomeGuard::set(temp_dir.path());
349 test(temp_dir.path());
350 }
351
352 struct HomeGuard {
353 old_home: Option<OsString>,
354 _lock: MutexGuard<'static, ()>,
355 }
356
357 impl HomeGuard {
358 fn set(path: &Path) -> Self {
359 let lock = home_lock().lock().unwrap_or_else(|err| err.into_inner());
360 let old_home = env::var_os("HOME");
361 unsafe {
362 env::set_var("HOME", path);
363 }
364
365 Self {
366 old_home,
367 _lock: lock,
368 }
369 }
370 }
371
372 impl Drop for HomeGuard {
373 fn drop(&mut self) {
374 if let Some(ref home) = self.old_home {
375 unsafe {
376 env::set_var("HOME", home);
377 }
378 } else {
379 unsafe {
380 env::remove_var("HOME");
381 }
382 }
383 }
384 }
385
386 fn home_lock() -> &'static Mutex<()> {
387 static HOME_MUTEX: OnceLock<Mutex<()>> = OnceLock::new();
388 HOME_MUTEX.get_or_init(|| Mutex::new(()))
389 }
390
391 struct CurrentDirGuard {
392 old_dir: PathBuf,
393 _lock: MutexGuard<'static, ()>,
394 }
395
396 impl CurrentDirGuard {
397 fn set(path: &Path) -> Self {
398 let lock = current_dir_lock()
399 .lock()
400 .unwrap_or_else(|err| err.into_inner());
401 let old_dir = env::current_dir().unwrap();
402 env::set_current_dir(path).unwrap();
403
404 Self {
405 old_dir,
406 _lock: lock,
407 }
408 }
409 }
410
411 impl Drop for CurrentDirGuard {
412 fn drop(&mut self) {
413 env::set_current_dir(&self.old_dir).unwrap();
414 }
415 }
416
417 fn current_dir_lock() -> &'static Mutex<()> {
418 static CURRENT_DIR_MUTEX: OnceLock<Mutex<()>> = OnceLock::new();
419 CURRENT_DIR_MUTEX.get_or_init(|| Mutex::new(()))
420 }
421}