--- /dev/null +++ b/tools/resource_references.py @@ -0,0 +1,31 @@ +"""Hard resource references, with one fail-closed audited optional identity test. + +This is not a general Enforce parser or a new external dependency allowlist. +Changing any byte of the audited source requires reviewing this exception again. +""" +import hashlib +import re + +OPTIONAL_FILE = 'addons/core/Scripts/Game/ERR/00_Core/ERR_ReaperRHSCompat.c' +OPTIONAL_SHA256 = 'bcdf98c05ac6cdf0c7eaa854531b02e9ee4f53fbb346ae01d58114e572e7dd4f' +OPTIONAL_RESOURCE = '{574AA64002B8E308}Prefabs/Characters/REAPER_SpectatorCameraHelper.et' +OPTIONAL_METHOD = ''' protected static bool CameraSlot(BaseContainer slot) + { + if (!slot || slot.GetName() != "SpectatorCameraHelper") return false; + ResourceName prefab; + if (!slot.Get("Prefab", prefab)) return false; + return prefab == "{574AA64002B8E308}Prefabs/Characters/REAPER_SpectatorCameraHelper.et"; + }''' + + +def hard_resource_references(relative_path, text): + """Exclude only the pinned comparison occurrence; keep all other checks.""" + identity_offset = -1 + if (str(relative_path) == OPTIONAL_FILE + and hashlib.sha256(text.encode('utf-8')).hexdigest() == OPTIONAL_SHA256 + and text.count(OPTIONAL_METHOD) == 1 + and text.count(OPTIONAL_RESOURCE) == 1): + identity_offset = text.index(OPTIONAL_METHOD) + OPTIONAL_METHOD.index(OPTIONAL_RESOURCE) + for match in re.finditer(r'\{([A-F0-9]{16})\}([^"\n]+)', text): + if match.start() != identity_offset: + yield match.groups() --- a/tools/check.py +++ b/tools/check.py @@ -2,6 +2,7 @@ """Validate addon/resource isolation and execute policy regressions. Not an engine compiler.""" from pathlib import Path import json,re,subprocess,sys +from resource_references import hard_resource_references from external_addons import policy, ALLOWED, direct_for, resource_allowed from prefab_identity import check_prefabs ROOT=Path(__file__).resolve().parents[1] @@ -65,7 +66,7 @@ if not p.is_file() or p.suffix not in ['.c','.et','.conf','.ent','.layer','.layout','.emat','.bt']:continue s=p.read_text() assert not re.search(r'\b(?:ESCT\w*|SCRTC_\w+|SCRT_\w+|EAI_\w+)\b',s),f'Original ecosystem dependency: {p}' - for g,path in re.findall(r'\{([A-F0-9]{16})\}([^"\n]+)',s): + for g,path in hard_resource_references(p.relative_to(ROOT).as_posix(),s): assert resources.get(g)==path or vanilla.get(path)==g or resource_allowed(external,visibility[p.relative_to(ROOT/'addons').parts[0]],g,path),f'Unresolved/mismatched resource in {p}: {g} {path}' if g in owners: addon=p.relative_to(ROOT/'addons').parts[0] --- a/tools/source_bundle.py +++ b/tools/source_bundle.py @@ -16,6 +16,7 @@ from source_manifest import addon_manifest from project_schema import parse_project +from resource_references import hard_resource_references from external_addons import policy as external_policy, direct_for, resource_allowed, ALLOWED from release_check import audit, read_json, evidence_path @@ -151,7 +152,7 @@ owner = name.split('/')[1] visible = {owner, *PROJECTS[owner][2]} if 'core' in visible: visible.update(ALLOWED) - for guid, resource in re.findall(r'\{([A-F0-9]{16})\}([^"\n]+)', text): + for guid, resource in hard_resource_references(name, text): if guid in resources: path, target = resources[guid] if path != resource or target not in visible: --- a/tests/test_external_addons.py +++ b/tests/test_external_addons.py @@ -1,5 +1,9 @@ #!/usr/bin/env python3 """Exercise exact dependency closure, resource ownership and isolated profile inputs.""" +import ast +import importlib.util +import os +import re import copy import hashlib import json @@ -9,8 +13,11 @@ import tempfile import unittest sys.path.insert(0,str(Path(__file__).resolve().parents[1]/'tools')) +TOOLS=Path(os.environ.get('ERR_OPTIONAL_IDENTITY_TOOLS',Path(__file__).resolve().parents[1]/'tools')) +sys.path.insert(0,str(TOOLS)) +import resource_references as refs import external_addons as ext -ROOT=Path(__file__).resolve().parents[1] +ROOT=Path(os.environ.get('ERR_REPO',Path(__file__).resolve().parents[1])) class ExternalAddonsTests(unittest.TestCase): def setUp(self): @@ -69,4 +76,68 @@ pak=self.source/ext.DIRECT[0]/'data.pak';pak.unlink();pak.symlink_to(self.source/ext.DIRECT[1]/'data.pak') with self.assertRaisesRegex(ValueError,'regular file'):ext.installed(self.source,self.value) + +class OptionalIdentityTests(unittest.TestCase): + def setUp(self): + self.name=refs.OPTIONAL_FILE + self.text=(ROOT/self.name).read_text() + self.guid='574AA64002B8E308';self.resource='Prefabs/Characters/REAPER_SpectatorCameraHelper.et' + def rejected_reference(self,name,text): + self.assertIn((self.guid,self.resource),list(refs.hard_resource_references(name,text))) + def test_exact_comparison_only_and_every_other_reference_retained(self): + all_refs=re.findall(r'\{([A-F0-9]{16})\}([^"\n]+)',self.text) + expected=[pair for pair in all_refs if pair!=(self.guid,self.resource)] + self.assertEqual(list(refs.hard_resource_references(self.name,self.text)),expected) + self.assertIn(('37578B1666981FCE','Prefabs/Characters/Core/Character_Base.et'),expected) + self.rejected_reference('addons/core/Scripts/Game/Other.c',self.text) + for changed in [ + self.text+'\n', # even a benign source edit needs renewed audit + self.text.replace('return prefab ==', 'Resource.Load(prefab); return prefab =='), + self.text.replace('return prefab ==', 'return Resource.Load(').replace(refs.OPTIONAL_RESOURCE+'";',refs.OPTIONAL_RESOURCE+'");'), + self.text+'\nResource.Load("'+refs.OPTIONAL_RESOURCE+'");', + self.text+'\nSpawn("'+refs.OPTIONAL_RESOURCE+'");', + self.text+'\nResourceName extra = "'+refs.OPTIONAL_RESOURCE+'";', + self.text.replace(refs.OPTIONAL_METHOD,'/*'+refs.OPTIONAL_METHOD+'*/'), + self.text.replace('CameraSlot(BaseContainer slot)','DifferentMethod(BaseContainer slot)'), + self.text.replace('if (!ModsPresent()) return false;',''), + ]: self.rejected_reference(self.name,changed) + unknown=self.text.replace(self.guid,'574AA64002B8E309') + self.assertIn(('574AA64002B8E309',self.resource),list(refs.hard_resource_references(self.name,unknown))) + # The optional addon never enters either resource policy or dependency closure. + value=ext.policy(ROOT) + self.assertNotIn('5EB139459EBF5C16',ext.ALLOWED) + self.assertFalse(ext.resource_allowed(value,set(ext.ALLOWED),self.guid,self.resource)) + def repository_resource_loop(self,name,text): + # Execute the actual dependency-scanner loop without running the full suite. + source=Path(os.environ.get('ERR_OPTIONAL_CHECK_SOURCE',TOOLS/'check.py')).read_text() + tree=ast.parse(source) + loops=[node for node in ast.walk(tree) if isinstance(node,ast.For) and isinstance(node.target,ast.Tuple) + and [getattr(n,'id',None) for n in node.target.elts]==['g','path']] + self.assertEqual(len(loops),1) + code=compile(ast.fix_missing_locations(ast.Module(body=loops,type_ignores=[])),'actual_repository_resource_loop','exec') + env=dict(re=re,hard_resource_references=refs.hard_resource_references,p=ROOT/name,ROOT=ROOT,s=text, + resources={},owners={},vanilla=json.loads((ROOT/'docs/vanilla-resources.json').read_text()), + external=ext.policy(ROOT),visibility={'core':{'core',*ext.ALLOWED}},resource_allowed=ext.resource_allowed) + exec(code,env) + def test_actual_repository_resource_loop(self): + self.repository_resource_loop(self.name,self.text) + for name,text in [(self.name,self.text+'\nResource.Load("'+refs.OPTIONAL_RESOURCE+'");'), + ('addons/core/Scripts/Game/Other.c',self.text), + (self.name,self.text.replace('return prefab ==','return Resource.Load(').replace(refs.OPTIONAL_RESOURCE+'";',refs.OPTIONAL_RESOURCE+'");'))]: + with self.assertRaisesRegex(AssertionError,'Unresolved/mismatched'): + self.repository_resource_loop(name,text) + def test_actual_bundle_validator(self): + path=Path(os.environ.get('ERR_OPTIONAL_BUNDLE_SOURCE',TOOLS/'source_bundle.py')) + spec=importlib.util.spec_from_file_location('optional_test_source_bundle',path) + bundle=importlib.util.module_from_spec(spec);spec.loader.exec_module(bundle) + payload={name:(ROOT/name).read_bytes() for name in bundle.inventory(ROOT)} + before=ext.policy(ROOT) + bundle.validate(payload,ROOT) + self.assertEqual(before,ext.policy(ROOT)) + for changed in [self.text+'\nResource.Load("'+refs.OPTIONAL_RESOURCE+'");', + self.text+'\nSpawn("'+refs.OPTIONAL_RESOURCE+'");']: + altered=dict(payload);altered[self.name]=changed.encode() + with self.assertRaisesRegex(ValueError,'unresolved or nonvanilla'): + bundle.validate(altered,ROOT) + if __name__=='__main__':unittest.main() --- a/docs/vanilla-resources.json +++ b/docs/vanilla-resources.json @@ -48,6 +48,7 @@ "Prefabs/AI/Waypoints/AIWaypoint_Move.et": "750A8D1695BD6998", "Prefabs/AI/Waypoints/AIWaypoint_SearchAndDestroy.et": "B3E7B8DC2BAB8ACC", "Prefabs/Characters/Campaign/Final/OPFOR/USSR_Army/Regular/Campaign_USSR_Player_MortarCrew.et": "86E565387A634103", + "Prefabs/Characters/Core/Character_Base.et": "37578B1666981FCE", "Prefabs/Characters/Factions/BLUFOR/US_Army/Character_US_Base.et": "836E7E39AAC5888B", "Prefabs/Characters/Factions/BLUFOR/US_Army/Character_US_HeliPilot.et": "42A502E3BB727CEB", "Prefabs/Characters/Factions/BLUFOR/US_Army/Character_US_Sniper.et": "0F6689B491641155",