#!/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 <inode> [<inode> ...]
    python3 diagnose_inode.py --dir <dir_inode>   # 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('<HHHH', idata, base)
    print(f"  ext magic : {magic:#06x}  {'OK' if magic == 0xF30A else 'BAD — not a valid extent header'}")
    print(f"  ext depth : {depth}   entries: {entries}")

    if magic != 0xF30A:
        print("  DIAGNOSIS : invalid extent tree magic — inode body is corrupt/zeroed")
        return

    blocks = ext4lib.read_extent_tree_blocks(f, idata, slot)
    print(f"  extents   : {len(blocks)} logical→physical block pairs")

    if not blocks:
        print("  DIAGNOSIS : extent tree parsed but returned no blocks")
        print("              (entries=0, or all extents have phys=0, or index nodes unreadable)")
        return

    zeroed  = [(l, p) for l, p in blocks if p * ext4lib.BLOCK // ext4lib.BLOCK <= zeroed_max_inum * 8]
    # More direct check: block in zeroed region means block < zeroed_groups * blocks_per_group
    zeroed_block_max = ext4db.get_fs_meta_int(db, 'zeroed_groups', 13) * \
                       ext4db.get_fs_meta_int(db, 'blocks_per_group', 32768) if db else 0
    zeroed_blocks = [(l, p) for l, p in blocks if p < zeroed_block_max]
    good_blocks   = [(l, p) for l, p in blocks if p >= 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 <inode>')

    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()

