1use anyhow::{Context, Result};
2use sha2::{Digest, Sha256};
3use std::fs::{self, File, OpenOptions};
4use std::io::{Read, Write};
5use std::path::{Path, PathBuf};
6use std::time::{SystemTime, UNIX_EPOCH};
7
8#[cfg(unix)]
9use std::os::unix::fs::PermissionsExt;
10
11pub const SECRET_FILE_MODE: u32 = 0o600;
12
13pub fn sha256_file(path: &Path) -> Result<String> {
14 let mut file = File::open(path)
15 .with_context(|| format!("failed to open for sha256: {}", path.display()))?;
16 let mut hasher = Sha256::new();
17 let mut buf = [0u8; 8192];
18 loop {
19 let read = file.read(&mut buf)?;
20 if read == 0 {
21 break;
22 }
23 hasher.update(&buf[..read]);
24 }
25 let digest = hasher.finalize();
26 let mut out = String::with_capacity(digest.len() * 2);
27 for byte in digest {
28 out.push_str(&format!("{:02x}", byte));
29 }
30 Ok(out)
31}
32
33pub fn write_atomic(path: &Path, contents: &[u8], mode: u32) -> Result<()> {
34 if let Some(parent) = path.parent() {
35 fs::create_dir_all(parent)
36 .with_context(|| format!("failed to create dir: {}", parent.display()))?;
37 }
38
39 let mut attempt = 0u32;
40 loop {
41 let tmp_path = temp_path(path, attempt);
42 match OpenOptions::new()
43 .write(true)
44 .create_new(true)
45 .open(&tmp_path)
46 {
47 Ok(mut file) => {
48 file.write_all(contents).with_context(|| {
49 format!("failed to write temp file: {}", tmp_path.display())
50 })?;
51 file.flush().ok();
52
53 set_permissions(&tmp_path, mode)?;
54 drop(file);
55
56 fs::rename(&tmp_path, path).with_context(|| {
57 format!(
58 "failed to rename {} -> {}",
59 tmp_path.display(),
60 path.display()
61 )
62 })?;
63 set_permissions(path, mode)?;
64 return Ok(());
65 }
66 Err(err) if err.kind() == std::io::ErrorKind::AlreadyExists => {
67 attempt += 1;
68 if attempt > 10 {
69 return Err(err).context("failed to create unique temp file");
70 }
71 }
72 Err(err) => return Err(err).context("failed to create temp file"),
73 }
74 }
75}
76
77pub fn write_timestamp(path: &Path, iso: Option<&str>) -> Result<()> {
78 if let Some(parent) = path.parent() {
79 fs::create_dir_all(parent)
80 .with_context(|| format!("failed to create dir: {}", parent.display()))?;
81 }
82
83 if let Some(raw) = iso {
84 let trimmed = raw.split(&['\n', '\r'][..]).next().unwrap_or("");
85 if !trimmed.is_empty() {
86 fs::write(path, trimmed)
87 .with_context(|| format!("failed to write timestamp: {}", path.display()))?;
88 return Ok(());
89 }
90 }
91
92 let _ = fs::remove_file(path);
93 Ok(())
94}
95
96#[cfg(unix)]
97fn set_permissions(path: &Path, mode: u32) -> Result<()> {
98 let perm = fs::Permissions::from_mode(mode);
99 fs::set_permissions(path, perm)
100 .with_context(|| format!("failed to set permissions: {}", path.display()))?;
101 Ok(())
102}
103
104#[cfg(not(unix))]
105fn set_permissions(_path: &Path, _mode: u32) -> Result<()> {
106 Ok(())
107}
108
109fn temp_path(path: &Path, attempt: u32) -> PathBuf {
110 let filename = path
111 .file_name()
112 .and_then(|name| name.to_str())
113 .unwrap_or("tmp");
114 let pid = std::process::id();
115 let nanos = SystemTime::now()
116 .duration_since(UNIX_EPOCH)
117 .map(|d| d.as_nanos())
118 .unwrap_or(0);
119 let tmp_name = format!(".{filename}.tmp-{pid}-{nanos}-{attempt}");
120 path.with_file_name(tmp_name)
121}