#!/usr/bin/env python3
import struct, os

DEVICE     = '/dev/nbd0'
BSIZE      = 4096
IPG        = 8192
INODE_SZ   = 256
NUM_GROUPS = 35728
MIN_GROUP  = 13

TARGETS = [
    b'pterodactyl',
    b'mysql',
    b'www',
    b'log',
    b'docker',
    b'nginx',
    b'apache2',
    b'archives',
    b'wings',
    b'grub2-efi.cfg',
    b'commons-codec',
]

def is_valid_dirent(block, off, name):
    if off + 8 + len(name) > BSIZE: return False
    inode    = struct.unpack_from('<I', block, off)[0]
    rec_len  = struct.unpack_from('<H', block, off+4)[0]
    name_len = block[off+6]
    ftype    = block[off+7]
    if not (10 < inode < 500_000_000): return False
    if name_len != len(name): return False
    if rec_len < 8+name_len or rec_len > BSIZE or rec_len%4 != 0: return False
    if ftype not in (1,2,7): return False
    if block[off+8:off+8+name_len] != name: return False
    pad = off+8+name_len
    if pad < BSIZE and block[pad] != 0: return False
    return True

def parse_extents(inode_data):
    blocks = []
    magic = struct.unpack_from('<H', inode_data, 40)[0]
    if magic != 0xf30a:
        return blocks
    depth   = struct.unpack_from('<H', inode_data, 46)[0]
    entries = struct.unpack_from('<H', inode_data, 42)[0]
    if depth == 0:
        for i in range(min(entries, 4)):
            off      = 52 + i*12
            ee_len   = struct.unpack_from('<H', inode_data, off+4)[0]
            ee_hi    = struct.unpack_from('<H', inode_data, off+6)[0]
            ee_lo    = struct.unpack_from('<I', inode_data, off+8)[0]
            ee_start = (ee_hi << 32) | ee_lo
            if ee_len > 1024: continue
            for b in range(min(ee_len, 8)):
                blocks.append(ee_start + b)
    return blocks

results = {}

print(f'Device: {DEVICE}')
print(f'Scanning groups {MIN_GROUP} to {NUM_GROUPS-1}...')
print()

with open(DEVICE, 'rb', buffering=0) as f:
    for group in range(MIN_GROUP, NUM_GROUPS):
        it_block = 1070 + group * 512
        try:
            f.seek(it_block * BSIZE)
            inode_table = f.read(IPG * INODE_SZ)
        except OSError:
            continue

        for idx in range(IPG):
            inode_data = inode_table[idx*INODE_SZ:(idx+1)*INODE_SZ]
            if not any(inode_data):
                continue

            mode  = struct.unpack_from('<H', inode_data, 0)[0]
            links = struct.unpack_from('<H', inode_data, 26)[0]

            if (mode & 0xf000) != 0x4000:
                continue
            if links < 2:
                continue

            inode_num = group * IPG + idx + 1
            blocks = parse_extents(inode_data)

            for blk in blocks:
                try:
                    f.seek(blk * BSIZE)
                    blk_data = f.read(BSIZE)
                except OSError:
                    continue

                for target in TARGETS:
                    if target not in blk_data:
                        continue
                    for off in range(0, BSIZE-8):
                        if blk_data[off+8:off+8+len(target)] != target:
                            continue
                        if is_valid_dirent(blk_data, off, target):
                            child_ino = struct.unpack_from('<I',blk_data,off)[0]
                            ftype     = blk_data[off+7]
                            child_grp = (child_ino-1)//IPG
                            key       = (target.decode(), child_ino)
                            if key not in results:
                                results[key] = (inode_num, ftype, child_grp)
                                status = 'INTACT' if child_grp>=13 else 'LOST'
                                tname  = {1:'file',2:'dir',7:'link'}.get(ftype,'?')
                                print(f'[{status}] {target.decode()!r:15s} '
                                      f'child={child_ino:10d} '
                                      f'parent={inode_num:10d} '
                                      f'type={tname}', flush=True)

        if group % 1000 == 0:
            print(f'  Group {group}/{NUM_GROUPS}...', flush=True)

print()
print('=== SUMMARY ===')
for (name, child_ino), (parent_ino, ftype, grp) in sorted(results.items()):
    status = 'INTACT' if grp >= 13 else 'LOST'
    tname  = {1:'file',2:'dir',7:'link'}.get(ftype,'?')
    print(f'[{status}] {name!r:15s} child={child_ino} '
          f'parent={parent_ino} type={tname}')

