Btrfs: árvores atuais, mapa livre e arquivos apagados em versões antigas de nós; extensões comprimidas (zlib, zstd, lzo)

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
Lucas RayanandClaude Sonnet 5.5 committed 2026-10-19 00:19:47 -03:00
1 parent a017d79b00
commit 0ced7b725a
7 files changed
+1186 -2

No files matched your search

+1
View File
@@ -22,6 +22,7 @@ tiny_http = "0.12"
bzip2 = "0.6"
zip = { version = "2", default-features = false, features = ["deflate"] }
snap = "1"
ruzstd = "0.7"
lz4_flex = { version = "0.11", default-features = false, features = ["safe-decode", "safe-encode"] }
aes = "0.8"
hmac = "0.12"
+822
View File
@@ -0,0 +1,822 @@
//! Btrfs (um dispositivo; perfis single, DUP e RAID 1): árvores atuais para nomes e mapa de
//! espaço livre, e arquivos apagados achados em nós antigos das árvores. Como o Btrfs nunca
//! sobrescreve metadados no lugar (copy-on-write), as versões anteriores dos nós continuam no
//! disco até o espaço ser reaproveitado: varremos os blocos de metadata em busca delas.
//! Extensões comprimidas (zlib, zstd, lzo) são descompactadas na recuperação.
use super::{make_found, merge_extents, Ctx, NewFound, VolumeScan};
use crate::source::{read_vec, Source};
use crate::types::{Confidence, DataLoc, Extent, Origin, Packed, PackedCodec, Part};
use std::collections::{BTreeMap, HashMap, HashSet};
const MAGIC: &[u8; 8] = b"_BHRfS_M";
const SB_OFF: u64 = 0x10000;
// chaves
const INODE_ITEM: u8 = 1;
const INODE_REF: u8 = 12;
const EXTENT_DATA: u8 = 108;
const ROOT_ITEM: u8 = 132;
const EXTENT_ITEM: u8 = 168;
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)
}
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)
}
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)
}
pub fn crc32c(data: &[u8]) -> u32 {
static TABLE: std::sync::OnceLock<[u32; 256]> = std::sync::OnceLock::new();
let t = TABLE.get_or_init(|| {
let mut t = [0u32; 256];
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 };
}
t[i as usize] = c;
}
t
});
let mut c = !0u32;
for &b in data {
c = t[((c ^ b as u32) & 0xFF) as usize] ^ (c >> 8);
}
!c
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct Key {
pub objectid: u64,
pub ty: u8,
pub offset: u64,
}
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) }
}
#[derive(Clone, Debug)]
struct Chunk {
logical: u64,
len: u64,
ty: u64,
/// (offset físico do primeiro stripe) por cópia
phys: Vec<u64>,
}
#[derive(Clone, Debug)]
enum NodeKind {
Leaf(Vec<(Key, Vec<u8>)>),
Internal(Vec<(Key, u64)>),
}
#[derive(Clone, Debug)]
struct Node {
bytenr: u64,
generation: u64,
owner: u64,
kind: NodeKind,
}
struct Fs<'a> {
src: &'a dyn Source,
base: u64,
nodesize: u64,
sectorsize: u64,
fsid: [u8; 16],
chunks: Vec<Chunk>,
root: u64,
chunk_root: u64,
}
impl<'a> Fs<'a> {
fn open(src: &'a dyn Source, base: u64) -> Option<Fs<'a>> {
let sb = read_vec(src, base + SB_OFF, 4096).ok()?;
if sb.len() < 4096 || &sb[64..72] != MAGIC {
return None;
}
let csum_type = le16(&sb, 196);
if csum_type == 0 && crc32c(&sb[32..4096]) != le32(&sb, 0) {
return None;
}
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() {
return None;
}
let mut fsid = [0u8; 16];
fsid.copy_from_slice(&sb[32..48]);
let mut fs = Fs {
src,
base,
nodesize,
sectorsize,
fsid,
chunks: vec![],
root: le64(&sb, 80),
chunk_root: le64(&sb, 88),
};
// sys_chunk_array
let n = le32(&sb, 160) as usize;
let arr = sb.get(0x32B..0x32B + n.min(2048))?;
let mut p = 0;
while p + 17 + 48 <= arr.len() {
let k = key_at(arr, p);
if k.ty != CHUNK_ITEM {
break;
}
let (ch, used) = parse_chunk(&arr[p + 17..], k.offset)?;
fs.chunks.push(ch);
p += 17 + used;
}
// chunk tree completa
let mut more = vec![];
let cr = fs.chunk_root;
fs.walk(cr, &mut |k, d| {
if k.ty == CHUNK_ITEM {
if let Some((ch, _)) = parse_chunk(d, k.offset) {
more.push(ch);
}
}
});
for c in more {
if !fs.chunks.iter().any(|x| x.logical == c.logical) {
fs.chunks.push(c);
}
}
fs.chunks.sort_by_key(|c| c.logical);
Some(fs)
}
fn phys(&self, logical: u64) -> Option<u64> {
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()
}
fn read_node(&self, logical: u64) -> Option<Node> {
let p = self.phys(logical)?;
let raw = read_vec(self.src, p, self.nodesize as usize).ok()?;
self.parse_node(&raw, Some(logical))
}
fn parse_node(&self, raw: &[u8], expect: Option<u64>) -> Option<Node> {
if raw.len() as u64 != self.nodesize || raw[32..48] != self.fsid {
return None;
}
let bytenr = le64(raw, 48);
if expect.map_or(false, |e| e != bytenr) {
return None;
}
if crc32c(&raw[32..]) != le32(raw, 0) {
return None;
}
let nritems = le32(raw, 96) as usize;
let level = raw[100];
if nritems > (self.nodesize as usize) / 25 {
return None;
}
let hdr = 101usize;
let kind = if level == 0 {
let mut items = Vec::with_capacity(nritems);
for i in 0..nritems {
let o = hdr + i * 25;
let k = key_at(raw, o);
let (off, size) = (le32(raw, o + 17) as usize, le32(raw, o + 21) as usize);
let s = hdr + off;
if s + size > raw.len() {
return None;
}
items.push((k, raw[s..s + size].to_vec()));
}
NodeKind::Leaf(items)
} else {
let mut ptrs = Vec::with_capacity(nritems);
for i in 0..nritems {
let o = hdr + i * 33;
if o + 33 > raw.len() {
return None;
}
ptrs.push((key_at(raw, o), le64(raw, o + 17)));
}
NodeKind::Internal(ptrs)
};
Some(Node { bytenr, generation: le64(raw, 80), owner: le64(raw, 88), kind })
}
/// Percorre uma árvore em ordem, chamando `f` para cada item de folha.
fn walk(&self, root: u64, f: &mut dyn FnMut(&Key, &[u8])) {
let mut seen = HashSet::new();
self.walk_inner(root, f, &mut seen, &mut |_| {}, 0);
}
fn walk_nodes(&self, root: u64, f: &mut dyn FnMut(&Key, &[u8]), nodes: &mut dyn FnMut(&Node)) {
let mut seen = HashSet::new();
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) {
if depth > 10 || !seen.insert(at) {
return;
}
let Some(n) = self.read_node(at) else { return };
nodes(&n);
match &n.kind {
NodeKind::Leaf(items) => {
for (k, d) in items {
f(k, d);
}
}
NodeKind::Internal(ptrs) => {
for (_, child) in ptrs {
self.walk_inner(*child, f, seen, nodes, depth + 1);
}
}
}
}
}
fn parse_chunk(d: &[u8], logical: u64) -> Option<(Chunk, usize)> {
if d.len() < 48 {
return None;
}
let len = le64(d, 0);
let ty = le64(d, 24);
let nstripes = le16(d, 44) as usize;
if nstripes == 0 || nstripes > 32 || d.len() < 48 + nstripes * 32 {
return None;
}
let phys = (0..nstripes).map(|i| le64(d, 48 + i * 32 + 8)).collect();
Some((Chunk { logical, len, ty, phys }, 48 + nstripes * 32))
}
// ---------------------------------------------------------------- inodes
#[derive(Clone, Debug, Default)]
struct InodeData {
gen: u64,
size: u64,
mode: u32,
mtime: i64,
ctime: i64,
atime: i64,
has_inode: bool,
refs: HashMap<u64, String>,
/// offset no arquivo -> (geração do nó, item)
extents: BTreeMap<u64, (u64, Vec<u8>)>,
}
type Files = HashMap<(u64, u64), InodeData>;
fn add_item(files: &mut Files, owner: u64, gen: u64, k: &Key, d: &[u8]) {
match k.ty {
INODE_ITEM if d.len() >= 160 => {
let e = files.entry((owner, k.objectid)).or_default();
let g = le64(d, 0);
if !e.has_inode || g >= e.gen {
e.has_inode = true;
e.gen = g;
e.size = le64(d, 16);
e.mode = le32(d, 52);
e.atime = le64(d, 112) as i64;
e.ctime = le64(d, 124) as i64;
e.mtime = le64(d, 136) as i64;
}
}
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());
}
}
EXTENT_DATA if d.len() >= 21 => {
let e = files.entry((owner, k.objectid)).or_default();
match e.extents.get(&k.offset) {
Some((g, _)) if *g > gen => {}
_ => {
e.extents.insert(k.offset, (gen, d.to_vec()));
}
}
}
_ => {}
}
}
fn is_fs_tree(id: u64) -> bool {
id == 5 || (256..(1u64 << 48)).contains(&id)
}
pub fn scan(ctx: &mut Ctx, base: u64, part_len: u64) -> VolumeScan {
let _ = part_len;
let mut res = VolumeScan::default();
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) {
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;
}
// 1) Árvores atuais: raízes das subvolumes e extents alocados.
let mut roots: Vec<(u64, u64)> = vec![]; // (id da árvore, bytenr)
let mut extent_root = 0u64;
let rr = fs.root;
fs.walk(rr, &mut |k, d| {
if k.ty == ROOT_ITEM && d.len() >= 239 {
let bytenr = le64(d, 176);
if k.objectid == 2 {
extent_root = bytenr;
} else if is_fs_tree(k.objectid) {
roots.push((k.objectid, bytenr));
}
}
});
let mut live_nodes: HashSet<u64> = HashSet::new();
let mut live: Files = HashMap::new();
for &(id, bytenr) in &roots {
let mut tmp: Vec<(Key, Vec<u8>)> = vec![];
let mut gens: HashMap<u64, u64> = HashMap::new();
let mut seen_nodes: Vec<(u64, u64)> = vec![];
{
let mut items = |k: &Key, d: &[u8]| tmp.push((*k, d.to_vec()));
let mut nodes = |n: &Node| {
seen_nodes.push((n.bytenr, n.generation));
};
fs.walk_nodes(bytenr, &mut items, &mut nodes);
}
for (b, g) in seen_nodes {
live_nodes.insert(b);
gens.insert(b, g);
}
for (k, d) in &tmp {
add_item(&mut live, id, u64::MAX, k, d);
}
if (ctx.cancelled)() {
return res;
}
}
// Extents alocados (árvore de extents) e mapa livre dos blocos de dados.
let mut allocated: Vec<Extent> = vec![];
if extent_root != 0 {
let nodesize = fs.nodesize;
fs.walk(extent_root, &mut |k, _| {
if k.ty == EXTENT_ITEM {
allocated.push(Extent::new(k.objectid, k.offset));
} else if k.ty == METADATA_ITEM {
allocated.push(Extent::new(k.objectid, nodesize));
}
});
}
let allocated = merge_extents(allocated);
let mut free: Vec<Extent> = vec![];
for c in fs.chunks.iter().filter(|c| c.ty & 0x1 != 0) {
// blocos de dados: livres = chunk - alocado
let mut cur = c.logical;
let i = allocated.partition_point(|e| e.end() <= c.logical);
for e in &allocated[i..] {
if e.offset >= c.logical + c.len {
break;
}
if e.offset > cur {
if let Some(p) = fs.phys(cur) {
free.push(Extent::new(p, e.offset - cur));
}
}
cur = cur.max(e.end());
}
if cur < c.logical + c.len {
if let Some(p) = fs.phys(cur) {
free.push(Extent::new(p, c.logical + c.len - cur));
}
}
}
res.free = Some(merge_extents(free));
// 2) Varre os blocos de metadata atrás de versões antigas de folhas de árvores de arquivos.
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 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 mut off = 0u64;
let block = 8usize << 20;
while off < c.len {
if (ctx.cancelled)() {
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 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 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 {
for (k, d) in items {
add_item(&mut stale, node.owner, node.generation, k, d);
}
}
}
}
}
i += step;
}
off += block as u64;
done_md += block as u64;
(ctx.progress)(done_md.min(total_md), total_md.max(1));
}
}
// 3) Arquivos que só existem nas folhas antigas.
let path_of = |owner: u64, mut ino: u64| -> String {
let mut parts: Vec<String> = vec![];
for _ in 0..64 {
if ino == 256 {
break;
}
let e = stale.get(&(owner, ino)).or_else(|| live.get(&(owner, ino)));
match e.and_then(|e| e.refs.iter().next()) {
Some((parent, name)) => {
parts.push(name.clone());
ino = *parent;
}
None => {
parts.push("(pasta perdida)".into());
break;
}
}
}
parts.reverse();
parts.join("/")
};
let mut keys: Vec<&(u64, u64)> = stale.keys().collect();
keys.sort();
for key in keys {
let (owner, ino) = *key;
if live.contains_key(&(owner, ino)) {
continue;
}
let e = &stale[&(owner, ino)];
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 dir = {
let p = path_of(owner, parent);
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 (conf, note) = if !all_ok {
(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())
};
let ts = |t: i64| (t > 0).then_some(t);
let nf = NewFound {
name,
dir,
size: e.size,
origin: Origin::Btrfs,
data,
modified: ts(e.mtime),
created: ts(e.ctime),
accessed: ts(e.atime),
base: conf,
note: Some(&note),
};
let f = make_found(ctx, nf);
(ctx.emit)(f);
}
res
}
/// Converte os itens EXTENT_DATA em extents físicos ou partes (inline/comprimidas).
fn build_data(fs: &Fs, e: &InodeData, allocated: &[Extent]) -> Option<(DataLoc, bool, bool)> {
let mut parts: Vec<Part> = vec![];
let mut pos = 0u64;
let mut any_alloc = false;
let mut all_ok = true;
let mut plain_only = true;
let mut hole = |parts: &mut Vec<Part>, len: u64| {
if len > 0 {
parts.push(Part::Run { offset: 0, len });
// marcador: offset 0 + sparse tratado abaixo
}
};
let _ = &mut hole;
let mut sparse_marks: Vec<usize> = vec![];
for (&foff, (_, d)) in &e.extents {
if foff >= e.size {
break;
}
if foff > pos {
sparse_marks.push(parts.len());
parts.push(Part::Run { offset: 0, len: foff - pos });
plain_only = false;
pos = foff;
}
let ty = d[20];
let comp = d[16];
if ty == 0 {
// inline
let raw = &d[21..];
let ram = le64(d, 8) as usize;
let bytes = match comp {
0 => raw.to_vec(),
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() });
plain_only = false;
pos = foff + len;
continue;
}
if d.len() < 53 {
all_ok = false;
break;
}
let (disk, disk_len, offset, num) = (le64(d, 21), le64(d, 29), le64(d, 37), le64(d, 45));
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 });
plain_only = false;
} else {
let Some(phys) = fs.phys(disk) else {
all_ok = false;
break;
};
let overlaps = {
let i = allocated.partition_point(|x| x.end() <= disk);
allocated.get(i).map_or(false, |x| x.offset < disk + disk_len)
};
any_alloc |= overlaps;
if comp == 0 {
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,
_ => {
all_ok = false;
break;
}
};
parts.push(Part::Packed(Packed { offset: phys, disk_len, ram_len: le64(d, 8), skip: offset, len: num, codec }));
plain_only = false;
}
}
pos = foff + num;
}
if pos < e.size {
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();
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.
let mut out: Vec<Part> = vec![];
for (i, p) in parts.into_iter().enumerate() {
if sparse_marks.contains(&i) {
if let Part::Run { len, .. } = p {
out.push(Part::Zeros { len });
continue;
}
}
out.push(p);
}
Some((DataLoc::Parts { parts: out }, any_alloc, all_ok))
}
fn decode(codec: u8, raw: &[u8], ram: usize) -> Option<Vec<u8>> {
match codec {
1 => {
use std::io::Read;
let mut v = Vec::with_capacity(ram);
flate2::read::ZlibDecoder::new(raw).read_to_end(&mut v).ok()?;
Some(v)
}
2 => {
use std::io::Read;
let mut dec = ruzstd::StreamingDecoder::new(raw).ok()?;
let mut v = Vec::with_capacity(ram);
dec.read_to_end(&mut v).ok()?;
Some(v)
}
_ => lzo_btrfs(raw, ram),
}
}
/// Formato de extensão LZO do Btrfs: tamanho total (u32) + segmentos [tamanho (u32)][dados LZO1X]
/// de até um setor de 4 KiB descomprimido cada, sem cruzar fronteiras de setor.
pub fn lzo_btrfs(raw: &[u8], ram: usize) -> Option<Vec<u8>> {
if raw.len() < 4 {
return None;
}
let total = le32(raw, 0) as usize;
let mut out = Vec::with_capacity(ram);
let mut p = 4usize;
while p + 4 <= raw.len().min(total) && out.len() < ram {
// pula o preenchimento até o próximo setor se sobrar menos de 4 bytes
let in_sector = 4096 - (p % 4096);
if in_sector < 4 {
p += in_sector;
continue;
}
let seg = le32(raw, p) as usize;
p += 4;
let chunk = raw.get(p..p + seg)?;
let want = (ram - out.len()).min(4096);
out.extend(lzo1x_decompress(chunk, want)?);
p += seg;
}
Some(out)
}
/// LZO1X (descompressão segura, no mesmo esquema de estados do kernel Linux).
pub fn lzo1x_decompress(src: &[u8], max_out: usize) -> Option<Vec<u8>> {
let mut out: Vec<u8> = Vec::with_capacity(max_out);
let mut ip = 0usize;
let byte = |i: usize| src.get(i).copied().map(|b| b as usize);
let lits = |out: &mut Vec<u8>, ip: &mut usize, n: usize| -> Option<()> {
out.extend_from_slice(src.get(*ip..*ip + n)?);
*ip += n;
Some(())
};
let ext = |ip: &mut usize, base: usize| -> Option<usize> {
let mut extra = 0usize;
while byte(*ip)? == 0 {
extra += 255;
*ip += 1;
}
let v = base + extra + byte(*ip)?;
*ip += 1;
Some(v)
};
// estado: literais ao fim da instrução anterior (0..3) ou 4 depois de uma corrida de literais
let mut state;
if byte(0)? > 17 {
let t = byte(0)? - 17;
ip = 1;
lits(&mut out, &mut ip, t)?;
state = if t < 4 { t } else { 4 };
} else {
state = 0;
}
loop {
if out.len() > max_out + 4096 {
return None;
}
let t = byte(ip)?;
ip += 1;
if t < 16 {
if state == 0 {
let n = if t == 0 { ext(&mut ip, 15)? } else { t } + 3;
lits(&mut out, &mut ip, n)?;
state = 4;
continue;
}
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) };
copy_match(&mut out, dist, len)?;
let next = t & 3;
lits(&mut out, &mut ip, next)?;
state = next;
} else if t >= 64 {
let h = byte(ip)?;
ip += 1;
copy_match(&mut out, 1 + ((t >> 2) & 7) + (h << 3), (t >> 5) + 1)?;
let next = t & 3;
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 b = (byte(ip)?) | (byte(ip + 1)? << 8);
ip += 2;
copy_match(&mut out, 1 + (b >> 2), len)?;
let next = b & 3;
lits(&mut out, &mut ip, next)?;
state = next;
} else {
let len = if t & 7 == 0 { ext(&mut ip, 7)? } else { t & 7 } + 2;
let b = (byte(ip)?) | (byte(ip + 1)? << 8);
ip += 2;
let d = ((t & 8) << 11) + (b >> 2);
if d == 0 {
return Some(out);
}
copy_match(&mut out, d + 0x4000, len)?;
let next = b & 3;
lits(&mut out, &mut ip, next)?;
state = next;
}
}
}
fn copy_match(out: &mut Vec<u8>, dist: usize, len: usize) -> Option<()> {
if dist == 0 || dist > out.len() {
return None;
}
let start = out.len() - dist;
for i in 0..len {
let b = out[start + i];
out.push(b);
}
Some(())
}
/// 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 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) {
roots.push((k.objectid, le64(d, 176)));
}
});
let mut files: Files = HashMap::new();
for (id, b) in roots {
fs.walk(b, &mut |k, d| add_item(&mut files, id, u64::MAX, k, d));
}
let mut out = vec![];
for (&(owner, ino), e) in &files {
if !e.has_inode || ino == 256 {
continue;
}
let mut parts = vec![];
let mut cur = ino;
for _ in 0..64 {
if cur == 256 {
break;
}
match files.get(&(owner, cur)).and_then(|x| x.refs.iter().next()) {
Some((p, n)) => {
parts.push(n.clone());
cur = *p;
}
None => break,
}
}
parts.reverse();
out.push((parts.join("/"), e.size, e.mode & 0o170000 == 0o040000));
}
out.sort();
out
}
/// Posições das árvores principais, para ferramentas e testes que precisam editar a imagem.
pub struct TreeLocations {
pub nodesize: u64,
/// Endereço físico (absoluto na origem) do primeiro nó de cada árvore.
pub root_tree: u64,
pub fs_tree: u64,
pub extent_tree: u64,
/// Endereços lógicos correspondentes.
pub root_tree_logical: u64,
pub fs_tree_logical: u64,
pub extent_tree_logical: u64,
/// Converte endereço lógico em físico.
pub chunks: Vec<(u64, u64, u64)>,
}
pub fn locate_trees(src: &dyn Source, base: u64) -> Option<TreeLocations> {
let fs = Fs::open(src, base)?;
let (mut fs_t, mut ext_t) = (0u64, 0u64);
fs.walk(fs.root, &mut |k, d| {
if k.ty == ROOT_ITEM && d.len() >= 239 {
if k.objectid == 5 {
fs_t = le64(d, 176);
} else if k.objectid == 2 {
ext_t = le64(d, 176);
}
}
});
Some(TreeLocations {
nodesize: fs.nodesize,
root_tree: fs.phys(fs.root)?,
fs_tree: fs.phys(fs_t)?,
extent_tree: fs.phys(ext_t)?,
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(),
})
}
+1
View File
@@ -4,6 +4,7 @@
//! devolve os extents ainda alocados ("live") para o carving pular e, quando
//! possível, os trechos livres.
pub mod btrfs;
pub mod exfat;
pub mod ext;
pub mod fat;
+24
View File
@@ -382,6 +382,21 @@ pub fn write_file(src: &dyn Source, f: &Found, path: &Path) -> Result<Hashes> {
out.write_all(bytes)?;
hasher.update(bytes);
}
Part::Zeros { len } => {
let z = vec![0u8; (1 << 20).min(*len as usize).max(1)];
let mut left = *len;
while left > 0 {
let n = left.min(z.len() as u64) as usize;
out.write_all(&z[..n])?;
hasher.update(&z[..n]);
left -= n as u64;
}
}
Part::Packed(pk) => {
let v = pk.read(src)?;
out.write_all(&v)?;
hasher.update(&v);
}
Part::Run { offset, len } => {
let mut done = 0u64;
while done < *len {
@@ -461,6 +476,15 @@ pub fn read_found_at(
Ok(n)
}
Part::Run { offset: o, .. } => src.read_at(o + within, &mut buf[..n]),
Part::Zeros { .. } => {
buf[..n].fill(0);
Ok(n)
}
Part::Packed(pk) => {
let v = pk.read(src)?;
buf[..n].copy_from_slice(&v[within as usize..within as usize + n]);
Ok(n)
}
};
}
pos += plen;
+1
View File
@@ -147,6 +147,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)),
FsKind::Btrfs => Some(fs::btrfs::scan(&mut ctx, p.start, p.len)),
_ => None,
};
let _ = progress_phase;
+57 -2
View File
@@ -121,13 +121,68 @@ pub enum Part {
},
/// Trecho contíguo da origem.
Run { offset: u64, len: u64 },
/// Zeros (buracos de arquivos esparsos).
Zeros { len: u64 },
/// Extensão comprimida na origem (Btrfs): descompactada ao ler.
Packed(Packed),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum PackedCodec {
Zlib,
Zstd,
Lzo,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub struct Packed {
/// Onde estão os bytes comprimidos na origem.
pub offset: u64,
pub disk_len: u64,
/// Tamanho da extensão depois de descompactada.
pub ram_len: u64,
/// Bytes a pular no começo da extensão descompactada.
pub skip: u64,
/// Bytes úteis a partir de `skip`.
pub len: u64,
pub codec: PackedCodec,
}
impl Packed {
/// Descompacta a extensão e devolve os `len` bytes úteis.
pub fn read(&self, src: &dyn crate::source::Source) -> std::io::Result<Vec<u8>> {
use std::io::Read;
let raw = crate::source::read_vec(src, self.offset, self.disk_len as usize)?;
let bad = |m: &str| std::io::Error::new(std::io::ErrorKind::InvalidData, format!("extensão comprimida ilegível ({m})"));
let mut data = match self.codec {
PackedCodec::Zlib => {
let mut v = Vec::with_capacity(self.ram_len as usize);
flate2::read::ZlibDecoder::new(&raw[..]).read_to_end(&mut v).map_err(|_| bad("zlib"))?;
v
}
PackedCodec::Zstd => {
let mut v = Vec::with_capacity(self.ram_len as usize);
ruzstd::StreamingDecoder::new(&raw[..]).map_err(|_| bad("zstd"))?.read_to_end(&mut v).map_err(|_| bad("zstd"))?;
v
}
PackedCodec::Lzo => crate::fs::btrfs::lzo_btrfs(&raw, self.ram_len as usize).ok_or_else(|| bad("lzo"))?,
};
data.resize(self.ram_len.max(data.len() as u64) as usize, 0);
let s = (self.skip as usize).min(data.len());
let e = (s + self.len as usize).min(data.len());
let mut v = data[s..e].to_vec();
v.resize(self.len as usize, 0);
Ok(v)
}
}
impl Part {
pub fn len(&self) -> u64 {
match self {
Part::Bytes { bytes } => bytes.len() as u64,
Part::Run { len, .. } => *len,
Part::Run { len, .. } | Part::Zeros { len } => *len,
Part::Packed(p) => p.len,
}
}
pub fn is_empty(&self) -> bool {
@@ -331,7 +386,7 @@ impl FsKind {
pub fn has_metadata_scanner(self) -> bool {
matches!(
self,
FsKind::Ntfs | FsKind::Fat12 | FsKind::Fat16 | FsKind::Fat32 | FsKind::Exfat | FsKind::Ext | FsKind::HfsPlus
FsKind::Ntfs | FsKind::Fat12 | FsKind::Fat16 | FsKind::Fat32 | FsKind::Exfat | FsKind::Ext | FsKind::HfsPlus | FsKind::Btrfs
)
}
}
+280
View File
@@ -0,0 +1,280 @@
//! Btrfs: imagens criadas com mkfs.btrfs --rootdir; exclusão simulada como o COW faz
//! (nova versão da folha em outro bloco, a antiga continua no disco).
mod common;
use common::*;
use guara_core::fs::btrfs::{crc32c, live_paths, lzo1x_decompress};
use guara_core::source::{FileSource, Source};
use guara_core::*;
use std::path::{Path, PathBuf};
use std::process::Command;
fn have(t: &str) -> bool {
Command::new("sh").args(["-c", &format!("command -v {t}")]).output().map(|o| o.status.success()).unwrap_or(false)
}
fn data(seed: u64, n: usize) -> Vec<u8> {
let mut x = seed | 1;
(0..n)
.map(|_| {
x ^= x << 13;
x ^= x >> 7;
x ^= x << 17;
(x >> 24) as u8
})
.collect()
}
struct Img {
_dir: tempfile::TempDir,
path: PathBuf,
files: Vec<(&'static str, Vec<u8>)>,
}
fn build() -> Option<Img> {
if !have("mkfs.btrfs") {
eprintln!("SKIP: mkfs.btrfs ausente");
return None;
}
let dir = tempfile::tempdir().unwrap();
let root = dir.path().join("root");
std::fs::create_dir_all(root.join("docs/sub")).unwrap();
std::fs::create_dir_all(root.join("fotos")).unwrap();
let files = vec![
("docs/relatorio.dat", data(1, 150_000)),
("docs/sub/contrato.dat", data(2, 9_000)),
("fotos/viagem.dat", data(3, 700_000)),
("fotos/fica.dat", data(4, 30_000)),
("pequeno.txt", b"conteudo pequeno inline".to_vec()),
];
for (p, c) in &files {
std::fs::write(root.join(p), c).unwrap();
}
let path = dir.path().join("b.img");
std::fs::File::create(&path).unwrap().set_len(128 << 20).unwrap();
let o = Command::new("mkfs.btrfs").args(["-q", "-f", "--rootdir"]).arg(&root).arg(&path).output().unwrap();
assert!(o.status.success(), "{}", String::from_utf8_lossy(&o.stderr));
Some(Img { _dir: dir, path, files })
}
#[test]
fn lists_the_live_tree_with_paths_and_sizes() {
let Some(i) = build() else { return };
let src = FileSource::open(&i.path).unwrap();
let listed = live_paths(&src, 0);
for (p, c) in &i.files {
let f = listed.iter().find(|x| x.0 == *p).unwrap_or_else(|| panic!("{p} ausente: {listed:?}"));
assert_eq!(f.1, c.len() as u64, "{p}");
assert!(!f.2);
}
assert!(listed.iter().any(|x| x.0 == "docs/sub" && x.2));
}
// --- manipulação bruta de folhas (independente do módulo) ---
struct Leaf {
raw: Vec<u8>,
}
impl Leaf {
fn nritems(&self) -> usize {
u32::from_le_bytes(self.raw[96..100].try_into().unwrap()) as usize
}
fn item(&self, i: usize) -> (u64, u8, u64, usize, usize) {
let o = 101 + i * 25;
let r = &self.raw;
(
u64::from_le_bytes(r[o..o + 8].try_into().unwrap()),
r[o + 8],
u64::from_le_bytes(r[o + 9..o + 17].try_into().unwrap()),
u32::from_le_bytes(r[o + 17..o + 21].try_into().unwrap()) as usize,
u32::from_le_bytes(r[o + 21..o + 25].try_into().unwrap()) as usize,
)
}
fn data(&self, i: usize) -> &[u8] {
let (_, _, _, off, size) = self.item(i);
&self.raw[101 + off..101 + off + size]
}
fn seal(&mut self) {
let c = crc32c(&self.raw[32..]);
self.raw[0..4].copy_from_slice(&c.to_le_bytes());
}
/// Nova versão da folha sem os itens que `drop` rejeita, num novo endereço lógico.
fn without(&self, drop: impl Fn(u64, u8) -> bool, new_bytenr: u64) -> Leaf {
let mut kept: Vec<(usize, Vec<u8>)> = vec![];
for i in 0..self.nritems() {
let (obj, ty, _, _, _) = self.item(i);
if !drop(obj, ty) {
kept.push((i, self.data(i).to_vec()));
}
}
let mut raw = vec![0u8; self.raw.len()];
raw[..101].copy_from_slice(&self.raw[..101]);
raw[48..56].copy_from_slice(&new_bytenr.to_le_bytes());
let gen = u64::from_le_bytes(raw[80..88].try_into().unwrap()) + 1;
raw[80..88].copy_from_slice(&gen.to_le_bytes());
raw[96..100].copy_from_slice(&(kept.len() as u32).to_le_bytes());
let mut end = raw.len() - 101;
for (n, (i, d)) in kept.iter().enumerate() {
let o = 101 + n * 25;
raw[o..o + 17].copy_from_slice(&self.raw[101 + i * 25..101 + i * 25 + 17]);
end -= d.len();
raw[o + 17..o + 21].copy_from_slice(&(end as u32).to_le_bytes());
raw[o + 21..o + 25].copy_from_slice(&(d.len() as u32).to_le_bytes());
raw[101 + end..101 + end + d.len()].copy_from_slice(d);
}
let mut l = Leaf { raw };
l.seal();
l
}
}
fn superblock(img: &[u8]) -> &[u8] {
&img[0x10000..0x11000]
}
/// Mapa lógico -> físico dos chunks de sistema (suficiente para imagens pequenas de um dispositivo).
fn chunk_map(img: &[u8]) -> Vec<(u64, u64, u64)> {
let sb = superblock(img);
let n = u32::from_le_bytes(sb[160..164].try_into().unwrap()) as usize;
let arr = &sb[0x32B..0x32B + n];
let mut p = 0;
let mut out = vec![];
while p + 17 + 80 <= arr.len() {
let logical = u64::from_le_bytes(arr[p + 9..p + 17].try_into().unwrap());
let len = u64::from_le_bytes(arr[p + 17..p + 25].try_into().unwrap());
let phys = u64::from_le_bytes(arr[p + 17 + 48 + 8..p + 17 + 48 + 16].try_into().unwrap());
out.push((logical, len, phys));
let ns = u16::from_le_bytes(arr[p + 17 + 44..p + 17 + 46].try_into().unwrap()) as usize;
p += 17 + 48 + ns * 32;
}
out
}
#[test]
fn deleted_file_survives_in_the_previous_version_of_the_leaf() {
let Some(i) = build() else { return };
let mut img = std::fs::read(&i.path).unwrap();
let sb = superblock(&img).to_vec();
let nodesize = u32::from_le_bytes(sb[148..152].try_into().unwrap()) as usize;
let src = FileSource::open(&i.path).unwrap();
let t = guara_core::fs::btrfs::locate_trees(&src, 0).expect("árvores");
assert_eq!(t.nodesize as usize, nodesize);
let fs_leaf = (t.fs_tree as usize, t.fs_tree_logical);
let root_leaf = (t.root_tree as usize, t.root_tree_logical);
let ext_leaf = (t.extent_tree as usize, t.extent_tree_logical);
let to_logical = |phys: usize| -> u64 {
let c = t.chunks.iter().find(|c| (phys as u64) >= c.2 && (phys as u64) < c.2 + c.1).expect("chunk");
c.0 + (phys as u64 - c.2)
};
let fl = Leaf { raw: img[fs_leaf.0..fs_leaf.0 + nodesize].to_vec() };
// inode do arquivo apagado
let victim = "relatorio.dat";
let mut ino = 0;
for k in 0..fl.nritems() {
let (obj, ty, _, _, _) = fl.item(k);
if ty == 12 {
let d = fl.data(k);
let nl = u16::from_le_bytes([d[8], d[9]]) as usize;
if &d[10..10 + nl] == victim.as_bytes() {
ino = obj;
}
}
}
assert!(ino > 256);
// extent de dados do arquivo (para liberar no extent tree)
let mut data_bytenr = 0;
for k in 0..fl.nritems() {
let (obj, ty, _, _, _) = fl.item(k);
if obj == ino && ty == 108 {
data_bytenr = u64::from_le_bytes(fl.data(k)[21..29].try_into().unwrap());
}
}
assert!(data_bytenr > 0);
// Novo bloco livre para a nova versão: primeiro bloco do metadata sem folha válida e todo zero.
let free_slot = (0..img.len() / nodesize)
.map(|n| n * nodesize)
.find(|&o| o > fs_leaf.0 && img[o..o + nodesize].iter().all(|&x| x == 0) && t.chunks.iter().any(|c| (o as u64) >= c.2 && (o as u64) < c.2 + c.1) && o < fs_leaf.0 + (8 << 20))
.expect("bloco livre");
// logical = bytenr da folha antiga + (offset físico - offset físico antigo): mesmo chunk
let new_bytenr = to_logical(free_slot);
let newleaf = fl.without(|o, _| o == ino, new_bytenr);
img[free_slot..free_slot + nodesize].copy_from_slice(&newleaf.raw);
// Aponta o ROOT_ITEM da árvore 5 para a nova versão (e sobe a geração), na folha da árvore raiz.
let mut rl = Leaf { raw: img[root_leaf.0..root_leaf.0 + nodesize].to_vec() };
for k in 0..rl.nritems() {
let (obj, ty, _, off, _) = rl.item(k);
if obj == 5 && ty == 132 {
let at = 101 + off + 176;
rl.raw[at..at + 8].copy_from_slice(&new_bytenr.to_le_bytes());
}
}
rl.seal();
img[root_leaf.0..root_leaf.0 + nodesize].copy_from_slice(&rl.raw);
// Libera o extent de dados no extent tree (em outro endereço da mesma folha).
let el = Leaf { raw: img[ext_leaf.0..ext_leaf.0 + nodesize].to_vec() };
let newext = el.without(|o, ty| o == data_bytenr && ty == 168, ext_leaf.1);
let _ = to_logical;
img[ext_leaf.0..ext_leaf.0 + nodesize].copy_from_slice(&newext.raw);
let _ = chunk_map(&img);
let p2 = i._dir.path().join("deleted.img");
std::fs::write(&p2, &img).unwrap();
init();
let c = scan_path(&p2, &ScanOptions { mode: ScanMode::Deep, ..Default::default() });
let f = c
.found
.iter()
.find(|f| f.name == victim && f.origin == Origin::Btrfs)
.unwrap_or_else(|| panic!("não achou: {:?}", c.found.iter().map(|f| (&f.name, f.origin)).collect::<Vec<_>>()));
assert_eq!(f.size, 150_000);
assert_eq!(f.dir.as_deref(), Some("/docs"));
assert!(f.confidence >= Confidence::Medium && f.note.as_deref().unwrap_or("").contains("livres"), "{:?}", f.note);
let out = i._dir.path().join("out");
let rep = recover_all(&p2, std::slice::from_ref(f), &out);
assert!(rep.failed.is_empty(), "{:?}", rep.failed);
let rp = PathBuf::from(&rep.recovered[0].path);
let got = std::fs::read(if rp.is_absolute() { rp } else { out.join(rp) }).unwrap();
assert!(got == i.files[0].1);
// Os arquivos que continuam existindo não aparecem como apagados.
assert!(!c.found.iter().any(|f| f.name == "fica.dat" && f.origin == Origin::Btrfs));
let _ = Path::new("");
}
#[test]
fn lzo_literals_and_a_match() {
// 20 literais + fim
let mut s = vec![17 + 20];
s.extend((0..20u8).map(|i| b'a' + i));
s.extend_from_slice(&[0x11, 0, 0]);
assert_eq!(lzo1x_decompress(&s, 4096).unwrap(), (0..20u8).map(|i| b'a' + i).collect::<Vec<_>>());
// "abcd" + M2 (dist 4, len 4) + fim
let s = vec![17 + 4, b'a', b'b', b'c', b'd', 0x6C, 0x00, 0x11, 0, 0];
assert_eq!(lzo1x_decompress(&s, 4096).unwrap(), b"abcdabcd");
}
#[test]
fn packed_extents_decode_zlib_and_zstd() {
use guara_core::source::MemSource;
use guara_core::{Packed, PackedCodec};
use std::io::Write;
let plain: Vec<u8> = (0..100_000u32).map(|i| (i % 251) as u8).collect();
// zlib
let mut e = flate2::write::ZlibEncoder::new(vec![], flate2::Compression::default());
e.write_all(&plain).unwrap();
let z = e.finish().unwrap();
let mut disk = vec![0u8; 4096];
disk.extend_from_slice(&z);
let p = Packed { offset: 4096, disk_len: z.len() as u64, ram_len: plain.len() as u64, skip: 1000, len: 5000, codec: PackedCodec::Zlib };
assert_eq!(p.read(&MemSource(disk)).unwrap(), &plain[1000..6000]);
// zstd, se o utilitário existir
if have("zstd") {
let dir = tempfile::tempdir().unwrap();
let f = dir.path().join("p.bin");
std::fs::write(&f, &plain).unwrap();
assert!(Command::new("zstd").args(["-q", "-f"]).arg(&f).status().unwrap().success());
let z = std::fs::read(dir.path().join("p.bin.zst")).unwrap();
let p = Packed { offset: 0, disk_len: z.len() as u64, ram_len: plain.len() as u64, skip: 0, len: plain.len() as u64, codec: PackedCodec::Zstd };
assert!(p.read(&MemSource(z)).unwrap() == plain);
}
}