use std::fs::{self, DirBuilder, File, OpenOptions};
use std::io;
use std::path::Path;
use tempfile::{NamedTempFile, TempDir};
#[cfg(unix)]
const PRIVATE_DIR_MODE: u32 = 0o700;
#[cfg(unix)]
const PRIVATE_FILE_MODE: u32 = 0o600;
fn create_new_options() -> OpenOptions {
let mut options = OpenOptions::new();
options.write(true).create_new(true);
#[cfg(unix)]
{
use std::os::unix::fs::OpenOptionsExt;
options.mode(PRIVATE_FILE_MODE);
}
options
}
pub fn open_lock_file(path: &Path) -> io::Result<File> {
let mut options = OpenOptions::new();
options.create(true).truncate(false).read(true).write(true);
#[cfg(unix)]
{
use std::os::unix::fs::OpenOptionsExt;
options
.mode(PRIVATE_FILE_MODE)
.custom_flags(libc::O_NOFOLLOW | libc::O_NONBLOCK);
}
#[cfg(not(unix))]
if fs::symlink_metadata(path).is_ok_and(|metadata| metadata.file_type().is_symlink()) {
return Err(io::Error::other("lock path is a symlink"));
}
let file = options.open(path)?;
if !file.metadata()?.is_file() {
return Err(io::Error::other("lock path is not a regular file"));
}
Ok(file)
}
pub fn open_append_file(path: &Path) -> io::Result<File> {
let mut options = OpenOptions::new();
options.create(true).append(true);
#[cfg(unix)]
{
use std::os::unix::fs::OpenOptionsExt;
options
.mode(PRIVATE_FILE_MODE)
.custom_flags(libc::O_NOFOLLOW | libc::O_NONBLOCK);
}
#[cfg(not(unix))]
if fs::symlink_metadata(path).is_ok_and(|metadata| metadata.file_type().is_symlink()) {
return Err(io::Error::other("append path is a symlink"));
}
let file = options.open(path)?;
if !file.metadata()?.is_file() {
return Err(io::Error::other("append path is not a regular file"));
}
Ok(file)
}
pub fn create_new_file(path: &Path) -> io::Result<File> {
create_new_options().open(path)
}
pub fn write_new_file(path: &Path, bytes: &[u8]) -> io::Result<()> {
io::Write::write_all(&mut create_new_file(path)?, bytes)
}
pub fn create_dir_all(path: &Path) -> io::Result<()> {
let mut builder = DirBuilder::new();
builder.recursive(true);
#[cfg(unix)]
{
use std::os::unix::fs::DirBuilderExt;
builder.mode(PRIVATE_DIR_MODE);
}
builder.create(path)
}
pub fn create_dir(path: &Path) -> io::Result<()> {
let mut builder = DirBuilder::new();
#[cfg(unix)]
{
use std::os::unix::fs::DirBuilderExt;
builder.mode(PRIVATE_DIR_MODE);
}
builder.create(path)
}
pub fn tempdir() -> io::Result<TempDir> {
tempdir_in(&std::env::temp_dir(), ".aven-")
}
pub fn tempdir_in(parent: &Path, prefix: &str) -> io::Result<TempDir> {
let mut builder = tempfile::Builder::new();
builder.prefix(prefix);
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
builder.permissions(fs::Permissions::from_mode(PRIVATE_DIR_MODE));
}
builder.tempdir_in(parent)
}
pub fn sibling_tempfile(target: &Path) -> io::Result<NamedTempFile> {
let parent = target
.parent()
.filter(|parent| !parent.as_os_str().is_empty())
.unwrap_or(Path::new("."));
let mut prefix = std::ffi::OsString::from(".");
prefix.push(target.file_name().unwrap_or_default());
prefix.push(".");
let mut builder = tempfile::Builder::new();
builder.prefix(&prefix).suffix(".tmp");
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
builder.permissions(fs::Permissions::from_mode(PRIVATE_FILE_MODE));
}
builder.tempfile_in(parent)
}
pub fn copy_to_new_file(source: &Path, target: &Path) -> io::Result<()> {
let mut input = File::open(source)?;
let mut output = create_new_file(target)?;
io::copy(&mut input, &mut output)?;
Ok(())
}
#[cfg(all(test, unix))]
pub(crate) mod test_umask {
use std::sync::{Mutex, MutexGuard};
static UMASK: Mutex<()> = Mutex::new(());
pub(crate) struct Umask {
previous: libc::mode_t,
_lock: MutexGuard<'static, ()>,
}
impl Umask {
pub(crate) fn set(mask: libc::mode_t) -> Self {
let lock = UMASK.lock().unwrap_or_else(|error| error.into_inner());
let previous = unsafe { libc::umask(mask) };
Self {
previous,
_lock: lock,
}
}
}
impl Drop for Umask {
fn drop(&mut self) {
unsafe { libc::umask(self.previous) };
}
}
pub(crate) fn mode(path: &std::path::Path) -> u32 {
use std::os::unix::fs::PermissionsExt;
std::fs::symlink_metadata(path)
.unwrap()
.permissions()
.mode()
& 0o777
}
}
#[cfg(all(test, unix))]
mod tests {
use super::{create_dir_all, open_append_file};
use crate::private_fs::test_umask::{Umask, mode};
use std::fs;
use std::io::Write as _;
#[test]
fn append_file_and_parent_are_owner_only_under_permissive_umask() {
let _umask = Umask::set(0o022);
let temp = tempfile::tempdir().unwrap();
let parent = temp.path().join("logs");
create_dir_all(&parent).unwrap();
let path = parent.join("aven.log");
open_append_file(&path)
.unwrap()
.write_all(b"first\n")
.unwrap();
open_append_file(&path)
.unwrap()
.write_all(b"second\n")
.unwrap();
assert_eq!(mode(&parent), 0o700);
assert_eq!(mode(&path), 0o600);
assert_eq!(fs::read(&path).unwrap(), b"first\nsecond\n");
}
#[test]
fn append_file_preserves_existing_mode() {
use std::os::unix::fs::PermissionsExt;
let _umask = Umask::set(0o022);
let temp = tempfile::tempdir().unwrap();
let path = temp.path().join("aven.log");
fs::write(&path, b"before\n").unwrap();
fs::set_permissions(&path, fs::Permissions::from_mode(0o640)).unwrap();
open_append_file(&path)
.unwrap()
.write_all(b"after\n")
.unwrap();
assert_eq!(mode(&path), 0o640);
assert_eq!(fs::read(&path).unwrap(), b"before\nafter\n");
}
#[test]
fn append_file_refuses_symlink_without_following_it() {
use std::os::unix::fs::symlink;
let temp = tempfile::tempdir().unwrap();
let target = temp.path().join("victim");
let path = temp.path().join("aven.log");
fs::write(&target, b"sentinel").unwrap();
symlink(&target, &path).unwrap();
assert!(open_append_file(&path).is_err());
assert_eq!(fs::read(&target).unwrap(), b"sentinel");
assert!(
fs::symlink_metadata(&path)
.unwrap()
.file_type()
.is_symlink()
);
}
}