use std::{
ffi::{OsStr, OsString},
fs::{self, DirBuilder, File, OpenOptions},
io::{self, ErrorKind, Write},
path::{Component, Path, PathBuf},
sync::atomic::{AtomicU64, Ordering},
time::{SystemTime, UNIX_EPOCH},
};
use std::os::unix::{
fs::{DirBuilderExt, MetadataExt, OpenOptionsExt},
io::AsRawFd,
};
static TEMPORARY_FILE_COUNTER: AtomicU64 = AtomicU64::new(0);
#[derive(Debug, Clone, Copy)]
pub struct AnchoredPathOptions {
pub directory_mode: Option<u32>,
pub file_mode: Option<u32>,
}
impl AnchoredPathOptions {
#[must_use]
pub const fn new(directory_mode: Option<u32>, file_mode: Option<u32>) -> Self {
Self {
directory_mode,
file_mode,
}
}
}
pub struct AnchoredTarget {
parent_dir: File,
parent_path: PathBuf,
file_name: OsString,
}
impl AnchoredTarget {
#[must_use]
pub const fn new(parent_dir: File, parent_path: PathBuf, file_name: OsString) -> Self {
Self {
parent_dir,
parent_path,
file_name,
}
}
#[must_use]
pub const fn parent_dir(&self) -> &File {
&self.parent_dir
}
#[must_use]
pub fn file_name(&self) -> &OsStr {
&self.file_name
}
#[must_use]
pub fn final_path(&self) -> PathBuf {
fd_child_path(&self.parent_dir, &self.file_name)
}
#[must_use]
pub fn logical_path(&self) -> PathBuf {
self.parent_path.join(&self.file_name)
}
}
pub fn open_anchored_target(
root: &Path,
path: &Path,
options: AnchoredPathOptions,
invalid_path_error: fn() -> io::Error,
) -> io::Result<AnchoredTarget> {
let parent_path = path.parent().ok_or_else(invalid_path_error)?;
let file_name = path.file_name().ok_or_else(invalid_path_error)?;
let relative_parent = parent_path
.strip_prefix(root)
.map_err(|_error| invalid_path_error())?;
let mut current = match open_directory(root) {
Ok(directory) => directory,
Err(error) if error.kind() == ErrorKind::NotFound => {
create_directory_all(root, options.directory_mode)?;
open_directory(root)?
}
Err(error) => return Err(error),
};
for component in relative_parent.components() {
let Component::Normal(segment) = component else {
return Err(invalid_path_error());
};
current = open_or_create_child_directory(¤t, segment, true, options.directory_mode)?;
}
Ok(AnchoredTarget::new(
current,
parent_path.to_path_buf(),
file_name.to_os_string(),
))
}
pub fn open_directory_chain(
path: &Path,
create_missing: bool,
directory_mode: Option<u32>,
invalid_path_error: fn() -> io::Error,
) -> io::Result<File> {
let mut current = if path.is_absolute() {
open_directory(Path::new("/"))?
} else {
open_directory(Path::new("."))?
};
for component in path.components() {
match component {
Component::RootDir | Component::CurDir => {}
Component::ParentDir => {
current = open_directory(&fd_child_path(¤t, OsStr::new("..")))?;
}
Component::Normal(segment) => {
current = open_or_create_child_directory(
¤t,
segment,
create_missing,
directory_mode,
)?;
}
Component::Prefix(_prefix) => return Err(invalid_path_error()),
}
}
Ok(current)
}
pub fn open_or_create_child_directory(
parent: &File,
segment: &OsStr,
create_missing: bool,
directory_mode: Option<u32>,
) -> io::Result<File> {
let child_path = fd_child_path(parent, segment);
match open_directory(&child_path) {
Ok(directory) => Ok(directory),
Err(error) if error.kind() == ErrorKind::NotFound && create_missing => {
match create_directory(&child_path, directory_mode) {
Ok(()) => {}
Err(create_error) if create_error.kind() == ErrorKind::AlreadyExists => {}
Err(create_error) => return Err(create_error),
}
open_directory(&child_path)
}
Err(error) => Err(error),
}
}
pub fn write_anchored_temporary_file(
anchored: &AnchoredTarget,
bytes: &[u8],
file_mode: Option<u32>,
) -> io::Result<PathBuf> {
loop {
let temporary = fd_child_path(
anchored.parent_dir(),
&temporary_file_name(anchored.file_name()),
);
match open_new_file(&temporary, file_mode) {
Ok(mut file) => {
file.write_all(bytes)?;
file.flush()?;
return Ok(temporary);
}
Err(error) if error.kind() == ErrorKind::AlreadyExists => {}
Err(error) => return Err(error),
}
}
}
#[must_use]
pub fn temporary_file_name(file_name: &OsStr) -> OsString {
let counter = TEMPORARY_FILE_COUNTER.fetch_add(1, Ordering::Relaxed);
let unix_nanos = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map_or(0_u128, |duration| duration.as_nanos());
let mut name = file_name.to_os_string();
name.push(format!(".tmp-{unix_nanos}-{counter}"));
name
}
pub fn ensure_parent_path_matches_anchor(
anchored: &AnchoredTarget,
changed_message: &'static str,
) -> io::Result<()> {
let anchored_metadata = anchored.parent_dir.metadata()?;
let current_metadata = fs::symlink_metadata(&anchored.parent_path)?;
if current_metadata.file_type().is_symlink() || !current_metadata.is_dir() {
return Err(io::Error::new(ErrorKind::InvalidData, changed_message));
}
if anchored_metadata.dev() != current_metadata.dev()
|| anchored_metadata.ino() != current_metadata.ino()
{
return Err(io::Error::new(ErrorKind::InvalidData, changed_message));
}
Ok(())
}
pub fn open_directory(path: &Path) -> io::Result<File> {
let mut options = OpenOptions::new();
options.read(true);
#[cfg(not(target_os = "macos"))]
options.custom_flags(libc::O_DIRECTORY | libc::O_NOFOLLOW);
#[cfg(target_os = "macos")]
options.custom_flags(libc::O_DIRECTORY);
options.open(path)
}
pub fn open_new_file(path: &Path, file_mode: Option<u32>) -> io::Result<File> {
let mut options = OpenOptions::new();
options
.write(true)
.create_new(true)
.custom_flags(libc::O_NOFOLLOW);
if let Some(mode) = file_mode {
options.mode(mode);
}
options.open(path)
}
pub fn create_directory(path: &Path, directory_mode: Option<u32>) -> io::Result<()> {
let mut builder = DirBuilder::new();
if let Some(mode) = directory_mode {
builder.mode(mode);
}
builder.create(path)
}
pub fn create_directory_all(path: &Path, directory_mode: Option<u32>) -> io::Result<()> {
let mut builder = DirBuilder::new();
builder.recursive(true);
if let Some(mode) = directory_mode {
builder.mode(mode);
}
builder.create(path)
}
#[must_use]
pub fn fd_child_path(directory: &File, child: &OsStr) -> PathBuf {
let mut path = fd_base_path(directory);
path.push(child);
path
}
#[cfg(target_os = "macos")]
#[allow(unsafe_code)]
pub fn rename_at(parent: &File, old_name: &OsStr, new_name: &OsStr) -> io::Result<()> {
use std::ffi::CString;
use std::os::unix::io::AsRawFd;
let old_cstr = CString::new(old_name.as_encoded_bytes()).map_err(|e| {
io::Error::new(
ErrorKind::InvalidInput,
format!("name contains null byte: {e}"),
)
})?;
let new_cstr = CString::new(new_name.as_encoded_bytes()).map_err(|e| {
io::Error::new(
ErrorKind::InvalidInput,
format!("name contains null byte: {e}"),
)
})?;
let result = unsafe {
libc::renameat(
parent.as_raw_fd(),
old_cstr.as_ptr(),
parent.as_raw_fd(),
new_cstr.as_ptr(),
)
};
if result == 0 {
Ok(())
} else {
Err(io::Error::last_os_error())
}
}
#[cfg(not(target_os = "macos"))]
pub fn rename_at(parent: &File, old_name: &OsStr, new_name: &OsStr) -> io::Result<()> {
let old_path = fd_child_path(parent, old_name);
let new_path = fd_child_path(parent, new_name);
std::fs::rename(old_path, new_path)
}
#[cfg(target_os = "macos")]
#[allow(unsafe_code)]
pub fn remove_at(parent: &File, name: &OsStr) -> io::Result<()> {
use std::ffi::CString;
use std::os::unix::io::AsRawFd;
let cstr = CString::new(name.as_encoded_bytes()).map_err(|e| {
io::Error::new(
ErrorKind::InvalidInput,
format!("name contains null byte: {e}"),
)
})?;
let result = unsafe { libc::unlinkat(parent.as_raw_fd(), cstr.as_ptr(), 0) };
if result == 0 {
Ok(())
} else {
Err(io::Error::last_os_error())
}
}
#[cfg(not(target_os = "macos"))]
pub fn remove_at(parent: &File, name: &OsStr) -> io::Result<()> {
let path = fd_child_path(parent, name);
std::fs::remove_file(path)
}
pub fn remove_if_present(path: &Path) -> io::Result<()> {
match fs::remove_file(path) {
Ok(()) => Ok(()),
Err(error) if error.kind() == ErrorKind::NotFound => Ok(()),
Err(error) => Err(error),
}
}
#[cfg(target_os = "macos")]
#[allow(unsafe_code)]
fn macos_fd_real_path(fd: std::os::unix::io::RawFd) -> PathBuf {
let mut buf = [0i8; 1024];
let result = unsafe { libc::fcntl(fd, libc::F_GETPATH, buf.as_mut_ptr()) };
if result < 0 {
return PathBuf::from(format!("/dev/fd/{fd}"));
}
let c_str = unsafe { std::ffi::CStr::from_ptr(buf.as_ptr()) };
PathBuf::from(c_str.to_string_lossy().as_ref())
}
fn fd_base_path(directory: &File) -> PathBuf {
#[cfg(target_os = "linux")]
{
PathBuf::from(format!("/proc/self/fd/{}", directory.as_raw_fd()))
}
#[cfg(target_os = "macos")]
{
macos_fd_real_path(directory.as_raw_fd())
}
#[cfg(not(target_os = "linux"))]
#[cfg(not(target_os = "macos"))]
{
PathBuf::from(format!("/dev/fd/{}", directory.as_raw_fd()))
}
}