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:
1 parent
a017d79b00
commit
0ced7b725a
7 files changed
+1186
-2
No files matched your search
@@ -22,6 +22,7 @@ tiny_http = "0.12"
|
|||||||
bzip2 = "0.6"
|
bzip2 = "0.6"
|
||||||
zip = { version = "2", default-features = false, features = ["deflate"] }
|
zip = { version = "2", default-features = false, features = ["deflate"] }
|
||||||
snap = "1"
|
snap = "1"
|
||||||
|
ruzstd = "0.7"
|
||||||
lz4_flex = { version = "0.11", default-features = false, features = ["safe-decode", "safe-encode"] }
|
lz4_flex = { version = "0.11", default-features = false, features = ["safe-decode", "safe-encode"] }
|
||||||
aes = "0.8"
|
aes = "0.8"
|
||||||
hmac = "0.12"
|
hmac = "0.12"
|
||||||
|
|||||||
@@ -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(¬e),
|
||||||
|
};
|
||||||
|
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(),
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -4,6 +4,7 @@
|
|||||||
//! devolve os extents ainda alocados ("live") para o carving pular e, quando
|
//! devolve os extents ainda alocados ("live") para o carving pular e, quando
|
||||||
//! possível, os trechos livres.
|
//! possível, os trechos livres.
|
||||||
|
|
||||||
|
pub mod btrfs;
|
||||||
pub mod exfat;
|
pub mod exfat;
|
||||||
pub mod ext;
|
pub mod ext;
|
||||||
pub mod fat;
|
pub mod fat;
|
||||||
|
|||||||
@@ -382,6 +382,21 @@ pub fn write_file(src: &dyn Source, f: &Found, path: &Path) -> Result<Hashes> {
|
|||||||
out.write_all(bytes)?;
|
out.write_all(bytes)?;
|
||||||
hasher.update(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 } => {
|
Part::Run { offset, len } => {
|
||||||
let mut done = 0u64;
|
let mut done = 0u64;
|
||||||
while done < *len {
|
while done < *len {
|
||||||
@@ -461,6 +476,15 @@ pub fn read_found_at(
|
|||||||
Ok(n)
|
Ok(n)
|
||||||
}
|
}
|
||||||
Part::Run { offset: o, .. } => src.read_at(o + within, &mut buf[..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;
|
pos += plen;
|
||||||
|
|||||||
@@ -147,6 +147,7 @@ pub fn run(
|
|||||||
FsKind::Exfat => Some(fs::exfat::scan(&mut ctx, p.start)),
|
FsKind::Exfat => Some(fs::exfat::scan(&mut ctx, p.start)),
|
||||||
FsKind::Ext => Some(fs::ext::scan(&mut ctx, p.start, p.len)),
|
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::HfsPlus => Some(fs::hfsplus::scan(&mut ctx, p.start, p.len)),
|
||||||
|
FsKind::Btrfs => Some(fs::btrfs::scan(&mut ctx, p.start, p.len)),
|
||||||
_ => None,
|
_ => None,
|
||||||
};
|
};
|
||||||
let _ = progress_phase;
|
let _ = progress_phase;
|
||||||
|
|||||||
@@ -121,13 +121,68 @@ pub enum Part {
|
|||||||
},
|
},
|
||||||
/// Trecho contíguo da origem.
|
/// Trecho contíguo da origem.
|
||||||
Run { offset: u64, len: u64 },
|
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 {
|
impl Part {
|
||||||
pub fn len(&self) -> u64 {
|
pub fn len(&self) -> u64 {
|
||||||
match self {
|
match self {
|
||||||
Part::Bytes { bytes } => bytes.len() as u64,
|
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 {
|
pub fn is_empty(&self) -> bool {
|
||||||
@@ -331,7 +386,7 @@ impl FsKind {
|
|||||||
pub fn has_metadata_scanner(self) -> bool {
|
pub fn has_metadata_scanner(self) -> bool {
|
||||||
matches!(
|
matches!(
|
||||||
self,
|
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
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in new issue
Block a user