Versão 0.5.0: bancos SQLite, saúde do disco, notificações, retomada, mapa GPS, inglês
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
4bb2397e3b
commit
bbcb42ae6e
27 files changed
+2681
-41
No files matched your search
@@ -0,0 +1,900 @@
|
||||
//! 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
|
||||
}
|
||||
Reference in new issue
Block a user