use std::io;
use std::path::Path;
pub fn secure_file_perms(path: &Path) -> io::Result<()> {
crate::platform::set_mode(path, 0o600)
}
pub fn secure_dir_perms(path: &Path) -> io::Result<()> {
crate::platform::set_mode(path, 0o700)
}
pub fn ensure_file_private(path: &Path) -> io::Result<Option<u32>> {
crate::platform::ensure_private(path, 0o600)
}
pub fn write_private(path: &Path, contents: &[u8]) -> io::Result<()> {
crate::platform::write_with_mode(path, contents, 0o600)
}
pub fn open_private_append(path: &Path) -> io::Result<std::fs::File> {
crate::platform::open_append_with_mode(path, 0o600)
}
pub fn create_private_dir_all(path: &Path) -> io::Result<()> {
crate::platform::create_dir_all_with_mode(path, 0o700)
}
#[cfg(test)]
mod tests {
use super::*;
#[cfg(unix)]
fn mode_of(path: &Path) -> u32 {
use std::os::unix::fs::PermissionsExt;
std::fs::metadata(path).unwrap().permissions().mode() & 0o777
}
#[cfg(unix)]
fn set_mode(path: &Path, mode: u32) {
use std::os::unix::fs::PermissionsExt;
std::fs::set_permissions(path, std::fs::Permissions::from_mode(mode)).unwrap();
}
#[cfg(windows)]
fn env_lock() -> std::sync::MutexGuard<'static, ()> {
crate::platform::ENV_LOCK.lock().expect("env lock")
}
#[test]
fn write_private_creates_an_owner_only_file_with_the_content() {
#[cfg(windows)]
let _env = env_lock();
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("secret");
write_private(&path, b"the key").unwrap();
assert_eq!(std::fs::read(&path).unwrap(), b"the key");
#[cfg(unix)]
assert_eq!(mode_of(&path), 0o600);
}
#[test]
fn write_private_retightens_an_existing_permissive_file() {
#[cfg(windows)]
let _env = env_lock();
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("secret");
std::fs::write(&path, b"old").unwrap();
#[cfg(unix)]
set_mode(&path, 0o644);
write_private(&path, b"new").unwrap();
assert_eq!(std::fs::read(&path).unwrap(), b"new");
#[cfg(unix)]
assert_eq!(mode_of(&path), 0o600);
}
#[test]
fn write_private_propagates_an_unwritable_path() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("no-such-dir").join("secret");
assert!(write_private(&path, b"x").is_err());
}
#[test]
fn open_private_append_creates_an_owner_only_file() {
#[cfg(windows)]
let _env = env_lock();
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("run.lvr");
{
use std::io::Write;
let mut f = open_private_append(&path).unwrap();
f.write_all(b"first").unwrap();
}
assert_eq!(std::fs::read(&path).unwrap(), b"first");
#[cfg(unix)]
assert_eq!(mode_of(&path), 0o600);
}
#[test]
fn open_private_append_adds_to_an_existing_file() {
#[cfg(windows)]
let _env = env_lock();
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("log");
for chunk in [b"one", b"two"] {
use std::io::Write;
let mut f = open_private_append(&path).unwrap();
f.write_all(chunk).unwrap();
}
assert_eq!(std::fs::read(&path).unwrap(), b"onetwo");
}
#[test]
fn open_private_append_retightens_an_existing_permissive_file() {
#[cfg(windows)]
let _env = env_lock();
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("run.lvr");
std::fs::write(&path, b"old").unwrap();
#[cfg(unix)]
set_mode(&path, 0o644);
drop(open_private_append(&path).unwrap());
#[cfg(unix)]
assert_eq!(mode_of(&path), 0o600);
}
#[test]
fn open_private_append_propagates_an_unwritable_path() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("no-such-dir").join("log");
assert!(open_private_append(&path).is_err());
}
#[test]
fn create_private_dir_all_makes_owner_only_directories() {
#[cfg(windows)]
let _env = env_lock();
let dir = tempfile::tempdir().unwrap();
let nested = dir.path().join("stages").join("0");
create_private_dir_all(&nested).unwrap();
assert!(nested.is_dir());
#[cfg(unix)]
assert_eq!(mode_of(&nested), 0o700);
}
#[test]
fn create_private_dir_all_propagates_a_path_it_cannot_create() {
#[cfg(windows)]
let _env = env_lock();
let dir = tempfile::tempdir().unwrap();
let blocker = dir.path().join("not-a-dir");
std::fs::write(&blocker, b"x").unwrap();
assert!(create_private_dir_all(&blocker.join("child")).is_err());
assert!(create_private_dir_all(&blocker).is_err());
}
#[test]
fn create_private_dir_all_is_idempotent() {
#[cfg(windows)]
let _env = env_lock();
let dir = tempfile::tempdir().unwrap();
let nested = dir.path().join("stages").join("0");
create_private_dir_all(&nested).unwrap();
create_private_dir_all(&nested).unwrap();
assert!(nested.is_dir());
}
#[test]
fn secure_file_perms_restricts_on_unix_and_succeeds_everywhere() {
#[cfg(windows)]
let _env = env_lock();
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("secret");
std::fs::write(&path, b"x").unwrap();
#[cfg(unix)]
set_mode(&path, 0o644);
secure_file_perms(&path).unwrap();
#[cfg(unix)]
assert_eq!(mode_of(&path), 0o600);
}
#[test]
fn secure_dir_perms_restricts_on_unix_and_succeeds_everywhere() {
#[cfg(windows)]
let _env = env_lock();
let dir = tempfile::tempdir().unwrap();
let sub = dir.path().join("d");
std::fs::create_dir(&sub).unwrap();
#[cfg(unix)]
set_mode(&sub, 0o755);
secure_dir_perms(&sub).unwrap();
#[cfg(unix)]
assert_eq!(mode_of(&sub), 0o700);
}
#[test]
fn secure_file_perms_missing_path_behavior() {
#[cfg(windows)]
let _env = env_lock();
let dir = tempfile::tempdir().unwrap();
let missing = dir.path().join("nope");
let result = secure_file_perms(&missing);
#[cfg(any(unix, windows))]
assert!(result.is_err());
#[cfg(not(any(unix, windows)))]
assert!(result.is_ok());
}
#[test]
fn ensure_file_private_tightens_permissive_file_on_unix() {
#[cfg(windows)]
let _env = env_lock();
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("cfg");
std::fs::write(&path, b"x").unwrap();
#[cfg(unix)]
set_mode(&path, 0o644);
let previous = ensure_file_private(&path).unwrap();
#[cfg(unix)]
{
assert_eq!(previous, Some(0o100644));
assert_eq!(mode_of(&path), 0o600);
}
#[cfg(not(unix))]
assert_eq!(previous, None);
}
#[test]
fn ensure_file_private_leaves_private_file_untouched() {
#[cfg(windows)]
let _env = env_lock();
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("cfg");
std::fs::write(&path, b"x").unwrap();
#[cfg(unix)]
set_mode(&path, 0o600);
assert_eq!(ensure_file_private(&path).unwrap(), None);
#[cfg(unix)]
assert_eq!(mode_of(&path), 0o600);
}
#[test]
fn ensure_file_private_is_noop_for_missing_path() {
let dir = tempfile::tempdir().unwrap();
let missing = dir.path().join("nope");
assert_eq!(ensure_file_private(&missing).unwrap(), None);
}
}