use std::fs::File;
use std::io;
use std::path::Path;
#[cfg(unix)]
mod unix;
#[cfg(unix)]
pub use unix::UnixPrivateFs;
#[cfg(unix)]
pub type ActivePrivateFs = UnixPrivateFs;
#[cfg(windows)]
mod windows;
#[cfg(windows)]
pub(crate) use windows::owner_only_attributes;
#[cfg(windows)]
pub use windows::WindowsPrivateFs;
#[cfg(windows)]
pub type ActivePrivateFs = WindowsPrivateFs;
pub trait PrivateFs: crate::sealed::Sealed {
fn create_dir(&self, path: &Path) -> io::Result<()>;
fn create_dir_all(&self, path: &Path) -> io::Result<()> {
if path.as_os_str().is_empty() {
return Ok(());
}
if std::fs::metadata(path).is_ok_and(|m| m.is_dir()) {
return Ok(());
}
if let Some(parent) = path.parent().filter(|p| !p.as_os_str().is_empty()) {
self.create_dir_all(parent)?;
}
match self.create_dir(path) {
Err(e) if e.kind() == io::ErrorKind::AlreadyExists => Ok(()),
other => other,
}
}
fn create_file_new(&self, path: &Path, writes: Writes) -> io::Result<File>;
fn create_file_truncate(&self, path: &Path) -> io::Result<File>;
fn harden_existing(&self, path: &Path) -> io::Result<()>;
fn effective_access(&self, path: &Path) -> io::Result<EffectiveAccess>;
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Writes {
FromStart,
Append,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct EffectiveAccess {
pub owner_only: bool,
pub other_readers: Vec<String>,
}
#[cfg(test)]
pub(crate) fn assert_private_fs_contract<P: PrivateFs>(fs: &P, scratch: &Path) {
let dir = scratch.join("private-dir");
fs.create_dir(&dir).unwrap();
let access = fs.effective_access(&dir).unwrap();
assert!(
access.owner_only,
"a freshly created private dir must exclude everyone else, got {:?}",
access.other_readers
);
let file = dir.join("secret");
drop(fs.create_file_new(&file, Writes::FromStart).unwrap());
assert!(fs.effective_access(&file).unwrap().owner_only);
std::fs::write(&file, b"payload").unwrap();
assert_eq!(
fs.create_file_new(&file, Writes::FromStart)
.unwrap_err()
.kind(),
io::ErrorKind::AlreadyExists
);
assert_eq!(std::fs::read(&file).unwrap(), b"payload");
let blob = dir.join("blob");
std::fs::write(&blob, b"old").unwrap();
loosen(&blob);
{
use std::io::Write as _;
let mut f = fs.create_file_truncate(&blob).unwrap();
f.write_all(b"new").unwrap();
}
assert_eq!(std::fs::read(&blob).unwrap(), b"new");
assert!(fs.effective_access(&blob).unwrap().owner_only);
let logfile = dir.join("log");
{
use std::io::Write as _;
let mut a = fs.create_file_new(&logfile, Writes::Append).unwrap();
a.write_all(b"one").unwrap();
a.write_all(b"two").unwrap();
}
assert_eq!(std::fs::read(&logfile).unwrap(), b"onetwo");
loosen(&file);
fs.harden_existing(&file).unwrap();
assert!(fs.effective_access(&file).unwrap().owner_only);
}
#[cfg(all(test, unix))]
fn loosen(path: &Path) {
use std::os::unix::fs::PermissionsExt;
std::fs::set_permissions(path, std::fs::Permissions::from_mode(0o644)).unwrap();
}
#[cfg(all(test, windows))]
fn loosen(path: &Path) {
windows::allow_inheritance(path).unwrap();
}
#[cfg(test)]
mod tests {
#[test]
fn active_adapter_upholds_the_contract() {
let scratch =
std::env::temp_dir().join(format!("hotl-privatefs-{}-{}", std::process::id(), line!()));
std::fs::create_dir_all(&scratch).unwrap();
super::assert_private_fs_contract(&crate::PRIVATE_FS, &scratch);
let _ = std::fs::remove_dir_all(&scratch);
}
}