use noxu_db::{
DatabaseConfig, DatabaseEntry, EnvironmentConfig, OperationStatus,
};
use std::path::{Path, PathBuf};
use tempfile::TempDir;
fn open_env(dir: &Path) -> noxu_db::Environment {
let mut cfg = EnvironmentConfig::new(dir.to_path_buf())
.with_allow_create(true)
.with_transactional(true)
.with_log_file_max_bytes(4096);
cfg.set_run_cleaner(false);
cfg.set_run_checkpointer(false);
cfg.set_run_evictor(false);
cfg.set_run_in_compressor(false);
noxu_db::Environment::open(cfg).unwrap()
}
fn open_db(env: &noxu_db::Environment) -> noxu_db::Database {
env.open_database(
None,
"corruptdb",
&DatabaseConfig::new().with_allow_create(true).with_transactional(true),
)
.unwrap()
}
fn list_log_files(dir: &Path) -> Vec<PathBuf> {
let mut files: Vec<PathBuf> = std::fs::read_dir(dir)
.unwrap()
.filter_map(|e| e.ok().map(|e| e.path()))
.filter(|p| p.extension().map(|x| x == "ndb").unwrap_or(false))
.collect();
files.sort();
files
}
fn write_committed_workload(dir: &Path, n: u32) {
let env = open_env(dir);
let db = open_db(&env);
for i in 0..n {
let txn = env.begin_transaction(None).unwrap();
db.put_in(
&txn,
DatabaseEntry::from_bytes(format!("k_{i:05}").as_bytes()),
DatabaseEntry::from_bytes(format!("v_{i:05}").as_bytes()),
)
.unwrap();
txn.commit().unwrap();
}
db.close().unwrap();
env.close().unwrap();
}
fn try_recover_and_scan(
dir: &Path,
) -> Result<std::collections::BTreeMap<Vec<u8>, Vec<u8>>, String> {
let result = std::panic::catch_unwind(|| {
let env = noxu_db::Environment::open(
EnvironmentConfig::new(dir.to_path_buf())
.with_allow_create(true)
.with_transactional(true),
)
.map_err(|e| format!("open/recovery error: {e}"))?;
let db = env
.open_database(
None,
"corruptdb",
&DatabaseConfig::new()
.with_allow_create(true)
.with_transactional(true),
)
.map_err(|e| format!("open_database error: {e}"))?;
let mut cursor = db
.open_cursor(None)
.map_err(|e| format!("open_cursor error: {e}"))?;
let mut map = std::collections::BTreeMap::new();
let mut key = DatabaseEntry::new();
let mut val = DatabaseEntry::new();
loop {
match cursor.get(&mut key, &mut val, noxu_db::Get::Next, None) {
Ok(OperationStatus::Success) => {
map.insert(
key.data_opt().unwrap_or(&[]).to_vec(),
val.data_opt().unwrap_or(&[]).to_vec(),
);
}
Ok(_) => break,
Err(e) => return Err(format!("scan error: {e}")),
}
}
let _ = cursor.close();
Ok(map)
});
match result {
Ok(inner) => inner,
Err(_) => Err("panic during recovery/scan".to_string()),
}
}
#[test]
fn byte_flip_in_committed_entry_is_detected() {
let dir = TempDir::new().unwrap();
let n = 200u32;
write_committed_workload(dir.path(), n);
let mut full_expected = std::collections::BTreeMap::new();
for i in 0..n {
full_expected.insert(
format!("k_{i:05}").into_bytes(),
format!("v_{i:05}").into_bytes(),
);
}
{
let clean = try_recover_and_scan(dir.path())
.expect("clean reopen before corruption must succeed");
assert_eq!(
clean, full_expected,
"pre-corruption clean recovery must see all {n} committed keys"
);
}
let files = list_log_files(dir.path());
assert!(!files.is_empty(), "expected at least one .ndb file");
let target = if files.len() >= 2 {
files[files.len() / 2].clone()
} else {
files[0].clone()
};
{
let mut bytes = std::fs::read(&target).unwrap();
assert!(bytes.len() > 64, "log file too small to corrupt meaningfully");
let pos = bytes.len() / 2;
bytes[pos] ^= 0xFF; std::fs::write(&target, &bytes).unwrap();
}
match try_recover_and_scan(dir.path()) {
Err(_e) => {
}
Ok(recovered) => {
for (k, v) in &recovered {
let expected = full_expected.get(k);
assert_eq!(
Some(v),
expected,
"corruption returned a wrong/garbage value for key {:?}: \
got {:?}",
std::str::from_utf8(k),
std::str::from_utf8(v),
);
}
assert!(
recovered.len() < full_expected.len(),
"byte flip in a committed entry was SILENTLY MASKED: recovered \
set equals the full committed set ({} keys) despite \
corruption — corruption was not detected",
recovered.len()
);
}
}
}
#[test]
fn mid_entry_truncation_torn_tail_not_returned() {
let dir = TempDir::new().unwrap();
let n = 200u32;
write_committed_workload(dir.path(), n);
let mut full_expected = std::collections::BTreeMap::new();
for i in 0..n {
full_expected.insert(
format!("k_{i:05}").into_bytes(),
format!("v_{i:05}").into_bytes(),
);
}
let files = list_log_files(dir.path());
let last = files.last().unwrap().clone();
{
let len = std::fs::metadata(&last).unwrap().len();
assert!(len > 32, "last file too small");
let new_len = len - 7;
let f = std::fs::OpenOptions::new().write(true).open(&last).unwrap();
f.set_len(new_len).unwrap();
}
match try_recover_and_scan(dir.path()) {
Err(_e) => {
}
Ok(recovered) => {
for (k, v) in &recovered {
assert_eq!(
full_expected.get(k),
Some(v),
"torn-tail recovery returned a wrong value for key {:?}",
std::str::from_utf8(k),
);
}
assert!(
recovered.len() <= full_expected.len(),
"torn-tail recovery produced MORE keys than were committed"
);
for k in recovered.keys() {
assert!(
full_expected.contains_key(k),
"torn-tail recovery surfaced a key that was never \
committed: {:?}",
std::str::from_utf8(k)
);
}
}
}
}