|
@@ -0,0 +1,101 @@
|
|
|
+#!/usr/bin/python3
|
|
|
+
|
|
|
+# tests for vtable computations
|
|
|
+
|
|
|
+import hashlib
|
|
|
+import os
|
|
|
+import pathlib
|
|
|
+import typing
|
|
|
+
|
|
|
+import pytest
|
|
|
+import smgdc.angr
|
|
|
+import smgdc.vtable as vt_helpers
|
|
|
+from smgdc.validate import LinuxBinary
|
|
|
+
|
|
|
+
|
|
|
+@pytest.fixture(scope="session")
|
|
|
+def game_bin(request) -> LinuxBinary:
|
|
|
+ # this is tested against the TF2 Linux DS server binary, game version 8622567
|
|
|
+ # (steam app 232250, depot 232256, manifest 2590057218013527366)
|
|
|
+ #
|
|
|
+ # the author will not provide further support in obtaining this file, nor will the author
|
|
|
+ # update the tests for other game versions
|
|
|
+ #
|
|
|
+ # if you have the file: to enable tests that depend on the binary, create a `.env` file with
|
|
|
+ # TF_LINUX_BINARY set to the binary path (use single quotes on Windows to avoid escaping
|
|
|
+ # backslashes), then do `just test-extended`
|
|
|
+ #
|
|
|
+ # TODO: find a libre binary that we can do similarly extensive tests against in its place
|
|
|
+ path_string = os.environ.get("TF_LINUX_BINARY")
|
|
|
+ if not path_string:
|
|
|
+ pytest.skip("No Linux binary given")
|
|
|
+
|
|
|
+ bin_path = pathlib.Path(path_string)
|
|
|
+ with bin_path.open("rb") as f:
|
|
|
+ h = hashlib.file_digest(f, "sha256")
|
|
|
+ if h.hexdigest() != "a5a1adde851be2c71f8de73830466cd20f8c34d26ade580d110c4f93c6ef1374":
|
|
|
+ pytest.skip("Incorrect Linux binary")
|
|
|
+
|
|
|
+ return LinuxBinary(bin_path)
|
|
|
+
|
|
|
+
|
|
|
+class VTableCheck(typing.NamedTuple):
|
|
|
+ linux_index: int
|
|
|
+ windows_index: int | None
|
|
|
+ symbol_name: str
|
|
|
+
|
|
|
+
|
|
|
+@pytest.mark.extended
|
|
|
+@pytest.mark.parametrize(
|
|
|
+ "vtsym_name,vtasserts",
|
|
|
+ [
|
|
|
+ (
|
|
|
+ "_ZTV9CTFPlayer",
|
|
|
+ [
|
|
|
+ (149, 147, "_ZN20CBaseCombatCharacter8FVisibleERK6VectoriPP11CBaseEntity"),
|
|
|
+ (490, None, "_ZN9CTFPlayer16ReapplyProvisionEv"),
|
|
|
+ (493, 486, "_ZN9CTFPlayer13GiveNamedItemEPKciPK13CEconItemViewb"),
|
|
|
+ ],
|
|
|
+ ),
|
|
|
+ (
|
|
|
+ "_ZTV11CBaseObject",
|
|
|
+ [
|
|
|
+ (356, 354, "_ZN11CBaseObject6KilledERK15CTakeDamageInfo"),
|
|
|
+ (367, 365, "_ZN11CBaseObject17CheckUpgradeOnHitEP9CTFPlayer"),
|
|
|
+ (383, 382, "_ZN11CBaseObject13CanBeUpgradedEP9CTFPlayer"),
|
|
|
+ (384, 383, "_ZN11CBaseObject14StartUpgradingEv"),
|
|
|
+ (413, 381, "_ZNK11CBaseObject13CanBeUpgradedEv"),
|
|
|
+ ],
|
|
|
+ ),
|
|
|
+ (
|
|
|
+ "_ZTV13CBaseNPCMaker",
|
|
|
+ [
|
|
|
+ # on newer binaries the functions are optimized out to 0
|
|
|
+ # this also affects CBaseObject
|
|
|
+ (2, 1, "_ZN11CBaseEntity13SetRefEHandleERK11CBaseHandle"),
|
|
|
+ ],
|
|
|
+ ),
|
|
|
+ (
|
|
|
+ "_ZTV12CTFGameRules",
|
|
|
+ [
|
|
|
+ # parent vtable spans are not completely in descending order (some of them increase)
|
|
|
+ (140, 139, "_ZNK12CTFGameRules15IsHolidayActiveEi"),
|
|
|
+ ],
|
|
|
+ ),
|
|
|
+ ],
|
|
|
+)
|
|
|
+def test_vtable(game_bin: LinuxBinary, vtsym_name: str, vtasserts: list[VTableCheck]):
|
|
|
+ # tests vtable translation
|
|
|
+ vtsym = game_bin.angr.loader.find_symbol(vtsym_name)
|
|
|
+ assert vtsym, f"Could not find vtable symbol {vtsym_name}"
|
|
|
+ orig_vtable, *thunk_vtables = vt_helpers.get_vtables_from_address(game_bin, vtsym)
|
|
|
+ win_vtable = vt_helpers.get_windows_vtables_from(game_bin, vtsym)
|
|
|
+
|
|
|
+ for check in map(VTableCheck._make, vtasserts):
|
|
|
+ symbol = game_bin.angr.loader.find_symbol(check.symbol_name)
|
|
|
+ assert orig_vtable.index(symbol) == check.linux_index
|
|
|
+
|
|
|
+ if check.windows_index is not None:
|
|
|
+ assert win_vtable.index(symbol) == check.windows_index
|
|
|
+ else:
|
|
|
+ assert symbol not in win_vtable
|