use std::fs::{File, OpenOptions};
use std::io;
use std::path::Path;
#[cfg(unix)]
const PRIVATE_FILE_MODE: u32 = 0o600;
#[cfg(unix)]
const PRIVATE_DIR_MODE: u32 = 0o700;
fn new_private_file_options() -> OpenOptions {
let mut opts = OpenOptions::new();
let _ = opts.read(true).write(true).create_new(true);
#[cfg(unix)]
{
use std::os::unix::fs::OpenOptionsExt;
let _ = opts.mode(PRIVATE_FILE_MODE);
}
opts
}
pub(crate) fn create_private_file(path: &Path) -> io::Result<bool> {
match new_private_file_options().open(path) {
Ok(_file) => Ok(true),
Err(err) if err.kind() == io::ErrorKind::AlreadyExists => Ok(false),
Err(err) => Err(err),
}
}
pub(crate) fn create_new_private_file(path: &Path) -> io::Result<File> {
new_private_file_options().open(path)
}
pub(crate) fn open_or_create_private_file(path: &Path) -> io::Result<File> {
let mut opts = OpenOptions::new();
let _ = opts.read(true).write(true).create(true).truncate(false);
#[cfg(unix)]
{
use std::os::unix::fs::OpenOptionsExt;
let _ = opts.mode(PRIVATE_FILE_MODE);
}
opts.open(path)
}
pub(crate) fn keep_permissions(original: &Path, replacement: &Path) -> io::Result<()> {
#[cfg(unix)]
{
let permissions = std::fs::metadata(original)?.permissions();
std::fs::set_permissions(replacement, permissions)
}
#[cfg(not(unix))]
{
let _ = (original, replacement);
Ok(())
}
}
fn private_dir_builder(recursive: bool) -> std::fs::DirBuilder {
let mut builder = std::fs::DirBuilder::new();
let _ = builder.recursive(recursive);
#[cfg(unix)]
{
use std::os::unix::fs::DirBuilderExt;
let _ = builder.mode(PRIVATE_DIR_MODE);
}
builder
}
pub(crate) fn create_private_dir_all(path: &Path) -> io::Result<()> {
private_dir_builder(true).create(path)
}
pub(crate) fn create_new_private_dir(path: &Path) -> io::Result<()> {
private_dir_builder(false).create(path)
}
#[cfg(test)]
mod tests {
use super::{
create_new_private_dir, create_new_private_file, create_private_dir_all,
create_private_file, keep_permissions, open_or_create_private_file,
};
fn scratch(name: &str) -> std::path::PathBuf {
let nanos = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map_or(0_u128, |d| d.as_nanos());
std::env::temp_dir().join(format!(
"emdb-private-fs-{name}-{}-{nanos}",
std::process::id()
))
}
#[test]
fn test_create_private_file_new_path_returns_true() {
let path = scratch("new");
assert!(create_private_file(&path).unwrap());
assert_eq!(std::fs::metadata(&path).unwrap().len(), 0);
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let mode = std::fs::metadata(&path).unwrap().permissions().mode() & 0o777;
assert_eq!(mode & 0o077, 0, "group/other bits set: {mode:o}");
}
let _ = std::fs::remove_file(&path);
}
#[test]
fn test_create_private_file_existing_path_returns_false_and_keeps_content() {
let path = scratch("existing");
std::fs::write(&path, b"keep me").unwrap();
assert!(!create_private_file(&path).unwrap());
assert_eq!(std::fs::read(&path).unwrap(), b"keep me");
assert!(create_new_private_file(&path).is_err());
let _ = std::fs::remove_file(&path);
}
#[test]
fn test_create_private_file_missing_parent_returns_err() {
let path = scratch("missing-parent").join("child.emdb");
assert!(create_private_file(&path).is_err());
}
#[test]
fn test_open_or_create_private_file_does_not_truncate() {
let path = scratch("open-or-create");
std::fs::write(&path, b"abc").unwrap();
drop(open_or_create_private_file(&path).unwrap());
assert_eq!(std::fs::read(&path).unwrap(), b"abc");
let _ = std::fs::remove_file(&path);
}
#[test]
fn test_private_dirs_are_owner_only_and_new_dir_is_exclusive() {
let root = scratch("dirs");
let nested = root.join("a").join("b");
create_private_dir_all(&nested).unwrap();
create_private_dir_all(&nested).unwrap(); #[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
for dir in [&root, &nested] {
let mode = std::fs::metadata(dir).unwrap().permissions().mode() & 0o777;
assert_eq!(mode & 0o077, 0, "{dir:?} mode {mode:o}");
}
}
let single = root.join("single");
create_new_private_dir(&single).unwrap();
assert!(create_new_private_dir(&single).is_err());
let _ = std::fs::remove_dir_all(&root);
}
#[test]
fn test_keep_permissions_copies_mode_and_missing_original_errors() {
let original = scratch("keep-original");
let replacement = scratch("keep-replacement");
std::fs::write(&original, b"a").unwrap();
assert!(create_private_file(&replacement).unwrap());
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
std::fs::set_permissions(&original, std::fs::Permissions::from_mode(0o640)).unwrap();
keep_permissions(&original, &replacement).unwrap();
let mode = std::fs::metadata(&replacement)
.unwrap()
.permissions()
.mode()
& 0o777;
assert_eq!(mode, 0o640);
assert!(keep_permissions(&scratch("keep-missing"), &replacement).is_err());
}
#[cfg(not(unix))]
keep_permissions(&original, &replacement).unwrap();
let _ = std::fs::remove_file(&original);
let _ = std::fs::remove_file(&replacement);
}
#[cfg(unix)]
#[test]
fn test_create_private_file_does_not_follow_symlink() {
let target = scratch("symlink-target");
let link = scratch("symlink-link");
std::os::unix::fs::symlink(&target, &link).unwrap();
assert!(!create_private_file(&link).unwrap());
assert!(!target.exists());
let _ = std::fs::remove_file(&link);
}
}