use super::{FileHint, Fs, FsDirEntry, FsFile, FsMetadata, FsOpenOptions, SyncMode};
use crate::io;
use crate::path::{Path, PathBuf};
#[cfg(not(feature = "std"))]
use alloc::{boxed::Box, string::String, vec::Vec};
use std::fs::{File, OpenOptions};
#[cfg(windows)]
const TRANSIENT_HOLD_BACKOFF_MS: [u64; 9] = [1, 2, 4, 8, 16, 32, 64, 128, 256];
#[cfg(windows)]
fn retry_transient_hold(mut op: impl FnMut() -> std::io::Result<()>) -> std::io::Result<()> {
const ERROR_ACCESS_DENIED: i32 = 5;
const ERROR_SHARING_VIOLATION: i32 = 32;
const ERROR_LOCK_VIOLATION: i32 = 33;
const ERROR_USER_MAPPED_FILE: i32 = 1224;
for delay in TRANSIENT_HOLD_BACKOFF_MS {
match op() {
Err(e)
if matches!(
e.raw_os_error(),
Some(
ERROR_ACCESS_DENIED
| ERROR_SHARING_VIOLATION
| ERROR_LOCK_VIOLATION
| ERROR_USER_MAPPED_FILE
)
) =>
{
std::thread::sleep(std::time::Duration::from_millis(delay));
}
result => return result,
}
}
op()
}
#[cfg(not(windows))]
fn retry_transient_hold(mut op: impl FnMut() -> std::io::Result<()>) -> std::io::Result<()> {
op()
}
#[cfg(target_os = "macos")]
fn normal_fsync(file: &File) -> io::Result<()> {
use std::os::unix::io::AsRawFd;
let rc = unsafe { libc::fsync(file.as_raw_fd()) };
if rc == 0 {
Ok(())
} else {
Err(std::io::Error::last_os_error().into())
}
}
#[cfg(not(target_os = "macos"))]
fn normal_fsync(file: &File) -> io::Result<()> {
File::sync_all(file).map_err(io::Error::from)
}
#[cfg(target_os = "macos")]
fn barrier_fsync(file: &File) -> io::Result<()> {
use std::os::unix::io::AsRawFd;
let rc = unsafe { libc::fcntl(file.as_raw_fd(), libc::F_BARRIERFSYNC) };
if rc == -1 {
Err(std::io::Error::last_os_error().into())
} else {
Ok(())
}
}
#[cfg(not(target_os = "macos"))]
fn barrier_fsync(file: &File) -> io::Result<()> {
normal_fsync(file)
}
fn sync_file_with(file: &File, mode: SyncMode) -> io::Result<()> {
match mode {
SyncMode::Full => File::sync_all(file).map_err(io::Error::from),
SyncMode::Normal => normal_fsync(file),
SyncMode::Barrier => barrier_fsync(file),
}
}
#[derive(Clone, Copy, Debug, Default)]
pub struct StdFs;
impl FsFile for File {
fn sync_all(&self) -> io::Result<()> {
Self::sync_all(self).map_err(io::Error::from)
}
fn sync_data(&self) -> io::Result<()> {
Self::sync_data(self).map_err(io::Error::from)
}
fn sync_all_with(&self, mode: SyncMode) -> io::Result<()> {
sync_file_with(self, mode)
}
fn sync_data_with(&self, mode: SyncMode) -> io::Result<()> {
#[cfg(not(target_os = "macos"))]
{
let _ = mode;
Self::sync_data(self).map_err(io::Error::from)
}
#[cfg(target_os = "macos")]
match mode {
SyncMode::Full => Self::sync_data(self).map_err(io::Error::from),
SyncMode::Normal | SyncMode::Barrier => sync_file_with(self, mode),
}
}
fn metadata(&self) -> io::Result<FsMetadata> {
let m = Self::metadata(self)?;
Ok(FsMetadata {
len: m.len(),
is_dir: m.is_dir(),
is_file: m.is_file(),
})
}
#[cfg(unix)]
fn hard_link_count(&self) -> io::Result<u64> {
use std::os::unix::fs::MetadataExt as _;
Ok(Self::metadata(self)?.nlink())
}
#[cfg(windows)]
fn hard_link_count(&self) -> io::Result<u64> {
hard_link_count_sys::hard_link_count(self).map_err(io::Error::from)
}
fn set_len(&self, size: u64) -> io::Result<()> {
Self::set_len(self, size).map_err(io::Error::from)
}
fn read_at(&self, buf: &mut [u8], offset: u64) -> io::Result<usize> {
let mut filled = 0usize;
while filled < buf.len() {
#[expect(clippy::expect_used, reason = "filled < buf.len() by loop guard")]
let remaining = buf.get_mut(filled..).expect("filled < buf.len()");
let off = offset.checked_add(filled as u64).ok_or_else(|| {
io::Error::new(io::ErrorKind::InvalidInput, "read offset overflow")
})?;
let n = {
#[cfg(unix)]
{
use std::os::unix::fs::FileExt;
match FileExt::read_at(self, remaining, off) {
Ok(n) => n,
Err(e) if e.kind() == std::io::ErrorKind::Interrupted => continue,
Err(e) => return Err(e.into()),
}
}
#[cfg(windows)]
{
use std::os::windows::fs::FileExt;
match FileExt::seek_read(self, remaining, off) {
Ok(n) => n,
Err(e) if e.kind() == std::io::ErrorKind::Interrupted => continue,
Err(e) => return Err(e.into()),
}
}
#[cfg(not(any(unix, windows)))]
{
let _ = (remaining, off);
return Err(io::Error::new(
io::ErrorKind::Unsupported,
"read_at is not supported on this platform",
));
}
};
if n == 0 {
break; }
filled += n;
}
Ok(filled)
}
fn lock_exclusive(&self) -> io::Result<()> {
sys::lock_exclusive(self).map_err(io::Error::from)
}
fn try_lock_exclusive(&self) -> io::Result<bool> {
sys::try_lock_exclusive(self).map_err(io::Error::from)
}
fn hint(&self, hint: FileHint) -> io::Result<()> {
sys::fadvise(self, hint).map_err(io::Error::from)
}
fn start_writeback(&self, offset: u64, len: u64) -> io::Result<()> {
#[cfg(target_os = "linux")]
{
use std::os::unix::io::AsRawFd;
let (Ok(offset), Ok(len)) = (i64::try_from(offset), i64::try_from(len)) else {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"writeback range past the largest file offset",
));
};
let rc = unsafe {
libc::sync_file_range(self.as_raw_fd(), offset, len, libc::SYNC_FILE_RANGE_WRITE)
};
if rc == -1 {
Err(std::io::Error::last_os_error().into())
} else {
Ok(())
}
}
#[cfg(not(target_os = "linux"))]
{
let _ = (offset, len);
Ok(())
}
}
fn set_write_lifetime(&self, lifetime: super::WriteLifetime) -> io::Result<()> {
#[cfg(target_os = "linux")]
{
use std::os::unix::io::AsRawFd;
set_write_lifetime_fd(self.as_raw_fd(), lifetime)
}
#[cfg(not(target_os = "linux"))]
{
let _ = lifetime;
Ok(())
}
}
}
#[cfg(target_os = "linux")]
fn set_write_lifetime_fd(fd: i32, lifetime: super::WriteLifetime) -> io::Result<()> {
const F_SET_RW_HINT: libc::c_int = 1024 + 12;
let hint: u64 = match lifetime {
super::WriteLifetime::Short => 2,
super::WriteLifetime::Medium => 3,
super::WriteLifetime::Long => 4,
super::WriteLifetime::Extreme => 5,
};
let rc = unsafe { libc::fcntl(fd, F_SET_RW_HINT, &raw const hint) };
if rc == -1 {
Err(std::io::Error::last_os_error().into())
} else {
Ok(())
}
}
impl Fs for StdFs {
fn open(&self, path: &Path, opts: &FsOpenOptions) -> io::Result<Box<dyn FsFile>> {
let mut builder = OpenOptions::new();
builder
.read(opts.read)
.write(opts.write)
.create(opts.create)
.create_new(opts.create_new)
.truncate(opts.truncate)
.append(opts.append);
#[cfg(feature = "std")]
super::open_flags::apply_open_flags(&mut builder, opts);
let file = builder.open(path)?;
#[cfg(all(feature = "std", unix))]
super::open_flags::finish_open(&file, opts)?;
Ok(Box::new(file))
}
fn create_dir_all(&self, path: &Path) -> io::Result<()> {
std::fs::create_dir_all(path).map_err(io::Error::from)
}
fn create_dir(&self, path: &Path) -> io::Result<()> {
std::fs::create_dir(path).map_err(io::Error::from)
}
fn read_dir(&self, path: &Path) -> io::Result<Vec<FsDirEntry>> {
std::fs::read_dir(path)?
.map(|res| {
let entry = res?;
let file_type = entry.file_type()?;
let file_name_os = entry.file_name();
let file_name = file_name_os.into_string().map_err(|os| {
#[expect(
clippy::unnecessary_debug_formatting,
reason = "OsString has no Display impl - Debug is required"
)]
let msg = format!("non-UTF-8 filename in directory {}: {os:?}", path.display());
io::Error::new(io::ErrorKind::InvalidData, msg)
})?;
Ok(FsDirEntry {
path: entry.path(),
file_name,
is_dir: file_type.is_dir(),
})
})
.collect()
}
fn remove_file(&self, path: &Path) -> io::Result<()> {
retry_transient_hold(|| std::fs::remove_file(path)).map_err(io::Error::from)
}
fn remove_dir_all(&self, path: &Path) -> io::Result<()> {
std::fs::remove_dir_all(path).map_err(io::Error::from)
}
fn remove_dir(&self, path: &Path) -> io::Result<()> {
std::fs::remove_dir(path).map_err(io::Error::from)
}
fn same_file(&self, a: &Path, b: &Path) -> io::Result<bool> {
#[cfg(unix)]
{
use std::os::unix::fs::MetadataExt;
let ma = std::fs::metadata(a).map_err(io::Error::from)?;
let mb = std::fs::metadata(b).map_err(io::Error::from)?;
Ok(ma.dev() == mb.dev() && ma.ino() == mb.ino())
}
#[cfg(windows)]
{
let ia = hard_link_count_sys::file_identity(a).map_err(io::Error::from)?;
let ib = hard_link_count_sys::file_identity(b).map_err(io::Error::from)?;
Ok(ia == ib)
}
#[cfg(not(any(unix, windows)))]
{
let ca = std::fs::canonicalize(a).map_err(io::Error::from)?;
let cb = std::fs::canonicalize(b).map_err(io::Error::from)?;
Ok(ca == cb)
}
}
fn read_link(&self, path: &Path) -> io::Result<Option<PathBuf>> {
match std::fs::symlink_metadata(path) {
Ok(meta) if meta.file_type().is_symlink() => {
std::fs::read_link(path).map(Some).map_err(io::Error::from)
}
Ok(_) => Ok(None),
Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(None),
Err(e) => Err(io::Error::from(e)),
}
}
fn symlink(&self, target: &Path, link: &Path, to_dir: bool) -> io::Result<()> {
#[cfg(unix)]
let made = {
let _ = to_dir;
std::os::unix::fs::symlink(target, link)
};
#[cfg(windows)]
let made = if to_dir {
std::os::windows::fs::symlink_dir(target, link)
} else {
std::os::windows::fs::symlink_file(target, link)
};
#[cfg(not(any(unix, windows)))]
let made: std::io::Result<()> = {
let _ = (target, link, to_dir);
Err(std::io::Error::from(std::io::ErrorKind::Unsupported))
};
made.map_err(io::Error::from)
}
fn symlink_to_dir(&self, path: &Path) -> io::Result<bool> {
#[cfg(windows)]
{
use std::os::windows::fs::FileTypeExt;
Ok(std::fs::symlink_metadata(path)?
.file_type()
.is_symlink_dir())
}
#[cfg(not(windows))]
{
let _ = path;
Ok(false)
}
}
fn rename(&self, from: &Path, to: &Path) -> io::Result<()> {
retry_transient_hold(|| std::fs::rename(from, to)).map_err(io::Error::from)
}
fn metadata(&self, path: &Path) -> io::Result<FsMetadata> {
let m = std::fs::metadata(path)?;
Ok(FsMetadata {
len: m.len(),
is_dir: m.is_dir(),
is_file: m.is_file(),
})
}
#[cfg(unix)]
fn available_space(&self, path: &Path) -> io::Result<u64> {
super::statvfs_available_space(path).map_err(io::Error::from)
}
#[cfg(windows)]
fn available_space(&self, path: &Path) -> io::Result<u64> {
available_space_sys::disk_free_available(path).map_err(io::Error::from)
}
#[cfg(unix)]
fn allocated_size(&self, path: &Path) -> io::Result<Option<u64>> {
Ok(super::unix_allocated_size(path)?)
}
fn sync_directory(&self, path: &Path) -> io::Result<()> {
#[cfg(not(target_os = "windows"))]
{
let dir = File::open(path)?;
if !dir.metadata()?.is_dir() {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"sync_directory: path is not a directory",
));
}
dir.sync_all().map_err(io::Error::from)
}
#[cfg(target_os = "windows")]
{
let _ = path;
Ok(())
}
}
fn sync_directory_with(&self, path: &Path, mode: SyncMode) -> io::Result<()> {
#[cfg(not(target_os = "windows"))]
{
let dir = File::open(path)?;
if !dir.metadata()?.is_dir() {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"sync_directory: path is not a directory",
));
}
sync_file_with(&dir, mode)
}
#[cfg(target_os = "windows")]
{
let _ = (path, mode);
Ok(())
}
}
fn sync_device(&self, path: &Path) -> io::Result<()> {
#[cfg(target_os = "macos")]
{
File::open(path)?.sync_all().map_err(io::Error::from)
}
#[cfg(not(target_os = "macos"))]
{
let _ = path;
Ok(())
}
}
fn exists(&self, path: &Path) -> io::Result<bool> {
path.try_exists().map_err(io::Error::from)
}
fn hard_link(&self, src: &Path, dst: &Path) -> io::Result<()> {
std::fs::hard_link(src, dst).map_err(|e| {
if is_std_cross_device(&e) {
io::Error::from_kind(io::ErrorKind::CrossesDevices)
} else {
e.into()
}
})
}
fn backend_id(&self) -> Option<u64> {
Some(super::KERNEL_BACKEND_ID)
}
#[cfg(unix)]
fn volume_id(&self, path: &Path) -> Option<u64> {
super::unix_volume_id(path)
}
fn hard_link_count(&self, path: &Path) -> io::Result<u64> {
#[cfg(unix)]
{
use std::os::unix::fs::MetadataExt;
Ok(std::fs::metadata(path)?.nlink())
}
#[cfg(not(unix))]
{
let _ = path;
Err(io::Error::new(
io::ErrorKind::Unsupported,
"hard_link_count requires a Unix target",
))
}
}
#[cfg(target_os = "macos")]
fn capabilities(&self, path: &Path) -> super::FsCapabilities {
macos_caps::capabilities(path)
}
#[cfg(target_os = "macos")]
fn reflink_file(&self, src: &Path, dst: &Path) -> io::Result<()> {
match macos_caps::clonefile(src, dst) {
Ok(()) => Ok(()),
Err(e) if macos_caps::clone_should_fall_back(&e) => {
super::copy_file_streamed(self, src, dst)
}
Err(e) => Err(e.into()),
}
}
#[cfg(target_os = "linux")]
fn capabilities(&self, path: &Path) -> super::FsCapabilities {
linux_caps::capabilities(path)
}
#[cfg(target_os = "windows")]
fn capabilities(&self, _path: &Path) -> super::FsCapabilities {
super::FsCapabilities {
directory_sync_noop: true,
..super::FsCapabilities::default()
}
}
#[cfg(target_os = "linux")]
fn reflink_file(&self, src: &Path, dst: &Path) -> io::Result<()> {
match linux_caps::ficlone(src, dst) {
Ok(()) => Ok(()),
Err(e) if linux_caps::clone_should_fall_back(&e) => {
super::copy_file_streamed(self, src, dst)
}
Err(e) => Err(e.into()),
}
}
#[cfg(target_os = "linux")]
fn try_disable_cow(&self, path: &Path) -> io::Result<()> {
linux_caps::try_disable_cow(path).map_err(io::Error::from)
}
#[cfg(target_os = "linux")]
fn punch_hole(&self, path: &Path, offset: u64, len: u64) -> io::Result<()> {
linux_caps::punch_hole(path, offset, len).map_err(io::Error::from)
}
#[cfg(target_os = "linux")]
fn extent_is_hole(&self, path: &Path, offset: u64, len: u64) -> io::Result<Option<bool>> {
Ok(linux_caps::extent_is_hole(path, offset, len)?)
}
#[cfg(target_os = "linux")]
fn extent_contains_hole(&self, path: &Path, offset: u64, len: u64) -> io::Result<Option<bool>> {
Ok(linux_caps::extent_contains_hole(path, offset, len)?)
}
}
fn is_std_cross_device(err: &std::io::Error) -> bool {
#[cfg(unix)]
{
const EXDEV: i32 = 18;
if err.raw_os_error() == Some(EXDEV) {
return true;
}
}
matches!(
err.kind(),
std::io::ErrorKind::CrossesDevices | std::io::ErrorKind::Unsupported
)
}
#[expect(
clippy::redundant_pub_crate,
reason = "re-exported crate-wide via fs::mod; pub(crate) communicates the intended scope"
)]
pub(crate) fn is_cross_device(err: &io::Error) -> bool {
matches!(
err.kind(),
io::ErrorKind::CrossesDevices | io::ErrorKind::Unsupported
)
}
#[cfg(unix)]
mod sys {
use super::FileHint;
use std::ffi::c_int;
use std::fs::File;
use std::io;
use std::os::unix::io::AsRawFd;
const LOCK_EX: c_int = 2;
const LOCK_NB: c_int = 4;
unsafe extern "C" {
fn flock(fd: c_int, operation: c_int) -> c_int;
}
pub(super) fn lock_exclusive(file: &File) -> io::Result<()> {
let fd = file.as_raw_fd();
loop {
#[expect(unsafe_code, reason = "flock FFI call with valid fd")]
let ret = unsafe { flock(fd, LOCK_EX) };
if ret == 0 {
return Ok(());
}
let err = std::io::Error::last_os_error();
if err.kind() == std::io::ErrorKind::Interrupted {
continue;
}
return Err(err);
}
}
pub(super) fn try_lock_exclusive(file: &File) -> io::Result<bool> {
let fd = file.as_raw_fd();
loop {
#[expect(unsafe_code, reason = "flock FFI call with valid fd")]
let ret = unsafe { flock(fd, LOCK_EX | LOCK_NB) };
if ret == 0 {
return Ok(true);
}
let err = std::io::Error::last_os_error();
if err.kind() == std::io::ErrorKind::Interrupted {
continue;
}
if err.kind() == std::io::ErrorKind::WouldBlock {
return Ok(false);
}
return Err(err);
}
}
#[cfg(not(target_os = "macos"))]
const POSIX_FADV_NORMAL: c_int = 0;
#[cfg(not(target_os = "macos"))]
const POSIX_FADV_RANDOM: c_int = 1;
#[cfg(not(target_os = "macos"))]
const POSIX_FADV_SEQUENTIAL: c_int = 2;
#[cfg(not(target_os = "macos"))]
const POSIX_FADV_DONTNEED: c_int = 4;
#[cfg(not(target_os = "macos"))]
const EINVAL: c_int = 22;
#[cfg(all(
any(target_os = "linux", target_os = "android"),
target_pointer_width = "32",
any(target_env = "gnu", target_env = "")
))]
unsafe extern "C" {
fn posix_fadvise64(fd: c_int, offset: i64, len: i64, advice: c_int) -> c_int;
}
#[cfg(all(
not(target_os = "macos"),
not(all(
any(target_os = "linux", target_os = "android"),
target_pointer_width = "32",
any(target_env = "gnu", target_env = "")
))
))]
unsafe extern "C" {
fn posix_fadvise(fd: c_int, offset: i64, len: i64, advice: c_int) -> c_int;
}
#[cfg(not(target_os = "macos"))]
pub(super) fn fadvise(file: &File, hint: FileHint) -> io::Result<()> {
let advice = match hint {
FileHint::Default => POSIX_FADV_NORMAL,
FileHint::Sequential => POSIX_FADV_SEQUENTIAL,
FileHint::Random => POSIX_FADV_RANDOM,
FileHint::WriteOnce => POSIX_FADV_DONTNEED,
};
let fd = file.as_raw_fd();
#[expect(unsafe_code, reason = "posix_fadvise FFI call with valid fd")]
let ret = unsafe {
#[cfg(all(
any(target_os = "linux", target_os = "android"),
target_pointer_width = "32",
any(target_env = "gnu", target_env = "")
))]
{
posix_fadvise64(fd, 0, 0, advice)
}
#[cfg(not(all(
any(target_os = "linux", target_os = "android"),
target_pointer_width = "32",
any(target_env = "gnu", target_env = "")
)))]
{
posix_fadvise(fd, 0, 0, advice)
}
};
if ret == 0 || ret == EINVAL {
Ok(())
} else {
Err(std::io::Error::from_raw_os_error(ret))
}
}
#[cfg(target_os = "macos")]
#[expect(
clippy::unnecessary_wraps,
reason = "FsFile::hint signature requires io::Result<()>; macOS branch is a no-op until we wire fcntl(F_RDADVISE)"
)]
pub(super) fn fadvise(_file: &File, _hint: FileHint) -> io::Result<()> {
Ok(())
}
}
#[cfg(windows)]
mod sys {
use super::FileHint;
use std::fs::File;
use std::io;
use std::os::windows::io::AsRawHandle;
#[expect(
clippy::unnecessary_wraps,
reason = "FsFile::hint signature requires io::Result<()>; Windows branch is a no-op until we thread the hint through FsOpenOptions for CreateFile flags"
)]
pub(super) fn fadvise(_file: &File, _hint: FileHint) -> io::Result<()> {
Ok(())
}
pub(super) fn lock_exclusive(file: &File) -> io::Result<()> {
use core::ptr;
const LOCKFILE_EXCLUSIVE_LOCK: u32 = 0x0000_0002;
#[allow(non_snake_case, reason = "FFI name matches Windows API")]
unsafe extern "system" {
fn LockFileEx(
h_file: *mut std::ffi::c_void,
dw_flags: u32,
dw_reserved: u32,
n_number_of_bytes_to_lock_low: u32,
n_number_of_bytes_to_lock_high: u32,
lp_overlapped: *mut Overlapped,
) -> i32;
}
#[repr(C)]
struct Overlapped {
internal: usize,
internal_high: usize,
offset: u32,
offset_high: u32,
h_event: *mut std::ffi::c_void,
}
let handle = file.as_raw_handle();
let mut overlapped = Overlapped {
internal: 0,
internal_high: 0,
offset: 0,
offset_high: 0,
h_event: ptr::null_mut(),
};
#[expect(unsafe_code, reason = "LockFileEx FFI call with valid handle")]
let ret = unsafe {
LockFileEx(
handle,
LOCKFILE_EXCLUSIVE_LOCK,
0,
u32::MAX,
u32::MAX,
&raw mut overlapped,
)
};
if ret == 0 {
return Err(std::io::Error::last_os_error());
}
Ok(())
}
pub(super) fn try_lock_exclusive(file: &File) -> io::Result<bool> {
use core::ptr;
const LOCKFILE_EXCLUSIVE_LOCK: u32 = 0x0000_0002;
const LOCKFILE_FAIL_IMMEDIATELY: u32 = 0x0000_0001;
const ERROR_LOCK_VIOLATION: i32 = 33;
#[allow(non_snake_case, reason = "FFI name matches Windows API")]
unsafe extern "system" {
fn LockFileEx(
h_file: *mut std::ffi::c_void,
dw_flags: u32,
dw_reserved: u32,
n_number_of_bytes_to_lock_low: u32,
n_number_of_bytes_to_lock_high: u32,
lp_overlapped: *mut Overlapped,
) -> i32;
}
#[repr(C)]
struct Overlapped {
internal: usize,
internal_high: usize,
offset: u32,
offset_high: u32,
h_event: *mut std::ffi::c_void,
}
let handle = file.as_raw_handle();
let mut overlapped = Overlapped {
internal: 0,
internal_high: 0,
offset: 0,
offset_high: 0,
h_event: ptr::null_mut(),
};
#[expect(unsafe_code, reason = "LockFileEx FFI call with valid handle")]
let ret = unsafe {
LockFileEx(
handle,
LOCKFILE_EXCLUSIVE_LOCK | LOCKFILE_FAIL_IMMEDIATELY,
0,
u32::MAX,
u32::MAX,
&raw mut overlapped,
)
};
if ret == 0 {
let err = std::io::Error::last_os_error();
if err.raw_os_error() == Some(ERROR_LOCK_VIOLATION) {
return Ok(false);
}
return Err(err);
}
Ok(true)
}
}
#[cfg(not(any(unix, windows)))]
mod sys {
use super::FileHint;
use std::fs::File;
use std::io;
pub(super) fn lock_exclusive(_file: &File) -> io::Result<()> {
Err(io::Error::new(
io::ErrorKind::Unsupported,
"file locking is not supported on this platform",
))
}
pub(super) fn try_lock_exclusive(_file: &File) -> io::Result<bool> {
Err(io::Error::new(
io::ErrorKind::Unsupported,
"file locking is not supported on this platform",
))
}
#[expect(
clippy::unnecessary_wraps,
reason = "FsFile::hint signature requires io::Result<()>; unsupported platforms have no fallible path"
)]
pub(super) fn fadvise(_file: &File, _hint: FileHint) -> io::Result<()> {
Ok(())
}
}
#[cfg(target_os = "macos")]
mod macos_caps {
use crate::fs::FsCapabilities;
use core::mem::MaybeUninit;
use std::ffi::{CStr, CString};
use std::io;
use std::os::unix::ffi::OsStrExt;
use std::path::Path;
fn to_cstring(path: &Path) -> io::Result<CString> {
CString::new(path.as_os_str().as_bytes()).map_err(|_| {
io::Error::new(
io::ErrorKind::InvalidInput,
"path contains an interior NUL byte",
)
})
}
fn fs_type_name(path: &Path) -> io::Result<String> {
let c = to_cstring(path)?;
let mut buf = MaybeUninit::<libc::statfs>::uninit();
#[expect(unsafe_code, reason = "statfs FFI to read the filesystem type name")]
let rc = unsafe { libc::statfs(c.as_ptr(), buf.as_mut_ptr()) };
if rc != 0 {
return Err(std::io::Error::last_os_error());
}
#[expect(unsafe_code, reason = "read initialized statfs.f_fstypename")]
let name = unsafe {
let st = buf.assume_init();
CStr::from_ptr(st.f_fstypename.as_ptr())
.to_string_lossy()
.into_owned()
};
Ok(name)
}
pub(super) fn capabilities(path: &Path) -> FsCapabilities {
if matches!(fs_type_name(path).as_deref(), Ok("apfs")) {
FsCapabilities {
copy_on_write: true,
reflink: true,
native_snapshot: true,
..FsCapabilities::default()
}
} else {
FsCapabilities::default()
}
}
pub(super) fn clonefile(src: &Path, dst: &Path) -> io::Result<()> {
let s = to_cstring(src)?;
let d = to_cstring(dst)?;
#[expect(unsafe_code, reason = "clonefile FFI for an O(1) APFS clone")]
let rc = unsafe { libc::clonefile(s.as_ptr(), d.as_ptr(), 0) };
if rc != 0 {
return Err(std::io::Error::last_os_error());
}
Ok(())
}
pub(super) fn clone_should_fall_back(err: &io::Error) -> bool {
const ENOTSUP: i32 = 45;
const EXDEV: i32 = 18;
matches!(err.raw_os_error(), Some(ENOTSUP | EXDEV))
|| matches!(
err.kind(),
io::ErrorKind::Unsupported | io::ErrorKind::CrossesDevices
)
}
}
#[cfg(target_os = "linux")]
mod linux_caps {
use crate::fs::FsCapabilities;
use std::fs::{File, OpenOptions};
use std::io;
use std::os::unix::io::AsRawFd;
use std::path::Path;
#[cfg(target_pointer_width = "64")]
use core::mem::MaybeUninit;
#[cfg(target_pointer_width = "64")]
use std::ffi::CString;
#[cfg(target_pointer_width = "64")]
use std::os::unix::ffi::OsStrExt;
#[cfg(target_pointer_width = "64")]
fn to_cstring(path: &Path) -> io::Result<CString> {
CString::new(path.as_os_str().as_bytes()).map_err(|_| {
io::Error::new(
io::ErrorKind::InvalidInput,
"path contains an interior NUL byte",
)
})
}
#[cfg(target_pointer_width = "64")]
pub(super) fn capabilities(path: &Path) -> FsCapabilities {
let Ok(c) = to_cstring(path) else {
return FsCapabilities::default();
};
let mut buf = MaybeUninit::<libc::statfs>::uninit();
#[expect(unsafe_code, reason = "statfs FFI to read f_type")]
let rc = unsafe { libc::statfs(c.as_ptr(), buf.as_mut_ptr()) };
if rc != 0 {
return FsCapabilities::default();
}
#[expect(unsafe_code, reason = "read initialized statfs.f_type")]
#[allow(
clippy::unnecessary_cast,
clippy::cast_possible_wrap,
reason = "f_type is i64 on glibc (no-op cast) and u64 on musl (cast required)"
)]
let f_type = unsafe { buf.assume_init() }.f_type as i64;
crate::fs::linux_fs_type_capabilities(f_type)
}
#[cfg(target_pointer_width = "64")]
pub(super) fn punch_hole(path: &Path, offset: u64, len: u64) -> io::Result<()> {
let off = i64::try_from(offset)
.map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, "punch offset exceeds i64"))?;
let length = i64::try_from(len)
.map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, "punch len exceeds i64"))?;
let f = OpenOptions::new().write(true).open(path)?;
#[expect(
unsafe_code,
reason = "fallocate(PUNCH_HOLE) FFI for in-place extent reclaim"
)]
let rc = unsafe {
libc::fallocate(
f.as_raw_fd(),
libc::FALLOC_FL_PUNCH_HOLE | libc::FALLOC_FL_KEEP_SIZE,
off,
length,
)
};
if rc != 0 {
return Err(io::Error::last_os_error());
}
Ok(())
}
#[cfg(target_pointer_width = "64")]
pub(super) fn extent_is_hole(path: &Path, offset: u64, len: u64) -> io::Result<Option<bool>> {
if len == 0 {
return Ok(Some(false));
}
let off = i64::try_from(offset)
.map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, "offset exceeds i64"))?;
let end = offset
.checked_add(len)
.ok_or_else(|| io::Error::new(io::ErrorKind::InvalidInput, "extent end overflows"))?;
let f = File::open(path)?;
#[expect(unsafe_code, reason = "lseek(SEEK_DATA) FFI for an extent hole probe")]
let next_data = unsafe { libc::lseek(f.as_raw_fd(), off, libc::SEEK_DATA) };
if next_data < 0 {
let err = io::Error::last_os_error();
if err.raw_os_error() == Some(libc::ENXIO) {
return Ok(Some(true));
}
if matches!(err.raw_os_error(), Some(libc::EINVAL | libc::ENOTSUP)) {
return Ok(None);
}
return Err(err);
}
Ok(Some(u64::try_from(next_data).unwrap_or(u64::MAX) >= end))
}
#[cfg(target_pointer_width = "64")]
pub(super) fn extent_contains_hole(
path: &Path,
offset: u64,
len: u64,
) -> io::Result<Option<bool>> {
if len == 0 {
return Ok(Some(false));
}
let off = i64::try_from(offset)
.map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, "offset exceeds i64"))?;
let end = offset
.checked_add(len)
.ok_or_else(|| io::Error::new(io::ErrorKind::InvalidInput, "extent end overflows"))?;
let f = File::open(path)?;
let file_len = f.metadata()?.len();
#[expect(
unsafe_code,
reason = "lseek(SEEK_HOLE) FFI for a contained-hole probe"
)]
let next_hole = unsafe { libc::lseek(f.as_raw_fd(), off, libc::SEEK_HOLE) };
if next_hole < 0 {
let err = io::Error::last_os_error();
if err.raw_os_error() == Some(libc::ENXIO) {
return Ok(Some(false));
}
if matches!(err.raw_os_error(), Some(libc::EINVAL | libc::ENOTSUP)) {
return Ok(None);
}
return Err(err);
}
Ok(Some(
u64::try_from(next_hole).unwrap_or(u64::MAX) < end.min(file_len),
))
}
#[cfg(not(target_pointer_width = "64"))]
#[expect(
clippy::unnecessary_wraps,
reason = "the signature of the 64-bit probe the same caller uses"
)]
pub(super) fn extent_is_hole(
_path: &Path,
_offset: u64,
_len: u64,
) -> io::Result<Option<bool>> {
Ok(None)
}
#[cfg(not(target_pointer_width = "64"))]
#[expect(
clippy::unnecessary_wraps,
reason = "the signature of the 64-bit probe the same caller uses"
)]
pub(super) fn extent_contains_hole(
_path: &Path,
_offset: u64,
_len: u64,
) -> io::Result<Option<bool>> {
Ok(None)
}
#[cfg(not(target_pointer_width = "64"))]
pub(super) fn punch_hole(_path: &Path, _offset: u64, _len: u64) -> io::Result<()> {
Err(io::Error::new(
io::ErrorKind::Unsupported,
"punch_hole unsupported on 32-bit",
))
}
#[cfg(not(target_pointer_width = "64"))]
pub(super) fn capabilities(_path: &Path) -> FsCapabilities {
FsCapabilities::default()
}
pub(super) fn ficlone(src: &Path, dst: &Path) -> io::Result<()> {
let src_f = File::open(src)?;
let dst_f = OpenOptions::new().write(true).create_new(true).open(dst)?;
#[expect(unsafe_code, reason = "ioctl(FICLONE) for an O(1) reflink clone")]
let rc = unsafe {
libc::ioctl(
dst_f.as_raw_fd(),
libc::FICLONE as _,
src_f.as_raw_fd(),
)
};
if rc != 0 {
let err = std::io::Error::last_os_error();
drop(dst_f);
let _ = std::fs::remove_file(dst);
return Err(err);
}
Ok(())
}
pub(super) fn clone_should_fall_back(err: &io::Error) -> bool {
const EOPNOTSUPP: i32 = 95;
const EXDEV: i32 = 18;
const EINVAL: i32 = 22;
matches!(err.raw_os_error(), Some(EOPNOTSUPP | EXDEV | EINVAL))
|| matches!(
err.kind(),
io::ErrorKind::Unsupported | io::ErrorKind::CrossesDevices
)
}
#[cfg(any(
target_arch = "x86",
target_arch = "x86_64",
target_arch = "arm",
target_arch = "aarch64",
target_arch = "riscv32",
target_arch = "riscv64",
target_arch = "loongarch64",
target_arch = "s390x"
))]
pub(super) fn try_disable_cow(path: &Path) -> io::Result<()> {
const FS_IOC_GETFLAGS: libc::Ioctl = libc::FS_IOC_GETFLAGS;
const FS_IOC_SETFLAGS: libc::Ioctl = libc::FS_IOC_SETFLAGS;
const FS_NOCOW_FL: libc::c_int = 0x0080_0000;
let f = OpenOptions::new().read(true).write(true).open(path)?;
let mut flags: libc::c_int = 0;
#[expect(unsafe_code, reason = "ioctl(FS_IOC_GETFLAGS) to read inode flags")]
#[allow(
clippy::unnecessary_cast,
clippy::cast_possible_truncation,
reason = "ioctl request is c_ulong on glibc (no-op) but c_int on musl; \
the 32-bit request code's low bits are preserved by truncation"
)]
let rc = unsafe { libc::ioctl(f.as_raw_fd(), FS_IOC_GETFLAGS as _, &raw mut flags) };
if rc != 0 {
return ignore_if_unsupported(std::io::Error::last_os_error());
}
if flags & FS_NOCOW_FL != 0 {
return Ok(()); }
flags |= FS_NOCOW_FL;
#[expect(unsafe_code, reason = "ioctl(FS_IOC_SETFLAGS) to set FS_NOCOW_FL")]
#[allow(
clippy::unnecessary_cast,
clippy::cast_possible_truncation,
reason = "ioctl request is c_ulong on glibc (no-op) but c_int on musl; \
the 32-bit request code's low bits are preserved by truncation"
)]
let rc = unsafe { libc::ioctl(f.as_raw_fd(), FS_IOC_SETFLAGS as _, &raw const flags) };
if rc != 0 {
return ignore_if_unsupported(std::io::Error::last_os_error());
}
Ok(())
}
#[cfg(not(any(
target_arch = "x86",
target_arch = "x86_64",
target_arch = "arm",
target_arch = "aarch64",
target_arch = "riscv32",
target_arch = "riscv64",
target_arch = "loongarch64",
target_arch = "s390x"
)))]
pub(super) fn try_disable_cow(_path: &Path) -> io::Result<()> {
Ok(())
}
#[cfg(any(
target_arch = "x86",
target_arch = "x86_64",
target_arch = "arm",
target_arch = "aarch64",
target_arch = "riscv32",
target_arch = "riscv64",
target_arch = "loongarch64",
target_arch = "s390x"
))]
fn ignore_if_unsupported(err: io::Error) -> io::Result<()> {
const ENOTTY: i32 = 25;
const EOPNOTSUPP: i32 = 95;
const EINVAL: i32 = 22;
if matches!(err.raw_os_error(), Some(ENOTTY | EOPNOTSUPP | EINVAL))
|| matches!(err.kind(), io::ErrorKind::Unsupported)
{
Ok(())
} else {
Err(err)
}
}
}
#[cfg(windows)]
mod hard_link_count_sys {
use std::io;
use std::os::windows::io::AsRawHandle as _;
#[repr(C)]
#[expect(non_snake_case, reason = "Win32 API struct")]
#[derive(Default)]
struct ByHandleFileInformation {
dwFileAttributes: u32,
ftCreationTime: [u32; 2],
ftLastAccessTime: [u32; 2],
ftLastWriteTime: [u32; 2],
dwVolumeSerialNumber: u32,
nFileSizeHigh: u32,
nFileSizeLow: u32,
nNumberOfLinks: u32,
nFileIndexHigh: u32,
nFileIndexLow: u32,
}
#[allow(non_snake_case, reason = "Win32 API signature")]
unsafe extern "system" {
fn GetFileInformationByHandle(
hFile: *mut core::ffi::c_void,
lpFileInformation: *mut ByHandleFileInformation,
) -> i32;
}
pub(super) fn hard_link_count(file: &std::fs::File) -> io::Result<u64> {
let mut info = ByHandleFileInformation::default();
#[expect(
unsafe_code,
reason = "GetFileInformationByHandle for the NTFS link count"
)]
let rc = unsafe { GetFileInformationByHandle(file.as_raw_handle(), &raw mut info) };
if rc == 0 {
return Err(io::Error::last_os_error());
}
Ok(u64::from(info.nNumberOfLinks))
}
#[repr(C)]
#[expect(non_snake_case, reason = "Win32 API struct")]
#[derive(Default)]
struct FileIdInfo {
VolumeSerialNumber: u64,
FileId: [u8; 16],
}
const FILE_ID_INFO_CLASS: i32 = 18;
const FILE_ID_INFO_SIZE: u32 = 24;
const _: () = assert!(core::mem::size_of::<FileIdInfo>() == FILE_ID_INFO_SIZE as usize);
const FILE_FLAG_BACKUP_SEMANTICS: u32 = 0x0200_0000;
#[allow(non_snake_case, reason = "Win32 API signature")]
unsafe extern "system" {
fn GetFileInformationByHandleEx(
hFile: *mut core::ffi::c_void,
FileInformationClass: i32,
lpFileInformation: *mut core::ffi::c_void,
dwBufferSize: u32,
) -> i32;
}
pub(super) fn file_identity(path: &std::path::Path) -> io::Result<(u64, [u8; 16])> {
use std::os::windows::fs::OpenOptionsExt as _;
let file = std::fs::OpenOptions::new()
.access_mode(0)
.custom_flags(FILE_FLAG_BACKUP_SEMANTICS)
.open(path)?;
let mut info = FileIdInfo::default();
#[expect(
unsafe_code,
reason = "GetFileInformationByHandleEx for the file identity"
)]
let rc = unsafe {
GetFileInformationByHandleEx(
file.as_raw_handle(),
FILE_ID_INFO_CLASS,
(&raw mut info).cast(),
FILE_ID_INFO_SIZE,
)
};
if rc == 0 {
return Err(io::Error::last_os_error());
}
Ok((info.VolumeSerialNumber, info.FileId))
}
}
#[cfg(windows)]
mod available_space_sys {
use std::io;
use std::os::windows::ffi::OsStrExt;
use std::path::Path;
#[allow(non_snake_case, reason = "Win32 API signature")]
unsafe extern "system" {
fn GetDiskFreeSpaceExW(
lpDirectoryName: *const u16,
lpFreeBytesAvailableToCaller: *mut u64,
lpTotalNumberOfBytes: *mut u64,
lpTotalNumberOfFreeBytes: *mut u64,
) -> i32;
}
pub(super) fn disk_free_available(path: &Path) -> io::Result<u64> {
let mut wide: Vec<u16> = path.as_os_str().encode_wide().collect();
if wide.contains(&0) {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"path contains an interior NUL byte",
));
}
wide.push(0);
let mut avail: u64 = 0;
#[expect(unsafe_code, reason = "GetDiskFreeSpaceExW FFI for free space")]
let rc = unsafe {
GetDiskFreeSpaceExW(
wide.as_ptr(),
&raw mut avail,
core::ptr::null_mut(),
core::ptr::null_mut(),
)
};
if rc == 0 {
return Err(io::Error::last_os_error());
}
Ok(avail)
}
}
#[cfg(test)]
#[allow(
clippy::unwrap_used,
clippy::indexing_slicing,
clippy::useless_vec,
reason = "test code"
)]
mod tests;