#![cfg(feature = "std")]
use graphitesql::Connection;
use std::io::Write;
use std::panic::{catch_unwind, AssertUnwindSafe};
use std::sync::atomic::{AtomicU64, Ordering};
static SEQ: AtomicU64 = AtomicU64::new(0);
fn build_base() -> Vec<u8> {
let uniq = SEQ.fetch_add(1, Ordering::Relaxed);
let path =
std::env::temp_dir().join(format!("gsql-fuzz-base-{}-{uniq}.db", std::process::id()));
let p = path.to_string_lossy().into_owned();
let _ = std::fs::remove_file(&p);
{
let mut c = Connection::create(&p).unwrap();
c.execute("CREATE TABLE t(id INTEGER PRIMARY KEY, k INT, v TEXT)")
.unwrap();
c.execute("CREATE INDEX ik ON t(k)").unwrap();
for i in 0..50 {
c.execute(&format!(
"INSERT INTO t(k,v) VALUES ({}, 'val{}')",
i % 7,
i
))
.unwrap();
}
c.execute("CREATE TABLE big(b BLOB)").unwrap();
let big = "x".repeat(20000);
c.execute(&format!("INSERT INTO big VALUES ('{big}')"))
.unwrap();
}
let data = std::fs::read(&p).unwrap();
let _ = std::fs::remove_file(&p);
data
}
fn open_no_panic(bytes: &[u8], tag: &str) -> bool {
let uniq = SEQ.fetch_add(1, Ordering::Relaxed);
let path = std::env::temp_dir().join(format!(
"gsql-fuzz-{}-{uniq}-{}.db",
std::process::id(),
tag.replace(['/', ' '], "_")
));
let p = path.to_string_lossy().into_owned();
let _ = std::fs::remove_file(&p);
{
let mut f = std::fs::File::create(&p).unwrap();
f.write_all(bytes).unwrap();
}
let pc = p.clone();
let r = catch_unwind(AssertUnwindSafe(move || -> bool {
match Connection::open(&pc) {
Ok(c) => {
let _ = c.query("SELECT * FROM t");
let _ = c.query("SELECT * FROM t WHERE k = 3");
let _ = c.query("SELECT * FROM t WHERE id = 10");
let _ = c.query("SELECT * FROM big");
let _ = c.query("SELECT count(*) FROM t");
let _ = c.query("SELECT name FROM sqlite_schema");
let _ = c.query("PRAGMA integrity_check");
true
}
Err(_) => false,
}
}));
let _ = std::fs::remove_file(&p);
match r {
Ok(opened) => opened,
Err(_) => panic!("reader PANICKED on corrupted input (tag={tag})"),
}
}
fn page_size_of(base: &[u8]) -> usize {
let raw = u16::from_be_bytes([base[16], base[17]]) as usize;
if raw <= 1 {
65536
} else {
raw
}
}
#[test]
fn truncation_at_many_lengths() {
let base = build_base();
let cap = base.len().min(9000);
for len in 0..cap {
let opened = open_no_panic(&base[..len], &format!("trunc-{len}"));
if len < 100 {
assert!(!opened, "a {len}-byte file must not open as a database");
}
}
}
#[test]
fn corrupted_header_fields() {
let base = build_base();
for pos in 0..100usize {
for val in [0u8, 1, 7, 0x55, 0x80, 0xff] {
let mut b = base.clone();
b[pos] = val;
open_no_panic(&b, &format!("hdr-{pos}-{val}"));
}
}
for ps in [0u16, 1, 3, 7, 100, 0x1ff, 0x201, 3000, 4097, 0xffff] {
let mut b = base.clone();
b[16..18].copy_from_slice(&ps.to_be_bytes());
let opened = open_no_panic(&b, &format!("ps-{ps}"));
let valid = ps == 1 || (ps >= 512 && ps.is_power_of_two());
if !valid {
assert!(!opened, "page size {ps} must be rejected");
}
}
for r in 0u8..=255 {
let mut b = base.clone();
b[20] = r;
open_no_panic(&b, &format!("resv-{r}"));
}
for enc in [0u32, 4, 0xff, 0xffff_ffff] {
let mut b = base.clone();
b[56..60].copy_from_slice(&enc.to_be_bytes());
let opened = open_no_panic(&b, &format!("enc-{enc}"));
assert!(!opened, "text encoding {enc} must be rejected");
}
for off in [28u32, 32, 36, 52] {
for val in [1u32, 2, 0x7fff_ffff, 0xffff_ffff] {
let mut b = base.clone();
b[off as usize..off as usize + 4].copy_from_slice(&val.to_be_bytes());
open_no_panic(&b, &format!("h32-{off}-{val}"));
}
}
let mut bad_magic = base.clone();
bad_magic[0] = b'X';
assert!(!open_no_panic(&bad_magic, "bad-magic"));
}
#[test]
fn corrupted_btree_pages() {
let base = build_base();
let ps = page_size_of(&base);
let end = base.len().min(ps * 3);
for pos in 100..end {
for val in [0u8, 0xff, 0x02, 0x05, 0x0a, 0x0d] {
let mut b = base.clone();
b[pos] = val;
open_no_panic(&b, &format!("pg-{pos}-{val}"));
}
}
let npages = (base.len() / ps).min(4);
for pg in 0..npages {
let pstart = pg * ps;
let body = if pg == 0 { 100 } else { 0 };
for ty in 0u8..=255 {
let mut b = base.clone();
b[pstart + body] = ty;
open_no_panic(&b, &format!("ptype-{pg}-{ty}"));
}
for nc in [0xffffu16, 0x7fff, 0x00ff, 1] {
let mut b = base.clone();
b[pstart + body + 3..pstart + body + 5].copy_from_slice(&nc.to_be_bytes());
open_no_panic(&b, &format!("ncells-{pg}-{nc}"));
}
for k in 0..24usize {
for off in [
0xffffu16,
0x0000,
(ps as u16).wrapping_sub(1),
1,
8,
body as u16,
] {
let mut b = base.clone();
let at = pstart + body + 8 + 2 * k;
if at + 2 <= b.len() {
b[at..at + 2].copy_from_slice(&off.to_be_bytes());
open_no_panic(&b, &format!("cellptr-{pg}-{k}-{off}"));
}
}
}
}
}
#[test]
fn whole_page_and_random_garbage() {
let base = build_base();
let ps = page_size_of(&base);
let npages = base.len() / ps;
for pg in 0..npages {
for pat in [0u8, 0xff, 0xaa, 0x55] {
let mut b = base.clone();
let start = if pg == 0 { 100 } else { pg * ps };
let end = (pg + 1) * ps;
for byte in &mut b[start..end] {
*byte = pat;
}
open_no_panic(&b, &format!("fill-{pg}-{pat}"));
}
}
let mut state: u64 = 0x1234_5678_9abc_def0;
for trial in 0..300 {
let len = (state as usize % base.len()).max(100);
let mut b = vec![0u8; len];
for byte in &mut b {
state = state.wrapping_mul(6364136223846793005).wrapping_add(1);
*byte = (state >> 33) as u8;
}
if trial % 2 == 0 {
b[0..16].copy_from_slice(b"SQLite format 3\0");
}
open_no_panic(&b, &format!("rand-{trial}"));
}
}
fn forge_header(page_size: u32, reserved: u8) -> Vec<u8> {
let mut page = vec![0u8; page_size as usize];
page[0..16].copy_from_slice(b"SQLite format 3\0");
let raw_ps: u16 = if page_size == 65536 {
1
} else {
page_size as u16
};
page[16..18].copy_from_slice(&raw_ps.to_be_bytes());
page[18] = 1;
page[19] = 1;
page[20] = reserved;
page[21] = 64;
page[22] = 32;
page[23] = 32;
page[28..32].copy_from_slice(&1u32.to_be_bytes()); page[44..48].copy_from_slice(&4u32.to_be_bytes()); page[56..60].copy_from_slice(&1u32.to_be_bytes()); page[92..96].copy_from_slice(&1u32.to_be_bytes()); page[100] = 0x0d; page
}
#[test]
fn forged_small_pages_and_overflow() {
for &ps in &[512u32, 1024, 2048, 65536] {
for &reserved in &[0u8, 1, 100, 200, 250, 255] {
if reserved as u32 >= ps {
continue;
}
let mut b = forge_header(ps, reserved);
open_no_panic(&b, &format!("forge-{ps}-{reserved}"));
for at in [100usize, 108, 110, 116] {
let mut bb = b.clone();
if at + 9 <= bb.len() {
for byte in &mut bb[at..at + 8] {
*byte = 0xff;
}
bb[at + 8] = 0x7f;
open_no_panic(&bb, &format!("varint-{ps}-{reserved}-{at}"));
}
}
b[103..105].copy_from_slice(&1u16.to_be_bytes()); b[108..110].copy_from_slice(&111u16.to_be_bytes()); if 111 + 12 <= b.len() {
b[111] = 0x81; b[112] = 0x00; b[113] = 0x01; b[114..118].copy_from_slice(&0xffff_ffffu32.to_be_bytes()); open_no_panic(&b, &format!("overflow-{ps}-{reserved}"));
}
}
}
}
#[test]
fn corrupted_wal_sidecar() {
let base = build_base();
let ps = page_size_of(&base);
let path = std::env::temp_dir().join(format!(
"gsql-fuzz-wal-{}-{}.db",
std::process::id(),
SEQ.fetch_add(1, Ordering::Relaxed)
));
let p = path.to_string_lossy().into_owned();
let wp = format!("{p}-wal");
let _ = std::fs::remove_file(&p);
let _ = std::fs::remove_file(&wp);
std::fs::write(&p, &base).unwrap();
let mut state: u64 = 0xdead_beef_cafe_babe;
for trial in 0..200 {
let len = 32 + (state as usize % (4 * (ps + 24)));
let mut w = vec![0u8; len];
for byte in &mut w {
state = state.wrapping_mul(6364136223846793005).wrapping_add(1);
*byte = (state >> 40) as u8;
}
if trial % 3 == 0 {
w[0..4].copy_from_slice(&0x377f_0682u32.to_be_bytes());
w[8..12].copy_from_slice(&(ps as u32).to_be_bytes());
}
std::fs::write(&wp, &w).unwrap();
let r = catch_unwind(AssertUnwindSafe(|| {
if let Ok(c) = Connection::open(&p) {
let _ = c.query("SELECT * FROM t");
let _ = c.query("PRAGMA integrity_check");
}
}));
if r.is_err() {
let _ = std::fs::remove_file(&p);
let _ = std::fs::remove_file(&wp);
panic!("WAL reader PANICKED on corrupted sidecar (trial={trial})");
}
}
let _ = std::fs::remove_file(&p);
let _ = std::fs::remove_file(&wp);
}