use std::path::{Path, PathBuf};
use anyhow::{bail, Result};
pub fn resolve(profile: &str, dir: &Path) -> Result<PathBuf> {
if profile.is_empty() {
bail!("profile name cannot be empty");
}
if profile.contains('/') || profile.contains('\\') || profile.contains('\0') {
bail!(
"profile name '{}' contains invalid characters (path separators or null bytes)",
profile
);
}
if profile.contains("..") {
bail!(
"profile name '{}' contains '..', which is not allowed",
profile
);
}
let primary = dir.join(format!(".env.{}", profile));
if primary.exists() {
return Ok(primary);
}
let local = dir.join(format!(".env.{}.local", profile));
if local.exists() {
return Ok(local);
}
bail!(
"no .env file found for profile '{}'. Expected {} or {}",
profile,
primary.display(),
local.display()
);
}
#[allow(dead_code)]
pub fn resolve_file(file: Option<&str>, env_profile: Option<&str>, dir: &Path) -> Result<PathBuf> {
if let Some(profile) = env_profile {
return resolve(profile, dir);
}
let file = file.unwrap_or(".env");
let path = PathBuf::from(file);
Ok(path)
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::TempDir;
#[test]
fn resolve_primary() {
let dir = TempDir::new().unwrap();
std::fs::write(dir.path().join(".env.staging"), "KEY=value\n").unwrap();
let path = resolve("staging", dir.path()).unwrap();
assert_eq!(path, dir.path().join(".env.staging"));
}
#[test]
fn resolve_local_fallback() {
let dir = TempDir::new().unwrap();
std::fs::write(dir.path().join(".env.staging.local"), "KEY=local\n").unwrap();
let path = resolve("staging", dir.path()).unwrap();
assert_eq!(path, dir.path().join(".env.staging.local"));
}
#[test]
fn resolve_primary_preferred_over_local() {
let dir = TempDir::new().unwrap();
std::fs::write(dir.path().join(".env.staging"), "KEY=primary\n").unwrap();
std::fs::write(dir.path().join(".env.staging.local"), "KEY=local\n").unwrap();
let path = resolve("staging", dir.path()).unwrap();
assert_eq!(path, dir.path().join(".env.staging"));
}
#[test]
fn resolve_missing_profile() {
let dir = TempDir::new().unwrap();
let result = resolve("production", dir.path());
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("production"));
}
#[test]
fn resolve_file_with_profile() {
let dir = TempDir::new().unwrap();
std::fs::write(dir.path().join(".env.dev"), "KEY=dev\n").unwrap();
let path = resolve_file(Some("other.env"), Some("dev"), dir.path()).unwrap();
assert_eq!(path, dir.path().join(".env.dev"));
}
#[test]
fn resolve_file_without_profile() {
let dir = TempDir::new().unwrap();
let path = resolve_file(Some("custom.env"), None, dir.path()).unwrap();
assert_eq!(path, PathBuf::from("custom.env"));
}
#[test]
fn resolve_file_defaults() {
let dir = TempDir::new().unwrap();
let path = resolve_file(None, None, dir.path()).unwrap();
assert_eq!(path, PathBuf::from(".env"));
}
}