Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions docs/user/USAGE.md
Original file line number Diff line number Diff line change
Expand Up @@ -237,6 +237,8 @@ The report also lists needed libraries that have no file in `--libs` or `--modul
| `--names <file>` | NID and name per line, as in `aerolib.csv` from [`tools/nid_names.py`](../../tools/nid_names.py); names the absent imports. Never downloaded; a name that does not hash to its NID is marked. |
| `--json <file>` | Full result, stamped with the repository's commit and whether its tree was modified. |

Registry `nid` fields may contain either encoded NIDs or ordinary ELF symbol names such as `strcmp`. Ordinary names are matched against literal exports, without hashing them into NIDs.

Exit codes: `0` nothing blocks loading, `1` there are `absent` imports or needed libraries without a file, `2` an input could not be read; the message names the file and the value.

A module registry lists `nid`, `library` and `targetOffset` for each relocation against a function that no other bundled module exports, without the `#` suffix of the executable's NIDs.
6 changes: 2 additions & 4 deletions tools/import_audit.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import argparse
import hashlib
import json
import re
import struct
import subprocess
import sys
Expand All @@ -14,7 +13,6 @@

ROOT = Path(__file__).resolve().parent.parent
PRX = ROOT / "core" / "libs" / "prx"
NID = re.compile(r"[A-Za-z0-9+\-]{11}")
NID_POSTFIX = "_nid_postfix"
MODULE_SUFFIX = "#"
CLASSES = ("implemented", "stub", "absent", "module")
Expand Down Expand Up @@ -409,8 +407,8 @@ def read_registry(path):
if not isinstance(entry, dict) or not isinstance(entry.get("nid"), str) or not isinstance(entry.get("library"), str):
raise AuditError(f"{path}: entry {index} needs string fields 'nid' and 'library': {entry!r}")
nid = entry["nid"].split("#", 1)[0]
if not NID.fullmatch(nid):
raise AuditError(f"{path}: entry {index} has an invalid NID {entry['nid']!r}")
if not nid or any(character.isspace() or ord(character) < 32 or ord(character) == 127 for character in nid):
raise AuditError(f"{path}: entry {index} has an invalid import symbol {entry['nid']!r}")
imports.append((nid, entry["library"], bool(entry.get("weak", False))))
return imports

Expand Down
31 changes: 28 additions & 3 deletions tools/tests/test_import_audit.py
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,31 @@ 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, "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_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"))
Expand Down Expand Up @@ -1192,7 +1217,7 @@ def test_registry_errors(self):
with self.assertRaisesRegex(import_audit.AuditError, r"entry 0 needs string fields"):
import_audit.read_registry(path)
write_registry(path, [("AAAAAAAAAAA", ""), ("bad\x01nid", "")])
with self.assertRaisesRegex(import_audit.AuditError, r"entry 1 has an invalid NID 'bad\\x01nid'"):
with self.assertRaisesRegex(import_audit.AuditError, r"entry 1 has an invalid import symbol 'bad\\x01nid'"):
import_audit.read_registry(path)
write_registry(path, [("AAAAAAAAAAA#A#B", "libSceA.prx")], encoding="utf-8-sig")
self.assertEqual(import_audit.read_registry(path), [("AAAAAAAAAAA", "libSceA.prx", False)])
Expand Down Expand Up @@ -1226,10 +1251,10 @@ def test_errors_exit_two_with_the_failing_value(self):
work = self.work
(work.libs / "libSceA.prx").write_bytes(exporting("AAAAAAAAAAA"))
registry = work.root / "game.json"
write_registry(registry, [("short", "libSceA.prx")])
write_registry(registry, [("bad name", "libSceA.prx")])
code, out, err = work.run(registry)
self.assertEqual((code, out), (2, ""))
self.assertIn("game.json: entry 0 has an invalid NID 'short'", err)
self.assertIn("game.json: entry 0 has an invalid import symbol 'bad name'", err)

def test_unreadable_library_and_unwritable_json_exit_two(self):
work = self.work
Expand Down
Loading