use super::{err, interface::FileInterface};
use crate::{error::FrozenResult, hints};
use libc::{
EACCES, EAGAIN, EBADF, EBUSY, EEXIST, EFAULT, EINTR, EINVAL, EIO, EISDIR, ELOOP, ENAMETOOLONG,
ENOENT, ENOLCK, ENOSPC, ENOTDIR, EOPNOTSUPP, EPERM, EROFS, ESPIPE, EWOULDBLOCK, F_OK, LOCK_EX,
LOCK_NB, O_CLOEXEC, O_CREAT, O_DIRECTORY, O_EXCL, O_RDONLY, O_RDWR, S_IRUSR, S_IWUSR, access,
c_int, c_uint, c_void, close, flock, fstat, ftruncate, off_t, open, pread, pwrite, size_t,
stat, unlink,
};
use std::sync::atomic;
pub(super) type FileId = c_int;
pub(in crate::file) const CLOSED_FD: FileId = FileId::MIN;
const MAX_RETRIES: usize = 0x80;
#[derive(Debug)]
pub(super) struct POSIXFile {
fd: atomic::AtomicI32,
}
impl FileInterface for POSIXFile {
type Id = FileId;
const CLOSED_ID: Self::Id = CLOSED_FD;
fn fd(&self) -> FileId {
self.fd.load(atomic::Ordering::Acquire)
}
fn exists(path: &std::path::Path) -> FrozenResult<bool> {
let cpath = path_to_cstring(path)?;
let res = unsafe { access(cpath.as_ptr(), F_OK) };
if res == 0 {
return Ok(true);
}
let errno = last_errno();
let err_msg = err_msg(errno);
match errno {
ENOENT | ENOTDIR => Ok(false),
EACCES | EPERM => err::raw_error(err::PRM, err_msg),
EINVAL => err::raw_error(err::HCF, err_msg),
ELOOP | ENAMETOOLONG => err::raw_error(err::INV, err_msg),
_ => err::raw_error(err::UNK, err_msg),
}
}
fn create(path: &std::path::Path) -> FrozenResult<Self> {
let fd = open_raw(path, create_flags())?;
let file = Self { fd: atomic::AtomicI32::new(fd) };
if let Err(mut e) = sync_parent_dir(path) {
if let Err(close_err) = file.close() {
e.add_suppressed(close_err);
}
return Err(e);
}
#[cfg(target_os = "linux")]
{
let _res = f_advise_raw(file.fd());
#[cfg(debug_assertions)]
if let Err(mut e) = _res {
if let Err(close_err) = file.close() {
e.add_suppressed(close_err);
}
return Err(e);
}
}
Ok(file)
}
fn open(path: &std::path::Path) -> FrozenResult<Self> {
let fd = open_raw(path, open_flags())?;
let file = Self { fd: atomic::AtomicI32::new(fd) };
#[cfg(target_os = "linux")]
{
let _res = f_advise_raw(file.fd());
#[cfg(debug_assertions)]
if let Err(mut e) = _res {
if let Err(close_err) = file.close() {
e.add_suppressed(close_err);
}
return Err(e);
}
}
Ok(file)
}
fn new(path: &std::path::Path) -> FrozenResult<Self> {
let fd = open_raw(path, prep_flags())?;
let file = Self { fd: atomic::AtomicI32::new(fd) };
if let Err(mut e) = sync_parent_dir(path) {
if let Err(close_err) = file.close() {
e.add_suppressed(close_err);
}
return Err(e);
}
#[cfg(target_os = "linux")]
{
let _res = f_advise_raw(file.fd());
#[cfg(debug_assertions)]
if let Err(mut e) = _res {
if let Err(close_err) = file.close() {
e.add_suppressed(close_err);
}
return Err(e);
}
}
Ok(file)
}
fn flock(&self) -> FrozenResult<()> {
flock_raw(self.fd())
}
fn close(self) -> FrozenResult<()> {
let fd = self.fd.swap(CLOSED_FD, atomic::Ordering::AcqRel);
if fd == CLOSED_FD {
return Ok(());
}
close_raw(fd)
}
fn unlink(self, path: &std::path::Path) -> FrozenResult<()> {
let cpath = path_to_cstring(path)?;
let res = unsafe { unlink(cpath.as_ptr()) };
if res == 0 {
self.close()?;
return sync_parent_dir(path);
}
let errno = last_errno();
let err_msg = err_msg(errno);
match errno {
ENOENT | ENOTDIR => err::raw_error(err::INV, err_msg),
EACCES | EPERM | EROFS => err::raw_error(err::PRM, err_msg),
EIO => err::raw_error(err::SYN, err_msg),
_ => err::raw_error(err::UNK, err_msg),
}
}
fn length(&self) -> FrozenResult<usize> {
let mut st = unsafe { core::mem::zeroed::<stat>() };
let res = unsafe { fstat(self.fd(), &mut st) };
if res != 0 {
let errno = last_errno();
let err_msg = err_msg(errno);
if errno == EBADF || errno == EFAULT {
return err::raw_error(err::HCF, err_msg);
}
return err::raw_error(err::UNK, err_msg);
}
if hints::unlikely(st.st_size < 0) {
return err::raw_error(err::HCF, "filesystem reported negative file size");
}
match usize::try_from(st.st_size) {
Ok(sz) => Ok(sz),
Err(_) => err::raw_error(err::HCF, "file size exceeds usize address space"),
}
}
#[inline(always)]
fn grow(&self, curr_len: usize, len_to_add: usize) -> FrozenResult<()> {
if len_to_add == 0 {
return Ok(());
}
let _ = match curr_len.checked_add(len_to_add) {
Some(len) => match off_t::try_from(len) {
Ok(off) if off >= 0 => off,
_ => {
return err::raw_error(err::GRW, "target file size exceeds off_t capacity");
}
},
None => {
return err::raw_error(err::GRW, "file growth calculation overflowed usize");
}
};
let fd = self.fd();
#[cfg(target_os = "linux")]
fallocate_raw(fd, curr_len, len_to_add)?;
#[cfg(target_os = "macos")]
f_preallocate_raw(fd, len_to_add)?;
ftruncate_raw(fd, curr_len, len_to_add)?;
Ok(())
}
#[cfg(target_os = "macos")]
fn sync(&self) -> FrozenResult<()> {
f_fullsync_raw(self.fd())
}
#[cfg(target_os = "linux")]
fn sync(&self) -> FrozenResult<()> {
fdatasync_raw(self.fd())
}
#[inline(always)]
fn pread(&self, buf: &mut [u8], offset: usize) -> FrozenResult<()> {
if buf.is_empty() {
return Ok(());
}
let fd = self.fd();
let mut read = 0usize;
let mut retries = 0usize;
while read < buf.len() {
let chunk_offset = match offset.checked_add(read) {
Some(off) if off <= off_t::MAX as usize => off as off_t,
_ => return err::raw_error(err::INV, "offset overflow"),
};
let res = unsafe {
pread(
fd,
buf[read..].as_mut_ptr() as *mut c_void,
(buf.len() - read) as size_t,
chunk_offset,
)
};
if res == 0 {
return err::default_error(err::HCF);
}
if hints::unlikely(res < 0) {
let errno = last_errno();
let err_msg = err_msg(errno);
match errno {
EINTR | EAGAIN | EBUSY => {
if retries < MAX_RETRIES {
retries += 1;
continue;
}
return err::raw_error(err::UNK, err_msg);
}
EACCES | EPERM => return err::raw_error(err::RED, err_msg),
EINVAL | EBADF | EFAULT | ESPIPE => {
return err::raw_error(err::HCF, err_msg);
}
_ => return err::raw_error(err::UNK, err_msg),
}
}
read += res as usize;
retries = 0;
}
Ok(())
}
#[inline(always)]
fn pwrite(&self, buf: &[u8], offset: usize) -> FrozenResult<()> {
if buf.is_empty() {
return Ok(());
}
let fd = self.fd();
let mut written = 0usize;
let mut retries = 0usize;
while written < buf.len() {
let chunk_offset = match offset.checked_add(written) {
Some(off) if off <= off_t::MAX as usize => off as off_t,
_ => return err::raw_error(err::INV, "offset overflow"),
};
let res = unsafe {
pwrite(
fd,
buf[written..].as_ptr() as *const c_void,
(buf.len() - written) as size_t,
chunk_offset,
)
};
if res == 0 {
if retries < MAX_RETRIES {
retries += 1;
continue;
}
return err::default_error(err::HCF);
}
if hints::unlikely(res < 0) {
let errno = last_errno();
let err_msg = err_msg(errno);
match errno {
EINTR | EAGAIN | EBUSY => {
if retries < MAX_RETRIES {
retries += 1;
continue;
}
return err::raw_error(err::UNK, err_msg);
}
EACCES | EPERM | EROFS => {
return err::raw_error(err::WRT, err_msg);
}
ENOSPC | libc::EDQUOT => {
return err::raw_error(err::NSP, err_msg);
}
EINVAL | EBADF | EFAULT | ESPIPE => {
return err::raw_error(err::HCF, err_msg);
}
_ => return err::raw_error(err::UNK, err_msg),
}
}
written += res as usize;
retries = 0;
}
Ok(())
}
}
impl POSIXFile {
#[cfg(target_os = "linux")]
pub(super) fn sync_range(&self, offset: usize, len: usize) -> FrozenResult<()> {
if len == 0 {
return Ok(());
}
sync_file_range_raw(self.fd(), offset, len)
}
}
impl Drop for POSIXFile {
fn drop(&mut self) {
let fd = self.fd.swap(CLOSED_FD, atomic::Ordering::AcqRel);
if fd != CLOSED_FD {
let _ = close_raw(fd);
}
}
}
fn open_raw(path: &std::path::Path, flags: c_int) -> FrozenResult<FileId> {
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);
let mut retries = 0; loop {
let fd = if flags & O_CREAT != 0 {
unsafe { open(cpath.as_ptr(), flags, perm) }
} else {
unsafe { 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 | EAGAIN | EBUSY => {
if retries < MAX_RETRIES {
retries += 1;
continue;
}
return err::raw_error(err::UNK, err_msg);
}
ENOSPC => return err::raw_error(err::NSP, err_msg),
EISDIR | ENOENT | ENOTDIR => {
return err::raw_error(err::INV, err_msg);
}
EEXIST => return err::raw_error(err::EXS, err_msg),
EACCES | EPERM | EROFS => {
return err::raw_error(err::PRM, err_msg);
}
_ => return err::raw_error(err::UNK, err_msg),
}
}
return Ok(fd);
}
}
fn close_raw(fd: FileId) -> FrozenResult<()> {
let res = unsafe { close(fd) };
if res == 0 {
return Ok(());
}
let errno = last_errno();
let err_msg = err_msg(errno);
if errno == EINTR {
return Ok(());
}
if errno == EIO {
return err::raw_error(err::HCF, err_msg);
}
err::raw_error(err::UNK, err_msg)
}
#[cfg(target_os = "linux")]
fn fdatasync_raw(fd: FileId) -> FrozenResult<()> {
let mut retries = 0; loop {
let res = unsafe { libc::fdatasync(fd) };
if hints::likely(res == 0) {
return Ok(());
}
let errno = last_errno();
let err_msg = err_msg(errno);
match errno {
EINVAL | EBADF => return err::raw_error(err::HCF, err_msg),
EROFS => return err::raw_error(err::PRM, err_msg),
EIO => return err::raw_error(err::SYN, err_msg),
EINTR => continue,
EAGAIN | EBUSY => {
if retries < MAX_RETRIES {
retries += 1;
continue;
}
return err::raw_error(err::SYN, err_msg);
}
_ => return err::raw_error(err::UNK, err_msg),
}
}
}
#[cfg(target_os = "macos")]
fn f_fullsync_raw(fd: FileId) -> FrozenResult<()> {
let mut retries = 0; loop {
let res = unsafe { libc::fcntl(fd, libc::F_FULLFSYNC) };
if hints::likely(res == 0) {
return Ok(());
}
let errno = last_errno();
let err_msg = err_msg(errno);
match errno {
EINTR => continue,
EAGAIN | EBUSY => {
if retries < MAX_RETRIES {
retries += 1;
continue;
}
return err::raw_error(err::SYN, err_msg);
}
libc::ENOTSUP | EOPNOTSUPP | EINVAL => break,
EBADF => return err::raw_error(err::HCF, err_msg),
EROFS => return err::raw_error(err::PRM, err_msg),
EIO => return err::raw_error(err::SYN, err_msg),
_ => return err::raw_error(err::UNK, err_msg),
}
}
fsync_raw(fd)
}
fn fsync_raw(fd: FileId) -> FrozenResult<()> {
let mut retries = 0; loop {
let res = unsafe { libc::fsync(fd) };
if hints::unlikely(res != 0) {
let errno = last_errno();
let err_msg = err_msg(errno);
match errno {
EINTR => continue,
EAGAIN | EBUSY => {
if retries < MAX_RETRIES {
retries += 1;
continue;
}
return err::raw_error(err::SYN, err_msg);
}
EBADF | EINVAL => return err::raw_error(err::HCF, err_msg),
EROFS => return err::raw_error(err::PRM, err_msg),
EIO => return err::raw_error(err::SYN, err_msg),
_ => return err::raw_error(err::UNK, err_msg),
}
}
return Ok(());
}
}
#[cfg(target_os = "linux")]
fn sync_file_range_raw(fd: FileId, offset: usize, len: usize) -> FrozenResult<()> {
if len == 0 {
return Ok(());
}
let flag = libc::SYNC_FILE_RANGE_WRITE;
let mut retries = 0;
loop {
let res = unsafe { 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 | EAGAIN | EBUSY => {
if retries < MAX_RETRIES {
retries += 1;
continue;
}
return err::raw_error(err::SYN, err_msg);
}
EBADF | EINVAL => return err::raw_error(err::HCF, err_msg),
EROFS => return err::raw_error(err::PRM, err_msg),
EIO => return err::raw_error(err::SYN, err_msg),
EOPNOTSUPP | libc::ENOSYS => return Ok(()),
_ => return err::raw_error(err::UNK, err_msg),
}
}
}
#[cfg(target_os = "linux")]
fn fallocate_raw(fd: FileId, curr_len: usize, len_to_add: usize) -> FrozenResult<()> {
if len_to_add == 0 {
return Ok(());
}
let offset = match off_t::try_from(curr_len) {
Ok(off) if off >= 0 => off,
_ => return err::raw_error(err::GRW, "offset exceeds off_t capacity"),
};
let length = match off_t::try_from(len_to_add) {
Ok(off) if off >= 0 => off,
_ => {
return err::raw_error(err::GRW, "len_to_add exceeds off_t capacity");
}
};
let mut retries = 0; loop {
let res = unsafe { libc::fallocate(fd, 0, offset, length) };
if hints::likely(res == 0) {
return Ok(());
}
let errno = last_errno();
let err_msg = err_msg(errno);
match errno {
EINTR | EAGAIN | EBUSY => {
if retries < MAX_RETRIES {
retries += 1;
continue;
}
return err::raw_error(err::GRW, err_msg);
}
EBADF | EINVAL => return err::raw_error(err::HCF, err_msg),
EROFS => return err::raw_error(err::PRM, err_msg),
ENOSPC => return err::raw_error(err::NSP, err_msg),
EOPNOTSUPP | libc::ENOSYS => return Ok(()),
_ => return err::raw_error(err::UNK, err_msg),
}
}
}
fn ftruncate_raw(fd: FileId, curr_len: usize, len_to_add: usize) -> FrozenResult<()> {
let new_len = match curr_len.checked_add(len_to_add) {
Some(len) => match off_t::try_from(len) {
Ok(off) if off >= 0 => off,
_ => {
return err::raw_error(err::GRW, "target file size exceeds off_t capacity");
}
},
None => {
return err::raw_error(err::GRW, "file length calculation overflowed usize");
}
};
let mut retries = 0;
loop {
let res = unsafe { ftruncate(fd, new_len) };
if hints::likely(res == 0) {
return Ok(());
}
let errno = last_errno();
let err_msg = err_msg(errno);
match errno {
EINTR | EAGAIN | EBUSY => {
if retries < MAX_RETRIES {
retries += 1;
continue;
}
return err::raw_error(err::GRW, err_msg);
}
EINVAL | EBADF => return err::raw_error(err::HCF, err_msg),
EROFS => return err::raw_error(err::PRM, err_msg),
ENOSPC => return err::raw_error(err::NSP, err_msg),
_ => return err::raw_error(err::UNK, err_msg),
}
}
}
#[cfg(target_os = "macos")]
fn f_preallocate_raw(fd: FileId, len_to_add: usize) -> FrozenResult<()> {
let mut retries = 0;
let length = match off_t::try_from(len_to_add) {
Ok(len) if len >= 0 => len,
_ => {
return err::raw_error(err::GRW, "len_to_add exceeds off_t capacity");
}
};
let mut store = libc::fstore_t {
fst_flags: libc::F_ALLOCATECONTIG,
fst_posmode: libc::F_PEOFPOSMODE,
fst_offset: 0,
fst_length: length,
fst_bytesalloc: 0,
};
loop {
let res = unsafe { libc::fcntl(fd, libc::F_PREALLOCATE, &store) };
if res == 0 {
return Ok(());
}
let errno = last_errno();
let err_msg = err_msg(errno);
match errno {
EINTR | EAGAIN | EBUSY => {
if retries < MAX_RETRIES {
retries += 1;
continue;
}
return err::raw_error(err::GRW, err_msg);
}
ENOSPC => {
if store.fst_flags == libc::F_ALLOCATECONTIG {
store.fst_flags = libc::F_ALLOCATEALL;
retries = 0;
continue;
}
return err::raw_error(err::NSP, err_msg);
}
EOPNOTSUPP | libc::ENOTSUP => return Ok(()),
EINVAL => return Ok(()),
EBADF => return err::raw_error(err::HCF, err_msg),
EROFS => return err::raw_error(err::PRM, err_msg),
_ => return err::raw_error(err::UNK, err_msg),
}
}
}
fn flock_raw(fd: FileId) -> FrozenResult<()> {
let mut retries = 0; loop {
let res = unsafe { flock(fd, LOCK_EX | LOCK_NB) };
if res == 0 {
return Ok(());
}
let errno = last_errno();
let err_msg = err_msg(errno);
match errno {
_ if errno == EWOULDBLOCK || errno == EAGAIN => {
return err::raw_error(err::LCK, err_msg);
}
EINTR => {
if retries < MAX_RETRIES {
retries += 1;
continue;
}
return err::raw_error(err::UNK, err_msg);
}
EBADF | EINVAL => return err::raw_error(err::HCF, err_msg),
ENOLCK => return err::raw_error(err::LEX, err_msg),
_ => return err::raw_error(err::UNK, err_msg),
}
}
}
fn sync_parent_dir(path: &std::path::Path) -> FrozenResult<()> {
let parent = extract_parent_dir(path);
let flags = O_RDONLY | O_DIRECTORY | O_CLOEXEC;
let fd = match open_raw(&parent, flags) {
Ok(fd) => fd,
Err(e) if e.reason == err::PRM.reason || e.reason == err::INV.reason => {
return Ok(());
}
Err(e) => return Err(e),
};
#[cfg(target_os = "linux")]
let res = fsync_raw(fd);
#[cfg(target_os = "macos")]
let res = f_fullsync_raw(fd);
let _ = close_raw(fd);
match res {
Err(e) if e.reason == err::HCF.reason || e.reason == err::UNK.reason => Ok(()),
other => other,
}
}
#[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 {
O_RDWR | O_CLOEXEC | O_CREAT
}
#[cfg(target_os = "linux")]
const fn create_flags() -> c_int {
O_RDWR | O_CLOEXEC | libc::O_NOATIME | O_CREAT | O_EXCL
}
#[cfg(target_os = "macos")]
const fn create_flags() -> c_int {
O_RDWR | O_CLOEXEC | O_CREAT | O_EXCL
}
#[cfg(target_os = "linux")]
const fn open_flags() -> c_int {
O_RDWR | O_CLOEXEC | libc::O_NOATIME
}
#[cfg(target_os = "macos")]
const fn open_flags() -> c_int {
O_RDWR | O_CLOEXEC
}
fn path_to_cstring(path: &std::path::Path) -> FrozenResult<std::ffi::CString> {
match std::ffi::CString::new(path.as_os_str().as_encoded_bytes()) {
Ok(cs) => Ok(cs),
Err(e) => err::raw_error(err::INV, e),
}
}
#[inline]
fn last_errno() -> i32 {
#[cfg(target_os = "linux")]
unsafe {
*libc::__errno_location()
}
#[cfg(target_os = "macos")]
unsafe {
*libc::__error()
}
}
#[inline]
fn err_msg(errno: i32) -> String {
std::io::Error::from_raw_os_error(errno).to_string()
}
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(),
}
}
#[inline]
#[cfg(target_os = "linux")]
fn f_advise_raw(fd: FileId) -> FrozenResult<()> {
let mut retries = 0;
loop {
let res = unsafe { libc::posix_fadvise(fd, 0, 0, libc::POSIX_FADV_RANDOM) };
if res == 0 {
return Ok(());
}
match res {
EINTR | EAGAIN | EBUSY => {
if retries < MAX_RETRIES {
retries += 1;
continue;
}
return Ok(());
}
EBADF => {
let err_msg = err_msg(res);
return err::raw_error(err::HCF, err_msg);
}
_ => return Ok(()),
}
}
}
#[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();
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();
let file1 = POSIXFile::new(&path).unwrap();
file1.close().unwrap();
let file2 = POSIXFile::new(&path).unwrap();
file2.close().unwrap();
}
#[test]
fn err_new_on_missing_parent_dir() {
let (_dir, path) = tmp_path();
let missing = path.join("missing/sub/dir/file");
let err = POSIXFile::new(&missing).unwrap_err();
assert_eq!(err.reason, err::INV.reason);
}
#[test]
fn err_new_on_directory() {
let dir = tempfile::tempdir().unwrap();
let err = POSIXFile::new(dir.path()).unwrap_err();
assert_eq!(err.reason, err::INV.reason);
}
#[test]
fn err_new_on_interior_nul() {
use std::{ffi::OsStr, os::unix::ffi::OsStrExt};
let bad_path = std::path::Path::new(OsStr::from_bytes(b"bad\0file.db"));
let err = POSIXFile::new(bad_path).unwrap_err();
assert_eq!(err.reason, err::INV.reason);
}
#[test]
fn err_new_on_permission_denied() {
use std::os::unix::fs::PermissionsExt;
let dir = tempfile::tempdir().unwrap();
let sub_dir = dir.path().join("readonly_dir");
std::fs::create_dir(&sub_dir).unwrap();
std::fs::set_permissions(&sub_dir, std::fs::Permissions::from_mode(0o555)).unwrap();
let target = sub_dir.join("forbidden.db");
let err = POSIXFile::new(&target).unwrap_err();
std::fs::set_permissions(&sub_dir, std::fs::Permissions::from_mode(0o755)).unwrap();
assert_eq!(err.reason, err::PRM.reason);
}
}
mod file_create_open {
use super::*;
#[test]
fn ok_create_close_cycle() {
let (_dir, path) = tmp_path();
let file = POSIXFile::create(&path).unwrap();
assert!(path.exists());
file.close().unwrap();
}
#[test]
fn err_create_when_already_exists() {
let (_dir, path) = tmp_path();
let file = POSIXFile::create(&path).unwrap();
let err = POSIXFile::create(&path).unwrap_err();
assert_eq!(err.reason, err::EXS.reason);
file.close().unwrap();
}
#[test]
fn err_create_on_missing_parent_dir() {
let (_dir, path) = tmp_path();
let missing = path.join("missing/sub/dir/file");
let err = POSIXFile::create(&missing).unwrap_err();
assert_eq!(err.reason, err::INV.reason);
}
#[test]
fn err_create_on_directory() {
let dir = tempfile::tempdir().unwrap();
let err = POSIXFile::create(dir.path()).unwrap_err();
assert_eq!(err.reason, err::EXS.reason);
}
#[test]
fn ok_open_existing_file() {
let (_dir, path) = tmp_path();
let file = POSIXFile::create(&path).unwrap();
file.close().unwrap();
let opened = POSIXFile::open(&path).unwrap();
opened.close().unwrap();
}
#[test]
fn err_open_on_missing_file() {
let (_dir, path) = tmp_path();
let err = POSIXFile::open(&path).unwrap_err();
assert_eq!(err.reason, err::INV.reason);
}
#[test]
fn err_open_on_directory() {
let dir = tempfile::tempdir().unwrap();
let err = POSIXFile::open(dir.path()).unwrap_err();
assert_eq!(err.reason, err::INV.reason);
}
}
mod file_unlink {
use super::*;
#[test]
fn ok_unlink_existing() {
let (_dir, path) = tmp_path();
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();
let file = POSIXFile::new(&path).unwrap();
file.unlink(&path).unwrap();
let missing_path = path.join("non_existent_file");
let file2 = POSIXFile {
fd: atomic::AtomicI32::new(unsafe {
libc::open(c"/dev/null".as_ptr() as *const _, libc::O_RDONLY)
}),
};
let err = file2.unlink(&missing_path).unwrap_err();
assert_eq!(err.reason, err::INV.reason);
}
#[test]
fn err_unlink_permission_denied() {
use std::os::unix::fs::PermissionsExt;
let dir = tempfile::tempdir().unwrap();
let sub_dir = dir.path().join("readonly_dir");
std::fs::create_dir(&sub_dir).unwrap();
let target = sub_dir.join("victim.db");
let file = POSIXFile::new(&target).unwrap();
std::fs::set_permissions(&sub_dir, std::fs::Permissions::from_mode(0o555)).unwrap();
let err = file.unlink(&target).unwrap_err();
std::fs::set_permissions(&sub_dir, std::fs::Permissions::from_mode(0o755)).unwrap();
assert_eq!(err.reason, err::PRM.reason);
}
}
mod file_lock {
use super::*;
#[test]
fn ok_flock_acquires_exclusive_lock() {
let (_dir, path) = tmp_path();
let file = POSIXFile::new(&path).unwrap();
file.flock().unwrap();
file.close().unwrap();
}
#[test]
fn err_flock_when_already_locked() {
let (_dir, path) = tmp_path();
let file1 = POSIXFile::new(&path).unwrap();
file1.flock().unwrap();
let file2 = POSIXFile::new(&path).unwrap();
let err = file2.flock().unwrap_err();
assert_eq!(err.reason, err::LCK.reason);
file1.close().unwrap();
file2.close().unwrap();
}
#[test]
fn ok_flock_released_after_close() {
let (_dir, path) = tmp_path();
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();
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);
file.close().unwrap();
}
#[test]
fn ok_grow_extends_with_zero() {
let (_dir, path) = tmp_path();
let file = POSIXFile::new(&path).unwrap();
file.grow(0, 0x500).unwrap();
let mut buf = vec![0u8; 0x500];
file.pread(&mut buf, 0).unwrap();
assert!(buf.iter().all(|b| *b == 0));
file.close().unwrap();
}
#[test]
fn ok_grow_len_zero() {
let (_dir, path) = tmp_path();
let file = POSIXFile::new(&path).unwrap();
file.grow(0, 0).unwrap();
assert_eq!(file.length().unwrap(), 0);
file.close().unwrap();
}
#[test]
fn err_grow_overflow_usize() {
let (_dir, path) = tmp_path();
let file = POSIXFile::new(&path).unwrap();
let err = file.grow(usize::MAX - 0x0A, 0x14).unwrap_err();
assert_eq!(err.reason, err::GRW.reason);
file.close().unwrap();
}
#[test]
fn err_grow_exceeds_off_t_max() {
let (_dir, path) = tmp_path();
let file = POSIXFile::new(&path).unwrap();
if let Ok(too_large) = usize::try_from(libc::off_t::MAX) {
if let Some(target) = too_large.checked_add(1) {
let err = file.grow(0, target).unwrap_err();
assert_eq!(err.reason, err::GRW.reason);
}
}
file.close().unwrap();
}
}
mod fil_sync {
use super::*;
#[test]
fn ok_sync() {
let (_dir, path) = tmp_path();
let file = POSIXFile::new(&path).unwrap();
file.sync().unwrap();
file.close().unwrap();
}
#[test]
fn ok_sync_after_sync() {
let (_dir, path) = tmp_path();
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();
let file = POSIXFile::new(&path).unwrap();
file.grow(0, 0x200).unwrap();
let data = b"grave_engine";
file.pwrite(data, 0x80).unwrap();
let mut buf = vec![0u8; data.len()];
file.pread(&mut buf, 0x80).unwrap();
assert_eq!(&buf[..], data);
file.close().unwrap();
}
#[test]
fn ok_pwrite_pread_across_sessions() {
let (_dir, path) = tmp_path();
{
let file = POSIXFile::new(&path).unwrap();
file.grow(0, 0x1000).unwrap();
let data = b"persist_me";
file.pwrite(data, 0).unwrap();
file.sync().unwrap();
file.close().unwrap();
}
{
let file = POSIXFile::new(&path).unwrap();
let mut buf = vec![0u8; 0x0A];
file.pread(&mut buf, 0).unwrap();
assert_eq!(&buf[..], b"persist_me");
file.close().unwrap();
}
}
#[test]
fn ok_pwrite_concurrent_non_overlapping() {
let (_dir, path) = tmp_path();
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 data = vec![i as u8; 0x100];
f.pwrite(&data, i * 0x100).unwrap();
}));
}
for h in handles {
h.join().unwrap();
}
file.sync().unwrap();
for i in 0..0x0A {
let mut buf = vec![0u8; 0x100];
file.pread(&mut buf, i * 0x100).unwrap();
assert!(buf.iter().all(|b| *b == i as u8));
}
}
#[test]
fn ok_pwrite_when_overlapping_last_wins() {
let (_dir, path) = tmp_path();
let file = POSIXFile::new(&path).unwrap();
file.grow(0, 0x100).unwrap();
let a = [1u8; 0x80];
let b = [2u8; 0x80];
file.pwrite(&a, 0).unwrap();
file.pwrite(&b, 0).unwrap();
let mut buf = vec![0u8; 0x80];
file.pread(&mut buf, 0).unwrap();
assert!(buf.iter().all(|b| *b == 2));
file.close().unwrap();
}
#[test]
fn ok_pread_zero_len() {
let (_dir, path) = tmp_path();
let file = POSIXFile::new(&path).unwrap();
let mut empty = [];
file.pread(&mut empty, 0).unwrap();
file.close().unwrap();
}
#[test]
fn ok_pwrite_zero_len() {
let (_dir, path) = tmp_path();
let file = POSIXFile::new(&path).unwrap();
let empty = [];
file.pwrite(&empty, 0).unwrap();
file.close().unwrap();
}
#[test]
fn err_pread_past_eof() {
let (_dir, path) = tmp_path();
let file = POSIXFile::new(&path).unwrap();
let mut buf = [0u8; 0x10];
let err = file.pread(&mut buf, 0).unwrap_err();
assert_eq!(err.reason, err::HCF.reason);
file.close().unwrap();
}
#[test]
fn err_pwrite_offset_exceeds_off_t() {
let (_dir, path) = tmp_path();
let file = POSIXFile::new(&path).unwrap();
let data = [1u8; 8];
let err = file.pwrite(&data, usize::MAX).unwrap_err();
assert_eq!(err.reason, err::INV.reason);
file.close().unwrap();
}
#[test]
fn err_pread_offset_exceeds_off_t() {
let (_dir, path) = tmp_path();
let file = POSIXFile::new(&path).unwrap();
let mut buf = [0u8; 8];
let err = file.pread(&mut buf, usize::MAX).unwrap_err();
assert_eq!(err.reason, err::INV.reason);
file.close().unwrap();
}
}
mod file_exists {
use super::*;
#[test]
fn ok_exists_true_on_existing_file() {
let (_dir, path) = tmp_path();
let file = POSIXFile::new(&path).unwrap();
assert!(POSIXFile::exists(&path).unwrap());
file.close().unwrap();
}
#[test]
fn ok_exists_false_on_missing_file() {
let (_dir, path) = tmp_path();
assert!(!POSIXFile::exists(&path).unwrap());
}
#[test]
fn ok_exists_false_on_enotdir() {
let (_dir, path) = tmp_path();
let file = POSIXFile::new(&path).unwrap();
file.close().unwrap();
let non_dir_child = path.join("child.db");
let exists = POSIXFile::exists(&non_dir_child).unwrap();
assert!(!exists);
}
#[test]
fn err_exists_on_permission_denied() {
use std::os::unix::fs::PermissionsExt;
let dir = tempfile::tempdir().unwrap();
let inaccessible_dir = dir.path().join("inaccessible");
std::fs::create_dir(&inaccessible_dir).unwrap();
std::fs::set_permissions(&inaccessible_dir, std::fs::Permissions::from_mode(0o000))
.unwrap();
let target = inaccessible_dir.join("secret.db");
let res = POSIXFile::exists(&target);
std::fs::set_permissions(&inaccessible_dir, std::fs::Permissions::from_mode(0o755))
.unwrap();
let err = res.unwrap_err();
assert_eq!(err.reason, err::PRM.reason);
}
#[test]
fn err_exists_on_symlink_loop() {
let dir = tempfile::tempdir().unwrap();
let loop_path = dir.path().join("loop_link");
std::os::unix::fs::symlink(&loop_path, &loop_path).unwrap();
let err = POSIXFile::exists(&loop_path).unwrap_err();
assert_eq!(err.reason, err::INV.reason);
}
}
mod file_lifecycle {
use super::*;
#[test]
fn err_length_after_closed() {
let file = POSIXFile { fd: atomic::AtomicI32::new(CLOSED_FD) };
let err = file.length().unwrap_err();
assert_eq!(err.reason, err::HCF.reason);
}
#[test]
fn err_pread_after_closed() {
let file = POSIXFile { fd: atomic::AtomicI32::new(CLOSED_FD) };
let mut buf = vec![0u8; 8];
let err = file.pread(&mut buf, 0).unwrap_err();
assert_eq!(err.reason, err::HCF.reason);
}
#[test]
fn err_pwrite_after_closed() {
let file = POSIXFile { fd: atomic::AtomicI32::new(CLOSED_FD) };
let data = b"dead";
let err = file.pwrite(data, 0).unwrap_err();
assert_eq!(err.reason, err::HCF.reason);
}
#[test]
fn err_sync_after_closed() {
let file = POSIXFile { fd: atomic::AtomicI32::new(CLOSED_FD) };
let err = file.sync().unwrap_err();
assert_eq!(err.reason, err::HCF.reason);
}
#[test]
fn err_grow_after_closed() {
let file = POSIXFile { fd: atomic::AtomicI32::new(CLOSED_FD) };
let err = file.grow(0, 0x100).unwrap_err();
assert_eq!(err.reason, err::HCF.reason);
}
#[test]
fn ok_drop_closes_descriptor() {
let (_dir, path) = tmp_path();
let fd = {
let file = POSIXFile::new(&path).unwrap();
let fd = file.fd();
assert_ne!(fd, CLOSED_FD);
fd
};
let res = unsafe { libc::close(fd) };
assert_eq!(res, -1);
let errno = last_errno();
assert_eq!(errno, libc::EBADF);
}
}
mod raw_syscalls {
use super::*;
#[test]
fn ok_sync_cycle() {
let (_dir, path) = tmp_path();
let file = POSIXFile::new(&path).unwrap();
file.grow(0, 0x400).unwrap();
let data = [7u8; 0x80];
file.pwrite(&data, 0).unwrap();
file.sync().unwrap();
let mut buf = vec![0u8; 0x80];
file.pread(&mut buf, 0).unwrap();
assert_eq!(buf, data);
file.close().unwrap();
}
#[cfg(target_os = "linux")]
#[test]
fn ok_sync_range() {
let (_dir, path) = tmp_path();
let file = POSIXFile::new(&path).unwrap();
file.grow(0, 0x1000).unwrap();
let data = [5u8; 0x100];
file.pwrite(&data, 0x200).unwrap();
file.sync_range(0x200, 0x100).unwrap();
file.sync().unwrap();
let mut buf = vec![0u8; 0x100];
file.pread(&mut buf, 0x200).unwrap();
assert_eq!(buf, data);
file.close().unwrap();
}
#[cfg(target_os = "linux")]
#[test]
fn ok_sync_range_zero_len() {
let (_dir, path) = tmp_path();
let file = POSIXFile::new(&path).unwrap();
assert!(file.sync_range(0, 0).is_ok());
assert!(file.sync_range(0x100, 0).is_ok());
file.close().unwrap();
}
#[test]
fn ok_write_read_at_eof_boundary() {
let (_dir, path) = tmp_path();
let file = POSIXFile::new(&path).unwrap();
file.grow(0, 0x200).unwrap();
let data = [3u8; 0x40];
file.pwrite(&data, 0x200 - 0x40).unwrap();
let mut buf = vec![0u8; 0x40];
file.pread(&mut buf, 0x200 - 0x40).unwrap();
assert_eq!(buf, data);
file.close().unwrap();
}
#[test]
fn ok_multiple_open_close_cycles() {
let (_dir, path) = tmp_path();
for _ in 0..0x0A {
let file = POSIXFile::new(&path).unwrap();
file.sync().unwrap();
file.close().unwrap();
}
}
#[test]
#[cfg(target_os = "linux")]
fn ok_f_advice_random() {
let (_dir, path) = tmp_path();
let file = POSIXFile::new(&path).unwrap();
f_advise_raw(file.fd()).unwrap();
file.close().unwrap();
}
#[test]
#[cfg(target_os = "linux")]
fn err_f_advise_closed_fd() {
let err = f_advise_raw(CLOSED_FD).unwrap_err();
assert_eq!(err.reason, err::HCF.reason);
}
#[test]
fn err_ftruncate_overflow() {
let (_dir, path) = tmp_path();
let file = POSIXFile::new(&path).unwrap();
let err = ftruncate_raw(file.fd(), usize::MAX - 0x0A, 0x14).unwrap_err();
assert_eq!(err.reason, err::GRW.reason);
file.close().unwrap();
}
#[test]
fn ok_sync_parent_dir() {
let (_dir, path) = tmp_path();
let file = POSIXFile::new(&path).unwrap();
sync_parent_dir(&path).unwrap();
file.close().unwrap();
}
#[cfg(target_os = "macos")]
#[test]
fn err_macos_preallocate_failure_preserves_length() {
let (_dir, path) = tmp_path();
let file = POSIXFile::new(&path).unwrap();
let initial_len = 0x1000;
file.grow(0, initial_len).unwrap();
assert_eq!(file.length().unwrap(), initial_len);
let impossible_len = (off_t::MAX / 2) as usize;
let res = file.grow(initial_len, impossible_len);
assert!(res.is_err(), "grow with impossible size must fail");
assert_eq!(file.length().unwrap(), initial_len);
file.close().unwrap();
}
}
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_last_errno() {
unsafe {
let _ = libc::close(-1);
assert_eq!(last_errno(), libc::EBADF);
}
}
#[test]
fn ok_err_msg() {
let msg = err_msg(libc::ENOENT);
assert!(!msg.is_empty(), "ENOENT must produce message");
}
}
}