#!/usr/bin/env python3
"""枚举 exFAT 镜像中的目录项（含已删除项），找回误删文件夹。只读，不写盘。
exFAT EntryType 位: bit7=InUse  bit6=Category(1=二级)  bit0-4=类型码
  0x85 File(一级,码5) / 0xC0 流扩展(二级,码0) / 0xC1 文件名(二级,码1)
  删除 = InUse 清零 → 0x05 / 0x40 / 0x41（文件名本身不受影响，可完整还原）
"""
import struct, sys

IMG = sys.argv[1]
f = open(IMG, 'rb')

def rd(off, n):
    f.seek(off); return f.read(n)

bs_off = None
for off in range(0, 1 << 20, 512):
    if rd(off + 3, 8) == b'EXFAT   ':
        bs_off = off; break
if bs_off is None:
    sys.exit('未找到 exFAT 引导扇区')

bs = rd(bs_off, 512)
bps = 1 << bs[0x6C]
spc = 1 << bs[0x6D]
fat_off = struct.unpack_from('<I', bs, 0x50)[0]
heap_off = struct.unpack_from('<I', bs, 0x58)[0]
clu_count = struct.unpack_from('<I', bs, 0x5C)[0]
root_clu = struct.unpack_from('<I', bs, 0x60)[0]
print(f'bytes/sector={bps} sectors/cluster={spc} cluster={bps*spc}B '
      f'簇数={clu_count} 根目录簇={root_clu}')

FAT_BASE = bs_off + fat_off * bps
HEAP_BASE = bs_off + heap_off * bps
CLU_BYTES = bps * spc

def fat_entry(n):
    f.seek(FAT_BASE + 4 * n); return struct.unpack('<I', f.read(4))[0]

def read_clu(n):
    return rd(HEAP_BASE + (n - 2) * CLU_BYTES, CLU_BYTES)

def chain(start, limit=1 << 20):
    out, c, seen = [], start, set()
    while 2 <= c < 0xFFFFFFF8 and len(out) < limit and c not in seen:
        seen.add(c); out.append(c); c = fat_entry(c)
    return out

def parse_dir(cluster, contiguous=False, max_clusters=64):
    """解析目录内容。contiguous=True 时按物理连续簇读（删除后 FAT 链已清空）"""
    if contiguous:
        clus = list(range(cluster, cluster + max_clusters))
        raw = b''.join(read_clu(c) for c in clus)
        # 只保留到第一个 0x00 结尾项之前的有效区
    else:
        clus = chain(cluster) or [cluster]
        raw = b''.join(read_clu(c) for c in clus)
    ents = []
    for i in range(0, len(raw) - 31, 32):
        e = raw[i:i + 32]
        if e[0] == 0x00:
            break
        t = e[0]
        rec = {'i': i // 32, 'type': t, 'code': t & 0x1F,
               'cat2': bool(t & 0x40), 'live': bool(t & 0x80), 'raw': e}
        if not rec['cat2'] and rec['code'] == 5:            # File entry
            rec['attrs'] = struct.unpack_from('<H', e, 4)[0]
            rec['sec'] = e[1]
            rec['isdir'] = bool(rec['attrs'] & 0x10)
        elif rec['cat2'] and rec['code'] == 0:              # 流扩展
            rec['nofat'] = bool(e[1] & 2)
            rec['namelen'] = e[3]
            rec['first_clu'] = struct.unpack_from('<I', e, 0x14)[0]
            rec['datalen'] = struct.unpack_from('<Q', e, 0x18)[0]
            rec['validlen'] = struct.unpack_from('<Q', e, 0x08)[0]
        elif rec['cat2'] and rec['code'] == 1:              # 文件名
            rec['name'] = e[2:32].decode('utf-16-le', 'replace')
        ents.append(rec)
    return ents, clus, raw

def walk(ents, indent=''):
    out = []
    i = 0
    while i < len(ents):
        e = ents[i]
        if not e['cat2'] and e['code'] == 5:
            sec = e['sec']
            name, stream = '', None
            for j in range(i + 1, min(i + 1 + sec, len(ents))):
                s = ents[j]
                if s['cat2'] and s['code'] == 0:
                    stream = s
                elif s['cat2'] and s['code'] == 1:
                    name += s.get('name', '')
            if stream:
                name = name[:stream['namelen']]
            tag = '在用  ' if e['live'] else '已删除'
            d = {'name': name, 'dir': e['isdir'], 'live': e['live'],
                 'attrs': e['attrs'], 'sec': sec,
                 'clu': stream['first_clu'] if stream else None,
                 'size': stream['datalen'] if stream else None,
                 'valid': stream['validlen'] if stream else None,
                 'nofat': stream['nofat'] if stream else None}
            print(f"{indent}[{tag}] {'DIR ' if d['dir'] else 'FILE'} "
                  f"name={d['name']!r} 属性=0x{d['attrs']:04x} "
                  f"首簇={d['clu']} 大小={d['size']}")
            out.append(d)
            i += 1 + sec
        else:
            i += 1
    return out

print('\n===== 根目录（簇 %d） =====' % root_clu)
ents, clus, raw = parse_dir(root_clu)
entries = walk(ents)

print('\n===== 根目录前 1KB hexdump（找删除痕迹） =====')
for o in range(0, 1024, 32):
    row = raw[o:o + 32]
    mark = ''
    t = row[0]
    if t not in (0x00,):
        mark = f'  <- type=0x{t:02x} code={t & 0x1F} cat2={1 if t & 0x40 else 0} live={1 if t & 0x80 else 0}'
    print(f'{o:04x}  ' + ' '.join(f'{b:02x}' for b in row) + mark)
