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