use super::{FileHint, Fs, FsDirEntry, FsFile, FsMetadata, FsOpenOptions, SyncMode};
use std::fs::{File, OpenOptions};
use std::io;
use std::path::Path;
#[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(io::Error::last_os_error())
}
}
#[cfg(not(target_os = "macos"))]
fn normal_fsync(file: &File) -> io::Result<()> {
File::sync_all(file)
}
#[derive(Clone, Copy, Debug, Default)]
pub struct StdFs;
impl FsFile for File {
fn sync_all(&self) -> io::Result<()> {
Self::sync_all(self)
}
fn sync_data(&self) -> io::Result<()> {
Self::sync_data(self)
}
fn sync_all_with(&self, mode: SyncMode) -> io::Result<()> {
match mode {
SyncMode::Full => Self::sync_all(self),
SyncMode::Normal => normal_fsync(self),
}
}
fn sync_data_with(&self, mode: SyncMode) -> io::Result<()> {
match mode {
SyncMode::Full => Self::sync_data(self),
SyncMode::Normal => normal_fsync(self),
}
}
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(),
})
}
fn set_len(&self, size: u64) -> io::Result<()> {
Self::set_len(self, size)
}
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.saturating_add(filled as u64);
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() == io::ErrorKind::Interrupted => continue,
Err(e) => return Err(e),
}
}
#[cfg(windows)]
{
use std::os::windows::fs::FileExt;
match FileExt::seek_read(self, remaining, off) {
Ok(n) => n,
Err(e) if e.kind() == io::ErrorKind::Interrupted => continue,
Err(e) => return Err(e),
}
}
#[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)
}
fn hint(&self, hint: FileHint) -> io::Result<()> {
sys::fadvise(self, hint)
}
}
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::direct_io::apply_direct_io_flag(&mut builder, opts.direct_io);
let file = builder.open(path)?;
Ok(Box::new(file))
}
fn create_dir_all(&self, path: &Path) -> io::Result<()> {
std::fs::create_dir_all(path)
}
fn create_dir(&self, path: &Path) -> io::Result<()> {
std::fs::create_dir(path)
}
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<()> {
std::fs::remove_file(path)
}
fn remove_dir_all(&self, path: &Path) -> io::Result<()> {
std::fs::remove_dir_all(path)
}
fn rename(&self, from: &Path, to: &Path) -> io::Result<()> {
std::fs::rename(from, to)
}
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(),
})
}
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()
}
#[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",
));
}
match mode {
SyncMode::Full => dir.sync_all(),
SyncMode::Normal => normal_fsync(&dir),
}
}
#[cfg(target_os = "windows")]
{
let _ = (path, mode);
Ok(())
}
}
fn exists(&self, path: &Path) -> io::Result<bool> {
path.try_exists()
}
fn hard_link(&self, src: &Path, dst: &Path) -> io::Result<()> {
std::fs::hard_link(src, dst)
}
fn backend_id(&self) -> Option<u64> {
Some(KERNEL_BACKEND_ID)
}
#[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),
}
}
#[cfg(target_os = "linux")]
fn capabilities(&self, path: &Path) -> super::FsCapabilities {
linux_caps::capabilities(path)
}
#[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),
}
}
#[cfg(target_os = "linux")]
fn try_disable_cow(&self, path: &Path) -> io::Result<()> {
linux_caps::try_disable_cow(path)
}
}
pub const KERNEL_BACKEND_ID: u64 = 0x4b45_524e_454c_5f46;
#[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 {
#[cfg(unix)]
{
const EXDEV: i32 = 18;
if err.raw_os_error() == Some(EXDEV) {
return true;
}
}
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;
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 = io::Error::last_os_error();
if err.kind() == io::ErrorKind::Interrupted {
continue;
}
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(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 std::ptr;
const LOCKFILE_EXCLUSIVE_LOCK: u32 = 0x0000_0002;
#[expect(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 as *mut std::ffi::c_void,
LOCKFILE_EXCLUSIVE_LOCK,
0,
u32::MAX,
u32::MAX,
&mut overlapped,
)
};
if ret == 0 {
return Err(io::Error::last_os_error());
}
Ok(())
}
}
#[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",
))
}
#[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 std::ffi::{CStr, CString};
use std::io;
use std::mem::MaybeUninit;
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(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(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::ffi::CString;
use std::fs::{File, OpenOptions};
use std::io;
use std::mem::MaybeUninit;
use std::os::unix::ffi::OsStrExt;
use std::os::unix::io::AsRawFd;
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",
)
})
}
#[cfg(target_pointer_width = "64")]
pub(super) fn capabilities(path: &Path) -> FsCapabilities {
const BTRFS: i64 = 0x9123_683E;
const ZFS: i64 = 0x2FC1_2FC1;
const XFS: i64 = 0x5846_5342;
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")]
let f_type: i64 = unsafe { buf.assume_init() }.f_type;
match f_type {
BTRFS | ZFS => FsCapabilities {
per_block_integrity_on_read: true,
background_scrub: true,
copy_on_write: true,
reflink: true,
native_snapshot: true,
},
XFS => FsCapabilities {
copy_on_write: true,
reflink: true,
..FsCapabilities::default()
},
_ => FsCapabilities::default(),
}
}
#[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 libc::c_ulong,
src_f.as_raw_fd(),
)
};
if rc != 0 {
let err = 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 = "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::c_ulong = 0x8008_6601;
const FS_IOC_SETFLAGS: libc::c_ulong = 0x4008_6602;
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")]
let rc = unsafe { libc::ioctl(f.as_raw_fd(), FS_IOC_GETFLAGS, &raw mut flags) };
if rc != 0 {
return ignore_if_unsupported(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")]
let rc = unsafe { libc::ioctl(f.as_raw_fd(), FS_IOC_SETFLAGS, &raw const flags) };
if rc != 0 {
return ignore_if_unsupported(io::Error::last_os_error());
}
Ok(())
}
#[cfg(not(any(
target_arch = "x86",
target_arch = "x86_64",
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 = "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(test)]
#[allow(
clippy::unwrap_used,
clippy::indexing_slicing,
clippy::useless_vec,
reason = "test code"
)]
mod tests {
use super::*;
use std::io::{Read, Write};
use std::sync::Arc;
use test_log::test;
#[test]
fn std_fs_create_read_write() -> io::Result<()> {
let dir = tempfile::tempdir()?;
let fs = StdFs;
let path = dir.path().join("test.txt");
let opts = FsOpenOptions::new().write(true).create(true);
let mut file = fs.open(&path, &opts)?;
file.write_all(b"hello world")?;
file.sync_all()?;
drop(file);
let opts = FsOpenOptions::new().read(true);
let mut file = fs.open(&path, &opts)?;
let mut buf = String::new();
file.read_to_string(&mut buf)?;
assert_eq!(buf, "hello world");
Ok(())
}
#[test]
fn std_fs_sync_with_both_modes_persists() -> io::Result<()> {
let dir = tempfile::tempdir()?;
let fs = StdFs;
for (name, mode) in [("normal", SyncMode::Normal), ("full", SyncMode::Full)] {
let path = dir.path().join(name);
let mut file = fs.open(&path, &FsOpenOptions::new().write(true).create(true))?;
file.write_all(b"durable")?;
file.sync_data_with(mode)?;
file.sync_all_with(mode)?;
drop(file);
fs.sync_directory_with(dir.path(), mode)?;
let mut file = fs.open(&path, &FsOpenOptions::new().read(true))?;
let mut buf = String::new();
file.read_to_string(&mut buf)?;
assert_eq!(buf, "durable", "{name} mode lost data");
}
Ok(())
}
#[test]
fn std_fs_directory_operations() -> io::Result<()> {
let dir = tempfile::tempdir()?;
let fs = StdFs;
let nested = dir.path().join("a").join("b").join("c");
fs.create_dir_all(&nested)?;
assert!(fs.exists(&nested)?);
let file_path = nested.join("data.bin");
let opts = FsOpenOptions::new().write(true).create_new(true);
let mut file = fs.open(&file_path, &opts)?;
file.write_all(b"data")?;
drop(file);
let entries = fs.read_dir(&nested)?;
assert_eq!(entries.len(), 1);
assert_eq!(entries[0].file_name, "data.bin");
assert!(!entries[0].is_dir);
let meta = fs.metadata(&file_path)?;
assert!(meta.is_file);
assert!(!meta.is_dir);
assert_eq!(meta.len, 4);
fs.remove_file(&file_path)?;
assert!(!fs.exists(&file_path)?);
let top = dir.path().join("a");
fs.remove_dir_all(&top)?;
assert!(!fs.exists(&top)?);
Ok(())
}
#[test]
fn std_fs_rename() -> io::Result<()> {
let dir = tempfile::tempdir()?;
let fs = StdFs;
let src = dir.path().join("src.txt");
let dst = dir.path().join("dst.txt");
let opts = FsOpenOptions::new().write(true).create(true);
let mut file = fs.open(&src, &opts)?;
file.write_all(b"content")?;
drop(file);
fs.rename(&src, &dst)?;
assert!(!fs.exists(&src)?);
assert!(fs.exists(&dst)?);
Ok(())
}
#[test]
fn std_fs_sync_directory() -> io::Result<()> {
let dir = tempfile::tempdir()?;
let fs = StdFs;
fs.sync_directory(dir.path())?;
Ok(())
}
#[test]
fn fs_file_metadata() -> io::Result<()> {
let dir = tempfile::tempdir()?;
let fs = StdFs;
let path = dir.path().join("meta.bin");
let opts = FsOpenOptions::new().write(true).create(true).read(true);
let mut file = fs.open(&path, &opts)?;
file.write_all(b"12345")?;
let meta = file.metadata()?;
assert!(meta.is_file);
assert_eq!(meta.len, 5);
Ok(())
}
#[test]
fn fs_file_set_len() -> io::Result<()> {
let dir = tempfile::tempdir()?;
let fs = StdFs;
let path = dir.path().join("truncate.bin");
let opts = FsOpenOptions::new().write(true).create(true).read(true);
let mut file = fs.open(&path, &opts)?;
file.write_all(b"hello world")?;
file.set_len(5)?;
let meta = file.metadata()?;
assert_eq!(meta.len, 5);
Ok(())
}
#[test]
#[cfg(any(unix, windows))]
fn fs_file_lock_exclusive() -> io::Result<()> {
let dir = tempfile::tempdir()?;
let fs = StdFs;
let path = dir.path().join("lockfile");
let opts = FsOpenOptions::new().write(true).create(true);
let file = fs.open(&path, &opts)?;
file.lock_exclusive()?;
Ok(())
}
#[test]
#[cfg(any(unix, windows))]
fn fs_file_read_at() -> io::Result<()> {
let dir = tempfile::tempdir()?;
let fs = StdFs;
let path = dir.path().join("pread.bin");
let opts = FsOpenOptions::new().write(true).create(true).read(true);
let mut file = fs.open(&path, &opts)?;
file.write_all(b"hello world")?;
let mut buf = [0u8; 5];
let n = file.read_at(&mut buf, 6)?;
assert_eq!(n, 5);
assert_eq!(&buf, b"world");
let n = file.read_at(&mut buf, 0)?;
assert_eq!(n, 5);
assert_eq!(&buf, b"hello");
Ok(())
}
#[test]
fn fs_open_options_default() {
let opts = FsOpenOptions::default();
assert!(!opts.read);
assert!(!opts.write);
assert!(!opts.create);
assert!(!opts.create_new);
assert!(!opts.truncate);
assert!(!opts.append);
assert!(!opts.direct_io);
}
#[test]
fn fs_open_options_builders() {
let opts = FsOpenOptions::new()
.read(true)
.write(true)
.create(true)
.create_new(false)
.truncate(true)
.append(false)
.direct_io(true);
assert!(opts.read);
assert!(opts.write);
assert!(opts.create);
assert!(!opts.create_new);
assert!(opts.truncate);
assert!(!opts.append);
assert!(opts.direct_io);
}
#[test]
fn fs_file_sync_data() -> io::Result<()> {
let dir = tempfile::tempdir()?;
let fs = StdFs;
let path = dir.path().join("sync_data.bin");
let opts = FsOpenOptions::new().write(true).create(true);
let mut file = fs.open(&path, &opts)?;
file.write_all(b"data")?;
file.sync_data()?;
Ok(())
}
#[test]
fn fs_open_truncate_and_append() -> io::Result<()> {
let dir = tempfile::tempdir()?;
let fs = StdFs;
let path = dir.path().join("trunc.txt");
let opts = FsOpenOptions::new().write(true).create(true);
let mut file = fs.open(&path, &opts)?;
file.write_all(b"hello world")?;
drop(file);
let opts = FsOpenOptions::new().write(true).truncate(true);
let mut file = fs.open(&path, &opts)?;
file.write_all(b"hi")?;
drop(file);
let meta = fs.metadata(&path)?;
assert_eq!(meta.len, 2);
let opts = FsOpenOptions::new().write(true).append(true);
let mut file = fs.open(&path, &opts)?;
file.write_all(b"!")?;
drop(file);
let meta = fs.metadata(&path)?;
assert_eq!(meta.len, 3);
Ok(())
}
#[test]
fn fs_dir_entry_fields() -> io::Result<()> {
let dir = tempfile::tempdir()?;
let fs = StdFs;
let sub = dir.path().join("subdir");
fs.create_dir_all(&sub)?;
let file_path = dir.path().join("file.txt");
let opts = FsOpenOptions::new().write(true).create(true);
fs.open(&file_path, &opts)?;
let mut entries = fs.read_dir(dir.path())?;
entries.sort_by(|a, b| a.file_name.cmp(&b.file_name));
assert_eq!(entries.len(), 2);
assert_eq!(entries[0].file_name, "file.txt");
assert!(!entries[0].is_dir);
assert_eq!(entries[1].file_name, "subdir");
assert!(entries[1].is_dir);
Ok(())
}
#[test]
fn fs_metadata_directory() -> io::Result<()> {
let dir = tempfile::tempdir()?;
let fs = StdFs;
let meta = fs.metadata(dir.path())?;
assert!(meta.is_dir);
assert!(!meta.is_file);
Ok(())
}
#[test]
#[cfg(target_os = "linux")]
fn read_dir_rejects_non_utf8_filename() -> io::Result<()> {
use std::ffi::OsStr;
use std::os::unix::ffi::OsStrExt;
let dir = tempfile::tempdir()?;
let bad_name = OsStr::from_bytes(&[0xff, 0xfe]);
let bad_path = dir.path().join(bad_name);
if std::fs::write(&bad_path, b"data").is_err() {
return Ok(());
}
let fs = StdFs;
match fs.read_dir(dir.path()) {
Err(err) => {
assert_eq!(err.kind(), io::ErrorKind::InvalidData);
let msg = err.to_string();
assert!(
msg.contains("non-UTF-8 filename"),
"unexpected error: {msg}"
);
assert!(
msg.contains(&dir.path().display().to_string()),
"error should include directory path: {msg}",
);
}
Ok(_) => panic!("read_dir should fail on non-UTF-8 filename"),
}
Ok(())
}
#[test]
fn object_safety() -> io::Result<()> {
let fs: Arc<dyn Fs> = Arc::new(StdFs);
let dir = tempfile::tempdir()?;
let bogus = dir.path().join("nonexistent");
assert!(!fs.exists(&bogus)?);
Ok(())
}
#[test]
fn hard_link_creates_second_path_to_same_inode() -> io::Result<()> {
let dir = tempfile::tempdir()?;
let fs = StdFs;
let src = dir.path().join("src.bin");
let dst = dir.path().join("dst.bin");
std::fs::write(&src, b"checkpoint payload")?;
fs.hard_link(&src, &dst)?;
assert_eq!(std::fs::read(&dst)?, b"checkpoint payload");
#[cfg(unix)]
{
use std::os::unix::fs::MetadataExt;
let src_meta = std::fs::metadata(&src)?;
let dst_meta = std::fs::metadata(&dst)?;
assert_eq!(src_meta.ino(), dst_meta.ino());
assert_eq!(src_meta.dev(), dst_meta.dev());
assert_eq!(dst_meta.nlink(), 2);
}
fs.remove_file(&src)?;
assert_eq!(std::fs::read(&dst)?, b"checkpoint payload");
Ok(())
}
#[test]
fn hard_link_rejects_existing_destination() -> io::Result<()> {
let dir = tempfile::tempdir()?;
let fs = StdFs;
let src = dir.path().join("src");
let dst = dir.path().join("dst");
std::fs::write(&src, b"src")?;
std::fs::write(&dst, b"dst")?;
let err = fs.hard_link(&src, &dst).unwrap_err();
assert_eq!(err.kind(), io::ErrorKind::AlreadyExists);
Ok(())
}
#[test]
fn hard_link_rejects_missing_source() -> io::Result<()> {
let dir = tempfile::tempdir()?;
let fs = StdFs;
let err = fs
.hard_link(&dir.path().join("missing"), &dir.path().join("dst"))
.unwrap_err();
assert_eq!(err.kind(), io::ErrorKind::NotFound);
Ok(())
}
#[test]
fn is_cross_device_detects_exdev_and_kind_variants() {
#[cfg(unix)]
{
const EXDEV: i32 = 18;
let exdev = io::Error::from_raw_os_error(EXDEV);
assert!(is_cross_device(&exdev), "raw EXDEV must be recognised");
}
let crosses = io::Error::from(io::ErrorKind::CrossesDevices);
assert!(is_cross_device(&crosses));
let unsupported = io::Error::from(io::ErrorKind::Unsupported);
assert!(is_cross_device(&unsupported));
let notfound = io::Error::from(io::ErrorKind::NotFound);
assert!(!is_cross_device(¬found));
}
#[test]
fn fadvise_accepts_every_hint_variant() -> io::Result<()> {
let dir = tempfile::tempdir()?;
let fs = StdFs;
let path = dir.path().join("hint_smoke.bin");
let opts = FsOpenOptions::new().write(true).create_new(true);
let mut file = fs.open(&path, &opts)?;
file.write_all(&vec![0u8; 64 * 1024])?;
file.sync_all()?;
drop(file);
let opts = FsOpenOptions::new().read(true);
let file = fs.open(&path, &opts)?;
for hint in [
FileHint::Default,
FileHint::Sequential,
FileHint::Random,
FileHint::WriteOnce,
] {
file.hint(hint)?;
}
Ok(())
}
#[cfg(target_os = "macos")]
#[test]
fn capabilities_macos_is_apfs_profile_or_conservative() {
use crate::fs::FsCapabilities;
let dir = tempfile::tempdir().unwrap();
let caps = StdFs.capabilities(dir.path());
let apfs = FsCapabilities {
copy_on_write: true,
reflink: true,
native_snapshot: true,
..FsCapabilities::default()
};
assert!(
caps == apfs || caps == FsCapabilities::default(),
"unexpected macOS capability profile: {caps:?}"
);
}
#[cfg(target_os = "macos")]
#[test]
fn reflink_file_macos_produces_independent_identical_copy() -> io::Result<()> {
let dir = tempfile::tempdir()?;
let src = dir.path().join("src.bin");
let dst = dir.path().join("clone.bin");
let fs = StdFs;
let mut f = fs.open(&src, &FsOpenOptions::new().write(true).create_new(true))?;
f.write_all(b"reflink-source-data")?;
f.sync_all()?;
drop(f);
fs.reflink_file(&src, &dst)?;
let mut buf = Vec::new();
fs.open(&dst, &FsOpenOptions::new().read(true))?
.read_to_end(&mut buf)?;
assert_eq!(buf, b"reflink-source-data");
let mut w = fs.open(&src, &FsOpenOptions::new().write(true).truncate(true))?;
w.write_all(b"changed")?;
w.sync_all()?;
drop(w);
let mut after = Vec::new();
fs.open(&dst, &FsOpenOptions::new().read(true))?
.read_to_end(&mut after)?;
assert_eq!(
after, b"reflink-source-data",
"reflink clone must be independent"
);
Ok(())
}
#[cfg(target_os = "linux")]
#[test]
fn reflink_file_linux_produces_independent_identical_copy() -> io::Result<()> {
let dir = tempfile::tempdir()?;
let src = dir.path().join("src.bin");
let dst = dir.path().join("clone.bin");
let fs = StdFs;
let mut f = fs.open(&src, &FsOpenOptions::new().write(true).create_new(true))?;
f.write_all(b"reflink-source-data")?;
f.sync_all()?;
drop(f);
fs.reflink_file(&src, &dst)?;
let mut buf = Vec::new();
fs.open(&dst, &FsOpenOptions::new().read(true))?
.read_to_end(&mut buf)?;
assert_eq!(buf, b"reflink-source-data");
let mut w = fs.open(&src, &FsOpenOptions::new().write(true).truncate(true))?;
w.write_all(b"changed")?;
w.sync_all()?;
drop(w);
let mut after = Vec::new();
fs.open(&dst, &FsOpenOptions::new().read(true))?
.read_to_end(&mut after)?;
assert_eq!(
after, b"reflink-source-data",
"reflink clone must be independent"
);
Ok(())
}
#[cfg(target_os = "linux")]
#[test]
fn try_disable_cow_linux_succeeds_or_noops() -> io::Result<()> {
let dir = tempfile::tempdir()?;
let path = dir.path().join("sst.bin");
let fs = StdFs;
fs.open(&path, &FsOpenOptions::new().write(true).create_new(true))?;
fs.try_disable_cow(&path)?;
Ok(())
}
}