import contextlib import io import json import os import stat import struct import subprocess import sys import tempfile import unittest from pathlib import Path from unittest.mock import patch sys.path.insert(0, str(Path(__file__).resolve().parents[1])) import import_audit import nid_names GLOBAL_FUNCTION = 0x12 LOCAL_FUNCTION = 0x02 STUB_SOURCE = "int APS5_VABI {name}(int value) {{ NotImplemented_nid_no_patch(__func__); return 0; }}\n" DONE_SOURCE = "int APS5_VABI {name}(int value) {{ return value; }}\n" MESSAGE_NID = "wTpfglkmv34" MESSAGE_NAME = "sceMsgDialogProgressBarSetValue" def build_elf(symbols, needed=()): strings = b"\0" entries = [struct.pack(" 2: record["weak"] = entry[2] records.append(record) path.write_text(json.dumps(records), encoding=encoding) class Workspace: def __init__(self): self._directory = tempfile.TemporaryDirectory() self.root = Path(self._directory.name) self.libs = self.root / "libs" self.modules = self.root / "modules" self.source = self.root / "prx" for path in (self.libs, self.modules, self.source): path.mkdir() def close(self): self._directory.cleanup() def run(self, registry, *extra, registries=()): out, err = io.StringIO(), io.StringIO() arguments = [str(registry), *map(str, registries), "--libs", str(self.libs), "--modules", str(self.modules), "--source", str(self.source), *extra] with contextlib.redirect_stdout(out), contextlib.redirect_stderr(err): code = import_audit.main(arguments) return code, out.getvalue(), err.getvalue() class ImportAuditTests(unittest.TestCase): def test_cli_error_paths_are_utf8_under_an_ascii_environment(self): registry = self.work.root / "\u65e5\u672c\u8a9e.json" result = subprocess.run( [sys.executable, str(Path(import_audit.__file__)), str(registry), "--libs", str(self.work.libs), "--source", str(self.work.source)], env=dict(os.environ, PYTHONIOENCODING="ascii"), capture_output=True, timeout=20) self.assertEqual(result.returncode, 2) self.assertIn(str(registry), result.stderr.decode("utf-8")) self.assertNotIn(b"Traceback", result.stderr) self.assertEqual(result.stdout, b"") def test_deeply_nested_registries_are_audit_errors(self): registry = self.work.root / "game.registry.json" registry.write_text("[" * 200000, encoding="utf-8") code, out, err = self.work.run(registry) self.assertEqual(code, 2, err) self.assertIn("cannot read registry", err) registry.write_text("[]", encoding="utf-8") registry.with_name("game.needed.json").write_text("[" * 200000, encoding="utf-8") code, out, err = self.work.run(registry) self.assertEqual(code, 2, err) self.assertIn("cannot read needed libraries", err) def test_windows_guest_dependencies_use_host_prx_suffixes(self): work = self.work (work.libs / "libSceA.prx").write_bytes(build_pe(["AAAAAAAAAAA"])) (work.modules / "guest.so").write_bytes(build_elf([], ["libSceA.sprx"])) registry, result = work.root / "game.registry.json", work.root / "audit.json" for suffix in (".sprx", ".suprx", ".debug_prx"): with self.subTest(suffix=suffix): name = "libSceA" + suffix write_registry(registry, [("AAAAAAAAAAA", name)]) registry.with_name("game.needed.json").write_text(json.dumps([name])) code, out, err = work.run(registry, "--strict-providers", "--json", str(result)) self.assertEqual((code, err), (0, "")) data = json.loads(result.read_text()) self.assertEqual(data["missing_libraries"], {}) self.assertEqual(data["library_mismatch"], 0) self.assertEqual(data["imports"][0]["providers"], ["libSceA.prx"]) def test_windows_suffix_lookup_matches_provider_case_insensitively(self): work = self.work (work.libs / "LibSceA.prx").write_bytes(build_pe(["AAAAAAAAAAA"])) (work.modules / "guest.so").write_bytes(build_elf([], ["libscea.sprx"])) registry, result = work.root / "game.registry.json", work.root / "audit.json" write_registry(registry, [("AAAAAAAAAAA", "libscea.sprx")]) registry.with_name("game.needed.json").write_text(json.dumps(["libscea.sprx"])) code, _, err = work.run(registry, "--strict-providers", "--json", str(result)) self.assertEqual((code, err), (0, "")) data = json.loads(result.read_text()) self.assertEqual(data["missing_libraries"], {}) self.assertEqual(data["library_mismatch"], 0) self.assertEqual(data["imports"][0]["providers"], ["LibSceA.prx"]) def test_windows_prx_suffix_lookup_folds_the_suffix_case(self): work = self.work (work.libs / "LibSceA.prx").write_bytes(build_pe(["AAAAAAAAAAA"])) registry, result = work.root / "game.registry.json", work.root / "audit.json" for suffix, expected_missing in ( (".sprx", False), (".SPRX", True), (".suprx", False), (".SUPRX", True), (".debug_prx", False), (".DEBUG_PRX", True), (".prx", False), (".PRX", False)): with self.subTest(suffix=suffix): name = "libscea" + suffix (work.modules / "guest.so").write_bytes(build_elf([], [name])) write_registry(registry, [("AAAAAAAAAAA", name)]) registry.with_name("game.needed.json").write_text(json.dumps([name])) code, _, err = work.run(registry, "--strict-providers", "--json", str(result)) self.assertEqual((code, err), (1 if expected_missing else 0, "")) data = json.loads(result.read_text()) self.assertEqual(data["missing_libraries"], {name: 2} if expected_missing else {}) self.assertEqual(data["library_mismatch"], int(expected_missing)) self.assertEqual(data["imports"][0]["providers"], ["LibSceA.prx"]) def test_elf_guest_dependencies_keep_literal_host_names(self): work = self.work (work.libs / "libSceA.prx").write_bytes(exporting("AAAAAAAAAAA")) registry, result = work.root / "game.registry.json", work.root / "audit.json" write_registry(registry, [("AAAAAAAAAAA", "libSceA.sprx")]) registry.with_name("game.needed.json").write_text(json.dumps(["libSceA.sprx"])) code, _, err = work.run(registry, "--json", str(result)) self.assertEqual((code, err), (1, "")) data = json.loads(result.read_text()) self.assertEqual(data["missing_libraries"], {"libSceA.sprx": 1}) self.assertEqual(data["library_mismatch"], 1) def test_windows_suffix_lookup_preserves_guest_and_missing_libraries(self): work = self.work (work.libs / "libSceA.prx").write_bytes(build_pe(["AAAAAAAAAAA"])) (work.modules / "libSceA.sprx").write_bytes(exporting("BBBBBBBBBBB")) registry, result = work.root / "game.registry.json", work.root / "audit.json" write_registry(registry, [("BBBBBBBBBBB", "libSceA.sprx")]) registry.with_name("game.needed.json").write_text(json.dumps(["libSceA.sprx", "libMissing.sprx"])) code, _, err = work.run(registry, "--json", str(result)) self.assertEqual((code, err), (1, "")) data = json.loads(result.read_text()) self.assertEqual(data["missing_libraries"], {"libMissing.sprx": 0}) self.assertEqual(data["imports"][0]["class"], "module") self.assertEqual(data["library_mismatch"], 0) def test_registry_weak_flags_are_booleans(self): registry = self.work.root / "game.json" output = self.work.root / "audit.json" output.write_bytes(b"previous report") for weak in ("false", "true", 0, 1, None, [], {}): with self.subTest(weak=weak): registry.write_text(json.dumps([{"nid": "AAAAAAAAAAA", "library": "libSceA.prx", "weak": weak}]), encoding="utf-8") with self.assertRaisesRegex(import_audit.AuditError, "boolean 'weak'"): import_audit.read_registry(registry) code, out, err = self.work.run(registry, "--json", str(output)) self.assertEqual((code, out), (2, "")) self.assertIn("boolean 'weak'", err) self.assertEqual(output.read_bytes(), b"previous report") for fields, expected in (({}, False), ({"weak": False}, False), ({"weak": True}, True)): registry.write_text(json.dumps([dict(nid="AAAAAAAAAAA", library="libSceA.prx", **fields)]), encoding="utf-8") self.assertEqual(import_audit.read_registry(registry), [("AAAAAAAAAAA", "libSceA.prx", expected)]) def test_test_file_aliases_cannot_override_production_stubs(self): source = self.work.source text = 'APS5_EXPORT("AAAAAAAAAAA", sceExample);\n' + STUB_SOURCE.format(name="sceExample") write_source(source, "libSceExample", text) test = source / "libSceExample" / "tests" / "Alias.cpp" test.parent.mkdir() test.write_text('APS5_EXPORT("BBBBBBBBBBB", sceExample);\n', encoding="utf-8") stubs = import_audit.source_stubs(source) self.assertEqual(stubs, {("libSceExample.prx", "AAAAAAAAAAA"): "sceExample"}) records, _ = import_audit.audit([("AAAAAAAAAAA", "libSceExample.prx", False)], {"AAAAAAAAAAA": ["libSceExample.prx"]}, stubs, {"libSceExample.prx"}, set()) self.assertEqual(records[0]["class"], "stub") def setUp(self): self.work = Workspace() self.addCleanup(self.work.close) def test_named_imports_use_literal_exports_and_keep_unresolved_names(self): work = self.work (work.libs / "libc.prx").write_bytes(exporting("strcmp", "_Z3foov", "AAAAAAAAAAA")) registry, report = work.root / "homebrew.registry.json", work.root / "audit.json" write_registry(registry, [("strcmp", ""), ("strcmp", ""), ("_Z3foov", "libc.prx"), ("AAAAAAAAAAA#A#B", "libc.prx"), ("missing_function", "", True)]) code, _, err = work.run(registry, "--json", str(report)) self.assertEqual((code, err), (1, "")) data = json.loads(report.read_text(encoding="utf-8")) records = {record["nid"]: record for record in data["imports"]} self.assertEqual(data["references"], 5) self.assertEqual(data["unique_by_class"], {"implemented": 3, "partial": 0, "stub": 0, "absent": 1, "module": 0}) self.assertEqual(records["strcmp"]["references"], 2) self.assertEqual(records["strcmp"]["providers"], ["libc.prx"]) self.assertTrue(records["missing_function"]["weak"]) self.assertIsNone(data["stops"][0]["nid"]) def test_named_imports_fall_back_to_nid_exports(self): work = self.work (work.libs / "libc.prx").write_bytes(exporting("j4ViWNHEgww")) registry, report = work.root / "homebrew.registry.json", work.root / "audit.json" write_registry(registry, [("strlen", ""), ("strlen", ""), ("missing_function", "", True)]) code, _, err = work.run(registry, "--json", str(report)) self.assertEqual((code, err), (1, "")) data = json.loads(report.read_text(encoding="utf-8")) records = {record["nid"]: record for record in data["imports"]} self.assertEqual(data["references"], 3) self.assertEqual(records["strlen"]["references"], 2) self.assertEqual(records["strlen"]["class"], "implemented") self.assertEqual(records["strlen"]["providers"], ["libc.prx"]) self.assertEqual(records["missing_function"]["class"], "absent") self.assertTrue(records["missing_function"]["weak"]) self.assertIsNone(data["stops"][0]["nid"]) def test_nid_only_exports_do_not_resolve_literal_import_names(self): work = self.work name, nid = "strlen", nid_names.compute_nid("strlen") registry, report = work.root / "sdk.registry.json", work.root / "audit.json" for image in (exporting(nid), build_pe([nid])): (work.libs / "libc.prx").write_bytes(image) for stub in (False, True): write_source(work.source, "libc", (STUB_SOURCE if stub else DONE_SOURCE).format(name=name)) for weak in (False, True): with self.subTest(pe=image.startswith(b"MZ"), stub=stub, weak=weak): write_registry(registry, [(name, "libc.prx", weak)]) code, out, err = work.run(registry, "--json", str(report)) self.assertEqual((code, err), (1, "")) data = json.loads(report.read_text(encoding="utf-8")) record, = data["imports"] self.assertEqual(record["class"], "stub" if stub else "implemented") self.assertEqual(record["providers"], ["libc.prx"]) self.assertTrue(record["symbol_mismatch"]) self.assertEqual(data["symbol_mismatch"], 1) self.assertEqual(data["stops"][0]["nid"], None if weak else name) self.assertIn("Qualified import names exported only as their NID", out) self.assertIn(f"-> {nid}", out) def test_name_mismatch_requires_a_resolved_host_fallback(self): name, library = "strlen", "libc.prx" exports = {nid_names.compute_nid(name): [library]} for providers, modules, kind, mismatch in (({}, set(), "absent", False), (exports, {library}, "module", False), (exports, set(), "implemented", True)): with self.subTest(kind=kind): records, _ = import_audit.audit([(name, library, False)], providers, {}, {library}, modules) self.assertEqual(records[0]["class"], kind) self.assertEqual(records[0]["symbol_mismatch"], mismatch) def test_literal_exports_resolve_without_a_name_mismatch(self): work = self.work name, nid = "strlen", nid_names.compute_nid("strlen") registry, report = work.root / "sdk.registry.json", work.root / "audit.json" for image in (exporting(name, nid), build_pe(sorted([name, nid]))): with self.subTest(pe=image.startswith(b"MZ")): (work.libs / "libc.prx").write_bytes(image) write_registry(registry, [(name, "libc.prx"), (nid, "libc.prx")]) code, out, err = work.run(registry, "--json", str(report)) self.assertEqual((code, err), (0, "")) data = json.loads(report.read_text(encoding="utf-8")) self.assertEqual(data["symbol_mismatch"], 0) self.assertTrue(all(not record["symbol_mismatch"] for record in data["imports"])) self.assertIsNone(data["stops"][0]["nid"]) self.assertNotIn("Qualified import names exported only as their NID", out) def test_unscoped_windows_imports_keep_the_loaders_nid_fallback(self): work = self.work name, nid = "strlen", nid_names.compute_nid("strlen") registry, report = work.root / "sdk.registry.json", work.root / "audit.json" write_registry(registry, [(name, "")]) (work.libs / "libc.prx").write_bytes(build_pe([nid])) code, out, err = work.run(registry, "--json", str(report)) self.assertEqual((code, err), (0, "")) data = json.loads(report.read_text(encoding="utf-8")) self.assertEqual(data["symbol_mismatch"], 0) self.assertIsNone(data["stops"][0]["nid"]) def test_named_nid_fallback_preserves_stub_and_library_classification(self): nid = nid_names.compute_nid(MESSAGE_NAME) imports = [(MESSAGE_NAME, "libSceOther.prx", False)] records, missing = import_audit.audit(imports, {nid: ["libSceMsgDialog.prx"]}, {("libSceMsgDialog.prx", nid): MESSAGE_NAME}, {"libSceMsgDialog.prx", "libSceOther.prx"}, set()) self.assertEqual(missing, {}) self.assertEqual(records[0]["class"], "stub") self.assertEqual(records[0]["nid"], MESSAGE_NAME) self.assertEqual(records[0]["name"], MESSAGE_NAME) self.assertTrue(records[0]["library_mismatch"]) def test_literal_exports_take_precedence_over_nid_fallback(self): imports = [("strlen", "", False)] stubs = {("literal.prx", "strlen"): "strlen"} exports = {"strlen": ["literal.prx"], "j4ViWNHEgww": ["libc.prx"]} records, _ = import_audit.audit(imports, exports, stubs, set(exports), set()) self.assertEqual(records[0]["class"], "stub") self.assertEqual(records[0]["providers"], ["literal.prx"]) module_imports = {"guest.so": ([("strlen", False)], [])} records, _ = import_audit.audit([], {"j4ViWNHEgww": ["libc.prx"]}, {}, {"libc.prx"}, set(module_imports), module_imports, {"strlen": ["other.so"]}) self.assertEqual(records[0]["providers"], ["other.so"]) records, _ = import_audit.audit([], {"j4ViWNHEgww": ["libc.prx"]}, {}, {"libc.prx"}, set(module_imports), module_imports) self.assertEqual(records[0]["class"], "implemented") self.assertEqual(records[0]["providers"], ["libc.prx"]) def test_empty_and_control_character_imports_remain_invalid(self): registry = self.work.root / "homebrew.registry.json" for name in ("", "#A#B", "bad\x00name", "bad\nname", "bad\x7fname", "bad name"): with self.subTest(name=name): write_registry(registry, [(name, "")]) with self.assertRaisesRegex(import_audit.AuditError, "invalid import symbol"): import_audit.read_registry(registry) def test_failed_json_write_preserves_existing_report_and_leaves_no_partial_report(self): work = self.work (work.libs / "libSceA.prx").write_bytes(exporting("AAAAAAAAAAA")) registry = work.root / "game.json" write_registry(registry, [("AAAAAAAAAAA", "libSceA.prx")]) output = work.root / "audit.json" previous = b'{"previous": "complete report"}\n' def disk_full(path, content, **kwargs): with path.open("w", encoding="utf-8") as handle: handle.write(content[:24]) raise OSError(28, "No space left on device", str(path)) for exists in (True, False): with self.subTest(existing_report=exists): if exists: output.write_bytes(previous) else: output.unlink(missing_ok=True) files = set(work.root.iterdir()) with patch.object(Path, "write_text", disk_full): code, out, err = work.run(registry, "--json", str(output)) self.assertEqual((code, out), (2, "")) self.assertIn("No space left on device", err) self.assertEqual(set(work.root.iterdir()), files) if exists: self.assertEqual(output.read_bytes(), previous) else: self.assertFalse(output.exists()) def test_failed_json_replacement_preserves_report_and_cleans_staging(self): work = self.work (work.libs / "libSceA.prx").write_bytes(exporting("AAAAAAAAAAA")) registry = work.root / "game.json" write_registry(registry, [("AAAAAAAAAAA", "libSceA.prx")]) output = work.root / "audit.json" previous = b'{"previous": "complete report"}\n' output.write_bytes(previous) files = set(work.root.iterdir()) with patch.object(Path, "replace", side_effect=PermissionError(13, "Permission denied", str(output))): code, out, err = work.run(registry, "--json", str(output)) self.assertEqual((code, out), (2, "")) self.assertIn("Permission denied", err) self.assertEqual(output.read_bytes(), previous) self.assertEqual(set(work.root.iterdir()), files) def test_successful_json_write_replaces_complete_report_without_staging_files(self): work = self.work (work.libs / "libSceA.prx").write_bytes(exporting("AAAAAAAAAAA")) registry = work.root / "game.json" write_registry(registry, [("AAAAAAAAAAA", "libSceA.prx")]) output = work.root / "audit.json" output.write_text("old report", encoding="utf-8") files = set(work.root.iterdir()) code, _, err = work.run(registry, "--json", str(output)) self.assertEqual((code, err), (0, "")) self.assertEqual(json.loads(output.read_text(encoding="utf-8"))["unique_by_class"]["implemented"], 1) self.assertEqual(set(work.root.iterdir()), files) @unittest.skipUnless(sys.platform == "win32", "Windows read-only replacement behavior") def test_read_only_json_report_stays_intact_without_a_read_only_temporary(self): work = self.work (work.libs / "libSceA.prx").write_bytes(exporting("AAAAAAAAAAA")) registry = work.root / "game.json" write_registry(registry, [("AAAAAAAAAAA", "libSceA.prx")]) output = work.root / "audit.json" previous = b'{"previous": "complete report"}\n' output.write_bytes(previous) files = set(work.root.iterdir()) output.chmod(0o444) try: code, out, err = work.run(registry, "--json", str(output)) self.assertEqual((code, out), (2, "")) self.assertIn("FAIL:", err) self.assertEqual(output.read_bytes(), previous) self.assertEqual(set(work.root.iterdir()), files) finally: output.chmod(0o666) def test_json_report_symlink_keeps_its_target(self): work = self.work (work.libs / "libSceA.prx").write_bytes(exporting("AAAAAAAAAAA")) registry = work.root / "game.json" write_registry(registry, [("AAAAAAAAAAA", "libSceA.prx")]) target = work.root / "actual-report.json" target.write_text("old report", encoding="utf-8") output = work.root / "audit.json" try: output.symlink_to(target.name) except (OSError, NotImplementedError) as error: self.skipTest(str(error)) files = set(work.root.iterdir()) code, _, err = work.run(registry, "--json", str(output)) self.assertEqual((code, err), (0, "")) self.assertTrue(output.is_symlink()) self.assertEqual(json.loads(target.read_text(encoding="utf-8"))["unique_imports"], 1) self.assertEqual(set(work.root.iterdir()), files) def test_json_directory_error_preserves_the_supplied_path(self): work = self.work (work.libs / "libSceA.prx").write_bytes(exporting("AAAAAAAAAAA")) registry = work.root / "game.json" write_registry(registry, [("AAAAAAAAAAA", "libSceA.prx")]) output = work.modules / ".." / work.modules.name self.assertNotEqual(str(output), str(output.resolve())) files = set(work.root.iterdir()) code, out, err = work.run(registry, "--json", str(output)) self.assertEqual((code, out), (2, "")) self.assertEqual(err, f"FAIL: {output}: JSON output is a directory\n") self.assertEqual(set(work.root.iterdir()), files) @unittest.skipUnless(os.name == "posix", "POSIX report permissions") def test_new_json_report_permissions_follow_the_process_umask(self): script = "\n".join([ "import os, sys", "from pathlib import Path", "sys.path.insert(0, sys.argv[1])", "import import_audit", "os.umask(int(sys.argv[4], 8))", "Path(sys.argv[3]).write_text('{}\\n', encoding='utf-8')", "import_audit.write_json(Path(sys.argv[2]), {'complete': True})", ]) for mask, mode in ((0o022, 0o644), (0o002, 0o664), (0o077, 0o600)): with self.subTest(umask=oct(mask)): output = self.work.root / f"audit-{mask:o}.json" reference = self.work.root / f"plain-{mask:o}.json" files = set(self.work.root.iterdir()) completed = subprocess.run([sys.executable, "-c", script, str(import_audit.ROOT / "tools"), str(output), str(reference), oct(mask)], capture_output=True, text=True) self.assertEqual(completed.returncode, 0, completed.stderr) self.assertEqual(stat.S_IMODE(output.stat().st_mode), mode) self.assertEqual(stat.S_IMODE(output.stat().st_mode), stat.S_IMODE(reference.stat().st_mode)) self.assertEqual(json.loads(output.read_text(encoding="utf-8")), {"complete": True}) self.assertEqual(set(self.work.root.iterdir()), files | {output, reference}) @unittest.skipUnless(os.name == "posix", "POSIX report permissions") def test_existing_json_report_permissions_are_preserved(self): output = self.work.root / "audit.json" for mode in (0o600, 0o640, 0o664): with self.subTest(mode=oct(mode)): output.write_text("old report", encoding="utf-8") output.chmod(mode) files = set(self.work.root.iterdir()) import_audit.write_json(output, {"complete": True}) self.assertEqual(stat.S_IMODE(output.stat().st_mode), mode) self.assertEqual(json.loads(output.read_text(encoding="utf-8")), {"complete": True}) self.assertEqual(set(self.work.root.iterdir()), files) def test_classification_accounts_for_every_reference(self): work = self.work (work.libs / "libSceMsgDialog.prx").write_bytes(build_elf([ (MESSAGE_NID, 1, GLOBAL_FUNCTION), ("AAAAAAAAAAA", 1, GLOBAL_FUNCTION), ("BBBBBBBBBBB", 1, GLOBAL_FUNCTION), ("UNDEFINED11", 0, GLOBAL_FUNCTION), ("LOCALLOCAL1", 1, LOCAL_FUNCTION)])) (work.libs / "libc.prx").write_bytes(exporting("CCCCCCCCCCC")) (work.modules / "libSceNpCppWebApi.prx").write_bytes(exporting()) write_source(work.source, "libSceMsgDialog", STUB_SOURCE.format(name=MESSAGE_NAME) + 'APS5_EXPORT("BBBBBBBBBBB", sceFooUnknown_B);\n' + STUB_SOURCE.format(name="sceFooUnknown_B")) registry = work.root / "game.registry.json" write_registry(registry, [ (MESSAGE_NID + "#A#B", "libSceMsgDialog.prx"), (MESSAGE_NID, "libSceMsgDialog.prx"), ("AAAAAAAAAAA", "libSceMsgDialog.prx"), ("BBBBBBBBBBB", "libSceMsgDialog.prx"), ("DDDDDDDDDDD", "libSceMsgDialog.prx"), ("EEEEEEEEEEE", "libSceNpCppWebApi.prx"), ("CCCCCCCCCCC", "libSceFoo.prx"), ("AAAAAAAAAAA", "libSceOther.prx"), ("UNDEFINED11", "libSceMsgDialog.prx"), ("LOCALLOCAL1", "libSceMsgDialog.prx")]) result = work.root / "audit.json" code, out, err = work.run(registry, "--json", str(result)) self.assertEqual((code, err), (1, "")) data = json.loads(result.read_text()) self.assertEqual(data["references"], 10) self.assertEqual(data["unique_imports"], 9) self.assertEqual(data["unique_by_class"], {"implemented": 3, "partial": 0, "stub": 2, "absent": 3, "module": 1}) self.assertEqual(data["references_by_class"], {"implemented": 3, "partial": 0, "stub": 3, "absent": 3, "module": 1}) self.assertEqual(data["missing_libraries"], {"libSceFoo.prx": 1, "libSceOther.prx": 1}) self.assertEqual(data["library_mismatch"], 1) kinds = {(record["nid"], record["library"]): record["class"] for record in data["imports"]} self.assertEqual(kinds[(MESSAGE_NID, "libSceMsgDialog.prx")], "stub") self.assertEqual(kinds[("BBBBBBBBBBB", "libSceMsgDialog.prx")], "stub") self.assertEqual(kinds[("UNDEFINED11", "libSceMsgDialog.prx")], "absent") self.assertEqual(kinds[("LOCALLOCAL1", "libSceMsgDialog.prx")], "absent") self.assertEqual(kinds[("EEEEEEEEEEE", "libSceNpCppWebApi.prx")], "module") self.assertIn(f"{MESSAGE_NID} {MESSAGE_NAME}", out) self.assertIn("DDDDDDDDDDD", out) def test_module_imports_are_audited(self): work = self.work stub = nid_names.compute_nid("sceModuleStub") (work.libs / "libSceA.prx").write_bytes(exporting("AAAAAAAAAAA")) (work.libs / "libSceB.prx").write_bytes(exporting(stub)) write_source(work.source, "libSceB", STUB_SOURCE.format(name="sceModuleStub")) (work.modules / "sibling.prx").write_bytes(exporting("BBBBBBBBBBB")) (work.modules / "main.prx").write_bytes(build_elf([ ("AAAAAAAAAAA", 0, GLOBAL_FUNCTION), ("BBBBBBBBBBB", 0, GLOBAL_FUNCTION), (stub, 0, GLOBAL_FUNCTION), ("EEEEEEEEEEE", 0, GLOBAL_FUNCTION)], ["libSceA.prx", "sibling.prx"])) registry = work.root / "game.json" write_registry(registry, []) result = work.root / "audit.json" code, out, err = work.run(registry, "--json", str(result)) self.assertEqual((code, err), (1, "")) data = json.loads(result.read_text()) self.assertEqual(data["references"], 4) self.assertEqual(data["unique_by_class"], {"implemented": 2, "partial": 0, "stub": 1, "absent": 1, "module": 0}) self.assertEqual(data["missing_libraries"], {}) records = {(record["nid"], record["module"]): record for record in data["imports"]} self.assertEqual(records[("AAAAAAAAAAA", "main.prx")]["class"], "implemented") self.assertEqual(records[("AAAAAAAAAAA", "main.prx")]["providers"], ["libSceA.prx"]) self.assertEqual(records[("BBBBBBBBBBB", "main.prx")]["class"], "implemented") self.assertEqual(records[("BBBBBBBBBBB", "main.prx")]["providers"], ["sibling.prx"]) self.assertEqual(records[(stub, "main.prx")]["class"], "stub") self.assertEqual(records[(stub, "main.prx")]["name"], "sceModuleStub") self.assertEqual(records[("EEEEEEEEEEE", "main.prx")]["class"], "absent") self.assertIn("[library not named]", out) self.assertIn("EEEEEEEEEEE (imported by main.prx)", out) def test_module_import_bindings_survive_the_audit(self): work = self.work (work.libs / "libc.prx").write_bytes(exporting()) registry, result = work.root / "game.json", work.root / "audit.json" write_registry(registry, []) symbols = [("optional", 0, 0x22), ("required", 0, GLOBAL_FUNCTION), ("mixed#first", 0, 0x22), ("mixed#second", 0, GLOBAL_FUNCTION)] images = [build_elf(symbols), build_sectionless_elf(symbols), build_sectionless_elf(symbols, gnu_hash=True)] for index, image in enumerate(images): with self.subTest(image=index): (work.modules / "guest.so").write_bytes(image) code, out, err = work.run(registry, "--json", str(result)) self.assertEqual((code, err), (1, "")) data = json.loads(result.read_text()) records = {record["nid"]: record for record in data["imports"]} self.assertTrue(records["optional"]["weak"]) self.assertFalse(records["required"]["weak"]) self.assertFalse(records["mixed"]["weak"]) self.assertEqual(data["references"], 3) self.assertIn("optional (weak import)", out) def test_module_sidecars_do_not_change_the_audit(self): work = self.work (work.libs / "libSceA.prx").write_bytes(exporting("AAAAAAAAAAA")) (work.modules / "provider.prx").write_bytes(exporting("BBBBBBBBBBB")) (work.modules / "consumer.prx").write_bytes(build_elf([ ("AAAAAAAAAAA", 0, GLOBAL_FUNCTION), ("BBBBBBBBBBB", 0, GLOBAL_FUNCTION)], ["libSceA.prx", "provider.prx"])) registry, result = work.root / "game.json", work.root / "audit.json" write_registry(registry, [("BBBBBBBBBBB", "provider.prx")]) code, before_out, err = work.run(registry, "--json", str(result)) self.assertEqual((code, err), (0, "")) before = json.loads(result.read_text()) for name, data in (("README", b"module notes\n"), ("notes.txt", b"EEEEEEEEEEE\n"), ("provider.registry.json", b'[{"nid":"EEEEEEEEEEE","library":"missing.prx"}]'), (".DS_Store", b"\x00\xffmetadata"), ("empty.txt", b"")): (work.modules / name).write_bytes(data) code, out, err = work.run(registry, "--json", str(result)) self.assertEqual((code, err), (0, "")) self.assertEqual(out, before_out) self.assertEqual(json.loads(result.read_text()), before) write_registry(registry, [("EEEEEEEEEEE", "notes.txt")]) code, _, err = work.run(registry, "--json", str(result)) self.assertEqual((code, err), (1, "")) data = json.loads(result.read_text()) record = next(record for record in data["imports"] if record["nid"] == "EEEEEEEEEEE") self.assertEqual((record["class"], record["providers"]), ("absent", [])) self.assertEqual(data["missing_libraries"], {"notes.txt": 1}) def test_elf_modules_are_audited_regardless_of_filename(self): work = self.work (work.libs / "libSceA.prx").write_bytes(exporting("AAAAAAAAAAA")) (work.modules / "provider.data").write_bytes(build_elf([ ("AAAAAAAAAAA", 0, GLOBAL_FUNCTION), ("BBBBBBBBBBB", 1, GLOBAL_FUNCTION)], ["libSceA.prx"])) (work.modules / "consumer.prx").write_bytes(build_elf([ ("BBBBBBBBBBB", 0, GLOBAL_FUNCTION)], ["provider.data"])) registry, result = work.root / "game.json", work.root / "audit.json" write_registry(registry, [("BBBBBBBBBBB", "provider.data")]) code, _, err = work.run(registry, "--json", str(result)) self.assertEqual((code, err), (0, "")) data = json.loads(result.read_text()) self.assertEqual(data["references"], 3) self.assertEqual(data["unique_by_class"], {"implemented": 2, "partial": 0, "stub": 0, "absent": 0, "module": 1}) self.assertEqual(data["missing_libraries"], {}) records = {(record["nid"], record.get("module")): record for record in data["imports"]} self.assertEqual(records[("AAAAAAAAAAA", "provider.data")]["providers"], ["libSceA.prx"]) self.assertEqual(records[("BBBBBBBBBBB", "consumer.prx")]["providers"], ["provider.data"]) def test_module_matches_imports_that_name_another_module_suffix(self): work = self.work (work.libs / "libc.prx").write_bytes(exporting()) (work.modules / "shipping.prx").write_bytes(exporting("AAAAAAAAAAA")) registry, result = work.root / "game.json", work.root / "audit.json" write_registry(registry, [("AAAAAAAAAAA", "shipping.debug_prx")]) code, _, err = work.run(registry, "--json", str(result)) self.assertEqual((code, err), (0, "")) data = json.loads(result.read_text()) self.assertEqual(data["missing_libraries"], {}) record = next(record for record in data["imports"] if record["library"] == "shipping.debug_prx") self.assertEqual(record["class"], "module") def test_module_suffix_aliases_require_suffixes_on_both_filenames(self): work = self.work registry, report = work.root / "game.registry.json", work.root / "audit.json" cases = (("shipping.prx", "shipping"), ("shipping", "shipping.prx"), ("shipping.data", "shipping.data.prx"), (".prx", ".sprx")) for windows in (False, True): (work.libs / "libc.prx").write_bytes(build_pe([]) if windows else exporting()) for filename, alias in cases: module = work.modules / filename module.write_bytes(exporting("AAAAAAAAAAA")) try: for needed in (filename, alias): with self.subTest(windows=windows, filename=filename, needed=needed): write_registry(registry, [("AAAAAAAAAAA", needed)]) import_audit.needed_registry_path(registry).write_text(json.dumps([needed]), encoding="utf-8") code, _, err = work.run(registry, "--json", str(report)) exact = needed == filename self.assertEqual((code, err), (0 if exact else 1, "")) data = json.loads(report.read_text(encoding="utf-8")) self.assertEqual(data["missing_libraries"], {} if exact else {needed: 1}) self.assertEqual(data["imports"][0]["class"], "module" if exact else "absent") self.assertEqual(data["references"], 1) finally: module.unlink() def test_module_names_ignore_case_for_windows_output_only(self): imports = [("AAAAAAAAAAA", "SDK-Shipping.debug_prx", False)] records, missing = import_audit.audit(imports, {}, {}, set(), import_audit.ModuleNames({"sdk-shipping.prx"}, fold=True)) self.assertEqual((records[0]["class"], missing), ("module", {})) records, missing = import_audit.audit(imports, {}, {}, set(), import_audit.ModuleNames({"sdk-shipping.prx"})) self.assertEqual((records[0]["class"], missing), ("absent", {"SDK-Shipping.debug_prx": 1})) def test_windows_module_suffix_case_does_not_report_existing_modules_as_missing(self): work = self.work (work.libs / "libc.prx").write_bytes(build_pe([])) registry, report = work.root / "game.registry.json", work.root / "audit.json" cases = (("sdk-shipping.prx", "SDK-Shipping.DEBUG_PRX"), ("SDK-SHIPPING.PRX", "sdk-shipping.sprx"), ("Sdk-Shipping.SuPrX", "SDK-SHIPPING.debug_prx"), ("SDK-SHIPPING.DEBUG_PRX", "sdk-shipping.PRX")) for filename, needed in cases: with self.subTest(filename=filename, needed=needed): module = work.modules / filename module.write_bytes(exporting("AAAAAAAAAAA")) try: write_registry(registry, [("AAAAAAAAAAA", needed)]) import_audit.needed_registry_path(registry).write_text(json.dumps([needed]), encoding="utf-8") code, _, err = work.run(registry, "--json", str(report), "--strict-providers") self.assertEqual((code, err), (0, "")) data = json.loads(report.read_text(encoding="utf-8")) self.assertEqual(data["missing_libraries"], {}) self.assertEqual(data["unique_by_class"], {"implemented": 0, "partial": 0, "stub": 0, "absent": 0, "module": 1}) self.assertEqual(data["references"], 1) self.assertEqual(data["imports"][0]["library"], needed) finally: module.unlink() def test_elf_module_suffix_case_stays_significant(self): work = self.work (work.libs / "libc.prx").write_bytes(exporting()) (work.modules / "sdk-shipping.prx").write_bytes(exporting("AAAAAAAAAAA")) registry, report = work.root / "game.registry.json", work.root / "audit.json" for suffix in ("PRX", "SPRX", "SUPRX", "DEBUG_PRX"): needed = "sdk-shipping." + suffix with self.subTest(needed=needed): write_registry(registry, [("AAAAAAAAAAA", needed)]) code, _, err = work.run(registry, "--json", str(report)) self.assertEqual((code, err), (1, "")) data = json.loads(report.read_text(encoding="utf-8")) self.assertEqual(data["missing_libraries"], {needed: 1}) self.assertEqual(data["unique_by_class"], {"implemented": 0, "partial": 0, "stub": 0, "absent": 1, "module": 0}) def test_converted_module_matches_imports_of_its_base_name(self): work = self.work (work.libs / "libc.prx").write_bytes(exporting()) (work.modules / "libSceFace.prx.guest.prx").write_bytes(exporting("AAAAAAAAAAA")) registry, result = work.root / "game.json", work.root / "audit.json" write_registry(registry, [("AAAAAAAAAAA", "libSceFace.prx")]) code, _, err = work.run(registry, "--json", str(result)) self.assertEqual((code, err), (0, "")) data = json.loads(result.read_text()) self.assertEqual(data["missing_libraries"], {}) record = next(record for record in data["imports"] if record["library"] == "libSceFace.prx") self.assertEqual(record["class"], "module") def test_bad_named_modules_and_malformed_elf_are_not_ignored(self): work = self.work (work.libs / "libc.prx").write_bytes(exporting()) registry, result = work.root / "game.json", work.root / "audit.json" write_registry(registry, []) previous = b'{"previous":"report"}\n' invalid_class = bytearray(exporting()) invalid_class[4] = 1 candidates = [("guest" + suffix, b"module placeholder\n") for suffix in (".prx", ".sprx", ".suprx", ".debug_prx", ".elf", ".PRX")] candidates += [("truncated.data", b"\x7fELF"), ("unsupported.data", invalid_class)] for name, image in candidates: module = work.modules / name module.write_bytes(image) for exists in (False, True): with self.subTest(name=name, existing_report=exists): if exists: result.write_bytes(previous) else: result.unlink(missing_ok=True) code, out, err = work.run(registry, "--json", str(result)) self.assertEqual((code, out), (2, "")) self.assertIn(name, err) self.assertIn("ELF", err) if exists: self.assertEqual(result.read_bytes(), previous) else: self.assertFalse(result.exists()) module.unlink() def test_pe_and_self_modules_are_not_ignored_as_sidecars(self): work = self.work (work.libs / "libc.prx").write_bytes(exporting()) registry, result = work.root / "game.json", work.root / "audit.json" write_registry(registry, []) previous = b'{"previous":"report"}\n' for image in (build_pe(["alpha"]), b"\x4f\x15\x3d\x1d" + bytes(64), b"\x54\x14\xf5\xee" + bytes(64)): for name in ("guest.prx", "metadata.txt"): module = work.modules / name module.write_bytes(image) with self.subTest(name=name, magic=bytes(image[:4])): result.write_bytes(previous) code, out, err = work.run(registry, "--json", str(result)) self.assertEqual((code, out), (2, "")) self.assertIn(name, err) self.assertIn("ELF", err) self.assertEqual(result.read_bytes(), previous) module.unlink() def test_module_needed_library_without_a_file_is_missing(self): work = self.work (work.libs / "libc.prx").write_bytes(exporting()) (work.modules / "main.prx").write_bytes(build_elf([], ["libSceMissing.prx"])) registry = work.root / "game.json" write_registry(registry, []) result = work.root / "audit.json" code, out, err = work.run(registry, "--json", str(result)) self.assertEqual((code, err), (1, "")) data = json.loads(result.read_text()) self.assertEqual((data["references"], data["imports"]), (0, [])) self.assertEqual(data["missing_libraries"], {"libSceMissing.prx": 1}) self.assertIn("libSceMissing.prx", out) def test_missing_library_is_located_next_to_the_registry(self): work = self.work (work.libs / "libc.prx").write_bytes(exporting()) (work.modules / "main.prx").write_bytes(build_elf([], ["libSceMissing.prx", "libSceGone.prx"])) registry = work.root / "game.json" write_registry(registry, []) elsewhere = work.root / "in" / "Media Modules" elsewhere.mkdir(parents=True) (elsewhere / "libSceMissing.prx").write_bytes(exporting("DDDDDDDDDDD")) code, out, err = work.run(registry) self.assertEqual((code, err), (1, "")) self.assertIn(f"libSceMissing.prx is in {elsewhere.resolve()}: pass that directory with --modules", out) self.assertNotIn("libSceGone.prx is in", out) code, out, err = work.run(registry, "--modules", str(elsewhere)) self.assertNotIn("libSceMissing.prx is in", out) def test_conflicting_module_files_preserve_or_leave_no_json_report(self): work = self.work first, second = work.modules, work.root / "other-modules" second.mkdir() (work.libs / "libc.prx").write_bytes(exporting()) (first / "main.prx").write_bytes(build_elf([ ("AAAAAAAAAAA", 0, GLOBAL_FUNCTION), ("BBBBBBBBBBB", 1, GLOBAL_FUNCTION)], ["libSceMissing.prx"])) registry, result = work.root / "game.json", work.root / "audit.json" write_registry(registry, []) code, _, err = work.run(registry, "--json", str(result)) self.assertEqual((code, err), (1, "")) data = json.loads(result.read_text()) self.assertEqual(data["unique_by_class"]["absent"], 1) self.assertEqual(data["missing_libraries"], {"libSceMissing.prx": 1}) (second / "main.prx").write_bytes(exporting("CCCCCCCCCCC")) previous = b'{"previous": "complete report"}\n' for directories in ((first, second), (second, first)): for exists in (False, True): with self.subTest(directories=directories, existing_report=exists): if exists: result.write_bytes(previous) else: result.unlink(missing_ok=True) with patch.object(work, "modules", directories[0]): code, out, err = work.run(registry, "--modules", str(directories[1]), "--json", str(result)) self.assertEqual((code, out), (2, "")) self.assertIn("ambiguous guest module main.prx", err) self.assertIn(str(first / "main.prx"), err) self.assertIn(str(second / "main.prx"), err) if exists: self.assertEqual(result.read_bytes(), previous) else: self.assertFalse(result.exists()) def test_identical_module_files_are_audited_once(self): work = self.work first, second = work.modules, work.root / "other-modules" second.mkdir() (work.libs / "libc.prx").write_bytes(exporting()) image = build_elf([ ("AAAAAAAAAAA", 0, GLOBAL_FUNCTION), ("BBBBBBBBBBB", 1, GLOBAL_FUNCTION)], ["libSceMissing.prx"]) for directory in (first, second): (directory / "main.prx").write_bytes(image) (first / "consumer.prx").write_bytes(build_elf([("BBBBBBBBBBB", 0, GLOBAL_FUNCTION)])) registry, result = work.root / "game.json", work.root / "audit.json" write_registry(registry, []) for directories in ((first, second, first), (second, first, second)): with self.subTest(directories=directories), patch.object(work, "modules", directories[0]): extra = [argument for directory in directories[1:] for argument in ("--modules", str(directory))] code, _, err = work.run(registry, *extra, "--json", str(result)) self.assertEqual((code, err), (1, "")) data = json.loads(result.read_text()) self.assertEqual((data["references"], data["unique_imports"]), (2, 2)) self.assertEqual(data["unique_by_class"], {"implemented": 1, "partial": 0, "stub": 0, "absent": 1, "module": 0}) self.assertEqual(data["references_by_class"], data["unique_by_class"]) self.assertEqual(data["missing_libraries"], {"libSceMissing.prx": 1}) records = {record["nid"]: record for record in data["imports"]} self.assertEqual((records["AAAAAAAAAAA"]["class"], records["AAAAAAAAAAA"]["references"]), ("absent", 1)) self.assertEqual((records["BBBBBBBBBBB"]["class"], records["BBBBBBBBBBB"]["providers"]), ("implemented", ["main.prx"])) def test_module_files_must_be_elf(self): work = self.work (work.libs / "libc.prx").write_bytes(exporting()) (work.modules / "guest.prx").write_bytes(build_pe(["alpha"])) registry = work.root / "game.json" write_registry(registry, []) code, out, err = work.run(registry) self.assertEqual((code, out), (2, "")) self.assertIn("guest.prx", err) self.assertIn("ELF", err) def test_sectionless_module_imports_and_exports_are_audited(self): work = self.work (work.libs / "libSceA.prx").write_bytes(exporting("AAAAAAAAAAA")) (work.modules / "stripped.prx").write_bytes(build_sectionless_elf([ ("AAAAAAAAAAA", 0, GLOBAL_FUNCTION), ("BBBBBBBBBBB", 1, GLOBAL_FUNCTION)], ["libSceA.prx"])) (work.modules / "main.prx").write_bytes(build_elf([("BBBBBBBBBBB", 0, GLOBAL_FUNCTION)], [])) registry = work.root / "game.json" write_registry(registry, []) result = work.root / "audit.json" code, out, err = work.run(registry, "--json", str(result)) self.assertEqual((code, err), (0, "")) data = json.loads(result.read_text()) self.assertEqual(data["references"], 2) records = {(record["nid"], record["module"]): record for record in data["imports"]} self.assertEqual(records[("AAAAAAAAAAA", "stripped.prx")]["class"], "implemented") self.assertEqual(records[("BBBBBBBBBBB", "main.prx")]["class"], "implemented") def test_elf_with_out_of_file_section_table_uses_program_headers(self): path = Path("module.prx") image = build_sectionless_elf([ ("AAAAAAAAAAA", 0, GLOBAL_FUNCTION), ("BBBBBBBBBBB", 1, GLOBAL_FUNCTION)], ["libSceA.prx"]) expected_imports = import_audit.elf_imports(path, image) expected_exports = import_audit.elf_exports(path, image) self.assertEqual(expected_imports, ({"AAAAAAAAAAA": False}, {"libSceA.prx"})) self.assertEqual(expected_exports, {"BBBBBBBBBBB"}) for section_offset in (len(image), len(image) + 64): for section_count in (1, 0): modified = bytearray(image) struct.pack_into(" 1) NotImplemented_nid_no_patch(__func__); return value; }\n" "int APS5_VABI sceBlock(int value) { if (value) { NotImplemented_nid_no_patch(__func__); } return 0; }\n" "int APS5_VABI sceElse(int value) { if (value) return 1; else Fail(); return 0; }\n" "int APS5_VABI sceGuarded(int value) { Check(value); return value; }\n" "int APS5_VABI sceLoop(int value) { for (int i = 0; i < value; ++i) Fail(); return 0; }\n" "int APS5_VABI sceSwitch(int value) { switch (value) { case 0: return 0; default: Fail(); } return 1; }\n" "int APS5_VABI sceTernary(int value) { return value ? value : NotImplemented_nid_no_patch(__func__); }\n" "int APS5_VABI sceShort(int value) { return value && NotImplemented_nid_no_patch(__func__); }\n" "int APS5_VABI sceLambda(int value) { auto f = [&]() { Fail(); }; return value; }\n" "int APS5_VABI sceMember(int value) { if (value) Fail(); other.Fail(); return value; }\n" "int APS5_VABI sceHost(int value) {\n#if defined(_WIN32)\n return value;\n#elif defined(__linux__)\n" " return value + 1;\n#else\n NotImplemented_nid_no_patch(__func__);\n return 0;\n#endif\n}\n" "int APS5_VABI sceHostHelper(int value) {\n#ifdef HOST_WITHOUT_IT\n Fail();\n#endif\n return value;\n}\n" "#ifdef ONLY_HERE\nint APS5_VABI sceOuter(int value) { NotImplemented_nid_no_patch(__func__); return 0; }\n#endif\n" "int APS5_VABI sceEveryHost(int value) {\n#if defined(_WIN32)\n NotImplemented_nid_no_patch(__func__);\n#else\n" " NotImplemented_nid_no_patch(__func__);\n#endif\n return 0;\n}\n" "int APS5_VABI sceEveryElif(int value) {\n#if defined(HOST_A)\n Fail();\n#elif defined(HOST_B)\n" " NotImplemented_nid_no_patch(__func__);\n#else\n Fail();\n#endif\n return 0;\n}\n" "int APS5_VABI sceNested(int value) {\n#if defined(HOST_A)\n#ifdef FEATURE\n return value;\n#else\n Fail();\n#endif\n" "#else\n Fail();\n#endif\n return 0;\n}\n" "void SoftFail(int value) { if (value == 0) return; NotImplemented_nid_no_patch(__func__); }\n" "void HardFail(int value) { if (value == 0) throw std::invalid_argument(\"zero\"); NotImplemented_nid_no_patch(__func__); }\n" "int APS5_VABI sceGuardReturn(int value) { if (value == 0) return 0; NotImplemented_nid_no_patch(__func__); return 1; }\n" "int APS5_VABI sceGuardThrow(int value) { if (value == 0) throw std::invalid_argument(\"zero\"); " "NotImplemented_nid_no_patch(__func__); return 1; }\n" "int APS5_VABI sceGuardBlock(int value) { { if (value == 0) return 0; } Fail(); return 1; }\n" "int APS5_VABI sceGuardGoto(int value) { if (value == 0) goto done; Fail(); done: return 0; }\n" "int APS5_VABI sceGuardHost(int value) {\n#ifdef HOST_A\n if (value == 0) return 0;\n#endif\n Fail();\n return 1;\n}\n" "int APS5_VABI sceSoftHelper(int value) { SoftFail(value); return 0; }\n" "int APS5_VABI sceHardHelper(int value) { HardFail(value); return 0; }\n" "int APS5_VABI scePlainBlock(int value) { { NotImplemented_nid_no_patch(__func__); } return 0; }\n" "int APS5_VABI sceNestedBlock(int value) { value += 1; { { Fail(); } } return value; }\n" "int APS5_VABI sceBlockThenGuard(int value) { { if (value == 0) return 0; NotImplemented_nid_no_patch(__func__); } return 1; }\n") write_source(source, "libShared", '#include "prx/libGuard/Export.cpp"\n') stubs, partial = import_audit.source_stub_classes(source) names = ("sceDirect", "sceReturned", "sceWrapped", "sceIf", "sceBlock", "sceElse", "sceGuarded", "sceLoop", "sceSwitch", "sceTernary", "sceShort", "sceLambda", "sceMember", "sceHost", "sceHostHelper", "sceOuter", "sceEveryHost", "sceEveryElif", "sceNested", "sceGuardReturn", "sceGuardThrow", "sceGuardBlock", "sceGuardGoto", "sceGuardHost", "sceSoftHelper", "sceHardHelper", "scePlainBlock", "sceNestedBlock", "sceBlockThenGuard") kinds = {name: "partial" if ("libGuard.prx", nid_names.compute_nid(name)) in partial else "stub" for name in names} self.assertEqual({name for name, kind in kinds.items() if kind == "stub"}, {"sceDirect", "sceReturned", "sceWrapped", "sceOuter", "sceEveryHost", "sceEveryElif", "sceGuardThrow", "sceHardHelper", "scePlainBlock", "sceNestedBlock"}) self.assertEqual(set(stubs), {(library, nid_names.compute_nid(name)) for library in ("libGuard.prx", "libShared.prx") for name in names}) self.assertEqual(partial, {(library, nid_names.compute_nid(name)) for library in ("libGuard.prx", "libShared.prx") for name, kind in kinds.items() if kind == "partial"}) def test_many_host_branches_only_fail_when_every_combination_must_be_read(self): blocks = "".join(f"#ifdef HOST_{index}\n value += {index};\n#endif\n" for index in range(9)) source = self.work.source write_source(source, "libHosts", "void Fail() { NotImplemented_nid_no_patch(__func__); }\n" "int APS5_VABI sceStub(int value) { NotImplemented_nid_no_patch(__func__); return 0; }\n" f"int APS5_VABI sceHosts(int value) {{\n{blocks} return value;\n}}\n" f"int APS5_VABI sceLastHost(int value) {{\n{blocks}#ifdef HOST_LAST\n Fail();\n#endif\n return value;\n}}\n" f"int APS5_VABI sceNestedHosts(int value) {{\n#ifdef HOST_OUTER\n{blocks}#else\n Fail();\n#endif\n return value;\n}}\n") stubs, partial = import_audit.source_stub_classes(source) stub, hosts, last, nested = (("libHosts.prx", nid_names.compute_nid(name)) for name in ("sceStub", "sceHosts", "sceLastHost", "sceNestedHosts")) self.assertEqual(set(stubs), {stub, last, nested}) self.assertEqual(partial, {last, nested}) for every in (f"{blocks} Fail();\n", f"#ifdef HOST_OUTER\n{blocks}#endif\n Fail();\n"): with self.subTest(every=every): write_source(source, "libHosts", "void Fail() { NotImplemented_nid_no_patch(__func__); }\n" f"int APS5_VABI sceEveryHost(int value) {{\n{every} return value;\n}}\n") with self.assertRaisesRegex(import_audit.AuditError, "more than 256 preprocessor branch combinations"): import_audit.source_stub_classes(source) def test_partial_import_is_its_own_class(self): direct, guarded = nid_names.compute_nid("sceDirect"), nid_names.compute_nid("sceGuarded") stubs = {("libA.prx", direct): "sceDirect", ("libA.prx", guarded): "sceGuarded", ("libB.prx", guarded): "sceGuarded"} partial = {("libB.prx", guarded)} exports = {direct: ["libA.prx"], guarded: ["libA.prx", "libB.prx"]} imports = [(direct, "libA.prx", False), (guarded, "libA.prx", False), (guarded, "libA.prx", False)] records, _ = import_audit.audit(imports, exports, stubs, {"libA.prx", "libB.prx"}, set(), partial=partial) self.assertEqual({record["nid"]: (record["class"], record["name"]) for record in records}, {direct: ("stub", "sceDirect"), guarded: ("partial", "sceGuarded")}) summary = import_audit.summarize(records, 3, {}) self.assertEqual(summary["references_by_class"], {"implemented": 0, "partial": 2, "stub": 1, "absent": 0, "module": 0}) text = import_audit.render(summary, records, {}, []) self.assertIn("Stub: exported but throws when called (1)", text) self.assertIn("Partial: exported, throws only for inputs whose behaviour is unknown (1)", text) def test_shared_source_cycle_fails(self): source = self.work.source for library, owner in (("libA", "libB"), ("libB", "libA")): shared = source / library shared.mkdir() (shared / "CMakeLists.txt").write_text(f'include(${{CMAKE_CURRENT_SOURCE_DIR}}/../{owner}/OwnerLibrary.cmake)\n') with self.assertRaisesRegex(import_audit.AuditError, "shared source cycle.*libA.*libB"): import_audit.source_stubs(source) def test_multiple_aliases_of_one_function_keep_the_same_classification(self): aliases = 'APS5_EXPORT("AAAAAAAAAAA", sceExample);\nAPS5_EXPORT("BBBBBBBBBBB", sceExample);\n' for body, classification in ((STUB_SOURCE, "stub"), (DONE_SOURCE, "implemented")): with self.subTest(classification=classification): write_source(self.work.source, "libSceExample", aliases + body.format(name="sceExample")) stubs = import_audit.source_stubs(self.work.source) records, _ = import_audit.audit( [(nid, "libSceExample.prx", False) for nid in ("AAAAAAAAAAA", "BBBBBBBBBBB")], {nid: ["libSceExample.prx"] for nid in ("AAAAAAAAAAA", "BBBBBBBBBBB")}, stubs, {"libSceExample.prx"}, set()) self.assertEqual([record["class"] for record in records], [classification, classification]) def test_formatted_export_aliases_remain_stubs(self): work = self.work nid = "AAAAAAAAAAA" (work.libs / "libSceAlias.prx").write_bytes(exporting(nid)) registry = work.root / "game.json" result = work.root / "audit.json" write_registry(registry, [(nid, "libSceAlias.prx")]) aliases = [ 'APS5_EXPORT (\n "AAAAAAAAAAA",\n NAME\n);\n', 'APS5_EXPORT(/* nid */ "AAAAAAAAAAA", /* name */ NAME);\n', 'APS5_EX\\\nPORT("AAAAAAAAAAA", NAME);\n', ] for name in ("sceAliasUnknown", "sceAliasedCall", "sceAliasedCall_nid_no_patch"): for alias in aliases: with self.subTest(name=name, alias=alias): write_source(work.source, "libSceAlias", alias.replace("NAME", name) + STUB_SOURCE.format(name=name)) code, _, err = work.run(registry, "--json", str(result)) self.assertEqual((code, err), (0, "")) record, = json.loads(result.read_text())["imports"] self.assertEqual((record["class"], record["name"]), ("stub", name)) def test_comments_and_literals_do_not_override_export_aliases(self): work = self.work nid, name = "AAAAAAAAAAA", "sceAliasUnknown" (work.libs / "libSceAlias.prx").write_bytes(exporting(nid)) registry = work.root / "game.json" result = work.root / "audit.json" write_registry(registry, [(nid, "libSceAlias.prx")]) source = 'APS5_EXPORT("AAAAAAAAAAA", sceAliasUnknown);\n' + STUB_SOURCE.format(name=name) decoys = [ '// APS5_EXPORT("BBBBBBBBBBB", sceAliasUnknown);\n', '/* APS5_EXPORT("BBBBBBBBBBB", sceAliasUnknown); */\n', 'auto example = R"text(APS5_EXPORT("BBBBBBBBBBB", sceAliasUnknown);)text";\n', ] for decoy in decoys: with self.subTest(decoy=decoy): write_source(work.source, "libSceAlias", source + decoy) code, _, err = work.run(registry, "--json", str(result)) self.assertEqual((code, err), (0, "")) record, = json.loads(result.read_text())["imports"] self.assertEqual((record["class"], record["name"]), ("stub", name)) @unittest.skipUnless((import_audit.ROOT / ".git").exists(), "needs a git checkout") def test_json_is_stamped_with_the_source_commit(self): work = self.work import_audit.subprocess.run(["git", "-C", str(work.source), "init", "-q"], check=True) import_audit.subprocess.run(["git", "-C", str(work.source), "-c", "user.name=Audit test", "-c", "user.email=audit@example.invalid", "commit", "--allow-empty", "-qm", "Source stamp fixture"], check=True) commit, dirty = import_audit.git_stamp(work.source) (work.libs / "libSceA.prx").write_bytes(exporting("AAAAAAAAAAA")) registry = work.root / "game.json" write_registry(registry, [("AAAAAAAAAAA", "libSceA.prx")]) result = work.root / "stamped.json" work.run(registry, "--json", str(result)) data = json.loads(result.read_text()) self.assertRegex(data["source_commit"], r"^[0-9a-f]{40}$") self.assertEqual((data["source_commit"], data["source_dirty"]), (commit, dirty)) self.assertEqual(data["registries"], [str(registry)]) def test_nid_anchor_matches_the_known_pair(self): self.assertEqual(nid_names.compute_nid(MESSAGE_NAME), MESSAGE_NID) def test_unsupported_elf_versions_preserve_the_audit_report(self): work = self.work registry, result = work.root / "game.json", work.root / "audit.json" write_registry(registry, [("AAAAAAAAAAA", "libSceA.prx")]) library = work.libs / "libSceA.prx" images = (exporting("AAAAAAAAAAA"), build_sectionless_elf([("AAAAAAAAAAA", 1, GLOBAL_FUNCTION)]), build_sectionless_elf([("AAAAAAAAAAA", 1, GLOBAL_FUNCTION)], gnu_hash=True)) previous = b'{"previous":"complete report"}\n' for index, valid in enumerate(images): for offset, layout in ((6, " libSceIme.prx", out) self.assertIn("Importer: libSceVideoOut.prx Chain: libSceVideoOut.prx -> libSceKeyboard.prx", out) self.assertNotIn("KERNEL32", out) def test_transitive_host_dependency_reports_chain(self): work = self.work (work.libs / "libSceA.prx").write_bytes(build_pe_with_imports(["libSceB.prx"], ["AAAAAAAAAAA"])) (work.libs / "libSceB.prx").write_bytes(build_pe_with_imports(["libSceMissing.prx"], ["BBBBBBBBBBB"])) registry = work.root / "game.json" write_registry(registry, []) result = work.root / "audit.json" code, out, err = work.run(registry, "--json", str(result)) self.assertEqual((code, err), (1, "")) data = json.loads(result.read_text()) self.assertEqual(data["missing_host_dependencies"], [ {"importer": "libSceA.prx", "chain": ["libSceA.prx", "libSceB.prx", "libSceMissing.prx"]}, {"importer": "libSceB.prx", "chain": ["libSceB.prx", "libSceMissing.prx"]}, ]) self.assertIn("Importer: libSceA.prx Chain: libSceA.prx -> libSceB.prx -> libSceMissing.prx", out) def test_host_dependency_satisfied_by_module(self): work = self.work (work.libs / "libSceA.prx").write_bytes(build_pe_with_imports(["libSceIme.prx"], ["AAAAAAAAAAA"])) (work.modules / "libSceIme.prx").write_bytes(exporting()) registry = work.root / "game.json" write_registry(registry, []) code, out, err = work.run(registry) self.assertEqual((code, err), (0, "")) self.assertNotIn("Host dependencies", out) def test_complete_host_dependencies_pass(self): work = self.work (work.libs / "libc.prx").write_bytes(build_pe_with_imports([], ["AAAAAAAAAAA"])) (work.libs / "libSceVideoOut.prx").write_bytes(build_pe_with_imports(["libc.prx"], ["BBBBBBBBBBB"])) registry = work.root / "game.json" write_registry(registry, []) code, out, err = work.run(registry) self.assertEqual((code, err), (0, "")) self.assertNotIn("Host dependencies", out) def test_pe_host_dependencies_ignore_library_name_case(self): work = self.work (work.libs / "libSceB.prx").write_bytes(build_pe_with_imports([], ["BBBBBBBBBBB"])) registry, report = work.root / "game.json", work.root / "audit.json" write_registry(registry, []) for dependency in ("LIBSCEB.PRX", "libsceb.prx", "LiBsCeB.PrX"): with self.subTest(dependency=dependency): (work.libs / "libSceA.prx").write_bytes(build_pe_with_imports([dependency], ["AAAAAAAAAAA"])) code, out, err = work.run(registry, "--json", str(report)) self.assertEqual((code, err), (0, ""), out) self.assertEqual(json.loads(report.read_text())["missing_host_dependencies"], []) def test_pe_host_dependency_case_does_not_hide_a_transitive_gap(self): work = self.work (work.libs / "libSceA.prx").write_bytes(build_pe_with_imports(["LIBSCEB.PRX"])) (work.libs / "libSceB.prx").write_bytes(build_pe_with_imports(["LIBSCEMISSING.PRX"])) registry, report = work.root / "game.json", work.root / "audit.json" write_registry(registry, []) code, _, err = work.run(registry, "--json", str(report)) self.assertEqual((code, err), (1, "")) self.assertEqual(json.loads(report.read_text())["missing_host_dependencies"], [ {"importer": "libSceA.prx", "chain": ["libSceA.prx", "libSceB.prx", "LIBSCEMISSING.PRX"]}, {"importer": "libSceB.prx", "chain": ["libSceB.prx", "LIBSCEMISSING.PRX"]}, ]) def test_pe_host_dependency_can_find_a_module_with_different_case(self): work = self.work (work.libs / "libSceA.prx").write_bytes(build_pe_with_imports(["LIBSCEIME.PRX"])) (work.modules / "libSceIme.prx").write_bytes(exporting()) registry = work.root / "game.json" write_registry(registry, []) code, out, err = work.run(registry) self.assertEqual((code, err), (0, ""), out) def test_pe_host_dependency_can_find_a_converted_pe_module_with_different_case(self): work = self.work (work.libs / "libSceA.prx").write_bytes(build_pe_with_imports(["LIBSCEIME.PRX"])) (work.modules / "libSceIme.prx.guest.prx").write_bytes(build_pe_with_imports([])) registry, report = work.root / "game.json", work.root / "audit.json" write_registry(registry, []) code, out, err = work.run(registry, "--json", str(report)) self.assertEqual((code, err), (0, ""), out) self.assertEqual(json.loads(report.read_text())["missing_host_dependencies"], []) def test_elf_host_dependency_case_stays_significant_in_a_mixed_graph(self): work = self.work (work.libs / "libSceA.prx").write_bytes(build_pe_with_imports(["LIBSCEB.PRX"])) (work.libs / "libSceB.prx").write_bytes(build_elf([], ["LIBSCEC.PRX"])) (work.libs / "libSceC.prx").write_bytes(exporting()) registry, report = work.root / "game.json", work.root / "audit.json" write_registry(registry, []) code, _, err = work.run(registry, "--json", str(report)) self.assertEqual((code, err), (1, "")) self.assertEqual(json.loads(report.read_text())["missing_host_dependencies"], [ {"importer": "libSceA.prx", "chain": ["libSceA.prx", "libSceB.prx", "LIBSCEC.PRX"]}, {"importer": "libSceB.prx", "chain": ["libSceB.prx", "LIBSCEC.PRX"]}, ]) def test_pe_host_dependency_rejects_ambiguous_case_variants(self): work = self.work other = work.root / "other-libs" other.mkdir() (work.libs / "libSceA.prx").write_bytes(build_pe_with_imports(["LIBSCEB.PRX"])) (work.libs / "libSceB.prx").write_bytes(build_pe_with_imports([], ["AAAAAAAAAAA"])) (other / "LIBSCEB.prx").write_bytes(build_pe_with_imports([], ["BBBBBBBBBBB"])) registry, report = work.root / "game.json", work.root / "audit.json" write_registry(registry, []) report.write_bytes(b"previous complete report") code, out, err = work.run(registry, "--libs", str(other), "--json", str(report)) self.assertEqual((code, out), (2, "")) self.assertIn("ambiguous Windows host library", err) self.assertEqual(report.read_bytes(), b"previous complete report") def test_pe_host_dependency_cycle_with_different_case_terminates(self): work = self.work (work.libs / "libSceA.prx").write_bytes(build_pe_with_imports(["LIBSCEB.PRX"])) (work.libs / "libSceB.prx").write_bytes(build_pe_with_imports(["LIBSCEA.PRX"])) registry = work.root / "game.json" write_registry(registry, []) code, out, err = work.run(registry) self.assertEqual((code, err), (0, ""), out) def test_elf_host_dependencies_are_walked(self): work = self.work (work.libs / "libSceA.prx").write_bytes(build_elf( [("AAAAAAAAAAA", 1, GLOBAL_FUNCTION)], ["libSceGhost.prx", "KERNEL32.dll"])) registry = work.root / "game.json" write_registry(registry, []) result = work.root / "audit.json" code, out, err = work.run(registry, "--json", str(result)) self.assertEqual((code, err), (1, "")) data = json.loads(result.read_text()) self.assertEqual(data["missing_host_dependencies"], [ {"importer": "libSceA.prx", "chain": ["libSceA.prx", "libSceGhost.prx"]}, ]) self.assertNotIn("KERNEL32", out) def test_conflicting_host_libraries_are_ambiguous(self): first, second = self.work.libs, self.work.root / "other-libs" second.mkdir() (first / "libSceA.prx").write_bytes(build_pe_with_imports(["libSceB.prx"], ["AAAAAAAAAAA"])) (second / "libSceA.prx").write_bytes(build_pe_with_imports([], ["AAAAAAAAAAA"])) with self.assertRaisesRegex(import_audit.AuditError, "ambiguous"): import_audit.built_host_dependencies([first, second]) def test_elf_unnamed_defined_symbol_is_not_an_export(self): image = bytearray(exporting("alpha")) section_offset = struct.unpack_from("