Skip to main content

frozen_core/ffile/
mod.rs

1//! Custom implementation of `std::fs::File`
2//!
3//! ## Example
4//!
5//! ```
6//! use frozen_core::ffile::{FrozenFile, FFCfg};
7//!
8//! const MID: u8 = 0;
9//!
10//! let dir = tempfile::tempdir().unwrap();
11//! let path = dir.path().join("tmp_frozen_file");
12//!
13//! let cfg = FFCfg {
14//!     chunk_size: 0x10,
15//!     path: path.to_path_buf(),
16//!     initial_chunk_amount: 0x0A,
17//! };
18//!
19//! let file = FrozenFile::new::<MID>(cfg.clone()).unwrap();
20//! assert_eq!(file.length().unwrap(), 0x10 * 0x0A);
21//!
22//! let mut data = vec![1u8; 0x10];
23//! assert!(file.pwrite(data.as_mut_ptr(), 0).is_ok());
24//! assert!(file.sync().is_ok());
25//!
26//! let mut buf = vec![0u8; data.len()];
27//! assert!(file.pread(buf.as_mut_ptr(), 0).is_ok());
28//! assert_eq!(buf, data);
29//!
30//! assert!(FrozenFile::new::<MID>(cfg.clone()).is_err());
31//!
32//! assert!(file.delete().is_ok());
33//! assert!(!path.exists());
34//!
35//! drop(file);
36//! assert!(FrozenFile::new::<MID>(cfg).is_ok());
37//! ```
38
39#[cfg(any(target_os = "linux", target_os = "macos"))]
40mod posix;
41
42use crate::error::{ErrCode, FrozenError, FrozenResult};
43
44/// Domain Id for [`FrozenFile`] is **17**
45const ERRDOMAIN: u8 = 0x11;
46
47/// File descriptor of [`FrozenFile`]
48#[cfg(any(target_os = "linux", target_os = "macos"))]
49pub type TFileId = libc::c_int;
50
51#[cfg(any(target_os = "linux", target_os = "macos"))]
52type TFile = posix::POSIXFile;
53
54/// module id used for [`FrozenError`]
55static MID: std::sync::OnceLock<u8> = std::sync::OnceLock::new();
56
57#[cfg(not(test))]
58#[inline(always)]
59fn mid() -> &'static u8 {
60    MID.get().unwrap()
61}
62
63#[cfg(test)]
64#[inline(always)]
65fn mid() -> &'static u8 {
66    MID.get_or_init(|| 0)
67}
68
69/// Error codes for [`FrozenFile`]
70pub(in crate::ffile) mod err {
71    use super::ErrCode;
72
73    /// (256) internal fuck up (hault and catch fire)
74    pub const HCF: ErrCode = ErrCode::new(0x100, "hault and catch fire");
75
76    /// (257) unknown error (fallback)
77    pub const UNK: ErrCode = ErrCode::new(0x101, "unknown error");
78
79    /// (258) no more space available
80    pub const NSP: ErrCode = ErrCode::new(0x102, "not enough space available on the storage device");
81
82    /// (259) syncing error
83    pub const SYN: ErrCode = ErrCode::new(0x103, "failed to sync/flush data to storage device");
84
85    /// (260) no write perm
86    pub const WRT: ErrCode = ErrCode::new(0x104, "missing permissions for write");
87
88    /// (261) no read perm
89    pub const RED: ErrCode = ErrCode::new(0x105, "missing permissions for read");
90
91    /// (262) invalid file path
92    pub const INV: ErrCode = ErrCode::new(0x106, "invalid path to file");
93
94    /// (263) corrupted file
95    pub const CPT: ErrCode = ErrCode::new(0x107, "file is either invalid or corrupted");
96
97    /// (264) unable to grow
98    pub const GRW: ErrCode = ErrCode::new(0x108, "unable to zero-extend file");
99
100    /// (265) unable to obtain exclusive lock
101    pub const LCK: ErrCode = ErrCode::new(0x109, "failed to obtain exclusive lock as file may already opened");
102
103    /// (266) locks exhausted (mainly on nfs)
104    pub const LEX: ErrCode = ErrCode::new(0x10A, "failed to obtain lock, as no more locks available");
105
106    /// (267) no write/read perm
107    pub const PRM: ErrCode = ErrCode::new(0x10B, "missing permissions for IO");
108}
109
110#[inline]
111pub(in crate::ffile) fn new_err<R, E: std::fmt::Display>(code: ErrCode, error: E) -> FrozenResult<R> {
112    let err = FrozenError::new_raw(*mid(), ERRDOMAIN, code, error);
113    Err(err)
114}
115
116#[inline]
117pub(in crate::ffile) fn new_err_default<R>(code: ErrCode) -> FrozenResult<R> {
118    let err = FrozenError::new(*mid(), ERRDOMAIN, code, "");
119    Err(err)
120}
121
122/// Config for [`FrozenFile`]
123#[derive(Debug, Clone)]
124pub struct FFCfg {
125    /// Absolute path for/of the file
126    ///
127    /// *NOTE:* The caller must make sure that the path represents a file and all the parent directories included in
128    /// the path do exists
129    pub path: std::path::PathBuf,
130
131    /// Size (in bytes) of a single chunk in file
132    ///
133    /// A chunk is a small fixed size allocation and addressing unit used by [`FrozenFile`] for all the write/read ops.
134    /// These ops are operated by index of the chunk and not the offset of the byte
135    ///
136    /// *NOTE:* Chunk size when power of 2, is cache efficient and good for performance
137    pub chunk_size: usize,
138
139    /// Number of chunks to pre-allocate on fs when [`FrozenFile`] is initialized
140    ///
141    /// Initial file length will be `chunk_size * initial_chunk_amount` (bytes)
142    pub initial_chunk_amount: usize,
143}
144
145/// Custom implementation of `std::fs::File`
146///
147/// ## Example
148///
149/// ```
150/// use frozen_core::ffile::{FrozenFile, FFCfg};
151///
152/// const MID: u8 = 0;
153///
154/// let dir = tempfile::tempdir().unwrap();
155/// let path = dir.path().join("tmp_frozen_file");
156///
157/// let cfg = FFCfg {
158///     chunk_size: 0x10,
159///     path: path.to_path_buf(),
160///     initial_chunk_amount: 0x0A,
161/// };
162///
163/// let file = FrozenFile::new::<MID>(cfg.clone()).unwrap();
164/// assert_eq!(file.length().unwrap(), 0x10 * 0x0A);
165///
166/// let mut data = vec![1u8; 0x10];
167/// assert!(file.pwrite(data.as_mut_ptr(), 0).is_ok());
168/// assert!(file.sync().is_ok());
169///
170/// let mut buf = vec![0u8; data.len()];
171/// assert!(file.pread(buf.as_mut_ptr(), 0).is_ok());
172/// assert_eq!(buf, data);
173///
174/// assert!(FrozenFile::new::<MID>(cfg.clone()).is_err());
175///
176/// assert!(file.delete().is_ok());
177/// assert!(!path.exists());
178///
179/// drop(file);
180/// assert!(FrozenFile::new::<MID>(cfg).is_ok());
181/// ```
182#[derive(Debug)]
183pub struct FrozenFile {
184    cfg: FFCfg,
185    file: core::cell::UnsafeCell<core::mem::ManuallyDrop<TFile>>,
186}
187
188unsafe impl Send for FrozenFile {}
189unsafe impl Sync for FrozenFile {}
190
191impl FrozenFile {
192    /// Read current length of [`FrozenFile`]
193    #[inline]
194    pub fn length(&self) -> FrozenResult<usize> {
195        unsafe { self.get_file().length() }
196    }
197
198    /// Get file descriptor for [`FrozenFile`]
199    #[inline]
200    #[cfg(any(target_os = "linux", target_os = "macos"))]
201    pub fn fd(&self) -> TFileId {
202        self.get_file().fd()
203    }
204
205    /// Check if the [`FrozenFile`] exists on the fs
206    pub fn exists(&self) -> FrozenResult<bool> {
207        unsafe { TFile::exists(&self.cfg.path) }
208    }
209
210    /// Create a new or open an existing [`FrozenFile`]
211    ///
212    /// ## [`FFCfg`]
213    ///
214    /// All configs for [`FrozenFile`] are stored in [`FFCfg`]
215    ///
216    /// ## Important
217    ///
218    /// The provided [`FFCfg`] must remain identical across all reopen cycles of the [`FrozenFile`].
219    ///
220    /// Changing any of the feilds after initial creation, may violate internal layout invariants and
221    /// cause the file to be treated as corrupted.
222    ///
223    /// ## Multiple Instances
224    ///
225    /// Every instance of [`FrozenFile`] tries to acquire an exclusive lock, which protects against operating with
226    /// multiple simultenious instances.
227    ///
228    /// If trying to call [`FrozenFile::new`] when already called, [`FFileErr::Lck`] error will be thrown.
229    ///
230    /// ## Example
231    ///
232    /// ```
233    /// use frozen_core::ffile::{FrozenFile, FFCfg};
234    ///
235    /// const MID: u8 = 0;
236    ///
237    /// let dir = tempfile::tempdir().unwrap();
238    /// let path = dir.path().join("tmp_frozen_file");
239    ///
240    /// let cfg = FFCfg {
241    ///     chunk_size: 0x10,
242    ///     path: path.to_path_buf(),
243    ///     initial_chunk_amount: 0x0A,
244    /// };
245    ///
246    /// let file = FrozenFile::new::<MID>(cfg).unwrap();
247    /// assert_eq!(file.length().unwrap(), 0x10 * 0x0A);
248    /// ```
249    pub fn new<const MODULE_ID: u8>(cfg: FFCfg) -> FrozenResult<Self> {
250        let raw_file = unsafe { posix::POSIXFile::new(&cfg.path) }?;
251        let slf = Self { cfg: cfg.clone(), file: core::cell::UnsafeCell::new(core::mem::ManuallyDrop::new(raw_file)) };
252
253        let file = slf.get_file();
254
255        // INFO: right after open is successful, we must obtain an exclusive lock on the entire file. So the
256        // another instance of [`FrozenFile`] will try to access the same lock, would correctly fail with
257        // [`FFileErr::Lck`] error.
258        unsafe { file.flock() }?;
259
260        // NOTE: The value is used for error logging and is initialized only once, as `OnceLock` guarantees that the
261        // first caller sets the value and all subsequent calls reuse it
262        let _ = MID.get_or_init(|| MODULE_ID);
263
264        let curr_len = slf.length()?;
265        let init_len = cfg.chunk_size * cfg.initial_chunk_amount;
266
267        match curr_len {
268            0 => slf.grow(cfg.initial_chunk_amount)?,
269            _ => {
270                // NOTE: we can treat these invariants as errors only because, our system guarantees,
271                // whenever file size is updated, i.e. has grown, it'll always be a multiple of `chunk_size`,
272                // and will have minimum of `chunk_size * initial_chunk_amount` (bytes) as it's length, although
273                // it only holds true when any of the params in provided `cfg` are never updated after the initial
274                // creation of the file
275                //
276                // INFO: when true, we close the file to avoid resource leaks
277                if (curr_len < init_len) || (curr_len % cfg.chunk_size != 0) {
278                    // NOTE: we supress the close error as we are already in an errored state
279                    let _ = unsafe { file.close() };
280                    return new_err_default(err::CPT);
281                }
282            }
283        }
284
285        Ok(slf)
286    }
287
288    /// Syncs in-mem data on the storage device
289    pub fn sync(&self) -> FrozenResult<()> {
290        let file = self.get_file();
291        unsafe { file.sync() }
292    }
293
294    /// A best-effort call to prompt kernel to start flushing dirty pages in the specified range
295    ///
296    /// This call, by itself, does not guarantee any kind of durability, and must always be paired with
297    /// strong sync call i.e. [`FrozenFile::sync`]
298    #[cfg(target_os = "linux")]
299    pub fn sync_range(&self, index: usize, count: usize) -> FrozenResult<()> {
300        let offset = self.cfg.chunk_size * index;
301        let len_to_sync = self.cfg.chunk_size * count;
302        let file = self.get_file();
303
304        unsafe { file.sync_range(offset, len_to_sync) }
305    }
306
307    /// Delete [`FrozenFile`] from fs
308    ///
309    /// ## Example
310    ///
311    /// ```
312    /// use frozen_core::ffile::{FrozenFile, FFCfg};
313    ///
314    /// const MID: u8 = 0;
315    ///
316    /// let dir = tempfile::tempdir().unwrap();
317    /// let path = dir.path().join("tmp_frozen_file");
318    ///
319    /// let cfg = FFCfg {
320    ///     chunk_size: 0x10,
321    ///     path: path.to_path_buf(),
322    ///     initial_chunk_amount: 0x0A,
323    /// };
324    ///
325    /// let file = FrozenFile::new::<MID>(cfg).unwrap();
326    /// assert!(file.exists().unwrap());
327    ///
328    /// file.delete().unwrap();
329    /// assert!(!file.exists().unwrap());
330    /// ```
331    pub fn delete(&self) -> FrozenResult<()> {
332        let file = self.get_file();
333        unsafe { file.unlink(&self.cfg.path) }
334    }
335
336    /// Read a single chunk at given `index` w/ `pread` syscall
337    ///
338    /// ## Example
339    ///
340    /// ```
341    /// use frozen_core::ffile::{FrozenFile, FFCfg};
342    ///
343    /// const MID: u8 = 0;
344    ///
345    /// let dir = tempfile::tempdir().unwrap();
346    /// let path = dir.path().join("tmp_frozen_file");
347    ///
348    /// let cfg = FFCfg {
349    ///     chunk_size: 0x10,
350    ///     path: path.to_path_buf(),
351    ///     initial_chunk_amount: 0x0A,
352    /// };
353    ///
354    /// let file = FrozenFile::new::<MID>(cfg).unwrap();
355    ///
356    /// let mut data = [7u8; 0x10];
357    /// file.pwrite(data.as_mut_ptr(), 2).unwrap();
358    /// file.sync().unwrap();
359    ///
360    /// let mut buf = [0u8; 0x10];
361    /// file.pread(buf.as_mut_ptr(), 2).unwrap();
362    ///
363    /// assert_eq!(buf, data);
364    /// ```
365    #[inline(always)]
366    #[allow(clippy::not_unsafe_ptr_arg_deref)]
367    #[cfg(any(target_os = "linux", target_os = "macos"))]
368    pub fn pread(&self, buf: *mut u8, index: usize) -> FrozenResult<()> {
369        let offset = self.cfg.chunk_size * index;
370        let file = self.get_file();
371
372        unsafe { file.pread(buf, offset, self.cfg.chunk_size) }
373    }
374
375    /// Write a single chunk at given `index` w/ `pwrite` syscall
376    ///
377    /// ## Example
378    ///
379    /// ```
380    /// use frozen_core::ffile::{FrozenFile, FFCfg};
381    ///
382    /// const MID: u8 = 0;
383    ///
384    /// let dir = tempfile::tempdir().unwrap();
385    /// let path = dir.path().join("tmp_frozen_file");
386    ///
387    /// let cfg = FFCfg {
388    ///     chunk_size: 0x10,
389    ///     path: path.to_path_buf(),
390    ///     initial_chunk_amount: 0x0A,
391    /// };
392    ///
393    /// let file = FrozenFile::new::<MID>(cfg).unwrap();
394    ///
395    /// let mut data = [9u8; 0x10];
396    /// file.pwrite(data.as_mut_ptr(), 4).unwrap();
397    /// file.sync().unwrap();
398    ///
399    /// let mut buf = [0u8; 0x10];
400    /// file.pread(buf.as_mut_ptr(), 4).unwrap();
401    ///
402    /// assert_eq!(buf, data);
403    /// ```
404    #[inline(always)]
405    #[allow(clippy::not_unsafe_ptr_arg_deref)]
406    #[cfg(any(target_os = "linux", target_os = "macos"))]
407    pub fn pwrite(&self, buf: *mut u8, index: usize) -> FrozenResult<()> {
408        let offset = self.cfg.chunk_size * index;
409        let file = self.get_file();
410
411        unsafe { file.pwrite(buf, offset, self.cfg.chunk_size) }
412    }
413
414    /// Read multiple chunks starting from given `index` till `bufs.len()` w/ `preadv` syscall
415    ///
416    /// ## Example
417    ///
418    /// ```
419    /// use frozen_core::ffile::{FrozenFile, FFCfg};
420    ///
421    /// const MID: u8 = 0;
422    ///
423    /// let dir = tempfile::tempdir().unwrap();
424    /// let path = dir.path().join("tmp_frozen_file");
425    ///
426    /// let cfg = FFCfg {
427    ///     chunk_size: 0x10,
428    ///     path: path.to_path_buf(),
429    ///     initial_chunk_amount: 0x0A,
430    /// };
431    ///
432    /// let file = FrozenFile::new::<MID>(cfg).unwrap();
433    ///
434    /// let mut write_bufs = [[1u8; 0x10], [2u8; 0x10]];
435    /// let ptrs: Vec<*mut u8> = write_bufs.iter_mut().map(|b| b.as_mut_ptr()).collect();
436    ///
437    /// file.pwritev(&ptrs, 0).unwrap();
438    /// file.sync().unwrap();
439    ///
440    /// let mut read_bufs = [[0u8; 0x10], [0u8; 0x10]];
441    /// let rptrs: Vec<*mut u8> = read_bufs.iter_mut().map(|b| b.as_mut_ptr()).collect();
442    ///
443    /// file.preadv(&rptrs, 0).unwrap();
444    ///
445    /// assert!(read_bufs[0].iter().all(|b| *b == 1));
446    /// assert!(read_bufs[1].iter().all(|b| *b == 2));
447    /// ```
448    #[inline(always)]
449    #[cfg(any(target_os = "linux", target_os = "macos"))]
450    pub fn preadv(&self, bufs: &[*mut u8], index: usize) -> FrozenResult<()> {
451        let offset = self.cfg.chunk_size * index;
452        let file = self.get_file();
453
454        unsafe { file.preadv(bufs, offset, self.cfg.chunk_size) }
455    }
456
457    /// Write multiple chunks starting from given `index` till `bufs.len()` w/ `pwritev` syscall
458    ///
459    /// ## Example
460    ///
461    /// ```
462    /// use frozen_core::ffile::{FrozenFile, FFCfg};
463    ///
464    /// const MID: u8 = 0;
465    ///
466    /// let dir = tempfile::tempdir().unwrap();
467    /// let path = dir.path().join("tmp_frozen_file");
468    ///
469    /// let cfg = FFCfg {
470    ///     chunk_size: 0x10,
471    ///     path: path.to_path_buf(),
472    ///     initial_chunk_amount: 0x0A,
473    /// };
474    ///
475    /// let file = FrozenFile::new::<MID>(cfg).unwrap();
476    ///
477    /// let mut bufs = [[3u8; 0x10], [4u8; 0x10]];
478    /// let ptrs: Vec<*mut u8> = bufs.iter_mut().map(|b| b.as_mut_ptr()).collect();
479    ///
480    /// file.pwritev(&ptrs, 2).unwrap();
481    /// file.sync().unwrap();
482    ///
483    /// let mut a = [0u8; 0x10];
484    /// let mut b = [0u8; 0x10];
485    ///
486    /// file.pread(a.as_mut_ptr(), 2).unwrap();
487    /// file.pread(b.as_mut_ptr(), 3).unwrap();
488    ///
489    /// assert!(a.iter().all(|v| *v == 3));
490    /// assert!(b.iter().all(|v| *v == 4));
491    /// ```
492    #[inline(always)]
493    #[cfg(any(target_os = "linux", target_os = "macos"))]
494    pub fn pwritev(&self, bufs: &[*mut u8], index: usize) -> FrozenResult<()> {
495        let offset = self.cfg.chunk_size * index;
496        let file = self.get_file();
497
498        unsafe { file.pwritev(bufs, offset, self.cfg.chunk_size) }
499    }
500
501    /// Grow file size of [`FrozenFile`] by given `count` of chunks
502    ///
503    /// After successful execution, updated file length will be `current_length + (count * chunk_size)`
504    ///
505    /// ## Example
506    ///
507    /// ```
508    /// use frozen_core::ffile::{FrozenFile, FFCfg};
509    ///
510    /// const MID: u8 = 0;
511    ///
512    /// let dir = tempfile::tempdir().unwrap();
513    /// let path = dir.path().join("tmp_frozen_file");
514    ///
515    /// let cfg = FFCfg {
516    ///     chunk_size: 0x10,
517    ///     path: path.to_path_buf(),
518    ///     initial_chunk_amount: 0x0A,
519    /// };
520    ///
521    /// let file = FrozenFile::new::<MID>(cfg).unwrap();
522    /// assert_eq!(file.length().unwrap(), 0x10 * 0x0A);
523    ///
524    /// file.grow(0x20).unwrap();
525    /// assert_eq!(file.length().unwrap(), 0x10 * (0x0A + 0x20));
526    /// ```
527    pub fn grow(&self, count: usize) -> FrozenResult<()> {
528        let curr_len = self.length()?;
529        let len_to_add = self.cfg.chunk_size * count;
530
531        unsafe { self.get_file().grow(curr_len, len_to_add) }
532    }
533
534    /// Fetch total available chunks in [`FrozenFile`] from fs
535    ///
536    /// ## Working
537    ///
538    /// This call performs a syscall to fetch current length of [`FrozenFile`] from fs, as the current length of the
539    /// file is not cached anywhere in the pipeline to avoid TOCTAU race conditions
540    ///
541    /// ## Example
542    ///
543    /// ```
544    /// use frozen_core::ffile::{FrozenFile, FFCfg};
545    ///
546    /// const MID: u8 = 0;
547    ///
548    /// let dir = tempfile::tempdir().unwrap();
549    /// let path = dir.path().join("tmp_frozen_file");
550    ///
551    /// let cfg = FFCfg {
552    ///     chunk_size: 0x10,
553    ///     path: path.to_path_buf(),
554    ///     initial_chunk_amount: 0x0A,
555    /// };
556    ///
557    /// let file = FrozenFile::new::<MID>(cfg).unwrap();
558    /// assert_eq!(file.length().unwrap(), 0x10 * 0x0A);
559    ///
560    /// file.grow(0x20).unwrap();
561    /// assert_eq!(file.total_chunks().unwrap(), 0x0A + 0x20);
562    /// ```
563    #[inline]
564    pub fn total_chunks(&self) -> FrozenResult<usize> {
565        let curr_len = self.length()?;
566        let chunk_size = self.cfg.chunk_size;
567
568        if crate::hints::unlikely(curr_len % chunk_size != 0) {
569            return new_err_default(err::CPT);
570        }
571
572        Ok(curr_len / chunk_size)
573    }
574
575    #[inline]
576    fn get_file(&self) -> &core::mem::ManuallyDrop<TFile> {
577        unsafe { &*self.file.get() }
578    }
579}
580
581impl Drop for FrozenFile {
582    fn drop(&mut self) {
583        // guard for when delete is called (or drop on drop if its somehow possible)
584        #[cfg(any(target_os = "linux", target_os = "macos"))]
585        if self.fd() == posix::CLOSED_FD {
586            return;
587        }
588
589        // sync if dirty & close
590        let _ = self.sync();
591        let _ = unsafe { self.get_file().close() };
592    }
593}
594
595impl core::fmt::Display for FrozenFile {
596    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
597        write!(f, "FrozenFile {{fd: {}, len: {}}}", self.fd(), self.length().unwrap_or(0),)
598    }
599}
600
601#[cfg(test)]
602mod tests {
603    use super::*;
604    use std::sync::Arc;
605
606    const MID: u8 = 0;
607    const INIT_CHUNKS: usize = 4;
608    const CHUNK_SIZE: usize = 0x10;
609
610    fn tmp_path() -> (tempfile::TempDir, FFCfg) {
611        let dir = tempfile::tempdir().unwrap();
612        let path = dir.path().join("tmp_ff_file");
613        let cfg = FFCfg { path, chunk_size: CHUNK_SIZE, initial_chunk_amount: INIT_CHUNKS };
614
615        (dir, cfg)
616    }
617
618    mod ff_lifecycle {
619        use super::*;
620
621        #[test]
622        fn ok_new_with_init_len() {
623            let (_dir, cfg) = tmp_path();
624            let file = FrozenFile::new::<MID>(cfg).unwrap();
625
626            let exists = file.exists().unwrap();
627            assert!(exists);
628
629            assert_eq!(file.length().unwrap(), CHUNK_SIZE * INIT_CHUNKS);
630        }
631
632        #[test]
633        fn ok_new_existing() {
634            let (_dir, cfg) = tmp_path();
635
636            let file = FrozenFile::new::<MID>(cfg.clone()).unwrap();
637            assert_eq!(file.length().unwrap(), CHUNK_SIZE * INIT_CHUNKS);
638
639            // must be dropped to release the exclusive lock
640            drop(file);
641
642            let reopened = FrozenFile::new::<MID>(cfg.clone()).unwrap();
643            assert_eq!(reopened.length().unwrap(), CHUNK_SIZE * INIT_CHUNKS);
644        }
645
646        #[test]
647        fn err_new_when_file_smaller_than_init_len() {
648            let (_dir, mut cfg) = tmp_path();
649
650            let file = FrozenFile::new::<MID>(cfg.clone()).unwrap();
651            drop(file);
652
653            // updated cfg
654            cfg.chunk_size *= 2;
655
656            let err = FrozenFile::new::<MID>(cfg).unwrap_err();
657            assert_eq!((err.id & 0xffff) as u16, err::CPT.reason);
658        }
659
660        #[test]
661        fn ok_exists_true_when_exists() {
662            let (_dir, cfg) = tmp_path();
663            let file = FrozenFile::new::<MID>(cfg).unwrap();
664
665            let exists = file.exists().unwrap();
666            assert!(exists);
667        }
668
669        #[test]
670        fn ok_exists_false_when_missing() {
671            let (_dir, cfg) = tmp_path();
672            let file = FrozenFile::new::<MID>(cfg).unwrap();
673            file.delete().unwrap();
674
675            let exists = file.exists().unwrap();
676            assert!(!exists);
677        }
678
679        #[test]
680        fn ok_delete_file() {
681            let (_dir, cfg) = tmp_path();
682
683            let file = FrozenFile::new::<MID>(cfg).unwrap();
684            let exists = file.exists().unwrap();
685            assert!(exists);
686
687            file.delete().unwrap();
688            let exists = file.exists().unwrap();
689            assert!(!exists);
690        }
691
692        #[test]
693        fn err_delete_after_delete() {
694            let (_dir, cfg) = tmp_path();
695
696            let file = FrozenFile::new::<MID>(cfg).unwrap();
697            file.delete().unwrap();
698
699            let err = file.delete().unwrap_err();
700            assert_eq!((err.id & 0xffff) as u16, err::INV.reason);
701        }
702
703        #[test]
704        fn ok_drop_persists_without_explicit_sync() {
705            let mut data = [0x0Bu8; CHUNK_SIZE];
706            let (_dir, cfg) = tmp_path();
707
708            {
709                let file = FrozenFile::new::<MID>(cfg.clone()).unwrap();
710                file.pwrite(data.as_mut_ptr(), 0).unwrap();
711                drop(file);
712            }
713
714            {
715                let reopened = FrozenFile::new::<MID>(cfg).unwrap();
716                let mut buf = [0u8; CHUNK_SIZE];
717
718                reopened.pread(buf.as_mut_ptr(), 0).unwrap();
719                assert_eq!(buf, data);
720            }
721        }
722    }
723
724    mod ff_lock {
725        use super::*;
726
727        #[test]
728        fn err_new_when_already_open() {
729            let (_dir, cfg) = tmp_path();
730            let file = FrozenFile::new::<MID>(cfg.clone()).unwrap();
731
732            let err = FrozenFile::new::<MID>(cfg).unwrap_err();
733            assert_eq!((err.id & 0xffff) as u16, err::LCK.reason);
734
735            drop(file);
736        }
737
738        #[test]
739        fn ok_drop_releases_exclusive_lock() {
740            let (_dir, cfg) = tmp_path();
741
742            let file = FrozenFile::new::<MID>(cfg.clone()).unwrap();
743            drop(file);
744
745            let _ = FrozenFile::new::<MID>(cfg).expect("must not fail after drop");
746        }
747    }
748
749    mod ff_grow {
750        use super::*;
751
752        #[test]
753        fn ok_grow_updates_length() {
754            let (_dir, cfg) = tmp_path();
755
756            let file = FrozenFile::new::<MID>(cfg).unwrap();
757            assert_eq!(file.length().unwrap(), CHUNK_SIZE * INIT_CHUNKS);
758
759            file.grow(0x20).unwrap();
760            assert_eq!(file.length().unwrap(), CHUNK_SIZE * (INIT_CHUNKS + 0x20));
761        }
762
763        #[test]
764        fn ok_grow_sync_cycle() {
765            let (_dir, cfg) = tmp_path();
766            let file = FrozenFile::new::<MID>(cfg).unwrap();
767
768            for _ in 0..0x0A {
769                file.grow(0x100).unwrap();
770                file.sync().unwrap();
771            }
772
773            assert_eq!(file.length().unwrap(), CHUNK_SIZE * (INIT_CHUNKS + (0x0A * 0x100)));
774        }
775    }
776
777    mod ff_sync {
778        use super::*;
779
780        #[test]
781        fn ok_sync_after_sync() {
782            let (_dir, cfg) = tmp_path();
783            let file = FrozenFile::new::<MID>(cfg).unwrap();
784
785            file.sync().unwrap();
786            file.sync().unwrap();
787            file.sync().unwrap();
788        }
789
790        #[test]
791        fn err_sync_after_delete() {
792            let (_dir, cfg) = tmp_path();
793            let file = FrozenFile::new::<MID>(cfg).unwrap();
794            file.delete().unwrap();
795
796            let err = file.sync().unwrap_err();
797            assert_eq!((err.id & 0xffff) as u16, err::HCF.reason);
798        }
799    }
800
801    mod ff_write_read {
802        use super::*;
803
804        #[test]
805        fn ok_single_write_read_cycle() {
806            let (_dir, cfg) = tmp_path();
807            let file = FrozenFile::new::<MID>(cfg).unwrap();
808
809            let mut data = [0x0Bu8; CHUNK_SIZE];
810
811            file.pwrite(data.as_mut_ptr(), 4).unwrap();
812            file.sync().unwrap();
813
814            let mut buf = [0u8; CHUNK_SIZE];
815            file.pread(buf.as_mut_ptr(), 4).unwrap();
816            assert_eq!(buf, data);
817        }
818
819        #[test]
820        fn ok_vectored_write_read_cycle() {
821            let (_dir, cfg) = tmp_path();
822            let file = FrozenFile::new::<MID>(cfg).unwrap();
823
824            let mut bufs = [[1u8; CHUNK_SIZE], [2u8; CHUNK_SIZE]];
825            let bufs: Vec<*mut u8> = bufs.iter_mut().map(|b| b.as_mut_ptr()).collect();
826
827            file.pwritev(&bufs, 0).unwrap();
828            file.sync().unwrap();
829
830            let mut read_bufs = [[0u8; CHUNK_SIZE], [0u8; CHUNK_SIZE]];
831            let rbufs: Vec<*mut u8> = read_bufs.iter_mut().map(|b| b.as_mut_ptr()).collect();
832            file.preadv(&rbufs, 0).unwrap();
833
834            assert!(read_bufs[0].iter().all(|b| *b == 1));
835            assert!(read_bufs[1].iter().all(|b| *b == 2));
836        }
837
838        #[test]
839        fn ok_write_concurrent_non_overlapping() {
840            let (_dir, mut cfg) = tmp_path();
841            cfg.initial_chunk_amount = 0x100;
842            let file = Arc::new(FrozenFile::new::<MID>(cfg).unwrap());
843
844            let mut handles = vec![];
845            for i in 0..2 {
846                let f = file.clone();
847                handles.push(std::thread::spawn(move || {
848                    let mut data = [i as u8; CHUNK_SIZE];
849                    f.pwrite(data.as_mut_ptr(), i).unwrap();
850                }));
851            }
852
853            for h in handles {
854                h.join().unwrap();
855            }
856
857            file.sync().unwrap();
858
859            for i in 0..2 {
860                let mut buf = [0u8; CHUNK_SIZE];
861                file.pread(buf.as_mut_ptr(), i).unwrap();
862                assert!(buf.iter().all(|b| *b == i as u8));
863            }
864        }
865
866        #[test]
867        fn ok_concurrent_grow_and_write() {
868            let (_dir, cfg) = tmp_path();
869            let file = Arc::new(FrozenFile::new::<MID>(cfg).unwrap());
870
871            let writer = {
872                let f = file.clone();
873                std::thread::spawn(move || {
874                    for i in 0..INIT_CHUNKS {
875                        let mut data = [i as u8; CHUNK_SIZE];
876                        f.pwrite(data.as_mut_ptr(), i).unwrap();
877                    }
878                })
879            };
880
881            let chunks_to_grow = 0x20;
882            let grower = {
883                let f = file.clone();
884                std::thread::spawn(move || {
885                    f.grow(chunks_to_grow).unwrap();
886                })
887            };
888
889            writer.join().unwrap();
890            grower.join().unwrap();
891
892            file.sync().unwrap();
893            assert_eq!(file.length().unwrap(), CHUNK_SIZE * (INIT_CHUNKS + chunks_to_grow));
894
895            for i in 0..INIT_CHUNKS {
896                let mut buf = [0u8; CHUNK_SIZE];
897                file.pread(buf.as_mut_ptr(), i).unwrap();
898                assert!(buf.iter().all(|b| *b == i as u8));
899            }
900        }
901
902        #[test]
903        fn ok_concurrent_sync_and_write() {
904            let (_dir, cfg) = tmp_path();
905            let file = Arc::new(FrozenFile::new::<MID>(cfg).unwrap());
906
907            let writer = {
908                let f = file.clone();
909                std::thread::spawn(move || {
910                    for i in 0..INIT_CHUNKS {
911                        let mut data = [i as u8; CHUNK_SIZE];
912                        f.pwrite(data.as_mut_ptr(), i).unwrap();
913                    }
914                })
915            };
916
917            let syncer = {
918                let f = file.clone();
919                std::thread::spawn(move || {
920                    for _ in 0..2 {
921                        f.sync().unwrap();
922                    }
923                })
924            };
925
926            writer.join().unwrap();
927            syncer.join().unwrap();
928
929            file.sync().unwrap();
930
931            for i in 0..INIT_CHUNKS {
932                let mut buf = [0; CHUNK_SIZE];
933                file.pread(buf.as_mut_ptr(), i).unwrap();
934                assert!(buf.iter().all(|b| *b == i as u8));
935            }
936        }
937
938        #[test]
939        fn err_read_hcf_for_eof() {
940            let (_dir, cfg) = tmp_path();
941            let file = FrozenFile::new::<MID>(cfg).unwrap();
942
943            // index > curr_chunks
944            let mut buf = [0; CHUNK_SIZE];
945            let err = file.pread(buf.as_mut_ptr(), 0x100).unwrap_err();
946            assert_eq!((err.id & 0xffff) as u16, err::HCF.reason);
947        }
948    }
949}