//! ext2/3/4: recovers deleted files with names via three complementary techniques: //! //! 1. deleted inodes (`dtime != 0`) whose extent tree or block pointers still have remnants; //! 2. pre-deletion inode copies preserved in the jbd2 journal; //! 3. unlinked directory entries hidden inside `rec_len` slack space (recovering filenames). //! //! Also returns the free-block map so file carving only scans unallocated blocks. use super::{make_found, merge_extents, Ctx, NewFound, VolumeScan}; use crate::signatures; use crate::source::{read_vec, Source}; use crate::types::{Confidence, DataLoc, Extent, Origin}; use std::collections::{HashMap, HashSet}; const EXT4_EXTENTS_FL: u32 = 0x80000; const EXT4_INLINE_DATA_FL: u32 = 0x1000_0000; const EXT_MAGIC: u16 = 0xF30A; const JBD2_MAGIC: u32 = 0xC03B_3998; fn le16(b: &[u8], o: usize) -> u16 { b.get(o..o + 2) .map(|s| u16::from_le_bytes([s[0], s[1]])) .unwrap_or(0) } 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 be32(b: &[u8], o: usize) -> u32 { b.get(o..o + 4) .map(|s| u32::from_be_bytes(s.try_into().unwrap())) .unwrap_or(0) } #[derive(Clone, Copy, Default)] struct Group { block_bitmap: u64, inode_bitmap: u64, inode_table: u64, flags: u16, itable_unused: u32, } #[derive(Clone)] pub struct Inode { pub mode: u16, pub size: u64, pub atime: u32, pub ctime: u32, pub mtime: u32, pub dtime: u32, pub links: u16, pub flags: u32, pub block: [u8; 60], pub crtime: u32, } impl Inode { fn parse(b: &[u8]) -> Option { if b.len() < 128 { return None; } let mut block = [0u8; 60]; block.copy_from_slice(&b[40..100]); let mode = le16(b, 0); let mut size = le32(b, 4) as u64; if mode & 0xF000 == 0x8000 { size |= (le32(b, 0x6C) as u64) << 32; } Some(Inode { mode, size, atime: le32(b, 8), ctime: le32(b, 12), mtime: le32(b, 16), dtime: le32(b, 20), links: le16(b, 26), flags: le32(b, 32), block, crtime: if b.len() >= 0x98 { le32(b, 0x90) } else { 0 }, }) } fn is_reg(&self) -> bool { self.mode & 0xF000 == 0x8000 } fn is_dir(&self) -> bool { self.mode & 0xF000 == 0x4000 } } struct Ext<'a> { src: &'a dyn Source, base: u64, bs: u64, blocks: u64, first_data: u64, bpg: u64, ipg: u64, inode_size: u64, inodes: u64, groups: Vec, journal_inum: u32, } /// Sequence of `(logical_block, physical_block, block_count, is_uninitialized)`. type ExtentList = Vec<(u64, u64, u64, bool)>; impl<'a> Ext<'a> { fn open(src: &'a dyn Source, base: u64) -> Option { let sb = read_vec(src, base + 1024, 1024) .ok() .filter(|s| s.len() == 1024)?; if le16(&sb, 56) != 0xEF53 { return None; } let log = le32(&sb, 24); if log > 6 { return None; } let bs = 1024u64 << log; let incompat = le32(&sb, 96); let is64 = incompat & 0x80 != 0; let mut blocks = le32(&sb, 4) as u64; if is64 { blocks |= (le32(&sb, 336) as u64) << 32; } let first_data = le32(&sb, 20) as u64; let bpg = (le32(&sb, 32) as u64).max(1); let ipg = (le32(&sb, 40) as u64).max(1); let rev = le32(&sb, 76); let inode_size = if rev >= 1 { le16(&sb, 88) as u64 } else { 128 }; if !(128..=4096).contains(&inode_size) { return None; } let desc_size = if is64 { (le16(&sb, 254) as u64).max(64) } else { 32 }; let ngroups = blocks.saturating_sub(first_data).div_ceil(bpg); let gdt_block = first_data + 1; let gdt = read_vec(src, base + gdt_block * bs, (ngroups * desc_size) as usize).ok()?; if (gdt.len() as u64) < ngroups * desc_size { return None; } let mut groups = Vec::with_capacity(ngroups as usize); for g in 0..ngroups as usize { let d = &gdt[g * desc_size as usize..]; let mut gr = Group { block_bitmap: le32(d, 0) as u64, inode_bitmap: le32(d, 4) as u64, inode_table: le32(d, 8) as u64, flags: le16(d, 0x12), itable_unused: le16(d, 0x1C) as u32, }; if is64 && desc_size >= 64 { gr.block_bitmap |= (le32(d, 0x20) as u64) << 32; gr.inode_bitmap |= (le32(d, 0x24) as u64) << 32; gr.inode_table |= (le32(d, 0x28) as u64) << 32; gr.itable_unused |= (le16(d, 0x32) as u32) << 16; } groups.push(gr); } Some(Self { src, base, bs, blocks, first_data, bpg, ipg, inode_size, inodes: le32(&sb, 0) as u64, groups, journal_inum: le32(&sb, 224), }) } fn read_block(&self, blk: u64) -> Option> { if blk >= self.blocks { return None; } let v = read_vec(self.src, self.base + blk * self.bs, self.bs as usize).ok()?; (v.len() as u64 == self.bs).then_some(v) } fn inode_bytes(&self, ino: u64) -> Option> { if ino == 0 || ino > self.inodes { return None; } let g = ((ino - 1) / self.ipg) as usize; let idx = (ino - 1) % self.ipg; let grp = self.groups.get(g)?; let off = self.base + grp.inode_table * self.bs + idx * self.inode_size; let v = read_vec(self.src, off, self.inode_size as usize).ok()?; (v.len() as u64 == self.inode_size).then_some(v) } fn read_inode(&self, ino: u64) -> Option { Inode::parse(&self.inode_bytes(ino)?) } /// Collects extents from an ext4 extent tree (`node` is the 60-byte `i_block` root or a block). fn walk_extent_node( &self, node: &[u8], depth_limit: u32, out: &mut ExtentList, strict: bool, ) -> bool { if node.len() < 12 || le16(node, 0) != EXT_MAGIC { return false; } let entries = le16(node, 2) as usize; let max = le16(node, 4) as usize; let depth = le16(node, 6) as u32; if entries > max || depth > depth_limit || (strict && entries == 0) { return false; } for i in 0..entries { let e = match node.get(12 + i * 12..24 + i * 12) { Some(e) => e, None => return false, }; if depth == 0 { let lblk = le32(e, 0) as u64; let mut len = le16(e, 4) as u64; let uninit = len > 32768; if uninit { len -= 32768; } let start = ((le16(e, 6) as u64) << 32) | le32(e, 8) as u64; if len == 0 || start == 0 || start + len > self.blocks { return false; } out.push((lblk, start, len, uninit)); } else { let child = ((le16(e, 8) as u64) << 32) | le32(e, 4) as u64; let Some(b) = self.read_block(child) else { return false; }; if !self.walk_extent_node(&b, depth - 1, out, true) { return false; } } } true } /// Collects data blocks from an ext2/3 indirect block map. fn block_map(&self, blk: &[u8; 60], max_blocks: u64) -> ExtentList { let mut v: Vec = vec![]; let ptr = |i: usize| le32(blk, i * 4) as u64; for i in 0..12 { v.push(ptr(i)); } let ppb = self.bs / 4; let ind = |b: u64, v: &mut Vec| { if b == 0 { v.extend(std::iter::repeat(0).take(ppb as usize)); } else if let Some(d) = self.read_block(b) { v.extend((0..ppb as usize).map(|i| le32(&d, i * 4) as u64)); } else { v.extend(std::iter::repeat(0).take(ppb as usize)); } }; if v.len() as u64 > max_blocks { v.truncate(max_blocks as usize); } else { ind(ptr(12), &mut v); if (v.len() as u64) < max_blocks && ptr(13) != 0 { if let Some(d) = self.read_block(ptr(13)) { for i in 0..ppb as usize { ind(le32(&d, i * 4) as u64, &mut v); if v.len() as u64 >= max_blocks { break; } } } } if (v.len() as u64) < max_blocks && ptr(14) != 0 { if let Some(d1) = self.read_block(ptr(14)) { 'o: for i in 0..ppb as usize { if let Some(d2) = self.read_block(le32(&d1, i * 4) as u64) { for j in 0..ppb as usize { ind(le32(&d2, j * 4) as u64, &mut v); if v.len() as u64 >= max_blocks { break 'o; } } } } } } } v.truncate(max_blocks as usize); let mut out: ExtentList = vec![]; for (l, &p) in v.iter().enumerate() { if p == 0 || p >= self.blocks { continue; } match out.last_mut() { Some(last) if last.1 + last.2 == p && last.0 + last.2 == l as u64 => last.2 += 1, _ => out.push((l as u64, p, 1, false)), } } out } /// Returns file byte extents (ordered by logical block) truncated to `size` bytes. fn file_extents(&self, ino: &Inode, size: u64) -> Vec { let need = size.div_ceil(self.bs); let mut list: ExtentList = vec![]; if ino.flags & EXT4_EXTENTS_FL != 0 { if !self.walk_extent_node(&ino.block, 5, &mut list, false) { return vec![]; } } else if ino.flags & EXT4_INLINE_DATA_FL == 0 { list = self.block_map(&ino.block, need); } list.sort_by_key(|e| e.0); let mut out = vec![]; let mut left = size; let mut cur_l = 0u64; for (l, p, n, uninit) in list { if left == 0 { break; } if l > cur_l { let hole = ((l - cur_l) * self.bs).min(left); out.push(Extent { offset: 0, len: hole, sparse: true, }); left -= hole; if left == 0 { break; } } let take = (n * self.bs).min(left); out.push(Extent { offset: self.base + p * self.bs, len: take, sparse: uninit, }); left -= take; cur_l = l + n; } out } /// Free-block map (from block bitmaps) as absolute byte extents. fn free_map(&self, ctx: &Ctx) -> (Vec, Vec) { let mut free: Vec = vec![]; let mut allocated_all: Vec = vec![]; let total_groups = self.groups.len() as u64; for (g, gr) in self.groups.iter().enumerate() { if (ctx.cancelled)() { break; } let gstart = self.first_data + g as u64 * self.bpg; let gblocks = self.bpg.min(self.blocks.saturating_sub(gstart)); let mut used_bits = vec![true; gblocks as usize]; if gr.flags & 0x2 != 0 { used_bits.fill(false); free.push(Extent::new(self.base + gstart * self.bs, gblocks * self.bs)); } else if let Some(bm) = read_vec( self.src, self.base + gr.block_bitmap * self.bs, gblocks.div_ceil(8) as usize, ) .ok() .filter(|b| b.len() as u64 == gblocks.div_ceil(8)) { let mut run: Option = None; for i in 0..gblocks { let used = bm[(i / 8) as usize] >> (i % 8) & 1 == 1; used_bits[i as usize] = used; match (used, run) { (false, None) => run = Some(i), (true, Some(s)) => { free.push(Extent::new( self.base + (gstart + s) * self.bs, (i - s) * self.bs, )); run = None; } _ => {} } } if let Some(s) = run { free.push(Extent::new( self.base + (gstart + s) * self.bs, (gblocks - s) * self.bs, )); } } allocated_all.extend(used_bits); if g % 64 == 0 { (ctx.progress)(g as u64, total_groups); } } (merge_extents(free), allocated_all) } fn block_allocated(&self, alloc: &[bool], blk: u64) -> bool { blk.checked_sub(self.first_data) .and_then(|i| alloc.get(i as usize).copied()) .unwrap_or(true) } } struct DirEnt { ino: u64, name: String, live: bool, } /// Parses directory entries in a directory block, including unlinked entries in `rec_len` slack. fn parse_dir_block(b: &[u8], max_ino: u64, out: &mut Vec) { let mut off = 0usize; while off + 8 <= b.len() { let ino = le32(b, off) as u64; let rec = le16(b, off + 4) as usize; let nlen = b[off + 6] as usize; if rec < 8 || rec % 4 != 0 || off + rec > b.len() || 8 + nlen > rec { break; } let real = (8 + nlen).div_ceil(4) * 4; if ino != 0 && nlen > 0 && ino <= max_ino { let name = String::from_utf8_lossy(&b[off + 8..off + 8 + nlen]).to_string(); out.push(DirEnt { ino, name, live: true, }); } // Slack space: unlinked entries remain tucked right after the active entry's true length. let mut s = off + if ino != 0 || nlen > 0 { real } else { 8 }; let end = off + rec; if ino == 0 { s = end; // zeroed entry: no reliable slack to scan } while s + 8 <= end { let dino = le32(b, s) as u64; let drec = le16(b, s + 4) as usize; let dn = b[s + 6] as usize; let ok = dino != 0 && dino <= max_ino && dn > 0 && drec >= 8 + dn && drec % 4 == 0 && s + drec <= end && b[s + 8..s + 8 + dn] .iter() .all(|&c| c >= 0x20 && c != b'/' && c != 0x7f); if !ok { s += 4; continue; } out.push(DirEnt { ino: dino, name: String::from_utf8_lossy(&b[s + 8..s + 8 + dn]).to_string(), live: false, }); s += (8 + dn).div_ceil(4) * 4; } off += rec; } } struct Names { /// `inode -> (parent_dir, name)` from live directory entries. live: HashMap, /// `inode -> (parent_dir, name)` from unlinked slack directory entries. dead: HashMap, } fn walk_dirs(ext: &Ext, ctx: &Ctx) -> Names { let mut names = Names { live: HashMap::new(), dead: HashMap::new(), }; let mut queue: Vec<(u64, String)> = vec![(2, String::new())]; let mut seen: HashSet = HashSet::new(); let mut visited = 0usize; while let Some((dino, path)) = queue.pop() { if !seen.insert(dino) || (visited % 256 == 255 && (ctx.cancelled)()) || visited > 2_000_000 { continue; } visited += 1; let Some(ino) = ext.read_inode(dino).filter(|i| i.is_dir()) else { continue; }; let mut ents = vec![]; for e in ext.file_extents(&ino, ino.size.min(64 << 20)) { if e.sparse { continue; } let mut o = 0; while o < e.len { if let Ok(b) = read_vec(ext.src, e.offset + o, ext.bs as usize) { if b.len() as u64 == ext.bs { parse_dir_block(&b, ext.inodes, &mut ents); } } o += ext.bs; } } for en in ents { if en.name == "." || en.name == ".." { continue; } if en.live { names .live .entry(en.ino) .or_insert_with(|| (path.clone(), en.name.clone())); queue.push((en.ino, format!("{}/{}", path, en.name))); } else { names .dead .entry(en.ino) .or_insert_with(|| (path.clone(), en.name)); } } } names } // ---------------------------------------------------------------- journal /// Inode copies found in the jbd2 journal: `inode_num -> Vec` in journal order. fn journal_inodes(ext: &Ext, ctx: &Ctx) -> HashMap>> { let mut out: HashMap>> = HashMap::new(); if ext.journal_inum == 0 { return out; } let Some(jino) = ext.read_inode(ext.journal_inum as u64) else { return out; }; let jext = ext.file_extents(&jino, jino.size.min(1 << 30)); // Physical blocks of the journal in logical sequence. let mut jblocks: Vec = vec![]; for e in &jext { if e.sparse { continue; } for k in 0..e.len / ext.bs { jblocks.push((e.offset - ext.base) / ext.bs + k); } } if jblocks.len() < 4 { return out; } let Some(jsb) = ext.read_block(jblocks[0]) else { return out; }; if be32(&jsb, 0) != JBD2_MAGIC { return out; } let jbs = be32(&jsb, 12) as u64; if jbs != ext.bs { return out; } let incompat = be32(&jsb, 40); let is64 = incompat & 0x2 != 0; let csum_v3 = incompat & 0x10 != 0; let csum_v2 = incompat & 0x8 != 0; let tag_bytes = if csum_v3 { 16 } else { 8 + if is64 { 4 } else { 0 } }; let have_csum = csum_v2 || csum_v3; // Inode table block ranges for each block group. let itable_blocks = (ext.ipg * ext.inode_size).div_ceil(ext.bs); let mut tables: Vec<(u64, u64, u64)> = vec![]; // (start_block, end_block, first_inode_num) for (g, gr) in ext.groups.iter().enumerate() { tables.push(( gr.inode_table, gr.inode_table + itable_blocks, g as u64 * ext.ipg + 1, )); } let n = jblocks.len(); let first = (be32(&jsb, 20) as usize).clamp(1, n); let mut seqs: Vec<(u32, usize)> = vec![]; // (commit_sequence, descriptor_index) let mut idx = first; // Scan journal blocks for descriptor blocks in sequence order. while idx < n { if idx % 4096 == 4095 && (ctx.cancelled)() { break; } let Some(d) = ext.read_block(jblocks[idx]) else { idx += 1; continue; }; if be32(&d, 0) == JBD2_MAGIC && be32(&d, 4) == 1 { seqs.push((be32(&d, 8), idx)); } idx += 1; } seqs.sort_by_key(|s| s.0); for (_seq, didx) in seqs { let Some(d) = ext.read_block(jblocks[didx]) else { continue; }; let limit = d.len() - if have_csum { 4 } else { 0 }; let mut off = 12usize; let mut data_idx = didx + 1; loop { if off + tag_bytes > limit { break; } let mut blk = be32(&d, off) as u64; let (flags, hi) = if csum_v3 { (be32(&d, off + 4), be32(&d, off + 8)) } else { ( u16::from_be_bytes([d[off + 6], d[off + 7]]) as u32, if is64 { be32(&d, off + 8) } else { 0 }, ) }; blk |= (hi as u64) << 32; off += tag_bytes; if flags & 2 == 0 { off += 16; } let di = if data_idx >= n { first + (data_idx - n) } else { data_idx }; data_idx += 1; if flags & 4 == 0 { if let Some(t) = tables.iter().position(|t| blk >= t.0 && blk < t.1) { if let Some(mut data) = ext.read_block(jblocks[di.min(n - 1)]) { if flags & 1 != 0 { data[0..4].copy_from_slice(&JBD2_MAGIC.to_be_bytes()); } let per_block = (ext.bs / ext.inode_size) as usize; let first_ino = tables[t].2 + (blk - tables[t].0) * per_block as u64; for k in 0..per_block { let rec = &data [k * ext.inode_size as usize..(k + 1) * ext.inode_size as usize]; if rec[..128].iter().any(|&x| x != 0) { out.entry(first_ino + k as u64) .or_default() .push(rec.to_vec()); } } } } } if flags & 8 != 0 { break; } } } out } // -------------------------------------------------------------- scan pub fn scan(ctx: &mut Ctx, base: u64, part_len: u64) -> VolumeScan { let mut res = VolumeScan::default(); let Some(ext) = Ext::open(ctx.src, base) else { return res; }; let (free, alloc) = ext.free_map(ctx); let _ = part_len; res.free = Some(free); let names = walk_dirs(&ext, ctx); let journal = journal_inodes(&ext, ctx); let mut emitted: HashSet = HashSet::new(); let mut seen_extents: HashSet<(u64, u64)> = HashSet::new(); // 1) Deleted inodes in the current inode table. let total = ext.groups.len() as u64; for (g, gr) in ext.groups.iter().enumerate() { if g % 16 == 15 && (ctx.cancelled)() { return res; } if gr.flags & 0x1 != 0 { continue; // INODE_UNINIT } let used = ext.ipg.saturating_sub(gr.itable_unused as u64).min(ext.ipg); let Some(table) = read_vec( ext.src, ext.base + gr.inode_table * ext.bs, (used * ext.inode_size) as usize, ) .ok() else { continue; }; for i in 0..(table.len() as u64 / ext.inode_size) { let ino_no = g as u64 * ext.ipg + i + 1; let raw = &table[(i * ext.inode_size) as usize..((i + 1) * ext.inode_size) as usize]; let Some(ino) = Inode::parse(raw) else { continue; }; if names.live.contains_key(&ino_no) || ino_no < 11 || ino.mode == 0 { continue; } let deleted = ino.dtime != 0 || ino.links == 0; if !deleted || !ino.is_reg() { continue; } // Best source: pre-deletion copy from the journal; fallback to remnants in the current inode. let mut cand: Option<(Inode, &str)> = None; if let Some(snaps) = journal.get(&ino_no) { for raw in snaps.iter().rev() { if let Some(j) = Inode::parse(raw) { if j.is_reg() && j.size > 0 && j.dtime == 0 && j.links > 0 { let ex = ext.file_extents(&j, j.size); if ex.iter().any(|e| !e.sparse) { cand = Some((j, "diário")); break; } } } } } if cand.is_none() { cand = Some((ino.clone(), "inode")); } if let Some((c, src_kind)) = cand { if emit_inode( ctx, &ext, &alloc, &names, ino_no, &c, src_kind, &mut seen_extents, ) { emitted.insert(ino_no); } } } if g % 16 == 0 { (ctx.progress)(g as u64, total); } } // 2) Inodes only preserved in the journal (already zeroed in the main inode table). for (ino_no, snaps) in &journal { if emitted.contains(ino_no) || names.live.contains_key(ino_no) || *ino_no < 11 { continue; } let cur = ext.read_inode(*ino_no); // Skip if the inode is currently live in the filesystem. if cur .as_ref() .map(|c| c.dtime == 0 && c.links > 0 && c.mode != 0) .unwrap_or(false) { continue; } for raw in snaps.iter().rev() { if let Some(j) = Inode::parse(raw) { if j.is_reg() && j.size > 0 && j.dtime == 0 && j.links > 0 && emit_inode( ctx, &ext, &alloc, &names, *ino_no, &j, "diário", &mut seen_extents, ) { break; } } } } res } #[allow(clippy::too_many_arguments)] fn emit_inode( ctx: &mut Ctx, ext: &Ext, alloc: &[bool], names: &Names, ino_no: u64, ino: &Inode, src_kind: &str, seen: &mut HashSet<(u64, u64)>, ) -> bool { let mut extents: Vec; let mut size = ino.size; let mut estimated = false; if ino.flags & EXT4_EXTENTS_FL != 0 { let mut list: ExtentList = vec![]; if !ext.walk_extent_node(&ino.block, 5, &mut list, true) { // Extent remnants in a truncated inode: header count was zeroed, but extent entries remain. list = remnant_extents(ext, &ino.block); } if list.is_empty() { return false; } list.sort_by_key(|e| e.0); if size == 0 { size = list.iter().map(|e| e.2).sum::() * ext.bs; estimated = true; } extents = layout_from_list(ext, &list, size); } else { if size == 0 { return false; } extents = ext.file_extents(ino, size); } if extents.is_empty() || extents.iter().all(|e| e.sparse) { return false; } let first = extents .iter() .find(|e| !e.sparse) .map(|e| (e.offset, e.len)) .unwrap(); if !seen.insert((first.0, first.1)) { return false; } // Check how many data blocks are still unallocated; reallocated blocks lower confidence. let mut reused = 0u64; let mut total_blocks = 0u64; for e in extents.iter().filter(|e| !e.sparse) { let b0 = (e.offset - ext.base) / ext.bs; for k in 0..e.len.div_ceil(ext.bs) { total_blocks += 1; if ext.block_allocated(alloc, b0 + k) { reused += 1; } } } let mut note = Some(format!("recuperado do {src_kind} do ext4")); let mut conf = Confidence::High; if reused > 0 { conf = if reused * 2 > total_blocks { Confidence::Low } else { Confidence::Medium }; note = Some(format!( "{reused} de {total_blocks} blocos já foram reaproveitados por outros arquivos" )); } if estimated { // Unknown exact size (zeroed on deletion): try to refine size via format structure. if let Some(h) = extents .iter() .find(|e| !e.sparse) .and_then(|e| read_vec(ext.src, e.offset, 128).ok()) .and_then(|head| signatures::detect(ext.src, first.0, &head, size)) { if h.len <= size && h.len > 0 { size = h.len; extents = super::fat::trim_extents(extents, size); estimated = false; } } if estimated { conf = conf.min(Confidence::Medium); note = Some("tamanho exato perdido na exclusão; usando todos os blocos do arquivo".into()); } } let (dir, name) = match names.dead.get(&ino_no).or_else(|| names.live.get(&ino_no)) { Some((d, n)) => (Some(d.clone()), n.clone()), None => (None, format!("inode_{ino_no}")), }; let ts = |t: u32| (t != 0).then_some(t as i64); let nf = NewFound { name, dir, size, origin: Origin::Ext, data: DataLoc::Extents { extents }, modified: ts(ino.mtime), created: ts(ino.crtime), accessed: ts(ino.atime), base: conf, note: note.as_deref(), }; let f = make_found(ctx, nf); (ctx.emit)(f); true } fn layout_from_list(ext: &Ext, list: &ExtentList, size: u64) -> Vec { let mut out = vec![]; let mut left = size; let mut cur = 0u64; for &(l, p, n, uninit) in list { if left == 0 { break; } if l > cur { let hole = ((l - cur) * ext.bs).min(left); out.push(Extent { offset: 0, len: hole, sparse: true, }); left -= hole; if left == 0 { break; } } let take = (n * ext.bs).min(left); out.push(Extent { offset: ext.base + p * ext.bs, len: take, sparse: uninit, }); left -= take; cur = l + n; } out } /// Reads leftover extent records from `i_block` after `eh_entries` was zeroed on deletion. fn remnant_extents(ext: &Ext, block: &[u8; 60]) -> ExtentList { let mut out: ExtentList = vec![]; if le16(block, 0) != EXT_MAGIC || le16(block, 6) != 0 { return out; } let max = (le16(block, 4) as usize).min(4); let mut next_l = 0u64; for i in 0..max { let e = &block[12 + i * 12..24 + i * 12]; let lblk = le32(e, 0) as u64; let mut len = le16(e, 4) as u64; let uninit = len > 32768; if uninit { len -= 32768; } let start = ((le16(e, 6) as u64) << 32) | le32(e, 8) as u64; if len == 0 || start < ext.first_data || start + len > ext.blocks || lblk < next_l { break; } next_l = lblk + len; out.push((lblk, start, len, uninit)); } out } /// Measures filesystem structural coherence: fraction of directory entries whose target inode /// matches the declared file type. Used to score candidate metadata-less RAID parameters. pub fn consistency_score(src: &dyn Source, base: u64) -> Option { let ext = Ext::open(src, base)?; let mut queue = vec![2u64]; let mut seen = HashSet::new(); let (mut ok, mut total) = (0u32, 0u32); let (mut dots_ok, mut dots_total) = (0u32, 0u32); while let Some(d) = queue.pop() { if !seen.insert(d) || total >= 600 { continue; } let Some(ino) = ext.read_inode(d).filter(|i| i.is_dir()) else { continue; }; let mut ents = vec![]; for e in ext.file_extents(&ino, ino.size.min(1 << 20)) { if e.sparse { continue; } let mut o = 0; while o < e.len { if let Ok(b) = read_vec(ext.src, e.offset + o, ext.bs as usize) { if b.len() as u64 == ext.bs { parse_dir_block(&b, ext.inodes, &mut ents); } } o += ext.bs; } } // A directory's "." entry points to itself: only matches if both the inode table and // directory data block were mapped from the right RAID stripe. dots_total += 1; if ents.iter().any(|e| e.live && e.name == "." && e.ino == d) { dots_ok += 1; } for en in ents .iter() .filter(|e| e.live && e.name != "." && e.name != "..") { total += 1; if let Some(i) = ext.read_inode(en.ino) { if i.mode != 0 && i.links > 0 { ok += 1; if i.is_dir() { queue.push(en.ino); } } } if total >= 600 { break; } } } if total == 0 { return None; } let a = ok as f64 / total as f64; let b = if dots_total > 0 { dots_ok as f64 / dots_total as f64 } else { 0.0 }; Some(1.0 + 5.0 * a + 5.0 * b) }