1use std::fs::{self, File};
4use std::io::{Read, Write};
5use std::path::Path;
6
7use sha2::{Digest, Sha256};
8
9use crate::error::{Error, Result};
10
11pub fn atomic_write(path: &Path, bytes: &[u8]) -> Result<()> {
23 let parent = path
24 .parent()
25 .ok_or_else(|| Error::Other(format!("path has no parent: {}", path.display())))?;
26 fs::create_dir_all(parent)?;
27
28 let tmp = tmp_path(path)?;
29
30 let result = (|| -> Result<()> {
31 let mut f = File::create(&tmp)?;
32 f.write_all(bytes)?;
33 f.sync_all()?;
34 drop(f);
35 fs::rename(&tmp, path)?;
36 Ok(())
37 })();
38
39 if result.is_err() {
40 drop(fs::remove_file(&tmp));
41 }
42 result
43}
44
45fn tmp_path(path: &Path) -> Result<std::path::PathBuf> {
46 let parent = path
47 .parent()
48 .ok_or_else(|| Error::Other(format!("path has no parent: {}", path.display())))?;
49 let name = path
50 .file_name()
51 .ok_or_else(|| Error::Other(format!("path has no file name: {}", path.display())))?
52 .to_os_string();
53
54 let mut buf = [0_u8; 8];
55 getrandom::fill(&mut buf).map_err(|e| Error::Other(format!("getrandom: {e}")))?;
56 let mut tmp_name = std::ffi::OsString::new();
57 tmp_name.push(&name);
58 tmp_name.push(format!(".{}.tmp", encode_hex_lower(&buf)));
59 Ok(parent.join(tmp_name))
60}
61
62pub fn sha256_file(path: &Path) -> Result<String> {
68 let mut f = File::open(path)?;
69 let mut hasher = Sha256::new();
70 let mut buf = vec![0_u8; 64 * 1024].into_boxed_slice();
71 loop {
72 let n = f.read(&mut buf)?;
73 if n == 0 {
74 break;
75 }
76 hasher.update(&buf[..n]);
77 }
78 Ok(encode_hex_lower(&hasher.finalize()))
79}
80
81#[must_use]
83pub fn sha256_bytes(bytes: &[u8]) -> String {
84 let mut hasher = Sha256::new();
85 hasher.update(bytes);
86 encode_hex_lower(&hasher.finalize())
87}
88
89pub(crate) fn encode_hex_lower(bytes: &[u8]) -> String {
90 const HEX: &[u8; 16] = b"0123456789abcdef";
91 let mut out = String::with_capacity(bytes.len() * 2);
92 for b in bytes {
93 out.push(HEX[(b >> 4) as usize] as char);
94 out.push(HEX[(b & 0x0f) as usize] as char);
95 }
96 out
97}
98
99#[cfg(unix)]
105pub fn restrict_to_user(path: &Path) -> Result<()> {
106 use std::os::unix::fs::PermissionsExt;
107 let mut perms = fs::metadata(path)?.permissions();
108 perms.set_mode(0o600);
109 fs::set_permissions(path, perms)?;
110 Ok(())
111}
112
113#[cfg(windows)]
119#[allow(clippy::missing_const_for_fn)]
120pub fn restrict_to_user(_path: &Path) -> Result<()> {
121 Ok(())
122}
123
124#[cfg(test)]
125mod tests {
126 use super::*;
127
128 #[test]
129 fn atomic_write_creates_parent_dirs() {
130 let dir = tempfile::tempdir().unwrap();
131 let target = dir.path().join("nested/dir/output.txt");
132 atomic_write(&target, b"hello").unwrap();
133 assert_eq!(fs::read(&target).unwrap(), b"hello");
134 }
135
136 #[test]
137 fn atomic_write_overwrites_existing() {
138 let dir = tempfile::tempdir().unwrap();
139 let target = dir.path().join("output.txt");
140 atomic_write(&target, b"v1").unwrap();
141 atomic_write(&target, b"v2").unwrap();
142 assert_eq!(fs::read(&target).unwrap(), b"v2");
143 }
144
145 #[test]
146 fn sha256_matches_known_vector() {
147 assert_eq!(
149 sha256_bytes(b"abc"),
150 "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad"
151 );
152 }
153}