Skip to main content

skiff_cli/
fsutil.rs

1//! Restricted directory/file creation (`0o700` dirs, `0o600` files on Unix).
2//!
3//! Prefer [`atomic_write_0600`] / [`ensure_dir_0700`] over bare `fs::write` for
4//! anything that may hold secrets or session metadata.
5
6use std::fs::{self, File, OpenOptions};
7use std::io::Write;
8use std::path::Path;
9use std::sync::atomic::{AtomicU64, Ordering};
10
11use crate::error::Result;
12
13/// Monotonic per-process counter mixed into temp filenames so that multiple
14/// `atomic_write_0600` calls within the same process (even on the same
15/// target path, e.g. from concurrent threads) never collide either.
16static TMP_COUNTER: AtomicU64 = AtomicU64::new(0);
17
18/// Ensure `dir` exists with mode `0o700` on Unix.
19pub fn ensure_dir_0700(dir: &Path) -> Result<()> {
20    fs::create_dir_all(dir)?;
21    #[cfg(unix)]
22    {
23        use std::os::unix::fs::PermissionsExt;
24        let _ = fs::set_permissions(dir, fs::Permissions::from_mode(0o700));
25    }
26    Ok(())
27}
28
29/// Atomically write bytes to `path` with mode `0o600` (create temp with mode, then rename).
30pub fn atomic_write_0600(path: &Path, bytes: &[u8]) -> Result<()> {
31    if let Some(parent) = path.parent() {
32        ensure_dir_0700(parent)?;
33    }
34    // Unique per-call temp filename (pid + monotonic counter) so concurrent
35    // processes/threads writing the same target path never share a temp
36    // inode and interleave writes before either renames into place.
37    let counter = TMP_COUNTER.fetch_add(1, Ordering::Relaxed);
38    let tmp = path.with_extension(format!("tmp.write.{}.{}", std::process::id(), counter));
39    {
40        let mut file = open_0600(&tmp)?;
41        file.write_all(bytes)?;
42        file.sync_all()?;
43    }
44    fs::rename(&tmp, path)?;
45    #[cfg(unix)]
46    {
47        use std::os::unix::fs::PermissionsExt;
48        let _ = fs::set_permissions(path, fs::Permissions::from_mode(0o600));
49    }
50    Ok(())
51}
52
53fn open_0600(path: &Path) -> Result<File> {
54    let mut opts = OpenOptions::new();
55    opts.write(true).create(true).truncate(true);
56    #[cfg(unix)]
57    {
58        use std::os::unix::fs::OpenOptionsExt;
59        opts.mode(0o600);
60    }
61    opts.open(path).map_err(Into::into)
62}