Files
Felipe Prado 226fb0e969 Refactor(Branding): renamed the project to SpartanPort (#755)
EN-US

- Renamed code, binaries, packaging, CI and docs to SpartanPort, with the SP monogram and the SpartanPort logo
- Renamed only the names in the shadPS4 core and the FSR patch, keeping authors, dates and licenses
- Added scripts/legacy.py, the only place with the former name, kept for three releases
- Added a one-time copy of the game settings, the launcher settings and the Linux data folder from their former names, without overwriting or deleting anything
- Updated the Linux data move to leave the GPU caches (user/cache) and the run views (out/) in the former folder
- Added forwarding of the former GOW3_ variables to SPARTAN_
- Added the launcher under its former names, so the 2.5.0 updater and existing shortcuts keep working
- Added a note to the READMEs that the project was gow3_pc and that 2.5.0 players keep their saves and settings
- Passed the full Python suite (531 tests, 0 failures) and the Windows zip and Linux AppImage CI builds

---

PT-BR

- Renomeados código, executáveis, empacotamento, CI e docs para SpartanPort, com o monograma SP e o logotipo SpartanPort
- Renomeados só os nomes no núcleo do shadPS4 e no patch do FSR, mantendo autores, datas e licenças
- Adicionado o scripts/legacy.py, único lugar com o nome antigo, mantido por três releases
- Adicionada a cópia única das configurações do jogo, das configurações do launcher e da pasta de dados do Linux a partir dos nomes antigos, sem sobrescrever nem apagar nada
- Atualizada a migração da pasta de dados do Linux para deixar os caches da GPU (user/cache) e as pastas de execução (out/) na pasta antiga
- Adicionado o repasse das variáveis GOW3_ antigas para SPARTAN_
- Adicionado o launcher com os nomes antigos, para que o atualizador da 2.5.0 e os atalhos existentes continuem funcionando
- Adicionada aos READMEs a nota de que o projeto era o gow3_pc e de que jogadores da 2.5.0 mantêm saves e configurações
- Aprovadas a suíte Python completa (531 testes, 0 falhas) e as builds de CI do zip do Windows e da AppImage do Linux
2026-10-19 18:18:13 -03:00

194 lines
9.7 KiB
Python

"""Boundary tests for the native loader; uses tiny synthetic x86-64 images."""
from paths import ROOT
from pathlib import Path
import os
import struct
import subprocess
import sys
import tempfile
import unittest
EXE = ROOT / 'out/spartanport-probe'
HOST_EXE = ROOT / ('out/spartanport-probe' + ('.exe' if sys.platform == 'win32' else ''))
def package(code, relocs=(), names=(), capabilities=None):
image = code.ljust(4096, b'\0')
header = struct.pack('<8sQQQQQ', b'G3PROBE1' if capabilities is None else b'G3PROBE2', len(image), 0, 1, len(relocs), len(names))
if capabilities is not None:
header += struct.pack('<Q', capabilities)
segment = struct.pack('<QQQ', 0, len(image), 5)
return (header + segment + b''.join(n.encode().ljust(128, b'\0') for n in names)
+ b''.join(struct.pack('<QQqq', *r) for r in relocs) + image)
def native_package(name='fixture-native', binding_address=4112, binding_kind=1,
lib_flags=5, init=b'\x31\xc0\xc3', native=b'\xc3', metadata=None):
# Main calls an import then tail-jumps to the terminal diagnostic import.
code = b'\x48\x83\xec\x08\xff\x15\x16\0\0\0\x48\x83\xc4\x08\xff\x25\x14\0\0\0'
image = code.ljust(4096,b'\0') + init.ljust(16,b'\0') + native
image = image.ljust(12288,b'\0')
header=struct.pack('<8s6Q',b'G3PROBE3',len(image),0,3,2,2,1)
meta=metadata or (4096,8192,4096,8192,32,4,1,256)
return (header + struct.pack('<8Q',*meta) + struct.pack('<3Q',0,binding_address,binding_kind)
+ struct.pack('<9Q',0,4096,5,4096,4096,lib_flags,8192,4096,6)
+ name.encode().ljust(128,b'\0') + b'after-native'.ljust(128,b'\0')
+ struct.pack('<8Q',32,1,0,0,40,1,1,0) + image)
@unittest.skipUnless(EXE.exists(), 'build native probe first')
class LoaderTests(unittest.TestCase):
def test_invalid_content_profile_is_rejected(self):
with tempfile.TemporaryDirectory() as tmp:
path=Path(tmp)/'content.bin'
for data in (b'G3CONT01',struct.pack('<8s5I',b'G3CONT01',2,0,0,0,0),
struct.pack('<8s5I',b'G3CONT01',3,0,0,0,0)+b'extra'):
with self.subTest(data=data):
path.write_bytes(data)
r=self.run_image(package(b'\xc3'),'--content-profile',str(path))
self.assertEqual(r.returncode,1,r.stdout+r.stderr)
self.assertIn('content profile',r.stderr)
def run_image(self, data, *options):
with tempfile.TemporaryDirectory() as tmp:
path = Path(tmp) / 'boot.bin'
path.write_bytes(data)
return subprocess.run([str(EXE.resolve()), str(path), '--cpu-only', *options],
capture_output=True, text=True, timeout=5)
def test_original_instruction_reaches_named_import(self):
# jmp [rip+2]; two padding bytes; relocated function pointer at +8
r = self.run_image(package(b'\xff\x25\x02\0\0\0\x90\x90', [(8, 1, 0, 0)], ['fixture-import']))
self.assertEqual(r.returncode, 20, r.stderr)
self.assertIn('first unsupported PS4 import: fixture-import', r.stdout)
def test_native_initializer_and_export_return(self):
r=self.run_image(native_package())
self.assertEqual(r.returncode,20,r.stdout+r.stderr)
self.assertIn('Module 0 initializer returned 0',r.stdout)
self.assertIn('first unsupported PS4 import: after-native',r.stdout)
def test_host_contract_takes_priority_over_native_export(self):
r=self.run_image(native_package(name='bzQExy189ZI#libc',native=b'\x0f\x0b'))
self.assertEqual(r.returncode,20,r.stdout+r.stderr)
self.assertIn('_init_env returned',r.stdout)
self.assertIn('first unsupported PS4 import: after-native',r.stdout)
def test_strict_mode_skips_native_initialization_and_binding(self):
r=self.run_image(native_package(init=b'\x0f\x0b'), '--strict-imports')
self.assertEqual(r.returncode,20,r.stdout+r.stderr)
self.assertIn('first unsupported PS4 import: fixture-native',r.stdout)
self.assertNotIn('Starting native libc',r.stdout)
def test_native_metadata_and_bindings_are_validated(self):
for kwargs,message in [
({'binding_address':12288},'invalid native binding'),
({'binding_kind':2},'native export kind/range mismatch'),
({'lib_flags':4},'native function is not executable'),
({'metadata':(4096,8192,4096,8192,32,33,1,256)},'invalid linked module metadata'),
({'metadata':(4096,8192,4096,8192,32,4,1,12280)},'unmapped procparam'),
]:
with self.subTest(kwargs=kwargs):
r=self.run_image(native_package(**kwargs))
self.assertEqual(r.returncode,1,r.stdout+r.stderr)
self.assertIn(message,r.stderr)
def test_failed_native_initializer_does_not_enter_game(self):
r=self.run_image(native_package(init=b'\xb8\x01\0\0\0\xc3'))
self.assertEqual(r.returncode,1,r.stdout+r.stderr)
self.assertIn('module initializer failed',r.stderr)
self.assertNotIn('Entering original',r.stdout)
def test_base_relative_address_is_relocated(self):
# jump via a relative relocation to code at +16, then to an import at +24
code = b'\xff\x25\x02\0\0\0\x90\x90' + b'\0'*8 + b'\xff\x25\x02\0\0\0\x90\x90'
r = self.run_image(package(code, [(8, 0, 16, 0), (24, 1, 0, 0)], ['after-relative-jump']))
self.assertEqual(r.returncode, 20, r.stderr)
self.assertIn('after-relative-jump', r.stdout)
def test_out_of_bounds_relocation_is_rejected(self):
r = self.run_image(package(b'\xc3', [(4092, 0, 0, 0)]))
self.assertEqual(r.returncode, 1)
self.assertIn('bad relocation', r.stderr)
def test_truncated_image_is_rejected(self):
r = self.run_image(package(b'\xc3')[:-1])
self.assertEqual(r.returncode, 1)
self.assertIn('incorrect memory image size', r.stderr)
def test_illegal_instruction_reports_guest_offset(self):
r = self.run_image(package(b'\x0f\x0b'))
self.assertEqual(r.returncode, 132)
self.assertIn('at guest offset 0x0,', r.stderr)
def test_runtime_returns_from_verified_init_env(self):
# Align stack, call _init_env through +32, then tail-jump to unknown +40.
code = b'\x48\x83\xec\x08\xff\x15\x16\0\0\0\x48\x83\xc4\x08\xff\x25\x14\0\0\0'
data = package(code, [(32,1,0,0),(40,1,1,0)], ['bzQExy189ZI#libc','after-init'], 1)
r = self.run_image(data)
self.assertEqual(r.returncode,20,r.stderr)
self.assertIn('first unsupported PS4 import: after-init',r.stdout)
self.assertIn('_init_env=1',r.stdout)
strict = self.run_image(data,'--strict-imports')
self.assertEqual(strict.returncode,20,strict.stderr)
self.assertIn('first unsupported PS4 import: bzQExy189ZI#libc',strict.stdout)
def test_check_imports_lists_missing_without_running(self):
code = b'\x0f\x0b' # would fault if the game ran
data = package(code, [(32,1,0,0),(40,1,1,0)], ['bzQExy189ZI#libc','missing#libSceFixture'], 1)
r = self.run_image(data,'--check-imports')
self.assertEqual(r.returncode,1,r.stderr)
self.assertIn('MISSING: missing#libSceFixture',r.stdout)
self.assertNotIn('bzQExy189ZI',r.stdout)
ok = self.run_image(package(code,[(32,1,0,0)],['bzQExy189ZI#libc'],1),'--check-imports')
self.assertEqual(ok.returncode,0,ok.stderr)
self.assertIn('Imports: 0 missing',ok.stdout)
def test_unverified_runtime_stays_disabled(self):
r = self.run_image(package(b'\xff\x25\x02\0\0\0\x90\x90',[(8,1,0,0)],['bzQExy189ZI#libc'],0))
self.assertEqual(r.returncode,20,r.stderr)
self.assertIn('_init_env=0',r.stdout)
def test_unknown_capabilities_rejected(self):
r = self.run_image(package(b'\xc3',capabilities=2))
self.assertEqual(r.returncode,1)
self.assertIn('unknown runtime capabilities',r.stderr)
def host_env():
env = dict(os.environ)
# Development builds run from the MSYS2 tree: its CLANG64 DLLs (SDL3, ...), as in run.py.
if sys.platform == 'win32' and not (HOST_EXE.parent / 'SDL3.dll').is_file():
clang64 = Path(env.get('MSYS2_ROOT', r'C:\msys64')) / 'clang64' / 'bin'
env['PATH'] = f'{clang64}{os.pathsep}{env.get("PATH", "")}'
return env
@unittest.skipUnless(HOST_EXE.exists(), 'build native probe first')
class CpuFeaturesTests(unittest.TestCase):
def test_cpu_features_mode_reports_this_machine(self):
result = subprocess.run([str(HOST_EXE.resolve()), '--cpu-features'], capture_output=True, text=True, env=host_env())
self.assertEqual(result.returncode, 0)
self.assertRegex(result.stdout, r'^CPU: (complete|missing [A-Z0-9., ]+)\n$')
@unittest.skipUnless(HOST_EXE.exists(), 'build native probe first')
class GamepadAndMountTests(unittest.TestCase):
def test_gamepad_mode_prints_exactly_one_line(self):
# With or without a controller attached: "Gamepad: none" or the described pad.
result = subprocess.run([str(HOST_EXE.resolve()), '--gamepad'], capture_output=True, text=True, env=host_env())
self.assertEqual(result.returncode, 0, result.stderr)
self.assertEqual(result.stdout.count('\n'), 1, result.stdout)
self.assertTrue(result.stdout.startswith('Gamepad: '), result.stdout)
def test_malformed_mount_is_rejected(self):
# Without '=' the option is refused before the boot file is read.
result = subprocess.run([str(HOST_EXE.resolve()), 'missing.bin', '--mount', 'no-equals-sign'],
capture_output=True, text=True, env=host_env())
self.assertNotEqual(result.returncode, 0)
self.assertIn('--mount', result.stderr)
if __name__ == '__main__':
unittest.main()