Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
901 lines
28 KiB
Rust
901 lines
28 KiB
Rust
//! Leitor de bancos SQLite feito para arquivos recuperados.
|
|
//!
|
|
//! Lê as páginas B-tree diretamente (sem a biblioteca do SQLite), então funciona
|
|
//! em bancos danificados ou truncados, para os quais a biblioteca oficial desiste.
|
|
//! Também procura **registros apagados** nas áreas livres das páginas, onde o
|
|
//! SQLite deixa o conteúdo antigo até que outro dado o sobrescreva.
|
|
|
|
use crate::source::Source;
|
|
use serde::Serialize;
|
|
use std::collections::HashSet;
|
|
|
|
const MAGIC: &[u8; 16] = b"SQLite format 3\0";
|
|
|
|
#[derive(Debug, Clone, Serialize)]
|
|
#[serde(tag = "t", content = "v", rename_all = "lowercase")]
|
|
pub enum Cell {
|
|
Null,
|
|
Int(i64),
|
|
Real(f64),
|
|
Text(String),
|
|
/// Tamanho e os primeiros bytes (hex) para pré-visualizar; `mime` se reconhecido.
|
|
Blob {
|
|
len: usize,
|
|
head: String,
|
|
mime: Option<&'static str>,
|
|
},
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize)]
|
|
pub struct DbColumn {
|
|
pub name: String,
|
|
pub decl_type: String,
|
|
pub pk: bool,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize)]
|
|
pub struct DbTable {
|
|
pub name: String,
|
|
pub rootpage: u32,
|
|
pub sql: String,
|
|
pub columns: Vec<DbColumn>,
|
|
pub without_rowid: bool,
|
|
pub virtual_table: bool,
|
|
/// Coluna `INTEGER PRIMARY KEY` (alias do rowid), se houver.
|
|
#[serde(skip)]
|
|
pub rowid_alias: Option<usize>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize)]
|
|
pub struct DbSchema {
|
|
pub page_size: u32,
|
|
pub pages: u32,
|
|
pub file_pages: u64,
|
|
pub encoding: String,
|
|
pub tables: Vec<DbTable>,
|
|
pub indexes: Vec<String>,
|
|
pub views: Vec<String>,
|
|
pub warnings: Vec<String>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize)]
|
|
pub struct DbRow {
|
|
pub rowid: Option<i64>,
|
|
pub cells: Vec<Cell>,
|
|
pub deleted: bool,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Default)]
|
|
pub struct RowsPage {
|
|
pub columns: Vec<String>,
|
|
pub rows: Vec<DbRow>,
|
|
pub total: u64,
|
|
pub warnings: Vec<String>,
|
|
}
|
|
|
|
pub struct Db<'a> {
|
|
src: &'a dyn Source,
|
|
ps: usize,
|
|
usable: usize,
|
|
pages: u32,
|
|
enc: u32,
|
|
}
|
|
|
|
type Res<T> = Result<T, String>;
|
|
|
|
// ------------------------------------------------------------------ utilidades
|
|
|
|
fn varint(b: &[u8]) -> Option<(i64, usize)> {
|
|
let mut v: u64 = 0;
|
|
for i in 0..9 {
|
|
let c = *b.get(i)?;
|
|
if i == 8 {
|
|
return Some((((v << 8) | c as u64) as i64, 9));
|
|
}
|
|
v = (v << 7) | (c & 0x7F) as u64;
|
|
if c & 0x80 == 0 {
|
|
return Some((v as i64, i + 1));
|
|
}
|
|
}
|
|
None
|
|
}
|
|
|
|
fn be16(b: &[u8], o: usize) -> Option<usize> {
|
|
Some(u16::from_be_bytes([*b.get(o)?, *b.get(o + 1)?]) as usize)
|
|
}
|
|
|
|
fn be32(b: &[u8], o: usize) -> Option<u32> {
|
|
Some(u32::from_be_bytes([
|
|
*b.get(o)?,
|
|
*b.get(o + 1)?,
|
|
*b.get(o + 2)?,
|
|
*b.get(o + 3)?,
|
|
]))
|
|
}
|
|
|
|
fn serial_len(t: i64) -> Option<usize> {
|
|
Some(match t {
|
|
0 | 8 | 9 => 0,
|
|
1 => 1,
|
|
2 => 2,
|
|
3 => 3,
|
|
4 => 4,
|
|
5 => 6,
|
|
6 | 7 => 8,
|
|
10 | 11 => return None,
|
|
n if n >= 12 => ((n - 12) / 2) as usize,
|
|
_ => return None,
|
|
})
|
|
}
|
|
|
|
fn blob_mime(b: &[u8]) -> Option<&'static str> {
|
|
if b.starts_with(&[0xFF, 0xD8, 0xFF]) {
|
|
Some("image/jpeg")
|
|
} else if b.starts_with(&[0x89, b'P', b'N', b'G']) {
|
|
Some("image/png")
|
|
} else if b.starts_with(b"GIF8") {
|
|
Some("image/gif")
|
|
} else if b.len() > 12 && &b[0..4] == b"RIFF" && &b[8..12] == b"WEBP" {
|
|
Some("image/webp")
|
|
} else {
|
|
None
|
|
}
|
|
}
|
|
|
|
fn decode_text(b: &[u8], enc: u32) -> String {
|
|
match enc {
|
|
2 => String::from_utf16_lossy(
|
|
&b.chunks_exact(2)
|
|
.map(|c| u16::from_le_bytes([c[0], c[1]]))
|
|
.collect::<Vec<_>>(),
|
|
),
|
|
3 => String::from_utf16_lossy(
|
|
&b.chunks_exact(2)
|
|
.map(|c| u16::from_be_bytes([c[0], c[1]]))
|
|
.collect::<Vec<_>>(),
|
|
),
|
|
_ => String::from_utf8_lossy(b).into_owned(),
|
|
}
|
|
}
|
|
|
|
/// Decodifica um registro (cabeçalho de tipos + corpo).
|
|
fn decode_record(p: &[u8], enc: u32) -> Option<Vec<Cell>> {
|
|
let (hlen, n) = varint(p)?;
|
|
let hlen = hlen as usize;
|
|
if hlen < n || hlen > p.len() || hlen > 4096 {
|
|
return None;
|
|
}
|
|
let mut types = vec![];
|
|
let mut o = n;
|
|
while o < hlen {
|
|
let (t, m) = varint(&p[o..hlen])?;
|
|
types.push(t);
|
|
o += m;
|
|
}
|
|
let mut cells = Vec::with_capacity(types.len());
|
|
let mut o = hlen;
|
|
for t in types {
|
|
let l = serial_len(t)?;
|
|
let b = p.get(o..o + l)?;
|
|
o += l;
|
|
cells.push(match t {
|
|
0 => Cell::Null,
|
|
1..=6 => {
|
|
let mut v: i64 = if b[0] & 0x80 != 0 { -1 } else { 0 };
|
|
for &x in b {
|
|
v = (v << 8) | x as i64;
|
|
}
|
|
Cell::Int(v)
|
|
}
|
|
7 => Cell::Real(f64::from_be_bytes(b.try_into().ok()?)),
|
|
8 => Cell::Int(0),
|
|
9 => Cell::Int(1),
|
|
n if n % 2 == 1 => Cell::Text(decode_text(b, enc)),
|
|
_ => Cell::Blob {
|
|
len: b.len(),
|
|
head: b.iter().take(24).map(|x| format!("{x:02x}")).collect(),
|
|
mime: blob_mime(b),
|
|
},
|
|
});
|
|
}
|
|
Some(cells)
|
|
}
|
|
|
|
// ------------------------------------------------------------------ banco
|
|
|
|
impl<'a> Db<'a> {
|
|
pub fn open(src: &'a dyn Source) -> Res<Self> {
|
|
let mut h = [0u8; 100];
|
|
let n = src.read_at(0, &mut h).map_err(|e| e.to_string())?;
|
|
if n < 100 || &h[0..16] != MAGIC {
|
|
return Err("Não é um banco SQLite (cabeçalho ausente). Se o arquivo é um banco, ele pode estar criptografado (SQLCipher) ou ter o início sobrescrito.".into());
|
|
}
|
|
let ps = match u16::from_be_bytes([h[16], h[17]]) {
|
|
1 => 65536usize,
|
|
v if (512..=32768).contains(&v) && v.is_power_of_two() => v as usize,
|
|
_ => return Err("Tamanho de página inválido no cabeçalho do banco.".into()),
|
|
};
|
|
let reserved = h[20] as usize;
|
|
let mut pages = u32::from_be_bytes([h[28], h[29], h[30], h[31]]);
|
|
let file_pages = (src.size() / ps as u64) as u32;
|
|
if pages == 0 || pages > file_pages.max(1) * 4 + 1 {
|
|
pages = file_pages;
|
|
}
|
|
let enc = u32::from_be_bytes([h[56], h[57], h[58], h[59]]);
|
|
Ok(Self {
|
|
src,
|
|
ps,
|
|
usable: ps - reserved,
|
|
pages: pages.max(1),
|
|
enc: if (1..=3).contains(&enc) { enc } else { 1 },
|
|
})
|
|
}
|
|
|
|
fn page(&self, n: u32) -> Res<Vec<u8>> {
|
|
if n == 0 {
|
|
return Err("página 0 inválida".into());
|
|
}
|
|
let off = (n as u64 - 1) * self.ps as u64;
|
|
if off + self.ps as u64 > self.src.size() {
|
|
return Err(format!("página {n} fora do arquivo (banco truncado)"));
|
|
}
|
|
let mut b = vec![0u8; self.ps];
|
|
let mut got = 0;
|
|
while got < b.len() {
|
|
match self.src.read_at(off + got as u64, &mut b[got..]) {
|
|
Ok(0) => return Err(format!("página {n} incompleta")),
|
|
Ok(k) => got += k,
|
|
Err(e) => return Err(e.to_string()),
|
|
}
|
|
}
|
|
Ok(b)
|
|
}
|
|
|
|
fn enc_name(&self) -> &'static str {
|
|
match self.enc {
|
|
2 => "UTF-16 LE",
|
|
3 => "UTF-16 BE",
|
|
_ => "UTF-8",
|
|
}
|
|
}
|
|
|
|
/// Lê o payload completo de uma célula (segue as páginas de overflow).
|
|
fn payload(
|
|
&self,
|
|
page: &[u8],
|
|
cell_off: usize,
|
|
p: usize,
|
|
index: bool,
|
|
hdr: usize,
|
|
) -> Option<Vec<u8>> {
|
|
let u = self.usable;
|
|
let x = if index {
|
|
((u - 12) * 64 / 255).saturating_sub(23)
|
|
} else {
|
|
u - 35
|
|
};
|
|
let m = ((u - 12) * 32 / 255).saturating_sub(23);
|
|
let local = if p <= x {
|
|
p
|
|
} else {
|
|
let k = m + (p - m) % (u - 4);
|
|
if k <= x {
|
|
k
|
|
} else {
|
|
m
|
|
}
|
|
};
|
|
let start = cell_off + hdr;
|
|
let mut out = page.get(start..start + local)?.to_vec();
|
|
if p > local {
|
|
let mut next = be32(page, start + local)?;
|
|
let mut guard = 0;
|
|
while out.len() < p && next != 0 && guard < 100_000 {
|
|
guard += 1;
|
|
let ov = self.page(next).ok()?;
|
|
next = be32(&ov, 0)?;
|
|
let take = (p - out.len()).min(u - 4);
|
|
out.extend_from_slice(ov.get(4..4 + take)?);
|
|
}
|
|
if out.len() < p {
|
|
return None;
|
|
}
|
|
}
|
|
Some(out)
|
|
}
|
|
|
|
/// Percorre as folhas de uma árvore de tabela chamando `f(rowid, payload)`.
|
|
/// `f` devolve false para parar. Devolve os avisos de corrupção encontrados.
|
|
fn walk_table(&self, root: u32, f: &mut dyn FnMut(i64, &[u8]) -> bool) -> Vec<String> {
|
|
let mut warnings = vec![];
|
|
let mut stack = vec![(root, 0u32)];
|
|
let mut seen = HashSet::new();
|
|
while let Some((pn, depth)) = stack.pop() {
|
|
if depth > 40 || !seen.insert(pn) {
|
|
continue;
|
|
}
|
|
let pg = match self.page(pn) {
|
|
Ok(p) => p,
|
|
Err(e) => {
|
|
if warnings.len() < 5 {
|
|
warnings.push(e);
|
|
}
|
|
continue;
|
|
}
|
|
};
|
|
let hb = if pn == 1 { 100 } else { 0 };
|
|
let Some(&ty) = pg.get(hb) else { continue };
|
|
let ncell = be16(&pg, hb + 3).unwrap_or(0);
|
|
match ty {
|
|
0x05 => {
|
|
let mut kids = vec![];
|
|
for i in 0..ncell {
|
|
let Some(co) = be16(&pg, hb + 12 + i * 2) else {
|
|
break;
|
|
};
|
|
if let Some(c) = be32(&pg, co) {
|
|
kids.push(c);
|
|
}
|
|
}
|
|
if let Some(r) = be32(&pg, hb + 8) {
|
|
kids.push(r);
|
|
}
|
|
for k in kids.into_iter().rev() {
|
|
stack.push((k, depth + 1));
|
|
}
|
|
}
|
|
0x0D => {
|
|
for i in 0..ncell {
|
|
let Some(co) = be16(&pg, hb + 8 + i * 2) else {
|
|
break;
|
|
};
|
|
let Some(tail) = pg.get(co..) else { continue };
|
|
let Some((plen, a)) = varint(tail) else {
|
|
continue;
|
|
};
|
|
let Some((rowid, b)) = varint(&tail[a..]) else {
|
|
continue;
|
|
};
|
|
if plen < 0 || plen as usize > 1 << 28 {
|
|
continue;
|
|
}
|
|
if let Some(pl) = self.payload(&pg, co, plen as usize, false, a + b) {
|
|
if !f(rowid, &pl) {
|
|
return warnings;
|
|
}
|
|
} else if warnings.len() < 5 {
|
|
warnings.push(format!("célula ilegível na página {pn}"));
|
|
}
|
|
}
|
|
}
|
|
_ => {
|
|
if warnings.len() < 5 {
|
|
warnings.push(format!("página {pn} de tipo inesperado (0x{ty:02x})"));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
warnings
|
|
}
|
|
|
|
fn leaf_pages(&self, root: u32) -> Vec<u32> {
|
|
let mut leaves = vec![];
|
|
let mut stack = vec![(root, 0u32)];
|
|
let mut seen = HashSet::new();
|
|
while let Some((pn, depth)) = stack.pop() {
|
|
if depth > 40 || !seen.insert(pn) {
|
|
continue;
|
|
}
|
|
let Ok(pg) = self.page(pn) else { continue };
|
|
let hb = if pn == 1 { 100 } else { 0 };
|
|
match pg.get(hb) {
|
|
Some(0x05) => {
|
|
let n = be16(&pg, hb + 3).unwrap_or(0);
|
|
for i in 0..n {
|
|
if let Some(c) = be16(&pg, hb + 12 + i * 2).and_then(|co| be32(&pg, co)) {
|
|
stack.push((c, depth + 1));
|
|
}
|
|
}
|
|
if let Some(r) = be32(&pg, hb + 8) {
|
|
stack.push((r, depth + 1));
|
|
}
|
|
}
|
|
Some(0x0D) => leaves.push(pn),
|
|
_ => {}
|
|
}
|
|
}
|
|
leaves
|
|
}
|
|
|
|
// ---------------------------------------------------------- esquema
|
|
|
|
pub fn schema(&self) -> DbSchema {
|
|
let mut s = DbSchema {
|
|
page_size: self.ps as u32,
|
|
pages: self.pages,
|
|
file_pages: self.src.size() / self.ps as u64,
|
|
encoding: self.enc_name().into(),
|
|
tables: vec![],
|
|
indexes: vec![],
|
|
views: vec![],
|
|
warnings: vec![],
|
|
};
|
|
if s.file_pages < self.pages as u64 {
|
|
s.warnings.push(format!(
|
|
"Arquivo truncado: o cabeçalho indica {} páginas, mas só há {}. Parte dos dados pode estar faltando.",
|
|
self.pages, s.file_pages
|
|
));
|
|
}
|
|
let mut rows: Vec<Vec<Cell>> = vec![];
|
|
let w = self.walk_table(1, &mut |_, p| {
|
|
if let Some(c) = decode_record(p, self.enc) {
|
|
rows.push(c);
|
|
}
|
|
true
|
|
});
|
|
s.warnings.extend(w);
|
|
let text = |c: &Cell| {
|
|
if let Cell::Text(t) = c {
|
|
t.clone()
|
|
} else {
|
|
String::new()
|
|
}
|
|
};
|
|
for r in rows {
|
|
if r.len() < 5 {
|
|
continue;
|
|
}
|
|
let (ty, name, rootpage, sql) = (
|
|
text(&r[0]),
|
|
text(&r[1]),
|
|
if let Cell::Int(i) = r[3] { i as u32 } else { 0 },
|
|
text(&r[4]),
|
|
);
|
|
match ty.as_str() {
|
|
"table" => {
|
|
let (columns, rowid_alias, without_rowid) = parse_create(&sql);
|
|
s.tables.push(DbTable {
|
|
virtual_table: sql.to_uppercase().starts_with("CREATE VIRTUAL"),
|
|
name,
|
|
rootpage,
|
|
sql,
|
|
columns,
|
|
without_rowid,
|
|
rowid_alias,
|
|
});
|
|
}
|
|
"index" => s.indexes.push(name),
|
|
"view" => s.views.push(name),
|
|
_ => {}
|
|
}
|
|
}
|
|
if s.tables.is_empty() {
|
|
s.warnings.push(
|
|
"Nenhuma tabela encontrada no esquema. A primeira página pode estar danificada."
|
|
.into(),
|
|
);
|
|
}
|
|
s
|
|
}
|
|
|
|
fn table(&self, name: &str) -> Res<DbTable> {
|
|
self.schema()
|
|
.tables
|
|
.into_iter()
|
|
.find(|t| t.name == name)
|
|
.ok_or_else(|| format!("tabela {name} não encontrada"))
|
|
}
|
|
|
|
/// Conta as linhas da tabela (sem decodificar o conteúdo).
|
|
pub fn count_rows(&self, t: &DbTable) -> u64 {
|
|
if t.rootpage == 0 || t.virtual_table || t.without_rowid {
|
|
return 0;
|
|
}
|
|
let mut n = 0u64;
|
|
for leaf in self.leaf_pages(t.rootpage) {
|
|
if let Ok(pg) = self.page(leaf) {
|
|
n += be16(&pg, if leaf == 1 { 103 } else { 3 }).unwrap_or(0) as u64;
|
|
}
|
|
}
|
|
n
|
|
}
|
|
|
|
// ---------------------------------------------------------- linhas
|
|
|
|
pub fn rows(&self, table: &str, offset: usize, limit: usize) -> Res<RowsPage> {
|
|
let t = self.table(table)?;
|
|
let mut page = RowsPage {
|
|
columns: t.columns.iter().map(|c| c.name.clone()).collect(),
|
|
..Default::default()
|
|
};
|
|
if t.virtual_table || t.rootpage == 0 {
|
|
page.warnings
|
|
.push("Tabela virtual: o conteúdo não fica armazenado como linhas comuns.".into());
|
|
return Ok(page);
|
|
}
|
|
if t.without_rowid {
|
|
page.warnings.push(
|
|
"Tabela WITHOUT ROWID: as linhas ficam numa árvore de índice, ainda não exibida."
|
|
.into(),
|
|
);
|
|
return Ok(page);
|
|
}
|
|
let mut seen = 0usize;
|
|
let ncols = t.columns.len();
|
|
page.warnings = self.walk_table(t.rootpage, &mut |rowid, p| {
|
|
seen += 1;
|
|
if seen <= offset {
|
|
return true;
|
|
}
|
|
if page.rows.len() >= limit {
|
|
return false;
|
|
}
|
|
if let Some(mut cells) = decode_record(p, self.enc) {
|
|
if let Some(a) = t.rowid_alias {
|
|
if a < cells.len() && matches!(cells[a], Cell::Null) {
|
|
cells[a] = Cell::Int(rowid);
|
|
}
|
|
}
|
|
cells.resize(cells.len().max(ncols), Cell::Null);
|
|
apply_affinity(&mut cells, &t.columns);
|
|
page.rows.push(DbRow {
|
|
rowid: Some(rowid),
|
|
cells,
|
|
deleted: false,
|
|
});
|
|
}
|
|
true
|
|
});
|
|
page.total = self.count_rows(&t);
|
|
Ok(page)
|
|
}
|
|
|
|
// ---------------------------------------------------------- registros apagados
|
|
|
|
/// Procura registros apagados nas áreas livres das páginas da tabela.
|
|
/// São "prováveis": o SQLite não marca o que foi apagado, então validamos pela forma
|
|
/// do registro (mesmo número de colunas, tipos e textos coerentes).
|
|
pub fn deleted_rows(&self, table: &str) -> Res<RowsPage> {
|
|
let t = self.table(table)?;
|
|
let n = t.columns.len();
|
|
let mut out = RowsPage {
|
|
columns: t.columns.iter().map(|c| c.name.clone()).collect(),
|
|
..Default::default()
|
|
};
|
|
if n < 2 || t.rootpage == 0 || t.virtual_table || t.without_rowid {
|
|
return Ok(out);
|
|
}
|
|
for leaf in self.leaf_pages(t.rootpage) {
|
|
let Ok(pg) = self.page(leaf) else { continue };
|
|
let hb = if leaf == 1 { 100 } else { 0 };
|
|
let ncell = be16(&pg, hb + 3).unwrap_or(0);
|
|
let content = match be16(&pg, hb + 5) {
|
|
Some(0) => 65536,
|
|
Some(c) => c,
|
|
None => continue,
|
|
};
|
|
let ptr_end = hb + 8 + ncell * 2;
|
|
let mut regions: Vec<(usize, usize)> = vec![];
|
|
if content > ptr_end {
|
|
regions.push((ptr_end, content));
|
|
}
|
|
// cadeia de blocos livres
|
|
let mut fb = be16(&pg, hb + 1).unwrap_or(0);
|
|
let mut guard = 0;
|
|
while fb != 0 && guard < 2000 {
|
|
guard += 1;
|
|
let (Some(next), Some(size)) = (be16(&pg, fb), be16(&pg, fb + 2)) else {
|
|
break;
|
|
};
|
|
if fb + size <= self.usable && size >= 4 {
|
|
regions.push((fb, fb + size));
|
|
}
|
|
fb = next;
|
|
}
|
|
for (a, b) in regions {
|
|
let end = b.min(pg.len()).min(self.usable);
|
|
if a >= end {
|
|
continue;
|
|
}
|
|
let mut p = a;
|
|
while p + 3 < end {
|
|
if let Some((mut cells, used)) = self.try_deleted(&pg[p..end], &t) {
|
|
apply_affinity(&mut cells, &t.columns);
|
|
out.rows.push(DbRow {
|
|
rowid: None,
|
|
cells,
|
|
deleted: true,
|
|
});
|
|
p += used.max(1);
|
|
} else {
|
|
p += 1;
|
|
}
|
|
}
|
|
}
|
|
if out.rows.len() >= 5000 {
|
|
break;
|
|
}
|
|
}
|
|
out.total = out.rows.len() as u64;
|
|
Ok(out)
|
|
}
|
|
|
|
/// Tenta ler um registro apagado no início de `b`, em três formas:
|
|
/// (a) cabeçalho íntegro; (b) tamanho do cabeçalho perdido (o SQLite sobrescreve os 4
|
|
/// primeiros bytes do bloco livre); (c) como (b), mas sem o primeiro tipo (coluna que é
|
|
/// alias do rowid, sempre NULL).
|
|
fn try_deleted(&self, b: &[u8], t: &DbTable) -> Option<(Vec<Cell>, usize)> {
|
|
let n = t.columns.len();
|
|
// (a)
|
|
if let Some(r) = self.record_with_types(b, 1, n, None, t) {
|
|
return Some(r);
|
|
}
|
|
// (b)
|
|
if let Some(r) = self.record_with_types(b, 0, n, None, t) {
|
|
return Some(r);
|
|
}
|
|
// (c)
|
|
if t.rowid_alias == Some(0) {
|
|
return self.record_with_types(b, 0, n - 1, Some(0), t);
|
|
}
|
|
None
|
|
}
|
|
|
|
/// Lê `ntypes` tipos seriais a partir de `b[start..]` (com um tipo inicial opcional já
|
|
/// conhecido) e o corpo logo depois do cabeçalho.
|
|
fn record_with_types(
|
|
&self,
|
|
b: &[u8],
|
|
start: usize,
|
|
ntypes: usize,
|
|
first: Option<i64>,
|
|
t: &DbTable,
|
|
) -> Option<(Vec<Cell>, usize)> {
|
|
let n = t.columns.len();
|
|
let mut types: Vec<i64> = first.into_iter().collect();
|
|
let mut o = start;
|
|
let mut hdr_bytes = 1usize; // o próprio byte do tamanho do cabeçalho
|
|
for _ in 0..ntypes {
|
|
let (ty, m) = varint(b.get(o..)?)?;
|
|
types.push(ty);
|
|
o += m;
|
|
hdr_bytes += m;
|
|
}
|
|
if first.is_some() {
|
|
hdr_bytes += 1;
|
|
}
|
|
if start == 1 {
|
|
// forma (a): o primeiro byte declara o tamanho do cabeçalho e precisa bater
|
|
if *b.first()? as usize != o {
|
|
return None;
|
|
}
|
|
}
|
|
if types.len() != n {
|
|
return None;
|
|
}
|
|
for (ty, col) in types.iter().zip(&t.columns) {
|
|
if !type_ok(&col.decl_type, *ty) {
|
|
return None;
|
|
}
|
|
}
|
|
let body: usize = types
|
|
.iter()
|
|
.map(|&x| serial_len(x))
|
|
.sum::<Option<usize>>()?;
|
|
if body == 0 || o + body > b.len() {
|
|
return None;
|
|
}
|
|
// monta um registro canônico para reaproveitar o decodificador
|
|
let mut rec = vec![];
|
|
let mut hdr = vec![];
|
|
for &ty in &types {
|
|
put_varint(&mut hdr, ty);
|
|
}
|
|
put_varint(&mut rec, (hdr.len() + 1) as i64);
|
|
rec.extend(hdr);
|
|
rec.extend_from_slice(&b[o..o + body]);
|
|
let cells = decode_record(&rec, self.enc)?;
|
|
let mut texts = 0;
|
|
for c in &cells {
|
|
if let Cell::Text(s) = c {
|
|
if s.contains('\u{FFFD}')
|
|
|| s.chars()
|
|
.any(|ch| ch.is_control() && !matches!(ch, '\n' | '\r' | '\t'))
|
|
{
|
|
return None;
|
|
}
|
|
texts += 1;
|
|
}
|
|
}
|
|
let non_null = cells.iter().filter(|c| !matches!(c, Cell::Null)).count();
|
|
if non_null < 2 || (texts == 0 && n < 3) {
|
|
return None;
|
|
}
|
|
let _ = hdr_bytes;
|
|
Some((cells, o + body))
|
|
}
|
|
}
|
|
|
|
/// O tipo serial é compatível com o tipo declarado da coluna (afinidade do SQLite)?
|
|
fn type_ok(decl: &str, ty: i64) -> bool {
|
|
if ty == 10 || ty == 11 || ty < 0 {
|
|
return false;
|
|
}
|
|
if ty == 0 {
|
|
return true;
|
|
}
|
|
let d = decl.to_uppercase();
|
|
let is_text = ty >= 13 && ty % 2 == 1;
|
|
let is_blob = ty >= 12 && ty % 2 == 0;
|
|
if d.contains("INT") {
|
|
ty <= 9 || is_text
|
|
} else if d.contains("CHAR") || d.contains("CLOB") || d.contains("TEXT") {
|
|
is_text || is_blob || ty <= 9
|
|
} else if d.contains("REAL") || d.contains("FLOA") || d.contains("DOUB") {
|
|
ty <= 9
|
|
} else {
|
|
true
|
|
}
|
|
}
|
|
|
|
fn put_varint(out: &mut Vec<u8>, v: i64) {
|
|
let v = v as u64;
|
|
if v <= 0x7F {
|
|
out.push(v as u8);
|
|
return;
|
|
}
|
|
let mut tmp = vec![];
|
|
let mut x = v;
|
|
while x > 0 {
|
|
tmp.push((x & 0x7F) as u8);
|
|
x >>= 7;
|
|
}
|
|
tmp.reverse();
|
|
let last = tmp.len() - 1;
|
|
for (i, b) in tmp.iter().enumerate() {
|
|
out.push(if i == last { *b } else { *b | 0x80 });
|
|
}
|
|
}
|
|
|
|
/// O SQLite guarda REAIS inteiros como inteiros; devolve-os como REAL quando a coluna é REAL.
|
|
fn apply_affinity(cells: &mut [Cell], cols: &[DbColumn]) {
|
|
for (c, col) in cells.iter_mut().zip(cols) {
|
|
let d = col.decl_type.to_uppercase();
|
|
if d.contains("REAL") || d.contains("FLOA") || d.contains("DOUB") {
|
|
if let Cell::Int(i) = c {
|
|
*c = Cell::Real(*i as f64);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// ------------------------------------------------------------------ CREATE TABLE
|
|
|
|
fn unquote(s: &str) -> String {
|
|
let t = s.trim();
|
|
let b = t.as_bytes();
|
|
if b.len() >= 2
|
|
&& matches!(
|
|
(b[0], b[b.len() - 1]),
|
|
(b'"', b'"') | (b'`', b'`') | (b'[', b']') | (b'\'', b'\'')
|
|
)
|
|
{
|
|
t[1..t.len() - 1].to_string()
|
|
} else {
|
|
t.to_string()
|
|
}
|
|
}
|
|
|
|
/// Extrai colunas, alias de rowid e WITHOUT ROWID de um `CREATE TABLE`.
|
|
fn parse_create(sql: &str) -> (Vec<DbColumn>, Option<usize>, bool) {
|
|
let upper = sql.to_uppercase();
|
|
let without_rowid = upper.trim_end().ends_with("WITHOUT ROWID");
|
|
let Some(open) = sql.find('(') else {
|
|
return (vec![], None, without_rowid);
|
|
};
|
|
// fecha o parêntese correspondente
|
|
let mut depth = 0;
|
|
let mut close = sql.len();
|
|
let mut in_q: Option<char> = None;
|
|
for (i, ch) in sql.char_indices().skip_while(|(i, _)| *i < open) {
|
|
match (in_q, ch) {
|
|
(Some(q), c) if c == q => in_q = None,
|
|
(Some(_), _) => {}
|
|
(None, '"' | '\'' | '`') => in_q = Some(ch),
|
|
(None, '(') => depth += 1,
|
|
(None, ')') => {
|
|
depth -= 1;
|
|
if depth == 0 {
|
|
close = i;
|
|
break;
|
|
}
|
|
}
|
|
_ => {}
|
|
}
|
|
}
|
|
let body = &sql[open + 1..close.min(sql.len())];
|
|
// separa por vírgulas de nível 0
|
|
let mut parts = vec![];
|
|
let (mut d, mut start, mut q) = (0, 0, None::<char>);
|
|
for (i, ch) in body.char_indices() {
|
|
match (q, ch) {
|
|
(Some(c), x) if c == x => q = None,
|
|
(Some(_), _) => {}
|
|
(None, '"' | '\'' | '`') => q = Some(ch),
|
|
(None, '(') => d += 1,
|
|
(None, ')') => d -= 1,
|
|
(None, ',') if d == 0 => {
|
|
parts.push(&body[start..i]);
|
|
start = i + 1;
|
|
}
|
|
_ => {}
|
|
}
|
|
}
|
|
parts.push(&body[start..]);
|
|
let mut cols = vec![];
|
|
let mut alias = None;
|
|
for part in parts {
|
|
let p = part.trim();
|
|
let up = p.to_uppercase();
|
|
if ["PRIMARY", "UNIQUE", "CHECK", "FOREIGN", "CONSTRAINT"]
|
|
.iter()
|
|
.any(|k| up.starts_with(k))
|
|
{
|
|
continue;
|
|
}
|
|
let (name, rest) = if p.starts_with(['"', '`', '[']) {
|
|
let close = match p.as_bytes()[0] {
|
|
b'[' => ']',
|
|
b'`' => '`',
|
|
_ => '"',
|
|
};
|
|
let end = p[1..].find(close).map(|e| e + 2).unwrap_or(p.len());
|
|
(unquote(&p[..end]), p[end..].trim())
|
|
} else {
|
|
match p.split_once(char::is_whitespace) {
|
|
Some((a, b)) => (a.to_string(), b.trim()),
|
|
None => (p.to_string(), ""),
|
|
}
|
|
};
|
|
if name.is_empty() {
|
|
continue;
|
|
}
|
|
let ru = rest.to_uppercase();
|
|
let decl: String = rest
|
|
.split_whitespace()
|
|
.take_while(|w| {
|
|
![
|
|
"PRIMARY",
|
|
"NOT",
|
|
"NULL",
|
|
"UNIQUE",
|
|
"DEFAULT",
|
|
"CHECK",
|
|
"REFERENCES",
|
|
"COLLATE",
|
|
"CONSTRAINT",
|
|
"GENERATED",
|
|
"AS",
|
|
]
|
|
.contains(&w.to_uppercase().as_str())
|
|
})
|
|
.collect::<Vec<_>>()
|
|
.join(" ");
|
|
let pk = ru.contains("PRIMARY KEY");
|
|
if pk && decl.eq_ignore_ascii_case("INTEGER") && !ru.contains("DESC") {
|
|
alias = Some(cols.len());
|
|
}
|
|
cols.push(DbColumn {
|
|
name,
|
|
decl_type: decl,
|
|
pk,
|
|
});
|
|
}
|
|
(cols, alias, without_rowid)
|
|
}
|
|
|
|
/// O arquivo começa com o cabeçalho de um banco SQLite?
|
|
pub fn is_sqlite(head: &[u8]) -> bool {
|
|
head.len() >= 16 && &head[0..16] == MAGIC
|
|
}
|