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