Skip to main content

scv_client/
fs.rs

1//! Private instance files: SCV's state, records, and credentials are
2//! written whole, readable only by the user, and survive a crash.
3
4use std::ffi::OsString;
5use std::io::{self, Write as _};
6use std::path::Path;
7
8/// Replace `path` with `bytes` atomically, as a file only the user can read.
9///
10/// The bytes go to a temporary file in the same directory, which is synced
11/// and renamed over `path`; the directory is then synced so the rename
12/// itself survives a crash. A reader sees the old file or the new one, never
13/// a partial write. The directory must exist; callers create it with the
14/// privacy their data needs. The temporary file is named for its target
15/// ([`temporary_prefix`]), so one a crash leaves shows whose it was.
16pub fn replace_private(path: &Path, bytes: &[u8]) -> io::Result<()> {
17    let parent = path
18        .parent()
19        .ok_or_else(|| io::Error::other(format!("{} has no parent", path.display())))?;
20    // Named temporary files are created with mode 0600.
21    let mut temporary = tempfile::Builder::new()
22        .prefix(&temporary_prefix(path))
23        .suffix(".tmp")
24        .tempfile_in(parent)?;
25    temporary.write_all(bytes)?;
26    temporary.as_file().sync_all()?;
27    temporary.persist(path).map_err(|error| error.error)?;
28    sync_directory(parent)
29}
30
31/// The longest file name a temporary file is named for; a longer one would
32/// make the temporary file's name too long for some file systems.
33const MAX_NAMED: usize = 200;
34
35/// How [`replace_private`] starts the name of the temporary file it writes
36/// `path` through: `.<file name>.`, followed by random characters and
37/// `.tmp`. A file name over 200 bytes gets `.tmp.` instead.
38pub fn temporary_prefix(path: &Path) -> OsString {
39    let mut prefix = OsString::from(".");
40    match path.file_name() {
41        Some(name) if name.len() <= MAX_NAMED => prefix.push(name),
42        _ => prefix.push("tmp"),
43    }
44    prefix.push(".");
45    prefix
46}
47
48/// Flush a directory's entries, so files created, renamed, or removed in it
49/// persist. A no-op where directories cannot be opened (not Unix).
50pub fn sync_directory(directory: &Path) -> io::Result<()> {
51    #[cfg(unix)]
52    std::fs::File::open(directory)?.sync_all()?;
53    #[cfg(not(unix))]
54    let _ = directory;
55    Ok(())
56}
57
58#[cfg(test)]
59mod tests;