#![allow(clippy::unwrap_used, clippy::expect_used)]
use std::path::Path;
use sqlite_core::{Database, Value};
use sqlite_forensic::{carve_all_deleted_records, carve_at_commit, RecoverySource};
const DELETED: &[u8] = include_bytes!("../../tests/data/deleted_places.db");
const NEMETZ_0C_01: &[u8] = include_bytes!("../../tests/data/nemetz/0C/0C-01.db");
fn dc3(name: &str) -> Option<Vec<u8>> {
let path = Path::new(env!("CARGO_MANIFEST_DIR"))
.join("../tests-oracle-corpus/dc3-sqlite-dissect")
.join(name);
std::fs::read(path).ok()
}
#[test]
fn fixture_full_recovery_includes_in_page_remnants() {
let db = Database::open(DELETED.to_vec()).expect("open");
let carved = carve_all_deleted_records(&db);
let mut ids: Vec<i64> = carved.iter().map(|c| c.rowid).collect();
ids.sort_unstable();
ids.dedup();
for must in [237, 300, 400] {
assert!(
ids.contains(&must),
"full carving must recover deleted row {must}; got {} distinct rows",
ids.len()
);
}
assert!(
carved.iter().all(|c| c.rowid > 200),
"full carving must never re-surface a live (id<=200) row"
);
}
#[test]
fn dc3_dropped_table_recovered() {
let Some(bytes) = dc3("corpus_0A-01.db") else {
eprintln!("SKIP dc3_dropped_table_recovered: DC3 corpus absent (gitignored)");
return;
};
let db = Database::open(bytes).expect("open 0A-01");
let carved = carve_all_deleted_records(&db);
assert!(
carved.len() >= 18,
"dropped-table carving must recover the bulk of 20 rows; got {}",
carved.len()
);
let r20 = carved
.iter()
.find(|c| c.rowid == 20)
.expect("must recover dropped rowid 20");
assert_eq!(r20.values.get(1), Some(&Value::Text("Erich".into())));
assert_eq!(r20.values.get(2), Some(&Value::Text("Graf".into())));
assert!(carved.iter().all(|c| !c.allocated));
}
#[test]
fn freeblock_reconstruction_wired_into_full_carver() {
let db = Database::open(NEMETZ_0C_01.to_vec()).expect("open 0C-01");
let carved = carve_all_deleted_records(&db);
let want = vec![
Value::Integer(20005),
Value::Integer(3_780_322_152),
Value::Integer(3_909_007_646),
Value::Integer(120_462_986),
Value::Integer(1_290_558_629),
];
let r20005 = carved
.iter()
.find(|c| c.values == want)
.expect("full carver must recover freeblock-clobbered row 20005");
assert_eq!(
r20005.source,
RecoverySource::FreeblockReconstructed,
"reconstructed rows must carry the FreeblockReconstructed provenance"
);
let live = db.live_rows();
for rec in &carved {
if rec.values.first() == Some(&Value::Integer(0)) {
continue; }
let collides = live.values().any(|lv| lv == &rec.values);
assert!(
!collides,
"freeblock reconstruction re-surfaced a LIVE row: {:?}",
rec.values
);
}
}
#[test]
fn dc3_dropped_table_0a02_recovered() {
let Some(bytes) = dc3("corpus_0A-02.db") else {
eprintln!("SKIP dc3_dropped_table_0a02_recovered: DC3 corpus absent (gitignored)");
return;
};
let db = Database::open(bytes).expect("open 0A-02");
let carved = carve_all_deleted_records(&db);
assert!(
carved.len() >= 10,
"dropped-table 0A-02 carving must recover the bulk of its rows; got {}",
carved.len()
);
}
const WAL_CARVE_MAIN: &[u8] = include_bytes!("../../tests/data/wal_carve.db");
const WAL_CARVE_WAL: &[u8] = include_bytes!("../../tests/data/wal_carve.db-wal");
fn deleted_body(n: i64) -> Value {
Value::Text(format!("secret WAL body {n}"))
}
#[test]
fn on_disk_only_carve_misses_wal_only_deleted_rows() {
let db = Database::open(WAL_CARVE_MAIN.to_vec()).expect("open main only");
let carved = carve_all_deleted_records(&db);
let found: Vec<i64> = (121..=140)
.filter(|&n| carved.iter().any(|c| c.values.contains(&deleted_body(n))))
.collect();
assert!(
found.is_empty(),
"on-disk-only carve must NOT see WAL-resident deleted rows; saw {found:?}"
);
}
#[test]
fn wal_frame_carve_recovers_wal_only_deleted_rows_with_provenance() {
let db =
Database::open_with_wal(WAL_CARVE_MAIN.to_vec(), WAL_CARVE_WAL).expect("open with wal");
let carved = carve_all_deleted_records(&db);
let recovered: Vec<i64> = (121..=140)
.filter(|&n| carved.iter().any(|c| c.values.contains(&deleted_body(n))))
.collect();
assert!(
recovered.len() >= 15,
"WAL-frame carve must recover the WAL-only deleted rows; got {recovered:?}"
);
for n in &recovered {
let rec = carved
.iter()
.find(|c| c.values.contains(&deleted_body(*n)))
.expect("recovered record present");
assert_eq!(
rec.source,
RecoverySource::WalFrame,
"WAL-resident residue must be tagged WalFrame, not an on-disk class"
);
let prov = rec
.wal
.as_ref()
.expect("WalFrame record carries WAL provenance");
assert!(prov.salt1 != 0 && prov.salt2 != 0, "salts recorded");
assert!(prov.frame_index < 8, "frame_index within the tiny WAL");
}
let live = db.live_rows();
for rec in &carved {
let collides = live.values().any(|lv| lv == &rec.values);
assert!(
!collides,
"WAL-frame carve re-surfaced a LIVE row: {:?}",
rec.values
);
}
}
#[test]
fn carve_at_first_commit_recovers_rows_deleted_only_later() {
let db =
Database::open_with_wal(WAL_CARVE_MAIN.to_vec(), WAL_CARVE_WAL).expect("open with wal");
let tl = db.wal_timeline().expect("timeline present");
let snaps = tl.commit_snapshots();
assert_eq!(snaps.len(), 2, "wal_carve has two commit snapshots");
let first = snaps[0].id();
let carved = carve_at_commit(&db, &tl, first);
let recovered: Vec<i64> = (121..=140)
.filter(|&n| carved.iter().any(|c| c.values.contains(&deleted_body(n))))
.collect();
assert!(
recovered.len() >= 15,
"carve@first-commit must recover rows alive then, deleted later; got {recovered:?}"
);
let lsn = snaps[0].lsn();
for rec in &carved {
let prov = rec.wal.as_ref().expect("snapshot record carries WAL LSN");
assert_eq!(prov.salt1, lsn.salt1, "salt1 matches the commit's segment");
assert_eq!(prov.salt2, lsn.salt2, "salt2 matches the commit's segment");
assert_eq!(
prov.frame_index, lsn.frame_index,
"frame_index is the COMMIT's frame index, labelling which snapshot"
);
}
}
#[test]
fn carve_at_commit_never_resurfaces_a_live_row() {
let db =
Database::open_with_wal(WAL_CARVE_MAIN.to_vec(), WAL_CARVE_WAL).expect("open with wal");
let tl = db.wal_timeline().expect("timeline present");
let live = db.live_rows();
for snap in tl.commit_snapshots() {
let carved = carve_at_commit(&db, &tl, snap.id());
for rec in &carved {
let collides = live.values().any(|lv| lv == &rec.values);
assert!(
!collides,
"carve@commit cfi={} re-surfaced a LIVE row: {:?}",
snap.id().commit_frame_index,
rec.values
);
}
}
}
#[test]
fn carve_at_unknown_commit_is_empty_not_a_panic() {
let db =
Database::open_with_wal(WAL_CARVE_MAIN.to_vec(), WAL_CARVE_WAL).expect("open with wal");
let tl = db.wal_timeline().expect("timeline present");
let mut bogus = tl.commit_snapshots()[0].id();
bogus.commit_frame_index = 9999;
assert!(
carve_at_commit(&db, &tl, bogus).is_empty(),
"unknown CommitId must carve nothing"
);
}
fn live_schema_rows(db: &Database) -> Vec<Vec<Value>> {
db.read_table(1, 5)
.expect("page-1 schema is readable")
.into_iter()
.map(|row| row.values)
.collect()
}
#[test]
fn carve_at_commit_never_resurfaces_the_live_schema_row() {
let db =
Database::open_with_wal(WAL_CARVE_MAIN.to_vec(), WAL_CARVE_WAL).expect("open with wal");
let tl = db.wal_timeline().expect("timeline present");
let schema = live_schema_rows(&db);
assert!(
!schema.is_empty(),
"wal_carve.db has a live sqlite_master entry to guard against"
);
for snap in tl.commit_snapshots() {
let carved = carve_at_commit(&db, &tl, snap.id());
for rec in &carved {
assert!(
!schema.contains(&rec.values),
"carve@commit cfi={} re-surfaced the LIVE sqlite_master row: {:?}",
snap.id().commit_frame_index,
rec.values
);
}
}
}
#[test]
fn live_schema_filter_keeps_genuinely_deleted_user_rows() {
let db =
Database::open_with_wal(WAL_CARVE_MAIN.to_vec(), WAL_CARVE_WAL).expect("open with wal");
let tl = db.wal_timeline().expect("timeline present");
let first = tl.commit_snapshots()[0].id();
let carved = carve_at_commit(&db, &tl, first);
let recovered: Vec<i64> = (121..=140)
.filter(|&n| carved.iter().any(|c| c.values.contains(&deleted_body(n))))
.collect();
assert!(
recovered.len() >= 15,
"live-schema filter must not drop genuinely-deleted rows 121..=140; got {recovered:?}"
);
}
#[test]
fn carve_all_never_resurfaces_the_live_schema_row_non_wal() {
let db = Database::open(DELETED.to_vec()).expect("open");
let schema = live_schema_rows(&db);
assert!(
!schema.is_empty(),
"deleted_places.db has a live sqlite_master entry to guard against"
);
let carved = carve_all_deleted_records(&db);
for rec in &carved {
assert!(
!schema.contains(&rec.values),
"carve_all re-surfaced the LIVE sqlite_master row: {:?}",
rec.values
);
}
}