#!/usr/bin/env python3 """ Recursive ext4 directory dumper by inode number. Bypasses all metadata validation - uses extent trees directly. """ import struct, os, sys, stat from pathlib import Path DEV = '/dev/dm-0' BLOCK = 4096 BACKUP_SB_BLOCK = 32768 # ── low-level helpers ──────────────────────────────────────────────────────── def read_at(f, offset, size): f.seek(offset) return f.read(size) def parse_superblock(data): sb = {} sb['inodes_count'] = struct.unpack_from('= 64: hi = struct.unpack_from(' len(data): break l_block = struct.unpack_from(' 0: extents.append((l_block, phys, ee_len & 0x7FFF)) else: # Depth > 0: extent index node - follow first child # (handles large dirs gracefully) o = base + 12 ei_leaf_lo = struct.unpack_from(' (child_inum, ftype).""" idata, slot = read_inode(f, sb, gdt_data, inum) entries = {} for _, phys, length in parse_extent_tree(idata, slot): for blk in range(length): try: bdata = read_at(f, (phys + blk) * BLOCK, BLOCK) offset = 0 while offset < BLOCK - 8: e_ino, rec_len, name_len, ftype = \ struct.unpack_from(' BLOCK: break if e_ino != 0 and name_len > 0: name = bdata[offset+8:offset+8+name_len]\ .decode('utf-8', errors='replace') entries[name] = (e_ino, ftype) offset += rec_len except OSError: pass return entries def dump_file(f, sb, gdt_data, inum, dest_path): """Extract a regular file by inode to dest_path.""" try: idata, slot = read_inode(f, sb, gdt_data, inum) size_lo = struct.unpack_from('= size: break chunk = read_at(f, (phys + blk) * BLOCK, BLOCK) remaining = size - written out.write(chunk[:remaining] if remaining < BLOCK else chunk) written += min(BLOCK, remaining) return True except OSError: return False def dump_symlink(f, sb, gdt_data, inum, dest_path): """Extract symlink target.""" try: idata, slot = read_inode(f, sb, gdt_data, inum) size = struct.unpack_from(' ") sys.exit(1) root_inum = int(sys.argv[1]) dest_dir = sys.argv[2] with open(DEV, 'rb') as f: sb_data = read_at(f, BACKUP_SB_BLOCK * BLOCK, 1024) sb = parse_superblock(sb_data) assert sb['magic'] == 0xef53 num_groups = (sb['blocks_count'] + sb['blocks_per_group'] - 1) \ // sb['blocks_per_group'] gdt_data = read_at(f, (BACKUP_SB_BLOCK + 1) * BLOCK, num_groups * sb['desc_size']) print(f"Dumping inode {root_inum} -> {dest_dir}") dump_tree(f, sb, gdt_data, root_inum, dest_dir) print("Done") if __name__ == '__main__': main()