use std::fs::File;
use std::io::{self, Read as _};
use std::path::{Path, PathBuf};
pub fn user_runtime_dir(product: &str) -> PathBuf {
if let Some(dir) = std::env::var_os("XDG_RUNTIME_DIR") {
return PathBuf::from(dir).join(product);
}
let uid = unsafe { libc::getuid() };
PathBuf::from(format!("/tmp/{product}-{uid}"))
}
pub fn user_state_dir(product: &str) -> PathBuf {
if let Some(dir) = std::env::var_os("XDG_STATE_HOME") {
PathBuf::from(dir).join(product)
} else if let Some(home) = dirs::home_dir() {
home.join(".local/state").join(product)
} else {
PathBuf::from(format!("/tmp/{product}-state"))
}
}
pub fn user_run_data_root(product: &str) -> PathBuf {
dirs::home_dir()
.unwrap_or_else(|| PathBuf::from("/tmp"))
.join("Library/Caches")
.join(product)
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct FileIdentity {
pub device: u64,
pub file: u64,
}
pub fn file_identity(file: &File) -> io::Result<Option<FileIdentity>> {
use std::os::unix::fs::MetadataExt as _;
let metadata = file.metadata()?;
Ok(Some(FileIdentity {
device: metadata.dev(),
file: metadata.ino(),
}))
}
pub fn path_identity(path: &Path) -> io::Result<Option<FileIdentity>> {
use std::os::unix::fs::MetadataExt as _;
let metadata = path.metadata()?;
Ok(Some(FileIdentity {
device: metadata.dev(),
file: metadata.ino(),
}))
}
pub fn open_lock_file(path: &Path) -> io::Result<File> {
use std::os::unix::fs::OpenOptionsExt as _;
std::fs::OpenOptions::new()
.read(true)
.write(true)
.create(true)
.truncate(false)
.mode(0o600)
.open(path)
}
pub fn try_lock_exclusive(file: &File) -> io::Result<()> {
use std::os::unix::io::AsRawFd as _;
let result = unsafe { libc::flock(file.as_raw_fd(), libc::LOCK_EX | libc::LOCK_NB) };
if result == 0 {
Ok(())
} else {
Err(io::Error::last_os_error())
}
}
pub fn lock_exclusive(file: &File) -> io::Result<()> {
use std::os::unix::io::AsRawFd as _;
let result = unsafe { libc::flock(file.as_raw_fd(), libc::LOCK_EX) };
if result == 0 {
Ok(())
} else {
Err(io::Error::last_os_error())
}
}
pub fn lock_shared(file: &File) -> io::Result<()> {
use std::os::unix::io::AsRawFd as _;
let result = unsafe { libc::flock(file.as_raw_fd(), libc::LOCK_SH) };
if result == 0 {
Ok(())
} else {
Err(io::Error::last_os_error())
}
}
pub fn try_lock_shared(file: &File) -> io::Result<()> {
use std::os::unix::io::AsRawFd as _;
let result = unsafe { libc::flock(file.as_raw_fd(), libc::LOCK_SH | libc::LOCK_NB) };
if result == 0 {
Ok(())
} else {
Err(io::Error::last_os_error())
}
}
pub fn unlock(file: &File) -> io::Result<()> {
use std::os::unix::io::AsRawFd as _;
let result = unsafe { libc::flock(file.as_raw_fd(), libc::LOCK_UN) };
if result == 0 {
Ok(())
} else {
Err(io::Error::last_os_error())
}
}
pub fn set_file_mtime(
path: &Path,
seconds_since_unix_epoch: i64,
nanoseconds: u32,
) -> io::Result<()> {
use std::os::unix::ffi::OsStrExt as _;
let c_path = std::ffi::CString::new(path.as_os_str().as_bytes())
.map_err(|error| io::Error::new(io::ErrorKind::InvalidInput, error))?;
#[allow(clippy::unnecessary_cast)]
let times = [
libc::timespec {
tv_sec: 0,
tv_nsec: libc::UTIME_OMIT,
},
libc::timespec {
tv_sec: seconds_since_unix_epoch as libc::time_t,
tv_nsec: nanoseconds as _,
},
];
let result = unsafe { libc::utimensat(libc::AT_FDCWD, c_path.as_ptr(), times.as_ptr(), 0) };
if result == 0 {
Ok(())
} else {
Err(io::Error::last_os_error())
}
}
pub fn is_lock_conflict(error: &io::Error) -> bool {
error.raw_os_error() == Some(libc::EWOULDBLOCK) || error.raw_os_error() == Some(libc::EAGAIN)
}
pub fn encode_path_bytes(path: &Path) -> Vec<u8> {
use std::os::unix::ffi::OsStrExt as _;
path.as_os_str().as_bytes().to_vec()
}
pub fn decode_path_bytes(bytes: &[u8]) -> io::Result<PathBuf> {
use std::os::unix::ffi::OsStringExt as _;
Ok(PathBuf::from(std::ffi::OsString::from_vec(bytes.to_vec())))
}
pub fn user_data_dir(product: &str) -> PathBuf {
dirs::home_dir()
.unwrap_or_else(std::env::temp_dir)
.join("Library")
.join("Application Support")
.join(product)
}
pub fn user_config_dir(product: &str) -> PathBuf {
user_data_dir(product)
}
pub fn replace_file(tmp: &Path, target: &Path) -> io::Result<()> {
std::fs::rename(tmp, target)
}
pub fn sync_directory(directory: &Path) -> io::Result<()> {
File::open(directory)?.sync_all()
}
pub fn open_shared_append(path: &Path) -> io::Result<File> {
std::fs::OpenOptions::new()
.create(true)
.append(true)
.open(path)
}
pub fn ensure_dir_private(path: &Path) -> io::Result<bool> {
use std::os::unix::fs::PermissionsExt as _;
let metadata = std::fs::metadata(path)?;
let full_mode = metadata.permissions().mode();
const STICKY: u32 = 0o1000;
if full_mode & STICKY != 0 {
return Ok(false);
}
if full_mode & 0o022 == 0 {
return Ok(false);
}
std::fs::set_permissions(path, std::fs::Permissions::from_mode(0o700))?;
let after = std::fs::metadata(path)?;
if after.permissions().mode() & 0o022 != 0 {
return Err(io::Error::new(
io::ErrorKind::PermissionDenied,
format!(
"{} is writable by others and could not be tightened",
path.display()
),
));
}
Ok(true)
}
pub fn create_dir_all_private(path: &Path) -> io::Result<()> {
use std::os::unix::fs::DirBuilderExt as _;
std::fs::DirBuilder::new()
.recursive(true)
.mode(0o700)
.create(path)
}
pub fn create_private_file(path: &Path) -> io::Result<File> {
use std::os::unix::fs::OpenOptionsExt as _;
std::fs::OpenOptions::new()
.write(true)
.create_new(true)
.mode(0o600)
.open(path)
}
pub fn read_private_regular_file_bounded(path: &Path, max_bytes: usize) -> io::Result<Vec<u8>> {
use std::os::unix::fs::{MetadataExt as _, OpenOptionsExt as _, PermissionsExt as _};
let parent = path.parent().ok_or_else(|| io::Error::new(io::ErrorKind::InvalidInput, "private file path has no parent"))?;
let parent_metadata = parent.metadata()?;
if !parent_metadata.is_dir() || parent_metadata.uid() != unsafe { libc::geteuid() } || parent_metadata.permissions().mode() & 0o077 != 0 {
return Err(io::Error::new(io::ErrorKind::PermissionDenied, "private file parent is not current-user private"));
}
let file = std::fs::OpenOptions::new().read(true).custom_flags(libc::O_NOFOLLOW | libc::O_NONBLOCK).open(path)?;
let metadata = file.metadata()?;
if !metadata.is_file() {
return Err(io::Error::new(io::ErrorKind::InvalidInput, "private input is not a regular file"));
}
if metadata.uid() != unsafe { libc::geteuid() } || metadata.permissions().mode() & 0o077 != 0 {
return Err(io::Error::new(io::ErrorKind::PermissionDenied, "private input is not current-user private"));
}
let bound_plus_one = max_bytes.checked_add(1).ok_or_else(|| io::Error::new(io::ErrorKind::InvalidInput, "private file limit overflows"))?;
if metadata.len() > max_bytes as u64 {
return Err(io::Error::new(io::ErrorKind::InvalidData, "private input exceeds limit"));
}
let mut bytes = Vec::with_capacity(bound_plus_one);
(&mut &file)
.take(bound_plus_one as u64)
.read_to_end(&mut bytes)?;
if bytes.len() > max_bytes {
return Err(io::Error::new(io::ErrorKind::InvalidData, "private input exceeds limit"));
}
if path_identity(path)? != file_identity(&file)? {
return Err(io::Error::new(io::ErrorKind::InvalidData, "private input path changed while it was read"));
}
Ok(bytes)
}
pub fn read_context_regular_file_bounded(
path: &Path,
max_bytes: usize,
) -> io::Result<crate::platform::fs::ContextFileObservation> {
use std::os::unix::fs::{MetadataExt as _, OpenOptionsExt as _};
let bound_plus_one = max_bytes.checked_add(1).ok_or_else(|| io::Error::new(io::ErrorKind::InvalidInput, "context file limit overflows"))?;
if std::fs::symlink_metadata(path)?.file_type().is_symlink() {
return Err(io::Error::new(io::ErrorKind::InvalidInput, "context input final component is a symbolic link"));
}
let file = std::fs::OpenOptions::new().read(true).custom_flags(libc::O_NOFOLLOW | libc::O_NONBLOCK).open(path)?;
let before = file.metadata()?;
if !before.is_file() {
return Err(io::Error::new(io::ErrorKind::InvalidInput, "context input is not a regular file"));
}
let identity = file_identity(&file)?.ok_or_else(|| io::Error::new(io::ErrorKind::Unsupported, "context file identity is unavailable"))?;
let before_modified = before.modified()?;
if before.len() > max_bytes as u64 {
return Err(io::Error::new(io::ErrorKind::InvalidData, "context input exceeds limit"));
}
let mut bytes = Vec::with_capacity(bound_plus_one);
(&mut &file).take(bound_plus_one as u64).read_to_end(&mut bytes)?;
if bytes.len() > max_bytes {
return Err(io::Error::new(io::ErrorKind::InvalidData, "context input exceeds limit"));
}
let after = file.metadata()?;
let after_identity = file_identity(&file)?.ok_or_else(|| io::Error::new(io::ErrorKind::Unsupported, "context file identity is unavailable"))?;
if after_identity != identity || after.len() != before.len() || after.modified()? != before_modified || bytes.len() as u64 != after.len() {
return Err(io::Error::new(io::ErrorKind::InvalidData, "context input changed while it was read"));
}
let path_metadata = std::fs::symlink_metadata(path)?;
if !path_metadata.is_file()
|| (FileIdentity {
device: path_metadata.dev(),
file: path_metadata.ino(),
}) != identity
{
return Err(io::Error::new(io::ErrorKind::InvalidData, "context input path changed while it was read"));
}
Ok(crate::platform::fs::ContextFileObservation { bytes, metadata: crate::platform::fs::context_regular_file_metadata(&after, identity)? })
}
#[cfg(test)]
mod private_directory_tests {
use super::*;
use std::os::unix::fs::PermissionsExt as _;
fn mode_of(path: &Path) -> u32 {
std::fs::metadata(path).expect("metadata").permissions().mode() & 0o777
}
#[test]
fn an_exposed_directory_is_tightened_to_owner_only() {
let root = tempfile::tempdir().expect("temp root");
let path = root.path().join("exposed");
std::fs::create_dir(&path).expect("create");
std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o777)).expect("expose");
assert!(ensure_dir_private(&path).expect("tighten"), "it was exposed");
assert_eq!(mode_of(&path), 0o700);
}
#[test]
fn an_owner_only_directory_is_left_alone() {
let root = tempfile::tempdir().expect("temp root");
let path = root.path().join("private");
std::fs::create_dir(&path).expect("create");
std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o700)).expect("tighten");
assert!(!ensure_dir_private(&path).expect("inspect"));
assert_eq!(mode_of(&path), 0o700);
}
#[test]
fn a_sticky_shared_root_is_not_tightened() {
let root = tempfile::tempdir().expect("temp root");
let path = root.path().join("shared");
std::fs::create_dir(&path).expect("create");
std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o1777)).expect("sticky");
assert!(!ensure_dir_private(&path).expect("inspect sticky"));
assert_eq!(mode_of(&path), 0o777, "the shared root keeps its mode");
}
#[test]
fn created_directories_and_their_parents_are_owner_only() {
let root = tempfile::tempdir().expect("temp root");
let outer = root.path().join("outer");
let inner = outer.join("inner");
create_dir_all_private(&inner).expect("create");
assert_eq!(mode_of(&inner), 0o700);
assert_eq!(mode_of(&outer), 0o700);
}
}