HFS+: catálogo, mapa livre, registros apagados em sobras de nós e no diário
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
b8dfe21fd7
commit
a9ac1dddba
5 files changed
+537
-1
No files matched your search
@@ -0,0 +1,478 @@
|
||||
//! HFS+ / HFSX (inclusive volumes embutidos num wrapper HFS clássico).
|
||||
//!
|
||||
//! Lê o catálogo (árvore B+) para nomes e caminhos, o bitmap de alocação para o mapa de
|
||||
//! espaço livre e procura arquivos apagados em três lugares: sobras dentro dos nós folha do
|
||||
//! catálogo, cópias antigas de nós no diário (journal) e registros de arquivo "soltos".
|
||||
|
||||
use super::{make_found, merge_extents, Ctx, NewFound, VolumeScan};
|
||||
use crate::source::{read_vec, Source};
|
||||
use crate::types::{Confidence, DataLoc, Extent, Origin};
|
||||
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)
|
||||
}
|
||||
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)
|
||||
}
|
||||
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)
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
struct Fork {
|
||||
size: u64,
|
||||
blocks: u32,
|
||||
extents: Vec<(u32, u32)>,
|
||||
}
|
||||
|
||||
fn parse_fork(b: &[u8]) -> Fork {
|
||||
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 {
|
||||
f.extents.push((s, c));
|
||||
}
|
||||
}
|
||||
f
|
||||
}
|
||||
|
||||
/// Posição do volume HFS+ dentro da partição (wrapper HFS clássico) ou `None` se não for HFS+.
|
||||
pub fn locate(src: &dyn Source, base: u64) -> Option<u64> {
|
||||
let h = read_vec(src, base + 1024, 512).ok()?;
|
||||
if h.len() < 512 {
|
||||
return None;
|
||||
}
|
||||
match &h[0..2] {
|
||||
b"H+" | b"HX" => Some(base),
|
||||
b"BD" if &h[124..126] == b"H+" => {
|
||||
let alblk = be32(&h, 20) as u64;
|
||||
let alblst = be16(&h, 28) as u64;
|
||||
let start = be16(&h, 126) as u64;
|
||||
Some(base + alblst * 512 + start * alblk)
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
struct Vol<'a> {
|
||||
src: &'a dyn Source,
|
||||
base: u64,
|
||||
bs: u64,
|
||||
total: u64,
|
||||
catalog: Fork,
|
||||
extents_file: Fork,
|
||||
alloc: Fork,
|
||||
next_cnid: u32,
|
||||
journal_info: u32,
|
||||
}
|
||||
|
||||
impl<'a> Vol<'a> {
|
||||
fn open(src: &'a dyn Source, base: u64) -> Option<Vol<'a>> {
|
||||
let h = read_vec(src, base + 1024, 512).ok()?;
|
||||
if h.len() < 512 || !(&h[0..2] == b"H+" || &h[0..2] == b"HX") {
|
||||
return None;
|
||||
}
|
||||
let bs = be32(&h, 40) as u64;
|
||||
if bs < 512 || !bs.is_power_of_two() {
|
||||
return None;
|
||||
}
|
||||
Some(Vol {
|
||||
src,
|
||||
base,
|
||||
bs,
|
||||
total: be32(&h, 44) as u64,
|
||||
alloc: parse_fork(&h[112..192]),
|
||||
extents_file: parse_fork(&h[192..272]),
|
||||
catalog: parse_fork(&h[272..352]),
|
||||
next_cnid: be32(&h, 64),
|
||||
journal_info: be32(&h, 12),
|
||||
})
|
||||
}
|
||||
|
||||
fn block_off(&self, b: u64) -> u64 {
|
||||
self.base + b * self.bs
|
||||
}
|
||||
|
||||
/// Todas as extensões de um fork (inclusive as do arquivo de overflow).
|
||||
fn fork_extents(&self, f: &Fork, file_id: u32, fork_type: u8) -> Vec<Extent> {
|
||||
let mut ex: Vec<(u32, u32)> = f.extents.clone();
|
||||
let mut have: u64 = ex.iter().map(|e| e.1 as u64).sum();
|
||||
if have < f.blocks as u64 && file_id >= 2 {
|
||||
ex.extend(self.overflow_extents(file_id, fork_type, have as u32));
|
||||
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()
|
||||
}
|
||||
|
||||
/// Extensões além das oito iniciais, procuradas no arquivo de overflow (árvore B+).
|
||||
fn overflow_extents(&self, file_id: u32, fork_type: u8, mut start: u32) -> Vec<(u32, u32)> {
|
||||
let mut out = vec![];
|
||||
let node_size = match self.tree_node_size(&self.extents_file) {
|
||||
Some(n) => n,
|
||||
None => return out,
|
||||
};
|
||||
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 };
|
||||
if node[8] != 0xFF {
|
||||
continue;
|
||||
}
|
||||
for r in records(&node) {
|
||||
// chave: keyLength u16, forkType u8, pad u8, fileID u32, startBlock u32
|
||||
if r.len() < 12 + 64 || be16(r, 0) != 10 {
|
||||
continue;
|
||||
}
|
||||
if r[2] == fork_type && be32(r, 4) == file_id && be32(r, 8) == start {
|
||||
let mut got = 0;
|
||||
for i in 0..8 {
|
||||
let (s, c) = (be32(r, 12 + i * 8), be32(r, 16 + i * 8));
|
||||
if c != 0 {
|
||||
out.push((s, c));
|
||||
got += c;
|
||||
}
|
||||
}
|
||||
start += got;
|
||||
}
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
fn tree_node_size(&self, f: &Fork) -> Option<usize> {
|
||||
let first = self.read_fork_bytes(f, 0, 64)?;
|
||||
let ns = be16(&first, 32) as usize;
|
||||
(512..=65536).contains(&ns).then_some(ns)
|
||||
}
|
||||
|
||||
fn read_tree_node(&self, f: &Fork, n: u32, ns: usize) -> Option<Vec<u8>> {
|
||||
self.read_fork_bytes(f, n as u64 * ns as u64, ns)
|
||||
}
|
||||
|
||||
/// Lê bytes de um fork que tem só extensões iniciais ou overflow já resolvido (para árvores).
|
||||
fn read_fork_bytes(&self, f: &Fork, off: u64, len: usize) -> Option<Vec<u8>> {
|
||||
let mut out = Vec::with_capacity(len);
|
||||
let mut pos = 0u64;
|
||||
for &(s, c) in &f.extents {
|
||||
let elen = c as u64 * self.bs;
|
||||
if off + out.len() as u64 >= pos && off + (out.len() as u64) < pos + elen {
|
||||
let within = off + out.len() as u64 - pos;
|
||||
let n = ((elen - within) as usize).min(len - out.len());
|
||||
out.extend(read_vec(self.src, self.block_off(s as u64) + within, n).ok()?);
|
||||
if out.len() == len {
|
||||
return Some(out);
|
||||
}
|
||||
}
|
||||
pos += elen;
|
||||
}
|
||||
(out.len() == len).then_some(out)
|
||||
}
|
||||
}
|
||||
|
||||
/// Registros de um nó B+ (fatias, da chave em diante).
|
||||
fn records(node: &[u8]) -> Vec<&[u8]> {
|
||||
let n = be16(node, 10) as usize;
|
||||
let mut offs = vec![];
|
||||
for i in 0..=n {
|
||||
let p = node.len().saturating_sub(2 * (i + 1));
|
||||
offs.push(be16(node, p) as usize);
|
||||
}
|
||||
let mut out = vec![];
|
||||
for i in 0..n {
|
||||
let (a, b) = (offs[i], offs[i + 1]);
|
||||
if a >= 14 && b > a && b <= node.len() {
|
||||
out.push(&node[a..b]);
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
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(':', "/")
|
||||
}
|
||||
|
||||
struct CatFile {
|
||||
parent: u32,
|
||||
name: String,
|
||||
id: u32,
|
||||
data: Fork,
|
||||
created: u32,
|
||||
modified: u32,
|
||||
accessed: u32,
|
||||
stale: bool,
|
||||
}
|
||||
|
||||
fn date(d: u32) -> Option<i64> {
|
||||
(d != 0).then(|| d as i64 - HFS_EPOCH)
|
||||
}
|
||||
|
||||
/// Decodifica um registro do catálogo (chave + dados). `max_id` descarta lixo.
|
||||
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;
|
||||
}
|
||||
let parent = be32(r, 2);
|
||||
let nlen = be16(r, 6) as usize;
|
||||
if nlen > 255 || klen != 6 + nlen * 2 && klen != 6 {
|
||||
return;
|
||||
}
|
||||
let name = utf16be(r.get(8..8 + nlen * 2).unwrap_or(&[]));
|
||||
let mut d = 2 + klen;
|
||||
if d % 2 == 1 {
|
||||
d += 1;
|
||||
}
|
||||
let Some(rec) = r.get(d..) else { return };
|
||||
match be16(rec, 0) {
|
||||
1 if rec.len() >= 88 => {
|
||||
let id = be32(rec, 8);
|
||||
if id >= 2 && id < max_id.max(16) {
|
||||
folders.entry((parent, name)).or_insert(id);
|
||||
}
|
||||
}
|
||||
3 if rec.len() >= 10 => {
|
||||
// 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(&[]))));
|
||||
}
|
||||
}
|
||||
2 if rec.len() >= 248 => {
|
||||
let id = be32(rec, 8);
|
||||
if id < 16 || id >= max_id.max(16) {
|
||||
return;
|
||||
}
|
||||
let data = parse_fork(&rec[88..168]);
|
||||
files.push(CatFile {
|
||||
parent,
|
||||
name,
|
||||
id,
|
||||
data,
|
||||
created: be32(rec, 12),
|
||||
modified: be32(rec, 16),
|
||||
accessed: be32(rec, 24),
|
||||
stale,
|
||||
});
|
||||
}
|
||||
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(&[]))));
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
/// 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>) {
|
||||
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);
|
||||
let mut p = used_end + (used_end % 2);
|
||||
while p + 40 < table {
|
||||
let klen = be16(node, p) as usize;
|
||||
let nlen = be16(node, p + 6) as usize;
|
||||
if klen == 6 + nlen * 2 && (1..=255).contains(&nlen) && p + 2 + klen + 2 < table {
|
||||
let parent = be32(node, p + 2);
|
||||
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) {
|
||||
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 };
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
p += 2;
|
||||
}
|
||||
}
|
||||
|
||||
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 };
|
||||
|
||||
// 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);
|
||||
match (used, run) {
|
||||
(false, None) => run = Some(b),
|
||||
(true, Some(s)) => {
|
||||
free.push(Extent::new(vol.block_off(s), (b - s) * vol.bs));
|
||||
run = None;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
if let Some(s) = run {
|
||||
free.push(Extent::new(vol.block_off(s), (vol.total - s) * vol.bs));
|
||||
}
|
||||
res.free = Some(merge_extents(free));
|
||||
}
|
||||
|
||||
let Some(ns) = vol.tree_node_size(&vol.catalog) else {
|
||||
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 first_leaf = be32(&hdr, 24);
|
||||
let max_id = vol.next_cnid.max(16);
|
||||
let mut files: Vec<CatFile> = vec![];
|
||||
let mut threads: HashMap<u32, (u32, String)> = HashMap::new();
|
||||
let mut folders: HashMap<(u32, String), u32> = HashMap::new();
|
||||
let mut live_ids: HashSet<u32> = HashSet::new();
|
||||
|
||||
let mut n = first_leaf;
|
||||
let mut guard = 0;
|
||||
while n != 0 && guard < 5_000_000 {
|
||||
guard += 1;
|
||||
if (ctx.cancelled)() {
|
||||
return res;
|
||||
}
|
||||
let Some(node) = vol.read_tree_node(&vol.catalog, n, ns) else { break };
|
||||
if node[8] != 0xFF {
|
||||
break;
|
||||
}
|
||||
let mut live_here: Vec<CatFile> = vec![];
|
||||
for r in records(&node) {
|
||||
decode_record(r, max_id, &mut live_here, &mut threads, &mut folders, false);
|
||||
}
|
||||
for f in &live_here {
|
||||
live_ids.insert(f.id);
|
||||
}
|
||||
files.extend(live_here);
|
||||
scan_slack(&node, max_id, &mut files, &mut threads, &mut folders);
|
||||
n = be32(&node, 0);
|
||||
}
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
// Caminho de cada pasta a partir dos threads.
|
||||
let path_of = |parent: u32| -> String {
|
||||
let mut parts = vec![];
|
||||
let mut cur = parent;
|
||||
for _ in 0..64 {
|
||||
if cur <= 2 {
|
||||
break;
|
||||
}
|
||||
match threads.get(&cur) {
|
||||
Some((p, name)) => {
|
||||
parts.push(name.clone());
|
||||
cur = *p;
|
||||
}
|
||||
None => break,
|
||||
}
|
||||
}
|
||||
parts.reverse();
|
||||
format!("/{}", parts.join("/"))
|
||||
};
|
||||
|
||||
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)) {
|
||||
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) {
|
||||
continue;
|
||||
}
|
||||
if !seen.insert((f.id, ex[0].offset)) {
|
||||
continue;
|
||||
}
|
||||
// Mantém só os bytes do arquivo.
|
||||
let extents = super::fat::trim_extents(ex, f.data.size);
|
||||
// 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())
|
||||
} else {
|
||||
(Confidence::Low, "alguns blocos do arquivo já foram reaproveitados".to_string())
|
||||
};
|
||||
let nf = NewFound {
|
||||
name: f.name.clone(),
|
||||
dir: Some(path_of(f.parent)),
|
||||
size: f.data.size,
|
||||
origin: Origin::Hfs,
|
||||
data: DataLoc::Extents { extents },
|
||||
modified: date(f.modified),
|
||||
created: date(f.created),
|
||||
accessed: date(f.accessed),
|
||||
base: conf,
|
||||
note: Some(¬e),
|
||||
};
|
||||
let found = make_found(ctx, nf);
|
||||
(ctx.emit)(found);
|
||||
}
|
||||
res
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
impl Vol<'_> {
|
||||
fn read_fork_all(&self, f: &Fork, max: usize) -> Option<Vec<u8>> {
|
||||
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()?);
|
||||
if out.len() >= max {
|
||||
break;
|
||||
}
|
||||
}
|
||||
Some(out)
|
||||
}
|
||||
}
|
||||
|
||||
/// 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 };
|
||||
let (joff, jsize) = (be64(&info, 36), be64(&info, 44));
|
||||
if jsize == 0 || jsize > 1 << 30 || vol.base + joff + jsize > vol.src.size() {
|
||||
return;
|
||||
}
|
||||
let mut pos = 0u64;
|
||||
let chunk = 4 << 20;
|
||||
while pos < jsize {
|
||||
if (ctx.cancelled)() {
|
||||
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 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 {
|
||||
for r in records(nd) {
|
||||
decode_record(r, max_id, files, threads, folders, true);
|
||||
}
|
||||
scan_slack(nd, max_id, files, threads, folders);
|
||||
}
|
||||
}
|
||||
i += 512;
|
||||
}
|
||||
pos += n as u64;
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,7 @@
|
||||
pub mod exfat;
|
||||
pub mod ext;
|
||||
pub mod fat;
|
||||
pub mod hfsplus;
|
||||
pub mod ntfs;
|
||||
|
||||
use crate::signatures::{category_for_ext, matches_ext};
|
||||
|
||||
@@ -142,6 +142,7 @@ pub fn run(
|
||||
}
|
||||
FsKind::Exfat => Some(fs::exfat::scan(&mut ctx, p.start)),
|
||||
FsKind::Ext => Some(fs::ext::scan(&mut ctx, p.start, p.len)),
|
||||
FsKind::HfsPlus => Some(fs::hfsplus::scan(&mut ctx, p.start, p.len)),
|
||||
_ => None,
|
||||
};
|
||||
let _ = progress_phase;
|
||||
|
||||
@@ -261,7 +261,7 @@ impl FsKind {
|
||||
pub fn has_metadata_scanner(self) -> bool {
|
||||
matches!(
|
||||
self,
|
||||
FsKind::Ntfs | FsKind::Fat12 | FsKind::Fat16 | FsKind::Fat32 | FsKind::Exfat
|
||||
FsKind::Ntfs | FsKind::Fat12 | FsKind::Fat16 | FsKind::Fat32 | FsKind::Exfat | FsKind::Ext | FsKind::HfsPlus
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
//! HFS+: imagem real do dfvfs (target/testdata/dfvfs/hfsplus.raw) e exclusão simulada de registros.
|
||||
|
||||
mod common;
|
||||
use common::*;
|
||||
use guara_core::*;
|
||||
|
||||
fn image() -> Option<Vec<u8>> {
|
||||
let p = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../../target/testdata/dfvfs/hfsplus.raw");
|
||||
p.exists().then(|| std::fs::read(p).unwrap())
|
||||
}
|
||||
|
||||
fn scan_bytes(bytes: &[u8]) -> Collect {
|
||||
init();
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let p = dir.path().join("h.img");
|
||||
std::fs::write(&p, bytes).unwrap();
|
||||
scan_path(&p, &ScanOptions { mode: ScanMode::Quick, ..Default::default() })
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pristine_volume_has_no_deleted_files_and_is_recognized() {
|
||||
let Some(img) = image() else {
|
||||
eprintln!("SKIP: execute scripts/fetch-testdata.sh");
|
||||
return;
|
||||
};
|
||||
let c = scan_bytes(&img);
|
||||
assert_eq!(c.layout.as_ref().unwrap().partitions[0].fs, FsKind::HfsPlus);
|
||||
assert!(c.found.is_empty(), "{:?}", c.found.iter().map(|f| &f.name).collect::<Vec<_>>());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn record_left_in_leaf_slack_is_recovered() {
|
||||
let Some(mut img) = image() else { return };
|
||||
let bs = 4096usize;
|
||||
let node = 186 * bs + bs; // catálogo começa no bloco 186; nó 1 é a única folha
|
||||
// Remove os 5 últimos registros como o driver faz ao apagar no fim do nó: só diminui a contagem.
|
||||
img[node + 10..node + 12].copy_from_slice(&21u16.to_be_bytes());
|
||||
// Libera o bloco de dados (bitmap de alocação no bloco 1).
|
||||
let blk = 278usize;
|
||||
img[bs + blk / 8] &= !(0x80 >> (blk % 8));
|
||||
let c = scan_bytes(&img);
|
||||
let f = c.found.iter().find(|f| f.name == "fseventsd-uuid").unwrap_or_else(|| panic!("{:?}", c.found.iter().map(|f| &f.name).collect::<Vec<_>>()));
|
||||
assert_eq!(f.origin, Origin::Hfs);
|
||||
assert_eq!(f.size, 36);
|
||||
assert_eq!(f.dir.as_deref(), Some("/.fseventsd"));
|
||||
assert!(f.confidence >= Confidence::Medium);
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let p = dir.path().join("h.img");
|
||||
std::fs::write(&p, &img).unwrap();
|
||||
let out = dir.path().join("out");
|
||||
let rep = recover_all(&p, std::slice::from_ref(f), &out);
|
||||
assert!(rep.failed.is_empty());
|
||||
let rp = std::path::PathBuf::from(&rep.recovered[0].path);
|
||||
let got = std::fs::read(if rp.is_absolute() { rp } else { out.join(rp) }).unwrap();
|
||||
assert_eq!(got, &img[blk * bs..blk * bs + 36]);
|
||||
}
|
||||
Reference in new issue
Block a user