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