"""Close commodity definition/enumeration source bindings across fixed PE images."""
from pathlib import Path
import hashlib,importlib.util,json,struct
spec=importlib.util.spec_from_file_location('audit',Path(__file__).with_name('Audit-GalaxyClientCompatibility.py'))
m=importlib.util.module_from_spec(spec);spec.loader.exec_module(m)
old=m.Image(Path('artifacts/ship-model-reference/client-copy/EliteDangerous64.exe'))
new=m.Image(Path('D:/SteamLibrary/steamapps/common/Elite Dangerous/Products/elite-dangerous-odyssey-64/EliteDangerous64.exe'))
functions=[];refs=[]
for before in (0x1DD20C0,0x1B29840,0x1B31E60,0x1B3B800,0x3A603C0,0x3B3DF50,0x3B3ED60,0x2534630,0x2A60C90,0x2A6F920,0x3A2F7A0,0x1EC2CA0,0x1DF25B0,0x1E34530,
0x1DD2B60,0x1E4D5E0,0x1E6BC80,0x1B390F0,0x3ACA1A0,0x3AC3C20,0x3B27B00,0x3B3D9A0,
0xB1F390,0x1E369C0):
span=old.function(before);length=span[1]-before if span else 128
row=m.match(old,new,before,length)
if row['status']!='unique':raise ValueError('Unclosed commodity body '+hex(before)+': '+row['status'])
row['oldRva']=hex(before);functions.append(row)
refs.extend(m.references(old,new,before,int(row['newRva'],16),row['length']))
tables={}
for before in (0x5241918,0x5241948,0x5241A48,0x5219630,0x5219660,0x5219760,0x5292628,0x5292658,0x5292758,0x530B758,0x530B788,0x530B888,0x5241FB0,0x5241FF0,0x52420F0,
0x552D4C0,0x552D4F0,0x552D7B8,0x552D9F8):
after={r[1] for r in refs if r[0]==before}
if len(after)!=1:raise ValueError('Unclosed commodity source table '+hex(before))
tables[before]=after.pop()
slots=[]
for table,offset in ((0x5241948,0x48),(0x5219660,0x48),(0x5241A48,0),(0x5241A48,0x20),(0x5241A48,0x28),(0x5241A48,0x38),(0x5241A48,0x40),(0x5219760,0),(0x5219760,0x28),(0x5292758,0),(0x5292758,0x68),(0x530B888,0),(0x530B888,0x18),
(0x52420F0,0),(0x52420F0,0x30),(0x52420F0,0x78),(0x552D7B8,0)):
before=struct.unpack('<Q',old.data(table+offset,8))[0]-old.base
after=struct.unpack('<Q',new.data(tables[table]+offset,8))[0]-new.base
length=next(i.address+i.size-old.base-before for i in old.instructions(before,2048) if i.mnemonic=='ret')
if table==0x5241A48 and offset in (0x20,0x28):
# The price/id getters share duplicate bodies elsewhere. Bind them by
# the already-proven interface slot, comparing both return branches.
span=old.function(before)
if span and span[0]==before:length=span[1]-before
else:length=next(i.address-old.base-before for i in old.instructions(before,512) if i.mnemonic=='int3')
if not 0<length<512:raise ValueError('Unbounded enum value getter')
# These leaf bodies contain only relative internal jumps and register/
# member operations; exact equality is stronger than a partial leaf mask.
raw=old.data(before,length);mask=bytes([255])*length
else:raw,mask,*_=m.signature(old,before,length)
actual=new.data(after,length)
if any(a!=b for a,b,keep in zip(raw,actual,mask) if keep):raise ValueError('Commodity slot changed')
paired=m.references(old,new,before,after,length);refs.extend(paired)
slots.append(dict(oldVtable=hex(table),newVtable=hex(tables[table]),slot=hex(offset),oldRva=hex(before),newRva=hex(after),bytes=length,
oldSha256=hashlib.sha256(raw).hexdigest(),newSha256=hashlib.sha256(actual).hexdigest(),references=[list(map(hex,r)) for r in paired]))
globals={}
for before in (0x5F05CE8,0x5F05C34,0x5F05E70,0x5F2F25C,0x5F7E844,0x5F05DB4):
candidates={r[1] for r in refs if r[0]==before}
if len(candidates)!=1:raise ValueError('Commodity dynamic ID binding ambiguous')
globals[hex(before)]=hex(candidates.pop())
getters=[]
for before in (0x1B3AC10,0x1B39630):
candidates={r[1] for r in refs if r[0]==before}
if len(candidates)!=1:raise ValueError('Commodity name/category getter call binding ambiguous')
after=candidates.pop()
length=next(i.address+i.size-old.base-before for i in old.instructions(before,512) if i.mnemonic=='ret')
raw,mask,*_=m.signature(old,before,length);actual=new.data(after,length)
if any(a!=b for a,b,keep in zip(raw,actual,mask) if keep):raise ValueError('Commodity name/category getter changed')
getters.append(dict(oldRva=hex(before),newRva=hex(after),bytes=length,oldSha256=hashlib.sha256(raw).hexdigest(),newSha256=hashlib.sha256(actual).hexdigest()))
output=dict(runtimeNeutral=True,canInstallHooks=False,gameCalls=0,networkRequests=0,
oldImage=dict(path=str(old.path),sha256=hashlib.sha256(old.raw).hexdigest()),
newImage=dict(path=str(new.path),sha256=hashlib.sha256(new.raw).hexdigest()),functions=functions,
vtables={hex(a):hex(b) for a,b in tables.items()},slots=slots,getters=getters,globals=globals)
path=Path('artifacts/galaxy-query-research/commodity-tree/compatibility.json');path.write_text(json.dumps(output,indent=2)+'\n')
print(json.dumps(dict(vtables=output['vtables'],globals=globals,functions=len(functions),slots=len(slots)),indent=2))
"""Close commodity definition/enumeration source bindings across fixed PE images."""
from pathlib import Path
import hashlib,importlib.util,json,struct
spec=importlib.util.spec_from_file_location('audit',Path(__file__).with_name('Audit-GalaxyClientCompatibility.py'))
m=importlib.util.module_from_spec(spec);spec.loader.exec_module(m)
old=m.Image(Path('artifacts/ship-model-reference/client-copy/EliteDangerous64.exe'))
new=m.Image(Path('D:/SteamLibrary/steamapps/common/Elite Dangerous/Products/elite-dangerous-odyssey-64/EliteDangerous64.exe'))
functions=[];refs=[]
for before in (0x1DD20C0,0x1B29840,0x1B31E60,0x1B3B800,0x3A603C0,0x3B3DF50,0x3B3ED60,0x2534630,0x2A60C90,0x2A6F920,0x3A2F7A0,0x1EC2CA0,0x1DF25B0,0x1E34530,
0x1DD2B60,0x1E4D5E0,0x1E6BC80,0x1B390F0,0x3ACA1A0,0x3AC3C20,0x3B27B00,0x3B3D9A0,
0xB1F390,0x1E369C0):
span=old.function(before);length=span[1]-before if span else 128
row=m.match(old,new,before,length)
if row['status']!='unique':raise ValueError('Unclosed commodity body '+hex(before)+': '+row['status'])
row['oldRva']=hex(before);functions.append(row)
refs.extend(m.references(old,new,before,int(row['newRva'],16),row['length']))
tables={}
for before in (0x5241918,0x5241948,0x5241A48,0x5219630,0x5219660,0x5219760,0x5292628,0x5292658,0x5292758,0x530B758,0x530B788,0x530B888,0x5241FB0,0x5241FF0,0x52420F0,
0x552D4C0,0x552D4F0,0x552D7B8,0x552D9F8):
after={r[1] for r in refs if r[0]==before}
if len(after)!=1:raise ValueError('Unclosed commodity source table '+hex(before))
tables[before]=after.pop()
slots=[]
for table,offset in ((0x5241948,0x48),(0x5219660,0x48),(0x5241A48,0),(0x5241A48,0x20),(0x5241A48,0x28),(0x5241A48,0x38),(0x5241A48,0x40),(0x5219760,0),(0x5219760,0x28),(0x5292758,0),(0x5292758,0x68),(0x530B888,0),(0x530B888,0x18),
(0x52420F0,0),(0x52420F0,0x30),(0x52420F0,0x78),(0x552D7B8,0)):
before=struct.unpack('<Q',old.data(table+offset,8))[0]-old.base
after=struct.unpack('<Q',new.data(tables[table]+offset,8))[0]-new.base
length=next(i.address+i.size-old.base-before for i in old.instructions(before,2048) if i.mnemonic=='ret')
if table==0x5241A48 and offset in (0x20,0x28):
# The price/id getters share duplicate bodies elsewhere. Bind them by
# the already-proven interface slot, comparing both return branches.
span=old.function(before)
if span and span[0]==before:length=span[1]-before
else:length=next(i.address-old.base-before for i in old.instructions(before,512) if i.mnemonic=='int3')
if not 0<length<512:raise ValueError('Unbounded enum value getter')
# These leaf bodies contain only relative internal jumps and register/
# member operations; exact equality is stronger than a partial leaf mask.
raw=old.data(before,length);mask=bytes([255])*length
else:raw,mask,*_=m.signature(old,before,length)
actual=new.data(after,length)
if any(a!=b for a,b,keep in zip(raw,actual,mask) if keep):raise ValueError('Commodity slot changed')
paired=m.references(old,new,before,after,length);refs.extend(paired)
slots.append(dict(oldVtable=hex(table),newVtable=hex(tables[table]),slot=hex(offset),oldRva=hex(before),newRva=hex(after),bytes=length,
oldSha256=hashlib.sha256(raw).hexdigest(),newSha256=hashlib.sha256(actual).hexdigest(),references=[list(map(hex,r)) for r in paired]))
globals={}
for before in (0x5F05CE8,0x5F05C34,0x5F05E70,0x5F2F25C,0x5F7E844,0x5F05DB4):
candidates={r[1] for r in refs if r[0]==before}
if len(candidates)!=1:raise ValueError('Commodity dynamic ID binding ambiguous')
globals[hex(before)]=hex(candidates.pop())
getters=[]
for before in (0x1B3AC10,0x1B39630):
candidates={r[1] for r in refs if r[0]==before}
if len(candidates)!=1:raise ValueError('Commodity name/category getter call binding ambiguous')
after=candidates.pop()
length=next(i.address+i.size-old.base-before for i in old.instructions(before,512) if i.mnemonic=='ret')
raw,mask,*_=m.signature(old,before,length);actual=new.data(after,length)
if any(a!=b for a,b,keep in zip(raw,actual,mask) if keep):raise ValueError('Commodity name/category getter changed')
getters.append(dict(oldRva=hex(before),newRva=hex(after),bytes=length,oldSha256=hashlib.sha256(raw).hexdigest(),newSha256=hashlib.sha256(actual).hexdigest()))
output=dict(runtimeNeutral=True,canInstallHooks=False,gameCalls=0,networkRequests=0,
oldImage=dict(path=str(old.path),sha256=hashlib.sha256(old.raw).hexdigest()),
newImage=dict(path=str(new.path),sha256=hashlib.sha256(new.raw).hexdigest()),functions=functions,
vtables={hex(a):hex(b) for a,b in tables.items()},slots=slots,getters=getters,globals=globals)
path=Path('artifacts/galaxy-query-research/commodity-tree/compatibility.json');path.write_text(json.dumps(output,indent=2)+'\n')
print(json.dumps(dict(vtables=output['vtables'],globals=globals,functions=len(functions),slots=len(slots)),indent=2))