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:
Lucas RayanandClaude Sonnet 5.5 committed 2026-10-18 15:02:19 -03:00
commit 765cb0d4a2
57 files changed
+8621

No files matched your search

+87
View File
@@ -0,0 +1,87 @@
//! Imagem forense de um disco: leitura tolerante a setores ruins, com mapa de
//! erros e hash. Serve para trabalhar sempre sobre a cópia.
use crate::error::{Error, Result};
use crate::source::Source;
use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};
use std::io::Write;
use std::path::Path;
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct ImageReport {
pub bytes: u64,
pub sha256: String,
/// (offset, tamanho) dos trechos ilegíveis, preenchidos com zeros.
pub bad_ranges: Vec<(u64, u64)>,
pub cancelled: bool,
}
const BLOCK: usize = 1 << 20;
const SECTOR: usize = 512;
pub fn create_image(
src: &dyn Source,
dest: &Path,
cancelled: &(dyn Fn() -> bool + Sync),
progress: &mut dyn FnMut(u64, u64),
) -> Result<ImageReport> {
let mut out = std::fs::OpenOptions::new()
.write(true)
.create_new(true)
.open(dest)
.map_err(|e| {
Error::msg(format!(
"não foi possível criar {}: {e} (o arquivo não pode existir)",
dest.display()
))
})?;
let size = src.size();
let mut rep = ImageReport::default();
let mut hasher = Sha256::new();
let mut buf = vec![0u8; BLOCK];
let mut pos = 0u64;
while pos < size {
if cancelled() {
rep.cancelled = true;
break;
}
let want = (size - pos).min(BLOCK as u64) as usize;
match read_full(src, pos, &mut buf[..want]) {
Ok(()) => {}
Err(_) => {
// Refaz setor a setor para isolar os trechos ruins.
let mut o = 0;
while o < want {
let n = SECTOR.min(want - o);
if read_full(src, pos + o as u64, &mut buf[o..o + n]).is_err() {
buf[o..o + n].fill(0);
match rep.bad_ranges.last_mut() {
Some(l) if l.0 + l.1 == pos + o as u64 => l.1 += n as u64,
_ => rep.bad_ranges.push((pos + o as u64, n as u64)),
}
}
o += n;
}
}
}
out.write_all(&buf[..want])?;
hasher.update(&buf[..want]);
pos += want as u64;
progress(pos, size);
}
out.flush()?;
rep.bytes = pos;
rep.sha256 = hasher
.finalize()
.iter()
.map(|b| format!("{b:02x}"))
.collect();
let map = dest.with_extension("map.json");
std::fs::write(map, serde_json::to_vec_pretty(&rep)?)?;
Ok(rep)
}
fn read_full(src: &dyn Source, off: u64, buf: &mut [u8]) -> std::io::Result<()> {
crate::source::read_exact_at(src, off, buf)
}