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