use std::path::Path;
use std::sync::Arc;
use crate::api::{Store, StoreOptions};
use crate::format::{Header, WalFrameHeader, WalHeader};
use crate::record::MemoryRecord;
use crate::storage::btree;
use crate::storage::sim::SimVfs;
use crate::storage::vfs::{OpenMode, Vfs};
use crate::storage::{Pager, PagerOptions};
pub fn fuzz_header(data: &[u8]) {
let _ = Header::decode(data);
let _ = Header::peek_page_size(data);
let _ = WalHeader::decode(data);
let _ = WalFrameHeader::decode(data, data, 0x5EED);
}
pub fn fuzz_record(data: &[u8]) {
let _ = MemoryRecord::decode(data);
}
pub fn fuzz_page(data: &[u8]) {
btree::fuzz_decode_page(data);
}
pub fn fuzz_fts_page(data: &[u8]) {
crate::index::fts::fuzz_decode_page(data);
}
pub fn fuzz_graph_page(data: &[u8]) {
crate::index::graph::fuzz_decode_page(data);
}
pub fn fuzz_filter_meta_page(data: &[u8]) {
crate::index::filter_meta::fuzz_decode_page(data);
}
pub fn fuzz_wal_replay(data: &[u8]) {
let vfs = SimVfs::new();
let shared: Arc<dyn Vfs> = Arc::new(vfs.clone());
let store_path = Path::new("m.mind");
let opts = PagerOptions {
page_size: 512, ..Default::default()
};
let Ok(mut pager) = Pager::create(Arc::clone(&shared), store_path, opts) else {
return;
};
if commit_two_pages(&mut pager).is_err() {
return;
}
drop(pager);
if let Ok(wal) = vfs.open(Path::new("m.mind-wal"), OpenMode::OpenOrCreate)
&& (wal.truncate(0).is_err() || wal.write_at(data, 0).is_err())
{
return;
}
if let Ok(pager) = Pager::open(shared, store_path, opts) {
read_everything(&pager);
}
}
pub fn fuzz_open_full(data: &[u8]) {
let vfs: Arc<dyn Vfs> = Arc::new(SimVfs::new());
let path = Path::new("m.mind");
if let Ok(file) = vfs.open(path, OpenMode::CreateNew)
&& file.write_at(data, 0).is_err()
{
return;
}
if let Ok(store) = Store::open_with(vfs, path, StoreOptions::default()) {
for item in store.iter_all().take(10_000) {
if item.is_err() {
break; }
}
}
}
fn commit_two_pages(pager: &mut Pager) -> crate::Result<()> {
let mut txn = pager.begin()?;
btree::insert(&mut txn, [1u8; 16], b"base record a")?;
btree::insert(&mut txn, [2u8; 16], b"base record b")?;
txn.commit()?;
Ok(())
}
fn read_everything(pager: &Pager) {
for page_no in 0..pager.page_count().min(1024) {
let _ = pager.read_page(page_no); }
let _ = btree::scan(pager, pager.header().root_btree_page)
.take(10_000)
.count();
}
#[cfg(test)]
mod tests {
#![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
use super::*;
use crate::storage::sim::SplitMix64;
#[test]
fn fuzz_bodies_never_panic_on_arbitrary_input() {
let mut rng = SplitMix64(0xC0FFEE);
for round in 0..300 {
let len = (rng.next_u64() % 2048) as usize;
let mut data = vec![0u8; len];
for b in &mut data {
*b = rng.next_u64() as u8;
}
fuzz_header(&data);
fuzz_record(&data);
fuzz_page(&data);
fuzz_fts_page(&data);
fuzz_graph_page(&data);
fuzz_filter_meta_page(&data);
if round % 10 == 0 {
fuzz_wal_replay(&data);
fuzz_open_full(&data);
}
}
}
#[test]
#[allow(clippy::type_complexity)]
fn fuzz_corpus_seeds_never_panic() {
let root = Path::new(env!("CARGO_MANIFEST_DIR")).join("../../fuzz/corpus");
let targets: &[(&str, fn(&[u8]))] = &[
("fuzz_header", fuzz_header),
("fuzz_record", fuzz_record),
("fuzz_page", fuzz_page),
("fuzz_fts_page", fuzz_fts_page),
("fuzz_graph_page", fuzz_graph_page),
("fuzz_filter_meta_page", fuzz_filter_meta_page),
("fuzz_wal_replay", fuzz_wal_replay),
("fuzz_open_full", fuzz_open_full),
];
let mut total_files = 0usize;
for (name, body) in targets {
let dir = root.join(name);
let entries = std::fs::read_dir(&dir)
.unwrap_or_else(|e| panic!("corpus dir {}: {e}", dir.display()));
for entry in entries {
let path = entry
.unwrap_or_else(|e| panic!("corpus dir {}: {e}", dir.display()))
.path();
if !path.is_file() {
continue;
}
let data = std::fs::read(&path)
.unwrap_or_else(|e| panic!("reading {}: {e}", path.display()));
body(&data);
total_files += 1;
}
}
assert!(
total_files >= targets.len(),
"expected at least one seed per fuzz target"
);
}
#[test]
fn fuzz_open_full_survives_truncated_valid_files() {
let vfs: Arc<dyn Vfs> = Arc::new(SimVfs::new());
let path = Path::new("valid.mind");
let mut store =
Store::create_with(Arc::clone(&vfs), path, StoreOptions::default()).unwrap();
store
.remember(crate::api::MemoryDraft::new("seed memory"))
.unwrap();
store.close().unwrap();
let file = vfs.open(path, OpenMode::MustExist).unwrap();
let len = file.len().unwrap() as usize;
let mut bytes = vec![0u8; len];
file.read_at(&mut bytes, 0).unwrap();
drop(file);
let mut rng = SplitMix64(0xBADF00D);
for _ in 0..100 {
let mut mutated = bytes.clone();
let cut = (rng.next_u64() as usize) % (len + 1);
mutated.truncate(cut);
if rng.next_bool() && !mutated.is_empty() {
let i = (rng.next_u64() as usize) % mutated.len();
mutated[i] ^= (rng.next_u64() as u8) | 1;
}
fuzz_open_full(&mutated);
}
}
}