Переглянути джерело

Add TF2 server binary vtable test

Optional test with proprietary binary.
nosoop 10 місяців тому
батько
коміт
2ca53b9168
4 змінених файлів з 113 додано та 2 видалено
  1. 1 1
      .gitignore
  2. 6 1
      .justfile
  3. 5 0
      pyproject.toml
  4. 101 0
      tests/test_vtable.py

+ 1 - 1
.gitignore

@@ -1,4 +1,4 @@
 __pycache__
 *.egg-info
 .venv
-
+.env

+ 6 - 1
.justfile

@@ -1,8 +1,13 @@
+set dotenv-load
+
 test:
   ruff check src/smgdc
-  pytest tests
+  pytest tests -m "not extended"
   mypy -p src
 
+test-extended:
+  pytest tests -m extended
+
 format:
   ruff check src --select I001 --fix
   ruff format src/smgdc

+ 5 - 0
pyproject.toml

@@ -33,3 +33,8 @@ lint.extend-select = ["ANN001"]
 
 [tool.mypy]
 disable_error_code = ["import-untyped"]
+
+[tool.pytest.ini_options]
+markers = [
+    "extended",
+]

+ 101 - 0
tests/test_vtable.py

@@ -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