use std::fs::OpenOptions;
use std::io::{Seek, SeekFrom, Write};
use std::path::PathBuf;
use std::time::{Duration, Instant};
use emdb::{Emdb, Result};
use fastrand::Rng;
fn tmp_path(label: &str) -> PathBuf {
let mut p = std::env::temp_dir();
let nanos = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map_or(0_u128, |d| d.as_nanos());
let tid = std::thread::current().id();
p.push(format!("emdb-decoder-{label}-{nanos}-{tid:?}.emdb"));
p
}
fn cleanup(path: &PathBuf) {
let _ = std::fs::remove_file(path);
let display = path.display();
let _ = std::fs::remove_file(format!("{display}.lock"));
let _ = std::fs::remove_file(format!("{display}.compact.tmp"));
}
#[test]
fn empty_data_region_opens_with_zero_records() -> Result<()> {
let path = tmp_path("empty-data");
cleanup(&path);
drop(Emdb::open(&path)?);
let db = Emdb::open(&path)?;
assert_eq!(db.len()?, 0);
assert!(db.is_empty()?);
drop(db);
cleanup(&path);
Ok(())
}
#[test]
fn random_bytes_after_header_never_panic() {
let mut rng = Rng::new();
rng.seed(0xEDB_DEC0_DEAD_BEEF);
const ITERATIONS: usize = 64;
const PER_ITERATION_TIMEOUT: Duration = Duration::from_secs(5);
for iter in 0..ITERATIONS {
let path = tmp_path(&format!("random-{iter}"));
cleanup(&path);
drop(Emdb::open(&path).expect("seed open"));
let garbage_len = rng.usize(0..2_048);
let mut garbage = vec![0_u8; garbage_len];
for byte in &mut garbage {
*byte = rng.u8(..);
}
{
let mut file = OpenOptions::new()
.write(true)
.open(&path)
.expect("open for garbage");
let _seek = file.seek(SeekFrom::Start(4096)).expect("seek past header");
file.write_all(&garbage).expect("write garbage");
file.sync_data().expect("sync garbage");
}
let started = Instant::now();
let result = Emdb::open(&path);
let elapsed = started.elapsed();
assert!(
elapsed < PER_ITERATION_TIMEOUT,
"iter {iter}: open took {elapsed:?} on {garbage_len} random bytes — possible infinite loop"
);
match result {
Ok(db) => {
let _ = db.len();
let _ = db.is_empty();
let count = db.iter().expect("iter").take(10_000).count();
std::hint::black_box(count);
drop(db);
}
Err(_) => { }
}
cleanup(&path);
}
}
#[test]
fn valid_prefix_then_garbage_recovers_only_the_prefix() -> Result<()> {
let path = tmp_path("prefix-then-garbage");
cleanup(&path);
{
let db = Emdb::open(&path)?;
for i in 0..50 {
db.insert(format!("k{i:03}"), format!("v{i}"))?;
}
db.flush()?;
db.checkpoint()?;
}
let tail_offset = std::fs::metadata(&path)?.len();
{
let mut file = OpenOptions::new().append(true).open(&path)?;
let mut rng = Rng::new();
rng.seed(42);
let mut garbage = vec![0_u8; 256];
for byte in &mut garbage {
*byte = rng.u8(..);
}
file.write_all(&garbage)?;
file.sync_data()?;
let _ = tail_offset;
}
let db = Emdb::open(&path)?;
assert_eq!(db.len()?, 50, "only the valid prefix should be recovered");
for i in 0..50 {
let key = format!("k{i:03}");
let want = format!("v{i}");
assert_eq!(
db.get(&key)?,
Some(want.into_bytes()),
"record {i} missing from valid prefix"
);
}
drop(db);
cleanup(&path);
Ok(())
}
#[test]
fn varying_key_and_value_sizes_round_trip() -> Result<()> {
let path = tmp_path("size-variations");
cleanup(&path);
let cases: &[(&[u8], Vec<u8>)] = &[
(b"", Vec::new()),
(b"x", b"y".to_vec()),
(b"empty-value", Vec::new()),
(&[0_u8; 1], vec![0_u8; 1]),
(&[b'k'; 1024], vec![b'v'; 16]),
(b"big-value", vec![b'V'; 64 * 1024]),
(
b"binary-key",
(0_u8..=255).chain(0_u8..=255).collect::<Vec<_>>(),
),
];
{
let db = Emdb::open(&path)?;
for (key, value) in cases {
db.insert(*key, value.as_slice())?;
}
db.flush()?;
db.checkpoint()?;
}
let db = Emdb::open(&path)?;
for (key, value) in cases {
assert_eq!(
db.get(*key)?,
Some(value.clone()),
"round-trip mismatch for {} byte key",
key.len()
);
}
drop(db);
cleanup(&path);
Ok(())
}