#!/usr/bin/env python3 """ Diagnose why a file inode extracted as empty (or wrong size). For each inode given, reports: - size recorded in the inode - flags (inline-data, extents, etc.) - extent tree validity - whether data blocks fall in the zeroed region - actual bytes readable from each block Usage: python3 diagnose_inode.py [ ...] python3 diagnose_inode.py --dir # diagnose all files in a dir Options: --device DEV [/dev/dm-0] --backup-sb BLOCK [32768] --db PATH [inodes.db] --dir INUM Diagnose every file entry in this directory inode """ import argparse, struct, sys import ext4lib import ext4db DEFAULT_DEV = '/dev/dm-0' DEFAULT_BACKUP_SB = 32768 FLAG_INLINE_DATA = 0x10000000 FLAG_EXTENTS = 0x00080000 def diagnose_inode(f, sb, gdt_data, db, inum, zeroed_max_inum): print(f"\ninode {inum}") print(f" {'─'*50}") # ── DB metadata ─────────────────────────────────────────────────────────── row = ext4db.get_inode(db, inum) if db else None if row: print(f" DB status : {row['status']}") print(f" DB size : {row['size']:,} bytes") print(f" DB type : {row['itype']:#06x}") # ── raw inode from disk ─────────────────────────────────────────────────── try: idata, slot = ext4lib.read_inode(f, sb, gdt_data, inum) except Exception as e: print(f" ERROR reading inode from disk: {e}") return inode = ext4lib.parse_inode_full(idata, slot, sb) if inode is None: print(" ERROR inode data too short") return size = inode['size'] flags = inode['flags'] status = ext4lib.classify_inode(idata, slot) print(f" disk size : {size:,} bytes") print(f" flags : {flags:#010x}", end='') flag_names = [] if flags & FLAG_INLINE_DATA: flag_names.append('INLINE_DATA') if flags & FLAG_EXTENTS: flag_names.append('EXTENTS') print(f" ({', '.join(flag_names)})" if flag_names else '') print(f" status : {status}") if size == 0: print(" DIAGNOSIS : inode size is 0 — file was empty or size field is corrupted") return # ── inline data ─────────────────────────────────────────────────────────── if flags & FLAG_INLINE_DATA: inline = idata[slot + 40:slot + 40 + size] nonzero = sum(1 for b in inline if b != 0) print(f" DIAGNOSIS : inline data ({size} bytes, {nonzero} non-zero bytes in inode body)") return # ── extent tree ─────────────────────────────────────────────────────────── base = slot + 40 magic, entries, _, depth = struct.unpack_from('= zeroed_block_max] print(f" blocks in zeroed region : {len(zeroed_blocks)}/{len(blocks)}") print(f" blocks in good region : {len(good_blocks)}/{len(blocks)}") # Sample first good and first zeroed block if zeroed_blocks and not good_blocks: print(" DIAGNOSIS : ALL data blocks are in the zeroed region — file content is lost") print(f" first block: phys={zeroed_blocks[0][1]} (zeroed region ends at block {zeroed_block_max})") return if zeroed_blocks and good_blocks: print(" DIAGNOSIS : PARTIAL — some blocks recoverable, some in zeroed region") print(f" {len(good_blocks)} of {len(blocks)} blocks are readable") # Check readability of first block l0, p0 = blocks[0] try: bdata = ext4lib.read_at(f, p0 * ext4lib.BLOCK, ext4lib.BLOCK) nonzero = sum(1 for b in bdata if b != 0) print(f" first block phys={p0}: {nonzero}/4096 non-zero bytes") if nonzero == 0: print(" DIAGNOSIS : first data block reads as all zeros — block content wiped or wrong address") else: print(" DIAGNOSIS : data blocks appear readable — extraction should work") except OSError as e: print(f" first block read error: {e}") print(" DIAGNOSIS : I/O error reading data block") def main(): parser = argparse.ArgumentParser(description='Diagnose why an inode extracted as empty') parser.add_argument('inodes', nargs='*', type=int) parser.add_argument('--device', default=DEFAULT_DEV) parser.add_argument('--backup-sb', type=int, default=DEFAULT_BACKUP_SB) parser.add_argument('--db', default='inodes.db') parser.add_argument('--dir', type=int, metavar='INUM', help='Diagnose all file entries under this directory inode') args = parser.parse_args() db = ext4db.open_db(args.db) zeroed_max_inum = (ext4db.get_fs_meta_int(db, 'zeroed_groups', 13) * ext4db.get_fs_meta_int(db, 'inodes_per_group', 8192)) inodes = list(args.inodes) if args.dir: entries = ext4db.get_dir_entries(db, args.dir) for name, (child_inum, ftype) in sorted(entries.items()): if name in ('.', '..'): continue if ftype == ext4lib.FTYPE_REG or ftype == 0: print(f"\n{'='*60}") print(f" {name!r} (inode {child_inum})") inodes.append(child_inum) if not inodes: parser.error('Provide inode numbers or use --dir ') with open(args.device, 'rb') as f: sb, gdt_data, _ = ext4lib.load_fs(f, args.backup_sb) for inum in inodes: diagnose_inode(f, sb, gdt_data, db, inum, zeroed_max_inum) if __name__ == '__main__': main()