feat: Guará 0.1.0 — recuperação de arquivos (núcleo, CLI e interface gráfica)
Núcleo em Rust somente leitura: NTFS, FAT, exFAT, partições MBR/GPT e perdidas, carving por estrutura, fusão sem duplicatas, confiança por arquivo, imagem forense, pausar/retomar. CLI `guara`, UI web/Tauri, CI e releases por tag. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
commit
765cb0d4a2
57 files changed
+8621
No files matched your search
@@ -0,0 +1,183 @@
|
||||
//! Leitores de metadados de sistemas de arquivos (somente leitura).
|
||||
//!
|
||||
//! Cada leitor percorre estruturas do volume atrás de arquivos apagados,
|
||||
//! devolve os extents ainda alocados ("live") para o carving pular e, quando
|
||||
//! possível, os trechos livres.
|
||||
|
||||
pub mod exfat;
|
||||
pub mod ext;
|
||||
pub mod fat;
|
||||
pub mod ntfs;
|
||||
|
||||
use crate::signatures::{category_for_ext, matches_ext};
|
||||
use crate::source::{read_vec, Source};
|
||||
use crate::types::{Confidence, DataLoc, Extent, Found, Origin};
|
||||
use std::sync::atomic::{AtomicU64, Ordering};
|
||||
|
||||
/// Contexto compartilhado por todos os leitores.
|
||||
pub struct Ctx<'a> {
|
||||
pub src: &'a dyn Source,
|
||||
pub next_id: &'a AtomicU64,
|
||||
pub cancelled: &'a (dyn Fn() -> bool + Sync),
|
||||
/// (feito, total) dentro da fase atual do leitor.
|
||||
pub progress: &'a (dyn Fn(u64, u64) + Sync),
|
||||
pub emit: &'a mut dyn FnMut(Found),
|
||||
}
|
||||
|
||||
impl Ctx<'_> {
|
||||
pub fn id(&self) -> u64 {
|
||||
self.next_id.fetch_add(1, Ordering::Relaxed)
|
||||
}
|
||||
}
|
||||
|
||||
/// Resultado de varrer um volume.
|
||||
#[derive(Default)]
|
||||
pub struct VolumeScan {
|
||||
/// Extents (absolutos) ocupados por arquivos existentes e metadados.
|
||||
pub live: Vec<Extent>,
|
||||
/// Se o leitor conhece o mapa de espaço livre (ext), os trechos livres (absolutos).
|
||||
pub free: Option<Vec<Extent>>,
|
||||
pub warnings: Vec<String>,
|
||||
}
|
||||
|
||||
pub struct NewFound<'a> {
|
||||
pub name: String,
|
||||
pub dir: Option<String>,
|
||||
pub size: u64,
|
||||
pub origin: Origin,
|
||||
pub data: DataLoc,
|
||||
pub modified: Option<i64>,
|
||||
pub base: Confidence,
|
||||
pub note: Option<&'a str>,
|
||||
}
|
||||
|
||||
/// Monta um `Found`, ajustando a confiança pelo conteúdo real no disco.
|
||||
pub fn make_found(ctx: &Ctx, n: NewFound) -> Found {
|
||||
let ext = n
|
||||
.name
|
||||
.rsplit_once('.')
|
||||
.map(|(_, e)| e.to_ascii_lowercase())
|
||||
.filter(|e| e.len() <= 8 && e.chars().all(|c| c.is_ascii_alphanumeric()))
|
||||
.unwrap_or_default();
|
||||
let mut conf = n.base;
|
||||
let mut note = n.note.map(String::from);
|
||||
let head: Vec<u8> = match &n.data {
|
||||
DataLoc::Resident { bytes } => bytes.iter().take(32).copied().collect(),
|
||||
DataLoc::Extents { extents } => extents
|
||||
.iter()
|
||||
.find(|e| !e.sparse)
|
||||
.and_then(|e| read_vec(ctx.src, e.offset, 32.min(e.len as usize)).ok())
|
||||
.unwrap_or_default(),
|
||||
};
|
||||
if n.size > 0
|
||||
&& !head.is_empty()
|
||||
&& head.iter().all(|&b| b == 0)
|
||||
&& !matches!(n.data, DataLoc::Resident { .. })
|
||||
{
|
||||
conf = Confidence::Low;
|
||||
note = Some("início do arquivo zerado (apagado/TRIM ou sobrescrito)".into());
|
||||
} else {
|
||||
match matches_ext(&ext, &head) {
|
||||
Some(false) => {
|
||||
conf = Confidence::Low;
|
||||
note =
|
||||
Some("conteúdo não confere com a extensão: provavelmente sobrescrito".into());
|
||||
}
|
||||
Some(true) => {}
|
||||
None => conf = conf.min(Confidence::Medium),
|
||||
}
|
||||
}
|
||||
Found {
|
||||
id: ctx.id(),
|
||||
category: category_for_ext(&ext),
|
||||
ext,
|
||||
name: n.name,
|
||||
dir: n.dir,
|
||||
size: n.size,
|
||||
origin: n.origin,
|
||||
confidence: conf,
|
||||
data: n.data,
|
||||
modified: n.modified,
|
||||
note,
|
||||
}
|
||||
}
|
||||
|
||||
/// Ordena e funde extents adjacentes/sobrepostos.
|
||||
pub fn merge_extents(mut v: Vec<Extent>) -> Vec<Extent> {
|
||||
v.retain(|e| e.len > 0);
|
||||
v.sort_by_key(|e| e.offset);
|
||||
let mut out: Vec<Extent> = Vec::with_capacity(v.len());
|
||||
for e in v {
|
||||
if let Some(last) = out.last_mut() {
|
||||
if e.offset <= last.end() {
|
||||
if e.end() > last.end() {
|
||||
last.len = e.end() - last.offset;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
}
|
||||
out.push(e);
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// Há sobreposição entre [off, off+len) e algum extent de `sorted` (já mesclado)?
|
||||
pub fn overlaps(sorted: &[Extent], off: u64, len: u64) -> bool {
|
||||
let i = sorted.partition_point(|e| e.end() <= off);
|
||||
sorted.get(i).map(|e| e.offset < off + len).unwrap_or(false)
|
||||
}
|
||||
|
||||
pub fn dos_to_unix(date: u16, time: u16) -> Option<i64> {
|
||||
if date == 0 {
|
||||
return None;
|
||||
}
|
||||
let year = 1980 + (date >> 9) as i64;
|
||||
let month = ((date >> 5) & 0xF) as i64;
|
||||
let day = (date & 0x1F) as i64;
|
||||
if !(1..=12).contains(&month) || day == 0 {
|
||||
return None;
|
||||
}
|
||||
let (h, m, s) = (
|
||||
(time >> 11) as i64,
|
||||
((time >> 5) & 0x3F) as i64,
|
||||
((time & 0x1F) * 2) as i64,
|
||||
);
|
||||
Some(days_from_civil(year, month, day) * 86400 + h * 3600 + m * 60 + s)
|
||||
}
|
||||
|
||||
/// Dias desde 1970-01-01 (algoritmo de Howard Hinnant).
|
||||
pub fn days_from_civil(y: i64, m: i64, d: i64) -> i64 {
|
||||
let y = if m <= 2 { y - 1 } else { y };
|
||||
let era = if y >= 0 { y } else { y - 399 } / 400;
|
||||
let yoe = y - era * 400;
|
||||
let doy = (153 * (if m > 2 { m - 3 } else { m + 9 }) + 2) / 5 + d - 1;
|
||||
let doe = yoe * 365 + yoe / 4 - yoe / 100 + doy;
|
||||
era * 146097 + doe - 719468
|
||||
}
|
||||
|
||||
pub fn utf16le(bytes: &[u8]) -> String {
|
||||
let u: Vec<u16> = bytes
|
||||
.chunks_exact(2)
|
||||
.map(|c| u16::from_le_bytes([c[0], c[1]]))
|
||||
.collect();
|
||||
String::from_utf16_lossy(&u)
|
||||
}
|
||||
|
||||
pub fn sanitize_name(s: &str) -> String {
|
||||
let t: String = s
|
||||
.chars()
|
||||
.map(|c| {
|
||||
if c.is_control() || matches!(c, '/' | '\\' | ':' | '*' | '?' | '"' | '<' | '>' | '|') {
|
||||
'_'
|
||||
} else {
|
||||
c
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
let t = t.trim().trim_matches('.').to_string();
|
||||
if t.is_empty() {
|
||||
"arquivo".into()
|
||||
} else {
|
||||
t
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user