use alloc::vec;
use alloc::vec::Vec;
use crate::block::BlockDevice;
use crate::storage::StorageEngine;
use crate::wal::Wal;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Fault {
TruncateTail(usize),
CorruptTail,
ZeroTail,
}
impl Fault {
pub fn apply(&self, log: &[u8]) -> Vec<u8> {
if log.is_empty() {
return log.to_vec();
}
let mut out = log.to_vec();
match self {
Fault::TruncateTail(n) => {
let cut = out.len().saturating_sub(*n);
out.truncate(cut);
}
Fault::CorruptTail => {
let last = out.len() - 1;
out[last] ^= 0xFF;
}
Fault::ZeroTail => {
let start = out.len().saturating_sub(4);
for b in out.iter_mut().skip(start) {
*b = 0;
}
}
}
out
}
}
pub fn trial<D: BlockDevice + Clone>(
device: D,
committed: usize,
fault: Fault,
) -> (usize, Vec<Option<u8>>) {
let log;
{
let mut e = StorageEngine::new(device.clone(), committed.max(1) + 1);
for i in 0..committed {
let page = vec![i as u8; crate::page::PAGE_SIZE];
e.write_page(i as u64, &page).unwrap();
}
e.commit().unwrap();
log = e.wal_bytes().to_vec();
}
let corrupted = fault.apply(&log);
let mut e2 = StorageEngine::new(device, committed.max(1) + 1);
let applied = e2.recover(&corrupted).unwrap();
let wal = Wal::from_bytes(&corrupted);
let mut expected: Vec<Option<u8>> = (0..committed).map(|_| None).collect();
let mut pending: Vec<(u64, u8)> = Vec::new();
wal.replay(|rec| match rec.kind {
crate::wal::RecordKind::PageWrite if !rec.payload.is_empty() => {
pending.push((rec.block_id, rec.payload[0]));
}
crate::wal::RecordKind::Commit | crate::wal::RecordKind::Checkpoint => {
for (block_id, byte) in pending.drain(..) {
let idx = block_id as usize;
if idx < expected.len() {
expected[idx] = Some(byte);
}
}
}
_ => {}
});
(applied, expected)
}
pub fn fuzz<D, F>(make_device: F, trials: usize)
where
D: BlockDevice + Clone,
F: Fn() -> D,
{
let faults = [
Fault::TruncateTail(0),
Fault::TruncateTail(1),
Fault::TruncateTail(4),
Fault::CorruptTail,
Fault::ZeroTail,
];
let mut seed: u64 = 0x9E37_79B9_7F4A_7C15;
for t in 0..trials {
seed ^= seed << 13;
seed ^= seed >> 7;
seed ^= seed << 17;
let committed = (seed as usize % 8) + 1; let fault = faults[seed as usize % faults.len()];
let device = make_device();
let (applied, expected) = trial(device, committed, fault);
assert!(
applied <= committed,
"trial {t}: recovery replayed {applied} > {committed} committed"
);
let device2 = make_device();
let log;
{
let mut e = StorageEngine::new(device2.clone(), committed.max(1) + 1);
for i in 0..committed {
let page = vec![i as u8; crate::page::PAGE_SIZE];
e.write_page(i as u64, &page).unwrap();
}
e.commit().unwrap();
log = e.wal_bytes().to_vec();
}
let corrupted = fault.apply(&log);
let mut e2 = StorageEngine::new(device2, committed.max(1) + 1);
e2.recover(&corrupted).unwrap();
for (idx, exp) in expected.iter().enumerate() {
if let Some(exp) = exp {
let got = e2.read_page(idx as u64);
if let Ok(bytes) = got {
assert_eq!(
bytes[0], *exp,
"trial {t}: block {idx} replayed image mismatch"
);
}
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::block::InMemoryBlockDevice;
fn device() -> InMemoryBlockDevice {
InMemoryBlockDevice::new(16)
}
#[test]
fn truncate_tail_recovers_prefix() {
let (applied, _) = trial(device(), 4, Fault::TruncateTail(1));
assert!(applied <= 4);
}
#[test]
fn corrupt_tail_recovers_prefix() {
let (applied, _) = trial(device(), 4, Fault::CorruptTail);
assert!(applied <= 4);
}
#[test]
fn zero_tail_recovers_prefix() {
let (applied, _) = trial(device(), 4, Fault::ZeroTail);
assert!(applied <= 4);
}
#[test]
fn fuzz_many_faults_stay_prefix_consistent() {
fuzz(device, 256);
}
}