"""Find verified RIP-relative MOV/LEA references in the pinned image, offline."""
import argparse,bisect,hashlib,json,re
from pathlib import Path
import pefile
from capstone import Cs,CS_ARCH_X86,CS_MODE_64
from capstone.x86_const import X86_OP_MEM,X86_REG_RIP

parser=argparse.ArgumentParser(description=__doc__)
parser.add_argument('targets',nargs='+',type=lambda v:int(v,0))
parser.add_argument('--output',required=True,type=Path)
args=parser.parse_args()
image=Path('artifacts/ship-model-reference/client-copy/EliteDangerous64.exe').read_bytes()
sha=hashlib.sha256(image).hexdigest()
if sha!='6891f70a021fa9f3a0e00458c284f48d1226e68ae7fc02ad48081aed47857e93':raise ValueError('Pinned image changed')
pe=pefile.PE(data=image,fast_load=True);base=pe.OPTIONAL_HEADER.ImageBase
pe.parse_data_directories(directories=[3]);entries=[e.struct for e in pe.DIRECTORY_ENTRY_EXCEPTION];starts=[e.BeginAddress for e in entries]
decoder=Cs(CS_ARCH_X86,CS_MODE_64);decoder.detail=True;owners={};rows=[]
for section in pe.sections:
    if not section.Characteristics&0x20000000:continue
    data=section.get_data();start=section.VirtualAddress
    # Only shortlist byte candidates here. Each returned reference must also
    # be an instruction boundary in its exact PE exception range below.
    for match in re.finditer(rb'[\x40-\x4f][\x8b\x8d][\x05\x0d\x15\x1d\x25\x2d\x35\x3d][\s\S]{4}',data):
        rva=start+match.start();code=match.group();target=rva+7+int.from_bytes(code[3:],'little',signed=True)
        if target not in args.targets:continue
        index=bisect.bisect_right(starts,rva)-1
        if index<0 or rva>=entries[index].EndAddress:continue
        entry=entries[index]
        if entry.BeginAddress not in owners:
            body=pe.get_data(entry.BeginAddress,entry.EndAddress-entry.BeginAddress)
            owners[entry.BeginAddress]=(hashlib.sha256(body).hexdigest(),{i.address:i for i in decoder.disasm(body,entry.BeginAddress)})
        digest,instructions=owners[entry.BeginAddress];instruction=instructions.get(rva)
        if instruction is None or instruction.size!=7 or instruction.bytes!=code:continue
        if not any(o.type==X86_OP_MEM and o.mem.base==X86_REG_RIP and rva+instruction.size+o.mem.disp==target for o in instruction.operands):continue
        rows.append(dict(targetRva=hex(target),rva=hex(rva),functionRangeRva=hex(entry.BeginAddress),
            functionRangeEndRva=hex(entry.EndAddress),functionRangeSha256=digest,bytes=code.hex(),instruction=instruction.mnemonic+' '+instruction.op_str))
result=dict(imageSha256=sha,gameProcessAccess=False,gameCalls=0,targets=[hex(v) for v in args.targets],references=rows)
args.output.write_text(json.dumps(result,indent=2)+'\n');print(json.dumps(result,indent=2))
