#![allow(clippy::unwrap_used, clippy::expect_used)]
use std::path::PathBuf;
use std::process::Command;
use sqlite_core::rebuild::{
build_recovered_db, build_recovered_db_with_fragments, FragmentRow, RebuildRow,
};
use sqlite_core::Value;
fn sqlite3_bin() -> Option<String> {
let bin = std::env::var("SQLITE3_BIN").unwrap_or_else(|_| "sqlite3".to_string());
Command::new(&bin)
.arg("--version")
.output()
.ok()
.filter(|o| o.status.success())
.map(|_| bin)
}
fn temp_db(bytes: &[u8], tag: &str) -> PathBuf {
let mut p = std::env::temp_dir();
let nonce = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_nanos();
p.push(format!("sqlite4n6_rebuild_{tag}_{nonce}.db"));
std::fs::write(&p, bytes).unwrap();
p
}
fn run_sql(bin: &str, db: &PathBuf, sql: &str) -> String {
let out = Command::new(bin)
.arg(db)
.arg(sql)
.output()
.expect("sqlite3 must execute");
assert!(
out.status.success(),
"sqlite3 failed: {}",
String::from_utf8_lossy(&out.stderr)
);
String::from_utf8_lossy(&out.stdout).trim().to_string()
}
#[test]
fn sqlite3_reads_rebuilt_db_small_and_overflow() {
let Some(bin) = sqlite3_bin() else {
eprintln!("SKIP sqlite3_reads_rebuilt_db_small_and_overflow: no sqlite3 (set SQLITE3_BIN)");
return;
};
let big: Vec<u8> = (0..9000u32).map(|i| (i % 251) as u8).collect();
let rows = vec![
RebuildRow {
page: 7,
offset: 128,
rowid: Some(42),
source: "freelist-page".into(),
confidence: 0.9,
cells: vec![
Value::Integer(7),
Value::Text("alice".into()),
Value::Real(1.5),
Value::Blob(vec![0xDE, 0xAD, 0xBE, 0xEF]),
],
},
RebuildRow {
page: 8,
offset: 256,
rowid: None,
source: "freeblock-reconstructed".into(),
confidence: 0.4,
cells: vec![Value::Text("spilled".into()), Value::Blob(big.clone())],
},
];
let bytes = build_recovered_db(&rows);
let db = temp_db(&bytes, "small_overflow");
assert_eq!(run_sql(&bin, &db, "PRAGMA integrity_check;"), "ok");
assert_eq!(
run_sql(&bin, &db, "SELECT count(*) FROM recovered_records;"),
"2"
);
let typed = run_sql(
&bin,
&db,
"SELECT typeof(c0), quote(c0), typeof(c1), typeof(c2), typeof(c3) \
FROM recovered_records WHERE _rowid = 42;",
);
assert_eq!(typed, "integer|7|text|real|blob", "got: {typed}");
assert_eq!(
run_sql(
&bin,
&db,
"SELECT quote(c3) FROM recovered_records WHERE _rowid = 42;"
),
"X'DEADBEEF'"
);
assert_eq!(
run_sql(
&bin,
&db,
"SELECT count(*) FROM recovered_records WHERE _rowid IS NULL;"
),
"1"
);
let blob_len = run_sql(
&bin,
&db,
"SELECT length(c1) FROM recovered_records WHERE _source = 'freeblock-reconstructed';",
);
assert_eq!(blob_len, big.len().to_string());
let head = run_sql(
&bin,
&db,
"SELECT hex(substr(c1,1,8)) FROM recovered_records WHERE _source = 'freeblock-reconstructed';",
);
assert_eq!(head, "0001020304050607", "overflow BLOB head bytes");
let _ = std::fs::remove_file(&db);
}
#[test]
fn sqlite3_reads_rebuilt_empty_db() {
let Some(bin) = sqlite3_bin() else {
eprintln!("SKIP sqlite3_reads_rebuilt_empty_db: no sqlite3 (set SQLITE3_BIN)");
return;
};
let bytes = build_recovered_db(&[]);
let db = temp_db(&bytes, "empty");
assert_eq!(run_sql(&bin, &db, "PRAGMA integrity_check;"), "ok");
assert_eq!(
run_sql(&bin, &db, "SELECT count(*) FROM recovered_records;"),
"0"
);
let _ = std::fs::remove_file(&db);
}
#[test]
fn sqlite3_reads_two_table_db_with_fragments() {
let Some(bin) = sqlite3_bin() else {
eprintln!("SKIP sqlite3_reads_two_table_db_with_fragments: no sqlite3 (set SQLITE3_BIN)");
return;
};
let records = vec![RebuildRow {
page: 7,
offset: 128,
rowid: Some(42),
source: "freelist-page".into(),
confidence: 0.9,
cells: vec![Value::Integer(7), Value::Text("alice".into())],
}];
let fragments = vec![
FragmentRow {
page: 2,
offset: 3965,
missing: 1,
confidence: 0.2,
surviving: vec![
(0, Value::Integer(20004)),
(1, Value::Text("Anja".into())),
(2, Value::Text("Frank".into())),
],
},
FragmentRow {
page: 5,
offset: 64,
missing: 2,
confidence: 0.2,
surviving: vec![(2, Value::Blob(vec![0xCA, 0xFE, 0xBA]))],
},
];
let bytes = build_recovered_db_with_fragments(&records, Some(&fragments));
let db = temp_db(&bytes, "two_table");
assert_eq!(run_sql(&bin, &db, "PRAGMA integrity_check;"), "ok");
assert_eq!(
run_sql(&bin, &db, "SELECT count(*) FROM recovered_records;"),
"1"
);
assert_eq!(
run_sql(&bin, &db, "SELECT count(*) FROM recovered_fragments;"),
"2"
);
assert_eq!(
run_sql(
&bin,
&db,
"SELECT typeof(c1), quote(c1) FROM recovered_fragments WHERE c0 = 20004;"
),
"text|'Anja'"
);
assert_eq!(
run_sql(
&bin,
&db,
"SELECT typeof(c2), quote(c2), c0 IS NULL, c1 IS NULL \
FROM recovered_fragments WHERE _offset = 64;"
),
"blob|X'CAFEBA'|1|1"
);
let _ = std::fs::remove_file(&db);
}
#[test]
fn sqlite3_reads_two_table_db_with_empty_fragment_set() {
let Some(bin) = sqlite3_bin() else {
eprintln!(
"SKIP sqlite3_reads_two_table_db_with_empty_fragment_set: no sqlite3 (set SQLITE3_BIN)"
);
return;
};
let records = vec![RebuildRow {
page: 1,
offset: 0,
rowid: Some(1),
source: "freelist-page".into(),
confidence: 0.9,
cells: vec![Value::Integer(1)],
}];
let bytes = build_recovered_db_with_fragments(&records, Some(&[]));
let db = temp_db(&bytes, "two_table_empty");
assert_eq!(run_sql(&bin, &db, "PRAGMA integrity_check;"), "ok");
assert_eq!(
run_sql(&bin, &db, "SELECT count(*) FROM recovered_fragments;"),
"0"
);
let _ = std::fs::remove_file(&db);
}