1use age::secrecy::SecretString;
2use anyhow::{Context, Result};
3use std::collections::BTreeMap;
4use std::fs;
5use std::path::Path;
6
7use crate::crypto;
8
9pub type Sets = BTreeMap<String, BTreeMap<String, String>>;
10
11pub fn read_plain(path: &Path, passphrase: SecretString) -> Result<Option<Sets>> {
12 if !path.exists() {
13 return Ok(None);
14 }
15 let ciphertext =
16 fs::read(path).with_context(|| format!("failed to read {}", path.display()))?;
17 let plaintext = crypto::decrypt(&ciphertext, passphrase)?;
18 let sets = from_toml(&plaintext)?;
19 Ok(Some(sets))
20}
21
22pub fn from_toml(bytes: &[u8]) -> Result<Sets> {
23 let text = std::str::from_utf8(bytes).context("decrypted data is not valid UTF-8")?;
24 toml::from_str(text).context("decrypted data is not valid TOML")
25}
26
27pub fn to_toml(sets: &Sets) -> String {
28 toml::to_string_pretty(sets).expect("sets serialize to TOML")
29}
30
31pub fn atomic_write(path: &Path, bytes: &[u8]) -> Result<()> {
33 let dir = path.parent().unwrap_or_else(|| Path::new("."));
34 fs::create_dir_all(dir).with_context(|| format!("failed to create {}", dir.display()))?;
35 let file_name = path
36 .file_name()
37 .map(|n| n.to_string_lossy().into_owned())
38 .unwrap_or_else(|| "secure-env.enc".to_string());
39 let tmp = dir.join(format!(".{file_name}.tmp.{}", std::process::id()));
40 fs::write(&tmp, bytes).with_context(|| format!("failed to write {}", tmp.display()))?;
41 fs::rename(&tmp, path)
42 .with_context(|| format!("failed to move {} into place", tmp.display()))?;
43 Ok(())
44}