mirror of
https://github.com/spoopyghosty0/frameport.git
synced 2026-10-19 22:01:34 +02:00
190 lines
8.9 KiB
Python
190 lines
8.9 KiB
Python
"""Cpp2IL's development build for Unity metadata newer than its release reads (Unity 6.5: v107, GitHub #198)."""
|
|
import json
|
|
import sys
|
|
import zipfile
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
from frameport.analysis import il2cpp
|
|
from frameport.tools import cpp2il
|
|
|
|
ERROR = "---> System.FormatException: Unsupported metadata version found! We support 23-106, got 107"
|
|
WANTED = {"Assembly-CSharp/X.cs": ["Run"]}
|
|
|
|
|
|
def test_unsupported_metadata_and_runtime_ids(monkeypatch):
|
|
assert cpp2il.unsupported_metadata("Cpp2IL couldn't read this game's code: " + ERROR)
|
|
assert not cpp2il.unsupported_metadata("Cpp2IL couldn't read this game's code: Unity 2017 isn't supported")
|
|
monkeypatch.setattr(cpp2il.platform, "machine", lambda: "aarch64")
|
|
for plat, rid in (("linux", "linux-arm64"), ("darwin", "osx-arm64"), ("win32", "win-x64")):
|
|
monkeypatch.setattr(cpp2il.sys, "platform", plat)
|
|
assert cpp2il.nightly_rid() == rid
|
|
monkeypatch.setattr(cpp2il.platform, "machine", lambda: "x86_64")
|
|
monkeypatch.setattr(cpp2il.sys, "platform", "linux")
|
|
assert cpp2il.nightly_rid() == "linux-x64"
|
|
|
|
|
|
def test_release_path_ignores_the_development_build(monkeypatch, tmp_path):
|
|
monkeypatch.setattr(cpp2il, "tools_dir", lambda: tmp_path)
|
|
monkeypatch.delenv("FRAMEPORT_CPP2IL", raising=False)
|
|
monkeypatch.setattr(cpp2il.sys, "platform", "linux")
|
|
for d in ("cpp2il-2022.1.0-pre-release.21", cpp2il.NIGHTLY_DIR):
|
|
(tmp_path / d).mkdir()
|
|
(tmp_path / d / "Cpp2IL").write_text("")
|
|
assert cpp2il.path() == tmp_path / "cpp2il-2022.1.0-pre-release.21" / "Cpp2IL"
|
|
|
|
|
|
def test_ensure_nightly_unpacks_the_executable(monkeypatch, tmp_path):
|
|
monkeypatch.setattr(cpp2il, "tools_dir", lambda: tmp_path)
|
|
monkeypatch.setattr(cpp2il.sys, "platform", "linux")
|
|
urls = []
|
|
|
|
def download(url, dest, progress=None):
|
|
urls.append(url)
|
|
dest.parent.mkdir(parents=True, exist_ok=True)
|
|
with zipfile.ZipFile(dest, "w") as z:
|
|
z.writestr("Cpp2IL", b"\x7fELF fake")
|
|
return dest
|
|
|
|
monkeypatch.setattr(cpp2il.cache, "download", download)
|
|
exe = cpp2il.ensure_nightly()
|
|
assert exe == tmp_path / cpp2il.NIGHTLY_DIR / "Cpp2IL" and exe.read_bytes() == b"\x7fELF fake"
|
|
assert urls[0].startswith("https://nightly.link/SamboyCoding/Cpp2IL/") and urls[0].endswith(".zip")
|
|
assert not (tmp_path / cpp2il.NIGHTLY_DIR / "cpp2il.zip").exists()
|
|
assert cpp2il.ensure_nightly() == exe and len(urls) == 1 # downloaded once
|
|
|
|
|
|
@pytest.fixture
|
|
def tools(monkeypatch, tmp_path):
|
|
"""Release + development build stand-ins: the release can't read metadata v107."""
|
|
monkeypatch.setattr(il2cpp, "cache_dir", lambda: tmp_path)
|
|
monkeypatch.delenv("FRAMEPORT_CPP2IL", raising=False)
|
|
release, nightly = Path("release/Cpp2IL"), Path("nightly/Cpp2IL")
|
|
monkeypatch.setattr(cpp2il, "ensure", lambda progress=None: release)
|
|
monkeypatch.setattr(cpp2il, "ensure_nightly", lambda progress=None: nightly)
|
|
runs = []
|
|
|
|
def run(exe, lib, metadata, unity_version, wanted, timeout):
|
|
runs.append(exe)
|
|
if exe == release:
|
|
raise il2cpp._Unreadable(ERROR)
|
|
return {"Assembly-CSharp/X.cs": {"Run": (0x1000, 0x20)}}, []
|
|
|
|
monkeypatch.setattr(il2cpp, "_run", run)
|
|
return release, nightly, runs
|
|
|
|
|
|
def test_find_methods_falls_back_to_the_development_build(tools, tmp_path):
|
|
release, nightly, runs = tools
|
|
found = il2cpp.find_methods(b"lib", b"meta", "6000.5.6f1", WANTED)
|
|
assert found == {"Assembly-CSharp/X.cs": {"Run": (0x1000, 0x20)}} and runs == [release, nightly]
|
|
assert il2cpp.find_methods(b"lib", b"meta", "6000.5.6f1", WANTED) == found and len(runs) == 2 # cached
|
|
|
|
|
|
def test_cached_release_error_is_retried_once_with_the_development_build(tools, tmp_path):
|
|
release, nightly, runs = tools
|
|
cache = il2cpp._cache_file(b"lib", b"meta")
|
|
cache.parent.mkdir(parents=True, exist_ok=True)
|
|
cache.write_text(json.dumps({"unity_version": "6000.5.6f1", "error": "Cpp2IL couldn't read this game's code: "
|
|
+ ERROR}))
|
|
assert il2cpp.find_methods(b"lib", b"meta", "6000.5.6f1", WANTED)
|
|
assert runs == [nightly]
|
|
cache.write_text(json.dumps({"unity_version": "6000.5.6f1", "error": "x: " + ERROR, "nightly": True}))
|
|
with pytest.raises(RuntimeError): # the development build couldn't either: not run again
|
|
il2cpp.find_methods(b"lib", b"meta", "6000.5.6f1", WANTED)
|
|
assert runs == [nightly]
|
|
|
|
|
|
def test_any_release_failure_tries_the_development_build_once(tools, monkeypatch):
|
|
"""BattleTalent (Unity 2021.3, GitHub #293): the release crashes (IndexOutOfRangeException in GetTypeName), the
|
|
development build reads it. Any failure of the release gets the one development-build try."""
|
|
release, nightly, runs = tools
|
|
|
|
def run(exe, *a):
|
|
runs.append(exe)
|
|
raise il2cpp._Unreadable("System.NullReferenceException: Object reference not set")
|
|
|
|
monkeypatch.setattr(il2cpp, "_run", run)
|
|
with pytest.raises(RuntimeError, match="couldn't read this game's code: System.NullReferenceException"):
|
|
il2cpp.find_methods(b"lib", b"meta", "2022.3.1f1", WANTED)
|
|
assert runs == [release, nightly]
|
|
cached = json.loads(il2cpp._cache_file(b"lib", b"meta").read_text())
|
|
assert cached["nightly"] is True and "missing" not in cached # an error, never "no such classes"
|
|
with pytest.raises(RuntimeError): # both failed: not run again per patch
|
|
il2cpp.find_methods(b"lib", b"meta", "2022.3.1f1", WANTED)
|
|
assert runs == [release, nightly]
|
|
|
|
|
|
def test_explicit_cpp2il_is_never_swapped(tools, monkeypatch):
|
|
release, nightly, runs = tools
|
|
mine = Path("mine/Cpp2IL")
|
|
|
|
def run(exe, *a):
|
|
runs.append(exe)
|
|
raise il2cpp._Unreadable("System.IndexOutOfRangeException: Index was outside the bounds of the array.")
|
|
|
|
monkeypatch.setattr(il2cpp, "_run", run)
|
|
with pytest.raises(RuntimeError):
|
|
il2cpp.find_methods(b"lib", b"meta", "2021.3.45f2", WANTED, cpp2il=mine)
|
|
assert runs == [mine]
|
|
il2cpp._cache_file(b"lib", b"meta").unlink()
|
|
monkeypatch.setenv("FRAMEPORT_CPP2IL", str(release)) # the user's override (ensure() returns it)
|
|
with pytest.raises(RuntimeError):
|
|
il2cpp.find_methods(b"lib", b"meta", "2021.3.45f2", WANTED)
|
|
assert runs == [mine, release]
|
|
|
|
|
|
def test_poisoned_cache_is_run_again(tools):
|
|
"""An older FramePort cached a Cpp2IL crash as "every class missing"; such an entry is run again."""
|
|
release, nightly, runs = tools
|
|
cache = il2cpp._cache_file(b"lib", b"meta")
|
|
cache.parent.mkdir(parents=True, exist_ok=True)
|
|
cache.write_text(json.dumps({"unity_version": "2021.3.45f2", "missing": list(WANTED), "classes": {}}))
|
|
assert il2cpp.find_methods(b"lib", b"meta", "2021.3.45f2", WANTED) == {
|
|
"Assembly-CSharp/X.cs": {"Run": (0x1000, 0x20)}}
|
|
assert runs == [release, nightly]
|
|
# a genuine result (some classes found) is kept
|
|
assert il2cpp.find_methods(b"lib", b"meta", "2021.3.45f2", WANTED) and runs == [release, nightly]
|
|
|
|
|
|
def _fake_cpp2il(tmp_path, script: str) -> Path:
|
|
exe = tmp_path / "bin" / "Cpp2IL"
|
|
exe.parent.mkdir(parents=True, exist_ok=True)
|
|
exe.write_text("#!/bin/sh\n" + script)
|
|
exe.chmod(0o755)
|
|
return exe
|
|
|
|
|
|
@pytest.mark.skipif(sys.platform == "win32", reason="shell-script stand-in for Cpp2IL")
|
|
def test_cpp2il_crash_after_making_folders_is_unreadable(monkeypatch, tmp_path):
|
|
"""The release's crash on BattleTalent: the DiffableCs folder tree exists, no .cs file, exit 134. That's an
|
|
unreadable game (-> the development build), not "none of the classes are in it"."""
|
|
monkeypatch.setattr(il2cpp, "cache_dir", lambda: tmp_path / "cache")
|
|
monkeypatch.delenv("FRAMEPORT_CPP2IL", raising=False)
|
|
crash = _fake_cpp2il(
|
|
tmp_path / "release",
|
|
"""out=""; while [ $# -gt 0 ]; do [ "$1" = --output-to ] && out=$2; shift; done
|
|
mkdir -p "$out/DiffableCs/Assembly-CSharp"
|
|
echo "Unhandled exception. System.IndexOutOfRangeException: Index was outside the bounds of the array." >&2
|
|
echo " at Cpp2IL.Core.Utils.CsFileUtils.GetTypeName(...) in CsFileUtils.cs:line 282" >&2
|
|
exit 134
|
|
""")
|
|
good = _fake_cpp2il(
|
|
tmp_path / "nightly",
|
|
"""out=""; while [ $# -gt 0 ]; do [ "$1" = --output-to ] && out=$2; shift; done
|
|
mkdir -p "$out/DiffableCs/Assembly-CSharp"
|
|
cat > "$out/DiffableCs/Assembly-CSharp/X.cs" <<'CS'
|
|
\t[Address(RVA = "0x5000", Offset = "0x1000", Length = "0x20")]
|
|
\tpublic void Run() { }
|
|
CS
|
|
""")
|
|
monkeypatch.setattr(cpp2il, "ensure", lambda progress=None: crash)
|
|
monkeypatch.setattr(cpp2il, "ensure_nightly", lambda progress=None: good)
|
|
with pytest.raises(il2cpp._Unreadable, match=r"IndexOutOfRangeException.*\(exit code 134\)"):
|
|
il2cpp._run(crash, b"lib", b"meta", "2021.3.45f2", WANTED, 60)
|
|
assert il2cpp.find_methods(b"lib", b"meta", "2021.3.45f2", WANTED) == {
|
|
"Assembly-CSharp/X.cs": {"Run": (0x1000, 0x20)}}
|
|
cached = json.loads(il2cpp._cache_file(b"lib", b"meta").read_text())
|
|
assert cached["nightly"] is True and cached["classes"]
|