mirror of
https://github.com/par274/sharpemu.git
synced 2026-07-23 03:16:22 +08:00
182 lines
5.7 KiB
Python
182 lines
5.7 KiB
Python
#!/usr/bin/env python3
|
|
|
|
# Copyright (C) 2026 SharpEmu Emulator Project
|
|
# SPDX-License-Identifier: GPL-2.0-or-later
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import base64
|
|
import hashlib
|
|
import re
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
|
|
NID_SUFFIX = bytes.fromhex("518d64a635ded8c1e6b039b1c3e55230")
|
|
NID_PATTERN = re.compile(r"^[A-Za-z0-9+-]{11}$")
|
|
DEFAULT_NAMES_FILE = Path(__file__).resolve().with_name("ps5_names.txt")
|
|
DEFAULT_EXPORT_FILE = Path(__file__).resolve().parents[1] / "artifacts" / "aerolib.txt"
|
|
|
|
|
|
def compute_nid(export_name: str) -> str:
|
|
digest = hashlib.sha1(export_name.encode("utf-8") + NID_SUFFIX).digest()
|
|
encoded = base64.b64encode(digest[:8][::-1]).decode("ascii")
|
|
return encoded.rstrip("=").replace("/", "-")
|
|
|
|
|
|
def read_names(path: Path) -> list[str]:
|
|
try:
|
|
return [
|
|
line.strip()
|
|
for line in path.read_text(encoding="utf-8").splitlines()
|
|
if line.strip()
|
|
]
|
|
except OSError as error:
|
|
raise SystemExit(f"Unable to read catalog '{path}': {error}") from error
|
|
|
|
|
|
def write_pair(nid: str, export_name: str) -> None:
|
|
print(f"{nid}\t{export_name}")
|
|
|
|
|
|
def lookup(args: argparse.Namespace) -> int:
|
|
value = args.value.strip()
|
|
if NID_PATTERN.fullmatch(value):
|
|
for export_name in read_names(args.names):
|
|
if compute_nid(export_name) == value:
|
|
write_pair(value, export_name)
|
|
return 0
|
|
|
|
print(f"NID not found in catalog: {value}", file=sys.stderr)
|
|
return 1
|
|
|
|
names = set(read_names(args.names))
|
|
write_pair(compute_nid(value), value)
|
|
if value not in names:
|
|
print("Warning: export name is not present in the catalog.", file=sys.stderr)
|
|
return 0
|
|
|
|
|
|
def search(args: argparse.Namespace) -> int:
|
|
names = read_names(args.names)
|
|
if args.regex:
|
|
try:
|
|
pattern = re.compile(args.query, 0 if args.case_sensitive else re.IGNORECASE)
|
|
except re.error as error:
|
|
print(f"Invalid regular expression: {error}", file=sys.stderr)
|
|
return 2
|
|
|
|
matches = (name for name in names if pattern.search(name))
|
|
elif args.case_sensitive:
|
|
matches = (name for name in names if args.query in name)
|
|
else:
|
|
query = args.query.casefold()
|
|
matches = (name for name in names if query in name.casefold())
|
|
|
|
count = 0
|
|
for export_name in matches:
|
|
write_pair(compute_nid(export_name), export_name)
|
|
count += 1
|
|
if args.limit and count >= args.limit:
|
|
break
|
|
|
|
if count == 0:
|
|
print(f"No catalog names matched: {args.query}", file=sys.stderr)
|
|
return 1
|
|
return 0
|
|
|
|
|
|
def export_catalog(args: argparse.Namespace) -> int:
|
|
pairs = [(compute_nid(name), name) for name in read_names(args.names)]
|
|
if args.sort == "nid":
|
|
pairs.sort(key=lambda pair: (pair[0], pair[1]))
|
|
elif args.sort == "name":
|
|
pairs.sort(key=lambda pair: pair[1])
|
|
|
|
args.output.parent.mkdir(parents=True, exist_ok=True)
|
|
try:
|
|
with args.output.open("w", encoding="utf-8", newline="\n") as output:
|
|
output.write("# NID\tExportName\n")
|
|
for nid, export_name in pairs:
|
|
output.write(f"{nid}\t{export_name}\n")
|
|
except OSError as error:
|
|
print(f"Unable to write catalog '{args.output}': {error}", file=sys.stderr)
|
|
return 1
|
|
|
|
print(f"Wrote {len(pairs)} entries to {args.output}")
|
|
return 0
|
|
|
|
|
|
def create_parser() -> argparse.ArgumentParser:
|
|
parser = argparse.ArgumentParser(
|
|
description="Inspect the SharpEmu PS5 export-name/NID catalog.",
|
|
epilog=(
|
|
"Examples:\n"
|
|
" python scripts/aerolib_catalog.py lookup Zxa0VhQVTsk\n"
|
|
" python scripts/aerolib_catalog.py lookup sceKernelWaitSema\n"
|
|
" python scripts/aerolib_catalog.py search VideoOut --limit 20\n"
|
|
" python scripts/aerolib_catalog.py export"
|
|
),
|
|
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
)
|
|
parser.add_argument(
|
|
"--names",
|
|
type=Path,
|
|
default=DEFAULT_NAMES_FILE,
|
|
help=f"source name list (default: {DEFAULT_NAMES_FILE})",
|
|
)
|
|
|
|
subparsers = parser.add_subparsers(dest="command", required=True)
|
|
|
|
lookup_parser = subparsers.add_parser(
|
|
"lookup", help="resolve a NID or calculate the NID for an export name"
|
|
)
|
|
lookup_parser.add_argument("value", help="11-character NID or exact export name")
|
|
lookup_parser.set_defaults(handler=lookup)
|
|
|
|
search_parser = subparsers.add_parser(
|
|
"search", help="find export names and print matching NID/name pairs"
|
|
)
|
|
search_parser.add_argument("query", help="name substring or regular expression")
|
|
search_parser.add_argument(
|
|
"--limit", type=int, default=50, help="maximum matches; 0 means unlimited"
|
|
)
|
|
search_parser.add_argument(
|
|
"--case-sensitive", action="store_true", help="match case exactly"
|
|
)
|
|
search_parser.add_argument(
|
|
"--regex", action="store_true", help="treat the query as a regular expression"
|
|
)
|
|
search_parser.set_defaults(handler=search)
|
|
|
|
export_parser = subparsers.add_parser(
|
|
"export", help="write every NID/name pair to a tab-separated text file"
|
|
)
|
|
export_parser.add_argument(
|
|
"output",
|
|
type=Path,
|
|
nargs="?",
|
|
default=DEFAULT_EXPORT_FILE,
|
|
help=f"output file (default: {DEFAULT_EXPORT_FILE})",
|
|
)
|
|
export_parser.add_argument(
|
|
"--sort",
|
|
choices=("source", "nid", "name"),
|
|
default="nid",
|
|
help="output ordering (default: nid)",
|
|
)
|
|
export_parser.set_defaults(handler=export_catalog)
|
|
|
|
return parser
|
|
|
|
|
|
def main() -> int:
|
|
parser = create_parser()
|
|
args = parser.parse_args()
|
|
return args.handler(args)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|