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
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:
1 parent
da212e6926
commit
6328220c60
129 files changed
+1961
-1834
No files matched your search
@@ -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!(
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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"));
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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,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;
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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!(
|
||||
|
||||
@@ -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}");
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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!(
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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 =
|
||||
|
||||
Reference in new issue
Block a user