chore(i18n): finish translating code comments and scripts to English
CI / Format and clippy (push) Successful in 1m57s
CI / Dependency audit (push) Successful in 13s
CI / Check desktop app (Linux) (push) Successful in 3m54s
CI / Tests (Linux) (push) Failing after 7m25s
Release / Create release (push) Successful in 6s
Release / CLI (linux-aarch64) (push) Successful in 1m48s
Release / CLI (linux-x86_64) (push) Successful in 2m39s
Release / Desktop app (linux-aarch64) (push) Successful in 10m9s
Release / Desktop app (linux-x86_64) (push) Successful in 12m20s
Release / Desktop app (windows) (push) Successful in 7m32s
Release / Build latest.json (push) Successful in 5s

Completes the English-only standardization for code, comments, scripts
and CI templates (UI/CLI-facing strings stay bilingual pt-BR/en,
untouched). Covers the remaining crates/, apps/gui/ui/ and
apps/gui/src-tauri/ comments, .gitea/ workflow and template comments,
.githooks/ hooks, and the scripts/ source comments for the files
already renamed in a prior commit (check.sh, generate-logo.py,
install-hooks.sh).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VAyr8d7HuAdUGB6aA5xmDD
This commit is contained in:
Lucas RayanandClaude Sonnet 5 committed 2026-10-30 20:43:02 -03:00
1 parent da212e6926
commit 6328220c60
129 files changed
+1961 -1834

No files matched your search

+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "guara-core"
description = "Núcleo de recuperação de arquivos do Guará (somente leitura)"
description = "Guará read-only file and volume recovery core"
version.workspace = true
edition.workspace = true
license.workspace = true
+18 -18
View File
@@ -1,8 +1,8 @@
//! AFF4 (Advanced Forensic Format 4, padrão v1.0): contêiner ZIP com metadados em Turtle.
//! AFF4 (Advanced Forensic Format 4, v1.0 standard): ZIP container with Turtle RDF metadata.
//!
//! Leitura de imagens lineares e mapeadas (inclusive esparsas), com blocos comprimidos por
//! snappy, deflate, LZ4 ou sem compressão. Gravação de uma imagem linear com blocos snappy.
//! Conjuntos em vários volumes (spanned/striped) não são suportados.
//! Reads linear and mapped images (including sparse ranges), with chunks compressed via
//! snappy, deflate, LZ4, or stored uncompressed. Writes linear images with snappy chunks.
//! Multi-volume sets (spanned/striped) are not supported.
use crate::error::{Error, Result};
use crate::hashing::MultiHasher;
@@ -15,7 +15,7 @@ use std::path::Path;
use std::sync::Mutex;
pub fn is_aff4(head: &[u8]) -> bool {
// ZIP local file header + primeiro nome "container.description" ou "version.txt".
// ZIP local file header + first entry named "container.description" or "version.txt".
head.len() > 50
&& head.starts_with(b"PK\x03\x04")
&& (find(&head[30..head.len().min(80)], b"container.description")
@@ -80,7 +80,7 @@ impl Turtle<'_> {
}
tok.to_string()
}
/// Um termo: <iri>, "literal"[^^tipo], nome:prefixado ou `a`.
/// A Turtle term: <iri>, "literal"[^^type], prefixed:name, or `a`.
fn term(&mut self) -> Option<String> {
self.ws();
let c = *self.b.get(self.i)?;
@@ -123,7 +123,7 @@ impl Turtle<'_> {
{
self.i += 1;
}
// ponto final de sentença colado ao termo
// trailing statement period attached to the term
let mut e = self.i;
if e > s && self.b[e - 1] == b'.' {
e -= 1;
@@ -165,7 +165,7 @@ fn parse_turtle(text: &str) -> HashMap<String, Resource> {
t.prefixes.insert(name, iri);
continue;
}
// sujeito
// subject
let Some(subj) =
(if t.b[t.i] == b':' && t.b.get(t.i + 1).map_or(true, |c| c.is_ascii_whitespace()) {
t.i += 1;
@@ -200,7 +200,7 @@ fn parse_turtle(text: &str) -> HashMap<String, Resource> {
Some(b';') => {
t.i += 1;
t.ws();
// ";" final antes de "."
// trailing ";" before "."
if t.b.get(t.i) == Some(&b'.') {
t.i += 1;
break;
@@ -217,7 +217,7 @@ fn parse_turtle(text: &str) -> HashMap<String, Resource> {
out
}
// ------------------------------------------------------------------ leitor
// ------------------------------------------------------------------ reader
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum Method {
@@ -265,7 +265,7 @@ pub struct Aff4Source {
targets: Vec<Target>,
map: Vec<MapEntry>,
size: u64,
/// Metadados legíveis: caso, examinador, notas, hashes.
/// Human-readable metadata: case, examiner, notes, hashes.
pub info: HashMap<String, String>,
cache: Mutex<Option<(usize, u64, Vec<u8>)>>,
index_cache: Mutex<HashMap<(usize, u64), std::sync::Arc<Vec<u8>>>>,
@@ -344,7 +344,7 @@ impl Aff4Source {
}
}
let data_urn = image.get(&ds).unwrap().to_string();
// Hashes gravados no fluxo de dados (ou no mapa).
// Hashes recorded on the data stream (or map).
for r in res
.values()
.filter(|r| r.get(&format!("{NS}hash")).is_some())
@@ -443,7 +443,7 @@ impl Aff4Source {
})
}
/// MD5 do primeiro fluxo de blocos (o que o AFF4 grava em `aff4:hash`), para conferir a integridade.
/// MD5 of the first bevy stream (stored by AFF4 in `aff4:hash`) for integrity verification.
pub fn verify_stream_md5(&self) -> Result<Option<bool>> {
let Some(want) = self.info.get("md5").cloned() else {
return Ok(None);
@@ -604,7 +604,7 @@ impl Source for Aff4Source {
let pos = offset + done as u64;
let i = self.map.partition_point(|e| e.start + e.len <= pos);
let Some(e) = self.map.get(i).filter(|e| e.start <= pos) else {
// vão do mapa: zeros até a próxima entrada
// map gap: zeros up to the next entry
let next = self.map.get(i).map(|e| e.start).unwrap_or(self.size);
let n = ((next - pos) as usize).min(want - done);
buf[done..done + n].fill(0);
@@ -626,7 +626,7 @@ impl Source for Aff4Source {
}
}
// ------------------------------------------------------------------ gravação
// ------------------------------------------------------------------ writer
#[derive(Debug, Clone)]
pub struct Aff4Options {
@@ -703,7 +703,7 @@ fn esc(s: &str) -> String {
.replace('\n', "\\n")
}
/// Grava uma imagem AFF4 linear (um fluxo de blocos snappy) de `src`.
/// Writes a linear AFF4 image (single snappy bevy stream) from `src`.
pub fn write_aff4(
src: &dyn Source,
dest: &Path,
@@ -776,11 +776,11 @@ pub fn write_aff4(
}
}
hasher.update(b);
// bloco curto (último) é guardado como está; comprimido só se ficar menor
// short (final) chunk is stored as-is; compressed only if smaller
let comp = enc_snap
.compress_vec(b)
.map_err(|e| Error::msg(e.to_string()))?;
// Só um bloco cheio pode ficar sem compressão (comprimento == tamanho do bloco).
// Only a full chunk can be stored uncompressed (length == chunk_size).
let data: &[u8] = if want as u64 == chunk && comp.len() >= want {
b
} else {
+27 -27
View File
@@ -1,6 +1,6 @@
//! Monta o disco virtual a partir do que o usuário informou: membros extras (RAID/LVM) e
//! senhas ou chaves para volumes cifrados. É a única porta de entrada para varrer e recuperar
//! discos que não são "simples".
//! Assembles a virtual disk from user-supplied inputs: extra members (RAID/LVM) and
//! passwords or keys for encrypted volumes. Single entry point for scanning and recovering
//! non-trivial disk layouts.
use crate::crypt::{self, Secret, UnlockError};
use crate::error::{Error, Result};
@@ -21,17 +21,17 @@ pub enum SecretInput {
Recovery {
value: String,
},
/// Caminho de um arquivo de chave (.BEK do BitLocker, keyfile do LUKS).
/// Path to a key file (BitLocker .BEK, LUKS keyfile).
Keyfile {
path: String,
},
/// Chave do volume em hexadecimal.
/// Hex-encoded volume master key.
Key {
hex: String,
},
}
/// Chave de volume guardada na sessão (só quando o usuário pede para lembrar).
/// Volume key saved in the session (only when the user opts in to remember keys).
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct SavedKey {
pub start: u64,
@@ -39,29 +39,29 @@ pub struct SavedKey {
pub key_hex: String,
}
/// RAID sem metadados: o que o usuário sabe; o resto é descoberto.
/// Metadata-less RAID parameters: user-supplied overrides; missing fields are auto-detected.
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
pub struct RaidSpec {
pub level: Option<crate::raid::Level>,
/// Tamanho da faixa em KiB.
/// Chunk (stripe) size in KiB.
pub chunk_kb: Option<u64>,
pub layout: Option<crate::raid::Layout>,
/// Ordem dos discos: índice 0 é o alvo principal, depois `members` na ordem dada.
/// Disk order: index 0 is the primary target, followed by `members` in the given order.
pub order: Option<Vec<usize>>,
}
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
pub struct Assembly {
/// Trata o alvo e `members` como discos de um RAID sem metadados (parâmetros descobertos).
/// Treat target and `members` as a metadata-less RAID array (parameters auto-detected).
#[serde(default)]
pub raid: Option<RaidSpec>,
/// Outros discos ou imagens do mesmo conjunto (RAID/LVM), além do alvo principal.
/// Additional disks or images belonging to the same set (RAID/LVM) besides the primary target.
#[serde(default)]
pub members: Vec<String>,
/// Senhas e chaves: o `snapshot` da sessão as descarta antes de gravar.
/// Passwords and keys: stripped by session `snapshot` before saving to disk.
#[serde(default)]
pub secrets: Vec<SecretInput>,
/// Guarda as chaves de volume derivadas na sessão, para reabrir sem pedir a senha de novo.
/// Persist derived volume keys in the session so it can be reopened without re-prompting.
#[serde(default)]
pub remember_keys: bool,
#[serde(default)]
@@ -96,13 +96,13 @@ pub struct Assembled {
pub source: Arc<dyn Source>,
pub unlocked: Vec<UnlockInfo>,
pub locked: Vec<LockedInfo>,
/// Chaves derivadas, para guardar na sessão se o usuário permitir.
/// Derived keys to persist in the session when permitted by the user.
pub keys: Vec<SavedKey>,
/// Volumes lógicos / partições extras que não estão na tabela do disco.
/// Logical volumes / extra partitions not present in the physical partition table.
pub extra_parts: Vec<PartInfo>,
/// Partições reais que são só contêineres (membros de RAID/LVM já remontados).
/// Physical partitions that act solely as containers (reassembled RAID/LVM members).
pub covered: Vec<u64>,
/// O disco principal é só um membro de RAID: não varrer o disco cru.
/// Primary disk is merely a RAID member: skip scanning the raw disk directly.
pub skip_primary: bool,
pub notes: Vec<String>,
}
@@ -145,7 +145,7 @@ fn try_unlock(window: Arc<dyn Source>, fs: FsKind, secrets: &[Secret]) -> crypt:
}
}
/// Abre `path` e aplica a montagem pedida.
/// Opens `path` and applies the requested assembly.
pub fn assemble(path: &str, asm: &Assembly) -> Result<Assembled> {
let base = open_path(Path::new(path)).map_err(|e| Error::msg(format!("{path}: {e}")))?;
assemble_source(base, asm)
@@ -153,7 +153,7 @@ pub fn assemble(path: &str, asm: &Assembly) -> Result<Assembled> {
const MIB: u64 = 1 << 20;
/// Coloca `src` em `start`; se cair dentro de uma região virtual, a substitui por dentro.
/// Maps `src` at `start`; if it falls inside an existing virtual region, replaces it nestedly.
fn patch(regions: &mut Vec<Region>, start: u64, src: Arc<dyn Source>, label: String) {
let len = src.size();
if let Some(r) = regions
@@ -181,10 +181,10 @@ fn patch(regions: &mut Vec<Region>, start: u64, src: Arc<dyn Source>, label: Str
});
}
/// Candidato a membro de RAID/LVM: uma janela de disco (inteiro ou partição).
/// Candidate RAID/LVM member: a disk window (whole disk or partition).
struct Window {
src: Arc<dyn Source>,
/// Início no espaço do disco virtual, se a janela faz parte dele.
/// Start offset in the virtual disk address space, if part of it.
primary_start: Option<u64>,
}
@@ -248,7 +248,7 @@ pub fn assemble_source(base: Arc<dyn Source>, asm: &Assembly) -> Result<Assemble
}
let mut done_containers: std::collections::BTreeSet<String> = Default::default();
let mut tried_pool = false;
// Rodadas: o que foi aberto/montado pode conter outro volume cifrado, RAID ou LVM.
// Passes: newly unlocked/assembled volumes may themselves contain encrypted volumes, RAID, or LVM.
for round in 0..4 {
let current: Arc<dyn Source> = Arc::new(Overlay::new(base.clone(), regions.clone()));
let layout = partition::analyze(current.as_ref(), false);
@@ -260,7 +260,7 @@ pub fn assemble_source(base: Arc<dyn Source>, asm: &Assembly) -> Result<Assemble
}
let mut progress = false;
// 1) RAID e LVM: reúne janelas do disco principal e dos membros extras.
// 1) RAID and LVM: gather windows from the primary disk and extra members.
if round == 0 || !tried_pool || !out.extra_parts.is_empty() {
let mut pool: Vec<Window> = vec![];
pool.push(Window {
@@ -381,7 +381,7 @@ pub fn assemble_source(base: Arc<dyn Source>, asm: &Assembly) -> Result<Assemble
}
}
// 2) Desbloqueio de volumes cifrados.
// 2) Unlock encrypted volumes.
let current: Arc<dyn Source> = Arc::new(Overlay::new(base.clone(), regions.clone()));
let layout = partition::analyze(current.as_ref(), false);
let mut all_parts: Vec<PartInfo> = layout.partitions.clone();
@@ -438,7 +438,7 @@ pub fn assemble_source(base: Arc<dyn Source>, asm: &Assembly) -> Result<Assemble
}
if !regions.is_empty() {
out.source = Arc::new(Overlay::new(base, regions));
// Atualiza o que se sabe sobre as partições virtuais (algumas foram desbloqueadas depois).
// Refresh virtual partition metadata (some volumes may have been unlocked in a later pass).
for p in &mut out.extra_parts {
if let Some((fs, len)) = partition::detect_fs(out.source.as_ref(), p.start) {
p.fs = fs;
@@ -454,7 +454,7 @@ pub fn assemble_source(base: Arc<dyn Source>, asm: &Assembly) -> Result<Assemble
Ok(out)
}
/// Anexa um volume virtual depois do fim do disco e registra as partições que ele contém.
/// Appends a virtual volume past the end of the disk and registers its inner partitions.
fn add_virtual(
regions: &mut Vec<Region>,
next: &mut u64,
@@ -496,7 +496,7 @@ fn add_virtual(
});
}
/// Escolhe os parâmetros do RAID: o que o usuário deu manda; o resto vem da detecção.
/// Resolves RAID parameters: user overrides take precedence; remaining fields come from detection.
fn pick_raid(
disks: &[Arc<dyn Source>],
spec: &RaidSpec,
+23 -23
View File
@@ -1,13 +1,13 @@
//! Trilha de auditoria: cada ação do examinador (abrir uma origem, varrer, recuperar, criar
//! imagem, exportar) vira uma linha JSON com quem, quando e o quê.
//! Audit trail: each examiner action (opening a source, scanning, recovering, imaging,
//! exporting) is recorded as a JSON line with who, when, and what.
//!
//! As linhas são encadeadas por hash: o `hash` de cada uma é o SHA-256 do `hash` da anterior
//! seguido do corpo da própria linha. Alterar ou apagar uma linha do meio quebra a cadeia e
//! `guara audit --verify` aponta onde. Cortar o fim do arquivo não é detectável sozinho; para
//! isso, guarde o hash da última entrada (a "cabeça") fora da máquina, por exemplo no relatório.
//! Entries are hash-chained: each entry's `hash` is the SHA-256 of the previous `hash`
//! followed by the entry body itself. Modifying or deleting an intermediate line breaks the chain
//! and `guara audit --verify` pinpoints where. Tail truncation cannot be detected from the file
//! alone; store the last entry hash (the "head") off-machine, e.g. in the forensic report.
//!
//! O arquivo fica na pasta de configuração (`auditoria.jsonl`) ou onde `GUARA_AUDIT_LOG`
//! apontar. Segredos (senhas, chaves) nunca entram no registro.
//! Stored in the config directory (`auditoria.jsonl`) or at `GUARA_AUDIT_LOG`.
//! Secrets (passwords, keys) are never written to the log.
use crate::error::{Error, Result};
use serde::{Deserialize, Serialize};
@@ -32,11 +32,11 @@ struct Body<'a> {
prev: &'a str,
}
/// Uma linha da trilha.
/// A single audit log entry.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Entry {
pub seq: u64,
/// UTC, ISO 8601.
/// UTC timestamp in ISO 8601 format.
pub time: String,
pub user: String,
pub host: String,
@@ -49,14 +49,14 @@ pub struct Entry {
pub hash: String,
}
/// Resultado de conferir a cadeia.
/// Result of verifying the hash chain.
#[derive(Debug, Clone, Serialize)]
pub struct Verification {
pub entries: usize,
pub ok: bool,
/// Entrada (1, 2, ...) onde a cadeia quebra e o motivo.
/// 1-based entry index where the chain breaks and the reason.
pub broken_at: Option<(u64, String)>,
/// Hash da última entrada; guarde-o fora da máquina para detectar truncamento.
/// Hash of the last entry; store off-machine to detect tail truncation.
pub head: String,
}
@@ -72,7 +72,7 @@ fn sha_hex(prev: &str, body: &str) -> String {
h.finalize().iter().map(|b| format!("{b:02x}")).collect()
}
/// UTC em ISO 8601 a partir de segundos Unix (conversão civil de Howard Hinnant).
/// Formats Unix seconds as ISO 8601 UTC (Howard Hinnant civil calendar algorithm).
pub fn utc_iso(secs: i64) -> String {
let days = secs.div_euclid(86400);
let rem = secs.rem_euclid(86400);
@@ -106,7 +106,7 @@ fn env(k: &str) -> Option<String> {
std::env::var(k).ok().filter(|v| !v.is_empty())
}
/// Usuário do sistema por trás da ação (o que chamou sudo, quando houver).
/// Underlying OS user performing the action (the user who invoked sudo, if applicable).
fn current_user() -> String {
env("SUDO_USER")
.or_else(|| env("USER"))
@@ -127,7 +127,7 @@ fn host_name() -> String {
.unwrap_or_else(|| "?".into())
}
/// Última linha não vazia do arquivo, lendo só o fim.
/// Reads the last non-empty line of the file by seeking near the end.
fn last_line(path: &Path) -> std::io::Result<Option<String>> {
let mut f = match std::fs::File::open(path) {
Ok(f) => f,
@@ -158,7 +158,7 @@ impl Log {
Self { path: path.into() }
}
/// `GUARA_AUDIT_LOG` ou `auditoria.jsonl` na pasta de configuração.
/// `GUARA_AUDIT_LOG` or `auditoria.jsonl` in the configuration directory.
pub fn default_path() -> PathBuf {
match env("GUARA_AUDIT_LOG") {
Some(p) => PathBuf::from(p),
@@ -174,7 +174,7 @@ impl Log {
&self.path
}
/// Acrescenta uma entrada ao fim da cadeia.
/// Appends a new entry to the end of the hash chain.
pub fn append(&self, action: &str, detail: Value) -> Result<Entry> {
let (seq, prev) = match last_line(&self.path)? {
None => (1, GENESIS.to_string()),
@@ -227,7 +227,7 @@ impl Log {
})
}
/// Todas as entradas, na ordem. Linha ilegível vira erro (use [`Log::verify`] para saber onde).
/// Returns all entries in order. Malformed lines return an error (use [`Log::verify`] to locate).
pub fn entries(&self) -> Result<Vec<Entry>> {
let text = match std::fs::read_to_string(&self.path) {
Ok(t) => t,
@@ -244,7 +244,7 @@ impl Log {
.collect()
}
/// Confere a numeração, o encadeamento e o hash de cada linha.
/// Verifies sequence numbering, hash chaining, and each entry's digest.
pub fn verify(&self) -> Result<Verification> {
let text = match std::fs::read_to_string(&self.path) {
Ok(t) => t,
@@ -298,15 +298,15 @@ impl Log {
}
}
/// Registra uma ação na trilha padrão. Falha ao gravar não interrompe a operação, mas é
/// avisada no stderr: uma ação sem registro precisa ser notada.
/// Records an action in the default audit trail. Write failures do not abort the operation
/// but are logged to stderr so unrecorded actions do not go unnoticed.
pub fn record(action: &str, detail: Value) {
if let Err(e) = Log::open_default().append(action, detail) {
eprintln!("aviso: não foi possível gravar a trilha de auditoria ({action}): {e}");
}
}
/// Atalho para detalhes de um caminho de origem.
/// Helper to build details JSON for a source path.
pub fn path_detail(path: &str) -> Value {
json!({ "path": path })
}
+15 -15
View File
@@ -1,4 +1,4 @@
//! Carving por assinatura/estrutura sobre intervalos de bytes.
//! Signature- and structure-based file carving over byte ranges.
use crate::signatures::{category_for_ext, detect, might_start, Hit};
use crate::source::Source;
@@ -14,9 +14,9 @@ pub struct CarveCfg<'a> {
pub categories: &'a [Category],
}
/// Varre [start, end) a partir de `*next`. Chama `on_hit(offset, hit)` e retorna
/// `true` se terminou o intervalo (`false` se cancelado).
/// `advance(done_bytes)` informa o progresso dentro do intervalo.
/// Scans `[start, end)` starting from `*next`. Calls `on_hit(offset, hit)` and returns
/// `true` if the range completed (`false` if cancelled).
/// `advance(done_bytes)` reports progress within the range.
pub fn carve_range(
src: &dyn Source,
start: u64,
@@ -28,7 +28,7 @@ pub fn carve_range(
) -> CarveEnd {
let end = end.min(src.size());
let mut pos = (*next).max(start);
// Alinha ao setor.
// Align to sector boundary.
pos = pos.div_ceil(SECTOR) * SECTOR;
let mut buf = vec![0u8; CHUNK + LOOKAHEAD];
let mut skip_until = 0u64;
@@ -45,13 +45,13 @@ pub fn carve_range(
n
}
Err(_) => {
// Várias falhas seguidas = dispositivo sumiu: para com o ponto de retomada salvo.
// Multiple consecutive read failures = device disconnected: stop with resume point saved.
fails += 1;
if fails >= 4 {
*next = pos - (fails as u64 - 1) * CHUNK as u64;
return CarveEnd::IoLost;
}
// Setor ruim isolado: pula um bloco inteiro e segue (a imagem forense trata isso melhor).
// Isolated bad sector: skip a full block and continue (forensic imaging handles bad sectors better).
pos += CHUNK as u64;
ev.advance(CHUNK as u64);
*next = pos;
@@ -100,30 +100,30 @@ pub fn carve_range(
CarveEnd::Done
}
/// Como o carving de um intervalo terminou.
/// Outcome of carving a byte range.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CarveEnd {
Done,
Cancelled,
/// Várias leituras seguidas falharam: o dispositivo provavelmente foi desconectado.
/// Multiple consecutive reads failed: the device was likely disconnected.
IoLost,
}
/// Receptor de eventos do carving.
/// Receiver for file-carving events.
pub trait CarveEvents {
fn advance(&mut self, bytes: u64);
/// Cada trecho lido, antes de procurar assinaturas (para indexar o que serve a recomposição).
/// Called for each chunk read before signature scanning (used to index candidate defrag clusters).
fn chunk(&mut self, _abs: u64, _data: &[u8]) {}
fn hit(&mut self, offset: u64, hit: &Hit) -> HitAction;
}
/// O que fazer depois de um acerto.
/// Action to take after a signature hit.
pub enum HitAction {
/// Pula o conteúdo do arquivo reconhecido (comportamento padrão).
/// Skip past the recognized file content (default behavior).
Skip,
/// Pula só estes bytes (arquivo cujo resto não é confiável).
/// Skip only this many bytes (when the remainder of the file is unreliable).
SkipLen(u64),
/// Segue setor a setor (acerto descartado).
/// Continue sector by sector (hit discarded).
Continue,
}
+20 -20
View File
@@ -1,6 +1,6 @@
//! BitLocker (Windows 7 em diante, inclusive BitLocker To Go): AES-CBC com ou sem difusor
//! Elephant, e AES-XTS. Protetores: senha, chave de recuperação, chave de inicialização (.BEK),
//! chave limpa (proteção suspensa) e chave do volume já conhecida. TPM não é suportado.
//! BitLocker (Windows 7+, including BitLocker To Go): AES-CBC with or without Elephant
//! diffuser, and AES-XTS. Protectors: password, recovery password, startup key (.BEK),
//! clear key (suspended protection), and raw volume master key. TPM is not supported.
use super::*;
use crate::source::{read_vec, SubSource};
@@ -23,12 +23,12 @@ const GUID_EOW: [u8; 16] = [
0x3b, 0x4d, 0xa8, 0x92, 0x80, 0xdd, 0x0e, 0x4d, 0x9e, 0x4e, 0xb1, 0xe3, 0x28, 0x4e, 0xae, 0xd8,
];
/// Posição da tabela de deslocamentos FVE, se for BitLocker (normal ou To Go).
/// Offset of the FVE metadata table if the volume is BitLocker (standard or To Go).
fn offsets_pos(vbr: &[u8]) -> Option<usize> {
if vbr.len() >= 11 && &vbr[3..11] == b"-FVE-FS-" {
return Some(176);
}
// BitLocker To Go: parece FAT ("MSWIN4.1"), mas guarda o GUID do BitLocker no setor de boot.
// BitLocker To Go: looks like FAT ("MSWIN4.1") but stores the BitLocker GUID in the boot sector.
if vbr.len() >= 464 && (vbr[424..440] == GUID_NORMAL || vbr[424..440] == GUID_EOW) {
return Some(440);
}
@@ -54,7 +54,7 @@ fn words(buf: &[u8]) -> Vec<u32> {
.collect()
}
// Decifragem (ordem do dm-crypt do Linux): somas em ordem crescente.
// Decryption (Linux dm-crypt order): additions in ascending order.
fn diffuser_a_decrypt(d: &mut [u32]) {
let n = d.len();
for _ in 0..5 {
@@ -77,7 +77,7 @@ fn diffuser_b_decrypt(d: &mut [u32]) {
}
}
/// Decifra um setor AES-CBC + difusor Elephant (BitLocker Vista/7 sem XTS).
/// Decrypts an AES-CBC + Elephant diffuser sector (BitLocker Vista/7 pre-XTS).
pub fn elephant_decrypt(key: &AesAny, tweak: &AesAny, buf: &mut [u8], byte_off: u64) {
let mut iv = [0u8; 16];
iv[..8].copy_from_slice(&byte_off.to_le_bytes());
@@ -89,7 +89,7 @@ pub fn elephant_decrypt(key: &AesAny, tweak: &AesAny, buf: &mut [u8], byte_off:
for (i, w) in d.iter().enumerate() {
buf[i * 4..i * 4 + 4].copy_from_slice(&w.to_le_bytes());
}
// Chave do setor: duas cifragens do deslocamento com a chave de ajuste.
// Sector key: two encryptions of the byte offset with the tweak key.
let mut k0 = [0u8; 16];
k0[..8].copy_from_slice(&byte_off.to_le_bytes());
let mut k1 = k0;
@@ -106,7 +106,7 @@ pub fn elephant_decrypt(key: &AesAny, tweak: &AesAny, buf: &mut [u8], byte_off:
// ------------------------------------------------------------------ CCM
/// AES-CCM (nonce de 12 bytes, tag de 16): `blob` = tag cifrada (16) + dados cifrados.
/// AES-CCM (12-byte nonce, 16-byte tag): `blob` = encrypted tag (16) + ciphertext.
fn ccm_decrypt(key: &AesAny, nonce: &[u8], blob: &[u8]) -> Option<Vec<u8>> {
if nonce.len() != 12 || blob.len() < 16 {
return None;
@@ -130,7 +130,7 @@ fn ccm_decrypt(key: &AesAny, nonce: &[u8], blob: &[u8]) -> Option<Vec<u8>> {
let s = ctr(bi as u32 + 1);
pt.extend(ch.iter().zip(s.iter()).map(|(a, b)| a ^ b));
}
// CBC-MAC sobre B0 + dados.
// CBC-MAC over B0 + plaintext.
let mut b0 = [0u8; 16];
b0[0] = 0x3A;
b0[1..13].copy_from_slice(nonce);
@@ -146,7 +146,7 @@ fn ccm_decrypt(key: &AesAny, nonce: &[u8], blob: &[u8]) -> Option<Vec<u8>> {
(x == tag).then_some(pt)
}
// --------------------------------------------------------------- chaves
// --------------------------------------------------------------- keys
fn stretch(initial: &[u8; 32], salt: &[u8]) -> [u8; 32] {
let mut last = [0u8; 32];
@@ -169,7 +169,7 @@ fn password_key(pw: &str, salt: &[u8]) -> [u8; 32] {
stretch(&init, salt)
}
/// Chave de recuperação: 8 blocos de 6 dígitos, cada um múltiplo de 11.
/// Recovery password: 8 groups of 6 digits, each divisible by 11.
fn recovery_key(rp: &str, salt: &[u8]) -> Option<[u8; 32]> {
let digits: String = rp.chars().filter(|c| c.is_ascii_digit()).collect();
if digits.len() != 48 {
@@ -188,7 +188,7 @@ fn recovery_key(rp: &str, salt: &[u8]) -> Option<[u8; 32]> {
Some(stretch(&init, salt))
}
// ----------------------------------------------------------- metadados
// ----------------------------------------------------------- metadata
#[derive(Debug, Clone)]
struct Entry {
@@ -250,7 +250,7 @@ fn read_fve(src: &dyn Source, offset: u64) -> Option<Fve> {
}
let version = le16(&h, 10);
if version != 2 {
return None; // Vista (versão 1) não suportado
return None; // Vista (version 1) not supported
}
let encrypted_size = le64(&h, 16);
let hdr_sectors = le32(&h, 28) as u64;
@@ -293,7 +293,7 @@ fn read_fve(src: &dyn Source, offset: u64) -> Option<Fve> {
})
}
/// Chave de um arquivo .BEK (chave de inicialização).
/// Extracts the key from a .BEK startup key file.
fn bek_key(file: &[u8]) -> Option<Vec<u8>> {
if file.len() < 48 {
return None;
@@ -313,7 +313,7 @@ fn bek_key(file: &[u8]) -> Option<Vec<u8>> {
fn candidate_keys(v: &Vmk, secrets: &[Secret]) -> Vec<Vec<u8>> {
let mut out: Vec<Vec<u8>> = vec![];
// Chave limpa: o próprio protetor guarda a chave em claro.
// Clear key: the protector itself stores the unencrypted key.
if v.protection == 0 {
for n in &v.nested {
if n.vtype == 1 && n.data.len() >= 36 {
@@ -350,7 +350,7 @@ fn decrypt_key_entry(key: &[u8], e: &Entry) -> Option<Vec<u8>> {
return None;
}
let pt = ccm_decrypt(&aes, &e.data[..12], &e.data[12..])?;
// payload = cabeçalho de entrada (8) + método (4) + chave
// payload = entry header (8) + method (4) + key
(pt.len() > 12).then(|| pt[12..].to_vec())
}
@@ -361,7 +361,7 @@ pub fn unlock(src: Arc<dyn Source>, secrets: &[Secret]) -> UnlockResult {
}
let bps = le16(&vbr, 11).max(512) as u64;
let pos = offsets_pos(&vbr).unwrap_or(176);
// Três cópias dos metadados; usa a primeira legível.
// Three metadata copies exist; use the first valid one.
let mut fve = None;
let mut meta_offsets = vec![];
for i in 0..3 {
@@ -481,7 +481,7 @@ pub fn unlock(src: Arc<dyn Source>, secrets: &[Secret]) -> UnlockResult {
let len = src.size();
let mut cs = CryptSource::new(src.clone(), cipher, 0, len, bps);
let mut patches = vec![];
// Áreas de metadados FVE: o Windows não as expõe como dados.
// FVE metadata regions: Windows masks them out as zeros.
for off in meta_offsets {
patches.push(Patch {
at: off,
@@ -492,7 +492,7 @@ pub fn unlock(src: Arc<dyn Source>, secrets: &[Secret]) -> UnlockResult {
});
}
if fve.volume_header_size > 0 && fve.volume_header_offset > 0 {
// O início do volume original foi movido para outro lugar e cifrado lá.
// The original volume header was relocated and encrypted elsewhere.
patches.push(Patch {
at: fve.volume_header_offset,
len: fve.volume_header_size,
+5 -5
View File
@@ -1,5 +1,5 @@
//! FileVault 2 sobre Core Storage (volumes HFS+ cifrados do macOS 10.7 a 10.12 e Fusion Drives).
//! Senha ou chave do volume; chave de recuperação institucional não é suportada.
//! FileVault 2 on Core Storage (encrypted HFS+ volumes from macOS 10.7–10.12 and Fusion Drives).
//! Supports user password or volume key; institutional recovery keys are not supported.
use super::*;
use crate::source::{read_vec, SubSource};
@@ -18,7 +18,7 @@ pub fn detect(head: &[u8]) -> bool {
head.len() >= 512 && head[88] == 0x43 && head[89] == 0x53 && head[10] == 0x10
}
/// Desembrulha uma chave AES-128 de 24 bytes (RFC 3394).
/// Unwraps a 24-byte AES-128 key (RFC 3394).
fn unwrap_key(kek: &[u8], wrapped: &[u8]) -> Option<[u8; 16]> {
let aes = AesAny::new(kek)?;
if wrapped.len() != 24 {
@@ -73,7 +73,7 @@ fn b64(s: &str) -> Option<Vec<u8>> {
Some(out)
}
/// Valor de `<key>NOME</key><tipo ...>VALOR</tipo>` num plist simples (primeira ocorrência com valor).
/// Extracts the value of `<key>NAME</key><type ...>VALUE</type>` from a simple XML plist.
fn plist_value(xml: &str, key: &str) -> Option<String> {
let k = format!("<key>{key}</key>");
let mut from = 0;
@@ -163,7 +163,7 @@ pub fn unlock(src: Arc<dyn Source>, secrets: &[Secret]) -> UnlockResult {
if b.len() < MD_BLOCK || b.iter().all(|&x| x == 0) {
break;
}
// XTS sobre o bloco inteiro, com o número do bloco como ajuste inicial.
// XTS across the full block, using the block number as the initial tweak.
md_cipher.decrypt_sector(&mut b, i, 0);
match u16::from_le_bytes([b[10], b[11]]) {
0x0019 => {
+6 -6
View File
@@ -1,4 +1,4 @@
//! LUKS1 e LUKS2 (cifras AES; PBKDF2 e Argon2; hashes SHA-1/256/512 e RIPEMD-160).
//! LUKS1 and LUKS2 (AES ciphers; PBKDF2 and Argon2; SHA-1/256/512 and RIPEMD-160 hashes).
use super::*;
use crate::source::{read_vec, SubSource};
@@ -60,7 +60,7 @@ impl Hash {
}
}
/// Difusão do "anti-forensic splitter".
/// Anti-forensic splitter diffusion step.
fn diffuse(block: &mut [u8], h: Hash) {
let ds = h.size();
let mut i = 0u32;
@@ -74,7 +74,7 @@ fn diffuse(block: &mut [u8], h: Hash) {
}
}
/// Recompõe a chave a partir das `stripes` faixas (AFmerge).
/// Merges key material across `stripes` anti-forensic stripes (AFmerge).
pub fn af_merge(src: &[u8], block: usize, stripes: usize, h: Hash) -> Vec<u8> {
let mut buf = vec![0u8; block];
for i in 0..stripes.saturating_sub(1) {
@@ -87,7 +87,7 @@ pub fn af_merge(src: &[u8], block: usize, stripes: usize, h: Hash) -> Vec<u8> {
buf.iter().zip(last).map(|(a, b)| a ^ b).collect()
}
/// Cifra descrita por texto como "aes-xts-plain64" ou "aes-cbc-essiv:sha256".
/// Parses cipher spec strings such as "aes-xts-plain64" or "aes-cbc-essiv:sha256".
pub fn parse_cipher(spec: &str, key: &[u8]) -> std::result::Result<SectorCipher, UnlockError> {
let (alg, rest) = spec
.split_once('-')
@@ -279,7 +279,7 @@ fn jstr<'a>(v: &'a serde_json::Value, k: &str) -> Option<&'a str> {
v.get(k)?.as_str()
}
/// Números no JSON do LUKS2 vêm como texto (inteiros grandes) ou número.
/// Numbers in LUKS2 JSON metadata appear either as strings (for 64-bit ints) or numbers.
fn jnum(v: &serde_json::Value, k: &str) -> Option<u64> {
let x = v.get(k)?;
x.as_u64().or_else(|| x.as_str()?.parse().ok())
@@ -463,7 +463,7 @@ fn unlock_v2(src: Arc<dyn Source>, secrets: &[Secret]) -> UnlockResult {
}
}
/// Atalho: abre uma janela da origem como LUKS.
/// Helper: opens a byte window of the source as a LUKS volume.
pub fn unlock_window(
base: Arc<dyn Source>,
start: u64,
+34 -34
View File
@@ -1,8 +1,8 @@
//! Cifras de disco (somente leitura) e desbloqueio de volumes cifrados.
//! Read-only disk ciphers and encrypted volume unlocking.
//!
//! An unlocked volume is exposed as a standard `Source`: the rest of Guará
//! (scanning, recovery) sees the plaintext filesystem transparently.
//!
//! O volume desbloqueado é exposto como uma `Source` comum: o restante do Guará
//! (varredura, recuperação) enxerga o sistema de arquivos em claro, sem saber
//! que existe cifra por baixo.
pub mod bitlocker;
pub mod filevault;
@@ -14,7 +14,7 @@ use aes::{Aes128, Aes192, Aes256};
use std::io;
use std::sync::Arc;
/// AES com tamanho de chave escolhido em tempo de execução.
/// AES cipher with runtime key size selection.
#[derive(Clone)]
pub enum AesAny {
A128(Aes128),
@@ -55,21 +55,21 @@ fn xor16(a: &mut [u8; 16], b: &[u8]) {
}
}
/// Como o vetor de inicialização de cada setor é calculado.
/// Sector initialization vector (IV) generation mode.
#[derive(Clone)]
pub enum Iv {
/// Número do setor (de 512 bytes) em 64 bits little-endian.
/// 512-byte sector number as 64-bit little-endian.
Plain64,
/// Número do setor em 32 bits little-endian.
/// Sector number as 32-bit little-endian.
Plain32,
Plain64Be,
/// ESSIV: AES(SHA-256(chave), setor).
/// ESSIV: AES(SHA-256(key), sector).
Essiv(AesAny),
/// Número da unidade de dados (setor de `usize` bytes) em 64 bits little-endian.
/// Data unit number (`usize`-byte sector) as 64-bit little-endian.
Unit(u64),
/// BitLocker (Windows 10 1511+): AES(chave, offset em bytes).
/// BitLocker (Windows 10 1511+): AES(key, byte offset).
Eboiv(AesAny),
/// BitLocker CBC com difusor Elephant: IV = AES(chave, offset em bytes).
/// BitLocker CBC with Elephant diffuser: IV = AES(key, byte offset).
Bitlocker(AesAny),
}
@@ -94,7 +94,7 @@ impl Iv {
}
}
/// Cifra aplicada setor a setor.
/// Sector-level disk cipher.
#[derive(Clone)]
pub enum SectorCipher {
Xts {
@@ -106,7 +106,7 @@ pub enum SectorCipher {
key: AesAny,
iv: Iv,
},
/// BitLocker AES-CBC com difusor Elephant (chave de dados + chave de ajuste).
/// BitLocker AES-CBC with Elephant diffuser (data key + tweak key).
Elephant {
key: AesAny,
tweak: AesAny,
@@ -126,8 +126,8 @@ fn gf_double(t: &mut [u8; 16]) {
}
impl SectorCipher {
/// Decifra um setor no lugar. `sector_512` conta setores de 512 bytes; `byte_off` é a posição
/// em bytes relativa ao início dos dados cifrados.
/// Decrypts a sector in place. `sector_512` is the 512-byte sector index; `byte_off` is the
/// byte offset relative to the start of the encrypted payload.
pub fn decrypt_sector(&self, buf: &mut [u8], sector_512: u64, byte_off: u64) {
match self {
SectorCipher::Xts { k1, k2, iv } => {
@@ -178,33 +178,33 @@ pub fn cbc_encrypt(key: &AesAny, iv: &[u8; 16], buf: &mut [u8]) {
}
}
/// Volume cifrado visto em claro: `len` bytes que começam em `data_offset` na origem.
/// Plaintext view of an encrypted volume: `len` bytes starting at `data_offset` in the source.
pub struct CryptSource {
inner: Arc<dyn Source>,
cipher: SectorCipher,
data_offset: u64,
len: u64,
sector: u64,
/// Setores antes deste limite estão em claro (BitLocker parcialmente cifrado) ou
/// são cobertos por uma cópia (os primeiros setores do BitLocker).
/// Sectors before this threshold are either unencrypted (partially encrypted BitLocker) or
/// redirected to a backup copy (BitLocker relocated boot sectors).
patches: Vec<Patch>,
/// A partir deste byte (relativo aos dados) o conteúdo ainda não foi cifrado.
/// Bytes at or past this payload-relative offset are still unencrypted.
plain_from: u64,
/// Somado ao número do setor no cálculo do IV (iv_tweak do LUKS2).
/// Added to the sector index when computing the IV (LUKS2 `iv_tweak`).
iv_tweak: u64,
}
/// Trecho do volume lido de outro lugar da origem (BitLocker move o início do volume).
/// Volume range redirected to another offset on the source (BitLocker relocated header sectors).
#[derive(Clone)]
pub struct Patch {
/// Início no volume em claro.
/// Start offset in the logical plaintext volume.
pub at: u64,
pub len: u64,
/// Onde estão os dados, na origem, já em claro.
/// Source offset where the data resides.
pub from: u64,
/// Se os bytes de origem precisam ser decifrados (usando a posição `from_crypt`).
/// Whether the source bytes must be decrypted (using cipher offset `from_crypt`).
pub decrypt_from: Option<u64>,
/// Devolve zeros (áreas de metadados do BitLocker).
/// Returns zeros (BitLocker metadata regions masked out by the OS).
pub zero: bool,
}
@@ -242,7 +242,7 @@ impl CryptSource {
}
fn read_sector_plain(&self, rel: u64, out: &mut [u8]) -> io::Result<()> {
// `rel` alinhado ao setor; `out.len() == sector`.
// `rel` is sector-aligned; `out.len() == sector`.
for p in &self.patches {
if rel >= p.at && rel < p.at + p.len {
if p.zero {
@@ -292,15 +292,15 @@ impl Source for CryptSource {
}
}
/// Algo que o usuário fornece para abrir um volume.
/// User-supplied credential for unlocking an encrypted volume.
#[derive(Clone)]
pub enum Secret {
Password(String),
/// Chave de recuperação numérica do BitLocker (8 grupos de 6 dígitos).
/// BitLocker numeric recovery password (8 groups of 6 digits).
RecoveryKey(String),
/// Conteúdo de um arquivo de chave (.BEK do BitLocker, keyfile do LUKS).
/// Raw contents of a key file (BitLocker .BEK, LUKS keyfile).
KeyFile(Vec<u8>),
/// Chave do volume já conhecida (FVEK/master key).
/// Known volume master key (FVEK / LUKS master key).
VolumeKey(Vec<u8>),
}
@@ -317,7 +317,7 @@ impl std::fmt::Debug for Secret {
#[derive(Debug)]
pub enum UnlockError {
/// Nenhuma das senhas/chaves abriu o volume.
/// None of the supplied passwords or keys unlocked the volume.
WrongSecret,
Unsupported(String),
Corrupt(String),
@@ -339,11 +339,11 @@ impl From<UnlockError> for crate::Error {
}
}
/// Volume aberto.
/// Unlocked encrypted volume.
pub struct Unlocked {
pub source: Arc<dyn Source>,
pub description: String,
/// Chave do volume (para reabrir sem a senha, se o usuário permitir guardá-la).
/// Volume master key (can be saved in the session to reopen without a password if permitted).
pub volume_key: Option<Vec<u8>>,
}
+23 -23
View File
@@ -1,24 +1,24 @@
//! Recomposição de arquivos fragmentados no carving.
//! Fragmented file reassembly during carving.
//!
//! Para JPEG: valida os bytes contíguos com o decodificador de entropia; se a imagem quebra,
//! procura entre os clusters "com cara de JPEG" próximos a continuação cujo DC de luminância
//! emenda sem salto, até três fragmentos. É o que dá para fazer sem tabela de alocação, e só
//! vale quando o sistema de arquivos não deixou metadados para o arquivo.
//! For JPEG: validates contiguous bytes with the entropy decoder; if the stream breaks,
//! searches nearby JPEG-like entropy clusters for a continuation whose luminance DC
//! splices smoothly across up to three fragments. Best-effort without an allocation table,
//! used when filesystem metadata is no longer available.
use crate::jpegscan::{self, Header, State, Stop};
use crate::source::{read_vec, Source};
use crate::types::Extent;
const MAX_FILE: usize = 48 << 20;
/// Janela de busca da continuação, em bytes, depois e antes do primeiro fragmento.
/// Search window in bytes (after and before the first fragment) for candidate continuations.
const WINDOW_AFTER: u64 = 512 << 20;
const WINDOW_BEFORE: u64 = 64 << 20;
const PROBE: usize = 8192;
pub const CLUSTER: u64 = 4096;
const MAX_PROBES: usize = 20_000;
/// Marca os clusters (de 4 KiB, alinhados ao início da origem) que parecem dados entropy-coded
/// de JPEG: sem marcadores no meio e com bytes bem distribuídos.
/// Bitmap of 4 KiB source-aligned clusters that look like JPEG entropy-coded data
/// (no embedded unescaped markers and high byte entropy).
#[derive(Default, Clone)]
pub struct ClusterIndex {
bits: Vec<u64>,
@@ -43,7 +43,7 @@ impl ClusterIndex {
.map(|w| w >> (c % 64) & 1 == 1)
.unwrap_or(false)
}
/// Examina um trecho lido e marca os clusters candidatos.
/// Scans a read buffer and marks candidate JPEG entropy clusters.
pub fn scan_chunk(&mut self, abs: u64, buf: &[u8]) {
let first = abs.div_ceil(CLUSTER) * CLUSTER;
let mut o = first;
@@ -90,7 +90,7 @@ pub fn jpeg_like(c: &[u8]) -> bool {
pub struct Pool<'a> {
pub index: &'a ClusterIndex,
/// Extents já atribuídos a outros arquivos.
/// Extents already claimed by other recovered files.
pub used: &'a [Extent],
}
@@ -105,17 +105,17 @@ impl Pool<'_> {
#[derive(Debug)]
pub enum Analysis {
/// Contíguo e consistente; `len` inclui o EOI.
/// Contiguous and valid; `len` includes the EOI marker.
Intact { len: u64 },
/// Remontado a partir de fragmentos.
/// Reassembled from multiple non-contiguous fragments.
Rebuilt { extents: Vec<Extent>, size: u64 },
/// Só o início é confiável.
/// Only the leading prefix is reliable.
Broken { good_len: u64 },
/// Formato que não sabemos validar (progressivo etc.).
/// JPEG variant we do not entropy-validate (progressive, arithmetic, etc.).
Unsupported,
}
/// Procura FF D9 logo depois do último MCU (até 64 bytes de preenchimento).
/// Looks for FF D9 immediately after the final MCU (allowing up to 64 padding bytes).
fn eoi_after(data: &[u8], pos: usize) -> Option<usize> {
let end = (pos + 64).min(data.len().saturating_sub(1));
(pos.saturating_sub(2)..end)
@@ -123,7 +123,7 @@ fn eoi_after(data: &[u8], pos: usize) -> Option<usize> {
.map(|i| i + 2)
}
/// Valida o JPEG que começa em `off` e, se estiver quebrado, tenta recompô-lo.
/// Validates the JPEG starting at `off` and attempts fragment reassembly if truncated/broken.
pub fn analyze_jpeg(
src: &dyn Source,
off: u64,
@@ -168,8 +168,8 @@ pub fn analyze_jpeg(
let mut pieces: Vec<(u64, usize)> = vec![];
let mut joined = buf;
let mut cur_base = off; // início absoluto do fragmento atual
let mut frag_start = 0usize; // posição em `joined` onde o fragmento atual começa
let mut cur_base = off; // absolute start offset of the current fragment
let mut frag_start = 0usize; // byte offset in `joined` where the current fragment starts
let mut snaps = d.snaps;
let mut suspect = stop_pos;
@@ -223,7 +223,7 @@ pub fn analyze_jpeg(
}
ranked.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap());
ranked.truncate(120);
// Etapa 2: a continuação certa fecha a imagem exatamente no último MCU.
// Stage 2: the true continuation finishes the image right at the final MCU.
let mut longest: Option<(usize, u64, f64)> = None;
for (sc, q) in ranked {
let avail = (src.size() - q) as usize;
@@ -269,7 +269,7 @@ pub fn analyze_jpeg(
return finish(pieces);
}
}
// Quebrou de novo: onde?
// Broke again: find the new break point.
let mut sp = match d2.stop {
Stop::Marker(_, p) | Stop::Error(p) => p,
_ => joined.len(),
@@ -314,8 +314,8 @@ fn finish(pieces: Vec<(u64, usize)>) -> Analysis {
Analysis::Rebuilt { extents, size }
}
/// Pontuação (menor é melhor) de `q` como continuação de `prefix`: diferença média de pixels
/// nas bordas entre os MCUs novos e seus vizinhos. Natural: poucos níveis de cinza; lixo: dezenas.
/// Score (lower is better) for `q` as a continuation of `prefix`: mean pixel difference
/// along MCU borders between new blocks and their neighbors (smooth in natural images, high on noise).
fn probe_candidate(
src: &dyn Source,
h: &Header,
@@ -385,7 +385,7 @@ fn probe_candidate(
if n < 40 {
return None;
}
// Bytes por MCU depois da emenda, comparados com os do trecho anterior.
// Bytes per MCU after the splice compared against the preceding prefix.
let after = d.dc.len() as f64 - (seam.saturating_sub(snap.mcu)) as f64;
let used = d.state.pos.saturating_sub(prefix.len()) as f64;
let ratio = if after > 4.0 && used > 0.0 {
+5 -5
View File
@@ -1,4 +1,4 @@
//! Enumeração de dispositivos de armazenamento (melhor esforço por plataforma).
//! Storage device enumeration (best-effort per platform).
use serde::{Deserialize, Serialize};
@@ -7,18 +7,18 @@ pub struct DeviceInfo {
pub path: String,
pub name: String,
pub size: u64,
/// "HD", "SSD", "NVMe", "USB", "Disco" ou "Imagem".
/// Device kind label (e.g. "HD", "SSD", "NVMe", "USB").
pub kind: String,
pub removable: bool,
pub model: Option<String>,
pub mounted_at: Vec<String>,
/// O processo atual consegue abrir o dispositivo para leitura bruta.
/// Whether the current process has permission to open the device for raw reading.
pub readable: bool,
/// SSD com descarte (TRIM) ativo: arquivos apagados tendem a sumir de verdade.
/// SSD with TRIM/discard enabled: deleted file blocks are typically zeroed by the controller.
pub trim: Option<bool>,
}
/// (dispositivo, ponto de montagem)
/// `(device_path, mount_point)`
pub fn mount_table() -> Vec<(String, String)> {
#[cfg(target_os = "linux")]
{
+35 -35
View File
@@ -1,8 +1,8 @@
//! Imagens forenses Expert Witness (EWF): E01 (EnCase 1 a 6, SMART) e Ex01 (EnCase 7, v2).
//! Expert Witness Format (EWF) forensic images: E01 (EnCase 1–6, SMART) and Ex01 (EnCase 7, v2).
//!
//! Leitura de conjuntos com vários segmentos (`.E01`, `.E02`, ...), com chunks
//! deflate, bzip2, sem compressão ou com preenchimento por padrão. Escrita de
//! E01 no formato EnCase 6. A origem nunca é alterada.
//! Reads multi-segment sets (`.E01`, `.E02`, ...) with deflate, bzip2, uncompressed,
//! or pattern-filled chunks. Writes EnCase 6 compatible `.E01` images.
//! The source is never modified.
use crate::error::{Error, Result};
use crate::hashing::MultiHasher;
@@ -17,7 +17,7 @@ use std::sync::Mutex;
pub const SIG_V1: [u8; 8] = [0x45, 0x56, 0x46, 0x09, 0x0d, 0x0a, 0xff, 0x00];
pub const SIG_V2: [u8; 8] = [0x45, 0x56, 0x46, 0x32, 0x0d, 0x0a, 0x81, 0x00];
/// Reconhece o cabeçalho de um segmento EWF.
/// Checks whether `buf` starts with an EWF segment signature.
pub fn is_ewf_header(head: &[u8]) -> bool {
head.len() >= 8 && (head[..8] == SIG_V1 || head[..8] == SIG_V2)
}
@@ -85,12 +85,12 @@ enum Codec {
#[derive(Clone, Copy, Debug)]
enum Kind {
/// Dados guardados como estão; `checksum` indica 4 bytes de Adler-32 no fim.
/// Uncompressed chunk data; `checksum` indicates a trailing 4-byte Adler-32.
Stored {
checksum: bool,
},
Packed(Codec),
/// Chunk inteiro repetindo estes 8 bytes (EnCase 7 "empty block").
/// Full chunk filled by repeating these 8 bytes (EnCase 7 pattern/empty block).
Pattern([u8; 8]),
}
@@ -102,16 +102,16 @@ struct Chunk {
kind: Kind,
}
/// Metadados lidos do conjunto de segmentos.
/// Metadata extracted from the EWF segment set.
#[derive(Debug, Clone, Default)]
pub struct EwfInfo {
/// 1 para E01, 2 para Ex01.
/// Format version: 1 for E01, 2 for Ex01.
pub version: u8,
pub segments: usize,
pub chunk_size: u64,
pub bytes_per_sector: u32,
pub sectors: u64,
/// Caso, examinador, notas etc. (chaves legíveis).
/// Case, examiner, notes, etc. (human-readable key-value pairs).
pub fields: BTreeMap<String, String>,
pub md5: Option<String>,
pub sha1: Option<String>,
@@ -130,7 +130,7 @@ fn hex(b: &[u8]) -> String {
b.iter().map(|x| format!("{x:02x}")).collect()
}
/// Próximo nome de segmento: E01..E99, EAA..EZZ, FAA... (a mesma letra inicial vira Ex).
/// Computes the next segment extension: E01..E99, EAA..EZZ, FAA... (preserving prefix case).
pub fn next_segment_ext(ext: &str) -> Option<String> {
let b = ext.as_bytes();
if b.len() != 3 {
@@ -175,7 +175,7 @@ pub fn next_segment_ext(ext: &str) -> Option<String> {
Some(if upper { out } else { out.to_ascii_lowercase() })
}
/// Nome do segmento seguinte para E01 (`.E01` -> `.E02`) e Ex01 (`.Ex01` -> `.Ex02`).
/// Path of the next segment for E01 (`.E01` -> `.E02`) and Ex01 (`.Ex01` -> `.Ex02`).
fn next_segment_path(p: &Path) -> Option<PathBuf> {
let ext = p.extension()?.to_str()?;
let new = if ext.len() == 4 && ext[..2].eq_ignore_ascii_case("ex") {
@@ -375,7 +375,7 @@ impl EwfSource {
if flen < 96 {
continue;
}
// Os descritores formam uma lista encadeada para trás, a partir do fim do arquivo.
// Section descriptors form a backward-linked list starting from the end of the file.
let mut pos = flen - 64;
let mut guard = 0;
loop {
@@ -387,7 +387,7 @@ impl EwfSource {
let dsize = le64(&d, 16);
let pad = le32(&d, 28) as u64;
let dstart = pos.saturating_sub(dsize);
let data_len = dsize; // inclui o preenchimento; os decodificadores param no fim do fluxo
let data_len = dsize; // includes padding; decoders stop at the end of the compressed stream
let _ = pad;
let read_data = |max: u64| -> io::Result<Vec<u8>> {
let n = data_len.min(max) as usize;
@@ -515,7 +515,7 @@ impl EwfSource {
Ok(out)
}
/// Verifica o hash MD5 do conteúdo contra o guardado na imagem (`None` se a imagem não o guarda).
/// Verifies the content MD5 against the hash stored in the image (`None` if not present).
pub fn verify(&self) -> Result<Option<bool>> {
let Some(stored) = self.info.md5.clone() else {
return Ok(None);
@@ -559,7 +559,7 @@ impl Source for EwfSource {
let data = &cache.as_ref().unwrap().1;
let within = (pos % self.chunk_size) as usize;
if within >= data.len() {
// Chunk curto (último): o resto é zero até o fim lógico.
// Short (final) chunk: pad remaining bytes with zeros up to the logical end.
let n = (self.chunk_size as usize - within).min(want - done);
buf[done..done + n].fill(0);
done += n;
@@ -611,7 +611,7 @@ fn header_key_name(k: &str) -> &str {
}
}
/// Texto do "header" (E01): linhas "1 / main / chaves / valores", separados por TAB.
/// Parses E01 `header` text: tab-separated lines (`1` / `main` / keys / values).
fn parse_header_text(raw: &[u8], out: &mut BTreeMap<String, String>) {
let text = if raw.len() >= 2 && (raw[0] == 0xFF && raw[1] == 0xFE || raw.get(1) == Some(&0)) {
utf16_text(raw)
@@ -674,16 +674,16 @@ fn parse_v2_text(text: &str, info: &mut EwfInfo, total_bytes: &mut u64, ty: u32)
}
// ======================================================================
// Escrita de E01 (formato EnCase 6)
// E01 writer (EnCase 6 format)
// ======================================================================
#[derive(Debug, Clone)]
pub struct E01Options {
pub chunk_sectors: u32,
pub bytes_per_sector: u32,
/// 0 = sem compressão, 1..=9 = nível deflate.
/// 0 = uncompressed, 1..=9 = deflate compression level.
pub compression: u32,
/// Tamanho máximo de cada segmento (0 = um segmento só).
/// Maximum size of each segment file in bytes (0 = single segment).
pub segment_size: u64,
pub case_number: String,
pub description: String,
@@ -735,7 +735,7 @@ impl SegWriter {
self.pos += b.len() as u64;
Ok(())
}
/// Seção com dados completos (cabeçalho, volume, hash...).
/// Writes a complete EWF section (header, volume, hash, etc.).
fn section(&mut self, ty: &str, data: &[u8], last: bool) -> io::Result<()> {
let size = 76 + data.len() as u64;
let next = if last { self.pos } else { self.pos + size };
@@ -760,12 +760,12 @@ fn header_text(o: &E01Options, now: &str) -> Vec<u8> {
fn volume_data(o: &E01Options, chunks: u32, sectors: u64) -> Vec<u8> {
let mut v = vec![0u8; 1052];
v[0] = 1; // disco fixo
v[0] = 1; // fixed disk
v[4..8].copy_from_slice(&chunks.to_le_bytes());
v[8..12].copy_from_slice(&o.chunk_sectors.to_le_bytes());
v[12..16].copy_from_slice(&o.bytes_per_sector.to_le_bytes());
v[16..24].copy_from_slice(&sectors.to_le_bytes());
v[36] = 1; // mídia física
v[36] = 1; // physical media
v[52] = if o.compression == 0 { 0 } else { 1 };
v[56..60].copy_from_slice(&o.chunk_sectors.to_le_bytes());
let c = adler32(&v[..1048]);
@@ -780,7 +780,7 @@ fn utc_now() -> String {
.unwrap_or(0);
let days = secs.div_euclid(86400);
let rem = secs.rem_euclid(86400);
// Conversão civil (Howard Hinnant).
// Civil calendar conversion (Howard Hinnant).
let z = days + 719468;
let era = z.div_euclid(146097);
let doe = z.rem_euclid(146097);
@@ -799,8 +799,8 @@ fn utc_now() -> String {
)
}
/// Cria `base.E01` (e `.E02`... se houver divisão) a partir de `src`.
/// Devolve o relatório com os hashes do conteúdo lido.
/// Creates `base.E01` (and `.E02`... when split across segments) from `src`.
/// Returns the imaging report with hashes of the imaged content.
pub fn write_e01(
src: &dyn Source,
base: &Path,
@@ -838,7 +838,7 @@ pub fn write_e01(
base.with_extension(ext)
};
// O segmento está cheio? (reserva espaço para as tabelas e os hashes finais)
// Is the current segment full? (reserves room for tables and trailing hash sections)
let full = |pos: u64, entries: u64| {
opts.segment_size > 0
&& pos + chunk_size + 2 * (entries * 4 + 110) + 1024 >= opts.segment_size
@@ -875,14 +875,14 @@ pub fn write_e01(
false,
)?;
}
// Grupos "sectors" + "table" + "table2" até estourar o limite do segmento.
// Emit "sectors" + "table" + "table2" groups until the segment size limit is reached.
loop {
let remaining = chunks_total - chunk_idx;
if remaining == 0 {
break;
}
let sec_start = w.pos;
// Espaço reservado para a descrição da seção "sectors" (preenchida depois).
// Placeholder for the "sectors" section descriptor (backfilled later).
w.put(&[0u8; 76])?;
let base_off = w.pos;
let mut entries: Vec<u32> = vec![];
@@ -894,7 +894,7 @@ pub fn write_e01(
}
let off = chunk_idx * chunk_size;
let want = (total - off).min(chunk_size) as usize;
// Leitura tolerante: setor ruim vira zeros e entra no mapa de erros.
// Fault-tolerant read: bad sectors are zero-filled and recorded in the error map.
let b = &mut buf[..want];
if crate::source::read_exact_at(src, off, b).is_err() {
let mut o = 0;
@@ -930,21 +930,21 @@ pub fn write_e01(
chunk_idx += 1;
group_chunks += 1;
progress(chunk_idx * chunk_size.min(total), total);
// Mantém cada tabela dentro de 31 bits e o segmento perto do limite.
// Keep each table offset within 31 bits and the segment within its size limit.
if rel > 0x7000_0000 || full(w.pos, entries.len() as u64) {
break;
}
}
// Preenche a seção "sectors".
// Backfill the "sectors" section descriptor.
let sec_end = w.pos;
let table_size = 76 + 24 + entries.len() as u64 * 4 + 4;
let sectors_desc = section_desc("sectors", sec_end, sec_end - sec_start);
// Descrição "sectors" aponta para a tabela que vem logo depois.
// The "sectors" descriptor points to the table immediately following it.
use std::io::{Seek, SeekFrom};
w.f.seek(SeekFrom::Start(sec_start))?;
w.f.write_all(&sectors_desc)?;
w.f.seek(SeekFrom::Start(sec_end))?;
// table e table2 idênticas.
// `table` and `table2` are identical.
for ty in ["table", "table2"] {
let start = w.pos;
w.put(&section_desc(ty, start + table_size, table_size))?;
@@ -971,7 +971,7 @@ pub fn write_e01(
}
}
if chunk_idx >= chunks_total {
// Segmento final: hashes e "done".
// Final segment: hash sections and "done".
let h = std::mem::take(&mut hasher).finalize();
let md5: Vec<u8> = (0..16)
.map(|i| u8::from_str_radix(&h.md5[i * 2..i * 2 + 2], 16).unwrap())
+11 -11
View File
@@ -1,5 +1,5 @@
//! Navegação estilo gerenciador de arquivos: filtros, ordenação, facetas
//! (tipos, datas, pastas, câmeras) e cache de miniaturas.
//! File-manager style browsing: filtering, sorting, facets
//! (types, dates, folders, cameras), and thumbnail caching.
use crate::types::*;
use serde::{Deserialize, Serialize};
@@ -21,14 +21,14 @@ pub struct Query {
pub offset: usize,
#[serde(default = "default_limit")]
pub limit: usize,
/// Pasta (prefixo), "@carved" (achados por assinatura) ou "@raiz".
/// Folder path prefix, "@carved" (carved findings), or "@raiz" (root).
#[serde(default)]
pub folder: Option<String>,
#[serde(default)]
pub year: Option<i64>,
#[serde(default)]
pub month: Option<u32>,
/// Só arquivos sem data conhecida.
/// Match only files without a known date.
#[serde(default)]
pub undated: Option<bool>,
#[serde(default)]
@@ -39,12 +39,12 @@ pub struct Query {
pub min_size: Option<u64>,
#[serde(default)]
pub max_size: Option<u64>,
/// Megapixels mínimos.
/// Minimum resolution in megapixels.
#[serde(default)]
pub min_mp: Option<f64>,
#[serde(default)]
pub has_gps: Option<bool>,
/// Mostra só a primeira ocorrência de cada conteúdo repetido.
/// Deduplicate: show only the first occurrence of identical content.
#[serde(default)]
pub dedupe: Option<bool>,
}
@@ -67,7 +67,7 @@ impl Query {
.filter(|x| !x.is_empty())
}
/// Itens que atendem ao filtro (na ordem de achado), já sem duplicadas se pedido.
/// Returns items matching the filter (in discovery order), deduplicated if requested.
pub fn select<'a>(&self, found: &'a [Found]) -> Vec<&'a Found> {
let needle = self.needle();
let mut v: Vec<&Found> = found.iter().filter(|f| self.matches(f, &needle)).collect();
@@ -170,7 +170,7 @@ pub fn item_json(f: &Found) -> Value {
})
}
/// Contagens para a navegação lateral.
/// Facet counts for sidebar navigation.
pub fn facets(found: &[Found]) -> Value {
let mut cats: HashMap<&'static str, usize> = HashMap::new();
let mut years: BTreeMap<i64, (usize, BTreeMap<u32, usize>)> = BTreeMap::new();
@@ -266,7 +266,7 @@ pub fn cat_key(c: Category) -> &'static str {
}
}
// ------------------------------------------------------------------ cache de miniaturas
// ------------------------------------------------------------------ thumbnail cache
type Key = (String, u64, u32);
@@ -310,7 +310,7 @@ impl Default for ThumbCache {
}
}
/// Limita quantas miniaturas são decodificadas ao mesmo tempo (memória/CPU).
/// Bounds concurrent thumbnail decoding (memory/CPU cap).
pub struct Gate {
n: Mutex<usize>,
cv: Condvar,
@@ -344,7 +344,7 @@ impl Drop for Permit<'_> {
}
}
/// Assinatura rápida: tamanho + FNV-1a do início (32 KiB) e do fim (32 KiB).
/// Fast content fingerprint: file size + FNV-1a of the first 32 KiB and last 32 KiB.
pub fn quick_sig(src: &dyn crate::source::Source, f: &Found) -> Option<u64> {
use crate::recover::read_found_at;
if f.size == 0 {
+9 -9
View File
@@ -1,21 +1,21 @@
//! Abre um arquivo achado no aplicativo padrão do sistema (player de vídeo, leitor
//! de PDF, etc.). Usado quando o WebView do app não consegue reproduzir a mídia:
//! o WebKitGTK empacotado no AppImage não traz os plugins de mídia do GStreamer e
//! o processo da página morre ao criar um `<video>`.
//! Opens a recovered file in the OS default application (video player, PDF
//! reader, etc.). Used when the embedded WebView cannot play media directly:
//! WebKitGTK bundled in the AppImage lacks GStreamer media plugins and
//! crashes the web process when instantiating a `<video>` element.
use crate::error::{Error, Result};
use crate::fs::sanitize_name;
use crate::types::Found;
use std::path::{Path, PathBuf};
/// Pasta temporária própria do usuário (nunca compartilhada).
/// Per-user private temporary directory (never shared).
fn temp_dir() -> PathBuf {
let uid = crate::owner::invoking_user()
.map(|u| u.uid.to_string())
.unwrap_or_else(|| {
#[cfg(unix)]
{
// SAFETY: getuid não tem pré-condições.
// SAFETY: `getuid` has no preconditions.
unsafe { libc::getuid() }.to_string()
}
#[cfg(not(unix))]
@@ -26,7 +26,7 @@ fn temp_dir() -> PathBuf {
std::env::temp_dir().join(format!("guara-abrir-{uid}"))
}
/// Remove cópias com mais de um dia.
/// Removes temporary copies older than one day.
fn cleanup(dir: &Path) {
let Ok(rd) = std::fs::read_dir(dir) else {
return;
@@ -45,7 +45,7 @@ fn cleanup(dir: &Path) {
}
}
/// Copia o arquivo achado para uma pasta temporária do usuário e o abre no aplicativo padrão.
/// Materializes the recovered file into a user temp folder and opens it with the default app.
pub fn open_found(src: &dyn crate::source::Source, f: &Found) -> Result<PathBuf> {
const MAX: u64 = 8 * 1024 * 1024 * 1024;
if f.size > MAX {
@@ -62,7 +62,7 @@ pub fn open_found(src: &dyn crate::source::Source, f: &Found) -> Result<PathBuf>
Ok(path)
}
/// Abre o caminho com o aplicativo padrão, na sessão do usuário real mesmo se formos root.
/// Launches the default application for `path`, running in the real user session even under root.
pub fn open_path(path: &Path) -> std::io::Result<()> {
use std::process::{Command, Stdio};
#[cfg(target_os = "linux")]
+20 -20
View File
@@ -1,6 +1,6 @@
//! Mais formatos reconhecidos pela estrutura (cabeçalhos, blocos e tamanhos internos), para o
//! carving devolver o arquivo inteiro e não só o começo. Cada detector devolve o tamanho exato
//! quando a estrutura permite e uma confiança proporcional ao que foi conferido.
//! Additional file formats recognized by internal structure (headers, chunks, and embedded sizes)
//! so file carving recovers complete files rather than prefixes. Each detector returns the exact
//! byte length when determinable and a confidence level proportional to structural validation.
use crate::signatures::Hit;
use crate::source::{read_vec, Source};
@@ -40,7 +40,7 @@ fn hit(len: u64, ext: &'static str, confidence: Confidence) -> Option<Hit> {
})
}
/// Primeiros bytes que disparam os detectores deste módulo.
/// Leading byte values that trigger detectors in this module.
pub fn might_start(b0: u8) -> bool {
matches!(
b0,
@@ -67,7 +67,7 @@ pub fn might_start(b0: u8) -> bool {
)
}
/// Tenta os formatos adicionais. `h` são os primeiros bytes (>= 12) lidos em `off`.
/// Probes additional formats. `h` contains the leading bytes (>= 12) read at `off`.
pub fn detect(src: &dyn Source, off: u64, h: &[u8], max: u64) -> Option<Hit> {
if h.starts_with(&[0x1F, 0x8B, 0x08]) {
return gzip(src, off, max);
@@ -153,9 +153,9 @@ pub fn detect(src: &dyn Source, off: u64, h: &[u8], max: u64) -> Option<Hit> {
None
}
// -------------------------------------------------------------- compactadores
// -------------------------------------------------------------- compressed streams
/// Alimenta um descompactador com blocos de `src` até o fim do fluxo; devolve (bytes lidos, saída crc32 opcional).
/// Feeds chunks from `src` into a decompressor until end-of-stream; returns `(compressed_bytes, optional_crc32)`.
fn inflate_len(
src: &dyn Source,
start: u64,
@@ -311,7 +311,7 @@ fn zstd(src: &dyn Source, off: u64, max: u64) -> Option<Hit> {
if btype == 3 {
return None;
}
// RLE: o conteúdo é 1 byte; os outros ocupam `bsize`.
// RLE block: 1 byte in stream, expands to `bsize`.
let body = if btype == 1 { 1 } else { bsize };
pos += 3 + body;
if pos - off > max {
@@ -328,7 +328,7 @@ fn zstd(src: &dyn Source, off: u64, max: u64) -> Option<Hit> {
hit(p as u64, "zst", Confidence::Medium)
}
// ------------------------------------------------------------------ mídia
// ------------------------------------------------------------------ media
fn vint(b: &[u8]) -> Option<(u64, usize, bool)> {
let first = *b.first()?;
@@ -367,7 +367,7 @@ fn matroska(src: &dyn Source, off: u64, max: u64) -> Option<Hit> {
return hit(total, ext, Confidence::High);
}
}
// Tamanho desconhecido (gravação ao vivo): caminha pelos elementos de topo.
// Unknown segment size (live recording): walk top-level EBML elements.
let mut pos = off + (p + sl) as u64;
let mut last_good = pos;
for _ in 0..2_000_000 {
@@ -394,7 +394,7 @@ fn matroska(src: &dyn Source, off: u64, max: u64) -> Option<Hit> {
hit(last_good - off, ext, Confidence::Low)
}
/// ID de elemento EBML (mantém os bits de marcação).
/// Reads an EBML element ID (preserving marker bits).
fn vint_id(b: &[u8]) -> Option<(u64, usize, bool)> {
let first = *b.first()?;
let len = first.leading_zeros() as usize + 1;
@@ -456,7 +456,7 @@ fn ogg(src: &dyn Source, off: u64, max: u64) -> Option<Hit> {
}
fn mpegts(src: &dyn Source, off: u64, max: u64) -> Option<Hit> {
// 188 bytes por pacote (ou 192 com carimbo de 4 bytes antes do sync).
// 188 bytes per MPEG-TS packet (or 192 with a 4-byte timecode prefix before the sync byte).
let probe = read_vec(src, off, 188 * 8).ok()?;
if probe.len() < 188 * 8 || !(0..8).all(|i| probe[i * 188] == 0x47) {
return None;
@@ -492,7 +492,7 @@ fn flv(src: &dyn Source, off: u64, max: u64) -> Option<Hit> {
if t.len() < 15 {
break;
}
// PreviousTagSize(4) + tipo(1) + tamanho(3)
// PreviousTagSize(4) + tag_type(1) + data_size(3)
let ty = t[4];
if !matches!(ty, 8 | 9 | 18) {
break;
@@ -559,7 +559,7 @@ fn jp2(src: &dyn Source, off: u64, max: u64) -> Option<Hit> {
};
if ty == b"jp2c" {
if size == 0 {
// até o fim do arquivo: procura EOC FF D9 depois
// codestream extends to EOF: scan for EOC marker FF D9
return hit((pos - off) + 8 + (4 << 20).min(max), "jp2", Confidence::Low);
}
return hit(pos - off + size, "jp2", Confidence::High);
@@ -589,7 +589,7 @@ fn midi(src: &dyn Source, off: u64, max: u64) -> Option<Hit> {
hit(pos - off, "mid", Confidence::High)
}
// -------------------------------------------------------- arquivos compactados
// -------------------------------------------------------- archives
fn rar(src: &dyn Source, off: u64, max: u64) -> Option<Hit> {
let h = read_vec(src, off, 8).ok()?;
@@ -703,7 +703,7 @@ fn swf(src: &dyn Source, off: u64, max: u64) -> Option<Hit> {
.then_some(())
.and_then(|_| hit(len, "swf", Confidence::High));
}
// CWS: corpo em zlib a partir do byte 8
// CWS: zlib-compressed body starting at byte 8
let (consumed, _, out) = inflate_len(src, off + 8, max, false, true)?;
hit(
8 + consumed,
@@ -716,7 +716,7 @@ fn swf(src: &dyn Source, off: u64, max: u64) -> Option<Hit> {
)
}
// ---------------------------------------------------------- binários e dados
// ---------------------------------------------------------- binaries and data
fn elf(src: &dyn Source, off: u64, max: u64) -> Option<Hit> {
let h = read_vec(src, off, 64).ok()?;
@@ -757,7 +757,7 @@ fn elf(src: &dyn Source, off: u64, max: u64) -> Option<Hit> {
)
};
let mut end = shoff + shentsize * shnum;
// Segmentos de programa podem ir além (binários sem tabela de seções).
// Program segments may extend further (stripped binaries without a section header table).
if phnum > 0 && phnum < 256 && phentsize >= 32 {
let ph = read_vec(src, off + phoff, (phnum * phentsize) as usize).ok()?;
for i in 0..phnum as usize {
@@ -947,7 +947,7 @@ fn ico(src: &dyn Source, off: u64, max: u64) -> Option<Hit> {
}
end = end.max(o + size);
}
// a imagem de cada entrada: BMP/DIB (tamanho do cabeçalho 40) ou PNG
// each icon entry payload is either a BMP/DIB (40-byte header) or a PNG
let first_off = u32le(&d, 12);
let probe = read_vec(src, off + first_off, 8).ok()?;
if !(probe.starts_with(&[0x89, b'P', b'N', b'G']) || u32le(&probe, 0) == 40) {
@@ -961,7 +961,7 @@ fn regf(src: &dyn Source, off: u64, max: u64) -> Option<Hit> {
if h.len() < 4096 {
return None;
}
// soma XOR dos 127 primeiros u32 = valor em 0x1FC
// XOR checksum of the first 127 u32 words must match the word at 0x1FC
let mut x = 0u32;
for i in 0..127 {
x ^= u32le(&h, i * 4) as u32;
+28 -28
View File
@@ -1,7 +1,7 @@
//! APFS (volumes sem cifragem): lê o contêiner, o mapa de objetos e a árvore de arquivos de cada
//! volume; arquivos apagados vêm de duas fontes: as árvores dos instantâneos (snapshots) e os nós
//! antigos da árvore (copy-on-write) que ainda estão no disco. Volumes cifrados (FileVault) são
//! detectados e deixados para a varredura por assinatura.
//! APFS (unencrypted volumes): parses the container, object map, and file system tree of each
//! volume; deleted files are recovered from two sources: snapshot trees and older copy-on-write
//! B-tree nodes still resident on disk. Encrypted volumes (FileVault) are detected and left for
//! signature carving unless unlocked.
use super::{make_found, merge_extents, Ctx, NewFound, VolumeScan};
use crate::source::{read_vec, Source};
@@ -32,13 +32,13 @@ const T_CIB: u32 = 7;
const T_CHECKPOINT_MAP: u32 = 0x0C;
const SUBTYPE_FSTREE: u32 = 0x0E;
// tipos de registro da árvore de arquivos
// filesystem B-tree record types
const R_SNAP_METADATA: u8 = 1;
const R_INODE: u8 = 3;
const R_FILE_EXTENT: u8 = 8;
const R_DIR_REC: u8 = 9;
/// Soma de verificação Fletcher-64 do APFS (cobre o bloco menos os 8 bytes iniciais).
/// APFS Fletcher-64 checksum (covers the block excluding the leading 8 bytes).
pub fn fletcher64(data: &[u8]) -> u64 {
let (mut s1, mut s2) = (0u64, 0u64);
for w in data.chunks_exact(4) {
@@ -60,7 +60,7 @@ struct Apfs<'a> {
struct BNode {
flags: u16,
level: u16,
/// (chave, valor) por entrada
/// `(key, value)` slice pair for each node entry
entries: Vec<(Vec<u8>, Vec<u8>)>,
oid: u64,
xid: u64,
@@ -124,7 +124,7 @@ impl<'a> Apfs<'a> {
})
}
/// Todas as entradas de uma árvore física (mapa de objetos): filhos são endereços físicos.
/// Collects all entries from a physical B-tree (object map): child pointers are physical block addresses.
fn walk_physical(
&self,
root: u64,
@@ -156,7 +156,7 @@ impl<'a> Apfs<'a> {
}
}
/// Mapa de objetos: oid -> [(xid, paddr)] ordenado por xid.
/// Object map: `oid -> [(xid, paddr)]` sorted by transaction ID (`xid`).
fn omap(&self, omap_paddr: u64) -> HashMap<u64, Vec<(u64, u64)>> {
let mut m: HashMap<u64, Vec<(u64, u64)>> = HashMap::new();
let Some(ob) = self.checked_block(omap_paddr) else {
@@ -174,7 +174,7 @@ impl<'a> Apfs<'a> {
m
}
/// Endereço físico de um objeto ephemeral (spaceman), achado no mapa de checkpoint mais novo.
/// Physical block address of an ephemeral object (spaceman) from the latest checkpoint map.
fn ephemeral_paddr(&self, nx: &[u8], oid: u64) -> Option<u64> {
let desc_blocks = le32(nx, 104) as u64;
let desc_base = le64(nx, 112);
@@ -203,8 +203,8 @@ impl<'a> Apfs<'a> {
best.map(|(_, p)| p)
}
/// Mapa de espaço livre do contêiner: percorre o spaceman, os chunk-info blocks e os bitmaps
/// (bit 1 = bloco em uso).
/// Container free-space map: walks the spaceman, chunk-info blocks (CIBs), and allocation bitmaps
/// (bit 1 = allocated block).
fn free_map(&self, nx: &[u8]) -> Option<Vec<Extent>> {
let sm_oid = le64(nx, 152);
if sm_oid == 0 {
@@ -214,7 +214,7 @@ impl<'a> Apfs<'a> {
if le32(&sm, 24) & OBJ_TYPE_MASK != T_SPACEMAN {
return None;
}
// Com CABs (contêineres grandes) os endereços dos CIBs não ficam no spaceman.
// With CABs (large containers), CIB addresses live in chunk-info address blocks rather than inline in the spaceman.
if le32(&sm, 68) != 0 {
return None;
}
@@ -289,7 +289,7 @@ fn lookup(m: &HashMap<u64, Vec<(u64, u64)>>, oid: u64, max_xid: u64) -> Option<u
.map(|(_, p)| *p)
}
// ---------------------------------------------------------------- registros
// ---------------------------------------------------------------- records
#[derive(Clone, Debug, Default)]
struct Inode {
@@ -308,7 +308,7 @@ struct Inode {
#[derive(Clone, Debug, Default)]
struct FileRec {
inode: Inode,
names: BTreeMap<u64, String>, // pai -> nome
names: BTreeMap<u64, String>, // parent_id -> name
}
type Files = HashMap<u64, FileRec>;
@@ -327,7 +327,7 @@ fn add_record(files: &mut Files, extents: &mut Extents, k: &[u8], v: &[u8]) {
let mut ino = Inode {
parent: le64(v, 0),
private_id: le64(v, 8),
ctime: ns(le64(v, 16)), // criação
ctime: ns(le64(v, 16)), // creation timestamp
mtime: ns(le64(v, 24)),
atime: ns(le64(v, 40)),
mode: le16(v, 80),
@@ -338,7 +338,7 @@ fn add_record(files: &mut Files, extents: &mut Extents, k: &[u8], v: &[u8]) {
let flags = le64(v, 48);
ino.compressed = le32(v, 68) & 0x20 != 0;
let _ = flags;
// campos estendidos
// xfields (extended fields)
let xf = &v[92..];
if xf.len() >= 4 {
let n = le16(xf, 0) as usize;
@@ -383,7 +383,7 @@ fn add_record(files: &mut Files, extents: &mut Extents, k: &[u8], v: &[u8]) {
}
impl Apfs<'_> {
/// Percorre a árvore de arquivos a partir de uma raiz virtual.
/// Walks the filesystem B-tree starting from a virtual root OID.
fn walk_fstree(
&self,
root_oid: u64,
@@ -484,7 +484,7 @@ fn open_container<'a>(src: &'a dyn Source, base: u64) -> Option<(Apfs<'a>, Vec<u
bs,
blocks,
};
// O superbloco mais recente está na área de descritores de checkpoint.
// Find the latest valid container superblock in the checkpoint descriptor area.
let desc_blocks = le32(&b0, 104) as u64;
let desc_base = le64(&b0, 112);
let mut best = b0.clone();
@@ -548,7 +548,7 @@ pub fn scan(ctx: &mut Ctx, base: u64, part_len: u64) -> VolumeScan {
fn scan_volume(ctx: &mut Ctx, a: &Apfs, v: &Volume, allow_stale_scan: bool, res: &mut VolumeScan) {
let omap = a.omap(v.omap_paddr);
let current_xid = u64::MAX;
// Árvore atual.
// Current live filesystem tree.
let mut live: Files = HashMap::new();
let mut live_ext: Extents = HashMap::new();
let mut live_nodes: HashSet<u64> = HashSet::new();
@@ -556,7 +556,7 @@ fn scan_volume(ctx: &mut Ctx, a: &Apfs, v: &Volume, allow_stale_scan: bool, res:
live_nodes.insert(node);
add_record(&mut live, &mut live_ext, k, val);
});
// Instantâneos.
// Volume snapshots.
let mut stale: Files = HashMap::new();
let mut stale_ext: Extents = HashMap::new();
if v.snap_tree_oid != 0 {
@@ -583,7 +583,7 @@ fn scan_volume(ctx: &mut Ctx, a: &Apfs, v: &Volume, allow_stale_scan: bool, res:
}
} else {
for (_, val) in &n.entries {
// árvore de metadados de snapshot: filhos físicos ou virtuais
// snapshot metadata tree: physical or virtual child references
let c = le64(val, 0);
stack.push(lookup(&omap, c, current_xid).unwrap_or(c));
}
@@ -602,7 +602,7 @@ fn scan_volume(ctx: &mut Ctx, a: &Apfs, v: &Volume, allow_stale_scan: bool, res:
}
}
}
// Nós antigos da árvore de arquivos.
// Older evicted copy-on-write filesystem tree leaf nodes.
if allow_stale_scan {
let total = a.blocks;
for p in 0..total {
@@ -631,7 +631,7 @@ fn scan_volume(ctx: &mut Ctx, a: &Apfs, v: &Volume, allow_stale_scan: bool, res:
}
}
}
// Arquivos que só existem nas versões antigas.
// Files present only in older tree versions or snapshots.
let mut ids: Vec<&u64> = stale.keys().collect();
ids.sort();
let mut seen_first: HashSet<u64> = HashSet::new();
@@ -648,9 +648,9 @@ fn scan_volume(ctx: &mut Ctx, a: &Apfs, v: &Volume, allow_stale_scan: bool, res:
continue;
};
if ex.values().any(|e| e.2 != 0) {
continue; // extensão cifrada
continue; // encrypted extent
}
// nome e pasta: da entrada de diretório, senão do próprio inode
// file name and parent folder: prefer directory entry, fallback to inode xfield
let (name, parent) = match rec.names.iter().next() {
Some((p, n)) => (n.clone(), *p),
None => match &ino.name {
@@ -726,13 +726,13 @@ fn scan_volume(ctx: &mut Ctx, a: &Apfs, v: &Volume, allow_stale_scan: bool, res:
let _ = res;
}
/// Trechos livres (absolutos) do contêiner, segundo o spaceman.
/// Absolute free byte extents in the container according to the spaceman.
pub fn free_extents(src: &dyn Source, base: u64) -> Option<Vec<Extent>> {
let (a, nx) = open_container(src, base)?;
a.free_map(&nx)
}
/// Arquivos e pastas atuais do primeiro volume sem cifragem (caminho, tamanho, é pasta).
/// Live files and folders in the first unencrypted volume `(path, size, is_dir)`.
pub fn live_paths(src: &dyn Source, base: u64) -> Vec<(String, u64, bool)> {
let Some((a, nx)) = open_container(src, base) else {
return vec![];
+30 -30
View File
@@ -1,8 +1,8 @@
//! Btrfs (um dispositivo; perfis single, DUP e RAID 1): árvores atuais para nomes e mapa de
//! espaço livre, e arquivos apagados achados em nós antigos das árvores. Como o Btrfs nunca
//! sobrescreve metadados no lugar (copy-on-write), as versões anteriores dos nós continuam no
//! disco até o espaço ser reaproveitado: varremos os blocos de metadata em busca delas.
//! Extensões comprimidas (zlib, zstd, lzo) são descompactadas na recuperação.
//! Btrfs (single device; single, DUP, and RAID 1 profiles): reads live trees for paths and
//! free-space mapping, and recovers deleted files from historical copy-on-write B-tree nodes.
//! Because Btrfs never overwrites metadata in place, prior leaf generations remain on disk
//! until reallocated: we scan metadata chunks for those older leaves.
//! Compressed extents (zlib, zstd, LZO) are decompressed on recovery.
use super::{make_found, merge_extents, Ctx, NewFound, VolumeScan};
use crate::source::{read_vec, Source};
@@ -12,7 +12,7 @@ use std::collections::{BTreeMap, HashMap, HashSet};
const MAGIC: &[u8; 8] = b"_BHRfS_M";
const SB_OFF: u64 = 0x10000;
// chaves
// key item types
const INODE_ITEM: u8 = 1;
const INODE_REF: u8 = 12;
const EXTENT_DATA: u8 = 108;
@@ -81,7 +81,7 @@ struct Chunk {
logical: u64,
len: u64,
ty: u64,
/// (offset físico do primeiro stripe) por cópia
/// Physical byte offsets of each stripe replica.
phys: Vec<u64>,
}
@@ -153,7 +153,7 @@ impl<'a> Fs<'a> {
fs.chunks.push(ch);
p += 17 + used;
}
// chunk tree completa
// full chunk tree
let mut more = vec![];
let cr = fs.chunk_root;
fs.walk(cr, &mut |k, d| {
@@ -241,7 +241,7 @@ impl<'a> Fs<'a> {
})
}
/// Percorre uma árvore em ordem, chamando `f` para cada item de folha.
/// Walks a B-tree in order, invoking `f` for each leaf item.
fn walk(&self, root: u64, f: &mut dyn FnMut(&Key, &[u8])) {
let mut seen = HashSet::new();
self.walk_inner(root, f, &mut seen, &mut |_| {}, 0);
@@ -314,7 +314,7 @@ struct InodeData {
atime: i64,
has_inode: bool,
refs: HashMap<u64, String>,
/// offset no arquivo -> (geração do nó, item)
/// `file_offset -> (node_generation, extent_item)`
extents: BTreeMap<u64, (u64, Vec<u8>)>,
}
@@ -377,8 +377,8 @@ pub fn scan(ctx: &mut Ctx, base: u64, part_len: u64) -> VolumeScan {
return res;
}
// 1) Árvores atuais: raízes das subvolumes e extents alocados.
let mut roots: Vec<(u64, u64)> = vec![]; // (id da árvore, bytenr)
// 1) Live trees: subvolume roots and allocated extents.
let mut roots: Vec<(u64, u64)> = vec![]; // (tree_id, root_bytenr)
let mut extent_root = 0u64;
let rr = fs.root;
fs.walk(rr, &mut |k, d| {
@@ -416,7 +416,7 @@ pub fn scan(ctx: &mut Ctx, base: u64, part_len: u64) -> VolumeScan {
}
}
// Extents alocados (árvore de extents) e mapa livre dos blocos de dados.
// Allocated extents (from the extent tree) and free-space map across data chunks.
let mut allocated: Vec<Extent> = vec![];
if extent_root != 0 {
let nodesize = fs.nodesize;
@@ -431,7 +431,7 @@ pub fn scan(ctx: &mut Ctx, base: u64, part_len: u64) -> VolumeScan {
let allocated = merge_extents(allocated);
let mut free: Vec<Extent> = vec![];
for c in fs.chunks.iter().filter(|c| c.ty & 0x1 != 0) {
// blocos de dados: livres = chunk - alocado
// data chunks: free = chunk_range - allocated_extents
let mut cur = c.logical;
let i = allocated.partition_point(|e| e.end() <= c.logical);
for e in &allocated[i..] {
@@ -453,7 +453,7 @@ pub fn scan(ctx: &mut Ctx, base: u64, part_len: u64) -> VolumeScan {
}
res.free = Some(merge_extents(free));
// 2) Varre os blocos de metadata atrás de versões antigas de folhas de árvores de arquivos.
// 2) Scan metadata chunks for older copy-on-write leaves of filesystem trees.
let mut stale: Files = HashMap::new();
let ns = fs.nodesize as usize;
let step = fs.sectorsize as usize;
@@ -482,7 +482,7 @@ pub fn scan(ctx: &mut Ctx, base: u64, part_len: u64) -> VolumeScan {
let mut i = 0usize;
while i + ns <= buf.len() && (off + i as u64) < c.len {
let logical = c.logical + off + i as u64;
// cabeçalho barato antes do CRC
// cheap header sanity check before computing CRC32C
if buf[i + 32..i + 48] == fs.fsid
&& le64(&buf[i..], 48) == logical
&& buf[i + 100] == 0
@@ -505,7 +505,7 @@ pub fn scan(ctx: &mut Ctx, base: u64, part_len: u64) -> VolumeScan {
}
}
// 3) Arquivos que só existem nas folhas antigas.
// 3) Files that only exist in historical leaves.
let path_of = |owner: u64, mut ino: u64| -> String {
let mut parts: Vec<String> = vec![];
for _ in 0..64 {
@@ -584,7 +584,7 @@ pub fn scan(ctx: &mut Ctx, base: u64, part_len: u64) -> VolumeScan {
res
}
/// Converte os itens EXTENT_DATA em extents físicos ou partes (inline/comprimidas).
/// Converts `EXTENT_DATA` items into physical extents or composite parts (inline/compressed).
fn build_data(fs: &Fs, e: &InodeData, allocated: &[Extent]) -> Option<(DataLoc, bool, bool)> {
let mut parts: Vec<Part> = vec![];
let mut pos = 0u64;
@@ -594,7 +594,7 @@ fn build_data(fs: &Fs, e: &InodeData, allocated: &[Extent]) -> Option<(DataLoc,
let mut hole = |parts: &mut Vec<Part>, len: u64| {
if len > 0 {
parts.push(Part::Run { offset: 0, len });
// marcador: offset 0 + sparse tratado abaixo
// placeholder: offset 0 + sparse hole handled below
}
};
let _ = &mut hole;
@@ -697,7 +697,7 @@ fn build_data(fs: &Fs, e: &InodeData, allocated: &[Extent]) -> Option<(DataLoc,
.collect();
return Some((DataLoc::Extents { extents: ex }, any_alloc, all_ok));
}
// Buracos viram trechos esparsos: Part::Run com offset 0 não serve; usa Bytes de zeros pequenos.
// Sparse holes: `Part::Run` with offset 0 is not used here; represent holes accordingly.
let mut out: Vec<Part> = vec![];
for (i, p) in parts.into_iter().enumerate() {
if sparse_marks.contains(&i) {
@@ -732,8 +732,8 @@ fn decode(codec: u8, raw: &[u8], ram: usize) -> Option<Vec<u8>> {
}
}
/// Formato de extensão LZO do Btrfs: tamanho total (u32) + segmentos [tamanho (u32)][dados LZO1X]
/// de até um setor de 4 KiB descomprimido cada, sem cruzar fronteiras de setor.
/// Btrfs LZO extent format: total length (`u32`) + `[seg_len (u32)][LZO1X payload]` segments
/// of up to one 4 KiB uncompressed sector each, padded at sector boundaries.
pub fn lzo_btrfs(raw: &[u8], ram: usize) -> Option<Vec<u8>> {
if raw.len() < 4 {
return None;
@@ -742,7 +742,7 @@ pub fn lzo_btrfs(raw: &[u8], ram: usize) -> Option<Vec<u8>> {
let mut out = Vec::with_capacity(ram);
let mut p = 4usize;
while p + 4 <= raw.len().min(total) && out.len() < ram {
// pula o preenchimento até o próximo setor se sobrar menos de 4 bytes
// skip sector-boundary padding when fewer than 4 bytes remain in the sector
let in_sector = 4096 - (p % 4096);
if in_sector < 4 {
p += in_sector;
@@ -758,7 +758,7 @@ pub fn lzo_btrfs(raw: &[u8], ram: usize) -> Option<Vec<u8>> {
Some(out)
}
/// LZO1X (descompressão segura, no mesmo esquema de estados do kernel Linux).
/// Safe LZO1X decompressor (following the Linux kernel state machine).
pub fn lzo1x_decompress(src: &[u8], max_out: usize) -> Option<Vec<u8>> {
let mut out: Vec<u8> = Vec::with_capacity(max_out);
let mut ip = 0usize;
@@ -778,7 +778,7 @@ pub fn lzo1x_decompress(src: &[u8], max_out: usize) -> Option<Vec<u8>> {
*ip += 1;
Some(v)
};
// estado: literais ao fim da instrução anterior (0..3) ou 4 depois de uma corrida de literais
// state: trailing literals from the previous instruction (0..3), or 4 after a literal run
let mut state;
if byte(0)? > 17 {
let t = byte(0)? - 17;
@@ -859,7 +859,7 @@ fn copy_match(out: &mut Vec<u8>, dist: usize, len: usize) -> Option<()> {
Some(())
}
/// Arquivos e pastas atuais com caminho completo e tamanho (usa só as árvores vivas).
/// Lists live files and directories with full path and size from current subvolume trees.
pub fn live_paths(src: &dyn Source, base: u64) -> Vec<(String, u64, bool)> {
let Some(fs) = Fs::open(src, base) else {
return vec![];
@@ -900,18 +900,18 @@ pub fn live_paths(src: &dyn Source, base: u64) -> Vec<(String, u64, bool)> {
out
}
/// Posições das árvores principais, para ferramentas e testes que precisam editar a imagem.
/// Root locations of core Btrfs trees, used by tests that simulate COW deletions.
pub struct TreeLocations {
pub nodesize: u64,
/// Endereço físico (absoluto na origem) do primeiro nó de cada árvore.
/// Physical byte offset (in the source) of each tree root node.
pub root_tree: u64,
pub fs_tree: u64,
pub extent_tree: u64,
/// Endereços lógicos correspondentes.
/// Corresponding logical `bytenr` addresses.
pub root_tree_logical: u64,
pub fs_tree_logical: u64,
pub extent_tree_logical: u64,
/// Converte endereço lógico em físico.
/// Translates a logical `bytenr` to a physical source offset.
pub chunks: Vec<(u64, u64, u64)>,
}
+5 -5
View File
@@ -1,4 +1,4 @@
//! exFAT: conjuntos de entradas apagados (0x05/0x40/0x41) e bitmap de alocação.
//! exFAT: deleted directory entry sets (`0x05`/`0x40`/`0x41`) and allocation bitmap.
use super::{
make_found, merge_extents, overlaps, sanitize_name, utf16le, Ctx, NewFound, VolumeScan,
@@ -92,7 +92,7 @@ pub fn scan(ctx: &mut Ctx, base: u64) -> VolumeScan {
bitmap: vec![],
};
// Diretório raiz: procura a entrada do bitmap (0x81).
// Root directory: locate the allocation bitmap entry (0x81).
let root_chain = vol.chain(vol.root);
let root_data = vol.read(&root_chain, 64 << 20);
let mut bm: Option<(u32, u64)> = None;
@@ -115,7 +115,7 @@ pub fn scan(ctx: &mut Ctx, base: u64) -> VolumeScan {
.push("exFAT: bitmap de alocação não encontrado; confiança será menor.".into());
}
// Extents vivos: metadados (antes do heap) + clusters alocados no bitmap.
// Live extents: metadata region (before cluster heap) + clusters marked used in the bitmap.
let mut live = vec![Extent::new(base, heap_off - base)];
let mut run_start: Option<u32> = None;
for c in 2..clusters + 2 {
@@ -163,7 +163,7 @@ fn walk(
break;
}
let ty = e[0];
// 0x85 = arquivo/pasta vivo; 0x05 = apagado.
// 0x85 = live file/dir entry; 0x05 = deleted file/dir entry.
if ty != 0x85 && ty != 0x05 {
i += 1;
continue;
@@ -214,7 +214,7 @@ fn walk(
}
let need = size.div_ceil(vol.cbytes) as u32;
let cl: Vec<u32> = if no_chain || deleted {
// Apagados: a cadeia na FAT pode ter sido preservada; senão presume contíguo.
// Deleted files: follow the FAT chain if preserved, otherwise assume contiguous clusters.
let chain = if !no_chain { vol.chain(first) } else { vec![] };
if chain.len() as u32 >= need.max(1) {
chain
+35 -35
View File
@@ -1,10 +1,10 @@
//! ext2/3/4: arquivos apagados com nome, por três caminhos que se completam:
//! ext2/3/4: recovers deleted files with names via three complementary techniques:
//!
//! 1. inodes com `dtime` cujo mapa de extents ainda tem sobras legíveis;
//! 2. cópias antigas do inode guardadas no diário (jbd2), de antes da exclusão;
//! 3. entradas de diretório "escondidas" na folga do `rec_len` (dão o nome).
//! 1. deleted inodes (`dtime != 0`) whose extent tree or block pointers still have remnants;
//! 2. pre-deletion inode copies preserved in the jbd2 journal;
//! 3. unlinked directory entries hidden inside `rec_len` slack space (recovering filenames).
//!
//! Também devolve o mapa de blocos livres, para o carving varrer só o que está livre.
//! Also returns the free-block map so file carving only scans unallocated blocks.
use super::{make_found, merge_extents, Ctx, NewFound, VolumeScan};
use crate::signatures;
@@ -103,7 +103,7 @@ struct Ext<'a> {
journal_inum: u32,
}
/// Sequência de (bloco lógico, bloco físico, quantidade, não-inicializado).
/// Sequence of `(logical_block, physical_block, block_count, is_uninitialized)`.
type ExtentList = Vec<(u64, u64, u64, bool)>;
impl<'a> Ext<'a> {
@@ -201,7 +201,7 @@ impl<'a> Ext<'a> {
Inode::parse(&self.inode_bytes(ino)?)
}
/// Extents de uma árvore ext4 (raiz em `node`, 60 bytes ou um bloco).
/// Collects extents from an ext4 extent tree (`node` is the 60-byte `i_block` root or a block).
fn walk_extent_node(
&self,
node: &[u8],
@@ -248,7 +248,7 @@ impl<'a> Ext<'a> {
true
}
/// Blocos de um inode ext2/3 (mapa de blocos clássico).
/// Collects data blocks from an ext2/3 indirect block map.
fn block_map(&self, blk: &[u8; 60], max_blocks: u64) -> ExtentList {
let mut v: Vec<u64> = vec![];
let ptr = |i: usize| le32(blk, i * 4) as u64;
@@ -308,7 +308,7 @@ impl<'a> Ext<'a> {
out
}
/// Extents do arquivo (ordenados pelo bloco lógico), cortados em `size` bytes.
/// Returns file byte extents (ordered by logical block) truncated to `size` bytes.
fn file_extents(&self, ino: &Inode, size: u64) -> Vec<Extent> {
let need = size.div_ceil(self.bs);
let mut list: ExtentList = vec![];
@@ -351,7 +351,7 @@ impl<'a> Ext<'a> {
out
}
/// Mapa de blocos livres (bitmap), em extents absolutos.
/// Free-block map (from block bitmaps) as absolute byte extents.
fn free_map(&self, ctx: &Ctx) -> (Vec<Extent>, Vec<bool>) {
let mut free: Vec<Extent> = vec![];
let mut allocated_all: Vec<bool> = vec![];
@@ -418,7 +418,7 @@ struct DirEnt {
live: bool,
}
/// Lê as entradas de um bloco de diretório, inclusive as escondidas na folga.
/// Parses directory entries in a directory block, including unlinked entries in `rec_len` slack.
fn parse_dir_block(b: &[u8], max_ino: u64, out: &mut Vec<DirEnt>) {
let mut off = 0usize;
while off + 8 <= b.len() {
@@ -437,11 +437,11 @@ fn parse_dir_block(b: &[u8], max_ino: u64, out: &mut Vec<DirEnt>) {
live: true,
});
}
// Folga: entradas apagadas ficam coladas depois do fim "real" desta.
// Slack space: unlinked entries remain tucked right after the active entry's true length.
let mut s = off + if ino != 0 || nlen > 0 { real } else { 8 };
let end = off + rec;
if ino == 0 {
s = end; // entrada nula: sem folga confiável
s = end; // zeroed entry: no reliable slack to scan
}
while s + 8 <= end {
let dino = le32(b, s) as u64;
@@ -472,9 +472,9 @@ fn parse_dir_block(b: &[u8], max_ino: u64, out: &mut Vec<DirEnt>) {
}
struct Names {
/// inode -> (pasta, nome) de entradas vivas.
/// `inode -> (parent_dir, name)` from live directory entries.
live: HashMap<u64, (String, String)>,
/// inode -> (pasta, nome) de entradas apagadas (mais recente por varredura).
/// `inode -> (parent_dir, name)` from unlinked slack directory entries.
dead: HashMap<u64, (String, String)>,
}
@@ -531,9 +531,9 @@ fn walk_dirs(ext: &Ext, ctx: &Ctx) -> Names {
names
}
// ---------------------------------------------------------------- diário
// ---------------------------------------------------------------- journal
/// Cópias de inodes guardadas no diário: inode -> lista (ordem do diário) de registros brutos.
/// Inode copies found in the jbd2 journal: `inode_num -> Vec<raw_inode_bytes>` in journal order.
fn journal_inodes(ext: &Ext, ctx: &Ctx) -> HashMap<u64, Vec<Vec<u8>>> {
let mut out: HashMap<u64, Vec<Vec<u8>>> = HashMap::new();
if ext.journal_inum == 0 {
@@ -543,7 +543,7 @@ fn journal_inodes(ext: &Ext, ctx: &Ctx) -> HashMap<u64, Vec<Vec<u8>>> {
return out;
};
let jext = ext.file_extents(&jino, jino.size.min(1 << 30));
// Blocos físicos do diário em ordem lógica.
// Physical blocks of the journal in logical sequence.
let mut jblocks: Vec<u64> = vec![];
for e in &jext {
if e.sparse {
@@ -576,9 +576,9 @@ fn journal_inodes(ext: &Ext, ctx: &Ctx) -> HashMap<u64, Vec<Vec<u8>>> {
8 + if is64 { 4 } else { 0 }
};
let have_csum = csum_v2 || csum_v3;
// Tabela de inodes: faixa de blocos de cada grupo.
// Inode table block ranges for each block group.
let itable_blocks = (ext.ipg * ext.inode_size).div_ceil(ext.bs);
let mut tables: Vec<(u64, u64, u64)> = vec![]; // (início, fim, primeiro inode)
let mut tables: Vec<(u64, u64, u64)> = vec![]; // (start_block, end_block, first_inode_num)
for (g, gr) in ext.groups.iter().enumerate() {
tables.push((
gr.inode_table,
@@ -588,9 +588,9 @@ fn journal_inodes(ext: &Ext, ctx: &Ctx) -> HashMap<u64, Vec<Vec<u8>>> {
}
let n = jblocks.len();
let first = (be32(&jsb, 20) as usize).clamp(1, n);
let mut seqs: Vec<(u32, usize)> = vec![]; // (sequência, índice do descritor)
let mut seqs: Vec<(u32, usize)> = vec![]; // (commit_sequence, descriptor_index)
let mut idx = first;
// Varredura física: qualquer bloco que seja descritor, em ordem do diário.
// Scan journal blocks for descriptor blocks in sequence order.
while idx < n {
if idx % 4096 == 4095 && (ctx.cancelled)() {
break;
@@ -664,7 +664,7 @@ fn journal_inodes(ext: &Ext, ctx: &Ctx) -> HashMap<u64, Vec<Vec<u8>>> {
out
}
// -------------------------------------------------------------- varredura
// -------------------------------------------------------------- scan
pub fn scan(ctx: &mut Ctx, base: u64, part_len: u64) -> VolumeScan {
let mut res = VolumeScan::default();
@@ -680,7 +680,7 @@ pub fn scan(ctx: &mut Ctx, base: u64, part_len: u64) -> VolumeScan {
let mut emitted: HashSet<u64> = HashSet::new();
let mut seen_extents: HashSet<(u64, u64)> = HashSet::new();
// 1) Inodes apagados na tabela atual.
// 1) Deleted inodes in the current inode table.
let total = ext.groups.len() as u64;
for (g, gr) in ext.groups.iter().enumerate() {
if g % 16 == 15 && (ctx.cancelled)() {
@@ -711,7 +711,7 @@ pub fn scan(ctx: &mut Ctx, base: u64, part_len: u64) -> VolumeScan {
if !deleted || !ino.is_reg() {
continue;
}
// Melhor fonte: a cópia do diário anterior à exclusão; senão o que sobrou no inode.
// Best source: pre-deletion copy from the journal; fallback to remnants in the current inode.
let mut cand: Option<(Inode, &str)> = None;
if let Some(snaps) = journal.get(&ino_no) {
for raw in snaps.iter().rev() {
@@ -749,13 +749,13 @@ pub fn scan(ctx: &mut Ctx, base: u64, part_len: u64) -> VolumeScan {
}
}
// 2) Inodes que só existem no diário (liberados e já reaproveitados/zerados).
// 2) Inodes only preserved in the journal (already zeroed in the main inode table).
for (ino_no, snaps) in &journal {
if emitted.contains(ino_no) || names.live.contains_key(ino_no) || *ino_no < 11 {
continue;
}
let cur = ext.read_inode(*ino_no);
// Se o inode atual está vivo e é outro arquivo, não mexe.
// Skip if the inode is currently live in the filesystem.
if cur
.as_ref()
.map(|c| c.dtime == 0 && c.links > 0 && c.mode != 0)
@@ -805,7 +805,7 @@ fn emit_inode(
if ino.flags & EXT4_EXTENTS_FL != 0 {
let mut list: ExtentList = vec![];
if !ext.walk_extent_node(&ino.block, 5, &mut list, true) {
// Sobras de extent em inode truncado: cabeçalho zerado, registros ainda lá.
// Extent remnants in a truncated inode: header count was zeroed, but extent entries remain.
list = remnant_extents(ext, &ino.block);
}
if list.is_empty() {
@@ -834,7 +834,7 @@ fn emit_inode(
if !seen.insert((first.0, first.1)) {
return false;
}
// Quantos blocos de dados continuam livres? Reaproveitados => baixa confiança.
// Check how many data blocks are still unallocated; reallocated blocks lower confidence.
let mut reused = 0u64;
let mut total_blocks = 0u64;
for e in extents.iter().filter(|e| !e.sparse) {
@@ -859,7 +859,7 @@ fn emit_inode(
));
}
if estimated {
// Tamanho desconhecido: tenta achar o fim real pelo conteúdo.
// Unknown exact size (zeroed on deletion): try to refine size via format structure.
if let Some(h) = extents
.iter()
.find(|e| !e.sparse)
@@ -932,7 +932,7 @@ fn layout_from_list(ext: &Ext, list: &ExtentList, size: u64) -> Vec<Extent> {
out
}
/// Lê registros de extent deixados em i_block depois que o cabeçalho foi zerado.
/// Reads leftover extent records from `i_block` after `eh_entries` was zeroed on deletion.
fn remnant_extents(ext: &Ext, block: &[u8; 60]) -> ExtentList {
let mut out: ExtentList = vec![];
if le16(block, 0) != EXT_MAGIC || le16(block, 6) != 0 {
@@ -958,8 +958,8 @@ fn remnant_extents(ext: &Ext, block: &[u8; 60]) -> ExtentList {
out
}
/// Quão coerente é a estrutura do volume: fração das entradas de diretório cujo inode confere
/// com o tipo declarado. Serve para escolher parâmetros de RAID sem metadados.
/// Measures filesystem structural coherence: fraction of directory entries whose target inode
/// matches the declared file type. Used to score candidate metadata-less RAID parameters.
pub fn consistency_score(src: &dyn Source, base: u64) -> Option<f64> {
let ext = Ext::open(src, base)?;
let mut queue = vec![2u64];
@@ -988,8 +988,8 @@ pub fn consistency_score(src: &dyn Source, base: u64) -> Option<f64> {
o += ext.bs;
}
}
// A entrada "." de um diretório aponta para ele mesmo: só confere se inode e bloco
// vieram do lugar certo.
// A directory's "." entry points to itself: only matches if both the inode table and
// directory data block were mapped from the right RAID stripe.
dots_total += 1;
if ents.iter().any(|e| e.live && e.name == "." && e.ino == d) {
dots_ok += 1;
+9 -9
View File
@@ -1,5 +1,5 @@
//! FAT12/16/32: entradas de diretório apagadas (inclusive nomes longos) e
//! reconstrução dos clusters pela regra "próximos clusters livres".
//! FAT12/16/32: deleted directory entries (including VFAT Long File Names) and
//! cluster chain reconstruction using contiguous free clusters.
use super::{make_found, merge_extents, sanitize_name, utf16le, Ctx, NewFound, VolumeScan};
use crate::source::{read_vec, Source};
@@ -118,7 +118,7 @@ impl<'a> Fat<'a> {
}
}
/// Cadeia de clusters viva a partir de `start` (com limite contra ciclos).
/// Follows a live FAT cluster chain from `start` (bounded against cycles).
fn chain(&self, start: u32) -> Vec<u32> {
let mut out = vec![];
let mut c = start;
@@ -172,7 +172,7 @@ pub fn scan(ctx: &mut Ctx, base: u64, kind: FsKind) -> VolumeScan {
.push("FAT: setor de boot ilegível; usando apenas assinatura.".into());
return res;
};
// Estruturas do sistema (reservado + FATs + raiz fixa) são "vivas".
// Filesystem structures (reserved sectors + FATs + fixed root dir) count as allocated.
let mut live = vec![Extent::new(base, fat.data_off - base)];
let mut w = Walk {
fat: &fat,
@@ -257,7 +257,7 @@ impl Walk<'_, '_, '_, '_> {
continue;
}
if deleted || in_deleted {
// Diretório apagado: lê clusters contíguos livres enquanto parecerem entradas.
// Deleted directory: read contiguous free clusters while they look like directory entries.
let cl = self.contiguous_free(start, 16);
let data = self
.fat
@@ -359,7 +359,7 @@ impl Walk<'_, '_, '_, '_> {
}
}
/// Corta a lista de extents para exatamente `size` bytes.
/// Truncates an extent list to cover at most `size` bytes.
pub fn trim_extents(mut ex: Vec<Extent>, size: u64) -> Vec<Extent> {
let mut left = size;
let mut out = vec![];
@@ -416,15 +416,15 @@ fn long_name(parts: &[(u8, [u16; 13])], short: &[u8; 11]) -> Option<String> {
for &b in short {
sum = (sum >> 1).wrapping_add((sum & 1) << 7).wrapping_add(b);
}
// Entradas LFN de um arquivo apagado preservam o checksum do nome curto original,
// porém o primeiro byte do nome curto virou 0xE5; tentamos as duas variantes.
// Deleted LFN entries preserve the checksum of the original 8.3 short name,
// whereas the short entry's first byte became 0xE5; check both variants.
let ok = parts.iter().all(|p| p.0 == parts[0].0);
if !ok {
return None;
}
let target = parts[0].0;
if sum != target {
// Nome curto apagado perdeu o 1º caractere: aceita se o resto bater por tentativa.
// Deleted 8.3 name lost its first byte: accept LFN checksum if any valid first byte matches.
let mut hit = false;
for c in 0u8..=255 {
let mut s2 = *short;
+20 -20
View File
@@ -1,8 +1,8 @@
//! HFS+ / HFSX (inclusive volumes embutidos num wrapper HFS clássico).
//! HFS+ / HFSX (including volumes embedded inside a classic HFS wrapper).
//!
//! Lê o catálogo (árvore B+) para nomes e caminhos, o bitmap de alocação para o mapa de
//! espaço livre e procura arquivos apagados em três lugares: sobras dentro dos nós folha do
//! catálogo, cópias antigas de nós no diário (journal) e registros de arquivo "soltos".
//! Reads the catalog B-tree for names and paths, the allocation bitmap for free-space
//! mapping, and searches for deleted files in three places: slack space inside catalog leaf
//! nodes, older leaf node copies in the journal, and orphaned file records.
use super::{make_found, merge_extents, Ctx, NewFound, VolumeScan};
use crate::source::{read_vec, Source};
@@ -49,7 +49,7 @@ fn parse_fork(b: &[u8]) -> Fork {
f
}
/// Posição do volume HFS+ dentro da partição (wrapper HFS clássico) ou `None` se não for HFS+.
/// Byte offset of the HFS+ volume inside the partition (handles HFS wrapper), or `None`.
pub fn locate(src: &dyn Source, base: u64) -> Option<u64> {
let h = read_vec(src, base + 1024, 512).ok()?;
if h.len() < 512 {
@@ -106,7 +106,7 @@ impl<'a> Vol<'a> {
self.base + b * self.bs
}
/// Todas as extensões de um fork (inclusive as do arquivo de overflow).
/// Returns all extents of a fork (including overflow extents from the extents B-tree).
fn fork_extents(&self, f: &Fork, file_id: u32, fork_type: u8) -> Vec<Extent> {
let mut ex: Vec<(u32, u32)> = f.extents.clone();
let mut have: u64 = ex.iter().map(|e| e.1 as u64).sum();
@@ -124,7 +124,7 @@ impl<'a> Vol<'a> {
.collect()
}
/// Extensões além das oito iniciais, procuradas no arquivo de overflow (árvore B+).
/// Looks up extents beyond the first eight in the extents overflow B-tree.
fn overflow_extents(&self, file_id: u32, fork_type: u8, mut start: u32) -> Vec<(u32, u32)> {
let mut out = vec![];
let node_size = match self.tree_node_size(&self.extents_file) {
@@ -140,7 +140,7 @@ impl<'a> Vol<'a> {
continue;
}
for r in records(&node) {
// chave: keyLength u16, forkType u8, pad u8, fileID u32, startBlock u32
// key: keyLength u16, forkType u8, pad u8, fileID u32, startBlock u32
if r.len() < 12 + 64 || be16(r, 0) != 10 {
continue;
}
@@ -170,7 +170,7 @@ impl<'a> Vol<'a> {
self.read_fork_bytes(f, n as u64 * ns as u64, ns)
}
/// Lê bytes de um fork que tem só extensões iniciais ou overflow já resolvido (para árvores).
/// Reads bytes from a fork using its resolved extent list (used for system B-trees).
fn read_fork_bytes(&self, f: &Fork, off: u64, len: usize) -> Option<Vec<u8>> {
let mut out = Vec::with_capacity(len);
let mut pos = 0u64;
@@ -190,7 +190,7 @@ impl<'a> Vol<'a> {
}
}
/// Registros de um nó B+ (fatias, da chave em diante).
/// Returns the record slices of a B-tree node (each starting at its key).
fn records(node: &[u8]) -> Vec<&[u8]> {
let n = be16(node, 10) as usize;
let mut offs = vec![];
@@ -233,7 +233,7 @@ fn date(d: u32) -> Option<i64> {
(d != 0).then(|| d as i64 - HFS_EPOCH)
}
/// Decodifica um registro do catálogo (chave + dados). `max_id` descarta lixo.
/// Decodes a catalog record (key + data). `max_id` filters out garbage CNIDs.
fn decode_record(
r: &[u8],
max_id: u32,
@@ -265,7 +265,7 @@ fn decode_record(
}
}
3 if rec.len() >= 10 => {
// thread de pasta: chave = (id da pasta, ""); dados = (pai, nome)
// folder thread record: key = (folder_id, ""); data = (parent_id, folder_name)
let pn = be16(rec, 8) as usize;
if pn <= 255 {
threads.entry(parent).or_insert((
@@ -304,7 +304,7 @@ fn decode_record(
}
}
/// Procura registros de arquivo nas sobras de um nó (entre os dados e a tabela de offsets).
/// Carves deleted catalog file records from B-tree node slack space (between active records and offset table).
fn scan_slack(
node: &[u8],
max_id: u32,
@@ -346,7 +346,7 @@ pub fn scan(ctx: &mut Ctx, base: u64, part_len: u64) -> VolumeScan {
return res;
};
// Mapa de espaço livre.
// Free-space map from the allocation file.
if let Some(bm) = vol.read_fork_all(&vol.alloc, (vol.total / 8 + 1) as usize) {
let mut free = vec![];
let mut run: Option<u64> = None;
@@ -409,7 +409,7 @@ pub fn scan(ctx: &mut Ctx, base: u64, part_len: u64) -> VolumeScan {
n = be32(&node, 0);
}
// Diário: cópias antigas de nós folha do catálogo.
// Journal: older copies of catalog leaf nodes.
if vol.journal_info != 0 {
scan_journal(
&vol,
@@ -422,7 +422,7 @@ pub fn scan(ctx: &mut Ctx, base: u64, part_len: u64) -> VolumeScan {
);
}
// Caminho de cada pasta a partir dos threads.
// Reconstruct folder paths from folder thread records.
let path_of = |parent: u32| -> String {
let mut parts = vec![];
let mut cur = parent;
@@ -462,9 +462,9 @@ pub fn scan(ctx: &mut Ctx, base: u64, part_len: u64) -> VolumeScan {
if !seen.insert((f.id, ex[0].offset)) {
continue;
}
// Mantém só os bytes do arquivo.
// Trim extents to the logical file size.
let extents = super::fat::trim_extents(ex, f.data.size);
// Blocos já reaproveitados?
// Check whether any blocks have already been reallocated.
let reused = extents.iter().filter(|e| !covered_by(&free_set, e)).count();
let (conf, note) = if reused == 0 {
(
@@ -523,7 +523,7 @@ impl Vol<'_> {
}
}
/// Procura nós folha do catálogo nas transações guardadas no diário.
/// Scans the HFS+ journal buffer for historical catalog leaf nodes.
fn scan_journal(
vol: &Vol,
ns: usize,
@@ -556,7 +556,7 @@ fn scan_journal(
let mut i = 0;
while i + ns <= buf.len().min(n + ns) && i < n {
let nd = &buf[i..i + ns];
// nó folha do catálogo: kind = -1, altura 1, registros plausíveis
// catalog leaf node: kind = -1, height = 1, plausible record offsets
if nd[8] == 0xFF && nd[9] == 1 && be16(nd, 10) > 0 && (be16(nd, 10) as usize) < ns / 8 {
let table = ns - 2 * (be16(nd, 10) as usize + 1);
if (be16(nd, ns - 2) as usize) == 14
+13 -13
View File
@@ -1,8 +1,8 @@
//! Leitores de metadados de sistemas de arquivos (somente leitura).
//! Read-only filesystem metadata readers.
//!
//! Cada leitor percorre estruturas do volume atrás de arquivos apagados,
//! devolve os extents ainda alocados ("live") para o carving pular e, quando
//! possível, os trechos livres.
//! Each reader walks volume metadata structures looking for deleted files,
//! returns currently allocated ("live") extents so file carving can skip them,
//! and, when available, returns the exact free-space map.
pub mod apfs;
pub mod btrfs;
@@ -18,12 +18,12 @@ use crate::source::{read_vec, Source};
use crate::types::{Confidence, DataLoc, Extent, Found, Origin};
use std::sync::atomic::{AtomicU64, Ordering};
/// Contexto compartilhado por todos os leitores.
/// Context shared across all filesystem metadata readers.
pub struct Ctx<'a> {
pub src: &'a dyn Source,
pub next_id: &'a AtomicU64,
pub cancelled: &'a (dyn Fn() -> bool + Sync),
/// (feito, total) dentro da fase atual do leitor.
/// Reports `(done, total)` progress within the current reader phase.
pub progress: &'a (dyn Fn(u64, u64) + Sync),
pub emit: &'a mut dyn FnMut(Found),
}
@@ -34,12 +34,12 @@ impl Ctx<'_> {
}
}
/// Resultado de varrer um volume.
/// Output of scanning a volume's metadata.
#[derive(Default)]
pub struct VolumeScan {
/// Extents (absolutos) ocupados por arquivos existentes e metadados.
/// Absolute byte extents occupied by live files and filesystem metadata.
pub live: Vec<Extent>,
/// Se o leitor conhece o mapa de espaço livre (ext), os trechos livres (absolutos).
/// Absolute free byte extents when the filesystem provides an explicit allocation bitmap.
pub free: Option<Vec<Extent>>,
pub warnings: Vec<String>,
}
@@ -57,7 +57,7 @@ pub struct NewFound<'a> {
pub note: Option<&'a str>,
}
/// Monta um `Found`, ajustando a confiança pelo conteúdo real no disco.
/// Builds a `Found` record, adjusting confidence based on actual on-disk header bytes.
pub fn make_found(ctx: &Ctx, n: NewFound) -> Found {
let ext = n
.name
@@ -116,7 +116,7 @@ pub fn make_found(ctx: &Ctx, n: NewFound) -> Found {
}
}
/// Ordena e funde extents adjacentes/sobrepostos.
/// Sorts and merges adjacent or overlapping byte extents.
pub fn merge_extents(mut v: Vec<Extent>) -> Vec<Extent> {
v.retain(|e| e.len > 0);
v.sort_by_key(|e| e.offset);
@@ -135,7 +135,7 @@ pub fn merge_extents(mut v: Vec<Extent>) -> Vec<Extent> {
out
}
/// Há sobreposição entre [off, off+len) e algum extent de `sorted` (já mesclado)?
/// Returns `true` if `[off, off + len)` overlaps any extent in `sorted` (must be pre-merged).
pub fn overlaps(sorted: &[Extent], off: u64, len: u64) -> bool {
let i = sorted.partition_point(|e| e.end() <= off);
sorted.get(i).map(|e| e.offset < off + len).unwrap_or(false)
@@ -159,7 +159,7 @@ pub fn dos_to_unix(date: u16, time: u16) -> Option<i64> {
Some(days_from_civil(year, month, day) * 86400 + h * 3600 + m * 60 + s)
}
/// Dias desde 1970-01-01 (algoritmo de Howard Hinnant).
/// Days since 1970-01-01 UTC (Howard Hinnant civil calendar algorithm).
pub fn days_from_civil(y: i64, m: i64, d: i64) -> i64 {
let y = if m <= 2 { y - 1 } else { y };
let era = if y >= 0 { y } else { y - 399 } / 400;
+10 -10
View File
@@ -1,5 +1,5 @@
//! NTFS: percorre a MFT atrás de registros apagados, resolve caminhos e
//! reconstrói os data runs. Coleta também os extents vivos.
//! NTFS: walks the MFT for deleted file records, reconstructs folder paths,
//! and decodes data runs. Also collects live allocated extents.
use super::{
make_found, merge_extents, overlaps, sanitize_name, utf16le, Ctx, NewFound, VolumeScan,
@@ -9,10 +9,10 @@ use crate::types::{Confidence, DataLoc, Extent, Origin};
use std::collections::HashMap;
struct Run {
/// Cluster lógico inicial no arquivo (VCN).
/// Starting Virtual Cluster Number (VCN) within the file.
vcn: u64,
len: u64,
/// None = esparso.
/// `None` = sparse run.
lcn: Option<u64>,
}
@@ -61,7 +61,7 @@ pub fn scan(ctx: &mut Ctx, base: u64) -> VolumeScan {
return res;
}
// Registro 0 ($MFT) define onde está toda a MFT.
// MFT record 0 ($MFT) defines the data runs covering the entire MFT.
let mut mft_runs: Option<(Vec<Run>, u64)> = None;
for lcn in [mft_lcn, mirr_lcn] {
if let Some(mut r) = read_vec(ctx.src, base + lcn * cl, rec_size as usize)
@@ -96,7 +96,7 @@ pub fn scan(ctx: &mut Ctx, base: u64) -> VolumeScan {
let mut dirs: HashMap<u64, (u64, String)> = HashMap::new();
let mut deleted: Vec<(u64, Rec)> = vec![];
// Lê a MFT em blocos grandes seguindo os runs.
// Read the MFT in large chunks across its data runs.
let chunk_recs = (1 << 20) / rec_size.max(1);
let mut idx = 0u64;
let mut buf: Vec<u8>;
@@ -161,7 +161,7 @@ pub fn scan(ctx: &mut Ctx, base: u64) -> VolumeScan {
unsupported,
} => {
if unsupported {
continue; // comprimido/cifrado (EFS): fora do escopo da v1
continue; // compressed/encrypted (EFS) attributes are skipped
}
let mut ex = vec![];
for r in &runs {
@@ -291,7 +291,7 @@ fn le64(b: &[u8], o: usize) -> u64 {
u64::from_le_bytes(b[o..o + 8].try_into().unwrap())
}
/// Interpreta um registro de arquivo da MFT (aplica fixups no próprio buffer).
/// Parses an MFT `FILE` record (applies USA fixups in place).
fn parse_record(r: &mut [u8], bps: u64, _base: u64, _cl: u64) -> Option<Rec> {
if r.len() < 48 || &r[0..4] != b"FILE" {
return None;
@@ -308,7 +308,7 @@ fn parse_record(r: &mut [u8], bps: u64, _base: u64, _cl: u64) -> Option<Rec> {
break;
}
if r[end - 2..end] != usn {
return None; // registro rasgado
return None; // torn MFT record (fixup mismatch)
}
r[end - 2] = r[usa_off + i * 2];
r[end - 1] = r[usa_off + i * 2 + 1];
@@ -316,7 +316,7 @@ fn parse_record(r: &mut [u8], bps: u64, _base: u64, _cl: u64) -> Option<Rec> {
let flags = le16(r, 22);
let base_ref = le64(r, 32) & 0xFFFF_FFFF_FFFF;
if base_ref != 0 {
return None; // registro de extensão: tratado só via lista de atributos (não suportado)
return None; // extension MFT record (handled via $ATTRIBUTE_LIST, skipped as standalone)
}
let mut rec = Rec {
in_use: flags & 1 != 0,
+26 -26
View File
@@ -1,7 +1,7 @@
//! XFS (v4 e v5): mapa de espaço livre pela árvore de blocos livres de cada grupo de alocação e
//! arquivos apagados. Ao liberar um inode, o XFS zera o modo e o tamanho mas não apaga a lista de
//! extents que ficou dentro do inode; e a entrada de diretório liberada perde só os 4 primeiros
//! bytes (o número do inode), preservando o nome. Juntamos as duas sobras.
//! XFS (v4 and v5): free-space map from each allocation group's free-block B+tree and deleted
//! file recovery. When freeing an inode, XFS zeroes mode and size if not fully wiped, leaving
//! inline extent records in the inode fork; freed directory entries only overwrite the first
//! 4 bytes of the 8-byte inode number, preserving the low 32 bits and the filename.
use super::{make_found, merge_extents, Ctx, NewFound, VolumeScan};
use crate::signatures;
@@ -88,7 +88,7 @@ impl<'a> Xfs<'a> {
self.base + agno * self.agblocks * self.bs
}
/// Endereço físico de um bloco "fsblock" (agno << agblklog | agbno).
/// Converts an XFS `fsblock` (`agno << agblklog | agbno`) to a physical byte offset.
fn fsb_off(&self, fsb: u64) -> Option<u64> {
let (agno, agbno) = (fsb >> self.agblklog, fsb & ((1 << self.agblklog) - 1));
(agno < self.agcount && agbno < self.agblocks).then(|| self.ag_off(agno) + agbno * self.bs)
@@ -116,7 +116,7 @@ impl<'a> Xfs<'a> {
}
}
/// Extents de um inode com a lista no próprio inode (formato 2) ou em árvore (formato 3).
/// Reads extents from an inode data fork (format 2 inline extent list or format 3 B+tree).
fn extents_of(&self, ino: &[u8], max_blocks: u64) -> Vec<(u64, u64, u64)> {
let fmt = ino[5];
let start = self.core_size(ino);
@@ -135,7 +135,7 @@ impl<'a> Xfs<'a> {
}
}
3 => {
// raiz da árvore de extents no inode: nível, nregistros, chaves e ponteiros
// B+tree root inside the inode fork: level, numrecs, keys, and block pointers
let level = be16(lit, 0) as usize;
let nrecs = be16(lit, 2) as usize;
let maxrecs = (lit.len() - 4) / 16;
@@ -180,7 +180,7 @@ impl<'a> Xfs<'a> {
}
}
/// Registro de extent empacotado em 128 bits: flag(1) | início no arquivo(54) | bloco(52) | quantidade(21).
/// Unpacks a 128-bit XFS extent record: `flag(1) | startoff(54) | startblock(52) | blockcount(21)`.
fn unpack_extent(r: &[u8]) -> (u64, u64, u64) {
let hi = u64::from_be_bytes(r[0..8].try_into().unwrap());
let lo = u64::from_be_bytes(r[8..16].try_into().unwrap());
@@ -190,7 +190,7 @@ fn unpack_extent(r: &[u8]) -> (u64, u64, u64) {
(startoff, startblock, count)
}
/// Percorre as folhas de uma árvore B+ curta, entregando cada registro de 16 (inobt) ou 8 (bnobt) bytes.
/// Walks the leaves of an AG B+tree, yielding each 16-byte (`inobt`) or 8-byte (`bnobt`) record.
fn walk_short_btree(
x: &Xfs,
agno: u64,
@@ -201,7 +201,7 @@ fn walk_short_btree(
f: &mut dyn FnMut(&[u8]),
) {
let mut cur = root;
// desce pela esquerda até a folha
// descend leftmost branch down to the leaf level
for lvl in (1..levels).rev() {
let Ok(b) = read_vec(x.src, x.ag_off(agno) + cur * x.bs, x.bs as usize) else {
return;
@@ -241,10 +241,10 @@ pub fn scan(ctx: &mut Ctx, base: u64, part_len: u64) -> VolumeScan {
return res;
};
// Espaço livre por grupo e inodes livres.
// Collect free-space ranges and free inode records across all allocation groups.
let mut free: Vec<Extent> = vec![];
let mut free_set: Vec<(u64, u64)> = vec![]; // (fsblock inicial, quantidade) para checar blocos
let mut chunks: Vec<(u64, u64, u64)> = vec![]; // (agno, startino, máscara livre)
let mut free_set: Vec<(u64, u64)> = vec![]; // (start_fsblock, block_count) for free-block checks
let mut chunks: Vec<(u64, u64, u64)> = vec![]; // (agno, startino, free_bitmask)
for agno in 0..x.agcount {
if (ctx.cancelled)() {
return res;
@@ -278,10 +278,10 @@ pub fn scan(ctx: &mut Ctx, base: u64, part_len: u64) -> VolumeScan {
.is_some_and(|&(s, c)| s <= fsb && s + c >= fsb + count)
};
// Nomes: entradas de diretório liberadas (guardam o nome e os 4 bytes baixos do inode).
// Names: freed directory entries (preserve filename and low 32 bits of the inode number).
let (live_dirs, stale_names) = collect_names(&x, ctx);
// Inodes livres com a lista de extents ainda dentro do inode.
// Free inodes that still contain leftover extent records in their data fork.
let max_blocks = x.dblocks;
let mut seen_first: HashSet<u64> = HashSet::new();
for (agno, startino, mask) in &chunks {
@@ -290,7 +290,7 @@ pub fn scan(ctx: &mut Ctx, base: u64, part_len: u64) -> VolumeScan {
}
for i in 0..64u64 {
if mask >> i & 1 == 0 {
continue; // em uso
continue; // allocated inode
}
let ino = (agno << (x.agblklog + x.inopblog)) | (startino + i);
let Some(raw) = x.read_ino(ino) else { continue };
@@ -298,13 +298,13 @@ pub fn scan(ctx: &mut Ctx, base: u64, part_len: u64) -> VolumeScan {
if mode != 0 && be32(&raw, 16) != 0 {
continue;
}
// Só inodes que foram arquivos: o formato de extents continua gravado e há sobras.
// Check freed inodes that still have non-zero extent records in the data fork.
if raw[5] != 2 {
continue;
}
let start = x.core_size(&raw);
let lit = &raw[start..];
// lê registros plausíveis em sequência
// read consecutive plausible extent records
let mut ext: Vec<(u64, u64, u64)> = vec![];
let mut next_off = 0u64;
for k in 0..lit.len() / 16 {
@@ -346,7 +346,7 @@ pub fn scan(ctx: &mut Ctx, base: u64, part_len: u64) -> VolumeScan {
} else {
"blocos do arquivo já reaproveitados".to_string()
};
// tamanho exato: tenta achar o fim real pelo conteúdo
// exact size was zeroed on deletion: refine size from file structure when possible
let mut est = true;
if let Some(h) = read_vec(ctx.src, extents[0].offset, 128)
.ok()
@@ -394,7 +394,7 @@ pub fn scan(ctx: &mut Ctx, base: u64, part_len: u64) -> VolumeScan {
res
}
/// Percorre os diretórios vivos; devolve (caminho de cada diretório por inode, nome liberado por inode baixo).
/// Walks live directories; returns `(dir_paths_by_inode, freed_names_by_low_inode_u32)`.
fn collect_names(x: &Xfs, ctx: &Ctx) -> (HashMap<u64, String>, HashMap<u64, (u64, String)>) {
let mut paths: HashMap<u64, String> = HashMap::new();
let mut stale: HashMap<u64, (u64, String)> = HashMap::new();
@@ -414,7 +414,7 @@ fn collect_names(x: &Xfs, ctx: &Ctx) -> (HashMap<u64, String>, HashMap<u64, (u64
let start = x.core_size(&raw);
match raw[5] {
1 => {
// pasta curta guardada no inode
// shortform directory stored inline inside the inode
let lit = &raw[start..];
let count = lit[0] as usize;
let i8 = lit[1] != 0;
@@ -443,7 +443,7 @@ fn collect_names(x: &Xfs, ctx: &Ctx) -> (HashMap<u64, String>, HashMap<u64, (u64
let ex = x.extents_of(&raw, 1 << 20);
let _ = size;
for (off, fsb, cnt) in ex {
// blocos de dados de diretório ficam antes do offset de 32 GiB (depois vêm folhas e índice)
// directory data blocks live below the 32 GiB logical offset (leaf/freeindex blocks live above)
if off * x.bs >= 32u64 << 30 {
continue;
}
@@ -479,7 +479,7 @@ fn parse_dir_block(
_ => return,
};
let is_block = magic == b"XD2B" || magic == b"XDB3";
// em blocos únicos, o final tem a tabela de folhas
// single-block directory: trim the trailing leaf/tail table
let end = if is_block {
let cnt = be32(b, b.len() - 8) as usize;
b.len().saturating_sub(8 + cnt * 8)
@@ -493,7 +493,7 @@ fn parse_dir_block(
if len < 8 || p + len > end {
break;
}
// entrada liberada: sobraram (inode baixo, nome); outras ainda podem estar dentro da região
// freed entry region: extract `(low_inode_u32, name)` and any subsequent unlinked entries inside the region
scan_free_region(x, &b[p..p + len], dino, stale);
p += len;
continue;
@@ -517,7 +517,7 @@ fn parse_dir_block(
}
fn scan_free_region(x: &Xfs, r: &[u8], dir_ino: u64, stale: &mut HashMap<u64, (u64, String)>) {
// 1) a primeira entrada perdeu só os 4 primeiros bytes do número do inode
// 1) the first unlinked entry at the start of the free tag lost only the upper 4 bytes of its inode number
let try_at = |p: usize, low: u64, stale: &mut HashMap<u64, (u64, String)>| -> Option<usize> {
let nl = *r.get(p + 8)? as usize;
let name = r.get(p + 9..p + 9 + nl)?;
@@ -532,7 +532,7 @@ fn scan_free_region(x: &Xfs, r: &[u8], dir_ino: u64, stale: &mut HashMap<u64, (u
if r.len() >= 16 {
let low = be32(r, 4) as u64;
if let Some(sz) = try_at(0, low, stale) {
// entradas seguintes dentro da mesma região, com o inode inteiro
// subsequent coalesced entries inside the same free region still have their full 8-byte inode number
let mut p = sz;
while p + 16 <= r.len() {
let ino = be64(r, p);
+1 -1
View File
@@ -1,4 +1,4 @@
//! Hashes de integridade usados em perícia: MD5, SHA-1 e SHA-256 numa única passada.
//! Forensic integrity hashes: computes MD5, SHA-1, and SHA-256 in a single pass.
use md5::Md5;
use serde::{Deserialize, Serialize};
+31 -31
View File
@@ -1,6 +1,6 @@
//! Saúde do disco antes de varrer: lê o SMART (via `smartctl`, quando instalado) e
//! os contadores de erro do kernel. Um disco com defeito deve ser copiado para uma
//! imagem primeiro: cada leitura extra pode piorar o estado dele.
//! Pre-scan disk health assessment: reads SMART data (via `smartctl`, when installed) and
//! kernel I/O error counters. A failing drive should be imaged first, as every additional
//! read pass can accelerate media degradation.
use serde::Serialize;
use serde_json::Value;
@@ -8,43 +8,43 @@ use std::process::{Command, Stdio};
#[derive(Debug, Clone, Serialize, Default)]
pub struct Health {
/// Foi possível avaliar? (imagens de disco e dispositivos sem SMART ficam sem avaliação)
/// Whether SMART assessment was possible (disk images and unsupported bridges return `false`).
pub available: bool,
/// "ok", "warning", "failing" ou "unknown".
/// Overall status: `"ok"`, `"warning"`, `"failing"`, or `"unknown"`.
pub status: String,
pub summary: String,
pub details: Vec<(String, String)>,
pub advice: Vec<String>,
/// Ficha do dispositivo, no estilo do CrystalDiskInfo (modelo, firmware, interface...).
/// CrystalDiskInfo-style device summary (model, firmware, interface, etc.).
pub info: Vec<(String, String)>,
/// Saúde estimada em % (SSD: vida útil restante). `None` em HD, que não informa isso.
/// Estimated health percentage (SSD remaining life). `None` for HDDs that do not report it.
pub health_pct: Option<u8>,
pub temperature_c: Option<i64>,
pub power_on_hours: Option<i64>,
pub power_cycles: Option<i64>,
/// Total lido e gravado pelo computador, em bytes, quando o disco informa.
/// Total host bytes read and written, when reported by the drive.
pub host_reads: Option<u64>,
pub host_writes: Option<u64>,
/// Tabela completa de atributos SMART.
/// Full table of SMART attributes.
pub attributes: Vec<Attr>,
/// Caminho avaliado de fato (o disco inteiro, quando `path` é uma partição).
/// Actual device path queried (the containing whole disk when `path` is a partition).
pub disk: String,
/// Partição pedida, quando a avaliação foi feita no disco que a contém.
/// Original partition path when the query was redirected to its parent disk.
pub partition: Option<String>,
}
/// Uma linha da tabela de atributos SMART.
/// A single row in the SMART attribute table.
#[derive(Debug, Clone, Serialize, Default)]
pub struct Attr {
/// Identificador em hexadecimal, como o CrystalDiskInfo mostra ("05", "C5").
/// Two-digit hex attribute ID as displayed by CrystalDiskInfo (`"05"`, `"C5"`).
pub id: String,
pub name: String,
/// Valores normalizados (ATA): atual, pior já visto e limite de falha.
/// Normalized ATA values: current, worst, and failure threshold.
pub value: Option<i64>,
pub worst: Option<i64>,
pub thresh: Option<i64>,
pub raw: String,
/// "ok", "warning" ou "failing".
/// Attribute status: `"ok"`, `"warning"`, or `"failing"`.
pub status: String,
}
@@ -61,7 +61,7 @@ impl Health {
const IMAGE_ADVICE: &str = "Crie a imagem do disco primeiro (botão \"Criar imagem\" ou `guara image`) e varra a cópia. Cada leitura extra pode piorar um disco com defeito.";
/// Avalia o dispositivo `path`. Para arquivos comuns (imagens) devolve "sem avaliação".
/// Evaluates disk health for `path`. Regular image files return an unassessed report.
pub fn check(path: &str) -> Health {
#[cfg(unix)]
let is_dev = {
@@ -88,8 +88,8 @@ pub fn check(path: &str) -> Health {
h
}
/// O SMART é do disco, não da partição: `/dev/sda1` vira `/dev/sda`,
/// `/dev/nvme0n1p2` vira `/dev/nvme0n1` e `/dev/disk2s1` vira `/dev/disk2`.
/// SMART belongs to the physical disk rather than a partition: `/dev/sda1` -> `/dev/sda`,
/// `/dev/nvme0n1p2` -> `/dev/nvme0n1`, and `/dev/disk2s1` -> `/dev/disk2`.
pub fn parent_disk(path: &str) -> String {
#[cfg(target_os = "linux")]
{
@@ -117,7 +117,7 @@ pub fn parent_disk(path: &str) -> String {
}
fn smartctl_name(path: &str) -> String {
// Windows: \\.\PhysicalDriveN -> /dev/sdX (convenção do smartctl)
// Windows: \\.\PhysicalDriveN -> /dev/sdX (smartctl device naming convention)
if let Some(n) = path
.strip_prefix(r"\\.\PhysicalDrive")
.and_then(|n| n.parse::<u8>().ok())
@@ -139,9 +139,9 @@ fn run_smartctl(path: &str) -> Option<Value> {
.stderr(Stdio::null())
.output()
.ok()?;
// smartctl usa bits do código de saída para avisos; o JSON vale mesmo assim
// smartctl encodes warnings in exit status bits; the JSON output is still valid
let v: Value = serde_json::from_slice(&out.stdout).ok()?;
// sem permissão ou dispositivo não suportado
// insufficient permissions or unsupported device
if v.pointer("/smartctl/messages")
.and_then(|m| m.as_array())
.is_some_and(|m| m.iter().any(|x| x["severity"] == "error"))
@@ -163,14 +163,14 @@ fn attr(v: &Value, id: u64) -> Option<i64> {
.as_i64()
}
/// Interpreta a saída JSON do `smartctl` (testável sem disco).
/// Parses `smartctl -j` JSON output (unit-testable without hardware).
pub fn parse_smartctl(v: &Value) -> Health {
let mut h = Health {
available: true,
status: "ok".into(),
..Default::default()
};
let mut bad = 0u32; // 2 = falhando, 1 = atenção
let mut bad = 0u32; // 2 = failing, 1 = warning
let mut note = |h: &mut Health, sev: u32, k: &str, val: String| {
bad = bad.max(sev);
h.details.push((k.to_string(), val));
@@ -204,7 +204,7 @@ pub fn parse_smartctl(v: &Value) -> Health {
h.details
.push(("Autoavaliação SMART".into(), "aprovado".into()));
}
// ATA (HD e SSD SATA)
// ATA (SATA HDDs and SSDs)
if let Some(n) = attr(v, 5) {
if n > 0 {
note(
@@ -320,7 +320,7 @@ fn n(v: &Value, p: &str) -> Option<i64> {
v.pointer(p).and_then(|x| x.as_i64())
}
/// Ficha do dispositivo e métricas gerais (temperatura, horas, total lido/gravado, saúde %).
/// Extracts device identity and summary metrics (temperature, power-on hours, host R/W, health %).
fn fill_info(v: &Value, h: &mut Health) {
let mut put = |k: &str, val: Option<String>| {
if let Some(val) = val {
@@ -395,7 +395,7 @@ fn fill_info(v: &Value, h: &mut Health) {
h.power_on_hours = n(v, "/power_on_time/hours");
h.power_cycles = n(v, "/power_cycle_count");
if let Some(l) = v.get("nvme_smart_health_information_log") {
// a unidade NVMe vale 1000 setores de 512 bytes
// 1 NVMe data unit = 1,000 x 512-byte sectors
h.host_reads = l["data_units_read"].as_u64().map(|u| u * 512_000);
h.host_writes = l["data_units_written"].as_u64().map(|u| u * 512_000);
h.health_pct = l["percentage_used"]
@@ -406,7 +406,7 @@ fn fill_info(v: &Value, h: &mut Health) {
let lba = n(v, "/logical_block_size").unwrap_or(512) as u64;
h.host_reads = attr(v, 242).map(|x| x as u64 * lba);
h.host_writes = attr(v, 241).map(|x| x as u64 * lba);
// SSD SATA: vida útil restante no valor normalizado de um destes atributos
// SATA SSD: remaining life percentage stored in the normalized value of one of these attributes
// (231 SSD Life Left, 233 Media Wearout, 177 Wear Leveling, 202 Percent Lifetime Remain)
h.health_pct = [231u64, 233, 177, 202, 169]
.iter()
@@ -416,7 +416,7 @@ fn fill_info(v: &Value, h: &mut Health) {
}
}
/// Capacidade como o fabricante anuncia (base 1000): "500,1 GB".
/// Formats drive capacity in decimal manufacturer units (base 1000, e.g. `"500,1 GB"`).
fn decimal_size(b: u64) -> String {
let units = ["B", "kB", "MB", "GB", "TB", "PB"];
let mut x = b as f64;
@@ -437,7 +437,7 @@ fn attr_field(v: &Value, id: u64, f: &str) -> Option<i64> {
.as_i64()
}
/// Atributos cujo valor bruto acima de zero já é motivo de atenção.
/// Critical ATA attributes where any non-zero raw count warrants a warning.
const RAW_WARN: [u64; 5] = [5, 196, 197, 198, 187];
fn fill_attributes(v: &Value, h: &mut Health) {
@@ -484,7 +484,7 @@ fn fill_attributes(v: &Value, h: &mut Health) {
}
}
if let Some(l) = v.get("nvme_smart_health_information_log") {
// mesma ordem e nomes que o CrystalDiskInfo usa para NVMe
// same order and labels used by CrystalDiskInfo for NVMe health logs
let rows: [(u8, &str, &str); 15] = [
(1, "Critical Warning", "critical_warning"),
(2, "Composite Temperature", "temperature"),
@@ -529,7 +529,7 @@ fn fill_attributes(v: &Value, h: &mut Health) {
}
}
/// Contadores de erro de E/S do kernel (Linux): reforçam o aviso do SMART.
/// Linux kernel sysfs I/O error counters: supplement SMART warnings.
fn kernel_errors(path: &str, h: &mut Health) {
#[cfg(target_os = "linux")]
{
+4 -4
View File
@@ -1,4 +1,4 @@
//! Rotas HTTP de mídia (`/file` com Range e `/thumb`) compartilhadas pelo modo `guara ui`.
//! Media HTTP routes (`/file` with Range support and `/thumb`) shared by `guara ui` mode.
use crate::service::Engine;
use tiny_http::{Header, Method, Request, Response, StatusCode};
@@ -7,15 +7,15 @@ pub fn header(name: &str, value: &str) -> Header {
Header::from_bytes(name.as_bytes(), value.as_bytes()).unwrap()
}
/// O cabeçalho Host bate com um dos endereços esperados (defesa contra DNS rebinding)?
/// Checks whether the HTTP `Host` header matches an allowed local address (DNS rebinding defense).
pub fn host_ok(req: &Request, hosts: &[String]) -> bool {
req.headers()
.iter()
.any(|h| h.field.equiv("Host") && hosts.iter().any(|x| x == h.value.as_str()))
}
/// Responde `/file/<job>/<id>` (com Range) e `/thumb/<job>/<id>?s=NNN`.
/// Devolve a requisição quando ela não é uma rota de mídia.
/// Serves `/file/<job>/<id>` (with Range support) and `/thumb/<job>/<id>?s=NNN`.
/// Returns `Err(req)` when the request is not a media route.
pub fn handle_media(eng: &Engine, token: &str, req: Request) -> Option<Request> {
let full = req.url().to_string();
let (path, query) = full.split_once('?').unwrap_or((full.as_str(), ""));
+9 -9
View File
@@ -1,5 +1,5 @@
//! Imagem forense de um disco: leitura tolerante a setores ruins, com mapa de
//! erros e hash. Serve para trabalhar sempre sobre a cópia.
//! Forensic disk imaging: fault-tolerant reading across bad sectors with an error
//! map and integrity hashes so recovery always runs against a copy.
use crate::error::{Error, Result};
use crate::hashing::MultiHasher;
@@ -16,7 +16,7 @@ pub struct ImageReport {
pub md5: String,
#[serde(default)]
pub sha1: String,
/// (offset, tamanho) dos trechos ilegíveis, preenchidos com zeros.
/// `(offset, length)` of unreadable byte ranges that were zero-filled in the output image.
pub bad_ranges: Vec<(u64, u64)>,
pub cancelled: bool,
}
@@ -29,7 +29,7 @@ const SECTOR: usize = 512;
pub enum ImageFormat {
#[default]
Raw,
/// Expert Witness (EnCase 6): `.E01`, dividido em segmentos.
/// Expert Witness Format (EnCase 6): segmented `.E01`.
E01 {
#[serde(default)]
case_number: String,
@@ -41,14 +41,14 @@ pub enum ImageFormat {
examiner: String,
#[serde(default)]
notes: String,
/// Tamanho máximo de cada segmento em MiB (0 = um arquivo só).
/// Maximum size of each segment in MiB (0 = single segment).
#[serde(default = "default_segment_mib")]
segment_mib: u64,
/// 0 = sem compressão, 1 a 9 = deflate.
/// 0 = uncompressed, 1..=9 = deflate compression level.
#[serde(default = "default_compression")]
compression: u32,
},
/// AFF4 (padrão v1.0, blocos snappy).
/// AFF4 (v1.0 standard, snappy-compressed chunks).
Aff4 {
#[serde(default)]
case_number: String,
@@ -119,7 +119,7 @@ pub fn create_image_as(
compression: *compression,
..Default::default()
};
// O destino pode vir com ou sem a extensão .E01.
// Destination path may be passed with or without the `.E01` extension.
let base = match dest.extension().and_then(|e| e.to_str()) {
Some(e) if e.eq_ignore_ascii_case("e01") => dest.with_extension(""),
_ => dest.to_path_buf(),
@@ -160,7 +160,7 @@ pub fn create_image(
match read_full(src, pos, &mut buf[..want]) {
Ok(()) => {}
Err(_) => {
// Refaz setor a setor para isolar os trechos ruins.
// Retry sector by sector to isolate the exact bad sectors.
let mut o = 0;
while o < want {
let n = SECTOR.min(want - o);
+34 -34
View File
@@ -1,13 +1,13 @@
//! Decodificador de entropia de JPEG sequencial (Huffman, 8 bits), sem IDCT.
//! Baseline sequential JPEG Huffman entropy decoder (8-bit), plus lightweight IDCT.
//!
//! Serve para validar se os bytes de um JPEG são realmente contínuos e para reconstruir arquivos
//! fragmentados: o fluxo é decodificado MCU a MCU, guardando o DC de luminância (uma miniatura
//! 1/8 da imagem) e pontos de retomada. Depois de uma quebra os bytes viram lixo, e a
//! descontinuidade do DC (ou um erro de código) denuncia onde.
//! Validates whether a JPEG stream is contiguous and assists in reassembling fragmented
//! JPEGs: decodes MCU by MCU, tracking luminance DC values (a 1/8th thumbnail) and
//! decoder checkpoints. After a fragmentation break, foreign clusters cause either
//! a Huffman/marker error or a sharp luminance discontinuity.
#[derive(Clone, Debug)]
struct Huff {
// decodificação canônica
// canonical Huffman decoding tables
mincode: [i32; 17],
maxcode: [i32; 18],
valptr: [i32; 17],
@@ -54,9 +54,9 @@ pub struct Header {
pub restart: u32,
pub mcus_x: u32,
pub mcus_y: u32,
/// Onde começam os dados entropy-coded (depois do SOS).
/// Byte offset where entropy-coded scan data begins (immediately after SOS).
pub scan_start: usize,
/// Tabelas de quantização (ordem zigzag).
/// Quantization tables (in zigzag order).
pub qt: [[u16; 64]; 4],
dc: [Option<Huff>; 4],
ac: [Option<Huff>; 4],
@@ -68,7 +68,7 @@ impl Header {
}
}
/// Lê os segmentos até o SOS. `None` se não for JPEG sequencial suportado.
/// Parses JPEG marker segments up to SOS. Returns `None` if not a supported baseline JPEG.
pub fn parse_header(d: &[u8]) -> Option<Header> {
if d.len() < 4 || d[0] != 0xFF || d[1] != 0xD8 {
return None;
@@ -119,7 +119,7 @@ pub fn parse_header(d: &[u8]) -> Option<Header> {
.collect();
sof = Some((w, h, comps));
}
0xC2 | 0xC3 | 0xC5..=0xC7 | 0xC9..=0xCB | 0xCD..=0xCF => return None, // progressivo, aritmético etc.
0xC2 | 0xC3 | 0xC5..=0xC7 | 0xC9..=0xCB | 0xCD..=0xCF => return None, // progressive, arithmetic coding, etc.
0xC4 => {
let mut q = 0;
while q + 17 <= seg.len() {
@@ -167,7 +167,7 @@ pub fn parse_header(d: &[u8]) -> Option<Header> {
let (w, h, comps) = sof?;
let ns = *seg.first()? as usize;
if ns != comps.len() || seg.len() < 1 + ns * 2 + 3 {
return None; // varredura não intercalada
return None; // non-interleaved scan
}
let mut cs = vec![];
for i in 0..ns {
@@ -212,20 +212,20 @@ pub fn parse_header(d: &[u8]) -> Option<Header> {
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum Stop {
/// Todos os MCUs foram decodificados.
/// All MCUs in the frame were decoded.
Done,
/// Marcador inesperado (inclusive EOI) na posição dada.
/// Unexpected marker (including early EOI) at the given byte offset.
Marker(u8, usize),
/// Código inválido ou coeficiente fora do limite.
/// Invalid Huffman code or DCT coefficient index out of bounds.
Error(usize),
/// Faltam bytes.
/// Truncated bitstream (ran out of input bytes).
Truncated,
}
/// Estado completo do decodificador num limite de byte, para retomar com outros dados.
/// Complete entropy decoder snapshot at a byte boundary, allowing resumption on candidate fragments.
#[derive(Clone, Debug)]
pub struct State {
/// Próximo byte a buscar no fluxo.
/// Next byte offset to read from the entropy stream.
pub pos: usize,
bitbuf: u32,
bitcnt: u32,
@@ -236,7 +236,7 @@ pub struct State {
}
impl State {
/// Último DC de luminância (preditor) no ponto de retomada.
/// Last luminance DC predictor value at this checkpoint.
pub fn luma_pred(&self) -> i32 {
self.pred[0]
}
@@ -254,9 +254,9 @@ impl State {
}
pub struct Decoded {
/// DC de luminância (absoluto) por MCU decodificado.
/// Absolute luminance DC coefficient for each decoded MCU.
pub dc: Vec<i32>,
/// Estados guardados em intervalos (para retomada): (MCU, estado ao fim dele).
/// Periodic decoder checkpoints for resumption: `(mcu_index, state_after_mcu)`.
pub snaps: Vec<State>,
pub stop: Stop,
pub state: State,
@@ -341,13 +341,13 @@ fn extend(v: u32, t: u32) -> i32 {
}
}
/// Decodifica até `max_mcus` MCUs a partir de `state`, sobre `data` (fluxo bruto com FF00).
/// `snap_every` > 0 guarda estados a cada tantos bytes consumidos.
/// Decodes up to `max_mcus` MCUs starting from `state` over `data` (raw byte-stuffed stream).
/// When `snap_every > 0`, records a resumption snapshot every `snap_every` consumed bytes.
pub fn decode(h: &Header, data: &[u8], state: State, max_mcus: u32, snap_every: usize) -> Decoded {
decode_hook(h, data, state, max_mcus, snap_every, None)
}
/// Como `decode`, mas entrega os coeficientes (zigzag) dos blocos de luminância de cada MCU.
/// Like `decode`, but also invokes `on_luma` with the zigzag DCT coefficients of each MCU's luma blocks.
pub fn decode_hook(
h: &Header,
data: &[u8],
@@ -369,9 +369,9 @@ pub fn decode_hook(
let mut stop = Stop::Done;
let mut decoded = 0u32;
'mcu: while b.st.mcu < total && decoded < max_mcus {
// Marcador de reinício entre MCUs.
// Expected restart marker between MCUs.
if h.restart > 0 && b.st.mcus_since_rst == h.restart {
// descarta bits de preenchimento e espera RSTn
// discard byte-align padding bits and verify RSTn marker
b.st.bitbuf = 0;
b.st.bitcnt = 0;
let p = b.st.pos;
@@ -486,8 +486,8 @@ fn fail(b: &Bits, data: &[u8]) -> Stop {
}
}
/// Primeiro MCU suspeito: janela de `win` MCUs cuja diferença média de DC é anormalmente alta.
/// Devolve o índice do MCU onde a janela começa.
/// Detects the first suspicious MCU where a sliding window of `win` MCUs exhibits an abnormally
/// high mean DC jump. Returns the starting MCU index of that window.
pub fn first_discontinuity(h: &Header, dc: &[i32]) -> Option<usize> {
let mx = h.mcus_x as usize;
let d: Vec<f64> = (0..dc.len())
@@ -505,7 +505,7 @@ pub fn first_discontinuity(h: &Header, dc: &[i32]) -> Option<usize> {
if d.len() < win * 2 {
return None;
}
// Referência: mediana das diferenças vistas até aqui (histograma, robusta a bordas).
// Baseline reference: running median of row-to-row DC differences (robust to sharp edges).
let mut hist = vec![0u32; 2048];
let mut seen = 0u32;
let mut sum: f64 = d[..win].iter().sum();
@@ -529,7 +529,7 @@ pub fn first_discontinuity(h: &Header, dc: &[i32]) -> Option<usize> {
return Some(i + 1 - win);
}
}
// Só entra na referência o que ainda parece normal.
// Only update the baseline histogram with rows that still look normal.
if i < win * 3 || d[i] < 300.0 {
hist[(d[i] as usize).min(2047)] += 1;
seen += 1;
@@ -566,7 +566,7 @@ fn cos_table() -> &'static [[f32; 8]; 8] {
})
}
/// IDCT de um bloco (coeficientes em zigzag, quantizados) para 8x8 pixels de luminância.
/// Dequantizes and runs an 8x8 IDCT on a zigzag coefficient block into 8x8 luma pixels.
pub fn idct_block(coef: &[i32; 64], qt: &[u16; 64]) -> [u8; 64] {
let mut f = [[0f32; 8]; 8];
for z in 0..64 {
@@ -574,7 +574,7 @@ pub fn idct_block(coef: &[i32; 64], qt: &[u16; 64]) -> [u8; 64] {
f[n / 8][n % 8] = (coef[z] * qt[z] as i32) as f32;
}
let t = cos_table();
// linhas
// rows
let mut tmp = [[0f32; 8]; 8];
for v in 0..8 {
for x in 0..8 {
@@ -598,7 +598,7 @@ pub fn idct_block(coef: &[i32; 64], qt: &[u16; 64]) -> [u8; 64] {
out
}
/// Luminância de um MCU (blocos `h` x `v` da primeira componente) como imagem (8h x 8v).
/// Renders the luma blocks (`h` x `v`) of a single MCU into an `8h x 8v` pixel tile.
pub fn mcu_luma(h: &Header, blocks: &[[i32; 64]]) -> Vec<u8> {
let c = &h.comps[0];
let (bw, bh) = (c.h as usize, c.v as usize);
@@ -615,7 +615,7 @@ pub fn mcu_luma(h: &Header, blocks: &[[i32; 64]]) -> Vec<u8> {
out
}
/// Plano de luminância do que já foi decodificado.
/// Luma pixel plane of the decoded image prefix.
pub struct Plane {
pub w: usize,
pub h: usize,
@@ -648,7 +648,7 @@ impl Plane {
}
}
/// Decodifica e preenche o plano com as luminâncias.
/// Decodes `data` from `state` and renders the luma plane.
pub fn decode_plane(
h: &Header,
data: &[u8],
+3 -3
View File
@@ -5,10 +5,10 @@
clippy::large_enum_variant,
clippy::needless_range_loop
)]
//! Guará: núcleo de recuperação de arquivos.
//! Guará: forensic file recovery core.
//!
//! Princípio central: a origem é aberta **somente para leitura**; nada neste
//! crate escreve no dispositivo que está sendo recuperado.
//! Core principle: the source is opened **strictly read-only**; nothing in this
//! crate ever writes to the device being recovered.
pub mod aff4;
pub mod assemble;
+14 -14
View File
@@ -1,5 +1,5 @@
//! LVM2: lê o rótulo do volume físico (PV) e o texto de metadados, e remonta os volumes
//! lógicos (lineares, em faixas, espelhados e RAID 0/1/5/6/10 do LVM).
//! LVM2: parses Physical Volume (PV) labels and ASCII metadata rings to reassemble
//! Logical Volumes (linear, striped, mirrored, and LVM RAID 0/1/5/6/10).
use crate::raid::{Layout, Level, Member, RaidParams, RaidSource};
use crate::source::{read_vec, Source};
@@ -11,7 +11,7 @@ const MDA_MAGIC: &[u8; 16] = b" LVM2 x[5A%r0N*>";
#[derive(Clone, Debug)]
pub struct PvInfo {
/// UUID de 32 caracteres, sem hífens.
/// 32-character LVM UUID normalized without hyphens.
pub uuid: String,
pub size: u64,
pub seqno: u64,
@@ -19,7 +19,7 @@ pub struct PvInfo {
pub text: String,
}
/// Lê rótulo + metadados de um PV.
/// Reads the PV label and metadata text from a physical volume candidate.
pub fn read_pv(src: &dyn Source) -> Option<PvInfo> {
let head = read_vec(src, 0, 2048).ok()?;
if head.len() < 2048 {
@@ -34,7 +34,7 @@ pub fn read_pv(src: &dyn Source) -> Option<PvInfo> {
let pv = lab.get(off..)?;
let uuid = String::from_utf8_lossy(pv.get(..32)?).to_string();
let size = u64::from_le_bytes(pv.get(32..40)?.try_into().ok()?);
// Lista de áreas de dados, depois de áreas de metadados; cada lista termina em (0,0).
// Null-terminated list of data area descriptors followed by metadata area descriptors.
let mut o = 40;
let rd = |o: usize| -> Option<(u64, u64)> {
Some((
@@ -78,7 +78,7 @@ pub fn read_pv(src: &dyn Source) -> Option<PvInfo> {
text = t;
}
} else {
// O texto dá a volta no anel.
// Metadata text wraps around the circular ring buffer.
let first = mda_size - loc_off;
if let (Ok(a), Ok(b)) = (
read_vec(src, mda_off + loc_off, first as usize),
@@ -113,7 +113,7 @@ pub fn read_pv(src: &dyn Source) -> Option<PvInfo> {
best
}
// ------------------------------------------------------------------ texto
// ------------------------------------------------------------------ text parser
#[derive(Clone, Debug)]
pub enum Val {
@@ -125,7 +125,7 @@ pub enum Val {
#[derive(Clone, Debug, Default)]
pub struct Section {
pub values: BTreeMap<String, Val>,
/// Subseções na ordem do arquivo.
/// Child sections in declaration order.
pub sections: Vec<(String, Section)>,
}
@@ -277,7 +277,7 @@ pub fn parse(text: &str) -> Option<Section> {
.section(true)
}
// ------------------------------------------------------------- montagem
// ------------------------------------------------------------- assembly
fn dashed(u: &str) -> String {
if u.len() != 32 {
@@ -297,7 +297,7 @@ fn dashed(u: &str) -> String {
#[derive(Clone)]
enum Map {
/// Lê de `src` a partir de `base` (linear).
/// Linear segment: reads from `src` starting at `base`.
Linear {
src: Option<Arc<dyn Source>>,
base: u64,
@@ -413,7 +413,7 @@ fn mk_stripes(list: &[Val]) -> Vec<(String, u64)> {
struct Ctx<'a> {
ext: u64,
pvs: BTreeMap<String, (Option<Arc<dyn Source>>, u64)>, // nome -> (origem, pe_start em bytes)
pvs: BTreeMap<String, (Option<Arc<dyn Source>>, u64)>, // pv_name -> (source, pe_start_in_bytes)
lvs: &'a Section,
cache: BTreeMap<String, Option<Arc<dyn Source>>>,
warnings: Vec<String>,
@@ -487,7 +487,7 @@ impl Ctx<'_> {
}
})
.collect();
// Pares (rimage, rmeta) ou lista só de imagens.
// Pairs of `(rimage, rmeta)` or a list of image LVs.
let data_lvs: Vec<&String> =
imgs.iter().filter(|n| n.contains("_rimage_")).collect();
let mut members: Vec<Option<Member>> = vec![];
@@ -544,7 +544,7 @@ impl Ctx<'_> {
}
}
/// Monta os volumes lógicos de cada grupo de volumes presente nos PVs fornecidos.
/// Reassembles all user Logical Volumes across every Volume Group found in `pvs`.
pub fn assemble_vgs(pvs: Vec<(Arc<dyn Source>, PvInfo)>) -> Vec<VgResult> {
let mut by_vg: BTreeMap<String, Vec<(Arc<dyn Source>, PvInfo)>> = BTreeMap::new();
for (s, p) in pvs {
@@ -590,7 +590,7 @@ pub fn assemble_vgs(pvs: Vec<(Arc<dyn Source>, PvInfo)>) -> Vec<VgResult> {
};
let mut lvs = vec![];
for (name, def) in &lvsec.sections {
// Imagens internas do RAID e metadados não são volumes de usuário.
// Internal RAID sub-LVs (`_rimage_*`, `_rmeta_*`) are not user-visible volumes.
let hidden = def
.list("status")
.map(|l| !l.iter().any(|v| matches!(v, Val::Str(s) if s == "VISIBLE")))
+20 -20
View File
@@ -1,6 +1,6 @@
//! Extração de metadados de arquivos recuperados: EXIF (câmera, datas, GPS),
//! informações de PDF, propriedades de documentos Office/OpenDocument e
//! dados de vídeo MP4/MOV. Tudo é leitura posicional, sem gravar em disco.
//! Metadata extraction for recovered files: EXIF (camera, timestamps, GPS),
//! PDF info dictionary, Office/OpenDocument properties, and MP4/MOV
//! container headers. Performed entirely via positional reads without writing to disk.
use crate::recover::read_found_at;
use crate::source::Source;
@@ -12,16 +12,16 @@ use std::io::Read;
pub struct Meta {
pub kind: String,
pub fields: Vec<(String, String)>,
/// (latitude, longitude) em graus decimais.
/// `(latitude, longitude)` in decimal degrees.
pub gps: Option<(f64, f64)>,
/// Data em que a foto foi tirada (EXIF), s desde 1970 (hora da câmera lida como UTC).
/// EXIF capture timestamp in seconds since 1970 (camera wall-clock interpreted as UTC).
#[serde(default)]
pub taken: Option<i64>,
#[serde(default)]
pub camera: Option<String>,
#[serde(default)]
pub px: Option<(u32, u32)>,
/// Orientação EXIF (1..8).
/// EXIF orientation tag (`1..=8`).
#[serde(default)]
pub orientation: u16,
}
@@ -142,7 +142,7 @@ fn jpeg(fr: &Fr, m: &mut Meta) {
jpeg_bytes(&d, m);
}
/// Lê os segmentos de cabeçalho de um JPEG (já em memória): dimensões e EXIF.
/// Parses JPEG header segments (in memory) for dimensions and EXIF tags.
pub fn jpeg_bytes(d: &[u8], m: &mut Meta) {
let mut p = 2usize;
while p + 4 <= d.len() && d[p] == 0xFF {
@@ -168,7 +168,7 @@ pub fn jpeg_bytes(d: &[u8], m: &mut Meta) {
}
}
/// Miniatura JPEG embutida no EXIF (IFD1), útil quando a imagem principal está danificada.
/// Extracts the embedded EXIF thumbnail (IFD1), useful when the main image is corrupted.
pub fn exif_thumbnail(d: &[u8]) -> Option<Vec<u8>> {
let mut p = 2usize;
while p + 4 <= d.len() && d[p] == 0xFF {
@@ -222,7 +222,7 @@ struct Entry {
tag: u16,
typ: u16,
count: u32,
pos: usize, // onde estão os dados do valor dentro do TIFF
pos: usize, // offset of the tag value payload within the TIFF header
}
impl Tiff<'_> {
@@ -423,7 +423,7 @@ fn exif(d: &[u8], m: &mut Meta) {
}
}
/// "2023:07:14 18:22:05" -> segundos desde 1970 (hora da câmera lida como UTC)
/// `"2023:07:14 18:22:05"` -> seconds since 1970 (camera local time treated as UTC).
fn exif_epoch(s: &str) -> Option<i64> {
let b = s.as_bytes();
if b.len() < 19 || b[4] != b':' || b[7] != b':' {
@@ -444,7 +444,7 @@ fn exif_epoch(s: &str) -> Option<i64> {
Some(crate::fs::days_from_civil(y, mo, d) * 86400 + h * 3600 + mi * 60 + se)
}
/// "2023:07:14 18:22:05" -> "2023-07-14T18:22:05" (a interface formata por idioma)
/// `"2023:07:14 18:22:05"` -> `"2023-07-14T18:22:05"` (formatted per locale by the UI).
fn exif_date(s: &str) -> String {
let b = s.as_bytes();
if b.len() >= 19 && b[4] == b':' && b[7] == b':' {
@@ -526,7 +526,7 @@ fn pdf_string(d: &[u8], key: &str) -> Option<String> {
let k = key.as_bytes();
for at in find_all(d, k) {
let mut p = at + k.len();
// evita /Creator casar com /CreatorXyz
// prevent `/Creator` from prefix-matching `/CreatorXyz`
if d.get(p).map(|c| c.is_ascii_alphanumeric()).unwrap_or(false) {
continue;
}
@@ -544,7 +544,7 @@ fn pdf_string(d: &[u8], key: &str) -> Option<String> {
b'\\' if i + 1 < d.len() => {
i += 1;
if d[i].is_ascii_digit() && d[i] < b'8' {
// escape octal \ddd (1 a 3 dígitos)
// octal escape `\ddd` (1 to 3 digits)
let mut v = 0u32;
let mut n = 0;
while n < 3 && i < d.len() && d[i] >= b'0' && d[i] < b'8' {
@@ -609,11 +609,11 @@ fn pdf_text(b: &[u8]) -> String {
.collect();
return String::from_utf16_lossy(&u);
}
// PDFDocEncoding ~ Latin-1 na prática
// PDFDocEncoding is effectively Latin-1 for printable characters
b.iter().map(|&c| c as char).collect()
}
/// "D:20230714182205-03'00'" -> "2023-07-14T18:22:05" (a interface formata por idioma)
/// `"D:20230714182205-03'00'"` -> `"2023-07-14T18:22:05"` (formatted per locale by the UI).
fn pdf_date(s: &str) -> String {
let t = s.trim_start_matches("D:");
let digits: String = t.chars().take_while(|c| c.is_ascii_digit()).collect();
@@ -704,7 +704,7 @@ fn zip_read(fr: &Fr, e: &ZipEnt) -> Option<Vec<u8>> {
}
}
/// Texto do primeiro elemento `<tag ...>texto</tag>`.
/// Extracts the inner text of the first `<tag ...>text</tag>` element.
fn xml_tag(xml: &str, tag: &str) -> Option<String> {
let open = format!("<{tag}");
let mut from = 0;
@@ -733,7 +733,7 @@ fn unescape(s: &str) -> String {
.replace("&amp;", "&")
}
/// Datas ISO 8601 ficam como estão; a interface as formata no idioma escolhido.
/// Preserves ISO 8601 date prefixes (`YYYY-MM-DD[THH:MM:SS]`) for locale-aware UI formatting.
fn iso_date(s: &str) -> String {
let b = s.as_bytes();
if b.len() >= 19 && b[4] == b'-' && b[10] == b'T' {
@@ -916,7 +916,7 @@ fn moov(d: &[u8], m: &mut Meta) {
}
}
/// Instante em ISO 8601 UTC ("2023-07-14T18:22:05Z"), para a interface formatar.
/// Formats a Unix timestamp as ISO 8601 UTC (`"2023-07-14T18:22:05Z"`).
pub fn iso_unix(t: i64) -> String {
let days = t.div_euclid(86400);
let rem = t.rem_euclid(86400);
@@ -941,13 +941,13 @@ pub fn fmt_unix(t: i64) -> String {
)
}
/// (ano, mês, dia) de um instante em segundos desde 1970.
/// Returns `(year, month, day)` for a Unix timestamp in seconds.
pub fn ymd(t: i64) -> (i64, u32, u32) {
let (y, m, d) = civil_from_days(t.div_euclid(86400));
(y, m as u32, d as u32)
}
/// Inverso de `days_from_civil` (Howard Hinnant).
/// Inverse of `days_from_civil` (Howard Hinnant civil calendar algorithm).
fn civil_from_days(z: i64) -> (i64, i64, i64) {
let z = z + 719468;
let era = if z >= 0 { z } else { z - 146096 } / 146097;
+49 -49
View File
@@ -1,7 +1,7 @@
//! Reconstrução de vídeo MP4/MOV sem o átomo `moov` (gravação interrompida de câmera ou drone,
//! arquivo cortado). Os quadros ficam no `mdat` como unidades NAL com prefixo de tamanho; o
//! índice (`moov`) é refeito a partir delas: tamanhos de amostra, quadros-chave, tempo e, para
//! H.264, a ordem de apresentação (POC) quando há quadros B. O áudio não é reconstruído.
//! Reconstructs MP4/MOV video files missing their `moov` atom (interrupted camera/drone recordings
//! or truncated files). Video frames sit in `mdat` as length-prefixed NAL units; we rebuild the
//! `moov` index from them: sample sizes, keyframes, timing, and for H.264, presentation order
//! (POC) when B-frames are present. Audio tracks are not reconstructed.
use crate::source::{read_vec, Source};
@@ -58,7 +58,7 @@ impl<'a> Bits<'a> {
}
}
/// Remove os bytes de "emulation prevention" (00 00 03 -> 00 00).
/// Strips H.264/HEVC emulation prevention bytes (`00 00 03` -> `00 00`).
fn unescape(nal: &[u8]) -> Vec<u8> {
let mut out = Vec::with_capacity(nal.len());
let mut z = 0;
@@ -86,7 +86,7 @@ pub struct Sps264 {
pub poc_type: u32,
pub log2_max_poc_lsb: u32,
pub frame_mbs_only: bool,
/// (unidades por tique, escala de tempo)
/// `(num_units_in_tick, time_scale)` from VUI timing info.
pub timing: Option<(u32, u32)>,
}
@@ -173,7 +173,7 @@ pub fn parse_sps264(nal: &[u8]) -> Option<Sps264> {
};
s.width = (w * 16).checked_sub((cl + cr) * sub_w)?;
s.height = ((2 - s.frame_mbs_only as u32) * h * 16).checked_sub((ct + cb) * sub_h)?;
// VUI até o timing_info.
// Parse VUI parameters up to `timing_info_present_flag`.
if b.bit()? == 1 {
if b.bit()? == 1 && b.u(8)? == 255 {
b.skip(32);
@@ -202,7 +202,7 @@ pub fn parse_sps264(nal: &[u8]) -> Option<Sps264> {
Some(s)
}
/// (primeiro_mb == 0, tipo de fatia, pps_id, frame_num, idr, poc_lsb) do cabeçalho de fatia H.264.
/// Parses `(first_mb == 0, slice_type, pps_id, frame_num, is_idr, poc_lsb)` from an H.264 slice header.
struct SliceInfo {
frame_num: u32,
first_mb0: bool,
@@ -240,7 +240,7 @@ fn slice264(nal: &[u8], sps: &Sps264, pic_order_present: bool) -> Option<SliceIn
})
}
/// bottom_field_pic_order_in_frame_present_flag do PPS.
/// Extracts `bottom_field_pic_order_in_frame_present_flag` from an H.264 PPS.
fn pps_pic_order_present(nal: &[u8]) -> bool {
let rbsp = unescape(&nal[1..]);
let mut b = Bits::new(&rbsp);
@@ -278,7 +278,7 @@ pub fn parse_sps_hevc(nal: &[u8]) -> Option<SpsHevc> {
*v = b.u(8)? as u8;
let _ = i;
}
// general: 2+1+5 bits, 32 compat, 48 flags, 8 level => lemos 12 bytes (1+4+6+1)
// general profile/tier/level: 2+1+5 bits, 32 compat, 48 constraint flags, 8 level => 12 bytes (1+4+6+1)
let mut sub_profile = vec![];
let mut sub_level = vec![];
for _ in 0..max_sub {
@@ -320,9 +320,9 @@ pub fn parse_sps_hevc(nal: &[u8]) -> Option<SpsHevc> {
Some(s)
}
// -------------------------------------------------------------- amostras
// -------------------------------------------------------------- samples
/// Configuração de codec (SPS/PPS/VPS) extraída de um vídeo íntegro da mesma câmera.
/// Codec parameter sets (SPS/PPS/VPS) extracted from a donor video recorded by the same camera.
#[derive(Clone, Debug)]
pub struct MediaConfig {
pub codec: Codec,
@@ -331,9 +331,9 @@ pub struct MediaConfig {
pub vps: Vec<Vec<u8>>,
}
/// Procura o `avcC`/`hvcC` de um MP4 completo (lê até `max` bytes a partir de `off`).
/// Extracts `avcC`/`hvcC` codec configuration from an intact MP4 (reading up to `max` bytes from `off`).
pub fn config_from_mp4(src: &dyn Source, off: u64, max: u64) -> Option<MediaConfig> {
// O moov costuma estar no começo ou no fim: procura nos dois lugares.
// The `moov` atom is typically near the beginning or the end of the file: check both.
let size = src.size();
let head = read_vec(src, off, max.min(4 << 20) as usize).ok()?;
let mut hay: Vec<Vec<u8>> = vec![head];
@@ -416,7 +416,7 @@ pub enum Codec {
#[derive(Clone, Debug)]
struct Sample {
/// Posição absoluta (na origem) do primeiro byte da amostra (prefixo de tamanho incluso).
/// Absolute source offset of the first byte of the sample (including the 4-byte length prefix).
off: u64,
size: u32,
sync: bool,
@@ -425,9 +425,9 @@ struct Sample {
}
pub struct Rebuilt {
/// Bytes do arquivo original antes do `mdat` (ftyp etc.).
/// Leading atoms from the original file before `mdat` (`ftyp`, etc.).
pub pre_len: u64,
/// Onde começam e terminam os dados dos quadros na origem.
/// Start and end offsets of the video sample payload in the source.
pub payload: (u64, u64),
pub moov: Vec<u8>,
pub frames: usize,
@@ -439,7 +439,7 @@ pub struct Rebuilt {
}
impl Rebuilt {
/// Cabeçalho `mdat` de 64 bits para o payload.
/// 16-byte 64-bit `mdat` atom header covering the sample payload.
pub fn mdat_header(&self) -> Vec<u8> {
let size = 16 + (self.payload.1 - self.payload.0);
let mut v = Vec::with_capacity(16);
@@ -457,7 +457,7 @@ fn hevc_nal_ok(b0: u8, b1: u8) -> bool {
b0 & 0x81 == 0 && ((b0 >> 1) & 0x3F) <= 40 && b1 & 0x07 != 0 && (b1 >> 3) == 0
}
/// Cabeçalho do átomo `mdat` que começa em `at`: (início dos dados, tamanho declarado ou 0).
/// Parses the `mdat` header at `at`: returns `(payload_start, declared_end_or_0)`.
fn mdat_at(src: &dyn Source, at: u64) -> Option<(u64, u64)> {
let h = read_vec(src, at, 16).ok()?;
if h.len() < 8 || &h[4..8] != b"mdat" {
@@ -471,7 +471,7 @@ fn mdat_at(src: &dyn Source, at: u64) -> Option<(u64, u64)> {
}
}
/// Reconstrói o índice de um MP4 que começa em `off`, sem `moov`. `avail` limita a leitura.
/// Rebuilds the `moov` index for a headerless/truncated MP4 starting at `off` (reading at most `avail` bytes).
pub fn rebuild(src: &dyn Source, off: u64, avail: u64, refs: &[MediaConfig]) -> Option<Rebuilt> {
if let Some(r) = rebuild_with(src, off, avail, None) {
return Some(r);
@@ -486,7 +486,7 @@ fn rebuild_with(
avail: u64,
preset: Option<&MediaConfig>,
) -> Option<Rebuilt> {
// Átomos antes do mdat.
// Parse leading atoms before `mdat`.
let mut p = 0u64;
let mut pre_end = None;
for _ in 0..16 {
@@ -520,7 +520,7 @@ fn rebuild_with(
(off + pre_len + declared).min(hard_end)
};
// Lê o payload em blocos, interpretando NALs.
// Scan the `mdat` payload in chunks, parsing length-prefixed NAL units.
let mut samples: Vec<Sample> = vec![];
let mut cache: Vec<u8> = vec![];
let mut cache_off = data_start;
@@ -536,7 +536,7 @@ fn rebuild_with(
let s = (o - *cache_off) as usize;
cache.get(s..s + n).map(|x| x.to_vec())
};
// Detecta o codec: o que forma a cadeia mais longa de unidades NAL válidas.
// Detect the codec (H.264 vs HEVC) that yields the longest initial chain of valid NAL units.
let chain = |c: Codec| -> usize {
let Ok(head) = read_vec(src, data_start, 64 << 10) else {
return 0;
@@ -599,10 +599,10 @@ fn rebuild_with(
let mut idr_total = 0usize;
let mut idr_bad = 0usize;
let mut cur: Option<Sample> = None;
let mut group_start: Option<u64> = None; // início do grupo de NALs não-VCL que precede um quadro
let mut group_start: Option<u64> = None; // start offset of the non-VCL NAL group preceding a frame
let mut nal_count = 0usize;
let mut max_len = 0u64;
// POC (H.264, tipo 0)
// POC (H.264 pic_order_cnt_type 0)
let mut prev_msb: i64 = 0;
let mut prev_lsb: i64 = 0;
@@ -617,7 +617,7 @@ fn rebuild_with(
};
let mut ok = ok;
if ok && nal_count >= 3 && len > (max_len * 6).max(1 << 20) {
// Um "tamanho" muito maior que os já vistos é lixo (áudio lido como vídeo).
// A declared NAL length far larger than valid range is interleaved audio or garbage.
ok = false;
}
if ok {
@@ -629,7 +629,7 @@ fn rebuild_with(
max_len = max_len.max(len);
}
if !valid {
// Pode ser áudio intercalado: procura a próxima unidade NAL plausível.
// Likely an interleaved audio chunk: scan ahead for the next plausible video NAL sequence.
let found = scan_nal(
src,
codec,
@@ -642,7 +642,7 @@ fn rebuild_with(
);
match found {
Some(q) if nal_count > 0 => {
// O que veio antes fecha o quadro atual.
// Flush the current frame before starting the next one.
if let Some(mut s) = cur.take() {
s.size = (group_start.unwrap_or(pos) - s.off) as u32;
samples.push(s);
@@ -734,9 +734,9 @@ fn rebuild_with(
let _ = ty;
if is_vcl {
if first_of_pic {
// fecha o quadro anterior
// flush previous frame
if let Some(mut s) = cur.take() {
// tamanho até o início do grupo atual (ou deste NAL)
// frame size up to the start of the current non-VCL group (or this VCL NAL)
let end = group_start.unwrap_or(pos);
s.size = (end - s.off) as u32;
samples.push(s);
@@ -816,7 +816,7 @@ fn rebuild_with(
return None;
}
let (num, den) = match timing {
Some((units, scale)) if codec == Codec::Avc => (scale, 2 * units), // quadros por segundo = scale / (2 * units)
Some((units, scale)) if codec == Codec::Avc => (scale, 2 * units), // fps = time_scale / (2 * num_units_in_tick)
Some((units, scale)) => (scale, units),
None => (30, 1),
};
@@ -827,10 +827,10 @@ fn rebuild_with(
(num, den)
};
let fps = num as f64 / den as f64;
// Escala: tique do quadro = den, escala de tempo = num.
// Timescale: frame tick = den, timescale = num.
let (timescale, dur) = (num, den);
// Ordem de apresentação (POC) por trecho entre IDR.
// Compute presentation order (ctts composition offsets) from POC within each IDR GOP.
let mut ctts: Option<Vec<i64>> = None;
if samples.iter().all(|s| s.poc.is_some()) {
let mut offs = vec![0i64; samples.len()];
@@ -936,7 +936,7 @@ fn build_moov(
let movie_ts = 1000u32;
let movie_dur = media_dur * movie_ts as u64 / timescale as u64;
// Entrada de amostra
// Visual sample entry (`avc1` / `hvc1`)
let mut entry = vec![];
entry.extend_from_slice(&[0; 6]);
entry.extend_from_slice(&1u16.to_be_bytes());
@@ -975,18 +975,18 @@ fn build_moov(
Codec::Hevc => {
let h = hevc.unwrap();
let mut c = vec![1];
c.extend_from_slice(&h.ptl[..12.min(h.ptl.len())]); // espaço/tier/perfil, compat, restrições, nível
c.extend_from_slice(&h.ptl[..12.min(h.ptl.len())]); // profile_space/tier/profile_idc, profile_compatibility, constraint_indicator, level_idc
c.truncate(13);
while c.len() < 13 {
c.push(0);
}
c.extend_from_slice(&0xF000u16.to_be_bytes()); // segmentação espacial
c.push(0xFC); // paralelismo
c.extend_from_slice(&0xF000u16.to_be_bytes()); // min_spatial_segmentation_idc
c.push(0xFC); // parallelismType
c.push(0xFC | h.chroma as u8);
c.push(0xF8 | (h.bit_depth_luma - 8) as u8);
c.push(0xF8 | (h.bit_depth_chroma - 8) as u8);
c.extend_from_slice(&0u16.to_be_bytes()); // avgFrameRate
c.push(0x0B); // constFrameRate=0, 1 camada temporal, lengthSizeMinusOne=3
c.push(0x0B); // constantFrameRate=0, numTemporalLayers=1, lengthSizeMinusOne=3
let arrays: [(u8, &[Vec<u8>]); 3] = [(32, vps), (33, sps), (34, pps)];
let present: Vec<_> = arrays.iter().filter(|(_, v)| !v.is_empty()).collect();
c.push(present.len() as u8);
@@ -1047,7 +1047,7 @@ fn build_moov(
.collect();
let mut stbl_parts = vec![stsd, stts];
if let Some(c) = ctts {
// entradas agrupadas
// run-length encoded entries
let mut runs: Vec<(u32, u32)> = vec![];
for &v in c {
match runs.last_mut() {
@@ -1080,7 +1080,7 @@ fn build_moov(
let vmhd = full(b"vmhd", 0, 1, &[0; 8]);
let minf = bx(b"minf", &[vmhd, dinf, stbl].concat());
let mut mdhd_body = vec![];
mdhd_body.extend_from_slice(&[0; 8]); // criação/modificação
mdhd_body.extend_from_slice(&[0; 8]); // creation_time / modification_time
mdhd_body.extend_from_slice(&u32b(timescale));
mdhd_body.extend_from_slice(&u32b(media_dur as u32));
mdhd_body.extend_from_slice(&0x55C4u16.to_be_bytes()); // "und"
@@ -1100,8 +1100,8 @@ fn build_moov(
tkhd_body.extend_from_slice(&[0; 4]);
tkhd_body.extend_from_slice(&u32b(movie_dur as u32));
tkhd_body.extend_from_slice(&[0; 8]);
tkhd_body.extend_from_slice(&[0; 4]); // camada, grupo
tkhd_body.extend_from_slice(&[0; 4]); // volume, reservado
tkhd_body.extend_from_slice(&[0; 4]); // layer, alternate_group
tkhd_body.extend_from_slice(&[0; 4]); // volume, reserved
for m in matrix {
tkhd_body.extend_from_slice(&u32b(m));
}
@@ -1136,7 +1136,7 @@ fn build_moov(
bx(b"moov", &[mvhd, trak].concat())
}
/// A unidade NAL parece de verdade? Valida o cabeçalho de fatia e o tamanho de SPS/PPS/AUD.
/// Checks whether a candidate NAL unit looks structurally valid (slice header, SPS/PPS/AUD sizes).
fn nal_plausible(
codec: Codec,
head: &[u8],
@@ -1167,7 +1167,7 @@ fn nal_plausible(
return false;
}
if let Some(sp) = sps {
// frame_num de IDR é sempre 0
// IDR slice `frame_num` is always 0
if t == 5 {
if let Some(fnum) = b.u(sp.log2_max_frame_num) {
return fnum == 0;
@@ -1194,7 +1194,7 @@ fn nal_plausible(
}
}
/// Quantas unidades NAL válidas seguidas (sem vãos) começam em `at` (até `max`).
/// Counts how many consecutive valid length-prefixed NAL units start at `at` (up to `max`).
fn chain_len(
buf: &[u8],
base: u64,
@@ -1232,8 +1232,8 @@ fn chain_len(
n
}
/// Próxima posição em [from, limit) onde começa uma sequência de unidades NAL plausíveis.
/// Prefere o primeiro ponto com duas ou mais unidades seguidas; senão, uma unidade isolada.
/// Finds the next offset in `[from, limit)` where a plausible NAL unit sequence begins.
/// Prefers an offset with two or more back-to-back valid NALs; falls back to a single valid NAL.
fn scan_nal(
src: &dyn Source,
codec: Codec,
@@ -1247,7 +1247,7 @@ fn scan_nal(
if limit <= from + 8 {
return None;
}
// Lê um pouco além do limite para poder seguir a cadeia de unidades.
// Read slightly past `limit` so we can verify the following NAL in the chain.
let want =
((limit - from) as usize + (cap as usize).min(4 << 20)).min((data_end - from) as usize);
let buf = read_vec(src, from, want).ok()?;
@@ -1259,7 +1259,7 @@ fn scan_nal(
if l2 >= 2 && l2 <= cap && q + 4 + l2 <= data_end {
let nb = buf[i + 4];
let good_head = match codec {
// Um quadro novo começa por parâmetros/AUD/SEI ou por fatia do macrobloco 0.
// A new access unit starts with parameter sets/AUD/SEI or a slice at macroblock 0.
Codec::Avc => {
avc_nal_ok(nb)
&& (matches!(nb & 0x1F, 6..=9)
+7 -7
View File
@@ -1,11 +1,11 @@
//! Notificações do sistema (varredura concluída, disco desconectado...), usando os
//! comandos nativos de cada sistema, sem dependências extras.
//! Native OS desktop notifications (scan finished, disk disconnected, etc.) using
//! standard platform CLI tools with zero extra crate dependencies.
use std::process::{Command, Stdio};
/// Avisos que o núcleo envia ao sistema operacional. O texto é montado no
/// idioma efetivo da interface (`settings::ui_lang`), porque notificações não
/// passam pelo dicionário do DOM.
/// Notification events emitted by the core. Messages are formatted in the
/// effective UI language (`settings::ui_lang`) since OS notifications bypass
/// the DOM i18n translator.
pub enum Notice {
ScanDone { count: usize, target: String },
RecoverDone { count: usize, destination: String },
@@ -53,7 +53,7 @@ impl Notice {
}
}
/// Envia uma notificação sem bloquear. Falhas são ignoradas (é só um aviso).
/// Sends a desktop notification asynchronously. Failures are silently ignored.
pub fn send(notice: Notice) {
let (title, body) = notice.text(&crate::settings::ui_lang());
std::thread::spawn(move || {
@@ -72,7 +72,7 @@ pub fn send(notice: Notice) {
#[cfg(target_os = "linux")]
fn command(title: &str, body: &str) -> Option<Command> {
// Como root, notifica na sessão do usuário real.
// When running as root, dispatch the notification inside the real user's session.
let mut c = if let Some(u) = crate::owner::invoking_user() {
let mut c = Command::new("runuser");
c.args(["-u", &u.name, "--", "env"]);
+11 -11
View File
@@ -1,6 +1,6 @@
//! Quando o Guará roda como administrador (sudo/pkexec), tudo que ele grava para
//! o usuário (sessões, relatórios, arquivos recuperados) pertence ao usuário que
//! o abriu, e não ao root, e as pastas sugeridas ficam na pasta desse usuário.
//! When Guará runs elevated as root (`sudo`/`pkexec`), all user-facing outputs
//! (sessions, reports, recovered files) are chowned back to the invoking user
//! rather than root, and default paths resolve inside that user's home directory.
use std::io;
use std::path::{Path, PathBuf};
@@ -13,10 +13,10 @@ pub struct User {
pub home: PathBuf,
}
/// O usuário real por trás de um processo root iniciado por sudo/pkexec.
/// Identifies the real non-root user who launched an elevated `sudo`/`pkexec` process.
#[cfg(unix)]
pub fn invoking_user() -> Option<User> {
// SAFETY: geteuid não tem pré-condições.
// SAFETY: `geteuid` has no preconditions.
if unsafe { libc::geteuid() } != 0 {
return None;
}
@@ -30,7 +30,7 @@ pub fn invoking_user() -> Option<User> {
lookup_user(&passwd, uid, name.as_deref())
}
/// Procura o usuário (por uid ou, na falta dele, por nome) num conteúdo no formato do /etc/passwd.
/// Looks up a user (by UID first, falling back to username) in `/etc/passwd`-formatted text.
pub fn lookup_user(passwd: &str, uid: Option<u32>, name: Option<&str>) -> Option<User> {
for l in passwd.lines() {
let f: Vec<&str> = l.split(':').collect();
@@ -57,7 +57,7 @@ pub fn invoking_user() -> Option<User> {
None
}
/// Pasta pessoal para sugerir ao usuário (a dele, mesmo se estivermos como root).
/// Returns the invoking user's home directory (even when running as root).
pub fn user_home() -> PathBuf {
if let Some(u) = invoking_user() {
return u.home;
@@ -69,7 +69,7 @@ pub fn user_home() -> PathBuf {
)
}
/// Entrega o arquivo/pasta ao usuário que abriu o Guará (não faz nada fora do modo root).
/// Transfers ownership of `path` to the invoking user (no-op when not running as root).
pub fn give_to_user(path: &Path) {
#[cfg(unix)]
if let Some(u) = invoking_user() {
@@ -79,7 +79,7 @@ pub fn give_to_user(path: &Path) {
let _ = path;
}
/// Como `create_dir_all`, mas as pastas criadas ficam com o dono certo.
/// Like `fs::create_dir_all`, but chowns newly created directories to the invoking user.
pub fn create_dir_all_owned(path: &Path) -> io::Result<()> {
let mut missing = vec![];
let mut cur = Some(path);
@@ -97,7 +97,7 @@ pub fn create_dir_all_owned(path: &Path) -> io::Result<()> {
Ok(())
}
/// Como `fs::write`, mas o arquivo fica com o dono certo.
/// Like `fs::write`, but chowns the written file to the invoking user.
pub fn write_owned(path: &Path, bytes: impl AsRef<[u8]>) -> io::Result<()> {
if let Some(parent) = path.parent() {
create_dir_all_owned(parent)?;
@@ -126,7 +126,7 @@ mod tests {
(1001, 1002, "/home/maria")
);
assert!(lookup_user(PASSWD, Some(4242), None).is_none());
// o uid manda sobre o nome
// UID takes precedence over username
assert_eq!(
lookup_user(PASSWD, Some(1001), Some("lucas")).unwrap().uid,
1001
+23 -23
View File
@@ -1,5 +1,5 @@
//! Tabelas de partição (MBR/GPT), detecção de sistemas de arquivos,
//! volumes cifrados e busca de partições perdidas.
//! Partition tables (MBR/GPT), filesystem and encrypted volume detection,
//! and lost-partition scanning across unallocated gaps.
use crate::source::{read_vec, Source};
use crate::types::{DiskLayout, FsKind, Gap, PartInfo};
@@ -7,7 +7,7 @@ use crate::types::{DiskLayout, FsKind, Gap, PartInfo};
const SECTOR: u64 = 512;
const MIN_GAP: u64 = 1024 * 1024;
/// Identifica o sistema de arquivos que começa em `start` e (se possível) seu tamanho.
/// Identifies the filesystem or container starting at `start` and its byte size when known.
pub fn detect_fs(src: &dyn Source, start: u64) -> Option<(FsKind, Option<u64>)> {
let b = read_vec(src, start, 4096).ok()?;
if b.len() < 1100 {
@@ -16,7 +16,7 @@ pub fn detect_fs(src: &dyn Source, start: u64) -> Option<(FsKind, Option<u64>)>
if let Some(r) = detect_fs_in(&b) {
return Some(r);
}
// Assinaturas mais adiante no volume: Btrfs (64 KiB) e RAID mdadm 1.2 (4 KiB).
// Signatures located further into the volume: Btrfs (64 KiB) and mdadm 1.2 RAID (4 KiB).
if let Ok(h) = read_vec(src, start + 0x10000, 0x100) {
if h.len() >= 0x80 && &h[0x40..0x48] == b"_BHRfS_M" {
return Some((
@@ -33,7 +33,7 @@ pub fn detect_fs(src: &dyn Source, start: u64) -> Option<(FsKind, Option<u64>)>
None
}
/// Mesma detecção sobre um buffer que começa no setor candidato (>= 1100 bytes).
/// Runs volume signature detection on a buffer starting at the candidate sector (>= 1100 bytes).
pub fn detect_fs_in(b: &[u8]) -> Option<(FsKind, Option<u64>)> {
if b.len() < 1100 {
return None;
@@ -133,8 +133,8 @@ fn detect_fat(b: &[u8]) -> Option<(FsKind, Option<u64>)> {
let root_secs = (root_entries * 32).div_ceil(bps);
let data = total.saturating_sub(reserved + nfats * fatsz + root_secs);
let clusters = data / spc;
// O layout do BPB manda: raiz fixa zerada + FAT de 32 bits = FAT32, mesmo com poucos
// clusters (mkfs.vfat -F 32 em mídias pequenas gera isso e o Linux monta).
// BPB structure takes precedence: zero root-entry count + 32-bit FAT size = FAT32 even on
// small cluster counts (`mkfs.vfat -F 32` on small images produces this and Linux mounts it).
let is32 = root_entries == 0 && fat16 == 0 && fat32 != 0;
let kind = if is32 {
FsKind::Fat32
@@ -146,7 +146,7 @@ fn detect_fat(b: &[u8]) -> Option<(FsKind, Option<u64>)> {
Some((kind, Some(total * bps)))
}
/// Lê o rótulo (label) do volume quando disponível de forma simples.
/// Reads the volume label when directly accessible in the superblock/boot sector.
fn fat_label(b: &[u8], kind: FsKind) -> Option<String> {
let off = if kind == FsKind::Fat32 { 71 } else { 43 };
let s = String::from_utf8_lossy(b.get(off..off + 11)?)
@@ -155,9 +155,9 @@ fn fat_label(b: &[u8], kind: FsKind) -> Option<String> {
(!s.is_empty() && s != "NO NAME").then_some(s)
}
// ----------------------------------------------------------------- tabelas
// ----------------------------------------------------------------- partition tables
/// Analisa o disco. `search_lost` varre lacunas atrás de volumes perdidos (lento em discos grandes).
/// Inspects disk layout. `search_lost` scans unpartitioned gaps for lost volumes.
pub fn analyze(src: &dyn Source, search_lost: bool) -> DiskLayout {
let size = src.size();
let mut layout = DiskLayout {
@@ -174,7 +174,7 @@ pub fn analyze(src: &dyn Source, search_lost: bool) -> DiskLayout {
layout.table = "gpt".into();
parts = g;
} else if has_mbr {
// Pode ser MBR de verdade ou um setor de boot de volume sem tabela (superfloppy).
// Sector 0 may be either a partitioned MBR or a partitionless VBR ("superfloppy").
let looks_boot = detect_fs_in(&read_vec(src, 0, 4096).unwrap_or_default()).is_some();
if !looks_boot {
let mp = read_mbr(src, &sector0, size);
@@ -197,7 +197,7 @@ pub fn analyze(src: &dyn Source, search_lost: bool) -> DiskLayout {
}
}
// Volume sem tabela de partições (pendrive "superfloppy" ou partição crua).
// Unpartitioned volume ("superfloppy" USB drive or single-partition image).
if parts.is_empty() {
if let Some((fs, len)) = detect_fs(src, 0) {
parts.push(PartInfo {
@@ -217,12 +217,12 @@ pub fn analyze(src: &dyn Source, search_lost: bool) -> DiskLayout {
parts.sort_by_key(|p| p.start);
layout.gaps = compute_gaps(&parts, size);
// Partições perdidas: procurar assinaturas de volumes nas lacunas.
// Lost partitions: scan unallocated gaps for volume signatures.
let mut lost = Vec::new();
if search_lost {
lost = find_lost(src, &layout.gaps, &parts);
if parts.is_empty() && layout.table == "none" {
// Disco sem tabela nem volume no início: procurar no disco todo.
// No partition table or volume at sector 0: scan the entire disk for lost volumes.
lost = find_lost(
src,
&[Gap {
@@ -304,7 +304,7 @@ fn read_mbr(src: &dyn Source, s0: &[u8], size: u64) -> Vec<PartInfo> {
continue;
}
if ty == 0xEE {
continue; // MBR protetor de GPT
continue; // GPT protective MBR
}
if matches!(ty, 0x05 | 0x0F | 0x85) {
ext_base = Some(lba * SECTOR);
@@ -365,7 +365,7 @@ fn part(index: usize, start: u64, len: u64, table_type: Option<String>) -> PartI
}
fn read_gpt(src: &dyn Source, size: u64) -> Option<Vec<PartInfo>> {
// Tenta o cabeçalho primário (LBA 1) e depois o de backup (último setor).
// Try the primary GPT header (LBA 1) first, falling back to the backup header (last LBA).
let hdr = read_vec(src, SECTOR, 512)
.ok()
.filter(|h| h.len() == 512 && &h[0..8] == b"EFI PART")
@@ -383,7 +383,7 @@ fn read_gpt(src: &dyn Source, size: u64) -> Option<Vec<PartInfo>> {
}
let mut tbl = read_vec(src, table_lba * SECTOR, n * esz).ok()?;
if tbl.len() < n * esz {
// Cabeçalho de backup aponta para a tabela de backup; se falhar, desiste.
// Backup GPT header points to the backup partition array; abort if both fail.
tbl.resize(n * esz, 0);
}
let mut out = vec![];
@@ -418,7 +418,7 @@ fn read_gpt(src: &dyn Source, size: u64) -> Option<Vec<PartInfo>> {
}
fn gpt_type_name(guid: &[u8]) -> &'static str {
// GUIDs em formato de disco (mixed-endian).
// Known partition type GUIDs in on-disk mixed-endian byte order.
match guid {
[0x28, 0x73, 0x2A, 0xC1, 0x1F, 0xF8, 0xD2, 0x11, 0xBA, 0x4B, 0x00, 0xA0, 0xC9, 0x3E, 0xC9, 0x3B] => {
"EFI System"
@@ -445,7 +445,7 @@ fn gpt_type_name(guid: &[u8]) -> &'static str {
}
}
/// Procura volumes (por assinatura) dentro de lacunas, em limites de setor.
/// Scans unallocated byte gaps on sector boundaries for lost filesystem/volume headers.
fn find_lost(src: &dyn Source, gaps: &[Gap], known: &[PartInfo]) -> Vec<PartInfo> {
const CHUNK: u64 = 4 * 1024 * 1024;
let mut found: Vec<PartInfo> = vec![];
@@ -468,7 +468,7 @@ fn find_lost(src: &dyn Source, gaps: &[Gap], known: &[PartInfo]) -> Vec<PartInfo
if abs >= skip_until {
let s = &buf[off as usize..];
if s.len() >= 1100 {
// Filtro rápido: só chama a detecção completa se algum magic casa.
// Fast pre-filter: only run full detection when a known magic byte sequence matches.
let quick = &s[3..11] == b"NTFS "
|| &s[3..11] == b"EXFAT "
|| &s[3..11] == b"-FVE-FS-"
@@ -477,13 +477,13 @@ fn find_lost(src: &dyn Source, gaps: &[Gap], known: &[PartInfo]) -> Vec<PartInfo
|| (s[1080] == 0x53 && s[1081] == 0xEF);
if quick {
if let Some((fs, len)) = detect_fs_in(s) {
// ext: o superbloco está a 1024 bytes do início do volume,
// por isso o candidato ext é o setor atual (início do volume).
// ext2/3/4: the superblock sits 1024 bytes into the volume,
// so `off` (1024 bytes before the magic) is the candidate volume start.
let len = len.unwrap_or(0);
let ok = len >= 64 * 1024
&& abs + len <= src.size() + 1024 * 1024
&& !overlaps(known, abs, len);
// Evita falso positivo de FAT em setor de boot MBR genérico.
// Avoid false-positive FAT detection on generic MBR boot code.
if ok && (fs != FsKind::Fat12 || len >= 512 * 1024) {
let mut p =
part(0, abs, len, Some("encontrada por varredura".into()));
+14 -14
View File
@@ -1,8 +1,8 @@
//! Gerador mínimo de PDF (A4, texto e tabelas) para o relatório de recuperação.
//! Minimal standalone PDF generator (A4, text, and tables) for forensic recovery reports.
//!
//! Usa só as fontes padrão do PDF (Helvetica, Helvetica-Bold e Courier) com codificação
//! WinAnsi, então o arquivo é autocontido e não depende de fontes instaladas. Caracteres fora
//! do Latin-1/WinAnsi viram `?`.
//! Uses only built-in PDF Type 1 fonts (Helvetica, Helvetica-Bold, and Courier) with WinAnsi
//! encoding so output files are self-contained with no embedded font dependencies. Characters
//! outside Latin-1/WinAnsi are replaced with `?`.
use flate2::write::ZlibEncoder;
use flate2::Compression;
@@ -29,7 +29,7 @@ impl Font {
}
}
// Larguras AFM (milésimos de em) dos caracteres ASCII 32..=126.
// AFM glyph widths (in thousandths of an em) for ASCII 32..=126.
const HELV: [u16; 95] = [
278, 278, 355, 556, 556, 889, 667, 191, 333, 333, 389, 584, 278, 333, 278, 278, 556, 556, 556,
556, 556, 556, 556, 556, 556, 556, 278, 278, 584, 584, 584, 556, 1015, 667, 667, 722, 722, 667,
@@ -45,7 +45,7 @@ const HELV_B: [u16; 95] = [
278, 889, 611, 611, 611, 611, 389, 556, 333, 611, 556, 778, 556, 556, 500, 389, 280, 389, 584,
];
/// Letra base de um caractere acentuado do Latin-1 (a largura é a da letra sem acento).
/// Maps a Latin-1 accented character to its ASCII base letter for glyph width lookup.
fn base_letter(c: char) -> char {
match c {
'À'..='Å' => 'A',
@@ -88,7 +88,7 @@ pub fn text_width(s: &str, size: f64, f: Font) -> f64 {
s.chars().map(|c| char_width(c, f)).sum::<f64>() * size
}
/// Byte WinAnsi de um caractere (ou `?`).
/// Converts a Unicode char to its WinAnsi byte (or `?` if unrepresentable).
fn win_ansi(c: char) -> u8 {
let n = c as u32;
match c {
@@ -120,8 +120,8 @@ fn escape(s: &str) -> String {
o
}
/// Quebra `s` em linhas de no máximo `width` pontos. Sem espaço onde quebrar (caminhos,
/// hashes), corta no meio da palavra.
/// Greedy word-wraps `s` into lines of at most `width` points. Long tokens without spaces
/// (paths, hashes) are broken mid-token.
pub fn wrap(s: &str, size: f64, f: Font, width: f64) -> Vec<String> {
let mut lines = vec![];
for para in s.split('\n') {
@@ -270,7 +270,7 @@ impl Pdf {
self.y -= 3.0;
}
/// Aviso destacado com barra lateral.
/// Renders a highlighted callout box with a colored left accent bar.
pub fn note(&mut self, s: &str) {
let size = 9.0;
let lines = wrap(s, size, Font::Regular, Self::content_width() - 16.0);
@@ -292,7 +292,7 @@ impl Pdf {
self.y -= h + 6.0;
}
/// Linhas rótulo/valor; o valor pode ser monoespaçado.
/// Renders key/value metadata rows (with optional monospace value styling).
pub fn kv(&mut self, rows: &[(&str, &str, Font)]) {
let lw = 140.0;
let size = 9.5;
@@ -318,7 +318,7 @@ impl Pdf {
self.y -= 4.0;
}
/// Tabela com cabeçalho repetido a cada página e linhas que quebram texto.
/// Renders a multi-page table with repeating headers and wrapped cell text.
pub fn table(&mut self, cols: &[Col], rows: &[Vec<String>]) {
let size = 8.0;
let pad = 3.0;
@@ -391,7 +391,7 @@ impl Pdf {
self.y -= 6.0;
}
/// Fecha o documento. `date` é a data de criação em UTC (`AAAAMMDDHHMMSS`).
/// Finalizes and serializes the PDF document. `date` is the UTC creation timestamp (`YYYYMMDDHHMMSS`).
pub fn finish(mut self, date: &str) -> Vec<u8> {
if !self.cur.is_empty() || self.pages.is_empty() {
self.pages.push(std::mem::take(&mut self.cur));
@@ -416,7 +416,7 @@ impl Pdf {
out.extend_from_slice(body);
out.extend_from_slice(b"\nendobj\n");
};
// 1 catálogo, 2 páginas, 3-5 fontes, 6 info, depois (página, conteúdo) por página
// PDF objects: 1 catalog, 2 pages root, 3-5 fonts, 6 info dict, then (page, content stream) per page
obj(&mut out, b"<< /Type /Catalog /Pages 2 0 R >>");
let kids: String = (0..total)
.map(|i| format!("{} 0 R", 7 + 2 * i))
+27 -27
View File
@@ -1,8 +1,8 @@
//! RAID por software: linear, 0, 1, 4, 5, 6 e 10, a partir dos discos soltos.
//! Software RAID reconstruction: linear, 0, 1, 4, 5, 6, and 10 from member disks.
//!
//! Lê os metadados do mdadm (superbloco 0.90 e 1.x) para montar sozinho; sem metadados, o
//! módulo `raid_detect` descobre ordem, tamanho de faixa e layout. Se faltarem discos, o
//! array é lido em modo degradado (paridade reconstrói o que faltar).
//! Parses Linux `mdadm` superblocks (v0.90 and v1.x) for automatic assembly; for metadata-less
//! arrays, `raid_detect` infers disk order, chunk size, and parity layout. Missing disks are
//! reconstructed on the fly in degraded mode via XOR / GF(2^8) parity.
use crate::source::{read_vec, Source};
use std::io;
@@ -34,7 +34,7 @@ impl Level {
}
}
/// Posição da paridade e ordem dos dados, como no driver md do Linux.
/// Parity placement and data stripe ordering, matching the Linux kernel `md` driver.
#[derive(Clone, Copy, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum Layout {
@@ -42,7 +42,7 @@ pub enum Layout {
RightAsymmetric,
LeftSymmetric,
RightSymmetric,
/// RAID 4 e variantes "parity-0"/"parity-n".
/// RAID 4 and fixed-parity (`parity-first` / `parity-last`) variants.
ParityFirst,
ParityLast,
}
@@ -72,23 +72,23 @@ impl Layout {
#[derive(Clone)]
pub struct Member {
pub src: Arc<dyn Source>,
/// Onde começam os dados dentro do membro.
/// Byte offset where array data begins within this member disk.
pub data_offset: u64,
}
#[derive(Clone)]
pub struct RaidParams {
pub level: Level,
/// Tamanho da faixa por disco, em bytes.
/// Stripe chunk size per disk in bytes.
pub chunk: u64,
pub layout: Layout,
/// Membros na ordem do array; `None` = disco ausente.
/// Member disks in array slot order (`None` = missing/failed disk).
pub members: Vec<Option<Member>>,
/// RAID 10: cópias "perto" (near) e "longe" (far).
/// RAID 10 layout parameters: `near` and `far` replica counts.
pub near: u32,
pub far: u32,
pub far_offset: bool,
/// Bytes úteis por membro (0 = até o fim do menor).
/// Usable data bytes per member (0 = use the shortest member's length).
pub per_member: u64,
}
@@ -113,7 +113,7 @@ pub struct RaidSource {
per_member: u64,
}
// ------------------------------------------------------------ GF(2^8) para RAID 6
// ------------------------------------------------------------ GF(2^8) arithmetic for RAID 6
fn gf_mul(mut a: u8, mut b: u8) -> u8 {
let mut r = 0u8;
@@ -231,7 +231,7 @@ impl RaidSource {
Ok(())
}
/// (disco de paridade P, disco de paridade Q, disco que guarda o dado `d` da linha `row`)
/// Returns `(p_disk, q_disk, data_disk_for_d)` for stripe `row`.
fn raid5_map(&self, row: u64, d: usize) -> (usize, usize) {
let n = self.p.members.len();
let r = (row % n as u64) as usize;
@@ -273,7 +273,7 @@ impl RaidSource {
let disk = if self.p.layout == Layout::LeftSymmetric {
(pd + 2 + d) % n
} else {
// assimétrico: dados em ordem pulando P e Q
// asymmetric layout: data chunks placed in ascending disk order, skipping P and Q
let mut k = d;
for skip in sorted2(pd, qd) {
if k >= skip {
@@ -305,7 +305,7 @@ impl RaidSource {
}
}
/// Lê `buf.len()` bytes dentro de um único chunk lógico.
/// Reads `buf.len()` bytes within a single logical stripe chunk.
fn read_in_chunk(&self, chunk_idx: u64, within: u64, buf: &mut [u8]) -> io::Result<()> {
let n = self.p.members.len();
let c = self.p.chunk;
@@ -333,7 +333,7 @@ impl RaidSource {
if self.p.members[disk].is_some() {
return self.read_member(disk, off, buf);
}
// Reconstrói por XOR de todos os outros (dados + paridade) da linha.
// Reconstruct missing disk chunk by XORing all remaining data and parity chunks in the stripe.
buf.fill(0);
let mut tmp = vec![0u8; buf.len()];
for i in 0..n {
@@ -374,7 +374,7 @@ impl RaidSource {
}
}
/// Onde ficam todas as cópias de um chunk lógico (como `raid10_find_phys` do kernel).
/// Computes all physical `(disk, chunk_index)` replicas for a logical RAID 10 chunk (matches Linux `raid10_find_phys`).
fn r10_locations(&self, chunk_idx: u64) -> Vec<(usize, u64)> {
let n = self.p.members.len() as u64;
let c = self.p.chunk;
@@ -416,7 +416,7 @@ impl RaidSource {
let dd = n - 2;
let (pd, qd, _) = self.raid6_map(row, 0);
let len = buf.len();
// Estado de cada dado da linha.
// Read all available data chunks in the stripe.
let disks: Vec<usize> = (0..dd).map(|k| self.raid6_map(row, k).2).collect();
let missing_data: Vec<usize> = (0..dd)
.filter(|&k| self.p.members[disks[k]].is_none())
@@ -549,7 +549,7 @@ impl Source for RaidSource {
}
}
// ============================================================ metadados do mdadm
// ============================================================ mdadm superblock metadata
#[derive(Clone, Debug)]
pub struct MdSuper {
@@ -560,10 +560,10 @@ pub struct MdSuper {
pub layout: u32,
pub chunk: u64,
pub raid_disks: u32,
/// Papel deste disco no array (None = reserva/falho).
/// Slot index of this disk in the array (`None` = spare or failed).
pub role: Option<u32>,
pub data_offset: u64,
/// Bytes de dados por membro.
/// Usable data size per member in bytes.
pub data_size: u64,
pub events: u64,
pub array_size: u64,
@@ -589,10 +589,10 @@ fn level_from(v: i32) -> Option<Level> {
})
}
/// Procura um superbloco md nas posições possíveis de `src` (um disco ou partição).
/// Probes `src` (disk or partition) for an `mdadm` superblock at standard v1.x and v0.90 offsets.
pub fn read_md_super(src: &dyn Source) -> Option<MdSuper> {
let size = src.size();
// 1.1 no início, 1.2 a 4 KiB, 1.0 a 8 KiB do fim.
// v1.1 at 0, v1.2 at 4 KiB, v1.0 near end of device (8 KiB-aligned).
let mut cands: Vec<(u64, &str)> = vec![(0, "1.1"), (4096, "1.2")];
if size > 16384 {
cands.push(((size & !4095) - 8192, "1.0"));
@@ -644,7 +644,7 @@ pub fn read_md_super(src: &dyn Source) -> Option<MdSuper> {
array_size,
});
}
// 0.90: no fim, alinhado a 64 KiB.
// v0.90: within the last 64 KiB-aligned boundary at the end of the device.
if size > 131072 {
let off = (size & !65535) - 65536;
if let Ok(b) = read_vec(src, off, 4096) {
@@ -688,7 +688,7 @@ fn uuid_str(u: &[u8; 16]) -> String {
u.iter().map(|b| format!("{b:02x}")).collect()
}
/// Um array reconhecido pelos superblocos dos discos fornecidos.
/// A software RAID array assembled from `mdadm` superblocks.
pub struct FoundArray {
pub uuid: String,
pub name: String,
@@ -697,7 +697,7 @@ pub struct FoundArray {
pub missing: usize,
}
/// Agrupa membros por UUID e monta cada array (degradado se for possível).
/// Groups member disks by array UUID and assembles each array (including degraded arrays when recoverable).
pub fn assemble_md(members: Vec<(Arc<dyn Source>, MdSuper)>) -> Vec<Result<FoundArray, String>> {
let mut groups: Vec<(String, Vec<(Arc<dyn Source>, MdSuper)>)> = vec![];
for (s, m) in members {
@@ -709,7 +709,7 @@ pub fn assemble_md(members: Vec<(Arc<dyn Source>, MdSuper)>) -> Vec<Result<Found
}
let mut out = vec![];
for (uuid, mut list) in groups {
// O membro com mais eventos manda; membros desatualizados são tratados como ausentes.
// Use the member with the highest event counter as authoritative; stale members are treated as missing.
list.sort_by_key(|(_, m)| std::cmp::Reverse(m.events));
let top = list[0].1.clone();
let n = top.raid_disks as usize;
+10 -10
View File
@@ -1,5 +1,5 @@
//! Descobre sozinho nível, ordem dos discos, tamanho de faixa e layout de um RAID sem
//! metadados, testando as combinações e medindo a coerência do sistema de arquivos que sai.
//! Automatically detects level, disk order, chunk size, and parity layout of a metadata-less
//! RAID array by scoring parity consistency and filesystem structural coherence across combinations.
use crate::partition;
use crate::raid::{Layout, Level, Member, RaidParams, RaidSource};
@@ -12,7 +12,7 @@ pub struct Candidate {
pub level: Level,
pub chunk: u64,
pub layout: Layout,
/// Índices dos membros fornecidos, na ordem do array.
/// Indices into the input member slice, ordered by array slot.
pub order: Vec<usize>,
pub score: f64,
}
@@ -96,7 +96,7 @@ fn ntfs_score(src: &dyn Source, start: u64) -> f64 {
if r.len() < 64 {
continue;
}
// Registros além do fim do MFT estão zerados: não contam contra.
// Zero-filled blocks past the active MFT records do not penalize the score.
if r.iter().all(|&x| x == 0) {
continue;
}
@@ -112,7 +112,7 @@ fn ntfs_score(src: &dyn Source, start: u64) -> f64 {
}
}
/// Coerência do volume que começa em `start`.
/// Scores the structural coherence of a candidate volume starting at `start`.
fn volume_score(src: &dyn Source, start: u64, fs: FsKind) -> f64 {
match fs {
FsKind::Ntfs => ntfs_score(src, start),
@@ -140,7 +140,7 @@ fn first_sector_plausible(src: &dyn Source) -> bool {
b.len() >= 1100 && ((b[510] == 0x55 && b[511] == 0xAA) || partition::detect_fs_in(&b).is_some())
}
/// Testa as combinações e devolve as melhores (maior pontuação primeiro).
/// Evaluates candidate RAID configurations and returns them sorted by score (best first).
pub fn detect(
members: &[Arc<dyn Source>],
cancelled: &(dyn Fn() -> bool + Sync),
@@ -149,7 +149,7 @@ pub fn detect(
if n < 2 {
return vec![];
}
// Trabalha sobre um prefixo em memória de cada disco.
// Operate on an in-memory prefix of each member disk for fast candidate evaluation.
let cached: Vec<Arc<dyn Source>> = members
.iter()
.map(|m| {
@@ -163,7 +163,7 @@ pub fn detect(
}
let mut out: Vec<Candidate> = vec![];
// RAID 1: cópias idênticas.
// RAID 1: identical mirror copies.
let probe = |s: &Arc<dyn Source>| read_vec(s.as_ref(), 0, 1 << 20).unwrap_or_default();
let first = probe(&cached[0]);
if first.iter().any(|&x| x != 0) && cached.iter().skip(1).all(|c| probe(c) == first) {
@@ -177,7 +177,7 @@ pub fn detect(
out.push(c);
}
// RAID 5/4 satisfaz "XOR de todos os discos = 0" em toda faixa.
// RAID 5/4 satisfies `XOR across all disks == 0` on every active stripe.
let sample = 4usize << 20;
let bufs: Vec<Vec<u8>> = cached
.iter()
@@ -246,7 +246,7 @@ pub fn detect(
let Ok(r) = RaidSource::new(params) else {
continue;
};
// O primeiro setor do volume precisa estar num disco que o tenha.
// The first sector of the candidate volume must reside on a disk where that signature was seen.
if any_first {
let mut b = [0u8; 512];
let _ = r.read_at(0, &mut b);
+6 -6
View File
@@ -1,19 +1,19 @@
//! Reencontra o disco de uma sessão quando ele foi desconectado e voltou com
//! outro nome (por exemplo `/dev/sda` virando `/dev/sdb`).
//! Re-identifies and reconnects a session's source disk when unplugged and reattached
//! under a different OS device node (e.g. `/dev/sda` becoming `/dev/sdb`).
use crate::device;
use crate::source::{fingerprint, open_path, Source};
use std::path::Path;
use std::sync::Arc;
/// A origem é compatível com o que a sessão espera?
/// `size == 0` ou `fp == None` significam "não sei" e não reprovam.
/// Checks whether `src` matches the size and fingerprint expected by the session.
/// `size == 0` or `fp == None` mean unknown (legacy session) and do not reject the source.
pub fn matches(src: &dyn Source, size: u64, fp: Option<&str>) -> bool {
(size == 0 || src.size() == size) && fp.map_or(true, |f| fingerprint(src).as_deref() == Some(f))
}
/// Procura entre os discos conectados o que corresponde à sessão.
/// Com impressão digital, exige igualdade; sem ela, só aceita se houver um único disco do mesmo tamanho.
/// Searches attached storage devices for the disk matching the session's fingerprint.
/// Requires an exact fingerprint match when present; otherwise accepts a unique size match.
pub fn find_matching_source(
size: u64,
fp: Option<&str>,
+23 -23
View File
@@ -1,5 +1,5 @@
//! Gravação dos arquivos recuperados (sempre em outro destino) com SHA-256
//! e manifesto da sessão.
//! Writes recovered files to a separate destination volume along with SHA-256
//! digests, recovery journal, and session manifest.
use crate::error::{Error, Result};
use crate::fs::sanitize_name;
@@ -33,12 +33,12 @@ pub struct RecoverReport {
pub failed: Vec<(u64, String)>,
pub total_bytes: u64,
pub cancelled: bool,
/// Arquivos já recuperados e conferidos numa execução anterior (pulados ao retomar).
/// Files already recovered and SHA-256 verified in a prior run (skipped on resume).
#[serde(default)]
pub skipped: usize,
}
/// Recusa destinos que ficam no mesmo dispositivo da origem.
/// Rejects destination paths located on the same physical device as the source.
pub fn check_destination(source_path: Option<&Path>, dest: &Path) -> Result<()> {
crate::owner::create_dir_all_owned(dest)?;
let dest = dest.canonicalize()?;
@@ -69,24 +69,24 @@ pub fn check_destination(source_path: Option<&Path>, dest: &Path) -> Result<()>
}
if let Ok(meta) = std::fs::metadata(sp) {
if meta.is_file() {
// Imagem: evita gravar ao lado só se for o mesmo arquivo (não faz sentido), mas aceita.
// For image files: only reject writing directly over the source image file itself.
}
}
Ok(())
}
/// Como organizar os arquivos recuperados dentro do destino.
/// Directory layout strategy for recovered files inside the destination folder.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum RecoverLayout {
/// Pastas originais quando conhecidas; o resto separado por tipo (padrão).
/// Preserve original folder hierarchy when known; group carved files by type (default).
#[default]
Folders,
/// Uma pasta por tipo (Fotos, Documentos, Vídeos...).
/// Group into one folder per category (Photos, Documents, Videos, etc.).
Type,
/// Pastas por ano e mês (data da foto, ou a de modificação).
/// Group into `YYYY/YYYY-MM` folders by capture or modification date.
Date,
/// Tudo direto na pasta de destino.
/// Place all files flat inside the destination folder.
Flat,
}
@@ -130,7 +130,7 @@ pub fn recover_with(
recover_resumable(src, files, dest, layout, "", false, cancelled, progress)
}
/// Uma linha do diário de recuperação (`guara-diario.jsonl`): cada arquivo gravado e conferido.
/// A single entry in the recovery journal (`guara-diario.jsonl`) for a written and verified file.
#[derive(Debug, Clone, Serialize, Deserialize)]
struct JournalEntry {
source: String,
@@ -156,9 +156,9 @@ fn sha256_of_file(p: &Path) -> Option<String> {
Some(h.finalize().sha256)
}
/// Como `recover_with`, mas mantém um diário e, com `resume`, pula o que já foi
/// recuperado (e conferido por SHA-256) numa execução interrompida.
/// `source_key` identifica a origem para que o diário de outro disco não seja reaproveitado.
/// Like `recover_with`, but maintains an append-only journal and, when `resume` is set, skips
/// files already written and SHA-256 verified during an interrupted run.
/// `source_key` binds the journal to the source disk so another disk's journal is never reused.
#[allow(clippy::too_many_arguments)]
pub fn recover_resumable(
src: &dyn Source,
@@ -202,7 +202,7 @@ pub fn recover_resumable(
break;
}
progress(i, files.len(), report.total_bytes);
// retomada: já está lá e confere?
// resume check: does the file already exist on disk with the expected size and SHA-256?
if let Some(e) = prior.get(&f.id) {
let p = dest.join(&e.file.path);
if e.name == f.name && e.file.size == f.size && p.is_file() {
@@ -215,7 +215,7 @@ pub fn recover_resumable(
report.recovered.push(e.file.clone());
continue;
}
// incompleto ou alterado: refaz (é um arquivo nosso)
// incomplete or modified partial file from our previous run: overwrite it
let _ = std::fs::remove_file(&p);
}
}
@@ -321,7 +321,7 @@ fn target_rel(f: &Found, layout: RecoverLayout) -> PathBuf {
p
}
/// Espaço livre (bytes) no volume onde `path` está (ou estaria).
/// Returns available free bytes on the filesystem containing `path`.
pub fn free_space(path: &Path) -> Option<u64> {
let mut p = path;
while !p.exists() {
@@ -331,7 +331,7 @@ pub fn free_space(path: &Path) -> Option<u64> {
{
use std::os::unix::ffi::OsStrExt;
let c = std::ffi::CString::new(p.as_os_str().as_bytes()).ok()?;
// SAFETY: `c` é uma string C válida e `st` é uma estrutura de saída local.
// SAFETY: `c` is a valid NUL-terminated C string and `st` is a valid local output struct.
let mut st: libc::statvfs = unsafe { std::mem::zeroed() };
if unsafe { libc::statvfs(c.as_ptr(), &mut st) } != 0 {
return None;
@@ -348,7 +348,7 @@ pub fn free_space(path: &Path) -> Option<u64> {
.chain(std::iter::once(0))
.collect();
let mut free = 0u64;
// SAFETY: `wide` termina em NUL e o ponteiro de saída é válido.
// SAFETY: `wide` is NUL-terminated and the output pointer is valid.
let ok = unsafe {
GetDiskFreeSpaceExW(
wide.as_ptr(),
@@ -361,7 +361,7 @@ pub fn free_space(path: &Path) -> Option<u64> {
}
}
/// Escreve o conteúdo e devolve o SHA-256.
/// Streams the recovered file content to `out` and returns its hex SHA-256 digest.
pub fn write_file(src: &dyn Source, f: &Found, path: &Path) -> Result<Hashes> {
let mut out = std::fs::OpenOptions::new()
.write(true)
@@ -443,8 +443,8 @@ pub fn write_file(src: &dyn Source, f: &Found, path: &Path) -> Result<Hashes> {
Ok(hasher.finalize())
}
/// Lê `buf.len()` bytes do conteúdo lógico de um achado a partir de `offset`
/// (usado para streaming/pré-visualização sem gravar em disco).
/// Reads up to `buf.len()` bytes of a recovered file's logical content starting at `offset`
/// (used for zero-copy streaming and preview without writing to disk).
pub fn read_found_at(
src: &dyn Source,
f: &Found,
@@ -510,7 +510,7 @@ pub fn read_found_at(
}
}
/// Um arquivo achado visto como origem de leitura posicional (para leitores de formato).
/// Adapter exposing a recovered `Found` file as a positional `Source` (for format parsers).
pub struct FoundSource<'a> {
pub src: &'a dyn Source,
pub found: &'a Found,
+8 -8
View File
@@ -1,5 +1,5 @@
//! Exportação para análise e relatório de recuperação (cadeia de custódia):
//! CSV dos achados e um relatório HTML autocontido, pronto para imprimir.
//! Forensic export and chain-of-custody recovery reporting:
//! CSV findings table and self-contained, print-ready HTML/PDF reports.
use crate::hashing::Hashes;
use crate::meta::fmt_unix;
@@ -19,11 +19,11 @@ pub struct CaseInfo {
pub notes: String,
}
/// Situação da trilha de auditoria no momento do relatório.
/// Status of the audit hash chain at the time the report is generated.
pub struct AuditNote {
pub entries: usize,
pub ok: bool,
/// Hash da última entrada, para guardar fora da máquina.
/// Head hash of the latest audit entry, to be archived off-machine.
pub head: String,
}
@@ -42,7 +42,7 @@ pub struct ReportInput<'a> {
}
fn cell(s: &str) -> String {
// aspas e escapa; neutraliza "injeção de fórmula" em planilhas
// quote and escape; neutralize spreadsheet formula injection (`=`, `+`, `-`, `@`)
let mut t = s.replace('"', "\"\"");
if t.starts_with(['=', '+', '-', '@', '\t', '\r']) {
t.insert(0, '\'');
@@ -66,7 +66,7 @@ fn iso(t: Option<i64>) -> String {
}
}
/// CSV com `;` (abre direto no Excel em português) e BOM UTF-8.
/// Writes a UTF-8 BOM `;`-delimited CSV of findings.
pub fn csv(found: &[&Found]) -> String {
let mut out = String::from("\u{feff}id;nome;pasta;extensao;categoria;tamanho_bytes;origem;confianca;modificado_utc;criado_utc;acessado_utc;foto_tirada;camera;largura;altura;gps;deslocamento_no_disco;observacao\r\n");
for f in found {
@@ -237,7 +237,7 @@ footer{margin-top:34px;color:#5b6f6d;font-size:12px}@media print{body{background
o.push_str("</table>");
}
// Resumo
// Summary
let mut by_cat = std::collections::BTreeMap::new();
let mut conf = [0usize; 3];
let mut bytes = 0u64;
@@ -318,7 +318,7 @@ footer{margin-top:34px;color:#5b6f6d;font-size:12px}@media print{body{background
o
}
/// Relatório em PDF (A4), com as mesmas seções do HTML.
/// Generates an A4 PDF recovery report containing the same sections as the HTML report.
pub fn pdf(r: &ReportInput) -> Vec<u8> {
use crate::pdf::{Col, Font, Pdf, BLACK, GRAY};
let now = std::time::SystemTime::now()
+15 -15
View File
@@ -1,5 +1,5 @@
//! Orquestra a varredura: layout → metadados dos sistemas de arquivos →
//! carving no espaço livre/desconhecido → fusão sem duplicatas.
//! Scan orchestrator: partition layout -> filesystem metadata readers ->
//! signature carving across free/unallocated space -> deduplicated merge.
use crate::carve::{carve_range, CarveCfg, CarveEnd, CarveEvents, HitAction};
use crate::fs::{self, merge_extents, overlaps, Ctx};
@@ -18,12 +18,12 @@ pub struct Progress {
pub total: u64,
}
/// Receptor dos resultados (a interface/CLI implementa).
/// Callback sink for scan progress and findings (implemented by the CLI and UI service).
pub trait Sink {
fn found(&mut self, f: Found);
/// Um achado de carving confirmou um arquivo já listado por metadados.
/// A signature-carved hit confirmed a file already discovered via filesystem metadata.
fn confirm(&mut self, id: u64, confidence: Confidence);
/// Um achado já emitido ganhou dados melhores (ex.: JPEG remontado de fragmentos).
/// A previously emitted finding was upgraded (e.g. fragmented JPEG reassembled or MP4 `moov` rebuilt).
fn replace(&mut self, f: Found) {
self.found(f);
}
@@ -39,14 +39,14 @@ pub struct RangeState {
pub next: u64,
}
/// Estado serializável para pausar/retomar.
/// Serializable scan state for pause/resume checkpoints.
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct Checkpoint {
pub fs_done: bool,
pub ranges: Vec<RangeState>,
pub next_id: u64,
pub layout: Option<DiskLayout>,
/// A varredura parou porque o dispositivo parou de responder (desconectado?).
/// True if the scan halted because the source device stopped responding (disconnected).
#[serde(default)]
pub io_lost: bool,
}
@@ -93,7 +93,7 @@ pub fn run(
layout.gaps.clear();
layout.table = "membro de RAID".into();
}
// Volumes lógicos e outros volumes montados que não estão na tabela do disco.
// Logical volumes and unlocked virtual volumes not present in the physical partition table.
for p in &opts.extra_parts {
if !layout.partitions.iter().any(|q| q.start == p.start) {
let mut p = p.clone();
@@ -103,7 +103,7 @@ pub fn run(
}
cp.layout = Some(layout.clone());
// Índices para fusão: primeiro offset -> (id, confiança), e extents de achados por metadados.
// Merge indices: `first_offset -> (id, confidence)` and allocated extents of metadata findings.
let mut starts: HashMap<u64, (u64, Confidence)> = HashMap::new();
let mut fs_extents: Vec<Extent> = vec![];
for f in existing {
@@ -239,7 +239,7 @@ pub fn run(
Outcome::Completed
}
/// Arquivo que parece quebrado: tenta recompor depois de varrer o disco todo.
/// Truncated/fragmented candidate queued for post-scan reassembly once all clusters are indexed.
struct Pending {
found: Found,
hit_len: u64,
@@ -256,16 +256,16 @@ struct Handler<'a> {
total: u64,
index: crate::defrag::ClusterIndex,
pending: Vec<Pending>,
/// Vídeos íntegros achados no carving (início, tamanho): fontes de SPS/PPS.
/// Intact MP4 videos found during carving `(offset, size)`: donor sources for SPS/PPS.
intact_mp4: Vec<(u64, u64)>,
refs: Vec<crate::mp4rebuild::MediaConfig>,
/// Extents de arquivos carved já emitidos (não podem servir de fragmento para outro).
/// Byte extents of already-emitted carved files (excluded from candidate defrag fragments).
carved: Vec<Extent>,
}
impl Handler<'_> {
fn rebuild_mp4(&mut self, p: Pending, off: u64) {
// Referências: as informadas pelo usuário e os vídeos íntegros mais próximos no disco.
// Donor references: user-supplied reference videos plus the closest intact videos on disk.
let mut refs = self.refs.clone();
let mut near = self.intact_mp4.clone();
near.sort_by_key(|&(o, _)| o.abs_diff(off));
@@ -316,7 +316,7 @@ impl Handler<'_> {
}
let mut used = self.fs_extents.clone();
used.extend(self.carved.iter().copied());
// O próprio começo do arquivo não conta como "ocupado" para ele mesmo.
// Exclude the file's own leading prefix from the claimed-extents filter.
used.retain(|e| !(e.offset == off));
let pool = crate::defrag::Pool {
index: &self.index,
@@ -365,7 +365,7 @@ impl CarveEvents for Handler<'_> {
return HitAction::Skip;
}
if overlaps(&self.fs_extents, off, 1) {
return HitAction::Continue; // dentro de arquivo já listado (ex.: miniatura embutida)
return HitAction::Continue; // embedded inside an already-listed file (e.g. EXIF thumbnail): skip duplicate
}
let mut hit = hit.clone();
let mut skip_len = None;
+8 -8
View File
@@ -1,5 +1,5 @@
//! Busca de texto bruta em todo o disco (inclusive espaço livre e não particionado),
//! em UTF-8 e UTF-16LE, útil quando não há estrutura de arquivo para recuperar.
//! Raw full-disk text search (including free and unpartitioned space)
//! across UTF-8 and UTF-16LE encodings, useful when filesystem structures are gone.
use crate::source::Source;
use serde::{Deserialize, Serialize};
@@ -7,10 +7,10 @@ use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SearchOpts {
pub terms: Vec<String>,
/// Ignora maiúsculas/minúsculas (letras ASCII).
/// Case-insensitive matching (ASCII letters).
#[serde(default = "yes")]
pub ignore_case: bool,
/// Procura também a codificação UTF-16 (Windows, Office, bancos de dados).
/// Also match UTF-16LE encoded strings (Windows, Office, SQLite, etc.).
#[serde(default = "yes")]
pub utf16: bool,
#[serde(default = "default_max")]
@@ -116,7 +116,7 @@ fn snippet(buf: &[u8], at: usize, len: usize, utf16: bool) -> String {
.collect()
}
/// Varre `src` inteira. Retorna o total de ocorrências. `on_hit` pode devolver false para parar.
/// Scans `src` from start to end. Returns total matches found; `on_hit` may return `false` to stop.
pub fn search(
src: &dyn Source,
o: &SearchOpts,
@@ -144,7 +144,7 @@ pub fn search(
Ok(0) => break,
Ok(n) => got += n,
Err(_) => {
// setor ruim: pula este trecho
// unreadable chunk: skip and continue
for b in &mut buf[got..want] {
*b = 0;
}
@@ -155,7 +155,7 @@ pub fn search(
if got == 0 {
break;
}
let fresh = got.min(CHUNK); // início de ocorrências aceitas neste bloco
let fresh = got.min(CHUNK); // start offsets of accepted hits within this chunk (prevents duplicate UTF-8/16 hits at same pos)
let folded: Vec<u8>;
let hay: &[u8] = if o.ignore_case {
folded = buf[..got].to_ascii_lowercase();
@@ -163,7 +163,7 @@ pub fn search(
} else {
&buf[..got]
};
let mut chunk_hits: Vec<(usize, usize)> = vec![]; // (offset no bloco, índice do padrão)
let mut chunk_hits: Vec<(usize, usize)> = vec![]; // (offset_in_chunk, pattern_index)
for (pi, p) in pats.iter().enumerate() {
find_all(hay, p, |i| {
if i < fresh {
+56 -56
View File
@@ -1,5 +1,5 @@
//! Camada de serviço usada pela interface gráfica (Tauri e modo `guara ui`):
//! jobs em segundo plano (varredura, recuperação, imagem) e despachante RPC.
//! Background service layer backing the GUI (Tauri and `guara ui` mode):
//! manages scan/recovery/imaging jobs, sessions, and JSON-RPC dispatch.
use crate::device;
use crate::error::{Error, Result};
@@ -49,7 +49,7 @@ struct State {
report: Option<Value>,
started: Instant,
finished_in: Option<f64>,
/// Leitura de datas EXIF/dimensões em andamento: (feitos, total).
/// Background EXIF/dimension enrichment progress: `(done, total)`.
enrich: Option<(u64, u64)>,
recoveries: Vec<recover::RecoverReport>,
started_unix: Option<i64>,
@@ -64,11 +64,11 @@ pub struct Job {
cancel: AtomicBool,
stop_requested: AtomicBool,
src: Mutex<Option<Arc<dyn Source>>>,
/// Caminho atualmente usado para ler a origem (pode mudar se o disco voltar com outro nome).
/// Active device/image path used to read the source (may update if reattached under a new node).
src_path: Mutex<String>,
/// Impressão digital do disco (reconhecer o mesmo disco depois de reconectar).
/// Source disk content fingerprint (used to recognize the drive after reconnection).
fingerprint: Mutex<Option<String>>,
/// Arquivo de sessão salvo automaticamente.
/// Path to the autosaved session file.
session_file: Mutex<Option<PathBuf>>,
st: Mutex<State>,
}
@@ -213,7 +213,7 @@ impl Engine {
job
}
// ---------------------------------------------------------------- varredura
// ---------------------------------------------------------------- scan
pub fn start_scan(&self, path: &str, mut opts: ScanOptions) -> Result<String> {
let raw = open_path(Path::new(path)).map_err(|e| open_error(path, e))?;
@@ -282,7 +282,7 @@ impl Engine {
}
s.checkpoint.as_ref().is_some_and(|c| c.io_lost)
};
// Sem origem, ou depois de uma desconexão: reabre o disco (mesmo com outro nome).
// Source not yet open or previously disconnected: try to reopen (even under a new device name).
if j.source().is_none() || io_lost {
self.reconnect(&j).map_err(|e| {
Error::msg(format!(
@@ -359,14 +359,14 @@ impl Engine {
Ok(json!({ "total": total, "items": page }))
}
/// Contagens para a navegação lateral (tipos, datas, pastas, câmeras).
/// Sidebar navigation facet counts (categories, dates, folders, cameras).
pub fn facets(&self, id: &str) -> Result<Value> {
let j = self.job(id)?;
let s = j.st.lock().unwrap();
Ok(crate::explorer::facets(&s.found))
}
/// Esquema de um banco SQLite achado, com o número de linhas de cada tabela.
/// Schema of a recovered SQLite database, including row counts per table.
pub fn db_info(&self, id: &str, file: u64) -> Result<Value> {
let (f, src) = self.open_file(id, file)?;
let fs = recover::FoundSource {
@@ -383,7 +383,7 @@ impl Engine {
Ok(v)
}
/// Uma página de linhas de uma tabela (ou os registros apagados prováveis dela).
/// A page of live table rows (or carvable deleted records from free blocks).
pub fn db_rows(
&self,
id: &str,
@@ -415,13 +415,13 @@ impl Engine {
Ok(serde_json::to_value(&page)?)
}
/// Metadados detalhados (EXIF, PDF, Office, vídeo…) de um arquivo.
/// Detailed format metadata (EXIF, PDF, Office, video, etc.) for a recovered file.
pub fn metadata(&self, id: &str, file: u64) -> Result<crate::meta::Meta> {
let (f, src) = self.open_file(id, file)?;
Ok(crate::meta::extract(src.as_ref(), &f))
}
/// Miniatura JPEG (cache em memória; decodificações limitadas em paralelo).
/// Generates or fetches an in-memory cached JPEG thumbnail (with bounded concurrency).
pub fn thumb(&self, job: &str, id: u64, size: u32) -> std::result::Result<Arc<Vec<u8>>, u16> {
let size = size.clamp(48, 640);
let key = (job.to_string(), id, size);
@@ -445,7 +445,7 @@ impl Engine {
}
}
/// Achado + origem para leitura posicional (streaming de pré-visualização).
/// Returns `(Found, Source)` for positional streaming preview.
pub fn open_file(&self, id: &str, file: u64) -> Result<(Found, Arc<dyn Source>)> {
let j = self.job(id)?;
let s = j.st.lock().unwrap();
@@ -456,7 +456,7 @@ impl Engine {
Ok((s.found[i].clone(), j.source().ok_or_else(no_source)?))
}
// ---------------------------------------------------------------- recuperação
// ---------------------------------------------------------------- recovery
pub fn start_recover(
&self,
@@ -575,13 +575,13 @@ impl Engine {
Ok(job.id.clone())
}
// ---------------------------------------------------------------- imagem
// ---------------------------------------------------------------- disk imaging
pub fn start_image(&self, path: &str, dest: &str) -> Result<String> {
self.start_image_as(path, dest, &image::ImageFormat::Raw)
}
/// Cria a imagem em bruto ou em outro formato forense (E01 ou AFF4).
/// Creates a raw, E01, or AFF4 forensic disk image.
pub fn start_image_as(
&self,
path: &str,
@@ -644,9 +644,9 @@ impl Engine {
Ok(job.id.clone())
}
// ---------------------------------------------------------------- exportação e relatório
// ---------------------------------------------------------------- export and report
/// CSV dos achados que atendem ao filtro (ou todos). Devolve quantas linhas gravou.
/// Exports matching findings (or all findings) to CSV. Returns the number of rows written.
pub fn export_csv(&self, id: &str, path: &str, q: Option<&Query>) -> Result<usize> {
let j = self.job(id)?;
let s = j.st.lock().unwrap();
@@ -662,7 +662,7 @@ impl Engine {
Ok(items.len())
}
/// Relatório HTML de recuperação (identificação do caso, origem, achados, hashes).
/// Generates an HTML or PDF forensic recovery report (case info, source, findings, hashes).
pub fn export_report(
&self,
id: &str,
@@ -714,7 +714,7 @@ impl Engine {
Ok(())
}
// ---------------------------------------------------------------- busca de texto e hash da origem
// ---------------------------------------------------------------- text search and source hashing
pub fn start_search(&self, path: &str, opts: crate::search::SearchOpts) -> Result<String> {
if opts.terms.iter().all(|t| t.trim().is_empty()) {
@@ -776,7 +776,7 @@ impl Engine {
}))
}
/// Calcula MD5/SHA-1/SHA-256 da origem inteira (verificação de integridade).
/// Computes MD5/SHA-1/SHA-256 across the entire source (integrity verification job).
pub fn start_hash(&self, path: &str) -> Result<String> {
let src = open_path(Path::new(path)).map_err(|e| open_error(path, e))?;
let job = self.register(
@@ -833,7 +833,7 @@ impl Engine {
Ok(job.id.clone())
}
// ---------------------------------------------------------------- sessões
// ---------------------------------------------------------------- sessions
pub fn save_session(&self, id: &str, path: &str) -> Result<()> {
let j = self.job(id)?;
@@ -860,7 +860,7 @@ impl Engine {
"O arquivo {path} não é uma sessão do Guará válida (ou foi criado por uma versão incompatível): {e}"
))
})?;
// Clicar de novo numa sessão já aberta reaproveita o job em vez de criar outro igual.
// Re-opening an already loaded session reuses its existing job ID.
let want = Path::new(path);
if let Some(j) = self
.jobs
@@ -899,8 +899,8 @@ impl Engine {
Status::Paused
};
}
// A sessão sempre abre. Tenta religar o disco (inclusive se ele voltou com outro nome);
// sem a origem ainda dá para navegar, exportar e gerar relatório.
// Always load the session metadata. Attempt to reconnect the source disk (even under a new path);
// if the source is offline, browsing, filtering, CSV export, and reporting still work.
match self.reconnect(&job) {
Ok(Some(nome)) => job.st.lock().unwrap().warnings.push(format!(
"O disco foi reconhecido de novo como {nome} (o nome mudou desde a varredura)."
@@ -911,7 +911,7 @@ impl Engine {
data.target
)),
}
// continua salvando automaticamente no mesmo arquivo se a varredura for retomada
// keep autosaving to the same file if the scan is resumed
*job.session_file.lock().unwrap() = Some(PathBuf::from(path));
crate::audit::record(
"session.load",
@@ -923,9 +923,9 @@ impl Engine {
Ok(job.id.clone())
}
/// Reabre a origem da sessão: tenta o caminho atual e o original e, se o disco voltou com
/// outro nome, procura entre os dispositivos conectados o que tem o mesmo conteúdo.
/// `Ok(Some(caminho))` quando achou o disco num caminho diferente do esperado.
/// Reopens the session's source: tries the current and original paths first, then scans attached
/// devices for a matching fingerprint if the drive reconnected under a new device node.
/// Returns `Ok(Some(new_path))` when found at a different device path.
fn reconnect(&self, j: &Arc<Job>) -> Result<Option<String>> {
let fp = j.fingerprint.lock().unwrap().clone();
let size =
@@ -970,8 +970,8 @@ impl Engine {
))
}
/// Aponta uma sessão carregada para a origem (disco ou imagem) atual.
/// Recusa uma origem que não seja o mesmo disco, a menos que `force`.
/// Rebinds a loaded session to a user-selected source device or disk image.
/// Rejects a source whose fingerprint/size mismatches the session unless `force` is set.
pub fn relink(&self, id: &str, path: &str, force: bool) -> Result<()> {
let j = self.job(id)?;
let src = open_path(Path::new(path)).map_err(|e| open_error(path, e))?;
@@ -1010,7 +1010,7 @@ impl Engine {
Ok(())
}
/// Discos conectados que podem ser a origem desta sessão (mesmo tamanho primeiro).
/// Lists attached disks that could match this session's source (same-size candidates first).
pub fn candidates(&self, id: &str) -> Result<Value> {
let j = self.job(id)?;
let fp = j.fingerprint.lock().unwrap().clone();
@@ -1042,9 +1042,9 @@ impl Engine {
Ok(json!(out))
}
// ---------------------------------------------------------------- sessões automáticas
// ---------------------------------------------------------------- autosaved sessions
/// Sessões salvas automaticamente (mais recentes primeiro).
/// Lists autosaved sessions (most recent first).
pub fn sessions_list(&self) -> Value {
let dir = sessions_dir();
let mut items: Vec<Value> = vec![];
@@ -1071,7 +1071,7 @@ impl Engine {
json!(items)
}
/// Apaga uma sessão automática (e a miniatura de metadados).
/// Deletes an autosaved session file (and its `.meta.json` sidecar).
pub fn session_delete(&self, path: &str) -> Result<()> {
let p = Path::new(path);
if p.parent() != Some(sessions_dir().as_path()) {
@@ -1162,7 +1162,7 @@ impl Engine {
"health" => json!(crate::health::check(&s("path")?)),
"open_url" => {
let u = s("url")?;
// só mapas do OpenStreetMap: evita abrir endereços arbitrários a partir de metadados
// restrict to OpenStreetMap URLs: never open arbitrary URLs derived from untrusted file metadata
if !u.starts_with("https://www.openstreetmap.org/") {
return Err(Error::msg("endereço não permitido"));
}
@@ -1332,7 +1332,7 @@ struct SessionFile {
use crate::explorer::cat_key;
/// 503 quando a origem da sessão está desconectada; 404 nos demais casos.
/// Returns HTTP 503 when the session source is disconnected, or 404 otherwise.
fn status_of(e: Error) -> u16 {
if e.to_string().contains("não está disponível") {
503
@@ -1356,7 +1356,7 @@ fn open_error(path: &str, e: std::io::Error) -> Error {
}
fn spawn_scan(job: Arc<Job>, cp: Option<Checkpoint>) {
// Salva a sessão sozinho enquanto a varredura roda (e ao terminar/pausar).
// Periodically autosaves the session while scanning (and upon completion or pause).
let saver = job.clone();
std::thread::spawn(move || autosave_loop(saver));
std::thread::spawn(move || {
@@ -1420,8 +1420,8 @@ fn spawn_scan(job: Arc<Job>, cp: Option<Checkpoint>) {
});
}
/// Lê EXIF/dimensões das imagens achadas (em segundo plano) para permitir
/// navegar por data, câmera e resolução.
/// Background worker that extracts EXIF dates, camera models, GPS, and image dimensions
/// so findings can be browsed by date, camera, and resolution.
fn spawn_enrich(job: Arc<Job>) {
std::thread::spawn(move || {
let Some(src) = job.source() else { return };
@@ -1487,8 +1487,8 @@ pub fn base64(data: &[u8]) -> String {
out
}
/// Tipo MIME para servir a pré-visualização. Formatos que poderiam executar
/// script (HTML/SVG/XML) saem como texto puro.
/// Returns a safe MIME type for preview streaming. Formats capable of executing
/// scripts (HTML/SVG/XML) are served as `text/plain`.
pub fn mime_for(ext: &str) -> &'static str {
match ext.to_ascii_lowercase().as_str() {
"jpg" | "jpeg" => "image/jpeg",
@@ -1513,13 +1513,13 @@ pub fn mime_for(ext: &str) -> &'static str {
}
}
/// Trecho de um arquivo achado, pronto para responder a uma requisição HTTP
/// (com suporte a `Range`) tanto no modo `guara ui` quanto no app Tauri.
/// Byte slice of a recovered file, ready to answer an HTTP `Range` request
/// in both `guara ui` mode and the Tauri custom protocol handler.
pub struct FileSlice {
found: Found,
src: Arc<dyn Source>,
pub start: u64,
/// Inclusivo.
/// Inclusive end byte offset.
pub end: u64,
pub total: u64,
pub partial: bool,
@@ -1549,7 +1549,7 @@ impl FileSlice {
("Accept-Ranges", "bytes".into()),
("Cache-Control", "no-store".into()),
("X-Content-Type-Options", "nosniff".into()),
// Conteúdo recuperado é não confiável: nunca executa script nem carrega nada.
// Recovered content is untrusted: forbid script execution and external resource loading via CSP.
(
"Content-Security-Policy",
"sandbox; default-src 'none'".into(),
@@ -1569,7 +1569,7 @@ impl FileSlice {
}
h
}
/// Lê o trecho inteiro em memória (limitado a `cap` bytes).
/// Reads the requested byte range into memory (capped at `cap` bytes).
pub fn read_all(&self, cap: u64) -> std::io::Result<Vec<u8>> {
let n = self.len().min(cap) as usize;
let mut out = vec![0u8; n];
@@ -1621,7 +1621,7 @@ impl std::io::Read for FileSliceReader {
}
impl Engine {
/// Resolve `/file/<job>/<id>` + cabeçalho Range. `Err(status)` em caso de falha.
/// Resolves `/file/<job>/<id>` with optional `Range` header. Returns `Err(http_status)` on failure.
pub fn file_slice(
&self,
job: &str,
@@ -1663,9 +1663,9 @@ impl Engine {
}
}
// ---------------------------------------------------------------- sessões automáticas
// ---------------------------------------------------------------- autosaved sessions
/// Reaplica ao disco reaberto a montagem (volumes desbloqueados, RAID) da sessão.
/// Re-applies the session's virtual disk assembly (unlocked volumes, RAID, LVM) to a reopened source.
fn finish_source(j: &Job, raw: Arc<dyn Source>) -> Arc<dyn Source> {
match crate::assemble::assemble_source(raw.clone(), &j.opts.assembly) {
Ok(a) => a.source,
@@ -1700,7 +1700,7 @@ fn new_session_path(target: &str) -> PathBuf {
fn snapshot(j: &Job) -> SessionFile {
let s = j.st.lock().unwrap();
// Senhas e chaves nunca vão para o arquivo de sessão (só as chaves de volume, se o usuário pediu).
// Passwords and raw keyfiles are stripped before saving (only derived volume keys are kept if opted in).
let mut options = j.opts.clone();
options.assembly.secrets.clear();
if !options.assembly.remember_keys {
@@ -1722,14 +1722,14 @@ fn snapshot(j: &Job) -> SessionFile {
}
}
/// Grava a sessão de forma atômica (arquivo temporário + renomear), com o dono certo.
/// Atomically writes the session file (temp file + rename) owned by the invoking user.
fn write_session(j: &Job, path: &Path) -> Result<()> {
let data = snapshot(j);
let tmp = path.with_extension("json.tmp");
crate::owner::write_owned(&tmp, serde_json::to_vec(&data)?)?;
std::fs::rename(&tmp, path)?;
crate::owner::give_to_user(path);
// metadados pequenos para listar as sessões recentes sem ler o arquivo inteiro
// lightweight `.meta.json` sidecar so recent sessions can be listed without parsing full snapshots
let status = if data.done {
"concluída"
} else if data.checkpoint.is_some() {
@@ -1749,7 +1749,7 @@ fn write_session(j: &Job, path: &Path) -> Result<()> {
Ok(())
}
/// Salva de tempos em tempos (o intervalo cresce se a sessão for grande) e no fim.
/// Autosaves periodically (backing off as snapshot write time grows) and once on completion.
fn autosave_loop(job: Arc<Job>) {
let mut last = Instant::now();
let mut interval = std::time::Duration::from_secs(45);
@@ -1766,7 +1766,7 @@ fn autosave_loop(job: Arc<Job>) {
if write_session(&job, &path).is_err() {
return;
}
// nunca gasta mais de ~10% do tempo salvando
// spend at most ~10% of wall-clock time writing snapshots
interval = (t0.elapsed() * 10).max(std::time::Duration::from_secs(45));
last = Instant::now();
if !running {
+14 -14
View File
@@ -1,5 +1,5 @@
//! Preferências do usuário (tema etc.), compartilhadas entre o app normal e o
//! reiniciado como administrador.
//! User preferences (theme, language, notifications, etc.), shared between normal and
//! elevated (root/administrator) instances.
use crate::error::Result;
use serde::{Deserialize, Serialize};
@@ -7,24 +7,24 @@ use std::path::PathBuf;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Settings {
/// "auto", "light" ou "dark".
/// Theme mode: `"auto"`, `"light"`, or `"dark"`.
#[serde(default = "auto")]
pub theme: String,
/// Idioma da interface: "auto", "pt" ou "en".
/// UI language preference: `"auto"`, `"pt"`, or `"en"`.
#[serde(default = "auto")]
pub lang: String,
/// Idioma que a interface resolveu de fato ("pt" ou "en"), para as
/// notificações do sistema e a saída do terminal acompanharem a tela.
/// Effective language resolved by the UI (`"pt"` or `"en"`), so OS desktop
/// notifications and CLI messages match the active UI language.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub ui_lang: Option<String>,
/// Notificação do sistema quando uma tarefa longa termina.
/// Send a desktop notification when a long-running task finishes.
#[serde(default = "yes")]
pub notify: bool,
/// Mostrar mapas das fotos com GPS (baixa blocos do OpenStreetMap pela internet).
/// Show OpenStreetMap tiles for geotagged photos (fetches tiles over the network).
#[serde(default)]
pub maps: bool,
/// Procurar versão nova no Gitea ao abrir o app (uma consulta ao `latest.json` da release,
/// sem enviar nada além do que qualquer navegador envia).
/// Check Gitea Releases for updates on startup (fetches `latest.json` once
/// without sending any telemetry).
#[serde(default = "yes")]
pub updates: bool,
}
@@ -50,8 +50,8 @@ impl Default for Settings {
}
}
/// Pasta de configuração do usuário *real*, mesmo quando rodando como root
/// via sudo/pkexec (para o tema não "mudar" ao elevar privilégios).
/// Configuration directory of the *real* invoking user, even when running as root
/// via `sudo`/`pkexec` (so theme and language settings persist across elevation).
pub fn config_dir() -> PathBuf {
if let Ok(d) = std::env::var("GUARA_CONFIG_DIR") {
if !d.is_empty() {
@@ -111,8 +111,8 @@ pub fn load() -> Settings {
.unwrap_or_default()
}
/// Idioma efetivo para textos gerados fora da interface (notificações e CLI).
/// Usa o que a interface informou; sem isso, a preferência e o ambiente.
/// Effective language for strings generated outside the webview (OS notifications and CLI).
/// Uses the UI-resolved language when available, falling back to saved preference and `LANG`.
pub fn ui_lang() -> String {
let s = load();
if let Some(l) = s.ui_lang.as_deref() {
+30 -30
View File
@@ -1,19 +1,19 @@
//! Assinatura digital (Ed25519) de manifestos, relatórios e qualquer outro arquivo, e conferência
//! de uma recuperação inteira.
//! Ed25519 digital signatures for manifests, reports, and arbitrary files, plus full recovery
//! folder verification.
//!
//! A assinatura fica ao lado do arquivo, em `<arquivo>.assinatura.json`, e cobre o SHA-256 do
//! conteúdo, o tamanho, o nome, a hora e o assinante. Quem confere recalcula o hash do arquivo,
//! refaz a mensagem a partir dos campos do JSON e valida com a chave pública que vem nele. Isso
//! prova que o arquivo não mudou desde que foi assinado **por quem tem a chave**; para saber de
//! quem é a chave, compare a impressão digital (`fingerprint`) com a que o perito publicou ou
//! registrou fora da máquina, passando-a em `trusted_fingerprint`.
//! Signatures are stored alongside the file in `<file>.assinatura.json` and cover the content
//! SHA-256, byte size, filename, timestamp, and signer name. Verification recomputes the file
//! hash, reconstructs the canonical signed message, and checks the Ed25519 signature against the
//! embedded public key. To bind the signature to a specific examiner, compare `fingerprint`
//! against the examiner's published fingerprint via `trusted_fingerprint`.
//!
//! A hora é a do relógio da máquina de quem assinou: não é um carimbo de tempo de terceiros
//! (RFC 3161) e, sozinha, não prova *quando* a assinatura foi feita.
//!
//! A chave privada fica em `assinatura.key` na pasta de configuração (ou onde
//! `GUARA_SIGNING_KEY` apontar), em arquivo de texto com permissão 0600 nos sistemas Unix. Quem
//! precisa de mais proteção deve guardá-la num pendrive ou volume cifrado e apontar para lá.
//! Note: `signed_at_utc` comes from the signing machine's local clock, not an RFC 3161 trusted
//! timestamp authority.
//!
//! The private key is stored in `assinatura.key` inside the config directory (or at
//! `GUARA_SIGNING_KEY`) with `0600` permissions on Unix. For higher security, store the key on
//! an encrypted removable volume and point `GUARA_SIGNING_KEY` to it.
use crate::error::{Error, Result};
use ed25519_compact::{KeyPair, Noise, PublicKey, Seed, Signature};
@@ -41,7 +41,7 @@ fn unhex(s: &str) -> Option<Vec<u8>> {
.collect()
}
/// Impressão digital da chave pública: os 10 primeiros bytes do SHA-256, em grupos de 4 hex.
/// Public key fingerprint: first 10 bytes of SHA-256(pubkey), formatted as 5 groups of 4 hex chars.
pub fn fingerprint(public_key: &[u8]) -> String {
let h = Sha256::digest(public_key);
let h = hex(&h[..10]);
@@ -52,7 +52,7 @@ pub fn fingerprint(public_key: &[u8]) -> String {
.join(":")
}
/// Compara impressões digitais ignorando maiúsculas, `:` e espaços.
/// Compares two key fingerprints ignoring case, colons, hyphens, and whitespace.
fn same_fingerprint(a: &str, b: &str) -> bool {
let n = |s: &str| {
s.chars()
@@ -64,7 +64,7 @@ fn same_fingerprint(a: &str, b: &str) -> bool {
!a.is_empty() && a == b
}
/// Par de chaves de assinatura.
/// Ed25519 signing keypair.
pub struct SigningKey {
kp: KeyPair,
}
@@ -108,7 +108,7 @@ impl SigningKey {
})
}
/// Grava a chave privada (0600 no Unix) e, ao lado, a pública (`.pub`). Recusa sobrescrever.
/// Writes the private key (`0600` on Unix) and companion `.pub` file. Refuses to overwrite existing keys.
pub fn save(&self, path: &Path) -> Result<()> {
use std::io::Write;
if let Some(dir) = path.parent() {
@@ -148,7 +148,7 @@ fn pub_path(key: &Path) -> PathBuf {
key.with_extension("pub")
}
/// `GUARA_SIGNING_KEY` ou `assinatura.key` na pasta de configuração.
/// Resolves `GUARA_SIGNING_KEY` or `assinatura.key` in the configuration directory.
pub fn default_key_path() -> PathBuf {
match std::env::var("GUARA_SIGNING_KEY") {
Ok(p) if !p.is_empty() => PathBuf::from(p),
@@ -156,7 +156,7 @@ pub fn default_key_path() -> PathBuf {
}
}
/// Arquivo que acompanha `path` com a assinatura.
/// Companion `<path>.assinatura.json` sidecar path.
pub fn signature_path(path: &Path) -> PathBuf {
let mut n = path.as_os_str().to_os_string();
n.push(".assinatura.json");
@@ -190,7 +190,7 @@ fn message(f: &SignatureFile) -> Vec<u8> {
.into_bytes()
}
/// SHA-256 e tamanho de um arquivo, lido em blocos.
/// Computes the hex SHA-256 and byte length of a file.
pub fn hash_file(path: &Path) -> Result<(String, u64)> {
let mut f = std::fs::File::open(path)?;
let mut h = Sha256::new();
@@ -215,7 +215,7 @@ fn now_iso() -> String {
crate::audit::utc_iso(secs)
}
/// Assina `path` e grava `<path>.assinatura.json`. Devolve o caminho da assinatura.
/// Signs `path` and writes `<path>.assinatura.json`. Returns the signature file path.
pub fn sign_file(key: &SigningKey, path: &Path, signer: Option<&str>) -> Result<PathBuf> {
let (sha256, size) = hash_file(path)?;
let mut sig = SignatureFile {
@@ -242,17 +242,17 @@ pub fn sign_file(key: &SigningKey, path: &Path, signer: Option<&str>) -> Result<
Ok(out)
}
/// Resultado de conferir uma assinatura.
/// Result of verifying a detached `.assinatura.json` signature.
#[derive(Debug, Clone, Serialize)]
pub struct Check {
/// O arquivo é o mesmo e a assinatura vale para a chave que vem nela.
/// True if the file hash matches and the Ed25519 signature is valid for the embedded public key.
pub valid: bool,
pub file_matches: bool,
pub signature_valid: bool,
pub fingerprint: String,
pub signed_at: String,
pub signer: Option<String>,
/// `Some(true/false)` quando uma impressão digital de confiança foi informada.
/// `Some(true/false)` when a `trusted_fingerprint` was provided for comparison.
pub trusted: Option<bool>,
pub problems: Vec<String>,
}
@@ -320,8 +320,8 @@ pub fn verify_file(
})
}
/// Confere uma pasta de recuperação: a assinatura do manifesto (se houver) e o hash de cada
/// arquivo contra o que o manifesto registrou.
/// Verifies a recovery directory: checks the manifest signature (if present) and verifies every
/// recovered file's SHA-256 against the manifest.
#[derive(Debug, Clone, Serialize)]
pub struct RecoveryCheck {
pub manifest_signature: Option<Check>,
@@ -329,7 +329,7 @@ pub struct RecoveryCheck {
pub ok: usize,
pub missing: Vec<String>,
pub changed: Vec<String>,
/// `true` quando tudo confere (e, havendo assinatura, ela vale).
/// `true` when all recovered files match their manifest hashes (and the manifest signature is valid if present).
pub valid: bool,
}
@@ -353,7 +353,7 @@ pub fn verify_recovery(dir: &Path, trusted_fingerprint: Option<&str>) -> Result<
let (Some(rel), Some(want)) = (f["path"].as_str(), f["sha256"].as_str()) else {
continue;
};
// o manifesto é dado de entrada: não deixa sair da pasta
// manifest paths are untrusted input: reject path traversal outside the recovery folder
if rel.split('/').any(|c| c == "..") || Path::new(rel).is_absolute() {
changed.push(format!("{rel} (caminho fora da pasta)"));
continue;
@@ -380,8 +380,8 @@ pub fn verify_recovery(dir: &Path, trusted_fingerprint: Option<&str>) -> Result<
})
}
/// Assina `path` se existir uma chave configurada; nunca interrompe quem chamou. Registra na
/// trilha de auditoria.
/// Auto-signs `path` if a signing key exists; never aborts the caller on error. Logs to the
/// audit trail.
pub fn sign_if_configured(path: &Path) {
let kp = default_key_path();
if !kp.is_file() {
+22 -22
View File
@@ -1,13 +1,13 @@
//! Assinaturas de formatos e validação estrutural para o carving.
//! File format signatures and structural validators for file carving.
//!
//! Cada detector lê a estrutura real do arquivo (cabeçalhos, blocos, rodapés)
//! para determinar o tamanho exato e o nível de confiança, em vez de apenas
//! procurar bytes mágicos.
//! Each detector parses the internal container structure (headers, chunks, footers)
//! to determine the exact file size and confidence level rather than relying on
//! magic bytes alone.
use crate::source::{read_vec, Source};
use crate::types::{Category, Confidence};
/// Resultado de um detector: arquivo reconhecido a partir de `offset`.
/// Output of a format detector: a file recognized starting at `offset`.
#[derive(Debug, Clone)]
pub struct Hit {
pub len: u64,
@@ -37,8 +37,8 @@ pub fn category_for_ext(ext: &str) -> Category {
}
}
/// Verifica se o início do conteúdo é coerente com a extensão.
/// `None` = extensão sem assinatura conhecida (não dá para julgar).
/// Checks whether the leading bytes `head` match the expected magic for `ext`.
/// Returns `None` if `ext` has no known magic signature.
pub fn matches_ext(ext: &str, head: &[u8]) -> Option<bool> {
let ext = ext.to_ascii_lowercase();
let starts = |m: &[u8]| head.starts_with(m);
@@ -86,8 +86,8 @@ pub fn matches_ext(ext: &str, head: &[u8]) -> Option<bool> {
Some(r)
}
/// Tabela de primeiros bytes -> detectores candidatos.
/// O carving consulta `candidates(first_byte)` e chama `detect`.
/// Dispatch table mapping leading byte -> candidate format detectors.
/// The carver queries `candidates(first_byte)` and invokes `detect`.
pub fn might_start(b0: u8) -> bool {
crate::formats::might_start(b0)
|| matches!(
@@ -111,8 +111,8 @@ pub fn might_start(b0: u8) -> bool {
const MAX_HEADER: usize = 64 * 1024;
/// Tenta reconhecer um arquivo que começa em `off`.
/// `head` já contém os primeiros bytes lidos em `off` (pelo menos 16, se existirem).
/// Attempts to recognize a file starting at `off`.
/// `head` contains the leading bytes already read at `off` (at least 16 when available).
pub fn detect(src: &dyn Source, off: u64, head: &[u8], max_size: u64) -> Option<Hit> {
if head.len() < 12 {
return None;
@@ -204,7 +204,7 @@ fn jpeg(src: &dyn Source, off: u64, max: u64) -> Option<Hit> {
if !saw_sof {
return None;
}
// Dados entropy-coded: procurar EOI (FF D9) ignorando FF00 e RSTn.
// Entropy-coded scan data: scan for EOI (`FF D9`), skipping `FF 00` byte stuffing and `RSTn`.
let mut pos = off + start as u64;
let limit = off + max.min(512 * 1024 * 1024);
let mut buf = vec![0u8; 1 << 20];
@@ -230,7 +230,7 @@ fn jpeg(src: &dyn Source, off: u64, max: u64) -> Option<Hit> {
}
pos += n as u64;
}
// Sem EOI: imagem truncada/sobrescrita; ainda útil, mas incerta.
// Missing EOI: truncated or overwritten JPEG; still emit with Low confidence.
Some(Hit {
len: (pos - off).min(4 * 1024 * 1024),
ext: "jpg",
@@ -252,8 +252,8 @@ fn png(src: &dyn Source, off: u64, max: u64) -> Option<Hit> {
}
let len = u32::from_be_bytes([c[0], c[1], c[2], c[3]]) as u64;
let ty = &c[4..8];
// Bloco válido: 4 letras, 3ª maiúscula (bit reservado), tamanho plausível e dentro do
// disco. Texto ASCII solto parece "tamanho" de ~1,9 GB e não pode virar um PNG gigante.
// Valid PNG chunk: 4 ASCII letters with uppercase 3rd letter (reserved bit), plausible size within
// the source. Prevents random ASCII text after a truncated PNG from being read as a ~1.9 GB chunk.
let valid = ty.iter().all(|b| b.is_ascii_alphabetic())
&& ty[2].is_ascii_uppercase()
&& len <= 128 * 1024 * 1024
@@ -278,7 +278,7 @@ fn png(src: &dyn Source, off: u64, max: u64) -> Option<Hit> {
});
}
}
// Sem IEND: só vale como imagem truncada se já tem dados e é de tamanho razoável.
// Missing `IEND`: only keep as a truncated PNG if at least one `IDAT` was seen and size is reasonable.
if first || !saw_idat || p > 64 * 1024 * 1024 {
return None;
}
@@ -388,7 +388,7 @@ fn pdf(src: &dyn Source, off: u64, max: u64) -> Option<Hit> {
let mut i = 0;
while let Some(f) = find(&hay[i..], b"%%EOF") {
let end = base + (i + f) as u64 + 5;
// Continua se logo após o %%EOF vier uma atualização incremental.
// Continue scanning if `%%EOF` is followed by an incremental PDF update.
let tail = read_vec(src, end, 32).ok()?;
let rest: Vec<u8> = tail
.iter()
@@ -428,7 +428,7 @@ fn find(hay: &[u8], needle: &[u8]) -> Option<usize> {
hay.windows(needle.len()).position(|w| w == needle)
}
// ---------------------------------------------------------------- ZIP & derivados
// ---------------------------------------------------------------- ZIP & derivatives
fn zip(src: &dyn Source, off: u64, max: u64) -> Option<Hit> {
let limit = max.min(4 << 30);
@@ -508,7 +508,7 @@ fn classify_zip(src: &dyn Source, start: u64, names: &[String]) -> &'static str
} else if has("ppt/") {
"pptx"
} else if names.first().map(|n| n == "mimetype").unwrap_or(false) {
// O primeiro item (sem compressão) traz o tipo MIME logo após o cabeçalho.
// An uncompressed `mimetype` entry at the start identifies ODF/EPUB containers.
let head = read_vec(src, start, 256).unwrap_or_default();
let s = String::from_utf8_lossy(&head);
if s.contains("opendocument.text") {
@@ -574,7 +574,7 @@ fn isobmff(src: &dyn Source, off: u64, max: u64) -> Option<Hit> {
}
size = u64::from_be_bytes(b[8..16].try_into().unwrap());
} else if size == 0 {
// Até o fim do arquivo: tamanho desconhecido.
// Box extends to EOF: exact size unknown.
size = total - p;
}
if size < 8 || p + size > max {
@@ -705,7 +705,7 @@ pub fn crc32(data: &[u8]) -> u32 {
!crc
}
// ---------------------------------------------------------------- OLE2 (doc/xls/ppt legado)
// ---------------------------------------------------------------- OLE2 (legacy doc/xls/ppt)
fn ole(src: &dyn Source, off: u64, max: u64) -> Option<Hit> {
let hdr = read_vec(src, off, 512).ok()?;
@@ -782,7 +782,7 @@ fn mp3(src: &dyn Source, off: u64, max: u64) -> Option<Hit> {
if frames < 20 {
return None;
}
// Possível tag ID3v1 de 128 bytes ao final.
// Include optional trailing 128-byte ID3v1 tag.
if let Ok(t) = read_vec(src, off + p, 3) {
if t == b"TAG" {
p += 128;
+20 -20
View File
@@ -1,16 +1,16 @@
//! Acesso somente leitura a discos, partições e imagens.
//! Read-only positional access to disks, partitions, and forensic images.
//!
//! Nenhum tipo deste módulo expõe escrita: a origem nunca é modificada.
//! No type in this module exposes write operations: the source is never modified.
use std::fs::File;
use std::io;
use std::path::Path;
use std::sync::Arc;
/// Origem de bytes legível por posição absoluta.
/// Read-only byte source addressable by absolute 64-bit byte offset.
pub trait Source: Send + Sync {
fn size(&self) -> u64;
/// Lê até `buf.len()` bytes a partir de `offset`. Retorna 0 no fim.
/// Reads up to `buf.len()` bytes starting at `offset`. Returns `0` at EOF.
fn read_at(&self, offset: u64, buf: &mut [u8]) -> io::Result<usize>;
}
@@ -23,7 +23,7 @@ impl<T: Source + ?Sized> Source for Arc<T> {
}
}
/// Lê exatamente `buf.len()` bytes, ou falha com `UnexpectedEof`.
/// Reads exactly `buf.len()` bytes at `offset`, or fails with `UnexpectedEof`.
pub fn read_exact_at(src: &dyn Source, mut offset: u64, mut buf: &mut [u8]) -> io::Result<()> {
while !buf.is_empty() {
let n = src.read_at(offset, buf)?;
@@ -39,7 +39,7 @@ pub fn read_exact_at(src: &dyn Source, mut offset: u64, mut buf: &mut [u8]) -> i
Ok(())
}
/// Lê o máximo possível (pode devolver menos que `len` no fim da origem).
/// Reads up to `len` bytes into a `Vec<u8>` (may return fewer bytes near EOF).
pub fn read_vec(src: &dyn Source, offset: u64, len: usize) -> io::Result<Vec<u8>> {
let mut v = vec![0u8; len];
let mut got = 0;
@@ -54,11 +54,11 @@ pub fn read_vec(src: &dyn Source, offset: u64, len: usize) -> io::Result<Vec<u8>
Ok(v)
}
/// Arquivo de imagem ou dispositivo de blocos aberto somente para leitura.
/// Image file or raw block device opened strictly read-only.
pub struct FileSource {
file: File,
size: u64,
/// Dispositivos brutos (Windows/macOS) exigem leituras alinhadas ao setor.
/// Raw block devices on Windows/macOS require sector-aligned reads.
align: u64,
}
@@ -105,7 +105,7 @@ impl Source for FileSource {
if self.align <= 1 {
return self.raw_read(offset, &mut buf[..want]);
}
// Leitura alinhada para dispositivos brutos.
// Bounce buffer for sector-aligned reads on raw devices.
let a = self.align;
let start = offset / a * a;
let end = ((offset + want as u64).div_ceil(a) * a).min(self.size.div_ceil(a) * a);
@@ -128,7 +128,7 @@ impl Source for FileSource {
}
}
/// Janela [start, start+len) de outra origem (uma partição, por exemplo).
/// Sub-window `[start, start + len)` of another `Source` (e.g. a partition).
pub struct SubSource {
inner: Arc<dyn Source>,
start: u64,
@@ -155,7 +155,7 @@ impl Source for SubSource {
}
}
/// Origem em memória (testes e dados pequenos).
/// In-memory byte source (for tests and small buffers).
pub struct MemSource(pub Vec<u8>);
impl Source for MemSource {
@@ -195,7 +195,7 @@ fn open_readonly(path: &Path, _device: bool) -> io::Result<File> {
#[cfg(windows)]
fn open_readonly(path: &Path, _device: bool) -> io::Result<File> {
use std::os::windows::fs::OpenOptionsExt;
// FILE_SHARE_READ | FILE_SHARE_WRITE: não bloqueia outros processos.
// `FILE_SHARE_READ | FILE_SHARE_WRITE`: do not lock out other processes.
std::fs::OpenOptions::new()
.read(true)
.share_mode(0x1 | 0x2)
@@ -211,7 +211,7 @@ fn device_size(file: &File, _path: &Path) -> io::Result<u64> {
const DKIOCGETBLOCKCOUNT: libc::c_ulong = 0x4008_6419;
let fd = file.as_raw_fd();
let (mut bs, mut bc) = (0u32, 0u64);
// SAFETY: ioctls de leitura com ponteiros para variáveis locais válidas.
// SAFETY: read-only size query ioctls with pointers to valid stack variables.
let ok = unsafe {
libc::ioctl(fd, DKIOCGETBLOCKSIZE, &mut bs) == 0
&& libc::ioctl(fd, DKIOCGETBLOCKCOUNT, &mut bc) == 0
@@ -225,7 +225,7 @@ fn device_size(file: &File, _path: &Path) -> io::Result<u64> {
f.seek(SeekFrom::End(0))
}
/// Abre um dispositivo Windows (`\\.\PhysicalDriveN`) e obtém o tamanho.
/// Opens a Windows raw device (`\\.\PhysicalDriveN`) read-only and queries its byte size.
#[cfg(windows)]
pub fn open_windows_device(path: &Path) -> io::Result<FileSource> {
let file = open_readonly(path, true)?;
@@ -244,7 +244,7 @@ fn device_size(file: &File, _path: &Path) -> io::Result<u64> {
use windows_sys::Win32::System::IO::DeviceIoControl;
let mut info = GET_LENGTH_INFORMATION { Length: 0 };
let mut ret = 0u32;
// SAFETY: buffer de saída válido com o tamanho exato da estrutura.
// SAFETY: valid output buffer matching the exact struct layout.
let ok = unsafe {
DeviceIoControl(
file.as_raw_handle() as _,
@@ -263,7 +263,7 @@ fn device_size(file: &File, _path: &Path) -> io::Result<u64> {
Ok(info.Length as u64)
}
/// Abre um caminho (imagem ou dispositivo) como origem somente leitura.
/// Opens a path (raw disk, partition, or forensic/virtual disk image) as a read-only `Source`.
pub fn open_path(path: &Path) -> io::Result<Arc<dyn Source>> {
#[cfg(windows)]
{
@@ -273,7 +273,7 @@ pub fn open_path(path: &Path) -> io::Result<Arc<dyn Source>> {
}
}
let file = FileSource::open(path)?;
// Imagens forenses EWF (E01/Ex01) são abertas como o disco que contêm.
// Forensic containers (EWF, AFF4) and virtual disks (QCOW2, VMDK, VHD, VHDX, DMG) are opened transparently.
let mut head = [0u8; 96];
let got = file.read_at(0, &mut head).unwrap_or(0);
if got >= 4 && head.starts_with(b"PK") && crate::aff4::is_aff4(&head[..got]) {
@@ -295,9 +295,9 @@ pub fn open_path(path: &Path) -> io::Result<Arc<dyn Source>> {
Ok(Arc::new(FileSource::open(path)?))
}
/// Identidade do conteúdo de um disco: SHA-256 do tamanho + primeiro e último MiB
/// (tabela de partições, cabeçalhos e backup). Serve para reconhecer o mesmo disco
/// quando ele volta com outro nome (`/dev/sda` virando `/dev/sdb`).
/// Content fingerprint of a source disk: SHA-256 of its byte size plus the first and last MiB
/// (partition tables, headers, and backup GPT). Used to recognize the same physical drive
/// if it is unplugged and reattached under a different device name (`/dev/sda` -> `/dev/sdb`).
pub fn fingerprint(src: &dyn Source) -> Option<String> {
use sha2::{Digest, Sha256};
const MIB: u64 = 1024 * 1024;
+36 -36
View File
@@ -1,9 +1,9 @@
//! Leitor de bancos SQLite feito para arquivos recuperados.
//! Standalone SQLite B-tree reader tailored for recovered databases.
//!
//! Lê as páginas B-tree diretamente (sem a biblioteca do SQLite), então funciona
//! em bancos danificados ou truncados, para os quais a biblioteca oficial desiste.
//! Também procura **registros apagados** nas áreas livres das páginas, onde o
//! SQLite deixa o conteúdo antigo até que outro dado o sobrescreva.
//! Parses B-tree pages directly (without linking `libsqlite3`), so it works on
//! damaged or truncated database files that the official library rejects.
//! Also carves **deleted records** from page freeblocks and unallocated cell areas
//! where SQLite leaves unlinked row payloads until overwritten.
use crate::source::Source;
use serde::Serialize;
@@ -18,7 +18,7 @@ pub enum Cell {
Int(i64),
Real(f64),
Text(String),
/// Tamanho e os primeiros bytes (hex) para pré-visualizar; `mime` se reconhecido.
/// Byte length and leading hex preview of a BLOB, plus `mime` when recognized.
Blob {
len: usize,
head: String,
@@ -41,7 +41,7 @@ pub struct DbTable {
pub columns: Vec<DbColumn>,
pub without_rowid: bool,
pub virtual_table: bool,
/// Coluna `INTEGER PRIMARY KEY` (alias do rowid), se houver.
/// Index of the `INTEGER PRIMARY KEY` column (`rowid` alias), if any.
#[serde(skip)]
pub rowid_alias: Option<usize>,
}
@@ -83,7 +83,7 @@ pub struct Db<'a> {
type Res<T> = Result<T, String>;
// ------------------------------------------------------------------ utilidades
// ------------------------------------------------------------------ utilities
fn varint(b: &[u8]) -> Option<(i64, usize)> {
let mut v: u64 = 0;
@@ -158,7 +158,7 @@ fn decode_text(b: &[u8], enc: u32) -> String {
}
}
/// Decodifica um registro (cabeçalho de tipos + corpo).
/// Decodes a SQLite record payload (serial type header + column values).
fn decode_record(p: &[u8], enc: u32) -> Option<Vec<Cell>> {
let (hlen, n) = varint(p)?;
let hlen = hlen as usize;
@@ -201,7 +201,7 @@ fn decode_record(p: &[u8], enc: u32) -> Option<Vec<Cell>> {
Some(cells)
}
// ------------------------------------------------------------------ banco
// ------------------------------------------------------------------ database
impl<'a> Db<'a> {
pub fn open(src: &'a dyn Source) -> Res<Self> {
@@ -259,7 +259,7 @@ impl<'a> Db<'a> {
}
}
/// Lê o payload completo de uma célula (segue as páginas de overflow).
/// Reads the full payload of a B-tree cell, following overflow page chains when needed.
fn payload(
&self,
page: &[u8],
@@ -304,8 +304,8 @@ impl<'a> Db<'a> {
Some(out)
}
/// Percorre as folhas de uma árvore de tabela chamando `f(rowid, payload)`.
/// `f` devolve false para parar. Devolve os avisos de corrupção encontrados.
/// Walks all leaf pages of a table B-tree, invoking `f(rowid, payload)` for each cell.
/// `f` returns `false` to stop early. Returns any page/cell corruption warnings encountered.
fn walk_table(&self, root: u32, f: &mut dyn FnMut(i64, &[u8]) -> bool) -> Vec<String> {
let mut warnings = vec![];
let mut stack = vec![(root, 0u32)];
@@ -407,7 +407,7 @@ impl<'a> Db<'a> {
leaves
}
// ---------------------------------------------------------- esquema
// ---------------------------------------------------------- schema
pub fn schema(&self) -> DbSchema {
let mut s = DbSchema {
@@ -486,7 +486,7 @@ impl<'a> Db<'a> {
.ok_or_else(|| format!("tabela {name} não encontrada"))
}
/// Conta as linhas da tabela (sem decodificar o conteúdo).
/// Counts rows in a table B-tree without decoding cell payloads.
pub fn count_rows(&self, t: &DbTable) -> u64 {
if t.rootpage == 0 || t.virtual_table || t.without_rowid {
return 0;
@@ -500,7 +500,7 @@ impl<'a> Db<'a> {
n
}
// ---------------------------------------------------------- linhas
// ---------------------------------------------------------- rows
pub fn rows(&self, table: &str, offset: usize, limit: usize) -> Res<RowsPage> {
let t = self.table(table)?;
@@ -550,11 +550,11 @@ impl<'a> Db<'a> {
Ok(page)
}
// ---------------------------------------------------------- registros apagados
// ---------------------------------------------------------- deleted records
/// Procura registros apagados nas áreas livres das páginas da tabela.
/// São "prováveis": o SQLite não marca o que foi apagado, então validamos pela forma
/// do registro (mesmo número de colunas, tipos e textos coerentes).
/// Carves deleted records from unallocated gaps and freeblocks across table leaf pages.
/// Candidates are validated structurally against the table schema (column count,
/// column type affinities, and valid UTF-8 text).
pub fn deleted_rows(&self, table: &str) -> Res<RowsPage> {
let t = self.table(table)?;
let n = t.columns.len();
@@ -579,7 +579,7 @@ impl<'a> Db<'a> {
if content > ptr_end {
regions.push((ptr_end, content));
}
// cadeia de blocos livres
// page freeblock chain
let mut fb = be16(&pg, hb + 1).unwrap_or(0);
let mut guard = 0;
while fb != 0 && guard < 2000 {
@@ -620,10 +620,10 @@ impl<'a> Db<'a> {
Ok(out)
}
/// Tenta ler um registro apagado no início de `b`, em três formas:
/// (a) cabeçalho íntegro; (b) tamanho do cabeçalho perdido (o SQLite sobrescreve os 4
/// primeiros bytes do bloco livre); (c) como (b), mas sem o primeiro tipo (coluna que é
/// alias do rowid, sempre NULL).
/// Attempts to parse a deleted record at the start of `b` in three layouts:
/// (a) intact record header; (b) overwritten header-size varint (SQLite overwrites the first
/// 4 bytes of a freeblock with `next`/`size`); (c) like (b), but with the first serial type
/// (`INTEGER PRIMARY KEY` rowid alias, always `NULL`) also overwritten.
fn try_deleted(&self, b: &[u8], t: &DbTable) -> Option<(Vec<Cell>, usize)> {
let n = t.columns.len();
// (a)
@@ -641,8 +641,8 @@ impl<'a> Db<'a> {
None
}
/// Lê `ntypes` tipos seriais a partir de `b[start..]` (com um tipo inicial opcional já
/// conhecido) e o corpo logo depois do cabeçalho.
/// Reads `ntypes` serial type varints from `b[start..]` (with an optional synthesized leading
/// type) followed immediately by the value body.
fn record_with_types(
&self,
b: &[u8],
@@ -654,7 +654,7 @@ impl<'a> Db<'a> {
let n = t.columns.len();
let mut types: Vec<i64> = first.into_iter().collect();
let mut o = start;
let mut hdr_bytes = 1usize; // o próprio byte do tamanho do cabeçalho
let mut hdr_bytes = 1usize; // the header-length varint byte itself
for _ in 0..ntypes {
let (ty, m) = varint(b.get(o..)?)?;
types.push(ty);
@@ -665,7 +665,7 @@ impl<'a> Db<'a> {
hdr_bytes += 1;
}
if start == 1 {
// forma (a): o primeiro byte declara o tamanho do cabeçalho e precisa bater
// layout (a): the leading byte declares the total header size and must match
if *b.first()? as usize != o {
return None;
}
@@ -685,7 +685,7 @@ impl<'a> Db<'a> {
if body == 0 || o + body > b.len() {
return None;
}
// monta um registro canônico para reaproveitar o decodificador
// reassemble a canonical record buffer to reuse `decode_record`
let mut rec = vec![];
let mut hdr = vec![];
for &ty in &types {
@@ -716,7 +716,7 @@ impl<'a> Db<'a> {
}
}
/// O tipo serial é compatível com o tipo declarado da coluna (afinidade do SQLite)?
/// Checks whether a serial type is plausible for the declared column affinity.
fn type_ok(decl: &str, ty: i64) -> bool {
if ty == 10 || ty == 11 || ty < 0 {
return false;
@@ -757,7 +757,7 @@ fn put_varint(out: &mut Vec<u8>, v: i64) {
}
}
/// O SQLite guarda REAIS inteiros como inteiros; devolve-os como REAL quando a coluna é REAL.
/// SQLite stores whole-number REAL values as integers; promote them back to `Real` for `REAL` columns.
fn apply_affinity(cells: &mut [Cell], cols: &[DbColumn]) {
for (c, col) in cells.iter_mut().zip(cols) {
let d = col.decl_type.to_uppercase();
@@ -786,14 +786,14 @@ fn unquote(s: &str) -> String {
}
}
/// Extrai colunas, alias de rowid e WITHOUT ROWID de um `CREATE TABLE`.
/// Parses column definitions, `INTEGER PRIMARY KEY` rowid alias, and `WITHOUT ROWID` from a `CREATE TABLE` statement.
fn parse_create(sql: &str) -> (Vec<DbColumn>, Option<usize>, bool) {
let upper = sql.to_uppercase();
let without_rowid = upper.trim_end().ends_with("WITHOUT ROWID");
let Some(open) = sql.find('(') else {
return (vec![], None, without_rowid);
};
// fecha o parêntese correspondente
// find matching closing parenthesis
let mut depth = 0;
let mut close = sql.len();
let mut in_q: Option<char> = None;
@@ -814,7 +814,7 @@ fn parse_create(sql: &str) -> (Vec<DbColumn>, Option<usize>, bool) {
}
}
let body = &sql[open + 1..close.min(sql.len())];
// separa por vírgulas de nível 0
// split on top-level commas (depth 0)
let mut parts = vec![];
let (mut d, mut start, mut q) = (0, 0, None::<char>);
for (i, ch) in body.char_indices() {
@@ -894,7 +894,7 @@ fn parse_create(sql: &str) -> (Vec<DbColumn>, Option<usize>, bool) {
(cols, alias, without_rowid)
}
/// O arquivo começa com o cabeçalho de um banco SQLite?
/// Returns `true` if `b` starts with the 16-byte SQLite 3 file header magic.
pub fn is_sqlite(head: &[u8]) -> bool {
head.len() >= 16 && &head[0..16] == MAGIC
}
+6 -6
View File
@@ -1,6 +1,6 @@
//! Miniaturas para a grade de arquivos. Decodifica com redução na própria
//! decodificação (JPEG) para ser rápido mesmo com milhares de fotos e, se a
//! imagem estiver danificada, tenta a miniatura embutida no EXIF.
//! Thumbnail generator for the file explorer grid. Uses DCT-scaled JPEG decoding
//! for fast generation across thousands of photos, falling back to the embedded
//! EXIF thumbnail when the main image is damaged.
use crate::recover::read_found_at;
use crate::source::Source;
@@ -25,7 +25,7 @@ fn read_all(src: &dyn Source, f: &Found) -> Option<Vec<u8>> {
(!v.is_empty()).then_some(v)
}
/// Miniatura JPEG com o maior lado = `max` (None se não for imagem ou não decodificar).
/// Renders a JPEG thumbnail with longest edge `<= max` (`None` if not a decodable image).
pub fn thumbnail(src: &dyn Source, f: &Found, max: u32) -> Option<Vec<u8>> {
let data = read_all(src, f)?;
let img = match f.ext.to_ascii_lowercase().as_str() {
@@ -59,7 +59,7 @@ fn decode_jpeg(data: &[u8], max: u32) -> Option<RgbImage> {
jpeg_decoder::PixelFormat::CMYK32 => px
.chunks_exact(4)
.flat_map(|c| {
// Adobe CMYK vem invertido
// Adobe CMYK JPEGs store inverted channel values
let k = c[3] as u32;
[
(c[0] as u32 * k / 255) as u8,
@@ -76,7 +76,7 @@ fn decode_jpeg(data: &[u8], max: u32) -> Option<RgbImage> {
fn decode_other(data: &[u8], max: u32) -> Option<RgbImage> {
let dynimg = image::load_from_memory(data).ok()?;
let rgba = dynimg.to_rgba8();
// Achata a transparência sobre cinza claro.
// Composite alpha channel over a neutral light-gray background.
let mut rgb = RgbImage::new(rgba.width(), rgba.height());
for (x, y, p) in rgba.enumerate_pixels() {
let a = p[3] as u32;
+40 -40
View File
@@ -1,4 +1,4 @@
//! Tipos compartilhados entre núcleo, CLI e interface gráfica.
//! Core data types shared across `guara-core`, the CLI, and the GUI.
use serde::{Deserialize, Serialize};
@@ -69,12 +69,12 @@ impl Category {
}
}
/// Trecho contíguo de bytes na origem (offset absoluto).
/// Contiguous byte range `[ offset, offset + len )` on the source.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub struct Extent {
pub offset: u64,
pub len: u64,
/// Trecho esparso: lido como zeros.
/// Sparse hole (`offset == 0`): read as zero bytes.
#[serde(default, skip_serializing_if = "is_false")]
pub sparse: bool,
}
@@ -99,14 +99,14 @@ impl Extent {
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "kind", rename_all = "lowercase")]
pub enum DataLoc {
/// Dados dentro do próprio registro (NTFS residente).
/// Resident file payload stored inline inside the metadata record (e.g. resident NTFS `$DATA`).
Resident {
bytes: Vec<u8>,
},
Extents {
extents: Vec<Extent>,
},
/// Arquivo montado: trechos em memória (cabeçalhos refeitos) e trechos do disco.
/// Composite file made of in-memory slices (rebuilt headers/`moov`) and on-disk runs.
Parts {
parts: Vec<Part>,
},
@@ -119,11 +119,11 @@ pub enum Part {
#[serde(with = "b64")]
bytes: Vec<u8>,
},
/// Trecho contíguo da origem.
/// Contiguous byte run on the source.
Run { offset: u64, len: u64 },
/// Zeros (buracos de arquivos esparsos).
/// Zero-filled range (sparse file hole).
Zeros { len: u64 },
/// Extensão comprimida na origem (Btrfs): descompactada ao ler.
/// Compressed on-disk extent (Btrfs): decompressed on the fly when read.
Packed(Packed),
}
@@ -137,20 +137,20 @@ pub enum PackedCodec {
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub struct Packed {
/// Onde estão os bytes comprimidos na origem.
/// Source byte offset of the compressed extent.
pub offset: u64,
pub disk_len: u64,
/// Tamanho da extensão depois de descompactada.
/// Total uncompressed byte length of the extent.
pub ram_len: u64,
/// Bytes a pular no começo da extensão descompactada.
/// Leading uncompressed bytes to skip within the extent.
pub skip: u64,
/// Bytes úteis a partir de `skip`.
/// Number of uncompressed bytes to expose starting at `skip`.
pub len: u64,
pub codec: PackedCodec,
}
impl Packed {
/// Descompacta a extensão e devolve os `len` bytes úteis.
/// Decompresses the extent and returns the `[skip .. skip + len]` slice.
pub fn read(&self, src: &dyn crate::source::Source) -> std::io::Result<Vec<u8>> {
use std::io::Read;
let raw = crate::source::read_vec(src, self.offset, self.disk_len as usize)?;
@@ -283,12 +283,12 @@ impl Origin {
}
}
/// Um arquivo encontrado (apagado, perdido ou reconstruído por assinatura).
/// A discovered file (deleted metadata entry, orphaned inode, or signature-carved hit).
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Found {
pub id: u64,
pub name: String,
/// Pasta de origem, quando o sistema de arquivos a preserva.
/// Original parent directory path, when preserved by filesystem metadata.
pub dir: Option<String>,
pub ext: String,
pub category: Category,
@@ -296,31 +296,31 @@ pub struct Found {
pub origin: Origin,
pub confidence: Confidence,
pub data: DataLoc,
/// Segundos desde 1970-01-01 UTC.
/// Seconds since 1970-01-01 00:00:00 UTC.
pub modified: Option<i64>,
#[serde(default)]
pub created: Option<i64>,
#[serde(default)]
pub accessed: Option<i64>,
/// Data em que a foto foi tirada (EXIF; hora da câmera lida como UTC), s desde 1970.
/// EXIF capture timestamp (camera local time interpreted as UTC), in seconds since 1970.
#[serde(default)]
pub taken: Option<i64>,
#[serde(default)]
pub camera: Option<String>,
/// Largura × altura em pixels, quando conhecidas.
/// Image/video dimensions `(width, height)` in pixels, when known.
#[serde(default)]
pub px: Option<(u32, u32)>,
/// Há coordenadas GPS nos metadados.
/// Whether EXIF GPS coordinates are present.
#[serde(default)]
pub gps: bool,
/// Assinatura rápida do conteúdo (tamanho + início + fim) para achar duplicadas.
/// Fast content hash (size + head + tail) used for duplicate detection.
#[serde(default)]
pub sig: Option<u64>,
pub note: Option<String>,
}
impl Found {
/// Data mais representativa: quando foi tirada > modificada > criada.
/// Best representative timestamp: `taken > modified > created`.
pub fn when(&self) -> Option<i64> {
self.taken.or(self.modified).or(self.created)
}
@@ -362,11 +362,11 @@ pub enum FsKind {
Refs,
Bitlocker,
Luks,
/// FileVault 2 (Core Storage).
/// Apple FileVault 2 (Core Storage).
Corestorage,
/// Membro de volume LVM (physical volume).
/// Linux LVM2 Physical Volume (PV).
Lvm,
/// Membro de RAID por software (mdadm).
/// Linux `mdadm` software RAID member.
Raid,
Unknown,
}
@@ -394,7 +394,7 @@ impl FsKind {
}
}
/// Volume cifrado: só detectamos (v1), sem desbloquear.
/// Returns `true` if this kind represents an encrypted container.
pub fn encrypted(self) -> Option<&'static str> {
match self {
FsKind::Bitlocker => Some("BitLocker"),
@@ -404,7 +404,7 @@ impl FsKind {
}
}
/// Há leitor de metadados (arquivos apagados com nome) para este sistema.
/// Returns `true` if a dedicated filesystem metadata reader exists for this format.
pub fn has_metadata_scanner(self) -> bool {
matches!(
self,
@@ -429,10 +429,10 @@ pub struct PartInfo {
pub len: u64,
pub fs: FsKind,
pub label: Option<String>,
/// Encontrada por varredura, não consta da tabela de partições.
/// Discovered by scanning unallocated gaps rather than listed in the partition table.
pub lost: bool,
pub encrypted: Option<String>,
/// Descrição do tipo na tabela (ex.: "Microsoft basic data").
/// Partition type label from the partition table (e.g. `"Microsoft basic data"`).
pub table_type: Option<String>,
}
@@ -445,10 +445,10 @@ pub struct Gap {
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct DiskLayout {
pub size: u64,
/// "gpt", "mbr" ou "none".
/// Partition table scheme: `"gpt"`, `"mbr"`, or `"none"`.
pub table: String,
pub partitions: Vec<PartInfo>,
/// Espaço não particionado relevante (> 1 MiB).
/// Significant unpartitioned byte gaps (`> 1 MiB`).
pub gaps: Vec<Gap>,
pub warnings: Vec<String>,
}
@@ -456,37 +456,37 @@ pub struct DiskLayout {
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum ScanMode {
/// Só metadados dos sistemas de arquivos (segundos).
/// Fast scan: filesystem metadata readers only (completes in seconds).
Quick,
/// Metadados + carving por assinatura em todo espaço livre/desconhecido.
/// Deep scan: filesystem metadata plus signature carving across free/unallocated space.
Deep,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ScanOptions {
pub mode: ScanMode,
/// Inclui áreas ainda alocadas no carving (mais lento, acha cópias antigas).
/// Also carve currently allocated space (slower, finds older copies inside allocated blocks).
#[serde(default)]
pub include_allocated: bool,
/// Limita o carving a estas categorias (vazio = todas).
/// Restrict signature carving to these categories (empty = all categories).
#[serde(default)]
pub categories: Vec<Category>,
/// Tamanho máximo aceito para arquivos carved (bytes).
/// Maximum byte size accepted for a single carved file.
#[serde(default = "default_max_carve")]
pub max_carve_size: u64,
/// Membros extras (RAID/LVM) e segredos para volumes cifrados.
/// Extra disk members (RAID/LVM) and credentials for unlocking encrypted volumes.
#[serde(default)]
pub assembly: crate::assemble::Assembly,
/// Partições que não estão na tabela do disco (volumes lógicos, desbloqueados).
/// Virtual partitions not present in the physical partition table (LVM LVs, unlocked volumes).
#[serde(default)]
pub extra_parts: Vec<PartInfo>,
/// Partições que só contêm RAID/LVM remontado: a varredura entra pelos volumes virtuais.
/// Physical partitions that act solely as assembled RAID/LVM containers (skipped in favor of virtual volumes).
#[serde(default)]
pub covered: Vec<u64>,
/// O disco principal é só membro de RAID sem metadados: varre apenas os volumes virtuais.
/// Primary disk is part of a whole-disk RAID array: scan only the assembled virtual volumes.
#[serde(default)]
pub skip_primary: bool,
/// Vídeos íntegros da mesma câmera/drone: fornecem SPS/PPS para remontar MP4 sem moov.
/// Intact donor videos from the same camera/drone: supply SPS/PPS to rebuild headerless MP4s.
#[serde(default)]
pub references: Vec<String>,
}
+6 -6
View File
@@ -1,6 +1,6 @@
//! DMG (Apple UDIF): rodapé `koly`, tabelas `mish` do plist e chunks zerados, brutos, ADC,
//! zlib, bzip2 e LZFSE (`ULFO`). Imagens cifradas (`encrcdsa`) são recusadas com mensagem
//! clara.
//! DMG (Apple UDIF): parses the `koly` trailer, plist `mish` block tables, and zero-fill, raw,
//! ADC, zlib, bzip2, and LZFSE (`ULFO`) chunks. Encrypted DMG images (`encrcdsa`) are rejected
//! with a descriptive error.
use super::{bad, be32, be64, read_exact, read_vec, Backend};
use crate::source::{FileSource, Source};
@@ -21,7 +21,7 @@ pub(super) fn is_dmg(file: &FileSource) -> bool {
&& be32(&t, 8) == 512
}
/// Imagem cifrada pelo `hdiutil`: começa com `encrcdsa` ou `cdsaencr`.
/// Checks whether `head` starts with an `hdiutil` encrypted DMG magic (`encrcdsa` / `cdsaencr`).
pub(super) fn is_encrypted(head: &[u8]) -> bool {
head.starts_with(b"encrcdsa") || head.starts_with(b"cdsaencr")
}
@@ -65,7 +65,7 @@ fn base64(s: &str) -> Vec<u8> {
out
}
/// Descompacta ADC (Apple Data Compression).
/// Decompresses an Apple Data Compression (ADC) chunk.
fn adc(src: &[u8], max: usize) -> io::Result<Vec<u8>> {
let mut out: Vec<u8> = Vec::with_capacity(max);
let mut i = 0;
@@ -210,7 +210,7 @@ impl Backend for Dmg {
return Ok(n);
};
if voff < c.start {
// espaço que nenhuma tabela cobre
// unmapped gap between `mish` runs: zero-fill
let n = buf.len().min((c.start - voff) as usize);
buf[..n].fill(0);
return Ok(n);
+23 -23
View File
@@ -1,10 +1,10 @@
//! Imagens de disco virtual e contêineres: QCOW2, VMDK, VHD, VHDX e DMG (UDIF).
//! Virtual disk images and containers: QCOW2, VMDK, VHD, VHDX, and DMG (UDIF).
//!
//! Each format is opened read-only as a logical block device and plugs transparently into
//! the standard pipeline (partitions, filesystems, encryption) via [`Source`]. Differencing
//! disks (VHD/VHDX parent locators, QCOW2 backing files, VMDK parent chains) resolve reads
//! up the backing chain to the base image.
//!
//! Cada formato é aberto como o disco que contém, somente para leitura, e entra na cadeia
//! normal (partições, sistemas de arquivos, cifras) por meio de [`Source`]. Discos
//! diferenciais (VHD e VHDX com pai, QCOW2 com arquivo de apoio, VMDK com pai) seguem a
//! cadeia até o disco base. A identidade do pai (UUID, CID, data) não é conferida: se o pai foi
//! alterado depois do filho, os dados lidos refletem o pai atual.
mod dmg;
mod qcow2;
@@ -36,21 +36,21 @@ pub(crate) fn le64(b: &[u8], o: usize) -> u64 {
u64::from_le_bytes(b[o..o + 8].try_into().unwrap())
}
/// Leitura dentro de um único bloco do formato; devolve quantos bytes entregou (pode ser
/// menos que `buf.len()`, nunca zero antes do fim do disco).
/// Reads within a single format block/cluster; returns the number of bytes filled (may be
/// less than `buf.len()`, but never `0` before the logical end of disk).
pub(crate) trait Backend: Send + Sync {
fn size(&self) -> u64;
fn read_chunk(&self, voff: u64, buf: &mut [u8]) -> io::Result<usize>;
}
/// Disco virtual aberto de uma imagem de contêiner.
/// Opened virtual disk backed by a container image.
pub struct VDisk {
inner: Box<dyn Backend>,
format: &'static str,
}
impl VDisk {
/// Nome do formato (`QCOW2`, `VMDK`, `VHD`, `VHDX`, `DMG`).
/// Format label (`"QCOW2"`, `"VMDK"`, `"VHD"`, `"VHDX"`, `"DMG"`).
pub fn format(&self) -> &'static str {
self.format
}
@@ -88,12 +88,12 @@ pub(crate) fn io_err(e: io::Error, what: &str) -> Error {
Error::msg(format!("{what}: {e}"))
}
/// Cadeias de arquivos pai (VHD/VHDX diferenciais) não passam deste limite, o que também
/// corta ciclos.
/// Maximum parent/backing chain depth (prevents infinite loops on cyclic parent references).
///
const MAX_DEPTH: u32 = 16;
/// Formato de contêiner reconhecido pelos primeiros bytes (e, para VHD fixo e DMG, pelo
/// fim do arquivo). `None` quando é um disco bruto.
/// Identifies a virtual disk container format from its leading bytes (and trailing footer
/// for fixed VHD and DMG). Returns `None` for raw disk images.
pub fn detect(file: &FileSource, head: &[u8]) -> Option<&'static str> {
if head.starts_with(b"QFI\xfb") {
return Some("QCOW2");
@@ -113,7 +113,7 @@ pub fn detect(file: &FileSource, head: &[u8]) -> Option<&'static str> {
None
}
/// Abre `path` como disco virtual, ou `Ok(None)` se não for um contêiner conhecido.
/// Opens `path` as a virtual disk, or returns `Ok(None)` if it is not a recognized container.
pub fn open(path: &Path) -> Result<Option<VDisk>> {
open_depth(path, 0)
}
@@ -139,8 +139,8 @@ pub(crate) fn open_depth(path: &Path, depth: u32) -> Result<Option<VDisk>> {
Ok(Some(VDisk { inner, format }))
}
/// Abre o disco pai (VHD/VHDX diferencial, QCOW2 com apoio, VMDK com pai). Com `raw`, o
/// arquivo é tratado como disco bruto sem tentar reconhecer o formato.
/// Opens a parent/backing image at `depth`. When `raw` is `true`, treats the backing file
/// directly as a raw image without probing for a container header.
pub(crate) fn open_parent(
candidates: &[std::path::PathBuf],
depth: u32,
@@ -170,7 +170,7 @@ pub(crate) fn open_parent(
)))
}
/// Lê de um arquivo por posição e exige todos os bytes.
/// Positional read helper that fills all of `buf` or returns `UnexpectedEof`.
pub(crate) fn read_exact(f: &FileSource, off: u64, buf: &mut [u8]) -> io::Result<()> {
crate::source::read_exact_at(f, off, buf)
}
@@ -181,7 +181,7 @@ pub(crate) fn read_vec(f: &FileSource, off: u64, len: usize) -> io::Result<Vec<u
Ok(v)
}
/// Tabelas em memória têm teto: um cabeçalho adulterado não pode pedir gigabytes.
/// Upper bound on in-memory allocation tables so a corrupted header cannot trigger multi-GB allocations.
pub(crate) fn check_table(entries: u64, entry_size: u64, what: &str) -> io::Result<usize> {
const MAX: u64 = 512 * 1024 * 1024;
if entries.saturating_mul(entry_size) > MAX {
@@ -190,7 +190,7 @@ pub(crate) fn check_table(entries: u64, entry_size: u64, what: &str) -> io::Resu
Ok(entries as usize)
}
/// UTF-16 (big ou little endian) até o primeiro nulo.
/// Decodes UTF-16 (BE or LE) up to the first NUL code unit.
pub(crate) fn utf16(b: &[u8], big: bool) -> String {
let units: Vec<u16> = b
.chunks_exact(2)
@@ -200,8 +200,8 @@ pub(crate) fn utf16(b: &[u8], big: bool) -> String {
String::from_utf16_lossy(&units)
}
/// Caminhos onde procurar o disco pai: o caminho gravado (relativo ao arquivo filho, com `\`
/// virando `/`) e, como alternativa, só o nome do arquivo na mesma pasta.
/// Candidate paths for locating a parent/backing disk: relative to the child image's directory
/// (normalizing `\` to `/`), falling back to the basename in the same directory.
pub(crate) fn candidate_paths(dir: &Path, names: &[String]) -> Vec<std::path::PathBuf> {
let mut out: Vec<std::path::PathBuf> = vec![];
for n in names {
@@ -228,7 +228,7 @@ pub(crate) fn candidate_paths(dir: &Path, names: &[String]) -> Vec<std::path::Pa
out
}
/// Lê do disco pai (ou zeros, sem pai); o que o pai não cobre também é zero.
/// Reads from the parent disk (or fills zeros if there is no parent or `off` is past parent EOF).
pub(crate) fn read_parent(
parent: &Option<Box<dyn Source>>,
voff: u64,
+9 -9
View File
@@ -1,7 +1,7 @@
//! QCOW2 (QEMU), versões 2 e 3: tabelas L1/L2, clusters comprimidos (deflate e zstd),
//! clusters zerados e arquivo de apoio (backing file): o que o filho não alocou vem do pai.
//! Imagens cifradas, com arquivo de dados externo, com L2 estendido ou com apoio que não é um
//! arquivo local (`json:`, `nbd:`, URLs) são recusadas.
//! QCOW2 (QEMU) v2 and v3: L1/L2 tables, compressed clusters (deflate and zstd),
//! zero-flagged clusters, and backing files (unallocated child clusters fall through to parent).
//! Encrypted images, external data files, extended L2 subclusters, and non-local backing URIs
//! (`json:`, `nbd:`, URLs) are rejected.
use super::{
bad, be32, be64, candidate_paths, check_table, open_parent, read_exact, read_parent, read_vec,
@@ -22,7 +22,7 @@ pub(super) struct Qcow2 {
l1: Vec<u64>,
zstd: bool,
parent: Option<Box<dyn Source>>,
/// Último cluster comprimido decodificado: (deslocamento do cluster virtual, dados).
/// Single-entry cache of the last decompressed cluster: `(guest_cluster_offset, data)`.
cache: Mutex<Option<(u64, Vec<u8>)>>,
}
@@ -85,8 +85,8 @@ impl Qcow2 {
})
}
/// Abre o arquivo de apoio. O formato vem da extensão de cabeçalho `0xE2792ACA`; `raw`
/// (ou ausente em v2) é lido sem reconhecer formato, como o QEMU faz.
/// Opens the backing file. Its format comes from header extension `0xE2792ACA`; `"raw"`
/// makes the backing file open directly as raw bytes without probing.
fn open_backing(
file: &FileSource,
h: &[u8],
@@ -109,7 +109,7 @@ impl Qcow2 {
"o arquivo de apoio {name} não é um arquivo local; só caminhos de arquivo são suportados"
)));
}
// extensões de cabeçalho: depois do cabeçalho, alinhadas a 8 bytes
// header extensions follow the header, 8-byte aligned
let hlen = if version == 3 && h.len() >= 104 {
be32(h, 100) as u64
} else {
@@ -201,7 +201,7 @@ impl Backend for Qcow2 {
}
let off = entry & OFFSET_MASK;
if entry & 1 != 0 {
buf[..n].fill(0); // zerado de propósito: não consulta o pai
buf[..n].fill(0); // explicitly zeroed grain (`GTE == 1`): does not fall back to parent
} else if off == 0 {
read_parent(&self.parent, voff, &mut buf[..n])?;
} else {
+4 -4
View File
@@ -1,6 +1,6 @@
//! VHD (Microsoft Virtual PC e Hyper-V): discos fixos, dinâmicos e diferenciais. O disco
//! diferencial lê o pai (achado pelos localizadores do cabeçalho) nos setores que a camada
//! filha não preencheu.
//! VHD (Microsoft Virtual PC and Hyper-V): fixed, dynamic, and differencing disks.
//! Differencing disks resolve parent locators (`W2ku`/`W2ru`) and fall back to the parent
//! image for sectors not present in the child block bitmap.
use super::{
bad, be32, be64, candidate_paths, check_table, open_parent, read_exact, read_parent, read_vec,
@@ -42,7 +42,7 @@ fn footer_ok(f: &[u8]) -> bool {
!sum == be32(f, 64)
}
/// VHD fixo: disco bruto seguido de um rodapé de 512 bytes.
/// Fixed VHD: raw disk payload followed by a 512-byte `conectix` footer.
pub(super) fn is_fixed(file: &FileSource) -> bool {
let size = file.size();
if size < 2 * SECTOR {
+7 -7
View File
@@ -1,8 +1,8 @@
//! VHDX (Hyper-V): tabela de regiões, metadados, BAT com blocos de bitmap intercalados,
//! discos fixos, dinâmicos e diferenciais (pai achado pelo localizador de pai).
//! VHDX (Hyper-V): region table, metadata table, Block Allocation Table (BAT) with interleaved
//! sector bitmap blocks, and fixed, dynamic, and differencing disks.
//!
//! The VHDX transaction log is not replayed on disk (opening an image never writes to it).
//!
//! O log de transações não é reproduzido: abrir a imagem nunca a escreve. Se o disco foi
//! desligado no meio de uma escrita, os dados ainda no log não aparecem.
use super::{
bad, candidate_paths, check_table, le16, le32, le64, open_parent, read_exact, read_parent,
@@ -14,7 +14,7 @@ use std::path::Path;
const MB: u64 = 1 << 20;
/// GUID em texto para os 16 bytes gravados em disco (três primeiros campos little-endian).
/// Converts a hyphenated GUID string into on-disk mixed-endian 16-byte representation.
fn guid(s: &str) -> [u8; 16] {
let hex: Vec<u8> = s
.replace('-', "")
@@ -93,7 +93,7 @@ impl Vhdx {
}
let chunk_ratio = (1u64 << 23) * sector_size / block_size;
let payload = size.div_ceil(block_size);
// Com pai, cada trecho termina com a entrada do bitmap de setores, mesmo que o disco acabe antes.
// With a parent (`has_parent`), each chunk ratio group is followed by a sector bitmap BAT entry.
let want = if has_parent {
payload.div_ceil(chunk_ratio) * (chunk_ratio + 1)
} else {
@@ -166,7 +166,7 @@ impl Backend for Vhdx {
read_exact(&self.file, off + within, &mut buf[..n])?;
}
7 => {
// Parcialmente presente: o bitmap do trecho diz, setor a setor, onde está o dado.
// Partially present block: consult the chunk's sector bitmap sector by sector.
let chunk = block / self.chunk_ratio;
let bm_entry = self.bat(chunk * (self.chunk_ratio + 1) + self.chunk_ratio);
let ss = self.sector_size;
+14 -14
View File
@@ -1,7 +1,7 @@
//! VMDK (VMware, VirtualBox): extensões esparsas (inclusive `streamOptimized` com grãos
//! comprimidos) e descritores de texto com extensões `FLAT`, `SPARSE` e `ZERO`, como
//! `monolithicFlat` e `twoGbMaxExtentSparse`. Discos com pai (snapshots) leem o pai nos grãos
//! que o filho não alocou; o formato SEsparse é recusado.
//! VMDK (VMware, VirtualBox): sparse extents (including `streamOptimized` with deflate-compressed
//! grains) and text descriptors referencing `FLAT`, `SPARSE`, and `ZERO` extents such as
//! `monolithicFlat` and `twoGbMaxExtentSparse`. Snapshot/delta VMDKs fall back to their parent
//! for unallocated grains; `SEsparse` is rejected.
use super::{
bad, candidate_paths, check_table, le16, le32, le64, open_parent, read_exact, read_parent,
@@ -42,7 +42,7 @@ pub(super) struct Vmdk {
parent: Option<Box<dyn Source>>,
}
/// Valor de uma chave `chave=valor` do descritor, sem aspas.
/// Extracts the unquoted value of a `key=value` line from a VMDK text descriptor.
fn desc_value(descriptor: &str, key: &str) -> Option<String> {
descriptor.lines().find_map(|l| {
l.trim()
@@ -52,8 +52,8 @@ fn desc_value(descriptor: &str, key: &str) -> Option<String> {
})
}
/// Descritor (texto) de um arquivo VMDK: o próprio arquivo, se for descritor, ou o embutido
/// logo depois do cabeçalho de uma extensão esparsa.
/// Reads the VMDK text descriptor from `path` (either a standalone text file or embedded inside
/// a monolithic sparse header).
fn descriptor_of(path: &Path) -> Option<String> {
let f = FileSource::open(path).ok()?;
let head = read_vec(&f, 0, (f.size() as usize).min(24 * 512)).ok()?;
@@ -68,8 +68,8 @@ fn descriptor_of(path: &Path) -> Option<String> {
.then(|| String::from_utf8_lossy(&head).into_owned())
}
/// Como o descritor identifica o pai: pelo nome (`parentFileNameHint`) e/ou, no VirtualBox, só
/// pelo UUID (`ddb.uuid.parent`); `cid` é o `parentCID` que o pai precisa ter.
/// Parent reference parsed from a VMDK descriptor: `parentFileNameHint` and/or VirtualBox
/// `ddb.uuid.parent`, plus the expected `parentCID`.
struct ParentRef {
name: Option<String>,
uuid: Option<String>,
@@ -85,7 +85,7 @@ fn parent_ref(descriptor: &str) -> Option<ParentRef> {
(cid.is_some() || uuid.is_some()).then_some(ParentRef { name, uuid, cid })
}
/// Procura, na pasta do filho e nas duas acima, um VMDK cujo `ddb.uuid.image` seja `uuid`.
/// Searches the child directory (and up to two parent directories) for a VMDK whose `ddb.uuid.image` matches `uuid`.
fn find_by_uuid(dir: &Path, uuid: &str) -> Option<std::path::PathBuf> {
let mut d = Some(dir);
for _ in 0..3 {
@@ -169,8 +169,8 @@ impl Sparse {
))
}
/// Lê dentro de um grão. O `bool` é `false` quando o grão não está alocado neste arquivo
/// (o chamador completa com o pai); grão zerado de propósito devolve zeros.
/// Reads within a single grain. Returns `(bytes_read, allocated_or_zeroed)`: when the `bool` is
/// `false`, the grain is unallocated in this extent and the caller should read from the parent.
fn read(&self, off: u64, buf: &mut [u8]) -> io::Result<(usize, bool)> {
let g = off / self.grain;
let within = off % self.grain;
@@ -213,7 +213,7 @@ impl Sparse {
}
}
/// Extensões de um descritor de texto: `RW 2048 FLAT "disco-f001.vmdk" 0`.
/// Parses extent lines from a text descriptor (e.g. `RW 2048 FLAT "disk-f001.vmdk" 0`).
fn parse_descriptor(text: &str, dir: &Path) -> io::Result<Vec<Extent>> {
let mut out = vec![];
let mut start = 0u64;
@@ -290,7 +290,7 @@ impl Vmdk {
));
}
if let Some(cid) = &r.cid {
// o pai precisa ser o mesmo de quando o filho foi criado
// verify that the parent's `CID` matches the child's `parentCID`
if let Some(p) = found.iter().find(|p| p.is_file()) {
let have = descriptor_of(p).and_then(|d| desc_value(&d, "CID"));
if have
+6 -6
View File
@@ -1,8 +1,8 @@
//! Disco virtual: a origem real com trechos substituídos por volumes desbloqueados,
//! membros de RAID montados ou volumes lógicos (LVM) anexados depois do fim do disco.
//! Virtual disk address space: overlays a base `Source` with unlocked encrypted volumes,
//! assembled RAID arrays, or LVM logical volumes appended beyond the physical end of disk.
//!
//! Os deslocamentos continuam sendo "do disco": quem varre e quem recupera enxergam o mesmo
//! espaço de endereços, desde que a montagem seja refeita igual (a `Assembly` é determinística).
//! Byte offsets remain globally addressable: scanning and recovery operate on a single unified
//! address space as long as the deterministic `Assembly` is re-applied.
use crate::source::Source;
use std::io;
@@ -58,7 +58,7 @@ impl Source for Overlay {
let n = want.min((r.start + r.len - offset) as usize);
return r.src.read_at(offset - r.start, &mut buf[..n]);
}
// Antes da próxima região: lê da base só até ela.
// Offset precedes the next virtual region: read from the base source up to the region start.
let n = want.min((r.start - offset) as usize);
return self.read_base(offset, &mut buf[..n]);
}
@@ -69,7 +69,7 @@ impl Source for Overlay {
impl Overlay {
fn read_base(&self, offset: u64, buf: &mut [u8]) -> io::Result<usize> {
if offset >= self.base.size() {
// Lacuna entre a base e uma região anexada.
// Unmapped gap between the end of the base source and an appended virtual region.
buf.fill(0);
return Ok(buf.len());
}
+3 -3
View File
@@ -1,4 +1,4 @@
//! AFF4: imagens de referência da Evimetry (github.com/aff4/ReferenceImages) e ida e volta.
//! AFF4: Evimetry reference images (`github.com/aff4/ReferenceImages`) and write/read roundtrip.
use guara_core::aff4::{write_aff4, Aff4Options, Aff4Source};
use guara_core::source::{open_path, read_vec, MemSource, Source};
@@ -28,11 +28,11 @@ fn reads_reference_images_and_stored_hash_matches() {
Some(true),
"{name}: hash do fluxo"
);
// setor 0 legível pelo caminho genérico
// sector 0 is readable through the generic `source::open` entrypoint
let s = open_path(&p).unwrap();
let head = read_vec(s.as_ref(), 0, 4096).unwrap();
assert_eq!(head.len(), 4096);
// leitura em vãos do mapa devolve zeros
// reading unmapped gaps in a sparse AFF4 map returns zeros
let far = read_vec(s.as_ref(), 200 << 20, 512).unwrap();
assert_eq!(far.len(), 512);
eprintln!(
+11 -11
View File
@@ -1,4 +1,4 @@
//! APFS: imagem real do dfvfs (4 checkpoints) e exclusão simulada pela troca da raiz da árvore.
//! APFS: real `dfvfs` image (4 checkpoints) and simulated COW deletion by swapping the fs-tree root.
mod common;
use common::*;
@@ -69,7 +69,7 @@ fn quick_scan_is_clean_on_pristine_volume() {
);
}
// --- manipulação bruta de nós (independente do módulo) ---
// --- raw B-tree node helpers (independent of the production parser) ---
fn le16(b: &[u8], o: usize) -> usize {
u16::from_le_bytes([b[o], b[o + 1]]) as usize
@@ -144,7 +144,7 @@ fn file_removed_from_the_current_tree_survives_in_the_old_node() {
let mut d = std::fs::read(&p).unwrap();
let bs = 4096usize;
let blk = |d: &[u8], n: u64| d[n as usize * bs..(n as usize + 1) * bs].to_vec();
// Superbloco mais recente do contêiner.
// Locate the latest container superblock (`NXSB`).
let (desc_blocks, desc_base) = (le32(&d, 104) as u64, le64(&d, 112));
let mut nx = d[..bs].to_vec();
for i in 0..desc_blocks {
@@ -153,11 +153,11 @@ fn file_removed_from_the_current_tree_survives_in_the_old_node() {
nx = b;
}
}
// omap do contêiner -> volume
// container omap -> volume superblock (`APSB`)
let cont_omap = blk(&d, le64(&nx, 160));
let cont_tree = blk(&d, le64(&cont_omap, 48));
let (_, cont_entries) = read_node(&{
// omap usa chaves fixas (16/16): reaproveita o leitor variável convertendo a tabela
// omap uses fixed-size keys/values (16/16): convert its TOC to variable-size format for `read_var_node`
let mut c = cont_tree.clone();
fixed_to_var(&mut c);
c
@@ -187,7 +187,7 @@ fn file_removed_from_the_current_tree_survives_in_the_old_node() {
let root = blk(&d, root_paddr);
assert_eq!(le16(&root, 34), 0, "raiz deveria ser folha nesta imagem");
let (_, recs) = read_node(&root);
// inode do arquivo
// find target file inode number
let victim = b"passwords.txt";
let ino = recs
.iter()
@@ -216,12 +216,12 @@ fn file_removed_from_the_current_tree_survives_in_the_old_node() {
.collect();
assert!(kept.len() < recs.len());
let newnode = write_node(&root, &kept);
// bloco livre para a nova versão
// pick an unused block for the new COW root leaf
let free = (10..d.len() / bs)
.find(|&n| d[n * bs..(n + 1) * bs].iter().all(|&x| x == 0) && n != vol_tree_paddr as usize)
.expect("bloco livre");
d[free * bs..(free + 1) * bs].copy_from_slice(&newnode);
// aponta o mapa de objetos para a nova versão (mesmo xid), editando o valor da entrada
// repoint the volume omap entry to `free_blk` (keeping the same `xid`)
let toc_off = le16(&vt_orig, 40);
let key_base = 56 + toc_off + le16(&vt_orig, 42);
let _ = key_base;
@@ -236,7 +236,7 @@ fn file_removed_from_the_current_tree_survives_in_the_old_node() {
let dir = tempfile::tempdir().unwrap();
let p2 = dir.path().join("a.img");
std::fs::write(&p2, &d).unwrap();
// O arquivo sumiu da árvore atual...
// The file is gone from the current live tree...
let src = FileSource::open(&p2).unwrap();
assert!(!live_paths(&src, 0).iter().any(|f| f.0 == "passwords.txt"));
init();
@@ -275,7 +275,7 @@ fn file_removed_from_the_current_tree_survives_in_the_old_node() {
);
}
/// Converte a tabela de entradas fixas (4 bytes) de um nó de omap para o formato variável (8 bytes) em memória.
/// Converts a fixed-entry TOC (4 bytes/entry) of an omap node into variable-entry format (8 bytes/entry) in memory.
fn fixed_to_var(b: &mut Vec<u8>) {
let flags = le16(b, 32) as u16;
if flags & 4 == 0 {
@@ -302,7 +302,7 @@ fn fixed_to_var(b: &mut Vec<u8>) {
fn write_node_flagsless(old: &[u8], entries: &[Entry]) -> Vec<u8> {
let mut b = write_node(old, entries);
// write_node usa as flags de `old`
// `write_node` preserves the node flags from `old`
let fl = le16(&b, 32) as u16;
b[32..34].copy_from_slice(&fl.to_le_bytes());
b
+3 -3
View File
@@ -1,4 +1,4 @@
//! Trilha de auditoria: encadeamento por hash, detecção de adulteração e registro das ações do motor.
//! Audit trail: SHA-256 hash chaining, tamper detection, and engine action logging.
mod common;
use common::*;
@@ -28,7 +28,7 @@ fn entries_are_numbered_and_chained() {
assert!(e[0].time.ends_with('Z') && e[0].time.len() == 20);
let v = log.verify().unwrap();
assert!(v.ok && v.entries == 4 && v.head == e[3].hash);
// continua de onde parou, inclusive numa instância nova
// continues the chain across separate `Log` instances
let again = Log::at(log.path());
assert_eq!(again.append("mais", json!({})).unwrap().seq, 5);
assert!(again.verify().unwrap().ok);
@@ -74,7 +74,7 @@ fn removing_or_reordering_lines_is_detected() {
tamper(&log, |l| l.swap(1, 2));
assert_eq!(log.verify().unwrap().broken_at.unwrap().0, 2);
// trocar o hash por outro também não passa: o seguinte deixa de continuar
// recomputing a single tampered line's hash still breaks the next entry's `prev_hash` link
let (_d, log) = log_with(3);
tamper(&log, |l| {
let h = l[0].rsplit('"').nth(1).unwrap().to_string();
+4 -4
View File
@@ -1,5 +1,5 @@
//! BitLocker: imagens reais do conjunto de testes do cryptsetup (GitLab), com senhas conhecidas.
//! Baixe com `scripts/fetch-testdata.sh`; sem elas os testes são pulados.
//! BitLocker: real test images from the `cryptsetup` test suite (with known passwords/recovery keys).
//! Download with `scripts/fetch-testdata.sh`; tests are skipped if images are absent.
use guara_core::crypt::{bitlocker, Secret, UnlockError};
use guara_core::partition::detect_fs_in;
@@ -31,7 +31,7 @@ fn conf(d: &std::path::Path) -> BTreeMap<String, BTreeMap<String, String>> {
}
fn check_fs(src: &dyn Source, what: &str) {
// Imagens "encrypt-on-write" não têm sistema de arquivos reconhecível (UUID vazio no images.conf).
// Used-space-only ("encrypt-on-write") images in `images.conf` have an empty filesystem UUID.
if what.contains("-eow ") {
return;
}
@@ -59,7 +59,7 @@ fn opens_all_reference_images_with_password_and_recovery_key() {
let src: Arc<dyn Source> = Arc::new(FileSource::open(&path).unwrap());
let pw = c.get("PW").cloned().unwrap_or_default();
let rp = c.get("RP").cloned().unwrap_or_default();
// Proteção suspensa (chave limpa): abre sem segredo nenhum.
// Suspended protection (clear key present on disk): unlocks without any user secret.
if let Ok(u) = bitlocker::unlock(src.clone(), &[]) {
check_fs(u.source.as_ref(), &format!("{name} (chave limpa)"));
eprintln!("ok {name}: chave limpa");
+15 -15
View File
@@ -1,5 +1,5 @@
//! Btrfs: imagens criadas com mkfs.btrfs --rootdir; exclusão simulada como o COW faz
//! (nova versão da folha em outro bloco, a antiga continua no disco).
//! Btrfs: images created with `mkfs.btrfs --rootdir`; COW deletion simulated by writing a
//! new fs-tree leaf generation to a free metadata block while leaving the old leaf on disk.
mod common;
use common::*;
@@ -89,7 +89,7 @@ fn lists_the_live_tree_with_paths_and_sizes() {
assert!(listed.iter().any(|x| x.0 == "docs/sub" && x.2));
}
// --- manipulação bruta de folhas (independente do módulo) ---
// --- raw B-tree leaf manipulation helpers (independent of the production reader) ---
struct Leaf {
raw: Vec<u8>,
@@ -118,7 +118,7 @@ impl Leaf {
let c = crc32c(&self.raw[32..]);
self.raw[0..4].copy_from_slice(&c.to_le_bytes());
}
/// Nova versão da folha sem os itens que `drop` rejeita, num novo endereço lógico.
/// Rebuilds a B-tree leaf without items matching `drop`, at a new logical `bytenr` and generation.
fn without(&self, drop: impl Fn(u64, u8) -> bool, new_bytenr: u64) -> Leaf {
let mut kept: Vec<(usize, Vec<u8>)> = vec![];
for i in 0..self.nritems() {
@@ -152,7 +152,7 @@ fn superblock(img: &[u8]) -> &[u8] {
&img[0x10000..0x11000]
}
/// Mapa lógico -> físico dos chunks de sistema (suficiente para imagens pequenas de um dispositivo).
/// Logical -> physical chunk mapping from `sys_chunk_array` (sufficient for small single-device images).
fn chunk_map(img: &[u8]) -> Vec<(u64, u64, u64)> {
let sb = superblock(img);
let n = u32::from_le_bytes(sb[160..164].try_into().unwrap()) as usize;
@@ -193,7 +193,7 @@ fn deleted_file_survives_in_the_previous_version_of_the_leaf() {
let fl = Leaf {
raw: img[fs_leaf.0..fs_leaf.0 + nodesize].to_vec(),
};
// inode do arquivo apagado
// find the objectid (inode) of the target file
let victim = "relatorio.dat";
let mut ino = 0;
for k in 0..fl.nritems() {
@@ -207,7 +207,7 @@ fn deleted_file_survives_in_the_previous_version_of_the_leaf() {
}
}
assert!(ino > 256);
// extent de dados do arquivo (para liberar no extent tree)
// locate the file's data extent `(disk_bytenr, disk_num_bytes)` so we can free it in the extent tree
let mut data_bytenr = 0;
for k in 0..fl.nritems() {
let (obj, ty, _, _, _) = fl.item(k);
@@ -216,7 +216,7 @@ fn deleted_file_survives_in_the_previous_version_of_the_leaf() {
}
}
assert!(data_bytenr > 0);
// Novo bloco livre para a nova versão: primeiro bloco do metadata sem folha válida e todo zero.
// Pick an all-zero metadata block for the new COW leaf generation.
let free_slot = (0..img.len() / nodesize)
.map(|n| n * nodesize)
.find(|&o| {
@@ -228,11 +228,11 @@ fn deleted_file_survives_in_the_previous_version_of_the_leaf() {
&& o < fs_leaf.0 + (8 << 20)
})
.expect("bloco livre");
// logical = bytenr da folha antiga + (offset físico - offset físico antigo): mesmo chunk
// logical `bytenr` within the same metadata chunk
let new_bytenr = to_logical(free_slot);
let newleaf = fl.without(|o, _| o == ino, new_bytenr);
img[free_slot..free_slot + nodesize].copy_from_slice(&newleaf.raw);
// Aponta o ROOT_ITEM da árvore 5 para a nova versão (e sobe a geração), na folha da árvore raiz.
// Update tree 5 (`FS_TREE`) `ROOT_ITEM` in the root tree to point to the new leaf and bump generation.
let mut rl = Leaf {
raw: img[root_leaf.0..root_leaf.0 + nodesize].to_vec(),
};
@@ -245,7 +245,7 @@ fn deleted_file_survives_in_the_previous_version_of_the_leaf() {
}
rl.seal();
img[root_leaf.0..root_leaf.0 + nodesize].copy_from_slice(&rl.raw);
// Libera o extent de dados no extent tree (em outro endereço da mesma folha).
// Remove the file's data extent from the extent tree so its data blocks are marked free.
let el = Leaf {
raw: img[ext_leaf.0..ext_leaf.0 + nodesize].to_vec(),
};
@@ -290,7 +290,7 @@ fn deleted_file_survives_in_the_previous_version_of_the_leaf() {
let rp = PathBuf::from(&rep.recovered[0].path);
let got = std::fs::read(if rp.is_absolute() { rp } else { out.join(rp) }).unwrap();
assert!(got == i.files[0].1);
// Os arquivos que continuam existindo não aparecem como apagados.
// Files that are still live in the current tree must not be reported as deleted.
assert!(!c
.found
.iter()
@@ -300,7 +300,7 @@ fn deleted_file_survives_in_the_previous_version_of_the_leaf() {
#[test]
fn lzo_literals_and_a_match() {
// 20 literais + fim
// 20 literals + EOF marker
let mut s = vec![17 + 20];
s.extend((0..20u8).map(|i| b'a' + i));
s.extend_from_slice(&[0x11, 0, 0]);
@@ -308,7 +308,7 @@ fn lzo_literals_and_a_match() {
lzo1x_decompress(&s, 4096).unwrap(),
(0..20u8).map(|i| b'a' + i).collect::<Vec<_>>()
);
// "abcd" + M2 (dist 4, len 4) + fim
// "abcd" + M2 backreference (distance 4, length 4) + EOF marker
let s = vec![17 + 4, b'a', b'b', b'c', b'd', 0x6C, 0x00, 0x11, 0, 0];
assert_eq!(lzo1x_decompress(&s, 4096).unwrap(), b"abcdabcd");
}
@@ -334,7 +334,7 @@ fn packed_extents_decode_zlib_and_zstd() {
codec: PackedCodec::Zlib,
};
assert_eq!(p.read(&MemSource(disk)).unwrap(), &plain[1000..6000]);
// zstd, se o utilitário existir
// zstd-compressed extent (if the `zstd` CLI is installed)
if have("zstd") {
let dir = tempfile::tempdir().unwrap();
let f = dir.path().join("p.bin");
+2 -2
View File
@@ -37,7 +37,7 @@ impl Sink for Collect {
}
}
/// Isola as configurações/sessões automáticas dos testes numa pasta temporária.
/// Isolates `GUARA_CONFIG_DIR` (settings and autosaved sessions) inside a per-test temp directory.
pub fn init() {
static DIR: std::sync::OnceLock<tempfile::TempDir> = std::sync::OnceLock::new();
let d = DIR.get_or_init(|| tempfile::tempdir().unwrap());
@@ -69,7 +69,7 @@ pub fn sha256(b: &[u8]) -> String {
.collect()
}
/// Gera (uma vez) as imagens de teste; `None` se as ferramentas não existirem.
/// Builds (or reuses) the filesystem test fixtures once per test binary; returns `None` if `mkfs` tools are missing.
pub fn fixtures() -> Option<PathBuf> {
static ONCE: std::sync::OnceLock<Option<PathBuf>> = std::sync::OnceLock::new();
ONCE.get_or_init(fixtures_inner).clone()
+5 -5
View File
@@ -66,7 +66,7 @@ fn rebuilds_a_jpeg_split_around_another_one() {
let mut bb = b.clone();
pad(&mut bb);
disk.extend_from_slice(&bb);
disk.extend(noise(20 * 4096, 5)); // lixo
disk.extend(noise(20 * 4096, 5)); // non-JPEG filler block
let a2_off = disk.len() as u64;
let mut rest = a[split..].to_vec();
pad(&mut rest);
@@ -76,7 +76,7 @@ fn rebuilds_a_jpeg_split_around_another_one() {
let mut idx = ClusterIndex::new(disk.len() as u64);
idx.scan_chunk(0, &disk);
// O fragmento correto precisa estar marcado.
// The continuation cluster must be flagged as a JPEG entropy candidate.
assert!(idx.get(a2_off));
eprintln!("TRUE a2_off={a2_off}");
let pool = Pool {
@@ -97,7 +97,7 @@ fn rebuilds_a_jpeg_split_around_another_one() {
}
other => panic!("esperava Rebuilt, veio {other:?}"),
}
// O outro JPEG, íntegro, continua íntegro.
// The intact decoy JPEG remains a single contiguous extent.
let pool2 = Pool {
index: &idx,
used: &[],
@@ -124,7 +124,7 @@ fn rebuilds_three_fragments_among_many_decoys() {
decoys.push((disk.len() as u64, d.len() as u64));
disk.extend_from_slice(&d);
if k == 1 {
// segundo fragmento de A no meio
// second fragment of image A placed in the middle
let off2 = disk.len() as u64;
disk.extend_from_slice(&a[s1..s2]);
decoys.push((off2, (s2 - s1) as u64));
@@ -142,7 +142,7 @@ fn rebuilds_three_fragments_among_many_decoys() {
let used: Vec<Extent> = decoys
.iter()
.enumerate()
.filter(|(i, _)| *i != 2) // o fragmento do meio de A não é de outro arquivo
.filter(|(i, _)| *i != 2) // only the intact image B is marked as already claimed; A's middle fragment is unclaimed
.map(|(_, (o, l))| Extent::new(*o, *l))
.collect();
let pool = Pool {
+37 -37
View File
@@ -1,4 +1,4 @@
//! Discos com tabela de partições, espaço não particionado, partições perdidas e cifragem.
//! Partitioned disks (GPT/MBR), unpartitioned space, lost partitions, sessions, and disconnect handling.
mod common;
use common::*;
@@ -39,7 +39,7 @@ fn png(extra: usize) -> Vec<u8> {
v
}
/// Disco: [1 MiB vazio][partição FAT32 do fixture][8 MiB não particionado com um PNG]
/// Synthetic disk: `[1 MiB gap][FAT32 partition][8 MiB unpartitioned gap containing a PNG]`
#[test]
fn particao_mbr_e_espaco_nao_particionado() {
let Some(dir) = fixtures() else { return };
@@ -66,14 +66,14 @@ fn particao_mbr_e_espaco_nao_particionado() {
"gap: {:?}",
layout.gaps
);
// Metadados da partição (offsets corrigidos pela posição da partição)...
// Partition metadata findings (with byte offsets translated to disk-global offsets)...
let fat_found: Vec<_> = res
.found
.iter()
.filter(|f| f.origin == Origin::Fat)
.collect();
assert_eq!(fat_found.len(), 7);
// ... e o PNG no espaço não particionado.
// ...plus the carved PNG from the unpartitioned gap.
let carved: Vec<_> = res
.found
.iter()
@@ -117,11 +117,11 @@ fn particao_perdida_e_encontrada_sem_tabela() {
fn volume_cifrado_e_detectado_e_nao_varrido() {
let mut disk = vec![0u8; 8 * MIB as usize];
disk[..512].copy_from_slice(&mbr(&[(0x07, MIB, 4 * MIB), (0x83, 5 * MIB, 2 * MIB)]));
// BitLocker na partição 1, LUKS na 2.
// BitLocker signature in partition 1, LUKS signature in partition 2.
disk[MIB as usize + 3..MIB as usize + 11].copy_from_slice(b"-FVE-FS-");
let l = 5 * MIB as usize;
disk[l..l + 6].copy_from_slice(b"LUKS\xBA\xBE");
// Um PNG "dentro" do volume cifrado não deve ser tocado.
// A plaintext PNG planted inside the locked container range must not be carved while locked.
let p = png(10_000);
let at = (2 * MIB) as usize;
disk[at..at + p.len()].copy_from_slice(&p);
@@ -141,7 +141,7 @@ fn volume_cifrado_e_detectado_e_nao_varrido() {
#[test]
fn gpt_e_lido() {
// GPT mínimo com 1 partição de dados.
// Minimal GPT with 1 basic data partition.
let mut disk = vec![0u8; 8 * MIB as usize];
disk[..512].copy_from_slice(&mbr(&[(0xEE, 512, 8 * MIB - 512)]));
let mut h = vec![0u8; 512];
@@ -183,7 +183,7 @@ fn imagem_forense_e_identica_e_origem_intacta() {
assert!(rep.bad_ranges.is_empty());
assert_eq!(rep.sha256, sha256_file(&src_path));
assert_eq!(sha256_file(&dest), sha256_file(&src_path));
// Não sobrescreve arquivo existente.
// Never overwrite an existing file in the destination directory.
assert!(image::create_image(src.as_ref(), &dest, &|| false, &mut |_, _| {}).is_err());
}
@@ -193,7 +193,7 @@ fn pausa_e_retoma_encontra_o_mesmo() {
let path = dir.join("ext4.img");
let full = scan_path(&path, &ScanOptions::default());
let src = source::open_path(&path).unwrap();
// Cancela logo na 3ª consulta, retoma do checkpoint.
// Cancel on the 3rd progress callback, then resume from the saved checkpoint.
let n = std::sync::atomic::AtomicUsize::new(0);
let cancel = || n.fetch_add(1, std::sync::atomic::Ordering::Relaxed) == 2;
let mut sink = Collect::default();
@@ -233,7 +233,7 @@ fn pausa_e_retoma_encontra_o_mesmo() {
assert_eq!(a, b);
}
/// Streaming de pré-visualização: Range devolve exatamente os bytes do arquivo recuperado.
/// Preview streaming: HTTP `Range` slices return the exact bytes of the recovered file.
#[test]
fn file_slice_com_range_bate_com_o_arquivo_recuperado() {
init();
@@ -302,7 +302,7 @@ fn configuracoes_de_tema_persistem() {
settings::load().updates,
"a verificação de atualização vem ligada"
);
// arquivo de uma versão anterior, sem o campo novo
// legacy session JSON missing newly added fields
std::fs::write(
settings::config_dir().join("settings.json"),
r#"{"theme":"dark"}"#,
@@ -328,8 +328,8 @@ fn wait_done(eng: &service::Engine, job: &str) {
panic!("job não terminou");
}
/// Gera uma sessão no formato da v0.1.0 (sem os campos que vieram depois) a partir de uma
/// varredura real da imagem, com o alvo informado.
/// Strips post-v0.1.0 fields from a real scan session JSON to simulate a legacy v0.1.0 session file.
///
fn sessao_formato_v0_1_0(img: &std::path::Path, alvo: &str) -> tempfile::NamedTempFile {
init();
let eng = service::Engine::new();
@@ -358,7 +358,7 @@ fn sessao_formato_v0_1_0(img: &std::path::Path, alvo: &str) -> tempfile::NamedTe
tmp
}
/// Uma sessão gravada pela v0.1.0 continua abrindo nas versões novas.
/// Backward compatibility: a session saved by v0.1.0 still loads and recovers cleanly.
#[test]
fn sessao_da_v0_1_0_abre_e_funciona() {
init();
@@ -374,7 +374,7 @@ fn sessao_da_v0_1_0_abre_e_funciona() {
assert_eq!(st["source"], true);
let res = eng.results(&job, &service::Query::default()).unwrap();
assert_eq!(res["total"], 7);
// recuperar a partir da sessão antiga continua dando o mesmo conteúdo
// recovering from the legacy session produces the exact same file contents
let out = tempfile::tempdir().unwrap();
let rj = eng
.start_recover(
@@ -392,14 +392,14 @@ fn sessao_da_v0_1_0_abre_e_funciona() {
}
}
/// Origem indisponível não impede abrir a sessão; reapontar a origem religa tudo.
/// A missing source does not block opening a session; relinking the source restores full access.
#[test]
fn sessao_abre_sem_a_origem_e_pode_ser_reconectada() {
init();
let Some(dir) = fixtures() else { return };
let img = dir.join("ntfs.img");
let tmp = sessao_formato_v0_1_0(&img, "/dev/disco-que-nao-existe");
// sessão pausada (como as de uma varredura interrompida)
// paused session (as produced by an interrupted scan)
let mut v: serde_json::Value =
serde_json::from_slice(&std::fs::read(tmp.path()).unwrap()).unwrap();
v["done"] = serde_json::json!(false);
@@ -413,7 +413,7 @@ fn sessao_abre_sem_a_origem_e_pode_ser_reconectada() {
.as_str()
.unwrap()
.contains("não está disponível"));
// navegar e exportar funcionam
// browsing, filtering, CSV export, and HTML reporting work even while the source is offline
assert_eq!(
eng.results(&job, &service::Query::default()).unwrap()["total"],
7
@@ -424,7 +424,7 @@ fn sessao_abre_sem_a_origem_e_pode_ser_reconectada() {
.unwrap(),
7
);
// pré-visualização, retomada e recuperação avisam com clareza
// preview, resume, and recovery return a clear error while the source is offline
assert_eq!(eng.thumb(&job, 1, 128).err(), Some(503));
assert!(eng
.resume(&job)
@@ -442,7 +442,7 @@ fn sessao_abre_sem_a_origem_e_pode_ser_reconectada() {
)
.unwrap_err();
assert!(e.to_string().contains("não está disponível"), "{e}");
// reconecta
// relink the session to the relocated disk image
eng.relink(&job, img.to_str().unwrap(), false).unwrap();
let st = eng.status(&job).unwrap();
assert_eq!(st["source"], true);
@@ -458,7 +458,7 @@ fn sessao_abre_sem_a_origem_e_pode_ser_reconectada() {
.unwrap();
wait_done(&eng, &rj);
assert_eq!(hashes_in(out.path()).len(), 7);
// arquivo que não é sessão
// non-session file returns a clear error
std::fs::write(tmp.path(), b"nao e json").unwrap();
let e = eng.load_session(tmp.path().to_str().unwrap()).unwrap_err();
assert!(
@@ -467,7 +467,7 @@ fn sessao_abre_sem_a_origem_e_pode_ser_reconectada() {
);
}
// ------------------------------------------------------------------ desconexão e reconexão do disco
// ------------------------------------------------------------------ disk disconnect and reconnect
#[test]
fn reconexao_so_aceita_o_mesmo_disco_ou_um_clone() {
@@ -477,14 +477,14 @@ fn reconexao_so_aceita_o_mesmo_disco_ou_um_clone() {
let tmp = sessao_formato_v0_1_0(&img, img.to_str().unwrap());
let eng = service::Engine::new();
let job = eng.load_session(tmp.path().to_str().unwrap()).unwrap();
// sessão antiga não tem impressão digital: o relink aprende a do primeiro disco aceito
// legacy session had no fingerprint: `relink_session` records the fingerprint of the accepted disk
eng.relink(&job, img.to_str().unwrap(), false).unwrap();
let work = tempfile::tempdir().unwrap();
// clone idêntico (por exemplo uma imagem do disco): aceito
// identical clone (e.g. a raw disk image of the drive): accepted without `force`
let clone = work.path().join("clone.img");
std::fs::copy(&img, &clone).unwrap();
eng.relink(&job, clone.to_str().unwrap(), false).unwrap();
// mesmo tamanho, mas outro conteúdo inicial: recusado, a menos que force
// same byte size but different disk content: rejected unless `force` is true
let outro = work.path().join("outro.img");
let mut bytes = std::fs::read(&img).unwrap();
bytes[0..16].copy_from_slice(b"OUTRO DISCO....!");
@@ -494,7 +494,7 @@ fn reconexao_so_aceita_o_mesmo_disco_ou_um_clone() {
.unwrap_err();
assert!(e.to_string().contains("conteúdo inicial"), "{e}");
eng.relink(&job, outro.to_str().unwrap(), true).unwrap();
// tamanho diferente: recusado
// different byte size: rejected unless `force` is true
let menor = work.path().join("menor.img");
std::fs::write(&menor, &bytes[..bytes.len() / 2]).unwrap();
let e = eng
@@ -503,7 +503,7 @@ fn reconexao_so_aceita_o_mesmo_disco_ou_um_clone() {
assert!(e.to_string().contains("bytes"), "{e}");
}
/// Fonte que "some" a partir de certo ponto, como um disco puxado da porta USB.
/// Simulated `Source` that fails with `EIO` past `fail_from`, mimicking an unplugged USB drive.
struct Flaky {
inner: MemSource,
broken_from: u64,
@@ -541,8 +541,8 @@ fn varredura_pausa_quando_o_disco_some_e_retoma_do_ponto_certo() {
v
};
let (a, b) = (png(3000), png(4000));
disk[2 * mib..2 * mib + a.len()].copy_from_slice(&a); // antes da falha
disk[50 * mib..50 * mib + b.len()].copy_from_slice(&b); // depois da falha
disk[2 * mib..2 * mib + a.len()].copy_from_slice(&a); // PNG before the disconnect point
disk[50 * mib..50 * mib + b.len()].copy_from_slice(&b); // PNG after the disconnect point
let flaky = Flaky {
inner: MemSource(disk.clone()),
broken_from: 16 * mib as u64,
@@ -555,9 +555,9 @@ fn varredura_pausa_quando_o_disco_some_e_retoma_do_ponto_certo() {
};
assert!(cp.io_lost, "deveria marcar perda de E/S");
assert_eq!(sink.found.len(), 1, "só o PNG anterior à falha");
// o ponto de retomada não pula os blocos que falharam
// the saved checkpoint offset does not skip past the unreadable blocks
assert!(cp.ranges[0].next <= 16 * mib as u64 + 8 * mib as u64);
// disco volta: retoma e acha o resto, sem duplicar o primeiro
// once the drive is reconnected, resuming finds the second PNG without duplicating the first
let healthy = MemSource(disk);
let before = sink.found.clone();
let out = scan::run(&healthy, &opts, Some(cp), &before, &|| false, &mut sink);
@@ -577,7 +577,7 @@ fn varredura_salva_sessao_sozinha_e_ela_reabre() {
.start_scan(img.to_str().unwrap(), ScanOptions::default())
.unwrap();
wait_done(&eng, &job);
// o salvamento final roda numa thread própria
// wait briefly for the background autosave thread to flush the final snapshot
let mut item = None;
for _ in 0..200 {
let list = eng.sessions_list();
@@ -596,7 +596,7 @@ fn varredura_salva_sessao_sozinha_e_ela_reabre() {
assert_eq!(item["found"], 7);
let path = item["path"].as_str().unwrap().to_string();
assert!(std::path::Path::new(&path).exists());
// reabre como qualquer sessão
// reopening the autosaved session restores all findings
let job2 = eng.load_session(&path).unwrap();
let st = eng.status(&job2).unwrap();
assert_eq!(
@@ -607,13 +607,13 @@ fn varredura_salva_sessao_sozinha_e_ela_reabre() {
),
("done", 7, true)
);
// apagar
// deleting the autosaved session removes it from the list
eng.session_delete(&path).unwrap();
assert!(!std::path::Path::new(&path).exists());
assert!(eng.session_delete("/etc/passwd").is_err());
}
// ------------------------------------------------------------------ organização e espaço
// ------------------------------------------------------------------ output organization and free-space check
#[test]
fn recuperar_organiza_por_tipo_data_ou_tudo_junto() {
@@ -652,7 +652,7 @@ fn recuperar_organiza_por_tipo_data_ou_tudo_junto() {
});
assert!(achou, "{layout:?}: {:?}", walk_names(out.path()));
}
// por data: usa a data de modificação (ano/mês)
// organize by date: groups into `YYYY/YYYY-MM` by modification/capture date
let out = tempfile::tempdir().unwrap();
recover::recover_with(
src.as_ref(),
@@ -699,7 +699,7 @@ fn espaco_insuficiente_no_destino_e_avisado_antes_de_copiar() {
let Some(dir) = fixtures() else { return };
let img = dir.join("ntfs.img");
let tmp = sessao_formato_v0_1_0(&img, img.to_str().unwrap());
// sessão com um arquivo "enorme" (1 EB)
// synthetic session with a 1 EB file to trigger the insufficient destination space check
let mut v: serde_json::Value =
serde_json::from_slice(&std::fs::read(tmp.path()).unwrap()).unwrap();
v["found"][0]["size"] = serde_json::json!(1u64 << 60);
+2 -2
View File
@@ -1,4 +1,4 @@
//! Ponta a ponta: volume LUKS2 com ext4 dentro, arquivo apagado, senha informada ao Guará.
//! End-to-end encrypted recovery: LUKS2 container holding an ext4 volume with a deleted file.
mod common;
use common::*;
@@ -150,7 +150,7 @@ fn deleted_file_inside_luks2_is_recovered_with_password() {
.success());
let base: Arc<dyn Source> = Arc::new(FileSource::open(&img).unwrap());
// Sem senha: o volume segue fechado e a varredura não inventa arquivos.
// Without the password: the LUKS2 volume stays locked and no plaintext files are exposed.
let locked = assemble_source(base.clone(), &Assembly::default()).unwrap();
assert_eq!(locked.unlocked.len(), 0);
assert_eq!(locked.locked.len(), 1);
+4 -4
View File
@@ -1,4 +1,4 @@
//! Ponta a ponta: RAID 5 de 4 discos (um perdido) com ext4 dentro e um arquivo apagado.
//! End-to-end RAID recovery: degraded 4-disk RAID 5 (one missing disk) with ext4 and a deleted file.
mod common;
use common::*;
@@ -92,7 +92,7 @@ fn deleted_file_on_degraded_raid5() {
let logical = std::fs::read(&plain).unwrap();
// RAID 5 left-symmetric
let data_off = 2048u64; // setores
let data_off = 2048u64; // capacity in 512-byte sectors
let mut disks = vec![vec![0u8; data_off as usize * 512 + rows * CHUNK]; n];
for row in 0..rows {
let pd = n - 1 - row % n;
@@ -110,7 +110,7 @@ fn deleted_file_on_degraded_raid5() {
let mut paths = vec![];
for (i, d) in disks.iter_mut().enumerate() {
if i == 2 {
continue; // disco que morreu
continue; // simulate failed/missing disk 1
}
let sb = md_super(n as u32, i as u32, data_off, (rows * CHUNK / 512) as u64);
d[4096..8192].copy_from_slice(&sb);
@@ -230,7 +230,7 @@ fn raid5_without_metadata_is_detected_and_scanned() {
}
disks[pd][row * chunk..(row + 1) * chunk].copy_from_slice(&p);
}
let order = [1usize, 2, 0]; // fornecidos fora de ordem
let order = [1usize, 2, 0]; // member disks passed in scrambled order `[2, 0, 3, 1]`
let mut paths = vec![];
for (j, &i) in order.iter().enumerate() {
let p = dir.path().join(format!("m{j}.img"));
+2 -2
View File
@@ -20,7 +20,7 @@ fn reads_references_made_by_libewf() {
assert_eq!(s.size(), want.len() as u64, "{name}");
let got = read_vec(s.as_ref(), 0, want.len()).unwrap();
assert!(got == want, "{name}: conteúdo difere");
// leitura que cruza chunks e termina no fim
// cross-chunk read reaching EOF
let tail = read_vec(s.as_ref(), want.len() as u64 - 5000, 9000).unwrap();
assert_eq!(tail, &want[want.len() - 5000..], "{name}");
}
@@ -105,7 +105,7 @@ fn compressed_output_is_smaller_and_odd_sizes_work() {
assert_eq!(read_vec(r.as_ref(), 0, 100_352).unwrap(), data);
}
/// Gera uma imagem para validar com ferramentas externas (ewfverify). `GUARA_E01_OUT=/caminho/base`.
/// Optional helper to write an `.E01` for external `ewfverify` validation when `GUARA_E01_OUT` is set.
#[test]
#[ignore]
fn emit_for_external_verification() {
+5 -5
View File
@@ -1,4 +1,4 @@
//! exFAT: injeta um conjunto de entradas APAGADO num sistema criado por mkfs.exfat.
//! exFAT: injects a deleted directory entry set (`0x05`/`0x40`/`0x41`) into a `mkfs.exfat` image.
mod common;
use common::*;
@@ -30,7 +30,7 @@ fn exfat_apagado_e_recuperado_com_nome() {
let heap = u32::from_le_bytes(d[88..92].try_into().unwrap()) as usize * ss;
let root = u32::from_le_bytes(d[96..100].try_into().unwrap()) as usize;
let root_off = heap + (root - 2) * cs;
// Primeira entrada livre da raiz.
// Find the first unused 32-byte slot in the root directory cluster.
let mut slot = root_off;
while d[slot] != 0 {
slot += 32;
@@ -43,15 +43,15 @@ fn exfat_apagado_e_recuperado_com_nome() {
d[data_off..data_off + content.len()].copy_from_slice(&content);
let name = "relatório final.txt";
let units: Vec<u16> = name.encode_utf16().collect();
// 0x05 = arquivo apagado (0x85 sem o bit "em uso"), secundárias 0x40 e 0x41.
// `0x05` = deleted file entry (`0x85` with in-use bit cleared), followed by `0x40` stream and `0x41` name entries.
let n_name = units.len().div_ceil(15);
d[slot] = 0x05;
d[slot + 1] = (1 + n_name) as u8;
d[slot + 12..slot + 14].copy_from_slice(&0x6000u16.to_le_bytes()); // hora
d[slot + 12..slot + 14].copy_from_slice(&0x6000u16.to_le_bytes()); // timestamp
d[slot + 14..slot + 16].copy_from_slice(&((44u16 << 9) | (6 << 5) | 15).to_le_bytes()); // 2024-06-15
let st = slot + 32;
d[st] = 0x40;
d[st + 1] = 0x03; // alocação possível + sem cadeia na FAT
d[st + 1] = 0x03; // `NoFatChain` bit set (contiguous allocation)
d[st + 3] = units.len() as u8;
d[st + 20..st + 24].copy_from_slice(&first_cluster.to_le_bytes());
d[st + 24..st + 32].copy_from_slice(&(content.len() as u64).to_le_bytes());
+8 -8
View File
@@ -1,4 +1,4 @@
//! ext4: arquivos apagados com nome, a partir de imagens criadas com mke2fs/debugfs.
//! ext2/3/4: deleted file recovery (with original filenames) on images built with `mke2fs`/`debugfs`.
mod common;
use common::*;
@@ -152,7 +152,7 @@ fn deleted_by_debugfs_keeps_extents_and_name() {
);
assert!(recovered(&i.img, f) == *data, "{name}");
}
// O arquivo que não foi apagado não aparece.
// Live files that were not deleted must not be listed.
assert!(!c
.found
.iter()
@@ -161,7 +161,7 @@ fn deleted_by_debugfs_keeps_extents_and_name() {
#[test]
fn truncated_inode_recovered_by_remnant_extents() {
// Simula o que o kernel faz ao apagar: tamanho zerado e contagem de extents zerada.
// Simulate Linux kernel `ext4_evict_inode`: zero `i_size` and `eh_entries` while leaving extent records.
let Some(i) = build(&["-t", "ext4"]) else {
return;
};
@@ -181,7 +181,7 @@ fn truncated_inode_recovered_by_remnant_extents() {
.find(|f| f.name == "contrato.dat")
.expect("contrato.dat");
assert_eq!(f.origin, Origin::Ext);
// Tamanho exato é perdido: recupera todos os blocos (9000 bytes arredondados para 3 blocos de 4K).
// Exact byte size was zeroed on deletion: recovers all 3 allocated 4 KiB blocks (12,288 bytes).
assert_eq!(f.size, 12288);
let got = recovered(&i.img, f);
assert_eq!(&got[..9000], &i.files[1].1[..]);
@@ -195,7 +195,7 @@ fn journal_copy_restores_exact_size() {
let Some(i) = build(&["-t", "ext4", "-O", "has_journal"]) else {
return;
};
// Bloco da tabela de inodes que contém o inode do arquivo.
// Locate the inode table block and byte offset for ` target ` via `debugfs imap`.
let ino = inode_of(&i.img, "/fotos/viagem.dat");
let o = debugfs(&i.img, &["stats"]);
let _ = o;
@@ -234,7 +234,7 @@ fn journal_copy_restores_exact_size() {
eprintln!("SKIP: debugfs sem suporte a escrita no diário: {j}");
return;
}
// Exclui e zera como o kernel faria.
// Delete the file and wipe its inode table slot (leaving only the journal copy).
debugfs(
&i.img,
&[
@@ -260,8 +260,8 @@ fn journal_copy_restores_exact_size() {
#[test]
fn ext2_blockmap_deleted_is_not_invented() {
// ext2 zera os ponteiros ao apagar; sem diário não há o que recuperar por metadados,
// mas a varredura não pode falhar nem listar o arquivo que continua existindo.
// Linux ext2 zeroes block pointers on unlink; without a journal, metadata recovery finds nothing,
// but the scan must succeed cleanly without reporting live files as deleted.
let Some(i) = build(&["-t", "ext2"]) else {
return;
};
+10 -10
View File
@@ -1,11 +1,11 @@
//! Saúde do disco, notificações, configurações e retomada da recuperação.
//! Disk health (SMART), notifications, user settings, and resumable recovery journal tests.
mod common;
use common::*;
use guara_core::*;
use serde_json::json;
// ------------------------------------------------------------------ saúde (SMART)
// ------------------------------------------------------------------ disk health (SMART)
#[test]
fn smart_reprovado_e_setores_ruins_viram_alerta_serio() {
@@ -119,12 +119,12 @@ fn imagem_de_disco_nao_tem_smart() {
assert_eq!(h.status, "unknown");
}
// ------------------------------------------------------------------ configurações
// ------------------------------------------------------------------ settings
#[test]
fn configuracoes_antigas_ganham_os_valores_padrao_novos() {
init();
// arquivo gravado pela v0.3: só tinha o tema
// legacy v0.3 settings file containing only `theme`: newer fields use defaults
std::fs::create_dir_all(settings::config_dir()).unwrap();
std::fs::write(
settings::config_dir().join("settings.json"),
@@ -158,7 +158,7 @@ fn configuracoes_antigas_ganham_os_valores_padrao_novos() {
);
}
// ------------------------------------------------------------------ notificações
// ------------------------------------------------------------------ notifications
#[cfg(target_os = "linux")]
#[test]
@@ -195,7 +195,7 @@ fn notificacao_chama_o_notify_send() {
);
}
// ------------------------------------------------------------------ retomar recuperação
// ------------------------------------------------------------------ resumable recovery
#[test]
fn recuperacao_interrompida_retoma_pulando_o_que_ja_foi_conferido() {
@@ -214,7 +214,7 @@ fn recuperacao_interrompida_retoma_pulando_o_que_ja_foi_conferido() {
let out = tempfile::tempdir().unwrap();
let key = "disco-de-teste";
// 1ª execução: "cai a energia" depois de 3 arquivos
// 1st run: abort after writing 3 of the 7 files
let n = std::sync::atomic::AtomicUsize::new(0);
let cancel = || n.fetch_add(1, std::sync::atomic::Ordering::Relaxed) >= 3;
let r1 = recover::recover_resumable(
@@ -233,7 +233,7 @@ fn recuperacao_interrompida_retoma_pulando_o_que_ja_foi_conferido() {
let journal = std::fs::read_to_string(out.path().join(recover::JOURNAL)).unwrap();
assert_eq!(journal.lines().count(), 3);
// 2ª execução: retoma, pula os 3 e faz os outros 4
// 2nd run: resume skips the 3 verified files and writes the remaining 4
let r2 = recover::recover_resumable(
src.as_ref(),
&files,
@@ -259,7 +259,7 @@ fn recuperacao_interrompida_retoma_pulando_o_que_ja_foi_conferido() {
7
);
// um arquivo é danificado entre as execuções: é refeito, os outros 6 são pulados
// corrupt one recovered file on disk: resume re-recovers that file and skips the other 6
let victim = out.path().join(&r2.recovered[0].path);
std::fs::write(&victim, b"lixo").unwrap();
let r3 = recover::recover_resumable(
@@ -279,7 +279,7 @@ fn recuperacao_interrompida_retoma_pulando_o_que_ja_foi_conferido() {
"o arquivo danificado deveria ter sido refeito"
);
// outro disco (outra chave) não reaproveita o diário
// a different `source_key` does not reuse the previous disk's recovery journal
let r4 = recover::recover_resumable(
src.as_ref(),
&files,
+1 -1
View File
@@ -1,4 +1,4 @@
//! FileVault 2 (Core Storage): imagem de referência do cryptsetup (senha heslo123).
//! FileVault 2 (Core Storage): `cryptsetup` reference image (password `heslo123`).
use guara_core::crypt::{filevault, Secret, UnlockError};
use guara_core::hashing::MultiHasher;
+23 -23
View File
@@ -1,5 +1,5 @@
//! Recursos periciais: hashes, metadados (EXIF/PDF/Office/MP4), miniaturas,
//! busca de texto, exportações e navegação (facetas/duplicadas).
//! Forensic features: multi-hash, metadata extraction (EXIF/PDF/Office/MP4), thumbnails,
//! full-disk text search, CSV/HTML exports, and explorer facets/deduplication.
use guara_core::explorer::{self, Query};
use guara_core::hashing::hash_bytes;
@@ -79,7 +79,7 @@ fn rat(p: &[(u32, u32)]) -> Vec<u8> {
}
fn jpeg_with_exif(orientation: u16, w: usize, h: usize) -> Vec<u8> {
// pixels: gradiente
// gradient pixel pattern
let mut px = Vec::with_capacity(w * h * 3);
for y in 0..h {
for x in 0..w {
@@ -143,7 +143,7 @@ fn exif_camera_data_e_gps() {
let m = meta::extract(&MemSource(vec![]), &f);
assert_eq!(m.camera.as_deref(), Some("Canon EOS 80D"));
assert_eq!(m.px, Some((64, 48)));
// 2023-07-14 18:22:05 lido como UTC
// `2023:07:14 18:22:05` interpreted as UTC
assert_eq!(m.taken, Some(1_689_358_925));
let (lat, lon) = m.gps.unwrap();
assert!((lat + 23.5550).abs() < 0.001, "{lat}");
@@ -157,14 +157,14 @@ fn exif_camera_data_e_gps() {
#[test]
fn miniatura_respeita_orientacao_e_tamanho() {
let src = MemSource(vec![]);
// 64x48 com orientação 6 (girar 90 graus) => retrato 48x64, reduzido para caber em 32
// 64x48 with EXIF orientation 6 (90° CW) -> portrait 48x64, scaled down to fit within 32px
let f = resident("foto.jpg", jpeg_with_exif(6, 64, 48));
let t = thumb::thumbnail(&src, &f, 32).expect("miniatura");
let mut dec = jpeg_decoder::Decoder::new(&t[..]);
dec.decode().unwrap();
let i = dec.info().unwrap();
assert_eq!((i.width, i.height), (24, 32));
// PNG também
// PNG thumbnail generation
let mut img = ::image::RgbImage::new(100, 50);
for p in img.pixels_mut() {
*p = ::image::Rgb([200, 30, 30]);
@@ -179,7 +179,7 @@ fn miniatura_respeita_orientacao_e_tamanho() {
(dec.info().unwrap().width, dec.info().unwrap().height),
(40, 20)
);
// lixo não vira miniatura
// random bytes return `None`
assert!(thumb::thumbnail(&src, &resident("x.jpg", vec![1, 2, 3, 4, 5]), 32).is_none());
}
@@ -325,7 +325,7 @@ fn metadados_de_mp4() {
assert_eq!(get("Dimensões").as_deref(), Some("1920 × 1080 px"));
}
// ------------------------------------------------------------------ busca de texto
// ------------------------------------------------------------------ text search
#[test]
fn busca_acha_utf8_utf16_e_na_fronteira_dos_blocos() {
@@ -338,7 +338,7 @@ fn busca_acha_utf8_utf16_e_na_fronteira_dos_blocos() {
.flat_map(|c| c.to_le_bytes())
.collect();
put(&mut disk, 3 * mb + 6, &u16s);
// exatamente cruzando o limite do primeiro bloco de 8 MiB
// string placed right across the 8 MiB chunk boundary
put(&mut disk, 8 * mb - 4, b"SEGREDO-123");
let opts = search::SearchOpts {
terms: vec!["segredo-123".into()],
@@ -364,7 +364,7 @@ fn busca_acha_utf8_utf16_e_na_fronteira_dos_blocos() {
assert_eq!(hits[1].offset, 3 * mb as u64 + 6);
assert_eq!(hits[1].encoding, "UTF-16");
assert_eq!(hits[2].offset, 8 * mb as u64 - 4);
// sem ignorar maiúsculas só acha a grafia exata
// case-sensitive mode matches only exact casing
let opts = search::SearchOpts {
terms: vec!["Segredo-123".into()],
ignore_case: false,
@@ -381,7 +381,7 @@ fn busca_acha_utf8_utf16_e_na_fronteira_dos_blocos() {
assert_eq!(n, 1);
}
// ------------------------------------------------------------------ exportações
// ------------------------------------------------------------------ exports
#[test]
fn csv_escapa_e_neutraliza_formulas() {
@@ -431,7 +431,7 @@ fn relatorio_html_escapa_conteudo_e_traz_o_caso() {
assert!(html.contains("somente para leitura"));
}
// ------------------------------------------------------------------ navegação
// ------------------------------------------------------------------ explorer facets & sorting
#[test]
fn facetas_filtros_e_duplicadas() {
@@ -473,7 +473,7 @@ fn facetas_filtros_e_duplicadas() {
Some("Canon"),
Some(8),
),
mk(3, "c.jpg", Some("Viagens"), Some(jul23), None, Some(7)), // duplicada de a.jpg
mk(3, "c.jpg", Some("Viagens"), Some(jul23), None, Some(7)), // exact content duplicate of `a.jpg`
mk(4, "d.jpg", None, None, None, None),
];
let q = |json: &str| -> Query { serde_json::from_str(json).unwrap() };
@@ -485,7 +485,7 @@ fn facetas_filtros_e_duplicadas() {
assert_eq!(f["undated"], 1);
assert_eq!(f["carved"], 1);
assert_eq!(f["duplicates"], 1);
assert_eq!(f["small"], 4); // todos têm 10 bytes
assert_eq!(f["small"], 4); // all three files are 10 bytes long
assert_eq!(q(r#"{"min_size":30720}"#).select(&found).len(), 0);
assert_eq!(f["cameras"][0]["name"], "Canon");
let dcim = f["folders"]
@@ -507,8 +507,8 @@ fn facetas_filtros_e_duplicadas() {
assert_eq!(q(r#"{"query":"viagens"}"#).select(&found).len(), 1);
let mut items = q("{}").select(&found);
explorer::sort_items(&mut items, Some("date"));
assert_eq!(items[0].id, 2); // mais recente primeiro
assert_eq!(items.last().unwrap().id, 4); // sem data por último
assert_eq!(items[0].id, 2); // newest first
assert_eq!(items.last().unwrap().id, 4); // undated files sort last
}
// ------------------------------------------------------------------ carving
@@ -521,8 +521,8 @@ fn png_chunk(ty: &[u8; 4], data: &[u8]) -> Vec<u8> {
c
}
/// Regressão: texto ASCII depois de um PNG truncado vira "tamanho" de ~1,9 GB;
/// o carving não pode inventar um PNG gigante.
/// Regression test: ASCII text immediately following a truncated PNG looks like a ~1.9 GB chunk
/// length; the carver must reject the bogus chunk and never emit a multi-GB PNG.
#[test]
fn png_truncado_seguido_de_texto_nao_vira_arquivo_gigante() {
let mut d = vec![0x89, b'P', b'N', b'G', 0x0D, 0x0A, 0x1A, 0x0A];
@@ -540,13 +540,13 @@ fn png_truncado_seguido_de_texto_nao_vira_arquivo_gigante() {
);
assert_eq!(hit.confidence, Confidence::Low);
// sem nenhum dado de imagem (só cabeçalho) não é arquivo
// header-only PNG without any `IDAT` chunk is rejected
let mut d2 = vec![0x89, b'P', b'N', b'G', 0x0D, 0x0A, 0x1A, 0x0A];
d2.extend(png_chunk(b"IHDR", &[0, 0, 0, 8, 0, 0, 0, 8, 8, 2, 0, 0, 0]));
d2.resize(1 << 20, b'e');
assert!(signatures::detect(&MemSource(d2.clone()), 0, &d2[..64], 8 << 30).is_none());
// PNG completo continua com confiança alta e tamanho exato
// complete PNG with `IEND` still carves with `High` confidence and exact byte length
let mut ok = vec![0x89, b'P', b'N', b'G', 0x0D, 0x0A, 0x1A, 0x0A];
ok.extend(png_chunk(b"IHDR", &[0, 0, 0, 8, 0, 0, 0, 8, 8, 2, 0, 0, 0]));
ok.extend(png_chunk(b"IDAT", &[1; 100]));
@@ -557,10 +557,10 @@ fn png_truncado_seguido_de_texto_nao_vira_arquivo_gigante() {
assert_eq!((h.len as usize, h.confidence), (n, Confidence::High));
}
// ------------------------------------------------------------------ abrir no player do sistema
// ------------------------------------------------------------------ open in external system app
/// O vídeo é copiado para uma pasta temporária e entregue ao aplicativo padrão do sistema
/// (aqui um `xdg-open`/`open` falso grava o que recebeu).
/// `open_external` materializes the recovered file into a private temp folder and invokes
/// the platform opener (stubbed here via a fake `xdg-open`/`open` script in `PATH`).
#[cfg(any(target_os = "linux", target_os = "macos"))]
#[test]
fn abre_no_player_do_sistema() {
+3 -3
View File
@@ -1,4 +1,4 @@
//! Detectores estruturais: arquivos reais do sistema com lixo colado no fim (o tamanho tem de ser exato).
//! Structural format detectors: real files with trailing garbage appended (carved size must be exact).
use guara_core::signatures::detect;
use guara_core::source::MemSource;
@@ -13,7 +13,7 @@ fn check(path: &str, ext: &str, min_conf: Confidence) -> bool {
}
let mut data = std::fs::read(p).unwrap();
let real = data.len();
// lixo depois do fim real (outro arquivo no disco)
// append trailing bytes to simulate subsequent disk blocks after EOF
data.extend((0..5000u32).map(|i| (i.wrapping_mul(2654435761) >> 7) as u8 | 0x81));
let src = MemSource(data.clone());
let h =
@@ -129,7 +129,7 @@ fn webm_rar_deb_with_unknown_locations() {
#[test]
fn synthetic_structures() {
// CAB, MIDI, pcap e AIFF montados à mão.
// Synthetic CAB, MIDI, PCAP, and AIFF containers with trailing garbage.
let mut cab = b"MSCF\0\0\0\0".to_vec();
cab.extend_from_slice(&200u32.to_le_bytes());
cab.resize(200, 0);
+4 -4
View File
@@ -1,4 +1,4 @@
//! HFS+: imagem real do dfvfs (target/testdata/dfvfs/hfsplus.raw) e exclusão simulada de registros.
//! HFS+: real `dfvfs` image (`target/testdata/dfvfs/hfsplus.raw`) with simulated leaf-record deletion.
mod common;
use common::*;
@@ -43,10 +43,10 @@ fn pristine_volume_has_no_deleted_files_and_is_recognized() {
fn record_left_in_leaf_slack_is_recovered() {
let Some(mut img) = image() else { return };
let bs = 4096usize;
let node = 186 * bs + bs; // catálogo começa no bloco 186; nó 1 é a única folha
// Remove os 5 últimos registros como o driver faz ao apagar no fim do nó: só diminui a contagem.
let node = 186 * bs + bs; // catalog starts at allocation block 186; node 1 is the single leaf node
// Simulate deleting the last 5 records at the end of the leaf node by decrementing `numRecords`.
img[node + 10..node + 12].copy_from_slice(&21u16.to_be_bytes());
// Libera o bloco de dados (bitmap de alocação no bloco 1).
// Clear the allocation bitmap (at block 1) so the deleted files' data blocks are marked free.
let blk = 278usize;
img[bs + blk / 8] &= !(0x80 >> (blk % 8));
let c = scan_bytes(&img);
+7 -7
View File
@@ -1,7 +1,7 @@
use guara_core::jpegscan::*;
fn photo(w: u16, h: u16) -> Vec<u8> {
// Imagem "natural": gradientes suaves + textura + bordas.
// Synthetic natural-looking test image: smooth gradients + texture + geometric edges.
let mut rgb = Vec::with_capacity(w as usize * h as usize * 3);
let mut x = 0x9E3779B97F4A7C15u64;
for yy in 0..h as i32 {
@@ -35,7 +35,7 @@ fn decodes_whole_image_and_ends_at_eoi() {
let d = decode(&h, &j, State::start(&h), u32::MAX, 0);
assert_eq!(d.stop, Stop::Done);
assert_eq!(d.dc.len() as u32, h.total_mcus());
// O que sobra depois do último MCU é o EOI.
// Only the 2-byte EOI marker (`FF D9`) remains after decoding all MCUs.
let tail = &j[d.state.pos.min(j.len())..];
assert!(
tail.windows(2).any(|w| w == [0xFF, 0xD9]),
@@ -48,9 +48,9 @@ fn decodes_whole_image_and_ends_at_eoi() {
fn detects_break_and_resumes_with_the_right_continuation() {
let j = photo(1024, 768);
let h = parse_header(&j).unwrap();
let cut = 12 * 4096; // quebra no limite de cluster
let cut = 12 * 4096; // simulate a fragmentation break on a 4 KiB cluster boundary
assert!(j.len() > cut + 8192, "imagem pequena demais: {}", j.len());
// Dados estranhos no lugar do resto.
// Splice foreign pseudo-random bytes (without `0xFF` markers) after the cut point.
let mut x = 12345u64;
let junk: Vec<u8> = (0..40_000)
.map(|_| {
@@ -64,16 +64,16 @@ fn detects_break_and_resumes_with_the_right_continuation() {
broken.extend_from_slice(&junk);
let d = decode(&h, &broken, State::start(&h), u32::MAX, 512);
assert_ne!(d.stop, Stop::Done);
// Decodificação segue o lixo por um tempo; mas a quebra aparece pelo erro ou pela descontinuidade.
// Decoding continues into the foreign cluster briefly, then triggers a Huffman error or DC jump.
let mcu_break = first_discontinuity(&h, &d.dc);
// MCUs bons: os decodificados ao fim do cluster `cut`.
// Last valid checkpoint at the end of the first fragment (`cut`).
let good = d
.snaps
.iter()
.rfind(|s| s.pos <= cut)
.expect("snapshot antes da quebra")
.clone();
// Retomada com a continuação verdadeira reproduz a decodificação inteira.
// Resuming from that checkpoint with the true second fragment reconstructs the entire image.
let mut joined = j[..cut].to_vec();
joined.extend_from_slice(&j[cut..]);
let r = decode(&h, &joined, good.clone(), u32::MAX, 0);
+4 -4
View File
@@ -1,5 +1,5 @@
//! LUKS1/LUKS2: imagens criadas pelo cryptsetup; o conteúdo é cifrado por uma
//! implementação independente (python cryptography) com a chave real do volume.
//! LUKS1/LUKS2: headers formatted by `cryptsetup` with payload sectors encrypted by an
//! independent implementation (`python3-cryptography`) using the volume's master key.
use guara_core::crypt::{luks, Secret, UnlockError};
use guara_core::source::{read_vec, FileSource, Source};
@@ -136,7 +136,7 @@ fn run_case(c: &Case) {
assert!(st.success());
let src: Arc<dyn Source> = Arc::new(FileSource::open(&img).unwrap());
// Senha errada primeiro.
// Wrong password is ignored when followed by the right password.
match luks::unlock(src.clone(), &[Secret::Password("errada".into())]) {
Err(UnlockError::WrongSecret) => {}
Err(e) => panic!("{}: erro inesperado {e}", c.name),
@@ -149,7 +149,7 @@ fn run_case(c: &Case) {
.unwrap_or_else(|e| panic!("{}: {e}", c.name));
let got = read_vec(u.source.as_ref(), 0, data.len()).unwrap();
assert!(got == data, "{}: conteúdo difere", c.name);
// Também abre com a chave do volume.
// Also unlocks directly via the hex-encoded volume master key.
let vk = std::fs::read(&vkf).unwrap();
let u2 = luks::unlock(src, &[Secret::VolumeKey(vk)]).unwrap();
assert_eq!(
+2 -2
View File
@@ -1,4 +1,4 @@
//! LVM2: imagem real do conjunto de testes do dfvfs (target/testdata/dfvfs/lvm.raw).
//! LVM2: real image from the `dfvfs` test suite (`target/testdata/dfvfs/lvm.raw`).
use guara_core::lvm;
use guara_core::partition::detect_fs;
@@ -26,7 +26,7 @@ fn assembles_real_lvm_image() {
assert_eq!(lv.size(), 4 << 20, "{n}");
let fs = detect_fs(lv.as_ref(), 0);
eprintln!("{n}: {fs:?}");
// O segundo volume da imagem de referência foi criado e nunca formatado.
// The second logical volume (`test_volume_2`) in the reference image is unformatted.
assert_eq!(fs.is_some(), n == "test_logical_volume1", "{n}");
}
}
+14 -14
View File
@@ -1,4 +1,4 @@
//! Reconstrução do moov de MP4 H.264: arquivos sintéticos e, se existir, um vídeo real.
//! H.264 MP4 `moov` reconstruction: synthetic B-frame streams and optional real video test.
use guara_core::mp4rebuild;
use guara_core::source::MemSource;
@@ -44,7 +44,7 @@ fn hex(s: &str) -> Vec<u8> {
.collect()
}
/// Cabeçalho de fatia H.264 conforme o SPS acima (log2_max_frame_num e log2_max_poc_lsb lidos do SPS).
/// Builds an H.264 slice header matching `SPS` above (`log2_max_frame_num = 4`, `log2_max_poc_lsb = 4`).
fn slice(
idr: bool,
frame_num: u32,
@@ -78,7 +78,7 @@ fn nal(out: &mut Vec<u8>, n: &[u8]) {
out.extend_from_slice(n);
}
/// Monta um MP4 sem moov: ftyp, free, mdat. Devolve (arquivo, tamanhos esperados, sincronos, ordem de exibição).
/// Builds a headerless MP4 (`ftyp` + `free` + `mdat`). Returns `(bytes, sample_sizes, sync_sample_nums, display_order_pocs)`.
fn synthetic(gops: usize) -> (Vec<u8>, Vec<usize>, Vec<bool>) {
synthetic_opt(gops, true)
}
@@ -90,7 +90,7 @@ fn synthetic_opt(gops: usize, inband: bool) -> (Vec<u8>, Vec<usize>, Vec<bool>)
let mut mdat = vec![];
let mut sizes = vec![];
let mut sync = vec![];
// GOP: I P B B P B B (ordem de decodificação) com POC de exibição 0 6 2 4 12 8 10 (em unidades de 2)
// GOP in decode order: `I P B B P B B` with display POCs `0, 6, 2, 4, 12, 8, 10`
let pocs = [0u32, 6, 2, 4, 12, 8, 10];
let mut frame_num = 0;
for g in 0..gops {
@@ -144,7 +144,7 @@ fn assemble(file: &[u8], r: &mp4rebuild::Rebuilt) -> Vec<u8> {
fn rebuilds_synthetic_stream_with_b_frames_and_garbage_tail() {
let (mut file, sizes, sync) = synthetic(6);
let real_len = file.len();
// Lixo depois do fim real (outro arquivo no disco) e tamanho do mdat declarado como "até o fim".
// Append non-NAL garbage and set `mdat` size to `0` (extends to EOF) to test stream end detection.
file.extend((0..20_000u32).map(|i| (i.wrapping_mul(2654435761) >> 11) as u8 | 0x80));
let n = file.len() as u32 - 32;
file[32..36].copy_from_slice(&n.to_be_bytes());
@@ -168,7 +168,7 @@ fn rebuilds_synthetic_stream_with_b_frames_and_garbage_tail() {
assert_eq!(s.bytes.len(), sizes[i], "tamanho da amostra {}", i + 1);
assert_eq!(s.is_sync, sync[i], "sincronia da amostra {}", i + 1);
}
// Ordem de exibição: pts = dts + deslocamento deve crescer na ordem do POC.
// Composition timestamps (`pts = dts + ctts_offset`) must follow the expected POC display order.
let mut pts: Vec<(i64, usize)> = vec![];
for i in 0..sizes.len() {
let s = mp4.read_sample(track_id, i as u32 + 1).unwrap().unwrap();
@@ -193,7 +193,7 @@ fn rebuilds_real_h264_file_like_the_original_index() {
return;
};
let orig = std::fs::read(&path).unwrap();
// Corta o moov original.
// Strip the original `moov` atom.
let mut p = 0usize;
let mut cut = orig.len();
while p + 8 <= orig.len() {
@@ -229,7 +229,7 @@ fn rebuilds_real_h264_file_like_the_original_index() {
#[test]
fn rebuilds_real_video_with_interleaved_audio() {
// GUARA_MP4_BIG: vídeo real com áudio; o teste corta o moov e os primeiros 40 MB do mdat bastam.
// Optional real-video test (`GUARA_MP4_BIG`): strips `moov` and truncates `mdat` to the first 40 MB.
let Ok(path) = std::env::var("GUARA_MP4_BIG") else {
return;
};
@@ -238,7 +238,7 @@ fn rebuilds_real_video_with_interleaved_audio() {
let total = f.metadata().unwrap().len();
let mut head = vec![0u8; 1 << 20];
f.read_exact(&mut head).unwrap();
// átomos no começo
// locate leading `ftyp` and `mdat` atoms
let mut p = 0usize;
let (mut ftyp_end, mut mdat_at) = (0usize, None);
while p + 8 <= head.len() {
@@ -260,7 +260,7 @@ fn rebuilds_real_video_with_interleaved_audio() {
eprintln!("SKIP: mdat fora do primeiro MiB (moov no começo é grande)");
return;
};
// Reexecuta com a leitura completa do começo para montar: ftyp + mdat (40 MB).
// Re-read the leading `ftyp` + first 40 MB of `mdat` into a truncated test buffer.
use std::io::Seek;
let mut f = std::fs::File::open(&path).unwrap();
f.seek(std::io::SeekFrom::Start(mp as u64)).unwrap();
@@ -268,9 +268,9 @@ fn rebuilds_real_video_with_interleaved_audio() {
f.read_exact(&mut mdat).unwrap();
let mut file = head[..ftyp_end].to_vec();
file.extend_from_slice(&mdat);
// O tamanho do mdat declarado vai muito além dos 40 MB que temos (arquivo cortado).
// The original `mdat` header declares the full multi-hundred-MB length (simulating an interrupted recording).
let src = MemSource(file.clone());
// Sem SPS/PPS no mdat, é preciso um vídeo de referência da mesma câmera (aqui, o próprio original).
// When SPS/PPS are not repeated in `mdat`, extract donor codec parameters from a reference video.
let cfg = mp4rebuild::config_from_mp4(&MemSource(head.clone()), 0, head.len() as u64)
.expect("avcC do original");
assert!(
@@ -282,7 +282,7 @@ fn rebuilds_real_video_with_interleaved_audio() {
let osize = out.len() as u64;
let mut b = mp4::Mp4Reader::read_header(std::io::Cursor::new(out), osize).unwrap();
let tb = *b.tracks().keys().next().unwrap();
// Original (índice completo) para comparar as primeiras amostras.
// Parse the original intact MP4 sample table to verify the first 100 reconstructed frames.
let f = std::fs::File::open(&path).unwrap();
let mut a = mp4::Mp4Reader::read_header(std::io::BufReader::new(f), total).unwrap();
let vid = a
@@ -321,7 +321,7 @@ mod common;
fn deep_scan_rebuilds_a_video_without_moov_using_a_neighbour_as_reference() {
use guara_core::{ScanMode, ScanOptions};
common::init();
// Arquivo íntegro (com moov) da "mesma câmera" e um segundo que perdeu o moov e não traz SPS/PPS.
// Disk image containing an intact donor MP4 (with `moov`) plus a truncated MP4 without inline SPS/PPS.
let (good_raw, _, _) = synthetic_opt(8, true);
let r =
mp4rebuild::rebuild(&MemSource(good_raw.clone()), 0, good_raw.len() as u64, &[]).unwrap();
+8 -8
View File
@@ -1,4 +1,4 @@
//! Relatório em PDF: estrutura do arquivo (xref, páginas), texto com acentos e paginação.
//! PDF report generator tests: xref table offsets, WinAnsi accented text escaping, and multi-page tables.
use guara_core::recover::{RecoverReport, RecoveredFile};
use guara_core::report::{self, AuditNote, CaseInfo, ReportInput};
@@ -81,7 +81,7 @@ fn find(hay: &[u8], needle: &[u8], from: usize) -> Option<usize> {
.map(|p| p + from)
}
/// Texto dos fluxos de conteúdo, já descomprimidos.
/// Concatenates all uncompressed content streams in the generated PDF.
fn streams(pdf: &[u8]) -> Vec<String> {
let mut out = vec![];
let mut at = 0;
@@ -107,7 +107,7 @@ fn pdf_has_a_valid_cross_reference_table() {
let nl = find(&pdf, b"\n", sx).unwrap();
let xref: usize = std::str::from_utf8(&pdf[sx..nl]).unwrap().parse().unwrap();
assert_eq!(&pdf[xref..xref + 4], b"xref");
// cada entrada aponta exatamente para "N 0 obj"
// every xref entry offset points directly to `"N 0 obj"`
let table = &pdf[xref..];
let header_end = find(table, b"\n", 5).unwrap();
let count: usize = std::str::from_utf8(&table[5..header_end])
@@ -128,14 +128,14 @@ fn pdf_has_a_valid_cross_reference_table() {
#[test]
fn pdf_text_survives_accents_and_escaping() {
let all = streams(&build(5)).join("\n");
// acentos viram bytes WinAnsi em octal; parênteses e barra são escapados
// Latin-1 accents encode as WinAnsi octal escapes; parentheses and backslashes are escaped
assert!(all.contains("Identifica\\347\\343o do caso"), "{all}");
assert!(all.contains("Perita \\(Ana\\) \\\\ Souza"));
assert!(all.contains("Laborat\\363rio"));
assert!(all.contains("fotografia_0003.jpg"));
assert!(all.contains(&"ab".repeat(32)));
assert!(all.contains(&"12".repeat(32)), "hash da trilha ausente");
// aspas curvas viram os bytes WinAnsi 0x93/0x94
// curly quotes map to WinAnsi `0x93`/`0x94` (`\223`/`\224`)
assert!(all.contains("\\223aspas\\224"));
}
@@ -147,12 +147,12 @@ fn long_lists_paginate_with_numbered_footers() {
let last = pages.len();
assert!(pages[last - 1].contains(&format!("p\\341gina {last} de {last}")));
assert!(pages[0].contains(&format!("p\\341gina 1 de {last}")));
// o cabeçalho da tabela se repete nas páginas seguintes
// the table header row is repeated at the top of each continuation page
assert!(pages[2].contains("Modificado \\(UTC\\)"));
// todas as linhas aparecem exatamente uma vez
// every table row appears exactly once across all pages
let all = pages.join("\n");
assert_eq!(all.matches("fotografia_0399.jpg").count(), 1);
// a primeira também consta da lista de recuperados
// `arq-000.jpg` appears both in the findings table and in the recovered files section
assert_eq!(all.matches("fotografia_0000.jpg").count(), 2);
}
+11 -11
View File
@@ -1,5 +1,5 @@
//! RAID por software: arrays gerados por codificadores independentes (faixa a faixa),
//! lidos de volta inteiros, degradados e com superblocos md 1.x e 0.90.
//! Software RAID tests: arrays built by independent stripe encoders and read back
//! intact, degraded, and via Linux `mdadm` v1.x and v0.90 superblocks.
use guara_core::raid::*;
use guara_core::source::{read_vec, MemSource, Source};
@@ -39,7 +39,7 @@ fn gpow(k: usize) -> u8 {
(0..k).fold(1, |a, _| gmul(a, 2))
}
/// Gera as imagens dos membros de um RAID 5/6 com a paridade na posição pedida.
/// Builds member disk buffers for a RAID 5/6 array using the specified parity layout.
fn encode_parity(level: Level, layout: Layout, n: usize, logical: &[u8]) -> Vec<Vec<u8>> {
let dd = if level == Level::R6 { n - 2 } else { n - 1 };
let rows = logical.len() / (C * dd);
@@ -54,7 +54,7 @@ fn encode_parity(level: Level, layout: Layout, n: usize, logical: &[u8]) -> Vec<
_ => (r, (r + 1) % n),
};
let sym = matches!(layout, Layout::LeftSymmetric | Layout::RightSymmetric);
// ordem dos discos de dados
// data disk slot order for this stripe row
let order: Vec<usize> = if sym {
let start = if level == Level::R6 { qd + 1 } else { pd + 1 };
(0..dd).map(|d| (start + d) % n).collect()
@@ -110,7 +110,7 @@ fn check(level: Level, layout: Layout, n: usize, missing: &[usize]) {
got == logical,
"{level:?} {layout:?} n={n} faltando {missing:?}"
);
// leitura desalinhada
// unaligned cross-stripe read
let part = read_vec(&r, 5000, 30000).unwrap();
assert_eq!(part, &logical[5000..35000]);
}
@@ -145,7 +145,7 @@ fn raid6_layouts_with_one_and_two_missing() {
#[test]
fn raid0_raid1_linear_and_raid10_near() {
let logical = data(C * 24);
// RAID 0 com 3 discos
// 3-disk RAID 0
let mut imgs = vec![vec![]; 3];
for (c, ch) in logical.chunks(C).enumerate() {
imgs[c % 3].extend_from_slice(ch);
@@ -158,7 +158,7 @@ fn raid0_raid1_linear_and_raid10_near() {
))
.unwrap();
assert_eq!(read_vec(&r, 0, logical.len()).unwrap(), logical);
// RAID 1, um membro caído
// 2-disk RAID 1 with first mirror missing
let m = vec![logical.clone(), logical.clone()];
let r = RaidSource::new(RaidParams::simple(
Level::R1,
@@ -178,7 +178,7 @@ fn raid0_raid1_linear_and_raid10_near() {
))
.unwrap();
assert_eq!(read_vec(&r, 0, logical.len()).unwrap(), logical);
// RAID 10 near=2 com 4 discos: slots em sequência.
// 4-disk RAID 10 (`near = 2`): sequential slot pairs.
let n = 4;
let mut imgs = vec![vec![0u8; C * 12]; n];
for (c, ch) in logical.chunks(C).enumerate() {
@@ -240,11 +240,11 @@ fn assembles_from_md_v12_superblocks_with_a_missing_disk() {
let n = 4;
let logical = data(C * 3 * 40);
let imgs = encode_parity(Level::R5, Layout::LeftSymmetric, n, &logical);
let data_off = 2048u64; // 1 MiB em setores
let data_off = 2048u64; // 1 MiB in 512-byte sectors
let mut members_in = vec![];
for (i, im) in imgs.iter().enumerate() {
if i == 2 {
continue; // disco perdido
continue; // simulate missing disk 2
}
let mut full = vec![0u8; 4096 + (data_off as usize) * 512 + im.len()];
let sb = md_v1_super(
@@ -260,7 +260,7 @@ fn assembles_from_md_v12_superblocks_with_a_missing_disk() {
);
full[4096..8192].copy_from_slice(&sb);
full[4096 + data_off as usize * 512..][..im.len()].copy_from_slice(im);
// o superbloco 1.2 fica a 4 KiB; os dados começam em data_offset a partir do início
// mdadm v1.2 superblock sits at 4 KiB; array data starts at `data_offset` sectors
let mut real = vec![0u8; data_off as usize * 512 + im.len()];
real[4096..8192].copy_from_slice(&sb);
real[data_off as usize * 512..].copy_from_slice(im);
+3 -3
View File
@@ -1,4 +1,4 @@
//! Descoberta automática dos parâmetros de um RAID sem metadados.
//! Automatic parameter detection for metadata-less RAID arrays.
use guara_core::raid::Level;
use guara_core::raid_detect;
@@ -69,7 +69,7 @@ fn finds_raid5_chunk_layout_and_shuffled_order() {
}
disks[pd][row * chunk..(row + 1) * chunk].copy_from_slice(&p);
}
// Entrega os discos embaralhados: fornecidos = [2, 0, 1] do array real.
// Pass member disks in scrambled order `[2, 0, 1]`.
let given: Vec<Arc<dyn Source>> = [2usize, 0, 1]
.iter()
.map(|&i| Arc::new(MemSource(disks[i].clone())) as Arc<dyn Source>)
@@ -85,7 +85,7 @@ fn finds_raid5_chunk_layout_and_shuffled_order() {
let best = &found[0];
assert_eq!(best.level, Level::R5);
assert_eq!(best.chunk, chunk as u64);
// Layouts e ordens diferentes podem ser equivalentes; o que importa é o conteúdo montado.
// Equivalent 3-disk parity permutations produce identical reconstructed volume bytes.
let r = guara_core::raid::RaidSource::new(best.params(&given)).unwrap();
let got = guara_core::source::read_vec(&r, 0, logical.len()).unwrap();
assert!(
+4 -4
View File
@@ -1,4 +1,4 @@
//! Recuperação de ponta a ponta sobre imagens reais (FAT32, NTFS, ext4).
//! End-to-end recovery tests across real filesystem images (FAT32, NTFS, ext4).
mod common;
use common::*;
@@ -25,7 +25,7 @@ fn check(fs: &str, only_carvable: bool) {
"[{fs}] não recuperado bit a bit: {path}"
);
}
// Os dois métodos juntos não podem listar o mesmo conteúdo duas vezes.
// Combining metadata + signature carving must deduplicate findings at the same offset.
let mut seen = std::collections::HashSet::new();
for f in &res.found {
if let Some(o) = f.first_offset() {
@@ -79,7 +79,7 @@ fn varredura_rapida_nao_faz_carving() {
..Default::default()
},
);
// O ext4 agora tem leitor de metadados (inodes e diário), mas a varredura rápida nunca faz carving.
// Quick scan runs the ext4 metadata/journal reader without running signature carving.
assert!(res.found.iter().all(|f| f.origin != Origin::Carved));
let res = scan_path(
&dir.join("ntfs.img"),
@@ -97,7 +97,7 @@ fn varredura_rapida_nao_faz_carving() {
);
}
/// Regressão: mkfs.vfat -F 32 em mídia pequena (< 65525 clusters) é FAT32 pelo BPB.
/// Regression test: `mkfs.vfat -F 32` on a small image (`< 65525` clusters) is still detected as FAT32 via BPB.
#[test]
fn fat32_pequeno_com_poucos_clusters_e_reconhecido() {
use std::process::Command;
+13 -13
View File
@@ -1,4 +1,4 @@
//! Assinatura digital: chaves, arquivos, adulteração, recuperação inteira e interoperabilidade.
//! Ed25519 digital signature tests: keygen, signing, tamper detection, recovery verification, and OpenSSL interop.
use guara_core::recover;
use guara_core::sign::{self, SigningKey};
@@ -30,7 +30,7 @@ fn resident(id: u64, name: &str, bytes: Vec<u8>) -> Found {
}
}
/// `recover` assina sozinho quando `GUARA_SIGNING_KEY` existe; testes que recuperam não se misturam.
/// Serializes tests that mutate `GUARA_SIGNING_KEY` / `GUARA_CONFIG_DIR`.
static ENV: std::sync::Mutex<()> = std::sync::Mutex::new(());
fn keyed(dir: &Path) -> SigningKey {
@@ -46,7 +46,7 @@ fn key_roundtrip_and_no_overwrite() {
let again = SigningKey::load(&dir.path().join("k.key")).unwrap();
assert_eq!(k.public_bytes(), again.public_bytes());
assert_eq!(k.fingerprint(), again.fingerprint());
assert_eq!(k.fingerprint().len(), 24); // 5 grupos de 4 + 4 separadores
assert_eq!(k.fingerprint().len(), 24); // 5 groups of 4 hex chars + 4 colons = 24 chars
let pubfile = std::fs::read_to_string(dir.path().join("k.pub")).unwrap();
assert!(pubfile.contains(&k.fingerprint()));
let e = SigningKey::generate()
@@ -55,7 +55,7 @@ fn key_roundtrip_and_no_overwrite() {
.unwrap()
.to_string();
assert!(e.contains("já existe uma chave"), "{e}");
// a chave antiga continua intacta
// existing private key is preserved untouched
assert_eq!(
SigningKey::load(&dir.path().join("k.key"))
.unwrap()
@@ -92,26 +92,26 @@ fn signed_file_verifies_and_tampering_is_caught() {
c.valid && c.trusted == Some(true) && c.signer.as_deref() == Some("Perita Ana"),
"{c:?}"
);
// fingerprint aceito em outro formato (maiúsculas, sem dois pontos)
// fingerprint comparison is case- and separator-insensitive
let loose = k.fingerprint().replace(':', " ").to_uppercase();
assert_eq!(
sign::verify_file(&f, None, Some(&loose)).unwrap().trusted,
Some(true)
);
// outra chave não é a esperada
// mismatched trusted fingerprint sets `trusted = Some(false)`
let other = SigningKey::generate().fingerprint();
let c = sign::verify_file(&f, None, Some(&other)).unwrap();
assert!(!c.valid && c.trusted == Some(false));
// arquivo alterado
// modifying the signed file invalidates the signature
std::fs::write(&f, b"conteudo ALTERADO do relatorio").unwrap();
let c = sign::verify_file(&f, None, None).unwrap();
assert!(!c.valid && !c.file_matches && c.signature_valid, "{c:?}");
std::fs::write(&f, b"conteudo do relatorio").unwrap();
assert!(sign::verify_file(&f, None, None).unwrap().valid);
// campos da assinatura alterados (hora, assinante, hash) invalidam
// tampering with any signed metadata field (timestamp, signer, sha256) invalidates verification
let raw = std::fs::read_to_string(&sig).unwrap();
for (from, to) in [
("Perita Ana", "Outra Pessoa"),
@@ -122,7 +122,7 @@ fn signed_file_verifies_and_tampering_is_caught() {
let c = sign::verify_file(&f, None, None).unwrap();
assert!(!c.valid && !c.signature_valid, "{from}: {c:?}");
}
// trocar a chave pública pela de outra pessoa também não passa
// replacing `public_key` with another keypair's public key fails verification
let mut j: serde_json::Value = serde_json::from_str(&raw).unwrap();
let evil = SigningKey::generate();
j["chave_publica"] = evil
@@ -164,7 +164,7 @@ fn recovery_folder_is_checked_file_by_file() {
"{c:?}"
);
// manifesto adulterado (hash trocado para acompanhar o arquivo alterado) quebra a assinatura
// updating the manifest hash to match a tampered file invalidates the manifest's Ed25519 signature
let m = std::fs::read_to_string(&manifest).unwrap();
let real = &rep.recovered[0].sha256;
let fake = sign::hash_file(&first).unwrap().0;
@@ -190,7 +190,7 @@ fn manifest_paths_cannot_escape_the_folder() {
assert!(!c.valid && c.changed[0].contains("fora da pasta"), "{c:?}");
}
/// O mesmo que o `openssl` faria: a assinatura é Ed25519 padrão sobre a mensagem documentada.
/// Verifies that our Ed25519 signature validates with external `openssl pkeyutl -verify -rawin`.
#[test]
fn signature_verifies_with_openssl() {
let Ok(v) = std::process::Command::new("openssl")
@@ -239,7 +239,7 @@ fn signature_verifies_with_openssl() {
])
.output()
.unwrap();
// OpenSSL 1.1 não tem -rawin para Ed25519; só exige sucesso onde ele existe
// OpenSSL 1.1 lacks `-rawin` for Ed25519; only assert when `-rawin` is supported
let text = String::from_utf8_lossy(&r.stdout).to_string() + &String::from_utf8_lossy(&r.stderr);
if text.contains("Unknown option") || text.contains("unknown option") {
return;
@@ -247,7 +247,7 @@ fn signature_verifies_with_openssl() {
assert!(r.status.success(), "{text}");
}
/// Com chave configurada, a recuperação e o relatório saem assinados sem ninguém pedir.
/// When `GUARA_SIGNING_KEY` is configured, `recover` and `report` auto-sign their outputs.
#[test]
fn configured_key_signs_manifest_automatically() {
let _g = ENV.lock().unwrap_or_else(|e| e.into_inner());
+10 -10
View File
@@ -1,5 +1,5 @@
//! Leitor de SQLite: esquema, linhas (com overflow e árvores de vários níveis),
//! registros apagados e robustez contra bancos danificados.
//! SQLite B-tree reader tests: schema parsing, multi-level B-trees with overflow pages,
//! deleted-row carving from freeblocks, and fuzz/truncation resilience.
use guara_core::source::MemSource;
use guara_core::sqlite::{is_sqlite, Cell, Db};
@@ -37,7 +37,7 @@ fn esquema_com_tabelas_indices_e_views() {
assert_eq!(s.indexes, ["idx_usuarios_email"]);
assert_eq!(s.views, ["maiores"]);
assert!(s.warnings.is_empty(), "{:?}", s.warnings);
assert_eq!(db.count_rows(u), 493); // 500 menos 7 apagados
assert_eq!(db.count_rows(u), 493); // 500 inserted rows minus 7 deleted rows
}
#[test]
@@ -47,17 +47,17 @@ fn linhas_em_arvore_de_varios_niveis_e_paginacao() {
let p = db.rows("usuarios", 0, 5).unwrap();
assert_eq!(p.total, 493);
assert_eq!(p.rows.len(), 5);
// o alias do rowid (INTEGER PRIMARY KEY) vem preenchido
// `INTEGER PRIMARY KEY` column (`id`) is populated from the B-tree cell `rowid`
assert!(matches!(p.rows[0].cells[0], Cell::Int(1)));
assert_eq!(text(&p.rows[0].cells[1]), "Pessoa 0001");
assert_eq!(text(&p.rows[0].cells[2]), "pessoa1@exemplo.com");
assert!(matches!(p.rows[0].cells[3], Cell::Int(19)));
assert!(matches!(p.rows[1].cells[4], Cell::Real(r) if (r - 3.0).abs() < 1e-9));
assert!(matches!(&p.rows[2].cells[5], Cell::Blob { len: 3, .. }));
// pula os apagados 101..103: a linha de índice 100 é o id 104
// rows 101..=103 were deleted, so row index 100 is `id = 104`
let q = db.rows("usuarios", 100, 2).unwrap();
assert!(matches!(q.rows[0].cells[0], Cell::Int(104)));
// última linha
// last row (`id = 500`)
let last = db.rows("usuarios", 492, 10).unwrap();
assert_eq!(last.rows.len(), 1);
assert_eq!(text(&last.rows[0].cells[1]), "Pessoa 0500");
@@ -114,13 +114,13 @@ fn recupera_registros_apagados_das_areas_livres() {
"só {achados} de 7 registros apagados recuperados: {nomes:?}"
);
assert!(d.rows.iter().all(|r| r.deleted));
// a tabela de notas não teve nada apagado
// the second table had no deleted rows
assert!(db.deleted_rows("notas").unwrap().rows.is_empty());
}
#[test]
fn banco_truncado_e_corrompido_nao_derruba_nem_trava() {
// metade do arquivo
// truncated to half the file size
let half = DB[..DB.len() / 2].to_vec();
let src = MemSource(half);
let db = Db::open(&src).unwrap();
@@ -132,11 +132,11 @@ fn banco_truncado_e_corrompido_nao_derruba_nem_trava() {
);
let p = db.rows("usuarios", 0, 1000).unwrap();
assert!(!p.rows.is_empty() && p.rows.len() < 493, "{}", p.rows.len());
// sem cabeçalho
// wiped SQLite header
let mut bad = DB.to_vec();
bad[0] = b'X';
assert!(Db::open(&MemSource(bad)).is_err());
// corrupção aleatória (semente fixa): nunca pode dar panic
// deterministic pseudo-random byte corruption: must never panic
let mut x: u64 = 0x1234_5678_9abc_def0;
for _ in 0..60 {
let mut d = DB.to_vec();
+52 -52
View File
@@ -1,10 +1,10 @@
//! Imagens de disco virtual: QCOW2, VMDK, VHD, VHDX e DMG.
//! Virtual disk image tests: QCOW2, VMDK, VHD, VHDX, and DMG.
//!
//! Os arquivos de `tests/fixtures/vdisk` foram feitos por ferramentas reais (VirtualBox para
//! VMDK/VHD, qemu para QCOW2, hdiutil para DMG) e conferidos contra o conteúdo bruto. VHDX, DMG
//! sintético e discos diferenciais são montados aqui, byte a byte, a partir da especificação.
//! As imagens do dfvfs em target/testdata (scripts/fetch-testdata.sh) são conferidas por hash,
//! calculado com um leitor independente (7-Zip), e pulam quando ausentes.
//! Fixtures in `tests/fixtures/vdisk` were created by real tools (`VBoxManage` for VMDK/VHD,
//! `qemu-img` for QCOW2, `hdiutil` for DMG) and verified against the raw disk image. VHDX,
//! synthetic DMG chunks, and differencing chains are constructed byte-for-byte from the specs.
//! External `dfvfs` images in `target/testdata` (`scripts/fetch-testdata.sh`) are verified by
//! SHA-256 against independent `7z` extractions and skipped when absent.
use guara_core::source::{open_path, read_vec, Source};
use sha2::{Digest, Sha256};
@@ -39,7 +39,7 @@ fn sha(src: &dyn Source) -> String {
h.finalize().iter().map(|b| format!("{b:02x}")).collect()
}
/// Lê o disco inteiro e também em pedaços que cruzam blocos, comparando com `want`.
/// Verifies both full-disk reads and unaligned cross-block reads against `want`.
fn check_content(path: &Path, want: &[u8]) {
let s = open_path(path).unwrap_or_else(|e| panic!("{}: {e}", path.display()));
assert_eq!(s.size(), want.len() as u64, "{}", path.display());
@@ -63,19 +63,19 @@ fn check_content(path: &Path, want: &[u8]) {
path.display()
);
}
// leitura além do fim devolve só o que existe
// reads extending past EOF return only the remaining bytes
let mut buf = [0u8; 64];
assert_eq!(s.read_at(want.len() as u64 - 10, &mut buf).unwrap(), 10);
assert_eq!(s.read_at(want.len() as u64, &mut buf).unwrap(), 0);
}
// ---------- imagens feitas por ferramentas reais ----------
// ---------- fixtures created by real tools ----------
#[test]
fn virtualbox_vmdk_sparse_and_stream_optimized() {
let want = raw();
check_content(&fx("disk.vmdk"), &want);
// streamOptimized: grãos comprimidos com zlib e cabeçalho no rodapé
// `streamOptimized` VMDK: deflate-compressed grains and footer descriptor
check_content(&fx("disk-stream.vmdk"), &want);
}
@@ -92,7 +92,7 @@ fn qemu_qcow2_and_hdiutil_dmg_match_a_reference_reader() {
sha(s.as_ref()),
"a6c2f0e39afe6c6ab432ca5465349fcefe8dc944398e97b2d957d3f89dbb5d80"
);
// UDIF com chunks zlib, zerados e ignorados
// UDIF DMG with zlib, zero-fill, and ignored chunks
let d = open_path(&fx("hfsplus_zlib.dmg")).unwrap();
assert_eq!(d.size(), 1_954_304);
assert_eq!(
@@ -101,7 +101,7 @@ fn qemu_qcow2_and_hdiutil_dmg_match_a_reference_reader() {
);
}
// ---------- montagem de imagens sintéticas ----------
// ---------- synthetic image builders ----------
fn be(v: &mut [u8], o: usize, x: u64, n: usize) {
v[o..o + n].copy_from_slice(&x.to_be_bytes()[8 - n..]);
@@ -122,7 +122,7 @@ fn zlib(data: &[u8]) -> Vec<u8> {
e.finish().unwrap()
}
/// QCOW2 v3 com clusters de 64 KiB: zerados (sem entrada e com a flag), brutos e comprimidos.
/// Builds a QCOW2 v3 image (64 KiB clusters) with unallocated, zero-flagged, raw, and compressed clusters.
fn build_qcow2(data: &[u8]) -> Vec<u8> {
const CS: usize = 1 << 16;
let clusters = data.len() / CS;
@@ -134,14 +134,14 @@ fn build_qcow2(data: &[u8]) -> Vec<u8> {
be(&mut img, 36, 1, 4);
be(&mut img, 40, CS as u64, 8);
be(&mut img, 100, 112, 4);
be(&mut img, CS, (2 * CS as u64) | (1 << 63), 8); // L1 -> L2 no cluster 2
be(&mut img, CS, (2 * CS as u64) | (1 << 63), 8); // L1 entry pointing to L2 table at cluster 2
for i in 0..clusters {
let c = &data[i * CS..(i + 1) * CS];
let entry: u64 = if c.iter().all(|&b| b == 0) {
if i % 2 == 0 {
0
} else {
1 // zerado explícito
1 // explicit zero-cluster flag (bit 0)
}
} else if i % 3 == 0 {
let z = deflate(c);
@@ -196,7 +196,7 @@ fn base64(d: &[u8]) -> String {
s
}
/// ADC só com literais e cópias de 3 bytes (deslocamento 8), como o `hdiutil` produz.
/// Encodes payload using ADC literal runs and 3-byte backreferences (offset 8).
fn adc_pattern(len: usize) -> (Vec<u8>, Vec<u8>) {
let pat = *b"ADC-test";
let mut enc = vec![0x80 | 7];
@@ -211,7 +211,7 @@ fn adc_pattern(len: usize) -> (Vec<u8>, Vec<u8>) {
if n >= 4 {
enc.extend_from_slice(&[0x40 | (n as u8 - 4), 0, 7]);
} else {
enc.extend_from_slice(&[((n as u8 - 3) << 2), 7]); // 2 bytes, comprimento 3
enc.extend_from_slice(&[((n as u8 - 3) << 2), 7]); // 2-byte match opcode, length 3
}
made += n;
}
@@ -301,7 +301,7 @@ fn qcow2_with_encryption_or_a_remote_backing_file_is_refused() {
assert!(e.contains("não é um arquivo local"), "{e}");
}
/// Filho QCOW2 que só aloca o cluster 1 (dados), marca o 2 como zerado e deixa o resto para o pai.
/// Builds a child QCOW2 that allocates cluster 1, zero-flags cluster 2, and leaves the rest to `backing`.
fn build_qcow2_child(backing: &str, fmt: Option<&str>, own: &[u8]) -> Vec<u8> {
const CS: usize = 1 << 16;
let mut img = vec![0u8; 3 * CS];
@@ -316,7 +316,7 @@ fn build_qcow2_child(backing: &str, fmt: Option<&str>, own: &[u8]) -> Vec<u8> {
be(&mut img, 40, CS as u64, 8);
be(&mut img, 100, 112, 4);
if let Some(f) = fmt {
// extensão 0xE2792ACA com o nome do formato do apoio, depois o fim das extensões
// header extension `0xE2792ACA` (backing file format name) followed by end-of-extensions (`0`)
be(&mut img, 112, 0xE279_2ACA, 4);
be(&mut img, 116, f.len() as u64, 4);
img[120..120 + f.len()].copy_from_slice(f.as_bytes());
@@ -326,7 +326,7 @@ fn build_qcow2_child(backing: &str, fmt: Option<&str>, own: &[u8]) -> Vec<u8> {
img.extend_from_slice(own);
img.resize(img.len().div_ceil(CS) * CS, 0);
be(&mut img, 2 * CS + 8, host | (1 << 63), 8); // cluster 1
be(&mut img, 2 * CS + 16, 1, 8); // cluster 2: zerado de propósito
be(&mut img, 2 * CS + 16, 1, 8); // cluster 2: explicitly zeroed (must not fall back to parent)
img
}
@@ -345,12 +345,12 @@ fn qcow2_backing_file_supplies_what_the_child_did_not_allocate() {
std::fs::write(&p, build_qcow2_child("base.raw", fmt, &own)).unwrap();
check_content(&p, &expect);
}
// com o pai em outro formato: o filho aponta para um QCOW2
// multi-hop backing chain: QCOW2 child -> QCOW2 parent -> raw base
std::fs::write(dir.path().join("base.qcow2"), build_qcow2(&base)).unwrap();
let p = dir.path().join("c.qcow2");
std::fs::write(&p, build_qcow2_child("base.qcow2", Some("qcow2"), &own)).unwrap();
check_content(&p, &expect);
// sem o pai, a abertura diz o que falta
// missing backing file produces a clear error naming the missing parent
std::fs::remove_file(dir.path().join("base.raw")).unwrap();
let e = open_path(&dir.path().join("a.qcow2"))
.err()
@@ -384,10 +384,10 @@ fn vmdk_descriptor_with_flat_and_zero_extents() {
check_content(&p, &expect);
}
/// VMDK esparso feito à mão: 16 grãos de 64 KiB; `grains[i]` = `None` (não alocado, vem do pai),
/// `Some(None)` (zerado de propósito) ou `Some(Some(dados))`.
/// Builds a sparse VMDK with sixteen 64 KiB grains: `None` = unallocated (falls back to parent),
/// `Some(None)` = zeroed grain (`GTE == 1`), or `Some(Some(data))` = allocated grain.
fn build_vmdk_sparse(descriptor: &str, grains: &[Option<Option<Vec<u8>>>]) -> Vec<u8> {
const GRAIN: usize = 128; // setores
const GRAIN: usize = 128; // capacity in 512-byte sectors
let mut img = vec![0u8; 26 * 512];
img[..4].copy_from_slice(b"KDMV");
le(&mut img, 4, 1, 4);
@@ -400,7 +400,7 @@ fn build_vmdk_sparse(descriptor: &str, grains: &[Option<Option<Vec<u8>>>]) -> Ve
le(&mut img, 56, 21, 8);
le(&mut img, 64, 26, 8);
img[512..512 + descriptor.len()].copy_from_slice(descriptor.as_bytes());
le(&mut img, 21 * 512, 22, 4); // diretório de grãos -> tabela no setor 22
le(&mut img, 21 * 512, 22, 4); // grain directory entry pointing to the grain table at sector 22
for (i, g) in grains.iter().enumerate() {
let entry = match g {
None => 0,
@@ -432,7 +432,7 @@ fn vmdk_child_reads_the_parent_for_unallocated_grains() {
let own = vec![0x37u8; 65536];
let mut grains: Vec<Option<Option<Vec<u8>>>> = vec![None; 16];
grains[3] = Some(Some(own.clone()));
grains[5] = Some(None); // zerado de propósito: não consulta o pai
grains[5] = Some(None); // explicitly zeroed grain (`GTE == 1`): does not fall back to parent
let child = build_vmdk_sparse(
"# Disk DescriptorFile\nversion=1\nCID=22222222\nparentCID=11111111\ncreateType=\"monolithicSparse\"\nparentFileNameHint=\"base.vmdk\"\n",
&grains,
@@ -445,17 +445,17 @@ fn vmdk_child_reads_the_parent_for_unallocated_grains() {
check_content(&p, &expect);
}
/// Snapshot real do VirtualBox: o filho só diz o UUID do pai (`ddb.uuid.parent`), sem nome.
/// VirtualBox snapshot VMDK where the child references its parent solely via `ddb.uuid.parent`.
#[test]
fn virtualbox_snapshot_vmdk_finds_its_parent_by_uuid() {
let dir = tempfile::tempdir().unwrap();
let sub = dir.path().join("Snapshots");
std::fs::create_dir(&sub).unwrap();
// o pai fica na pasta de cima, como o VirtualBox organiza a máquina
// parent VMDK sits in the parent directory, matching VirtualBox's `Snapshots/` layout
std::fs::copy(fx("disk.vmdk"), dir.path().join("disco-base.vmdk")).unwrap();
std::fs::copy(fx("vbox-snapshot.vmdk"), sub.join("filho.vmdk")).unwrap();
check_content(&sub.join("filho.vmdk"), &raw());
// sem o pai por perto, a abertura explica
// when no VMDK with the parent UUID exists nearby, opening returns a descriptive error
std::fs::remove_file(dir.path().join("disco-base.vmdk")).unwrap();
let e = open_path(&sub.join("filho.vmdk"))
.err()
@@ -516,7 +516,7 @@ fn vhd_fixed_hides_the_footer() {
check_content(&p, &want);
}
/// VHD diferencial: a filha só traz o setor 100 e o bloco inteiro do fim; o resto vem do pai.
/// Builds a differencing VHD where only sector 100 of block 0 and all of block 1 are present in the child.
#[test]
fn vhd_differencing_reads_the_parent_for_missing_sectors() {
let want = raw();
@@ -525,7 +525,7 @@ fn vhd_differencing_reads_the_parent_for_missing_sectors() {
parent.extend_from_slice(&vhd_footer(want.len() as u64, 2, u64::MAX));
std::fs::write(dir.path().join("pai.vhd"), &parent).unwrap();
const BLOCK: usize = 1 << 20; // um bloco só
const BLOCK: usize = 1 << 20; // single 2 MiB block
let mut f = vec![0u8; 512 + 1024 + 512];
let hdr = vhd_footer(want.len() as u64, 4, 512);
f[..512].copy_from_slice(&hdr);
@@ -541,9 +541,9 @@ fn vhd_differencing_reads_the_parent_for_missing_sectors() {
.collect::<Vec<u8>>();
dh[64..64 + name.len()].copy_from_slice(&name);
f[512..1536].copy_from_slice(&dh);
be(&mut f, 1536, 4, 4); // bloco 0 no setor 4
be(&mut f, 1536, 4, 4); // block 0 starts at sector 4
f.resize(4 * 512, 0);
// bitmap (4096 setores = 512 bytes) com o setor 100 presente, depois os dados
// 512-byte sector bitmap (4096 sectors) with only sector 100 marked present, followed by data
let mut bitmap = vec![0u8; 512];
bitmap[100 / 8] |= 0x80 >> (100 % 8);
f.extend_from_slice(&bitmap);
@@ -559,7 +559,7 @@ fn vhd_differencing_reads_the_parent_for_missing_sectors() {
expect[100 * 512..101 * 512].copy_from_slice(&marker);
check_content(&p, &expect);
// sem o pai a abertura falha com o motivo
// missing parent VHD returns a descriptive error
std::fs::remove_file(dir.path().join("pai.vhd")).unwrap();
let e = open_path(&p).err().expect("deveria recusar").to_string();
assert!(e.contains("disco pai não encontrado"), "{e}");
@@ -584,8 +584,8 @@ fn guid(s: &str) -> [u8; 16] {
const MB: usize = 1 << 20;
/// VHDX com blocos de 1 MiB. `blocks[i]`: `None` = não presente, `Some((estado, dados))`.
/// Com `parent`, acrescenta o localizador de pai e o bitmap de setores (`bitmap`).
/// Builds a VHDX image with 1 MiB blocks (`None` = not present, `Some((state, data))`).
/// When `parent` is provided, writes a VHDX parent locator and sector bitmap block.
fn build_vhdx(
size: u64,
blocks: &[Option<(u64, Vec<u8>)>],
@@ -596,7 +596,7 @@ fn build_vhdx(
img[..8].copy_from_slice(b"vhdxfile");
img[64 * 1024..64 * 1024 + 4].copy_from_slice(b"head");
img[128 * 1024..128 * 1024 + 4].copy_from_slice(b"head");
// tabela de regiões: BAT em 2 MiB, metadados em 1 MiB
// region table: BAT at 2 MiB, metadata region at 1 MiB
let rt = 192 * 1024;
img[rt..rt + 4].copy_from_slice(b"regi");
le(&mut img, rt + 8, 2, 4);
@@ -606,7 +606,7 @@ fn build_vhdx(
img[rt + 48..rt + 64].copy_from_slice(&guid("8B7CA206-4790-4B9A-B8FE-575F050F886E"));
le(&mut img, rt + 64, MB as u64, 8);
le(&mut img, rt + 72, MB as u64, 4);
// metadados
// metadata region items
let m = MB;
img[m..m + 8].copy_from_slice(b"metadata");
let mut items: Vec<([u8; 16], Vec<u8>)> = vec![];
@@ -650,7 +650,7 @@ fn build_vhdx(
img[m + at..m + at + data.len()].copy_from_slice(data);
at += data.len().div_ceil(8) * 8;
}
// blocos de dados a partir de 4 MiB; o BAT guarda estado e deslocamento (em MiB)
// payload blocks start at 4 MiB; BAT entries encode state in low 3 bits and file offset
let ratio = (1usize << 23) * 512 / MB;
let bat = 2 * MB;
for (i, b) in blocks.iter().enumerate() {
@@ -681,7 +681,7 @@ fn build_vhdx(
#[test]
fn vhdx_dynamic_with_zero_and_missing_blocks() {
let want = raw(); // 1 MiB = um bloco
let want = raw(); // 1 MiB = single payload block
let mut expect = want.clone();
expect.extend_from_slice(&vec![0u8; 3 * MB]);
let tail = vec![0x5Au8; 100_000];
@@ -690,8 +690,8 @@ fn vhdx_dynamic_with_zero_and_missing_blocks() {
4 * MB as u64,
&[
Some((6, want.clone())),
Some((2, vec![])), // zerado
None, // não presente
Some((2, vec![])), // `PAYLOAD_BLOCK_ZERO`
None, // `PAYLOAD_BLOCK_NOT_PRESENT`
Some((6, tail)),
],
None,
@@ -708,7 +708,7 @@ fn vhdx_differencing_uses_the_sector_bitmap() {
let want = raw();
let dir = tempfile::tempdir().unwrap();
std::fs::write(dir.path().join("pai.raw"), &want).unwrap();
// filho: bloco 0 parcialmente presente, só com os setores 3 e 2000..2010 no filho
// differencing child: block 0 is `PAYLOAD_BLOCK_PARTIALLY_PRESENT` with sectors 3 and 2000..2010 in child
let mut data = vec![0u8; MB];
let mut bm = vec![0u8; 512];
let mut expect = want.clone();
@@ -729,7 +729,7 @@ fn vhdx_differencing_uses_the_sector_bitmap() {
fn dmg_with_every_chunk_type() {
let want = raw();
let sec = 512usize;
// 8 chunks de 128 setores (64 KiB)
// 8 chunks of 128 sectors (64 KiB each)
let part = |i: usize| want[i * 128 * sec..(i + 1) * 128 * sec].to_vec();
let (adc_enc, adc_want) = adc_pattern(128 * sec);
let mut bz = bzip2::write::BzEncoder::new(vec![], bzip2::Compression::default());
@@ -739,12 +739,12 @@ fn dmg_with_every_chunk_type() {
ty: 1,
sectors: 128,
payload: part(0),
}, // bruto
}, // raw uncompressed chunk (`0x00000001`)
DmgChunk {
ty: 0,
sectors: 128,
payload: vec![],
}, // zerado
}, // `PAYLOAD_BLOCK_ZERO`
DmgChunk {
ty: 0x8000_0005,
sectors: 128,
@@ -764,7 +764,7 @@ fn dmg_with_every_chunk_type() {
ty: 2,
sectors: 128,
payload: vec![],
}, // ignorado
}, // ignored/comment chunk
DmgChunk {
ty: 0x8000_0005,
sectors: 128,
@@ -785,8 +785,8 @@ fn dmg_with_every_chunk_type() {
check_content(&p, &expect);
}
/// Chunks `ULFO` com dados compactados pelo compressor de referência da Apple (`lzfse`):
/// um bloco `bvx2` (FSE, 64 KiB) e um pequeno (`bvxn`/LZVN).
/// DMG with `ULFO` (LZFSE) chunks compressed by Apple's reference `lzfse` implementation:
/// one 64 KiB `bvx2` (FSE) block and one small `bvxn` (LZVN) block.
#[test]
fn dmg_lzfse_chunks_from_the_reference_compressor() {
let big = std::fs::read(fx("lzfse-64k.bin")).unwrap();
@@ -835,11 +835,11 @@ fn dmg_with_a_corrupt_lzfse_chunk_and_encrypted_are_refused_with_a_reason() {
assert!(e.contains("cifrado"), "{e}");
}
// ---------- imagens reais do dfvfs (target/testdata) ----------
// ---------- external `dfvfs` reference images (`target/testdata`) ----------
#[test]
fn dfvfs_images_match_a_reference_reader() {
// hashes do disco virtual inteiro, conferidos com o 7-Zip (árvore de arquivos e partições)
// full virtual disk SHA-256 digests cross-checked against `7z` extraction
let table: &[(&str, &str)] = &[
(
"ext2.vhdx",
+20 -20
View File
@@ -1,5 +1,5 @@
//! XFS: imagem real do mkfs.xfs + exclusão simulada (inode liberado e entrada de diretório
//! liberada, como o kernel deixa) e a imagem de referência do dfvfs.
//! XFS: real `mkfs.xfs` image + simulated unlink (freed inode with leftover extents and freed
//! directory entry, matching XFS v4/unwiped behavior) plus the `dfvfs` reference image.
mod common;
use common::*;
@@ -135,19 +135,19 @@ fn deleted_file_is_recovered_from_free_inode_and_stale_directory_entry() {
let mut b = std::fs::read(&img).unwrap();
let bs = 4096usize;
// extents do arquivo (no inode, formato 3)
// read the file's inline extent list before simulating deletion
assert_eq!(b[ino_off + 5], 2);
let nex = be32(&b[ino_off..], 76) as usize;
let exts: Vec<_> = (0..nex)
.map(|i| unpack(&b[ino_off + 176 + i * 16..]))
.collect();
assert!(exts.iter().map(|e| e.2).sum::<u64>() * 4096 >= 150_000);
// 1) libera o inode como o kernel: modo 0, links 0, tamanho 0, 0 extents
// 1) free the inode (mode = 0, nlink = 0, size = 0, nextents = 0) while leaving fork bytes
b[ino_off + 2..ino_off + 4].fill(0);
b[ino_off + 16..ino_off + 20].fill(0);
b[ino_off + 56..ino_off + 64].fill(0);
b[ino_off + 76..ino_off + 80].fill(0);
// 2) marca livre na árvore de inodes (AGI -> raiz folha)
// 2) mark the inode bit as free in the AGI `inobt` root leaf
let agno = (ino >> (agblklog + inopblog)) as usize;
let ag_off = agno * agblocks as usize * bs;
let agi = &b[ag_off + 2 * 512..ag_off + 2 * 512 + 64];
@@ -169,7 +169,7 @@ fn deleted_file_is_recovered_from_free_inode_and_stale_directory_entry() {
}
}
assert!(done);
// 3) devolve os blocos à árvore de espaço livre (folha única)
// 3) return the file's data blocks to the AGF `bnobt` free-space B+tree leaf
let agf = &b[ag_off + 512..ag_off + 512 + 64].to_vec();
let (broot, blevel) = (be32(agf, 16) as usize, be32(agf, 28));
assert_eq!(blevel, 1);
@@ -199,7 +199,7 @@ fn deleted_file_is_recovered_from_free_inode_and_stale_directory_entry() {
b[bb + 56 + i * 8 + 4..bb + 56 + i * 8 + 8].copy_from_slice(&r.1.to_be_bytes());
}
b[bb + 6..bb + 8].copy_from_slice(&(merged.len() as u16).to_be_bytes());
// 4) entrada de diretório liberada: perde os 4 primeiros bytes do inode (viram marca + tamanho)
// 4) mark the directory entry freed: overwrite the upper 4 bytes of its 8-byte inode with `(0xFFFF, length)`
let name = b"relatorio.dat";
let pos = (0..b.len() - 20)
.find(|&p| {
@@ -241,10 +241,10 @@ fn deleted_file_is_recovered_from_free_inode_and_stale_directory_entry() {
assert!(rep.failed.is_empty());
let rp = PathBuf::from(&rep.recovered[0].path);
let got = std::fs::read(if rp.is_absolute() { rp } else { out.join(rp) }).unwrap();
// O tamanho exato se perde na exclusão: o arquivo vem com os blocos inteiros (150000 bytes + preenchimento).
// Exact byte size was zeroed on deletion: recovered size is rounded up to full filesystem blocks.
assert!(got.starts_with(&doc), "conteúdo inicial difere");
assert_eq!(got.len() % 4096, 0);
// Só um arquivo apagado.
// Only the deleted file is reported (live files are excluded).
assert_eq!(
c.found.iter().filter(|f| f.origin == Origin::Xfs).count(),
1
@@ -269,14 +269,14 @@ fn reference_image_is_recognized() {
assert_eq!(l.partitions[0].fs, FsKind::Xfs);
}
/// Meta: exclusão feita pelo kernel (scripts/make-xfs-real-deletion.sh, precisa de root): cada
/// arquivo apagado volta pelo nome, na pasta certa, com o conteúdo original no começo, e os que
/// ficaram não aparecem como apagados.
/// Target goal for kernel-level XFS v5 unlink (`scripts/make-xfs-real-deletion.sh`, requires root):
/// each deleted file should be recovered with its original name, folder, and content without
/// reporting live files as deleted.
///
/// Currently `#[ignore]` because on XFS v5 the Linux kernel zeroes the inode fork on unlink,
/// so recovering filenames and extents from a real XFS v5 unlink requires parsing the XFS
/// journal log. Run with `cargo test -p guara-core --test xfs -- --ignored`.
///
/// HOJE FALHA, e é por isso que está `ignore`: num XFS v5 real o kernel zera o inode e a lista de
/// extensões ao apagar, então só a simulação (teste acima) deixa a metadados para ler. Recuperar
/// nome e conteúdo de um XFS real exige ler o diário (log) do sistema de arquivos. Rodar com
/// `cargo test -p guara-core --test xfs -- --ignored` para ver o estado.
#[test]
#[ignore = "XFS real: o kernel apaga os metadados; falta ler o diário (ver ROADMAP)"]
fn real_kernel_deletion_names_are_recovered() {
@@ -358,7 +358,7 @@ fn real_kernel_deletion_names_are_recovered() {
out.path().join(name).join(rp)
})
.unwrap();
// o tamanho exato se perde na exclusão: o arquivo vem com os blocos inteiros
// exact byte size is lost on unlink: recovered size is rounded up to full blocks
assert!(
got.len() as u64 >= *size,
"{rel}: veio menor ({} < {size})",
@@ -372,9 +372,9 @@ fn real_kernel_deletion_names_are_recovered() {
}
}
/// O que funciona hoje num XFS apagado de verdade: o conteúdo dos blocos livres continua lá e a
/// varredura profunda recupera, por carving, o arquivo com estrutura reconhecível (SQLite), com
/// o conteúdo idêntico ao original. O nome e a pasta se perdem.
/// What works today on a real kernel-unlinked XFS v5 image: the freed data blocks remain intact
/// on disk and deep scan recovers structured files (such as SQLite) via signature carving with
/// bit-identical content (though original filename and folder path are lost).
#[test]
fn real_kernel_deletion_content_is_recovered_by_carving() {
let dir =