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
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:
1 parent
74218e7beb
commit
beb83fef92
49 files changed
+3806
-854
No files matched your search
@@ -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();
|
||||
|
||||
@@ -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
@@ -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)
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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
@@ -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(¬e),
|
||||
@@ -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 {
|
||||
|
||||
Reference in new issue
Block a user