From 2bda25392712d9410544ab8193f75bff72a97049 Mon Sep 17 00:00:00 2001 From: ParantezTech Date: Sun, 19 Jul 2026 01:33:15 +0300 Subject: [PATCH] [script] added aerolib_catalog.py and docs/aerolib-catalog.md, renamed scripts/RELEASE-USE.md to docs/release-use.md --- docs/aerolib-catalog.md | 20 ++ scripts/RELEASE-USE.md => docs/release-use.md | 0 scripts/aerolib_catalog.py | 181 ++++++++++++++++++ 3 files changed, 201 insertions(+) create mode 100644 docs/aerolib-catalog.md rename scripts/RELEASE-USE.md => docs/release-use.md (100%) create mode 100644 scripts/aerolib_catalog.py diff --git a/docs/aerolib-catalog.md b/docs/aerolib-catalog.md new file mode 100644 index 0000000..4549c08 --- /dev/null +++ b/docs/aerolib-catalog.md @@ -0,0 +1,20 @@ + + +# Aerolib Catalog + +```bash +# NID to export name +python scripts/aerolib_catalog.py lookup Zxa0VhQVTsk + +# Export name to NID +python scripts/aerolib_catalog.py lookup sceKernelWaitSema + +# Search export names +python scripts/aerolib_catalog.py search VideoOut --limit 20 + +# Export all NID/name pairs to artifacts/aerolib.txt +python scripts/aerolib_catalog.py export +``` diff --git a/scripts/RELEASE-USE.md b/docs/release-use.md similarity index 100% rename from scripts/RELEASE-USE.md rename to docs/release-use.md diff --git a/scripts/aerolib_catalog.py b/scripts/aerolib_catalog.py new file mode 100644 index 0000000..76b72f8 --- /dev/null +++ b/scripts/aerolib_catalog.py @@ -0,0 +1,181 @@ +#!/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())