1//! A VFS over the platform's, made the process's default by `install`, that fails what a
2//! test arms: once, the WAL's sync or commit frame or the next database open; or every WAL
3//! write of the database `FAILING` names, for as long as it names it.
4#![allow(dead_code)]
5
6use rusqlite::ffi;
7use std::{
8 ffi::{CStr, c_char, c_int, c_void},
9 path::PathBuf,
10 sync::{
11 Mutex, Once, OnceLock,
12 atomic::{AtomicBool, AtomicU8, AtomicUsize, Ordering},
13 },
14};
15
16/// The WAL's sync at commit fails before it reaches the disk.
17pub const SYNC_BEFORE: u8 = 1;
18/// The WAL's sync at commit reaches the disk and still reports failure.
19pub const SYNC_AFTER: u8 = 2;
20/// The commit frame's header is never written.
21pub const WRITE_COMMIT: u8 = 4;
22/// The next open of a database fails, as the cold reopen after a failed commit.
23pub const OPEN: u8 = 8;
24/// After a fault, the next sync fails, as the checkpoint closing the warm connection makes,
25/// which keeps the WAL.
26pub const KEEP_WAL: u8 = 16;
27
28pub static ARMED: AtomicU8 = AtomicU8::new(0);
29/// Between a fault and the next database open, as the replica reopens the cache cold.
30static FIRED: AtomicBool = AtomicBool::new(false);
31pub static WAL_WRITES_AFTER_FAULT: AtomicUsize = AtomicUsize::new(0);
32static REAL: OnceLock<usize> = OnceLock::new();
33/// The platform's WAL file methods, the shim's copy of them, and the copy failing every write.
34static WAL: OnceLock<(usize, usize, usize)> = OnceLock::new();
35/// While set, every write to this database's WAL fails, for WALs opened while it was set.
36pub static FAILING: Mutex<Option<PathBuf>> = Mutex::new(None);
37/// Held by a test for as long as it sets `FAILING`, which one test sets at a time.
38pub static EXCLUSIVE: Mutex<()> = Mutex::new(());
39
40fn fire(fault: u8) -> bool {
41 let fired = ARMED.fetch_and(!fault, Ordering::SeqCst) & fault != 0;
42 if fired {
43 FIRED.store(true, Ordering::SeqCst);
44 }
45 fired
46}
47
48fn real_wal() -> &'static ffi::sqlite3_io_methods {
49 unsafe { &*(WAL.get().unwrap().0 as *const ffi::sqlite3_io_methods) }
50}
51
52unsafe extern "C" fn open(
53 _: *mut ffi::sqlite3_vfs,
54 name: *const c_char,
55 file: *mut ffi::sqlite3_file,
56 flags: c_int,
57 out: *mut c_int,
58) -> c_int {
59 if flags & ffi::SQLITE_OPEN_MAIN_DB != 0 {
60 FIRED.store(false, Ordering::SeqCst);
61 if fire(OPEN) {
62 FIRED.store(false, Ordering::SeqCst);
63 unsafe { (*file).pMethods = std::ptr::null() };
64 return ffi::SQLITE_CANTOPEN;
65 }
66 }
67 let real = *REAL.get().unwrap() as *mut ffi::sqlite3_vfs;
68 let opened = unsafe { (*real).xOpen.unwrap()(real, name, file, flags, out) };
69 if opened == ffi::SQLITE_OK && flags & ffi::SQLITE_OPEN_WAL != 0 {
70 let methods = unsafe { (*file).pMethods };
71 let &(platform, shim, failing) = WAL.get_or_init(|| {
72 let mut shim = unsafe { *methods };
73 shim.xWrite = Some(write);
74 shim.xSync = Some(sync);
75 let mut failing = shim;
76 failing.xWrite = Some(fail);
77 let leak = |methods| Box::leak(Box::new(methods)) as *const _ as usize;
78 (methods as usize, leak(shim), leak(failing))
79 });
80 assert_eq!(platform, methods as usize);
81 let name = unsafe { CStr::from_ptr(name) }.to_string_lossy();
82 let fails = FAILING
83 .lock()
84 .unwrap()
85 .as_ref()
86 .is_some_and(|database| name.starts_with(&*database.to_string_lossy()));
87 let methods = if fails { failing } else { shim };
88 unsafe { (*file).pMethods = methods as *const ffi::sqlite3_io_methods };
89 }
90 opened
91}
92
93unsafe extern "C" fn write(
94 file: *mut ffi::sqlite3_file,
95 data: *const c_void,
96 amount: c_int,
97 offset: ffi::sqlite3_int64,
98) -> c_int {
99 // A frame header names the database size only in the frame that commits.
100 if amount == 24
101 && unsafe { std::slice::from_raw_parts(data.cast::<u8>().add(4), 4) } != [0; 4]
102 && fire(WRITE_COMMIT)
103 {
104 return ffi::SQLITE_IOERR_WRITE;
105 }
106 if FIRED.load(Ordering::SeqCst) {
107 WAL_WRITES_AFTER_FAULT.fetch_add(1, Ordering::SeqCst);
108 }
109 unsafe { real_wal().xWrite.unwrap()(file, data, amount, offset) }
110}
111
112unsafe extern "C" fn fail(
113 file: *mut ffi::sqlite3_file,
114 data: *const c_void,
115 amount: c_int,
116 offset: ffi::sqlite3_int64,
117) -> c_int {
118 if FAILING.lock().unwrap().is_some() {
119 return ffi::SQLITE_IOERR_WRITE;
120 }
121 unsafe { write(file, data, amount, offset) }
122}
123
124unsafe extern "C" fn sync(file: *mut ffi::sqlite3_file, flags: c_int) -> c_int {
125 if fire(SYNC_BEFORE) || FIRED.load(Ordering::SeqCst) && fire(KEEP_WAL) {
126 return ffi::SQLITE_IOERR_FSYNC;
127 }
128 let synced = unsafe { real_wal().xSync.unwrap()(file, flags) };
129 if synced == ffi::SQLITE_OK && fire(SYNC_AFTER) {
130 return ffi::SQLITE_IOERR_FSYNC;
131 }
132 synced
133}
134
135pub fn install() {
136 static INSTALL: Once = Once::new();
137 INSTALL.call_once(|| unsafe {
138 let real = ffi::sqlite3_vfs_find(std::ptr::null());
139 REAL.set(real as usize).unwrap();
140 let mut shim = *real;
141 shim.zName = c"commit-fault".as_ptr();
142 shim.pNext = std::ptr::null_mut();
143 shim.xOpen = Some(open);
144 assert_eq!(
145 ffi::sqlite3_vfs_register(Box::leak(Box::new(shim)), 1),
146 ffi::SQLITE_OK
147 );
148 });
149}