Formatação (cargo fmt) e ajustes do clippy para o CI da 1.0.0
Release / Cria a release (push) Canceled after 0s
Release / CLI (linux-aarch64) (push) Canceled after 0s
Release / CLI (linux-x86_64) (push) Canceled after 0s
Release / CLI (macos-aarch64) (push) Canceled after 0s
Release / CLI (macos-x86_64) (push) Canceled after 0s
Release / CLI (windows-x86_64) (push) Canceled after 0s
Release / App desktop (linux-x86_64) (push) Canceled after 0s
Release / App desktop (macos-aarch64) (push) Canceled after 0s
Release / App desktop (macos-x86_64) (push) Canceled after 0s
Release / App desktop (windows-x86_64) (push) Canceled after 0s
Release / Publica a release (push) Canceled after 0s

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Lucas RayanandClaude Opus 5.5 committed 2026-10-19 11:41:58 -03:00
1 parent 74218e7beb
commit beb83fef92
49 files changed
+3806 -854

No files matched your search

+162 -42
View File
@@ -9,13 +9,19 @@ use crate::types::{Confidence, DataLoc, Extent, Origin};
use std::collections::{BTreeMap, HashMap, HashSet};
fn le16(b: &[u8], o: usize) -> u16 {
b.get(o..o + 2).map(|s| u16::from_le_bytes([s[0], s[1]])).unwrap_or(0)
b.get(o..o + 2)
.map(|s| u16::from_le_bytes([s[0], s[1]]))
.unwrap_or(0)
}
fn le32(b: &[u8], o: usize) -> u32 {
b.get(o..o + 4).map(|s| u32::from_le_bytes(s.try_into().unwrap())).unwrap_or(0)
b.get(o..o + 4)
.map(|s| u32::from_le_bytes(s.try_into().unwrap()))
.unwrap_or(0)
}
fn le64(b: &[u8], o: usize) -> u64 {
b.get(o..o + 8).map(|s| u64::from_le_bytes(s.try_into().unwrap())).unwrap_or(0)
b.get(o..o + 8)
.map(|s| u64::from_le_bytes(s.try_into().unwrap()))
.unwrap_or(0)
}
const OBJ_TYPE_MASK: u32 = 0xFFFF;
@@ -99,22 +105,42 @@ impl<'a> Apfs<'a> {
}
let ks = key_base + koff;
let k = b.get(ks..ks + klen)?.to_vec();
let v = if voff == 0xFFFF { vec![] } else { b.get(val_end.checked_sub(voff)?..)?.get(..vlen)?.to_vec() };
let v = if voff == 0xFFFF {
vec![]
} else {
b.get(val_end.checked_sub(voff)?..)?.get(..vlen)?.to_vec()
};
entries.push((k, v));
}
Some(BNode { flags, level, entries, oid: le64(b, 8), xid: le64(b, 16) })
Some(BNode {
flags,
level,
entries,
oid: le64(b, 8),
xid: le64(b, 16),
})
}
/// Todas as entradas de uma árvore física (mapa de objetos): filhos são endereços físicos.
fn walk_physical(&self, root: u64, fixed_k: usize, fixed_v: usize, f: &mut dyn FnMut(&[u8], &[u8])) {
fn walk_physical(
&self,
root: u64,
fixed_k: usize,
fixed_v: usize,
f: &mut dyn FnMut(&[u8], &[u8]),
) {
let mut stack = vec![(root, 0u32)];
let mut seen = HashSet::new();
while let Some((p, depth)) = stack.pop() {
if depth > 8 || !seen.insert(p) {
continue;
}
let Some(b) = self.checked_block(p) else { continue };
let Some(n) = self.parse_node(&b, fixed_k, fixed_v) else { continue };
let Some(b) = self.checked_block(p) else {
continue;
};
let Some(n) = self.parse_node(&b, fixed_k, fixed_v) else {
continue;
};
if n.level == 0 {
for (k, v) in &n.entries {
f(k, v);
@@ -130,10 +156,14 @@ impl<'a> Apfs<'a> {
/// Mapa de objetos: oid -> [(xid, paddr)] ordenado por 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 { return m };
let Some(ob) = self.checked_block(omap_paddr) else {
return m;
};
let tree = le64(&ob, 48);
self.walk_physical(tree, 16, 16, &mut |k, v| {
m.entry(le64(k, 0)).or_default().push((le64(k, 8), le64(v, 8)));
m.entry(le64(k, 0))
.or_default()
.push((le64(k, 8), le64(v, 8)));
});
for v in m.values_mut() {
v.sort();
@@ -143,7 +173,11 @@ impl<'a> Apfs<'a> {
}
fn lookup(m: &HashMap<u64, Vec<(u64, u64)>>, oid: u64, max_xid: u64) -> Option<u64> {
m.get(&oid)?.iter().rev().find(|(x, _)| *x <= max_xid).map(|(_, p)| *p)
m.get(&oid)?
.iter()
.rev()
.find(|(x, _)| *x <= max_xid)
.map(|(_, p)| *p)
}
// ---------------------------------------------------------------- registros
@@ -188,7 +222,7 @@ fn add_record(files: &mut Files, extents: &mut Extents, k: &[u8], v: &[u8]) {
mtime: ns(le64(v, 24)),
atime: ns(le64(v, 40)),
mode: le16(v, 80),
size: le64(v, 84).max(0),
size: le64(v, 84),
has: true,
..Default::default()
};
@@ -204,7 +238,14 @@ fn add_record(files: &mut Files, extents: &mut Extents, k: &[u8], v: &[u8]) {
let (xt, xs) = (xf[4 + i * 4], le16(xf, 6 + i * 4) as usize);
let data = xf.get(p..p + xs).unwrap_or(&[]);
match xt {
4 => ino.name = Some(String::from_utf8_lossy(data.split(|&c| c == 0).next().unwrap_or(&[])).to_string()),
4 => {
ino.name = Some(
String::from_utf8_lossy(
data.split(|&c| c == 0).next().unwrap_or(&[]),
)
.to_string(),
)
}
8 if data.len() >= 8 => ino.size = le64(data, 0),
_ => {}
}
@@ -216,13 +257,17 @@ fn add_record(files: &mut Files, extents: &mut Extents, k: &[u8], v: &[u8]) {
R_DIR_REC if k.len() > 12 && v.len() >= 18 => {
let nl = (le32(k, 8) & 0x3FF) as usize;
if let Some(name) = k.get(12..12 + nl) {
let name = String::from_utf8_lossy(name.split(|&c| c == 0).next().unwrap_or(&[])).to_string();
let name = String::from_utf8_lossy(name.split(|&c| c == 0).next().unwrap_or(&[]))
.to_string();
files.entry(le64(v, 0)).or_default().names.insert(id, name);
}
}
R_FILE_EXTENT if k.len() >= 16 && v.len() >= 24 => {
let len = le64(v, 0) & 0x00FF_FFFF_FFFF_FFFF;
extents.entry(id).or_default().insert(le64(k, 8), (len, le64(v, 8), le64(v, 16)));
extents
.entry(id)
.or_default()
.insert(le64(k, 8), (len, le64(v, 8), le64(v, 16)));
}
_ => {}
}
@@ -230,16 +275,28 @@ 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.
fn walk_fstree(&self, root_oid: u64, omap: &HashMap<u64, Vec<(u64, u64)>>, xid: u64, f: &mut dyn FnMut(&[u8], &[u8], u64)) {
let Some(rp) = lookup(omap, root_oid, xid) else { return };
fn walk_fstree(
&self,
root_oid: u64,
omap: &HashMap<u64, Vec<(u64, u64)>>,
xid: u64,
f: &mut dyn FnMut(&[u8], &[u8], u64),
) {
let Some(rp) = lookup(omap, root_oid, xid) else {
return;
};
let mut stack = vec![(rp, 0u32)];
let mut seen = HashSet::new();
while let Some((p, depth)) = stack.pop() {
if depth > 10 || !seen.insert(p) {
continue;
}
let Some(b) = self.checked_block(p) else { continue };
let Some(n) = self.parse_node(&b, 16, 16) else { continue };
let Some(b) = self.checked_block(p) else {
continue;
};
let Some(n) = self.parse_node(&b, 16, 16) else {
continue;
};
if n.level == 0 {
for (k, v) in &n.entries {
f(k, v, p);
@@ -261,7 +318,10 @@ fn path_of(files: &Files, mut id: u64) -> String {
if id == 2 || id == 1 {
break;
}
match files.get(&id).and_then(|f| f.names.iter().next().map(|(p, n)| (*p, n.clone()))) {
match files
.get(&id)
.and_then(|f| f.names.iter().next().map(|(p, n)| (*p, n.clone())))
{
Some((p, n)) => {
parts.push(n);
id = p;
@@ -285,7 +345,11 @@ fn parse_volume(b: &[u8]) -> Option<Volume> {
if &b[32..36] != b"APSB" {
return None;
}
let name = String::from_utf8_lossy(&b[704..960]).split('\0').next().unwrap_or("").to_string();
let name = String::from_utf8_lossy(&b[704..960])
.split('\0')
.next()
.unwrap_or("")
.to_string();
Some(Volume {
name,
omap_paddr: le64(b, 128),
@@ -305,7 +369,12 @@ fn open_container<'a>(src: &'a dyn Source, base: u64) -> Option<(Apfs<'a>, Vec<u
if !(4096..=65536).contains(&bs) || !bs.is_power_of_two() {
return None;
}
let a = Apfs { src, base, bs, blocks };
let a = Apfs {
src,
base,
bs,
blocks,
};
// O superbloco mais recente está na área de descritores de checkpoint.
let desc_blocks = le32(&b0, 104) as u64;
let desc_base = le64(&b0, 112);
@@ -323,8 +392,12 @@ fn open_container<'a>(src: &'a dyn Source, base: u64) -> Option<(Apfs<'a>, Vec<u
pub fn scan(ctx: &mut Ctx, base: u64, part_len: u64) -> VolumeScan {
let _ = part_len;
let mut res = VolumeScan::default();
let Some((a, nx)) = open_container(ctx.src, base) else { return res };
let Some(cont_omap_block) = a.checked_block(le64(&nx, 160)) else { return res };
let Some((a, nx)) = open_container(ctx.src, base) else {
return res;
};
let Some(cont_omap_block) = a.checked_block(le64(&nx, 160)) else {
return res;
};
let _ = cont_omap_block;
let container_omap = a.omap(le64(&nx, 160));
let max_fs = le32(&nx, 180).min(100) as usize;
@@ -379,8 +452,12 @@ fn scan_volume(ctx: &mut Ctx, a: &Apfs, v: &Volume, allow_stale_scan: bool, res:
if !seen.insert(p) {
continue;
}
let Some(b) = a.checked_block(p) else { continue };
let Some(n) = a.parse_node(&b, 0, 0) else { continue };
let Some(b) = a.checked_block(p) else {
continue;
};
let Some(n) = a.parse_node(&b, 0, 0) else {
continue;
};
if n.level == 0 {
for (k, val) in &n.entries {
let hdr = le64(k, 0);
@@ -397,8 +474,12 @@ fn scan_volume(ctx: &mut Ctx, a: &Apfs, v: &Volume, allow_stale_scan: bool, res:
}
}
for (xid, sblock) in snaps {
let Some(sb) = a.checked_block(sblock) else { continue };
let Some(sv) = parse_volume(&sb) else { continue };
let Some(sb) = a.checked_block(sblock) else {
continue;
};
let Some(sv) = parse_volume(&sb) else {
continue;
};
a.walk_fstree(sv.root_oid, &omap, xid, &mut |k, val, _| {
add_record(&mut stale, &mut stale_ext, k, val);
});
@@ -417,7 +498,10 @@ fn scan_volume(ctx: &mut Ctx, a: &Apfs, v: &Volume, allow_stale_scan: bool, res:
}
let Some(b) = a.block(p) else { continue };
let ty = le32(&b, 24) & OBJ_TYPE_MASK;
if (ty != T_BTREE_NODE && ty != T_BTREE_ROOT) || le32(&b, 28) != SUBTYPE_FSTREE || le16(&b, 34) != 0 {
if (ty != T_BTREE_NODE && ty != T_BTREE_ROOT)
|| le32(&b, 28) != SUBTYPE_FSTREE
|| le16(&b, 34) != 0
{
continue;
}
if live_nodes.contains(&p) || fletcher64(&b[8..]) != le64(&b, 0) {
@@ -436,7 +520,7 @@ fn scan_volume(ctx: &mut Ctx, a: &Apfs, v: &Volume, allow_stale_scan: bool, res:
ids.sort();
let mut seen_first: HashSet<u64> = HashSet::new();
for id in ids {
if live.get(id).map_or(false, |f| f.inode.has) {
if live.get(id).is_some_and(|f| f.inode.has) {
continue;
}
let rec = &stale[id];
@@ -444,7 +528,9 @@ fn scan_volume(ctx: &mut Ctx, a: &Apfs, v: &Volume, allow_stale_scan: bool, res:
if !ino.has || ino.mode & 0o170000 != 0o100000 || ino.size == 0 {
continue;
}
let Some(ex) = stale_ext.get(&ino.private_id) else { continue };
let Some(ex) = stale_ext.get(&ino.private_id) else {
continue;
};
if ex.values().any(|e| e.2 != 0) {
continue; // extensão cifrada
}
@@ -456,17 +542,29 @@ fn scan_volume(ctx: &mut Ctx, a: &Apfs, v: &Volume, allow_stale_scan: bool, res:
None => (format!("arquivo_{id}"), ino.parent),
},
};
let merged: Files = stale.iter().map(|(k, v)| (*k, v.clone())).chain(live.iter().map(|(k, v)| (*k, v.clone()))).collect();
let merged: Files = stale
.iter()
.map(|(k, v)| (*k, v.clone()))
.chain(live.iter().map(|(k, v)| (*k, v.clone())))
.collect();
let dir = Some(format!("/{}", path_of(&merged, parent)).replace("//", "/"));
let mut extents: Vec<Extent> = vec![];
let mut pos = 0u64;
let mut ok = true;
for (&lo, &(len, paddr, _)) in ex {
if lo > pos {
extents.push(Extent { offset: 0, len: lo - pos, sparse: true });
extents.push(Extent {
offset: 0,
len: lo - pos,
sparse: true,
});
}
if paddr == 0 {
extents.push(Extent { offset: 0, len, sparse: true });
extents.push(Extent {
offset: 0,
len,
sparse: true,
});
} else if paddr < a.blocks {
extents.push(Extent::new(a.base + paddr * a.bs, len));
} else {
@@ -478,11 +576,17 @@ fn scan_volume(ctx: &mut Ctx, a: &Apfs, v: &Volume, allow_stale_scan: bool, res:
continue;
}
let extents = super::fat::trim_extents(extents, ino.size);
let Some(first) = extents.iter().find(|e| !e.sparse) else { continue };
let Some(first) = extents.iter().find(|e| !e.sparse) else {
continue;
};
if !seen_first.insert(first.offset) {
continue;
}
let note = if ino.compressed { "arquivo comprimido pelo macOS (decmpfs): o conteúdo pode estar incompleto" } else { "versão antiga da árvore ou instantâneo do APFS" };
let note = if ino.compressed {
"arquivo comprimido pelo macOS (decmpfs): o conteúdo pode estar incompleto"
} else {
"versão antiga da árvore ou instantâneo do APFS"
};
let ts = |t: i64| (t > 0).then_some(t);
let nf = NewFound {
name,
@@ -493,7 +597,11 @@ fn scan_volume(ctx: &mut Ctx, a: &Apfs, v: &Volume, allow_stale_scan: bool, res:
modified: ts(ino.mtime),
created: ts(ino.ctime),
accessed: ts(ino.atime),
base: if ino.compressed { Confidence::Low } else { Confidence::Medium },
base: if ino.compressed {
Confidence::Low
} else {
Confidence::Medium
},
note: Some(note),
};
let f = make_found(ctx, nf);
@@ -504,13 +612,19 @@ fn scan_volume(ctx: &mut Ctx, a: &Apfs, v: &Volume, allow_stale_scan: bool, res:
/// Arquivos e pastas atuais do primeiro volume sem cifragem (caminho, tamanho, é pasta).
pub fn live_paths(src: &dyn Source, base: u64) -> Vec<(String, u64, bool)> {
let Some((a, nx)) = open_container(src, base) else { return vec![] };
let Some((a, nx)) = open_container(src, base) else {
return vec![];
};
let cont = a.omap(le64(&nx, 160));
let mut out = vec![];
for i in 0..le32(&nx, 180).min(100) as usize {
let oid = le64(&nx, 184 + i * 8);
let Some(p) = lookup(&cont, oid, u64::MAX) else { continue };
let Some(vb) = a.checked_block(p) else { continue };
let Some(p) = lookup(&cont, oid, u64::MAX) else {
continue;
};
let Some(vb) = a.checked_block(p) else {
continue;
};
let Some(v) = parse_volume(&vb) else { continue };
if v.encrypted {
continue;
@@ -518,12 +632,18 @@ pub fn live_paths(src: &dyn Source, base: u64) -> Vec<(String, u64, bool)> {
let omap = a.omap(v.omap_paddr);
let mut files: Files = HashMap::new();
let mut ext: Extents = HashMap::new();
a.walk_fstree(v.root_oid, &omap, u64::MAX, &mut |k, val, _| add_record(&mut files, &mut ext, k, val));
a.walk_fstree(v.root_oid, &omap, u64::MAX, &mut |k, val, _| {
add_record(&mut files, &mut ext, k, val)
});
for (&id, f) in &files {
if !f.inode.has || id <= 2 {
continue;
}
out.push((path_of(&files, id), f.inode.size, f.inode.mode & 0o170000 == 0o040000));
out.push((
path_of(&files, id),
f.inode.size,
f.inode.mode & 0o170000 == 0o040000,
));
}
}
out.sort();
+158 -36
View File
@@ -22,13 +22,19 @@ const METADATA_ITEM: u8 = 169;
const CHUNK_ITEM: u8 = 228;
fn le16(b: &[u8], o: usize) -> u16 {
b.get(o..o + 2).map(|s| u16::from_le_bytes([s[0], s[1]])).unwrap_or(0)
b.get(o..o + 2)
.map(|s| u16::from_le_bytes([s[0], s[1]]))
.unwrap_or(0)
}
fn le32(b: &[u8], o: usize) -> u32 {
b.get(o..o + 4).map(|s| u32::from_le_bytes(s.try_into().unwrap())).unwrap_or(0)
b.get(o..o + 4)
.map(|s| u32::from_le_bytes(s.try_into().unwrap()))
.unwrap_or(0)
}
fn le64(b: &[u8], o: usize) -> u64 {
b.get(o..o + 8).map(|s| u64::from_le_bytes(s.try_into().unwrap())).unwrap_or(0)
b.get(o..o + 8)
.map(|s| u64::from_le_bytes(s.try_into().unwrap()))
.unwrap_or(0)
}
pub fn crc32c(data: &[u8]) -> u32 {
@@ -38,7 +44,11 @@ pub fn crc32c(data: &[u8]) -> u32 {
for i in 0..256u32 {
let mut c = i;
for _ in 0..8 {
c = if c & 1 != 0 { (c >> 1) ^ 0x82F6_3B78 } else { c >> 1 };
c = if c & 1 != 0 {
(c >> 1) ^ 0x82F6_3B78
} else {
c >> 1
};
}
t[i as usize] = c;
}
@@ -59,7 +69,11 @@ pub struct Key {
}
fn key_at(b: &[u8], o: usize) -> Key {
Key { objectid: le64(b, o), ty: b.get(o + 8).copied().unwrap_or(0), offset: le64(b, o + 9) }
Key {
objectid: le64(b, o),
ty: b.get(o + 8).copied().unwrap_or(0),
offset: le64(b, o + 9),
}
}
#[derive(Clone, Debug)]
@@ -108,7 +122,10 @@ impl<'a> Fs<'a> {
}
let nodesize = le32(&sb, 148) as u64;
let sectorsize = le32(&sb, 144) as u64;
if !nodesize.is_power_of_two() || !(4096..=65536).contains(&nodesize) || !sectorsize.is_power_of_two() {
if !nodesize.is_power_of_two()
|| !(4096..=65536).contains(&nodesize)
|| !sectorsize.is_power_of_two()
{
return None;
}
let mut fsid = [0u8; 16];
@@ -156,9 +173,17 @@ impl<'a> Fs<'a> {
}
fn phys(&self, logical: u64) -> Option<u64> {
let i = self.chunks.partition_point(|c| c.logical + c.len <= logical);
let i = self
.chunks
.partition_point(|c| c.logical + c.len <= logical);
let c = self.chunks.get(i)?;
(c.logical <= logical).then(|| c.phys.first().map(|p| self.base + p + (logical - c.logical))).flatten()
(c.logical <= logical)
.then(|| {
c.phys
.first()
.map(|p| self.base + p + (logical - c.logical))
})
.flatten()
}
fn read_node(&self, logical: u64) -> Option<Node> {
@@ -172,7 +197,7 @@ impl<'a> Fs<'a> {
return None;
}
let bytenr = le64(raw, 48);
if expect.map_or(false, |e| e != bytenr) {
if expect.is_some_and(|e| e != bytenr) {
return None;
}
if crc32c(&raw[32..]) != le32(raw, 0) {
@@ -208,7 +233,12 @@ impl<'a> Fs<'a> {
}
NodeKind::Internal(ptrs)
};
Some(Node { bytenr, generation: le64(raw, 80), owner: le64(raw, 88), kind })
Some(Node {
bytenr,
generation: le64(raw, 80),
owner: le64(raw, 88),
kind,
})
}
/// Percorre uma árvore em ordem, chamando `f` para cada item de folha.
@@ -222,7 +252,14 @@ impl<'a> Fs<'a> {
self.walk_inner(root, f, &mut seen, nodes, 0);
}
fn walk_inner(&self, at: u64, f: &mut dyn FnMut(&Key, &[u8]), seen: &mut HashSet<u64>, nodes: &mut dyn FnMut(&Node), depth: u32) {
fn walk_inner(
&self,
at: u64,
f: &mut dyn FnMut(&Key, &[u8]),
seen: &mut HashSet<u64>,
nodes: &mut dyn FnMut(&Node),
depth: u32,
) {
if depth > 10 || !seen.insert(at) {
return;
}
@@ -254,7 +291,15 @@ fn parse_chunk(d: &[u8], logical: u64) -> Option<(Chunk, usize)> {
return None;
}
let phys = (0..nstripes).map(|i| le64(d, 48 + i * 32 + 8)).collect();
Some((Chunk { logical, len, ty, phys }, 48 + nstripes * 32))
Some((
Chunk {
logical,
len,
ty,
phys,
},
48 + nstripes * 32,
))
}
// ---------------------------------------------------------------- inodes
@@ -293,7 +338,11 @@ fn add_item(files: &mut Files, owner: u64, gen: u64, k: &Key, d: &[u8]) {
INODE_REF if d.len() >= 10 => {
let nlen = le16(d, 8) as usize;
if let Some(n) = d.get(10..10 + nlen) {
files.entry((owner, k.objectid)).or_default().refs.insert(k.offset, String::from_utf8_lossy(n).to_string());
files
.entry((owner, k.objectid))
.or_default()
.refs
.insert(k.offset, String::from_utf8_lossy(n).to_string());
}
}
EXTENT_DATA if d.len() >= 21 => {
@@ -319,7 +368,11 @@ pub fn scan(ctx: &mut Ctx, base: u64, part_len: u64) -> VolumeScan {
let Some(fs) = Fs::open(ctx.src, base) else {
return res;
};
if fs.chunks.iter().any(|c| c.ty & 0x1 << 3 != 0 || c.ty & 0x10 != 0 || c.ty & 0x100 != 0 || c.ty & 0x200 != 0) {
if fs
.chunks
.iter()
.any(|c| c.ty & 0x1 << 3 != 0 || c.ty & 0x10 != 0 || c.ty & 0x100 != 0 || c.ty & 0x200 != 0)
{
res.warnings.push("Btrfs em RAID 0/10/5/6: a leitura de metadados ainda não suporta esses perfis; usando só assinaturas.".into());
return res;
}
@@ -404,11 +457,18 @@ pub fn scan(ctx: &mut Ctx, base: u64, part_len: u64) -> VolumeScan {
let mut stale: Files = HashMap::new();
let ns = fs.nodesize as usize;
let step = fs.sectorsize as usize;
let md_chunks: Vec<Chunk> = fs.chunks.iter().filter(|c| c.ty & 0x4 != 0).cloned().collect();
let md_chunks: Vec<Chunk> = fs
.chunks
.iter()
.filter(|c| c.ty & 0x4 != 0)
.cloned()
.collect();
let total_md: u64 = md_chunks.iter().map(|c| c.len).sum();
let mut done_md = 0u64;
for c in &md_chunks {
let Some(p0) = c.phys.first().copied() else { continue };
let Some(p0) = c.phys.first().copied() else {
continue;
};
let mut off = 0u64;
let block = 8usize << 20;
while off < c.len {
@@ -416,12 +476,17 @@ pub fn scan(ctx: &mut Ctx, base: u64, part_len: u64) -> VolumeScan {
return res;
}
let n = ((c.len - off) as usize).min(block + ns);
let Ok(buf) = read_vec(ctx.src, fs.base + p0 + off, n) else { break };
let Ok(buf) = read_vec(ctx.src, fs.base + p0 + off, n) else {
break;
};
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
if buf[i + 32..i + 48] == fs.fsid && le64(&buf[i..], 48) == logical && buf[i + 100] == 0 {
if buf[i + 32..i + 48] == fs.fsid
&& le64(&buf[i..], 48) == logical
&& buf[i + 100] == 0
{
if let Some(node) = fs.parse_node(&buf[i..i + ns], Some(logical)) {
if is_fs_tree(node.owner) && !live_nodes.contains(&logical) {
if let NodeKind::Leaf(items) = &node.kind {
@@ -473,18 +538,32 @@ pub fn scan(ctx: &mut Ctx, base: u64, part_len: u64) -> VolumeScan {
if !e.has_inode || e.mode & 0o170000 != 0o100000 || e.size == 0 || e.extents.is_empty() {
continue;
}
let Some((name, parent)) = e.refs.iter().next().map(|(p, n)| (n.clone(), *p)) else { continue };
let Some((name, parent)) = e.refs.iter().next().map(|(p, n)| (n.clone(), *p)) else {
continue;
};
let dir = {
let p = path_of(owner, parent);
Some(if p.is_empty() { String::new() } else { format!("/{p}") })
Some(if p.is_empty() {
String::new()
} else {
format!("/{p}")
})
};
let Some((data, any_alloc, all_ok)) = build_data(&fs, e, &allocated) else {
continue;
};
let Some((data, any_alloc, all_ok)) = build_data(&fs, e, &allocated) else { continue };
let (conf, note) = if !all_ok {
(Confidence::Low, "faltam trechos do arquivo na versão antiga da árvore".to_string())
(
Confidence::Low,
"faltam trechos do arquivo na versão antiga da árvore".to_string(),
)
} else if any_alloc {
(Confidence::Medium, "versão antiga do Btrfs; alguns blocos ainda constam como alocados (podem ser de outro arquivo)".to_string())
} else {
(Confidence::High, "versão antiga da árvore do Btrfs; blocos de dados livres".to_string())
(
Confidence::High,
"versão antiga da árvore do Btrfs; blocos de dados livres".to_string(),
)
};
let ts = |t: i64| (t > 0).then_some(t);
let nf = NewFound {
@@ -526,7 +605,10 @@ fn build_data(fs: &Fs, e: &InodeData, allocated: &[Extent]) -> Option<(DataLoc,
}
if foff > pos {
sparse_marks.push(parts.len());
parts.push(Part::Run { offset: 0, len: foff - pos });
parts.push(Part::Run {
offset: 0,
len: foff - pos,
});
plain_only = false;
pos = foff;
}
@@ -541,7 +623,9 @@ fn build_data(fs: &Fs, e: &InodeData, allocated: &[Extent]) -> Option<(DataLoc,
c => decode(c, raw, ram)?,
};
let len = (bytes.len() as u64).min(e.size - foff);
parts.push(Part::Bytes { bytes: bytes[..len as usize].to_vec() });
parts.push(Part::Bytes {
bytes: bytes[..len as usize].to_vec(),
});
plain_only = false;
pos = foff + len;
continue;
@@ -554,7 +638,10 @@ fn build_data(fs: &Fs, e: &InodeData, allocated: &[Extent]) -> Option<(DataLoc,
let num = num.min(e.size.saturating_sub(foff));
if disk == 0 || ty == 2 {
sparse_marks.push(parts.len());
parts.push(Part::Run { offset: 0, len: num });
parts.push(Part::Run {
offset: 0,
len: num,
});
plain_only = false;
} else {
let Some(phys) = fs.phys(disk) else {
@@ -563,22 +650,32 @@ fn build_data(fs: &Fs, e: &InodeData, allocated: &[Extent]) -> Option<(DataLoc,
};
let overlaps = {
let i = allocated.partition_point(|x| x.end() <= disk);
allocated.get(i).map_or(false, |x| x.offset < disk + disk_len)
allocated.get(i).is_some_and(|x| x.offset < disk + disk_len)
};
any_alloc |= overlaps;
if comp == 0 {
parts.push(Part::Run { offset: phys + offset, len: num });
parts.push(Part::Run {
offset: phys + offset,
len: num,
});
} else {
let codec = match comp {
1 => PackedCodec::Zlib,
2 => PackedCodec::Zstd,
3 | 4 | 5 | 6 => PackedCodec::Lzo,
3..=6 => PackedCodec::Lzo,
_ => {
all_ok = false;
break;
}
};
parts.push(Part::Packed(Packed { offset: phys, disk_len, ram_len: le64(d, 8), skip: offset, len: num, codec }));
parts.push(Part::Packed(Packed {
offset: phys,
disk_len,
ram_len: le64(d, 8),
skip: offset,
len: num,
codec,
}));
plain_only = false;
}
}
@@ -588,7 +685,16 @@ fn build_data(fs: &Fs, e: &InodeData, allocated: &[Extent]) -> Option<(DataLoc,
all_ok = false;
}
if plain_only {
let ex: Vec<Extent> = parts.iter().filter_map(|p| if let Part::Run { offset, len } = p { Some(Extent::new(*offset, *len)) } else { None }).collect();
let ex: Vec<Extent> = parts
.iter()
.filter_map(|p| {
if let Part::Run { offset, len } = p {
Some(Extent::new(*offset, *len))
} else {
None
}
})
.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.
@@ -610,7 +716,9 @@ fn decode(codec: u8, raw: &[u8], ram: usize) -> Option<Vec<u8>> {
1 => {
use std::io::Read;
let mut v = Vec::with_capacity(ram);
flate2::read::ZlibDecoder::new(raw).read_to_end(&mut v).ok()?;
flate2::read::ZlibDecoder::new(raw)
.read_to_end(&mut v)
.ok()?;
Some(v)
}
2 => {
@@ -695,7 +803,11 @@ pub fn lzo1x_decompress(src: &[u8], max_out: usize) -> Option<Vec<u8>> {
}
let h = byte(ip)?;
ip += 1;
let (dist, len) = if state == 4 { ((t >> 2) + (h << 2) + 2049, 3) } else { ((t >> 2) + (h << 2) + 1, 2) };
let (dist, len) = if state == 4 {
((t >> 2) + (h << 2) + 2049, 3)
} else {
((t >> 2) + (h << 2) + 1, 2)
};
copy_match(&mut out, dist, len)?;
let next = t & 3;
lits(&mut out, &mut ip, next)?;
@@ -708,7 +820,11 @@ pub fn lzo1x_decompress(src: &[u8], max_out: usize) -> Option<Vec<u8>> {
lits(&mut out, &mut ip, next)?;
state = next;
} else if t >= 32 {
let len = if t & 31 == 0 { ext(&mut ip, 31)? } else { t & 31 } + 2;
let len = if t & 31 == 0 {
ext(&mut ip, 31)?
} else {
t & 31
} + 2;
let b = (byte(ip)?) | (byte(ip + 1)? << 8);
ip += 2;
copy_match(&mut out, 1 + (b >> 2), len)?;
@@ -745,7 +861,9 @@ fn copy_match(out: &mut Vec<u8>, dist: usize, len: usize) -> Option<()> {
/// Arquivos e pastas atuais com caminho completo e tamanho (usa só as árvores vivas).
pub fn live_paths(src: &dyn Source, base: u64) -> Vec<(String, u64, bool)> {
let Some(fs) = Fs::open(src, base) else { return vec![] };
let Some(fs) = Fs::open(src, base) else {
return vec![];
};
let mut roots: Vec<(u64, u64)> = vec![];
fs.walk(fs.root, &mut |k, d| {
if k.ty == ROOT_ITEM && d.len() >= 239 && is_fs_tree(k.objectid) {
@@ -817,6 +935,10 @@ pub fn locate_trees(src: &dyn Source, base: u64) -> Option<TreeLocations> {
root_tree_logical: fs.root,
fs_tree_logical: fs_t,
extent_tree_logical: ext_t,
chunks: fs.chunks.iter().filter_map(|c| c.phys.first().map(|p| (c.logical, c.len, fs.base + p))).collect(),
chunks: fs
.chunks
.iter()
.filter_map(|c| c.phys.first().map(|p| (c.logical, c.len, fs.base + p)))
.collect(),
})
}
+193 -55
View File
@@ -18,13 +18,19 @@ const EXT_MAGIC: u16 = 0xF30A;
const JBD2_MAGIC: u32 = 0xC03B_3998;
fn le16(b: &[u8], o: usize) -> u16 {
b.get(o..o + 2).map(|s| u16::from_le_bytes([s[0], s[1]])).unwrap_or(0)
b.get(o..o + 2)
.map(|s| u16::from_le_bytes([s[0], s[1]]))
.unwrap_or(0)
}
fn le32(b: &[u8], o: usize) -> u32 {
b.get(o..o + 4).map(|s| u32::from_le_bytes(s.try_into().unwrap())).unwrap_or(0)
b.get(o..o + 4)
.map(|s| u32::from_le_bytes(s.try_into().unwrap()))
.unwrap_or(0)
}
fn be32(b: &[u8], o: usize) -> u32 {
b.get(o..o + 4).map(|s| u32::from_be_bytes(s.try_into().unwrap())).unwrap_or(0)
b.get(o..o + 4)
.map(|s| u32::from_be_bytes(s.try_into().unwrap()))
.unwrap_or(0)
}
#[derive(Clone, Copy, Default)]
@@ -102,7 +108,9 @@ type ExtentList = Vec<(u64, u64, u64, bool)>;
impl<'a> Ext<'a> {
fn open(src: &'a dyn Source, base: u64) -> Option<Self> {
let sb = read_vec(src, base + 1024, 1024).ok().filter(|s| s.len() == 1024)?;
let sb = read_vec(src, base + 1024, 1024)
.ok()
.filter(|s| s.len() == 1024)?;
if le16(&sb, 56) != 0xEF53 {
return None;
}
@@ -125,7 +133,11 @@ impl<'a> Ext<'a> {
if !(128..=4096).contains(&inode_size) {
return None;
}
let desc_size = if is64 { (le16(&sb, 254) as u64).max(64) } else { 32 };
let desc_size = if is64 {
(le16(&sb, 254) as u64).max(64)
} else {
32
};
let ngroups = blocks.saturating_sub(first_data).div_ceil(bpg);
let gdt_block = first_data + 1;
let gdt = read_vec(src, base + gdt_block * bs, (ngroups * desc_size) as usize).ok()?;
@@ -190,7 +202,13 @@ impl<'a> Ext<'a> {
}
/// Extents de uma árvore ext4 (raiz em `node`, 60 bytes ou um bloco).
fn walk_extent_node(&self, node: &[u8], depth_limit: u32, out: &mut ExtentList, strict: bool) -> bool {
fn walk_extent_node(
&self,
node: &[u8],
depth_limit: u32,
out: &mut ExtentList,
strict: bool,
) -> bool {
if node.len() < 12 || le16(node, 0) != EXT_MAGIC {
return false;
}
@@ -219,7 +237,9 @@ impl<'a> Ext<'a> {
out.push((lblk, start, len, uninit));
} else {
let child = ((le16(e, 8) as u64) << 32) | le32(e, 4) as u64;
let Some(b) = self.read_block(child) else { return false };
let Some(b) = self.read_block(child) else {
return false;
};
if !self.walk_extent_node(&b, depth - 1, out, true) {
return false;
}
@@ -249,14 +269,12 @@ impl<'a> Ext<'a> {
v.truncate(max_blocks as usize);
} else {
ind(ptr(12), &mut v);
if (v.len() as u64) < max_blocks {
if ptr(13) != 0 {
if let Some(d) = self.read_block(ptr(13)) {
for i in 0..ppb as usize {
ind(le32(&d, i * 4) as u64, &mut v);
if v.len() as u64 >= max_blocks {
break;
}
if (v.len() as u64) < max_blocks && ptr(13) != 0 {
if let Some(d) = self.read_block(ptr(13)) {
for i in 0..ppb as usize {
ind(le32(&d, i * 4) as u64, &mut v);
if v.len() as u64 >= max_blocks {
break;
}
}
}
@@ -311,14 +329,22 @@ impl<'a> Ext<'a> {
}
if l > cur_l {
let hole = ((l - cur_l) * self.bs).min(left);
out.push(Extent { offset: 0, len: hole, sparse: true });
out.push(Extent {
offset: 0,
len: hole,
sparse: true,
});
left -= hole;
if left == 0 {
break;
}
}
let take = (n * self.bs).min(left);
out.push(Extent { offset: self.base + p * self.bs, len: take, sparse: uninit });
out.push(Extent {
offset: self.base + p * self.bs,
len: take,
sparse: uninit,
});
left -= take;
cur_l = l + n;
}
@@ -340,9 +366,13 @@ impl<'a> Ext<'a> {
if gr.flags & 0x2 != 0 {
used_bits.fill(false);
free.push(Extent::new(self.base + gstart * self.bs, gblocks * self.bs));
} else if let Some(bm) = read_vec(self.src, self.base + gr.block_bitmap * self.bs, gblocks.div_ceil(8) as usize)
.ok()
.filter(|b| b.len() as u64 == gblocks.div_ceil(8))
} else if let Some(bm) = read_vec(
self.src,
self.base + gr.block_bitmap * self.bs,
gblocks.div_ceil(8) as usize,
)
.ok()
.filter(|b| b.len() as u64 == gblocks.div_ceil(8))
{
let mut run: Option<u64> = None;
for i in 0..gblocks {
@@ -351,14 +381,20 @@ impl<'a> Ext<'a> {
match (used, run) {
(false, None) => run = Some(i),
(true, Some(s)) => {
free.push(Extent::new(self.base + (gstart + s) * self.bs, (i - s) * self.bs));
free.push(Extent::new(
self.base + (gstart + s) * self.bs,
(i - s) * self.bs,
));
run = None;
}
_ => {}
}
}
if let Some(s) = run {
free.push(Extent::new(self.base + (gstart + s) * self.bs, (gblocks - s) * self.bs));
free.push(Extent::new(
self.base + (gstart + s) * self.bs,
(gblocks - s) * self.bs,
));
}
}
allocated_all.extend(used_bits);
@@ -395,7 +431,11 @@ fn parse_dir_block(b: &[u8], max_ino: u64, out: &mut Vec<DirEnt>) {
let real = (8 + nlen).div_ceil(4) * 4;
if ino != 0 && nlen > 0 && ino <= max_ino {
let name = String::from_utf8_lossy(&b[off + 8..off + 8 + nlen]).to_string();
out.push(DirEnt { ino, name, live: true });
out.push(DirEnt {
ino,
name,
live: true,
});
}
// Folga: entradas apagadas ficam coladas depois do fim "real" desta.
let mut s = off + if ino != 0 || nlen > 0 { real } else { 8 };
@@ -413,7 +453,9 @@ fn parse_dir_block(b: &[u8], max_ino: u64, out: &mut Vec<DirEnt>) {
&& drec >= 8 + dn
&& drec % 4 == 0
&& s + drec <= end
&& b[s + 8..s + 8 + dn].iter().all(|&c| c >= 0x20 && c != b'/' && c != 0x7f);
&& b[s + 8..s + 8 + dn]
.iter()
.all(|&c| c >= 0x20 && c != b'/' && c != 0x7f);
if !ok {
s += 4;
continue;
@@ -437,16 +479,22 @@ struct Names {
}
fn walk_dirs(ext: &Ext, ctx: &Ctx) -> Names {
let mut names = Names { live: HashMap::new(), dead: HashMap::new() };
let mut names = Names {
live: HashMap::new(),
dead: HashMap::new(),
};
let mut queue: Vec<(u64, String)> = vec![(2, String::new())];
let mut seen: HashSet<u64> = HashSet::new();
let mut visited = 0usize;
while let Some((dino, path)) = queue.pop() {
if !seen.insert(dino) || (visited % 256 == 255 && (ctx.cancelled)()) || visited > 2_000_000 {
if !seen.insert(dino) || (visited % 256 == 255 && (ctx.cancelled)()) || visited > 2_000_000
{
continue;
}
visited += 1;
let Some(ino) = ext.read_inode(dino).filter(|i| i.is_dir()) else { continue };
let Some(ino) = ext.read_inode(dino).filter(|i| i.is_dir()) else {
continue;
};
let mut ents = vec![];
for e in ext.file_extents(&ino, ino.size.min(64 << 20)) {
if e.sparse {
@@ -467,10 +515,16 @@ fn walk_dirs(ext: &Ext, ctx: &Ctx) -> Names {
continue;
}
if en.live {
names.live.entry(en.ino).or_insert_with(|| (path.clone(), en.name.clone()));
names
.live
.entry(en.ino)
.or_insert_with(|| (path.clone(), en.name.clone()));
queue.push((en.ino, format!("{}/{}", path, en.name)));
} else {
names.dead.entry(en.ino).or_insert_with(|| (path.clone(), en.name));
names
.dead
.entry(en.ino)
.or_insert_with(|| (path.clone(), en.name));
}
}
}
@@ -485,7 +539,9 @@ fn journal_inodes(ext: &Ext, ctx: &Ctx) -> HashMap<u64, Vec<Vec<u8>>> {
if ext.journal_inum == 0 {
return out;
}
let Some(jino) = ext.read_inode(ext.journal_inum as u64) else { return out };
let Some(jino) = ext.read_inode(ext.journal_inum as u64) else {
return out;
};
let jext = ext.file_extents(&jino, jino.size.min(1 << 30));
// Blocos físicos do diário em ordem lógica.
let mut jblocks: Vec<u64> = vec![];
@@ -500,7 +556,9 @@ fn journal_inodes(ext: &Ext, ctx: &Ctx) -> HashMap<u64, Vec<Vec<u8>>> {
if jblocks.len() < 4 {
return out;
}
let Some(jsb) = ext.read_block(jblocks[0]) else { return out };
let Some(jsb) = ext.read_block(jblocks[0]) else {
return out;
};
if be32(&jsb, 0) != JBD2_MAGIC {
return out;
}
@@ -512,13 +570,21 @@ fn journal_inodes(ext: &Ext, ctx: &Ctx) -> HashMap<u64, Vec<Vec<u8>>> {
let is64 = incompat & 0x2 != 0;
let csum_v3 = incompat & 0x10 != 0;
let csum_v2 = incompat & 0x8 != 0;
let tag_bytes = if csum_v3 { 16 } else { 8 + if is64 { 4 } else { 0 } };
let tag_bytes = if csum_v3 {
16
} else {
8 + if is64 { 4 } else { 0 }
};
let have_csum = csum_v2 || csum_v3;
// Tabela de inodes: faixa de blocos de cada grupo.
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)
for (g, gr) in ext.groups.iter().enumerate() {
tables.push((gr.inode_table, gr.inode_table + itable_blocks, g as u64 * ext.ipg + 1));
tables.push((
gr.inode_table,
gr.inode_table + itable_blocks,
g as u64 * ext.ipg + 1,
));
}
let n = jblocks.len();
let first = (be32(&jsb, 20) as usize).clamp(1, n);
@@ -540,7 +606,9 @@ fn journal_inodes(ext: &Ext, ctx: &Ctx) -> HashMap<u64, Vec<Vec<u8>>> {
}
seqs.sort_by_key(|s| s.0);
for (_seq, didx) in seqs {
let Some(d) = ext.read_block(jblocks[didx]) else { continue };
let Some(d) = ext.read_block(jblocks[didx]) else {
continue;
};
let limit = d.len() - if have_csum { 4 } else { 0 };
let mut off = 12usize;
let mut data_idx = didx + 1;
@@ -552,14 +620,21 @@ fn journal_inodes(ext: &Ext, ctx: &Ctx) -> HashMap<u64, Vec<Vec<u8>>> {
let (flags, hi) = if csum_v3 {
(be32(&d, off + 4), be32(&d, off + 8))
} else {
(u16::from_be_bytes([d[off + 6], d[off + 7]]) as u32, if is64 { be32(&d, off + 8) } else { 0 })
(
u16::from_be_bytes([d[off + 6], d[off + 7]]) as u32,
if is64 { be32(&d, off + 8) } else { 0 },
)
};
blk |= (hi as u64) << 32;
off += tag_bytes;
if flags & 2 == 0 {
off += 16;
}
let di = if data_idx >= n { first + (data_idx - n) } else { data_idx };
let di = if data_idx >= n {
first + (data_idx - n)
} else {
data_idx
};
data_idx += 1;
if flags & 4 == 0 {
if let Some(t) = tables.iter().position(|t| blk >= t.0 && blk < t.1) {
@@ -570,9 +645,12 @@ fn journal_inodes(ext: &Ext, ctx: &Ctx) -> HashMap<u64, Vec<Vec<u8>>> {
let per_block = (ext.bs / ext.inode_size) as usize;
let first_ino = tables[t].2 + (blk - tables[t].0) * per_block as u64;
for k in 0..per_block {
let rec = &data[k * ext.inode_size as usize..(k + 1) * ext.inode_size as usize];
let rec = &data
[k * ext.inode_size as usize..(k + 1) * ext.inode_size as usize];
if rec[..128].iter().any(|&x| x != 0) {
out.entry(first_ino + k as u64).or_default().push(rec.to_vec());
out.entry(first_ino + k as u64)
.or_default()
.push(rec.to_vec());
}
}
}
@@ -612,13 +690,20 @@ pub fn scan(ctx: &mut Ctx, base: u64, part_len: u64) -> VolumeScan {
continue; // INODE_UNINIT
}
let used = ext.ipg.saturating_sub(gr.itable_unused as u64).min(ext.ipg);
let Some(table) = read_vec(ext.src, ext.base + gr.inode_table * ext.bs, (used * ext.inode_size) as usize).ok() else {
let Some(table) = read_vec(
ext.src,
ext.base + gr.inode_table * ext.bs,
(used * ext.inode_size) as usize,
)
.ok() else {
continue;
};
for i in 0..(table.len() as u64 / ext.inode_size) {
let ino_no = g as u64 * ext.ipg + i + 1;
let raw = &table[(i * ext.inode_size) as usize..((i + 1) * ext.inode_size) as usize];
let Some(ino) = Inode::parse(raw) else { continue };
let Some(ino) = Inode::parse(raw) else {
continue;
};
if names.live.contains_key(&ino_no) || ino_no < 11 || ino.mode == 0 {
continue;
}
@@ -645,7 +730,16 @@ pub fn scan(ctx: &mut Ctx, base: u64, part_len: u64) -> VolumeScan {
cand = Some((ino.clone(), "inode"));
}
if let Some((c, src_kind)) = cand {
if emit_inode(ctx, &ext, &alloc, &names, ino_no, &c, src_kind, &mut seen_extents) {
if emit_inode(
ctx,
&ext,
&alloc,
&names,
ino_no,
&c,
src_kind,
&mut seen_extents,
) {
emitted.insert(ino_no);
}
}
@@ -662,15 +756,31 @@ pub fn scan(ctx: &mut Ctx, base: u64, part_len: u64) -> VolumeScan {
}
let cur = ext.read_inode(*ino_no);
// Se o inode atual está vivo e é outro arquivo, não mexe.
if cur.as_ref().map(|c| c.dtime == 0 && c.links > 0 && c.mode != 0).unwrap_or(false) {
if cur
.as_ref()
.map(|c| c.dtime == 0 && c.links > 0 && c.mode != 0)
.unwrap_or(false)
{
continue;
}
for raw in snaps.iter().rev() {
if let Some(j) = Inode::parse(raw) {
if j.is_reg() && j.size > 0 && j.dtime == 0 && j.links > 0 {
if emit_inode(ctx, &ext, &alloc, &names, *ino_no, &j, "diário", &mut seen_extents) {
break;
}
if j.is_reg()
&& j.size > 0
&& j.dtime == 0
&& j.links > 0
&& emit_inode(
ctx,
&ext,
&alloc,
&names,
*ino_no,
&j,
"diário",
&mut seen_extents,
)
{
break;
}
}
}
@@ -716,7 +826,11 @@ fn emit_inode(
if extents.is_empty() || extents.iter().all(|e| e.sparse) {
return false;
}
let first = extents.iter().find(|e| !e.sparse).map(|e| (e.offset, e.len)).unwrap();
let first = extents
.iter()
.find(|e| !e.sparse)
.map(|e| (e.offset, e.len))
.unwrap();
if !seen.insert((first.0, first.1)) {
return false;
}
@@ -735,8 +849,14 @@ fn emit_inode(
let mut note = Some(format!("recuperado do {src_kind} do ext4"));
let mut conf = Confidence::High;
if reused > 0 {
conf = if reused * 2 > total_blocks { Confidence::Low } else { Confidence::Medium };
note = Some(format!("{reused} de {total_blocks} blocos já foram reaproveitados por outros arquivos"));
conf = if reused * 2 > total_blocks {
Confidence::Low
} else {
Confidence::Medium
};
note = Some(format!(
"{reused} de {total_blocks} blocos já foram reaproveitados por outros arquivos"
));
}
if estimated {
// Tamanho desconhecido: tenta achar o fim real pelo conteúdo.
@@ -754,7 +874,8 @@ fn emit_inode(
}
if estimated {
conf = conf.min(Confidence::Medium);
note = Some("tamanho exato perdido na exclusão; usando todos os blocos do arquivo".into());
note =
Some("tamanho exato perdido na exclusão; usando todos os blocos do arquivo".into());
}
}
let (dir, name) = match names.dead.get(&ino_no).or_else(|| names.live.get(&ino_no)) {
@@ -789,14 +910,22 @@ fn layout_from_list(ext: &Ext, list: &ExtentList, size: u64) -> Vec<Extent> {
}
if l > cur {
let hole = ((l - cur) * ext.bs).min(left);
out.push(Extent { offset: 0, len: hole, sparse: true });
out.push(Extent {
offset: 0,
len: hole,
sparse: true,
});
left -= hole;
if left == 0 {
break;
}
}
let take = (n * ext.bs).min(left);
out.push(Extent { offset: ext.base + p * ext.bs, len: take, sparse: uninit });
out.push(Extent {
offset: ext.base + p * ext.bs,
len: take,
sparse: uninit,
});
left -= take;
cur = l + n;
}
@@ -841,7 +970,9 @@ pub fn consistency_score(src: &dyn Source, base: u64) -> Option<f64> {
if !seen.insert(d) || total >= 600 {
continue;
}
let Some(ino) = ext.read_inode(d).filter(|i| i.is_dir()) else { continue };
let Some(ino) = ext.read_inode(d).filter(|i| i.is_dir()) else {
continue;
};
let mut ents = vec![];
for e in ext.file_extents(&ino, ino.size.min(1 << 20)) {
if e.sparse {
@@ -863,7 +994,10 @@ pub fn consistency_score(src: &dyn Source, base: u64) -> Option<f64> {
if ents.iter().any(|e| e.live && e.name == "." && e.ino == d) {
dots_ok += 1;
}
for en in ents.iter().filter(|e| e.live && e.name != "." && e.name != "..") {
for en in ents
.iter()
.filter(|e| e.live && e.name != "." && e.name != "..")
{
total += 1;
if let Some(i) = ext.read_inode(en.ino) {
if i.mode != 0 && i.links > 0 {
@@ -882,6 +1016,10 @@ pub fn consistency_score(src: &dyn Source, base: u64) -> Option<f64> {
return None;
}
let a = ok as f64 / total as f64;
let b = if dots_total > 0 { dots_ok as f64 / dots_total as f64 } else { 0.0 };
let b = if dots_total > 0 {
dots_ok as f64 / dots_total as f64
} else {
0.0
};
Some(1.0 + 5.0 * a + 5.0 * b)
}
+129 -31
View File
@@ -12,13 +12,19 @@ use std::collections::{HashMap, HashSet};
const HFS_EPOCH: i64 = 2_082_844_800;
fn be16(b: &[u8], o: usize) -> u16 {
b.get(o..o + 2).map(|s| u16::from_be_bytes([s[0], s[1]])).unwrap_or(0)
b.get(o..o + 2)
.map(|s| u16::from_be_bytes([s[0], s[1]]))
.unwrap_or(0)
}
fn be32(b: &[u8], o: usize) -> u32 {
b.get(o..o + 4).map(|s| u32::from_be_bytes(s.try_into().unwrap())).unwrap_or(0)
b.get(o..o + 4)
.map(|s| u32::from_be_bytes(s.try_into().unwrap()))
.unwrap_or(0)
}
fn be64(b: &[u8], o: usize) -> u64 {
b.get(o..o + 8).map(|s| u64::from_be_bytes(s.try_into().unwrap())).unwrap_or(0)
b.get(o..o + 8)
.map(|s| u64::from_be_bytes(s.try_into().unwrap()))
.unwrap_or(0)
}
#[derive(Clone, Debug, Default)]
@@ -29,7 +35,11 @@ struct Fork {
}
fn parse_fork(b: &[u8]) -> Fork {
let mut f = Fork { size: be64(b, 0), blocks: be32(b, 12), extents: vec![] };
let mut f = Fork {
size: be64(b, 0),
blocks: be32(b, 12),
extents: vec![],
};
for i in 0..8 {
let (s, c) = (be32(b, 16 + i * 8), be32(b, 20 + i * 8));
if c != 0 {
@@ -105,9 +115,13 @@ impl<'a> Vol<'a> {
have = ex.iter().map(|e| e.1 as u64).sum();
let _ = have;
}
merge_extents(ex.into_iter().map(|(s, c)| Extent::new(self.block_off(s as u64), c as u64 * self.bs)).collect::<Vec<_>>())
.into_iter()
.collect()
merge_extents(
ex.into_iter()
.map(|(s, c)| Extent::new(self.block_off(s as u64), c as u64 * self.bs))
.collect::<Vec<_>>(),
)
.into_iter()
.collect()
}
/// Extensões além das oito iniciais, procuradas no arquivo de overflow (árvore B+).
@@ -119,7 +133,9 @@ impl<'a> Vol<'a> {
};
let total_nodes = self.extents_file.size / node_size as u64;
for n in 1..total_nodes.min(100_000) {
let Some(node) = self.read_tree_node(&self.extents_file, n as u32, node_size) else { continue };
let Some(node) = self.read_tree_node(&self.extents_file, n as u32, node_size) else {
continue;
};
if node[8] != 0xFF {
continue;
}
@@ -193,8 +209,13 @@ fn records(node: &[u8]) -> Vec<&[u8]> {
}
fn utf16be(b: &[u8]) -> String {
let u: Vec<u16> = b.chunks_exact(2).map(|c| u16::from_be_bytes([c[0], c[1]])).collect();
String::from_utf16_lossy(&u).replace('/', "_").replace(':', "/")
let u: Vec<u16> = b
.chunks_exact(2)
.map(|c| u16::from_be_bytes([c[0], c[1]]))
.collect();
String::from_utf16_lossy(&u)
.replace('/', "_")
.replace(':', "/")
}
struct CatFile {
@@ -213,7 +234,14 @@ fn date(d: u32) -> Option<i64> {
}
/// Decodifica um registro do catálogo (chave + dados). `max_id` descarta lixo.
fn decode_record(r: &[u8], max_id: u32, files: &mut Vec<CatFile>, threads: &mut HashMap<u32, (u32, String)>, folders: &mut HashMap<(u32, String), u32>, stale: bool) {
fn decode_record(
r: &[u8],
max_id: u32,
files: &mut Vec<CatFile>,
threads: &mut HashMap<u32, (u32, String)>,
folders: &mut HashMap<(u32, String), u32>,
stale: bool,
) {
let klen = be16(r, 0) as usize;
if klen < 6 || klen + 2 > r.len() {
return;
@@ -240,7 +268,10 @@ fn decode_record(r: &[u8], max_id: u32, files: &mut Vec<CatFile>, threads: &mut
// thread de pasta: chave = (id da pasta, ""); dados = (pai, nome)
let pn = be16(rec, 8) as usize;
if pn <= 255 {
threads.entry(parent).or_insert((be32(rec, 4), utf16be(rec.get(10..10 + pn * 2).unwrap_or(&[]))));
threads.entry(parent).or_insert((
be32(rec, 4),
utf16be(rec.get(10..10 + pn * 2).unwrap_or(&[])),
));
}
}
2 if rec.len() >= 248 => {
@@ -263,7 +294,10 @@ fn decode_record(r: &[u8], max_id: u32, files: &mut Vec<CatFile>, threads: &mut
4 if rec.len() >= 10 => {
let pn = be16(rec, 8) as usize;
if pn <= 255 {
threads.entry(parent).or_insert((be32(rec, 4), utf16be(rec.get(10..10 + pn * 2).unwrap_or(&[]))));
threads.entry(parent).or_insert((
be32(rec, 4),
utf16be(rec.get(10..10 + pn * 2).unwrap_or(&[])),
));
}
}
_ => {}
@@ -271,7 +305,13 @@ fn decode_record(r: &[u8], max_id: u32, files: &mut Vec<CatFile>, threads: &mut
}
/// Procura registros de arquivo nas sobras de um nó (entre os dados e a tabela de offsets).
fn scan_slack(node: &[u8], max_id: u32, files: &mut Vec<CatFile>, threads: &mut HashMap<u32, (u32, String)>, folders: &mut HashMap<(u32, String), u32>) {
fn scan_slack(
node: &[u8],
max_id: u32,
files: &mut Vec<CatFile>,
threads: &mut HashMap<u32, (u32, String)>,
folders: &mut HashMap<(u32, String), u32>,
) {
let n = be16(node, 10) as usize;
let table = node.len().saturating_sub(2 * (n + 1));
let used_end = (be16(node, table) as usize).max(14);
@@ -284,7 +324,7 @@ fn scan_slack(node: &[u8], max_id: u32, files: &mut Vec<CatFile>, threads: &mut
if parent >= 1 && parent < max_id.max(16) {
let d = (p + 2 + klen + 1) & !1;
let rt = be16(node, d);
if matches!(rt, 1 | 2 | 3 | 4) {
if matches!(rt, 1..=4) {
let end = (d + 248).min(table);
decode_record(&node[p..end], max_id, files, threads, folders, true);
p = d + if rt == 2 { 248 } else { 16 };
@@ -299,15 +339,22 @@ fn scan_slack(node: &[u8], max_id: u32, files: &mut Vec<CatFile>, threads: &mut
pub fn scan(ctx: &mut Ctx, base: u64, part_len: u64) -> VolumeScan {
let mut res = VolumeScan::default();
let _ = part_len;
let Some(vbase) = locate(ctx.src, base) else { return res };
let Some(vol) = Vol::open(ctx.src, vbase) else { return res };
let Some(vbase) = locate(ctx.src, base) else {
return res;
};
let Some(vol) = Vol::open(ctx.src, vbase) else {
return res;
};
// Mapa de espaço livre.
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;
for b in 0..vol.total {
let used = bm.get((b / 8) as usize).map(|x| x >> (7 - (b % 8)) & 1 == 1).unwrap_or(true);
let used = bm
.get((b / 8) as usize)
.map(|x| x >> (7 - (b % 8)) & 1 == 1)
.unwrap_or(true);
match (used, run) {
(false, None) => run = Some(b),
(true, Some(s)) => {
@@ -327,7 +374,9 @@ pub fn scan(ctx: &mut Ctx, base: u64, part_len: u64) -> VolumeScan {
res.warnings.push("HFS+: catálogo ilegível.".into());
return res;
};
let Some(hdr) = vol.read_tree_node(&vol.catalog, 0, ns) else { return res };
let Some(hdr) = vol.read_tree_node(&vol.catalog, 0, ns) else {
return res;
};
let first_leaf = be32(&hdr, 24);
let max_id = vol.next_cnid.max(16);
let mut files: Vec<CatFile> = vec![];
@@ -342,7 +391,9 @@ pub fn scan(ctx: &mut Ctx, base: u64, part_len: u64) -> VolumeScan {
if (ctx.cancelled)() {
return res;
}
let Some(node) = vol.read_tree_node(&vol.catalog, n, ns) else { break };
let Some(node) = vol.read_tree_node(&vol.catalog, n, ns) else {
break;
};
if node[8] != 0xFF {
break;
}
@@ -360,7 +411,15 @@ pub fn scan(ctx: &mut Ctx, base: u64, part_len: u64) -> VolumeScan {
// Diário: cópias antigas de nós folha do catálogo.
if vol.journal_info != 0 {
scan_journal(&vol, ns, max_id, &mut files, &mut threads, &mut folders, ctx);
scan_journal(
&vol,
ns,
max_id,
&mut files,
&mut threads,
&mut folders,
ctx,
);
}
// Caminho de cada pasta a partir dos threads.
@@ -385,13 +444,19 @@ pub fn scan(ctx: &mut Ctx, base: u64, part_len: u64) -> VolumeScan {
let free_set = res.free.clone().unwrap_or_default();
let mut seen: HashSet<(u32, u64)> = HashSet::new();
for f in files.iter().filter(|f| f.stale && !live_ids.contains(&f.id)) {
for f in files
.iter()
.filter(|f| f.stale && !live_ids.contains(&f.id))
{
if f.data.size == 0 || f.data.extents.is_empty() {
continue;
}
let ex = vol.fork_extents(&f.data, f.id, 0);
let total_len: u64 = ex.iter().map(|e| e.len).sum();
if ex.is_empty() || total_len < f.data.size || ex.iter().any(|e| e.end() > vol.base + vol.total * vol.bs) {
if ex.is_empty()
|| total_len < f.data.size
|| ex.iter().any(|e| e.end() > vol.base + vol.total * vol.bs)
{
continue;
}
if !seen.insert((f.id, ex[0].offset)) {
@@ -402,9 +467,15 @@ pub fn scan(ctx: &mut Ctx, base: u64, part_len: u64) -> VolumeScan {
// Blocos já reaproveitados?
let reused = extents.iter().filter(|e| !covered_by(&free_set, e)).count();
let (conf, note) = if reused == 0 {
(Confidence::High, "registro apagado do catálogo HFS+; blocos ainda livres".to_string())
(
Confidence::High,
"registro apagado do catálogo HFS+; blocos ainda livres".to_string(),
)
} else {
(Confidence::Low, "alguns blocos do arquivo já foram reaproveitados".to_string())
(
Confidence::Low,
"alguns blocos do arquivo já foram reaproveitados".to_string(),
)
};
let nf = NewFound {
name: f.name.clone(),
@@ -426,7 +497,9 @@ pub fn scan(ctx: &mut Ctx, base: u64, part_len: u64) -> VolumeScan {
fn covered_by(free: &[Extent], e: &Extent) -> bool {
let i = free.partition_point(|f| f.end() <= e.offset);
free.get(i).map(|f| f.offset <= e.offset && f.end() >= e.end()).unwrap_or(false)
free.get(i)
.map(|f| f.offset <= e.offset && f.end() >= e.end())
.unwrap_or(false)
}
impl Vol<'_> {
@@ -434,7 +507,14 @@ impl Vol<'_> {
let mut out = vec![];
for &(s, c) in &f.extents {
let n = (c as u64 * self.bs) as usize;
out.extend(read_vec(self.src, self.block_off(s as u64), n.min(max.saturating_sub(out.len()))).ok()?);
out.extend(
read_vec(
self.src,
self.block_off(s as u64),
n.min(max.saturating_sub(out.len())),
)
.ok()?,
);
if out.len() >= max {
break;
}
@@ -444,8 +524,21 @@ impl Vol<'_> {
}
/// Procura nós folha do catálogo nas transações guardadas no diário.
fn scan_journal(vol: &Vol, ns: usize, max_id: u32, files: &mut Vec<CatFile>, threads: &mut HashMap<u32, (u32, String)>, folders: &mut HashMap<(u32, String), u32>, ctx: &Ctx) {
let Some(info) = read_vec(vol.src, vol.block_off(vol.journal_info as u64), 52).ok().filter(|b| b.len() == 52) else { return };
fn scan_journal(
vol: &Vol,
ns: usize,
max_id: u32,
files: &mut Vec<CatFile>,
threads: &mut HashMap<u32, (u32, String)>,
folders: &mut HashMap<(u32, String), u32>,
ctx: &Ctx,
) {
let Some(info) = read_vec(vol.src, vol.block_off(vol.journal_info as u64), 52)
.ok()
.filter(|b| b.len() == 52)
else {
return;
};
let (joff, jsize) = (be64(&info, 36), be64(&info, 44));
if jsize == 0 || jsize > 1 << 30 || vol.base + joff + jsize > vol.src.size() {
return;
@@ -457,14 +550,19 @@ fn scan_journal(vol: &Vol, ns: usize, max_id: u32, files: &mut Vec<CatFile>, thr
return;
}
let n = chunk.min(jsize - pos) as usize;
let Ok(buf) = read_vec(vol.src, vol.base + joff + pos, n + ns) else { break };
let Ok(buf) = read_vec(vol.src, vol.base + joff + pos, n + ns) else {
break;
};
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
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 && be16(nd, table) as usize > 14 && (be16(nd, table) as usize) <= table {
if (be16(nd, ns - 2) as usize) == 14
&& be16(nd, table) as usize > 14
&& (be16(nd, table) as usize) <= table
{
for r in records(nd) {
decode_record(r, max_id, files, threads, folders, true);
}
+1 -1
View File
@@ -10,8 +10,8 @@ pub mod exfat;
pub mod ext;
pub mod fat;
pub mod hfsplus;
pub mod xfs;
pub mod ntfs;
pub mod xfs;
use crate::signatures::{category_for_ext, matches_ext};
use crate::source::{read_vec, Source};
+105 -26
View File
@@ -10,13 +10,19 @@ use crate::types::{Confidence, DataLoc, Extent, Origin};
use std::collections::{HashMap, HashSet};
fn be16(b: &[u8], o: usize) -> u16 {
b.get(o..o + 2).map(|s| u16::from_be_bytes([s[0], s[1]])).unwrap_or(0)
b.get(o..o + 2)
.map(|s| u16::from_be_bytes([s[0], s[1]]))
.unwrap_or(0)
}
fn be32(b: &[u8], o: usize) -> u32 {
b.get(o..o + 4).map(|s| u32::from_be_bytes(s.try_into().unwrap())).unwrap_or(0)
b.get(o..o + 4)
.map(|s| u32::from_be_bytes(s.try_into().unwrap()))
.unwrap_or(0)
}
fn be64(b: &[u8], o: usize) -> u64 {
b.get(o..o + 8).map(|s| u64::from_be_bytes(s.try_into().unwrap())).unwrap_or(0)
b.get(o..o + 8)
.map(|s| u64::from_be_bytes(s.try_into().unwrap()))
.unwrap_or(0)
}
struct Xfs<'a> {
@@ -46,7 +52,12 @@ impl<'a> Xfs<'a> {
let agblocks = be32(&sb, 84) as u64;
let agcount = be32(&sb, 88) as u64;
let inodesize = be16(&sb, 104) as u64;
if !(512..=65536).contains(&bs) || !bs.is_power_of_two() || agblocks == 0 || agcount == 0 || !(256..=2048).contains(&inodesize) {
if !(512..=65536).contains(&bs)
|| !bs.is_power_of_two()
|| agblocks == 0
|| agcount == 0
|| !(256..=2048).contains(&inodesize)
{
return None;
}
let incompat = be32(&sb, 216);
@@ -63,7 +74,11 @@ impl<'a> Xfs<'a> {
agblklog: sb[124] as u32,
sectsize: be16(&sb, 102) as u64,
v5,
ftype: if v5 { incompat & 1 != 0 } else { features2 & 0x200 != 0 },
ftype: if v5 {
incompat & 1 != 0
} else {
features2 & 0x200 != 0
},
root_ino: be64(&sb, 56),
dblocks: be64(&sb, 8),
})
@@ -84,7 +99,8 @@ impl<'a> Xfs<'a> {
let agno = ino >> shift;
let agbno = (ino >> self.inopblog) & ((1 << self.agblklog) - 1);
let idx = ino & ((1 << self.inopblog) - 1);
(agno < self.agcount && agbno < self.agblocks).then(|| self.ag_off(agno) + agbno * self.bs + idx * self.inodesize)
(agno < self.agcount && agbno < self.agblocks)
.then(|| self.ag_off(agno) + agbno * self.bs + idx * self.inodesize)
}
fn read_ino(&self, ino: u64) -> Option<Vec<u8>> {
@@ -104,7 +120,11 @@ impl<'a> Xfs<'a> {
fn extents_of(&self, ino: &[u8], max_blocks: u64) -> Vec<(u64, u64, u64)> {
let fmt = ino[5];
let start = self.core_size(ino);
let fork_end = if ino[82] != 0 { start.max(100 + ino[82] as usize * 8) } else { self.inodesize as usize };
let fork_end = if ino[82] != 0 {
start.max(100 + ino[82] as usize * 8)
} else {
self.inodesize as usize
};
let lit = &ino[start..fork_end.min(ino.len())];
let mut out = vec![];
match fmt {
@@ -136,7 +156,9 @@ impl<'a> Xfs<'a> {
return;
}
let Some(off) = self.fsb_off(fsb) else { return };
let Ok(b) = read_vec(self.src, off, self.bs as usize) else { return };
let Ok(b) = read_vec(self.src, off, self.bs as usize) else {
return;
};
if b.len() < 24 || !(&b[0..4] == b"BMAP" || &b[0..4] == b"BMA3") {
return;
}
@@ -169,11 +191,21 @@ fn unpack_extent(r: &[u8]) -> (u64, u64, u64) {
}
/// Percorre as folhas de uma árvore B+ curta, entregando cada registro de 16 (inobt) ou 8 (bnobt) bytes.
fn walk_short_btree(x: &Xfs, agno: u64, root: u64, levels: u32, rec: usize, key_size: usize, f: &mut dyn FnMut(&[u8])) {
fn walk_short_btree(
x: &Xfs,
agno: u64,
root: u64,
levels: u32,
rec: usize,
key_size: usize,
f: &mut dyn FnMut(&[u8]),
) {
let mut cur = root;
// desce pela esquerda até a folha
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 };
let Ok(b) = read_vec(x.src, x.ag_off(agno) + cur * x.bs, x.bs as usize) else {
return;
};
if b.len() < 24 {
return;
}
@@ -185,7 +217,9 @@ fn walk_short_btree(x: &Xfs, agno: u64, root: u64, levels: u32, rec: usize, key_
}
let mut seen = HashSet::new();
while cur != 0 && cur != u32::MAX as u64 && seen.insert(cur) {
let Ok(b) = read_vec(x.src, x.ag_off(agno) + cur * x.bs, x.bs as usize) else { return };
let Ok(b) = read_vec(x.src, x.ag_off(agno) + cur * x.bs, x.bs as usize) else {
return;
};
if b.len() < 24 {
return;
}
@@ -203,7 +237,9 @@ fn walk_short_btree(x: &Xfs, agno: u64, root: u64, levels: u32, rec: usize, key_
pub fn scan(ctx: &mut Ctx, base: u64, part_len: u64) -> VolumeScan {
let _ = part_len;
let mut res = VolumeScan::default();
let Some(x) = Xfs::open(ctx.src, base) else { return res };
let Some(x) = Xfs::open(ctx.src, base) else {
return res;
};
// Espaço livre por grupo e inodes livres.
let mut free: Vec<Extent> = vec![];
@@ -237,7 +273,9 @@ pub fn scan(ctx: &mut Ctx, base: u64, part_len: u64) -> VolumeScan {
free_set.sort();
let is_free = |fsb: u64, count: u64| -> bool {
let i = free_set.partition_point(|&(s, c)| s + c <= fsb);
free_set.get(i).map_or(false, |&(s, c)| s <= fsb && s + c >= fsb + count)
free_set
.get(i)
.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).
@@ -290,16 +328,30 @@ pub fn scan(ctx: &mut Ctx, base: u64, part_len: u64) -> VolumeScan {
continue;
}
let all_free = ext.iter().all(|e| is_free(e.1, e.2));
let extents: Vec<Extent> = ext.iter().filter_map(|e| x.fsb_off(e.1).map(|o| Extent::new(o, e.2 * x.bs))).collect();
let extents: Vec<Extent> = ext
.iter()
.filter_map(|e| x.fsb_off(e.1).map(|o| Extent::new(o, e.2 * x.bs)))
.collect();
if extents.is_empty() || !seen_first.insert(extents[0].offset) {
continue;
}
let mut size = total_blocks * x.bs;
let mut conf = if all_free { Confidence::High } else { Confidence::Low };
let mut note = if all_free { "inode XFS liberado; lista de extents recuperada do próprio inode".to_string() } else { "blocos do arquivo já reaproveitados".to_string() };
let mut conf = if all_free {
Confidence::High
} else {
Confidence::Low
};
let mut note = if all_free {
"inode XFS liberado; lista de extents recuperada do próprio inode".to_string()
} else {
"blocos do arquivo já reaproveitados".to_string()
};
// tamanho exato: tenta achar o fim real pelo conteúdo
let mut est = true;
if let Some(h) = read_vec(ctx.src, extents[0].offset, 128).ok().and_then(|head| signatures::detect(ctx.src, extents[0].offset, &head, size)) {
if let Some(h) = read_vec(ctx.src, extents[0].offset, 128)
.ok()
.and_then(|head| signatures::detect(ctx.src, extents[0].offset, &head, size))
{
if h.len > 0 && h.len <= size {
size = h.len;
est = false;
@@ -312,7 +364,10 @@ pub fn scan(ctx: &mut Ctx, base: u64, part_len: u64) -> VolumeScan {
let extents = super::fat::trim_extents(extents, size);
let low = ino & 0xFFFF_FFFF;
let (dir, name) = match stale_names.get(&low) {
Some((parent, n)) => (Some(live_dirs.get(parent).cloned().unwrap_or_default()), n.clone()),
Some((parent, n)) => (
Some(live_dirs.get(parent).cloned().unwrap_or_default()),
n.clone(),
),
None => (None, format!("inode_{ino}")),
};
let ts = |s: u32| (s != 0).then_some(s as i64);
@@ -323,7 +378,11 @@ pub fn scan(ctx: &mut Ctx, base: u64, part_len: u64) -> VolumeScan {
origin: Origin::Xfs,
data: DataLoc::Extents { extents },
modified: ts(be32(&raw, 40)),
created: if raw[4] >= 3 { ts(be32(&raw, 144)) } else { None },
created: if raw[4] >= 3 {
ts(be32(&raw, 144))
} else {
None
},
accessed: ts(be32(&raw, 32)),
base: conf,
note: Some(&note),
@@ -345,7 +404,9 @@ fn collect_names(x: &Xfs, ctx: &Ctx) -> (HashMap<u64, String>, HashMap<u64, (u64
if !seen.insert(dino) || (ctx.cancelled)() || seen.len() > 200_000 {
continue;
}
let Some(raw) = x.read_ino(dino) else { continue };
let Some(raw) = x.read_ino(dino) else {
continue;
};
if be16(&raw, 2) & 0o170000 != 0o040000 {
continue;
}
@@ -365,9 +426,14 @@ fn collect_names(x: &Xfs, ctx: &Ctx) -> (HashMap<u64, String>, HashMap<u64, (u64
}
let nl = lit[p] as usize;
p += 3;
let name = String::from_utf8_lossy(lit.get(p..p + nl).unwrap_or(&[])).to_string();
let name =
String::from_utf8_lossy(lit.get(p..p + nl).unwrap_or(&[])).to_string();
p += nl + if x.ftype { 1 } else { 0 };
let ino = if i8 { be64(lit, p) } else { be32(lit, p) as u64 };
let ino = if i8 {
be64(lit, p)
} else {
be32(lit, p) as u64
};
p += isz;
queue.push((ino, format!("{path}/{name}")));
}
@@ -382,8 +448,12 @@ fn collect_names(x: &Xfs, ctx: &Ctx) -> (HashMap<u64, String>, HashMap<u64, (u64
continue;
}
for k in 0..cnt.min(256) {
let Some(o) = x.fsb_off(fsb + k) else { continue };
let Ok(b) = read_vec(x.src, o, x.bs as usize) else { continue };
let Some(o) = x.fsb_off(fsb + k) else {
continue;
};
let Ok(b) = read_vec(x.src, o, x.bs as usize) else {
continue;
};
parse_dir_block(x, &b, dino, &path, &mut queue, &mut stale);
}
}
@@ -394,7 +464,14 @@ fn collect_names(x: &Xfs, ctx: &Ctx) -> (HashMap<u64, String>, HashMap<u64, (u64
(paths, stale)
}
fn parse_dir_block(x: &Xfs, b: &[u8], dino: u64, path: &str, queue: &mut Vec<(u64, String)>, stale: &mut HashMap<u64, (u64, String)>) {
fn parse_dir_block(
x: &Xfs,
b: &[u8],
dino: u64,
path: &str,
queue: &mut Vec<(u64, String)>,
stale: &mut HashMap<u64, (u64, String)>,
) {
let magic = &b[0..4];
let hdr = match magic {
b"XD2B" | b"XD2D" => 16,
@@ -447,7 +524,9 @@ fn scan_free_region(x: &Xfs, r: &[u8], dir_ino: u64, stale: &mut HashMap<u64, (u
if nl == 0 || name.iter().any(|&c| c < 0x20 || c == b'/' || c == 0x7F) {
return None;
}
stale.entry(low).or_insert((dir_ino, String::from_utf8_lossy(name).to_string()));
stale
.entry(low)
.or_insert((dir_ino, String::from_utf8_lossy(name).to_string()));
Some((8 + 1 + nl + if x.ftype { 1 } else { 0 } + 2).div_ceil(8) * 8)
};
if r.len() >= 16 {