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