use super::{new_err, new_err_default, FFId, FFileErr};
use crate::{error::FrozenRes, hints};
use libc::{
access, c_int, c_uint, c_void, close, flock, fstat, ftruncate, iovec, off_t, open, pread, preadv, pwrite, pwritev,
size_t, stat, strerror, sysconf, unlink, EACCES, EAGAIN, EBADF, EFAULT, EINTR, EINVAL, EIO, EISDIR, EMSGSIZE,
ENOENT, ENOLCK, ENOSPC, ENOTDIR, EOPNOTSUPP, EPERM, EROFS, ESPIPE, EWOULDBLOCK, F_OK, LOCK_EX, LOCK_NB, O_CLOEXEC,
O_CREAT, O_DIRECTORY, O_RDONLY, O_RDWR, S_IRUSR, S_IWUSR, _SC_IOV_MAX,
};
use std::{
ffi::CStr,
sync::{atomic, OnceLock},
};
static IOV_MAX_CACHE: OnceLock<usize> = OnceLock::new();
pub(in crate::ffile) const CLOSED_FD: FFId = FFId::MIN;
const MAX_RETRIES: usize = 0x0A;
const MAX_IOVECS: usize = 0x200;
#[derive(Debug)]
pub(super) struct POSIXFile(atomic::AtomicI32);
unsafe impl Send for POSIXFile {}
unsafe impl Sync for POSIXFile {}
impl POSIXFile {
#[inline]
pub(super) fn fd(&self) -> FFId {
self.0.load(atomic::Ordering::Acquire)
}
pub(super) unsafe fn exists(path: &std::path::Path) -> FrozenRes<bool> {
let cpath = path_to_cstring(path)?;
Ok(access(cpath.as_ptr(), F_OK) == 0)
}
pub(super) unsafe fn new(path: &std::path::Path) -> FrozenRes<Self> {
let fd = open_raw(path, prep_flags())?;
Ok(Self(atomic::AtomicI32::new(fd)))
}
pub(super) unsafe fn flock(&self) -> FrozenRes<()> {
flock_raw(self.fd())
}
pub(super) unsafe fn close(&self) -> FrozenRes<()> {
let fd = self.0.swap(CLOSED_FD, atomic::Ordering::AcqRel);
if fd == CLOSED_FD {
return Ok(());
}
close_raw(fd)
}
pub(super) unsafe fn unlink(&self, path: &std::path::Path) -> FrozenRes<()> {
let cpath = path_to_cstring(path)?;
self.close()?;
if unlink(cpath.as_ptr()) == 0 {
return sync_parent_dir(path);
}
let errno = last_errno();
let err_msg = err_msg(errno);
match errno {
ENOENT | ENOTDIR => new_err(FFileErr::Inv, err_msg),
EACCES | EPERM | EROFS => new_err(FFileErr::Prm, err_msg),
EIO => new_err(FFileErr::Hcf, err_msg),
_ => new_err(FFileErr::Unk, err_msg),
}
}
pub(super) unsafe fn length(&self) -> FrozenRes<usize> {
let mut st = core::mem::zeroed::<stat>();
let res = fstat(self.fd(), &mut st);
if res != 0 {
let errno = last_errno();
let err_msg = err_msg(errno);
if errno == EBADF || errno == EFAULT {
return new_err(FFileErr::Hcf, err_msg);
}
return new_err(FFileErr::Unk, err_msg);
}
Ok(st.st_size as usize)
}
pub(super) unsafe fn grow(&self, curr_len: usize, len_to_add: usize) -> FrozenRes<()> {
let fd = self.fd();
#[cfg(target_os = "linux")]
fallocate_raw(fd, curr_len, len_to_add)?;
ftruncate_raw(fd, curr_len, len_to_add)?;
#[cfg(target_os = "macos")]
f_preallocate_raw(fd, curr_len, len_to_add)?;
Ok(())
}
#[cfg(target_os = "macos")]
pub(super) unsafe fn sync(&self) -> FrozenRes<()> {
f_fullsync_raw(self.fd())
}
#[cfg(target_os = "linux")]
pub(super) unsafe fn sync(&self) -> FrozenRes<()> {
fdatasync_raw(self.fd())
}
#[cfg(target_os = "linux")]
pub(super) unsafe fn sync_range(&self, offset: usize, len: usize) -> FrozenRes<()> {
sync_file_range_raw(self.fd(), offset, len)
}
#[inline(always)]
pub(super) unsafe fn pread(&self, ptr: *mut u8, offset: usize, chunk_size: usize) -> FrozenRes<()> {
let fd = self.fd();
let mut read = 0usize;
while read < chunk_size {
let res = pread(
fd,
ptr.add(read) as *mut c_void,
(chunk_size - read) as size_t,
(offset + read) as off_t,
);
if res == 0 {
return new_err_default(FFileErr::Hcf);
}
if hints::unlikely(res < 0) {
let errno = last_errno();
let err_msg = err_msg(errno);
match errno {
EINTR | EAGAIN => continue,
EACCES | EPERM => return new_err(FFileErr::Prm, err_msg),
EINVAL | EBADF | EFAULT | ESPIPE => return new_err(FFileErr::Hcf, err_msg),
_ => return new_err(FFileErr::Unk, err_msg),
}
}
read += res as usize;
}
Ok(())
}
#[inline(always)]
pub(super) unsafe fn pwrite(&self, ptr: *mut u8, offset: usize, chunk_size: usize) -> FrozenRes<()> {
let fd = self.fd();
let mut written = 0usize;
while written < chunk_size {
let res = pwrite(
fd,
ptr.add(written) as *mut c_void,
(chunk_size - written) as size_t,
(offset + written) as off_t,
);
if res == 0 {
return new_err_default(FFileErr::Hcf);
}
if hints::unlikely(res < 0) {
let errno = last_errno();
let err_msg = err_msg(errno);
match errno {
EINTR | EAGAIN => continue,
EACCES | EPERM | EROFS => return new_err(FFileErr::Prm, err_msg),
EINVAL | EBADF | EFAULT | ESPIPE => return new_err(FFileErr::Hcf, err_msg),
_ => return new_err(FFileErr::Unk, err_msg),
}
}
written += res as usize;
}
Ok(())
}
#[inline(always)]
pub(super) unsafe fn preadv(&self, bufs: &[*mut u8], offset: usize, chunk_size: usize) -> FrozenRes<()> {
let fd = self.fd();
let iovecs_len = bufs.len();
let max_iovs = read_max_iovecs();
let mut iovecs: Vec<iovec> = bufs
.iter()
.map(|b| iovec {
iov_base: *b as *mut c_void,
iov_len: chunk_size,
})
.collect();
let mut head = 0usize;
let mut off = offset as off_t;
while head < iovecs_len {
let remaining_iov = iovecs_len - head;
let cnt = remaining_iov.min(max_iovs) as c_int;
let ptr = iovecs.as_mut_ptr().add(head);
let res = preadv(fd, ptr, cnt, off);
if res == 0 {
return new_err_default(FFileErr::Hcf);
}
if res < 0 {
let errno = last_errno();
let err_msg = err_msg(errno);
match errno {
EINTR | EAGAIN => continue,
EACCES | EPERM => return new_err(FFileErr::Prm, err_msg),
EINVAL | EBADF | EFAULT | ESPIPE | EMSGSIZE => return new_err(FFileErr::Hcf, err_msg),
_ => return new_err(FFileErr::Unk, err_msg),
}
}
off += res as off_t;
let read = res as usize;
let partial = read % chunk_size;
let full_pages = read / chunk_size;
head += full_pages;
if partial > 0 && head < iovecs_len {
let iov = &mut iovecs[head];
iov.iov_base = (iov.iov_base as *mut u8).add(partial) as *mut c_void;
iov.iov_len -= partial;
}
}
Ok(())
}
#[inline(always)]
pub(super) unsafe fn pwritev(&self, bufs: &[*mut u8], offset: usize, chunk_size: usize) -> FrozenRes<()> {
let fd = self.fd();
let max_iovs = read_max_iovecs();
let iovecs_len = bufs.len();
let mut iovecs: Vec<iovec> = bufs
.iter()
.map(|b| iovec {
iov_base: *b as *mut c_void,
iov_len: chunk_size,
})
.collect();
let mut head = 0usize;
let mut off = offset as off_t;
while head < iovecs_len {
let remaining_iov = iovecs_len - head;
let cnt = remaining_iov.min(max_iovs) as c_int;
let ptr = iovecs.as_mut_ptr().add(head);
let res = pwritev(fd, ptr, cnt, off);
if res == 0 {
return new_err_default(FFileErr::Hcf);
}
if res < 0 {
let errno = last_errno();
let err_msg = err_msg(errno);
match errno {
EINTR | EAGAIN => continue,
EACCES | EPERM => return new_err(FFileErr::Prm, err_msg),
EINVAL | EBADF | EFAULT | ESPIPE | EMSGSIZE => return new_err(FFileErr::Hcf, err_msg),
_ => return new_err(FFileErr::Unk, err_msg),
}
}
off += res as off_t;
let written = res as usize;
let partial = written % chunk_size;
let full_pages = written / chunk_size;
head += full_pages;
if partial > 0 && head < iovecs_len {
let iov = &mut iovecs[head];
iov.iov_base = (iov.iov_base as *mut u8).add(partial) as *mut c_void;
iov.iov_len -= partial;
}
}
Ok(())
}
}
unsafe fn open_raw(path: &std::path::Path, flags: c_int) -> FrozenRes<FFId> {
let cpath = path_to_cstring(path)?;
let perm = (S_IRUSR | S_IWUSR) as c_uint;
#[cfg(target_os = "linux")]
let (mut flags, mut tried_noatime) = (flags, false);
loop {
let fd = if flags & O_CREAT != 0 {
open(cpath.as_ptr(), flags, perm)
} else {
open(cpath.as_ptr(), flags)
};
if hints::unlikely(fd < 0) {
let errno = last_errno();
let err_msg = err_msg(errno);
#[cfg(target_os = "linux")]
if errno == EPERM && (flags & libc::O_NOATIME) != 0 && !tried_noatime {
flags &= !libc::O_NOATIME;
tried_noatime = true;
continue;
}
match errno {
EINTR => continue,
ENOSPC => return new_err(FFileErr::Nsp, err_msg),
EISDIR => return new_err(FFileErr::Hcf, err_msg),
ENOENT | ENOTDIR => return new_err(FFileErr::Inv, err_msg),
EACCES | EPERM | EROFS => return new_err(FFileErr::Prm, err_msg),
_ => return new_err(FFileErr::Unk, err_msg),
}
}
return Ok(fd);
}
}
unsafe fn close_raw(fd: FFId) -> FrozenRes<()> {
if close(fd) == 0 {
return Ok(());
}
let errno = last_errno();
let err_msg = err_msg(errno);
if errno == EINTR {
return Ok(());
}
if errno == EIO {
return new_err(FFileErr::Hcf, err_msg);
}
new_err(FFileErr::Unk, err_msg)
}
#[cfg(target_os = "linux")]
unsafe fn fdatasync_raw(fd: FFId) -> FrozenRes<()> {
let mut retries = 0; loop {
if hints::likely(libc::fdatasync(fd) == 0) {
return Ok(());
}
let errno = last_errno();
let err_msg = err_msg(errno);
match errno {
EINVAL | EBADF => return new_err(FFileErr::Hcf, err_msg),
EROFS => return new_err(FFileErr::Prm, err_msg),
EIO => return new_err(FFileErr::Syn, err_msg),
EINTR => {
if retries < MAX_RETRIES {
retries += 1;
continue;
}
return new_err(FFileErr::Syn, err_msg);
}
_ => return new_err(FFileErr::Unk, err_msg),
}
}
}
#[cfg(target_os = "macos")]
unsafe fn f_fullsync_raw(fd: FFId) -> FrozenRes<()> {
let mut retries = 0; loop {
if hints::likely(libc::fcntl(fd, libc::F_FULLFSYNC) == 0) {
return Ok(());
}
let errno = last_errno();
let err_msg = err_msg(errno);
match errno {
EINTR => {
if retries < MAX_RETRIES {
retries += 1;
continue;
}
return new_err(FFileErr::Syn, err_msg);
}
libc::ENOTSUP | EOPNOTSUPP => break,
EINVAL | EBADF => return new_err(FFileErr::Hcf, err_msg),
EROFS => return new_err(FFileErr::Prm, err_msg),
EIO => return new_err(FFileErr::Syn, err_msg),
_ => return new_err(FFileErr::Unk, err_msg),
}
}
fsync_raw(fd)
}
unsafe fn fsync_raw(fd: FFId) -> FrozenRes<()> {
let mut retries = 0; loop {
if hints::unlikely(libc::fsync(fd) != 0) {
let errno = last_errno();
let err_msg = err_msg(errno);
match errno {
EINTR => {
if retries < MAX_RETRIES {
retries += 1;
continue;
}
return new_err(FFileErr::Syn, err_msg);
}
EBADF | EINVAL => return new_err(FFileErr::Hcf, err_msg),
EROFS => return new_err(FFileErr::Prm, err_msg),
EIO => return new_err(FFileErr::Syn, err_msg),
_ => return new_err(FFileErr::Unk, err_msg),
}
}
return Ok(());
}
}
#[cfg(target_os = "linux")]
unsafe fn sync_file_range_raw(fd: FFId, offset: usize, len: usize) -> FrozenRes<()> {
let flag = libc::SYNC_FILE_RANGE_WRITE;
let mut retries = 0;
loop {
let res = libc::sync_file_range(fd, offset as off_t, len as off_t, flag);
if hints::likely(res == 0) {
return Ok(());
}
let errno = last_errno();
let err_msg = err_msg(errno);
match errno {
EINTR => {
if retries < MAX_RETRIES {
retries += 1;
continue;
}
return new_err(FFileErr::Syn, err_msg);
}
EBADF | EINVAL => return new_err(FFileErr::Hcf, err_msg),
EROFS => return new_err(FFileErr::Prm, err_msg),
EIO => return new_err(FFileErr::Syn, err_msg),
EOPNOTSUPP | libc::ENOSYS => return Ok(()),
_ => return new_err(FFileErr::Unk, err_msg),
}
}
}
#[cfg(target_os = "linux")]
unsafe fn fallocate_raw(fd: FFId, curr_len: usize, len_to_add: usize) -> FrozenRes<()> {
let mut retries = 0; loop {
if hints::likely(libc::fallocate(fd, 0, curr_len as off_t, len_to_add as off_t) == 0) {
return Ok(());
}
let errno = last_errno();
let err_msg = err_msg(errno);
match errno {
EINTR => {
if retries < MAX_RETRIES {
retries += 1;
continue;
}
return new_err(FFileErr::Grw, err_msg);
}
EBADF | EINVAL => return new_err(FFileErr::Hcf, err_msg),
EROFS => return new_err(FFileErr::Prm, err_msg),
ENOSPC => return new_err(FFileErr::Nsp, err_msg),
EOPNOTSUPP | libc::ENOSYS => return Ok(()),
_ => return new_err(FFileErr::Unk, err_msg),
}
}
}
unsafe fn ftruncate_raw(fd: FFId, curr_len: usize, len_to_add: usize) -> FrozenRes<()> {
let new_len = (curr_len + len_to_add) as off_t;
let mut retries = 0;
loop {
if hints::likely(ftruncate(fd, new_len) == 0) {
return Ok(());
}
let errno = last_errno();
let err_msg = err_msg(errno);
match errno {
EINTR => {
if retries < MAX_RETRIES {
retries += 1;
continue;
}
return new_err(FFileErr::Grw, err_msg);
}
EINVAL | EBADF => return new_err(FFileErr::Hcf, err_msg),
EROFS => return new_err(FFileErr::Prm, err_msg),
ENOSPC => return new_err(FFileErr::Nsp, err_msg),
_ => return new_err(FFileErr::Unk, err_msg),
}
}
}
#[cfg(target_os = "macos")]
unsafe fn f_preallocate_raw(fd: FFId, curr_len: usize, len_to_add: usize) -> FrozenRes<()> {
let mut retries = 0;
let mut store = libc::fstore_t {
fst_flags: libc::F_ALLOCATECONTIG,
fst_posmode: libc::F_PEOFPOSMODE,
fst_offset: curr_len as off_t,
fst_length: len_to_add as off_t,
fst_bytesalloc: 0,
};
loop {
if libc::fcntl(fd, libc::F_PREALLOCATE, &store) == 0 {
return Ok(());
}
let errno = last_errno();
let err_msg = err_msg(errno);
match errno {
EINTR => {
if retries < MAX_RETRIES {
retries += 1;
continue;
}
return new_err(FFileErr::Grw, err_msg);
}
ENOSPC => {
if store.fst_flags == libc::F_ALLOCATECONTIG {
store.fst_flags = libc::F_ALLOCATEALL;
retries = 0;
continue;
}
return new_err(FFileErr::Nsp, err_msg);
}
EOPNOTSUPP | libc::ENOTSUP => return Ok(()),
EINVAL => return Ok(()),
EBADF => return new_err(FFileErr::Hcf, err_msg),
EROFS => return new_err(FFileErr::Prm, err_msg),
_ => return new_err(FFileErr::Unk, err_msg),
}
}
}
unsafe fn flock_raw(fd: FFId) -> FrozenRes<()> {
let mut retries = 0; loop {
if flock(fd, LOCK_EX | LOCK_NB) == 0 {
return Ok(());
}
let errno = last_errno();
let err_msg = err_msg(errno);
match errno {
EWOULDBLOCK => {
return new_err(FFileErr::Lck, err_msg);
}
EINTR => {
if retries < MAX_RETRIES {
retries += 1;
continue;
}
return new_err(FFileErr::Lck, err_msg);
}
EBADF | EINVAL => return new_err(FFileErr::Hcf, err_msg),
ENOLCK => return new_err(FFileErr::Lex, err_msg),
_ => return new_err(FFileErr::Unk, err_msg),
}
}
}
unsafe fn sync_parent_dir(path: &std::path::Path) -> FrozenRes<()> {
let parent = extract_parent_dir(path);
let flags = O_RDONLY | O_DIRECTORY | O_CLOEXEC;
let fd = open_raw(&parent, flags)?;
#[cfg(target_os = "linux")]
let res = fsync_raw(fd);
#[cfg(target_os = "macos")]
let res = f_fullsync_raw(fd);
let _ = close_raw(fd);
res
}
#[cfg(target_os = "linux")]
const fn prep_flags() -> c_int {
O_RDWR | O_CLOEXEC | libc::O_NOATIME | O_CREAT
}
#[cfg(target_os = "macos")]
const fn prep_flags() -> c_int {
return O_RDWR | O_CLOEXEC | O_CREAT;
}
fn path_to_cstring(path: &std::path::Path) -> FrozenRes<std::ffi::CString> {
match std::ffi::CString::new(path.as_os_str().as_encoded_bytes()) {
Ok(cs) => Ok(cs),
Err(e) => new_err(FFileErr::Inv, e.into_vec()),
}
}
#[inline]
fn last_errno() -> i32 {
#[cfg(target_os = "linux")]
unsafe {
*libc::__errno_location()
}
#[cfg(target_os = "macos")]
unsafe {
*libc::__error()
}
}
#[inline]
unsafe fn err_msg(errno: i32) -> Vec<u8> {
let ptr = strerror(errno);
if ptr.is_null() {
return Vec::new();
}
CStr::from_ptr(ptr).to_bytes().to_vec()
}
fn read_max_iovecs() -> usize {
*IOV_MAX_CACHE.get_or_init(|| unsafe {
let res = sysconf(_SC_IOV_MAX);
if res <= 0 {
MAX_IOVECS
} else {
res as usize
}
})
}
fn extract_parent_dir(path: &std::path::Path) -> std::path::PathBuf {
match path.parent() {
Some(p) if !p.as_os_str().is_empty() => p.to_path_buf(),
_ => std::path::Path::new(".").to_path_buf(),
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::path::PathBuf;
fn tmp_path() -> (tempfile::TempDir, PathBuf) {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("tmp_file");
(dir, path)
}
mod file_new_close {
use super::*;
#[test]
fn ok_new_close_cycle() {
let (_dir, path) = tmp_path();
unsafe {
let file = POSIXFile::new(&path).unwrap();
assert!(path.exists());
file.close().unwrap();
}
}
#[test]
fn ok_new_close_cycle_on_existing() {
let (_dir, path) = tmp_path();
unsafe {
let file1 = POSIXFile::new(&path).unwrap();
file1.close().unwrap();
let file2 = POSIXFile::new(&path).unwrap();
file2.close().unwrap();
}
}
#[test]
fn ok_close_on_close() {
let (_dir, path) = tmp_path();
unsafe {
let file = POSIXFile::new(&path).unwrap();
file.close().unwrap();
file.close().unwrap();
file.close().unwrap();
}
}
#[test]
fn err_new_on_missing_parent_dir() {
let (_dir, path) = tmp_path();
unsafe {
let missing = path.join("missing/file");
let err = POSIXFile::new(&missing).unwrap_err();
assert!(err.compare(FFileErr::Inv as u16))
}
}
}
mod file_unlink {
use super::*;
#[test]
fn ok_unlink_existing() {
let (_dir, path) = tmp_path();
unsafe {
let file = POSIXFile::new(&path).unwrap();
assert!(path.exists());
file.unlink(&path).unwrap();
assert!(!path.exists());
}
}
#[test]
fn err_unlink_missing() {
let (_dir, path) = tmp_path();
unsafe {
let file = POSIXFile::new(&path).unwrap();
file.unlink(&path).unwrap();
let err = file.unlink(&path).unwrap_err();
assert!(err.compare(FFileErr::Inv as u16));
}
}
}
mod file_lock {
use super::*;
#[test]
fn ok_flock_acquires_exclusive_lock() {
let (_dir, path) = tmp_path();
unsafe {
let file = POSIXFile::new(&path).unwrap();
file.flock().unwrap();
file.close().unwrap();
}
}
#[test]
fn err_flock_when_already_locked() {
let (_dir, path) = tmp_path();
unsafe {
let file1 = POSIXFile::new(&path).unwrap();
file1.flock().unwrap();
let file2 = POSIXFile::new(&path).unwrap();
let err = file2.flock().unwrap_err();
assert!(err.compare(FFileErr::Lck as u16));
file1.close().unwrap();
file2.close().unwrap();
}
}
#[test]
fn ok_flock_released_after_close() {
let (_dir, path) = tmp_path();
unsafe {
let file1 = POSIXFile::new(&path).unwrap();
file1.flock().unwrap();
file1.close().unwrap();
let file2 = POSIXFile::new(&path).unwrap();
file2.flock().unwrap();
file2.close().unwrap();
}
}
}
mod file_grow {
use super::*;
#[test]
fn ok_grow() {
let (_dir, path) = tmp_path();
unsafe {
let file = POSIXFile::new(&path).unwrap();
let initial = file.length().unwrap();
assert_eq!(initial, 0);
file.grow(0, 0x1000).unwrap();
let new_len = file.length().unwrap();
assert_eq!(new_len, 0x1000);
let actual = file.length().unwrap();
assert_eq!(actual, 0x1000);
file.close().unwrap();
}
}
#[test]
fn ok_grow_extends_with_zero() {
let (_dir, path) = tmp_path();
unsafe {
let file = POSIXFile::new(&path).unwrap();
file.grow(0, 0x500).unwrap();
let mut buf = vec![0u8; 0x500];
file.pread(buf.as_mut_ptr(), 0, 0x500).unwrap();
assert!(buf.iter().all(|b| *b == 0));
file.close().unwrap();
}
}
}
mod fil_sync {
use super::*;
#[test]
fn ok_sync() {
let (_dir, path) = tmp_path();
unsafe {
let file = POSIXFile::new(&path).unwrap();
file.sync().unwrap();
file.close().unwrap();
}
}
#[test]
fn ok_sync_after_sync() {
let (_dir, path) = tmp_path();
unsafe {
let file = POSIXFile::new(&path).unwrap();
file.sync().unwrap();
file.sync().unwrap();
file.sync().unwrap();
file.sync().unwrap();
file.close().unwrap();
}
}
}
mod write_read_single {
use super::*;
#[test]
fn ok_pwrite_pread_cycle() {
let (_dir, path) = tmp_path();
unsafe {
let file = POSIXFile::new(&path).unwrap();
file.grow(0, 0x200).unwrap();
let mut data = b"grave_engine".to_vec();
file.pwrite(data.as_mut_ptr(), 0x80, 0x0C).unwrap();
let mut buf = vec![0u8; data.len()];
file.pread(buf.as_mut_ptr(), 0x80, 0x0C).unwrap();
assert_eq!(buf, data);
}
}
#[test]
fn ok_pwrite_pread_across_sessions() {
let (_dir, path) = tmp_path();
unsafe {
let file = POSIXFile::new(&path).unwrap();
file.grow(0, 0x1000).unwrap();
let mut data = b"persist_me".to_vec();
file.pwrite(data.as_mut_ptr(), 0, data.len()).unwrap();
file.sync().unwrap();
file.close().unwrap();
}
unsafe {
let file = POSIXFile::new(&path).unwrap();
let mut buf = vec![0u8; 0x0A];
file.pread(buf.as_mut_ptr(), 0, buf.len()).unwrap();
assert_eq!(&buf, b"persist_me");
}
}
#[test]
fn ok_pwrite_concurrent_non_overlapping() {
let (_dir, path) = tmp_path();
unsafe {
let file = std::sync::Arc::new(POSIXFile::new(&path).unwrap());
file.grow(0, 0x2000).unwrap();
let mut handles = vec![];
for i in 0..0x0A {
let f = file.clone();
handles.push(std::thread::spawn(move || {
let mut data = vec![i as u8; 0x100];
f.pwrite(data.as_mut_ptr(), i * 0x100, data.len()).unwrap();
}));
}
for h in handles {
h.join().unwrap();
}
file.sync().unwrap();
for i in 0..0x0A {
let mut buf = vec![0u8; 0x100];
file.pread(buf.as_mut_ptr(), i * 0x100, buf.len()).unwrap();
assert!(buf.iter().all(|b| *b == i as u8));
}
}
}
#[test]
fn ok_pwrite_when_overlapping_last_wins() {
let (_dir, path) = tmp_path();
unsafe {
let file = POSIXFile::new(&path).unwrap();
file.grow(0, 0x100).unwrap();
let mut a = [1u8; 0x80];
let mut b = [2u8; 0x80];
file.pwrite(a.as_mut_ptr(), 0, a.len()).unwrap();
file.pwrite(b.as_mut_ptr(), 0, b.len()).unwrap();
let mut buf = vec![0u8; 0x80];
file.pread(buf.as_mut_ptr(), 0, buf.len()).unwrap();
assert!(buf.iter().all(|b| *b == 2));
}
}
}
mod write_read_vectored {
use super::*;
#[test]
fn ok_pwritev_preadv_cycle() {
let (_dir, path) = tmp_path();
unsafe {
let file = POSIXFile::new(&path).unwrap();
file.grow(0, 0x1000).unwrap();
let mut buffers = [vec![1u8; 0x80], vec![2u8; 0x80], vec![3u8; 0x80]];
let ptrs: Vec<*mut u8> = buffers.iter_mut().map(|b| b.as_mut_ptr()).collect();
file.pwritev(&ptrs, 0, 0x80).unwrap();
let mut read_bufs = [vec![0u8; 0x80], vec![0u8; 0x80], vec![0u8; 0x80]];
let read_ptrs: Vec<*mut u8> = read_bufs.iter_mut().map(|b| b.as_mut_ptr()).collect();
file.preadv(&read_ptrs, 0, 0x80).unwrap();
assert!(read_bufs[0].iter().all(|b| *b == 1));
assert!(read_bufs[1].iter().all(|b| *b == 2));
assert!(read_bufs[2].iter().all(|b| *b == 3));
}
}
#[test]
fn ok_pwritev_handles_large_iovec_batches() {
let (_dir, path) = tmp_path();
unsafe {
let count = read_max_iovecs() + 5;
let file = POSIXFile::new(&path).unwrap();
file.grow(0, count * 0x40).unwrap();
let mut buffers: Vec<Vec<u8>> = (0..count).map(|i| vec![i as u8; 0x40]).collect();
let ptrs: Vec<*mut u8> = buffers.iter_mut().map(|b| b.as_mut_ptr()).collect();
file.pwritev(&ptrs, 0, 0x40).unwrap();
file.sync().unwrap();
for i in 0..count {
let mut buf = vec![0u8; 0x40];
file.pread(buf.as_mut_ptr(), i * 0x40, 0x40).unwrap();
assert!(buf.iter().all(|b| *b == i as u8));
}
}
}
#[test]
fn ok_pwritev_preadv_across_sessions() {
let (_dir, path) = tmp_path();
unsafe {
let file = POSIXFile::new(&path).unwrap();
file.grow(0, 0x400).unwrap();
let mut bufs = [vec![9u8; 0x80], vec![8u8; 0x80]];
let ptrs: Vec<*mut u8> = bufs.iter_mut().map(|b| b.as_mut_ptr()).collect();
file.pwritev(&ptrs, 0, 0x80).unwrap();
file.sync().unwrap();
file.close().unwrap();
}
unsafe {
let file = POSIXFile::new(&path).unwrap();
let mut read_bufs = [vec![0u8; 0x80], vec![0u8; 0x80]];
let read_ptrs: Vec<*mut u8> = read_bufs.iter_mut().map(|b| b.as_mut_ptr()).collect();
file.preadv(&read_ptrs, 0, 0x80).unwrap();
assert!(read_bufs[0].iter().all(|b| *b == 9));
assert!(read_bufs[1].iter().all(|b| *b == 8));
}
}
#[test]
fn ok_pwritev_concurrent_non_overlapping() {
let (_dir, path) = tmp_path();
unsafe {
let file = std::sync::Arc::new(POSIXFile::new(&path).unwrap());
let threads = 8usize;
let page = 0x80usize;
let per_thread_iovs = 4usize;
let total = threads * per_thread_iovs * page;
file.grow(0, total).unwrap();
let mut handles = Vec::new();
for t in 0..threads {
let f = file.clone();
handles.push(std::thread::spawn(move || {
let mut bufs: Vec<Vec<u8>> =
(0..per_thread_iovs).map(|i| vec![(t * 10 + i) as u8; page]).collect();
let ptrs: Vec<*mut u8> = bufs.iter_mut().map(|b| b.as_mut_ptr()).collect();
let offset = t * per_thread_iovs * page;
f.pwritev(&ptrs, offset, page).unwrap();
}));
}
for h in handles {
h.join().unwrap();
}
file.sync().unwrap();
for t in 0..threads {
for i in 0..per_thread_iovs {
let mut buf = vec![0u8; page];
let offset = (t * per_thread_iovs + i) * page;
file.pread(buf.as_mut_ptr(), offset, page).unwrap();
let expected = (t * 10 + i) as u8;
assert!(buf.iter().all(|b| *b == expected));
}
}
}
}
}
mod write_read_vectored_load {
use super::*;
#[test]
fn ok_single_thread_large_batch() {
let (_dir, path) = tmp_path();
unsafe {
let count = read_max_iovecs() * 3 + 17; let page = 0x40usize;
let file = POSIXFile::new(&path).unwrap();
file.grow(0, count * page).unwrap();
let mut buffers: Vec<Vec<u8>> = (0..count).map(|i| vec![(i % 0xFB) as u8; page]).collect();
let ptrs: Vec<*mut u8> = buffers.iter_mut().map(|b| b.as_mut_ptr()).collect();
file.pwritev(&ptrs, 0, page).unwrap();
file.sync().unwrap();
let mut read_bufs: Vec<Vec<u8>> = (0..count).map(|_| vec![0u8; page]).collect();
let read_ptrs: Vec<*mut u8> = read_bufs.iter_mut().map(|b| b.as_mut_ptr()).collect();
file.preadv(&read_ptrs, 0, page).unwrap();
for (i, item) in read_bufs.iter().enumerate().take(count) {
let expected = (i % 0xFB) as u8;
assert!(item.iter().all(|b| *b == expected));
}
}
}
#[test]
fn ok_multi_thread_large_batch() {
let (_dir, path) = tmp_path();
unsafe {
let threads = 6usize;
let page = 0x40usize;
let per_thread = read_max_iovecs() * 2 + 0x0B;
let total_pages = threads * per_thread;
let file = std::sync::Arc::new(POSIXFile::new(&path).unwrap());
file.grow(0, total_pages * page).unwrap();
let mut handles = Vec::new();
for t in 0..threads {
let f = file.clone();
handles.push(std::thread::spawn(move || {
let mut bufs: Vec<Vec<u8>> =
(0..per_thread).map(|i| vec![(t * 0x1F + i) as u8; page]).collect();
let ptrs: Vec<*mut u8> = bufs.iter_mut().map(|b| b.as_mut_ptr()).collect();
let offset = t * per_thread * page;
f.pwritev(&ptrs, offset, page).unwrap();
}));
}
for h in handles {
h.join().unwrap();
}
file.sync().unwrap();
for t in 0..threads {
for i in 0..per_thread {
let mut buf = vec![0u8; page];
let offset = (t * per_thread + i) * page;
file.pread(buf.as_mut_ptr(), offset, page).unwrap();
let expected = (t * 0x1F + i) as u8;
assert!(buf.iter().all(|b| *b == expected));
}
}
}
}
}
mod utils {
use super::*;
use std::{ffi::CString, os::unix::ffi::OsStrExt};
#[test]
fn ok_extract_parent_dir() {
let cases = [
("/", "."),
("file.db", "."),
("./a/b/c.log", "./a/b"),
("data/file.db", "data"),
("/var/lib/grave/", "/var/lib"),
("/tmp/grave/file.db", "/tmp/grave"),
];
for (input, expected) in cases {
let path = PathBuf::from(input);
let parent = extract_parent_dir(&path);
assert_eq!(parent, PathBuf::from(expected), "failed for input: {input}");
}
}
#[test]
fn ok_path_to_cstring() {
let cases: &[(&[u8], bool)] = &[
(b"", true),
(b"file.db", true),
(b"bad\0path.db", false),
(b"relative/path.db", true),
(b"/tmp/grave/file.db", true),
];
for (bytes, should_ok) in cases {
let path = PathBuf::from(std::ffi::OsStr::from_bytes(bytes));
let res = path_to_cstring(&path);
match (res, should_ok) {
(Ok(cs), true) => {
let expected = CString::new(*bytes).expect("valid test case must not contain interior NUL");
assert_eq!(cs.as_bytes(), expected.as_bytes(), "mismatch for input: {:?}", bytes);
}
(Err(_), false) => {}
(other, _) => {
panic!("unexpected result for input {:?}: {:?}", bytes, other);
}
}
}
}
#[test]
fn ok_read_max_iovecs() {
let first = read_max_iovecs();
let second = read_max_iovecs();
assert!(first > 0, "IOV_MAX must be positive");
assert!(first >= MAX_IOVECS && second >= MAX_IOVECS);
assert_eq!(first, second, "value must be cached and stable");
}
#[test]
fn ok_last_errno() {
unsafe {
let _ = libc::close(-1);
assert_eq!(last_errno(), libc::EBADF);
}
}
#[test]
fn ok_err_msg() {
unsafe {
let msg = err_msg(libc::ENOENT);
assert!(!msg.is_empty(), "ENOENT must produce message");
}
}
}
mod file_lifecycle {
use super::*;
#[test]
fn err_length_after_closed() {
let (_dir, path) = tmp_path();
unsafe {
let file = POSIXFile::new(&path).unwrap();
file.close().unwrap();
let err = file.length().unwrap_err();
assert!(err.compare(FFileErr::Hcf as u16));
}
}
#[test]
fn err_pread_after_closed() {
let (_dir, path) = tmp_path();
unsafe {
let file = POSIXFile::new(&path).unwrap();
file.grow(0, 0x100).unwrap();
file.close().unwrap();
let mut buf = vec![0u8; 8];
let err = file.pread(buf.as_mut_ptr(), 0, buf.len()).unwrap_err();
assert!(err.compare(FFileErr::Hcf as u16));
}
}
#[test]
fn err_pwrite_after_closed() {
let (_dir, path) = tmp_path();
unsafe {
let file = POSIXFile::new(&path).unwrap();
file.grow(0, 0x100).unwrap();
file.close().unwrap();
let mut data = b"dead".to_vec();
let err = file.pwrite(data.as_mut_ptr(), 0, data.len()).unwrap_err();
assert!(err.compare(FFileErr::Hcf as u16));
}
}
#[test]
fn err_sync_after_closed() {
let (_dir, path) = tmp_path();
unsafe {
let file = POSIXFile::new(&path).unwrap();
file.close().unwrap();
let err = file.sync().unwrap_err();
assert!(err.compare(FFileErr::Hcf as u16));
}
}
#[test]
fn err_grow_after_closed() {
let (_dir, path) = tmp_path();
unsafe {
let file = POSIXFile::new(&path).unwrap();
file.close().unwrap();
let err = file.grow(0, 0x100).unwrap_err();
assert!(err.compare(FFileErr::Hcf as u16));
}
}
#[test]
fn err_double_unlink_after_close() {
let (_dir, path) = tmp_path();
unsafe {
let file = POSIXFile::new(&path).unwrap();
file.unlink(&path).unwrap();
assert!(!path.exists());
let err = file.unlink(&path).unwrap_err();
assert!(err.compare(FFileErr::Inv as u16));
}
}
}
mod raw_syscalls {
use super::*;
#[test]
fn ok_sync_cycle() {
let (_dir, path) = tmp_path();
unsafe {
let file = POSIXFile::new(&path).unwrap();
file.grow(0, 0x400).unwrap();
let mut data = [7u8; 0x80];
file.pwrite(data.as_mut_ptr(), 0, data.len()).unwrap();
file.sync().unwrap();
let mut buf = vec![0u8; 0x80];
file.pread(buf.as_mut_ptr(), 0, buf.len()).unwrap();
assert_eq!(buf, data);
}
}
#[cfg(target_os = "linux")]
#[test]
fn ok_sync_range() {
let (_dir, path) = tmp_path();
unsafe {
let file = POSIXFile::new(&path).unwrap();
file.grow(0, 0x1000).unwrap();
let mut data = [5u8; 0x100];
file.pwrite(data.as_mut_ptr(), 0x200, data.len()).unwrap();
file.sync_range(0x200, 0x100).unwrap();
file.sync().unwrap();
let mut buf = vec![0u8; 0x100];
file.pread(buf.as_mut_ptr(), 0x200, buf.len()).unwrap();
assert_eq!(buf, data);
}
}
#[test]
fn ok_write_read_at_eof_boundary() {
let (_dir, path) = tmp_path();
unsafe {
let file = POSIXFile::new(&path).unwrap();
file.grow(0, 0x200).unwrap();
let mut data = [3u8; 0x40];
file.pwrite(data.as_mut_ptr(), 0x200 - 0x40, data.len()).unwrap();
let mut buf = vec![0u8; 0x40];
file.pread(buf.as_mut_ptr(), 0x200 - 0x40, buf.len()).unwrap();
assert_eq!(buf, data);
}
}
#[test]
fn ok_multiple_open_close_cycles() {
let (_dir, path) = tmp_path();
unsafe {
for _ in 0..0x0A {
let file = POSIXFile::new(&path).unwrap();
file.sync().unwrap();
file.close().unwrap();
}
}
}
}
}