use alloc::borrow::Cow;
use alloc::format;
use alloc::string::String;
use std::path::{Path, PathBuf};
use super::{FetchContext, KeyFetch, RawKey};
use crate::Result;
use crate::error::Error;
#[derive(Debug, Clone)]
pub struct FileFetch {
path: PathBuf,
strict_perms: bool,
}
impl FileFetch {
#[must_use]
pub fn new(path: impl Into<PathBuf>) -> Self {
Self {
path: path.into(),
strict_perms: true,
}
}
#[must_use]
pub fn allow_loose_perms(mut self) -> Self {
self.strict_perms = false;
self
}
#[must_use]
pub fn path(&self) -> &Path {
&self.path
}
}
impl KeyFetch for FileFetch {
fn fetch(&self, _ctx: &FetchContext) -> Result<RawKey> {
if self.strict_perms {
check_perms(&self.path)?;
}
std::fs::read(&self.path).map_or_else(
|e| {
Err(Error::Acquisition {
source: Cow::Borrowed("file"),
reason: io_failure_message(&self.path, &e),
})
},
|bytes| Ok(RawKey::new(bytes)),
)
}
fn describe(&self) -> Cow<'_, str> {
Cow::Borrowed("file")
}
}
#[cfg(unix)]
fn check_perms(path: &Path) -> Result<()> {
use std::os::unix::fs::PermissionsExt;
let meta = std::fs::metadata(path).map_err(|e| Error::Acquisition {
source: Cow::Borrowed("file"),
reason: io_failure_message(path, &e),
})?;
let mode = meta.permissions().mode();
if (mode & 0o077) != 0 {
return Err(Error::Acquisition {
source: Cow::Borrowed("file"),
reason: format!(
"{} is too permissive (mode {:o}); expected 0600 or stricter",
path.display(),
mode & 0o777
),
});
}
Ok(())
}
#[cfg(not(unix))]
#[allow(clippy::unnecessary_wraps)] fn check_perms(_path: &Path) -> Result<()> {
Ok(())
}
fn io_failure_message(path: &Path, e: &std::io::Error) -> String {
format!("failed to read {}: {:?}", path.display(), e.kind())
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used)]
mod tests {
use super::*;
use std::io::Write;
struct TempFile {
path: PathBuf,
}
impl TempFile {
fn new(prefix: &str, contents: &[u8]) -> Self {
let mut path = std::env::temp_dir();
let suffix = format!("{}_{}", std::process::id(), prefix);
path.push(format!("kv_test_{suffix}.bin"));
let mut f = std::fs::File::create(&path).unwrap();
f.write_all(contents).unwrap();
drop(f);
Self { path }
}
fn path(&self) -> &Path {
&self.path
}
}
impl Drop for TempFile {
fn drop(&mut self) {
let _ = std::fs::remove_file(&self.path);
}
}
#[test]
fn reads_file_contents() {
let f = TempFile::new("read_ok", b"hello, world!");
let fetcher = FileFetch::new(f.path()).allow_loose_perms();
let raw = fetcher.fetch(&FetchContext::new("k")).unwrap();
assert_eq!(raw.len(), 13);
}
#[test]
fn missing_file_returns_acquisition_error() {
let fetcher =
FileFetch::new("/nonexistent/path/key-vault-test-missing.bin").allow_loose_perms();
let err = fetcher.fetch(&FetchContext::new("k")).unwrap_err();
match err {
Error::Acquisition { source, reason } => {
assert_eq!(source, "file");
assert!(reason.contains("failed to read"));
}
other => panic!("expected Acquisition, got {other:?}"),
}
}
#[cfg(unix)]
#[test]
fn strict_perms_rejects_world_readable_file() {
use std::os::unix::fs::PermissionsExt;
let f = TempFile::new("strict_perm", b"key");
std::fs::set_permissions(f.path(), std::fs::Permissions::from_mode(0o644)).unwrap();
let fetcher = FileFetch::new(f.path());
let err = fetcher.fetch(&FetchContext::new("k")).unwrap_err();
match err {
Error::Acquisition { reason, .. } => {
assert!(reason.contains("too permissive"));
}
other => panic!("expected Acquisition, got {other:?}"),
}
}
#[cfg(unix)]
#[test]
fn strict_perms_accepts_0600() {
use std::os::unix::fs::PermissionsExt;
let f = TempFile::new("strict_0600", b"key");
std::fs::set_permissions(f.path(), std::fs::Permissions::from_mode(0o600)).unwrap();
let fetcher = FileFetch::new(f.path());
let raw = fetcher.fetch(&FetchContext::new("k")).unwrap();
assert_eq!(raw.len(), 3);
}
#[test]
fn describe_returns_file() {
assert_eq!(FileFetch::new("/dev/null").describe(), "file");
}
}