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

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

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

No files matched your search

+28 -28
View File
@@ -1,7 +1,7 @@
//! APFS (volumes sem cifragem): lê o contêiner, o mapa de objetos e a árvore de arquivos de cada
//! volume; arquivos apagados vêm de duas fontes: as árvores dos instantâneos (snapshots) e os nós
//! antigos da árvore (copy-on-write) que ainda estão no disco. Volumes cifrados (FileVault) são
//! detectados e deixados para a varredura por assinatura.
//! APFS (unencrypted volumes): parses the container, object map, and file system tree of each
//! volume; deleted files are recovered from two sources: snapshot trees and older copy-on-write
//! B-tree nodes still resident on disk. Encrypted volumes (FileVault) are detected and left for
//! signature carving unless unlocked.
use super::{make_found, merge_extents, Ctx, NewFound, VolumeScan};
use crate::source::{read_vec, Source};
@@ -32,13 +32,13 @@ const T_CIB: u32 = 7;
const T_CHECKPOINT_MAP: u32 = 0x0C;
const SUBTYPE_FSTREE: u32 = 0x0E;
// tipos de registro da árvore de arquivos
// filesystem B-tree record types
const R_SNAP_METADATA: u8 = 1;
const R_INODE: u8 = 3;
const R_FILE_EXTENT: u8 = 8;
const R_DIR_REC: u8 = 9;
/// Soma de verificação Fletcher-64 do APFS (cobre o bloco menos os 8 bytes iniciais).
/// APFS Fletcher-64 checksum (covers the block excluding the leading 8 bytes).
pub fn fletcher64(data: &[u8]) -> u64 {
let (mut s1, mut s2) = (0u64, 0u64);
for w in data.chunks_exact(4) {
@@ -60,7 +60,7 @@ struct Apfs<'a> {
struct BNode {
flags: u16,
level: u16,
/// (chave, valor) por entrada
/// `(key, value)` slice pair for each node entry
entries: Vec<(Vec<u8>, Vec<u8>)>,
oid: u64,
xid: u64,
@@ -124,7 +124,7 @@ impl<'a> Apfs<'a> {
})
}
/// Todas as entradas de uma árvore física (mapa de objetos): filhos são endereços físicos.
/// Collects all entries from a physical B-tree (object map): child pointers are physical block addresses.
fn walk_physical(
&self,
root: u64,
@@ -156,7 +156,7 @@ impl<'a> Apfs<'a> {
}
}
/// Mapa de objetos: oid -> [(xid, paddr)] ordenado por xid.
/// Object map: `oid -> [(xid, paddr)]` sorted by transaction ID (`xid`).
fn omap(&self, omap_paddr: u64) -> HashMap<u64, Vec<(u64, u64)>> {
let mut m: HashMap<u64, Vec<(u64, u64)>> = HashMap::new();
let Some(ob) = self.checked_block(omap_paddr) else {
@@ -174,7 +174,7 @@ impl<'a> Apfs<'a> {
m
}
/// Endereço físico de um objeto ephemeral (spaceman), achado no mapa de checkpoint mais novo.
/// Physical block address of an ephemeral object (spaceman) from the latest checkpoint map.
fn ephemeral_paddr(&self, nx: &[u8], oid: u64) -> Option<u64> {
let desc_blocks = le32(nx, 104) as u64;
let desc_base = le64(nx, 112);
@@ -203,8 +203,8 @@ impl<'a> Apfs<'a> {
best.map(|(_, p)| p)
}
/// Mapa de espaço livre do contêiner: percorre o spaceman, os chunk-info blocks e os bitmaps
/// (bit 1 = bloco em uso).
/// Container free-space map: walks the spaceman, chunk-info blocks (CIBs), and allocation bitmaps
/// (bit 1 = allocated block).
fn free_map(&self, nx: &[u8]) -> Option<Vec<Extent>> {
let sm_oid = le64(nx, 152);
if sm_oid == 0 {
@@ -214,7 +214,7 @@ impl<'a> Apfs<'a> {
if le32(&sm, 24) & OBJ_TYPE_MASK != T_SPACEMAN {
return None;
}
// Com CABs (contêineres grandes) os endereços dos CIBs não ficam no spaceman.
// With CABs (large containers), CIB addresses live in chunk-info address blocks rather than inline in the spaceman.
if le32(&sm, 68) != 0 {
return None;
}
@@ -289,7 +289,7 @@ fn lookup(m: &HashMap<u64, Vec<(u64, u64)>>, oid: u64, max_xid: u64) -> Option<u
.map(|(_, p)| *p)
}
// ---------------------------------------------------------------- registros
// ---------------------------------------------------------------- records
#[derive(Clone, Debug, Default)]
struct Inode {
@@ -308,7 +308,7 @@ struct Inode {
#[derive(Clone, Debug, Default)]
struct FileRec {
inode: Inode,
names: BTreeMap<u64, String>, // pai -> nome
names: BTreeMap<u64, String>, // parent_id -> name
}
type Files = HashMap<u64, FileRec>;
@@ -327,7 +327,7 @@ fn add_record(files: &mut Files, extents: &mut Extents, k: &[u8], v: &[u8]) {
let mut ino = Inode {
parent: le64(v, 0),
private_id: le64(v, 8),
ctime: ns(le64(v, 16)), // criação
ctime: ns(le64(v, 16)), // creation timestamp
mtime: ns(le64(v, 24)),
atime: ns(le64(v, 40)),
mode: le16(v, 80),
@@ -338,7 +338,7 @@ fn add_record(files: &mut Files, extents: &mut Extents, k: &[u8], v: &[u8]) {
let flags = le64(v, 48);
ino.compressed = le32(v, 68) & 0x20 != 0;
let _ = flags;
// campos estendidos
// xfields (extended fields)
let xf = &v[92..];
if xf.len() >= 4 {
let n = le16(xf, 0) as usize;
@@ -383,7 +383,7 @@ fn add_record(files: &mut Files, extents: &mut Extents, k: &[u8], v: &[u8]) {
}
impl Apfs<'_> {
/// Percorre a árvore de arquivos a partir de uma raiz virtual.
/// Walks the filesystem B-tree starting from a virtual root OID.
fn walk_fstree(
&self,
root_oid: u64,
@@ -484,7 +484,7 @@ fn open_container<'a>(src: &'a dyn Source, base: u64) -> Option<(Apfs<'a>, Vec<u
bs,
blocks,
};
// O superbloco mais recente está na área de descritores de checkpoint.
// Find the latest valid container superblock in the checkpoint descriptor area.
let desc_blocks = le32(&b0, 104) as u64;
let desc_base = le64(&b0, 112);
let mut best = b0.clone();
@@ -548,7 +548,7 @@ pub fn scan(ctx: &mut Ctx, base: u64, part_len: u64) -> VolumeScan {
fn scan_volume(ctx: &mut Ctx, a: &Apfs, v: &Volume, allow_stale_scan: bool, res: &mut VolumeScan) {
let omap = a.omap(v.omap_paddr);
let current_xid = u64::MAX;
// Árvore atual.
// Current live filesystem tree.
let mut live: Files = HashMap::new();
let mut live_ext: Extents = HashMap::new();
let mut live_nodes: HashSet<u64> = HashSet::new();
@@ -556,7 +556,7 @@ fn scan_volume(ctx: &mut Ctx, a: &Apfs, v: &Volume, allow_stale_scan: bool, res:
live_nodes.insert(node);
add_record(&mut live, &mut live_ext, k, val);
});
// Instantâneos.
// Volume snapshots.
let mut stale: Files = HashMap::new();
let mut stale_ext: Extents = HashMap::new();
if v.snap_tree_oid != 0 {
@@ -583,7 +583,7 @@ fn scan_volume(ctx: &mut Ctx, a: &Apfs, v: &Volume, allow_stale_scan: bool, res:
}
} else {
for (_, val) in &n.entries {
// árvore de metadados de snapshot: filhos físicos ou virtuais
// snapshot metadata tree: physical or virtual child references
let c = le64(val, 0);
stack.push(lookup(&omap, c, current_xid).unwrap_or(c));
}
@@ -602,7 +602,7 @@ fn scan_volume(ctx: &mut Ctx, a: &Apfs, v: &Volume, allow_stale_scan: bool, res:
}
}
}
// Nós antigos da árvore de arquivos.
// Older evicted copy-on-write filesystem tree leaf nodes.
if allow_stale_scan {
let total = a.blocks;
for p in 0..total {
@@ -631,7 +631,7 @@ fn scan_volume(ctx: &mut Ctx, a: &Apfs, v: &Volume, allow_stale_scan: bool, res:
}
}
}
// Arquivos que só existem nas versões antigas.
// Files present only in older tree versions or snapshots.
let mut ids: Vec<&u64> = stale.keys().collect();
ids.sort();
let mut seen_first: HashSet<u64> = HashSet::new();
@@ -648,9 +648,9 @@ fn scan_volume(ctx: &mut Ctx, a: &Apfs, v: &Volume, allow_stale_scan: bool, res:
continue;
};
if ex.values().any(|e| e.2 != 0) {
continue; // extensão cifrada
continue; // encrypted extent
}
// nome e pasta: da entrada de diretório, senão do próprio inode
// file name and parent folder: prefer directory entry, fallback to inode xfield
let (name, parent) = match rec.names.iter().next() {
Some((p, n)) => (n.clone(), *p),
None => match &ino.name {
@@ -726,13 +726,13 @@ fn scan_volume(ctx: &mut Ctx, a: &Apfs, v: &Volume, allow_stale_scan: bool, res:
let _ = res;
}
/// Trechos livres (absolutos) do contêiner, segundo o spaceman.
/// Absolute free byte extents in the container according to the spaceman.
pub fn free_extents(src: &dyn Source, base: u64) -> Option<Vec<Extent>> {
let (a, nx) = open_container(src, base)?;
a.free_map(&nx)
}
/// Arquivos e pastas atuais do primeiro volume sem cifragem (caminho, tamanho, é pasta).
/// Live files and folders in the first unencrypted volume `(path, size, is_dir)`.
pub fn live_paths(src: &dyn Source, base: u64) -> Vec<(String, u64, bool)> {
let Some((a, nx)) = open_container(src, base) else {
return vec![];
+30 -30
View File
@@ -1,8 +1,8 @@
//! Btrfs (um dispositivo; perfis single, DUP e RAID 1): árvores atuais para nomes e mapa de
//! espaço livre, e arquivos apagados achados em nós antigos das árvores. Como o Btrfs nunca
//! sobrescreve metadados no lugar (copy-on-write), as versões anteriores dos nós continuam no
//! disco até o espaço ser reaproveitado: varremos os blocos de metadata em busca delas.
//! Extensões comprimidas (zlib, zstd, lzo) são descompactadas na recuperação.
//! Btrfs (single device; single, DUP, and RAID 1 profiles): reads live trees for paths and
//! free-space mapping, and recovers deleted files from historical copy-on-write B-tree nodes.
//! Because Btrfs never overwrites metadata in place, prior leaf generations remain on disk
//! until reallocated: we scan metadata chunks for those older leaves.
//! Compressed extents (zlib, zstd, LZO) are decompressed on recovery.
use super::{make_found, merge_extents, Ctx, NewFound, VolumeScan};
use crate::source::{read_vec, Source};
@@ -12,7 +12,7 @@ use std::collections::{BTreeMap, HashMap, HashSet};
const MAGIC: &[u8; 8] = b"_BHRfS_M";
const SB_OFF: u64 = 0x10000;
// chaves
// key item types
const INODE_ITEM: u8 = 1;
const INODE_REF: u8 = 12;
const EXTENT_DATA: u8 = 108;
@@ -81,7 +81,7 @@ struct Chunk {
logical: u64,
len: u64,
ty: u64,
/// (offset físico do primeiro stripe) por cópia
/// Physical byte offsets of each stripe replica.
phys: Vec<u64>,
}
@@ -153,7 +153,7 @@ impl<'a> Fs<'a> {
fs.chunks.push(ch);
p += 17 + used;
}
// chunk tree completa
// full chunk tree
let mut more = vec![];
let cr = fs.chunk_root;
fs.walk(cr, &mut |k, d| {
@@ -241,7 +241,7 @@ impl<'a> Fs<'a> {
})
}
/// Percorre uma árvore em ordem, chamando `f` para cada item de folha.
/// Walks a B-tree in order, invoking `f` for each leaf item.
fn walk(&self, root: u64, f: &mut dyn FnMut(&Key, &[u8])) {
let mut seen = HashSet::new();
self.walk_inner(root, f, &mut seen, &mut |_| {}, 0);
@@ -314,7 +314,7 @@ struct InodeData {
atime: i64,
has_inode: bool,
refs: HashMap<u64, String>,
/// offset no arquivo -> (geração do nó, item)
/// `file_offset -> (node_generation, extent_item)`
extents: BTreeMap<u64, (u64, Vec<u8>)>,
}
@@ -377,8 +377,8 @@ pub fn scan(ctx: &mut Ctx, base: u64, part_len: u64) -> VolumeScan {
return res;
}
// 1) Árvores atuais: raízes das subvolumes e extents alocados.
let mut roots: Vec<(u64, u64)> = vec![]; // (id da árvore, bytenr)
// 1) Live trees: subvolume roots and allocated extents.
let mut roots: Vec<(u64, u64)> = vec![]; // (tree_id, root_bytenr)
let mut extent_root = 0u64;
let rr = fs.root;
fs.walk(rr, &mut |k, d| {
@@ -416,7 +416,7 @@ pub fn scan(ctx: &mut Ctx, base: u64, part_len: u64) -> VolumeScan {
}
}
// Extents alocados (árvore de extents) e mapa livre dos blocos de dados.
// Allocated extents (from the extent tree) and free-space map across data chunks.
let mut allocated: Vec<Extent> = vec![];
if extent_root != 0 {
let nodesize = fs.nodesize;
@@ -431,7 +431,7 @@ pub fn scan(ctx: &mut Ctx, base: u64, part_len: u64) -> VolumeScan {
let allocated = merge_extents(allocated);
let mut free: Vec<Extent> = vec![];
for c in fs.chunks.iter().filter(|c| c.ty & 0x1 != 0) {
// blocos de dados: livres = chunk - alocado
// data chunks: free = chunk_range - allocated_extents
let mut cur = c.logical;
let i = allocated.partition_point(|e| e.end() <= c.logical);
for e in &allocated[i..] {
@@ -453,7 +453,7 @@ pub fn scan(ctx: &mut Ctx, base: u64, part_len: u64) -> VolumeScan {
}
res.free = Some(merge_extents(free));
// 2) Varre os blocos de metadata atrás de versões antigas de folhas de árvores de arquivos.
// 2) Scan metadata chunks for older copy-on-write leaves of filesystem trees.
let mut stale: Files = HashMap::new();
let ns = fs.nodesize as usize;
let step = fs.sectorsize as usize;
@@ -482,7 +482,7 @@ pub fn scan(ctx: &mut Ctx, base: u64, part_len: u64) -> VolumeScan {
let mut i = 0usize;
while i + ns <= buf.len() && (off + i as u64) < c.len {
let logical = c.logical + off + i as u64;
// cabeçalho barato antes do CRC
// cheap header sanity check before computing CRC32C
if buf[i + 32..i + 48] == fs.fsid
&& le64(&buf[i..], 48) == logical
&& buf[i + 100] == 0
@@ -505,7 +505,7 @@ pub fn scan(ctx: &mut Ctx, base: u64, part_len: u64) -> VolumeScan {
}
}
// 3) Arquivos que só existem nas folhas antigas.
// 3) Files that only exist in historical leaves.
let path_of = |owner: u64, mut ino: u64| -> String {
let mut parts: Vec<String> = vec![];
for _ in 0..64 {
@@ -584,7 +584,7 @@ pub fn scan(ctx: &mut Ctx, base: u64, part_len: u64) -> VolumeScan {
res
}
/// Converte os itens EXTENT_DATA em extents físicos ou partes (inline/comprimidas).
/// Converts `EXTENT_DATA` items into physical extents or composite parts (inline/compressed).
fn build_data(fs: &Fs, e: &InodeData, allocated: &[Extent]) -> Option<(DataLoc, bool, bool)> {
let mut parts: Vec<Part> = vec![];
let mut pos = 0u64;
@@ -594,7 +594,7 @@ fn build_data(fs: &Fs, e: &InodeData, allocated: &[Extent]) -> Option<(DataLoc,
let mut hole = |parts: &mut Vec<Part>, len: u64| {
if len > 0 {
parts.push(Part::Run { offset: 0, len });
// marcador: offset 0 + sparse tratado abaixo
// placeholder: offset 0 + sparse hole handled below
}
};
let _ = &mut hole;
@@ -697,7 +697,7 @@ fn build_data(fs: &Fs, e: &InodeData, allocated: &[Extent]) -> Option<(DataLoc,
.collect();
return Some((DataLoc::Extents { extents: ex }, any_alloc, all_ok));
}
// Buracos viram trechos esparsos: Part::Run com offset 0 não serve; usa Bytes de zeros pequenos.
// Sparse holes: `Part::Run` with offset 0 is not used here; represent holes accordingly.
let mut out: Vec<Part> = vec![];
for (i, p) in parts.into_iter().enumerate() {
if sparse_marks.contains(&i) {
@@ -732,8 +732,8 @@ fn decode(codec: u8, raw: &[u8], ram: usize) -> Option<Vec<u8>> {
}
}
/// Formato de extensão LZO do Btrfs: tamanho total (u32) + segmentos [tamanho (u32)][dados LZO1X]
/// de até um setor de 4 KiB descomprimido cada, sem cruzar fronteiras de setor.
/// Btrfs LZO extent format: total length (`u32`) + `[seg_len (u32)][LZO1X payload]` segments
/// of up to one 4 KiB uncompressed sector each, padded at sector boundaries.
pub fn lzo_btrfs(raw: &[u8], ram: usize) -> Option<Vec<u8>> {
if raw.len() < 4 {
return None;
@@ -742,7 +742,7 @@ pub fn lzo_btrfs(raw: &[u8], ram: usize) -> Option<Vec<u8>> {
let mut out = Vec::with_capacity(ram);
let mut p = 4usize;
while p + 4 <= raw.len().min(total) && out.len() < ram {
// pula o preenchimento até o próximo setor se sobrar menos de 4 bytes
// skip sector-boundary padding when fewer than 4 bytes remain in the sector
let in_sector = 4096 - (p % 4096);
if in_sector < 4 {
p += in_sector;
@@ -758,7 +758,7 @@ pub fn lzo_btrfs(raw: &[u8], ram: usize) -> Option<Vec<u8>> {
Some(out)
}
/// LZO1X (descompressão segura, no mesmo esquema de estados do kernel Linux).
/// Safe LZO1X decompressor (following the Linux kernel state machine).
pub fn lzo1x_decompress(src: &[u8], max_out: usize) -> Option<Vec<u8>> {
let mut out: Vec<u8> = Vec::with_capacity(max_out);
let mut ip = 0usize;
@@ -778,7 +778,7 @@ pub fn lzo1x_decompress(src: &[u8], max_out: usize) -> Option<Vec<u8>> {
*ip += 1;
Some(v)
};
// estado: literais ao fim da instrução anterior (0..3) ou 4 depois de uma corrida de literais
// state: trailing literals from the previous instruction (0..3), or 4 after a literal run
let mut state;
if byte(0)? > 17 {
let t = byte(0)? - 17;
@@ -859,7 +859,7 @@ fn copy_match(out: &mut Vec<u8>, dist: usize, len: usize) -> Option<()> {
Some(())
}
/// Arquivos e pastas atuais com caminho completo e tamanho (usa só as árvores vivas).
/// Lists live files and directories with full path and size from current subvolume trees.
pub fn live_paths(src: &dyn Source, base: u64) -> Vec<(String, u64, bool)> {
let Some(fs) = Fs::open(src, base) else {
return vec![];
@@ -900,18 +900,18 @@ pub fn live_paths(src: &dyn Source, base: u64) -> Vec<(String, u64, bool)> {
out
}
/// Posições das árvores principais, para ferramentas e testes que precisam editar a imagem.
/// Root locations of core Btrfs trees, used by tests that simulate COW deletions.
pub struct TreeLocations {
pub nodesize: u64,
/// Endereço físico (absoluto na origem) do primeiro nó de cada árvore.
/// Physical byte offset (in the source) of each tree root node.
pub root_tree: u64,
pub fs_tree: u64,
pub extent_tree: u64,
/// Endereços lógicos correspondentes.
/// Corresponding logical `bytenr` addresses.
pub root_tree_logical: u64,
pub fs_tree_logical: u64,
pub extent_tree_logical: u64,
/// Converte endereço lógico em físico.
/// Translates a logical `bytenr` to a physical source offset.
pub chunks: Vec<(u64, u64, u64)>,
}
+5 -5
View File
@@ -1,4 +1,4 @@
//! exFAT: conjuntos de entradas apagados (0x05/0x40/0x41) e bitmap de alocação.
//! exFAT: deleted directory entry sets (`0x05`/`0x40`/`0x41`) and allocation bitmap.
use super::{
make_found, merge_extents, overlaps, sanitize_name, utf16le, Ctx, NewFound, VolumeScan,
@@ -92,7 +92,7 @@ pub fn scan(ctx: &mut Ctx, base: u64) -> VolumeScan {
bitmap: vec![],
};
// Diretório raiz: procura a entrada do bitmap (0x81).
// Root directory: locate the allocation bitmap entry (0x81).
let root_chain = vol.chain(vol.root);
let root_data = vol.read(&root_chain, 64 << 20);
let mut bm: Option<(u32, u64)> = None;
@@ -115,7 +115,7 @@ pub fn scan(ctx: &mut Ctx, base: u64) -> VolumeScan {
.push("exFAT: bitmap de alocação não encontrado; confiança será menor.".into());
}
// Extents vivos: metadados (antes do heap) + clusters alocados no bitmap.
// Live extents: metadata region (before cluster heap) + clusters marked used in the bitmap.
let mut live = vec![Extent::new(base, heap_off - base)];
let mut run_start: Option<u32> = None;
for c in 2..clusters + 2 {
@@ -163,7 +163,7 @@ fn walk(
break;
}
let ty = e[0];
// 0x85 = arquivo/pasta vivo; 0x05 = apagado.
// 0x85 = live file/dir entry; 0x05 = deleted file/dir entry.
if ty != 0x85 && ty != 0x05 {
i += 1;
continue;
@@ -214,7 +214,7 @@ fn walk(
}
let need = size.div_ceil(vol.cbytes) as u32;
let cl: Vec<u32> = if no_chain || deleted {
// Apagados: a cadeia na FAT pode ter sido preservada; senão presume contíguo.
// Deleted files: follow the FAT chain if preserved, otherwise assume contiguous clusters.
let chain = if !no_chain { vol.chain(first) } else { vec![] };
if chain.len() as u32 >= need.max(1) {
chain
+35 -35
View File
@@ -1,10 +1,10 @@
//! ext2/3/4: arquivos apagados com nome, por três caminhos que se completam:
//! ext2/3/4: recovers deleted files with names via three complementary techniques:
//!
//! 1. inodes com `dtime` cujo mapa de extents ainda tem sobras legíveis;
//! 2. cópias antigas do inode guardadas no diário (jbd2), de antes da exclusão;
//! 3. entradas de diretório "escondidas" na folga do `rec_len` (dão o nome).
//! 1. deleted inodes (`dtime != 0`) whose extent tree or block pointers still have remnants;
//! 2. pre-deletion inode copies preserved in the jbd2 journal;
//! 3. unlinked directory entries hidden inside `rec_len` slack space (recovering filenames).
//!
//! Também devolve o mapa de blocos livres, para o carving varrer só o que está livre.
//! Also returns the free-block map so file carving only scans unallocated blocks.
use super::{make_found, merge_extents, Ctx, NewFound, VolumeScan};
use crate::signatures;
@@ -103,7 +103,7 @@ struct Ext<'a> {
journal_inum: u32,
}
/// Sequência de (bloco lógico, bloco físico, quantidade, não-inicializado).
/// Sequence of `(logical_block, physical_block, block_count, is_uninitialized)`.
type ExtentList = Vec<(u64, u64, u64, bool)>;
impl<'a> Ext<'a> {
@@ -201,7 +201,7 @@ impl<'a> Ext<'a> {
Inode::parse(&self.inode_bytes(ino)?)
}
/// Extents de uma árvore ext4 (raiz em `node`, 60 bytes ou um bloco).
/// Collects extents from an ext4 extent tree (`node` is the 60-byte `i_block` root or a block).
fn walk_extent_node(
&self,
node: &[u8],
@@ -248,7 +248,7 @@ impl<'a> Ext<'a> {
true
}
/// Blocos de um inode ext2/3 (mapa de blocos clássico).
/// Collects data blocks from an ext2/3 indirect block map.
fn block_map(&self, blk: &[u8; 60], max_blocks: u64) -> ExtentList {
let mut v: Vec<u64> = vec![];
let ptr = |i: usize| le32(blk, i * 4) as u64;
@@ -308,7 +308,7 @@ impl<'a> Ext<'a> {
out
}
/// Extents do arquivo (ordenados pelo bloco lógico), cortados em `size` bytes.
/// Returns file byte extents (ordered by logical block) truncated to `size` bytes.
fn file_extents(&self, ino: &Inode, size: u64) -> Vec<Extent> {
let need = size.div_ceil(self.bs);
let mut list: ExtentList = vec![];
@@ -351,7 +351,7 @@ impl<'a> Ext<'a> {
out
}
/// Mapa de blocos livres (bitmap), em extents absolutos.
/// Free-block map (from block bitmaps) as absolute byte extents.
fn free_map(&self, ctx: &Ctx) -> (Vec<Extent>, Vec<bool>) {
let mut free: Vec<Extent> = vec![];
let mut allocated_all: Vec<bool> = vec![];
@@ -418,7 +418,7 @@ struct DirEnt {
live: bool,
}
/// Lê as entradas de um bloco de diretório, inclusive as escondidas na folga.
/// Parses directory entries in a directory block, including unlinked entries in `rec_len` slack.
fn parse_dir_block(b: &[u8], max_ino: u64, out: &mut Vec<DirEnt>) {
let mut off = 0usize;
while off + 8 <= b.len() {
@@ -437,11 +437,11 @@ fn parse_dir_block(b: &[u8], max_ino: u64, out: &mut Vec<DirEnt>) {
live: true,
});
}
// Folga: entradas apagadas ficam coladas depois do fim "real" desta.
// Slack space: unlinked entries remain tucked right after the active entry's true length.
let mut s = off + if ino != 0 || nlen > 0 { real } else { 8 };
let end = off + rec;
if ino == 0 {
s = end; // entrada nula: sem folga confiável
s = end; // zeroed entry: no reliable slack to scan
}
while s + 8 <= end {
let dino = le32(b, s) as u64;
@@ -472,9 +472,9 @@ fn parse_dir_block(b: &[u8], max_ino: u64, out: &mut Vec<DirEnt>) {
}
struct Names {
/// inode -> (pasta, nome) de entradas vivas.
/// `inode -> (parent_dir, name)` from live directory entries.
live: HashMap<u64, (String, String)>,
/// inode -> (pasta, nome) de entradas apagadas (mais recente por varredura).
/// `inode -> (parent_dir, name)` from unlinked slack directory entries.
dead: HashMap<u64, (String, String)>,
}
@@ -531,9 +531,9 @@ fn walk_dirs(ext: &Ext, ctx: &Ctx) -> Names {
names
}
// ---------------------------------------------------------------- diário
// ---------------------------------------------------------------- journal
/// Cópias de inodes guardadas no diário: inode -> lista (ordem do diário) de registros brutos.
/// Inode copies found in the jbd2 journal: `inode_num -> Vec<raw_inode_bytes>` in journal order.
fn journal_inodes(ext: &Ext, ctx: &Ctx) -> HashMap<u64, Vec<Vec<u8>>> {
let mut out: HashMap<u64, Vec<Vec<u8>>> = HashMap::new();
if ext.journal_inum == 0 {
@@ -543,7 +543,7 @@ fn journal_inodes(ext: &Ext, ctx: &Ctx) -> HashMap<u64, Vec<Vec<u8>>> {
return out;
};
let jext = ext.file_extents(&jino, jino.size.min(1 << 30));
// Blocos físicos do diário em ordem lógica.
// Physical blocks of the journal in logical sequence.
let mut jblocks: Vec<u64> = vec![];
for e in &jext {
if e.sparse {
@@ -576,9 +576,9 @@ fn journal_inodes(ext: &Ext, ctx: &Ctx) -> HashMap<u64, Vec<Vec<u8>>> {
8 + if is64 { 4 } else { 0 }
};
let have_csum = csum_v2 || csum_v3;
// Tabela de inodes: faixa de blocos de cada grupo.
// Inode table block ranges for each block group.
let itable_blocks = (ext.ipg * ext.inode_size).div_ceil(ext.bs);
let mut tables: Vec<(u64, u64, u64)> = vec![]; // (início, fim, primeiro inode)
let mut tables: Vec<(u64, u64, u64)> = vec![]; // (start_block, end_block, first_inode_num)
for (g, gr) in ext.groups.iter().enumerate() {
tables.push((
gr.inode_table,
@@ -588,9 +588,9 @@ fn journal_inodes(ext: &Ext, ctx: &Ctx) -> HashMap<u64, Vec<Vec<u8>>> {
}
let n = jblocks.len();
let first = (be32(&jsb, 20) as usize).clamp(1, n);
let mut seqs: Vec<(u32, usize)> = vec![]; // (sequência, índice do descritor)
let mut seqs: Vec<(u32, usize)> = vec![]; // (commit_sequence, descriptor_index)
let mut idx = first;
// Varredura física: qualquer bloco que seja descritor, em ordem do diário.
// Scan journal blocks for descriptor blocks in sequence order.
while idx < n {
if idx % 4096 == 4095 && (ctx.cancelled)() {
break;
@@ -664,7 +664,7 @@ fn journal_inodes(ext: &Ext, ctx: &Ctx) -> HashMap<u64, Vec<Vec<u8>>> {
out
}
// -------------------------------------------------------------- varredura
// -------------------------------------------------------------- scan
pub fn scan(ctx: &mut Ctx, base: u64, part_len: u64) -> VolumeScan {
let mut res = VolumeScan::default();
@@ -680,7 +680,7 @@ pub fn scan(ctx: &mut Ctx, base: u64, part_len: u64) -> VolumeScan {
let mut emitted: HashSet<u64> = HashSet::new();
let mut seen_extents: HashSet<(u64, u64)> = HashSet::new();
// 1) Inodes apagados na tabela atual.
// 1) Deleted inodes in the current inode table.
let total = ext.groups.len() as u64;
for (g, gr) in ext.groups.iter().enumerate() {
if g % 16 == 15 && (ctx.cancelled)() {
@@ -711,7 +711,7 @@ pub fn scan(ctx: &mut Ctx, base: u64, part_len: u64) -> VolumeScan {
if !deleted || !ino.is_reg() {
continue;
}
// Melhor fonte: a cópia do diário anterior à exclusão; senão o que sobrou no inode.
// Best source: pre-deletion copy from the journal; fallback to remnants in the current inode.
let mut cand: Option<(Inode, &str)> = None;
if let Some(snaps) = journal.get(&ino_no) {
for raw in snaps.iter().rev() {
@@ -749,13 +749,13 @@ pub fn scan(ctx: &mut Ctx, base: u64, part_len: u64) -> VolumeScan {
}
}
// 2) Inodes que só existem no diário (liberados e já reaproveitados/zerados).
// 2) Inodes only preserved in the journal (already zeroed in the main inode table).
for (ino_no, snaps) in &journal {
if emitted.contains(ino_no) || names.live.contains_key(ino_no) || *ino_no < 11 {
continue;
}
let cur = ext.read_inode(*ino_no);
// Se o inode atual está vivo e é outro arquivo, não mexe.
// Skip if the inode is currently live in the filesystem.
if cur
.as_ref()
.map(|c| c.dtime == 0 && c.links > 0 && c.mode != 0)
@@ -805,7 +805,7 @@ fn emit_inode(
if ino.flags & EXT4_EXTENTS_FL != 0 {
let mut list: ExtentList = vec![];
if !ext.walk_extent_node(&ino.block, 5, &mut list, true) {
// Sobras de extent em inode truncado: cabeçalho zerado, registros ainda lá.
// Extent remnants in a truncated inode: header count was zeroed, but extent entries remain.
list = remnant_extents(ext, &ino.block);
}
if list.is_empty() {
@@ -834,7 +834,7 @@ fn emit_inode(
if !seen.insert((first.0, first.1)) {
return false;
}
// Quantos blocos de dados continuam livres? Reaproveitados => baixa confiança.
// Check how many data blocks are still unallocated; reallocated blocks lower confidence.
let mut reused = 0u64;
let mut total_blocks = 0u64;
for e in extents.iter().filter(|e| !e.sparse) {
@@ -859,7 +859,7 @@ fn emit_inode(
));
}
if estimated {
// Tamanho desconhecido: tenta achar o fim real pelo conteúdo.
// Unknown exact size (zeroed on deletion): try to refine size via format structure.
if let Some(h) = extents
.iter()
.find(|e| !e.sparse)
@@ -932,7 +932,7 @@ fn layout_from_list(ext: &Ext, list: &ExtentList, size: u64) -> Vec<Extent> {
out
}
/// Lê registros de extent deixados em i_block depois que o cabeçalho foi zerado.
/// Reads leftover extent records from `i_block` after `eh_entries` was zeroed on deletion.
fn remnant_extents(ext: &Ext, block: &[u8; 60]) -> ExtentList {
let mut out: ExtentList = vec![];
if le16(block, 0) != EXT_MAGIC || le16(block, 6) != 0 {
@@ -958,8 +958,8 @@ fn remnant_extents(ext: &Ext, block: &[u8; 60]) -> ExtentList {
out
}
/// Quão coerente é a estrutura do volume: fração das entradas de diretório cujo inode confere
/// com o tipo declarado. Serve para escolher parâmetros de RAID sem metadados.
/// Measures filesystem structural coherence: fraction of directory entries whose target inode
/// matches the declared file type. Used to score candidate metadata-less RAID parameters.
pub fn consistency_score(src: &dyn Source, base: u64) -> Option<f64> {
let ext = Ext::open(src, base)?;
let mut queue = vec![2u64];
@@ -988,8 +988,8 @@ pub fn consistency_score(src: &dyn Source, base: u64) -> Option<f64> {
o += ext.bs;
}
}
// A entrada "." de um diretório aponta para ele mesmo: só confere se inode e bloco
// vieram do lugar certo.
// A directory's "." entry points to itself: only matches if both the inode table and
// directory data block were mapped from the right RAID stripe.
dots_total += 1;
if ents.iter().any(|e| e.live && e.name == "." && e.ino == d) {
dots_ok += 1;
+9 -9
View File
@@ -1,5 +1,5 @@
//! FAT12/16/32: entradas de diretório apagadas (inclusive nomes longos) e
//! reconstrução dos clusters pela regra "próximos clusters livres".
//! FAT12/16/32: deleted directory entries (including VFAT Long File Names) and
//! cluster chain reconstruction using contiguous free clusters.
use super::{make_found, merge_extents, sanitize_name, utf16le, Ctx, NewFound, VolumeScan};
use crate::source::{read_vec, Source};
@@ -118,7 +118,7 @@ impl<'a> Fat<'a> {
}
}
/// Cadeia de clusters viva a partir de `start` (com limite contra ciclos).
/// Follows a live FAT cluster chain from `start` (bounded against cycles).
fn chain(&self, start: u32) -> Vec<u32> {
let mut out = vec![];
let mut c = start;
@@ -172,7 +172,7 @@ pub fn scan(ctx: &mut Ctx, base: u64, kind: FsKind) -> VolumeScan {
.push("FAT: setor de boot ilegível; usando apenas assinatura.".into());
return res;
};
// Estruturas do sistema (reservado + FATs + raiz fixa) são "vivas".
// Filesystem structures (reserved sectors + FATs + fixed root dir) count as allocated.
let mut live = vec![Extent::new(base, fat.data_off - base)];
let mut w = Walk {
fat: &fat,
@@ -257,7 +257,7 @@ impl Walk<'_, '_, '_, '_> {
continue;
}
if deleted || in_deleted {
// Diretório apagado: lê clusters contíguos livres enquanto parecerem entradas.
// Deleted directory: read contiguous free clusters while they look like directory entries.
let cl = self.contiguous_free(start, 16);
let data = self
.fat
@@ -359,7 +359,7 @@ impl Walk<'_, '_, '_, '_> {
}
}
/// Corta a lista de extents para exatamente `size` bytes.
/// Truncates an extent list to cover at most `size` bytes.
pub fn trim_extents(mut ex: Vec<Extent>, size: u64) -> Vec<Extent> {
let mut left = size;
let mut out = vec![];
@@ -416,15 +416,15 @@ fn long_name(parts: &[(u8, [u16; 13])], short: &[u8; 11]) -> Option<String> {
for &b in short {
sum = (sum >> 1).wrapping_add((sum & 1) << 7).wrapping_add(b);
}
// Entradas LFN de um arquivo apagado preservam o checksum do nome curto original,
// porém o primeiro byte do nome curto virou 0xE5; tentamos as duas variantes.
// Deleted LFN entries preserve the checksum of the original 8.3 short name,
// whereas the short entry's first byte became 0xE5; check both variants.
let ok = parts.iter().all(|p| p.0 == parts[0].0);
if !ok {
return None;
}
let target = parts[0].0;
if sum != target {
// Nome curto apagado perdeu o 1º caractere: aceita se o resto bater por tentativa.
// Deleted 8.3 name lost its first byte: accept LFN checksum if any valid first byte matches.
let mut hit = false;
for c in 0u8..=255 {
let mut s2 = *short;
+20 -20
View File
@@ -1,8 +1,8 @@
//! HFS+ / HFSX (inclusive volumes embutidos num wrapper HFS clássico).
//! HFS+ / HFSX (including volumes embedded inside a classic HFS wrapper).
//!
//! Lê o catálogo (árvore B+) para nomes e caminhos, o bitmap de alocação para o mapa de
//! espaço livre e procura arquivos apagados em três lugares: sobras dentro dos nós folha do
//! catálogo, cópias antigas de nós no diário (journal) e registros de arquivo "soltos".
//! Reads the catalog B-tree for names and paths, the allocation bitmap for free-space
//! mapping, and searches for deleted files in three places: slack space inside catalog leaf
//! nodes, older leaf node copies in the journal, and orphaned file records.
use super::{make_found, merge_extents, Ctx, NewFound, VolumeScan};
use crate::source::{read_vec, Source};
@@ -49,7 +49,7 @@ fn parse_fork(b: &[u8]) -> Fork {
f
}
/// Posição do volume HFS+ dentro da partição (wrapper HFS clássico) ou `None` se não for HFS+.
/// Byte offset of the HFS+ volume inside the partition (handles HFS wrapper), or `None`.
pub fn locate(src: &dyn Source, base: u64) -> Option<u64> {
let h = read_vec(src, base + 1024, 512).ok()?;
if h.len() < 512 {
@@ -106,7 +106,7 @@ impl<'a> Vol<'a> {
self.base + b * self.bs
}
/// Todas as extensões de um fork (inclusive as do arquivo de overflow).
/// Returns all extents of a fork (including overflow extents from the extents B-tree).
fn fork_extents(&self, f: &Fork, file_id: u32, fork_type: u8) -> Vec<Extent> {
let mut ex: Vec<(u32, u32)> = f.extents.clone();
let mut have: u64 = ex.iter().map(|e| e.1 as u64).sum();
@@ -124,7 +124,7 @@ impl<'a> Vol<'a> {
.collect()
}
/// Extensões além das oito iniciais, procuradas no arquivo de overflow (árvore B+).
/// Looks up extents beyond the first eight in the extents overflow B-tree.
fn overflow_extents(&self, file_id: u32, fork_type: u8, mut start: u32) -> Vec<(u32, u32)> {
let mut out = vec![];
let node_size = match self.tree_node_size(&self.extents_file) {
@@ -140,7 +140,7 @@ impl<'a> Vol<'a> {
continue;
}
for r in records(&node) {
// chave: keyLength u16, forkType u8, pad u8, fileID u32, startBlock u32
// key: keyLength u16, forkType u8, pad u8, fileID u32, startBlock u32
if r.len() < 12 + 64 || be16(r, 0) != 10 {
continue;
}
@@ -170,7 +170,7 @@ impl<'a> Vol<'a> {
self.read_fork_bytes(f, n as u64 * ns as u64, ns)
}
/// Lê bytes de um fork que tem só extensões iniciais ou overflow já resolvido (para árvores).
/// Reads bytes from a fork using its resolved extent list (used for system B-trees).
fn read_fork_bytes(&self, f: &Fork, off: u64, len: usize) -> Option<Vec<u8>> {
let mut out = Vec::with_capacity(len);
let mut pos = 0u64;
@@ -190,7 +190,7 @@ impl<'a> Vol<'a> {
}
}
/// Registros de um nó B+ (fatias, da chave em diante).
/// Returns the record slices of a B-tree node (each starting at its key).
fn records(node: &[u8]) -> Vec<&[u8]> {
let n = be16(node, 10) as usize;
let mut offs = vec![];
@@ -233,7 +233,7 @@ fn date(d: u32) -> Option<i64> {
(d != 0).then(|| d as i64 - HFS_EPOCH)
}
/// Decodifica um registro do catálogo (chave + dados). `max_id` descarta lixo.
/// Decodes a catalog record (key + data). `max_id` filters out garbage CNIDs.
fn decode_record(
r: &[u8],
max_id: u32,
@@ -265,7 +265,7 @@ fn decode_record(
}
}
3 if rec.len() >= 10 => {
// thread de pasta: chave = (id da pasta, ""); dados = (pai, nome)
// folder thread record: key = (folder_id, ""); data = (parent_id, folder_name)
let pn = be16(rec, 8) as usize;
if pn <= 255 {
threads.entry(parent).or_insert((
@@ -304,7 +304,7 @@ fn decode_record(
}
}
/// Procura registros de arquivo nas sobras de um nó (entre os dados e a tabela de offsets).
/// Carves deleted catalog file records from B-tree node slack space (between active records and offset table).
fn scan_slack(
node: &[u8],
max_id: u32,
@@ -346,7 +346,7 @@ pub fn scan(ctx: &mut Ctx, base: u64, part_len: u64) -> VolumeScan {
return res;
};
// Mapa de espaço livre.
// Free-space map from the allocation file.
if let Some(bm) = vol.read_fork_all(&vol.alloc, (vol.total / 8 + 1) as usize) {
let mut free = vec![];
let mut run: Option<u64> = None;
@@ -409,7 +409,7 @@ pub fn scan(ctx: &mut Ctx, base: u64, part_len: u64) -> VolumeScan {
n = be32(&node, 0);
}
// Diário: cópias antigas de nós folha do catálogo.
// Journal: older copies of catalog leaf nodes.
if vol.journal_info != 0 {
scan_journal(
&vol,
@@ -422,7 +422,7 @@ pub fn scan(ctx: &mut Ctx, base: u64, part_len: u64) -> VolumeScan {
);
}
// Caminho de cada pasta a partir dos threads.
// Reconstruct folder paths from folder thread records.
let path_of = |parent: u32| -> String {
let mut parts = vec![];
let mut cur = parent;
@@ -462,9 +462,9 @@ pub fn scan(ctx: &mut Ctx, base: u64, part_len: u64) -> VolumeScan {
if !seen.insert((f.id, ex[0].offset)) {
continue;
}
// Mantém só os bytes do arquivo.
// Trim extents to the logical file size.
let extents = super::fat::trim_extents(ex, f.data.size);
// Blocos já reaproveitados?
// Check whether any blocks have already been reallocated.
let reused = extents.iter().filter(|e| !covered_by(&free_set, e)).count();
let (conf, note) = if reused == 0 {
(
@@ -523,7 +523,7 @@ impl Vol<'_> {
}
}
/// Procura nós folha do catálogo nas transações guardadas no diário.
/// Scans the HFS+ journal buffer for historical catalog leaf nodes.
fn scan_journal(
vol: &Vol,
ns: usize,
@@ -556,7 +556,7 @@ fn scan_journal(
let mut i = 0;
while i + ns <= buf.len().min(n + ns) && i < n {
let nd = &buf[i..i + ns];
// nó folha do catálogo: kind = -1, altura 1, registros plausíveis
// catalog leaf node: kind = -1, height = 1, plausible record offsets
if nd[8] == 0xFF && nd[9] == 1 && be16(nd, 10) > 0 && (be16(nd, 10) as usize) < ns / 8 {
let table = ns - 2 * (be16(nd, 10) as usize + 1);
if (be16(nd, ns - 2) as usize) == 14
+13 -13
View File
@@ -1,8 +1,8 @@
//! Leitores de metadados de sistemas de arquivos (somente leitura).
//! Read-only filesystem metadata readers.
//!
//! Cada leitor percorre estruturas do volume atrás de arquivos apagados,
//! devolve os extents ainda alocados ("live") para o carving pular e, quando
//! possível, os trechos livres.
//! Each reader walks volume metadata structures looking for deleted files,
//! returns currently allocated ("live") extents so file carving can skip them,
//! and, when available, returns the exact free-space map.
pub mod apfs;
pub mod btrfs;
@@ -18,12 +18,12 @@ use crate::source::{read_vec, Source};
use crate::types::{Confidence, DataLoc, Extent, Found, Origin};
use std::sync::atomic::{AtomicU64, Ordering};
/// Contexto compartilhado por todos os leitores.
/// Context shared across all filesystem metadata readers.
pub struct Ctx<'a> {
pub src: &'a dyn Source,
pub next_id: &'a AtomicU64,
pub cancelled: &'a (dyn Fn() -> bool + Sync),
/// (feito, total) dentro da fase atual do leitor.
/// Reports `(done, total)` progress within the current reader phase.
pub progress: &'a (dyn Fn(u64, u64) + Sync),
pub emit: &'a mut dyn FnMut(Found),
}
@@ -34,12 +34,12 @@ impl Ctx<'_> {
}
}
/// Resultado de varrer um volume.
/// Output of scanning a volume's metadata.
#[derive(Default)]
pub struct VolumeScan {
/// Extents (absolutos) ocupados por arquivos existentes e metadados.
/// Absolute byte extents occupied by live files and filesystem metadata.
pub live: Vec<Extent>,
/// Se o leitor conhece o mapa de espaço livre (ext), os trechos livres (absolutos).
/// Absolute free byte extents when the filesystem provides an explicit allocation bitmap.
pub free: Option<Vec<Extent>>,
pub warnings: Vec<String>,
}
@@ -57,7 +57,7 @@ pub struct NewFound<'a> {
pub note: Option<&'a str>,
}
/// Monta um `Found`, ajustando a confiança pelo conteúdo real no disco.
/// Builds a `Found` record, adjusting confidence based on actual on-disk header bytes.
pub fn make_found(ctx: &Ctx, n: NewFound) -> Found {
let ext = n
.name
@@ -116,7 +116,7 @@ pub fn make_found(ctx: &Ctx, n: NewFound) -> Found {
}
}
/// Ordena e funde extents adjacentes/sobrepostos.
/// Sorts and merges adjacent or overlapping byte extents.
pub fn merge_extents(mut v: Vec<Extent>) -> Vec<Extent> {
v.retain(|e| e.len > 0);
v.sort_by_key(|e| e.offset);
@@ -135,7 +135,7 @@ pub fn merge_extents(mut v: Vec<Extent>) -> Vec<Extent> {
out
}
/// Há sobreposição entre [off, off+len) e algum extent de `sorted` (já mesclado)?
/// Returns `true` if `[off, off + len)` overlaps any extent in `sorted` (must be pre-merged).
pub fn overlaps(sorted: &[Extent], off: u64, len: u64) -> bool {
let i = sorted.partition_point(|e| e.end() <= off);
sorted.get(i).map(|e| e.offset < off + len).unwrap_or(false)
@@ -159,7 +159,7 @@ pub fn dos_to_unix(date: u16, time: u16) -> Option<i64> {
Some(days_from_civil(year, month, day) * 86400 + h * 3600 + m * 60 + s)
}
/// Dias desde 1970-01-01 (algoritmo de Howard Hinnant).
/// Days since 1970-01-01 UTC (Howard Hinnant civil calendar algorithm).
pub fn days_from_civil(y: i64, m: i64, d: i64) -> i64 {
let y = if m <= 2 { y - 1 } else { y };
let era = if y >= 0 { y } else { y - 399 } / 400;
+10 -10
View File
@@ -1,5 +1,5 @@
//! NTFS: percorre a MFT atrás de registros apagados, resolve caminhos e
//! reconstrói os data runs. Coleta também os extents vivos.
//! NTFS: walks the MFT for deleted file records, reconstructs folder paths,
//! and decodes data runs. Also collects live allocated extents.
use super::{
make_found, merge_extents, overlaps, sanitize_name, utf16le, Ctx, NewFound, VolumeScan,
@@ -9,10 +9,10 @@ use crate::types::{Confidence, DataLoc, Extent, Origin};
use std::collections::HashMap;
struct Run {
/// Cluster lógico inicial no arquivo (VCN).
/// Starting Virtual Cluster Number (VCN) within the file.
vcn: u64,
len: u64,
/// None = esparso.
/// `None` = sparse run.
lcn: Option<u64>,
}
@@ -61,7 +61,7 @@ pub fn scan(ctx: &mut Ctx, base: u64) -> VolumeScan {
return res;
}
// Registro 0 ($MFT) define onde está toda a MFT.
// MFT record 0 ($MFT) defines the data runs covering the entire MFT.
let mut mft_runs: Option<(Vec<Run>, u64)> = None;
for lcn in [mft_lcn, mirr_lcn] {
if let Some(mut r) = read_vec(ctx.src, base + lcn * cl, rec_size as usize)
@@ -96,7 +96,7 @@ pub fn scan(ctx: &mut Ctx, base: u64) -> VolumeScan {
let mut dirs: HashMap<u64, (u64, String)> = HashMap::new();
let mut deleted: Vec<(u64, Rec)> = vec![];
// Lê a MFT em blocos grandes seguindo os runs.
// Read the MFT in large chunks across its data runs.
let chunk_recs = (1 << 20) / rec_size.max(1);
let mut idx = 0u64;
let mut buf: Vec<u8>;
@@ -161,7 +161,7 @@ pub fn scan(ctx: &mut Ctx, base: u64) -> VolumeScan {
unsupported,
} => {
if unsupported {
continue; // comprimido/cifrado (EFS): fora do escopo da v1
continue; // compressed/encrypted (EFS) attributes are skipped
}
let mut ex = vec![];
for r in &runs {
@@ -291,7 +291,7 @@ fn le64(b: &[u8], o: usize) -> u64 {
u64::from_le_bytes(b[o..o + 8].try_into().unwrap())
}
/// Interpreta um registro de arquivo da MFT (aplica fixups no próprio buffer).
/// Parses an MFT `FILE` record (applies USA fixups in place).
fn parse_record(r: &mut [u8], bps: u64, _base: u64, _cl: u64) -> Option<Rec> {
if r.len() < 48 || &r[0..4] != b"FILE" {
return None;
@@ -308,7 +308,7 @@ fn parse_record(r: &mut [u8], bps: u64, _base: u64, _cl: u64) -> Option<Rec> {
break;
}
if r[end - 2..end] != usn {
return None; // registro rasgado
return None; // torn MFT record (fixup mismatch)
}
r[end - 2] = r[usa_off + i * 2];
r[end - 1] = r[usa_off + i * 2 + 1];
@@ -316,7 +316,7 @@ fn parse_record(r: &mut [u8], bps: u64, _base: u64, _cl: u64) -> Option<Rec> {
let flags = le16(r, 22);
let base_ref = le64(r, 32) & 0xFFFF_FFFF_FFFF;
if base_ref != 0 {
return None; // registro de extensão: tratado só via lista de atributos (não suportado)
return None; // extension MFT record (handled via $ATTRIBUTE_LIST, skipped as standalone)
}
let mut rec = Rec {
in_use: flags & 1 != 0,
+26 -26
View File
@@ -1,7 +1,7 @@
//! XFS (v4 e v5): mapa de espaço livre pela árvore de blocos livres de cada grupo de alocação e
//! arquivos apagados. Ao liberar um inode, o XFS zera o modo e o tamanho mas não apaga a lista de
//! extents que ficou dentro do inode; e a entrada de diretório liberada perde só os 4 primeiros
//! bytes (o número do inode), preservando o nome. Juntamos as duas sobras.
//! XFS (v4 and v5): free-space map from each allocation group's free-block B+tree and deleted
//! file recovery. When freeing an inode, XFS zeroes mode and size if not fully wiped, leaving
//! inline extent records in the inode fork; freed directory entries only overwrite the first
//! 4 bytes of the 8-byte inode number, preserving the low 32 bits and the filename.
use super::{make_found, merge_extents, Ctx, NewFound, VolumeScan};
use crate::signatures;
@@ -88,7 +88,7 @@ impl<'a> Xfs<'a> {
self.base + agno * self.agblocks * self.bs
}
/// Endereço físico de um bloco "fsblock" (agno << agblklog | agbno).
/// Converts an XFS `fsblock` (`agno << agblklog | agbno`) to a physical byte offset.
fn fsb_off(&self, fsb: u64) -> Option<u64> {
let (agno, agbno) = (fsb >> self.agblklog, fsb & ((1 << self.agblklog) - 1));
(agno < self.agcount && agbno < self.agblocks).then(|| self.ag_off(agno) + agbno * self.bs)
@@ -116,7 +116,7 @@ impl<'a> Xfs<'a> {
}
}
/// Extents de um inode com a lista no próprio inode (formato 2) ou em árvore (formato 3).
/// Reads extents from an inode data fork (format 2 inline extent list or format 3 B+tree).
fn extents_of(&self, ino: &[u8], max_blocks: u64) -> Vec<(u64, u64, u64)> {
let fmt = ino[5];
let start = self.core_size(ino);
@@ -135,7 +135,7 @@ impl<'a> Xfs<'a> {
}
}
3 => {
// raiz da árvore de extents no inode: nível, nregistros, chaves e ponteiros
// B+tree root inside the inode fork: level, numrecs, keys, and block pointers
let level = be16(lit, 0) as usize;
let nrecs = be16(lit, 2) as usize;
let maxrecs = (lit.len() - 4) / 16;
@@ -180,7 +180,7 @@ impl<'a> Xfs<'a> {
}
}
/// Registro de extent empacotado em 128 bits: flag(1) | início no arquivo(54) | bloco(52) | quantidade(21).
/// Unpacks a 128-bit XFS extent record: `flag(1) | startoff(54) | startblock(52) | blockcount(21)`.
fn unpack_extent(r: &[u8]) -> (u64, u64, u64) {
let hi = u64::from_be_bytes(r[0..8].try_into().unwrap());
let lo = u64::from_be_bytes(r[8..16].try_into().unwrap());
@@ -190,7 +190,7 @@ fn unpack_extent(r: &[u8]) -> (u64, u64, u64) {
(startoff, startblock, count)
}
/// Percorre as folhas de uma árvore B+ curta, entregando cada registro de 16 (inobt) ou 8 (bnobt) bytes.
/// Walks the leaves of an AG B+tree, yielding each 16-byte (`inobt`) or 8-byte (`bnobt`) record.
fn walk_short_btree(
x: &Xfs,
agno: u64,
@@ -201,7 +201,7 @@ fn walk_short_btree(
f: &mut dyn FnMut(&[u8]),
) {
let mut cur = root;
// desce pela esquerda até a folha
// descend leftmost branch down to the leaf level
for lvl in (1..levels).rev() {
let Ok(b) = read_vec(x.src, x.ag_off(agno) + cur * x.bs, x.bs as usize) else {
return;
@@ -241,10 +241,10 @@ pub fn scan(ctx: &mut Ctx, base: u64, part_len: u64) -> VolumeScan {
return res;
};
// Espaço livre por grupo e inodes livres.
// Collect free-space ranges and free inode records across all allocation groups.
let mut free: Vec<Extent> = vec![];
let mut free_set: Vec<(u64, u64)> = vec![]; // (fsblock inicial, quantidade) para checar blocos
let mut chunks: Vec<(u64, u64, u64)> = vec![]; // (agno, startino, máscara livre)
let mut free_set: Vec<(u64, u64)> = vec![]; // (start_fsblock, block_count) for free-block checks
let mut chunks: Vec<(u64, u64, u64)> = vec![]; // (agno, startino, free_bitmask)
for agno in 0..x.agcount {
if (ctx.cancelled)() {
return res;
@@ -278,10 +278,10 @@ pub fn scan(ctx: &mut Ctx, base: u64, part_len: u64) -> VolumeScan {
.is_some_and(|&(s, c)| s <= fsb && s + c >= fsb + count)
};
// Nomes: entradas de diretório liberadas (guardam o nome e os 4 bytes baixos do inode).
// Names: freed directory entries (preserve filename and low 32 bits of the inode number).
let (live_dirs, stale_names) = collect_names(&x, ctx);
// Inodes livres com a lista de extents ainda dentro do inode.
// Free inodes that still contain leftover extent records in their data fork.
let max_blocks = x.dblocks;
let mut seen_first: HashSet<u64> = HashSet::new();
for (agno, startino, mask) in &chunks {
@@ -290,7 +290,7 @@ pub fn scan(ctx: &mut Ctx, base: u64, part_len: u64) -> VolumeScan {
}
for i in 0..64u64 {
if mask >> i & 1 == 0 {
continue; // em uso
continue; // allocated inode
}
let ino = (agno << (x.agblklog + x.inopblog)) | (startino + i);
let Some(raw) = x.read_ino(ino) else { continue };
@@ -298,13 +298,13 @@ pub fn scan(ctx: &mut Ctx, base: u64, part_len: u64) -> VolumeScan {
if mode != 0 && be32(&raw, 16) != 0 {
continue;
}
// Só inodes que foram arquivos: o formato de extents continua gravado e há sobras.
// Check freed inodes that still have non-zero extent records in the data fork.
if raw[5] != 2 {
continue;
}
let start = x.core_size(&raw);
let lit = &raw[start..];
// lê registros plausíveis em sequência
// read consecutive plausible extent records
let mut ext: Vec<(u64, u64, u64)> = vec![];
let mut next_off = 0u64;
for k in 0..lit.len() / 16 {
@@ -346,7 +346,7 @@ pub fn scan(ctx: &mut Ctx, base: u64, part_len: u64) -> VolumeScan {
} else {
"blocos do arquivo já reaproveitados".to_string()
};
// tamanho exato: tenta achar o fim real pelo conteúdo
// exact size was zeroed on deletion: refine size from file structure when possible
let mut est = true;
if let Some(h) = read_vec(ctx.src, extents[0].offset, 128)
.ok()
@@ -394,7 +394,7 @@ pub fn scan(ctx: &mut Ctx, base: u64, part_len: u64) -> VolumeScan {
res
}
/// Percorre os diretórios vivos; devolve (caminho de cada diretório por inode, nome liberado por inode baixo).
/// Walks live directories; returns `(dir_paths_by_inode, freed_names_by_low_inode_u32)`.
fn collect_names(x: &Xfs, ctx: &Ctx) -> (HashMap<u64, String>, HashMap<u64, (u64, String)>) {
let mut paths: HashMap<u64, String> = HashMap::new();
let mut stale: HashMap<u64, (u64, String)> = HashMap::new();
@@ -414,7 +414,7 @@ fn collect_names(x: &Xfs, ctx: &Ctx) -> (HashMap<u64, String>, HashMap<u64, (u64
let start = x.core_size(&raw);
match raw[5] {
1 => {
// pasta curta guardada no inode
// shortform directory stored inline inside the inode
let lit = &raw[start..];
let count = lit[0] as usize;
let i8 = lit[1] != 0;
@@ -443,7 +443,7 @@ fn collect_names(x: &Xfs, ctx: &Ctx) -> (HashMap<u64, String>, HashMap<u64, (u64
let ex = x.extents_of(&raw, 1 << 20);
let _ = size;
for (off, fsb, cnt) in ex {
// blocos de dados de diretório ficam antes do offset de 32 GiB (depois vêm folhas e índice)
// directory data blocks live below the 32 GiB logical offset (leaf/freeindex blocks live above)
if off * x.bs >= 32u64 << 30 {
continue;
}
@@ -479,7 +479,7 @@ fn parse_dir_block(
_ => return,
};
let is_block = magic == b"XD2B" || magic == b"XDB3";
// em blocos únicos, o final tem a tabela de folhas
// single-block directory: trim the trailing leaf/tail table
let end = if is_block {
let cnt = be32(b, b.len() - 8) as usize;
b.len().saturating_sub(8 + cnt * 8)
@@ -493,7 +493,7 @@ fn parse_dir_block(
if len < 8 || p + len > end {
break;
}
// entrada liberada: sobraram (inode baixo, nome); outras ainda podem estar dentro da região
// freed entry region: extract `(low_inode_u32, name)` and any subsequent unlinked entries inside the region
scan_free_region(x, &b[p..p + len], dino, stale);
p += len;
continue;
@@ -517,7 +517,7 @@ fn parse_dir_block(
}
fn scan_free_region(x: &Xfs, r: &[u8], dir_ino: u64, stale: &mut HashMap<u64, (u64, String)>) {
// 1) a primeira entrada perdeu só os 4 primeiros bytes do número do inode
// 1) the first unlinked entry at the start of the free tag lost only the upper 4 bytes of its inode number
let try_at = |p: usize, low: u64, stale: &mut HashMap<u64, (u64, String)>| -> Option<usize> {
let nl = *r.get(p + 8)? as usize;
let name = r.get(p + 9..p + 9 + nl)?;
@@ -532,7 +532,7 @@ fn scan_free_region(x: &Xfs, r: &[u8], dir_ino: u64, stale: &mut HashMap<u64, (u
if r.len() >= 16 {
let low = be32(r, 4) as u64;
if let Some(sz) = try_at(0, low, stale) {
// entradas seguintes dentro da mesma região, com o inode inteiro
// subsequent coalesced entries inside the same free region still have their full 8-byte inode number
let mut p = sz;
while p + 16 <= r.len() {
let ino = be64(r, p);