chore(i18n): finish translating code comments and scripts to English
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
This commit is contained in:
Lucas RayanandClaude Sonnet 5 committed 2026-10-30 20:43:02 -03:00
1 parent da212e6926
commit 6328220c60
129 files changed
+1961 -1834

No files matched your search

+3 -3
View File
@@ -1,4 +1,4 @@
//! AFF4: imagens de referência da Evimetry (github.com/aff4/ReferenceImages) e ida e volta.
//! AFF4: Evimetry reference images (`github.com/aff4/ReferenceImages`) and write/read roundtrip.
use guara_core::aff4::{write_aff4, Aff4Options, Aff4Source};
use guara_core::source::{open_path, read_vec, MemSource, Source};
@@ -28,11 +28,11 @@ fn reads_reference_images_and_stored_hash_matches() {
Some(true),
"{name}: hash do fluxo"
);
// setor 0 legível pelo caminho genérico
// sector 0 is readable through the generic `source::open` entrypoint
let s = open_path(&p).unwrap();
let head = read_vec(s.as_ref(), 0, 4096).unwrap();
assert_eq!(head.len(), 4096);
// leitura em vãos do mapa devolve zeros
// reading unmapped gaps in a sparse AFF4 map returns zeros
let far = read_vec(s.as_ref(), 200 << 20, 512).unwrap();
assert_eq!(far.len(), 512);
eprintln!(
+11 -11
View File
@@ -1,4 +1,4 @@
//! APFS: imagem real do dfvfs (4 checkpoints) e exclusão simulada pela troca da raiz da árvore.
//! APFS: real `dfvfs` image (4 checkpoints) and simulated COW deletion by swapping the fs-tree root.
mod common;
use common::*;
@@ -69,7 +69,7 @@ fn quick_scan_is_clean_on_pristine_volume() {
);
}
// --- manipulação bruta de nós (independente do módulo) ---
// --- raw B-tree node helpers (independent of the production parser) ---
fn le16(b: &[u8], o: usize) -> usize {
u16::from_le_bytes([b[o], b[o + 1]]) as usize
@@ -144,7 +144,7 @@ fn file_removed_from_the_current_tree_survives_in_the_old_node() {
let mut d = std::fs::read(&p).unwrap();
let bs = 4096usize;
let blk = |d: &[u8], n: u64| d[n as usize * bs..(n as usize + 1) * bs].to_vec();
// Superbloco mais recente do contêiner.
// Locate the latest container superblock (`NXSB`).
let (desc_blocks, desc_base) = (le32(&d, 104) as u64, le64(&d, 112));
let mut nx = d[..bs].to_vec();
for i in 0..desc_blocks {
@@ -153,11 +153,11 @@ fn file_removed_from_the_current_tree_survives_in_the_old_node() {
nx = b;
}
}
// omap do contêiner -> volume
// container omap -> volume superblock (`APSB`)
let cont_omap = blk(&d, le64(&nx, 160));
let cont_tree = blk(&d, le64(&cont_omap, 48));
let (_, cont_entries) = read_node(&{
// omap usa chaves fixas (16/16): reaproveita o leitor variável convertendo a tabela
// omap uses fixed-size keys/values (16/16): convert its TOC to variable-size format for `read_var_node`
let mut c = cont_tree.clone();
fixed_to_var(&mut c);
c
@@ -187,7 +187,7 @@ fn file_removed_from_the_current_tree_survives_in_the_old_node() {
let root = blk(&d, root_paddr);
assert_eq!(le16(&root, 34), 0, "raiz deveria ser folha nesta imagem");
let (_, recs) = read_node(&root);
// inode do arquivo
// find target file inode number
let victim = b"passwords.txt";
let ino = recs
.iter()
@@ -216,12 +216,12 @@ fn file_removed_from_the_current_tree_survives_in_the_old_node() {
.collect();
assert!(kept.len() < recs.len());
let newnode = write_node(&root, &kept);
// bloco livre para a nova versão
// pick an unused block for the new COW root leaf
let free = (10..d.len() / bs)
.find(|&n| d[n * bs..(n + 1) * bs].iter().all(|&x| x == 0) && n != vol_tree_paddr as usize)
.expect("bloco livre");
d[free * bs..(free + 1) * bs].copy_from_slice(&newnode);
// aponta o mapa de objetos para a nova versão (mesmo xid), editando o valor da entrada
// repoint the volume omap entry to `free_blk` (keeping the same `xid`)
let toc_off = le16(&vt_orig, 40);
let key_base = 56 + toc_off + le16(&vt_orig, 42);
let _ = key_base;
@@ -236,7 +236,7 @@ fn file_removed_from_the_current_tree_survives_in_the_old_node() {
let dir = tempfile::tempdir().unwrap();
let p2 = dir.path().join("a.img");
std::fs::write(&p2, &d).unwrap();
// O arquivo sumiu da árvore atual...
// The file is gone from the current live tree...
let src = FileSource::open(&p2).unwrap();
assert!(!live_paths(&src, 0).iter().any(|f| f.0 == "passwords.txt"));
init();
@@ -275,7 +275,7 @@ fn file_removed_from_the_current_tree_survives_in_the_old_node() {
);
}
/// Converte a tabela de entradas fixas (4 bytes) de um nó de omap para o formato variável (8 bytes) em memória.
/// Converts a fixed-entry TOC (4 bytes/entry) of an omap node into variable-entry format (8 bytes/entry) in memory.
fn fixed_to_var(b: &mut Vec<u8>) {
let flags = le16(b, 32) as u16;
if flags & 4 == 0 {
@@ -302,7 +302,7 @@ fn fixed_to_var(b: &mut Vec<u8>) {
fn write_node_flagsless(old: &[u8], entries: &[Entry]) -> Vec<u8> {
let mut b = write_node(old, entries);
// write_node usa as flags de `old`
// `write_node` preserves the node flags from `old`
let fl = le16(&b, 32) as u16;
b[32..34].copy_from_slice(&fl.to_le_bytes());
b
+3 -3
View File
@@ -1,4 +1,4 @@
//! Trilha de auditoria: encadeamento por hash, detecção de adulteração e registro das ações do motor.
//! Audit trail: SHA-256 hash chaining, tamper detection, and engine action logging.
mod common;
use common::*;
@@ -28,7 +28,7 @@ fn entries_are_numbered_and_chained() {
assert!(e[0].time.ends_with('Z') && e[0].time.len() == 20);
let v = log.verify().unwrap();
assert!(v.ok && v.entries == 4 && v.head == e[3].hash);
// continua de onde parou, inclusive numa instância nova
// continues the chain across separate `Log` instances
let again = Log::at(log.path());
assert_eq!(again.append("mais", json!({})).unwrap().seq, 5);
assert!(again.verify().unwrap().ok);
@@ -74,7 +74,7 @@ fn removing_or_reordering_lines_is_detected() {
tamper(&log, |l| l.swap(1, 2));
assert_eq!(log.verify().unwrap().broken_at.unwrap().0, 2);
// trocar o hash por outro também não passa: o seguinte deixa de continuar
// recomputing a single tampered line's hash still breaks the next entry's `prev_hash` link
let (_d, log) = log_with(3);
tamper(&log, |l| {
let h = l[0].rsplit('"').nth(1).unwrap().to_string();
+4 -4
View File
@@ -1,5 +1,5 @@
//! BitLocker: imagens reais do conjunto de testes do cryptsetup (GitLab), com senhas conhecidas.
//! Baixe com `scripts/fetch-testdata.sh`; sem elas os testes são pulados.
//! BitLocker: real test images from the `cryptsetup` test suite (with known passwords/recovery keys).
//! Download with `scripts/fetch-testdata.sh`; tests are skipped if images are absent.
use guara_core::crypt::{bitlocker, Secret, UnlockError};
use guara_core::partition::detect_fs_in;
@@ -31,7 +31,7 @@ fn conf(d: &std::path::Path) -> BTreeMap<String, BTreeMap<String, String>> {
}
fn check_fs(src: &dyn Source, what: &str) {
// Imagens "encrypt-on-write" não têm sistema de arquivos reconhecível (UUID vazio no images.conf).
// Used-space-only ("encrypt-on-write") images in `images.conf` have an empty filesystem UUID.
if what.contains("-eow ") {
return;
}
@@ -59,7 +59,7 @@ fn opens_all_reference_images_with_password_and_recovery_key() {
let src: Arc<dyn Source> = Arc::new(FileSource::open(&path).unwrap());
let pw = c.get("PW").cloned().unwrap_or_default();
let rp = c.get("RP").cloned().unwrap_or_default();
// Proteção suspensa (chave limpa): abre sem segredo nenhum.
// Suspended protection (clear key present on disk): unlocks without any user secret.
if let Ok(u) = bitlocker::unlock(src.clone(), &[]) {
check_fs(u.source.as_ref(), &format!("{name} (chave limpa)"));
eprintln!("ok {name}: chave limpa");
+15 -15
View File
@@ -1,5 +1,5 @@
//! Btrfs: imagens criadas com mkfs.btrfs --rootdir; exclusão simulada como o COW faz
//! (nova versão da folha em outro bloco, a antiga continua no disco).
//! Btrfs: images created with `mkfs.btrfs --rootdir`; COW deletion simulated by writing a
//! new fs-tree leaf generation to a free metadata block while leaving the old leaf on disk.
mod common;
use common::*;
@@ -89,7 +89,7 @@ fn lists_the_live_tree_with_paths_and_sizes() {
assert!(listed.iter().any(|x| x.0 == "docs/sub" && x.2));
}
// --- manipulação bruta de folhas (independente do módulo) ---
// --- raw B-tree leaf manipulation helpers (independent of the production reader) ---
struct Leaf {
raw: Vec<u8>,
@@ -118,7 +118,7 @@ impl Leaf {
let c = crc32c(&self.raw[32..]);
self.raw[0..4].copy_from_slice(&c.to_le_bytes());
}
/// Nova versão da folha sem os itens que `drop` rejeita, num novo endereço lógico.
/// Rebuilds a B-tree leaf without items matching `drop`, at a new logical `bytenr` and generation.
fn without(&self, drop: impl Fn(u64, u8) -> bool, new_bytenr: u64) -> Leaf {
let mut kept: Vec<(usize, Vec<u8>)> = vec![];
for i in 0..self.nritems() {
@@ -152,7 +152,7 @@ fn superblock(img: &[u8]) -> &[u8] {
&img[0x10000..0x11000]
}
/// Mapa lógico -> físico dos chunks de sistema (suficiente para imagens pequenas de um dispositivo).
/// Logical -> physical chunk mapping from `sys_chunk_array` (sufficient for small single-device images).
fn chunk_map(img: &[u8]) -> Vec<(u64, u64, u64)> {
let sb = superblock(img);
let n = u32::from_le_bytes(sb[160..164].try_into().unwrap()) as usize;
@@ -193,7 +193,7 @@ fn deleted_file_survives_in_the_previous_version_of_the_leaf() {
let fl = Leaf {
raw: img[fs_leaf.0..fs_leaf.0 + nodesize].to_vec(),
};
// inode do arquivo apagado
// find the objectid (inode) of the target file
let victim = "relatorio.dat";
let mut ino = 0;
for k in 0..fl.nritems() {
@@ -207,7 +207,7 @@ fn deleted_file_survives_in_the_previous_version_of_the_leaf() {
}
}
assert!(ino > 256);
// extent de dados do arquivo (para liberar no extent tree)
// locate the file's data extent `(disk_bytenr, disk_num_bytes)` so we can free it in the extent tree
let mut data_bytenr = 0;
for k in 0..fl.nritems() {
let (obj, ty, _, _, _) = fl.item(k);
@@ -216,7 +216,7 @@ fn deleted_file_survives_in_the_previous_version_of_the_leaf() {
}
}
assert!(data_bytenr > 0);
// Novo bloco livre para a nova versão: primeiro bloco do metadata sem folha válida e todo zero.
// Pick an all-zero metadata block for the new COW leaf generation.
let free_slot = (0..img.len() / nodesize)
.map(|n| n * nodesize)
.find(|&o| {
@@ -228,11 +228,11 @@ fn deleted_file_survives_in_the_previous_version_of_the_leaf() {
&& o < fs_leaf.0 + (8 << 20)
})
.expect("bloco livre");
// logical = bytenr da folha antiga + (offset físico - offset físico antigo): mesmo chunk
// logical `bytenr` within the same metadata chunk
let new_bytenr = to_logical(free_slot);
let newleaf = fl.without(|o, _| o == ino, new_bytenr);
img[free_slot..free_slot + nodesize].copy_from_slice(&newleaf.raw);
// Aponta o ROOT_ITEM da árvore 5 para a nova versão (e sobe a geração), na folha da árvore raiz.
// Update tree 5 (`FS_TREE`) `ROOT_ITEM` in the root tree to point to the new leaf and bump generation.
let mut rl = Leaf {
raw: img[root_leaf.0..root_leaf.0 + nodesize].to_vec(),
};
@@ -245,7 +245,7 @@ fn deleted_file_survives_in_the_previous_version_of_the_leaf() {
}
rl.seal();
img[root_leaf.0..root_leaf.0 + nodesize].copy_from_slice(&rl.raw);
// Libera o extent de dados no extent tree (em outro endereço da mesma folha).
// Remove the file's data extent from the extent tree so its data blocks are marked free.
let el = Leaf {
raw: img[ext_leaf.0..ext_leaf.0 + nodesize].to_vec(),
};
@@ -290,7 +290,7 @@ fn deleted_file_survives_in_the_previous_version_of_the_leaf() {
let rp = PathBuf::from(&rep.recovered[0].path);
let got = std::fs::read(if rp.is_absolute() { rp } else { out.join(rp) }).unwrap();
assert!(got == i.files[0].1);
// Os arquivos que continuam existindo não aparecem como apagados.
// Files that are still live in the current tree must not be reported as deleted.
assert!(!c
.found
.iter()
@@ -300,7 +300,7 @@ fn deleted_file_survives_in_the_previous_version_of_the_leaf() {
#[test]
fn lzo_literals_and_a_match() {
// 20 literais + fim
// 20 literals + EOF marker
let mut s = vec![17 + 20];
s.extend((0..20u8).map(|i| b'a' + i));
s.extend_from_slice(&[0x11, 0, 0]);
@@ -308,7 +308,7 @@ fn lzo_literals_and_a_match() {
lzo1x_decompress(&s, 4096).unwrap(),
(0..20u8).map(|i| b'a' + i).collect::<Vec<_>>()
);
// "abcd" + M2 (dist 4, len 4) + fim
// "abcd" + M2 backreference (distance 4, length 4) + EOF marker
let s = vec![17 + 4, b'a', b'b', b'c', b'd', 0x6C, 0x00, 0x11, 0, 0];
assert_eq!(lzo1x_decompress(&s, 4096).unwrap(), b"abcdabcd");
}
@@ -334,7 +334,7 @@ fn packed_extents_decode_zlib_and_zstd() {
codec: PackedCodec::Zlib,
};
assert_eq!(p.read(&MemSource(disk)).unwrap(), &plain[1000..6000]);
// zstd, se o utilitário existir
// zstd-compressed extent (if the `zstd` CLI is installed)
if have("zstd") {
let dir = tempfile::tempdir().unwrap();
let f = dir.path().join("p.bin");
+2 -2
View File
@@ -37,7 +37,7 @@ impl Sink for Collect {
}
}
/// Isola as configurações/sessões automáticas dos testes numa pasta temporária.
/// Isolates `GUARA_CONFIG_DIR` (settings and autosaved sessions) inside a per-test temp directory.
pub fn init() {
static DIR: std::sync::OnceLock<tempfile::TempDir> = std::sync::OnceLock::new();
let d = DIR.get_or_init(|| tempfile::tempdir().unwrap());
@@ -69,7 +69,7 @@ pub fn sha256(b: &[u8]) -> String {
.collect()
}
/// Gera (uma vez) as imagens de teste; `None` se as ferramentas não existirem.
/// Builds (or reuses) the filesystem test fixtures once per test binary; returns `None` if `mkfs` tools are missing.
pub fn fixtures() -> Option<PathBuf> {
static ONCE: std::sync::OnceLock<Option<PathBuf>> = std::sync::OnceLock::new();
ONCE.get_or_init(fixtures_inner).clone()
+5 -5
View File
@@ -66,7 +66,7 @@ fn rebuilds_a_jpeg_split_around_another_one() {
let mut bb = b.clone();
pad(&mut bb);
disk.extend_from_slice(&bb);
disk.extend(noise(20 * 4096, 5)); // lixo
disk.extend(noise(20 * 4096, 5)); // non-JPEG filler block
let a2_off = disk.len() as u64;
let mut rest = a[split..].to_vec();
pad(&mut rest);
@@ -76,7 +76,7 @@ fn rebuilds_a_jpeg_split_around_another_one() {
let mut idx = ClusterIndex::new(disk.len() as u64);
idx.scan_chunk(0, &disk);
// O fragmento correto precisa estar marcado.
// The continuation cluster must be flagged as a JPEG entropy candidate.
assert!(idx.get(a2_off));
eprintln!("TRUE a2_off={a2_off}");
let pool = Pool {
@@ -97,7 +97,7 @@ fn rebuilds_a_jpeg_split_around_another_one() {
}
other => panic!("esperava Rebuilt, veio {other:?}"),
}
// O outro JPEG, íntegro, continua íntegro.
// The intact decoy JPEG remains a single contiguous extent.
let pool2 = Pool {
index: &idx,
used: &[],
@@ -124,7 +124,7 @@ fn rebuilds_three_fragments_among_many_decoys() {
decoys.push((disk.len() as u64, d.len() as u64));
disk.extend_from_slice(&d);
if k == 1 {
// segundo fragmento de A no meio
// second fragment of image A placed in the middle
let off2 = disk.len() as u64;
disk.extend_from_slice(&a[s1..s2]);
decoys.push((off2, (s2 - s1) as u64));
@@ -142,7 +142,7 @@ fn rebuilds_three_fragments_among_many_decoys() {
let used: Vec<Extent> = decoys
.iter()
.enumerate()
.filter(|(i, _)| *i != 2) // o fragmento do meio de A não é de outro arquivo
.filter(|(i, _)| *i != 2) // only the intact image B is marked as already claimed; A's middle fragment is unclaimed
.map(|(_, (o, l))| Extent::new(*o, *l))
.collect();
let pool = Pool {
+37 -37
View File
@@ -1,4 +1,4 @@
//! Discos com tabela de partições, espaço não particionado, partições perdidas e cifragem.
//! Partitioned disks (GPT/MBR), unpartitioned space, lost partitions, sessions, and disconnect handling.
mod common;
use common::*;
@@ -39,7 +39,7 @@ fn png(extra: usize) -> Vec<u8> {
v
}
/// Disco: [1 MiB vazio][partição FAT32 do fixture][8 MiB não particionado com um PNG]
/// Synthetic disk: `[1 MiB gap][FAT32 partition][8 MiB unpartitioned gap containing a PNG]`
#[test]
fn particao_mbr_e_espaco_nao_particionado() {
let Some(dir) = fixtures() else { return };
@@ -66,14 +66,14 @@ fn particao_mbr_e_espaco_nao_particionado() {
"gap: {:?}",
layout.gaps
);
// Metadados da partição (offsets corrigidos pela posição da partição)...
// Partition metadata findings (with byte offsets translated to disk-global offsets)...
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.
// ...plus the carved PNG from the unpartitioned gap.
let carved: Vec<_> = res
.found
.iter()
@@ -117,11 +117,11 @@ fn particao_perdida_e_encontrada_sem_tabela() {
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.
// BitLocker signature in partition 1, LUKS signature in partition 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.
// A plaintext PNG planted inside the locked container range must not be carved while locked.
let p = png(10_000);
let at = (2 * MIB) as usize;
disk[at..at + p.len()].copy_from_slice(&p);
@@ -141,7 +141,7 @@ fn volume_cifrado_e_detectado_e_nao_varrido() {
#[test]
fn gpt_e_lido() {
// GPT mínimo com 1 partição de dados.
// Minimal GPT with 1 basic data partition.
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];
@@ -183,7 +183,7 @@ fn imagem_forense_e_identica_e_origem_intacta() {
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.
// Never overwrite an existing file in the destination directory.
assert!(image::create_image(src.as_ref(), &dest, &|| false, &mut |_, _| {}).is_err());
}
@@ -193,7 +193,7 @@ fn pausa_e_retoma_encontra_o_mesmo() {
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.
// Cancel on the 3rd progress callback, then resume from the saved 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();
@@ -233,7 +233,7 @@ fn pausa_e_retoma_encontra_o_mesmo() {
assert_eq!(a, b);
}
/// Streaming de pré-visualização: Range devolve exatamente os bytes do arquivo recuperado.
/// Preview streaming: HTTP `Range` slices return the exact bytes of the recovered file.
#[test]
fn file_slice_com_range_bate_com_o_arquivo_recuperado() {
init();
@@ -302,7 +302,7 @@ fn configuracoes_de_tema_persistem() {
settings::load().updates,
"a verificação de atualização vem ligada"
);
// arquivo de uma versão anterior, sem o campo novo
// legacy session JSON missing newly added fields
std::fs::write(
settings::config_dir().join("settings.json"),
r#"{"theme":"dark"}"#,
@@ -328,8 +328,8 @@ fn wait_done(eng: &service::Engine, job: &str) {
panic!("job não terminou");
}
/// Gera uma sessão no formato da v0.1.0 (sem os campos que vieram depois) a partir de uma
/// varredura real da imagem, com o alvo informado.
/// Strips post-v0.1.0 fields from a real scan session JSON to simulate a legacy v0.1.0 session file.
///
fn sessao_formato_v0_1_0(img: &std::path::Path, alvo: &str) -> tempfile::NamedTempFile {
init();
let eng = service::Engine::new();
@@ -358,7 +358,7 @@ fn sessao_formato_v0_1_0(img: &std::path::Path, alvo: &str) -> tempfile::NamedTe
tmp
}
/// Uma sessão gravada pela v0.1.0 continua abrindo nas versões novas.
/// Backward compatibility: a session saved by v0.1.0 still loads and recovers cleanly.
#[test]
fn sessao_da_v0_1_0_abre_e_funciona() {
init();
@@ -374,7 +374,7 @@ fn sessao_da_v0_1_0_abre_e_funciona() {
assert_eq!(st["source"], true);
let res = eng.results(&job, &service::Query::default()).unwrap();
assert_eq!(res["total"], 7);
// recuperar a partir da sessão antiga continua dando o mesmo conteúdo
// recovering from the legacy session produces the exact same file contents
let out = tempfile::tempdir().unwrap();
let rj = eng
.start_recover(
@@ -392,14 +392,14 @@ fn sessao_da_v0_1_0_abre_e_funciona() {
}
}
/// Origem indisponível não impede abrir a sessão; reapontar a origem religa tudo.
/// A missing source does not block opening a session; relinking the source restores full access.
#[test]
fn sessao_abre_sem_a_origem_e_pode_ser_reconectada() {
init();
let Some(dir) = fixtures() else { return };
let img = dir.join("ntfs.img");
let tmp = sessao_formato_v0_1_0(&img, "/dev/disco-que-nao-existe");
// sessão pausada (como as de uma varredura interrompida)
// paused session (as produced by an interrupted scan)
let mut v: serde_json::Value =
serde_json::from_slice(&std::fs::read(tmp.path()).unwrap()).unwrap();
v["done"] = serde_json::json!(false);
@@ -413,7 +413,7 @@ fn sessao_abre_sem_a_origem_e_pode_ser_reconectada() {
.as_str()
.unwrap()
.contains("não está disponível"));
// navegar e exportar funcionam
// browsing, filtering, CSV export, and HTML reporting work even while the source is offline
assert_eq!(
eng.results(&job, &service::Query::default()).unwrap()["total"],
7
@@ -424,7 +424,7 @@ fn sessao_abre_sem_a_origem_e_pode_ser_reconectada() {
.unwrap(),
7
);
// pré-visualização, retomada e recuperação avisam com clareza
// preview, resume, and recovery return a clear error while the source is offline
assert_eq!(eng.thumb(&job, 1, 128).err(), Some(503));
assert!(eng
.resume(&job)
@@ -442,7 +442,7 @@ fn sessao_abre_sem_a_origem_e_pode_ser_reconectada() {
)
.unwrap_err();
assert!(e.to_string().contains("não está disponível"), "{e}");
// reconecta
// relink the session to the relocated disk image
eng.relink(&job, img.to_str().unwrap(), false).unwrap();
let st = eng.status(&job).unwrap();
assert_eq!(st["source"], true);
@@ -458,7 +458,7 @@ fn sessao_abre_sem_a_origem_e_pode_ser_reconectada() {
.unwrap();
wait_done(&eng, &rj);
assert_eq!(hashes_in(out.path()).len(), 7);
// arquivo que não é sessão
// non-session file returns a clear error
std::fs::write(tmp.path(), b"nao e json").unwrap();
let e = eng.load_session(tmp.path().to_str().unwrap()).unwrap_err();
assert!(
@@ -467,7 +467,7 @@ fn sessao_abre_sem_a_origem_e_pode_ser_reconectada() {
);
}
// ------------------------------------------------------------------ desconexão e reconexão do disco
// ------------------------------------------------------------------ disk disconnect and reconnect
#[test]
fn reconexao_so_aceita_o_mesmo_disco_ou_um_clone() {
@@ -477,14 +477,14 @@ fn reconexao_so_aceita_o_mesmo_disco_ou_um_clone() {
let tmp = sessao_formato_v0_1_0(&img, img.to_str().unwrap());
let eng = service::Engine::new();
let job = eng.load_session(tmp.path().to_str().unwrap()).unwrap();
// sessão antiga não tem impressão digital: o relink aprende a do primeiro disco aceito
// legacy session had no fingerprint: `relink_session` records the fingerprint of the accepted disk
eng.relink(&job, img.to_str().unwrap(), false).unwrap();
let work = tempfile::tempdir().unwrap();
// clone idêntico (por exemplo uma imagem do disco): aceito
// identical clone (e.g. a raw disk image of the drive): accepted without `force`
let clone = work.path().join("clone.img");
std::fs::copy(&img, &clone).unwrap();
eng.relink(&job, clone.to_str().unwrap(), false).unwrap();
// mesmo tamanho, mas outro conteúdo inicial: recusado, a menos que force
// same byte size but different disk content: rejected unless `force` is true
let outro = work.path().join("outro.img");
let mut bytes = std::fs::read(&img).unwrap();
bytes[0..16].copy_from_slice(b"OUTRO DISCO....!");
@@ -494,7 +494,7 @@ fn reconexao_so_aceita_o_mesmo_disco_ou_um_clone() {
.unwrap_err();
assert!(e.to_string().contains("conteúdo inicial"), "{e}");
eng.relink(&job, outro.to_str().unwrap(), true).unwrap();
// tamanho diferente: recusado
// different byte size: rejected unless `force` is true
let menor = work.path().join("menor.img");
std::fs::write(&menor, &bytes[..bytes.len() / 2]).unwrap();
let e = eng
@@ -503,7 +503,7 @@ fn reconexao_so_aceita_o_mesmo_disco_ou_um_clone() {
assert!(e.to_string().contains("bytes"), "{e}");
}
/// Fonte que "some" a partir de certo ponto, como um disco puxado da porta USB.
/// Simulated `Source` that fails with `EIO` past `fail_from`, mimicking an unplugged USB drive.
struct Flaky {
inner: MemSource,
broken_from: u64,
@@ -541,8 +541,8 @@ fn varredura_pausa_quando_o_disco_some_e_retoma_do_ponto_certo() {
v
};
let (a, b) = (png(3000), png(4000));
disk[2 * mib..2 * mib + a.len()].copy_from_slice(&a); // antes da falha
disk[50 * mib..50 * mib + b.len()].copy_from_slice(&b); // depois da falha
disk[2 * mib..2 * mib + a.len()].copy_from_slice(&a); // PNG before the disconnect point
disk[50 * mib..50 * mib + b.len()].copy_from_slice(&b); // PNG after the disconnect point
let flaky = Flaky {
inner: MemSource(disk.clone()),
broken_from: 16 * mib as u64,
@@ -555,9 +555,9 @@ fn varredura_pausa_quando_o_disco_some_e_retoma_do_ponto_certo() {
};
assert!(cp.io_lost, "deveria marcar perda de E/S");
assert_eq!(sink.found.len(), 1, "só o PNG anterior à falha");
// o ponto de retomada não pula os blocos que falharam
// the saved checkpoint offset does not skip past the unreadable blocks
assert!(cp.ranges[0].next <= 16 * mib as u64 + 8 * mib as u64);
// disco volta: retoma e acha o resto, sem duplicar o primeiro
// once the drive is reconnected, resuming finds the second PNG without duplicating the first
let healthy = MemSource(disk);
let before = sink.found.clone();
let out = scan::run(&healthy, &opts, Some(cp), &before, &|| false, &mut sink);
@@ -577,7 +577,7 @@ fn varredura_salva_sessao_sozinha_e_ela_reabre() {
.start_scan(img.to_str().unwrap(), ScanOptions::default())
.unwrap();
wait_done(&eng, &job);
// o salvamento final roda numa thread própria
// wait briefly for the background autosave thread to flush the final snapshot
let mut item = None;
for _ in 0..200 {
let list = eng.sessions_list();
@@ -596,7 +596,7 @@ fn varredura_salva_sessao_sozinha_e_ela_reabre() {
assert_eq!(item["found"], 7);
let path = item["path"].as_str().unwrap().to_string();
assert!(std::path::Path::new(&path).exists());
// reabre como qualquer sessão
// reopening the autosaved session restores all findings
let job2 = eng.load_session(&path).unwrap();
let st = eng.status(&job2).unwrap();
assert_eq!(
@@ -607,13 +607,13 @@ fn varredura_salva_sessao_sozinha_e_ela_reabre() {
),
("done", 7, true)
);
// apagar
// deleting the autosaved session removes it from the list
eng.session_delete(&path).unwrap();
assert!(!std::path::Path::new(&path).exists());
assert!(eng.session_delete("/etc/passwd").is_err());
}
// ------------------------------------------------------------------ organização e espaço
// ------------------------------------------------------------------ output organization and free-space check
#[test]
fn recuperar_organiza_por_tipo_data_ou_tudo_junto() {
@@ -652,7 +652,7 @@ fn recuperar_organiza_por_tipo_data_ou_tudo_junto() {
});
assert!(achou, "{layout:?}: {:?}", walk_names(out.path()));
}
// por data: usa a data de modificação (ano/mês)
// organize by date: groups into `YYYY/YYYY-MM` by modification/capture date
let out = tempfile::tempdir().unwrap();
recover::recover_with(
src.as_ref(),
@@ -699,7 +699,7 @@ fn espaco_insuficiente_no_destino_e_avisado_antes_de_copiar() {
let Some(dir) = fixtures() else { return };
let img = dir.join("ntfs.img");
let tmp = sessao_formato_v0_1_0(&img, img.to_str().unwrap());
// sessão com um arquivo "enorme" (1 EB)
// synthetic session with a 1 EB file to trigger the insufficient destination space check
let mut v: serde_json::Value =
serde_json::from_slice(&std::fs::read(tmp.path()).unwrap()).unwrap();
v["found"][0]["size"] = serde_json::json!(1u64 << 60);
+2 -2
View File
@@ -1,4 +1,4 @@
//! Ponta a ponta: volume LUKS2 com ext4 dentro, arquivo apagado, senha informada ao Guará.
//! End-to-end encrypted recovery: LUKS2 container holding an ext4 volume with a deleted file.
mod common;
use common::*;
@@ -150,7 +150,7 @@ fn deleted_file_inside_luks2_is_recovered_with_password() {
.success());
let base: Arc<dyn Source> = Arc::new(FileSource::open(&img).unwrap());
// Sem senha: o volume segue fechado e a varredura não inventa arquivos.
// Without the password: the LUKS2 volume stays locked and no plaintext files are exposed.
let locked = assemble_source(base.clone(), &Assembly::default()).unwrap();
assert_eq!(locked.unlocked.len(), 0);
assert_eq!(locked.locked.len(), 1);
+4 -4
View File
@@ -1,4 +1,4 @@
//! Ponta a ponta: RAID 5 de 4 discos (um perdido) com ext4 dentro e um arquivo apagado.
//! End-to-end RAID recovery: degraded 4-disk RAID 5 (one missing disk) with ext4 and a deleted file.
mod common;
use common::*;
@@ -92,7 +92,7 @@ fn deleted_file_on_degraded_raid5() {
let logical = std::fs::read(&plain).unwrap();
// RAID 5 left-symmetric
let data_off = 2048u64; // setores
let data_off = 2048u64; // capacity in 512-byte sectors
let mut disks = vec![vec![0u8; data_off as usize * 512 + rows * CHUNK]; n];
for row in 0..rows {
let pd = n - 1 - row % n;
@@ -110,7 +110,7 @@ fn deleted_file_on_degraded_raid5() {
let mut paths = vec![];
for (i, d) in disks.iter_mut().enumerate() {
if i == 2 {
continue; // disco que morreu
continue; // simulate failed/missing disk 1
}
let sb = md_super(n as u32, i as u32, data_off, (rows * CHUNK / 512) as u64);
d[4096..8192].copy_from_slice(&sb);
@@ -230,7 +230,7 @@ fn raid5_without_metadata_is_detected_and_scanned() {
}
disks[pd][row * chunk..(row + 1) * chunk].copy_from_slice(&p);
}
let order = [1usize, 2, 0]; // fornecidos fora de ordem
let order = [1usize, 2, 0]; // member disks passed in scrambled order `[2, 0, 3, 1]`
let mut paths = vec![];
for (j, &i) in order.iter().enumerate() {
let p = dir.path().join(format!("m{j}.img"));
+2 -2
View File
@@ -20,7 +20,7 @@ fn reads_references_made_by_libewf() {
assert_eq!(s.size(), want.len() as u64, "{name}");
let got = read_vec(s.as_ref(), 0, want.len()).unwrap();
assert!(got == want, "{name}: conteúdo difere");
// leitura que cruza chunks e termina no fim
// cross-chunk read reaching EOF
let tail = read_vec(s.as_ref(), want.len() as u64 - 5000, 9000).unwrap();
assert_eq!(tail, &want[want.len() - 5000..], "{name}");
}
@@ -105,7 +105,7 @@ fn compressed_output_is_smaller_and_odd_sizes_work() {
assert_eq!(read_vec(r.as_ref(), 0, 100_352).unwrap(), data);
}
/// Gera uma imagem para validar com ferramentas externas (ewfverify). `GUARA_E01_OUT=/caminho/base`.
/// Optional helper to write an `.E01` for external `ewfverify` validation when `GUARA_E01_OUT` is set.
#[test]
#[ignore]
fn emit_for_external_verification() {
+5 -5
View File
@@ -1,4 +1,4 @@
//! exFAT: injeta um conjunto de entradas APAGADO num sistema criado por mkfs.exfat.
//! exFAT: injects a deleted directory entry set (`0x05`/`0x40`/`0x41`) into a `mkfs.exfat` image.
mod common;
use common::*;
@@ -30,7 +30,7 @@ fn exfat_apagado_e_recuperado_com_nome() {
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.
// Find the first unused 32-byte slot in the root directory cluster.
let mut slot = root_off;
while d[slot] != 0 {
slot += 32;
@@ -43,15 +43,15 @@ fn exfat_apagado_e_recuperado_com_nome() {
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.
// `0x05` = deleted file entry (`0x85` with in-use bit cleared), followed by `0x40` stream and `0x41` name entries.
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 + 12..slot + 14].copy_from_slice(&0x6000u16.to_le_bytes()); // timestamp
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 + 1] = 0x03; // `NoFatChain` bit set (contiguous allocation)
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());
+8 -8
View File
@@ -1,4 +1,4 @@
//! ext4: arquivos apagados com nome, a partir de imagens criadas com mke2fs/debugfs.
//! ext2/3/4: deleted file recovery (with original filenames) on images built with `mke2fs`/`debugfs`.
mod common;
use common::*;
@@ -152,7 +152,7 @@ fn deleted_by_debugfs_keeps_extents_and_name() {
);
assert!(recovered(&i.img, f) == *data, "{name}");
}
// O arquivo que não foi apagado não aparece.
// Live files that were not deleted must not be listed.
assert!(!c
.found
.iter()
@@ -161,7 +161,7 @@ fn deleted_by_debugfs_keeps_extents_and_name() {
#[test]
fn truncated_inode_recovered_by_remnant_extents() {
// Simula o que o kernel faz ao apagar: tamanho zerado e contagem de extents zerada.
// Simulate Linux kernel `ext4_evict_inode`: zero `i_size` and `eh_entries` while leaving extent records.
let Some(i) = build(&["-t", "ext4"]) else {
return;
};
@@ -181,7 +181,7 @@ fn truncated_inode_recovered_by_remnant_extents() {
.find(|f| f.name == "contrato.dat")
.expect("contrato.dat");
assert_eq!(f.origin, Origin::Ext);
// Tamanho exato é perdido: recupera todos os blocos (9000 bytes arredondados para 3 blocos de 4K).
// Exact byte size was zeroed on deletion: recovers all 3 allocated 4 KiB blocks (12,288 bytes).
assert_eq!(f.size, 12288);
let got = recovered(&i.img, f);
assert_eq!(&got[..9000], &i.files[1].1[..]);
@@ -195,7 +195,7 @@ fn journal_copy_restores_exact_size() {
let Some(i) = build(&["-t", "ext4", "-O", "has_journal"]) else {
return;
};
// Bloco da tabela de inodes que contém o inode do arquivo.
// Locate the inode table block and byte offset for ` target ` via `debugfs imap`.
let ino = inode_of(&i.img, "/fotos/viagem.dat");
let o = debugfs(&i.img, &["stats"]);
let _ = o;
@@ -234,7 +234,7 @@ fn journal_copy_restores_exact_size() {
eprintln!("SKIP: debugfs sem suporte a escrita no diário: {j}");
return;
}
// Exclui e zera como o kernel faria.
// Delete the file and wipe its inode table slot (leaving only the journal copy).
debugfs(
&i.img,
&[
@@ -260,8 +260,8 @@ fn journal_copy_restores_exact_size() {
#[test]
fn ext2_blockmap_deleted_is_not_invented() {
// ext2 zera os ponteiros ao apagar; sem diário não há o que recuperar por metadados,
// mas a varredura não pode falhar nem listar o arquivo que continua existindo.
// Linux ext2 zeroes block pointers on unlink; without a journal, metadata recovery finds nothing,
// but the scan must succeed cleanly without reporting live files as deleted.
let Some(i) = build(&["-t", "ext2"]) else {
return;
};
+10 -10
View File
@@ -1,11 +1,11 @@
//! Saúde do disco, notificações, configurações e retomada da recuperação.
//! Disk health (SMART), notifications, user settings, and resumable recovery journal tests.
mod common;
use common::*;
use guara_core::*;
use serde_json::json;
// ------------------------------------------------------------------ saúde (SMART)
// ------------------------------------------------------------------ disk health (SMART)
#[test]
fn smart_reprovado_e_setores_ruins_viram_alerta_serio() {
@@ -119,12 +119,12 @@ fn imagem_de_disco_nao_tem_smart() {
assert_eq!(h.status, "unknown");
}
// ------------------------------------------------------------------ configurações
// ------------------------------------------------------------------ settings
#[test]
fn configuracoes_antigas_ganham_os_valores_padrao_novos() {
init();
// arquivo gravado pela v0.3: só tinha o tema
// legacy v0.3 settings file containing only `theme`: newer fields use defaults
std::fs::create_dir_all(settings::config_dir()).unwrap();
std::fs::write(
settings::config_dir().join("settings.json"),
@@ -158,7 +158,7 @@ fn configuracoes_antigas_ganham_os_valores_padrao_novos() {
);
}
// ------------------------------------------------------------------ notificações
// ------------------------------------------------------------------ notifications
#[cfg(target_os = "linux")]
#[test]
@@ -195,7 +195,7 @@ fn notificacao_chama_o_notify_send() {
);
}
// ------------------------------------------------------------------ retomar recuperação
// ------------------------------------------------------------------ resumable recovery
#[test]
fn recuperacao_interrompida_retoma_pulando_o_que_ja_foi_conferido() {
@@ -214,7 +214,7 @@ fn recuperacao_interrompida_retoma_pulando_o_que_ja_foi_conferido() {
let out = tempfile::tempdir().unwrap();
let key = "disco-de-teste";
// 1ª execução: "cai a energia" depois de 3 arquivos
// 1st run: abort after writing 3 of the 7 files
let n = std::sync::atomic::AtomicUsize::new(0);
let cancel = || n.fetch_add(1, std::sync::atomic::Ordering::Relaxed) >= 3;
let r1 = recover::recover_resumable(
@@ -233,7 +233,7 @@ fn recuperacao_interrompida_retoma_pulando_o_que_ja_foi_conferido() {
let journal = std::fs::read_to_string(out.path().join(recover::JOURNAL)).unwrap();
assert_eq!(journal.lines().count(), 3);
// 2ª execução: retoma, pula os 3 e faz os outros 4
// 2nd run: resume skips the 3 verified files and writes the remaining 4
let r2 = recover::recover_resumable(
src.as_ref(),
&files,
@@ -259,7 +259,7 @@ fn recuperacao_interrompida_retoma_pulando_o_que_ja_foi_conferido() {
7
);
// um arquivo é danificado entre as execuções: é refeito, os outros 6 são pulados
// corrupt one recovered file on disk: resume re-recovers that file and skips the other 6
let victim = out.path().join(&r2.recovered[0].path);
std::fs::write(&victim, b"lixo").unwrap();
let r3 = recover::recover_resumable(
@@ -279,7 +279,7 @@ fn recuperacao_interrompida_retoma_pulando_o_que_ja_foi_conferido() {
"o arquivo danificado deveria ter sido refeito"
);
// outro disco (outra chave) não reaproveita o diário
// a different `source_key` does not reuse the previous disk's recovery journal
let r4 = recover::recover_resumable(
src.as_ref(),
&files,
+1 -1
View File
@@ -1,4 +1,4 @@
//! FileVault 2 (Core Storage): imagem de referência do cryptsetup (senha heslo123).
//! FileVault 2 (Core Storage): `cryptsetup` reference image (password `heslo123`).
use guara_core::crypt::{filevault, Secret, UnlockError};
use guara_core::hashing::MultiHasher;
+23 -23
View File
@@ -1,5 +1,5 @@
//! Recursos periciais: hashes, metadados (EXIF/PDF/Office/MP4), miniaturas,
//! busca de texto, exportações e navegação (facetas/duplicadas).
//! Forensic features: multi-hash, metadata extraction (EXIF/PDF/Office/MP4), thumbnails,
//! full-disk text search, CSV/HTML exports, and explorer facets/deduplication.
use guara_core::explorer::{self, Query};
use guara_core::hashing::hash_bytes;
@@ -79,7 +79,7 @@ fn rat(p: &[(u32, u32)]) -> Vec<u8> {
}
fn jpeg_with_exif(orientation: u16, w: usize, h: usize) -> Vec<u8> {
// pixels: gradiente
// gradient pixel pattern
let mut px = Vec::with_capacity(w * h * 3);
for y in 0..h {
for x in 0..w {
@@ -143,7 +143,7 @@ fn exif_camera_data_e_gps() {
let m = meta::extract(&MemSource(vec![]), &f);
assert_eq!(m.camera.as_deref(), Some("Canon EOS 80D"));
assert_eq!(m.px, Some((64, 48)));
// 2023-07-14 18:22:05 lido como UTC
// `2023:07:14 18:22:05` interpreted as UTC
assert_eq!(m.taken, Some(1_689_358_925));
let (lat, lon) = m.gps.unwrap();
assert!((lat + 23.5550).abs() < 0.001, "{lat}");
@@ -157,14 +157,14 @@ fn exif_camera_data_e_gps() {
#[test]
fn miniatura_respeita_orientacao_e_tamanho() {
let src = MemSource(vec![]);
// 64x48 com orientação 6 (girar 90 graus) => retrato 48x64, reduzido para caber em 32
// 64x48 with EXIF orientation 6 (90° CW) -> portrait 48x64, scaled down to fit within 32px
let f = resident("foto.jpg", jpeg_with_exif(6, 64, 48));
let t = thumb::thumbnail(&src, &f, 32).expect("miniatura");
let mut dec = jpeg_decoder::Decoder::new(&t[..]);
dec.decode().unwrap();
let i = dec.info().unwrap();
assert_eq!((i.width, i.height), (24, 32));
// PNG também
// PNG thumbnail generation
let mut img = ::image::RgbImage::new(100, 50);
for p in img.pixels_mut() {
*p = ::image::Rgb([200, 30, 30]);
@@ -179,7 +179,7 @@ fn miniatura_respeita_orientacao_e_tamanho() {
(dec.info().unwrap().width, dec.info().unwrap().height),
(40, 20)
);
// lixo não vira miniatura
// random bytes return `None`
assert!(thumb::thumbnail(&src, &resident("x.jpg", vec![1, 2, 3, 4, 5]), 32).is_none());
}
@@ -325,7 +325,7 @@ fn metadados_de_mp4() {
assert_eq!(get("Dimensões").as_deref(), Some("1920 × 1080 px"));
}
// ------------------------------------------------------------------ busca de texto
// ------------------------------------------------------------------ text search
#[test]
fn busca_acha_utf8_utf16_e_na_fronteira_dos_blocos() {
@@ -338,7 +338,7 @@ fn busca_acha_utf8_utf16_e_na_fronteira_dos_blocos() {
.flat_map(|c| c.to_le_bytes())
.collect();
put(&mut disk, 3 * mb + 6, &u16s);
// exatamente cruzando o limite do primeiro bloco de 8 MiB
// string placed right across the 8 MiB chunk boundary
put(&mut disk, 8 * mb - 4, b"SEGREDO-123");
let opts = search::SearchOpts {
terms: vec!["segredo-123".into()],
@@ -364,7 +364,7 @@ fn busca_acha_utf8_utf16_e_na_fronteira_dos_blocos() {
assert_eq!(hits[1].offset, 3 * mb as u64 + 6);
assert_eq!(hits[1].encoding, "UTF-16");
assert_eq!(hits[2].offset, 8 * mb as u64 - 4);
// sem ignorar maiúsculas só acha a grafia exata
// case-sensitive mode matches only exact casing
let opts = search::SearchOpts {
terms: vec!["Segredo-123".into()],
ignore_case: false,
@@ -381,7 +381,7 @@ fn busca_acha_utf8_utf16_e_na_fronteira_dos_blocos() {
assert_eq!(n, 1);
}
// ------------------------------------------------------------------ exportações
// ------------------------------------------------------------------ exports
#[test]
fn csv_escapa_e_neutraliza_formulas() {
@@ -431,7 +431,7 @@ fn relatorio_html_escapa_conteudo_e_traz_o_caso() {
assert!(html.contains("somente para leitura"));
}
// ------------------------------------------------------------------ navegação
// ------------------------------------------------------------------ explorer facets & sorting
#[test]
fn facetas_filtros_e_duplicadas() {
@@ -473,7 +473,7 @@ fn facetas_filtros_e_duplicadas() {
Some("Canon"),
Some(8),
),
mk(3, "c.jpg", Some("Viagens"), Some(jul23), None, Some(7)), // duplicada de a.jpg
mk(3, "c.jpg", Some("Viagens"), Some(jul23), None, Some(7)), // exact content duplicate of `a.jpg`
mk(4, "d.jpg", None, None, None, None),
];
let q = |json: &str| -> Query { serde_json::from_str(json).unwrap() };
@@ -485,7 +485,7 @@ fn facetas_filtros_e_duplicadas() {
assert_eq!(f["undated"], 1);
assert_eq!(f["carved"], 1);
assert_eq!(f["duplicates"], 1);
assert_eq!(f["small"], 4); // todos têm 10 bytes
assert_eq!(f["small"], 4); // all three files are 10 bytes long
assert_eq!(q(r#"{"min_size":30720}"#).select(&found).len(), 0);
assert_eq!(f["cameras"][0]["name"], "Canon");
let dcim = f["folders"]
@@ -507,8 +507,8 @@ fn facetas_filtros_e_duplicadas() {
assert_eq!(q(r#"{"query":"viagens"}"#).select(&found).len(), 1);
let mut items = q("{}").select(&found);
explorer::sort_items(&mut items, Some("date"));
assert_eq!(items[0].id, 2); // mais recente primeiro
assert_eq!(items.last().unwrap().id, 4); // sem data por último
assert_eq!(items[0].id, 2); // newest first
assert_eq!(items.last().unwrap().id, 4); // undated files sort last
}
// ------------------------------------------------------------------ carving
@@ -521,8 +521,8 @@ fn png_chunk(ty: &[u8; 4], data: &[u8]) -> Vec<u8> {
c
}
/// Regressão: texto ASCII depois de um PNG truncado vira "tamanho" de ~1,9 GB;
/// o carving não pode inventar um PNG gigante.
/// Regression test: ASCII text immediately following a truncated PNG looks like a ~1.9 GB chunk
/// length; the carver must reject the bogus chunk and never emit a multi-GB PNG.
#[test]
fn png_truncado_seguido_de_texto_nao_vira_arquivo_gigante() {
let mut d = vec![0x89, b'P', b'N', b'G', 0x0D, 0x0A, 0x1A, 0x0A];
@@ -540,13 +540,13 @@ fn png_truncado_seguido_de_texto_nao_vira_arquivo_gigante() {
);
assert_eq!(hit.confidence, Confidence::Low);
// sem nenhum dado de imagem (só cabeçalho) não é arquivo
// header-only PNG without any `IDAT` chunk is rejected
let mut d2 = vec![0x89, b'P', b'N', b'G', 0x0D, 0x0A, 0x1A, 0x0A];
d2.extend(png_chunk(b"IHDR", &[0, 0, 0, 8, 0, 0, 0, 8, 8, 2, 0, 0, 0]));
d2.resize(1 << 20, b'e');
assert!(signatures::detect(&MemSource(d2.clone()), 0, &d2[..64], 8 << 30).is_none());
// PNG completo continua com confiança alta e tamanho exato
// complete PNG with `IEND` still carves with `High` confidence and exact byte length
let mut ok = vec![0x89, b'P', b'N', b'G', 0x0D, 0x0A, 0x1A, 0x0A];
ok.extend(png_chunk(b"IHDR", &[0, 0, 0, 8, 0, 0, 0, 8, 8, 2, 0, 0, 0]));
ok.extend(png_chunk(b"IDAT", &[1; 100]));
@@ -557,10 +557,10 @@ fn png_truncado_seguido_de_texto_nao_vira_arquivo_gigante() {
assert_eq!((h.len as usize, h.confidence), (n, Confidence::High));
}
// ------------------------------------------------------------------ abrir no player do sistema
// ------------------------------------------------------------------ open in external system app
/// O vídeo é copiado para uma pasta temporária e entregue ao aplicativo padrão do sistema
/// (aqui um `xdg-open`/`open` falso grava o que recebeu).
/// `open_external` materializes the recovered file into a private temp folder and invokes
/// the platform opener (stubbed here via a fake `xdg-open`/`open` script in `PATH`).
#[cfg(any(target_os = "linux", target_os = "macos"))]
#[test]
fn abre_no_player_do_sistema() {
+3 -3
View File
@@ -1,4 +1,4 @@
//! Detectores estruturais: arquivos reais do sistema com lixo colado no fim (o tamanho tem de ser exato).
//! Structural format detectors: real files with trailing garbage appended (carved size must be exact).
use guara_core::signatures::detect;
use guara_core::source::MemSource;
@@ -13,7 +13,7 @@ fn check(path: &str, ext: &str, min_conf: Confidence) -> bool {
}
let mut data = std::fs::read(p).unwrap();
let real = data.len();
// lixo depois do fim real (outro arquivo no disco)
// append trailing bytes to simulate subsequent disk blocks after EOF
data.extend((0..5000u32).map(|i| (i.wrapping_mul(2654435761) >> 7) as u8 | 0x81));
let src = MemSource(data.clone());
let h =
@@ -129,7 +129,7 @@ fn webm_rar_deb_with_unknown_locations() {
#[test]
fn synthetic_structures() {
// CAB, MIDI, pcap e AIFF montados à mão.
// Synthetic CAB, MIDI, PCAP, and AIFF containers with trailing garbage.
let mut cab = b"MSCF\0\0\0\0".to_vec();
cab.extend_from_slice(&200u32.to_le_bytes());
cab.resize(200, 0);
+4 -4
View File
@@ -1,4 +1,4 @@
//! HFS+: imagem real do dfvfs (target/testdata/dfvfs/hfsplus.raw) e exclusão simulada de registros.
//! HFS+: real `dfvfs` image (`target/testdata/dfvfs/hfsplus.raw`) with simulated leaf-record deletion.
mod common;
use common::*;
@@ -43,10 +43,10 @@ fn pristine_volume_has_no_deleted_files_and_is_recognized() {
fn record_left_in_leaf_slack_is_recovered() {
let Some(mut img) = image() else { return };
let bs = 4096usize;
let node = 186 * bs + bs; // catálogo começa no bloco 186; nó 1 é a única folha
// Remove os 5 últimos registros como o driver faz ao apagar no fim do nó: só diminui a contagem.
let node = 186 * bs + bs; // catalog starts at allocation block 186; node 1 is the single leaf node
// Simulate deleting the last 5 records at the end of the leaf node by decrementing `numRecords`.
img[node + 10..node + 12].copy_from_slice(&21u16.to_be_bytes());
// Libera o bloco de dados (bitmap de alocação no bloco 1).
// Clear the allocation bitmap (at block 1) so the deleted files' data blocks are marked free.
let blk = 278usize;
img[bs + blk / 8] &= !(0x80 >> (blk % 8));
let c = scan_bytes(&img);
+7 -7
View File
@@ -1,7 +1,7 @@
use guara_core::jpegscan::*;
fn photo(w: u16, h: u16) -> Vec<u8> {
// Imagem "natural": gradientes suaves + textura + bordas.
// Synthetic natural-looking test image: smooth gradients + texture + geometric edges.
let mut rgb = Vec::with_capacity(w as usize * h as usize * 3);
let mut x = 0x9E3779B97F4A7C15u64;
for yy in 0..h as i32 {
@@ -35,7 +35,7 @@ fn decodes_whole_image_and_ends_at_eoi() {
let d = decode(&h, &j, State::start(&h), u32::MAX, 0);
assert_eq!(d.stop, Stop::Done);
assert_eq!(d.dc.len() as u32, h.total_mcus());
// O que sobra depois do último MCU é o EOI.
// Only the 2-byte EOI marker (`FF D9`) remains after decoding all MCUs.
let tail = &j[d.state.pos.min(j.len())..];
assert!(
tail.windows(2).any(|w| w == [0xFF, 0xD9]),
@@ -48,9 +48,9 @@ fn decodes_whole_image_and_ends_at_eoi() {
fn detects_break_and_resumes_with_the_right_continuation() {
let j = photo(1024, 768);
let h = parse_header(&j).unwrap();
let cut = 12 * 4096; // quebra no limite de cluster
let cut = 12 * 4096; // simulate a fragmentation break on a 4 KiB cluster boundary
assert!(j.len() > cut + 8192, "imagem pequena demais: {}", j.len());
// Dados estranhos no lugar do resto.
// Splice foreign pseudo-random bytes (without `0xFF` markers) after the cut point.
let mut x = 12345u64;
let junk: Vec<u8> = (0..40_000)
.map(|_| {
@@ -64,16 +64,16 @@ fn detects_break_and_resumes_with_the_right_continuation() {
broken.extend_from_slice(&junk);
let d = decode(&h, &broken, State::start(&h), u32::MAX, 512);
assert_ne!(d.stop, Stop::Done);
// Decodificação segue o lixo por um tempo; mas a quebra aparece pelo erro ou pela descontinuidade.
// Decoding continues into the foreign cluster briefly, then triggers a Huffman error or DC jump.
let mcu_break = first_discontinuity(&h, &d.dc);
// MCUs bons: os decodificados ao fim do cluster `cut`.
// Last valid checkpoint at the end of the first fragment (`cut`).
let good = d
.snaps
.iter()
.rfind(|s| s.pos <= cut)
.expect("snapshot antes da quebra")
.clone();
// Retomada com a continuação verdadeira reproduz a decodificação inteira.
// Resuming from that checkpoint with the true second fragment reconstructs the entire image.
let mut joined = j[..cut].to_vec();
joined.extend_from_slice(&j[cut..]);
let r = decode(&h, &joined, good.clone(), u32::MAX, 0);
+4 -4
View File
@@ -1,5 +1,5 @@
//! LUKS1/LUKS2: imagens criadas pelo cryptsetup; o conteúdo é cifrado por uma
//! implementação independente (python cryptography) com a chave real do volume.
//! LUKS1/LUKS2: headers formatted by `cryptsetup` with payload sectors encrypted by an
//! independent implementation (`python3-cryptography`) using the volume's master key.
use guara_core::crypt::{luks, Secret, UnlockError};
use guara_core::source::{read_vec, FileSource, Source};
@@ -136,7 +136,7 @@ fn run_case(c: &Case) {
assert!(st.success());
let src: Arc<dyn Source> = Arc::new(FileSource::open(&img).unwrap());
// Senha errada primeiro.
// Wrong password is ignored when followed by the right password.
match luks::unlock(src.clone(), &[Secret::Password("errada".into())]) {
Err(UnlockError::WrongSecret) => {}
Err(e) => panic!("{}: erro inesperado {e}", c.name),
@@ -149,7 +149,7 @@ fn run_case(c: &Case) {
.unwrap_or_else(|e| panic!("{}: {e}", c.name));
let got = read_vec(u.source.as_ref(), 0, data.len()).unwrap();
assert!(got == data, "{}: conteúdo difere", c.name);
// Também abre com a chave do volume.
// Also unlocks directly via the hex-encoded volume master key.
let vk = std::fs::read(&vkf).unwrap();
let u2 = luks::unlock(src, &[Secret::VolumeKey(vk)]).unwrap();
assert_eq!(
+2 -2
View File
@@ -1,4 +1,4 @@
//! LVM2: imagem real do conjunto de testes do dfvfs (target/testdata/dfvfs/lvm.raw).
//! LVM2: real image from the `dfvfs` test suite (`target/testdata/dfvfs/lvm.raw`).
use guara_core::lvm;
use guara_core::partition::detect_fs;
@@ -26,7 +26,7 @@ fn assembles_real_lvm_image() {
assert_eq!(lv.size(), 4 << 20, "{n}");
let fs = detect_fs(lv.as_ref(), 0);
eprintln!("{n}: {fs:?}");
// O segundo volume da imagem de referência foi criado e nunca formatado.
// The second logical volume (`test_volume_2`) in the reference image is unformatted.
assert_eq!(fs.is_some(), n == "test_logical_volume1", "{n}");
}
}
+14 -14
View File
@@ -1,4 +1,4 @@
//! Reconstrução do moov de MP4 H.264: arquivos sintéticos e, se existir, um vídeo real.
//! H.264 MP4 `moov` reconstruction: synthetic B-frame streams and optional real video test.
use guara_core::mp4rebuild;
use guara_core::source::MemSource;
@@ -44,7 +44,7 @@ fn hex(s: &str) -> Vec<u8> {
.collect()
}
/// Cabeçalho de fatia H.264 conforme o SPS acima (log2_max_frame_num e log2_max_poc_lsb lidos do SPS).
/// Builds an H.264 slice header matching `SPS` above (`log2_max_frame_num = 4`, `log2_max_poc_lsb = 4`).
fn slice(
idr: bool,
frame_num: u32,
@@ -78,7 +78,7 @@ fn nal(out: &mut Vec<u8>, n: &[u8]) {
out.extend_from_slice(n);
}
/// Monta um MP4 sem moov: ftyp, free, mdat. Devolve (arquivo, tamanhos esperados, sincronos, ordem de exibição).
/// Builds a headerless MP4 (`ftyp` + `free` + `mdat`). Returns `(bytes, sample_sizes, sync_sample_nums, display_order_pocs)`.
fn synthetic(gops: usize) -> (Vec<u8>, Vec<usize>, Vec<bool>) {
synthetic_opt(gops, true)
}
@@ -90,7 +90,7 @@ fn synthetic_opt(gops: usize, inband: bool) -> (Vec<u8>, Vec<usize>, Vec<bool>)
let mut mdat = vec![];
let mut sizes = vec![];
let mut sync = vec![];
// GOP: I P B B P B B (ordem de decodificação) com POC de exibição 0 6 2 4 12 8 10 (em unidades de 2)
// GOP in decode order: `I P B B P B B` with display POCs `0, 6, 2, 4, 12, 8, 10`
let pocs = [0u32, 6, 2, 4, 12, 8, 10];
let mut frame_num = 0;
for g in 0..gops {
@@ -144,7 +144,7 @@ fn assemble(file: &[u8], r: &mp4rebuild::Rebuilt) -> Vec<u8> {
fn rebuilds_synthetic_stream_with_b_frames_and_garbage_tail() {
let (mut file, sizes, sync) = synthetic(6);
let real_len = file.len();
// Lixo depois do fim real (outro arquivo no disco) e tamanho do mdat declarado como "até o fim".
// Append non-NAL garbage and set `mdat` size to `0` (extends to EOF) to test stream end detection.
file.extend((0..20_000u32).map(|i| (i.wrapping_mul(2654435761) >> 11) as u8 | 0x80));
let n = file.len() as u32 - 32;
file[32..36].copy_from_slice(&n.to_be_bytes());
@@ -168,7 +168,7 @@ fn rebuilds_synthetic_stream_with_b_frames_and_garbage_tail() {
assert_eq!(s.bytes.len(), sizes[i], "tamanho da amostra {}", i + 1);
assert_eq!(s.is_sync, sync[i], "sincronia da amostra {}", i + 1);
}
// Ordem de exibição: pts = dts + deslocamento deve crescer na ordem do POC.
// Composition timestamps (`pts = dts + ctts_offset`) must follow the expected POC display order.
let mut pts: Vec<(i64, usize)> = vec![];
for i in 0..sizes.len() {
let s = mp4.read_sample(track_id, i as u32 + 1).unwrap().unwrap();
@@ -193,7 +193,7 @@ fn rebuilds_real_h264_file_like_the_original_index() {
return;
};
let orig = std::fs::read(&path).unwrap();
// Corta o moov original.
// Strip the original `moov` atom.
let mut p = 0usize;
let mut cut = orig.len();
while p + 8 <= orig.len() {
@@ -229,7 +229,7 @@ fn rebuilds_real_h264_file_like_the_original_index() {
#[test]
fn rebuilds_real_video_with_interleaved_audio() {
// GUARA_MP4_BIG: vídeo real com áudio; o teste corta o moov e os primeiros 40 MB do mdat bastam.
// Optional real-video test (`GUARA_MP4_BIG`): strips `moov` and truncates `mdat` to the first 40 MB.
let Ok(path) = std::env::var("GUARA_MP4_BIG") else {
return;
};
@@ -238,7 +238,7 @@ fn rebuilds_real_video_with_interleaved_audio() {
let total = f.metadata().unwrap().len();
let mut head = vec![0u8; 1 << 20];
f.read_exact(&mut head).unwrap();
// átomos no começo
// locate leading `ftyp` and `mdat` atoms
let mut p = 0usize;
let (mut ftyp_end, mut mdat_at) = (0usize, None);
while p + 8 <= head.len() {
@@ -260,7 +260,7 @@ fn rebuilds_real_video_with_interleaved_audio() {
eprintln!("SKIP: mdat fora do primeiro MiB (moov no começo é grande)");
return;
};
// Reexecuta com a leitura completa do começo para montar: ftyp + mdat (40 MB).
// Re-read the leading `ftyp` + first 40 MB of `mdat` into a truncated test buffer.
use std::io::Seek;
let mut f = std::fs::File::open(&path).unwrap();
f.seek(std::io::SeekFrom::Start(mp as u64)).unwrap();
@@ -268,9 +268,9 @@ fn rebuilds_real_video_with_interleaved_audio() {
f.read_exact(&mut mdat).unwrap();
let mut file = head[..ftyp_end].to_vec();
file.extend_from_slice(&mdat);
// O tamanho do mdat declarado vai muito além dos 40 MB que temos (arquivo cortado).
// The original `mdat` header declares the full multi-hundred-MB length (simulating an interrupted recording).
let src = MemSource(file.clone());
// Sem SPS/PPS no mdat, é preciso um vídeo de referência da mesma câmera (aqui, o próprio original).
// When SPS/PPS are not repeated in `mdat`, extract donor codec parameters from a reference video.
let cfg = mp4rebuild::config_from_mp4(&MemSource(head.clone()), 0, head.len() as u64)
.expect("avcC do original");
assert!(
@@ -282,7 +282,7 @@ fn rebuilds_real_video_with_interleaved_audio() {
let osize = out.len() as u64;
let mut b = mp4::Mp4Reader::read_header(std::io::Cursor::new(out), osize).unwrap();
let tb = *b.tracks().keys().next().unwrap();
// Original (índice completo) para comparar as primeiras amostras.
// Parse the original intact MP4 sample table to verify the first 100 reconstructed frames.
let f = std::fs::File::open(&path).unwrap();
let mut a = mp4::Mp4Reader::read_header(std::io::BufReader::new(f), total).unwrap();
let vid = a
@@ -321,7 +321,7 @@ mod common;
fn deep_scan_rebuilds_a_video_without_moov_using_a_neighbour_as_reference() {
use guara_core::{ScanMode, ScanOptions};
common::init();
// Arquivo íntegro (com moov) da "mesma câmera" e um segundo que perdeu o moov e não traz SPS/PPS.
// Disk image containing an intact donor MP4 (with `moov`) plus a truncated MP4 without inline SPS/PPS.
let (good_raw, _, _) = synthetic_opt(8, true);
let r =
mp4rebuild::rebuild(&MemSource(good_raw.clone()), 0, good_raw.len() as u64, &[]).unwrap();
+8 -8
View File
@@ -1,4 +1,4 @@
//! Relatório em PDF: estrutura do arquivo (xref, páginas), texto com acentos e paginação.
//! PDF report generator tests: xref table offsets, WinAnsi accented text escaping, and multi-page tables.
use guara_core::recover::{RecoverReport, RecoveredFile};
use guara_core::report::{self, AuditNote, CaseInfo, ReportInput};
@@ -81,7 +81,7 @@ fn find(hay: &[u8], needle: &[u8], from: usize) -> Option<usize> {
.map(|p| p + from)
}
/// Texto dos fluxos de conteúdo, já descomprimidos.
/// Concatenates all uncompressed content streams in the generated PDF.
fn streams(pdf: &[u8]) -> Vec<String> {
let mut out = vec![];
let mut at = 0;
@@ -107,7 +107,7 @@ fn pdf_has_a_valid_cross_reference_table() {
let nl = find(&pdf, b"\n", sx).unwrap();
let xref: usize = std::str::from_utf8(&pdf[sx..nl]).unwrap().parse().unwrap();
assert_eq!(&pdf[xref..xref + 4], b"xref");
// cada entrada aponta exatamente para "N 0 obj"
// every xref entry offset points directly to `"N 0 obj"`
let table = &pdf[xref..];
let header_end = find(table, b"\n", 5).unwrap();
let count: usize = std::str::from_utf8(&table[5..header_end])
@@ -128,14 +128,14 @@ fn pdf_has_a_valid_cross_reference_table() {
#[test]
fn pdf_text_survives_accents_and_escaping() {
let all = streams(&build(5)).join("\n");
// acentos viram bytes WinAnsi em octal; parênteses e barra são escapados
// Latin-1 accents encode as WinAnsi octal escapes; parentheses and backslashes are escaped
assert!(all.contains("Identifica\\347\\343o do caso"), "{all}");
assert!(all.contains("Perita \\(Ana\\) \\\\ Souza"));
assert!(all.contains("Laborat\\363rio"));
assert!(all.contains("fotografia_0003.jpg"));
assert!(all.contains(&"ab".repeat(32)));
assert!(all.contains(&"12".repeat(32)), "hash da trilha ausente");
// aspas curvas viram os bytes WinAnsi 0x93/0x94
// curly quotes map to WinAnsi `0x93`/`0x94` (`\223`/`\224`)
assert!(all.contains("\\223aspas\\224"));
}
@@ -147,12 +147,12 @@ fn long_lists_paginate_with_numbered_footers() {
let last = pages.len();
assert!(pages[last - 1].contains(&format!("p\\341gina {last} de {last}")));
assert!(pages[0].contains(&format!("p\\341gina 1 de {last}")));
// o cabeçalho da tabela se repete nas páginas seguintes
// the table header row is repeated at the top of each continuation page
assert!(pages[2].contains("Modificado \\(UTC\\)"));
// todas as linhas aparecem exatamente uma vez
// every table row appears exactly once across all pages
let all = pages.join("\n");
assert_eq!(all.matches("fotografia_0399.jpg").count(), 1);
// a primeira também consta da lista de recuperados
// `arq-000.jpg` appears both in the findings table and in the recovered files section
assert_eq!(all.matches("fotografia_0000.jpg").count(), 2);
}
+11 -11
View File
@@ -1,5 +1,5 @@
//! RAID por software: arrays gerados por codificadores independentes (faixa a faixa),
//! lidos de volta inteiros, degradados e com superblocos md 1.x e 0.90.
//! Software RAID tests: arrays built by independent stripe encoders and read back
//! intact, degraded, and via Linux `mdadm` v1.x and v0.90 superblocks.
use guara_core::raid::*;
use guara_core::source::{read_vec, MemSource, Source};
@@ -39,7 +39,7 @@ fn gpow(k: usize) -> u8 {
(0..k).fold(1, |a, _| gmul(a, 2))
}
/// Gera as imagens dos membros de um RAID 5/6 com a paridade na posição pedida.
/// Builds member disk buffers for a RAID 5/6 array using the specified parity layout.
fn encode_parity(level: Level, layout: Layout, n: usize, logical: &[u8]) -> Vec<Vec<u8>> {
let dd = if level == Level::R6 { n - 2 } else { n - 1 };
let rows = logical.len() / (C * dd);
@@ -54,7 +54,7 @@ fn encode_parity(level: Level, layout: Layout, n: usize, logical: &[u8]) -> Vec<
_ => (r, (r + 1) % n),
};
let sym = matches!(layout, Layout::LeftSymmetric | Layout::RightSymmetric);
// ordem dos discos de dados
// data disk slot order for this stripe row
let order: Vec<usize> = if sym {
let start = if level == Level::R6 { qd + 1 } else { pd + 1 };
(0..dd).map(|d| (start + d) % n).collect()
@@ -110,7 +110,7 @@ fn check(level: Level, layout: Layout, n: usize, missing: &[usize]) {
got == logical,
"{level:?} {layout:?} n={n} faltando {missing:?}"
);
// leitura desalinhada
// unaligned cross-stripe read
let part = read_vec(&r, 5000, 30000).unwrap();
assert_eq!(part, &logical[5000..35000]);
}
@@ -145,7 +145,7 @@ fn raid6_layouts_with_one_and_two_missing() {
#[test]
fn raid0_raid1_linear_and_raid10_near() {
let logical = data(C * 24);
// RAID 0 com 3 discos
// 3-disk RAID 0
let mut imgs = vec![vec![]; 3];
for (c, ch) in logical.chunks(C).enumerate() {
imgs[c % 3].extend_from_slice(ch);
@@ -158,7 +158,7 @@ fn raid0_raid1_linear_and_raid10_near() {
))
.unwrap();
assert_eq!(read_vec(&r, 0, logical.len()).unwrap(), logical);
// RAID 1, um membro caído
// 2-disk RAID 1 with first mirror missing
let m = vec![logical.clone(), logical.clone()];
let r = RaidSource::new(RaidParams::simple(
Level::R1,
@@ -178,7 +178,7 @@ fn raid0_raid1_linear_and_raid10_near() {
))
.unwrap();
assert_eq!(read_vec(&r, 0, logical.len()).unwrap(), logical);
// RAID 10 near=2 com 4 discos: slots em sequência.
// 4-disk RAID 10 (`near = 2`): sequential slot pairs.
let n = 4;
let mut imgs = vec![vec![0u8; C * 12]; n];
for (c, ch) in logical.chunks(C).enumerate() {
@@ -240,11 +240,11 @@ fn assembles_from_md_v12_superblocks_with_a_missing_disk() {
let n = 4;
let logical = data(C * 3 * 40);
let imgs = encode_parity(Level::R5, Layout::LeftSymmetric, n, &logical);
let data_off = 2048u64; // 1 MiB em setores
let data_off = 2048u64; // 1 MiB in 512-byte sectors
let mut members_in = vec![];
for (i, im) in imgs.iter().enumerate() {
if i == 2 {
continue; // disco perdido
continue; // simulate missing disk 2
}
let mut full = vec![0u8; 4096 + (data_off as usize) * 512 + im.len()];
let sb = md_v1_super(
@@ -260,7 +260,7 @@ fn assembles_from_md_v12_superblocks_with_a_missing_disk() {
);
full[4096..8192].copy_from_slice(&sb);
full[4096 + data_off as usize * 512..][..im.len()].copy_from_slice(im);
// o superbloco 1.2 fica a 4 KiB; os dados começam em data_offset a partir do início
// mdadm v1.2 superblock sits at 4 KiB; array data starts at `data_offset` sectors
let mut real = vec![0u8; data_off as usize * 512 + im.len()];
real[4096..8192].copy_from_slice(&sb);
real[data_off as usize * 512..].copy_from_slice(im);
+3 -3
View File
@@ -1,4 +1,4 @@
//! Descoberta automática dos parâmetros de um RAID sem metadados.
//! Automatic parameter detection for metadata-less RAID arrays.
use guara_core::raid::Level;
use guara_core::raid_detect;
@@ -69,7 +69,7 @@ fn finds_raid5_chunk_layout_and_shuffled_order() {
}
disks[pd][row * chunk..(row + 1) * chunk].copy_from_slice(&p);
}
// Entrega os discos embaralhados: fornecidos = [2, 0, 1] do array real.
// Pass member disks in scrambled order `[2, 0, 1]`.
let given: Vec<Arc<dyn Source>> = [2usize, 0, 1]
.iter()
.map(|&i| Arc::new(MemSource(disks[i].clone())) as Arc<dyn Source>)
@@ -85,7 +85,7 @@ fn finds_raid5_chunk_layout_and_shuffled_order() {
let best = &found[0];
assert_eq!(best.level, Level::R5);
assert_eq!(best.chunk, chunk as u64);
// Layouts e ordens diferentes podem ser equivalentes; o que importa é o conteúdo montado.
// Equivalent 3-disk parity permutations produce identical reconstructed volume bytes.
let r = guara_core::raid::RaidSource::new(best.params(&given)).unwrap();
let got = guara_core::source::read_vec(&r, 0, logical.len()).unwrap();
assert!(
+4 -4
View File
@@ -1,4 +1,4 @@
//! Recuperação de ponta a ponta sobre imagens reais (FAT32, NTFS, ext4).
//! End-to-end recovery tests across real filesystem images (FAT32, NTFS, ext4).
mod common;
use common::*;
@@ -25,7 +25,7 @@ fn check(fs: &str, only_carvable: bool) {
"[{fs}] não recuperado bit a bit: {path}"
);
}
// Os dois métodos juntos não podem listar o mesmo conteúdo duas vezes.
// 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() {
@@ -79,7 +79,7 @@ fn varredura_rapida_nao_faz_carving() {
..Default::default()
},
);
// O ext4 agora tem leitor de metadados (inodes e diário), mas a varredura rápida nunca faz carving.
// 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"),
@@ -97,7 +97,7 @@ fn varredura_rapida_nao_faz_carving() {
);
}
/// Regressão: mkfs.vfat -F 32 em mídia pequena (< 65525 clusters) é FAT32 pelo BPB.
/// 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;
+13 -13
View File
@@ -1,4 +1,4 @@
//! Assinatura digital: chaves, arquivos, adulteração, recuperação inteira e interoperabilidade.
//! Ed25519 digital signature tests: keygen, signing, tamper detection, recovery verification, and OpenSSL interop.
use guara_core::recover;
use guara_core::sign::{self, SigningKey};
@@ -30,7 +30,7 @@ fn resident(id: u64, name: &str, bytes: Vec<u8>) -> Found {
}
}
/// `recover` assina sozinho quando `GUARA_SIGNING_KEY` existe; testes que recuperam não se misturam.
/// Serializes tests that mutate `GUARA_SIGNING_KEY` / `GUARA_CONFIG_DIR`.
static ENV: std::sync::Mutex<()> = std::sync::Mutex::new(());
fn keyed(dir: &Path) -> SigningKey {
@@ -46,7 +46,7 @@ fn key_roundtrip_and_no_overwrite() {
let again = SigningKey::load(&dir.path().join("k.key")).unwrap();
assert_eq!(k.public_bytes(), again.public_bytes());
assert_eq!(k.fingerprint(), again.fingerprint());
assert_eq!(k.fingerprint().len(), 24); // 5 grupos de 4 + 4 separadores
assert_eq!(k.fingerprint().len(), 24); // 5 groups of 4 hex chars + 4 colons = 24 chars
let pubfile = std::fs::read_to_string(dir.path().join("k.pub")).unwrap();
assert!(pubfile.contains(&k.fingerprint()));
let e = SigningKey::generate()
@@ -55,7 +55,7 @@ fn key_roundtrip_and_no_overwrite() {
.unwrap()
.to_string();
assert!(e.contains("já existe uma chave"), "{e}");
// a chave antiga continua intacta
// existing private key is preserved untouched
assert_eq!(
SigningKey::load(&dir.path().join("k.key"))
.unwrap()
@@ -92,26 +92,26 @@ fn signed_file_verifies_and_tampering_is_caught() {
c.valid && c.trusted == Some(true) && c.signer.as_deref() == Some("Perita Ana"),
"{c:?}"
);
// fingerprint aceito em outro formato (maiúsculas, sem dois pontos)
// fingerprint comparison is case- and separator-insensitive
let loose = k.fingerprint().replace(':', " ").to_uppercase();
assert_eq!(
sign::verify_file(&f, None, Some(&loose)).unwrap().trusted,
Some(true)
);
// outra chave não é a esperada
// mismatched trusted fingerprint sets `trusted = Some(false)`
let other = SigningKey::generate().fingerprint();
let c = sign::verify_file(&f, None, Some(&other)).unwrap();
assert!(!c.valid && c.trusted == Some(false));
// arquivo alterado
// modifying the signed file invalidates the signature
std::fs::write(&f, b"conteudo ALTERADO do relatorio").unwrap();
let c = sign::verify_file(&f, None, None).unwrap();
assert!(!c.valid && !c.file_matches && c.signature_valid, "{c:?}");
std::fs::write(&f, b"conteudo do relatorio").unwrap();
assert!(sign::verify_file(&f, None, None).unwrap().valid);
// campos da assinatura alterados (hora, assinante, hash) invalidam
// tampering with any signed metadata field (timestamp, signer, sha256) invalidates verification
let raw = std::fs::read_to_string(&sig).unwrap();
for (from, to) in [
("Perita Ana", "Outra Pessoa"),
@@ -122,7 +122,7 @@ fn signed_file_verifies_and_tampering_is_caught() {
let c = sign::verify_file(&f, None, None).unwrap();
assert!(!c.valid && !c.signature_valid, "{from}: {c:?}");
}
// trocar a chave pública pela de outra pessoa também não passa
// replacing `public_key` with another keypair's public key fails verification
let mut j: serde_json::Value = serde_json::from_str(&raw).unwrap();
let evil = SigningKey::generate();
j["chave_publica"] = evil
@@ -164,7 +164,7 @@ fn recovery_folder_is_checked_file_by_file() {
"{c:?}"
);
// manifesto adulterado (hash trocado para acompanhar o arquivo alterado) quebra a assinatura
// updating the manifest hash to match a tampered file invalidates the manifest's Ed25519 signature
let m = std::fs::read_to_string(&manifest).unwrap();
let real = &rep.recovered[0].sha256;
let fake = sign::hash_file(&first).unwrap().0;
@@ -190,7 +190,7 @@ fn manifest_paths_cannot_escape_the_folder() {
assert!(!c.valid && c.changed[0].contains("fora da pasta"), "{c:?}");
}
/// O mesmo que o `openssl` faria: a assinatura é Ed25519 padrão sobre a mensagem documentada.
/// Verifies that our Ed25519 signature validates with external `openssl pkeyutl -verify -rawin`.
#[test]
fn signature_verifies_with_openssl() {
let Ok(v) = std::process::Command::new("openssl")
@@ -239,7 +239,7 @@ fn signature_verifies_with_openssl() {
])
.output()
.unwrap();
// OpenSSL 1.1 não tem -rawin para Ed25519; só exige sucesso onde ele existe
// OpenSSL 1.1 lacks `-rawin` for Ed25519; only assert when `-rawin` is supported
let text = String::from_utf8_lossy(&r.stdout).to_string() + &String::from_utf8_lossy(&r.stderr);
if text.contains("Unknown option") || text.contains("unknown option") {
return;
@@ -247,7 +247,7 @@ fn signature_verifies_with_openssl() {
assert!(r.status.success(), "{text}");
}
/// Com chave configurada, a recuperação e o relatório saem assinados sem ninguém pedir.
/// When `GUARA_SIGNING_KEY` is configured, `recover` and `report` auto-sign their outputs.
#[test]
fn configured_key_signs_manifest_automatically() {
let _g = ENV.lock().unwrap_or_else(|e| e.into_inner());
+10 -10
View File
@@ -1,5 +1,5 @@
//! Leitor de SQLite: esquema, linhas (com overflow e árvores de vários níveis),
//! registros apagados e robustez contra bancos danificados.
//! SQLite B-tree reader tests: schema parsing, multi-level B-trees with overflow pages,
//! deleted-row carving from freeblocks, and fuzz/truncation resilience.
use guara_core::source::MemSource;
use guara_core::sqlite::{is_sqlite, Cell, Db};
@@ -37,7 +37,7 @@ fn esquema_com_tabelas_indices_e_views() {
assert_eq!(s.indexes, ["idx_usuarios_email"]);
assert_eq!(s.views, ["maiores"]);
assert!(s.warnings.is_empty(), "{:?}", s.warnings);
assert_eq!(db.count_rows(u), 493); // 500 menos 7 apagados
assert_eq!(db.count_rows(u), 493); // 500 inserted rows minus 7 deleted rows
}
#[test]
@@ -47,17 +47,17 @@ fn linhas_em_arvore_de_varios_niveis_e_paginacao() {
let p = db.rows("usuarios", 0, 5).unwrap();
assert_eq!(p.total, 493);
assert_eq!(p.rows.len(), 5);
// o alias do rowid (INTEGER PRIMARY KEY) vem preenchido
// `INTEGER PRIMARY KEY` column (`id`) is populated from the B-tree cell `rowid`
assert!(matches!(p.rows[0].cells[0], Cell::Int(1)));
assert_eq!(text(&p.rows[0].cells[1]), "Pessoa 0001");
assert_eq!(text(&p.rows[0].cells[2]), "pessoa1@exemplo.com");
assert!(matches!(p.rows[0].cells[3], Cell::Int(19)));
assert!(matches!(p.rows[1].cells[4], Cell::Real(r) if (r - 3.0).abs() < 1e-9));
assert!(matches!(&p.rows[2].cells[5], Cell::Blob { len: 3, .. }));
// pula os apagados 101..103: a linha de índice 100 é o id 104
// rows 101..=103 were deleted, so row index 100 is `id = 104`
let q = db.rows("usuarios", 100, 2).unwrap();
assert!(matches!(q.rows[0].cells[0], Cell::Int(104)));
// última linha
// last row (`id = 500`)
let last = db.rows("usuarios", 492, 10).unwrap();
assert_eq!(last.rows.len(), 1);
assert_eq!(text(&last.rows[0].cells[1]), "Pessoa 0500");
@@ -114,13 +114,13 @@ fn recupera_registros_apagados_das_areas_livres() {
"só {achados} de 7 registros apagados recuperados: {nomes:?}"
);
assert!(d.rows.iter().all(|r| r.deleted));
// a tabela de notas não teve nada apagado
// the second table had no deleted rows
assert!(db.deleted_rows("notas").unwrap().rows.is_empty());
}
#[test]
fn banco_truncado_e_corrompido_nao_derruba_nem_trava() {
// metade do arquivo
// truncated to half the file size
let half = DB[..DB.len() / 2].to_vec();
let src = MemSource(half);
let db = Db::open(&src).unwrap();
@@ -132,11 +132,11 @@ fn banco_truncado_e_corrompido_nao_derruba_nem_trava() {
);
let p = db.rows("usuarios", 0, 1000).unwrap();
assert!(!p.rows.is_empty() && p.rows.len() < 493, "{}", p.rows.len());
// sem cabeçalho
// wiped SQLite header
let mut bad = DB.to_vec();
bad[0] = b'X';
assert!(Db::open(&MemSource(bad)).is_err());
// corrupção aleatória (semente fixa): nunca pode dar panic
// deterministic pseudo-random byte corruption: must never panic
let mut x: u64 = 0x1234_5678_9abc_def0;
for _ in 0..60 {
let mut d = DB.to_vec();
+52 -52
View File
@@ -1,10 +1,10 @@
//! Imagens de disco virtual: QCOW2, VMDK, VHD, VHDX e DMG.
//! Virtual disk image tests: QCOW2, VMDK, VHD, VHDX, and DMG.
//!
//! Os arquivos de `tests/fixtures/vdisk` foram feitos por ferramentas reais (VirtualBox para
//! VMDK/VHD, qemu para QCOW2, hdiutil para DMG) e conferidos contra o conteúdo bruto. VHDX, DMG
//! sintético e discos diferenciais são montados aqui, byte a byte, a partir da especificação.
//! As imagens do dfvfs em target/testdata (scripts/fetch-testdata.sh) são conferidas por hash,
//! calculado com um leitor independente (7-Zip), e pulam quando ausentes.
//! Fixtures in `tests/fixtures/vdisk` were created by real tools (`VBoxManage` for VMDK/VHD,
//! `qemu-img` for QCOW2, `hdiutil` for DMG) and verified against the raw disk image. VHDX,
//! synthetic DMG chunks, and differencing chains are constructed byte-for-byte from the specs.
//! External `dfvfs` images in `target/testdata` (`scripts/fetch-testdata.sh`) are verified by
//! SHA-256 against independent `7z` extractions and skipped when absent.
use guara_core::source::{open_path, read_vec, Source};
use sha2::{Digest, Sha256};
@@ -39,7 +39,7 @@ fn sha(src: &dyn Source) -> String {
h.finalize().iter().map(|b| format!("{b:02x}")).collect()
}
/// Lê o disco inteiro e também em pedaços que cruzam blocos, comparando com `want`.
/// Verifies both full-disk reads and unaligned cross-block reads against `want`.
fn check_content(path: &Path, want: &[u8]) {
let s = open_path(path).unwrap_or_else(|e| panic!("{}: {e}", path.display()));
assert_eq!(s.size(), want.len() as u64, "{}", path.display());
@@ -63,19 +63,19 @@ fn check_content(path: &Path, want: &[u8]) {
path.display()
);
}
// leitura além do fim devolve só o que existe
// reads extending past EOF return only the remaining bytes
let mut buf = [0u8; 64];
assert_eq!(s.read_at(want.len() as u64 - 10, &mut buf).unwrap(), 10);
assert_eq!(s.read_at(want.len() as u64, &mut buf).unwrap(), 0);
}
// ---------- imagens feitas por ferramentas reais ----------
// ---------- fixtures created by real tools ----------
#[test]
fn virtualbox_vmdk_sparse_and_stream_optimized() {
let want = raw();
check_content(&fx("disk.vmdk"), &want);
// streamOptimized: grãos comprimidos com zlib e cabeçalho no rodapé
// `streamOptimized` VMDK: deflate-compressed grains and footer descriptor
check_content(&fx("disk-stream.vmdk"), &want);
}
@@ -92,7 +92,7 @@ fn qemu_qcow2_and_hdiutil_dmg_match_a_reference_reader() {
sha(s.as_ref()),
"a6c2f0e39afe6c6ab432ca5465349fcefe8dc944398e97b2d957d3f89dbb5d80"
);
// UDIF com chunks zlib, zerados e ignorados
// UDIF DMG with zlib, zero-fill, and ignored chunks
let d = open_path(&fx("hfsplus_zlib.dmg")).unwrap();
assert_eq!(d.size(), 1_954_304);
assert_eq!(
@@ -101,7 +101,7 @@ fn qemu_qcow2_and_hdiutil_dmg_match_a_reference_reader() {
);
}
// ---------- montagem de imagens sintéticas ----------
// ---------- synthetic image builders ----------
fn be(v: &mut [u8], o: usize, x: u64, n: usize) {
v[o..o + n].copy_from_slice(&x.to_be_bytes()[8 - n..]);
@@ -122,7 +122,7 @@ fn zlib(data: &[u8]) -> Vec<u8> {
e.finish().unwrap()
}
/// QCOW2 v3 com clusters de 64 KiB: zerados (sem entrada e com a flag), brutos e comprimidos.
/// Builds a QCOW2 v3 image (64 KiB clusters) with unallocated, zero-flagged, raw, and compressed clusters.
fn build_qcow2(data: &[u8]) -> Vec<u8> {
const CS: usize = 1 << 16;
let clusters = data.len() / CS;
@@ -134,14 +134,14 @@ fn build_qcow2(data: &[u8]) -> Vec<u8> {
be(&mut img, 36, 1, 4);
be(&mut img, 40, CS as u64, 8);
be(&mut img, 100, 112, 4);
be(&mut img, CS, (2 * CS as u64) | (1 << 63), 8); // L1 -> L2 no cluster 2
be(&mut img, CS, (2 * CS as u64) | (1 << 63), 8); // L1 entry pointing to L2 table at cluster 2
for i in 0..clusters {
let c = &data[i * CS..(i + 1) * CS];
let entry: u64 = if c.iter().all(|&b| b == 0) {
if i % 2 == 0 {
0
} else {
1 // zerado explícito
1 // explicit zero-cluster flag (bit 0)
}
} else if i % 3 == 0 {
let z = deflate(c);
@@ -196,7 +196,7 @@ fn base64(d: &[u8]) -> String {
s
}
/// ADC só com literais e cópias de 3 bytes (deslocamento 8), como o `hdiutil` produz.
/// Encodes payload using ADC literal runs and 3-byte backreferences (offset 8).
fn adc_pattern(len: usize) -> (Vec<u8>, Vec<u8>) {
let pat = *b"ADC-test";
let mut enc = vec![0x80 | 7];
@@ -211,7 +211,7 @@ fn adc_pattern(len: usize) -> (Vec<u8>, Vec<u8>) {
if n >= 4 {
enc.extend_from_slice(&[0x40 | (n as u8 - 4), 0, 7]);
} else {
enc.extend_from_slice(&[((n as u8 - 3) << 2), 7]); // 2 bytes, comprimento 3
enc.extend_from_slice(&[((n as u8 - 3) << 2), 7]); // 2-byte match opcode, length 3
}
made += n;
}
@@ -301,7 +301,7 @@ fn qcow2_with_encryption_or_a_remote_backing_file_is_refused() {
assert!(e.contains("não é um arquivo local"), "{e}");
}
/// Filho QCOW2 que só aloca o cluster 1 (dados), marca o 2 como zerado e deixa o resto para o pai.
/// Builds a child QCOW2 that allocates cluster 1, zero-flags cluster 2, and leaves the rest to `backing`.
fn build_qcow2_child(backing: &str, fmt: Option<&str>, own: &[u8]) -> Vec<u8> {
const CS: usize = 1 << 16;
let mut img = vec![0u8; 3 * CS];
@@ -316,7 +316,7 @@ fn build_qcow2_child(backing: &str, fmt: Option<&str>, own: &[u8]) -> Vec<u8> {
be(&mut img, 40, CS as u64, 8);
be(&mut img, 100, 112, 4);
if let Some(f) = fmt {
// extensão 0xE2792ACA com o nome do formato do apoio, depois o fim das extensões
// header extension `0xE2792ACA` (backing file format name) followed by end-of-extensions (`0`)
be(&mut img, 112, 0xE279_2ACA, 4);
be(&mut img, 116, f.len() as u64, 4);
img[120..120 + f.len()].copy_from_slice(f.as_bytes());
@@ -326,7 +326,7 @@ fn build_qcow2_child(backing: &str, fmt: Option<&str>, own: &[u8]) -> Vec<u8> {
img.extend_from_slice(own);
img.resize(img.len().div_ceil(CS) * CS, 0);
be(&mut img, 2 * CS + 8, host | (1 << 63), 8); // cluster 1
be(&mut img, 2 * CS + 16, 1, 8); // cluster 2: zerado de propósito
be(&mut img, 2 * CS + 16, 1, 8); // cluster 2: explicitly zeroed (must not fall back to parent)
img
}
@@ -345,12 +345,12 @@ fn qcow2_backing_file_supplies_what_the_child_did_not_allocate() {
std::fs::write(&p, build_qcow2_child("base.raw", fmt, &own)).unwrap();
check_content(&p, &expect);
}
// com o pai em outro formato: o filho aponta para um QCOW2
// multi-hop backing chain: QCOW2 child -> QCOW2 parent -> raw base
std::fs::write(dir.path().join("base.qcow2"), build_qcow2(&base)).unwrap();
let p = dir.path().join("c.qcow2");
std::fs::write(&p, build_qcow2_child("base.qcow2", Some("qcow2"), &own)).unwrap();
check_content(&p, &expect);
// sem o pai, a abertura diz o que falta
// missing backing file produces a clear error naming the missing parent
std::fs::remove_file(dir.path().join("base.raw")).unwrap();
let e = open_path(&dir.path().join("a.qcow2"))
.err()
@@ -384,10 +384,10 @@ fn vmdk_descriptor_with_flat_and_zero_extents() {
check_content(&p, &expect);
}
/// VMDK esparso feito à mão: 16 grãos de 64 KiB; `grains[i]` = `None` (não alocado, vem do pai),
/// `Some(None)` (zerado de propósito) ou `Some(Some(dados))`.
/// Builds a sparse VMDK with sixteen 64 KiB grains: `None` = unallocated (falls back to parent),
/// `Some(None)` = zeroed grain (`GTE == 1`), or `Some(Some(data))` = allocated grain.
fn build_vmdk_sparse(descriptor: &str, grains: &[Option<Option<Vec<u8>>>]) -> Vec<u8> {
const GRAIN: usize = 128; // setores
const GRAIN: usize = 128; // capacity in 512-byte sectors
let mut img = vec![0u8; 26 * 512];
img[..4].copy_from_slice(b"KDMV");
le(&mut img, 4, 1, 4);
@@ -400,7 +400,7 @@ fn build_vmdk_sparse(descriptor: &str, grains: &[Option<Option<Vec<u8>>>]) -> Ve
le(&mut img, 56, 21, 8);
le(&mut img, 64, 26, 8);
img[512..512 + descriptor.len()].copy_from_slice(descriptor.as_bytes());
le(&mut img, 21 * 512, 22, 4); // diretório de grãos -> tabela no setor 22
le(&mut img, 21 * 512, 22, 4); // grain directory entry pointing to the grain table at sector 22
for (i, g) in grains.iter().enumerate() {
let entry = match g {
None => 0,
@@ -432,7 +432,7 @@ fn vmdk_child_reads_the_parent_for_unallocated_grains() {
let own = vec![0x37u8; 65536];
let mut grains: Vec<Option<Option<Vec<u8>>>> = vec![None; 16];
grains[3] = Some(Some(own.clone()));
grains[5] = Some(None); // zerado de propósito: não consulta o pai
grains[5] = Some(None); // explicitly zeroed grain (`GTE == 1`): does not fall back to parent
let child = build_vmdk_sparse(
"# Disk DescriptorFile\nversion=1\nCID=22222222\nparentCID=11111111\ncreateType=\"monolithicSparse\"\nparentFileNameHint=\"base.vmdk\"\n",
&grains,
@@ -445,17 +445,17 @@ fn vmdk_child_reads_the_parent_for_unallocated_grains() {
check_content(&p, &expect);
}
/// Snapshot real do VirtualBox: o filho só diz o UUID do pai (`ddb.uuid.parent`), sem nome.
/// VirtualBox snapshot VMDK where the child references its parent solely via `ddb.uuid.parent`.
#[test]
fn virtualbox_snapshot_vmdk_finds_its_parent_by_uuid() {
let dir = tempfile::tempdir().unwrap();
let sub = dir.path().join("Snapshots");
std::fs::create_dir(&sub).unwrap();
// o pai fica na pasta de cima, como o VirtualBox organiza a máquina
// parent VMDK sits in the parent directory, matching VirtualBox's `Snapshots/` layout
std::fs::copy(fx("disk.vmdk"), dir.path().join("disco-base.vmdk")).unwrap();
std::fs::copy(fx("vbox-snapshot.vmdk"), sub.join("filho.vmdk")).unwrap();
check_content(&sub.join("filho.vmdk"), &raw());
// sem o pai por perto, a abertura explica
// when no VMDK with the parent UUID exists nearby, opening returns a descriptive error
std::fs::remove_file(dir.path().join("disco-base.vmdk")).unwrap();
let e = open_path(&sub.join("filho.vmdk"))
.err()
@@ -516,7 +516,7 @@ fn vhd_fixed_hides_the_footer() {
check_content(&p, &want);
}
/// VHD diferencial: a filha só traz o setor 100 e o bloco inteiro do fim; o resto vem do pai.
/// Builds a differencing VHD where only sector 100 of block 0 and all of block 1 are present in the child.
#[test]
fn vhd_differencing_reads_the_parent_for_missing_sectors() {
let want = raw();
@@ -525,7 +525,7 @@ fn vhd_differencing_reads_the_parent_for_missing_sectors() {
parent.extend_from_slice(&vhd_footer(want.len() as u64, 2, u64::MAX));
std::fs::write(dir.path().join("pai.vhd"), &parent).unwrap();
const BLOCK: usize = 1 << 20; // um bloco só
const BLOCK: usize = 1 << 20; // single 2 MiB block
let mut f = vec![0u8; 512 + 1024 + 512];
let hdr = vhd_footer(want.len() as u64, 4, 512);
f[..512].copy_from_slice(&hdr);
@@ -541,9 +541,9 @@ fn vhd_differencing_reads_the_parent_for_missing_sectors() {
.collect::<Vec<u8>>();
dh[64..64 + name.len()].copy_from_slice(&name);
f[512..1536].copy_from_slice(&dh);
be(&mut f, 1536, 4, 4); // bloco 0 no setor 4
be(&mut f, 1536, 4, 4); // block 0 starts at sector 4
f.resize(4 * 512, 0);
// bitmap (4096 setores = 512 bytes) com o setor 100 presente, depois os dados
// 512-byte sector bitmap (4096 sectors) with only sector 100 marked present, followed by data
let mut bitmap = vec![0u8; 512];
bitmap[100 / 8] |= 0x80 >> (100 % 8);
f.extend_from_slice(&bitmap);
@@ -559,7 +559,7 @@ fn vhd_differencing_reads_the_parent_for_missing_sectors() {
expect[100 * 512..101 * 512].copy_from_slice(&marker);
check_content(&p, &expect);
// sem o pai a abertura falha com o motivo
// missing parent VHD returns a descriptive error
std::fs::remove_file(dir.path().join("pai.vhd")).unwrap();
let e = open_path(&p).err().expect("deveria recusar").to_string();
assert!(e.contains("disco pai não encontrado"), "{e}");
@@ -584,8 +584,8 @@ fn guid(s: &str) -> [u8; 16] {
const MB: usize = 1 << 20;
/// VHDX com blocos de 1 MiB. `blocks[i]`: `None` = não presente, `Some((estado, dados))`.
/// Com `parent`, acrescenta o localizador de pai e o bitmap de setores (`bitmap`).
/// Builds a VHDX image with 1 MiB blocks (`None` = not present, `Some((state, data))`).
/// When `parent` is provided, writes a VHDX parent locator and sector bitmap block.
fn build_vhdx(
size: u64,
blocks: &[Option<(u64, Vec<u8>)>],
@@ -596,7 +596,7 @@ fn build_vhdx(
img[..8].copy_from_slice(b"vhdxfile");
img[64 * 1024..64 * 1024 + 4].copy_from_slice(b"head");
img[128 * 1024..128 * 1024 + 4].copy_from_slice(b"head");
// tabela de regiões: BAT em 2 MiB, metadados em 1 MiB
// region table: BAT at 2 MiB, metadata region at 1 MiB
let rt = 192 * 1024;
img[rt..rt + 4].copy_from_slice(b"regi");
le(&mut img, rt + 8, 2, 4);
@@ -606,7 +606,7 @@ fn build_vhdx(
img[rt + 48..rt + 64].copy_from_slice(&guid("8B7CA206-4790-4B9A-B8FE-575F050F886E"));
le(&mut img, rt + 64, MB as u64, 8);
le(&mut img, rt + 72, MB as u64, 4);
// metadados
// metadata region items
let m = MB;
img[m..m + 8].copy_from_slice(b"metadata");
let mut items: Vec<([u8; 16], Vec<u8>)> = vec![];
@@ -650,7 +650,7 @@ fn build_vhdx(
img[m + at..m + at + data.len()].copy_from_slice(data);
at += data.len().div_ceil(8) * 8;
}
// blocos de dados a partir de 4 MiB; o BAT guarda estado e deslocamento (em MiB)
// payload blocks start at 4 MiB; BAT entries encode state in low 3 bits and file offset
let ratio = (1usize << 23) * 512 / MB;
let bat = 2 * MB;
for (i, b) in blocks.iter().enumerate() {
@@ -681,7 +681,7 @@ fn build_vhdx(
#[test]
fn vhdx_dynamic_with_zero_and_missing_blocks() {
let want = raw(); // 1 MiB = um bloco
let want = raw(); // 1 MiB = single payload block
let mut expect = want.clone();
expect.extend_from_slice(&vec![0u8; 3 * MB]);
let tail = vec![0x5Au8; 100_000];
@@ -690,8 +690,8 @@ fn vhdx_dynamic_with_zero_and_missing_blocks() {
4 * MB as u64,
&[
Some((6, want.clone())),
Some((2, vec![])), // zerado
None, // não presente
Some((2, vec![])), // `PAYLOAD_BLOCK_ZERO`
None, // `PAYLOAD_BLOCK_NOT_PRESENT`
Some((6, tail)),
],
None,
@@ -708,7 +708,7 @@ fn vhdx_differencing_uses_the_sector_bitmap() {
let want = raw();
let dir = tempfile::tempdir().unwrap();
std::fs::write(dir.path().join("pai.raw"), &want).unwrap();
// filho: bloco 0 parcialmente presente, só com os setores 3 e 2000..2010 no filho
// differencing child: block 0 is `PAYLOAD_BLOCK_PARTIALLY_PRESENT` with sectors 3 and 2000..2010 in child
let mut data = vec![0u8; MB];
let mut bm = vec![0u8; 512];
let mut expect = want.clone();
@@ -729,7 +729,7 @@ fn vhdx_differencing_uses_the_sector_bitmap() {
fn dmg_with_every_chunk_type() {
let want = raw();
let sec = 512usize;
// 8 chunks de 128 setores (64 KiB)
// 8 chunks of 128 sectors (64 KiB each)
let part = |i: usize| want[i * 128 * sec..(i + 1) * 128 * sec].to_vec();
let (adc_enc, adc_want) = adc_pattern(128 * sec);
let mut bz = bzip2::write::BzEncoder::new(vec![], bzip2::Compression::default());
@@ -739,12 +739,12 @@ fn dmg_with_every_chunk_type() {
ty: 1,
sectors: 128,
payload: part(0),
}, // bruto
}, // raw uncompressed chunk (`0x00000001`)
DmgChunk {
ty: 0,
sectors: 128,
payload: vec![],
}, // zerado
}, // `PAYLOAD_BLOCK_ZERO`
DmgChunk {
ty: 0x8000_0005,
sectors: 128,
@@ -764,7 +764,7 @@ fn dmg_with_every_chunk_type() {
ty: 2,
sectors: 128,
payload: vec![],
}, // ignorado
}, // ignored/comment chunk
DmgChunk {
ty: 0x8000_0005,
sectors: 128,
@@ -785,8 +785,8 @@ fn dmg_with_every_chunk_type() {
check_content(&p, &expect);
}
/// Chunks `ULFO` com dados compactados pelo compressor de referência da Apple (`lzfse`):
/// um bloco `bvx2` (FSE, 64 KiB) e um pequeno (`bvxn`/LZVN).
/// DMG with `ULFO` (LZFSE) chunks compressed by Apple's reference `lzfse` implementation:
/// one 64 KiB `bvx2` (FSE) block and one small `bvxn` (LZVN) block.
#[test]
fn dmg_lzfse_chunks_from_the_reference_compressor() {
let big = std::fs::read(fx("lzfse-64k.bin")).unwrap();
@@ -835,11 +835,11 @@ fn dmg_with_a_corrupt_lzfse_chunk_and_encrypted_are_refused_with_a_reason() {
assert!(e.contains("cifrado"), "{e}");
}
// ---------- imagens reais do dfvfs (target/testdata) ----------
// ---------- external `dfvfs` reference images (`target/testdata`) ----------
#[test]
fn dfvfs_images_match_a_reference_reader() {
// hashes do disco virtual inteiro, conferidos com o 7-Zip (árvore de arquivos e partições)
// full virtual disk SHA-256 digests cross-checked against `7z` extraction
let table: &[(&str, &str)] = &[
(
"ext2.vhdx",
+20 -20
View File
@@ -1,5 +1,5 @@
//! XFS: imagem real do mkfs.xfs + exclusão simulada (inode liberado e entrada de diretório
//! liberada, como o kernel deixa) e a imagem de referência do dfvfs.
//! XFS: real `mkfs.xfs` image + simulated unlink (freed inode with leftover extents and freed
//! directory entry, matching XFS v4/unwiped behavior) plus the `dfvfs` reference image.
mod common;
use common::*;
@@ -135,19 +135,19 @@ fn deleted_file_is_recovered_from_free_inode_and_stale_directory_entry() {
let mut b = std::fs::read(&img).unwrap();
let bs = 4096usize;
// extents do arquivo (no inode, formato 3)
// read the file's inline extent list before simulating deletion
assert_eq!(b[ino_off + 5], 2);
let nex = be32(&b[ino_off..], 76) as usize;
let exts: Vec<_> = (0..nex)
.map(|i| unpack(&b[ino_off + 176 + i * 16..]))
.collect();
assert!(exts.iter().map(|e| e.2).sum::<u64>() * 4096 >= 150_000);
// 1) libera o inode como o kernel: modo 0, links 0, tamanho 0, 0 extents
// 1) free the inode (mode = 0, nlink = 0, size = 0, nextents = 0) while leaving fork bytes
b[ino_off + 2..ino_off + 4].fill(0);
b[ino_off + 16..ino_off + 20].fill(0);
b[ino_off + 56..ino_off + 64].fill(0);
b[ino_off + 76..ino_off + 80].fill(0);
// 2) marca livre na árvore de inodes (AGI -> raiz folha)
// 2) mark the inode bit as free in the AGI `inobt` root leaf
let agno = (ino >> (agblklog + inopblog)) as usize;
let ag_off = agno * agblocks as usize * bs;
let agi = &b[ag_off + 2 * 512..ag_off + 2 * 512 + 64];
@@ -169,7 +169,7 @@ fn deleted_file_is_recovered_from_free_inode_and_stale_directory_entry() {
}
}
assert!(done);
// 3) devolve os blocos à árvore de espaço livre (folha única)
// 3) return the file's data blocks to the AGF `bnobt` free-space B+tree leaf
let agf = &b[ag_off + 512..ag_off + 512 + 64].to_vec();
let (broot, blevel) = (be32(agf, 16) as usize, be32(agf, 28));
assert_eq!(blevel, 1);
@@ -199,7 +199,7 @@ fn deleted_file_is_recovered_from_free_inode_and_stale_directory_entry() {
b[bb + 56 + i * 8 + 4..bb + 56 + i * 8 + 8].copy_from_slice(&r.1.to_be_bytes());
}
b[bb + 6..bb + 8].copy_from_slice(&(merged.len() as u16).to_be_bytes());
// 4) entrada de diretório liberada: perde os 4 primeiros bytes do inode (viram marca + tamanho)
// 4) mark the directory entry freed: overwrite the upper 4 bytes of its 8-byte inode with `(0xFFFF, length)`
let name = b"relatorio.dat";
let pos = (0..b.len() - 20)
.find(|&p| {
@@ -241,10 +241,10 @@ fn deleted_file_is_recovered_from_free_inode_and_stale_directory_entry() {
assert!(rep.failed.is_empty());
let rp = PathBuf::from(&rep.recovered[0].path);
let got = std::fs::read(if rp.is_absolute() { rp } else { out.join(rp) }).unwrap();
// O tamanho exato se perde na exclusão: o arquivo vem com os blocos inteiros (150000 bytes + preenchimento).
// Exact byte size was zeroed on deletion: recovered size is rounded up to full filesystem blocks.
assert!(got.starts_with(&doc), "conteúdo inicial difere");
assert_eq!(got.len() % 4096, 0);
// Só um arquivo apagado.
// Only the deleted file is reported (live files are excluded).
assert_eq!(
c.found.iter().filter(|f| f.origin == Origin::Xfs).count(),
1
@@ -269,14 +269,14 @@ fn reference_image_is_recognized() {
assert_eq!(l.partitions[0].fs, FsKind::Xfs);
}
/// Meta: exclusão feita pelo kernel (scripts/make-xfs-real-deletion.sh, precisa de root): cada
/// arquivo apagado volta pelo nome, na pasta certa, com o conteúdo original no começo, e os que
/// ficaram não aparecem como apagados.
/// Target goal for kernel-level XFS v5 unlink (`scripts/make-xfs-real-deletion.sh`, requires root):
/// each deleted file should be recovered with its original name, folder, and content without
/// reporting live files as deleted.
///
/// Currently `#[ignore]` because on XFS v5 the Linux kernel zeroes the inode fork on unlink,
/// so recovering filenames and extents from a real XFS v5 unlink requires parsing the XFS
/// journal log. Run with `cargo test -p guara-core --test xfs -- --ignored`.
///
/// HOJE FALHA, e é por isso que está `ignore`: num XFS v5 real o kernel zera o inode e a lista de
/// extensões ao apagar, então só a simulação (teste acima) deixa a metadados para ler. Recuperar
/// nome e conteúdo de um XFS real exige ler o diário (log) do sistema de arquivos. Rodar com
/// `cargo test -p guara-core --test xfs -- --ignored` para ver o estado.
#[test]
#[ignore = "XFS real: o kernel apaga os metadados; falta ler o diário (ver ROADMAP)"]
fn real_kernel_deletion_names_are_recovered() {
@@ -358,7 +358,7 @@ fn real_kernel_deletion_names_are_recovered() {
out.path().join(name).join(rp)
})
.unwrap();
// o tamanho exato se perde na exclusão: o arquivo vem com os blocos inteiros
// exact byte size is lost on unlink: recovered size is rounded up to full blocks
assert!(
got.len() as u64 >= *size,
"{rel}: veio menor ({} < {size})",
@@ -372,9 +372,9 @@ fn real_kernel_deletion_names_are_recovered() {
}
}
/// O que funciona hoje num XFS apagado de verdade: o conteúdo dos blocos livres continua lá e a
/// varredura profunda recupera, por carving, o arquivo com estrutura reconhecível (SQLite), com
/// o conteúdo idêntico ao original. O nome e a pasta se perdem.
/// What works today on a real kernel-unlinked XFS v5 image: the freed data blocks remain intact
/// on disk and deep scan recovers structured files (such as SQLite) via signature carving with
/// bit-identical content (though original filename and folder path are lost).
#[test]
fn real_kernel_deletion_content_is_recovered_by_carving() {
let dir =