use crate::vault::Vault;
use anyhow::{Context, Result};
use std::fs;
use std::path::Path;
pub fn run(name: &str, file: &str, placeholder: Option<&str>, env_format: bool, quiet: bool) -> Result<()> {
let vault = Vault::open().context("failed to open vault")?;
let value = vault.get(name).context("failed to get secret")?;
let path = Path::new(file);
if env_format {
inject_env_format(path, name, &value)?;
} else if let Some(placeholder) = placeholder {
inject_placeholder(path, placeholder, &value)?;
} else {
anyhow::bail!("either --placeholder or --env-format is required");
}
if !quiet {
println!("Injected {} into {}", name, file);
}
Ok(())
}
fn inject_placeholder(path: &Path, placeholder: &str, value: &str) -> Result<()> {
let content = fs::read_to_string(path)
.with_context(|| format!("failed to read file: {}", path.display()))?;
if !content.contains(placeholder) {
anyhow::bail!(
"placeholder '{}' not found in file: {}",
placeholder,
path.display()
);
}
let new_content = content.replace(placeholder, value);
fs::write(path, new_content)
.with_context(|| format!("failed to write file: {}", path.display()))?;
Ok(())
}
fn inject_env_format(path: &Path, name: &str, value: &str) -> Result<()> {
let mut content = if path.exists() {
fs::read_to_string(path)
.with_context(|| format!("failed to read file: {}", path.display()))?
} else {
String::new()
};
let var_pattern = format!("{}=", name);
let mut found = false;
let mut new_lines: Vec<String> = Vec::new();
for line in content.lines() {
if line.starts_with(&var_pattern) || line.starts_with(&format!("export {}=", name)) {
new_lines.push(format!("{}={}", name, quote_env_value(value)));
found = true;
} else {
new_lines.push(line.to_string());
}
}
if !found {
new_lines.push(format!("{}={}", name, quote_env_value(value)));
}
content = new_lines.join("\n");
if !content.ends_with('\n') {
content.push('\n');
}
fs::write(path, content)
.with_context(|| format!("failed to write file: {}", path.display()))?;
Ok(())
}
fn quote_env_value(value: &str) -> String {
if value.contains(' ')
|| value.contains('"')
|| value.contains('\'')
|| value.contains('$')
|| value.contains('\n')
|| value.contains('#')
{
let escaped = value
.replace('\\', "\\\\")
.replace('"', "\\\"")
.replace('$', "\\$")
.replace('\n', "\\n");
format!("\"{}\"", escaped)
} else {
value.to_string()
}
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::NamedTempFile;
use std::io::Write;
#[test]
fn test_inject_placeholder() {
let mut file = NamedTempFile::new().unwrap();
writeln!(file, "password={{{{DB_PASS}}}}").unwrap();
inject_placeholder(file.path(), "{{DB_PASS}}", "secret123").unwrap();
let content = fs::read_to_string(file.path()).unwrap();
assert_eq!(content.trim(), "password=secret123");
}
#[test]
fn test_inject_env_format_new_file() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join(".env");
inject_env_format(&path, "API_KEY", "sk-12345").unwrap();
let content = fs::read_to_string(&path).unwrap();
assert_eq!(content, "API_KEY=sk-12345\n");
}
#[test]
fn test_inject_env_format_existing_var() {
let mut file = NamedTempFile::new().unwrap();
writeln!(file, "API_KEY=old-value").unwrap();
writeln!(file, "OTHER=keep").unwrap();
inject_env_format(file.path(), "API_KEY", "new-value").unwrap();
let content = fs::read_to_string(file.path()).unwrap();
assert!(content.contains("API_KEY=new-value"));
assert!(content.contains("OTHER=keep"));
assert!(!content.contains("old-value"));
}
#[test]
fn test_inject_env_format_append() {
let mut file = NamedTempFile::new().unwrap();
writeln!(file, "EXISTING=value").unwrap();
inject_env_format(file.path(), "NEW_KEY", "new-value").unwrap();
let content = fs::read_to_string(file.path()).unwrap();
assert!(content.contains("EXISTING=value"));
assert!(content.contains("NEW_KEY=new-value"));
}
#[test]
fn test_quote_env_value() {
assert_eq!(quote_env_value("simple"), "simple");
assert_eq!(quote_env_value("has space"), "\"has space\"");
assert_eq!(quote_env_value("has$dollar"), "\"has\\$dollar\"");
assert_eq!(quote_env_value("has\"quote"), "\"has\\\"quote\"");
}
}