#[cfg(any(target_os = "linux", target_os = "macos"))]
mod posix;
use crate::error::FrozenResult;
#[cfg(any(target_os = "linux", target_os = "macos"))]
pub type TFileId = libc::c_int;
#[cfg(any(target_os = "linux", target_os = "macos"))]
type TFile = posix::POSIXFile;
pub(in crate::ffile) mod err {
use crate::error::{ErrCode, FrozenError, FrozenResult};
const ERRDOMAIN: u8 = 0x11;
pub static MID: std::sync::OnceLock<u8> = std::sync::OnceLock::new();
#[cfg(not(test))]
#[inline(always)]
pub fn mid() -> &'static u8 {
MID.get().unwrap()
}
#[cfg(test)]
#[inline(always)]
pub fn mid() -> &'static u8 {
MID.get_or_init(|| 0)
}
pub const HCF: ErrCode = ErrCode::new(0x02, "hault and catch fire");
pub const UNK: ErrCode = ErrCode::new(0x04, "unknown error");
pub const NSP: ErrCode = ErrCode::new(0x08, "not enough space available on the storage device");
pub const SYN: ErrCode = ErrCode::new(0x0A, "failed to sync/flush data to storage device");
pub const WRT: ErrCode = ErrCode::new(0x0C, "missing permissions for write");
pub const RED: ErrCode = ErrCode::new(0x0E, "missing permissions for read");
pub const INV: ErrCode = ErrCode::new(0x10, "invalid path to file");
pub const CPT: ErrCode = ErrCode::new(0x12, "file is either invalid or corrupted");
pub const GRW: ErrCode = ErrCode::new(0x14, "unable to zero-extend file");
pub const LEX: ErrCode =
ErrCode::new(0x18, "failed to obtain lock, as no more locks available");
pub const PRM: ErrCode = ErrCode::new(0x1A, "missing permissions for IO");
pub const LCK: ErrCode =
ErrCode::new(0x1C, "failed to obtain exclusive lock as file may already opened");
#[inline]
pub(in crate::ffile) fn new_err<R, E: std::fmt::Display>(
code: ErrCode,
error: E,
) -> FrozenResult<R> {
let err = FrozenError::new_raw(*mid(), ERRDOMAIN, code, error);
Err(err)
}
#[inline]
pub(in crate::ffile) fn new_err_default<R>(code: ErrCode) -> FrozenResult<R> {
let err = FrozenError::new(*mid(), ERRDOMAIN, code, "");
Err(err)
}
}
#[derive(Debug, Clone)]
pub struct FrozenFileCfg {
pub module_id: u8,
pub path: std::path::PathBuf,
pub buffer_size: usize,
pub initial_available_buffers: usize,
}
#[derive(Debug)]
pub struct FrozenFile {
cfg: FrozenFileCfg,
file: core::cell::UnsafeCell<core::mem::ManuallyDrop<TFile>>,
}
unsafe impl Send for FrozenFile {}
unsafe impl Sync for FrozenFile {}
impl FrozenFile {
#[inline]
pub fn length(&self) -> FrozenResult<usize> {
unsafe { self.get_file().length() }
}
#[inline]
#[cfg(any(target_os = "linux", target_os = "macos"))]
pub fn fd(&self) -> TFileId {
self.get_file().fd()
}
pub fn exists(&self) -> FrozenResult<bool> {
unsafe { TFile::exists(&self.cfg.path) }
}
pub fn new(cfg: FrozenFileCfg) -> FrozenResult<Self> {
let raw_file = unsafe { posix::POSIXFile::new(&cfg.path) }?;
let slf = Self {
cfg: cfg.clone(),
file: core::cell::UnsafeCell::new(core::mem::ManuallyDrop::new(raw_file)),
};
let file = slf.get_file();
unsafe { file.flock() }?;
let _ = err::MID.get_or_init(|| cfg.module_id);
let curr_len = slf.length()?;
let init_len = cfg.buffer_size * cfg.initial_available_buffers;
match curr_len {
0 => slf.grow(cfg.initial_available_buffers)?,
_ => {
if (curr_len < init_len) || (curr_len % cfg.buffer_size != 0) {
let _ = unsafe { file.close() };
return err::new_err_default(err::CPT);
}
}
}
Ok(slf)
}
pub fn sync(&self) -> FrozenResult<()> {
let file = self.get_file();
unsafe { file.sync() }
}
#[cfg(target_os = "linux")]
pub fn sync_range(&self, index: usize, count: usize) -> FrozenResult<()> {
let offset = self.cfg.buffer_size * index;
let len_to_sync = self.cfg.buffer_size * count;
let file = self.get_file();
unsafe { file.sync_range(offset, len_to_sync) }
}
pub fn delete(&self) -> FrozenResult<()> {
let file = self.get_file();
unsafe { file.unlink(&self.cfg.path) }
}
#[inline(always)]
#[allow(clippy::not_unsafe_ptr_arg_deref)]
#[cfg(any(target_os = "linux", target_os = "macos"))]
pub fn pread(&self, buf: *mut u8, index: usize) -> FrozenResult<()> {
let offset = self.cfg.buffer_size * index;
let file = self.get_file();
unsafe { file.pread(buf, offset, self.cfg.buffer_size) }
}
#[inline(always)]
#[allow(clippy::not_unsafe_ptr_arg_deref)]
#[cfg(any(target_os = "linux", target_os = "macos"))]
pub fn pwrite(&self, buf: *mut u8, index: usize) -> FrozenResult<()> {
let offset = self.cfg.buffer_size * index;
let file = self.get_file();
unsafe { file.pwrite(buf, offset, self.cfg.buffer_size) }
}
#[inline(always)]
#[cfg(any(target_os = "linux", target_os = "macos"))]
pub fn preadv(&self, bufs: &[*mut u8], index: usize) -> FrozenResult<()> {
let offset = self.cfg.buffer_size * index;
let file = self.get_file();
unsafe { file.preadv(bufs, offset, self.cfg.buffer_size) }
}
#[inline(always)]
#[cfg(any(target_os = "linux", target_os = "macos"))]
pub fn pwritev(&self, bufs: &[*mut u8], index: usize) -> FrozenResult<()> {
let offset = self.cfg.buffer_size * index;
let file = self.get_file();
unsafe { file.pwritev(bufs, offset, self.cfg.buffer_size) }
}
pub fn grow(&self, count: usize) -> FrozenResult<()> {
let curr_len = self.length()?;
let len_to_add = self.cfg.buffer_size * count;
unsafe { self.get_file().grow(curr_len, len_to_add) }
}
#[inline]
pub fn total_chunks(&self) -> FrozenResult<usize> {
let curr_len = self.length()?;
let buffer_size = self.cfg.buffer_size;
if crate::hints::unlikely(curr_len % buffer_size != 0) {
return err::new_err_default(err::CPT);
}
Ok(curr_len / buffer_size)
}
#[inline]
fn get_file(&self) -> &core::mem::ManuallyDrop<TFile> {
unsafe { &*self.file.get() }
}
}
impl Drop for FrozenFile {
fn drop(&mut self) {
#[cfg(any(target_os = "linux", target_os = "macos"))]
if self.fd() == posix::CLOSED_FD {
return;
}
let _ = self.sync();
let _ = unsafe { self.get_file().close() };
}
}
impl core::fmt::Display for FrozenFile {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
write!(f, "FrozenFile {{fd: {}, len: {}}}", self.fd(), self.length().unwrap_or(0),)
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync;
const MID: u8 = 0;
const INIT_BUFFERS: usize = 4;
const BUFFER_SIZE: usize = 0x10;
fn tmp_path() -> (tempfile::TempDir, FrozenFileCfg) {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("tmp_ff_file");
let cfg = FrozenFileCfg {
path,
module_id: MID,
buffer_size: BUFFER_SIZE,
initial_available_buffers: INIT_BUFFERS,
};
(dir, cfg)
}
mod ff_lifecycle {
use super::*;
#[test]
fn ok_new_with_init_len() {
let (_dir, cfg) = tmp_path();
let file = FrozenFile::new(cfg).unwrap();
let exists = file.exists().unwrap();
assert!(exists);
assert_eq!(file.length().unwrap(), BUFFER_SIZE * INIT_BUFFERS);
}
#[test]
fn ok_new_existing() {
let (_dir, cfg) = tmp_path();
let file = FrozenFile::new(cfg.clone()).unwrap();
assert_eq!(file.length().unwrap(), BUFFER_SIZE * INIT_BUFFERS);
drop(file);
let reopened = FrozenFile::new(cfg.clone()).unwrap();
assert_eq!(reopened.length().unwrap(), BUFFER_SIZE * INIT_BUFFERS);
}
#[test]
fn err_new_when_file_smaller_than_init_len() {
let (_dir, mut cfg) = tmp_path();
let file = FrozenFile::new(cfg.clone()).unwrap();
drop(file);
cfg.buffer_size *= 2;
let err = FrozenFile::new(cfg).unwrap_err();
assert_eq!(err.reason, err::CPT.reason);
}
#[test]
fn ok_exists_true_when_exists() {
let (_dir, cfg) = tmp_path();
let file = FrozenFile::new(cfg).unwrap();
let exists = file.exists().unwrap();
assert!(exists);
}
#[test]
fn ok_exists_false_when_missing() {
let (_dir, cfg) = tmp_path();
let file = FrozenFile::new(cfg).unwrap();
file.delete().unwrap();
let exists = file.exists().unwrap();
assert!(!exists);
}
#[test]
fn ok_delete_file() {
let (_dir, cfg) = tmp_path();
let file = FrozenFile::new(cfg).unwrap();
let exists = file.exists().unwrap();
assert!(exists);
file.delete().unwrap();
let exists = file.exists().unwrap();
assert!(!exists);
}
#[test]
fn err_delete_after_delete() {
let (_dir, cfg) = tmp_path();
let file = FrozenFile::new(cfg).unwrap();
file.delete().unwrap();
let err = file.delete().unwrap_err();
assert_eq!(err.reason, err::INV.reason);
}
#[test]
fn ok_drop_persists_without_explicit_sync() {
let mut data = [0x0Bu8; BUFFER_SIZE];
let (_dir, cfg) = tmp_path();
{
let file = FrozenFile::new(cfg.clone()).unwrap();
file.pwrite(data.as_mut_ptr(), 0).unwrap();
drop(file);
}
{
let reopened = FrozenFile::new(cfg).unwrap();
let mut buf = [0u8; BUFFER_SIZE];
reopened.pread(buf.as_mut_ptr(), 0).unwrap();
assert_eq!(buf, data);
}
}
}
mod ff_lock {
use super::*;
#[test]
fn err_new_when_already_open() {
let (_dir, cfg) = tmp_path();
let file = FrozenFile::new(cfg.clone()).unwrap();
let err = FrozenFile::new(cfg).unwrap_err();
assert_eq!(err.reason, err::LCK.reason);
drop(file);
}
#[test]
fn ok_drop_releases_exclusive_lock() {
let (_dir, cfg) = tmp_path();
let file = FrozenFile::new(cfg.clone()).unwrap();
drop(file);
let _ = FrozenFile::new(cfg).expect("must not fail after drop");
}
}
mod ff_grow {
use super::*;
#[test]
fn ok_grow_updates_length() {
let (_dir, cfg) = tmp_path();
let file = FrozenFile::new(cfg).unwrap();
assert_eq!(file.length().unwrap(), BUFFER_SIZE * INIT_BUFFERS);
file.grow(0x20).unwrap();
assert_eq!(file.length().unwrap(), BUFFER_SIZE * (INIT_BUFFERS + 0x20));
}
#[test]
fn ok_grow_sync_cycle() {
let (_dir, cfg) = tmp_path();
let file = FrozenFile::new(cfg).unwrap();
for _ in 0..0x0A {
file.grow(0x100).unwrap();
file.sync().unwrap();
}
assert_eq!(file.length().unwrap(), BUFFER_SIZE * (INIT_BUFFERS + (0x0A * 0x100)));
}
}
mod ff_sync {
use super::*;
#[test]
fn ok_sync_after_sync() {
let (_dir, cfg) = tmp_path();
let file = FrozenFile::new(cfg).unwrap();
file.sync().unwrap();
file.sync().unwrap();
file.sync().unwrap();
}
#[test]
fn err_sync_after_delete() {
let (_dir, cfg) = tmp_path();
let file = FrozenFile::new(cfg).unwrap();
file.delete().unwrap();
let err = file.sync().unwrap_err();
assert_eq!(err.reason, err::HCF.reason);
}
}
mod ff_write_read {
use super::*;
#[test]
fn ok_single_write_read_cycle() {
let (_dir, cfg) = tmp_path();
let file = FrozenFile::new(cfg).unwrap();
let mut data = [0x0Bu8; BUFFER_SIZE];
file.pwrite(data.as_mut_ptr(), 4).unwrap();
file.sync().unwrap();
let mut buf = [0u8; BUFFER_SIZE];
file.pread(buf.as_mut_ptr(), 4).unwrap();
assert_eq!(buf, data);
}
#[test]
fn ok_vectored_write_read_cycle() {
let (_dir, cfg) = tmp_path();
let file = FrozenFile::new(cfg).unwrap();
let mut bufs = [[1u8; BUFFER_SIZE], [2u8; BUFFER_SIZE]];
let bufs: Vec<*mut u8> = bufs.iter_mut().map(|b| b.as_mut_ptr()).collect();
file.pwritev(&bufs, 0).unwrap();
file.sync().unwrap();
let mut read_bufs = [[0u8; BUFFER_SIZE], [0u8; BUFFER_SIZE]];
let rbufs: Vec<*mut u8> = read_bufs.iter_mut().map(|b| b.as_mut_ptr()).collect();
file.preadv(&rbufs, 0).unwrap();
assert!(read_bufs[0].iter().all(|b| *b == 1));
assert!(read_bufs[1].iter().all(|b| *b == 2));
}
#[test]
fn ok_write_concurrent_non_overlapping() {
let (_dir, mut cfg) = tmp_path();
cfg.initial_available_buffers = 0x100;
let file = sync::Arc::new(FrozenFile::new(cfg).unwrap());
let mut handles = vec![];
for i in 0..2 {
let f = file.clone();
handles.push(std::thread::spawn(move || {
let mut data = [i as u8; BUFFER_SIZE];
f.pwrite(data.as_mut_ptr(), i).unwrap();
}));
}
for h in handles {
h.join().unwrap();
}
file.sync().unwrap();
for i in 0..2 {
let mut buf = [0u8; BUFFER_SIZE];
file.pread(buf.as_mut_ptr(), i).unwrap();
assert!(buf.iter().all(|b| *b == i as u8));
}
}
#[test]
fn ok_concurrent_grow_and_write() {
let (_dir, cfg) = tmp_path();
let file = sync::Arc::new(FrozenFile::new(cfg).unwrap());
let writer = {
let f = file.clone();
std::thread::spawn(move || {
for i in 0..INIT_BUFFERS {
let mut data = [i as u8; BUFFER_SIZE];
f.pwrite(data.as_mut_ptr(), i).unwrap();
}
})
};
let chunks_to_grow = 0x20;
let grower = {
let f = file.clone();
std::thread::spawn(move || {
f.grow(chunks_to_grow).unwrap();
})
};
writer.join().unwrap();
grower.join().unwrap();
file.sync().unwrap();
assert_eq!(file.length().unwrap(), BUFFER_SIZE * (INIT_BUFFERS + chunks_to_grow));
for i in 0..INIT_BUFFERS {
let mut buf = [0u8; BUFFER_SIZE];
file.pread(buf.as_mut_ptr(), i).unwrap();
assert!(buf.iter().all(|b| *b == i as u8));
}
}
#[test]
fn ok_concurrent_sync_and_write() {
let (_dir, cfg) = tmp_path();
let file = sync::Arc::new(FrozenFile::new(cfg).unwrap());
let writer = {
let f = file.clone();
std::thread::spawn(move || {
for i in 0..INIT_BUFFERS {
let mut data = [i as u8; BUFFER_SIZE];
f.pwrite(data.as_mut_ptr(), i).unwrap();
}
})
};
let syncer = {
let f = file.clone();
std::thread::spawn(move || {
for _ in 0..2 {
f.sync().unwrap();
}
})
};
writer.join().unwrap();
syncer.join().unwrap();
file.sync().unwrap();
for i in 0..INIT_BUFFERS {
let mut buf = [0; BUFFER_SIZE];
file.pread(buf.as_mut_ptr(), i).unwrap();
assert!(buf.iter().all(|b| *b == i as u8));
}
}
#[test]
fn err_read_hcf_for_eof() {
let (_dir, cfg) = tmp_path();
let file = FrozenFile::new(cfg).unwrap();
let mut buf = [0; BUFFER_SIZE];
let err = file.pread(buf.as_mut_ptr(), 0x100).unwrap_err();
assert_eq!(err.reason, err::HCF.reason);
}
}
}