#![cfg(target_os = "macos")]
use crate::{Error, Result};
use std::fs::{File, OpenOptions};
use std::io::Read;
use std::os::unix::io::{AsRawFd, FromRawFd};
use std::path::Path;
pub(crate) fn open_write_new(path: &Path, use_direct: bool) -> Result<(File, bool)> {
let path_cstr = path_to_cstr(path)?;
let flags = libc::O_WRONLY | libc::O_CREAT | libc::O_EXCL | libc::O_CLOEXEC;
let fd = unsafe { libc::open(path_cstr.as_ptr(), flags, 0o600_i32) };
if fd < 0 {
return Err(Error::Io(std::io::Error::last_os_error()));
}
if use_direct {
let ret = unsafe { libc::fcntl(fd, libc::F_NOCACHE, 1_i32) };
if ret < 0 {
let file = unsafe { File::from_raw_fd(fd) };
return Ok((file, false));
}
}
Ok((unsafe { File::from_raw_fd(fd) }, use_direct))
}
pub(crate) fn open_read(path: &Path, use_direct: bool) -> Result<(File, bool)> {
let path_cstr = path_to_cstr(path)?;
let flags = libc::O_RDONLY | libc::O_CLOEXEC;
let fd = unsafe { libc::open(path_cstr.as_ptr(), flags, 0) };
if fd < 0 {
return Err(Error::Io(std::io::Error::last_os_error()));
}
if use_direct {
let ret = unsafe { libc::fcntl(fd, libc::F_NOCACHE, 1_i32) };
if ret < 0 {
let file = unsafe { File::from_raw_fd(fd) };
return Ok((file, false));
}
}
Ok((unsafe { File::from_raw_fd(fd) }, use_direct))
}
pub(crate) fn open_append(path: &Path) -> Result<File> {
OpenOptions::new()
.append(true)
.create(true)
.open(path)
.map_err(Error::Io)
}
pub(crate) fn open_write_at(path: &Path) -> Result<File> {
OpenOptions::new()
.write(true)
.create(true)
.open(path)
.map_err(Error::Io)
}
pub(crate) fn write_all(file: &File, data: &[u8]) -> Result<()> {
let fd = file.as_raw_fd();
let mut written = 0usize;
while written < data.len() {
let n = unsafe {
libc::write(
fd,
data[written..].as_ptr().cast::<libc::c_void>(),
data.len() - written,
)
};
if n < 0 {
let err = std::io::Error::last_os_error();
if err.kind() == std::io::ErrorKind::Interrupted {
continue;
}
return Err(Error::Io(err));
}
written += n as usize;
}
Ok(())
}
pub(crate) fn write_all_direct(file: &File, data: &[u8], sector_size: u32) -> Result<()> {
use super::{round_up, AlignedBuf};
if data.is_empty() {
return Ok(());
}
let ss = sector_size as usize;
let aligned_len = round_up(data.len(), ss);
let mut buf = AlignedBuf::new(aligned_len, ss)?;
buf.as_mut_slice()[..data.len()].copy_from_slice(data);
let fd = file.as_raw_fd();
let base = buf.as_slice().as_ptr();
let mut written = 0usize;
while written < aligned_len {
let n = unsafe {
libc::pwrite(
fd,
base.add(written).cast::<libc::c_void>(),
aligned_len - written,
written as libc::off_t,
)
};
if n < 0 {
let err = std::io::Error::last_os_error();
if err.kind() == std::io::ErrorKind::Interrupted {
continue;
}
return Err(Error::Io(err));
}
if n == 0 {
return Err(Error::Io(std::io::Error::other(
"pwrite returned 0 in write_all_direct (no progress)",
)));
}
written += n as usize;
}
Ok(())
}
pub(crate) fn write_at(file: &File, offset: u64, data: &[u8]) -> Result<()> {
let fd = file.as_raw_fd();
let mut written = 0usize;
while written < data.len() {
let off = (offset as i64).checked_add(written as i64).ok_or_else(|| {
Error::Io(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
"write_at: offset overflow",
))
})?;
let n = unsafe {
libc::pwrite(
fd,
data[written..].as_ptr().cast::<libc::c_void>(),
data.len() - written,
off as libc::off_t,
)
};
if n < 0 {
let err = std::io::Error::last_os_error();
if err.kind() == std::io::ErrorKind::Interrupted {
continue;
}
return Err(Error::Io(err));
}
if n == 0 {
return Err(Error::Io(std::io::Error::new(
std::io::ErrorKind::WriteZero,
"pwrite returned 0 in write_at (no progress)",
)));
}
written += n as usize;
}
Ok(())
}
pub(crate) fn write_at_direct(file: &File, offset: u64, data: &[u8]) -> Result<()> {
write_at(file, offset, data)
}
pub(crate) fn read_all(file: &File) -> Result<Vec<u8>> {
let mut buf = Vec::new();
let _ = (&*file).read_to_end(&mut buf).map_err(Error::Io)?;
Ok(buf)
}
pub(crate) fn read_all_direct(file: &File, file_size: u64, sector_size: u32) -> Result<Vec<u8>> {
use super::{round_up, AlignedBuf};
if file_size == 0 {
return Ok(Vec::new());
}
let ss = sector_size as usize;
let aligned_len = round_up(file_size as usize, ss);
let mut buf = AlignedBuf::new(aligned_len, ss)?;
let fd = file.as_raw_fd();
let mut total = 0usize;
while total < aligned_len {
let n = unsafe {
libc::pread(
fd,
buf.as_mut_slice()[total..]
.as_mut_ptr()
.cast::<libc::c_void>(),
aligned_len - total,
total as libc::off_t,
)
};
if n < 0 {
let err = std::io::Error::last_os_error();
if err.kind() == std::io::ErrorKind::Interrupted {
continue;
}
return Err(Error::Io(err));
}
if n == 0 {
break;
}
total += n as usize;
}
let trimmed = usize::min(total, file_size as usize);
Ok(buf.as_slice()[..trimmed].to_vec())
}
pub(crate) fn read_range(file: &File, offset: u64, len: usize) -> Result<Vec<u8>> {
let fd = file.as_raw_fd();
let mut buf = vec![0u8; len];
let mut total_read = 0usize;
while total_read < len {
let off = (offset as i64)
.checked_add(total_read as i64)
.ok_or_else(|| {
Error::Io(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
"read_range: offset overflow",
))
})?;
let n = unsafe {
libc::pread(
fd,
buf[total_read..].as_mut_ptr().cast::<libc::c_void>(),
len - total_read,
off as libc::off_t,
)
};
if n < 0 {
let err = std::io::Error::last_os_error();
if err.kind() == std::io::ErrorKind::Interrupted {
continue;
}
return Err(Error::Io(err));
}
if n == 0 {
buf.truncate(total_read);
break;
}
total_read += n as usize;
}
buf.truncate(total_read);
Ok(buf)
}
pub(crate) fn sync_data(file: &File) -> Result<()> {
sync_full(file)
}
pub(crate) fn sync_full(file: &File) -> Result<()> {
let fd = file.as_raw_fd();
let ret = unsafe { libc::fcntl(fd, libc::F_FULLFSYNC, 0_i32) };
if ret == 0 {
Ok(())
} else {
Err(Error::Io(std::io::Error::last_os_error()))
}
}
pub(crate) fn sync_barrier(file: &File) -> Result<()> {
let fd = file.as_raw_fd();
let ret = unsafe { libc::fcntl(fd, libc::F_BARRIERFSYNC, 0_i32) };
if ret == 0 {
Ok(())
} else {
Err(Error::Io(std::io::Error::last_os_error()))
}
}
pub(crate) fn punch_hole(file: &File, offset: u64, len: u64) -> Result<()> {
if len == 0 {
return Ok(());
}
#[repr(C)]
#[derive(Default)]
struct FPunchHole {
fp_flags: u32, reserved: u32, fp_offset: u64, fp_length: u64, }
const F_PUNCHHOLE: libc::c_int = 99;
let payload = FPunchHole {
fp_flags: 0,
reserved: 0,
fp_offset: offset,
fp_length: len,
};
let fd = file.as_raw_fd();
let ret = unsafe { libc::fcntl(fd, F_PUNCHHOLE, &payload as *const FPunchHole) };
if ret == 0 {
Ok(())
} else {
Err(Error::Io(std::io::Error::last_os_error()))
}
}
pub(crate) fn atomic_rename(from: &Path, to: &Path) -> Result<()> {
std::fs::rename(from, to).map_err(Error::Io)
}
pub(crate) fn sync_parent_dir(path: &Path) -> Result<()> {
let parent = path.parent().unwrap_or_else(|| Path::new("."));
let dir = File::open(parent).map_err(Error::Io)?;
let fd = dir.as_raw_fd();
let ret = unsafe { libc::fcntl(fd, libc::F_FULLFSYNC, 0_i32) };
if ret == 0 {
Ok(())
} else {
Err(Error::Io(std::io::Error::last_os_error()))
}
}
pub(crate) fn copy_file(src: &Path, dst: &Path) -> Result<u64> {
if let (Ok(src_cstr), Ok(dst_cstr)) = (path_to_cstr(src), path_to_cstr(dst)) {
let rc = unsafe { libc::clonefile(src_cstr.as_ptr(), dst_cstr.as_ptr(), 0) };
if rc == 0 {
return std::fs::metadata(src).map(|m| m.len()).map_err(Error::Io);
}
}
std::fs::copy(src, dst).map_err(Error::Io)
}
pub(crate) fn preallocate(file: &File, offset: u64, len: u64) -> Result<()> {
if len == 0 {
return Ok(());
}
#[repr(C)]
struct Fstore {
fst_flags: u32,
fst_posmode: i32,
fst_offset: libc::off_t,
fst_length: libc::off_t,
fst_bytesalloc: libc::off_t,
}
const F_PREALLOCATE: libc::c_int = 42;
const F_ALLOCATECONTIG: u32 = 0x0000_0002;
const F_ALLOCATEALL: u32 = 0x0000_0004;
const F_PEOFPOSMODE: i32 = 3;
let fd = file.as_raw_fd();
let total_to_reserve = offset.saturating_add(len) as libc::off_t;
let mut store = Fstore {
fst_flags: F_ALLOCATECONTIG | F_ALLOCATEALL,
fst_posmode: F_PEOFPOSMODE,
fst_offset: 0,
fst_length: total_to_reserve,
fst_bytesalloc: 0,
};
let ret = unsafe { libc::fcntl(fd, F_PREALLOCATE, &mut store) };
if ret == 0 {
return Ok(());
}
store.fst_flags = F_ALLOCATEALL;
store.fst_bytesalloc = 0;
let ret = unsafe { libc::fcntl(fd, F_PREALLOCATE, &mut store) };
if ret == 0 {
Ok(())
} else {
Err(Error::Io(std::io::Error::last_os_error()))
}
}
pub(crate) fn advise(file: &File, offset: u64, len: u64, advice: crate::Advice) -> Result<()> {
let fd = file.as_raw_fd();
match advice {
crate::Advice::Sequential | crate::Advice::WillNeed => {
#[repr(C)]
struct Radvisory {
ra_offset: libc::off_t,
ra_count: libc::c_int,
}
const F_RDADVISE: libc::c_int = 44;
let count = if len == 0 || len > i32::MAX as u64 {
i32::MAX
} else {
len as i32
};
let mut adv = Radvisory {
ra_offset: offset as libc::off_t,
ra_count: count,
};
let ret = unsafe { libc::fcntl(fd, F_RDADVISE, &mut adv) };
if ret == 0 {
Ok(())
} else {
Ok(())
}
}
crate::Advice::DontNeed => {
let _ = (fd, offset, len);
Ok(())
}
crate::Advice::Random | crate::Advice::Normal => Ok(()),
}
}
pub(crate) fn probe_sector_size(path: &Path) -> u32 {
use libc::statfs;
let path_cstr = match path_to_cstr(path) {
Ok(c) => c,
Err(_) => return 512,
};
let mut st: statfs = unsafe { std::mem::zeroed() };
let ret = unsafe { libc::statfs(path_cstr.as_ptr(), &mut st) };
if ret == 0 && st.f_bsize > 0 {
let bs = st.f_bsize as u64;
if bs >= 512 && bs <= 65536 {
return bs as u32;
}
}
512
}
pub(crate) fn probe_direct_io_available() -> bool {
true
}
fn path_to_cstr(path: &Path) -> Result<std::ffi::CString> {
use std::os::unix::ffi::OsStrExt;
std::ffi::CString::new(path.as_os_str().as_bytes()).map_err(|_| Error::InvalidPath {
path: path.to_owned(),
reason: "path contains an interior NUL byte".into(),
})
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::atomic::{AtomicU64, Ordering};
static COUNTER: AtomicU64 = AtomicU64::new(0);
fn tmp_path(suffix: &str) -> std::path::PathBuf {
let n = COUNTER.fetch_add(1, Ordering::Relaxed);
std::env::temp_dir().join(format!(
"fsys_macos_{}_{}_{}",
std::process::id(),
n,
suffix
))
}
struct TmpFile(std::path::PathBuf);
impl Drop for TmpFile {
fn drop(&mut self) {
let _ = std::fs::remove_file(&self.0);
}
}
#[test]
fn test_open_write_new_creates_file() {
let path = tmp_path("create");
let _g = TmpFile(path.clone());
let (f, _) = open_write_new(&path, false).expect("open");
drop(f);
assert!(path.exists());
}
#[test]
fn test_write_read_roundtrip() {
let path = tmp_path("rw");
let _g = TmpFile(path.clone());
let (f, _) = open_write_new(&path, false).expect("open");
write_all(&f, b"macos").expect("write");
drop(f);
let (rf, _) = open_read(&path, false).expect("read open");
let data = read_all(&rf).expect("read");
assert_eq!(data, b"macos");
}
#[test]
fn test_sync_full_does_not_panic() {
let path = tmp_path("sync");
let _g = TmpFile(path.clone());
let (f, _) = open_write_new(&path, false).expect("open");
write_all(&f, b"sync test").expect("write");
sync_full(&f).expect("sync_full");
}
#[test]
fn test_atomic_rename_works() {
let src = tmp_path("ren_src");
let dst = tmp_path("ren_dst");
let _gs = TmpFile(src.clone());
let _gd = TmpFile(dst.clone());
std::fs::write(&src, b"new content").expect("write");
atomic_rename(&src, &dst).expect("rename");
assert!(!src.exists());
assert_eq!(std::fs::read(&dst).expect("read"), b"new content");
}
#[test]
fn test_probe_sector_size_returns_at_least_512() {
let size = probe_sector_size(Path::new("/tmp"));
assert!(size >= 512);
}
#[test]
fn test_copy_file_produces_correct_content() {
let src = tmp_path("cp_src");
let dst = tmp_path("cp_dst");
let _gs = TmpFile(src.clone());
let _gd = TmpFile(dst.clone());
std::fs::write(&src, b"copy content").expect("write");
let bytes = copy_file(&src, &dst).expect("copy");
assert_eq!(bytes, 12);
}
}