use forensic_carve::{CarveContext, CarvedItem, Carver, CarverRegistration, Signature};
use forensicnomicon::sqlite::{
SQLITE_DB_SIZE_OFFSET, SQLITE_HEADER_SIZE, SQLITE_MAGIC, SQLITE_PAGE_SIZE_OFFSET,
SQLITE_RESERVED_SPACE_OFFSET, SQLITE_TEXT_ENCODING_OFFSET,
};
const FORMAT: &str = "sqlite";
const MAX_WINDOW: u64 = 1 << 30;
static SIGNATURES: [Signature; 1] = [Signature::new(b"SQLite format 3\0", 0)];
#[derive(Debug, Clone, Copy, Default)]
pub struct SqliteCarver;
static SQLITE_CARVER: SqliteCarver = SqliteCarver;
inventory::submit! { CarverRegistration::new(&SQLITE_CARVER) }
fn be_u16(bytes: &[u8], off: usize) -> Option<u16> {
let end = off.checked_add(2)?;
let slice = bytes.get(off..end)?;
Some(u16::from_be_bytes([slice[0], slice[1]]))
}
fn be_u32(bytes: &[u8], off: usize) -> Option<u32> {
let end = off.checked_add(4)?;
let slice = bytes.get(off..end)?;
Some(u32::from_be_bytes([slice[0], slice[1], slice[2], slice[3]]))
}
impl Carver for SqliteCarver {
fn format(&self) -> &'static str {
FORMAT
}
fn signatures(&self) -> &[Signature] {
&SIGNATURES
}
fn max_window(&self) -> u64 {
MAX_WINDOW
}
fn carve(&self, window: &[u8], ctx: &CarveContext) -> Vec<CarvedItem> {
let Some(header) = window.get(..SQLITE_HEADER_SIZE) else {
return Vec::new();
};
if !header.starts_with(SQLITE_MAGIC) {
return Vec::new();
}
let Some(raw_page_size) = be_u16(header, SQLITE_PAGE_SIZE_OFFSET) else {
return Vec::new(); };
let page_size: u32 = if raw_page_size == 1 {
65536
} else {
u32::from(raw_page_size)
};
if !(512..=65536).contains(&page_size) || !page_size.is_power_of_two() {
return Vec::new();
}
let Some(page_count) = be_u32(header, SQLITE_DB_SIZE_OFFSET) else {
return Vec::new(); };
if page_count == 0 {
return Vec::new();
}
let reserved = header
.get(SQLITE_RESERVED_SPACE_OFFSET)
.copied()
.unwrap_or(0);
let reserved_sane = u32::from(reserved) < page_size;
let encoding_valid = matches!(be_u32(header, SQLITE_TEXT_ENCODING_OFFSET), Some(1..=3));
let declared_len = u64::from(page_size) * u64::from(page_count);
let full_db_present = declared_len <= window.len() as u64;
let bounded_len = declared_len.min(window.len() as u64).min(MAX_WINDOW);
let end = usize::try_from(bounded_len).unwrap_or(usize::MAX);
let Some(db_bytes) = window.get(..end) else {
return Vec::new(); };
let graded = [reserved_sane, encoding_valid, full_db_present]
.iter()
.filter(|p| **p)
.count();
#[allow(clippy::cast_precision_loss)]
let confidence = (3 + graded) as f32 / 6.0;
vec![CarvedItem::artifact_bytes(
FORMAT,
ctx.base_offset(),
confidence,
ctx.recovery_method(),
db_bytes.to_vec(),
)]
}
}