aboutsummaryrefslogtreecommitdiffstats
path: root/src/files.rs
diff options
context:
space:
mode:
Diffstat (limited to 'src/files.rs')
-rw-r--r--src/files.rs85
1 files changed, 83 insertions, 2 deletions
diff --git a/src/files.rs b/src/files.rs
index 0ec741e..bc8fb8e 100644
--- a/src/files.rs
+++ b/src/files.rs
@@ -167,20 +167,101 @@ fn sync_parent(path: &Path) {
}
}
+/// A key file's bytes: read from `path`; or with `create`, if there is no file there,
+/// `len` random bytes in a new one only its owner may read (then also `true`). Without
+/// `create`, a missing file is an error saying to give `create_option` (`what`: which
+/// key; `hint`: what a new one means). Never created where a file is: a mistyped path
+/// stays an error unless asked.
+pub(crate) fn key_file(
+ path: &Path,
+ what: &str,
+ create: bool,
+ create_option: &str,
+ hint: &str,
+ len: usize,
+) -> anyhow::Result<(Vec<u8>, bool)> {
+ use anyhow::Context;
+ use std::io::Write;
+ match std::fs::read(path) {
+ Ok(bytes) => {
+ warn_if_readable_by_others(path, what);
+ Ok((bytes, false))
+ }
+ Err(error) if error.kind() == std::io::ErrorKind::NotFound => {
+ if !create {
+ anyhow::bail!(
+ "The {what} file {} doesn't exist; add --{create_option} to create a new {what} there ({hint})",
+ path.display()
+ );
+ }
+ let mut bytes = vec![0u8; len];
+ getrandom::fill(&mut bytes)
+ .map_err(|error| anyhow::anyhow!("No randomness: {error}"))?;
+ let mut options = std::fs::OpenOptions::new();
+ options.write(true).create_new(true);
+ #[cfg(unix)]
+ {
+ use std::os::unix::fs::OpenOptionsExt;
+ options.mode(0o600);
+ }
+ let written = (|| {
+ let mut file = options.open(path)?;
+ file.write_all(&bytes)?;
+ file.sync_all()
+ })();
+ written.with_context(|| format!("Creating the {what} file {}", path.display()))?;
+ Ok((bytes, true))
+ }
+ Err(error) => {
+ Err(error).with_context(|| format!("Reading the {what} from {}", path.display()))
+ }
+ }
+}
+
+/// A key is a secret: a file others may read is worth a warning (not an error: a
+/// group may be deliberate, e.g. shared with a deployment tool). `what`: which key.
+pub(crate) fn warn_if_readable_by_others(path: &Path, what: &str) {
+ #[cfg(unix)]
+ {
+ use std::os::unix::fs::PermissionsExt;
+ if let Ok(metadata) = std::fs::metadata(path) {
+ let mode = metadata.permissions().mode();
+ if mode & 0o077 != 0 {
+ tracing::warn!(
+ path = %path.display(),
+ mode = format!("{:o}", mode & 0o777),
+ "The {what} file is readable by others than its owner; chmod 600 it"
+ );
+ }
+ }
+ }
+ #[cfg(not(unix))]
+ let _ = (path, what);
+}
+
fn with_suffix(path: &Path, suffix: &str) -> PathBuf {
let mut name = path.as_os_str().to_owned();
name.push(suffix);
PathBuf::from(name)
}
+/// An empty directory for the test `name`: `continuum-<name>-<process ID>` in the
+/// temporary directory, emptied first.
+#[cfg(test)]
+pub(crate) fn scratch_dir(name: &str) -> PathBuf {
+ let dir = std::env::temp_dir().join(format!("continuum-{name}-{}", std::process::id()));
+ let _ = std::fs::remove_dir_all(&dir);
+ std::fs::create_dir_all(&dir).unwrap();
+ dir
+}
+
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn writes_reads_and_counts() {
- let dir = std::env::temp_dir().join(format!("continuum-files-{}", std::process::id()));
- std::fs::create_dir_all(&dir).unwrap();
+ let dir = scratch_dir("files");
let path = dir.join("state.json");
let files = Files::default();