Skip to main content

llm_kernel/secrets/
atomic.rs

1use std::io::Write;
2use std::path::Path;
3
4use crate::error::{KernelError, Result};
5
6/// Write data to a file atomically using a temp file + rename.
7///
8/// On Unix, sets the file mode to `mode` (e.g. `0o600` for secrets).
9pub fn write_atomic(path: &str, data: &[u8], mode: u32) -> Result<()> {
10    let target = Path::new(path);
11    let parent = target
12        .parent()
13        .ok_or_else(|| KernelError::Vault(format!("path has no parent directory: {path}")))?;
14    std::fs::create_dir_all(parent)?;
15
16    let mut tmp = tempfile::NamedTempFile::new_in(parent)?;
17    tmp.write_all(data)?;
18
19    #[cfg(unix)]
20    {
21        use std::os::unix::fs::PermissionsExt;
22        tmp.as_file_mut()
23            .set_permissions(std::fs::Permissions::from_mode(mode))?;
24    }
25
26    tmp.persist(target)
27        .map_err(|e| KernelError::Vault(format!("atomic persist failed: {}", e)))?;
28    Ok(())
29}
30
31#[cfg(test)]
32mod tests {
33    use super::*;
34    use std::fs;
35
36    #[test]
37    fn test_write_atomic_creates_file() {
38        let dir = tempfile::tempdir().unwrap();
39        let path = dir.path().join("test.txt");
40        let path_str = path.to_string_lossy().to_string();
41
42        write_atomic(&path_str, b"hello", 0o644).unwrap();
43
44        let content = fs::read_to_string(&path).unwrap();
45        assert_eq!(content, "hello");
46    }
47
48    #[test]
49    fn test_write_atomic_overwrites() {
50        let dir = tempfile::tempdir().unwrap();
51        let path = dir.path().join("test.txt");
52        let path_str = path.to_string_lossy().to_string();
53
54        write_atomic(&path_str, b"first", 0o644).unwrap();
55        write_atomic(&path_str, b"second", 0o644).unwrap();
56
57        let content = fs::read_to_string(&path).unwrap();
58        assert_eq!(content, "second");
59    }
60}