#[cfg(any(feature = "sync", feature = "smol", feature = "tokio"))]
use crate::error::{Error, ErrorKind, Result};
#[cfg(any(feature = "sync", feature = "smol", feature = "tokio"))]
use std::path::Path;
#[cfg(any(feature = "sync", feature = "smol", feature = "tokio"))]
fn not_a_directory_error() -> Error {
Error::other("not a directory")
}
#[cfg(any(feature = "sync", feature = "smol", feature = "tokio"))]
fn no_parent_error() -> Error {
Error::new(ErrorKind::InvalidInput, "path has no parent directory")
}
#[cfg(all(windows, any(feature = "sync", feature = "smol", feature = "tokio")))]
pub(crate) fn sync_directory(path: &Path) -> Result<()> {
use std::os::windows::fs::OpenOptionsExt;
const FILE_FLAG_BACKUP_SEMANTICS: u32 = 0x02000000;
if !path.is_dir() {
return Err(not_a_directory_error());
}
let _file = std::fs::OpenOptions::new()
.access_mode(0)
.custom_flags(FILE_FLAG_BACKUP_SEMANTICS)
.open(path)?;
Ok(())
}
#[cfg(all(
not(windows),
any(feature = "sync", feature = "smol", feature = "tokio")
))]
pub(crate) fn sync_directory(path: &Path) -> Result<()> {
if !path.is_dir() {
return Err(not_a_directory_error());
}
std::fs::File::open(path)?.sync_all()
}
#[cfg(any(feature = "sync", feature = "smol", feature = "tokio"))]
fn sync_path_parent(path: &Path) -> Result<()> {
let canonical = path.canonicalize()?;
let parent = canonical.parent().ok_or_else(no_parent_error)?;
sync_directory(parent)
}
#[cfg(any(feature = "sync", feature = "smol", feature = "tokio"))]
pub(crate) fn open_parent_for_sync(path: &Path) -> Result<std::fs::File> {
let parent_buf = match path.parent() {
Some(p) if !p.as_os_str().is_empty() => p.to_path_buf(),
_ => std::path::PathBuf::from("."),
};
#[cfg(windows)]
{
use std::os::windows::fs::OpenOptionsExt;
const FILE_FLAG_BACKUP_SEMANTICS: u32 = 0x02000000;
std::fs::OpenOptions::new()
.access_mode(0)
.custom_flags(FILE_FLAG_BACKUP_SEMANTICS)
.open(&parent_buf)
}
#[cfg(not(windows))]
{
std::fs::File::open(&parent_buf)
}
}
#[cfg(any(feature = "sync", feature = "smol", feature = "tokio"))]
pub(crate) fn identity_at_or_path(
parent: &std::fs::File,
full_path: &Path,
) -> Result<FileIdentity> {
#[cfg(unix)]
{
use rustix::fs::{AtFlags, FileType};
let basename = full_path.file_name().ok_or_else(|| {
Error::new(
ErrorKind::InvalidInput,
"path has no basename (cannot statat)",
)
})?;
let stat = rustix::fs::statat(parent, basename, AtFlags::SYMLINK_NOFOLLOW)
.map_err(std::io::Error::from)?;
if FileType::from_raw_mode(stat.st_mode as _) == FileType::Symlink {
return Err(Error::other(format!(
"identity-checked delete refuses to follow symlink at '{}': remove_file would unlink the symlink, not the target.",
full_path.display(),
)));
}
Ok(FileIdentity {
dev: stat.st_dev as u64,
ino: stat.st_ino as u64,
})
}
#[cfg(not(unix))]
{
let _ = parent;
FileIdentity::from_path(full_path)
}
}
#[cfg(any(feature = "sync", feature = "smol", feature = "tokio"))]
pub(crate) fn unlink_at_or_path(
parent: &std::fs::File,
full_path: &Path,
expected_identity: FileIdentity,
) -> Result<()> {
#[cfg(unix)]
{
let _ = expected_identity;
use rustix::fs::AtFlags;
let basename = full_path.file_name().ok_or_else(|| {
Error::new(
ErrorKind::InvalidInput,
"path has no basename (cannot unlinkat)",
)
})?;
rustix::fs::unlinkat(parent, basename, AtFlags::empty()).map_err(std::io::Error::from)?;
Ok(())
}
#[cfg(windows)]
{
let _ = parent;
use ::windows_sys::Win32::Storage::FileSystem::{
FileBasicInfo, FileDispositionInfo, FileDispositionInfoEx, GetFileInformationByHandleEx,
ReOpenFile, SetFileInformationByHandle, FILE_ATTRIBUTE_NORMAL, FILE_ATTRIBUTE_READONLY,
FILE_BASIC_INFO, FILE_DISPOSITION_FLAG_DELETE,
FILE_DISPOSITION_FLAG_IGNORE_READONLY_ATTRIBUTE, FILE_DISPOSITION_FLAG_POSIX_SEMANTICS,
FILE_DISPOSITION_INFO, FILE_DISPOSITION_INFO_EX,
};
use std::os::windows::{
fs::{MetadataExt, OpenOptionsExt},
io::{AsRawHandle, FromRawHandle},
};
const FILE_FLAG_BACKUP_SEMANTICS: u32 = 0x02000000;
const FILE_FLAG_OPEN_REPARSE_POINT: u32 = 0x00200000;
const FILE_ATTRIBUTE_REPARSE_POINT: u32 = 0x00000400;
const DELETE_ACCESS: u32 = 0x00010000;
const FILE_WRITE_ATTRIBUTES: u32 = 0x00000100;
const FILE_SHARE_READ: u32 = 0x00000001;
const FILE_SHARE_WRITE: u32 = 0x00000002;
const FILE_SHARE_DELETE: u32 = 0x00000004;
const INVALID_HANDLE_VALUE: isize = -1;
let file = std::fs::OpenOptions::new()
.access_mode(DELETE_ACCESS)
.share_mode(FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE)
.custom_flags(FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OPEN_REPARSE_POINT)
.open(full_path)?;
let meta = file.metadata()?;
if meta.file_attributes() & FILE_ATTRIBUTE_REPARSE_POINT != 0 {
return Err(Error::other(format!(
"identity-checked delete refuses to follow reparse point at '{}': remove would unlink the link/junction, not the target.",
full_path.display(),
)));
}
let probe = unsafe { FileIdentity::from_raw_handle(file.as_raw_handle()) }?;
if !expected_identity.is_known_equal(&probe) {
return Err(Error::other(format!(
"cannot unlink '{}': path no longer names the original file (path-reuse detected between handle drop and unlink)",
full_path.display(),
)));
}
let handle_raw = file.as_raw_handle() as _;
let info_ex = FILE_DISPOSITION_INFO_EX {
Flags: FILE_DISPOSITION_FLAG_DELETE
| FILE_DISPOSITION_FLAG_POSIX_SEMANTICS
| FILE_DISPOSITION_FLAG_IGNORE_READONLY_ATTRIBUTE,
};
let ok_ex = unsafe {
SetFileInformationByHandle(
handle_raw,
FileDispositionInfoEx,
&info_ex as *const _ as *const _,
std::mem::size_of::<FILE_DISPOSITION_INFO_EX>() as u32,
)
};
if ok_ex == 0 {
let reopen_raw = unsafe {
ReOpenFile(
handle_raw,
DELETE_ACCESS | FILE_WRITE_ATTRIBUTES,
FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OPEN_REPARSE_POINT,
)
};
if reopen_raw as isize == INVALID_HANDLE_VALUE {
return Err(Error::last_os_error());
}
let reopened = unsafe { std::fs::File::from_raw_handle(reopen_raw as _) };
let reopened_raw = reopened.as_raw_handle() as _;
let mut basic: FILE_BASIC_INFO = unsafe { std::mem::zeroed() };
let ok_get = unsafe {
GetFileInformationByHandleEx(
reopened_raw,
FileBasicInfo,
&mut basic as *mut _ as *mut _,
std::mem::size_of::<FILE_BASIC_INFO>() as u32,
)
};
let was_readonly = ok_get != 0 && (basic.FileAttributes & FILE_ATTRIBUTE_READONLY) != 0;
if was_readonly {
let mut clear = basic;
clear.FileAttributes &= !FILE_ATTRIBUTE_READONLY;
if clear.FileAttributes == 0 {
clear.FileAttributes = FILE_ATTRIBUTE_NORMAL;
}
let _ = unsafe {
SetFileInformationByHandle(
reopened_raw,
FileBasicInfo,
&clear as *const _ as *const _,
std::mem::size_of::<FILE_BASIC_INFO>() as u32,
)
};
}
let info = FILE_DISPOSITION_INFO { DeleteFile: true };
let ok = unsafe {
SetFileInformationByHandle(
reopened_raw,
FileDispositionInfo,
&info as *const _ as *const _,
std::mem::size_of::<FILE_DISPOSITION_INFO>() as u32,
)
};
if ok == 0 {
let err = Error::last_os_error();
if was_readonly {
let _ = unsafe {
SetFileInformationByHandle(
reopened_raw,
FileBasicInfo,
&basic as *const _ as *const _,
std::mem::size_of::<FILE_BASIC_INFO>() as u32,
)
};
}
return Err(err);
}
drop(reopened);
}
drop(file);
Ok(())
}
#[cfg(not(any(unix, windows)))]
{
let _ = (parent, expected_identity);
std::fs::remove_file(full_path)
}
}
#[cfg(any(feature = "sync", feature = "smol", feature = "tokio"))]
pub(crate) fn sync_parent_handle(parent: &std::fs::File) -> Result<()> {
#[cfg(unix)]
{
parent.sync_all()
}
#[cfg(windows)]
{
let _ = parent;
Ok(())
}
#[cfg(not(any(unix, windows)))]
{
parent.sync_all()
}
}
#[cfg(any(feature = "sync", feature = "smol", feature = "tokio"))]
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub(crate) struct FileIdentity {
#[cfg(unix)]
dev: u64,
#[cfg(unix)]
ino: u64,
#[cfg(windows)]
volume_serial: u32,
#[cfg(windows)]
file_index: u64,
#[cfg(not(any(unix, windows)))]
_placeholder: (),
}
#[cfg(any(feature = "sync", feature = "smol", feature = "tokio"))]
impl FileIdentity {
#[allow(dead_code)]
pub(crate) fn from_file(file: &std::fs::File) -> Result<Self> {
#[cfg(unix)]
{
use std::os::unix::fs::MetadataExt;
let m = file.metadata()?;
Ok(Self {
dev: m.dev(),
ino: m.ino(),
})
}
#[cfg(windows)]
{
use std::os::windows::io::AsRawHandle;
let mut info = ::std::mem::MaybeUninit::<
::windows_sys::Win32::Storage::FileSystem::BY_HANDLE_FILE_INFORMATION,
>::uninit();
let ok = unsafe {
::windows_sys::Win32::Storage::FileSystem::GetFileInformationByHandle(
file.as_raw_handle() as ::windows_sys::Win32::Foundation::HANDLE,
info.as_mut_ptr(),
)
};
if ok == 0 {
return Err(Error::last_os_error());
}
let info = unsafe { info.assume_init() };
let file_index = ((info.nFileIndexHigh as u64) << 32) | (info.nFileIndexLow as u64);
Ok(Self {
volume_serial: info.dwVolumeSerialNumber,
file_index,
})
}
#[cfg(not(any(unix, windows)))]
{
let _ = file;
Ok(Self { _placeholder: () })
}
}
#[cfg(unix)]
#[allow(dead_code)] pub(crate) unsafe fn from_raw_fd(fd: std::os::fd::RawFd) -> Result<Self> {
use std::os::fd::BorrowedFd;
let borrowed = unsafe { BorrowedFd::borrow_raw(fd) };
let stat = rustix::fs::fstat(borrowed).map_err(std::io::Error::from)?;
Ok(Self {
dev: stat.st_dev as u64,
ino: stat.st_ino as u64,
})
}
#[cfg(windows)]
#[allow(dead_code)] pub(crate) unsafe fn from_raw_handle(handle: std::os::windows::io::RawHandle) -> Result<Self> {
use ::windows_sys::Win32::Storage::FileSystem::{
GetFileInformationByHandle, BY_HANDLE_FILE_INFORMATION,
};
let mut info: BY_HANDLE_FILE_INFORMATION = unsafe { std::mem::zeroed() };
let ok = unsafe { GetFileInformationByHandle(handle as _, &mut info) };
if ok == 0 {
return Err(Error::last_os_error());
}
Ok(Self {
volume_serial: info.dwVolumeSerialNumber,
file_index: ((info.nFileIndexHigh as u64) << 32) | info.nFileIndexLow as u64,
})
}
#[allow(dead_code)]
pub(crate) fn from_path(path: &Path) -> Result<Self> {
#[cfg(unix)]
{
let lmeta = std::fs::symlink_metadata(path)?;
if lmeta.file_type().is_symlink() {
return Err(Error::other(format!(
"identity-checked delete refuses to follow symlink at '{}': remove_file would unlink the symlink, not the target. Use std::fs::remove_file or canonicalize the path yourself.",
path.display(),
)));
}
use std::os::unix::fs::MetadataExt;
Ok(Self {
dev: lmeta.dev(),
ino: lmeta.ino(),
})
}
#[cfg(windows)]
{
use std::os::windows::fs::{MetadataExt, OpenOptionsExt};
const FILE_FLAG_BACKUP_SEMANTICS: u32 = 0x02000000;
const FILE_FLAG_OPEN_REPARSE_POINT: u32 = 0x00200000;
const FILE_ATTRIBUTE_REPARSE_POINT: u32 = 0x00000400;
let file = std::fs::OpenOptions::new()
.access_mode(0)
.custom_flags(FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OPEN_REPARSE_POINT)
.open(path)?;
let meta = file.metadata()?;
if meta.file_attributes() & FILE_ATTRIBUTE_REPARSE_POINT != 0 {
return Err(Error::other(format!(
"identity-checked delete refuses to follow reparse point at '{}': remove_file would unlink the link/junction, not the target.",
path.display(),
)));
}
Self::from_file(&file)
}
#[cfg(not(any(unix, windows)))]
{
let _ = path;
Err(Error::other("FileIdentity unsupported on this platform"))
}
}
pub(crate) fn is_known_equal(&self, other: &Self) -> bool {
#[cfg(unix)]
{
self.dev == other.dev && self.ino == other.ino
}
#[cfg(windows)]
{
self.volume_serial == other.volume_serial && self.file_index == other.file_index
}
#[cfg(not(any(unix, windows)))]
{
let _ = other;
false
}
}
#[allow(dead_code)] pub(crate) fn matches_path(&self, path: &Path) -> bool {
match Self::from_path(path) {
Ok(probe) => self.is_known_equal(&probe),
Err(_) => false,
}
}
}
cfg_sync! {
use std::fs::{File, OpenOptions};
pub fn sync_dir<P: AsRef<Path>>(path: P) -> Result<()> {
sync_directory(path.as_ref())
}
pub fn sync_parent<P: AsRef<Path>>(path: P) -> Result<()> {
sync_path_parent(path.as_ref())
}
pub fn open_read_only_file<P: AsRef<Path>>(path: P) -> Result<File> {
OpenOptions::new().read(true).open(path)
}
pub fn open_exist_file<P: AsRef<Path>>(path: P) -> Result<File> {
OpenOptions::new()
.read(true)
.write(true)
.append(false)
.open(path)
}
pub fn open_exist_file_with_append<P: AsRef<Path>>(path: P) -> Result<File> {
OpenOptions::new().read(true).append(true).open(path)
}
pub fn open_file_with_truncate<P: AsRef<Path>>(path: P) -> Result<File> {
OpenOptions::new()
.read(true)
.write(true)
.truncate(true)
.open(path)
}
pub fn open_or_create_file<P: AsRef<Path>>(path: P) -> Result<File> {
OpenOptions::new()
.create(true)
.truncate(false)
.read(true)
.write(true)
.open(path)
}
pub fn create_file<P: AsRef<Path>>(path: P) -> Result<File> {
OpenOptions::new()
.create_new(true)
.read(true)
.write(true)
.open(path)
}
}
cfg_async! {
macro_rules! impl_async_file_utils {
($file: ident, $open_options: ident) => {
pub async fn open_read_only_file_async<P: AsRef<Path>>(path: P) -> Result<$file> {
<$open_options>::new().read(true).open(path).await
}
pub async fn open_exist_file_async<P: AsRef<Path>>(path: P) -> Result<$file> {
<$open_options>::new()
.read(true)
.write(true)
.append(false)
.open(path)
.await
}
pub async fn open_exist_file_with_append_async<P: AsRef<Path>>(path: P) -> Result<$file> {
<$open_options>::new()
.read(true)
.write(true)
.append(true)
.open(path)
.await
}
pub async fn open_file_with_truncate_async<P: AsRef<Path>>(path: P) -> Result<$file> {
<$open_options>::new()
.read(true)
.write(true)
.truncate(true)
.open(path)
.await
}
pub async fn open_or_create_file_async<P: AsRef<Path>>(path: P) -> Result<$file> {
<$open_options>::new()
.create(true)
.read(true)
.write(true)
.open(path)
.await
}
pub async fn create_file_async<P: AsRef<Path>>(path: P) -> Result<$file> {
<$open_options>::new()
.create_new(true)
.read(true)
.write(true)
.open(path)
.await
}
};
}
}
cfg_smol! {
pub mod smol {
use crate::error::Result;
use smol::fs::{File, OpenOptions};
use std::path::Path;
pub async fn sync_dir_async<P: AsRef<Path>>(path: P) -> Result<()> {
let path = path.as_ref().to_path_buf();
::smol::unblock(move || super::sync_directory(&path)).await
}
pub async fn sync_parent_async<P: AsRef<Path>>(path: P) -> Result<()> {
let path = path.as_ref().to_path_buf();
::smol::unblock(move || super::sync_path_parent(&path)).await
}
impl_async_file_utils!(File, OpenOptions);
}
}
cfg_tokio! {
pub mod tokio {
use crate::error::Result;
use std::path::Path;
use tokio::fs::{File, OpenOptions};
pub async fn sync_dir_async<P: AsRef<Path>>(path: P) -> Result<()> {
let path = path.as_ref().to_path_buf();
::tokio::task::spawn_blocking(move || super::sync_directory(&path))
.await
.map_err(std::io::Error::other)?
}
pub async fn sync_parent_async<P: AsRef<Path>>(path: P) -> Result<()> {
let path = path.as_ref().to_path_buf();
::tokio::task::spawn_blocking(move || super::sync_path_parent(&path))
.await
.map_err(std::io::Error::other)?
}
impl_async_file_utils!(File, OpenOptions);
}
}