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(crate) fn write_atomic(path: impl AsRef<Path>, data: &[u8], mode: u32) -> Result<()> {
10    let target = path.as_ref();
11    let parent = target.parent().ok_or_else(|| {
12        KernelError::Vault(format!(
13            "path has no parent directory: {}",
14            target.display()
15        ))
16    })?;
17    std::fs::create_dir_all(parent)?;
18
19    let mut tmp = tempfile::NamedTempFile::new_in(parent)?;
20    tmp.write_all(data)?;
21    // Flush data blocks before the rename lands — a crash between rename and
22    // writeback would otherwise replace the old file with a truncated one.
23    tmp.as_file().sync_all()?;
24
25    #[cfg(unix)]
26    {
27        use std::os::unix::fs::PermissionsExt;
28        tmp.as_file_mut()
29            .set_permissions(std::fs::Permissions::from_mode(mode))?;
30    }
31
32    tmp.persist(target)
33        .map_err(|e| KernelError::Vault(format!("atomic persist failed: {}", e)))?;
34    Ok(())
35}
36
37#[cfg(test)]
38mod tests {
39    use super::*;
40    use std::fs;
41
42    #[test]
43    fn test_write_atomic_creates_file() {
44        let dir = tempfile::tempdir().unwrap();
45        let path = dir.path().join("test.txt");
46        let path_str = path.to_string_lossy().to_string();
47
48        write_atomic(&path_str, b"hello", 0o644).unwrap();
49
50        let content = fs::read_to_string(&path).unwrap();
51        assert_eq!(content, "hello");
52    }
53
54    #[test]
55    fn test_write_atomic_overwrites() {
56        let dir = tempfile::tempdir().unwrap();
57        let path = dir.path().join("test.txt");
58        let path_str = path.to_string_lossy().to_string();
59
60        write_atomic(&path_str, b"first", 0o644).unwrap();
61        write_atomic(&path_str, b"second", 0o644).unwrap();
62
63        let content = fs::read_to_string(&path).unwrap();
64        assert_eq!(content, "second");
65    }
66}