Integrate physical radio calls and finite vehicle maintenance
This commit is contained in:
@@ -0,0 +1,191 @@
|
||||
"""Explicit v3 DEVELOPMENT packed Chat consumption; v1/v2 remain unchanged.
|
||||
Only the audited installed external closure is linked, never copied or published.
|
||||
"""
|
||||
import hashlib
|
||||
import json
|
||||
from pathlib import Path
|
||||
import re
|
||||
import shutil
|
||||
import time
|
||||
import uuid
|
||||
import external_addons as external
|
||||
from project_schema import parse_project
|
||||
|
||||
PROJECTS = {
|
||||
'core': ('ERR_Core', '2528466DE220D91C', ()),
|
||||
'ai': ('ERR_Ai', '5192138CB9D5B911', ('core',)),
|
||||
'intel': ('ERR_Intel', 'D31039C7C53E348A', ('core',)),
|
||||
'map': ('ERR_Map', '9AAEE9041D79809E', ('core',)),
|
||||
'chat': ('ERR_Chat', 'FA804F411BDDF2C6', ('core','ai','intel','map')),
|
||||
}
|
||||
FILES = {'addon.gproj','data.pak','resourceDatabase.rdb'}
|
||||
MANIFEST = 'packed-input-provenance.json'
|
||||
|
||||
def require(condition, message):
|
||||
if not condition: raise ValueError(message)
|
||||
|
||||
def digest(path):
|
||||
require(path.is_file() and not path.is_symlink(),'Expected regular file: '+str(path))
|
||||
before=path.stat(); h=hashlib.sha256()
|
||||
with path.open('rb') as stream:
|
||||
for block in iter(lambda:stream.read(1024*1024),b''): h.update(block)
|
||||
after=path.stat()
|
||||
require((before.st_dev,before.st_ino,before.st_size,before.st_mtime_ns,before.st_ctime_ns)==(after.st_dev,after.st_ino,after.st_size,after.st_mtime_ns,after.st_ctime_ns),'Input changed while hashing: '+str(path))
|
||||
return h.hexdigest()
|
||||
|
||||
def inventory(folder):
|
||||
folder=Path(folder)
|
||||
require(folder.is_dir() and not folder.is_symlink() and folder==folder.resolve(),'Packed v3 closure must be a canonical real directory')
|
||||
require({p.name for p in folder.iterdir()}==set(PROJECTS),'Packed v3 requires exactly five successor addon folders')
|
||||
records={}
|
||||
for key,(name,guid,deps) in PROJECTS.items():
|
||||
addon=folder/key
|
||||
require(addon.is_dir() and not addon.is_symlink(),'Packed addon must be a real directory')
|
||||
require({p.name for p in addon.iterdir()}==FILES,'Packed addon requires exactly three runtime files')
|
||||
for filename in sorted(FILES):
|
||||
path=addon/filename;sha=digest(path)
|
||||
require(path.stat().st_size>0,'Empty packed input: '+str(path))
|
||||
records[key+'/'+filename]={'bytes':path.stat().st_size,'sha256':sha}
|
||||
descriptor=addon/'addon.gproj';text=descriptor.read_bytes().decode()
|
||||
require(hashlib.sha256(text.encode()).hexdigest()==records[key+'/addon.gproj']['sha256'],'Project changed during parsing')
|
||||
parsed=parse_project(text,allow_core_menu=key=='core')
|
||||
actual=re.findall(r'"([A-F0-9]{16})"',parsed[4]) if parsed else []
|
||||
expected={'58D0FB3206B6F859',*(PROJECTS[d][1] for d in deps),*external.direct_for(key)}
|
||||
require(parsed and parsed[1]==name and parsed[2]==guid and set(actual)==expected and len(actual)==len(expected),'Packed v3 exact project identity/dependency mismatch: '+key)
|
||||
return records
|
||||
|
||||
def request(args,root):
|
||||
require(args.project=='chat' and getattr(args,'packed_addons',None) is not None and getattr(args,'packed_core',None) is None,'--external-addons requires --project chat --packed-addons (explicit packed v3)')
|
||||
require(args.profile==Path('/tmp/erri-game-profile') and args.settle==15,'Packed v3 owns a new private profile and requires 15-second settling')
|
||||
session=getattr(args,'packed_session',None)
|
||||
require(session is not None,'Packed v3 requires --packed-session')
|
||||
session=Path(session)
|
||||
require(session.is_absolute() and session.parent==Path('/tmp') and session==session.resolve() and re.fullmatch(r'erri-packed-chat-[A-Za-z0-9_-]+',session.name) and not session.exists() and not session.is_symlink(),'Packed v3 requires a new canonical /tmp/erri-packed-chat-<name> session')
|
||||
candidate=Path(args.packed_addons);artifacts=inventory(candidate)
|
||||
policy_path=Path(root)/'docs/external-addons.json';policy_sha=digest(policy_path);rules=external.policy(root)
|
||||
require(digest(policy_path)==policy_sha,'External policy changed during parsing')
|
||||
cache={};source=Path(args.external_addons).resolve();records=external.installed(source,rules,cache)
|
||||
names={}
|
||||
for guid,record in records.items():
|
||||
descriptor=Path(record['path'])/'addon.gproj';data=descriptor.read_bytes()
|
||||
require(hashlib.sha256(data).hexdigest()==record['files']['addon.gproj']['sha256'],'External project changed while parsing its mount name')
|
||||
match=re.search(r'\bID\s+"([A-Za-z0-9_-]+)"',data.decode())
|
||||
require(match and match[1] not in {'core','ArmaReforger',*(p[0] for p in PROJECTS.values()),*names.values()},'Invalid/duplicate external project mount name')
|
||||
names[guid]=match[1]
|
||||
return {'schema_version':3,'project':'chat','candidate':candidate,'session':session,'input':session/'input/addons','manifest':artifacts,'external_source':source,'policy_path':policy_path,'policy_sha256':policy_sha,'policy':rules,'external_records':records,'external_names':names,'external_cache':cache}
|
||||
|
||||
def verify_external(packed,profile=None):
|
||||
require(digest(packed['policy_path'])==packed['policy_sha256'],'External policy changed')
|
||||
cache=packed['external_cache']
|
||||
if 'cache_path' in packed:
|
||||
require(digest(packed['cache_path'])==packed['cache_sha256'],'External fingerprint cache changed')
|
||||
cache=json.loads(packed['cache_path'].read_text())
|
||||
require(external.installed(packed['external_source'],packed['policy'],cache)==packed['external_records'],'External packed inputs changed')
|
||||
if profile is not None:
|
||||
require(profile.is_dir() and not profile.is_symlink() and profile==profile.resolve(),'Private runtime profile redirected')
|
||||
addons=profile/'addons'
|
||||
require(addons.is_dir() and not addons.is_symlink(),'Private dependency directory changed')
|
||||
expected={'ArmaReforger',*('ERRI_'+g for g in packed['external_records'])}
|
||||
require({p.name for p in addons.iterdir()}-{'saves'}==expected,'Missing/unrelated private addon link')
|
||||
require(not (addons/'saves').is_symlink(),'Redirected private saves directory')
|
||||
if (addons/'saves').exists():
|
||||
require((addons/'saves').is_dir() and not any((addons/'saves').rglob('*.gproj')),'Unexpected addon in private saves')
|
||||
vanilla=addons/'ArmaReforger'
|
||||
require(vanilla.is_symlink() and vanilla.resolve()==packed['vanilla'].resolve(),'Private vanilla link changed')
|
||||
require(external.verify_profile(packed['external_records'],packed['external_source'],packed['policy'],packed['cache_path'],addons),'External private profile changed')
|
||||
return True
|
||||
|
||||
def prepare(packed):
|
||||
verify_external(packed)
|
||||
require(inventory(packed['candidate'])==packed['manifest'],'Packed candidate changed before copy')
|
||||
packed['session'].mkdir(exist_ok=False);packed['input'].mkdir(parents=True,exist_ok=False)
|
||||
for name in packed['manifest']:
|
||||
target=packed['input']/name;target.parent.mkdir(parents=True,exist_ok=True)
|
||||
with (packed['candidate']/name).open('rb') as src,target.open('xb') as dst:shutil.copyfileobj(src,dst)
|
||||
require(inventory(packed['input'])==packed['manifest'] and inventory(packed['candidate'])==packed['manifest'],'Packed input changed while copying')
|
||||
packed['cache_path']=packed['session']/'external-source-cache.json'
|
||||
packed['cache_path'].write_text(json.dumps(packed['external_cache'],sort_keys=True))
|
||||
packed['cache_sha256']=digest(packed['cache_path'])
|
||||
(packed['session']/'external-addons-policy.json').write_text(json.dumps(packed['policy'],sort_keys=True))
|
||||
(packed['session']/'external-addons-inputs.json').write_text(json.dumps(packed['external_records'],sort_keys=True))
|
||||
|
||||
def prepare_profile(packed,steam,profile):
|
||||
verify_external(packed)
|
||||
packed['vanilla']=steam/'steamapps/common/Arma Reforger/addons/data'
|
||||
records=external.prepare_profile(profile/'addons',packed['external_source'],packed['policy'],packed['cache_path'])
|
||||
require(records==packed['external_records'],'External inputs changed while preparing private links')
|
||||
packed['cache_sha256']=digest(packed['cache_path'])
|
||||
packed['profile']=profile
|
||||
verify_external(packed,profile)
|
||||
|
||||
def snapshot(packed,command,profile):
|
||||
verify_external(packed,profile)
|
||||
require(inventory(packed['input'])==packed['manifest'] and inventory(packed['candidate'])==packed['manifest'],'Packed inputs changed before launch')
|
||||
value={'schema_version':3,'kind':'packed-chat-external-runtime-input','project':'chat','addon_order':list(PROJECTS),'launch_id':str(uuid.uuid4()),'captured_unix':time.time(),'candidate_directory':str(packed['candidate']),'runtime_directory':str(packed['input']),'profile':str(profile),'command':list(command),'artifacts':packed['manifest'],'external_policy_sha256':packed['policy_sha256'],'external_policy':packed['policy'],'external_addons':packed['external_records'],'external_project_names':packed['external_names'],'build_qualification':'DEVELOPMENT unqualified candidate; original packaging verdict unchanged'}
|
||||
raw=(json.dumps(value,indent=2)+'\n').encode();(packed['session']/MANIFEST).write_bytes(raw)
|
||||
return raw
|
||||
|
||||
def seal(packed,raw,session):
|
||||
value=json.loads(raw);unchanged=False;error=None
|
||||
try:
|
||||
require(value['schema_version']==3 and value['kind']=='packed-chat-external-runtime-input','Wrong packed v3 provenance schema')
|
||||
require(value['external_addons']==packed['external_records'] and value['external_policy_sha256']==packed['policy_sha256'] and value['external_policy']==packed['policy'] and value['external_project_names']==packed['external_names'],'External provenance mismatch')
|
||||
verify_external(packed,packed['profile'])
|
||||
require(inventory(packed['input'])==value['artifacts'] and inventory(packed['candidate'])==value['artifacts'],'Packed inputs changed after launch')
|
||||
require((packed['session']/MANIFEST).read_bytes()==raw,'Session packed provenance changed')
|
||||
if session:require((session/MANIFEST).read_bytes()==raw,'Native session packed provenance changed')
|
||||
unchanged=True
|
||||
except (OSError,ValueError,KeyError) as caught:error=str(caught)
|
||||
console=session/'console.log' if session else None
|
||||
return {'packed_only':True,'packed_schema_version':3,'packed_input_provenance':MANIFEST,'packed_input_provenance_sha256':hashlib.sha256(raw).hexdigest(),'packed_launch_id':value['launch_id'],'stopped_unix':time.time(),'packed_inputs_unchanged_after_run':unchanged,'packed_external_inputs_unchanged_after_run':unchanged,'packed_input_error':error,'external_addon_count':len(packed['external_records']),'console_sha256':digest(console) if console and console.is_file() else None,'build_qualified':False,'release_qualified':False}
|
||||
|
||||
def evidence(content,packed,steam,profile,legacy):
|
||||
report=legacy.game_evidence(content,'chat');game=steam/'steamapps/common/Arma Reforger'
|
||||
def normalized(path):return legacy.native_path(path,game)
|
||||
# Permit only each exact private alias and its captured canonical target, never
|
||||
# an arbitrary workshop path or generic symlink/path normalization.
|
||||
paths={};mounts={};counts={};databases={}
|
||||
def add(guid,name,folders,project_file,pak_count=None):
|
||||
paths[guid]={str(p/project_file) for p in folders}
|
||||
mounts[name]={str(p) for p in folders};databases[name]={str(p/'resourceDatabase.rdb') for p in folders};counts[name]=pak_count
|
||||
add('5614BBCCBB55ED1C','core',[game/'addons/core'],'core.gproj')
|
||||
add('58D0FB3206B6F859','ArmaReforger',[game/'addons/data',profile/'addons/ArmaReforger'],'ArmaReforger.gproj')
|
||||
for key,(name,guid,_) in PROJECTS.items():add(guid,name,[packed['input']/key],'addon.gproj',1)
|
||||
for guid,record in packed['external_records'].items():
|
||||
add(guid,packed['external_names'][guid],[profile/'addons'/('ERRI_'+guid),Path(record['path'])],'addon.gproj',sum(name.endswith('.pak') for name in record['files']))
|
||||
violations=[];complete=False
|
||||
for block in re.findall(r"Loaded addons:\r?\n((?:[^\n]*ENGINE[^\n]*gproj:[^\n]*\n)+)",content):
|
||||
entries=re.findall(r"gproj: '([^']+)' guid: '([0-9A-Fa-f]{16})'",block);seen=set()
|
||||
if len(entries)!=len(block.splitlines()):violations.append('Malformed loaded-addon entry')
|
||||
for path,guid in entries:
|
||||
guid=guid.upper()
|
||||
if guid in seen or guid not in paths or normalized(path) not in paths.get(guid,set()):violations.append('Unexpected/redirected/duplicate loaded addon: '+path+' '+guid)
|
||||
seen.add(guid)
|
||||
complete=seen==set(paths)
|
||||
mounted=set()
|
||||
for path,count,name in re.findall(r"FileSystem: Adding package '([^']+)' \(pak count: (\d+)\) to filesystem under name ([^\s]+)",content):
|
||||
if name not in mounts or normalized(path) not in mounts.get(name,set()) or int(count)<1 or (counts.get(name) is not None and int(count)!=counts[name]):violations.append('Unexpected packed mount/count: '+name+' '+path)
|
||||
else:mounted.add(name)
|
||||
for path,name in re.findall(r"FileSystem: Adding relative directory '([^']+)' to filesystem under name ([^\s]+)",content):
|
||||
resolved=normalized(path)
|
||||
if name=='profile' and resolved==str(profile/'profile'):continue
|
||||
if name=='logs' and re.fullmatch(re.escape(str(profile/'logs'))+r'/logs_[0-9_-]+',resolved):continue
|
||||
violations.append('Unexpected loose directory mount: '+name+' '+path)
|
||||
allowed_dirs={str(packed['input']),str(game/'addons'),str(profile/'addons')}
|
||||
dirs=re.findall(r"ENGINE\s*:\s*dir: '([^']+)'",content)
|
||||
for path in dirs:
|
||||
resolved=str(profile/'addons') if path=='$profile:../addons/' else normalized(path)
|
||||
if resolved not in allowed_dirs:violations.append('Unexpected addon search directory: '+path)
|
||||
loaded_databases=set()
|
||||
for name,path in re.findall(r'ResourceDB: loading cache \(id=\d+ name=([^\s]+) path=([^\r\n]+)\)',content):
|
||||
resolved=normalized(path)
|
||||
if name=='profile' and resolved==str(profile/'profile/resourceDatabase.rdb'):continue
|
||||
if name=='logs' and re.fullmatch(re.escape(str(profile/'logs'))+r'/logs_[0-9_-]+/resourceDatabase\.rdb',resolved):continue
|
||||
if name not in databases or resolved not in databases.get(name,set()):violations.append('Unexpected resource database: '+name+' '+path)
|
||||
else:loaded_databases.add(name)
|
||||
modules=re.findall(r'Module: (GameLib|Game);[^\n]*defines:\s*"([^"]*)"',content)
|
||||
normal={name for name,_ in modules}=={'GameLib','Game'} and all(not {'WORKBENCH','ENF_WB'}.intersection(flags.split(',')) for _,flags in modules)
|
||||
if any({'WORKBENCH','ENF_WB'}.intersection(flags.split(',')) for _,flags in modules):violations.append('Workbench defines in packed ordinary runtime')
|
||||
path_proof=complete and mounted==set(mounts) and loaded_databases==set(databases) and bool(dirs)
|
||||
failed=report['failed'] or bool(violations)
|
||||
return {**report,'packed_only':True,'packed_schema_version':3,'external_addon_count':len(packed['external_records']),'packed_loaded_paths_verified':path_proof,'packed_input_violations':violations,'packed_normal_modules':normal,'failed':failed,'ready':report['ready'] and path_proof and normal and not failed,'build_qualified':False,'release_qualified':False,'scope':'DEVELOPMENT packed Chat plus exact audited external closure; ordinary local startup only, no clean-build, human escape, multiplayer, FPS or release qualification'}
|
||||
Reference in New Issue
Block a user