#[cfg(any(target_os = "linux", target_os = "macos"))]
mod posix;
use crate::error::{FrozenErr, FrozenRes};
#[cfg(any(target_os = "linux", target_os = "macos"))]
pub type FFId = libc::c_int;
#[cfg(any(target_os = "linux", target_os = "macos"))]
type TFile = posix::POSIXFile;
const ERRDOMAIN: u8 = 0x11;
static mut MODULE_ID: u8 = 0;
#[repr(u16)]
pub enum FFileErr {
Hcf = 0x100,
Unk = 0x101,
Nsp = 0x102,
Syn = 0x103,
Prm = 0x104,
Inv = 0x105,
Cpt = 0x106,
Grw = 0x107,
Lck = 0x108,
Lex = 0x109,
}
impl FFileErr {
#[inline]
fn default_message(&self) -> &'static [u8] {
match self {
Self::Inv => b"invalid file path",
Self::Unk => b"unknown error type",
Self::Hcf => b"hault and catch fire",
Self::Grw => b"unable to grow the file",
Self::Prm => b"missing write/read permissions",
Self::Nsp => b"no space left on storage device",
Self::Cpt => b"file is either invalid or corrupted",
Self::Syn => b"failed to sync/flush data to storage device",
Self::Lex => b"failed to obtain lock, as no more locks available",
Self::Lck => b"failed to obtain exclusive lock, file may already be open",
}
}
}
#[inline]
pub(in crate::ffile) fn new_err<R>(res: FFileErr, message: Vec<u8>) -> FrozenRes<R> {
let detail = res.default_message();
let err = FrozenErr::new(unsafe { MODULE_ID }, ERRDOMAIN, res as u16, detail, message);
Err(err)
}
#[inline]
pub(in crate::ffile) fn new_err_default<R>(res: FFileErr) -> FrozenRes<R> {
let detail = res.default_message();
let err = FrozenErr::new(
unsafe { MODULE_ID },
ERRDOMAIN,
res as u16,
detail,
Vec::with_capacity(0),
);
Err(err)
}
#[derive(Debug, Clone)]
pub struct FFCfg {
pub mid: u8,
pub path: std::path::PathBuf,
pub chunk_size: usize,
pub initial_chunk_amount: usize,
}
#[derive(Debug)]
pub struct FrozenFile {
cfg: FFCfg,
file: core::cell::UnsafeCell<core::mem::ManuallyDrop<TFile>>,
}
unsafe impl Send for FrozenFile {}
unsafe impl Sync for FrozenFile {}
impl FrozenFile {
#[inline]
pub fn cfg(&self) -> &FFCfg {
&self.cfg
}
#[inline]
pub fn length(&self) -> FrozenRes<usize> {
unsafe { self.get_file().length() }
}
#[inline]
#[cfg(any(target_os = "linux", target_os = "macos"))]
pub fn fd(&self) -> i32 {
self.get_file().fd()
}
pub fn exists(&self) -> FrozenRes<bool> {
unsafe { TFile::exists(&self.cfg.path) }
}
pub fn new(cfg: FFCfg) -> FrozenRes<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() }?;
unsafe { MODULE_ID = cfg.mid };
let curr_len = slf.length()?;
let init_len = cfg.chunk_size * cfg.initial_chunk_amount;
match curr_len {
0 => slf.grow(cfg.initial_chunk_amount)?,
_ => {
if (curr_len < init_len) || (curr_len % cfg.chunk_size != 0) {
let _ = unsafe { file.close() };
return new_err_default(FFileErr::Cpt);
}
}
}
Ok(slf)
}
pub fn grow(&self, count: usize) -> FrozenRes<()> {
let curr_len = self.length()?;
let len_to_add = self.cfg.chunk_size * count;
unsafe { self.get_file().grow(curr_len, len_to_add) }
}
pub fn sync(&self) -> FrozenRes<()> {
let file = self.get_file();
unsafe { file.sync() }
}
#[cfg(target_os = "linux")]
pub fn sync_range(&self, index: usize, count: usize) -> FrozenRes<()> {
let offset = self.cfg.chunk_size * index;
let len_to_sync = self.cfg.chunk_size * count;
let file = self.get_file();
unsafe { file.sync_range(offset, len_to_sync) }
}
pub fn delete(&self) -> FrozenRes<()> {
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) -> FrozenRes<()> {
let offset = self.cfg.chunk_size * index;
let file = self.get_file();
unsafe { file.pread(buf, offset, self.cfg.chunk_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) -> FrozenRes<()> {
let offset = self.cfg.chunk_size * index;
let file = self.get_file();
unsafe { file.pwrite(buf, offset, self.cfg.chunk_size) }
}
#[inline(always)]
#[cfg(any(target_os = "linux", target_os = "macos"))]
pub fn preadv(&self, bufs: &[*mut u8], index: usize) -> FrozenRes<()> {
let offset = self.cfg.chunk_size * index;
let file = self.get_file();
unsafe { file.preadv(bufs, offset, self.cfg.chunk_size) }
}
#[inline(always)]
#[cfg(any(target_os = "linux", target_os = "macos"))]
pub fn pwritev(&self, bufs: &[*mut u8], index: usize) -> FrozenRes<()> {
let offset = self.cfg.chunk_size * index;
let file = self.get_file();
unsafe { file.pwritev(bufs, offset, self.cfg.chunk_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 crate::error::TEST_MID;
use std::sync::Arc;
const CHUNK_SIZE: usize = 0x10;
const INIT_CHUNKS: usize = 0x0A;
fn tmp_path() -> (tempfile::TempDir, FFCfg) {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("tmp_ff_file");
let cfg = FFCfg {
path,
mid: TEST_MID,
chunk_size: CHUNK_SIZE,
initial_chunk_amount: INIT_CHUNKS,
};
(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(), CHUNK_SIZE * INIT_CHUNKS);
}
#[test]
fn ok_new_existing() {
let (_dir, cfg) = tmp_path();
let file = FrozenFile::new(cfg.clone()).unwrap();
assert_eq!(file.length().unwrap(), CHUNK_SIZE * INIT_CHUNKS);
drop(file);
let reopened = FrozenFile::new(cfg.clone()).unwrap();
assert_eq!(reopened.length().unwrap(), CHUNK_SIZE * INIT_CHUNKS);
}
#[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.chunk_size *= 2;
let err = FrozenFile::new(cfg).unwrap_err();
assert!(err.compare(FFileErr::Cpt as u16));
}
#[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!(err.compare(FFileErr::Inv as u16));
}
#[test]
fn ok_drop_persists_without_explicit_sync() {
let mut data = [0x0Bu8; CHUNK_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; CHUNK_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!(err.compare(FFileErr::Lck as u16));
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(), CHUNK_SIZE * INIT_CHUNKS);
file.grow(0x20).unwrap();
assert_eq!(file.length().unwrap(), CHUNK_SIZE * (INIT_CHUNKS + 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(), CHUNK_SIZE * (INIT_CHUNKS + (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!(err.compare(FFileErr::Hcf as u16));
}
}
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; CHUNK_SIZE];
file.pwrite(data.as_mut_ptr(), 4).unwrap();
file.sync().unwrap();
let mut buf = [0u8; CHUNK_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; CHUNK_SIZE], [2u8; CHUNK_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; CHUNK_SIZE], [0u8; CHUNK_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_chunk_amount = 0x100;
let file = Arc::new(FrozenFile::new(cfg).unwrap());
let mut handles = vec![];
for i in 0..0x0A {
let f = file.clone();
handles.push(std::thread::spawn(move || {
let mut data = [i as u8; CHUNK_SIZE];
f.pwrite(data.as_mut_ptr(), i).unwrap();
}));
}
for h in handles {
h.join().unwrap();
}
file.sync().unwrap();
for i in 0..0x0A {
let mut buf = [0u8; CHUNK_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 = Arc::new(FrozenFile::new(cfg).unwrap());
let writer = {
let f = file.clone();
std::thread::spawn(move || {
for i in 0..INIT_CHUNKS {
let mut data = [i as u8; CHUNK_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(), CHUNK_SIZE * (INIT_CHUNKS + chunks_to_grow));
for i in 0..INIT_CHUNKS {
let mut buf = [0u8; CHUNK_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 = Arc::new(FrozenFile::new(cfg).unwrap());
let writer = {
let f = file.clone();
std::thread::spawn(move || {
for i in 0..INIT_CHUNKS {
let mut data = [i as u8; CHUNK_SIZE];
f.pwrite(data.as_mut_ptr(), i).unwrap();
}
})
};
let syncer = {
let f = file.clone();
std::thread::spawn(move || {
for _ in 0..0x0A {
f.sync().unwrap();
}
})
};
writer.join().unwrap();
syncer.join().unwrap();
file.sync().unwrap();
for i in 0..INIT_CHUNKS {
let mut buf = [0; CHUNK_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; CHUNK_SIZE];
let err = file.pread(buf.as_mut_ptr(), 0x100).unwrap_err();
assert!(err.compare(FFileErr::Hcf as u16));
}
}
}