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,114 @@
|
||||
#![allow(dead_code)]
|
||||
use guara_core::scan::{self, Outcome, Progress, Sink};
|
||||
use guara_core::*;
|
||||
use sha2::{Digest, Sha256};
|
||||
use std::collections::HashMap;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::process::Command;
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct Collect {
|
||||
pub found: Vec<Found>,
|
||||
pub warnings: Vec<String>,
|
||||
pub layout: Option<DiskLayout>,
|
||||
}
|
||||
|
||||
impl Sink for Collect {
|
||||
fn found(&mut self, f: Found) {
|
||||
self.found.push(f);
|
||||
}
|
||||
fn confirm(&mut self, id: u64, c: Confidence) {
|
||||
if let Some(f) = self.found.iter_mut().find(|f| f.id == id) {
|
||||
f.confidence = f.confidence.max(c);
|
||||
}
|
||||
}
|
||||
fn progress(&mut self, _: Progress) {}
|
||||
fn warning(&mut self, w: String) {
|
||||
self.warnings.push(w);
|
||||
}
|
||||
fn layout(&mut self, l: &DiskLayout) {
|
||||
self.layout = Some(l.clone());
|
||||
}
|
||||
}
|
||||
|
||||
pub fn scan_path(path: &Path, opts: &ScanOptions) -> Collect {
|
||||
let src = source::open_path(path).unwrap();
|
||||
let mut sink = Collect::default();
|
||||
let out = scan::run(src.as_ref(), opts, None, &[], &|| false, &mut sink);
|
||||
assert!(matches!(out, Outcome::Completed));
|
||||
sink
|
||||
}
|
||||
|
||||
pub fn recover_all(path: &Path, found: &[Found], dest: &Path) -> recover::RecoverReport {
|
||||
let src = source::open_path(path).unwrap();
|
||||
let refs: Vec<&Found> = found.iter().collect();
|
||||
recover::recover(src.as_ref(), &refs, dest, &|| false, &mut |_, _, _| {}).unwrap()
|
||||
}
|
||||
|
||||
pub fn sha256_file(p: &Path) -> String {
|
||||
sha256(&std::fs::read(p).unwrap())
|
||||
}
|
||||
|
||||
pub fn sha256(b: &[u8]) -> String {
|
||||
Sha256::digest(b)
|
||||
.iter()
|
||||
.map(|x| format!("{x:02x}"))
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Gera (uma vez) as imagens de teste; `None` se as ferramentas não existirem.
|
||||
pub fn fixtures() -> Option<PathBuf> {
|
||||
static ONCE: std::sync::OnceLock<Option<PathBuf>> = std::sync::OnceLock::new();
|
||||
ONCE.get_or_init(fixtures_inner).clone()
|
||||
}
|
||||
|
||||
fn fixtures_inner() -> Option<PathBuf> {
|
||||
let root = Path::new(env!("CARGO_MANIFEST_DIR")).join("../..");
|
||||
let dir = root.join("target/fixtures");
|
||||
if dir.join("ext4.img").exists() && dir.join("expected-ntfs.txt").exists() {
|
||||
return Some(dir);
|
||||
}
|
||||
let ok = Command::new("bash")
|
||||
.arg(root.join("scripts/make-fixtures.sh"))
|
||||
.arg(&dir)
|
||||
.status()
|
||||
.map(|s| s.success())
|
||||
.unwrap_or(false);
|
||||
if ok {
|
||||
Some(dir)
|
||||
} else if std::env::var("GUARA_REQUIRE_FIXTURES").is_ok() {
|
||||
panic!("não foi possível gerar os fixtures");
|
||||
} else {
|
||||
eprintln!(
|
||||
"SKIP: ferramentas de fixture ausentes (mtools, ntfs-3g, e2fsprogs, imagemagick, zip)"
|
||||
);
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub fn expected(dir: &Path, fs: &str) -> Vec<(String, String)> {
|
||||
std::fs::read_to_string(dir.join(format!("expected-{fs}.txt")))
|
||||
.unwrap()
|
||||
.lines()
|
||||
.map(|l| {
|
||||
let (h, p) = l.split_once(' ').unwrap();
|
||||
(h.to_string(), p.to_string())
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn hashes_in(dir: &Path) -> HashMap<String, PathBuf> {
|
||||
let mut m = HashMap::new();
|
||||
fn walk(d: &Path, m: &mut HashMap<String, PathBuf>) {
|
||||
for e in std::fs::read_dir(d).unwrap().flatten() {
|
||||
let p = e.path();
|
||||
if p.is_dir() {
|
||||
walk(&p, m);
|
||||
} else if p.file_name().unwrap() != "guara-manifesto.json" {
|
||||
m.insert(sha256_file(&p), p);
|
||||
}
|
||||
}
|
||||
}
|
||||
walk(dir, &mut m);
|
||||
m
|
||||
}
|
||||
@@ -0,0 +1,233 @@
|
||||
//! Discos com tabela de partições, espaço não particionado, partições perdidas e cifragem.
|
||||
|
||||
mod common;
|
||||
use common::*;
|
||||
use guara_core::*;
|
||||
use std::io::Write;
|
||||
|
||||
const MIB: u64 = 1024 * 1024;
|
||||
|
||||
fn mbr(parts: &[(u8, u64, u64)]) -> Vec<u8> {
|
||||
let mut s = vec![0u8; 512];
|
||||
for (i, (ty, start, len)) in parts.iter().enumerate() {
|
||||
let e = &mut s[446 + i * 16..462 + i * 16];
|
||||
e[4] = *ty;
|
||||
e[8..12].copy_from_slice(&((start / 512) as u32).to_le_bytes());
|
||||
e[12..16].copy_from_slice(&((len / 512) as u32).to_le_bytes());
|
||||
}
|
||||
s[510] = 0x55;
|
||||
s[511] = 0xAA;
|
||||
s
|
||||
}
|
||||
|
||||
fn png(extra: usize) -> Vec<u8> {
|
||||
let mut v = vec![0x89, b'P', b'N', b'G', 0x0D, 0x0A, 0x1A, 0x0A];
|
||||
let chunk = |ty: &[u8; 4], data: &[u8]| {
|
||||
let mut c = (data.len() as u32).to_be_bytes().to_vec();
|
||||
c.extend_from_slice(ty);
|
||||
c.extend_from_slice(data);
|
||||
c.extend_from_slice(&[0, 0, 0, 0]);
|
||||
c
|
||||
};
|
||||
v.extend(chunk(b"IHDR", &[0, 0, 0, 8, 0, 0, 0, 8, 8, 2, 0, 0, 0]));
|
||||
v.extend(chunk(
|
||||
b"IDAT",
|
||||
&(0..extra).map(|i| (i * 31 % 251) as u8).collect::<Vec<_>>(),
|
||||
));
|
||||
v.extend(chunk(b"IEND", &[]));
|
||||
v
|
||||
}
|
||||
|
||||
/// Disco: [1 MiB vazio][partição FAT32 do fixture][8 MiB não particionado com um PNG]
|
||||
#[test]
|
||||
fn particao_mbr_e_espaco_nao_particionado() {
|
||||
let Some(dir) = fixtures() else { return };
|
||||
let fat = std::fs::read(dir.join("fat32.img")).unwrap();
|
||||
let plen = fat.len() as u64;
|
||||
let gap_start = MIB + plen;
|
||||
let mut disk = vec![0u8; (gap_start + 8 * MIB) as usize];
|
||||
disk[..512].copy_from_slice(&mbr(&[(0x0C, MIB, plen)]));
|
||||
disk[MIB as usize..(MIB + plen) as usize].copy_from_slice(&fat);
|
||||
let lost_png = png(40_000);
|
||||
let at = (gap_start + 3 * MIB) as usize;
|
||||
disk[at..at + lost_png.len()].copy_from_slice(&lost_png);
|
||||
let path = tempfile::NamedTempFile::new().unwrap();
|
||||
path.as_file().write_all(&disk).unwrap();
|
||||
|
||||
let res = scan_path(path.path(), &ScanOptions::default());
|
||||
let layout = res.layout.unwrap();
|
||||
assert_eq!(layout.table, "mbr");
|
||||
assert_eq!(layout.partitions.len(), 1);
|
||||
assert_eq!(layout.partitions[0].fs, FsKind::Fat32);
|
||||
assert_eq!(layout.partitions[0].start, MIB);
|
||||
assert!(
|
||||
layout.gaps.iter().any(|g| g.start == gap_start),
|
||||
"gap: {:?}",
|
||||
layout.gaps
|
||||
);
|
||||
// Metadados da partição (offsets corrigidos pela posição da partição)...
|
||||
let fat_found: Vec<_> = res
|
||||
.found
|
||||
.iter()
|
||||
.filter(|f| f.origin == Origin::Fat)
|
||||
.collect();
|
||||
assert_eq!(fat_found.len(), 7);
|
||||
// ... e o PNG no espaço não particionado.
|
||||
let carved: Vec<_> = res
|
||||
.found
|
||||
.iter()
|
||||
.filter(|f| f.origin == Origin::Carved && f.ext == "png")
|
||||
.collect();
|
||||
assert_eq!(carved.len(), 1);
|
||||
assert_eq!(carved[0].first_offset(), Some(gap_start + 3 * MIB));
|
||||
let out = tempfile::tempdir().unwrap();
|
||||
recover_all(path.path(), &res.found, out.path());
|
||||
assert!(hashes_in(out.path()).contains_key(&sha256(&lost_png)));
|
||||
for (h, p) in expected(&dir, "fat32") {
|
||||
assert!(hashes_in(out.path()).contains_key(&h), "faltou {p}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn particao_perdida_e_encontrada_sem_tabela() {
|
||||
let Some(dir) = fixtures() else { return };
|
||||
let ntfs = std::fs::read(dir.join("ntfs.img")).unwrap();
|
||||
let off = 5 * MIB;
|
||||
let mut disk = vec![0u8; (off as usize) + ntfs.len() + MIB as usize * 2];
|
||||
disk[off as usize..off as usize + ntfs.len()].copy_from_slice(&ntfs);
|
||||
let path = tempfile::NamedTempFile::new().unwrap();
|
||||
path.as_file().write_all(&disk).unwrap();
|
||||
let res = scan_path(path.path(), &ScanOptions::default());
|
||||
let layout = res.layout.unwrap();
|
||||
let lost: Vec<_> = layout.partitions.iter().filter(|p| p.lost).collect();
|
||||
assert_eq!(lost.len(), 1, "{layout:?}");
|
||||
assert_eq!(lost[0].fs, FsKind::Ntfs);
|
||||
assert_eq!(lost[0].start, off);
|
||||
assert_eq!(
|
||||
res.found
|
||||
.iter()
|
||||
.filter(|f| f.origin == Origin::Ntfs)
|
||||
.count(),
|
||||
7
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn volume_cifrado_e_detectado_e_nao_varrido() {
|
||||
let mut disk = vec![0u8; 8 * MIB as usize];
|
||||
disk[..512].copy_from_slice(&mbr(&[(0x07, MIB, 4 * MIB), (0x83, 5 * MIB, 2 * MIB)]));
|
||||
// BitLocker na partição 1, LUKS na 2.
|
||||
disk[MIB as usize + 3..MIB as usize + 11].copy_from_slice(b"-FVE-FS-");
|
||||
let l = 5 * MIB as usize;
|
||||
disk[l..l + 6].copy_from_slice(b"LUKS\xBA\xBE");
|
||||
// Um PNG "dentro" do volume cifrado não deve ser tocado.
|
||||
let p = png(10_000);
|
||||
let at = (2 * MIB) as usize;
|
||||
disk[at..at + p.len()].copy_from_slice(&p);
|
||||
let path = tempfile::NamedTempFile::new().unwrap();
|
||||
path.as_file().write_all(&disk).unwrap();
|
||||
let res = scan_path(path.path(), &ScanOptions::default());
|
||||
let layout = res.layout.unwrap();
|
||||
assert_eq!(layout.partitions[0].encrypted.as_deref(), Some("BitLocker"));
|
||||
assert_eq!(layout.partitions[1].encrypted.as_deref(), Some("LUKS"));
|
||||
assert!(res.warnings.iter().any(|w| w.contains("BitLocker")));
|
||||
assert!(
|
||||
res.found.is_empty(),
|
||||
"não deve varrer volume cifrado: {:?}",
|
||||
res.found.len()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn gpt_e_lido() {
|
||||
// GPT mínimo com 1 partição de dados.
|
||||
let mut disk = vec![0u8; 8 * MIB as usize];
|
||||
disk[..512].copy_from_slice(&mbr(&[(0xEE, 512, 8 * MIB - 512)]));
|
||||
let mut h = vec![0u8; 512];
|
||||
h[0..8].copy_from_slice(b"EFI PART");
|
||||
h[72..80].copy_from_slice(&2u64.to_le_bytes());
|
||||
h[80..84].copy_from_slice(&128u32.to_le_bytes());
|
||||
h[84..88].copy_from_slice(&128u32.to_le_bytes());
|
||||
disk[512..1024].copy_from_slice(&h);
|
||||
let e = &mut disk[1024..1024 + 128];
|
||||
e[0..16].copy_from_slice(&[
|
||||
0xA2, 0xA0, 0xD0, 0xEB, 0xE5, 0xB9, 0x33, 0x44, 0x87, 0xC0, 0x68, 0xB6, 0xB7, 0x26, 0x99,
|
||||
0xC7,
|
||||
]);
|
||||
e[32..40].copy_from_slice(&(MIB / 512).to_le_bytes());
|
||||
e[40..48].copy_from_slice(&((5 * MIB) / 512 - 1).to_le_bytes());
|
||||
let name: Vec<u8> = "Dados"
|
||||
.encode_utf16()
|
||||
.flat_map(|c| c.to_le_bytes())
|
||||
.collect();
|
||||
e[56..56 + name.len()].copy_from_slice(&name);
|
||||
let path = tempfile::NamedTempFile::new().unwrap();
|
||||
path.as_file().write_all(&disk).unwrap();
|
||||
let src = source::open_path(path.path()).unwrap();
|
||||
let l = partition::analyze(src.as_ref(), false);
|
||||
assert_eq!(l.table, "gpt");
|
||||
assert_eq!(l.partitions.len(), 1);
|
||||
assert_eq!(l.partitions[0].start, MIB);
|
||||
assert_eq!(l.partitions[0].label.as_deref(), Some("Dados"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn imagem_forense_e_identica_e_origem_intacta() {
|
||||
let Some(dir) = fixtures() else { return };
|
||||
let src_path = dir.join("fat32.img");
|
||||
let src = source::open_path(&src_path).unwrap();
|
||||
let out = tempfile::tempdir().unwrap();
|
||||
let dest = out.path().join("copia.img");
|
||||
let rep = image::create_image(src.as_ref(), &dest, &|| false, &mut |_, _| {}).unwrap();
|
||||
assert!(rep.bad_ranges.is_empty());
|
||||
assert_eq!(rep.sha256, sha256_file(&src_path));
|
||||
assert_eq!(sha256_file(&dest), sha256_file(&src_path));
|
||||
// Não sobrescreve arquivo existente.
|
||||
assert!(image::create_image(src.as_ref(), &dest, &|| false, &mut |_, _| {}).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pausa_e_retoma_encontra_o_mesmo() {
|
||||
let Some(dir) = fixtures() else { return };
|
||||
let path = dir.join("ext4.img");
|
||||
let full = scan_path(&path, &ScanOptions::default());
|
||||
let src = source::open_path(&path).unwrap();
|
||||
// Cancela logo na 3ª consulta, retoma do checkpoint.
|
||||
let n = std::sync::atomic::AtomicUsize::new(0);
|
||||
let cancel = || n.fetch_add(1, std::sync::atomic::Ordering::Relaxed) == 2;
|
||||
let mut sink = Collect::default();
|
||||
let out = scan::run(
|
||||
src.as_ref(),
|
||||
&ScanOptions::default(),
|
||||
None,
|
||||
&[],
|
||||
&cancel,
|
||||
&mut sink,
|
||||
);
|
||||
let scan::Outcome::Interrupted(cp) = out else {
|
||||
panic!("deveria ter pausado")
|
||||
};
|
||||
let before = sink.found.clone();
|
||||
let out = scan::run(
|
||||
src.as_ref(),
|
||||
&ScanOptions::default(),
|
||||
Some(cp),
|
||||
&before,
|
||||
&|| false,
|
||||
&mut sink,
|
||||
);
|
||||
assert!(matches!(out, scan::Outcome::Completed));
|
||||
let mut a: Vec<_> = full
|
||||
.found
|
||||
.iter()
|
||||
.map(|f| (f.first_offset(), f.size))
|
||||
.collect();
|
||||
let mut b: Vec<_> = sink
|
||||
.found
|
||||
.iter()
|
||||
.map(|f| (f.first_offset(), f.size))
|
||||
.collect();
|
||||
a.sort();
|
||||
b.sort();
|
||||
assert_eq!(a, b);
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
//! exFAT: injeta um conjunto de entradas APAGADO num sistema criado por mkfs.exfat.
|
||||
|
||||
mod common;
|
||||
use common::*;
|
||||
use guara_core::*;
|
||||
use std::process::Command;
|
||||
|
||||
#[test]
|
||||
fn exfat_apagado_e_recuperado_com_nome() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let img = dir.path().join("e.img");
|
||||
let ok = Command::new("truncate")
|
||||
.args(["-s", "32M"])
|
||||
.arg(&img)
|
||||
.status()
|
||||
.map(|s| s.success())
|
||||
.unwrap_or(false)
|
||||
&& Command::new("mkfs.exfat")
|
||||
.arg(&img)
|
||||
.output()
|
||||
.map(|o| o.status.success())
|
||||
.unwrap_or(false);
|
||||
if !ok {
|
||||
eprintln!("SKIP: mkfs.exfat ausente");
|
||||
return;
|
||||
}
|
||||
let mut d = std::fs::read(&img).unwrap();
|
||||
let ss = 1usize << d[108];
|
||||
let cs = ss << d[109];
|
||||
let heap = u32::from_le_bytes(d[88..92].try_into().unwrap()) as usize * ss;
|
||||
let root = u32::from_le_bytes(d[96..100].try_into().unwrap()) as usize;
|
||||
let root_off = heap + (root - 2) * cs;
|
||||
// Primeira entrada livre da raiz.
|
||||
let mut slot = root_off;
|
||||
while d[slot] != 0 {
|
||||
slot += 32;
|
||||
}
|
||||
let content: Vec<u8> = (0..70_000u32)
|
||||
.map(|i| (i.wrapping_mul(2654435761) >> 13) as u8)
|
||||
.collect();
|
||||
let first_cluster = 300u32;
|
||||
let data_off = heap + (first_cluster as usize - 2) * cs;
|
||||
d[data_off..data_off + content.len()].copy_from_slice(&content);
|
||||
let name = "relatório final.txt";
|
||||
let units: Vec<u16> = name.encode_utf16().collect();
|
||||
// 0x05 = arquivo apagado (0x85 sem o bit "em uso"), secundárias 0x40 e 0x41.
|
||||
let n_name = units.len().div_ceil(15);
|
||||
d[slot] = 0x05;
|
||||
d[slot + 1] = (1 + n_name) as u8;
|
||||
d[slot + 12..slot + 14].copy_from_slice(&0x6000u16.to_le_bytes()); // hora
|
||||
d[slot + 14..slot + 16].copy_from_slice(&((44u16 << 9) | (6 << 5) | 15).to_le_bytes()); // 2024-06-15
|
||||
let st = slot + 32;
|
||||
d[st] = 0x40;
|
||||
d[st + 1] = 0x03; // alocação possível + sem cadeia na FAT
|
||||
d[st + 3] = units.len() as u8;
|
||||
d[st + 20..st + 24].copy_from_slice(&first_cluster.to_le_bytes());
|
||||
d[st + 24..st + 32].copy_from_slice(&(content.len() as u64).to_le_bytes());
|
||||
d[st + 8..st + 16].copy_from_slice(&(content.len() as u64).to_le_bytes());
|
||||
for k in 0..n_name {
|
||||
let e = slot + 64 + k * 32;
|
||||
d[e] = 0x41;
|
||||
for (j, u) in units.iter().skip(k * 15).take(15).enumerate() {
|
||||
d[e + 2 + j * 2..e + 4 + j * 2].copy_from_slice(&u.to_le_bytes());
|
||||
}
|
||||
}
|
||||
std::fs::write(&img, &d).unwrap();
|
||||
|
||||
let res = scan_path(
|
||||
&img,
|
||||
&ScanOptions {
|
||||
mode: ScanMode::Quick,
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
assert!(res.warnings.is_empty(), "{:?}", res.warnings);
|
||||
assert_eq!(res.found.len(), 1);
|
||||
let f = &res.found[0];
|
||||
assert_eq!(f.name, "relatório final.txt");
|
||||
assert_eq!(f.origin, Origin::Exfat);
|
||||
assert_eq!(f.size, content.len() as u64);
|
||||
assert!(f.modified.is_some());
|
||||
let out = tempfile::tempdir().unwrap();
|
||||
recover_all(&img, &res.found, out.path());
|
||||
assert!(hashes_in(out.path()).contains_key(&sha256(&content)));
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
//! Recuperação de ponta a ponta sobre imagens reais (FAT32, NTFS, ext4).
|
||||
|
||||
mod common;
|
||||
use common::*;
|
||||
use guara_core::*;
|
||||
|
||||
fn check(fs: &str, only_carvable: bool) {
|
||||
let Some(dir) = fixtures() else { return };
|
||||
let img = dir.join(format!("{fs}.img"));
|
||||
let before = sha256_file(&img);
|
||||
let res = scan_path(&img, &ScanOptions::default());
|
||||
let out = tempfile::tempdir().unwrap();
|
||||
let rep = recover_all(&img, &res.found, out.path());
|
||||
assert!(rep.failed.is_empty(), "falhas: {:?}", rep.failed);
|
||||
let got = hashes_in(out.path());
|
||||
for (hash, path) in expected(&dir, fs) {
|
||||
let carvable = [".jpg", ".png", ".pdf", ".docx", ".db"]
|
||||
.iter()
|
||||
.any(|e| path.ends_with(e));
|
||||
if only_carvable && !carvable {
|
||||
continue;
|
||||
}
|
||||
assert!(
|
||||
got.contains_key(&hash),
|
||||
"[{fs}] não recuperado bit a bit: {path}"
|
||||
);
|
||||
}
|
||||
// Os dois métodos juntos não podem listar o mesmo conteúdo duas vezes.
|
||||
let mut seen = std::collections::HashSet::new();
|
||||
for f in &res.found {
|
||||
if let Some(o) = f.first_offset() {
|
||||
assert!(seen.insert(o), "[{fs}] achado duplicado no offset {o}");
|
||||
}
|
||||
}
|
||||
assert_eq!(before, sha256_file(&img), "a origem foi modificada!");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fat32_recupera_apagados_com_nome_e_pasta() {
|
||||
check("fat32", false);
|
||||
let Some(dir) = fixtures() else { return };
|
||||
let res = scan_path(&dir.join("fat32.img"), &ScanOptions::default());
|
||||
assert!(res
|
||||
.found
|
||||
.iter()
|
||||
.any(|f| f.dir.as_deref() == Some("Documentos/Trabalho") && f.name == "relatorio.pdf"));
|
||||
assert!(
|
||||
res.found.iter().all(|f| f.name != "mantida.jpg"),
|
||||
"arquivo existente não deve ser listado"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ntfs_recupera_apagados() {
|
||||
check("ntfs", false);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ext4_recupera_por_assinatura() {
|
||||
check("ext4", true);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn varredura_rapida_nao_faz_carving() {
|
||||
let Some(dir) = fixtures() else { return };
|
||||
let res = scan_path(
|
||||
&dir.join("ext4.img"),
|
||||
&ScanOptions {
|
||||
mode: ScanMode::Quick,
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
assert!(res.found.is_empty());
|
||||
let res = scan_path(
|
||||
&dir.join("ntfs.img"),
|
||||
&ScanOptions {
|
||||
mode: ScanMode::Quick,
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
assert_eq!(
|
||||
res.found
|
||||
.iter()
|
||||
.filter(|f| f.origin == Origin::Ntfs)
|
||||
.count(),
|
||||
7
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user