mirror of
https://github.com/spoopyghosty0/frameport.git
synced 2026-10-06 17:00:43 +02:00
133 lines
6.6 KiB
Python
133 lines
6.6 KiB
Python
"""Unity text fields on the Frame: Cpp2IL output parsing, the release pick, the byte patch and the detection."""
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
from frameport.analysis.detect import unity_text_fields, unity_version
|
|
from frameport.analysis.il2cpp import _plain_version, parse_methods
|
|
from frameport.patches.frame import unity_text_input as U
|
|
from frameport.tools.cpp2il import pick_release
|
|
|
|
CS = '''public class TMP_InputField : Selectable
|
|
{
|
|
[Address(RVA = "0x71946E0", Offset = "0x71946E0", Length = "0x98")]
|
|
[Token(Token = "0x6000305")]
|
|
public bool get_shouldHideMobileInput() { }
|
|
|
|
[Address(RVA = "0x7194A78", Offset = "0x7194A78", Length = "0x10C")]
|
|
[Token(Token = "0x6000383")]
|
|
private bool isKeyboardUsingEvents() { }
|
|
|
|
[Address(RVA = "0x71988F0", Offset = "0x71988F0", Length = "0xB8")]
|
|
[Token(Token = "0x6000386")]
|
|
private bool TouchScreenKeyboardShouldBeUsed() { }
|
|
}
|
|
'''
|
|
|
|
|
|
def test_parse_methods_reads_file_offsets():
|
|
found = parse_methods(CS, ["TouchScreenKeyboardShouldBeUsed", "isKeyboardUsingEvents", "Missing"])
|
|
assert found == {"TouchScreenKeyboardShouldBeUsed": (0x71988F0, 0xB8), "isKeyboardUsingEvents": (0x7194A78, 0x10C)}
|
|
assert _plain_version("6000.2.7f2") == "6000.2.7" and _plain_version("2021.3.45f1") == "2021.3.45"
|
|
|
|
|
|
def test_pick_release_takes_the_newest_with_an_asset_for_this_computer():
|
|
releases = [{"tag_name": "2022.1.0-pre-release.21", "assets": [
|
|
{"name": "Cpp2IL-2022.1.0-pre-release.21-Linux", "browser_download_url": "u/linux"},
|
|
{"name": "Cpp2IL-2022.1.0-pre-release.21-Linux-ARM64", "browser_download_url": "u/arm"},
|
|
{"name": "Cpp2IL-2022.1.0-pre-release.21-Windows.exe", "browser_download_url": "u/win"}]},
|
|
{"tag_name": "old", "assets": [{"name": "Cpp2IL-old-OSX", "browser_download_url": "u/osx"}]}]
|
|
assert pick_release(releases, "Linux") == ("2022.1.0-pre-release.21", "u/linux")
|
|
assert pick_release(releases, "Linux-ARM64")[1] == "u/arm"
|
|
assert pick_release(releases, "Windows.exe")[1] == "u/win"
|
|
assert pick_release(releases, "OSX") == ("old", "u/osx")
|
|
assert pick_release(releases, "OSX-ARM64") is None
|
|
|
|
|
|
def _lib(tmp_path) -> bytes:
|
|
"""A small arm64 ELF with an executable segment (tests/fixtures), padded to hold the fake methods."""
|
|
fx = sorted(Path(__file__).parent.joinpath("fixtures").glob("libfake*_arm64.so"))[0]
|
|
return fx.read_bytes()
|
|
|
|
|
|
def test_patch_methods_writes_return_values_once(tmp_path):
|
|
lib = _lib(tmp_path)
|
|
(lo, hi), = U._executable(lib)[:1]
|
|
a, b = (lo + 0x40) & ~3, (lo + 0x80) & ~3
|
|
assert b + 8 <= hi
|
|
found = {"Unity.TextMeshPro/TMPro/TMP_InputField.cs": {"TouchScreenKeyboardShouldBeUsed": (a, 0xB8),
|
|
"isKeyboardUsingEvents": (b, 0x10C)}}
|
|
patched, notes = U.patch_methods(lib, found)
|
|
assert patched[a:a + 8] == U.RET_FALSE and patched[b:b + 8] == U.RET_TRUE and len(patched) == len(lib)
|
|
assert len(notes) == 2 and "TMP_InputField.isKeyboardUsingEvents -> true" in notes[1]
|
|
again, notes2 = U.patch_methods(patched, found) # idempotent
|
|
assert again is None and all("already patched" in n for n in notes2)
|
|
|
|
|
|
def test_patch_methods_refuses_offsets_outside_code(tmp_path):
|
|
lib = _lib(tmp_path)
|
|
bad = {"Unity.TextMeshPro/TMPro/TMP_InputField.cs": {"TouchScreenKeyboardShouldBeUsed": (len(lib) + 64, 8)}}
|
|
with pytest.raises(RuntimeError, match="isn't code"):
|
|
U.patch_methods(lib, bad)
|
|
misaligned = {"Unity.TextMeshPro/TMPro/TMP_InputField.cs": {"isKeyboardUsingEvents": (U._executable(lib)[0][0] + 2,
|
|
64)}}
|
|
with pytest.raises(RuntimeError):
|
|
U.patch_methods(lib, misaligned)
|
|
|
|
|
|
def test_detection_of_text_fields_and_unity_version():
|
|
meta = b"\0Selectable\0TMP_InputField\0TMPro\0UnityEngine.UI\0"
|
|
assert unity_text_fields(meta) == ["TMP_InputField"] # "InputField" only as the tail of TMP_InputField
|
|
assert unity_text_fields(meta + b"InputField\0") == ["TMP_InputField", "InputField"]
|
|
assert unity_text_fields(b"\0Button\0") == []
|
|
assert unity_version(b"\0" * 40 + b"2021.3.45f1\0") == "2021.3.45f1"
|
|
assert unity_version(None, b"x 6000.2.7f2 y 6000.2.7f2 z 6000.2.7f1 2018.3.0a1") == "6000.2.7f2"
|
|
|
|
|
|
def test_patches_suggested_for_unity_apps_with_text_fields():
|
|
from test_patches import _analysis
|
|
|
|
from frameport.patches import base
|
|
|
|
base.load_all()
|
|
a = _analysis(libs=["libil2cpp.so", "libunity.so"], extra={"text_fields": ["TMP_InputField"], "vr_kind": "quest"})
|
|
assert base.get("frame.unity_text_input").detect(a).recommended
|
|
assert base.get("device.text_input_window").detect(a).recommended
|
|
a.extra["text_fields"] = []
|
|
assert base.get("frame.unity_text_input").detect(a) is None and not base.get("frame.unity_text_input").applies(a)
|
|
|
|
|
|
def test_catalog_device_toggles_round_trip():
|
|
from frameport.recommend.catalog import CatalogEntry
|
|
|
|
e = CatalogEntry.from_dict({"package": "com.x", "title": "X", "frame": ["frame.unity_text_input"],
|
|
"device": ["device.text_input_window"]}, "bundled")
|
|
assert e.device == ["device.text_input_window"] and e.to_dict()["device"] == ["device.text_input_window"]
|
|
|
|
|
|
def test_reanalyze_refreshes_the_suggestion_and_keeps_a_users_recipe(monkeypatch, tmp_path):
|
|
from test_patches import _analysis
|
|
|
|
from frameport import pipeline
|
|
from frameport.core import library
|
|
from frameport.core.models import Recipe
|
|
|
|
apk = tmp_path / "game.apk"
|
|
apk.write_bytes(b"x")
|
|
a = _analysis(package="com.x", libs=["libil2cpp.so", "libunity.so"],
|
|
extra={"text_fields": ["TMP_InputField"], "vr_kind": "quest", "unity_version": "6000.2.7f2"})
|
|
monkeypatch.setattr(pipeline, "analyze", lambda path, data_bytes=0: a)
|
|
old = Recipe(package="com.x", patches={"frame.adapter": {}}, source="heuristics")
|
|
library.upsert_game("com.x", title="X", apk=str(apk), analysis=_analysis(package="com.x").to_dict(),
|
|
recipe=library.recipe_to_dict(old), suggested=library.recipe_to_dict(old))
|
|
pipeline.reanalyze("com.x")
|
|
g = library.game("com.x")
|
|
assert g["analysis"]["extra"]["text_fields"] == ["TMP_InputField"]
|
|
assert "frame.unity_text_input" in g["recipe"]["patches"] # heuristic recipe: replaced by the new suggestion
|
|
user = Recipe(package="com.x", patches={"frame.adapter": {}}, source="user")
|
|
library.upsert_game("com.x", recipe=library.recipe_to_dict(user))
|
|
pipeline.reanalyze("com.x")
|
|
g = library.game("com.x")
|
|
assert "frame.unity_text_input" not in g["recipe"]["patches"] # the user's own choices stay
|
|
assert "frame.unity_text_input" in g["suggested"]["patches"]
|