From 9e2d7ede3b761484a518290c812afb165d00a57e Mon Sep 17 00:00:00 2001 From: xXJsonDeruloXx Date: Thu, 1 Oct 2026 19:28:18 -0400 Subject: [PATCH] Run verified legacy PhysX prerequisite without its wizard --- .../usr/local/bin/bannerlator-steam-compat | 45 ++++++++++--- tools/tests/test_steam_redists.py | 63 +++++++++++++++++++ 2 files changed, 100 insertions(+), 8 deletions(-) diff --git a/tools/linuxfs/overlay/usr/local/bin/bannerlator-steam-compat b/tools/linuxfs/overlay/usr/local/bin/bannerlator-steam-compat index 34127b634..1cfdcd370 100755 --- a/tools/linuxfs/overlay/usr/local/bin/bannerlator-steam-compat +++ b/tools/linuxfs/overlay/usr/local/bin/bannerlator-steam-compat @@ -144,8 +144,8 @@ bl_game_cpus() { } """ -# Prepare Steam's transient evaluator input before Wine reads it. Only the old x86 IExpress -# redistributables are changed: their extracted MSI is run normally, without the stuck wrapper. +# Prepare Steam's transient evaluator input before Wine reads it. Recognized legacy VC bundles +# use their actual MSI; a verified legacy PhysX bundle uses its supported quiet argument. BL_INSTALLSCRIPT_SETUP = """ bl_installscript() { [ "${verb:-}" = run ] || return 0 @@ -528,6 +528,22 @@ def redist_msi(installer, cache_root): return None +# Mirror's Edge bundles Wise's PhysX 8.10.17 bootstrapper without arguments. /s is forwarded to +# msiexec and still opens its license wizard; /qn installs the actual runtime silently. Match the +# exact tested bundle, rather than guessing switches for other PhysX installer generations. +QUIET_PHYSX_SHA256 = '32d27a5e64e099adc4d3d2c87e5702f237c3d1292406f166059837c4c827e22f' + + +def quiet_physx(installer): + if os.path.getsize(installer) > 64 * 1024 * 1024 or not os.path.isfile(installer): + return False + digest = hashlib.sha256() + with open(installer, 'rb') as f: + for chunk in iter(lambda: f.read(65536), b''): + digest.update(chunk) + return digest.hexdigest() == QUIET_PHYSX_SHA256 + + def prepare_installscript(steam_root, script): # The input is Steam's temporary evaluator file, not a game's installed metadata. Restrict # resolution to that one directory so a Windows path or a traversal cannot rewrite other files. @@ -561,17 +577,18 @@ def prepare_installscript(steam_root, script): continue fields = dict(direct_entries(tokens, process)) exe_index, args_index = fields.get('process 1'), fields.get('command 1') - if exe_index is None or args_index is None: + if exe_index is None: continue match = re.fullmatch(r'%INSTALLDIR%[\\/]+(.+)', vdf_value(tokens[exe_index]), re.IGNORECASE) - args = vdf_value(tokens[args_index]).split() - if not match or not args or any(arg.lower() not in - ('/q:a', '/q', '/qn', '/quiet', '/norestart') for arg in args): + args = vdf_value(tokens[args_index]).split() if args_index is not None else [] + if not match: continue relative = re.split(r'[\\/]+', match[1]) - if any(part in ('', '.', '..') for part in relative) or not re.fullmatch( - r'vcredist_x86[^\\/]*\.exe', relative[-1], re.IGNORECASE): + physx = relative[-1].lower() == 'physx_systemsoftware.exe' and not args + vc = re.fullmatch(r'vcredist_x86[^\\/]*\.exe', relative[-1], re.IGNORECASE) + if any(part in ('', '.', '..') for part in relative) or not (physx or (vc and args and + all(arg.lower() in ('/q:a', '/q', '/qn', '/quiet', '/norestart') for arg in args))): continue try: installer = install_dir @@ -583,6 +600,18 @@ def prepare_installscript(steam_root, script): raise ValueError('ambiguous or missing redistributable path') part = matches[0] installer = os.path.join(installer, part) + if physx: + if not quiet_physx(installer): + continue + if args_index is None: + end = block_end(tokens, process) + tokens[end:end] = [vdf_quote('command 1'), vdf_quote('/qn')] + else: + tokens[args_index] = vdf_quote('/qn') + changed = True + print('bannerlator-steam-compat: running verified PhysX 8.10.17 quietly', + file=sys.stderr) + continue msi = redist_msi(installer, os.path.join(steam_root, 'legacycompat', '.droiddeck-redists')) except (OSError, ValueError, subprocess.SubprocessError) as e: diff --git a/tools/tests/test_steam_redists.py b/tools/tests/test_steam_redists.py index 8c955f676..ed89e7b07 100644 --- a/tools/tests/test_steam_redists.py +++ b/tools/tests/test_steam_redists.py @@ -1,4 +1,5 @@ from pathlib import Path +import hashlib import runpy import tempfile import unittest @@ -114,6 +115,68 @@ class SteamRedistTest(unittest.TestCase): self.assertTrue(Path(result).is_file()) self.assertFalse((Path(self.tmp.name) / 'outside').exists()) + def physx_fixture(self, command=None): + payload = b'verified Wise PhysX bundle fixture' + directory = self.installer_dir / 'PhysX' + directory.mkdir(exist_ok=True) + installer = directory / 'PhysX_SystemSoftware.exe' + installer.write_bytes(payload) + script = '''"evaluatorscript" { "0" { + "install_path" "%s" "compat_installscript" { "run process" { + "PhysX Version" { + "process 1" "%%INSTALLDIR%%\\\\physx\\\\PHYSX_SYSTEMSOFTWARE.EXE" + %s + "HasRunKey" "HKEY_LOCAL_MACHINE\\\\SOFTWARE\\\\AGEIA Technologies" + "MinimumHasRunValue" "81017" "NoCleanUp" "1" + } + "Other step" { "process 1" "%%INSTALLDIR%%\\\\other.exe" } + } } + } }''' % (self.installer_dir, '' if command is None else + '"command 1" "%s"' % command) + self.path.write_text(script) + return installer, hashlib.sha256(payload).hexdigest() + + def test_verified_physx_runs_quietly_and_preserves_steam_completion_checks(self): + for command in (None, ''): + installer, digest = self.physx_fixture(command) + with patch.dict(PREPARE.__globals__, QUIET_PHYSX_SHA256=digest): + self.assertTrue(self.prepare()) + tokens = COMPAT['tokenize'](self.path.read_text()) + decoded = [COMPAT['vdf_value'](t) for t in tokens if t.startswith('"')] + self.assertIn('/qn', decoded) + self.assertIn(r'%INSTALLDIR%\physx\PHYSX_SYSTEMSOFTWARE.EXE', decoded) + self.assertIn('81017', decoded) + self.assertIn(r'HKEY_LOCAL_MACHINE\SOFTWARE\AGEIA Technologies', decoded) + self.assertIn('Other step', decoded) + before = self.path.read_bytes() + self.assertFalse(self.prepare()) + self.assertEqual(before, self.path.read_bytes()) + + def test_physx_other_versions_and_existing_commands_are_untouched(self): + installer, digest = self.physx_fixture() + before = self.path.read_bytes() + self.assertFalse(self.prepare()) # Fixture is not the real bundle's hash. + self.assertEqual(before, self.path.read_bytes()) + for command in ('/uninstall', '/custom', '/s', '/qn'): + self.physx_fixture(command) + before = self.path.read_bytes() + with patch.dict(PREPARE.__globals__, QUIET_PHYSX_SHA256=digest): + self.assertFalse(self.prepare()) + self.assertEqual(before, self.path.read_bytes()) + + def test_physx_missing_or_traversing_installer_keeps_original(self): + installer, digest = self.physx_fixture() + installer.unlink() + before = self.path.read_bytes() + self.assertFalse(self.prepare()) + self.assertEqual(before, self.path.read_bytes()) + self.physx_fixture() + self.path.write_text(self.path.read_text().replace('physx\\\\', '..\\\\')) + before = self.path.read_bytes() + with patch.dict(PREPARE.__globals__, QUIET_PHYSX_SHA256=digest): + self.assertFalse(self.prepare()) + self.assertEqual(before, self.path.read_bytes()) + if __name__ == '__main__': unittest.main()