CI / Format and clippy (push) Successful in 1m57s
CI / Dependency audit (push) Successful in 13s
CI / Check desktop app (Linux) (push) Successful in 3m54s
CI / Tests (Linux) (push) Failing after 7m25s
Release / Create release (push) Successful in 6s
Release / CLI (linux-aarch64) (push) Successful in 1m48s
Release / CLI (linux-x86_64) (push) Successful in 2m39s
Release / Desktop app (linux-aarch64) (push) Successful in 10m9s
Release / Desktop app (linux-x86_64) (push) Successful in 12m20s
Release / Desktop app (windows) (push) Successful in 7m32s
Release / Build latest.json (push) Successful in 5s
Completes the English-only standardization for code, comments, scripts and CI templates (UI/CLI-facing strings stay bilingual pt-BR/en, untouched). Covers the remaining crates/, apps/gui/ui/ and apps/gui/src-tauri/ comments, .gitea/ workflow and template comments, .githooks/ hooks, and the scripts/ source comments for the files already renamed in a prior commit (check.sh, generate-logo.py, install-hooks.sh). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VAyr8d7HuAdUGB6aA5xmDD
154 lines
4.6 KiB
Rust
154 lines
4.6 KiB
Rust
//! End-to-end recovery tests across real filesystem images (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}"
|
|
);
|
|
}
|
|
// Combining metadata + signature carving must deduplicate findings at the same offset.
|
|
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}");
|
|
}
|
|
}
|
|
if fs != "ext4" {
|
|
for f in res.found.iter().filter(|f| f.origin != Origin::Carved) {
|
|
assert!(
|
|
f.modified.is_some() && f.created.is_some(),
|
|
"[{fs}] {} sem datas de modificação/criação",
|
|
f.name
|
|
);
|
|
}
|
|
}
|
|
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()
|
|
},
|
|
);
|
|
// Quick scan runs the ext4 metadata/journal reader without running signature carving.
|
|
assert!(res.found.iter().all(|f| f.origin != Origin::Carved));
|
|
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
|
|
);
|
|
}
|
|
|
|
/// Regression test: `mkfs.vfat -F 32` on a small image (`< 65525` clusters) is still detected as FAT32 via BPB.
|
|
#[test]
|
|
fn fat32_pequeno_com_poucos_clusters_e_reconhecido() {
|
|
use std::process::Command;
|
|
let dir = tempfile::tempdir().unwrap();
|
|
let img = dir.path().join("p.img");
|
|
let ok = Command::new("truncate")
|
|
.args(["-s", "32M"])
|
|
.arg(&img)
|
|
.status()
|
|
.map(|s| s.success())
|
|
.unwrap_or(false)
|
|
&& Command::new("mkfs.vfat")
|
|
.args(["-F", "32"])
|
|
.arg(&img)
|
|
.output()
|
|
.map(|o| o.status.success())
|
|
.unwrap_or(false);
|
|
let file = dir.path().join("arquivo.txt");
|
|
std::fs::write(&file, b"conteudo de teste\n".repeat(500)).unwrap();
|
|
let ok = ok
|
|
&& Command::new("mcopy")
|
|
.arg("-i")
|
|
.arg(&img)
|
|
.arg(&file)
|
|
.arg("::/")
|
|
.status()
|
|
.map(|s| s.success())
|
|
.unwrap_or(false)
|
|
&& Command::new("mdel")
|
|
.arg("-i")
|
|
.arg(&img)
|
|
.arg("::/arquivo.txt")
|
|
.status()
|
|
.map(|s| s.success())
|
|
.unwrap_or(false);
|
|
if !ok {
|
|
eprintln!("SKIP: mtools/dosfstools ausentes");
|
|
return;
|
|
}
|
|
let res = scan_path(
|
|
&img,
|
|
&ScanOptions {
|
|
mode: ScanMode::Quick,
|
|
..Default::default()
|
|
},
|
|
);
|
|
let layout = res.layout.unwrap();
|
|
assert_eq!(layout.partitions[0].fs, FsKind::Fat32);
|
|
assert_eq!(res.found.len(), 1);
|
|
let out = tempfile::tempdir().unwrap();
|
|
recover_all(&img, &res.found, out.path());
|
|
assert!(hashes_in(out.path()).contains_key(&sha256(&b"conteudo de teste\n".repeat(500))));
|
|
}
|