"""Read-only native evidence for local navigation; never attaches to or invokes the game."""
import argparse, bisect, hashlib, json, struct
from pathlib import Path
import pefile
from capstone import Cs, CS_ARCH_X86, CS_MODE_64
from capstone.x86 import X86_OP_MEM, X86_OP_IMM, X86_REG_RIP

p = argparse.ArgumentParser(description=__doc__)
p.add_argument('image', type=Path)
p.add_argument('--rva', action='append', type=lambda s: int(s, 0), required=True)
p.add_argument('--output', type=Path, required=True)
a = p.parse_args()
raw = a.image.read_bytes()
pe = pefile.PE(data=raw, fast_load=True)
pe.parse_data_directories(directories=[pefile.DIRECTORY_ENTRY['IMAGE_DIRECTORY_ENTRY_EXCEPTION']])
rows = sorted((e.struct.BeginAddress, e.struct.EndAddress, e.struct.UnwindData) for e in pe.DIRECTORY_ENTRY_EXCEPTION)
starts = [r[0] for r in rows]
decoder = Cs(CS_ARCH_X86, CS_MODE_64)
decoder.detail = True
parents={}
def parent(entry):
    if entry[0] in parents:return parents[entry[0]]
    first=entry[0]
    for _ in range(16):
        header=pe.get_data(entry[2],4)
        if not (header[0]>>3)&4:break
        entry=struct.unpack('<III',pe.get_data(entry[2]+4+((header[2]+1)&~1)*2,12))
    parents[first]=entry[0]
    return entry[0]
groups={}
for row in rows:groups.setdefault(parent(row),[]).append(row)

def text_at(rva):
    if not 0 <= rva < pe.OPTIONAL_HEADER.SizeOfImage:
        return None
    try:
        b = pe.get_data(rva, 160).split(b'\0')[0]
    except pefile.PEFormatError:
        return None
    return b.decode('ascii') if 2 < len(b) < 160 and all(32 <= c < 127 for c in b) else None

def function(rva):
    i = bisect.bisect_right(starts, rva)-1
    if i < 0 or not rows[i][0] <= rva < rows[i][1]:
        leaf=list(decoder.disasm(pe.get_data(rva,128),rva))
        stop=next((n for n,x in enumerate(leaf) if x.mnemonic in ('ret','jmp')),None)
        if stop is None:raise ValueError('Missing bounded leaf at '+hex(rva))
        end=leaf[stop].address+leaf[stop].size;begin=rva;fragments=[(begin,end,0)]
    else:
        begin=parent(rows[i]);fragments=groups[begin];end=max(r[1] for r in fragments)
    lines=[]
    for ins in (ins for start,finish,_ in fragments for ins in decoder.disasm(pe.get_data(start,min(finish-start,65536)),start)):
        annotations=[]
        for op in ins.operands:
            if op.type == X86_OP_MEM and op.mem.base == X86_REG_RIP:
                at = ins.address+ins.size+op.mem.disp
                value = text_at(at)
                try:
                    b = pe.get_data(at, 8)
                except pefile.PEFormatError:
                    b = b''
                annotations.append(hex(at)+(' "'+value+'"' if value else (' f32='+str(struct.unpack('<f',b[:4])[0]) if len(b)>=4 else '')))
            elif op.type == X86_OP_IMM and ins.mnemonic == 'call':
                annotations.append('call '+hex(op.imm))
        lines.append(hex(ins.address)+' '+ins.mnemonic+' '+ins.op_str+(' ; '+' | '.join(annotations) if annotations else ''))
    return {'requested':hex(rva), 'begin':hex(begin), 'end':hex(end), 'fragments':[(hex(r[0]),hex(r[1])) for r in fragments], 'lines':lines}

result={'imageSha256':hashlib.sha256(raw).hexdigest(), 'scope':'Static PE evidence only. A symbol reference does not validate an object layout or invocation ABI.', 'functions':[function(r) for r in a.rva]}
a.output.parent.mkdir(parents=True, exist_ok=True)
a.output.write_text(json.dumps(result, indent=2), encoding='utf-8')
print(json.dumps({'output':str(a.output), 'functions':[(f['begin'],len(f['lines'])) for f in result['functions']]}))
