use fsqlite_core::connection::Connection;
async fn build_clean_db(dir: &std::path::Path, name: &str) -> String {
let db = dir.join(name).to_string_lossy().into_owned();
let conn = Connection::open(&db).await.expect("open");
conn.execute("PRAGMA journal_mode=DELETE;")
.await
.expect("journal_mode");
conn.execute("CREATE TABLE t(a INTEGER PRIMARY KEY, v TEXT);")
.await
.expect("create");
conn.execute("INSERT INTO t VALUES (1,'x'),(2,'y'),(3,'z');")
.await
.expect("insert");
conn.close().await.expect("close");
db
}
fn set_byte(db: &str, offset: usize, value: u8) {
let mut bytes = std::fs::read(db).expect("read image");
assert!(offset < bytes.len());
bytes[offset] = value;
std::fs::write(db, &bytes).expect("write corrupted image");
}
async fn fsqlite_rejects(db: &str) -> bool {
let Ok(conn) = Connection::open(db).await else {
return true; };
let rejected = conn.query("SELECT count(*) FROM t;").await.is_err()
|| conn
.query("PRAGMA integrity_check;")
.await
.map(|rows| {
!matches!(rows.first().map(|r| &r.values()[0]),
Some(fsqlite_types::value::SqliteValue::Text(s)) if s.as_ref() == "ok")
})
.unwrap_or(true);
conn.close().await.ok();
rejected
}
fn stock_rejects(db: &str) -> bool {
let Ok(conn) = rusqlite::Connection::open(db) else {
return true;
};
conn.query_row("SELECT count(*) FROM t;", [], |r| r.get::<_, i64>(0))
.is_err()
}
async fn assert_header_corruption_rejected(name: &str, offset: usize, value: u8) {
let dir = tempfile::tempdir().expect("tempdir");
let db = build_clean_db(dir.path(), &format!("{name}.db")).await;
assert!(
!stock_rejects(&db),
"{name}: pristine fixture must be readable before corruption"
);
set_byte(&db, offset, value);
assert!(
stock_rejects(&db),
"{name}: fixture invalid — stock accepted header corruption at offset {offset} \
(value {value:#x}); choose a critical header field"
);
assert!(
fsqlite_rejects(&db).await,
"{name}: fsqlite SILENTLY ACCEPTED header corruption at offset {offset} \
(value {value:#x}) that stock rejects — a broken header must fail loudly, not \
open and serve wrong data"
);
}
macro_rules! header_scenario {
($fn_name:ident, $label:literal, $off:expr, $val:expr) => {
#[test]
fn $fn_name() {
asupersync::test_utils::run_test(|| async {
assert_header_corruption_rejected($label, $off, $val).await;
});
}
};
}
header_scenario!(header_magic_byte_0, "magic0", 0, 0xFF);
header_scenario!(header_magic_byte_8, "magic8", 8, 0x00);
header_scenario!(header_magic_byte_15, "magic15", 15, 0x01);
header_scenario!(header_page_size_high_invalid, "pagesize_hi", 16, 0xFF);
header_scenario!(header_page_size_low_not_pow2, "pagesize_lo", 17, 0x01);