Skip to main content

frozen_core/file/
mod.rs

1//! Custom implementation of `std::fs::File` with fixed-size buffer addressing, background
2//! synchronization, and epoch-based durability tracking
3//!
4//! ## Example
5//!
6//! ```
7//! use frozen_core::file::{File, FileCfg};
8//!
9//! const MID: u8 = 0;
10//!
11//! let dir = tempfile::tempdir().unwrap();
12//! let path = dir.path().join("tmp_file");
13//!
14//! let cfg = FileCfg {
15//!     module_id: MID,
16//!     buffer_size: 0x10,
17//!     path: path.clone(),
18//!     initial_available_buffers: 4,
19//!     sync_interval: None,
20//!     sequential_access_hint: false,
21//! };
22//!
23//! let file = File::new(cfg.clone()).unwrap();
24//! assert_eq!(file.length(), 0x10 * 4);
25//!
26//! let data = [1u8; 0x10];
27//! let ticket = file.write(&data, 0).unwrap();
28//! file.sync().unwrap();
29//! assert!(ticket.is_durable());
30//!
31//! let mut buf = [0u8; 0x10];
32//! file.read(&mut buf, 0).unwrap();
33//! assert_eq!(buf, data);
34//!
35//! assert!(File::new(cfg.clone()).is_err());
36//!
37//! assert!(file.delete().is_ok());
38//! assert!(!path.exists());
39//! ```
40
41// TODO: Tackle durability verification for uncommitted/non-durable reads internally in the future TODO: Tackle
42// coarse-grained RwLock write lock in sync_internal which blocks concurrent pread and pwrite ops during disk flushes
43// CRITICAL: On EIO during sync(), kernel clears dirty bits and consumes writeback errors (f_wb_err); subsequent sync()
44// calls return Ok(()), causing del_err() to falsely report lost data as durable (ref: PostgreSQL Bug #15096 /
45// fsyncgate); sync failures must poison the state or panic rather than retrying blindly
46
47mod interface;
48
49#[cfg(any(target_os = "linux", target_os = "macos"))]
50mod posix;
51
52#[cfg(target_os = "windows")]
53mod windows;
54
55use crate::{
56    ack::{AckTicket, Completion, SyncTrigger, TEpoch},
57    error::{BindModule, ErrCode, FrozenError, FrozenResult},
58};
59use interface::FileInterface;
60use std::sync::{Arc, Condvar, Mutex, RwLock, atomic};
61
62#[cfg(any(target_os = "linux", target_os = "macos"))]
63pub(in crate::file) type PlatformFile = posix::POSIXFile;
64
65#[cfg(target_os = "windows")]
66pub(in crate::file) type PlatformFile = windows::WINFile;
67
68/// Error codes for [`File`] module
69pub(in crate::file) mod err {
70    use super::{ErrCode, FrozenError, FrozenResult};
71
72    /// Domain Id for [`File`] is **8**
73    const ERRDOMAIN: u8 = 0x08;
74
75    /// internal fuck up (hault and catch fire)
76    pub const HCF: ErrCode = ErrCode::new(0x02, "hault and catch fire");
77
78    /// unknown error (fallback)
79    pub const UNK: ErrCode = ErrCode::new(0x04, "unknown error");
80
81    /// no more space available
82    pub const NSP: ErrCode = ErrCode::new(0x08, "not enough space available on the storage device");
83
84    /// syncing error
85    pub const SYN: ErrCode = ErrCode::new(0x0A, "failed to sync/flush data to storage device");
86
87    /// no write perm
88    pub const WRT: ErrCode = ErrCode::new(0x0C, "missing permissions for write");
89
90    /// no read perm
91    pub const RED: ErrCode = ErrCode::new(0x0E, "missing permissions for read");
92
93    /// invalid file path
94    pub const INV: ErrCode = ErrCode::new(0x10, "invalid path to file");
95
96    /// corrupted file
97    pub const CPT: ErrCode = ErrCode::new(0x12, "file is either invalid or corrupted");
98
99    /// unable to grow
100    pub const GRW: ErrCode = ErrCode::new(0x14, "unable to zero-extend file");
101
102    /// locks exhausted (can happen on systems w/ NFS)
103    pub const LEX: ErrCode =
104        ErrCode::new(0x18, "failed to obtain lock, as no more locks available");
105
106    /// no write/read permission
107    pub const PRM: ErrCode = ErrCode::new(0x1A, "missing permissions for IO");
108
109    /// unable to obtain exclusive lock
110    pub const LCK: ErrCode =
111        ErrCode::new(0x1C, "failed to obtain exclusive lock as file may already opened");
112
113    /// file already exists
114    pub const EXS: ErrCode = ErrCode::new(0x1E, "file already exists");
115
116    #[inline]
117    pub(in crate::file) fn raw_error<R, E: std::fmt::Display>(
118        code: ErrCode,
119        error: E,
120    ) -> FrozenResult<R> {
121        let err = FrozenError::unbound_raw(ERRDOMAIN, code, error);
122        Err(err)
123    }
124
125    #[inline]
126    pub(in crate::file) fn default_error<R>(code: ErrCode) -> FrozenResult<R> {
127        let err = FrozenError::unbound(ERRDOMAIN, code, "");
128        Err(err)
129    }
130
131    #[inline]
132    pub(in crate::file) fn make_error(code: ErrCode) -> FrozenError {
133        FrozenError::unbound(ERRDOMAIN, code, "")
134    }
135
136    #[inline]
137    pub(in crate::file) fn make_raw_error<E: std::fmt::Display>(
138        code: ErrCode,
139        error: E,
140    ) -> FrozenError {
141        FrozenError::unbound_raw(ERRDOMAIN, code, error)
142    }
143}
144
145/// File descriptor of [`File`]
146#[cfg(any(target_os = "linux", target_os = "macos"))]
147pub type FileId = libc::c_int;
148
149/// File handle of [`File`]
150///
151/// ## NOTE
152///
153/// On Windows, kernel HANDLE's are pointer sized but are stored as `isize` for atomic access; while
154/// the `INVALID_HANDLE_VALUE` maps to `-1isize`, which is [`windows::CLOSED_HANDLE`]
155#[cfg(target_os = "windows")]
156pub type FileId = isize;
157
158/// Configurations for [`File`]
159///
160/// ## Example
161///
162/// ```
163/// use frozen_core::file::FileCfg;
164/// use std::time::Duration;
165///
166/// let cfg = FileCfg {
167///     module_id: 1,
168///     path: std::path::PathBuf::from("test.db"),
169///     buffer_size: 0x100,
170///     initial_available_buffers: 0x10,
171///     sync_interval: Some(Duration::from_millis(0x64)),
172///     sequential_access_hint: false,
173/// };
174///
175/// assert_eq!(cfg.buffer_size, 0x100);
176/// assert_eq!(cfg.initial_available_buffers, 0x10);
177/// assert_eq!(cfg.sync_interval, Some(Duration::from_millis(0x64)));
178/// ```
179#[derive(Debug, Clone)]
180pub struct FileCfg {
181    /// Identifier used while error propagation
182    pub module_id: u8,
183
184    /// Absolute path for/of the file
185    ///
186    /// *NOTE:* The caller must make sure that the path represents a file and all the parent
187    /// directories included in the path do exists
188    pub path: std::path::PathBuf,
189
190    /// Size (in bytes) of a single chunk in file
191    ///
192    /// A chunk is a small fixed size allocation and addressing unit used by [`File`] for all the
193    /// write/read ops
194    ///
195    /// These ops are operated by index of the chunk and not the offset of the byte
196    ///
197    /// *NOTE:* Chunk size when power of 2, is cache efficient and good for performance
198    pub buffer_size: usize,
199
200    /// Number of chunks to pre-allocate on fs when [`File`] is initialized
201    ///
202    /// Initial file length will be `buffer_size * initial_available_buffers` (bytes)
203    pub initial_available_buffers: usize,
204
205    /// Optional interval for the background sync thread
206    ///
207    /// If `Some(duration)`, a background worker thread is spawned which flushes dirty pages at this
208    /// interval; and if `None`, background sync is disabled and durability advances via manual sync
209    /// or forced tickets
210    pub sync_interval: Option<std::time::Duration>,
211
212    /// Optional hint to configure sequential access optimizations on the backing file
213    ///
214    /// When set to `true`, Windows creates the file handle with `FILE_FLAG_SEQUENTIAL_SCAN`
215    /// instead of `FILE_FLAG_RANDOM_ACCESS`, and Linux applies `POSIX_FADV_SEQUENTIAL`
216    pub sequential_access_hint: bool,
217}
218
219#[derive(Debug)]
220struct FileInner {
221    cfg: FileCfg,
222    file: RwLock<Option<PlatformFile>>,
223    current_length: atomic::AtomicUsize,
224    completion: Arc<Completion>,
225    sync_condvar: Condvar,
226    sync_mutex: Mutex<bool>,
227    shutdown: atomic::AtomicBool,
228}
229
230impl SyncTrigger for FileInner {
231    fn trigger_sync(&self) -> FrozenResult<()> {
232        if self.cfg.sync_interval.is_some() {
233            {
234                let mut guard = self.sync_mutex.lock().unwrap_or_else(|e| e.into_inner());
235                *guard = true;
236            }
237
238            self.sync_condvar.notify_one();
239            Ok(())
240        } else {
241            self.sync_internal()
242        }
243    }
244}
245
246impl FileInner {
247    fn sync_internal(&self) -> FrozenResult<()> {
248        let guard = self.file.write().unwrap_or_else(|e| e.into_inner());
249        let file = match guard.as_ref() {
250            Some(f) => f,
251            None => return Ok(()),
252        };
253
254        let target_epoch = self.completion.read_current_epoch();
255        let durable_epoch = self.completion.read_durable_epoch();
256
257        if target_epoch == durable_epoch {
258            return Ok(());
259        }
260
261        match file.sync() {
262            Ok(()) => {
263                self.completion.mark_epoch_as_durable(target_epoch);
264                self.completion.del_err();
265                self.completion.notify_all_listeners();
266
267                Ok(())
268            }
269            Err(e) => {
270                let e = e.with_module(self.cfg.module_id);
271                self.completion.set_err(e.clone());
272                self.completion.notify_all_listeners();
273
274                Err(e)
275            }
276        }
277    }
278}
279
280/// Custom implementation of `std::fs::File`
281#[derive(Debug)]
282pub struct File {
283    cfg: FileCfg,
284    inner: Arc<FileInner>,
285    bg_thread: Option<std::thread::JoinHandle<()>>,
286}
287
288unsafe impl Send for File {}
289unsafe impl Sync for File {}
290
291impl File {
292    fn from_platform_file(
293        cfg: FileCfg,
294        file: PlatformFile,
295        current_length: usize,
296    ) -> FrozenResult<Self> {
297        let completion = Arc::new(Completion::default());
298        let sync_interval = cfg.sync_interval;
299
300        let inner = Arc::new(FileInner {
301            cfg: cfg.clone(),
302            file: RwLock::new(Some(file)),
303            current_length: atomic::AtomicUsize::new(current_length),
304            completion: completion.clone(),
305            sync_condvar: Condvar::new(),
306            sync_mutex: Mutex::new(false),
307            shutdown: atomic::AtomicBool::new(false),
308        });
309
310        let weak_inner: std::sync::Weak<dyn SyncTrigger> =
311            Arc::downgrade(&inner) as std::sync::Weak<dyn SyncTrigger>;
312        completion.set_sync_trigger(weak_inner);
313
314        let bg_thread = if let Some(interval) = sync_interval {
315            let worker_inner = Arc::clone(&inner);
316            let handle =
317                std::thread::Builder::new().name("frozen-sync-worker".into()).spawn(move || {
318                    let mut guard =
319                        worker_inner.sync_mutex.lock().unwrap_or_else(|e| e.into_inner());
320
321                    while !worker_inner.shutdown.load(atomic::Ordering::Acquire) {
322                        if *guard {
323                            *guard = false;
324                            drop(guard);
325
326                            let _ = worker_inner.sync_internal();
327                            guard =
328                                worker_inner.sync_mutex.lock().unwrap_or_else(|e| e.into_inner());
329
330                            continue;
331                        }
332
333                        let (new_guard, _) = worker_inner
334                            .sync_condvar
335                            .wait_timeout(guard, interval)
336                            .unwrap_or_else(|e| e.into_inner());
337
338                        guard = new_guard;
339                        if worker_inner.shutdown.load(atomic::Ordering::Acquire) {
340                            break;
341                        }
342
343                        *guard = false;
344                        drop(guard);
345
346                        let _ = worker_inner.sync_internal();
347                        guard = worker_inner.sync_mutex.lock().unwrap_or_else(|e| e.into_inner());
348                    }
349                });
350
351            let handle = match handle {
352                Ok(h) => h,
353                Err(spawn_err) => {
354                    let mut file_guard = inner.file.write().unwrap_or_else(|e| e.into_inner());
355                    if let Some(f) = file_guard.take() {
356                        let mut err = err::make_raw_error(err::HCF, spawn_err);
357                        if let Err(close_err) = f.close() {
358                            err.add_suppressed(close_err);
359                        }
360
361                        return Err(err.with_module(cfg.module_id));
362                    }
363
364                    return err::raw_error(err::HCF, spawn_err).with_module(cfg.module_id);
365                }
366            };
367
368            Some(handle)
369        } else {
370            None
371        };
372
373        Ok(Self { cfg, inner, bg_thread })
374    }
375
376    /// Creates and pre-allocates a new [`File`] at `cfg.path`
377    ///
378    /// ## TOCTAU Safe
379    ///
380    /// If the file already exists, an error w/ `err::EXS` is returned to guard against TOCTOU
381    /// overwrites
382    ///
383    /// ## Exclusive Lock
384    ///
385    /// Acquires an exclusive advisory lock via `flock(LOCK_EX | LOCK_NB)` immediately after
386    /// descriptor creation
387    ///
388    /// ## Example
389    ///
390    /// ```
391    /// use frozen_core::file::{File, FileCfg};
392    ///
393    /// let dir = tempfile::tempdir().unwrap();
394    /// let cfg = FileCfg {
395    ///     module_id: 1,
396    ///     path: dir.path().join("new_file.bin"),
397    ///     buffer_size: 64,
398    ///     initial_available_buffers: 4,
399    ///     sync_interval: None,
400    ///     sequential_access_hint: false,
401    /// };
402    ///
403    /// let file = File::new(cfg.clone()).unwrap();
404    /// assert_eq!(file.length(), 256);
405    ///
406    /// // Creating another file at the same path fails
407    /// assert!(File::new(cfg).is_err());
408    /// ```
409    pub fn new(cfg: FileCfg) -> FrozenResult<Self> {
410        let mid = cfg.module_id;
411        if cfg.buffer_size == 0 || cfg.initial_available_buffers == 0 {
412            return err::default_error(err::INV).with_module(mid);
413        }
414
415        let init_len = match cfg.buffer_size.checked_mul(cfg.initial_available_buffers) {
416            Some(len) => len,
417            None => return err::default_error(err::INV).with_module(mid),
418        };
419
420        let file = if cfg.sequential_access_hint {
421            PlatformFile::create_sequential(&cfg.path).with_module(mid)?
422        } else {
423            PlatformFile::create(&cfg.path).with_module(mid)?
424        };
425        if let Err(mut e) = file.flock() {
426            if let Err(unlink_err) = file.unlink(&cfg.path) {
427                e.add_suppressed(unlink_err);
428            }
429
430            return Err(e.with_module(mid));
431        }
432
433        if let Err(mut e) = file.grow(0, init_len) {
434            if let Err(unlink_err) = file.unlink(&cfg.path) {
435                e.add_suppressed(unlink_err);
436            }
437
438            return Err(e.with_module(mid));
439        }
440
441        if let Err(mut e) = file.sync() {
442            if let Err(unlink_err) = file.unlink(&cfg.path) {
443                e.add_suppressed(unlink_err);
444            }
445
446            return Err(e.with_module(mid));
447        }
448
449        Self::from_platform_file(cfg, file, init_len)
450    }
451
452    /// Open an existing [`File`] while validating its layout invariants
453    ///
454    /// ## Invariants
455    ///
456    /// Following invariants are checked,
457    ///
458    /// - Current file length is at least `buffer_size * initial_available_buffers`
459    /// - Current file length is a multiple of `buffer_size`
460    ///
461    /// If any invariant is violated, the file is closed and `err::CPT` is returned
462    ///
463    /// ## Example
464    ///
465    /// ```
466    /// use frozen_core::file::{File, FileCfg};
467    ///
468    /// let dir = tempfile::tempdir().unwrap();
469    /// let cfg = FileCfg {
470    ///     module_id: 1,
471    ///     path: dir.path().join("open_file.bin"),
472    ///     buffer_size: 64,
473    ///     initial_available_buffers: 4,
474    ///     sync_interval: None,
475    ///     sequential_access_hint: false,
476    /// };
477    ///
478    /// {
479    ///     let file = File::new(cfg.clone()).unwrap();
480    ///     assert_eq!(file.length(), 256);
481    /// }
482    ///
483    /// let opened = File::open(cfg).unwrap();
484    /// assert_eq!(opened.length(), 256);
485    /// ```
486    pub fn open(cfg: FileCfg) -> FrozenResult<Self> {
487        let mid = cfg.module_id;
488
489        if cfg.buffer_size == 0 || cfg.initial_available_buffers == 0 {
490            return err::default_error(err::INV).with_module(mid);
491        }
492
493        let init_len = match cfg.buffer_size.checked_mul(cfg.initial_available_buffers) {
494            Some(len) => len,
495            None => return err::default_error(err::INV).with_module(mid),
496        };
497
498        let file = if cfg.sequential_access_hint {
499            PlatformFile::open_sequential(&cfg.path).with_module(mid)?
500        } else {
501            PlatformFile::open(&cfg.path).with_module(mid)?
502        };
503
504        if let Err(mut e) = file.flock() {
505            if let Err(close_err) = file.close() {
506                e.add_suppressed(close_err);
507            }
508
509            return Err(e.with_module(mid));
510        }
511
512        let curr_len = match file.length() {
513            Ok(len) => len,
514            Err(mut e) => {
515                if let Err(close_err) = file.close() {
516                    e.add_suppressed(close_err);
517                }
518
519                return Err(e.with_module(mid));
520            }
521        };
522
523        if curr_len < init_len || curr_len % cfg.buffer_size != 0 {
524            let mut e = err::make_error(err::CPT);
525            if let Err(close_err) = file.close() {
526                e.add_suppressed(close_err);
527            }
528
529            return Err(e.with_module(mid));
530        }
531
532        Self::from_platform_file(cfg, file, curr_len)
533    }
534
535    /// Open an existing [`File`] or create it if it does not yet exist
536    ///
537    /// ## Semantics
538    ///
539    /// Uses idempotent open-or-create (`OPEN_ALWAYS` on Windows, `O_CREAT` without `O_EXCL` on
540    /// POSIX)
541    ///
542    /// If the file is newly created, it is grown to `buffer_size * initial_available_buffers` and
543    /// synced; and if the file already exists, it is opened and its layout invariants are validated
544    ///
545    /// In both cases, an exclusive advisory lock is acquired on the file
546    ///
547    /// ## Example
548    ///
549    /// ```
550    /// use frozen_core::file::{File, FileCfg};
551    ///
552    /// let dir = tempfile::tempdir().unwrap();
553    /// let cfg = FileCfg {
554    ///     module_id: 1,
555    ///     path: dir.path().join("idempotent.bin"),
556    ///     buffer_size: 64,
557    ///     initial_available_buffers: 2,
558    ///     sync_interval: None,
559    ///     sequential_access_hint: false,
560    /// };
561    ///
562    /// // First invocation creates and initializes the file
563    /// {
564    ///     let file = File::open_or_create(cfg.clone()).unwrap();
565    ///     assert_eq!(file.length(), 128);
566    /// }
567    ///
568    /// // Subsequent invocation opens the existing file safely
569    /// let reopened = File::open_or_create(cfg).unwrap();
570    /// assert_eq!(reopened.length(), 128);
571    /// ```
572    pub fn open_or_create(cfg: FileCfg) -> FrozenResult<Self> {
573        let mid = cfg.module_id;
574
575        if cfg.buffer_size == 0 || cfg.initial_available_buffers == 0 {
576            return err::default_error(err::INV).with_module(mid);
577        }
578
579        let init_len = match cfg.buffer_size.checked_mul(cfg.initial_available_buffers) {
580            Some(len) => len,
581            None => return err::default_error(err::INV).with_module(mid),
582        };
583
584        let file = if cfg.sequential_access_hint {
585            PlatformFile::new_sequential(&cfg.path).with_module(mid)?
586        } else {
587            PlatformFile::new(&cfg.path).with_module(mid)?
588        };
589        if let Err(mut e) = file.flock() {
590            if let Err(close_err) = file.close() {
591                e.add_suppressed(close_err);
592            }
593
594            return Err(e.with_module(mid));
595        }
596
597        let curr_len = match file.length() {
598            Ok(len) => len,
599            Err(mut e) => {
600                if let Err(close_err) = file.close() {
601                    e.add_suppressed(close_err);
602                }
603
604                return Err(e.with_module(mid));
605            }
606        };
607
608        if curr_len == 0 {
609            if let Err(mut e) = file.grow(0, init_len) {
610                if let Err(close_err) = file.close() {
611                    e.add_suppressed(close_err);
612                }
613
614                return Err(e.with_module(mid));
615            }
616
617            if let Err(mut e) = file.sync() {
618                if let Err(close_err) = file.close() {
619                    e.add_suppressed(close_err);
620                }
621
622                return Err(e.with_module(mid));
623            }
624
625            Self::from_platform_file(cfg, file, init_len)
626        } else {
627            if curr_len < init_len || curr_len % cfg.buffer_size != 0 {
628                let mut e = err::make_error(err::CPT);
629                if let Err(close_err) = file.close() {
630                    e.add_suppressed(close_err);
631                }
632
633                return Err(e.with_module(mid));
634            }
635
636            Self::from_platform_file(cfg, file, curr_len)
637        }
638    }
639
640    /// Read bytes starting from buffer `index` into `buf` w/ `pread` syscall
641    ///
642    /// ## Durability Guarantee & Caller Responsibility
643    ///
644    /// It is 100% the caller's responsibility to ensure that they do not read newly written data
645    /// unless the older data has been confirmed durable
646    /// (e.g. by checking or waiting on the write's [`AckTicket`])
647    ///
648    /// ## Multiple Buffers
649    ///
650    /// The input `buf` can span across a single or multiple buffers in memory
651    ///
652    /// ## Constraints
653    ///
654    /// - `buf.len()` must be a non-zero multiple of `cfg.buffer_size`
655    /// - Reading beyond current file length will return `err::HCF`
656    ///
657    /// ## Example
658    ///
659    /// ```
660    /// use frozen_core::file::{File, FileCfg};
661    ///
662    /// let dir = tempfile::tempdir().unwrap();
663    /// let cfg = FileCfg {
664    ///     module_id: 1,
665    ///     path: dir.path().join("read_test.bin"),
666    ///     buffer_size: 16,
667    ///     initial_available_buffers: 2,
668    ///     sync_interval: None,
669    ///     sequential_access_hint: false,
670    /// };
671    ///
672    /// let file = File::new(cfg).unwrap();
673    /// let data = [7u8; 16];
674    /// let ticket = file.write(&data, 0).unwrap();
675    /// ticket.force().unwrap();
676    ///
677    /// let mut out = [0u8; 16];
678    /// file.read(&mut out, 0).unwrap();
679    /// assert_eq!(out, data);
680    /// ```
681    #[inline(always)]
682    pub fn read(&self, buf: &mut [u8], index: usize) -> FrozenResult<()> {
683        let mid = self.cfg.module_id;
684        if buf.is_empty() {
685            return Ok(());
686        }
687
688        if buf.len() % self.cfg.buffer_size != 0 {
689            return err::default_error(err::INV).with_module(mid);
690        }
691
692        let offset = match index.checked_mul(self.cfg.buffer_size) {
693            Some(off) => off,
694            None => return err::default_error(err::INV).with_module(mid),
695        };
696
697        if offset.checked_add(buf.len()).is_none_or(|end| end > self.length()) {
698            return err::default_error(err::HCF).with_module(mid);
699        }
700
701        let guard = self.inner.file.read().unwrap_or_else(|e| e.into_inner());
702        let file = match guard.as_ref() {
703            Some(f) => f,
704            None => return err::default_error(err::INV).with_module(mid),
705        };
706
707        file.pread(buf, offset).with_module(mid)
708    }
709
710    /// Write bytes starting at buffer `index` from `buf` w/ `pwrite` syscall
711    ///
712    /// Returns an [`AckTicket`] representing the durability acknowledgement of this write operation
713    ///
714    /// ## Multiple Buffers
715    ///
716    /// The input `buf` can span across a single or multiple buffers in memory
717    ///
718    /// ## Constraints
719    ///
720    /// - `buf.len()` must be a non-zero multiple of `cfg.buffer_size`
721    /// - Writing beyond current file length will return `err::HCF`
722    ///
723    /// ## Example
724    ///
725    /// ```
726    /// use frozen_core::file::{File, FileCfg};
727    ///
728    /// let dir = tempfile::tempdir().unwrap();
729    /// let cfg = FileCfg {
730    ///     module_id: 1,
731    ///     path: dir.path().join("write_test.bin"),
732    ///     buffer_size: 16,
733    ///     initial_available_buffers: 2,
734    ///     sync_interval: None,
735    ///     sequential_access_hint: false,
736    /// };
737    ///
738    /// let file = File::new(cfg).unwrap();
739    /// let data = [42u8; 16];
740    /// let ticket = file.write(&data, 1).unwrap();
741    /// assert_eq!(ticket.is_durable(), false);
742    ///
743    /// file.sync().unwrap();
744    /// assert!(ticket.is_durable());
745    /// ```
746    #[inline(always)]
747    pub fn write(&self, buf: &[u8], index: usize) -> FrozenResult<AckTicket> {
748        let mid = self.cfg.module_id;
749        if buf.is_empty() {
750            let current = self.inner.completion.read_current_epoch();
751            return Ok(AckTicket::new(current, self.inner.completion.clone()));
752        }
753
754        if buf.len() % self.cfg.buffer_size != 0 {
755            return err::default_error(err::INV).with_module(mid);
756        }
757
758        let offset = match index.checked_mul(self.cfg.buffer_size) {
759            Some(off) => off,
760            None => return err::default_error(err::INV).with_module(mid),
761        };
762
763        if offset.checked_add(buf.len()).is_none_or(|end| end > self.length()) {
764            return err::default_error(err::HCF).with_module(mid);
765        }
766
767        let guard = self.inner.file.read().unwrap_or_else(|e| e.into_inner());
768        let file = match guard.as_ref() {
769            Some(f) => f,
770            None => return err::default_error(err::INV).with_module(mid),
771        };
772
773        file.pwrite(buf, offset).with_module(mid)?;
774
775        let epoch = self.inner.completion.increment_current_epoch();
776        Ok(AckTicket::new(epoch, self.inner.completion.clone()))
777    }
778
779    /// Grow file size of [`File`] by given `count` of buffers
780    ///
781    /// After successful execution, updated file length will be `current_length + (count *
782    /// buffer_size)`
783    ///
784    /// ## Example
785    ///
786    /// ```
787    /// use frozen_core::file::{File, FileCfg};
788    ///
789    /// let dir = tempfile::tempdir().unwrap();
790    /// let cfg = FileCfg {
791    ///     module_id: 1,
792    ///     path: dir.path().join("grow_test.bin"),
793    ///     buffer_size: 32,
794    ///     initial_available_buffers: 2,
795    ///     sync_interval: None,
796    ///     sequential_access_hint: false,
797    /// };
798    ///
799    /// let file = File::new(cfg).unwrap();
800    /// assert_eq!(file.length(), 64);
801    ///
802    /// file.grow(3).unwrap();
803    /// assert_eq!(file.length(), 160);
804    /// assert_eq!(file.total_buffers().unwrap(), 5);
805    /// ```
806    pub fn grow(&self, count: usize) -> FrozenResult<()> {
807        let mid = self.cfg.module_id;
808        if count == 0 {
809            return Ok(());
810        }
811
812        let len_to_add = match self.cfg.buffer_size.checked_mul(count) {
813            Some(len) => len,
814            None => return err::default_error(err::GRW).with_module(mid),
815        };
816
817        let guard = self.inner.file.write().unwrap_or_else(|e| e.into_inner());
818        let file = match guard.as_ref() {
819            Some(f) => f,
820            None => return err::default_error(err::INV).with_module(mid),
821        };
822
823        let curr_len = self.inner.current_length.load(atomic::Ordering::Acquire);
824        file.grow(curr_len, len_to_add).with_module(mid)?;
825
826        self.inner.current_length.fetch_add(len_to_add, atomic::Ordering::Release);
827        self.inner.completion.increment_current_epoch();
828
829        Ok(())
830    }
831
832    /// Syncs in-mem data to the storage device
833    ///
834    /// ## Example
835    ///
836    /// ```
837    /// use frozen_core::file::{File, FileCfg};
838    ///
839    /// let dir = tempfile::tempdir().unwrap();
840    /// let cfg = FileCfg {
841    ///     module_id: 1,
842    ///     path: dir.path().join("sync_test.bin"),
843    ///     buffer_size: 16,
844    ///     initial_available_buffers: 2,
845    ///     sync_interval: None,
846    ///     sequential_access_hint: false,
847    /// };
848    ///
849    /// let file = File::new(cfg).unwrap();
850    /// let ticket = file.write(&[9u8; 16], 0).unwrap();
851    /// assert!(!ticket.is_durable());
852    ///
853    /// file.sync().unwrap();
854    /// assert!(ticket.is_durable());
855    /// ```
856    #[inline]
857    pub fn sync(&self) -> FrozenResult<()> {
858        self.inner.sync_internal()
859    }
860
861    /// Best-effort call to prompt kernel to start flushing dirty pages in the specified chunk range
862    ///
863    /// If `count == 0`, this call is a no-op and returns `Ok(())`
864    ///
865    /// ## Example
866    ///
867    /// ```
868    /// # #[cfg(target_os = "linux")]
869    /// # {
870    /// use frozen_core::file::{File, FileCfg};
871    ///
872    /// let dir = tempfile::tempdir().unwrap();
873    /// let cfg = FileCfg {
874    ///     module_id: 1,
875    ///     path: dir.path().join("sync_range.bin"),
876    ///     buffer_size: 64,
877    ///     initial_available_buffers: 4,
878    ///     sync_interval: None,
879    ///     sequential_access_hint: false,
880    /// };
881    ///
882    /// let file = File::new(cfg).unwrap();
883    /// file.write(&[1u8; 64], 0).unwrap();
884    /// assert!(file.sync_range(0, 1).is_ok());
885    /// assert!(file.sync_range(0, 0).is_ok());
886    /// # }
887    /// ```
888    #[cfg(target_os = "linux")]
889    pub fn sync_range(&self, index: usize, count: usize) -> FrozenResult<()> {
890        if count == 0 {
891            return Ok(());
892        }
893
894        let mid = self.cfg.module_id;
895        let offset = match index.checked_mul(self.cfg.buffer_size) {
896            Some(off) => off,
897            None => return err::default_error(err::INV).with_module(mid),
898        };
899        let len_to_sync = match count.checked_mul(self.cfg.buffer_size) {
900            Some(len) => len,
901            None => return err::default_error(err::INV).with_module(mid),
902        };
903
904        let guard = self.inner.file.read().unwrap_or_else(|e| e.into_inner());
905        let file = match guard.as_ref() {
906            Some(f) => f,
907            None => return err::default_error(err::INV).with_module(mid),
908        };
909
910        file.sync_range(offset, len_to_sync).with_module(mid)
911    }
912
913    /// Fetch total available buffers in [`File`]
914    ///
915    /// ## Example
916    ///
917    /// ```
918    /// use frozen_core::file::{File, FileCfg};
919    ///
920    /// let dir = tempfile::tempdir().unwrap();
921    /// let cfg = FileCfg {
922    ///     module_id: 1,
923    ///     path: dir.path().join("buffers.bin"),
924    ///     buffer_size: 32,
925    ///     initial_available_buffers: 8,
926    ///     sync_interval: None,
927    ///     sequential_access_hint: false,
928    /// };
929    ///
930    /// let file = File::new(cfg).unwrap();
931    /// assert_eq!(file.total_buffers().unwrap(), 8);
932    /// ```
933    #[inline]
934    pub fn total_buffers(&self) -> FrozenResult<usize> {
935        let curr_len = self.length();
936        let buffer_size = self.cfg.buffer_size;
937
938        if crate::hints::unlikely(curr_len % buffer_size != 0) {
939            return err::default_error(err::CPT).with_module(self.cfg.module_id);
940        }
941
942        Ok(curr_len / buffer_size)
943    }
944
945    /// Get reference to configuration of [`File`]
946    ///
947    /// ## Example
948    ///
949    /// ```
950    /// use frozen_core::file::{File, FileCfg};
951    ///
952    /// let dir = tempfile::tempdir().unwrap();
953    /// let cfg = FileCfg {
954    ///     module_id: 1,
955    ///     path: dir.path().join("cfg.bin"),
956    ///     buffer_size: 64,
957    ///     initial_available_buffers: 2,
958    ///     sync_interval: None,
959    ///     sequential_access_hint: false,
960    /// };
961    ///
962    /// let file = File::new(cfg).unwrap();
963    /// assert_eq!(file.cfg().buffer_size, 64);
964    /// assert_eq!(file.cfg().initial_available_buffers, 2);
965    /// ```
966    #[inline]
967    pub fn cfg(&self) -> &FileCfg {
968        &self.cfg
969    }
970
971    /// Read current length (in bytes) of [`File`]
972    ///
973    /// ## Example
974    ///
975    /// ```
976    /// use frozen_core::file::{File, FileCfg};
977    ///
978    /// let dir = tempfile::tempdir().unwrap();
979    /// let cfg = FileCfg {
980    ///     module_id: 1,
981    ///     path: dir.path().join("len.bin"),
982    ///     buffer_size: 128,
983    ///     initial_available_buffers: 4,
984    ///     sync_interval: None,
985    ///     sequential_access_hint: false,
986    /// };
987    ///
988    /// let file = File::new(cfg).unwrap();
989    /// assert_eq!(file.length(), 512);
990    /// ```
991    #[inline]
992    pub fn length(&self) -> usize {
993        self.inner.current_length.load(atomic::Ordering::Acquire)
994    }
995
996    /// Fetch the latest assigned durability epoch
997    ///
998    /// ## Example
999    ///
1000    /// ```
1001    /// use frozen_core::file::{File, FileCfg};
1002    ///
1003    /// let dir = tempfile::tempdir().unwrap();
1004    /// let cfg = FileCfg {
1005    ///     module_id: 1,
1006    ///     path: dir.path().join("epoch.bin"),
1007    ///     buffer_size: 16,
1008    ///     initial_available_buffers: 2,
1009    ///     sync_interval: None,
1010    ///     sequential_access_hint: false,
1011    /// };
1012    ///
1013    /// let file = File::new(cfg).unwrap();
1014    /// assert_eq!(file.current_epoch(), 0);
1015    ///
1016    /// file.write(&[1u8; 16], 0).unwrap();
1017    /// assert_eq!(file.current_epoch(), 1);
1018    /// ```
1019    #[inline]
1020    pub fn current_epoch(&self) -> TEpoch {
1021        self.inner.completion.read_current_epoch()
1022    }
1023
1024    /// Fetch the latest durable epoch
1025    ///
1026    /// ## Example
1027    ///
1028    /// ```
1029    /// use frozen_core::file::{File, FileCfg};
1030    ///
1031    /// let dir = tempfile::tempdir().unwrap();
1032    /// let cfg = FileCfg {
1033    ///     module_id: 1,
1034    ///     path: dir.path().join("durable_epoch.bin"),
1035    ///     buffer_size: 16,
1036    ///     initial_available_buffers: 2,
1037    ///     sync_interval: None,
1038    ///     sequential_access_hint: false,
1039    /// };
1040    ///
1041    /// let file = File::new(cfg).unwrap();
1042    /// file.write(&[1u8; 16], 0).unwrap();
1043    /// assert_eq!(file.durable_epoch(), 0);
1044    ///
1045    /// file.sync().unwrap();
1046    /// assert_eq!(file.durable_epoch(), 1);
1047    /// ```
1048    #[inline]
1049    pub fn durable_epoch(&self) -> TEpoch {
1050        self.inner.completion.read_durable_epoch()
1051    }
1052
1053    /// Fetch reference to underlying durability [`Completion`]
1054    ///
1055    /// ## Example
1056    ///
1057    /// ```
1058    /// use frozen_core::file::{File, FileCfg};
1059    ///
1060    /// let dir = tempfile::tempdir().unwrap();
1061    /// let cfg = FileCfg {
1062    ///     module_id: 1,
1063    ///     path: dir.path().join("completion.bin"),
1064    ///     buffer_size: 16,
1065    ///     initial_available_buffers: 2,
1066    ///     sync_interval: None,
1067    ///     sequential_access_hint: false,
1068    /// };
1069    ///
1070    /// let file = File::new(cfg).unwrap();
1071    /// let completion = file.completion();
1072    /// assert_eq!(completion.read_current_epoch(), 0);
1073    /// ```
1074    #[inline]
1075    pub fn completion(&self) -> &Arc<Completion> {
1076        &self.inner.completion
1077    }
1078
1079    /// Check if [`File`] exists on storage device or not
1080    ///
1081    /// ## Access Semantics
1082    ///
1083    /// Uses `access(path, F_OK)` to verify the existence of the file
1084    ///
1085    /// ## Example
1086    ///
1087    /// ```
1088    /// use frozen_core::file::{File, FileCfg};
1089    ///
1090    /// let dir = tempfile::tempdir().unwrap();
1091    /// let cfg = FileCfg {
1092    ///     module_id: 1,
1093    ///     path: dir.path().join("exists.bin"),
1094    ///     buffer_size: 16,
1095    ///     initial_available_buffers: 2,
1096    ///     sync_interval: None,
1097    ///     sequential_access_hint: false,
1098    /// };
1099    ///
1100    /// let file = File::new(cfg).unwrap();
1101    /// assert!(file.exists().unwrap());
1102    /// ```
1103    #[inline]
1104    pub fn exists(&self) -> FrozenResult<bool> {
1105        PlatformFile::exists(&self.cfg.path).with_module(self.cfg.module_id)
1106    }
1107
1108    /// Deletes the [`File`] entry from the storage device
1109    ///
1110    /// Consumes `self` by value to prevent any concurrent or post-deletion operations
1111    ///
1112    /// Unlinks the file at `path`, closes the underlying descriptor, and syncs the parent directory
1113    /// to guarantee crash-safe durability
1114    ///
1115    /// ## Caller Responsibilities
1116    ///
1117    /// The caller must ensure that when calling `delete`, no write operations are pending for
1118    /// durability and no write operations are invoked during or after `delete` is called
1119    ///
1120    /// ## Example
1121    ///
1122    /// ```
1123    /// use frozen_core::file::{File, FileCfg};
1124    ///
1125    /// let dir = tempfile::tempdir().unwrap();
1126    /// let path = dir.path().join("delete_test.bin");
1127    /// let cfg = FileCfg {
1128    ///     module_id: 1,
1129    ///     path: path.clone(),
1130    ///     buffer_size: 16,
1131    ///     initial_available_buffers: 2,
1132    ///     sync_interval: None,
1133    ///     sequential_access_hint: false,
1134    /// };
1135    ///
1136    /// let file = File::new(cfg).unwrap();
1137    /// assert!(path.exists());
1138    ///
1139    /// file.delete().unwrap();
1140    /// assert!(!path.exists());
1141    /// ```
1142    pub fn delete(mut self) -> FrozenResult<()> {
1143        let mid = self.cfg.module_id;
1144        self.inner.shutdown.store(true, atomic::Ordering::Release);
1145        {
1146            let mut guard = self.inner.sync_mutex.lock().unwrap_or_else(|e| e.into_inner());
1147            *guard = true;
1148        }
1149        self.inner.sync_condvar.notify_all();
1150
1151        if let Some(handle) = self.bg_thread.take() {
1152            let _ = handle.join();
1153        }
1154
1155        let mut guard = self.inner.file.write().unwrap_or_else(|e| e.into_inner());
1156        let file = match guard.take() {
1157            Some(f) => f,
1158            None => return err::default_error(err::INV).with_module(mid),
1159        };
1160
1161        file.unlink(&self.cfg.path).with_module(mid)
1162    }
1163
1164    /// Get file descriptor or handle for [`File`]
1165    ///
1166    /// ## Example
1167    ///
1168    /// ```
1169    /// use frozen_core::file::{File, FileCfg};
1170    ///
1171    /// let dir = tempfile::tempdir().unwrap();
1172    /// let cfg = FileCfg {
1173    ///     module_id: 1,
1174    ///     path: dir.path().join("fd_test.bin"),
1175    ///     buffer_size: 16,
1176    ///     initial_available_buffers: 2,
1177    ///     sync_interval: None,
1178    ///     sequential_access_hint: false,
1179    /// };
1180    ///
1181    /// let file = File::new(cfg).unwrap();
1182    /// #[cfg(unix)]
1183    /// assert!(file.fd() >= 0);
1184    /// #[cfg(windows)]
1185    /// assert_ne!(file.fd(), -1isize);
1186    /// ```
1187    #[inline]
1188    pub fn fd(&self) -> FileId {
1189        let guard = self.inner.file.read().unwrap_or_else(|e| e.into_inner());
1190        match guard.as_ref() {
1191            Some(f) => f.fd(),
1192            None => PlatformFile::CLOSED_ID,
1193        }
1194    }
1195}
1196
1197impl Drop for File {
1198    fn drop(&mut self) {
1199        self.inner.shutdown.store(true, atomic::Ordering::Release);
1200        {
1201            let mut guard = self.inner.sync_mutex.lock().unwrap_or_else(|e| e.into_inner());
1202            *guard = true;
1203        }
1204        self.inner.sync_condvar.notify_all();
1205
1206        if let Some(handle) = self.bg_thread.take() {
1207            let _ = handle.join();
1208        }
1209
1210        let is_closed = {
1211            let guard = self.inner.file.read().unwrap_or_else(|e| e.into_inner());
1212            match guard.as_ref() {
1213                Some(f) => f.is_closed(),
1214                None => true,
1215            }
1216        };
1217
1218        if is_closed {
1219            return;
1220        }
1221
1222        let _ = self.inner.sync_internal();
1223    }
1224}
1225
1226#[cfg(test)]
1227mod tests {
1228    use super::*;
1229    use std::sync::{Arc, Barrier};
1230
1231    const MID: u8 = 0;
1232    const BUFFER_SIZE: usize = 0x10;
1233    const INIT_BUFFERS: usize = 4;
1234
1235    fn tmp_path() -> (tempfile::TempDir, FileCfg) {
1236        let dir = tempfile::tempdir().unwrap();
1237        let path = dir.path().join("tmp_file");
1238        let cfg = FileCfg {
1239            module_id: MID,
1240            path,
1241            buffer_size: BUFFER_SIZE,
1242            initial_available_buffers: INIT_BUFFERS,
1243            sync_interval: None,
1244            sequential_access_hint: false,
1245        };
1246
1247        (dir, cfg)
1248    }
1249
1250    mod file_new {
1251        use super::*;
1252
1253        #[test]
1254        fn ok_new_creates_file_with_correct_length() {
1255            let (_dir, cfg) = tmp_path();
1256            let file = File::new(cfg.clone()).unwrap();
1257
1258            let expected_len = BUFFER_SIZE * INIT_BUFFERS;
1259            assert_eq!(file.length(), expected_len);
1260            assert_eq!(file.cfg().buffer_size, BUFFER_SIZE);
1261            assert_ne!(file.fd(), PlatformFile::CLOSED_ID);
1262            assert!(cfg.path.exists());
1263        }
1264
1265        #[test]
1266        fn err_new_when_already_exists() {
1267            let (_dir, cfg) = tmp_path();
1268            let file = File::new(cfg.clone()).unwrap();
1269            drop(file);
1270
1271            let err = File::new(cfg).unwrap_err();
1272            assert_eq!(err.reason, err::EXS.reason);
1273        }
1274
1275        #[test]
1276        fn err_new_invalid_cfg() {
1277            let (_dir, mut cfg) = tmp_path();
1278
1279            cfg.buffer_size = 0;
1280            let err = File::new(cfg.clone()).unwrap_err();
1281            assert_eq!(err.reason, err::INV.reason);
1282
1283            cfg.buffer_size = BUFFER_SIZE;
1284            cfg.initial_available_buffers = 0;
1285            let err = File::new(cfg).unwrap_err();
1286            assert_eq!(err.reason, err::INV.reason);
1287        }
1288
1289        #[test]
1290        fn ok_new_sequential_access_hint() {
1291            let (_dir, mut cfg) = tmp_path();
1292            cfg.sequential_access_hint = true;
1293            let file = File::new(cfg.clone()).unwrap();
1294            assert_eq!(file.length(), BUFFER_SIZE * INIT_BUFFERS);
1295            assert!(file.cfg().sequential_access_hint);
1296            drop(file);
1297
1298            let opened = File::open(cfg.clone()).unwrap();
1299            assert_eq!(opened.length(), BUFFER_SIZE * INIT_BUFFERS);
1300            drop(opened);
1301
1302            let open_or_created = File::open_or_create(cfg).unwrap();
1303            assert_eq!(open_or_created.length(), BUFFER_SIZE * INIT_BUFFERS);
1304        }
1305
1306        #[test]
1307        fn err_new_cfg_overflow() {
1308            let (_dir, mut cfg) = tmp_path();
1309            cfg.buffer_size = usize::MAX;
1310            cfg.initial_available_buffers = 2;
1311            let err = File::new(cfg).unwrap_err();
1312            assert_eq!(err.reason, err::INV.reason);
1313        }
1314
1315        #[test]
1316        fn err_new_missing_parent_dir() {
1317            let (_dir, mut cfg) = tmp_path();
1318            cfg.path = cfg.path.join("missing/sub/dir/file.db");
1319
1320            let err = File::new(cfg).unwrap_err();
1321            assert_eq!(err.reason, err::INV.reason);
1322        }
1323
1324        #[test]
1325        fn err_new_cleans_up_on_grow_failure() {
1326            let (_dir, mut cfg) = tmp_path();
1327            // On 64-bit systems, setting buffer_size such that buffer_size * initial_buffers exceeds off_t::MAX /
1328            // i64::MAX triggers grow failure after successful creation
1329            cfg.buffer_size = 1 << 0x3F;
1330            cfg.initial_available_buffers = 1;
1331
1332            let err = File::new(cfg.clone()).unwrap_err();
1333            assert_eq!(err.reason, err::GRW.reason);
1334
1335            // File must be unlinked and not left as an orphan on disk
1336            assert!(!cfg.path.exists());
1337
1338            // A subsequent File::new with valid config must succeed and not fail with EXS
1339            cfg.buffer_size = BUFFER_SIZE;
1340            cfg.initial_available_buffers = INIT_BUFFERS;
1341            let file = File::new(cfg);
1342            assert!(file.is_ok());
1343        }
1344    }
1345
1346    mod file_open {
1347        use super::*;
1348
1349        #[test]
1350        fn ok_open_existing_valid_file() {
1351            let (_dir, cfg) = tmp_path();
1352
1353            {
1354                let file = File::new(cfg.clone()).unwrap();
1355                assert_eq!(file.length(), BUFFER_SIZE * INIT_BUFFERS);
1356            }
1357
1358            let file = File::open(cfg).unwrap();
1359            assert_eq!(file.length(), BUFFER_SIZE * INIT_BUFFERS);
1360        }
1361
1362        #[test]
1363        fn err_open_missing_file() {
1364            let (_dir, cfg) = tmp_path();
1365
1366            let err = File::open(cfg).unwrap_err();
1367            assert_eq!(err.reason, err::INV.reason);
1368        }
1369
1370        #[test]
1371        fn err_open_invalid_cfg() {
1372            let (_dir, mut cfg) = tmp_path();
1373
1374            cfg.buffer_size = 0;
1375            let err = File::open(cfg.clone()).unwrap_err();
1376            assert_eq!(err.reason, err::INV.reason);
1377
1378            cfg.buffer_size = BUFFER_SIZE;
1379            cfg.initial_available_buffers = 0;
1380            let err = File::open(cfg).unwrap_err();
1381            assert_eq!(err.reason, err::INV.reason);
1382        }
1383
1384        #[test]
1385        fn err_open_cfg_overflow() {
1386            let (_dir, mut cfg) = tmp_path();
1387            cfg.buffer_size = usize::MAX;
1388            cfg.initial_available_buffers = 2;
1389            let err = File::open(cfg).unwrap_err();
1390            assert_eq!(err.reason, err::INV.reason);
1391        }
1392
1393        #[test]
1394        fn err_open_when_file_smaller_than_init_len() {
1395            let (_dir, cfg) = tmp_path();
1396
1397            std::fs::write(&cfg.path, []).unwrap();
1398
1399            let err = File::open(cfg).unwrap_err();
1400            assert_eq!(err.reason, err::CPT.reason);
1401        }
1402
1403        #[test]
1404        fn err_open_when_file_not_buffer_multiple() {
1405            let (_dir, cfg) = tmp_path();
1406
1407            let non_aligned_len = (BUFFER_SIZE * INIT_BUFFERS) + 3;
1408            std::fs::write(&cfg.path, vec![0u8; non_aligned_len]).unwrap();
1409
1410            let err = File::open(cfg).unwrap_err();
1411            assert_eq!(err.reason, err::CPT.reason);
1412        }
1413    }
1414
1415    mod file_open_or_create {
1416        use super::*;
1417
1418        #[test]
1419        fn ok_creates_when_missing() {
1420            let (_dir, cfg) = tmp_path();
1421            assert!(!cfg.path.exists());
1422
1423            let file = File::open_or_create(cfg.clone()).unwrap();
1424            assert!(cfg.path.exists());
1425            assert_eq!(file.length(), BUFFER_SIZE * INIT_BUFFERS);
1426            assert!(file.exists().unwrap());
1427        }
1428
1429        #[test]
1430        fn ok_opens_when_existing() {
1431            let (_dir, cfg) = tmp_path();
1432            let file = File::new(cfg.clone()).unwrap();
1433            let data = [0x5Au8; BUFFER_SIZE];
1434            file.write(&data, 0).unwrap();
1435            file.sync().unwrap();
1436            drop(file);
1437
1438            let opened = File::open_or_create(cfg.clone()).unwrap();
1439            assert_eq!(opened.length(), BUFFER_SIZE * INIT_BUFFERS);
1440            let mut buf = [0u8; BUFFER_SIZE];
1441            opened.read(&mut buf, 0).unwrap();
1442            assert_eq!(buf, data);
1443        }
1444
1445        #[test]
1446        fn err_open_or_create_invalid_cfg() {
1447            let (_dir, mut cfg) = tmp_path();
1448            cfg.buffer_size = 0;
1449            let err = File::open_or_create(cfg.clone()).unwrap_err();
1450            assert_eq!(err.reason, err::INV.reason);
1451
1452            cfg.buffer_size = BUFFER_SIZE;
1453            cfg.initial_available_buffers = 0;
1454            let err = File::open_or_create(cfg).unwrap_err();
1455            assert_eq!(err.reason, err::INV.reason);
1456        }
1457
1458        #[test]
1459        fn err_open_or_create_cfg_overflow() {
1460            let (_dir, mut cfg) = tmp_path();
1461            cfg.buffer_size = usize::MAX;
1462            cfg.initial_available_buffers = 2;
1463            let err = File::open_or_create(cfg).unwrap_err();
1464            assert_eq!(err.reason, err::INV.reason);
1465        }
1466
1467        #[test]
1468        fn err_open_or_create_corrupt_existing() {
1469            let (_dir, cfg) = tmp_path();
1470            let non_aligned = (BUFFER_SIZE * INIT_BUFFERS) + 1;
1471            std::fs::write(&cfg.path, vec![0u8; non_aligned]).unwrap();
1472
1473            let err = File::open_or_create(cfg).unwrap_err();
1474            assert_eq!(err.reason, err::CPT.reason);
1475        }
1476
1477        #[test]
1478        fn err_open_or_create_when_locked() {
1479            let (_dir, cfg) = tmp_path();
1480            let file = File::open_or_create(cfg.clone()).unwrap();
1481
1482            let err = File::open_or_create(cfg).unwrap_err();
1483            assert_eq!(err.reason, err::LCK.reason);
1484            drop(file);
1485        }
1486    }
1487
1488    mod file_toctou_and_concurrency {
1489        use super::*;
1490
1491        #[test]
1492        fn err_concurrent_new_exact_same_path() {
1493            let (_dir, cfg) = tmp_path();
1494            let barrier = Arc::new(Barrier::new(2));
1495
1496            let t1 = {
1497                let cfg = cfg.clone();
1498                let b = barrier.clone();
1499                std::thread::spawn(move || {
1500                    b.wait();
1501                    File::new(cfg)
1502                })
1503            };
1504
1505            let t2 = {
1506                let b = barrier;
1507                std::thread::spawn(move || {
1508                    b.wait();
1509                    File::new(cfg)
1510                })
1511            };
1512
1513            let r1 = t1.join().unwrap();
1514            let r2 = t2.join().unwrap();
1515
1516            // NOTE:
1517            //
1518            // Due to atomic `O_CREAT | O_EXCL`, exactly one MUST succeed and the other MUST fail with EXS
1519            match (r1, r2) {
1520                (Ok(f), Err(e)) | (Err(e), Ok(f)) => {
1521                    assert_eq!(e.reason, err::EXS.reason);
1522                    assert_eq!(f.length(), BUFFER_SIZE * INIT_BUFFERS);
1523                }
1524                (Ok(_), Ok(_)) => {
1525                    panic!("TOCTOU violation: both concurrent File::new succeeded on same path!");
1526                }
1527                (Err(e1), Err(e2)) => {
1528                    panic!("Unexpected failure of both threads: {:?}, {:?}", e1, e2);
1529                }
1530            }
1531        }
1532
1533        #[test]
1534        fn err_concurrent_new_and_open() {
1535            let (_dir, cfg) = tmp_path();
1536            let barrier = Arc::new(Barrier::new(2));
1537
1538            let t_new = {
1539                let cfg = cfg.clone();
1540                let b = barrier.clone();
1541                std::thread::spawn(move || {
1542                    b.wait();
1543                    File::new(cfg)
1544                })
1545            };
1546
1547            let t_open = {
1548                let b = barrier;
1549                std::thread::spawn(move || {
1550                    b.wait();
1551                    File::open(cfg)
1552                })
1553            };
1554
1555            let r_new = t_new.join().unwrap();
1556            let r_open = t_open.join().unwrap();
1557
1558            let _file = r_new.expect("File::new should succeed");
1559
1560            // File::open either ran before creation (INV), while lock was held (LCK), or in the brief window between
1561            // creation and grow/sync (CPT)
1562            if let Err(e) = r_open {
1563                assert!(
1564                    e.reason == err::INV.reason
1565                        || e.reason == err::LCK.reason
1566                        || e.reason == err::CPT.reason,
1567                    "unexpected error: {:?}",
1568                    e
1569                );
1570            }
1571        }
1572
1573        #[test]
1574        fn err_concurrent_open_same_file() {
1575            let (_dir, cfg) = tmp_path();
1576
1577            {
1578                let file = File::new(cfg.clone()).unwrap();
1579                assert_eq!(file.length(), BUFFER_SIZE * INIT_BUFFERS);
1580            }
1581
1582            let barrier = Arc::new(Barrier::new(2));
1583
1584            let t1 = {
1585                let cfg = cfg.clone();
1586                let b = barrier.clone();
1587                std::thread::spawn(move || {
1588                    b.wait();
1589                    File::open(cfg)
1590                })
1591            };
1592
1593            let t2 = {
1594                let b = barrier;
1595                std::thread::spawn(move || {
1596                    b.wait();
1597                    File::open(cfg)
1598                })
1599            };
1600
1601            let r1 = t1.join().unwrap();
1602            let r2 = t2.join().unwrap();
1603
1604            // NOTE:
1605            //
1606            // Exactly one must acquire exclusive flock and succeed, the other fails with LCK
1607            match (r1, r2) {
1608                (Ok(_), Err(e)) | (Err(e), Ok(_)) => {
1609                    assert_eq!(e.reason, err::LCK.reason, "error must be LCK: {:?}", e);
1610                }
1611                (Ok(_), Ok(_)) => {
1612                    panic!(
1613                        "Locking violation: both concurrent File::open acquired exclusive lock!"
1614                    );
1615                }
1616                (Err(e1), Err(e2)) => {
1617                    panic!("Unexpected failure of both threads: {:?}, {:?}", e1, e2);
1618                }
1619            }
1620        }
1621
1622        #[test]
1623        fn err_new_while_file_open() {
1624            let (_dir, cfg) = tmp_path();
1625            let file = File::new(cfg.clone()).unwrap();
1626
1627            let err = File::new(cfg).unwrap_err();
1628            assert_eq!(err.reason, err::EXS.reason);
1629
1630            drop(file);
1631        }
1632
1633        #[test]
1634        fn ok_reopen_after_drop() {
1635            let (_dir, cfg) = tmp_path();
1636            let file = File::new(cfg.clone()).unwrap();
1637            drop(file);
1638
1639            let reopened = File::open(cfg).unwrap();
1640            assert_eq!(reopened.length(), BUFFER_SIZE * INIT_BUFFERS);
1641        }
1642
1643        #[test]
1644        fn err_concurrent_open_during_and_after_delete() {
1645            let (_dir, cfg) = tmp_path();
1646            let file = File::new(cfg.clone()).unwrap();
1647            file.grow(2).unwrap();
1648
1649            let running = Arc::new(atomic::AtomicBool::new(true));
1650
1651            // Thread 2: constantly attempts File::open
1652            let opener = {
1653                let cfg = cfg.clone();
1654                let running = running.clone();
1655                std::thread::spawn(move || {
1656                    let mut attempts = 0;
1657                    while running.load(atomic::Ordering::Relaxed) || attempts < 0x32 {
1658                        attempts += 1;
1659                        let res = File::open(cfg.clone());
1660                        // While the file is open, open fails with LCK On Windows, opening a delete-pending file yields
1661                        // PRM Once the file is deleted, open fails with INV
1662                        //
1663                        // On POSIX, a descriptor opened right before unlink may momentarily succeed after deletion
1664                        // releases the writer's lock; if so, dropping it is completely safe
1665                        if let Err(err) = res {
1666                            assert!(
1667                                err.reason == err::LCK.reason
1668                                    || err.reason == err::INV.reason
1669                                    || err.reason == err::PRM.reason,
1670                                "Unexpected error reason: {:?}",
1671                                err
1672                            );
1673                        }
1674                        std::thread::yield_now();
1675                    }
1676                })
1677            };
1678
1679            // Thread 1: deletes the file
1680            std::thread::sleep(std::time::Duration::from_millis(5));
1681            file.delete().unwrap();
1682            running.store(false, atomic::Ordering::Relaxed);
1683
1684            opener.join().unwrap();
1685
1686            // After delete, opening must consistently fail with INV
1687            let err = File::open(cfg).unwrap_err();
1688            assert_eq!(err.reason, err::INV.reason);
1689        }
1690
1691        #[test]
1692        fn ok_concurrent_new_fails_before_delete_and_succeeds_after() {
1693            let (_dir, cfg) = tmp_path();
1694            let file = File::new(cfg.clone()).unwrap();
1695            file.grow(2).unwrap();
1696
1697            let deleted = Arc::new(atomic::AtomicBool::new(false));
1698
1699            let creator = {
1700                let cfg = cfg.clone();
1701                let deleted = deleted.clone();
1702                std::thread::spawn(move || {
1703                    let mut created = None;
1704                    // Before delete occurs, File::new must fail with EXS (or PRM on Windows while delete is pending)
1705                    // Once delete completes, File::new must eventually succeed
1706                    while created.is_none() {
1707                        match File::new(cfg.clone()) {
1708                            Ok(f) => {
1709                                assert!(
1710                                    deleted.load(atomic::Ordering::Acquire),
1711                                    "File::new succeeded before file.delete completed!"
1712                                );
1713                                created = Some(f);
1714                            }
1715                            Err(e) => {
1716                                assert!(
1717                                    e.reason == err::EXS.reason || e.reason == err::PRM.reason,
1718                                    "Unexpected error reason during race: {:?}",
1719                                    e
1720                                );
1721                                std::thread::yield_now();
1722                            }
1723                        }
1724                    }
1725                    created.unwrap()
1726                })
1727            };
1728
1729            std::thread::sleep(std::time::Duration::from_millis(0x0A));
1730            deleted.store(true, atomic::Ordering::Release);
1731            file.delete().unwrap();
1732
1733            let new_file = creator.join().unwrap();
1734            assert!(new_file.exists().unwrap());
1735            new_file.delete().unwrap();
1736        }
1737
1738        #[test]
1739        fn ok_concurrent_open_or_create_same_file() {
1740            let (_dir, cfg) = tmp_path();
1741            let num_threads = 8;
1742            let barrier = Arc::new(Barrier::new(num_threads));
1743            let mut handles = vec![];
1744
1745            for _ in 0..num_threads {
1746                let c = cfg.clone();
1747                let b = barrier.clone();
1748                handles.push(std::thread::spawn(move || {
1749                    b.wait();
1750                    File::open_or_create(c)
1751                }));
1752            }
1753
1754            let mut opened_files = vec![];
1755            let mut lock_failures = 0;
1756
1757            for h in handles {
1758                match h.join().unwrap() {
1759                    Ok(f) => {
1760                        assert_eq!(f.length(), BUFFER_SIZE * INIT_BUFFERS);
1761                        opened_files.push(f);
1762                    }
1763                    Err(e) => {
1764                        assert_eq!(e.reason, err::LCK.reason, "error must be LCK: {:?}", e);
1765                        lock_failures += 1;
1766                    }
1767                }
1768            }
1769
1770            // Exactly one must acquire the exclusive lock while other concurrent callers fail with LCK
1771            assert_eq!(opened_files.len(), 1);
1772            assert_eq!(lock_failures, num_threads - 1);
1773            assert!(cfg.path.exists());
1774        }
1775
1776        #[test]
1777        fn ok_file_send_and_sync() {
1778            fn assert_send_sync<T: Send + Sync>() {}
1779            assert_send_sync::<File>();
1780        }
1781    }
1782
1783    mod file_write_read {
1784        use super::*;
1785
1786        #[test]
1787        fn ok_single_buffer_write_read() {
1788            let (_dir, cfg) = tmp_path();
1789            let file = File::new(cfg).unwrap();
1790
1791            let data = [0xABu8; BUFFER_SIZE];
1792            file.write(&data, 2).unwrap();
1793            file.sync().unwrap();
1794
1795            let mut buf = [0u8; BUFFER_SIZE];
1796            file.read(&mut buf, 2).unwrap();
1797            assert_eq!(buf, data);
1798        }
1799
1800        #[test]
1801        fn ok_multi_buffer_write_read() {
1802            let (_dir, cfg) = tmp_path();
1803            let file = File::new(cfg).unwrap();
1804
1805            let multi_buf_data = [0x77u8; BUFFER_SIZE * 2];
1806            file.write(&multi_buf_data, 1).unwrap();
1807            file.sync().unwrap();
1808
1809            let mut read_buf = [0u8; BUFFER_SIZE * 2];
1810            file.read(&mut read_buf, 1).unwrap();
1811            assert_eq!(read_buf, multi_buf_data);
1812
1813            let mut chunk1 = [0u8; BUFFER_SIZE];
1814            let mut chunk2 = [0u8; BUFFER_SIZE];
1815            file.read(&mut chunk1, 1).unwrap();
1816            file.read(&mut chunk2, 2).unwrap();
1817            assert_eq!(chunk1, [0x77u8; BUFFER_SIZE]);
1818            assert_eq!(chunk2, [0x77u8; BUFFER_SIZE]);
1819        }
1820
1821        #[test]
1822        fn ok_empty_buffer_noop() {
1823            let (_dir, cfg) = tmp_path();
1824            let file = File::new(cfg).unwrap();
1825
1826            assert!(file.write(&[], 0).is_ok());
1827            assert!(file.read(&mut [], 0).is_ok());
1828        }
1829
1830        #[test]
1831        fn err_write_unaligned_buffer_size() {
1832            let (_dir, cfg) = tmp_path();
1833            let file = File::new(cfg).unwrap();
1834
1835            let data = [0u8; BUFFER_SIZE - 1];
1836            let err = file.write(&data, 0).unwrap_err();
1837            assert_eq!(err.reason, err::INV.reason);
1838
1839            let data_multi_unaligned = [0u8; (BUFFER_SIZE * 2) + 1];
1840            let err = file.write(&data_multi_unaligned, 0).unwrap_err();
1841            assert_eq!(err.reason, err::INV.reason);
1842        }
1843
1844        #[test]
1845        fn err_read_unaligned_buffer_size() {
1846            let (_dir, cfg) = tmp_path();
1847            let file = File::new(cfg).unwrap();
1848
1849            let mut buf = [0u8; BUFFER_SIZE - 1];
1850            let err = file.read(&mut buf, 0).unwrap_err();
1851            assert_eq!(err.reason, err::INV.reason);
1852
1853            let mut buf_multi_unaligned = [0u8; (BUFFER_SIZE * 2) + 3];
1854            let err = file.read(&mut buf_multi_unaligned, 0).unwrap_err();
1855            assert_eq!(err.reason, err::INV.reason);
1856        }
1857
1858        #[test]
1859        fn err_read_past_eof() {
1860            let (_dir, cfg) = tmp_path();
1861            let file = File::new(cfg).unwrap();
1862
1863            let mut buf = [0u8; BUFFER_SIZE];
1864            let err = file.read(&mut buf, INIT_BUFFERS).unwrap_err();
1865            assert_eq!(err.reason, err::HCF.reason);
1866
1867            let mut multi_buf = [0u8; BUFFER_SIZE * 2];
1868            let err = file.read(&mut multi_buf, INIT_BUFFERS - 1).unwrap_err();
1869            assert_eq!(err.reason, err::HCF.reason);
1870        }
1871
1872        #[test]
1873        fn err_write_past_eof() {
1874            let (_dir, cfg) = tmp_path();
1875            let file = File::new(cfg).unwrap();
1876
1877            let data = [1u8; BUFFER_SIZE];
1878            let err = file.write(&data, INIT_BUFFERS).unwrap_err();
1879            assert_eq!(err.reason, err::HCF.reason);
1880
1881            let multi_data = [1u8; BUFFER_SIZE * 2];
1882            let err = file.write(&multi_data, INIT_BUFFERS - 1).unwrap_err();
1883            assert_eq!(err.reason, err::HCF.reason);
1884        }
1885
1886        #[test]
1887        fn ok_concurrent_non_overlapping_writes() {
1888            let (_dir, mut cfg) = tmp_path();
1889            cfg.initial_available_buffers = 0x10;
1890            let file = Arc::new(File::new(cfg).unwrap());
1891
1892            let mut handles = vec![];
1893            for i in 0..4 {
1894                let f = file.clone();
1895                handles.push(std::thread::spawn(move || {
1896                    let data = [i as u8 + 0x0A; BUFFER_SIZE * 2];
1897                    f.write(&data, i * 2).unwrap();
1898                }));
1899            }
1900
1901            for h in handles {
1902                h.join().unwrap();
1903            }
1904
1905            file.sync().unwrap();
1906
1907            for i in 0..4 {
1908                let mut buf = [0u8; BUFFER_SIZE * 2];
1909                file.read(&mut buf, i * 2).unwrap();
1910                assert_eq!(buf, [i as u8 + 0x0A; BUFFER_SIZE * 2]);
1911            }
1912        }
1913
1914        #[test]
1915        fn ok_concurrent_readers() {
1916            let (_dir, cfg) = tmp_path();
1917            let file = Arc::new(File::new(cfg).unwrap());
1918
1919            let data = [0x42u8; BUFFER_SIZE * 2];
1920            file.write(&data, 0).unwrap();
1921            file.sync().unwrap();
1922
1923            let mut handles = vec![];
1924            for _ in 0..8 {
1925                let f = file.clone();
1926                let expected = data;
1927                handles.push(std::thread::spawn(move || {
1928                    let mut buf = [0u8; BUFFER_SIZE * 2];
1929                    f.read(&mut buf, 0).unwrap();
1930                    assert_eq!(buf, expected);
1931                }));
1932            }
1933
1934            for h in handles {
1935                h.join().unwrap();
1936            }
1937        }
1938    }
1939
1940    mod file_grow {
1941        use super::*;
1942
1943        #[test]
1944        fn ok_grow_updates_length() {
1945            let (_dir, cfg) = tmp_path();
1946            let file = File::new(cfg).unwrap();
1947            assert_eq!(file.length(), BUFFER_SIZE * INIT_BUFFERS);
1948            assert_eq!(file.total_buffers().unwrap(), INIT_BUFFERS);
1949
1950            file.grow(0x20).unwrap();
1951            assert_eq!(file.length(), BUFFER_SIZE * (INIT_BUFFERS + 0x20));
1952            assert_eq!(file.total_buffers().unwrap(), INIT_BUFFERS + 0x20);
1953        }
1954
1955        #[test]
1956        fn ok_grow_zero_count() {
1957            let (_dir, cfg) = tmp_path();
1958            let file = File::new(cfg).unwrap();
1959            file.grow(0).unwrap();
1960            assert_eq!(file.length(), BUFFER_SIZE * INIT_BUFFERS);
1961        }
1962
1963        #[test]
1964        fn ok_grow_and_multi_buffer_write_read() {
1965            let (_dir, cfg) = tmp_path();
1966            let file = File::new(cfg).unwrap();
1967
1968            file.grow(4).unwrap();
1969            let new_index = INIT_BUFFERS;
1970
1971            let data = [0x55u8; BUFFER_SIZE * 3];
1972            file.write(&data, new_index).unwrap();
1973            file.sync().unwrap();
1974
1975            let mut buf = [0u8; BUFFER_SIZE * 3];
1976            file.read(&mut buf, new_index).unwrap();
1977            assert_eq!(buf, data);
1978        }
1979
1980        #[test]
1981        fn ok_concurrent_grow_and_write() {
1982            let (_dir, cfg) = tmp_path();
1983            let file = Arc::new(File::new(cfg).unwrap());
1984
1985            let writer = {
1986                let f = file.clone();
1987                std::thread::spawn(move || {
1988                    for i in 0..INIT_BUFFERS {
1989                        let data = [i as u8; BUFFER_SIZE];
1990                        f.write(&data, i).unwrap();
1991                    }
1992                })
1993            };
1994
1995            let chunks_to_grow = 0x20;
1996            let grower = {
1997                let f = file.clone();
1998                std::thread::spawn(move || {
1999                    f.grow(chunks_to_grow).unwrap();
2000                })
2001            };
2002
2003            writer.join().unwrap();
2004            grower.join().unwrap();
2005
2006            file.sync().unwrap();
2007            assert_eq!(file.length(), BUFFER_SIZE * (INIT_BUFFERS + chunks_to_grow));
2008
2009            for i in 0..INIT_BUFFERS {
2010                let mut buf = [0u8; BUFFER_SIZE];
2011                file.read(&mut buf, i).unwrap();
2012                assert_eq!(buf, [i as u8; BUFFER_SIZE]);
2013            }
2014        }
2015
2016        #[test]
2017        #[cfg(target_os = "linux")]
2018        fn ok_sync_range() {
2019            let (_dir, cfg) = tmp_path();
2020            let file = File::new(cfg).unwrap();
2021
2022            let data = [0x33u8; BUFFER_SIZE * 2];
2023            file.write(&data, 0).unwrap();
2024            file.sync_range(0, 2).unwrap();
2025            file.sync().unwrap();
2026
2027            let mut buf = [0u8; BUFFER_SIZE * 2];
2028            file.read(&mut buf, 0).unwrap();
2029            assert_eq!(buf, data);
2030        }
2031
2032        #[test]
2033        #[cfg(target_os = "linux")]
2034        fn ok_sync_range_zero_count() {
2035            let (_dir, cfg) = tmp_path();
2036            let file = File::new(cfg).unwrap();
2037            assert!(file.sync_range(0, 0).is_ok());
2038            assert!(file.sync_range(5, 0).is_ok());
2039        }
2040
2041        #[test]
2042        #[cfg(target_os = "linux")]
2043        fn err_sync_range_index_overflow() {
2044            let (_dir, cfg) = tmp_path();
2045            let file = File::new(cfg).unwrap();
2046            let err1 = file.sync_range(usize::MAX, 1).unwrap_err();
2047            assert_eq!(err1.reason, err::INV.reason);
2048
2049            let err2 = file.sync_range(0, usize::MAX).unwrap_err();
2050            assert_eq!(err2.reason, err::INV.reason);
2051        }
2052
2053        #[test]
2054        fn ok_concurrent_multiple_growers() {
2055            let (_dir, cfg) = tmp_path();
2056            let file = Arc::new(File::new(cfg).unwrap());
2057            let num_threads = 4;
2058            let chunks_per_thread = 4;
2059            let barrier = Arc::new(Barrier::new(num_threads));
2060            let mut handles = vec![];
2061
2062            for _ in 0..num_threads {
2063                let f = file.clone();
2064                let b = barrier.clone();
2065                handles.push(std::thread::spawn(move || {
2066                    b.wait();
2067                    f.grow(chunks_per_thread).unwrap();
2068                }));
2069            }
2070
2071            for h in handles {
2072                h.join().unwrap();
2073            }
2074
2075            let expected_total_chunks = INIT_BUFFERS + (num_threads * chunks_per_thread);
2076            assert_eq!(file.length(), BUFFER_SIZE * expected_total_chunks);
2077            assert_eq!(file.total_buffers().unwrap(), expected_total_chunks);
2078            assert_eq!(file.current_epoch(), num_threads as TEpoch);
2079        }
2080
2081        #[test]
2082        fn ok_concurrent_readers_during_grow() {
2083            let (_dir, cfg) = tmp_path();
2084            let file = Arc::new(File::new(cfg).unwrap());
2085            let initial_data = [0x5Au8; BUFFER_SIZE];
2086            file.write(&initial_data, 0).unwrap();
2087            file.sync().unwrap();
2088
2089            let running = Arc::new(atomic::AtomicBool::new(true));
2090            let num_readers = 4;
2091            let mut readers = vec![];
2092
2093            for _ in 0..num_readers {
2094                let f = file.clone();
2095                let r = running.clone();
2096                let expected = initial_data;
2097                readers.push(std::thread::spawn(move || {
2098                    let mut buf = [0u8; BUFFER_SIZE];
2099                    while r.load(atomic::Ordering::Relaxed) {
2100                        f.read(&mut buf, 0).unwrap();
2101                        assert_eq!(buf, expected);
2102                        std::thread::yield_now();
2103                    }
2104                }));
2105            }
2106
2107            // Grower thread grows file in multiple increments
2108            for _ in 0..5 {
2109                file.grow(2).unwrap();
2110                std::thread::sleep(std::time::Duration::from_millis(5));
2111            }
2112
2113            running.store(false, atomic::Ordering::Relaxed);
2114            for r in readers {
2115                r.join().unwrap();
2116            }
2117
2118            assert_eq!(file.length(), BUFFER_SIZE * (INIT_BUFFERS + 0x0A));
2119            assert_eq!(file.total_buffers().unwrap(), INIT_BUFFERS + 0x0A);
2120        }
2121
2122        #[test]
2123        fn err_total_buffers_unaligned_file() {
2124            let (_dir, cfg) = tmp_path();
2125            let file = File::new(cfg).unwrap();
2126            assert_eq!(file.total_buffers().unwrap(), INIT_BUFFERS);
2127
2128            // Corrupt file length in-memory to simulate an unaligned layout
2129            file.inner.current_length.fetch_add(1, atomic::Ordering::SeqCst);
2130
2131            let err = file.total_buffers().unwrap_err();
2132            assert_eq!(err.reason, err::CPT.reason);
2133        }
2134    }
2135
2136    mod file_delete_exists {
2137        use super::*;
2138
2139        #[test]
2140        fn ok_exists_true_on_created_file() {
2141            let (_dir, cfg) = tmp_path();
2142            let file = File::new(cfg.clone()).unwrap();
2143
2144            assert!(file.exists().unwrap());
2145            assert!(cfg.path.exists());
2146        }
2147
2148        #[test]
2149        fn ok_delete_removes_file() {
2150            let (_dir, cfg) = tmp_path();
2151            let file = File::new(cfg.clone()).unwrap();
2152            let path = cfg.path.clone();
2153
2154            assert!(file.exists().unwrap());
2155            file.delete().unwrap();
2156
2157            assert!(!path.exists());
2158            assert!(File::new(cfg).is_ok());
2159        }
2160
2161        #[test]
2162        fn ok_open_and_delete() {
2163            let (_dir, cfg) = tmp_path();
2164            let file = File::new(cfg.clone()).unwrap();
2165            drop(file);
2166
2167            let opened = File::open(cfg.clone()).unwrap();
2168            assert!(opened.exists().unwrap());
2169            opened.delete().unwrap();
2170
2171            assert!(!cfg.path.exists());
2172        }
2173
2174        #[test]
2175        fn err_open_after_delete_grown_file() {
2176            let (_dir, cfg) = tmp_path();
2177            let file = File::new(cfg.clone()).unwrap();
2178
2179            file.grow(2).unwrap();
2180            assert_eq!(file.length(), (INIT_BUFFERS + 2) * BUFFER_SIZE);
2181
2182            file.delete().unwrap();
2183            assert!(!cfg.path.exists());
2184
2185            let err = File::open(cfg).unwrap_err();
2186            assert_eq!(err.reason, err::INV.reason);
2187        }
2188
2189        #[test]
2190        #[cfg(unix)]
2191        fn ok_exists_false_after_external_removal() {
2192            let (_dir, cfg) = tmp_path();
2193            let file = File::new(cfg.clone()).unwrap();
2194            assert!(file.exists().unwrap());
2195
2196            std::fs::remove_file(&cfg.path).unwrap();
2197            assert!(!file.exists().unwrap());
2198        }
2199
2200        #[test]
2201        #[cfg(windows)]
2202        fn ok_exists_false_after_external_removal() {
2203            let (_dir, cfg) = tmp_path();
2204            let file = File::new(cfg.clone()).unwrap();
2205            assert!(file.exists().unwrap());
2206            drop(file);
2207
2208            std::fs::remove_file(&cfg.path).unwrap();
2209            assert!(!PlatformFile::exists(&cfg.path).unwrap());
2210        }
2211
2212        #[test]
2213        #[cfg(unix)]
2214        fn err_delete_when_unlinked_externally() {
2215            let (_dir, cfg) = tmp_path();
2216            let file = File::new(cfg.clone()).unwrap();
2217
2218            std::fs::remove_file(&cfg.path).unwrap();
2219            let err = file.delete().unwrap_err();
2220            assert_eq!(err.reason, err::INV.reason);
2221        }
2222
2223        #[test]
2224        #[cfg(windows)]
2225        fn err_delete_when_unlinked_externally() {
2226            let (_dir, cfg) = tmp_path();
2227            let file = File::new(cfg.clone()).unwrap();
2228
2229            // NOTE:
2230            //
2231            // With `FILE_SHARE_DELETE` enabled, external deletion is permitted while open
2232            //
2233            // After external deletion, calling `file.delete()` fails with `err::INV` or `err::PRM` (delete-pending)
2234            std::fs::remove_file(&cfg.path).unwrap();
2235            let err = file.delete().unwrap_err();
2236            assert!(
2237                err.reason == err::INV.reason || err.reason == err::PRM.reason,
2238                "Unexpected error reason: {:?}",
2239                err
2240            );
2241        }
2242
2243        #[test]
2244        #[cfg(unix)]
2245        fn err_delete_permission_denied() {
2246            use std::os::unix::fs::PermissionsExt;
2247
2248            let dir = tempfile::tempdir().unwrap();
2249            let sub_dir = dir.path().join("readonly_dir");
2250            std::fs::create_dir(&sub_dir).unwrap();
2251
2252            let path = sub_dir.join("victim.db");
2253            let cfg = FileCfg {
2254                module_id: MID,
2255                path,
2256                buffer_size: BUFFER_SIZE,
2257                initial_available_buffers: INIT_BUFFERS,
2258                sync_interval: None,
2259                sequential_access_hint: false,
2260            };
2261
2262            let file = File::new(cfg).unwrap();
2263            std::fs::set_permissions(&sub_dir, std::fs::Permissions::from_mode(0o555)).unwrap();
2264
2265            let err = file.delete().unwrap_err();
2266            std::fs::set_permissions(&sub_dir, std::fs::Permissions::from_mode(0o755)).unwrap();
2267
2268            assert_eq!(err.reason, err::PRM.reason);
2269        }
2270
2271        #[test]
2272        #[cfg(windows)]
2273        fn err_delete_permission_denied() {
2274            let (_dir, cfg) = tmp_path();
2275            let file = File::new(cfg.clone()).unwrap();
2276
2277            let orig_perms = std::fs::metadata(&cfg.path).unwrap().permissions();
2278            let mut read_only = orig_perms.clone();
2279            read_only.set_readonly(true);
2280            std::fs::set_permissions(&cfg.path, read_only).unwrap();
2281
2282            let err = file.delete().unwrap_err();
2283
2284            let _ = std::fs::set_permissions(&cfg.path, orig_perms);
2285
2286            assert_eq!(err.reason, err::PRM.reason);
2287        }
2288
2289        #[test]
2290        fn ok_delete_and_recreate_cycle() {
2291            let (_dir, cfg) = tmp_path();
2292
2293            for i in 0..3 {
2294                let file = File::new(cfg.clone()).unwrap();
2295                let data = [i as u8; BUFFER_SIZE];
2296                file.write(&data, 0).unwrap();
2297                assert!(file.exists().unwrap());
2298
2299                file.delete().unwrap();
2300                assert!(!cfg.path.exists());
2301            }
2302        }
2303    }
2304
2305    mod file_drop {
2306        use super::*;
2307
2308        #[test]
2309        fn ok_drop_persists_written_data() {
2310            let (_dir, cfg) = tmp_path();
2311            let data = [0x7Au8; BUFFER_SIZE * 2];
2312
2313            {
2314                let file = File::new(cfg.clone()).unwrap();
2315                file.write(&data, 0).unwrap();
2316                drop(file);
2317            }
2318
2319            {
2320                let opened = File::open(cfg).unwrap();
2321                let mut buf = [0u8; BUFFER_SIZE * 2];
2322                opened.read(&mut buf, 0).unwrap();
2323                assert_eq!(buf, data);
2324            }
2325        }
2326
2327        #[test]
2328        fn ok_drop_releases_exclusive_lock() {
2329            let (_dir, cfg) = tmp_path();
2330
2331            let file = File::new(cfg.clone()).unwrap();
2332            let err = File::open(cfg.clone()).unwrap_err();
2333            assert_eq!(err.reason, err::LCK.reason);
2334
2335            drop(file);
2336
2337            let opened = File::open(cfg);
2338            assert!(opened.is_ok());
2339        }
2340    }
2341
2342    mod file_sync_and_durability {
2343        use super::*;
2344        use std::time::{Duration, Instant};
2345
2346        #[test]
2347        fn ok_manual_sync_advances_epoch_and_tickets() {
2348            let (_dir, cfg) = tmp_path();
2349            let file = File::new(cfg).unwrap();
2350            let data = [0x55u8; BUFFER_SIZE];
2351
2352            assert_eq!(file.current_epoch(), 0);
2353            assert_eq!(file.durable_epoch(), 0);
2354
2355            let t1 = file.write(&data, 0).unwrap();
2356            assert_eq!(t1.epoch(), 1);
2357            assert!(!t1.is_durable());
2358            assert_eq!(file.current_epoch(), 1);
2359            assert_eq!(file.durable_epoch(), 0);
2360
2361            let t2 = file.write(&data, 1).unwrap();
2362            assert_eq!(t2.epoch(), 2);
2363            assert!(!t2.is_durable());
2364            assert_eq!(file.current_epoch(), 2);
2365            assert_eq!(file.durable_epoch(), 0);
2366
2367            file.sync().unwrap();
2368            assert!(t1.is_durable());
2369            assert!(t2.is_durable());
2370            assert_eq!(file.durable_epoch(), 2);
2371        }
2372
2373        #[test]
2374        fn ok_ticket_force_without_background_thread() {
2375            let (_dir, cfg) = tmp_path();
2376            let file = File::new(cfg).unwrap();
2377            let data = [0xAAu8; BUFFER_SIZE];
2378
2379            let ticket = file.write(&data, 0).unwrap();
2380            assert!(!ticket.is_durable());
2381
2382            let durable = ticket.force().unwrap();
2383            assert_eq!(durable, ticket.epoch());
2384            assert!(ticket.is_durable());
2385            assert_eq!(file.durable_epoch(), ticket.epoch());
2386        }
2387
2388        #[test]
2389        fn ok_background_sync_advances_epoch() {
2390            let (_dir, mut cfg) = tmp_path();
2391            cfg.sync_interval = Some(Duration::from_millis(0x19));
2392
2393            let file = File::new(cfg).unwrap();
2394            let data = [0x33u8; BUFFER_SIZE];
2395
2396            let ticket = file.write(&data, 0).unwrap();
2397            let epoch = ticket.wait().unwrap();
2398            assert_eq!(epoch, ticket.epoch());
2399            assert!(ticket.is_durable());
2400            assert!(file.durable_epoch() >= ticket.epoch());
2401        }
2402
2403        #[test]
2404        fn ok_ticket_force_with_background_thread() {
2405            let (_dir, mut cfg) = tmp_path();
2406            // Long interval to verify force wakes up immediately
2407            cfg.sync_interval = Some(Duration::from_secs(0x0A));
2408
2409            let file = File::new(cfg).unwrap();
2410            let data = [0x44u8; BUFFER_SIZE];
2411
2412            let ticket = file.write(&data, 0).unwrap();
2413            assert!(!ticket.is_durable());
2414
2415            let start = Instant::now();
2416            let epoch = ticket.force().unwrap();
2417            let elapsed = start.elapsed();
2418
2419            assert_eq!(epoch, ticket.epoch());
2420            assert!(ticket.is_durable());
2421            assert!(elapsed < Duration::from_secs(2));
2422        }
2423
2424        #[test]
2425        fn ok_drop_with_background_thread_persists_data() {
2426            let (_dir, mut cfg) = tmp_path();
2427            cfg.sync_interval = Some(Duration::from_secs(0x1E));
2428            let data = [0x88u8; BUFFER_SIZE];
2429
2430            {
2431                let file = File::new(cfg.clone()).unwrap();
2432                let _ticket = file.write(&data, 0).unwrap();
2433                // Drop without waiting for interval to expire
2434                drop(file);
2435            }
2436
2437            {
2438                let opened = File::open(cfg).unwrap();
2439                let mut buf = [0u8; BUFFER_SIZE];
2440                opened.read(&mut buf, 0).unwrap();
2441                assert_eq!(buf, data);
2442            }
2443        }
2444
2445        #[test]
2446        fn ok_delete_with_background_thread() {
2447            let (_dir, mut cfg) = tmp_path();
2448            cfg.sync_interval = Some(Duration::from_millis(0x14));
2449
2450            let file = File::new(cfg.clone()).unwrap();
2451            let data = [0x99u8; BUFFER_SIZE];
2452            let _ = file.write(&data, 0).unwrap();
2453
2454            assert!(file.exists().unwrap());
2455            file.delete().unwrap();
2456            assert!(!cfg.path.exists());
2457        }
2458
2459        #[test]
2460        fn ok_concurrent_writes_distinct_epochs() {
2461            let (_dir, cfg) = tmp_path();
2462            let file = Arc::new(File::new(cfg).unwrap());
2463            let num_threads = 4;
2464            let mut handles = Vec::new();
2465
2466            for i in 0..num_threads {
2467                let f = file.clone();
2468                handles.push(std::thread::spawn(move || {
2469                    let data = [i as u8; BUFFER_SIZE];
2470                    f.write(&data, i).unwrap()
2471                }));
2472            }
2473
2474            let mut epochs = Vec::new();
2475            for h in handles {
2476                let ticket = h.join().unwrap();
2477                epochs.push(ticket.epoch());
2478            }
2479
2480            epochs.sort();
2481            epochs.dedup();
2482            assert_eq!(epochs.len(), num_threads);
2483        }
2484
2485        #[test]
2486        fn err_ticket_wait_when_file_deleted() {
2487            let (_dir, cfg) = tmp_path();
2488            let file = File::new(cfg).unwrap();
2489            let data = [0x11u8; BUFFER_SIZE];
2490            let ticket = file.write(&data, 0).unwrap();
2491
2492            file.delete().unwrap();
2493
2494            // After delete, the weak SyncTrigger upgrade fails, returning an error on force
2495            let res = ticket.force();
2496            assert!(res.is_err());
2497        }
2498
2499        #[test]
2500        fn err_ticket_wait_surfaces_sync_failure() {
2501            let (_dir, cfg) = tmp_path();
2502            let file = File::new(cfg).unwrap();
2503            let data = [0x22u8; BUFFER_SIZE];
2504            let ticket = file.write(&data, 0).unwrap();
2505
2506            // Inject a synthetic durability error into the completion
2507            let sync_err = err::make_error(err::SYN);
2508            file.completion().set_err(sync_err.clone());
2509            file.completion().notify_all_listeners();
2510
2511            let err_wait = ticket.wait().unwrap_err();
2512            assert_eq!(err_wait.reason, err::SYN.reason);
2513
2514            let err_force = ticket.force().unwrap_err();
2515            assert_eq!(err_force.reason, err::SYN.reason);
2516        }
2517
2518        #[test]
2519        fn ok_concurrent_ticket_force_storm() {
2520            let (_dir, mut cfg) = tmp_path();
2521            cfg.initial_available_buffers = 0x20;
2522            cfg.sync_interval = Some(Duration::from_millis(0x0A));
2523            let file = Arc::new(File::new(cfg).unwrap());
2524            let num_threads = 0x10;
2525            let barrier = Arc::new(Barrier::new(num_threads));
2526            let mut handles = Vec::new();
2527
2528            for i in 0..num_threads {
2529                let f = file.clone();
2530                let b = barrier.clone();
2531                handles.push(std::thread::spawn(move || {
2532                    b.wait();
2533                    let data = [i as u8; BUFFER_SIZE];
2534                    let ticket = f.write(&data, i).unwrap();
2535                    let durable = ticket.force().unwrap();
2536                    assert_eq!(durable, ticket.epoch());
2537                    assert!(ticket.is_durable());
2538                }));
2539            }
2540
2541            for h in handles {
2542                h.join().unwrap();
2543            }
2544
2545            assert!(file.durable_epoch() >= num_threads as TEpoch);
2546        }
2547    }
2548
2549    mod module_binding {
2550        use super::*;
2551
2552        #[test]
2553        fn ok_multiple_files_distinct_module_ids() {
2554            let dir = tempfile::tempdir().unwrap();
2555            let cfg1 = FileCfg {
2556                module_id: 0x11,
2557                path: dir.path().join("file1.db"),
2558                buffer_size: BUFFER_SIZE,
2559                initial_available_buffers: INIT_BUFFERS,
2560                sync_interval: None,
2561                sequential_access_hint: false,
2562            };
2563            let cfg2 = FileCfg {
2564                module_id: 0x22,
2565                path: dir.path().join("file2.db"),
2566                buffer_size: BUFFER_SIZE,
2567                initial_available_buffers: INIT_BUFFERS,
2568                sync_interval: None,
2569                sequential_access_hint: false,
2570            };
2571
2572            let file1 = File::new(cfg1.clone()).unwrap();
2573            let file2 = File::new(cfg2.clone()).unwrap();
2574
2575            // Operations causing errors on file1 must carry module_id 0x11
2576            let mut invalid_buf = [0u8; BUFFER_SIZE - 1];
2577            let err1 = file1.read(&mut invalid_buf, 0).unwrap_err();
2578            assert_eq!(err1.module, 0x11);
2579            assert_eq!(err1.reason, err::INV.reason);
2580
2581            let err1_oob = file1.read(&mut [0u8; BUFFER_SIZE], 0x3E7).unwrap_err();
2582            assert_eq!(err1_oob.module, 0x11);
2583            assert_eq!(err1_oob.reason, err::HCF.reason);
2584
2585            // Operations causing errors on file2 must carry module_id 0x22
2586            let err2 = file2.read(&mut invalid_buf, 0).unwrap_err();
2587            assert_eq!(err2.module, 0x22);
2588            assert_eq!(err2.reason, err::INV.reason);
2589
2590            let err2_oob = file2.write(&[0u8; BUFFER_SIZE], 0x3E7).unwrap_err();
2591            assert_eq!(err2_oob.module, 0x22);
2592            assert_eq!(err2_oob.reason, err::HCF.reason);
2593
2594            // Existing file error on File::new carries respective module_id
2595            let err1_exs = File::new(cfg1).unwrap_err();
2596            assert_eq!(err1_exs.module, 0x11);
2597            assert_eq!(err1_exs.reason, err::EXS.reason);
2598
2599            let err2_exs = File::new(cfg2).unwrap_err();
2600            assert_eq!(err2_exs.module, 0x22);
2601            assert_eq!(err2_exs.reason, err::EXS.reason);
2602        }
2603    }
2604}