use std::path::Path;
use crate::error::Error;
use super::parser::{is_non_symlink_regular_file, read_envseal_nofollow, EnvMapping};
#[must_use]
pub fn discover(start_dir: &Path) -> Option<std::path::PathBuf> {
let mut dir = start_dir.to_path_buf();
loop {
let candidate = dir.join(".envseal");
if is_non_symlink_regular_file(&candidate) {
return Some(candidate);
}
if !dir.pop() {
break;
}
}
None
}
#[must_use]
pub fn global_envseal_path() -> Option<std::path::PathBuf> {
let home_var = if cfg!(target_os = "windows") {
std::env::var("USERPROFILE").or_else(|_| std::env::var("HOME"))
} else {
std::env::var("HOME")
};
home_var
.ok()
.map(|h| std::path::PathBuf::from(h).join(".envseal"))
.filter(|p| is_non_symlink_regular_file(p))
}
pub fn discover_and_load(start_dir: &Path) -> Result<Option<Vec<EnvMapping>>, Error> {
let project_file = discover(start_dir);
let global_file = global_envseal_path();
if project_file.is_none() && global_file.is_none() {
return Ok(None);
}
let mut mappings = Vec::new();
if let Some(ref global_path) = global_file {
mappings = read_envseal_nofollow(global_path)?;
}
if let Some(ref project_path) = project_file {
let project_mappings = read_envseal_nofollow(project_path)?;
merge_mappings(&mut mappings, &project_mappings);
}
if mappings.is_empty() {
Ok(None)
} else {
Ok(Some(mappings))
}
}
fn merge_mappings(base: &mut Vec<EnvMapping>, overrides: &[EnvMapping]) {
for new_mapping in overrides {
if let Some(existing) = base.iter_mut().find(|m| m.env_var == new_mapping.env_var) {
existing.secret_name.clone_from(&new_mapping.secret_name);
} else {
base.push(new_mapping.clone());
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn merge_project_overrides_global() {
let mut base = vec![
EnvMapping {
env_var: "A".into(),
secret_name: "global-a".into(),
},
EnvMapping {
env_var: "B".into(),
secret_name: "global-b".into(),
},
];
let overrides = vec![
EnvMapping {
env_var: "A".into(),
secret_name: "project-a".into(),
},
EnvMapping {
env_var: "C".into(),
secret_name: "project-c".into(),
},
];
merge_mappings(&mut base, &overrides);
assert_eq!(base.len(), 3);
assert_eq!(
base.iter().find(|m| m.env_var == "A").unwrap().secret_name,
"project-a"
);
assert_eq!(
base.iter().find(|m| m.env_var == "B").unwrap().secret_name,
"global-b"
);
assert_eq!(
base.iter().find(|m| m.env_var == "C").unwrap().secret_name,
"project-c"
);
}
#[test]
fn discover_walks_up_directories() {
let tmp = tempfile::tempdir().unwrap();
let deep = tmp.path().join("a").join("b").join("c");
std::fs::create_dir_all(&deep).unwrap();
std::fs::write(tmp.path().join(".envseal"), "X=y\n").unwrap();
let found = discover(&deep);
assert!(found.is_some());
assert_eq!(found.unwrap(), tmp.path().join(".envseal"));
}
#[test]
fn discover_returns_none_when_missing() {
let tmp = tempfile::tempdir().unwrap();
let deep = tmp.path().join("a").join("b");
std::fs::create_dir_all(&deep).unwrap();
assert!(discover(&deep).is_none());
}
}