use std::fs::File;
use std::io;
use std::path::{Path, PathBuf};
pub fn user_runtime_dir(product: &str) -> PathBuf {
if let Some(dir) = crate::env_vars::XDG_RUNTIME_DIR.os() {
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) = crate::env_vars::XDG_STATE_HOME.os() {
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_state_dir_from_environment(product: &str) -> PathBuf {
state_dir_from_environment_in(
crate::env_vars::XDG_STATE_HOME.os(),
crate::env_vars::HOME.os(),
product,
)
}
pub fn state_home_from_environment() -> Option<PathBuf> {
state_home_in(
crate::env_vars::XDG_STATE_HOME.os(),
crate::env_vars::HOME.os(),
)
}
fn state_home_in(
xdg_state_home: Option<std::ffi::OsString>,
home: Option<std::ffi::OsString>,
) -> Option<PathBuf> {
xdg_state_home.map(PathBuf::from).or_else(|| {
home.map(PathBuf::from)
.map(|home| home.join(".local").join("state"))
})
}
fn state_dir_from_environment_in(
xdg_state_home: Option<std::ffi::OsString>,
home: Option<std::ffi::OsString>,
product: &str,
) -> PathBuf {
state_home_in(xdg_state_home, home)
.map_or_else(
|| PathBuf::from(format!("/tmp/{product}-state")),
|base| base.join(product),
)
}
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 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 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 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 open_read_no_follow(path: &Path) -> io::Result<File> {
use std::os::unix::fs::OpenOptionsExt as _;
std::fs::OpenOptions::new()
.read(true)
.custom_flags(libc::O_NOFOLLOW)
.open(path)
}
pub fn is_link_handle(metadata: &std::fs::Metadata) -> bool {
metadata.file_type().is_symlink()
}
#[cfg(test)]
mod no_follow_tests {
#[test]
fn state_dir_from_environment_prefers_xdg_then_home_then_tmp() {
use super::state_dir_from_environment_in as dir;
use std::path::PathBuf;
assert_eq!(
dir(Some("/xdg".into()), Some("/home/u".into()), "rp"),
PathBuf::from("/xdg/rp")
);
assert_eq!(
dir(None, Some("/home/u".into()), "rp"),
PathBuf::from("/home/u/.local/state/rp")
);
assert_eq!(dir(None, None, "rp"), PathBuf::from("/tmp/rp-state"));
}
#[test]
fn state_home_prefers_xdg_then_home_and_has_no_fallback() {
use super::state_home_in as home;
use std::path::PathBuf;
assert_eq!(
home(Some("/xdg".into()), Some("/home/u".into())),
Some(PathBuf::from("/xdg"))
);
assert_eq!(
home(None, Some("/home/u".into())),
Some(PathBuf::from("/home/u/.local/state"))
);
assert_eq!(home(None, None), None);
}
#[test]
fn a_no_follow_open_refuses_a_symlink_at_the_final_component() {
let dir = std::env::temp_dir().join(format!("rp-nofollow-link-{}", std::process::id()));
std::fs::create_dir_all(&dir).unwrap();
let target = dir.join("target");
let link = dir.join("link");
std::fs::write(&target, b"secret").unwrap();
let _ = std::fs::remove_file(&link);
std::os::unix::fs::symlink(&target, &link).unwrap();
let error = super::open_read_no_follow(&link).expect_err("a symlink must not be followed");
assert_eq!(error.raw_os_error(), Some(libc::ELOOP));
let _ = std::fs::remove_dir_all(&dir);
}
}