Skip to main content

orion_sec/
load.rs

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            // EnvDict 中的值已经通过 no_sec() 转换,不再是 secret
282            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}